diff --git a/include/pacemaker.h b/include/pacemaker.h index a86665aae8..17c68e9099 100644 --- a/include/pacemaker.h +++ b/include/pacemaker.h @@ -1,361 +1,372 @@ /* * Copyright 2019-2022 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__PACEMAKER__H # define PCMK__PACEMAKER__H # include # include +# include # include # include #ifdef __cplusplus extern "C" { #endif /** * \file * \brief High Level API * \ingroup pacemaker */ /*! * \brief Modify operation of running a cluster simulation. */ enum pcmk_sim_flags { pcmk_sim_none = 0, pcmk_sim_all_actions = 1 << 0, pcmk_sim_show_pending = 1 << 1, pcmk_sim_process = 1 << 2, pcmk_sim_show_scores = 1 << 3, pcmk_sim_show_utilization = 1 << 4, pcmk_sim_simulate = 1 << 5, pcmk_sim_sanitized = 1 << 6, pcmk_sim_verbose = 1 << 7, }; /*! * \brief Synthetic cluster events that can be injected into the cluster * for running simulations. */ typedef struct { /*! A list of node names (gchar *) to simulate bringing online */ GList *node_up; /*! A list of node names (gchar *) to simulate bringing offline */ GList *node_down; /*! A list of node names (gchar *) to simulate failing */ GList *node_fail; /*! A list of operations (gchar *) to inject. The format of these strings * is described in the "Operation Specification" section of crm_simulate * help output. */ GList *op_inject; /*! A list of operations (gchar *) that should return a given error code * if they fail. The format of these strings is described in the * "Operation Specification" section of crm_simulate help output. */ GList *op_fail; /*! A list of tickets (gchar *) to simulate granting */ GList *ticket_grant; /*! A list of tickets (gchar *) to simulate revoking */ GList *ticket_revoke; /*! A list of tickets (gchar *) to simulate putting on standby */ GList *ticket_standby; /*! A list of tickets (gchar *) to simulate activating */ GList *ticket_activate; /*! Does the cluster have an active watchdog device? */ char *watchdog; /*! Does the cluster have quorum? */ char *quorum; } pcmk_injections_t; /*! * \brief Get controller status * * \param[in,out] xml The destination for the result, as an XML tree. * \param[in] dest_node Destination node for request * \param[in] message_timeout_ms Message timeout * * \return Standard Pacemaker return code */ int pcmk_controller_status(xmlNodePtr *xml, char *dest_node, unsigned int message_timeout_ms); /*! * \brief Get designated controller * * \param[in,out] xml The destination for the result, as an XML tree. * \param[in] message_timeout_ms Message timeout * * \return Standard Pacemaker return code */ int pcmk_designated_controller(xmlNodePtr *xml, unsigned int message_timeout_ms); /*! * \brief Free a :pcmk_injections_t structure * * \param[in,out] injections The structure to be freed */ void pcmk_free_injections(pcmk_injections_t *injections); /*! * \brief Get pacemakerd status * * \param[in,out] xml The destination for the result, as an XML tree. * \param[in] ipc_name IPC name for request * \param[in] message_timeout_ms Message timeout * * \return Standard Pacemaker return code */ int pcmk_pacemakerd_status(xmlNodePtr *xml, char *ipc_name, unsigned int message_timeout_ms); /*! * \brief Calculate and output resource operation digests * * \param[out] xml Where to store XML with result * \param[in] rsc Resource to calculate digests for * \param[in] node Node whose operation history should be used * \param[in] overrides Hash table of configuration parameters to override * \param[in] data_set Cluster working set (with status) * * \return Standard Pacemaker return code */ int pcmk_resource_digests(xmlNodePtr *xml, pe_resource_t *rsc, pe_node_t *node, GHashTable *overrides, pe_working_set_t *data_set); /** * \brief Simulate a cluster's response to events. * * This high-level function essentially implements crm_simulate(8). It operates * on an input CIB file and various lists of events that can be simulated. It * optionally writes out a variety of artifacts to show the results of the * simulation. Output can be modified with various flags. * * \param[in,out] xml The destination for the result, as an XML tree. * \param[in,out] data_set Working set for the cluster. * \param[in] events A structure containing cluster events * (node up/down, tickets, injected operations) * \param[in] flags A bitfield of :pcmk_sim_flags to modify * operation of the simulation. * \param[in] section_opts Which portions of the cluster status output * should be displayed? * \param[in] use_date The date to set the cluster's time to * (may be NULL). * \param[in] input_file The source CIB file, which may be overwritten by * this function (may be NULL). * \param[in] graph_file Where to write the XML-formatted transition graph * (may be NULL, in which case no file will be * written). * \param[in] dot_file Where to write the dot(1) formatted transition * graph (may be NULL, in which case no file will * be written). See \p pcmk__write_sim_dotfile(). * * \return Standard Pacemaker return code */ int pcmk_simulate(xmlNodePtr *xml, pe_working_set_t *data_set, pcmk_injections_t *injections, unsigned int flags, unsigned int section_opts, char *use_date, char *input_file, char *graph_file, char *dot_file); /*! * \brief Get nodes list * * \param[in,out] xml The destination for the result, as an XML tree. * \param[in] node_types Node type(s) to return (default: all) * * \return Standard Pacemaker return code */ int pcmk_list_nodes(xmlNodePtr *xml, char *node_types); +/*! + * \brief Output the current status of the cluster, formatted in the same way + * that `crm_mon --output-as=xml` would. + * + * \param[in,out] xml The destination for the result, as an XML tree. + * + * \return Standard Pacemaker return code + */ +int pcmk_status(xmlNodePtr *xml); + #ifdef BUILD_PUBLIC_LIBPACEMAKER /*! * \brief Ask the cluster to perform fencing * * \param[in] st A connection to the fencer API * \param[in] target The node that should be fenced * \param[in] action The fencing action (on, off, reboot) to perform * \param[in] name Who requested the fence action? * \param[in] timeout How long to wait for the operation to complete (in ms) * \param[in] tolerance If a successful action for \p target happened within * this many ms, return 0 without performing the action * again * \param[in] delay Apply this delay (in milliseconds) before initiating the * fencing action (a value of -1 applies no delay and also * disables any fencing delay from pcmk_delay_base and * pcmk_delay_max) * \param[out] reason If not NULL, where to put descriptive failure reason * * \return Standard Pacemaker return code * \note If \p reason is not NULL, the caller is responsible for freeing its * returned value. */ int pcmk_request_fencing(stonith_t *st, const char *target, const char *action, const char *name, unsigned int timeout, unsigned int tolerance, int delay, char **reason); /*! * \brief List the fencing operations that have occurred for a specific node. * * \note If \p xml is not NULL, it will be freed first and the previous * contents lost. * * \param[in,out] xml The destination for the result, as an XML tree. * \param[in] st A connection to the STONITH API. * \param[in] target The node to get history for. * \param[in] timeout How long to wait for the operation to complete (in ms). * \param[in] quiet Suppress most output. * \param[in] verbose Include additional output. * \param[in] broadcast Gather fencing history from all nodes. * \param[in] cleanup Clean up fencing history after listing. * * \return Standard Pacemaker return code */ int pcmk_fence_history(xmlNodePtr *xml, stonith_t *st, char *target, unsigned int timeout, bool quiet, int verbose, bool broadcast, bool cleanup); /*! * \brief List all installed STONITH agents. * * \note If \p xml is not NULL, it will be freed first and the previous * contents lost. * * \param[in,out] xml The destination for the result, as an XML tree. * \param[in] st A connection to the STONITH API. * \param[in] timeout How long to wait for the operation to complete (in ms). * * \return Standard Pacemaker return code */ int pcmk_fence_installed(xmlNodePtr *xml, stonith_t *st, unsigned int timeout); /*! * \brief When was a device last fenced? * * \note If \p xml is not NULL, it will be freed first and the previous * contents lost. * * \param[in,out] xml The destination for the result, as an XML tree. * \param[in] target The node that was fenced. * \param[in] as_nodeid * * \return Standard Pacemaker return code */ int pcmk_fence_last(xmlNodePtr *xml, const char *target, bool as_nodeid); /*! * \brief List nodes that can be fenced. * * \note If \p xml is not NULL, it will be freed first and the previous * contents lost. * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] st A connection to the STONITH API * \param[in] device_id Resource ID of fence device to check * \param[in] timeout How long to wait for the operation to complete (in ms) * * \return Standard Pacemaker return code */ int pcmk_fence_list_targets(xmlNodePtr *xml, stonith_t *st, const char *device_id, unsigned int timeout); /*! * \brief Get metadata for a resource. * * \note If \p xml is not NULL, it will be freed first and the previous * contents lost. * * \param[in,out] xml The destination for the result, as an XML tree. * \param[in] st A connection to the STONITH API. * \param[in] agent The fence agent to get metadata for. * \param[in] timeout How long to wait for the operation to complete (in ms). * * \return Standard Pacemaker return code */ int pcmk_fence_metadata(xmlNodePtr *xml, stonith_t *st, char *agent, unsigned int timeout); /*! * \brief List registered fence devices. * * \note If \p xml is not NULL, it will be freed first and the previous * contents lost. * * \param[in,out] xml The destination for the result, as an XML tree. * \param[in] st A connection to the STONITH API. * \param[in] target If not NULL, only return devices that can fence * this node. * \param[in] timeout How long to wait for the operation to complete (in ms). * * \return Standard Pacemaker return code */ int pcmk_fence_registered(xmlNodePtr *xml, stonith_t *st, char *target, unsigned int timeout); /*! * \brief Register a fencing level for a specific node, node regex, or attribute. * * \p target can take three different forms: * - name=value, in which case \p target is an attribute. * - @pattern, in which case \p target is a node regex. * - Otherwise, \p target is a node name. * * \param[in] st A connection to the STONITH API. * \param[in] target The object to register a fencing level for. * \param[in] fence_level Index number of level to add. * \param[in] devices Devices to use in level. * * \return Standard Pacemaker return code */ int pcmk_fence_register_level(stonith_t *st, char *target, int fence_level, stonith_key_value_t *devices); /*! * \brief Unregister a fencing level for a specific node, node regex, or attribute. * * \p target can take three different forms: * - name=value, in which case \p target is an attribute. * - @pattern, in which case \p target is a node regex. * - Otherwise, \p target is a node name. * * \param[in] st A connection to the STONITH API. * \param[in] target The object to unregister a fencing level for. * \param[in] fence_level Index number of level to remove. * * \return Standard Pacemaker return code */ int pcmk_fence_unregister_level(stonith_t *st, char *target, int fence_level); /*! * \brief Validate a STONITH device configuration. * * \note If \p xml is not NULL, it will be freed first and the previous * contents lost. * * \param[in,out] xml The destination for the result, as an XML tree. * \param[in] st A connection to the STONITH API. * \param[in] agent The agent to validate (for example, "fence_xvm"). * \param[in] id STONITH device ID (may be NULL). * \param[in] params STONITH device configuration parameters. * \param[in] timeout How long to wait for the operation to complete (in ms). * * \return Standard Pacemaker return code */ int pcmk_fence_validate(xmlNodePtr *xml, stonith_t *st, const char *agent, const char *id, stonith_key_value_t *params, unsigned int timeout); #endif #ifdef __cplusplus } #endif #endif diff --git a/include/pcmki/pcmki_status.h b/include/pcmki/pcmki_status.h index 75d043b520..6614fe4160 100644 --- a/include/pcmki/pcmki_status.h +++ b/include/pcmki/pcmki_status.h @@ -1,40 +1,55 @@ /* * Copyright 2022 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__PCMKI_PCMKI_STATUS__H #define PCMK__PCMKI_PCMKI_STATUS__H +#include +#include + +#include #include #include +#include #ifdef __cplusplus extern "C" { #endif /*! * \internal * \brief Print one-line status suitable for use with monitoring software * * \param[in] data_set Working set of CIB state * * \return Standard Pacemaker return code * * \note This function's output (and the return code when the program exits) * should conform to https://www.monitoring-plugins.org/doc/guidelines.html * * \note This function is planned to be deprecated and then removed in the * future. It should only be called from crm_mon, and no additional * callers should be added. */ int pcmk__output_simple_status(pcmk__output_t *out, pe_working_set_t *data_set); +int pcmk__output_cluster_status(pcmk__output_t *out, stonith_t *st, cib_t *cib, + xmlNode *current_cib, enum pcmk__fence_history fence_history, + uint32_t show, uint32_t show_opts, char *only_node, + char *only_rsc, char *neg_location_prefix, + bool simple_output); + +int pcmk__status(pcmk__output_t *out, cib_t *cib, enum pcmk__fence_history fence_history, + uint32_t show, uint32_t show_opts, char *only_node, char *only_rsc, + char *neg_location_prefix, bool simple_output); + #ifdef __cplusplus } #endif #endif diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h index fb1f64fd3c..6b7be9c68a 100644 --- a/lib/common/crmcommon_private.h +++ b/lib/common/crmcommon_private.h @@ -1,281 +1,283 @@ /* * Copyright 2018-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. */ #ifndef CRMCOMMON_PRIVATE__H # define CRMCOMMON_PRIVATE__H /* This header is for the sole use of libcrmcommon, so that functions can be * declared with G_GNUC_INTERNAL for efficiency. */ #include // uint8_t, uint32_t #include // bool #include // size_t #include // GList #include // xmlNode, xmlAttr #include // struct qb_ipc_response_header // Decent chunk size for processing large amounts of data #define PCMK__BUFFER_SIZE 4096 /* When deleting portions of an XML tree, we keep a record so we can know later * (e.g. when checking differences) that something was deleted. */ typedef struct pcmk__deleted_xml_s { char *path; int position; } pcmk__deleted_xml_t; typedef struct xml_private_s { long check; uint32_t flags; char *user; GList *acls; GList *deleted_objs; // List of pcmk__deleted_xml_t } xml_private_t; #define pcmk__set_xml_flags(xml_priv, flags_to_set) do { \ (xml_priv)->flags = pcmk__set_flags_as(__func__, __LINE__, \ LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ (flags_to_set), #flags_to_set); \ } while (0) #define pcmk__clear_xml_flags(xml_priv, flags_to_clear) do { \ (xml_priv)->flags = pcmk__clear_flags_as(__func__, __LINE__, \ LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ (flags_to_clear), #flags_to_clear); \ } while (0) G_GNUC_INTERNAL void pcmk__xml2text(xmlNode *data, int options, char **buffer, int *offset, int *max, int depth); G_GNUC_INTERNAL void pcmk__buffer_add_char(char **buffer, int *offset, int *max, char c); G_GNUC_INTERNAL bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy); G_GNUC_INTERNAL int pcmk__element_xpath(const char *prefix, xmlNode *xml, char *buffer, int offset, size_t buffer_size); G_GNUC_INTERNAL void pcmk__mark_xml_created(xmlNode *xml); G_GNUC_INTERNAL int pcmk__xml_position(xmlNode *xml, enum xml_private_flags ignore_if_set); G_GNUC_INTERNAL xmlNode *pcmk__xml_match(xmlNode *haystack, xmlNode *needle, bool exact); G_GNUC_INTERNAL void pcmk__xe_log(int log_level, const char *file, const char *function, int line, const char *prefix, xmlNode *data, int depth, int options); G_GNUC_INTERNAL void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, bool as_diff); G_GNUC_INTERNAL xmlNode *pcmk__xc_match(xmlNode *root, xmlNode *search_comment, bool exact); G_GNUC_INTERNAL void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update); G_GNUC_INTERNAL void pcmk__free_acls(GList *acls); G_GNUC_INTERNAL void pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user); G_GNUC_INTERNAL void pcmk__apply_acl(xmlNode *xml); G_GNUC_INTERNAL void pcmk__apply_creation_acl(xmlNode *xml, bool check_top); G_GNUC_INTERNAL void pcmk__mark_xml_attr_dirty(xmlAttr *a); G_GNUC_INTERNAL bool pcmk__xa_filterable(const char *name); static inline const char * pcmk__xml_attr_value(const xmlAttr *attr) { return ((attr == NULL) || (attr->children == NULL))? NULL : (const char *) attr->children->content; } /* * IPC */ #define PCMK__IPC_VERSION 1 #define PCMK__CONTROLD_API_MAJOR "1" #define PCMK__CONTROLD_API_MINOR "0" // IPC behavior that varies by daemon typedef struct pcmk__ipc_methods_s { /*! * \internal * \brief Allocate any private data needed by daemon IPC * * \param[in] api IPC API connection * * \return Standard Pacemaker return code */ int (*new_data)(pcmk_ipc_api_t *api); /*! * \internal * \brief Free any private data used by daemon IPC * * \param[in] api_data Data allocated by new_data() method */ void (*free_data)(void *api_data); /*! * \internal * \brief Perform daemon-specific handling after successful connection * * Some daemons require clients to register before sending any other * commands. The controller requires a CRM_OP_HELLO (with no reply), and * the CIB manager, executor, and fencer require a CRM_OP_REGISTER (with a * reply). Ideally this would be consistent across all daemons, but for now * this allows each to do its own authorization. * * \param[in] api IPC API connection * * \return Standard Pacemaker return code */ int (*post_connect)(pcmk_ipc_api_t *api); /*! * \internal * \brief Check whether an IPC request results in a reply * * \param[in] api IPC API connection * \param[in] request IPC request XML * * \return true if request would result in an IPC reply, false otherwise */ bool (*reply_expected)(pcmk_ipc_api_t *api, xmlNode *request); /*! * \internal * \brief Perform daemon-specific handling of an IPC message * * \param[in] api IPC API connection * \param[in] msg Message read from IPC connection + * + * \return true if more IPC reply messages should be expected */ - void (*dispatch)(pcmk_ipc_api_t *api, xmlNode *msg); + bool (*dispatch)(pcmk_ipc_api_t *api, xmlNode *msg); /*! * \internal * \brief Perform daemon-specific handling of an IPC disconnect * * \param[in] api IPC API connection */ void (*post_disconnect)(pcmk_ipc_api_t *api); } pcmk__ipc_methods_t; // Implementation of pcmk_ipc_api_t struct pcmk_ipc_api_s { enum pcmk_ipc_server server; // Daemon this IPC API instance is for enum pcmk_ipc_dispatch dispatch_type; // How replies should be dispatched size_t ipc_size_max; // maximum IPC buffer size crm_ipc_t *ipc; // IPC connection mainloop_io_t *mainloop_io; // If using mainloop, I/O source for IPC bool free_on_disconnect; // Whether disconnect should free object pcmk_ipc_callback_t cb; // Caller-registered callback (if any) void *user_data; // Caller-registered data (if any) void *api_data; // For daemon-specific use pcmk__ipc_methods_t *cmds; // Behavior that varies by daemon }; typedef struct pcmk__ipc_header_s { struct qb_ipc_response_header qb; uint32_t size_uncompressed; uint32_t size_compressed; uint32_t flags; uint8_t version; } pcmk__ipc_header_t; G_GNUC_INTERNAL int pcmk__send_ipc_request(pcmk_ipc_api_t *api, xmlNode *request); G_GNUC_INTERNAL void pcmk__call_ipc_callback(pcmk_ipc_api_t *api, enum pcmk_ipc_event event_type, crm_exit_t status, void *event_data); G_GNUC_INTERNAL unsigned int pcmk__ipc_buffer_size(unsigned int max); G_GNUC_INTERNAL bool pcmk__valid_ipc_header(const pcmk__ipc_header_t *header); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__controld_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__pacemakerd_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__schedulerd_api_methods(void); /* * Logging */ /*! * \brief Check the authenticity of the IPC socket peer process * * If everything goes well, peer's authenticity is verified by the means * of comparing against provided referential UID and GID (either satisfies), * and the result of this check can be deduced from the return value. * As an exception, detected UID of 0 ("root") satisfies arbitrary * provided referential daemon's credentials. * * \param[in] qb_ipc libqb client connection if available * \param[in] sock IPC related, connected Unix socket to check peer of * \param[in] refuid referential UID to check against * \param[in] refgid referential GID to check against * \param[out] gotpid to optionally store obtained PID of the peer * (not available on FreeBSD, special value of 1 * used instead, and the caller is required to * special case this value respectively) * \param[out] gotuid to optionally store obtained UID of the peer * \param[out] gotgid to optionally store obtained GID of the peer * * \return Standard Pacemaker return code * ie: 0 if it the connection is authentic * pcmk_rc_ipc_unauthorized if the connection is not authentic, * standard errors. * * \note While this function is tolerant on what constitutes authorized * IPC daemon process (its effective user matches UID=0 or \p refuid, * or at least its group matches \p refgid), either or both (in case * of UID=0) mismatches on the expected credentials of such peer * process \e shall be investigated at the caller when value of 1 * gets returned there, since higher-than-expected privileges in * respect to the expected/intended credentials possibly violate * the least privilege principle and may pose an additional risk * (i.e. such accidental inconsistency shall be eventually fixed). */ int pcmk__crm_ipc_is_authentic_process(qb_ipcc_connection_t *qb_ipc, int sock, uid_t refuid, gid_t refgid, pid_t *gotpid, uid_t *gotuid, gid_t *gotgid); #endif // CRMCOMMON_PRIVATE__H diff --git a/lib/common/ipc_client.c b/lib/common/ipc_client.c index 417b9ef175..adaa2c131b 100644 --- a/lib/common/ipc_client.c +++ b/lib/common/ipc_client.c @@ -1,1493 +1,1508 @@ /* * Copyright 2004-2022 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 #if defined(US_AUTH_PEERCRED_UCRED) || defined(US_AUTH_PEERCRED_SOCKPEERCRED) # ifdef US_AUTH_PEERCRED_UCRED # ifndef _GNU_SOURCE # define _GNU_SOURCE # endif # endif # include #elif defined(US_AUTH_GETPEERUCRED) # include #endif #include #include #include #include #include /* indirectly: pcmk_err_generic */ #include #include #include #include "crmcommon_private.h" /*! * \brief Create a new object for using Pacemaker daemon IPC * * \param[out] api Where to store new IPC object * \param[in] server Which Pacemaker daemon the object is for * * \return Standard Pacemaker result code * * \note The caller is responsible for freeing *api using pcmk_free_ipc_api(). * \note This is intended to supersede crm_ipc_new() but currently only * supports the controller, pacemakerd, and schedulerd IPC API. */ int pcmk_new_ipc_api(pcmk_ipc_api_t **api, enum pcmk_ipc_server server) { if (api == NULL) { return EINVAL; } *api = calloc(1, sizeof(pcmk_ipc_api_t)); if (*api == NULL) { return errno; } (*api)->server = server; if (pcmk_ipc_name(*api, false) == NULL) { pcmk_free_ipc_api(*api); *api = NULL; return EOPNOTSUPP; } (*api)->ipc_size_max = 0; // Set server methods and max_size (if not default) switch (server) { case pcmk_ipc_attrd: break; case pcmk_ipc_based: (*api)->ipc_size_max = 512 * 1024; // 512KB break; case pcmk_ipc_controld: (*api)->cmds = pcmk__controld_api_methods(); break; case pcmk_ipc_execd: break; case pcmk_ipc_fenced: break; case pcmk_ipc_pacemakerd: (*api)->cmds = pcmk__pacemakerd_api_methods(); break; case pcmk_ipc_schedulerd: (*api)->cmds = pcmk__schedulerd_api_methods(); // @TODO max_size could vary by client, maybe take as argument? (*api)->ipc_size_max = 5 * 1024 * 1024; // 5MB break; } if ((*api)->cmds == NULL) { pcmk_free_ipc_api(*api); *api = NULL; return ENOMEM; } (*api)->ipc = crm_ipc_new(pcmk_ipc_name(*api, false), (*api)->ipc_size_max); if ((*api)->ipc == NULL) { pcmk_free_ipc_api(*api); *api = NULL; return ENOMEM; } // If daemon API has its own data to track, allocate it if ((*api)->cmds->new_data != NULL) { if ((*api)->cmds->new_data(*api) != pcmk_rc_ok) { pcmk_free_ipc_api(*api); *api = NULL; return ENOMEM; } } crm_trace("Created %s API IPC object", pcmk_ipc_name(*api, true)); return pcmk_rc_ok; } static void free_daemon_specific_data(pcmk_ipc_api_t *api) { if ((api != NULL) && (api->cmds != NULL)) { if ((api->cmds->free_data != NULL) && (api->api_data != NULL)) { api->cmds->free_data(api->api_data); api->api_data = NULL; } free(api->cmds); api->cmds = NULL; } } /*! * \internal * \brief Call an IPC API event callback, if one is registed * * \param[in] api IPC API connection * \param[in] event_type The type of event that occurred * \param[in] status Event status * \param[in] event_data Event-specific data */ void pcmk__call_ipc_callback(pcmk_ipc_api_t *api, enum pcmk_ipc_event event_type, crm_exit_t status, void *event_data) { if ((api != NULL) && (api->cb != NULL)) { api->cb(api, event_type, status, event_data, api->user_data); } } /*! * \internal * \brief Clean up after an IPC disconnect * * \param[in] user_data IPC API connection that disconnected * * \note This function can be used as a main loop IPC destroy callback. */ static void ipc_post_disconnect(gpointer user_data) { pcmk_ipc_api_t *api = user_data; crm_info("Disconnected from %s IPC API", pcmk_ipc_name(api, true)); // Perform any daemon-specific handling needed if ((api->cmds != NULL) && (api->cmds->post_disconnect != NULL)) { api->cmds->post_disconnect(api); } // Call client's registered event callback pcmk__call_ipc_callback(api, pcmk_ipc_event_disconnect, CRM_EX_DISCONNECT, NULL); /* If this is being called from a running main loop, mainloop_gio_destroy() * will free ipc and mainloop_io immediately after calling this function. * If this is called from a stopped main loop, these will leak, so the best * practice is to close the connection before stopping the main loop. */ api->ipc = NULL; api->mainloop_io = NULL; if (api->free_on_disconnect) { /* pcmk_free_ipc_api() has already been called, but did not free api * or api->cmds because this function needed them. Do that now. */ free_daemon_specific_data(api); crm_trace("Freeing IPC API object after disconnect"); free(api); } } /*! * \brief Free the contents of an IPC API object * * \param[in] api IPC API object to free */ void pcmk_free_ipc_api(pcmk_ipc_api_t *api) { bool free_on_disconnect = false; if (api == NULL) { return; } crm_debug("Releasing %s IPC API", pcmk_ipc_name(api, true)); if (api->ipc != NULL) { if (api->mainloop_io != NULL) { /* We need to keep the api pointer itself around, because it is the * user data for the IPC client destroy callback. That will be * triggered by the pcmk_disconnect_ipc() call below, but it might * happen later in the main loop (if still running). * * This flag tells the destroy callback to free the object. It can't * do that unconditionally, because the application might call this * function after a disconnect that happened by other means. */ free_on_disconnect = api->free_on_disconnect = true; } pcmk_disconnect_ipc(api); // Frees api if free_on_disconnect is true } if (!free_on_disconnect) { free_daemon_specific_data(api); crm_trace("Freeing IPC API object"); free(api); } } /*! * \brief Get the IPC name used with an IPC API connection * * \param[in] api IPC API connection * \param[in] for_log If true, return human-friendly name instead of IPC name * * \return IPC API's human-friendly or connection name, or if none is available, * "Pacemaker" if for_log is true and NULL if for_log is false */ const char * pcmk_ipc_name(pcmk_ipc_api_t *api, bool for_log) { if (api == NULL) { return for_log? "Pacemaker" : NULL; } switch (api->server) { case pcmk_ipc_attrd: return for_log? "attribute manager" : NULL /* T_ATTRD */; case pcmk_ipc_based: return for_log? "CIB manager" : NULL /* PCMK__SERVER_BASED_RW */; case pcmk_ipc_controld: return for_log? "controller" : CRM_SYSTEM_CRMD; case pcmk_ipc_execd: return for_log? "executor" : NULL /* CRM_SYSTEM_LRMD */; case pcmk_ipc_fenced: return for_log? "fencer" : NULL /* "stonith-ng" */; case pcmk_ipc_pacemakerd: return for_log? "launcher" : CRM_SYSTEM_MCP; case pcmk_ipc_schedulerd: return for_log? "scheduler" : CRM_SYSTEM_PENGINE; default: return for_log? "Pacemaker" : NULL; } } /*! * \brief Check whether an IPC API connection is active * * \param[in] api IPC API connection * * \return true if IPC is connected, false otherwise */ bool pcmk_ipc_is_connected(pcmk_ipc_api_t *api) { return (api != NULL) && crm_ipc_connected(api->ipc); } /*! * \internal * \brief Call the daemon-specific API's dispatch function * * Perform daemon-specific handling of IPC reply dispatch. It is the daemon * method's responsibility to call the client's registered event callback, as * well as allocate and free any event data. * * \param[in] api IPC API connection */ -static void +static bool call_api_dispatch(pcmk_ipc_api_t *api, xmlNode *message) { crm_log_xml_trace(message, "ipc-received"); if ((api->cmds != NULL) && (api->cmds->dispatch != NULL)) { - api->cmds->dispatch(api, message); + return api->cmds->dispatch(api, message); } + + return false; } /*! * \internal * \brief Dispatch data read from IPC source * * \param[in] buffer Data read from IPC * \param[in] length Number of bytes of data in buffer (ignored) * \param[in] user_data IPC object * * \return Always 0 (meaning connection is still required) * * \note This function can be used as a main loop IPC dispatch callback. */ static int dispatch_ipc_data(const char *buffer, ssize_t length, gpointer user_data) { pcmk_ipc_api_t *api = user_data; xmlNode *msg; CRM_CHECK(api != NULL, return 0); if (buffer == NULL) { crm_warn("Empty message received from %s IPC", pcmk_ipc_name(api, true)); return 0; } msg = string2xml(buffer); if (msg == NULL) { crm_warn("Malformed message received from %s IPC", pcmk_ipc_name(api, true)); return 0; } call_api_dispatch(api, msg); free_xml(msg); return 0; } /*! * \brief Check whether an IPC connection has data available (without main loop) * * \param[in] api IPC API connection * \param[in] timeout_ms If less than 0, poll indefinitely; if 0, poll once * and return immediately; otherwise, poll for up to * this many milliseconds * * \return Standard Pacemaker return code * * \note Callers of pcmk_connect_ipc() using pcmk_ipc_dispatch_poll should call * this function to check whether IPC data is available. Return values of * interest include pcmk_rc_ok meaning data is available, and EAGAIN * meaning no data is available; all other values indicate errors. * \todo This does not allow the caller to poll multiple file descriptors at * once. If there is demand for that, we could add a wrapper for * crm_ipc_get_fd(api->ipc), so the caller can call poll() themselves. */ int pcmk_poll_ipc(pcmk_ipc_api_t *api, int timeout_ms) { int rc; struct pollfd pollfd = { 0, }; if ((api == NULL) || (api->dispatch_type != pcmk_ipc_dispatch_poll)) { return EINVAL; } pollfd.fd = crm_ipc_get_fd(api->ipc); pollfd.events = POLLIN; rc = poll(&pollfd, 1, timeout_ms); if (rc < 0) { return errno; } else if (rc == 0) { return EAGAIN; } return pcmk_rc_ok; } /*! * \brief Dispatch available messages on an IPC connection (without main loop) * * \param[in] api IPC API connection * * \return Standard Pacemaker return code * * \note Callers of pcmk_connect_ipc() using pcmk_ipc_dispatch_poll should call * this function when IPC data is available. */ void pcmk_dispatch_ipc(pcmk_ipc_api_t *api) { if (api == NULL) { return; } while (crm_ipc_ready(api->ipc) > 0) { if (crm_ipc_read(api->ipc) > 0) { dispatch_ipc_data(crm_ipc_buffer(api->ipc), 0, api); } } } // \return Standard Pacemaker return code static int connect_with_main_loop(pcmk_ipc_api_t *api) { int rc; struct ipc_client_callbacks callbacks = { .dispatch = dispatch_ipc_data, .destroy = ipc_post_disconnect, }; rc = pcmk__add_mainloop_ipc(api->ipc, G_PRIORITY_DEFAULT, api, &callbacks, &(api->mainloop_io)); if (rc != pcmk_rc_ok) { return rc; } crm_debug("Connected to %s IPC (attached to main loop)", pcmk_ipc_name(api, true)); /* After this point, api->mainloop_io owns api->ipc, so api->ipc * should not be explicitly freed. */ return pcmk_rc_ok; } // \return Standard Pacemaker return code static int connect_without_main_loop(pcmk_ipc_api_t *api) { int rc; if (!crm_ipc_connect(api->ipc)) { rc = errno; crm_ipc_close(api->ipc); return rc; } crm_debug("Connected to %s IPC (without main loop)", pcmk_ipc_name(api, true)); return pcmk_rc_ok; } /*! * \brief Connect to a Pacemaker daemon via IPC * * \param[in] api IPC API instance * \param[out] dispatch_type How IPC replies should be dispatched * * \return Standard Pacemaker return code */ int pcmk_connect_ipc(pcmk_ipc_api_t *api, enum pcmk_ipc_dispatch dispatch_type) { int rc = pcmk_rc_ok; if (api == NULL) { crm_err("Cannot connect to uninitialized API object"); return EINVAL; } if (api->ipc == NULL) { api->ipc = crm_ipc_new(pcmk_ipc_name(api, false), api->ipc_size_max); if (api->ipc == NULL) { crm_err("Failed to re-create IPC API"); return ENOMEM; } } if (crm_ipc_connected(api->ipc)) { crm_trace("Already connected to %s IPC API", pcmk_ipc_name(api, true)); return pcmk_rc_ok; } api->dispatch_type = dispatch_type; switch (dispatch_type) { case pcmk_ipc_dispatch_main: rc = connect_with_main_loop(api); break; case pcmk_ipc_dispatch_sync: case pcmk_ipc_dispatch_poll: rc = connect_without_main_loop(api); break; } if (rc != pcmk_rc_ok) { return rc; } if ((api->cmds != NULL) && (api->cmds->post_connect != NULL)) { rc = api->cmds->post_connect(api); if (rc != pcmk_rc_ok) { crm_ipc_close(api->ipc); } } return rc; } /*! * \brief Disconnect an IPC API instance * * \param[in] api IPC API connection * * \return Standard Pacemaker return code * * \note If the connection is attached to a main loop, this function should be * called before quitting the main loop, to ensure that all memory is * freed. */ void pcmk_disconnect_ipc(pcmk_ipc_api_t *api) { if ((api == NULL) || (api->ipc == NULL)) { return; } switch (api->dispatch_type) { case pcmk_ipc_dispatch_main: { mainloop_io_t *mainloop_io = api->mainloop_io; // Make sure no code with access to api can use these again api->mainloop_io = NULL; api->ipc = NULL; mainloop_del_ipc_client(mainloop_io); // After this point api might have already been freed } break; case pcmk_ipc_dispatch_poll: case pcmk_ipc_dispatch_sync: { crm_ipc_t *ipc = api->ipc; // Make sure no code with access to api can use ipc again api->ipc = NULL; // This should always be the case already, but to be safe api->free_on_disconnect = false; crm_ipc_destroy(ipc); ipc_post_disconnect(api); } break; } } /*! * \brief Register a callback for IPC API events * * \param[in] api IPC API connection * \param[in] callback Callback to register * \param[in] userdata Caller data to pass to callback * * \note This function may be called multiple times to update the callback * and/or user data. The caller remains responsible for freeing * userdata in any case (after the IPC is disconnected, if the * user data is still registered with the IPC). */ void pcmk_register_ipc_callback(pcmk_ipc_api_t *api, pcmk_ipc_callback_t cb, void *user_data) { if (api == NULL) { return; } api->cb = cb; api->user_data = user_data; } /*! * \internal * \brief Send an XML request across an IPC API connection * * \param[in] api IPC API connection * \param[in] request XML request to send * * \return Standard Pacemaker return code * * \note Daemon-specific IPC API functions should call this function to send * requests, because it handles different dispatch types appropriately. */ int pcmk__send_ipc_request(pcmk_ipc_api_t *api, xmlNode *request) { int rc; xmlNode *reply = NULL; enum crm_ipc_flags flags = crm_ipc_flags_none; if ((api == NULL) || (api->ipc == NULL) || (request == NULL)) { return EINVAL; } crm_log_xml_trace(request, "ipc-sent"); // Synchronous dispatch requires waiting for a reply if ((api->dispatch_type == pcmk_ipc_dispatch_sync) && (api->cmds != NULL) && (api->cmds->reply_expected != NULL) && (api->cmds->reply_expected(api, request))) { flags = crm_ipc_client_response; } // The 0 here means a default timeout of 5 seconds rc = crm_ipc_send(api->ipc, request, flags, 0, &reply); if (rc < 0) { return pcmk_legacy2rc(rc); } else if (rc == 0) { return ENODATA; } // With synchronous dispatch, we dispatch any reply now if (reply != NULL) { - call_api_dispatch(api, reply); + bool more = call_api_dispatch(api, reply); + free_xml(reply); + + while (more) { + rc = crm_ipc_read(api->ipc); + + if (rc == -ENOMSG || rc == pcmk_ok) { + return pcmk_rc_ok; + } else if (rc < 0) { + return -rc; + } + + dispatch_ipc_data(crm_ipc_buffer(api->ipc), 0, api); + } } return pcmk_rc_ok; } /*! * \internal * \brief Create the XML for an IPC request to purge a node from the peer cache * * \param[in] api IPC API connection * \param[in] node_name If not NULL, name of node to purge * \param[in] nodeid If not 0, node ID of node to purge * * \return Newly allocated IPC request XML * * \note The controller, fencer, and pacemakerd use the same request syntax, but * the attribute manager uses a different one. The CIB manager doesn't * have any syntax for it. The executor and scheduler don't connect to the * cluster layer and thus don't have or need any syntax for it. * * \todo Modify the attribute manager to accept the common syntax (as well * as its current one, for compatibility with older clients). Modify * the CIB manager to accept and honor the common syntax. Modify the * executor and scheduler to accept the syntax (immediately returning * success), just for consistency. Modify this function to use the * common syntax with all daemons if their version supports it. */ static xmlNode * create_purge_node_request(pcmk_ipc_api_t *api, const char *node_name, uint32_t nodeid) { xmlNode *request = NULL; const char *client = crm_system_name? crm_system_name : "client"; switch (api->server) { case pcmk_ipc_attrd: request = create_xml_node(NULL, __func__); crm_xml_add(request, F_TYPE, T_ATTRD); crm_xml_add(request, F_ORIG, crm_system_name); crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE); crm_xml_add(request, PCMK__XA_ATTR_NODE_NAME, node_name); if (nodeid > 0) { crm_xml_add_int(request, PCMK__XA_ATTR_NODE_ID, (int) nodeid); } break; case pcmk_ipc_controld: case pcmk_ipc_fenced: case pcmk_ipc_pacemakerd: request = create_request(CRM_OP_RM_NODE_CACHE, NULL, NULL, pcmk_ipc_name(api, false), client, NULL); if (nodeid > 0) { crm_xml_set_id(request, "%lu", (unsigned long) nodeid); } crm_xml_add(request, XML_ATTR_UNAME, node_name); break; case pcmk_ipc_based: case pcmk_ipc_execd: case pcmk_ipc_schedulerd: break; } return request; } /*! * \brief Ask a Pacemaker daemon to purge a node from its peer cache * * \param[in] api IPC API connection * \param[in] node_name If not NULL, name of node to purge * \param[in] nodeid If not 0, node ID of node to purge * * \return Standard Pacemaker return code * * \note At least one of node_name or nodeid must be specified. */ int pcmk_ipc_purge_node(pcmk_ipc_api_t *api, const char *node_name, uint32_t nodeid) { int rc = 0; xmlNode *request = NULL; if (api == NULL) { return EINVAL; } if ((node_name == NULL) && (nodeid == 0)) { return EINVAL; } request = create_purge_node_request(api, node_name, nodeid); if (request == NULL) { return EOPNOTSUPP; } rc = pcmk__send_ipc_request(api, request); free_xml(request); crm_debug("%s peer cache purge of node %s[%lu]: rc=%d", pcmk_ipc_name(api, true), node_name, (unsigned long) nodeid, rc); return rc; } /* * Generic IPC API (to eventually be deprecated as public API and made internal) */ struct crm_ipc_s { struct pollfd pfd; unsigned int max_buf_size; // maximum bytes we can send or receive over IPC unsigned int buf_size; // size of allocated buffer int msg_size; int need_reply; char *buffer; char *server_name; // server IPC name being connected to qb_ipcc_connection_t *ipc; }; /*! * \brief Create a new (legacy) object for using Pacemaker daemon IPC * * \param[in] name IPC system name to connect to * \param[in] max_size Use a maximum IPC buffer size of at least this size * * \return Newly allocated IPC object on success, NULL otherwise * * \note The caller is responsible for freeing the result using * crm_ipc_destroy(). * \note This should be considered deprecated for use with daemons supported by * pcmk_new_ipc_api(). */ crm_ipc_t * crm_ipc_new(const char *name, size_t max_size) { crm_ipc_t *client = NULL; client = calloc(1, sizeof(crm_ipc_t)); if (client == NULL) { crm_err("Could not create IPC connection: %s", strerror(errno)); return NULL; } client->server_name = strdup(name); if (client->server_name == NULL) { crm_err("Could not create %s IPC connection: %s", name, strerror(errno)); free(client); return NULL; } client->buf_size = pcmk__ipc_buffer_size(max_size); client->buffer = malloc(client->buf_size); if (client->buffer == NULL) { crm_err("Could not create %s IPC connection: %s", name, strerror(errno)); free(client->server_name); free(client); return NULL; } /* Clients initiating connection pick the max buf size */ client->max_buf_size = client->buf_size; client->pfd.fd = -1; client->pfd.events = POLLIN; client->pfd.revents = 0; return client; } /*! * \brief Establish an IPC connection to a Pacemaker component * * \param[in] client Connection instance obtained from crm_ipc_new() * * \return TRUE on success, FALSE otherwise (in which case errno will be set; * specifically, in case of discovering the remote side is not * authentic, its value is set to ECONNABORTED). */ bool crm_ipc_connect(crm_ipc_t * client) { uid_t cl_uid = 0; gid_t cl_gid = 0; pid_t found_pid = 0; uid_t found_uid = 0; gid_t found_gid = 0; int rv; client->need_reply = FALSE; client->ipc = qb_ipcc_connect(client->server_name, client->buf_size); if (client->ipc == NULL) { crm_debug("Could not establish %s IPC connection: %s (%d)", client->server_name, pcmk_rc_str(errno), errno); return FALSE; } client->pfd.fd = crm_ipc_get_fd(client); if (client->pfd.fd < 0) { rv = errno; /* message already omitted */ crm_ipc_close(client); errno = rv; return FALSE; } rv = pcmk_daemon_user(&cl_uid, &cl_gid); if (rv < 0) { /* message already omitted */ crm_ipc_close(client); errno = -rv; return FALSE; } if ((rv = pcmk__crm_ipc_is_authentic_process(client->ipc, client->pfd.fd, cl_uid, cl_gid, &found_pid, &found_uid, &found_gid)) == pcmk_rc_ipc_unauthorized) { crm_err("%s IPC provider authentication failed: process %lld has " "uid %lld (expected %lld) and gid %lld (expected %lld)", client->server_name, (long long) PCMK__SPECIAL_PID_AS_0(found_pid), (long long) found_uid, (long long) cl_uid, (long long) found_gid, (long long) cl_gid); crm_ipc_close(client); errno = ECONNABORTED; return FALSE; } else if (rv != pcmk_rc_ok) { crm_perror(LOG_ERR, "Could not verify authenticity of %s IPC provider", client->server_name); crm_ipc_close(client); if (rv > 0) { errno = rv; } else { errno = ENOTCONN; } return FALSE; } qb_ipcc_context_set(client->ipc, client); client->max_buf_size = qb_ipcc_get_buffer_size(client->ipc); if (client->max_buf_size > client->buf_size) { free(client->buffer); client->buffer = calloc(1, client->max_buf_size); client->buf_size = client->max_buf_size; } return TRUE; } void crm_ipc_close(crm_ipc_t * client) { if (client) { if (client->ipc) { qb_ipcc_connection_t *ipc = client->ipc; client->ipc = NULL; qb_ipcc_disconnect(ipc); } } } void crm_ipc_destroy(crm_ipc_t * client) { if (client) { if (client->ipc && qb_ipcc_is_connected(client->ipc)) { crm_notice("Destroying active %s IPC connection", client->server_name); /* The next line is basically unsafe * * If this connection was attached to mainloop and mainloop is active, * the 'disconnected' callback will end up back here and we'll end * up free'ing the memory twice - something that can still happen * even without this if we destroy a connection and it closes before * we call exit */ /* crm_ipc_close(client); */ } else { crm_trace("Destroying inactive %s IPC connection", client->server_name); } free(client->buffer); free(client->server_name); free(client); } } int crm_ipc_get_fd(crm_ipc_t * client) { int fd = 0; if (client && client->ipc && (qb_ipcc_fd_get(client->ipc, &fd) == 0)) { return fd; } errno = EINVAL; crm_perror(LOG_ERR, "Could not obtain file descriptor for %s IPC", (client? client->server_name : "unspecified")); return -errno; } bool crm_ipc_connected(crm_ipc_t * client) { bool rc = FALSE; if (client == NULL) { crm_trace("No client"); return FALSE; } else if (client->ipc == NULL) { crm_trace("No connection"); return FALSE; } else if (client->pfd.fd < 0) { crm_trace("Bad descriptor"); return FALSE; } rc = qb_ipcc_is_connected(client->ipc); if (rc == FALSE) { client->pfd.fd = -EINVAL; } return rc; } /*! * \brief Check whether an IPC connection is ready to be read * * \param[in] client Connection to check * * \return Positive value if ready to be read, 0 if not ready, -errno on error */ int crm_ipc_ready(crm_ipc_t *client) { int rc; CRM_ASSERT(client != NULL); if (crm_ipc_connected(client) == FALSE) { return -ENOTCONN; } client->pfd.revents = 0; rc = poll(&(client->pfd), 1, 0); return (rc < 0)? -errno : rc; } // \return Standard Pacemaker return code static int crm_ipc_decompress(crm_ipc_t * client) { pcmk__ipc_header_t *header = (pcmk__ipc_header_t *)(void*)client->buffer; if (header->size_compressed) { int rc = 0; unsigned int size_u = 1 + header->size_uncompressed; /* never let buf size fall below our max size required for ipc reads. */ unsigned int new_buf_size = QB_MAX((sizeof(pcmk__ipc_header_t) + size_u), client->max_buf_size); char *uncompressed = calloc(1, new_buf_size); crm_trace("Decompressing message data %u bytes into %u bytes", header->size_compressed, size_u); rc = BZ2_bzBuffToBuffDecompress(uncompressed + sizeof(pcmk__ipc_header_t), &size_u, client->buffer + sizeof(pcmk__ipc_header_t), header->size_compressed, 1, 0); if (rc != BZ_OK) { crm_err("Decompression failed: %s " CRM_XS " bzerror=%d", bz2_strerror(rc), rc); free(uncompressed); return EILSEQ; } /* * This assert no longer holds true. For an identical msg, some clients may * require compression, and others may not. If that same msg (event) is sent * to multiple clients, it could result in some clients receiving a compressed * msg even though compression was not explicitly required for them. * * CRM_ASSERT((header->size_uncompressed + sizeof(pcmk__ipc_header_t)) >= ipc_buffer_max); */ CRM_ASSERT(size_u == header->size_uncompressed); memcpy(uncompressed, client->buffer, sizeof(pcmk__ipc_header_t)); /* Preserve the header */ header = (pcmk__ipc_header_t *)(void*)uncompressed; free(client->buffer); client->buf_size = new_buf_size; client->buffer = uncompressed; } CRM_ASSERT(client->buffer[sizeof(pcmk__ipc_header_t) + header->size_uncompressed - 1] == 0); return pcmk_rc_ok; } long crm_ipc_read(crm_ipc_t * client) { pcmk__ipc_header_t *header = NULL; CRM_ASSERT(client != NULL); CRM_ASSERT(client->ipc != NULL); CRM_ASSERT(client->buffer != NULL); client->buffer[0] = 0; client->msg_size = qb_ipcc_event_recv(client->ipc, client->buffer, client->buf_size, 0); if (client->msg_size >= 0) { int rc = crm_ipc_decompress(client); if (rc != pcmk_rc_ok) { return pcmk_rc2legacy(rc); } header = (pcmk__ipc_header_t *)(void*)client->buffer; if (!pcmk__valid_ipc_header(header)) { return -EBADMSG; } crm_trace("Received %s IPC event %d size=%u rc=%d text='%.100s'", client->server_name, header->qb.id, header->qb.size, client->msg_size, client->buffer + sizeof(pcmk__ipc_header_t)); } else { crm_trace("No message received from %s IPC: %s", client->server_name, pcmk_strerror(client->msg_size)); } if (crm_ipc_connected(client) == FALSE || client->msg_size == -ENOTCONN) { crm_err("Connection to %s IPC failed", client->server_name); } if (header) { /* Data excluding the header */ return header->size_uncompressed; } return -ENOMSG; } const char * crm_ipc_buffer(crm_ipc_t * client) { CRM_ASSERT(client != NULL); return client->buffer + sizeof(pcmk__ipc_header_t); } uint32_t crm_ipc_buffer_flags(crm_ipc_t * client) { pcmk__ipc_header_t *header = NULL; CRM_ASSERT(client != NULL); if (client->buffer == NULL) { return 0; } header = (pcmk__ipc_header_t *)(void*)client->buffer; return header->flags; } const char * crm_ipc_name(crm_ipc_t * client) { CRM_ASSERT(client != NULL); return client->server_name; } // \return Standard Pacemaker return code static int internal_ipc_get_reply(crm_ipc_t *client, int request_id, int ms_timeout, ssize_t *bytes) { time_t timeout = time(NULL) + 1 + (ms_timeout / 1000); int rc = pcmk_rc_ok; /* get the reply */ crm_trace("Waiting on reply to %s IPC message %d", client->server_name, request_id); do { *bytes = qb_ipcc_recv(client->ipc, client->buffer, client->buf_size, 1000); if (*bytes > 0) { pcmk__ipc_header_t *hdr = NULL; rc = crm_ipc_decompress(client); if (rc != pcmk_rc_ok) { return rc; } hdr = (pcmk__ipc_header_t *)(void*)client->buffer; if (hdr->qb.id == request_id) { /* Got it */ break; } else if (hdr->qb.id < request_id) { xmlNode *bad = string2xml(crm_ipc_buffer(client)); crm_err("Discarding old reply %d (need %d)", hdr->qb.id, request_id); crm_log_xml_notice(bad, "OldIpcReply"); } else { xmlNode *bad = string2xml(crm_ipc_buffer(client)); crm_err("Discarding newer reply %d (need %d)", hdr->qb.id, request_id); crm_log_xml_notice(bad, "ImpossibleReply"); CRM_ASSERT(hdr->qb.id <= request_id); } } else if (crm_ipc_connected(client) == FALSE) { crm_err("%s IPC provider disconnected while waiting for message %d", client->server_name, request_id); break; } } while (time(NULL) < timeout); if (*bytes < 0) { rc = (int) -*bytes; // System errno } return rc; } /*! * \brief Send an IPC XML message * * \param[in] client Connection to IPC server * \param[in] message XML message to send * \param[in] flags Bitmask of crm_ipc_flags * \param[in] ms_timeout Give up if not sent within this much time * (5 seconds if 0, or no timeout if negative) * \param[out] reply Reply from server (or NULL if none) * * \return Negative errno on error, otherwise size of reply received in bytes * if reply was needed, otherwise number of bytes sent */ int crm_ipc_send(crm_ipc_t * client, xmlNode * message, enum crm_ipc_flags flags, int32_t ms_timeout, xmlNode ** reply) { int rc = 0; ssize_t qb_rc = 0; ssize_t bytes = 0; struct iovec *iov; static uint32_t id = 0; static int factor = 8; pcmk__ipc_header_t *header; if (client == NULL) { crm_notice("Can't send IPC request without connection (bug?): %.100s", message); return -ENOTCONN; } else if (crm_ipc_connected(client) == FALSE) { /* Don't even bother */ crm_notice("Can't send %s IPC requests: Connection closed", client->server_name); return -ENOTCONN; } if (ms_timeout == 0) { ms_timeout = 5000; } if (client->need_reply) { qb_rc = qb_ipcc_recv(client->ipc, client->buffer, client->buf_size, ms_timeout); if (qb_rc < 0) { crm_warn("Sending %s IPC disabled until pending reply received", client->server_name); return -EALREADY; } else { crm_notice("Sending %s IPC re-enabled after pending reply received", client->server_name); client->need_reply = FALSE; } } id++; CRM_LOG_ASSERT(id != 0); /* Crude wrap-around detection */ rc = pcmk__ipc_prepare_iov(id, message, client->max_buf_size, &iov, &bytes); if (rc != pcmk_rc_ok) { crm_warn("Couldn't prepare %s IPC request: %s " CRM_XS " rc=%d", client->server_name, pcmk_rc_str(rc), rc); return pcmk_rc2legacy(rc); } header = iov[0].iov_base; pcmk__set_ipc_flags(header->flags, client->server_name, flags); if (pcmk_is_set(flags, crm_ipc_proxied)) { /* Don't look for a synchronous response */ pcmk__clear_ipc_flags(flags, "client", crm_ipc_client_response); } if(header->size_compressed) { if(factor < 10 && (client->max_buf_size / 10) < (bytes / factor)) { crm_notice("Compressed message exceeds %d0%% of configured IPC " "limit (%u bytes); consider setting PCMK_ipc_buffer to " "%u or higher", factor, client->max_buf_size, 2 * client->max_buf_size); factor++; } } crm_trace("Sending %s IPC request %d of %u bytes using %dms timeout", client->server_name, header->qb.id, header->qb.size, ms_timeout); if ((ms_timeout > 0) || !pcmk_is_set(flags, crm_ipc_client_response)) { time_t timeout = time(NULL) + 1 + (ms_timeout / 1000); do { /* @TODO Is this check really needed? Won't qb_ipcc_sendv() return * an error if it's not connected? */ if (!crm_ipc_connected(client)) { goto send_cleanup; } qb_rc = qb_ipcc_sendv(client->ipc, iov, 2); } while ((qb_rc == -EAGAIN) && (time(NULL) < timeout)); rc = (int) qb_rc; // Negative of system errno, or bytes sent if (qb_rc <= 0) { goto send_cleanup; } else if (!pcmk_is_set(flags, crm_ipc_client_response)) { crm_trace("Not waiting for reply to %s IPC request %d", client->server_name, header->qb.id); goto send_cleanup; } rc = internal_ipc_get_reply(client, header->qb.id, ms_timeout, &bytes); if (rc != pcmk_rc_ok) { /* We didn't get the reply in time, so disable future sends for now. * The only alternative would be to close the connection since we * don't know how to detect and discard out-of-sequence replies. * * @TODO Implement out-of-sequence detection */ client->need_reply = TRUE; } rc = (int) bytes; // Negative system errno, or size of reply received } else { // No timeout, and client response needed do { qb_rc = qb_ipcc_sendv_recv(client->ipc, iov, 2, client->buffer, client->buf_size, -1); } while ((qb_rc == -EAGAIN) && crm_ipc_connected(client)); rc = (int) qb_rc; // Negative system errno, or size of reply received } if (rc > 0) { pcmk__ipc_header_t *hdr = (pcmk__ipc_header_t *)(void*)client->buffer; crm_trace("Received %d-byte reply %d to %s IPC %d: %.100s", rc, hdr->qb.id, client->server_name, header->qb.id, crm_ipc_buffer(client)); if (reply) { *reply = string2xml(crm_ipc_buffer(client)); } } else { crm_trace("No reply to %s IPC %d: rc=%d", client->server_name, header->qb.id, rc); } send_cleanup: if (crm_ipc_connected(client) == FALSE) { crm_notice("Couldn't send %s IPC request %d: Connection closed " CRM_XS " rc=%d", client->server_name, header->qb.id, rc); } else if (rc == -ETIMEDOUT) { crm_warn("%s IPC request %d failed: %s after %dms " CRM_XS " rc=%d", client->server_name, header->qb.id, pcmk_strerror(rc), ms_timeout, rc); crm_write_blackbox(0, NULL); } else if (rc <= 0) { crm_warn("%s IPC request %d failed: %s " CRM_XS " rc=%d", client->server_name, header->qb.id, ((rc == 0)? "No bytes sent" : pcmk_strerror(rc)), rc); } pcmk_free_ipc_event(iov); return rc; } int pcmk__crm_ipc_is_authentic_process(qb_ipcc_connection_t *qb_ipc, int sock, uid_t refuid, gid_t refgid, pid_t *gotpid, uid_t *gotuid, gid_t *gotgid) { int ret = 0; pid_t found_pid = 0; uid_t found_uid = 0; gid_t found_gid = 0; #if defined(US_AUTH_PEERCRED_UCRED) struct ucred ucred; socklen_t ucred_len = sizeof(ucred); #endif #ifdef HAVE_QB_IPCC_AUTH_GET if (qb_ipc && !qb_ipcc_auth_get(qb_ipc, &found_pid, &found_uid, &found_gid)) { goto do_checks; } #endif #if defined(US_AUTH_PEERCRED_UCRED) if (!getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &ucred, &ucred_len) && ucred_len == sizeof(ucred)) { found_pid = ucred.pid; found_uid = ucred.uid; found_gid = ucred.gid; #elif defined(US_AUTH_PEERCRED_SOCKPEERCRED) struct sockpeercred sockpeercred; socklen_t sockpeercred_len = sizeof(sockpeercred); if (!getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &sockpeercred, &sockpeercred_len) && sockpeercred_len == sizeof(sockpeercred_len)) { found_pid = sockpeercred.pid; found_uid = sockpeercred.uid; found_gid = sockpeercred.gid; #elif defined(US_AUTH_GETPEEREID) if (!getpeereid(sock, &found_uid, &found_gid)) { found_pid = PCMK__SPECIAL_PID; /* cannot obtain PID (FreeBSD) */ #elif defined(US_AUTH_GETPEERUCRED) ucred_t *ucred; if (!getpeerucred(sock, &ucred)) { errno = 0; found_pid = ucred_getpid(ucred); found_uid = ucred_geteuid(ucred); found_gid = ucred_getegid(ucred); ret = -errno; ucred_free(ucred); if (ret) { return (ret < 0) ? ret : -pcmk_err_generic; } #else # error "No way to authenticate a Unix socket peer" errno = 0; if (0) { #endif #ifdef HAVE_QB_IPCC_AUTH_GET do_checks: #endif if (gotpid != NULL) { *gotpid = found_pid; } if (gotuid != NULL) { *gotuid = found_uid; } if (gotgid != NULL) { *gotgid = found_gid; } if (found_uid == 0 || found_uid == refuid || found_gid == refgid) { ret = 0; } else { ret = pcmk_rc_ipc_unauthorized; } } else { ret = (errno > 0) ? errno : pcmk_rc_error; } return ret; } int crm_ipc_is_authentic_process(int sock, uid_t refuid, gid_t refgid, pid_t *gotpid, uid_t *gotuid, gid_t *gotgid) { int ret = pcmk__crm_ipc_is_authentic_process(NULL, sock, refuid, refgid, gotpid, gotuid, gotgid); /* The old function had some very odd return codes*/ if (ret == 0) { return 1; } else if (ret == pcmk_rc_ipc_unauthorized) { return 0; } else { return pcmk_rc2legacy(ret); } } int pcmk__ipc_is_authentic_process_active(const char *name, uid_t refuid, gid_t refgid, pid_t *gotpid) { static char last_asked_name[PATH_MAX / 2] = ""; /* log spam prevention */ int fd; int rc = pcmk_rc_ipc_unresponsive; int auth_rc = 0; int32_t qb_rc; pid_t found_pid = 0; uid_t found_uid = 0; gid_t found_gid = 0; qb_ipcc_connection_t *c; #ifdef HAVE_QB_IPCC_CONNECT_ASYNC struct pollfd pollfd = { 0, }; int poll_rc; c = qb_ipcc_connect_async(name, 0, &(pollfd.fd)); #else c = qb_ipcc_connect(name, 0); #endif if (c == NULL) { crm_info("Could not connect to %s IPC: %s", name, strerror(errno)); rc = pcmk_rc_ipc_unresponsive; goto bail; } #ifdef HAVE_QB_IPCC_CONNECT_ASYNC pollfd.events = POLLIN; do { poll_rc = poll(&pollfd, 1, 2000); } while ((poll_rc == -1) && (errno == EINTR)); if ((poll_rc <= 0) || (qb_ipcc_connect_continue(c) != 0)) { crm_info("Could not connect to %s IPC: %s", name, (poll_rc == 0)?"timeout":strerror(errno)); rc = pcmk_rc_ipc_unresponsive; if (poll_rc > 0) { c = NULL; // qb_ipcc_connect_continue cleaned up for us } goto bail; } #endif qb_rc = qb_ipcc_fd_get(c, &fd); if (qb_rc != 0) { rc = (int) -qb_rc; // System errno crm_err("Could not get fd from %s IPC: %s " CRM_XS " rc=%d", name, pcmk_rc_str(rc), rc); goto bail; } auth_rc = pcmk__crm_ipc_is_authentic_process(c, fd, refuid, refgid, &found_pid, &found_uid, &found_gid); if (auth_rc == pcmk_rc_ipc_unauthorized) { crm_err("Daemon (IPC %s) effectively blocked with unauthorized" " process %lld (uid: %lld, gid: %lld)", name, (long long) PCMK__SPECIAL_PID_AS_0(found_pid), (long long) found_uid, (long long) found_gid); rc = pcmk_rc_ipc_unauthorized; goto bail; } if (auth_rc != pcmk_rc_ok) { rc = auth_rc; crm_err("Could not get peer credentials from %s IPC: %s " CRM_XS " rc=%d", name, pcmk_rc_str(rc), rc); goto bail; } if (gotpid != NULL) { *gotpid = found_pid; } rc = pcmk_rc_ok; if ((found_uid != refuid || found_gid != refgid) && strncmp(last_asked_name, name, sizeof(last_asked_name))) { if ((found_uid == 0) && (refuid != 0)) { crm_warn("Daemon (IPC %s) runs as root, whereas the expected" " credentials are %lld:%lld, hazard of violating" " the least privilege principle", name, (long long) refuid, (long long) refgid); } else { crm_notice("Daemon (IPC %s) runs as %lld:%lld, whereas the" " expected credentials are %lld:%lld, which may" " mean a different set of privileges than expected", name, (long long) found_uid, (long long) found_gid, (long long) refuid, (long long) refgid); } memccpy(last_asked_name, name, '\0', sizeof(last_asked_name)); } bail: if (c != NULL) { qb_ipcc_disconnect(c); } return rc; } diff --git a/lib/common/ipc_controld.c b/lib/common/ipc_controld.c index 08979bac04..60d00c1b9a 100644 --- a/lib/common/ipc_controld.c +++ b/lib/common/ipc_controld.c @@ -1,613 +1,615 @@ /* * Copyright 2020-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 #include #include #include #include #include #include #include #include #include #include #include "crmcommon_private.h" struct controld_api_private_s { char *client_uuid; unsigned int replies_expected; }; // \return Standard Pacemaker return code static int new_data(pcmk_ipc_api_t *api) { struct controld_api_private_s *private = NULL; api->api_data = calloc(1, sizeof(struct controld_api_private_s)); if (api->api_data == NULL) { return errno; } private = api->api_data; /* This is set to the PID because that's how it was always done, but PIDs * are not unique because clients can be remote. The value appears to be * unused other than as part of F_CRM_SYS_FROM in IPC requests, which is * only compared against the internal system names (CRM_SYSTEM_TENGINE, * etc.), so it shouldn't be a problem. */ private->client_uuid = pcmk__getpid_s(); /* @TODO Implement a call ID model similar to the CIB, executor, and fencer * IPC APIs, so that requests and replies can be matched, and * duplicate replies can be discarded. */ return pcmk_rc_ok; } static void free_data(void *data) { free(((struct controld_api_private_s *) data)->client_uuid); free(data); } // \return Standard Pacemaker return code static int post_connect(pcmk_ipc_api_t *api) { /* The controller currently requires clients to register via a hello * request, but does not reply back. */ struct controld_api_private_s *private = api->api_data; const char *client_name = crm_system_name? crm_system_name : "client"; xmlNode *hello; int rc; hello = create_hello_message(private->client_uuid, client_name, PCMK__CONTROLD_API_MAJOR, PCMK__CONTROLD_API_MINOR); rc = pcmk__send_ipc_request(api, hello); free_xml(hello); if (rc != pcmk_rc_ok) { crm_info("Could not send IPC hello to %s: %s " CRM_XS " rc=%s", pcmk_ipc_name(api, true), pcmk_rc_str(rc), rc); } else { crm_debug("Sent IPC hello to %s", pcmk_ipc_name(api, true)); } return rc; } static void set_node_info_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data) { data->reply_type = pcmk_controld_reply_info; if (msg_data == NULL) { return; } data->data.node_info.have_quorum = pcmk__xe_attr_is_true(msg_data, XML_ATTR_HAVE_QUORUM); data->data.node_info.is_remote = pcmk__xe_attr_is_true(msg_data, XML_NODE_IS_REMOTE); crm_element_value_int(msg_data, XML_ATTR_ID, &(data->data.node_info.id)); data->data.node_info.uuid = crm_element_value(msg_data, XML_ATTR_UUID); data->data.node_info.uname = crm_element_value(msg_data, XML_ATTR_UNAME); data->data.node_info.state = crm_element_value(msg_data, XML_NODE_IS_PEER); } static void set_ping_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data) { data->reply_type = pcmk_controld_reply_ping; if (msg_data == NULL) { return; } data->data.ping.sys_from = crm_element_value(msg_data, XML_PING_ATTR_SYSFROM); data->data.ping.fsa_state = crm_element_value(msg_data, XML_PING_ATTR_CRMDSTATE); data->data.ping.result = crm_element_value(msg_data, XML_PING_ATTR_STATUS); } static void set_nodes_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data) { pcmk_controld_api_node_t *node_info; data->reply_type = pcmk_controld_reply_nodes; for (xmlNode *node = first_named_child(msg_data, XML_CIB_TAG_NODE); node != NULL; node = crm_next_same_xml(node)) { long long id_ll = 0; node_info = calloc(1, sizeof(pcmk_controld_api_node_t)); crm_element_value_ll(node, XML_ATTR_ID, &id_ll); if (id_ll > 0) { node_info->id = id_ll; } node_info->uname = crm_element_value(node, XML_ATTR_UNAME); node_info->state = crm_element_value(node, XML_NODE_IN_CLUSTER); data->data.nodes = g_list_prepend(data->data.nodes, node_info); } } static bool reply_expected(pcmk_ipc_api_t *api, xmlNode *request) { const char *command = crm_element_value(request, F_CRM_TASK); if (command == NULL) { return false; } // We only need to handle commands that functions in this file can send return !strcmp(command, CRM_OP_REPROBE) || !strcmp(command, CRM_OP_NODE_INFO) || !strcmp(command, CRM_OP_PING) || !strcmp(command, CRM_OP_LRM_FAIL) || !strcmp(command, CRM_OP_LRM_DELETE); } -static void +static bool dispatch(pcmk_ipc_api_t *api, xmlNode *reply) { struct controld_api_private_s *private = api->api_data; crm_exit_t status = CRM_EX_OK; xmlNode *msg_data = NULL; const char *value = NULL; pcmk_controld_api_reply_t reply_data = { pcmk_controld_reply_unknown, NULL, NULL, }; if (private->replies_expected > 0) { private->replies_expected--; } // Do some basic validation of the reply /* @TODO We should be able to verify that value is always a response, but * currently the controller doesn't always properly set the type. Even * if we fix the controller, we'll still need to handle replies from * old versions (feature set could be used to differentiate). */ value = crm_element_value(reply, F_CRM_MSG_TYPE); if ((value == NULL) || (strcmp(value, XML_ATTR_REQUEST) && strcmp(value, XML_ATTR_RESPONSE))) { crm_debug("Unrecognizable controller message: invalid message type '%s'", crm_str(value)); status = CRM_EX_PROTOCOL; goto done; } if (crm_element_value(reply, XML_ATTR_REFERENCE) == NULL) { crm_debug("Unrecognizable controller message: no reference"); status = CRM_EX_PROTOCOL; goto done; } value = crm_element_value(reply, F_CRM_TASK); if (value == NULL) { crm_debug("Unrecognizable controller message: no command name"); status = CRM_EX_PROTOCOL; goto done; } // Parse useful info from reply reply_data.feature_set = crm_element_value(reply, XML_ATTR_VERSION); reply_data.host_from = crm_element_value(reply, F_CRM_HOST_FROM); msg_data = get_message_xml(reply, F_CRM_DATA); if (!strcmp(value, CRM_OP_REPROBE)) { reply_data.reply_type = pcmk_controld_reply_reprobe; } else if (!strcmp(value, CRM_OP_NODE_INFO)) { set_node_info_data(&reply_data, msg_data); } else if (!strcmp(value, CRM_OP_INVOKE_LRM)) { reply_data.reply_type = pcmk_controld_reply_resource; reply_data.data.resource.node_state = msg_data; } else if (!strcmp(value, CRM_OP_PING)) { set_ping_data(&reply_data, msg_data); } else if (!strcmp(value, PCMK__CONTROLD_CMD_NODES)) { set_nodes_data(&reply_data, msg_data); } else { crm_debug("Unrecognizable controller message: unknown command '%s'", value); status = CRM_EX_PROTOCOL; } done: pcmk__call_ipc_callback(api, pcmk_ipc_event_reply, status, &reply_data); // Free any reply data that was allocated if (pcmk__str_eq(value, PCMK__CONTROLD_CMD_NODES, pcmk__str_casei)) { g_list_free_full(reply_data.data.nodes, free); } + + return false; } pcmk__ipc_methods_t * pcmk__controld_api_methods() { pcmk__ipc_methods_t *cmds = calloc(1, sizeof(pcmk__ipc_methods_t)); if (cmds != NULL) { cmds->new_data = new_data; cmds->free_data = free_data; cmds->post_connect = post_connect; cmds->reply_expected = reply_expected; cmds->dispatch = dispatch; } return cmds; } /*! * \internal * \brief Create XML for a controller IPC request * * \param[in] api Controller connection * \param[in] op Controller IPC command name * \param[in] node Node name to set as destination host * \param[in] msg_data XML to attach to request as message data * * \return Newly allocated XML for request */ static xmlNode * create_controller_request(pcmk_ipc_api_t *api, const char *op, const char *node, xmlNode *msg_data) { struct controld_api_private_s *private = NULL; const char *sys_to = NULL; if (api == NULL) { return NULL; } private = api->api_data; if ((node == NULL) && !strcmp(op, CRM_OP_PING)) { sys_to = CRM_SYSTEM_DC; } else { sys_to = CRM_SYSTEM_CRMD; } return create_request(op, msg_data, node, sys_to, (crm_system_name? crm_system_name : "client"), private->client_uuid); } // \return Standard Pacemaker return code static int send_controller_request(pcmk_ipc_api_t *api, xmlNode *request, bool reply_is_expected) { int rc; if (crm_element_value(request, XML_ATTR_REFERENCE) == NULL) { return EINVAL; } rc = pcmk__send_ipc_request(api, request); if ((rc == pcmk_rc_ok) && reply_is_expected) { struct controld_api_private_s *private = api->api_data; private->replies_expected++; } return rc; } static xmlNode * create_reprobe_message_data(const char *target_node, const char *router_node) { xmlNode *msg_data; msg_data = create_xml_node(NULL, "data_for_" CRM_OP_REPROBE); crm_xml_add(msg_data, XML_LRM_ATTR_TARGET, target_node); if ((router_node != NULL) && !pcmk__str_eq(router_node, target_node, pcmk__str_casei)) { crm_xml_add(msg_data, XML_LRM_ATTR_ROUTER_NODE, router_node); } return msg_data; } /*! * \brief Send a reprobe controller operation * * \param[in] api Controller connection * \param[in] target_node Name of node to reprobe * \param[in] router_node Router node for host * * \return Standard Pacemaker return code * \note Event callback will get a reply of type pcmk_controld_reply_reprobe. */ int pcmk_controld_api_reprobe(pcmk_ipc_api_t *api, const char *target_node, const char *router_node) { xmlNode *request; xmlNode *msg_data; int rc = pcmk_rc_ok; if (api == NULL) { return EINVAL; } if (router_node == NULL) { router_node = target_node; } crm_debug("Sending %s IPC request to reprobe %s via %s", pcmk_ipc_name(api, true), crm_str(target_node), crm_str(router_node)); msg_data = create_reprobe_message_data(target_node, router_node); request = create_controller_request(api, CRM_OP_REPROBE, router_node, msg_data); rc = send_controller_request(api, request, true); free_xml(msg_data); free_xml(request); return rc; } /*! * \brief Send a "node info" controller operation * * \param[in] api Controller connection * \param[in] nodeid ID of node to get info for (or 0 for local node) * * \return Standard Pacemaker return code * \note Event callback will get a reply of type pcmk_controld_reply_info. */ int pcmk_controld_api_node_info(pcmk_ipc_api_t *api, uint32_t nodeid) { xmlNode *request; int rc = pcmk_rc_ok; request = create_controller_request(api, CRM_OP_NODE_INFO, NULL, NULL); if (request == NULL) { return EINVAL; } if (nodeid > 0) { crm_xml_set_id(request, "%lu", (unsigned long) nodeid); } rc = send_controller_request(api, request, true); free_xml(request); return rc; } /*! * \brief Ask the controller for status * * \param[in] api Controller connection * \param[in] node_name Name of node whose status is desired (or NULL for DC) * * \return Standard Pacemaker return code * \note Event callback will get a reply of type pcmk_controld_reply_ping. */ int pcmk_controld_api_ping(pcmk_ipc_api_t *api, const char *node_name) { xmlNode *request; int rc = pcmk_rc_ok; request = create_controller_request(api, CRM_OP_PING, node_name, NULL); if (request == NULL) { return EINVAL; } rc = send_controller_request(api, request, true); free_xml(request); return rc; } /*! * \brief Ask the controller for cluster information * * \param[in] api Controller connection * * \return Standard Pacemaker return code * \note Event callback will get a reply of type pcmk_controld_reply_nodes. */ int pcmk_controld_api_list_nodes(pcmk_ipc_api_t *api) { xmlNode *request; int rc = EINVAL; request = create_controller_request(api, PCMK__CONTROLD_CMD_NODES, NULL, NULL); if (request != NULL) { rc = send_controller_request(api, request, true); free_xml(request); } return rc; } // \return Standard Pacemaker return code static int controller_resource_op(pcmk_ipc_api_t *api, const char *op, const char *target_node, const char *router_node, bool cib_only, const char *rsc_id, const char *rsc_long_id, const char *standard, const char *provider, const char *type) { int rc = pcmk_rc_ok; char *key; xmlNode *request, *msg_data, *xml_rsc, *params; if (api == NULL) { return EINVAL; } if (router_node == NULL) { router_node = target_node; } msg_data = create_xml_node(NULL, XML_GRAPH_TAG_RSC_OP); /* The controller logs the transition key from resource op requests, so we * need to have *something* for it. * @TODO don't use "crm-resource" */ key = pcmk__transition_key(0, getpid(), 0, "xxxxxxxx-xrsc-opxx-xcrm-resourcexxxx"); crm_xml_add(msg_data, XML_ATTR_TRANSITION_KEY, key); free(key); crm_xml_add(msg_data, XML_LRM_ATTR_TARGET, target_node); if (!pcmk__str_eq(router_node, target_node, pcmk__str_casei)) { crm_xml_add(msg_data, XML_LRM_ATTR_ROUTER_NODE, router_node); } if (cib_only) { // Indicate that only the CIB needs to be cleaned crm_xml_add(msg_data, PCMK__XA_MODE, XML_TAG_CIB); } xml_rsc = create_xml_node(msg_data, XML_CIB_TAG_RESOURCE); crm_xml_add(xml_rsc, XML_ATTR_ID, rsc_id); crm_xml_add(xml_rsc, XML_ATTR_ID_LONG, rsc_long_id); crm_xml_add(xml_rsc, XML_AGENT_ATTR_CLASS, standard); crm_xml_add(xml_rsc, XML_AGENT_ATTR_PROVIDER, provider); crm_xml_add(xml_rsc, XML_ATTR_TYPE, type); params = create_xml_node(msg_data, XML_TAG_ATTRS); crm_xml_add(params, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET); // The controller parses the timeout from the request key = crm_meta_name(XML_ATTR_TIMEOUT); crm_xml_add(params, key, "60000"); /* 1 minute */ //@TODO pass as arg free(key); request = create_controller_request(api, op, router_node, msg_data); rc = send_controller_request(api, request, true); free_xml(msg_data); free_xml(request); return rc; } /*! * \brief Ask the controller to fail a resource * * \param[in] api Controller connection * \param[in] target_node Name of node resource is on * \param[in] router_node Router node for target * \param[in] rsc_id ID of resource to fail * \param[in] rsc_long_id Long ID of resource (if any) * \param[in] standard Standard of resource * \param[in] provider Provider of resource (if any) * \param[in] type Type of resource to fail * * \return Standard Pacemaker return code * \note Event callback will get a reply of type pcmk_controld_reply_resource. */ int pcmk_controld_api_fail(pcmk_ipc_api_t *api, const char *target_node, const char *router_node, const char *rsc_id, const char *rsc_long_id, const char *standard, const char *provider, const char *type) { crm_debug("Sending %s IPC request to fail %s (a.k.a. %s) on %s via %s", pcmk_ipc_name(api, true), crm_str(rsc_id), crm_str(rsc_long_id), crm_str(target_node), crm_str(router_node)); return controller_resource_op(api, CRM_OP_LRM_FAIL, target_node, router_node, false, rsc_id, rsc_long_id, standard, provider, type); } /*! * \brief Ask the controller to refresh a resource * * \param[in] api Controller connection * \param[in] target_node Name of node resource is on * \param[in] router_node Router node for target * \param[in] rsc_id ID of resource to refresh * \param[in] rsc_long_id Long ID of resource (if any) * \param[in] standard Standard of resource * \param[in] provider Provider of resource (if any) * \param[in] type Type of resource * \param[in] cib_only If true, clean resource from CIB only * * \return Standard Pacemaker return code * \note Event callback will get a reply of type pcmk_controld_reply_resource. */ int pcmk_controld_api_refresh(pcmk_ipc_api_t *api, const char *target_node, const char *router_node, const char *rsc_id, const char *rsc_long_id, const char *standard, const char *provider, const char *type, bool cib_only) { crm_debug("Sending %s IPC request to refresh %s (a.k.a. %s) on %s via %s", pcmk_ipc_name(api, true), crm_str(rsc_id), crm_str(rsc_long_id), crm_str(target_node), crm_str(router_node)); return controller_resource_op(api, CRM_OP_LRM_DELETE, target_node, router_node, cib_only, rsc_id, rsc_long_id, standard, provider, type); } /*! * \brief Get the number of IPC replies currently expected from the controller * * \param[in] api Controller IPC API connection * * \return Number of replies expected */ unsigned int pcmk_controld_api_replies_expected(pcmk_ipc_api_t *api) { struct controld_api_private_s *private = api->api_data; return private->replies_expected; } /*! * \brief Create XML for a controller IPC "hello" message * * \deprecated This function is deprecated as part of the public C API. */ // \todo make this static to this file when breaking API backward compatibility xmlNode * create_hello_message(const char *uuid, const char *client_name, const char *major_version, const char *minor_version) { xmlNode *hello_node = NULL; xmlNode *hello = NULL; if (pcmk__str_empty(uuid) || pcmk__str_empty(client_name) || pcmk__str_empty(major_version) || pcmk__str_empty(minor_version)) { crm_err("Could not create IPC hello message from %s (UUID %s): " "missing information", client_name? client_name : "unknown client", uuid? uuid : "unknown"); return NULL; } hello_node = create_xml_node(NULL, XML_TAG_OPTIONS); if (hello_node == NULL) { crm_err("Could not create IPC hello message from %s (UUID %s): " "Message data creation failed", client_name, uuid); return NULL; } crm_xml_add(hello_node, "major_version", major_version); crm_xml_add(hello_node, "minor_version", minor_version); crm_xml_add(hello_node, "client_name", client_name); crm_xml_add(hello_node, "client_uuid", uuid); hello = create_request(CRM_OP_HELLO, hello_node, NULL, NULL, client_name, uuid); if (hello == NULL) { crm_err("Could not create IPC hello message from %s (UUID %s): " "Request creation failed", client_name, uuid); return NULL; } free_xml(hello_node); crm_trace("Created hello message from %s (UUID %s)", client_name, uuid); return hello; } diff --git a/lib/common/ipc_pacemakerd.c b/lib/common/ipc_pacemakerd.c index ddec489260..2bec0d115d 100644 --- a/lib/common/ipc_pacemakerd.c +++ b/lib/common/ipc_pacemakerd.c @@ -1,253 +1,256 @@ /* - * Copyright 2020 the Pacemaker project contributors + * Copyright 2020-2022 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 #include #include #include #include #include "crmcommon_private.h" typedef struct pacemakerd_api_private_s { enum pcmk_pacemakerd_state state; char *client_uuid; } pacemakerd_api_private_t; static const char *pacemakerd_state_str[] = { XML_PING_ATTR_PACEMAKERDSTATE_INIT, XML_PING_ATTR_PACEMAKERDSTATE_STARTINGDAEMONS, XML_PING_ATTR_PACEMAKERDSTATE_WAITPING, XML_PING_ATTR_PACEMAKERDSTATE_RUNNING, XML_PING_ATTR_PACEMAKERDSTATE_SHUTTINGDOWN, XML_PING_ATTR_PACEMAKERDSTATE_SHUTDOWNCOMPLETE }; enum pcmk_pacemakerd_state pcmk_pacemakerd_api_daemon_state_text2enum(const char *state) { int i; if (state == NULL) { return pcmk_pacemakerd_state_invalid; } for (i=pcmk_pacemakerd_state_init; i <= pcmk_pacemakerd_state_max; i++) { if (pcmk__str_eq(state, pacemakerd_state_str[i], pcmk__str_none)) { return i; } } return pcmk_pacemakerd_state_invalid; } const char * pcmk_pacemakerd_api_daemon_state_enum2text( enum pcmk_pacemakerd_state state) { if ((state >= pcmk_pacemakerd_state_init) && (state <= pcmk_pacemakerd_state_max)) { return pacemakerd_state_str[state]; } return "invalid"; } // \return Standard Pacemaker return code static int new_data(pcmk_ipc_api_t *api) { struct pacemakerd_api_private_s *private = NULL; api->api_data = calloc(1, sizeof(struct pacemakerd_api_private_s)); if (api->api_data == NULL) { return errno; } private = api->api_data; private->state = pcmk_pacemakerd_state_invalid; /* other as with cib, controld, ... we are addressing pacemakerd just from the local node -> pid is unique and thus sufficient as an ID */ private->client_uuid = pcmk__getpid_s(); return pcmk_rc_ok; } static void free_data(void *data) { free(((struct pacemakerd_api_private_s *) data)->client_uuid); free(data); } // \return Standard Pacemaker return code static int post_connect(pcmk_ipc_api_t *api) { struct pacemakerd_api_private_s *private = NULL; if (api->api_data == NULL) { return EINVAL; } private = api->api_data; private->state = pcmk_pacemakerd_state_invalid; return pcmk_rc_ok; } static void post_disconnect(pcmk_ipc_api_t *api) { struct pacemakerd_api_private_s *private = NULL; if (api->api_data == NULL) { return; } private = api->api_data; private->state = pcmk_pacemakerd_state_invalid; return; } static bool reply_expected(pcmk_ipc_api_t *api, xmlNode *request) { const char *command = crm_element_value(request, F_CRM_TASK); if (command == NULL) { return false; } // We only need to handle commands that functions in this file can send return pcmk__str_any_of(command, CRM_OP_PING, CRM_OP_QUIT, NULL); } -static void +static bool dispatch(pcmk_ipc_api_t *api, xmlNode *reply) { crm_exit_t status = CRM_EX_OK; xmlNode *msg_data = NULL; pcmk_pacemakerd_api_reply_t reply_data = { pcmk_pacemakerd_reply_unknown }; const char *value = NULL; long long value_ll = 0; if (pcmk__str_eq((const char *) reply->name, "ack", pcmk__str_casei)) { - return; + long long int ack_status = 0; + pcmk__scan_ll(crm_element_value(reply, "status"), &ack_status, CRM_EX_OK); + return ack_status == CRM_EX_INDETERMINATE; } value = crm_element_value(reply, F_CRM_MSG_TYPE); if ((value == NULL) || (strcmp(value, XML_ATTR_RESPONSE))) { crm_debug("Unrecognizable pacemakerd message: invalid message type '%s'", crm_str(value)); status = CRM_EX_PROTOCOL; goto done; } if (crm_element_value(reply, XML_ATTR_REFERENCE) == NULL) { crm_debug("Unrecognizable pacemakerd message: no reference"); status = CRM_EX_PROTOCOL; goto done; } value = crm_element_value(reply, F_CRM_TASK); // Parse useful info from reply msg_data = get_message_xml(reply, F_CRM_DATA); crm_element_value_ll(msg_data, XML_ATTR_TSTAMP, &value_ll); if (pcmk__str_eq(value, CRM_OP_PING, pcmk__str_none)) { reply_data.reply_type = pcmk_pacemakerd_reply_ping; reply_data.data.ping.state = pcmk_pacemakerd_api_daemon_state_text2enum( crm_element_value(msg_data, XML_PING_ATTR_PACEMAKERDSTATE)); reply_data.data.ping.status = pcmk__str_eq(crm_element_value(msg_data, XML_PING_ATTR_STATUS), "ok", pcmk__str_casei)?pcmk_rc_ok:pcmk_rc_error; reply_data.data.ping.last_good = (time_t) value_ll; reply_data.data.ping.sys_from = crm_element_value(msg_data, XML_PING_ATTR_SYSFROM); } else if (pcmk__str_eq(value, CRM_OP_QUIT, pcmk__str_none)) { reply_data.reply_type = pcmk_pacemakerd_reply_shutdown; reply_data.data.shutdown.status = atoi(crm_element_value(msg_data, XML_LRM_ATTR_OPSTATUS)); } else { crm_debug("Unrecognizable pacemakerd message: '%s'", crm_str(value)); status = CRM_EX_PROTOCOL; goto done; } done: pcmk__call_ipc_callback(api, pcmk_ipc_event_reply, status, &reply_data); + return false; } pcmk__ipc_methods_t * pcmk__pacemakerd_api_methods() { pcmk__ipc_methods_t *cmds = calloc(1, sizeof(pcmk__ipc_methods_t)); if (cmds != NULL) { cmds->new_data = new_data; cmds->free_data = free_data; cmds->post_connect = post_connect; cmds->reply_expected = reply_expected; cmds->dispatch = dispatch; cmds->post_disconnect = post_disconnect; } return cmds; } static int do_pacemakerd_api_call(pcmk_ipc_api_t *api, const char *ipc_name, const char *task) { pacemakerd_api_private_t *private; xmlNode *cmd; int rc; if (api == NULL) { return EINVAL; } private = api->api_data; CRM_ASSERT(private != NULL); cmd = create_request(task, NULL, NULL, CRM_SYSTEM_MCP, pcmk__ipc_sys_name(ipc_name, "client"), private->client_uuid); if (cmd) { rc = pcmk__send_ipc_request(api, cmd); if (rc != pcmk_rc_ok) { crm_debug("Couldn't send request to pacemakerd: %s rc=%d", pcmk_rc_str(rc), rc); } free_xml(cmd); } else { rc = ENOMSG; } return rc; } int pcmk_pacemakerd_api_ping(pcmk_ipc_api_t *api, const char *ipc_name) { return do_pacemakerd_api_call(api, ipc_name, CRM_OP_PING); } int pcmk_pacemakerd_api_shutdown(pcmk_ipc_api_t *api, const char *ipc_name) { return do_pacemakerd_api_call(api, ipc_name, CRM_OP_QUIT); } diff --git a/lib/common/ipc_schedulerd.c b/lib/common/ipc_schedulerd.c index 0f6a893dd1..2039d2eb44 100644 --- a/lib/common/ipc_schedulerd.c +++ b/lib/common/ipc_schedulerd.c @@ -1,177 +1,178 @@ /* * Copyright 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 #include #include #include #include #include #include #include #include #include "crmcommon_private.h" typedef struct schedulerd_api_private_s { char *client_uuid; } schedulerd_api_private_t; // \return Standard Pacemaker return code static int new_data(pcmk_ipc_api_t *api) { struct schedulerd_api_private_s *private = NULL; api->api_data = calloc(1, sizeof(struct schedulerd_api_private_s)); if (api->api_data == NULL) { return errno; } private = api->api_data; /* See comments in ipc_pacemakerd.c. */ private->client_uuid = pcmk__getpid_s(); return pcmk_rc_ok; } static void free_data(void *data) { free(((struct schedulerd_api_private_s *) data)->client_uuid); free(data); } // \return Standard Pacemaker return code static int post_connect(pcmk_ipc_api_t *api) { if (api->api_data == NULL) { return EINVAL; } return pcmk_rc_ok; } static bool reply_expected(pcmk_ipc_api_t *api, xmlNode *request) { const char *command = crm_element_value(request, F_CRM_TASK); if (command == NULL) { return false; } // We only need to handle commands that functions in this file can send return pcmk__str_any_of(command, CRM_OP_PECALC, NULL); } -static void +static bool dispatch(pcmk_ipc_api_t *api, xmlNode *reply) { crm_exit_t status = CRM_EX_OK; xmlNode *msg_data = NULL; pcmk_schedulerd_api_reply_t reply_data = { pcmk_schedulerd_reply_unknown }; const char *value = NULL; if (pcmk__str_eq((const char *) reply->name, "ack", pcmk__str_casei)) { - return; + return false; } value = crm_element_value(reply, F_CRM_MSG_TYPE); if ((value == NULL) || (strcmp(value, XML_ATTR_RESPONSE))) { crm_debug("Unrecognizable schedulerd message: invalid message type '%s'", crm_str(value)); status = CRM_EX_PROTOCOL; goto done; } if (crm_element_value(reply, XML_ATTR_REFERENCE) == NULL) { crm_debug("Unrecognizable schedulerd message: no reference"); status = CRM_EX_PROTOCOL; goto done; } // Parse useful info from reply msg_data = get_message_xml(reply, F_CRM_DATA); value = crm_element_value(reply, F_CRM_TASK); if (pcmk__str_eq(value, CRM_OP_PECALC, pcmk__str_none)) { reply_data.reply_type = pcmk_schedulerd_reply_graph; reply_data.data.graph.reference = crm_element_value(reply, XML_ATTR_REFERENCE); reply_data.data.graph.input = crm_element_value(reply, F_CRM_TGRAPH_INPUT); reply_data.data.graph.tgraph = msg_data; } else { crm_debug("Unrecognizable pacemakerd message: '%s'", crm_str(value)); status = CRM_EX_PROTOCOL; goto done; } done: pcmk__call_ipc_callback(api, pcmk_ipc_event_reply, status, &reply_data); + return false; } pcmk__ipc_methods_t * pcmk__schedulerd_api_methods() { pcmk__ipc_methods_t *cmds = calloc(1, sizeof(pcmk__ipc_methods_t)); if (cmds != NULL) { cmds->new_data = new_data; cmds->free_data = free_data; cmds->post_connect = post_connect; cmds->reply_expected = reply_expected; cmds->dispatch = dispatch; } return cmds; } static int do_schedulerd_api_call(pcmk_ipc_api_t *api, const char *task, xmlNode *cib, char **ref) { schedulerd_api_private_t *private; xmlNode *cmd = NULL; int rc; if (!pcmk_ipc_is_connected(api)) { return ENOTCONN; } private = api->api_data; CRM_ASSERT(private != NULL); cmd = create_request(task, cib, NULL, CRM_SYSTEM_PENGINE, crm_system_name? crm_system_name : "client", private->client_uuid); if (cmd) { rc = pcmk__send_ipc_request(api, cmd); if (rc != pcmk_rc_ok) { crm_debug("Couldn't send request to schedulerd: %s rc=%d", pcmk_rc_str(rc), rc); } *ref = strdup(crm_element_value(cmd, F_CRM_REFERENCE)); free_xml(cmd); } else { rc = ENOMSG; } return rc; } int pcmk_schedulerd_api_graph(pcmk_ipc_api_t *api, xmlNode *cib, char **ref) { return do_schedulerd_api_call(api, CRM_OP_PECALC, cib, ref); } diff --git a/lib/pacemaker/pcmk_status.c b/lib/pacemaker/pcmk_status.c index 85ccc4de32..757d8ad369 100644 --- a/lib/pacemaker/pcmk_status.c +++ b/lib/pacemaker/pcmk_status.c @@ -1,98 +1,371 @@ /* * Copyright 2004-2022 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 +#include #include +#include #include #include #include +#include #include #include #include +static int +cib_connect(pcmk__output_t *out, cib_t *cib, xmlNode **current_cib) +{ + int rc = pcmk_rc_ok; + + CRM_CHECK(cib != NULL, return EINVAL); + + if (cib->state == cib_connected_query || + cib->state == cib_connected_command) { + return rc; + } + + crm_trace("Connecting to the CIB"); + + rc = cib->cmds->signon(cib, crm_system_name, cib_query); + rc = pcmk_legacy2rc(rc); + + if (rc != pcmk_rc_ok) { + out->err(out, "Could not connect to the CIB: %s", + pcmk_rc_str(rc)); + return rc; + } + + rc = cib->cmds->query(cib, NULL, current_cib, + cib_scope_local | cib_sync_call); + rc = pcmk_legacy2rc(rc); + + return rc; +} + +static stonith_t * +fencing_connect(void) +{ + stonith_t *st = stonith_api_new(); + int rc = pcmk_rc_ok; + + if (st == NULL) { + return NULL; + } + + rc = st->cmds->connect(st, crm_system_name, NULL); + if (rc == pcmk_rc_ok) { + return st; + } else { + stonith_api_delete(st); + return NULL; + } +} + +static void +pacemakerd_event_cb(pcmk_ipc_api_t *pacemakerd_api, + enum pcmk_ipc_event event_type, crm_exit_t status, + void *event_data, void *user_data) +{ + pcmk_pacemakerd_api_reply_t *reply = event_data; + enum pcmk_pacemakerd_state *state = + (enum pcmk_pacemakerd_state *) user_data; + + /* we are just interested in the latest reply */ + *state = pcmk_pacemakerd_state_invalid; + + if (event_type != pcmk_ipc_event_reply || status != CRM_EX_OK) { + return; + } + + if (reply->reply_type == pcmk_pacemakerd_reply_ping && + reply->data.ping.last_good != (time_t) 0 && + reply->data.ping.status == pcmk_rc_ok) { + *state = reply->data.ping.state; + } +} + +static int +pacemakerd_status(pcmk__output_t *out) +{ + int rc = pcmk_rc_ok; + pcmk_ipc_api_t *pacemakerd_api = NULL; + enum pcmk_pacemakerd_state state = pcmk_pacemakerd_state_invalid; + + rc = pcmk_new_ipc_api(&pacemakerd_api, pcmk_ipc_pacemakerd); + if (pacemakerd_api == NULL) { + out->err(out, "Could not connect to pacemakerd: %s", + pcmk_rc_str(rc)); + return rc; + } + + pcmk_register_ipc_callback(pacemakerd_api, pacemakerd_event_cb, (void *) &state); + + rc = pcmk_connect_ipc(pacemakerd_api, pcmk_ipc_dispatch_sync); + if (rc == EREMOTEIO) { + return pcmk_rc_ok; + } else if (rc != pcmk_rc_ok) { + out->err(out, "Could not connect to pacemakerd: %s", + pcmk_rc_str(rc)); + pcmk_free_ipc_api(pacemakerd_api); + return rc; + } + + rc = pcmk_pacemakerd_api_ping(pacemakerd_api, crm_system_name); + + if (rc != pcmk_rc_ok) { + /* Got some error from pcmk_pacemakerd_api_ping, so return it. */ + } else if (state == pcmk_pacemakerd_state_running) { + rc = pcmk_rc_ok; + } else if (state == pcmk_pacemakerd_state_shutting_down) { + rc = ENOTCONN; + } else { + rc = EAGAIN; + } + + pcmk_free_ipc_api(pacemakerd_api); + return rc; +} + +int +pcmk__output_cluster_status(pcmk__output_t *out, stonith_t *st, cib_t *cib, + xmlNode *current_cib, enum pcmk__fence_history fence_history, + uint32_t show, uint32_t show_opts, char *only_node, + char *only_rsc, char *neg_location_prefix, bool simple_output) +{ + xmlNode *cib_copy = copy_xml(current_cib); + stonith_history_t *stonith_history = NULL; + int history_rc = 0; + pe_working_set_t *data_set = NULL; + GList *unames = NULL; + GList *resources = NULL; + + int rc = pcmk_rc_ok; + + if (cli_config_update(&cib_copy, NULL, FALSE) == FALSE) { + cib__clean_up_connection(&cib); + rc = pcmk_rc_schema_validation; + out->err(out, "Upgrade failed: %s", pcmk_rc_str(rc)); + return rc; + } + + /* get the stonith-history if there is evidence we need it */ + if (fence_history != pcmk__fence_history_none) { + history_rc = pcmk__get_fencing_history(st, &stonith_history, fence_history); + } + + data_set = pe_new_working_set(); + CRM_ASSERT(data_set != NULL); + pe__set_working_set_flags(data_set, pe_flag_no_compat); + + data_set->input = cib_copy; + data_set->priv = out; + cluster_status(data_set); + + /* Unpack constraints if any section will need them + * (tickets may be referenced in constraints but not granted yet, + * and bans need negative location constraints) */ + if (pcmk_is_set(show, pcmk_section_bans) || pcmk_is_set(show, pcmk_section_tickets)) { + pcmk__unpack_constraints(data_set); + } + + unames = pe__build_node_name_list(data_set, only_node); + resources = pe__build_rsc_list(data_set, only_rsc); + + /* Always print DC if NULL. */ + if (data_set->dc_node == NULL) { + show |= pcmk_section_dc; + } + + if (simple_output) { + rc = pcmk__output_simple_status(out, data_set); + } else { + out->message(out, "cluster-status", data_set, pcmk_rc2exitc(history_rc), + stonith_history, fence_history, show, show_opts, + neg_location_prefix, unames, resources); + } + + g_list_free_full(unames, free); + g_list_free_full(resources, free); + + stonith_history_free(stonith_history); + stonith_history = NULL; + pe_reset_working_set(data_set); + return rc; +} + +int +pcmk_status(xmlNodePtr *xml) +{ + cib_t *cib = NULL; + pcmk__output_t *out = NULL; + int rc = pcmk_rc_ok; + + uint32_t show_opts = pcmk_show_pending | pcmk_show_inactive_rscs | pcmk_show_timing; + + cib = cib_new(); + + if (cib == NULL) { + return pcmk_rc_cib_corrupt; + } + + rc = pcmk__out_prologue(&out, xml); + if (rc != pcmk_rc_ok) { + cib_delete(cib); + return rc; + } + + pcmk__register_lib_messages(out); + pe__register_messages(out); + stonith__register_messages(out); + + rc = pcmk__status(out, cib, pcmk__fence_history_full, pcmk_section_all, + show_opts, NULL, NULL, NULL, false); + pcmk__out_epilogue(out, xml, rc); + + cib_delete(cib); + return rc; +} + +int +pcmk__status(pcmk__output_t *out, cib_t *cib, enum pcmk__fence_history fence_history, + uint32_t show, uint32_t show_opts, char *only_node, char *only_rsc, + char *neg_location_prefix, bool simple_output) +{ + xmlNode *current_cib = NULL; + int rc = pcmk_rc_ok; + stonith_t *st = NULL; + + if (cib == NULL) { + return ENOTCONN; + } + + if (cib->variant == cib_native) { + if (cib->state == cib_connected_query || cib->state == cib_connected_command) { + rc = pcmk_rc_ok; + } else { + rc = pacemakerd_status(out); + } + } + + if (rc != pcmk_rc_ok) { + return rc; + } + + if (fence_history != pcmk__fence_history_none && cib->variant == cib_native) { + st = fencing_connect(); + + if (st == NULL) { + return ENOTCONN; + } + } + + rc = cib_connect(out, cib, ¤t_cib); + if (rc != pcmk_rc_ok) { + cib__clean_up_connection(&cib); + + goto done; + } + + rc = pcmk__output_cluster_status(out, st, cib, current_cib, fence_history, show, show_opts, + only_node, only_rsc, neg_location_prefix, simple_output); + +done: + if (st != NULL) { + if (st->state != stonith_disconnected) { + st->cmds->remove_notification(st, NULL); + st->cmds->disconnect(st); + } + + stonith_api_delete(st); + } + + return rc; +} + /* This is an internal-only function that is planned to be deprecated and removed. * It should only ever be called from crm_mon. */ int pcmk__output_simple_status(pcmk__output_t *out, pe_working_set_t *data_set) { int nodes_online = 0; int nodes_standby = 0; int nodes_maintenance = 0; char *offline_nodes = NULL; size_t offline_nodes_len = 0; bool no_dc = false; bool offline = false; bool has_warnings = false; if (data_set->dc_node == NULL) { has_warnings = true; no_dc = true; } for (GList *iter = data_set->nodes; iter != NULL; iter = iter->next) { pe_node_t *node = (pe_node_t *) iter->data; if (node->details->standby && node->details->online) { nodes_standby++; } else if (node->details->maintenance && node->details->online) { nodes_maintenance++; } else if (node->details->online) { nodes_online++; } else { char *s = crm_strdup_printf("offline node: %s", node->details->uname); /* coverity[leaked_storage] False positive */ pcmk__add_word(&offline_nodes, &offline_nodes_len, s); free(s); has_warnings = true; offline = true; } } if (has_warnings) { out->info(out, "CLUSTER WARN: %s%s%s", no_dc ? "No DC" : "", no_dc && offline ? ", " : "", (offline? offline_nodes : "")); free(offline_nodes); } else { char *nodes_standby_s = NULL; char *nodes_maint_s = NULL; if (nodes_standby > 0) { nodes_standby_s = crm_strdup_printf(", %d standby node%s", nodes_standby, pcmk__plural_s(nodes_standby)); } if (nodes_maintenance > 0) { nodes_maint_s = crm_strdup_printf(", %d maintenance node%s", nodes_maintenance, pcmk__plural_s(nodes_maintenance)); } out->info(out, "CLUSTER OK: %d node%s online%s%s, " "%d resource instance%s configured", nodes_online, pcmk__plural_s(nodes_online), nodes_standby_s != NULL ? nodes_standby_s : "", nodes_maint_s != NULL ? nodes_maint_s : "", data_set->ninstances, pcmk__plural_s(data_set->ninstances)); free(nodes_standby_s); free(nodes_maint_s); } if (has_warnings) { return pcmk_rc_error; } else { return pcmk_rc_ok; } /* coverity[leaked_storage] False positive */ } diff --git a/tools/crm_mon.c b/tools/crm_mon.c index 1996fa8d03..2aa20d6b9e 100644 --- a/tools/crm_mon.c +++ b/tools/crm_mon.c @@ -1,2250 +1,2200 @@ /* * Copyright 2004-2022 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // pcmk__ends_with_ext() #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include "crm_mon.h" #define SUMMARY "Provides a summary of cluster's current state.\n\n" \ "Outputs varying levels of detail in a number of different formats." /* * Definitions indicating which items to print */ static uint32_t show; static uint32_t show_opts = pcmk_show_pending; /* * Definitions indicating how to output */ static mon_output_format_t output_format = mon_output_unset; /* other globals */ static GIOChannel *io_channel = NULL; static GMainLoop *mainloop = NULL; static guint reconnect_timer = 0; static mainloop_timer_t *refresh_timer = NULL; static cib_t *cib = NULL; static stonith_t *st = NULL; static xmlNode *current_cib = NULL; static GError *error = NULL; static pcmk__common_args_t *args = NULL; static pcmk__output_t *out = NULL; static GOptionContext *context = NULL; static gchar **processed_args = NULL; static time_t last_refresh = 0; volatile crm_trigger_t *refresh_trigger = NULL; static enum pcmk__fence_history fence_history = pcmk__fence_history_none; static gboolean on_remote_node = FALSE; static gboolean use_cib_native = FALSE; int interactive_fence_level = 0; static pcmk__supported_format_t formats[] = { #if CURSES_ENABLED CRM_MON_SUPPORTED_FORMAT_CURSES, #endif PCMK__SUPPORTED_FORMAT_HTML, PCMK__SUPPORTED_FORMAT_NONE, PCMK__SUPPORTED_FORMAT_TEXT, PCMK__SUPPORTED_FORMAT_XML, { NULL, NULL, NULL } }; /* Define exit codes for monitoring-compatible output * For nagios plugins, the possibilities are * OK=0, WARN=1, CRIT=2, and UNKNOWN=3 */ #define MON_STATUS_WARN CRM_EX_ERROR #define MON_STATUS_CRIT CRM_EX_INVALID_PARAM #define MON_STATUS_UNKNOWN CRM_EX_UNIMPLEMENT_FEATURE #define RECONNECT_MSECS 5000 struct { guint reconnect_ms; gboolean daemonize; gboolean fence_connect; gboolean one_shot; gboolean print_pending; gboolean show_bans; gboolean watch_fencing; char *pid_file; char *external_agent; char *external_recipient; char *neg_location_prefix; char *only_node; char *only_rsc; GSList *user_includes_excludes; GSList *includes_excludes; } options = { .fence_connect = TRUE, .reconnect_ms = RECONNECT_MSECS }; static void clean_up_fencing_connection(void); static crm_exit_t clean_up(crm_exit_t exit_code); static void crm_diff_update(const char *event, xmlNode * msg); -static void handle_connection_failures(int rc); +static void clean_up_if_connection_failure(int rc); static int mon_refresh_display(gpointer user_data); -static int cib_connect(gboolean full); +static int cib_connect(void); static int fencing_connect(void); static int pacemakerd_status(void); static void mon_st_callback_event(stonith_t * st, stonith_event_t * e); static void mon_st_callback_display(stonith_t * st, stonith_event_t * e); static void refresh_after_event(gboolean data_updated, gboolean enforce); static uint32_t all_includes(mon_output_format_t fmt) { if (fmt == mon_output_monitor || fmt == mon_output_plain || fmt == mon_output_console) { return ~pcmk_section_options; } else { return pcmk_section_all; } } static uint32_t default_includes(mon_output_format_t fmt) { switch (fmt) { case mon_output_monitor: case mon_output_plain: case mon_output_console: return pcmk_section_summary | pcmk_section_nodes | pcmk_section_resources | pcmk_section_failures; case mon_output_xml: case mon_output_legacy_xml: return all_includes(fmt); case mon_output_html: case mon_output_cgi: return pcmk_section_summary | pcmk_section_nodes | pcmk_section_resources | pcmk_section_failures; default: return 0; } } struct { const char *name; uint32_t bit; } sections[] = { { "attributes", pcmk_section_attributes }, { "bans", pcmk_section_bans }, { "counts", pcmk_section_counts }, { "dc", pcmk_section_dc }, { "failcounts", pcmk_section_failcounts }, { "failures", pcmk_section_failures }, { "fencing", pcmk_section_fencing_all }, { "fencing-failed", pcmk_section_fence_failed }, { "fencing-pending", pcmk_section_fence_pending }, { "fencing-succeeded", pcmk_section_fence_worked }, { "maint-mode", pcmk_section_maint_mode }, { "nodes", pcmk_section_nodes }, { "operations", pcmk_section_operations }, { "options", pcmk_section_options }, { "resources", pcmk_section_resources }, { "stack", pcmk_section_stack }, { "summary", pcmk_section_summary }, { "tickets", pcmk_section_tickets }, { "times", pcmk_section_times }, { NULL } }; static uint32_t find_section_bit(const char *name) { for (int i = 0; sections[i].name != NULL; i++) { if (pcmk__str_eq(sections[i].name, name, pcmk__str_casei)) { return sections[i].bit; } } return 0; } static gboolean apply_exclude(const gchar *excludes, GError **error) { char **parts = NULL; gboolean result = TRUE; parts = g_strsplit(excludes, ",", 0); for (char **s = parts; *s != NULL; s++) { uint32_t bit = find_section_bit(*s); if (pcmk__str_eq(*s, "all", pcmk__str_none)) { show = 0; } else if (pcmk__str_eq(*s, "none", pcmk__str_none)) { show = all_includes(output_format); } else if (bit != 0) { show &= ~bit; } else { g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "--exclude options: all, attributes, bans, counts, dc, " "failcounts, failures, fencing, fencing-failed, " "fencing-pending, fencing-succeeded, maint-mode, nodes, " "none, operations, options, resources, stack, summary, " "tickets, times"); result = FALSE; break; } } g_strfreev(parts); return result; } static gboolean apply_include(const gchar *includes, GError **error) { char **parts = NULL; gboolean result = TRUE; parts = g_strsplit(includes, ",", 0); for (char **s = parts; *s != NULL; s++) { uint32_t bit = find_section_bit(*s); if (pcmk__str_eq(*s, "all", pcmk__str_none)) { show = all_includes(output_format); } else if (pcmk__starts_with(*s, "bans")) { show |= pcmk_section_bans; if (options.neg_location_prefix != NULL) { free(options.neg_location_prefix); options.neg_location_prefix = NULL; } if (strlen(*s) > 4 && (*s)[4] == ':') { options.neg_location_prefix = strdup(*s+5); } } else if (pcmk__str_any_of(*s, "default", "defaults", NULL)) { show |= default_includes(output_format); } else if (pcmk__str_eq(*s, "none", pcmk__str_none)) { show = 0; } else if (bit != 0) { show |= bit; } else { g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "--include options: all, attributes, bans[:PREFIX], counts, dc, " "default, failcounts, failures, fencing, fencing-failed, " "fencing-pending, fencing-succeeded, maint-mode, nodes, none, " "operations, options, resources, stack, summary, tickets, times"); result = FALSE; break; } } g_strfreev(parts); return result; } static gboolean apply_include_exclude(GSList *lst, GError **error) { gboolean rc = TRUE; GSList *node = lst; while (node != NULL) { char *s = node->data; if (pcmk__starts_with(s, "--include=")) { rc = apply_include(s+10, error); } else if (pcmk__starts_with(s, "-I=")) { rc = apply_include(s+3, error); } else if (pcmk__starts_with(s, "--exclude=")) { rc = apply_exclude(s+10, error); } else if (pcmk__starts_with(s, "-U=")) { rc = apply_exclude(s+3, error); } if (rc != TRUE) { break; } node = node->next; } return rc; } static gboolean user_include_exclude_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { char *s = crm_strdup_printf("%s=%s", option_name, optarg); options.user_includes_excludes = g_slist_append(options.user_includes_excludes, s); return TRUE; } static gboolean include_exclude_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { char *s = crm_strdup_printf("%s=%s", option_name, optarg); options.includes_excludes = g_slist_append(options.includes_excludes, s); return TRUE; } static gboolean as_cgi_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { pcmk__str_update(&args->output_ty, "html"); output_format = mon_output_cgi; options.one_shot = TRUE; return TRUE; } static gboolean as_html_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { pcmk__str_update(&args->output_dest, optarg); pcmk__str_update(&args->output_ty, "html"); output_format = mon_output_html; umask(S_IWGRP | S_IWOTH); return TRUE; } static gboolean as_simple_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { pcmk__str_update(&args->output_ty, "text"); output_format = mon_output_monitor; options.one_shot = TRUE; return TRUE; } static gboolean as_xml_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { pcmk__str_update(&args->output_ty, "xml"); output_format = mon_output_legacy_xml; return TRUE; } static gboolean fence_history_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { if (optarg == NULL) { interactive_fence_level = 2; } else { pcmk__scan_min_int(optarg, &interactive_fence_level, 0); } switch (interactive_fence_level) { case 3: options.fence_connect = TRUE; fence_history = pcmk__fence_history_full; return include_exclude_cb("--include", "fencing", data, err); case 2: options.fence_connect = TRUE; fence_history = pcmk__fence_history_full; return include_exclude_cb("--include", "fencing", data, err); case 1: options.fence_connect = TRUE; fence_history = pcmk__fence_history_full; return include_exclude_cb("--include", "fencing-failed,fencing-pending", data, err); case 0: options.fence_connect = FALSE; fence_history = pcmk__fence_history_none; return include_exclude_cb("--exclude", "fencing", data, err); default: g_set_error(err, PCMK__EXITC_ERROR, CRM_EX_INVALID_PARAM, "Fence history must be 0-3"); return FALSE; } } static gboolean group_by_node_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { show_opts |= pcmk_show_rscs_by_node; return TRUE; } static gboolean hide_headers_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { return user_include_exclude_cb("--exclude", "summary", data, err); } static gboolean inactive_resources_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { show_opts |= pcmk_show_inactive_rscs; return TRUE; } static gboolean no_curses_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { output_format = mon_output_plain; return TRUE; } static gboolean print_brief_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { show_opts |= pcmk_show_brief; return TRUE; } static gboolean print_detail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { show_opts |= pcmk_show_details; return TRUE; } static gboolean print_timing_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { show_opts |= pcmk_show_timing; return user_include_exclude_cb("--include", "operations", data, err); } static gboolean reconnect_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { int rc = crm_get_msec(optarg); if (rc == -1) { g_set_error(err, PCMK__EXITC_ERROR, CRM_EX_INVALID_PARAM, "Invalid value for -i: %s", optarg); return FALSE; } else { options.reconnect_ms = crm_parse_interval_spec(optarg); } return TRUE; } static gboolean show_attributes_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { return user_include_exclude_cb("--include", "attributes", data, err); } static gboolean show_bans_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { if (optarg != NULL) { char *s = crm_strdup_printf("bans:%s", optarg); gboolean rc = user_include_exclude_cb("--include", s, data, err); free(s); return rc; } else { return user_include_exclude_cb("--include", "bans", data, err); } } static gboolean show_failcounts_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { return user_include_exclude_cb("--include", "failcounts", data, err); } static gboolean show_operations_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { return user_include_exclude_cb("--include", "failcounts,operations", data, err); } static gboolean show_tickets_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { return user_include_exclude_cb("--include", "tickets", data, err); } static gboolean use_cib_file_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { setenv("CIB_file", optarg, 1); options.one_shot = TRUE; return TRUE; } #define INDENT " " /* *INDENT-OFF* */ static GOptionEntry addl_entries[] = { { "interval", 'i', 0, G_OPTION_ARG_CALLBACK, reconnect_cb, "Update frequency (default is 5 seconds)", "TIMESPEC" }, { "one-shot", '1', 0, G_OPTION_ARG_NONE, &options.one_shot, "Display the cluster status once on the console and exit", NULL }, { "daemonize", 'd', 0, G_OPTION_ARG_NONE, &options.daemonize, "Run in the background as a daemon.\n" INDENT "Requires at least one of --output-to and --external-agent.", NULL }, { "pid-file", 'p', 0, G_OPTION_ARG_FILENAME, &options.pid_file, "(Advanced) Daemon pid file location", "FILE" }, { "external-agent", 'E', 0, G_OPTION_ARG_FILENAME, &options.external_agent, "A program to run when resource operations take place", "FILE" }, { "external-recipient", 'e', 0, G_OPTION_ARG_STRING, &options.external_recipient, "A recipient for your program (assuming you want the program to send something to someone).", "RCPT" }, { "watch-fencing", 'W', 0, G_OPTION_ARG_NONE, &options.watch_fencing, "Listen for fencing events. For use with --external-agent.", NULL }, { "xml-file", 'x', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, use_cib_file_cb, NULL, NULL }, { NULL } }; static GOptionEntry display_entries[] = { { "include", 'I', 0, G_OPTION_ARG_CALLBACK, user_include_exclude_cb, "A list of sections to include in the output.\n" INDENT "See `Output Control` help for more information.", "SECTION(s)" }, { "exclude", 'U', 0, G_OPTION_ARG_CALLBACK, user_include_exclude_cb, "A list of sections to exclude from the output.\n" INDENT "See `Output Control` help for more information.", "SECTION(s)" }, { "node", 0, 0, G_OPTION_ARG_STRING, &options.only_node, "When displaying information about nodes, show only what's related to the given\n" INDENT "node, or to all nodes tagged with the given tag", "NODE" }, { "resource", 0, 0, G_OPTION_ARG_STRING, &options.only_rsc, "When displaying information about resources, show only what's related to the given\n" INDENT "resource, or to all resources tagged with the given tag", "RSC" }, { "group-by-node", 'n', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, group_by_node_cb, "Group resources by node", NULL }, { "inactive", 'r', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, inactive_resources_cb, "Display inactive resources", NULL }, { "failcounts", 'f', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, show_failcounts_cb, "Display resource fail counts", NULL }, { "operations", 'o', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, show_operations_cb, "Display resource operation history", NULL }, { "timing-details", 't', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, print_timing_cb, "Display resource operation history with timing details", NULL }, { "tickets", 'c', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, show_tickets_cb, "Display cluster tickets", NULL }, { "fence-history", 'm', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, fence_history_cb, "Show fence history:\n" INDENT "0=off, 1=failures and pending (default without option),\n" INDENT "2=add successes (default without value for option),\n" INDENT "3=show full history without reduction to most recent of each flavor", "LEVEL" }, { "neg-locations", 'L', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, show_bans_cb, "Display negative location constraints [optionally filtered by id prefix]", NULL }, { "show-node-attributes", 'A', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, show_attributes_cb, "Display node attributes", NULL }, { "hide-headers", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, hide_headers_cb, "Hide all headers", NULL }, { "show-detail", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, print_detail_cb, "Show more details (node IDs, individual clone instances)", NULL }, { "brief", 'b', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, print_brief_cb, "Brief output", NULL }, { "pending", 'j', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.print_pending, "Display pending state if 'record-pending' is enabled", NULL }, { "simple-status", 's', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, as_simple_cb, "Display the cluster status once as a simple one line output (suitable for nagios)", NULL }, { NULL } }; static GOptionEntry deprecated_entries[] = { { "as-html", 'h', G_OPTION_FLAG_FILENAME, G_OPTION_ARG_CALLBACK, as_html_cb, "Write cluster status to the named HTML file.\n" INDENT "Use --output-as=html --output-to=FILE instead.", "FILE" }, { "as-xml", 'X', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, as_xml_cb, "Write cluster status as XML to stdout. This will enable one-shot mode.\n" INDENT "Use --output-as=xml instead.", NULL }, { "disable-ncurses", 'N', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, no_curses_cb, "Disable the use of ncurses.\n" INDENT "Use --output-as=text instead.", NULL }, { "web-cgi", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, as_cgi_cb, "Web mode with output suitable for CGI (preselected when run as *.cgi).\n" INDENT "Use --output-as=html --html-cgi instead.", NULL }, { NULL } }; /* *INDENT-ON* */ /* Reconnect to the CIB and fencing agent after reconnect_ms has passed. This sounds * like it would be more broadly useful, but only ever happens after a disconnect via * mon_cib_connection_destroy. */ static gboolean reconnect_after_timeout(gpointer data) { #if CURSES_ENABLED if (output_format == mon_output_console) { clear(); refresh(); } #endif out->info(out, "Reconnecting..."); if (pacemakerd_status() == pcmk_rc_ok) { fencing_connect(); - if (cib_connect(TRUE) == pcmk_rc_ok) { + if (cib_connect() == pcmk_rc_ok) { /* trigger redrawing the screen (needs reconnect_timer == 0) */ reconnect_timer = 0; refresh_after_event(FALSE, TRUE); return G_SOURCE_REMOVE; } } reconnect_timer = g_timeout_add(options.reconnect_ms, reconnect_after_timeout, NULL); return G_SOURCE_REMOVE; } /* Called from various places when we are disconnected from the CIB or from the * fencing agent. If the CIB connection is still valid, this function will also * attempt to sign off and reconnect. */ static void mon_cib_connection_destroy(gpointer user_data) { out->info(out, "Connection to the cluster-daemons terminated"); if (refresh_timer != NULL) { /* we'll trigger a refresh after reconnect */ mainloop_timer_stop(refresh_timer); } if (reconnect_timer) { /* we'll trigger a new reconnect-timeout at the end */ g_source_remove(reconnect_timer); reconnect_timer = 0; } if (st) { /* the client API won't properly reconnect notifications * if they are still in the table - so remove them */ clean_up_fencing_connection(); } if (cib) { cib->cmds->signoff(cib); reconnect_timer = g_timeout_add(options.reconnect_ms, reconnect_after_timeout, NULL); } return; } /* Signal handler installed into the mainloop for normal program shutdown */ static void mon_shutdown(int nsig) { clean_up(CRM_EX_OK); } #if CURSES_ENABLED static volatile sighandler_t ncurses_winch_handler; /* Signal handler installed the regular way (not into the main loop) for when * the screen is resized. Commonly, this happens when running in an xterm and * the user changes its size. */ static void mon_winresize(int nsig) { static int not_done; int lines = 0, cols = 0; if (!not_done++) { if (ncurses_winch_handler) /* the original ncurses WINCH signal handler does the * magic of retrieving the new window size; * otherwise, we'd have to use ioctl or tgetent */ (*ncurses_winch_handler) (SIGWINCH); getmaxyx(stdscr, lines, cols); resizeterm(lines, cols); /* Alert the mainloop code we'd like the refresh_trigger to run next * time the mainloop gets around to checking. */ mainloop_set_trigger((crm_trigger_t *) refresh_trigger); } not_done--; } #endif static int fencing_connect(void) { int rc = pcmk_ok; if (options.fence_connect && st == NULL) { st = stonith_api_new(); } if (!options.fence_connect || st == NULL || st->state != stonith_disconnected) { return rc; } rc = st->cmds->connect(st, crm_system_name, NULL); if (rc == pcmk_ok) { crm_trace("Setting up stonith callbacks"); if (options.watch_fencing) { st->cmds->register_notification(st, T_STONITH_NOTIFY_DISCONNECT, mon_st_callback_event); st->cmds->register_notification(st, T_STONITH_NOTIFY_FENCE, mon_st_callback_event); } else { st->cmds->register_notification(st, T_STONITH_NOTIFY_DISCONNECT, mon_st_callback_display); st->cmds->register_notification(st, T_STONITH_NOTIFY_HISTORY, mon_st_callback_display); } } else { clean_up_fencing_connection(); } return rc; } static int -cib_connect(gboolean full) +cib_connect(void) { int rc = pcmk_rc_ok; CRM_CHECK(cib != NULL, return EINVAL); if (cib->state == cib_connected_query || cib->state == cib_connected_command) { return rc; } crm_trace("Connecting to the CIB"); rc = pcmk_legacy2rc(cib->cmds->signon(cib, crm_system_name, cib_query)); if (rc != pcmk_rc_ok) { out->err(out, "Could not connect to the CIB: %s", pcmk_rc_str(rc)); return rc; } /* just show this if refresh is gonna remove all traces */ if (output_format == mon_output_console) { out->info(out,"Waiting for CIB ..."); } rc = pcmk_legacy2rc(cib->cmds->query(cib, NULL, ¤t_cib, cib_scope_local | cib_sync_call)); - if (rc == pcmk_rc_ok && full) { + if (rc == pcmk_rc_ok) { rc = pcmk_legacy2rc(cib->cmds->set_connection_dnotify(cib, mon_cib_connection_destroy)); if (rc == EPROTONOSUPPORT) { out->err(out, "Notification setup not supported, won't be " "able to reconnect after failure"); if (output_format == mon_output_console) { sleep(2); } rc = pcmk_rc_ok; } if (rc == pcmk_rc_ok) { cib->cmds->del_notify_callback(cib, T_CIB_DIFF_NOTIFY, crm_diff_update); rc = pcmk_legacy2rc(cib->cmds->add_notify_callback(cib, T_CIB_DIFF_NOTIFY, crm_diff_update)); } if (rc != pcmk_rc_ok) { out->err(out, "Notification setup failed, could not monitor CIB actions"); cib__clean_up_connection(&cib); clean_up_fencing_connection(); } } return rc; } /* This is used to set up the fencing options after the interactive UI has been stared. * fence_history_cb can't be used because it builds up a list of includes/excludes that * then have to be processed with apply_include_exclude and that could affect other * things. */ static void set_fencing_options(int level) { switch (level) { case 3: options.fence_connect = TRUE; fence_history = pcmk__fence_history_full; show |= pcmk_section_fencing_all; break; case 2: options.fence_connect = TRUE; fence_history = pcmk__fence_history_full; show |= pcmk_section_fencing_all; break; case 1: options.fence_connect = TRUE; fence_history = pcmk__fence_history_full; show |= pcmk_section_fence_failed | pcmk_section_fence_pending; break; default: interactive_fence_level = 0; options.fence_connect = FALSE; fence_history = pcmk__fence_history_none; show &= ~pcmk_section_fencing_all; break; } } /* Before trying to connect to fencer or cib check for state of pacemakerd - just no sense in trying till pacemakerd has taken care of starting all the sub-processes Only noteworthy thing to show here is when pacemakerd is waiting for startup-trigger from SBD. */ static void pacemakerd_event_cb(pcmk_ipc_api_t *pacemakerd_api, enum pcmk_ipc_event event_type, crm_exit_t status, void *event_data, void *user_data) { pcmk_pacemakerd_api_reply_t *reply = event_data; enum pcmk_pacemakerd_state *state = (enum pcmk_pacemakerd_state *) user_data; /* we are just interested in the latest reply */ *state = pcmk_pacemakerd_state_invalid; switch (event_type) { case pcmk_ipc_event_reply: break; default: return; } if (status != CRM_EX_OK) { out->err(out, "Bad reply from pacemakerd: %s", crm_exit_str(status)); return; } if (reply->reply_type != pcmk_pacemakerd_reply_ping) { out->err(out, "Unknown reply type %d from pacemakerd", reply->reply_type); } else { if ((reply->data.ping.last_good != (time_t) 0) && (reply->data.ping.status == pcmk_rc_ok)) { *state = reply->data.ping.state; } } } static int pacemakerd_status(void) { int rc = pcmk_rc_ok; pcmk_ipc_api_t *pacemakerd_api = NULL; enum pcmk_pacemakerd_state state = pcmk_pacemakerd_state_invalid; if (!use_cib_native) { /* we don't need fully functional pacemakerd otherwise */ return rc; } if (cib != NULL && (cib->state == cib_connected_query || cib->state == cib_connected_command)) { /* As long as we have a cib-connection let's go with * that to fetch further cluster-status and avoid * unnecessary pings to pacemakerd. * If cluster is going down and fencer is down already * this will lead to a silently failing fencer reconnect. * On cluster startup we shouldn't see this situation * as first we do is wait for pacemakerd to report all * daemons running. */ return rc; } rc = pcmk_new_ipc_api(&pacemakerd_api, pcmk_ipc_pacemakerd); if (pacemakerd_api == NULL) { out->err(out, "Could not connect to pacemakerd: %s", pcmk_rc_str(rc)); /* this is unrecoverable so return with rc we have */ return rc; } pcmk_register_ipc_callback(pacemakerd_api, pacemakerd_event_cb, (void *) &state); rc = pcmk_connect_ipc(pacemakerd_api, pcmk_ipc_dispatch_poll); switch (rc) { case pcmk_rc_ok: rc = pcmk_pacemakerd_api_ping(pacemakerd_api, crm_system_name); if (rc == pcmk_rc_ok) { rc = pcmk_poll_ipc(pacemakerd_api, options.reconnect_ms/2); if (rc == pcmk_rc_ok) { pcmk_dispatch_ipc(pacemakerd_api); rc = ENOTCONN; if ((output_format == mon_output_console) || (output_format == mon_output_plain)) { switch (state) { case pcmk_pacemakerd_state_running: rc = pcmk_rc_ok; break; case pcmk_pacemakerd_state_starting_daemons: out->info(out,"Pacemaker daemons starting ..."); break; case pcmk_pacemakerd_state_wait_for_ping: out->info(out,"Waiting for startup-trigger from SBD ..."); break; case pcmk_pacemakerd_state_shutting_down: out->info(out,"Pacemaker daemons shutting down ..."); /* try our luck maybe CIB is still accessible */ rc = pcmk_rc_ok; break; case pcmk_pacemakerd_state_shutdown_complete: /* assuming pacemakerd doesn't dispatch any pings after entering * that state unless it is waiting for SBD */ out->info(out,"Pacemaker daemons shut down - reporting to SBD ..."); break; default: break; } } else { switch (state) { case pcmk_pacemakerd_state_running: rc = pcmk_rc_ok; break; case pcmk_pacemakerd_state_shutting_down: /* try our luck maybe CIB is still accessible */ rc = pcmk_rc_ok; break; default: break; } } } } break; case EREMOTEIO: rc = pcmk_rc_ok; on_remote_node = TRUE; /* just show this if refresh is gonna remove all traces */ if (output_format == mon_output_console) { out->info(out, "Running on remote-node waiting to be connected by cluster ..."); } break; default: break; } pcmk_free_ipc_api(pacemakerd_api); /* returning with ENOTCONN triggers a retry */ return (rc == pcmk_rc_ok)?rc:ENOTCONN; } #if CURSES_ENABLED static const char * get_option_desc(char c) { const char *desc = "No help available"; for (GOptionEntry *entry = display_entries; entry != NULL; entry++) { if (entry->short_name == c) { desc = entry->description; break; } } return desc; } #define print_option_help(out, option, condition) \ curses_formatted_printf(out, "%c %c: \t%s\n", ((condition)? '*': ' '), option, get_option_desc(option)); /* This function is called from the main loop when there is something to be read * on stdin, like an interactive user's keystroke. All it does is read the keystroke, * set flags (or show the page showing which keystrokes are valid), and redraw the * screen. It does not do anything with connections to the CIB or fencing agent * agent what would happen in mon_refresh_display. */ static gboolean detect_user_input(GIOChannel *channel, GIOCondition condition, gpointer user_data) { int c; gboolean config_mode = FALSE; while (1) { /* Get user input */ c = getchar(); switch (c) { case 'm': interactive_fence_level++; if (interactive_fence_level > 3) { interactive_fence_level = 0; } set_fencing_options(interactive_fence_level); break; case 'c': show ^= pcmk_section_tickets; break; case 'f': show ^= pcmk_section_failcounts; break; case 'n': show_opts ^= pcmk_show_rscs_by_node; break; case 'o': show ^= pcmk_section_operations; if (!pcmk_is_set(show, pcmk_section_operations)) { show_opts &= ~pcmk_show_timing; } break; case 'r': show_opts ^= pcmk_show_inactive_rscs; break; case 'R': show_opts ^= pcmk_show_details; #ifdef PCMK__COMPAT_2_0 // Keep failed action output the same as 2.0.x show_opts |= pcmk_show_failed_detail; #endif break; case 't': show_opts ^= pcmk_show_timing; if (pcmk_is_set(show_opts, pcmk_show_timing)) { show |= pcmk_section_operations; } break; case 'A': show ^= pcmk_section_attributes; break; case 'L': show ^= pcmk_section_bans; break; case 'D': /* If any header is shown, clear them all, otherwise set them all */ if (pcmk_any_flags_set(show, pcmk_section_summary)) { show &= ~pcmk_section_summary; } else { show |= pcmk_section_summary; } /* Regardless, we don't show options in console mode. */ show &= ~pcmk_section_options; break; case 'b': show_opts ^= pcmk_show_brief; break; case 'j': show_opts ^= pcmk_show_pending; break; case '?': config_mode = TRUE; break; default: /* All other keys just redraw the screen. */ goto refresh; } if (!config_mode) goto refresh; blank_screen(); curses_formatted_printf(out, "%s", "Display option change mode\n"); print_option_help(out, 'c', pcmk_is_set(show, pcmk_section_tickets)); print_option_help(out, 'f', pcmk_is_set(show, pcmk_section_failcounts)); print_option_help(out, 'n', pcmk_is_set(show_opts, pcmk_show_rscs_by_node)); print_option_help(out, 'o', pcmk_is_set(show, pcmk_section_operations)); print_option_help(out, 'r', pcmk_is_set(show_opts, pcmk_show_inactive_rscs)); print_option_help(out, 't', pcmk_is_set(show_opts, pcmk_show_timing)); print_option_help(out, 'A', pcmk_is_set(show, pcmk_section_attributes)); print_option_help(out, 'L', pcmk_is_set(show, pcmk_section_bans)); print_option_help(out, 'D', !pcmk_is_set(show, pcmk_section_summary)); #ifdef PCMK__COMPAT_2_0 print_option_help(out, 'R', pcmk_any_flags_set(show_opts, pcmk_show_details & ~pcmk_show_failed_detail)); #else print_option_help(out, 'R', pcmk_any_flags_set(show_opts, pcmk_show_details)); #endif print_option_help(out, 'b', pcmk_is_set(show_opts, pcmk_show_brief)); print_option_help(out, 'j', pcmk_is_set(show_opts, pcmk_show_pending)); curses_formatted_printf(out, "%d m: \t%s\n", interactive_fence_level, get_option_desc('m')); curses_formatted_printf(out, "%s", "\nToggle fields via field letter, type any other key to return\n"); } refresh: refresh_after_event(FALSE, TRUE); return TRUE; } #endif // Basically crm_signal_handler(SIGCHLD, SIG_IGN) plus the SA_NOCLDWAIT flag static void avoid_zombies(void) { struct sigaction sa; memset(&sa, 0, sizeof(struct sigaction)); if (sigemptyset(&sa.sa_mask) < 0) { crm_warn("Cannot avoid zombies: %s", pcmk_rc_str(errno)); return; } sa.sa_handler = SIG_IGN; sa.sa_flags = SA_RESTART|SA_NOCLDWAIT; if (sigaction(SIGCHLD, &sa, NULL) < 0) { crm_warn("Cannot avoid zombies: %s", pcmk_rc_str(errno)); } } 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 } }; const char *description = "Notes:\n\n" "If this program is called as crm_mon.cgi, --output-as=html --html-cgi will\n" "automatically be added to the command line arguments.\n\n" "Time Specification:\n\n" "The TIMESPEC in any command line option can be specified in many different\n" "formats. It can be just an integer number of seconds, a number plus units\n" "(ms/msec/us/usec/s/sec/m/min/h/hr), or an ISO 8601 period specification.\n\n" "Output Control:\n\n" "By default, a certain list of sections are written to the output destination.\n" "The default varies based on the output format - XML includes everything, while\n" "other output formats will display less. This list can be modified with the\n" "--include and --exclude command line options. Each option may be given multiple\n" "times on the command line, and each can give a comma-separated list of sections.\n" "The options are applied to the default set, from left to right as seen on the\n" "command line. For a list of valid sections, pass --include=list or --exclude=list.\n\n" "Interactive Use:\n\n" "When run interactively, crm_mon can be told to hide and display various sections\n" "of output. To see a help screen explaining the options, hit '?'. Any key stroke\n" "aside from those listed will cause the screen to refresh.\n\n" "Examples:\n\n" "Display the cluster status on the console with updates as they occur:\n\n" "\tcrm_mon\n\n" "Display the cluster status on the console just once then exit:\n\n" "\tcrm_mon -1\n\n" "Display your cluster status, group resources by node, and include inactive resources in the list:\n\n" "\tcrm_mon --group-by-node --inactive\n\n" "Start crm_mon as a background daemon and have it write the cluster status to an HTML file:\n\n" "\tcrm_mon --daemonize --output-as html --output-to /path/to/docroot/filename.html\n\n" "Start crm_mon and export the current cluster status as XML to stdout, then exit:\n\n" "\tcrm_mon --output-as xml\n\n"; #if CURSES_ENABLED context = pcmk__build_arg_context(args, "console (default), html, text, xml", group, NULL); #else context = pcmk__build_arg_context(args, "text (default), html, xml", group, NULL); #endif pcmk__add_main_args(context, extra_prog_entries); g_option_context_set_description(context, description); pcmk__add_arg_group(context, "display", "Display Options:", "Show display options", display_entries); pcmk__add_arg_group(context, "additional", "Additional Options:", "Show additional options", addl_entries); pcmk__add_arg_group(context, "deprecated", "Deprecated Options:", "Show deprecated options", deprecated_entries); return context; } /* If certain format options were specified, we want to set some extra * options. We can just process these like they were given on the * command line. */ static void add_output_args(void) { GError *err = NULL; if (output_format == mon_output_plain) { if (!pcmk__force_args(context, &err, "%s --text-fancy", g_get_prgname())) { g_propagate_error(&error, err); clean_up(CRM_EX_USAGE); } } else if (output_format == mon_output_cgi) { if (!pcmk__force_args(context, &err, "%s --html-cgi", g_get_prgname())) { g_propagate_error(&error, err); clean_up(CRM_EX_USAGE); } } else if (output_format == mon_output_xml) { if (!pcmk__force_args(context, &err, "%s --xml-simple-list --xml-substitute", g_get_prgname())) { g_propagate_error(&error, err); clean_up(CRM_EX_USAGE); } } else if (output_format == mon_output_legacy_xml) { output_format = mon_output_xml; if (!pcmk__force_args(context, &err, "%s --xml-legacy --xml-substitute", g_get_prgname())) { g_propagate_error(&error, err); clean_up(CRM_EX_USAGE); } } } /* Which output format to use could come from two places: The --as-xml * style arguments we gave in deprecated_entries above, or the formatted output * arguments added by pcmk__register_formats. If the latter were used, * output_format will be mon_output_unset. * * Call the callbacks as if those older style arguments were provided so * the various things they do get done. */ static void reconcile_output_format(pcmk__common_args_t *args) { gboolean retval = TRUE; GError *err = NULL; if (output_format != mon_output_unset) { return; } if (pcmk__str_eq(args->output_ty, "html", pcmk__str_casei)) { char *dest = NULL; pcmk__str_update(&dest, args->output_dest); retval = as_html_cb("h", dest, NULL, &err); free(dest); } else if (pcmk__str_eq(args->output_ty, "text", pcmk__str_casei)) { retval = no_curses_cb("N", NULL, NULL, &err); } else if (pcmk__str_eq(args->output_ty, "xml", pcmk__str_casei)) { pcmk__str_update(&args->output_ty, "xml"); output_format = mon_output_xml; } else if (options.one_shot) { pcmk__str_update(&args->output_ty, "text"); output_format = mon_output_plain; } else { /* Neither old nor new arguments were given, so set the default. */ pcmk__str_update(&args->output_ty, "console"); output_format = mon_output_console; } if (!retval) { g_propagate_error(&error, err); clean_up(CRM_EX_USAGE); } } static void -handle_connection_failures(int rc) +clean_up_if_connection_failure(int rc) { if (rc == pcmk_rc_ok) { return; } if (output_format == mon_output_monitor) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_ERROR, "CLUSTER CRIT: Connection to cluster failed: %s", pcmk_rc_str(rc)); - rc = MON_STATUS_CRIT; + clean_up(MON_STATUS_CRIT); } else if (rc == ENOTCONN) { if (on_remote_node) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_ERROR, "Error: remote-node not connected to cluster"); } else { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_ERROR, "Error: cluster is not available on this node"); } - rc = pcmk_rc2exitc(rc); } else { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_ERROR, "Connection to cluster failed: %s", pcmk_rc_str(rc)); - rc = pcmk_rc2exitc(rc); } - clean_up(rc); + clean_up(pcmk_rc2exitc(rc)); } static void one_shot(void) { - int rc; - - rc = pacemakerd_status(); + int rc = pcmk__status(out, cib, fence_history, show, show_opts, + options.only_node, options.only_rsc, + options.neg_location_prefix, + output_format == mon_output_monitor); - if (rc == pcmk_rc_ok) { - fencing_connect(); - rc = cib_connect(FALSE); - } - - if (rc == pcmk_rc_ok) { - mon_refresh_display(NULL); - } else { - handle_connection_failures(rc); - } + clean_up_if_connection_failure(rc); - clean_up(CRM_EX_OK); + /* If clean_up_if_connection_failure didn't call clean_up (because there wasn't + * a connection failure), we still need to call clean_up here because one_shot + * mode is done. + */ + clean_up(pcmk_rc2exitc(rc)); } static void exit_on_invalid_cib(void) { if (cib != NULL) { return; } // Shouldn't really be possible g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_ERROR, "Invalid CIB source"); clean_up(CRM_EX_ERROR); } int main(int argc, char **argv) { int rc = pcmk_rc_ok; GOptionGroup *output_group = NULL; args = pcmk__new_common_args(SUMMARY); context = build_arg_context(args, &output_group); pcmk__register_formats(output_group, formats); options.pid_file = strdup("/tmp/ClusterMon.pid"); pcmk__cli_init_logging("crm_mon", 0); // Avoid needing to wait for subprocesses forked for -E/--external-agent avoid_zombies(); if (pcmk__ends_with_ext(argv[0], ".cgi")) { output_format = mon_output_cgi; options.one_shot = TRUE; } processed_args = pcmk__cmdline_preproc(argv, "ehimpxEILU"); fence_history_cb("--fence-history", "1", NULL, NULL); /* Set an HTML title regardless of what format we will eventually use. This can't * be done in add_output_args. That function is called after command line * arguments are processed in the next block, which means it'll override whatever * title the user provides. Doing this here means the user can give their own * title on the command line. */ if (!pcmk__force_args(context, &error, "%s --html-title \"Cluster Status\"", g_get_prgname())) { return clean_up(CRM_EX_USAGE); } if (!g_option_context_parse_strv(context, &processed_args, &error)) { return clean_up(CRM_EX_USAGE); } for (int i = 0; i < args->verbosity; i++) { crm_bump_log_level(argc, argv); } if (!args->version) { if (args->quiet) { include_exclude_cb("--exclude", "times", NULL, NULL); } if (options.watch_fencing) { fence_history_cb("--fence-history", "0", NULL, NULL); options.fence_connect = TRUE; } /* create the cib-object early to be able to do further * decisions based on the cib-source */ cib = cib_new(); exit_on_invalid_cib(); switch (cib->variant) { case cib_native: /* cib & fencing - everything available */ use_cib_native = TRUE; break; case cib_file: /* Don't try to connect to fencing as we * either don't have a running cluster or * the fencing-information would possibly * not match the cib data from a file. * As we don't expect cib-updates coming * in enforce one-shot. */ fence_history_cb("--fence-history", "0", NULL, NULL); options.one_shot = TRUE; break; case cib_remote: /* updates coming in but no fencing */ fence_history_cb("--fence-history", "0", NULL, NULL); break; case cib_undefined: case cib_database: default: /* something is odd */ exit_on_invalid_cib(); break; } if (options.one_shot) { if (output_format == mon_output_console) { output_format = mon_output_plain; } } else if (options.daemonize) { if (pcmk__str_eq(args->output_dest, "-", pcmk__str_null_matches|pcmk__str_casei) && !options.external_agent) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "--daemonize requires at least one of --output-to and --external-agent"); return clean_up(CRM_EX_USAGE); } } else if (output_format == mon_output_console) { #if CURSES_ENABLED crm_enable_stderr(FALSE); #else options.one_shot = TRUE; output_format = mon_output_plain; printf("Defaulting to one-shot mode\n"); printf("You need to have curses available at compile time to enable console mode\n"); #endif } } reconcile_output_format(args); add_output_args(); /* output_format MUST NOT BE CHANGED AFTER THIS POINT. */ if (args->version && output_format == mon_output_console) { /* Use the text output format here if we are in curses mode but were given * --version. Displaying version information uses printf, and then we * immediately exit. We don't want to initialize curses for that. */ rc = pcmk__output_new(&out, "text", args->output_dest, argv); } else { rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv); } if (rc != pcmk_rc_ok) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_ERROR, "Error creating output format %s: %s", args->output_ty, pcmk_rc_str(rc)); return clean_up(CRM_EX_ERROR); } if (options.daemonize) { if (!options.external_agent && (output_format == mon_output_console || output_format == mon_output_unset || output_format == mon_output_none)) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "--daemonize requires --output-as=[html|text|xml]"); return clean_up(CRM_EX_USAGE); } crm_enable_stderr(FALSE); cib_delete(cib); cib = NULL; pcmk__daemonize(crm_system_name, options.pid_file); cib = cib_new(); exit_on_invalid_cib(); } show = default_includes(output_format); /* Apply --include/--exclude flags we used internally. There's no error reporting * here because this would be a programming error. */ apply_include_exclude(options.includes_excludes, &error); /* And now apply any --include/--exclude flags the user gave on the command line. * These are done in a separate pass from the internal ones because we want to * make sure whatever the user specifies overrides whatever we do. */ if (!apply_include_exclude(options.user_includes_excludes, &error)) { return clean_up(CRM_EX_USAGE); } /* Sync up the initial value of interactive_fence_level with whatever was set with * --include/--exclude= options. */ if (pcmk_all_flags_set(show, pcmk_section_fencing_all)) { interactive_fence_level = 3; } else if (pcmk_is_set(show, pcmk_section_fence_worked)) { interactive_fence_level = 2; } else if (pcmk_any_flags_set(show, pcmk_section_fence_failed | pcmk_section_fence_pending)) { interactive_fence_level = 1; } else { interactive_fence_level = 0; } pcmk__register_lib_messages(out); crm_mon_register_messages(out); pe__register_messages(out); stonith__register_messages(out); if (args->version) { out->version(out, false); return clean_up(CRM_EX_OK); } /* Extra sanity checks when in CGI mode */ if (output_format == mon_output_cgi) { if (cib->variant == cib_file) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "CGI mode used with CIB file"); return clean_up(CRM_EX_USAGE); } else if (options.external_agent != NULL) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "CGI mode cannot be used with --external-agent"); return clean_up(CRM_EX_USAGE); } else if (options.daemonize == TRUE) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "CGI mode cannot be used with -d"); return clean_up(CRM_EX_USAGE); } } if (output_format == mon_output_xml || output_format == mon_output_legacy_xml) { show_opts |= pcmk_show_inactive_rscs | pcmk_show_timing; if (!options.daemonize) { options.one_shot = TRUE; } } if ((output_format == mon_output_html || output_format == mon_output_cgi) && out->dest != stdout) { pcmk__html_add_header("meta", "http-equiv", "refresh", "content", pcmk__itoa(options.reconnect_ms / 1000), NULL); } #ifdef PCMK__COMPAT_2_0 // Keep failed action output the same as 2.0.x show_opts |= pcmk_show_failed_detail; #endif crm_info("Starting %s", crm_system_name); cib__set_output(cib, out); if (options.one_shot) { one_shot(); } do { out->info(out,"Waiting until cluster is available on this node ..."); rc = pacemakerd_status(); if (rc == pcmk_rc_ok) { fencing_connect(); - rc = cib_connect(TRUE); + rc = cib_connect(); } if (rc != pcmk_rc_ok) { pcmk__sleep_ms(options.reconnect_ms); #if CURSES_ENABLED if (output_format == mon_output_console) { clear(); refresh(); } #endif } else if (output_format == mon_output_html && out->dest != stdout) { printf("Writing html to %s ...\n", args->output_dest); } } while (rc == ENOTCONN); - handle_connection_failures(rc); + clean_up_if_connection_failure(rc); set_fencing_options(interactive_fence_level); mon_refresh_display(NULL); mainloop = g_main_loop_new(NULL, FALSE); mainloop_add_signal(SIGTERM, mon_shutdown); mainloop_add_signal(SIGINT, mon_shutdown); #if CURSES_ENABLED if (output_format == mon_output_console) { ncurses_winch_handler = crm_signal_handler(SIGWINCH, mon_winresize); if (ncurses_winch_handler == SIG_DFL || ncurses_winch_handler == SIG_IGN || ncurses_winch_handler == SIG_ERR) ncurses_winch_handler = NULL; io_channel = g_io_channel_unix_new(STDIN_FILENO); g_io_add_watch(io_channel, G_IO_IN, detect_user_input, NULL); } #endif /* When refresh_trigger->trigger is set to TRUE, call mon_refresh_display. In * this file, that is anywhere mainloop_set_trigger is called. */ refresh_trigger = mainloop_add_trigger(G_PRIORITY_LOW, mon_refresh_display, NULL); g_main_loop_run(mainloop); g_main_loop_unref(mainloop); if (io_channel != NULL) { g_io_channel_shutdown(io_channel, TRUE, NULL); } crm_info("Exiting %s", crm_system_name); return clean_up(CRM_EX_OK); } static int send_custom_trap(const char *node, const char *rsc, const char *task, int target_rc, int rc, int status, const char *desc) { pid_t pid; /*setenv needs chars, these are ints */ char *rc_s = pcmk__itoa(rc); char *status_s = pcmk__itoa(status); char *target_rc_s = pcmk__itoa(target_rc); crm_debug("Sending external notification to '%s' via '%s'", options.external_recipient, options.external_agent); if(rsc) { setenv("CRM_notify_rsc", rsc, 1); } if (options.external_recipient) { setenv("CRM_notify_recipient", options.external_recipient, 1); } setenv("CRM_notify_node", node, 1); setenv("CRM_notify_task", task, 1); setenv("CRM_notify_desc", desc, 1); setenv("CRM_notify_rc", rc_s, 1); setenv("CRM_notify_target_rc", target_rc_s, 1); setenv("CRM_notify_status", status_s, 1); pid = fork(); if (pid == -1) { crm_perror(LOG_ERR, "notification fork() failed."); } if (pid == 0) { /* crm_debug("notification: I am the child. Executing the nofitication program."); */ execl(options.external_agent, options.external_agent, NULL); exit(CRM_EX_ERROR); } crm_trace("Finished running custom notification program '%s'.", options.external_agent); free(target_rc_s); free(status_s); free(rc_s); return 0; } static void handle_rsc_op(xmlNode * xml, const char *node_id) { int rc = -1; int status = -1; int target_rc = -1; gboolean notify = TRUE; char *rsc = NULL; char *task = NULL; const char *desc = NULL; const char *magic = NULL; const char *id = NULL; const char *node = NULL; xmlNode *n = xml; xmlNode * rsc_op = xml; if(strcmp((const char*)xml->name, XML_LRM_TAG_RSC_OP) != 0) { xmlNode *cIter; for(cIter = xml->children; cIter; cIter = cIter->next) { handle_rsc_op(cIter, node_id); } return; } id = crm_element_value(rsc_op, XML_LRM_ATTR_TASK_KEY); if (id == NULL) { /* Compatibility with <= 1.1.5 */ id = ID(rsc_op); } magic = crm_element_value(rsc_op, XML_ATTR_TRANSITION_MAGIC); if (magic == NULL) { /* non-change */ return; } if (!decode_transition_magic(magic, NULL, NULL, NULL, &status, &rc, &target_rc)) { crm_err("Invalid event %s detected for %s", magic, id); return; } if (parse_op_key(id, &rsc, &task, NULL) == FALSE) { crm_err("Invalid event detected for %s", id); goto bail; } node = crm_element_value(rsc_op, XML_LRM_ATTR_TARGET); while (n != NULL && !pcmk__str_eq(XML_CIB_TAG_STATE, TYPE(n), pcmk__str_casei)) { n = n->parent; } if(node == NULL && n) { node = crm_element_value(n, XML_ATTR_UNAME); } if (node == NULL && n) { node = ID(n); } if (node == NULL) { node = node_id; } if (node == NULL) { crm_err("No node detected for event %s (%s)", magic, id); goto bail; } /* look up where we expected it to be? */ desc = pcmk_rc_str(pcmk_rc_ok); if ((status == PCMK_EXEC_DONE) && (target_rc == rc)) { crm_notice("%s of %s on %s completed: %s", task, rsc, node, desc); if (rc == PCMK_OCF_NOT_RUNNING) { notify = FALSE; } } else if (status == PCMK_EXEC_DONE) { desc = services_ocf_exitcode_str(rc); crm_warn("%s of %s on %s failed: %s", task, rsc, node, desc); } else { desc = pcmk_exec_status_str(status); crm_warn("%s of %s on %s failed: %s", task, rsc, node, desc); } if (notify && options.external_agent) { send_custom_trap(node, rsc, task, target_rc, rc, status, desc); } bail: free(rsc); free(task); } /* This function is just a wrapper around mainloop_set_trigger so that it can be * called from a mainloop directly. It's simply another way of ensuring the screen * gets redrawn. */ static gboolean mon_trigger_refresh(gpointer user_data) { mainloop_set_trigger((crm_trigger_t *) refresh_trigger); return FALSE; } static void crm_diff_update_v2(const char *event, xmlNode * msg) { xmlNode *change = NULL; xmlNode *diff = get_message_xml(msg, F_CIB_UPDATE_RESULT); for (change = pcmk__xml_first_child(diff); change != NULL; change = pcmk__xml_next(change)) { const char *name = NULL; const char *op = crm_element_value(change, XML_DIFF_OP); const char *xpath = crm_element_value(change, XML_DIFF_PATH); xmlNode *match = NULL; const char *node = NULL; if(op == NULL) { continue; } else if(strcmp(op, "create") == 0) { match = change->children; } else if(strcmp(op, "move") == 0) { continue; } else if(strcmp(op, "delete") == 0) { continue; } else if(strcmp(op, "modify") == 0) { match = first_named_child(change, XML_DIFF_RESULT); if(match) { match = match->children; } } if(match) { name = (const char *)match->name; } crm_trace("Handling %s operation for %s %p, %s", op, xpath, match, name); if(xpath == NULL) { /* Version field, ignore */ } else if(name == NULL) { crm_debug("No result for %s operation to %s", op, xpath); CRM_ASSERT(strcmp(op, "delete") == 0 || strcmp(op, "move") == 0); } else if(strcmp(name, XML_TAG_CIB) == 0) { xmlNode *state = NULL; xmlNode *status = first_named_child(match, XML_CIB_TAG_STATUS); for (state = pcmk__xe_first_child(status); state != NULL; state = pcmk__xe_next(state)) { node = crm_element_value(state, XML_ATTR_UNAME); if (node == NULL) { node = ID(state); } handle_rsc_op(state, node); } } else if(strcmp(name, XML_CIB_TAG_STATUS) == 0) { xmlNode *state = NULL; for (state = pcmk__xe_first_child(match); state != NULL; state = pcmk__xe_next(state)) { node = crm_element_value(state, XML_ATTR_UNAME); if (node == NULL) { node = ID(state); } handle_rsc_op(state, node); } } else if(strcmp(name, XML_CIB_TAG_STATE) == 0) { node = crm_element_value(match, XML_ATTR_UNAME); if (node == NULL) { node = ID(match); } handle_rsc_op(match, node); } else if(strcmp(name, XML_CIB_TAG_LRM) == 0) { node = ID(match); handle_rsc_op(match, node); } else if(strcmp(name, XML_LRM_TAG_RESOURCES) == 0) { char *local_node = pcmk__xpath_node_id(xpath, "lrm"); handle_rsc_op(match, local_node); free(local_node); } else if(strcmp(name, XML_LRM_TAG_RESOURCE) == 0) { char *local_node = pcmk__xpath_node_id(xpath, "lrm"); handle_rsc_op(match, local_node); free(local_node); } else if(strcmp(name, XML_LRM_TAG_RSC_OP) == 0) { char *local_node = pcmk__xpath_node_id(xpath, "lrm"); handle_rsc_op(match, local_node); free(local_node); } else { crm_trace("Ignoring %s operation for %s %p, %s", op, xpath, match, name); } } } static void crm_diff_update_v1(const char *event, xmlNode * msg) { /* Process operation updates */ xmlXPathObject *xpathObj = xpath_search(msg, "//" F_CIB_UPDATE_RESULT "//" XML_TAG_DIFF_ADDED "//" XML_LRM_TAG_RSC_OP); int lpc = 0, max = numXpathResults(xpathObj); for (lpc = 0; lpc < max; lpc++) { xmlNode *rsc_op = getXpathResult(xpathObj, lpc); handle_rsc_op(rsc_op, NULL); } freeXpathObject(xpathObj); } static void crm_diff_update(const char *event, xmlNode * msg) { int rc = -1; static bool stale = FALSE; gboolean cib_updated = FALSE; xmlNode *diff = get_message_xml(msg, F_CIB_UPDATE_RESULT); out->progress(out, false); if (current_cib != NULL) { rc = xml_apply_patchset(current_cib, diff, TRUE); switch (rc) { case -pcmk_err_diff_resync: case -pcmk_err_diff_failed: crm_notice("[%s] Patch aborted: %s (%d)", event, pcmk_strerror(rc), rc); free_xml(current_cib); current_cib = NULL; break; case pcmk_ok: cib_updated = TRUE; break; default: crm_notice("[%s] ABORTED: %s (%d)", event, pcmk_strerror(rc), rc); free_xml(current_cib); current_cib = NULL; } } if (current_cib == NULL) { crm_trace("Re-requesting the full cib"); cib->cmds->query(cib, NULL, ¤t_cib, cib_scope_local | cib_sync_call); } if (options.external_agent) { int format = 0; crm_element_value_int(diff, "format", &format); switch(format) { case 1: crm_diff_update_v1(event, msg); break; case 2: crm_diff_update_v2(event, msg); break; default: crm_err("Unknown patch format: %d", format); } } if (current_cib == NULL) { if(!stale) { out->info(out, "--- Stale data ---"); } stale = TRUE; return; } stale = FALSE; refresh_after_event(cib_updated, FALSE); } static int mon_refresh_display(gpointer user_data) { - pe_working_set_t *mon_data_set = NULL; - xmlNode *cib_copy = copy_xml(current_cib); - stonith_history_t *stonith_history = NULL; - int history_rc = pcmk_rc_ok; - GList *unames = NULL; - GList *resources = NULL; + int rc = pcmk_rc_ok; last_refresh = time(NULL); - if (cli_config_update(&cib_copy, NULL, FALSE) == FALSE) { - cib__clean_up_connection(&cib); - out->err(out, "Upgrade failed: %s", - pcmk_rc_str(pcmk_rc_schema_validation)); - clean_up(CRM_EX_CONFIG); - return G_SOURCE_REMOVE; - } - if (output_format == mon_output_none || output_format == mon_output_unset) { return G_SOURCE_REMOVE; } - /* get the stonith-history if there is evidence we need it */ - if (fence_history == pcmk__fence_history_full) { - if (!pcmk_all_flags_set(show, pcmk_section_fencing_all) && - (output_format != mon_output_xml)) { - fence_history = pcmk__fence_history_reduced; - } - - history_rc = pcmk__get_fencing_history(st, &stonith_history, fence_history); - } - - mon_data_set = pe_new_working_set(); - CRM_ASSERT(mon_data_set != NULL); - - pe__set_working_set_flags(mon_data_set, pe_flag_no_compat); - mon_data_set->input = cib_copy; - mon_data_set->priv = out; - cluster_status(mon_data_set); - - /* Unpack constraints if any section will need them - * (tickets may be referenced in constraints but not granted yet, - * and bans need negative location constraints) */ - if (pcmk_is_set(show, pcmk_section_bans) || pcmk_is_set(show, pcmk_section_tickets)) { - pcmk__unpack_constraints(mon_data_set); - } + if (fence_history == pcmk__fence_history_full && + !pcmk_all_flags_set(show, pcmk_section_fencing_all) && + output_format != mon_output_xml) { + fence_history = pcmk__fence_history_reduced; + } if (options.daemonize) { out->reset(out); } - unames = pe__build_node_name_list(mon_data_set, options.only_node); - resources = pe__build_rsc_list(mon_data_set, options.only_rsc); - - /* Always print DC if NULL. */ - if (mon_data_set->dc_node == NULL) { - show |= pcmk_section_dc; - } + rc = pcmk__output_cluster_status(out, st, cib, current_cib, fence_history, + show, show_opts, options.only_node, + options.only_rsc, options.neg_location_prefix, + output_format == mon_output_monitor); - if (output_format == mon_output_monitor) { - if (pcmk__output_simple_status(out, mon_data_set) != pcmk_rc_ok) { - pe_free_working_set(mon_data_set); - clean_up(MON_STATUS_WARN); - return G_SOURCE_REMOVE; - } - } else { - out->message(out, "cluster-status", mon_data_set, pcmk_rc2exitc(history_rc), - stonith_history, fence_history, show, show_opts, - options.neg_location_prefix, unames, resources); + if (output_format == mon_output_monitor && rc != pcmk_rc_ok) { + clean_up(MON_STATUS_WARN); + return G_SOURCE_REMOVE; + } else if (rc == pcmk_rc_schema_validation) { + clean_up(CRM_EX_CONFIG); + return G_SOURCE_REMOVE; } if (options.daemonize) { out->finish(out, CRM_EX_OK, true, NULL); } - g_list_free_full(unames, free); - g_list_free_full(resources, free); - - stonith_history_free(stonith_history); - stonith_history = NULL; - pe_free_working_set(mon_data_set); return G_SOURCE_CONTINUE; } /* This function is called for fencing events (see fencing_connect for which ones) when * --watch-fencing is used on the command line. */ static void mon_st_callback_event(stonith_t * st, stonith_event_t * e) { if (st->state == stonith_disconnected) { /* disconnect cib as well and have everything reconnect */ mon_cib_connection_destroy(NULL); } else if (options.external_agent) { char *desc = stonith__event_description(e); send_custom_trap(e->target, NULL, e->operation, pcmk_ok, e->result, 0, desc); free(desc); } } /* Cause the screen to be redrawn (via mainloop_set_trigger) when various conditions are met: * * - If the last update occurred more than reconnect_ms ago (defaults to 5s, but * can be changed via the -i command line option), or * - After every 10 CIB updates, or * - If it's been 2s since the last update * * This function sounds like it would be more broadly useful, but it is only called when a * fencing event is received or a CIB diff occurrs. */ static void refresh_after_event(gboolean data_updated, gboolean enforce) { static int updates = 0; time_t now = time(NULL); if (data_updated) { updates++; } if(refresh_timer == NULL) { refresh_timer = mainloop_timer_add("refresh", 2000, FALSE, mon_trigger_refresh, NULL); } if (reconnect_timer > 0) { /* we will receive a refresh request after successful reconnect */ mainloop_timer_stop(refresh_timer); return; } /* as we're not handling initial failure of fencer-connection as * fatal give it a retry here * not getting here if cib-reconnection is already on the way */ fencing_connect(); if (enforce || ((now - last_refresh) > (options.reconnect_ms / 1000)) || updates >= 10) { mainloop_set_trigger((crm_trigger_t *) refresh_trigger); mainloop_timer_stop(refresh_timer); updates = 0; } else { mainloop_timer_start(refresh_timer); } } /* This function is called for fencing events (see fencing_connect for which ones) when * --watch-fencing is NOT used on the command line. */ static void mon_st_callback_display(stonith_t * st, stonith_event_t * e) { if (st->state == stonith_disconnected) { /* disconnect cib as well and have everything reconnect */ mon_cib_connection_destroy(NULL); } else { out->progress(out, false); refresh_after_event(TRUE, FALSE); } } static void clean_up_fencing_connection(void) { if (st == NULL) { return; } if (st->state != stonith_disconnected) { st->cmds->remove_notification(st, NULL); st->cmds->disconnect(st); } stonith_api_delete(st); st = NULL; } /* * De-init ncurses, disconnect from the CIB manager, disconnect fencing, * deallocate memory and show usage-message if requested. * * We don't actually return, but nominally returning crm_exit_t allows a usage * like "return clean_up(exit_code);" which helps static analysis understand the * code flow. */ static crm_exit_t clean_up(crm_exit_t exit_code) { /* Quitting crm_mon is much more complicated than it ought to be. */ /* (1) Close connections, free things, etc. */ cib__clean_up_connection(&cib); clean_up_fencing_connection(); free(options.neg_location_prefix); free(options.only_node); free(options.only_rsc); free(options.pid_file); g_slist_free_full(options.includes_excludes, free); g_strfreev(processed_args); /* (2) If this is abnormal termination and we're in curses mode, shut down * curses first. Any messages displayed to the screen before curses is shut * down will be lost because doing the shut down will also restore the * screen to whatever it looked like before crm_mon was started. */ if ((error != NULL || exit_code == CRM_EX_USAGE) && output_format == mon_output_console) { out->finish(out, exit_code, false, NULL); pcmk__output_free(out); out = NULL; } /* (3) If this is a command line usage related failure, print the usage * message. */ if (exit_code == CRM_EX_USAGE && (output_format == mon_output_console || output_format == mon_output_plain)) { char *help = g_option_context_get_help(context, TRUE, NULL); fprintf(stderr, "%s", help); g_free(help); } pcmk__free_arg_context(context); /* (4) If this is any kind of error, print the error out and exit. Make * sure to handle situations both before and after formatted output is * set up. We want errors to appear formatted if at all possible. */ if (error != NULL) { if (out != NULL) { out->err(out, "%s: %s", g_get_prgname(), error->message); out->finish(out, exit_code, true, NULL); pcmk__output_free(out); } else { fprintf(stderr, "%s: %s\n", g_get_prgname(), error->message); } g_clear_error(&error); crm_exit(exit_code); } /* (5) Print formatted output to the screen if we made it far enough in * crm_mon to be able to do so. */ if (out != NULL) { if (!options.daemonize) { out->finish(out, exit_code, true, NULL); } pcmk__output_free(out); pcmk__unregister_formats(); } crm_exit(exit_code); }