diff --git a/include/pacemaker.h b/include/pacemaker.h index dc73cc0133..0c6883cc53 100644 --- a/include/pacemaker.h +++ b/include/pacemaker.h @@ -1,708 +1,716 @@ /* * Copyright 2019-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__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 and output controller status * * \param[in,out] xml Destination for the result, as an XML tree * \param[in] node_name Name of node whose status is desired * (\p NULL for DC) * \param[in] message_timeout_ms How long to wait for a reply from the * \p pacemaker-controld API. If 0, * \p pcmk_ipc_dispatch_sync will be used. * Otherwise, \p pcmk_ipc_dispatch_poll will * be used. * * \return Standard Pacemaker return code */ int pcmk_controller_status(xmlNodePtr *xml, const char *node_name, unsigned int message_timeout_ms); /*! * \brief Get and output designated controller node name * * \param[in,out] xml Destination for the result, as an XML tree * \param[in] message_timeout_ms How long to wait for a reply from the * \p pacemaker-controld API. If 0, * \p pcmk_ipc_dispatch_sync will be used. * Otherwise, \p pcmk_ipc_dispatch_poll will * be used. * * \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 and optionally output node info corresponding to a node ID from * the controller * * \param[in,out] xml Destination for the result, as an XML tree * \param[in,out] node_id ID of node whose name to get. If \p NULL * or 0, get the local node name. If not * \p NULL, store the true node ID here on * success. * \param[out] node_name If not \p NULL, where to store the node * name * \param[out] uuid If not \p NULL, where to store the node * UUID * \param[out] state If not \p NULL, where to store the * membership state * \param[out] is_remote If not \p NULL, where to store whether the * node is a Pacemaker Remote node * \param[out] have_quorum If not \p NULL, where to store whether the * node has quorum * \param[in] show_output Whether to output the node info * \param[in] message_timeout_ms How long to wait for a reply from the * \p pacemaker-controld API. If 0, * \p pcmk_ipc_dispatch_sync will be used. * Otherwise, \p pcmk_ipc_dispatch_poll will * be used. * * \return Standard Pacemaker return code * * \note The caller is responsible for freeing \p *node_name, \p *uuid, and * \p *state using \p free(). */ int pcmk_query_node_info(xmlNodePtr *xml, uint32_t *node_id, char **node_name, char **uuid, char **state, bool *have_quorum, bool *is_remote, bool show_output, unsigned int message_timeout_ms); /*! * \brief Get the node name corresponding to a node ID from the controller * * \param[in,out] xml Destination for the result, as an XML tree * \param[in,out] node_id ID of node whose name to get (or 0 for the * local node) * \param[out] node_name If not \p NULL, where to store the node * name * \param[in] message_timeout_ms How long to wait for a reply from the * \p pacemaker-controld API. If 0, * \p pcmk_ipc_dispatch_sync will be used. * Otherwise, \p pcmk_ipc_dispatch_poll will * be used. * * \return Standard Pacemaker return code * * \note The caller is responsible for freeing \p *node_name using \p free(). */ static inline int pcmk_query_node_name(xmlNodePtr *xml, uint32_t node_id, char **node_name, unsigned int message_timeout_ms) { return pcmk_query_node_info(xml, &node_id, node_name, NULL, NULL, NULL, NULL, false, message_timeout_ms); } /*! * \brief Get and output \p pacemakerd status * * \param[in,out] xml Destination for the result, as an XML tree * \param[in] ipc_name IPC name for request * \param[in] message_timeout_ms How long to wait for a reply from the * \p pacemakerd API. If 0, * \p pcmk_ipc_dispatch_sync will be used. * Otherwise, \p pcmk_ipc_dispatch_poll will * be used. * * \return Standard Pacemaker return code */ int pcmk_pacemakerd_status(xmlNodePtr *xml, const char *ipc_name, unsigned int message_timeout_ms); /*! * \brief Remove a resource * * \param[in,out] xml Destination for the result, as an XML tree * \param[in] rsc_id Resource to remove * \param[in] rsc_type Type of the resource ("primitive", "group", etc.) * * \return Standard Pacemaker return code * \note This function will return \p pcmk_rc_ok if \p rsc_id doesn't exist * or if \p rsc_type is incorrect for \p rsc_id (deleting something * that doesn't exist always succeeds). */ int pcmk_resource_delete(xmlNodePtr *xml, const char *rsc_id, const char *rsc_type); /*! * \brief Calculate and output resource operation digests * * \param[out] xml Where to store XML with result * \param[in,out] 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] scheduler Scheduler data (with status) * * \return Standard Pacemaker return code */ int pcmk_resource_digests(xmlNodePtr *xml, pcmk_resource_t *rsc, const pcmk_node_t *node, GHashTable *overrides, pcmk_scheduler_t *scheduler); /*! * \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] scheduler Scheduler data * \param[in] injections 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 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) * * \return Standard Pacemaker return code */ int pcmk_simulate(xmlNodePtr *xml, pcmk_scheduler_t *scheduler, const pcmk_injections_t *injections, unsigned int flags, unsigned int section_opts, const char *use_date, const char *input_file, const char *graph_file, const char *dot_file); /*! * \brief Verify that a CIB is error-free or output errors and warnings * * This high-level function essentially implements crm_verify(8). It operates * on an input CIB file, which can be inputted through one of several ways. It * writes out XML-formatted output. * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] cib_source Source of the CIB: * NULL -> use live cib, "-" -> stdin * "<..." -> xml str, otherwise -> xml file name * * \return Standard Pacemaker return code */ int pcmk_verify(xmlNodePtr *xml, const char *cib_source); /*! * \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, const char *node_types); /*! * \brief Output cluster status formatted like `crm_mon --output-as=xml` * * \param[in,out] xml The destination for the result, as an XML tree * * \return Standard Pacemaker return code */ int pcmk_status(xmlNodePtr *xml); /*! * \brief Check whether each rule in a list is in effect * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] input The CIB XML to check (if \c NULL, use current CIB) * \param[in] date Check whether the rule is in effect at this date and * time (if \c NULL, use current date and time) * \param[in] rule_ids The IDs of the rules to check, as a NULL- * terminated list. * * \return Standard Pacemaker return code */ int pcmk_check_rules(xmlNodePtr *xml, xmlNodePtr input, const crm_time_t *date, const char **rule_ids); /*! * \brief Check whether a given rule is in effect * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] input The CIB XML to check (if \c NULL, use current CIB) * \param[in] date Check whether the rule is in effect at this date and * time (if \c NULL, use current date and time) * \param[in] rule_ids The ID of the rule to check * * \return Standard Pacemaker return code */ static inline int pcmk_check_rule(xmlNodePtr *xml, xmlNodePtr input, const crm_time_t *date, const char *rule_id) { const char *rule_ids[] = {rule_id, NULL}; return pcmk_check_rules(xml, input, date, rule_ids); } /*! * \enum pcmk_rc_disp_flags * \brief Bit flags to control which fields of result code info are displayed */ enum pcmk_rc_disp_flags { pcmk_rc_disp_none = 0, //!< (Does nothing) pcmk_rc_disp_code = (1 << 0), //!< Display result code number pcmk_rc_disp_name = (1 << 1), //!< Display result code name pcmk_rc_disp_desc = (1 << 2), //!< Display result code description }; /*! * \brief Display the name and/or description of a result code * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] code The result code * \param[in] type Interpret \c code as this type of result code. * Supported values: \c pcmk_result_legacy, * \c pcmk_result_rc, \c pcmk_result_exitcode. * \param[in] flags Group of \c pcmk_rc_disp_flags * * \return Standard Pacemaker return code */ int pcmk_show_result_code(xmlNodePtr *xml, int code, enum pcmk_result_type type, uint32_t flags); /*! * \brief List all valid result codes in a particular family * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] type The family of result codes to list. Supported * values: \c pcmk_result_legacy, \c pcmk_result_rc, * \c pcmk_result_exitcode. * \param[in] flags Group of \c pcmk_rc_disp_flags * * \return Standard Pacemaker return code */ int pcmk_list_result_codes(xmlNodePtr *xml, enum pcmk_result_type type, uint32_t flags); /*! * \brief List available providers for the given OCF agent * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] agent_spec Resource agent name * * \return Standard Pacemaker return code */ int pcmk_list_alternatives(xmlNodePtr *xml, const char *agent_spec); /*! * \brief List all agents available for the named standard and/or provider * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] agent_spec STD[:PROV] * * \return Standard Pacemaker return code */ int pcmk_list_agents(xmlNodePtr *xml, char *agent_spec); /*! * \brief List all available OCF providers for the given agent * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] agent_spec Resource agent name * * \return Standard Pacemaker return code */ int pcmk_list_providers(xmlNodePtr *xml, const char *agent_spec); /*! * \brief List all available resource agent standards * * \param[in,out] xml The destination for the result, as an XML tree * * \return Standard Pacemaker return code */ int pcmk_list_standards(xmlNodePtr *xml); /*! * \brief List all available cluster options * * These are options that affect the entire cluster. * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] all If \c true, include advanced and deprecated options * (currently always treated as true) * * \return Standard Pacemaker return code */ int pcmk_list_cluster_options(xmlNode **xml, bool all); /*! * \brief List common fencing resource parameters * * These are parameters that are available for all fencing resources, regardless * of type. They are processed by Pacemaker, rather than by the fence agent or * the fencing library. * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] all If \c true, include advanced and deprecated options * (currently always treated as true) * * \return Standard Pacemaker return code */ int pcmk_list_fencing_params(xmlNode **xml, bool all); /*! * \internal * \brief List meta-attributes applicable to primitive resources as OCF-like XML * * \param[in,out] out Output object * \param[in] all If \c true, include advanced and deprecated options (this * is always treated as true for XML output objects) * * \return Standard Pacemaker return code */ int pcmk_list_primitive_meta(xmlNode **xml, bool all); /*! * \brief Return constraints that apply to the given ticket * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] ticket_id Ticket to find constraint for, or \c NULL for * all ticket constraints * * \return Standard Pacemaker return code */ int pcmk_ticket_constraints(xmlNodePtr *xml, const char *ticket_id); /*! * \brief Delete a ticket's state from the local cluster site * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] ticket_id Ticket to delete * \param[in] force If \c true, delete the ticket even if it has * been granted * * \return Standard Pacemaker return code */ int pcmk_ticket_delete(xmlNodePtr *xml, const char *ticket_id, bool force); /*! * \brief Return the value of a ticket's attribute * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] ticket_id Ticket to find attribute value for * \param[in] attr_name Attribute's name to find value for * \param[in] attr_default If either the ticket or the attribute do not * exist, use this as the value in \p xml * * \return Standard Pacemaker return code */ int pcmk_ticket_get_attr(xmlNodePtr *xml, const char *ticket_id, const char *attr_name, const char *attr_default); /*! * \brief Return information about the given ticket * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] ticket_id Ticket to find info value for, or \c NULL for * all tickets * * \return Standard Pacemaker return code */ int pcmk_ticket_info(xmlNodePtr *xml, const char *ticket_id); /*! * \brief Remove the given attribute(s) from a ticket * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] ticket_id Ticket to remove attributes from * \param[in] attr_delete A list of attribute names + * \param[in] force Attempting to remove the granted attribute of + * \p ticket_id will cause this function to return + * \c EACCES unless \p force is set to \c true * * \return Standard Pacemaker return code */ -int pcmk_ticket_remove_attr(xmlNodePtr *xml, const char *ticket_id, GList *attr_delete); +int pcmk_ticket_remove_attr(xmlNodePtr *xml, const char *ticket_id, GList *attr_delete, + bool force); /*! * \brief Set the given attribute(s) on a ticket * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] ticket_id Ticket to set attributes on * \param[in] attr_set A hash table of attributes, where keys are the * attribute names and the values are the attribute * values + * \param[in] force Attempting to change the granted status of + * \p ticket_id will cause this function to return + * \c EACCES unless \p force is set to \c true * * \return Standard Pacemaker return code * * \note If no \p ticket_id attribute exists but \p attr_set is non-NULL, the * ticket will be created with the given attributes. */ -int pcmk_ticket_set_attr(xmlNodePtr *xml, const char *ticket_id, GHashTable *attr_set); +int pcmk_ticket_set_attr(xmlNodePtr *xml, const char *ticket_id, GHashTable *attr_set, + bool force); /*! * \brief Return a ticket's state XML * * \param[in,out] xml The destination for the result, as an XML tree * \param[in] ticket_id Ticket to find state for, or \c NULL for all * tickets * * \return Standard Pacemaker return code * * \note If \p ticket_id is not \c NULL and more than one ticket exists with * that ID, this function returns \c pcmk_rc_duplicate_id. */ int pcmk_ticket_state(xmlNodePtr *xml, const char *ticket_id); #ifdef BUILD_PUBLIC_LIBPACEMAKER /*! * \brief Ask the cluster to perform fencing * * \param[in,out] 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 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 * fencing action (-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,out] st A connection to the fencer API * \param[in] target The node to get history for * \param[in] timeout How long to wait for 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, const char *target, unsigned int timeout, bool quiet, int verbose, bool broadcast, bool cleanup); /*! * \brief List all installed fence agents * * \param[in,out] xml The destination for the result, as an XML tree (if * not NULL, previous contents will be freed and lost) * \param[in,out] st A connection to the fencer API * \param[in] timeout How long to wait for 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? * * \param[in,out] xml The destination for the result, as an XML tree (if * not NULL, previous contents will be freed and lost) * \param[in] target The node that was fenced * \param[in] as_nodeid If true, \p target has node ID rather than name * * \return Standard Pacemaker return code */ int pcmk_fence_last(xmlNodePtr *xml, const char *target, bool as_nodeid); /*! * \brief List nodes that can be fenced * * \param[in,out] xml The destination for the result, as an XML tree (if * not NULL, previous contents will be freed and lost) * \param[in,out] st A connection to the fencer API * \param[in] device_id Resource ID of fence device to check * \param[in] timeout How long to wait for 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 fence agent * * \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 (if * not NULL, previous contents will be freed and lost) * \param[in,out] st A connection to the fencer API * \param[in] agent The fence agent to get metadata for * \param[in] timeout How long to wait for operation to complete (in ms) * * \return Standard Pacemaker return code */ int pcmk_fence_metadata(xmlNodePtr *xml, stonith_t *st, const char *agent, unsigned int timeout); /*! * \brief List registered fence devices * * \param[in,out] xml The destination for the result, as an XML tree (if * not NULL, previous contents will be freed and lost) * \param[in,out] st A connection to the fencer API * \param[in] target If not NULL, return only devices that can fence this * \param[in] timeout How long to wait for operation to complete (in ms) * * \return Standard Pacemaker return code */ int pcmk_fence_registered(xmlNodePtr *xml, stonith_t *st, const char *target, unsigned int timeout); /*! * \brief Register a fencing topology level * * \param[in,out] st A connection to the fencer API * \param[in] target What fencing level targets (as "name=value" to * target by given node attribute, or "@pattern" to * target by node name pattern, or a node name) * \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, const char *target, int fence_level, const stonith_key_value_t *devices); /*! * \brief Unregister a fencing topology level * * \param[in,out] st A connection to the fencer API * \param[in] target What fencing level targets (as "name=value" to * target by given node attribute, or "@pattern" to * target by node name pattern, or a node name) * \param[in] fence_level Index number of level to remove * * \return Standard Pacemaker return code */ int pcmk_fence_unregister_level(stonith_t *st, const char *target, int fence_level); /*! * \brief Validate a fence device configuration * * \param[in,out] xml The destination for the result, as an XML tree (if * not NULL, previous contents will be freed and lost) * \param[in,out] st A connection to the fencer API * \param[in] agent The agent to validate (for example, "fence_xvm") * \param[in] id Fence device ID (may be NULL) * \param[in] params Fence device configuration parameters * \param[in] timeout How long to wait for 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, const stonith_key_value_t *params, unsigned int timeout); #endif #ifdef __cplusplus } #endif #endif diff --git a/include/pcmki/pcmki_ticket.h b/include/pcmki/pcmki_ticket.h index cccd88836c..a4606fc159 100644 --- a/include/pcmki/pcmki_ticket.h +++ b/include/pcmki/pcmki_ticket.h @@ -1,148 +1,154 @@ /* * Copyright 2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__PCMKI_PCMKI_TICKET__H # define PCMK__PCMKI_PCMKI_TICKET__H #include #include /*! * \internal * \brief Return the state XML for a given ticket * * \param[in] cib Open CIB connection * \param[in] ticket_id Ticket to get state for, or \c NULL for all tickets * \param[out] state Where to store the result XML * * \return Standard Pacemaker return code * * \note If \p ticket_id is not \c NULL and more than one ticket exists with * that ID, this function returns \c pcmk_rc_duplicate_id. */ int pcmk__get_ticket_state(cib_t *cib, const char *ticket_id, xmlNode **state); /*! * \internal * \brief Display the constraints that apply to a given ticket * * \param[in,out] out Output object * \param[in] cib Open CIB connection * \param[in] ticket_id Ticket to find constraints for, * or \c NULL for all ticket constraints * * \return Standard Pacemaker return code */ int pcmk__ticket_constraints(pcmk__output_t *out, cib_t *cib, const char *ticket_id); /*! * \internal * \brief Delete a ticket's state from the local cluster site * * \param[in,out] out Output object * \param[in] cib Open CIB connection * \param[in] scheduler Scheduler data * \param[in] ticket_id Ticket to delete * \param[in] force If \c true, delete the ticket even if it has * been granted * * \return Standard Pacemaker return code */ int pcmk__ticket_delete(pcmk__output_t *out, cib_t *cib, pcmk_scheduler_t *scheduler, const char *ticket_id, bool force); /*! * \internal * \brief Return the value of a ticket's attribute * * \param[in,out] out Output object * \param[in,out] scheduler Scheduler data * \param[in] ticket_id Ticket to find attribute value for * \param[in] attr_name Attribute's name to find value for * \param[in] attr_default If either the ticket or the attribute do not * exist, use this as the value in the output * * \return Standard Pacemaker return code */ int pcmk__ticket_get_attr(pcmk__output_t *out, pcmk_scheduler_t *scheduler, const char *ticket_id, const char *attr_name, const char *attr_default); /*! * \brief Return information about the given ticket * * \param[in,out] out Output object * \param[in,out] scheduler Scheduler data * \param[in] ticket_id Ticket to display info for, or \c NULL for * all tickets * \param[in] details If true (and \p out is not an XML format * object), output any additional attributes * set on a ticket beyond the basics * \param[in] raw If true (and \p out is not an XML format * object), simply list the IDs of all tickets. * This does not make a lot of sense if * \p ticket_id is not NULL, but that will not * raise an error. * * \return Standard Pacemaker return code */ int pcmk__ticket_info(pcmk__output_t *out, pcmk_scheduler_t *scheduler, const char *ticket_id, bool details, bool raw); /*! * \brief Remove the given attribute(s) from a ticket * * \param[in,out] out Output object * \param[in] cib Open CIB connection * \param[in,out] scheduler Scheduler data * \param[in] ticket_id Ticket to remove attributes from * \param[in] attr_delete A list of attribute names + * \param[in] force Attempting to remove the granted attribute of + * \p ticket_id will cause this function to return + * \c EACCES unless \p force is set to \c true * * \return Standard Pacemaker return code */ int pcmk__ticket_remove_attr(pcmk__output_t *out, cib_t *cib, pcmk_scheduler_t *scheduler, - const char *ticket_id, GList *attr_delete); + const char *ticket_id, GList *attr_delete, bool force); /*! * \brief Set the given attribute(s) on a ticket * * \param[in,out] out Output object * \param[in] cib Open CIB connection * \param[in,out] scheduler Scheduler data * \param[in] ticket_id Ticket to set attributes on * \param[in] attr_set A hash table of attributes, where keys are the * attribute names and the values are the attribute * values + * \param[in] force Attempting to change the granted status of + * \p ticket_id will cause this function to return + * \c EACCES unless \p force is set to \c true * * \return Standard Pacemaker return code * * \note If no \p ticket_id attribute exists but \p attr_set is non-NULL, the * ticket will be created with the given attributes. */ int pcmk__ticket_set_attr(pcmk__output_t *out, cib_t *cib, pcmk_scheduler_t *scheduler, - const char *ticket_id, GHashTable *attr_set); + const char *ticket_id, GHashTable *attr_set, bool force); /*! * \internal * \brief Return a ticket's state XML * * \param[in,out] out Output object * \param[in] cib Open CIB connection * \param[in] ticket_id Ticket to find constraints for, * or \c NULL for all ticket constraints * * \return Standard Pacemaker return code * * \note If \p ticket_id is not \c NULL and more than one ticket exists with * that ID, this function returns \c pcmk_rc_duplicate_id. */ int pcmk__ticket_state(pcmk__output_t *out, cib_t *cib, const char *ticket_id); #endif diff --git a/lib/pacemaker/pcmk_ticket.c b/lib/pacemaker/pcmk_ticket.c index 4717abde17..6f158483fd 100644 --- a/lib/pacemaker/pcmk_ticket.c +++ b/lib/pacemaker/pcmk_ticket.c @@ -1,541 +1,553 @@ /* * Copyright 2024 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 "libpacemaker_private.h" static int build_ticket_modify_xml(cib_t *cib, const char *ticket_id, xmlNode **ticket_state_xml, xmlNode **xml_top) { int rc = pcmk__get_ticket_state(cib, ticket_id, ticket_state_xml); if (rc == pcmk_rc_ok || rc == pcmk_rc_duplicate_id) { /* Ticket(s) found - return their state */ *xml_top = *ticket_state_xml; } else if (rc == ENXIO) { /* No ticket found - build the XML needed to create it */ xmlNode *xml_obj = NULL; *xml_top = pcmk__xe_create(NULL, PCMK_XE_STATUS); xml_obj = pcmk__xe_create(*xml_top, PCMK_XE_TICKETS); *ticket_state_xml = pcmk__xe_create(xml_obj, PCMK__XE_TICKET_STATE); crm_xml_add(*ticket_state_xml, PCMK_XA_ID, ticket_id); rc = pcmk_rc_ok; } else { /* Some other error occurred - clean up and return */ free_xml(*ticket_state_xml); } return rc; } static void add_attribute_xml(pcmk_scheduler_t *scheduler, const char *ticket_id, GHashTable *attr_set, xmlNode **ticket_state_xml) { GHashTableIter hash_iter; char *key = NULL; char *value = NULL; pcmk_ticket_t *ticket = g_hash_table_lookup(scheduler->tickets, ticket_id); g_hash_table_iter_init(&hash_iter, attr_set); while (g_hash_table_iter_next(&hash_iter, (gpointer *) & key, (gpointer *) & value)) { crm_xml_add(*ticket_state_xml, key, value); if (pcmk__str_eq(key, PCMK__XA_GRANTED, pcmk__str_none) && (ticket == NULL || ticket->granted == FALSE) && crm_is_true(value)) { char *now = pcmk__ttoa(time(NULL)); crm_xml_add(*ticket_state_xml, PCMK_XA_LAST_GRANTED, now); free(now); } } } int pcmk__get_ticket_state(cib_t *cib, const char *ticket_id, xmlNode **state) { int rc = pcmk_rc_ok; xmlNode *xml_search = NULL; char *xpath = NULL; CRM_ASSERT(cib!= NULL && state != NULL); *state = NULL; if (ticket_id != NULL) { xpath = crm_strdup_printf("/" PCMK_XE_CIB "/" PCMK_XE_STATUS "/" PCMK_XE_TICKETS "/" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"%s\"]", ticket_id); } else { xpath = crm_strdup_printf("/" PCMK_XE_CIB "/" PCMK_XE_STATUS "/" PCMK_XE_TICKETS); } rc = cib->cmds->query(cib, xpath, &xml_search, cib_sync_call | cib_scope_local | cib_xpath); rc = pcmk_legacy2rc(rc); if (rc == pcmk_rc_ok) { crm_log_xml_debug(xml_search, "Match"); if (xml_search->children != NULL && ticket_id != NULL) { rc = pcmk_rc_duplicate_id; } } free(xpath); *state = xml_search; return rc; } int pcmk__ticket_constraints(pcmk__output_t *out, cib_t *cib, const char *ticket_id) { int rc = pcmk_rc_ok; xmlNode *result = NULL; const char *xpath_base = NULL; char *xpath = NULL; CRM_ASSERT(out != NULL && cib != NULL); xpath_base = pcmk_cib_xpath_for(PCMK_XE_CONSTRAINTS); CRM_ASSERT(xpath_base != NULL); if (ticket_id != NULL) { xpath = crm_strdup_printf("%s/" PCMK_XE_RSC_TICKET "[@" PCMK_XA_TICKET "=\"%s\"]", xpath_base, ticket_id); } else { xpath = crm_strdup_printf("%s/" PCMK_XE_RSC_TICKET, xpath_base); } rc = cib->cmds->query(cib, (const char *) xpath, &result, cib_sync_call | cib_scope_local | cib_xpath); rc = pcmk_legacy2rc(rc); if (result != NULL) { out->message(out, "ticket-constraints", result); free_xml(result); } free(xpath); return rc; } int pcmk_ticket_constraints(xmlNodePtr *xml, const char *ticket_id) { pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; cib_t *cib = NULL; rc = pcmk__setup_output_cib_sched(&out, &cib, NULL, xml); if (rc != pcmk_rc_ok) { goto done; } rc = pcmk__ticket_constraints(out, cib, ticket_id); done: if (cib != NULL) { cib__clean_up_connection(&cib); } pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); return rc; } static int delete_single_ticket(xmlNode *child, void *userdata) { int rc = pcmk_rc_ok; cib_t *cib = (cib_t *) userdata; rc = cib->cmds->remove(cib, PCMK_XE_STATUS, child, cib_sync_call); rc = pcmk_legacy2rc(rc); return rc; } int pcmk__ticket_delete(pcmk__output_t *out, cib_t *cib, pcmk_scheduler_t *scheduler, const char *ticket_id, bool force) { int rc = pcmk_rc_ok; xmlNode *state = NULL; CRM_ASSERT(cib != NULL && scheduler != NULL); if (ticket_id == NULL) { return EINVAL; } if (!force) { pcmk_ticket_t *ticket = g_hash_table_lookup(scheduler->tickets, ticket_id); if (ticket == NULL) { return ENXIO; } if (ticket->granted) { return EACCES; } } rc = pcmk__get_ticket_state(cib, ticket_id, &state); if (rc == pcmk_rc_duplicate_id) { out->info(out, "Multiple " PCMK__XE_TICKET_STATE "s match ticket=%s", ticket_id); } else if (rc == ENXIO) { return pcmk_rc_ok; } else if (rc != pcmk_rc_ok) { return rc; } crm_log_xml_debug(state, "Delete"); if (rc == pcmk_rc_duplicate_id) { rc = pcmk__xe_foreach_child(state, NULL, delete_single_ticket, cib); } else { rc = delete_single_ticket(state, cib); } if (rc == pcmk_rc_ok) { out->info(out, "Cleaned up %s", ticket_id); } free_xml(state); return rc; } int pcmk_ticket_delete(xmlNodePtr *xml, const char *ticket_id, bool force) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; cib_t *cib = NULL; int rc = pcmk_rc_ok; rc = pcmk__setup_output_cib_sched(&out, &cib, &scheduler, xml); if (rc != pcmk_rc_ok) { goto done; } rc = pcmk__ticket_delete(out, cib, scheduler, ticket_id, force); done: if (cib != NULL) { cib__clean_up_connection(&cib); } pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); pe_free_working_set(scheduler); return rc; } int pcmk__ticket_get_attr(pcmk__output_t *out, pcmk_scheduler_t *scheduler, const char *ticket_id, const char *attr_name, const char *attr_default) { int rc = pcmk_rc_ok; const char *attr_value = NULL; pcmk_ticket_t *ticket = NULL; CRM_ASSERT(out != NULL && scheduler != NULL); if (ticket_id == NULL || attr_name == NULL) { return EINVAL; } ticket = g_hash_table_lookup(scheduler->tickets, ticket_id); if (ticket != NULL) { attr_value = g_hash_table_lookup(ticket->state, attr_name); } if (attr_value != NULL) { out->message(out, "ticket-attribute", ticket_id, attr_name, attr_value); } else if (attr_default != NULL) { out->message(out, "ticket-attribute", ticket_id, attr_name, attr_default); } else { rc = ENXIO; } return rc; } int pcmk_ticket_get_attr(xmlNodePtr *xml, const char *ticket_id, const char *attr_name, const char *attr_default) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; rc = pcmk__setup_output_cib_sched(&out, NULL, &scheduler, xml); if (rc != pcmk_rc_ok) { goto done; } rc = pcmk__ticket_get_attr(out, scheduler, ticket_id, attr_name, attr_default); done: pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); pe_free_working_set(scheduler); return rc; } int pcmk__ticket_info(pcmk__output_t *out, pcmk_scheduler_t *scheduler, const char *ticket_id, bool details, bool raw) { int rc = pcmk_rc_ok; CRM_ASSERT(out != NULL && scheduler != NULL); if (ticket_id != NULL) { GHashTable *tickets = NULL; pcmk_ticket_t *ticket = g_hash_table_lookup(scheduler->tickets, ticket_id); if (ticket == NULL) { return ENXIO; } /* The ticket-list message expects a GHashTable, so we'll construct * one with just this single item. */ tickets = pcmk__strkey_table(free, NULL); g_hash_table_insert(tickets, strdup(ticket->id), ticket); out->message(out, "ticket-list", tickets, false, raw, details); g_hash_table_destroy(tickets); } else { out->message(out, "ticket-list", scheduler->tickets, false, raw, details); } return rc; } int pcmk_ticket_info(xmlNodePtr *xml, const char *ticket_id) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; rc = pcmk__setup_output_cib_sched(&out, NULL, &scheduler, xml); if (rc != pcmk_rc_ok) { goto done; } pe__register_messages(out); /* XML output (which is the only format supported by public API functions * due to the use of pcmk__xml_output_new above) always prints all details, * so just pass false for the last two arguments. */ rc = pcmk__ticket_info(out, scheduler, ticket_id, false, false); done: pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); pe_free_working_set(scheduler); return rc; } int pcmk__ticket_remove_attr(pcmk__output_t *out, cib_t *cib, pcmk_scheduler_t *scheduler, - const char *ticket_id, GList *attr_delete) + const char *ticket_id, GList *attr_delete, bool force) { xmlNode *ticket_state_xml = NULL; xmlNode *xml_top = NULL; int rc = pcmk_rc_ok; CRM_ASSERT(out != NULL && cib != NULL && scheduler != NULL); if (ticket_id == NULL) { return EINVAL; } /* Nothing to do */ if (attr_delete == NULL) { return pcmk_rc_ok; } rc = build_ticket_modify_xml(cib, ticket_id, &ticket_state_xml, &xml_top); if (rc == pcmk_rc_duplicate_id) { out->info(out, "Multiple " PCMK__XE_TICKET_STATE "s match ticket=%s", ticket_id); } else if (rc != pcmk_rc_ok) { free_xml(ticket_state_xml); return rc; } for (GList *list_iter = attr_delete; list_iter != NULL; list_iter = list_iter->next) { const char *key = list_iter->data; + + if (!force && pcmk__str_eq(key, PCMK__XA_GRANTED, pcmk__str_none)) { + free_xml(ticket_state_xml); + return EACCES; + } + pcmk__xe_remove_attr(ticket_state_xml, key); } crm_log_xml_debug(xml_top, "Replace"); rc = cib->cmds->replace(cib, PCMK_XE_STATUS, ticket_state_xml, cib_sync_call); rc = pcmk_legacy2rc(rc); free_xml(xml_top); return rc; } int -pcmk_ticket_remove_attr(xmlNodePtr *xml, const char *ticket_id, GList *attr_delete) +pcmk_ticket_remove_attr(xmlNodePtr *xml, const char *ticket_id, GList *attr_delete, bool force) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; cib_t *cib = NULL; rc = pcmk__setup_output_cib_sched(&out, &cib, &scheduler, xml); if (rc != pcmk_rc_ok) { goto done; } - rc = pcmk__ticket_remove_attr(out, cib, scheduler, ticket_id, attr_delete); + rc = pcmk__ticket_remove_attr(out, cib, scheduler, ticket_id, attr_delete, force); done: if (cib != NULL) { cib__clean_up_connection(&cib); } pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); pe_free_working_set(scheduler); return rc; } int pcmk__ticket_set_attr(pcmk__output_t *out, cib_t *cib, pcmk_scheduler_t *scheduler, - const char *ticket_id, GHashTable *attr_set) + const char *ticket_id, GHashTable *attr_set, bool force) { xmlNode *ticket_state_xml = NULL; xmlNode *xml_top = NULL; int rc = pcmk_rc_ok; CRM_ASSERT(out != NULL && cib != NULL && scheduler != NULL); if (ticket_id == NULL) { return EINVAL; } /* Nothing to do */ if (attr_set == NULL || g_hash_table_size(attr_set) == 0) { return pcmk_rc_ok; } rc = build_ticket_modify_xml(cib, ticket_id, &ticket_state_xml, &xml_top); if (rc == pcmk_rc_duplicate_id) { out->info(out, "Multiple " PCMK__XE_TICKET_STATE "s match ticket=%s", ticket_id); } else if (rc != pcmk_rc_ok) { free_xml(ticket_state_xml); return rc; } + if (!force && g_hash_table_lookup(attr_set, PCMK__XA_GRANTED)) { + free_xml(ticket_state_xml); + return EACCES; + } + add_attribute_xml(scheduler, ticket_id, attr_set, &ticket_state_xml); crm_log_xml_debug(xml_top, "Update"); rc = cib->cmds->modify(cib, PCMK_XE_STATUS, xml_top, cib_sync_call); rc = pcmk_legacy2rc(rc); free_xml(xml_top); return rc; } int -pcmk_ticket_set_attr(xmlNodePtr *xml, const char *ticket_id, GHashTable *attr_set) +pcmk_ticket_set_attr(xmlNodePtr *xml, const char *ticket_id, GHashTable *attr_set, + bool force) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; cib_t *cib = NULL; rc = pcmk__setup_output_cib_sched(&out, &cib, &scheduler, xml); if (rc != pcmk_rc_ok) { goto done; } - rc = pcmk__ticket_set_attr(out, cib, scheduler, ticket_id, attr_set); + rc = pcmk__ticket_set_attr(out, cib, scheduler, ticket_id, attr_set, force); done: if (cib != NULL) { cib__clean_up_connection(&cib); } pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); pe_free_working_set(scheduler); return rc; } int pcmk__ticket_state(pcmk__output_t *out, cib_t *cib, const char *ticket_id) { xmlNode *state_xml = NULL; int rc = pcmk_rc_ok; CRM_ASSERT(out != NULL && cib != NULL); rc = pcmk__get_ticket_state(cib, ticket_id, &state_xml); if (rc == pcmk_rc_duplicate_id) { out->info(out, "Multiple " PCMK__XE_TICKET_STATE "s match ticket=%s", ticket_id); } if (state_xml != NULL) { out->message(out, "ticket-state", state_xml); free_xml(state_xml); } return rc; } int pcmk_ticket_state(xmlNodePtr *xml, const char *ticket_id) { pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; cib_t *cib = NULL; rc = pcmk__setup_output_cib_sched(&out, &cib, NULL, xml); if (rc != pcmk_rc_ok) { goto done; } rc = pcmk__ticket_state(out, cib, ticket_id); done: if (cib != NULL) { cib__clean_up_connection(&cib); } pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); return rc; } diff --git a/lib/pacemaker/tests/pcmk_ticket/pcmk_ticket_remove_attr_test.c b/lib/pacemaker/tests/pcmk_ticket/pcmk_ticket_remove_attr_test.c index 5b0bc92104..18abb95222 100644 --- a/lib/pacemaker/tests/pcmk_ticket/pcmk_ticket_remove_attr_test.c +++ b/lib/pacemaker/tests/pcmk_ticket/pcmk_ticket_remove_attr_test.c @@ -1,177 +1,231 @@ /* * Copyright 2024 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 static char *cib_path = NULL; static void cib_not_connected(void **state) { xmlNode *xml = NULL; /* Without any special setup, cib_new() in pcmk_ticket_remove_attr will use the * native CIB which means IPC calls. But there's nothing listening for those * calls, so signon() will return ENOTCONN. Check that we handle that. */ - assert_int_equal(pcmk_ticket_remove_attr(&xml, NULL, NULL), ENOTCONN); + assert_int_equal(pcmk_ticket_remove_attr(&xml, NULL, NULL, false), ENOTCONN); pcmk__assert_validates(xml); free_xml(xml); } static int setup_test(void **state) { cib_path = pcmk__cib_test_copy_cib("tickets.xml"); if (cib_path == NULL) { return -1; } return 0; } static int teardown_test(void **state) { pcmk__cib_test_cleanup(cib_path); cib_path = NULL; return 0; } static void bad_arguments(void **state) { xmlNode *xml = NULL; - assert_int_equal(pcmk_ticket_remove_attr(NULL, "ticketA", NULL), EINVAL); + assert_int_equal(pcmk_ticket_remove_attr(NULL, "ticketA", NULL, false), EINVAL); - assert_int_equal(pcmk_ticket_remove_attr(&xml, NULL, NULL), EINVAL); + assert_int_equal(pcmk_ticket_remove_attr(&xml, NULL, NULL, false), EINVAL); pcmk__assert_validates(xml); free_xml(xml); } static void no_attrs(void **state) { GList *attrs = NULL; xmlNode *xml = NULL; xmlNode *xml_search = NULL; cib_t *cib = cib_new(); cib->cmds->signon(cib, crm_system_name, cib_command); /* Deleting no attributes on a ticket that doesn't exist is a no-op */ - assert_int_equal(pcmk_ticket_remove_attr(&xml, "XYZ", NULL), pcmk_rc_ok); + assert_int_equal(pcmk_ticket_remove_attr(&xml, "XYZ", NULL, false), pcmk_rc_ok); pcmk__assert_validates(xml); free_xml(xml); xml = NULL; cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"XYZ\"]", &xml_search, cib_xpath | cib_scope_local); assert_null(xml_search); /* Deleting no attributes on a ticket that exists is also a no-op */ - assert_int_equal(pcmk_ticket_remove_attr(&xml, "ticketA", NULL), pcmk_rc_ok); + assert_int_equal(pcmk_ticket_remove_attr(&xml, "ticketA", NULL, false), pcmk_rc_ok); pcmk__assert_validates(xml); free_xml(xml); xml = NULL; cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"ticketA\"]", &xml_search, cib_xpath | cib_scope_local); assert_string_equal("1", crm_element_value(xml_search, "owner")); free_xml(xml_search); /* Another way of specifying no attributes */ - assert_int_equal(pcmk_ticket_remove_attr(&xml, "XYZ", attrs), pcmk_rc_ok); + assert_int_equal(pcmk_ticket_remove_attr(&xml, "XYZ", attrs, false), pcmk_rc_ok); pcmk__assert_validates(xml); free_xml(xml); cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"XYZ\"]", &xml_search, cib_xpath | cib_scope_local); assert_null(xml_search); g_list_free(attrs); cib__clean_up_connection(&cib); } static void remove_missing_attrs(void **state) { GList *attrs = NULL; xmlNode *xml = NULL; xmlNode *xml_search = NULL; cib_t *cib; attrs = g_list_append(attrs, strdup("XYZ")); /* Deleting an attribute that doesn't exist is a no-op */ - assert_int_equal(pcmk_ticket_remove_attr(&xml, "ticketA", attrs), pcmk_rc_ok); + assert_int_equal(pcmk_ticket_remove_attr(&xml, "ticketA", attrs, false), pcmk_rc_ok); pcmk__assert_validates(xml); free_xml(xml); cib = cib_new(); cib->cmds->signon(cib, crm_system_name, cib_command); cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"ticketA\"]", &xml_search, cib_xpath | cib_scope_local); assert_string_equal("1", crm_element_value(xml_search, "owner")); assert_null(crm_element_value(xml_search, "XYZ")); free_xml(xml_search); g_list_free_full(attrs, free); cib__clean_up_connection(&cib); } static void remove_existing_attr(void **state) { GList *attrs = NULL; xmlNode *xml = NULL; xmlNode *xml_search = NULL; cib_t *cib; attrs = g_list_append(attrs, strdup("owner")); - assert_int_equal(pcmk_ticket_remove_attr(&xml, "ticketA", attrs), pcmk_rc_ok); + assert_int_equal(pcmk_ticket_remove_attr(&xml, "ticketA", attrs, false), pcmk_rc_ok); pcmk__assert_validates(xml); free_xml(xml); cib = cib_new(); cib->cmds->signon(cib, crm_system_name, cib_command); cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"ticketA\"]", &xml_search, cib_xpath | cib_scope_local); assert_null(crm_element_value(xml_search, "owner")); free_xml(xml_search); g_list_free_full(attrs, free); cib__clean_up_connection(&cib); } +static void +remove_granted_without_force(void **state) +{ + GList *attrs = NULL; + xmlNode *xml = NULL; + xmlNode *xml_search = NULL; + cib_t *cib; + + attrs = g_list_append(attrs, strdup(PCMK__XA_GRANTED)); + + assert_int_equal(pcmk_ticket_remove_attr(&xml, "ticketB", attrs, false), EACCES); + pcmk__assert_validates(xml); + free_xml(xml); + + cib = cib_new(); + cib->cmds->signon(cib, crm_system_name, cib_command); + cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"ticketB\"]", + &xml_search, cib_xpath | cib_scope_local); + + assert_string_equal("true", crm_element_value(xml_search, PCMK__XA_GRANTED)); + + free_xml(xml_search); + g_list_free_full(attrs, free); + cib__clean_up_connection(&cib); +} + +static void +remove_granted_with_force(void **state) +{ + GList *attrs = NULL; + xmlNode *xml = NULL; + xmlNode *xml_search = NULL; + cib_t *cib; + + attrs = g_list_append(attrs, strdup(PCMK__XA_GRANTED)); + + assert_int_equal(pcmk_ticket_remove_attr(&xml, "ticketB", attrs, true), pcmk_rc_ok); + pcmk__assert_validates(xml); + free_xml(xml); + + cib = cib_new(); + cib->cmds->signon(cib, crm_system_name, cib_command); + cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"ticketB\"]", + &xml_search, cib_xpath | cib_scope_local); + + assert_null(crm_element_value(xml_search, PCMK__XA_GRANTED)); + + free_xml(xml_search); + g_list_free_full(attrs, free); + cib__clean_up_connection(&cib); +} + /* There are two kinds of tests in this file: * * (1) Those that test what happens if the CIB is not set up correctly, and * (2) Those that test what happens when run against a CIB. * * Therefore, we need two kinds of setup/teardown functions. We only do * minimal overall setup for the entire group, and then setup the CIB for * those tests that need it. */ PCMK__UNIT_TEST(pcmk__cib_test_setup_group, NULL, cmocka_unit_test(cib_not_connected), cmocka_unit_test_setup_teardown(bad_arguments, setup_test, teardown_test), cmocka_unit_test_setup_teardown(no_attrs, setup_test, teardown_test), cmocka_unit_test_setup_teardown(remove_missing_attrs, setup_test, teardown_test), - cmocka_unit_test_setup_teardown(remove_existing_attr, setup_test, teardown_test)) + cmocka_unit_test_setup_teardown(remove_existing_attr, setup_test, teardown_test), + cmocka_unit_test_setup_teardown(remove_granted_without_force, setup_test, teardown_test), + cmocka_unit_test_setup_teardown(remove_granted_with_force, setup_test, teardown_test)) diff --git a/lib/pacemaker/tests/pcmk_ticket/pcmk_ticket_set_attr_test.c b/lib/pacemaker/tests/pcmk_ticket/pcmk_ticket_set_attr_test.c index cb10ca0da4..7c41e9cda9 100644 --- a/lib/pacemaker/tests/pcmk_ticket/pcmk_ticket_set_attr_test.c +++ b/lib/pacemaker/tests/pcmk_ticket/pcmk_ticket_set_attr_test.c @@ -1,225 +1,281 @@ /* * Copyright 2024 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 static char *cib_path = NULL; static void cib_not_connected(void **state) { xmlNode *xml = NULL; /* Without any special setup, cib_new() in pcmk_ticket_set_attr will use the * native CIB which means IPC calls. But there's nothing listening for those * calls, so signon() will return ENOTCONN. Check that we handle that. */ - assert_int_equal(pcmk_ticket_set_attr(&xml, NULL, NULL), ENOTCONN); + assert_int_equal(pcmk_ticket_set_attr(&xml, NULL, NULL, false), ENOTCONN); pcmk__assert_validates(xml); free_xml(xml); } static int setup_test(void **state) { cib_path = pcmk__cib_test_copy_cib("tickets.xml"); if (cib_path == NULL) { return -1; } return 0; } static int teardown_test(void **state) { pcmk__cib_test_cleanup(cib_path); cib_path = NULL; return 0; } static void bad_arguments(void **state) { xmlNode *xml = NULL; - assert_int_equal(pcmk_ticket_set_attr(NULL, "ticketA", NULL), EINVAL); + assert_int_equal(pcmk_ticket_set_attr(NULL, "ticketA", NULL, false), EINVAL); - assert_int_equal(pcmk_ticket_set_attr(&xml, NULL, NULL), EINVAL); + assert_int_equal(pcmk_ticket_set_attr(&xml, NULL, NULL, false), EINVAL); pcmk__assert_validates(xml); free_xml(xml); } static void unknown_ticket_no_attrs(void **state) { GHashTable *attrs = pcmk__strkey_table(free, free); xmlNode *xml = NULL; xmlNode *xml_search = NULL; cib_t *cib = cib_new(); cib->cmds->signon(cib, crm_system_name, cib_command); /* Setting no attributes on a ticket that doesn't exist is a no-op */ - assert_int_equal(pcmk_ticket_set_attr(&xml, "XYZ", NULL), pcmk_rc_ok); + assert_int_equal(pcmk_ticket_set_attr(&xml, "XYZ", NULL, false), pcmk_rc_ok); pcmk__assert_validates(xml); free_xml(xml); xml = NULL; cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"XYZ\"]", &xml_search, cib_xpath | cib_scope_local); assert_null(xml_search); /* Another way of specifying no attributes */ - assert_int_equal(pcmk_ticket_set_attr(&xml, "XYZ", attrs), pcmk_rc_ok); + assert_int_equal(pcmk_ticket_set_attr(&xml, "XYZ", attrs, false), pcmk_rc_ok); pcmk__assert_validates(xml); free_xml(xml); cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"XYZ\"]", &xml_search, cib_xpath | cib_scope_local); assert_null(xml_search); g_hash_table_destroy(attrs); cib__clean_up_connection(&cib); } static void unknown_ticket_with_attrs(void **state) { GHashTable *attrs = pcmk__strkey_table(free, free); xmlNode *xml = NULL; xmlNode *xml_search = NULL; cib_t *cib; pcmk__insert_dup(attrs, "attrA", "123"); pcmk__insert_dup(attrs, "attrB", "456"); /* Setting attributes on a ticket that doesn't exist causes the ticket to * be created with the given attributes */ - assert_int_equal(pcmk_ticket_set_attr(&xml, "XYZ", attrs), pcmk_rc_ok); + assert_int_equal(pcmk_ticket_set_attr(&xml, "XYZ", attrs, false), pcmk_rc_ok); pcmk__assert_validates(xml); free_xml(xml); cib = cib_new(); cib->cmds->signon(cib, crm_system_name, cib_command); cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"XYZ\"]", &xml_search, cib_xpath | cib_scope_local); assert_string_equal("123", crm_element_value(xml_search, "attrA")); assert_string_equal("456", crm_element_value(xml_search, "attrB")); free_xml(xml_search); g_hash_table_destroy(attrs); cib__clean_up_connection(&cib); } static void overwrite_existing_attr(void **state) { GHashTable *attrs = pcmk__strkey_table(free, free); xmlNode *xml = NULL; xmlNode *xml_search = NULL; cib_t *cib; pcmk__insert_dup(attrs, "owner", "2"); - assert_int_equal(pcmk_ticket_set_attr(&xml, "ticketA", attrs), pcmk_rc_ok); + assert_int_equal(pcmk_ticket_set_attr(&xml, "ticketA", attrs, false), pcmk_rc_ok); pcmk__assert_validates(xml); free_xml(xml); cib = cib_new(); cib->cmds->signon(cib, crm_system_name, cib_command); cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"ticketA\"]", &xml_search, cib_xpath | cib_scope_local); assert_string_equal("2", crm_element_value(xml_search, "owner")); free_xml(xml_search); g_hash_table_destroy(attrs); cib__clean_up_connection(&cib); } static void -not_granted_to_granted(void **state) +not_granted_to_granted_without_force(void **state) { GHashTable *attrs = pcmk__strkey_table(free, free); xmlNode *xml = NULL; xmlNode *xml_search = NULL; cib_t *cib; pcmk__insert_dup(attrs, PCMK__XA_GRANTED, "true"); - assert_int_equal(pcmk_ticket_set_attr(&xml, "ticketA", attrs), pcmk_rc_ok); + assert_int_equal(pcmk_ticket_set_attr(&xml, "ticketA", attrs, false), EACCES); + pcmk__assert_validates(xml); + free_xml(xml); + + cib = cib_new(); + cib->cmds->signon(cib, crm_system_name, cib_command); + cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"ticketA\"]", + &xml_search, cib_xpath | cib_scope_local); + + assert_string_equal("false", crm_element_value(xml_search, PCMK__XA_GRANTED)); + assert_null(crm_element_value(xml_search, PCMK_XA_LAST_GRANTED)); + + free_xml(xml_search); + g_hash_table_destroy(attrs); + cib__clean_up_connection(&cib); +} + +static void +not_granted_to_granted_with_force(void **state) +{ + GHashTable *attrs = pcmk__strkey_table(free, free); + xmlNode *xml = NULL; + xmlNode *xml_search = NULL; + cib_t *cib; + + pcmk__insert_dup(attrs, PCMK__XA_GRANTED, "true"); + + assert_int_equal(pcmk_ticket_set_attr(&xml, "ticketA", attrs, true), pcmk_rc_ok); pcmk__assert_validates(xml); free_xml(xml); cib = cib_new(); cib->cmds->signon(cib, crm_system_name, cib_command); cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"ticketA\"]", &xml_search, cib_xpath | cib_scope_local); assert_string_equal("true", crm_element_value(xml_search, PCMK__XA_GRANTED)); assert_non_null(crm_element_value(xml_search, PCMK_XA_LAST_GRANTED)); free_xml(xml_search); g_hash_table_destroy(attrs); cib__clean_up_connection(&cib); } static void -granted_to_not_granted(void **state) +granted_to_not_granted_without_force(void **state) +{ + GHashTable *attrs = pcmk__strkey_table(free, free); + xmlNode *xml = NULL; + xmlNode *xml_search = NULL; + cib_t *cib; + + pcmk__insert_dup(attrs, PCMK__XA_GRANTED, "false"); + + assert_int_equal(pcmk_ticket_set_attr(&xml, "ticketB", attrs, false), EACCES); + pcmk__assert_validates(xml); + free_xml(xml); + + cib = cib_new(); + cib->cmds->signon(cib, crm_system_name, cib_command); + cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"ticketB\"]", + &xml_search, cib_xpath | cib_scope_local); + + assert_string_equal("true", crm_element_value(xml_search, PCMK__XA_GRANTED)); + assert_null(crm_element_value(xml_search, PCMK_XA_LAST_GRANTED)); + + free_xml(xml_search); + g_hash_table_destroy(attrs); + cib__clean_up_connection(&cib); +} + +static void +granted_to_not_granted_with_force(void **state) { GHashTable *attrs = pcmk__strkey_table(free, free); xmlNode *xml = NULL; xmlNode *xml_search = NULL; cib_t *cib; pcmk__insert_dup(attrs, PCMK__XA_GRANTED, "false"); - assert_int_equal(pcmk_ticket_set_attr(&xml, "ticketB", attrs), pcmk_rc_ok); + assert_int_equal(pcmk_ticket_set_attr(&xml, "ticketB", attrs, true), pcmk_rc_ok); pcmk__assert_validates(xml); free_xml(xml); cib = cib_new(); cib->cmds->signon(cib, crm_system_name, cib_command); cib->cmds->query(cib, "//" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"ticketB\"]", &xml_search, cib_xpath | cib_scope_local); assert_string_equal("false", crm_element_value(xml_search, PCMK__XA_GRANTED)); assert_null(crm_element_value(xml_search, PCMK_XA_LAST_GRANTED)); free_xml(xml_search); g_hash_table_destroy(attrs); cib__clean_up_connection(&cib); } /* There are two kinds of tests in this file: * * (1) Those that test what happens if the CIB is not set up correctly, and * (2) Those that test what happens when run against a CIB. * * Therefore, we need two kinds of setup/teardown functions. We only do * minimal overall setup for the entire group, and then setup the CIB for * those tests that need it. */ PCMK__UNIT_TEST(pcmk__cib_test_setup_group, NULL, cmocka_unit_test(cib_not_connected), cmocka_unit_test_setup_teardown(bad_arguments, setup_test, teardown_test), cmocka_unit_test_setup_teardown(unknown_ticket_no_attrs, setup_test, teardown_test), cmocka_unit_test_setup_teardown(unknown_ticket_with_attrs, setup_test, teardown_test), cmocka_unit_test_setup_teardown(overwrite_existing_attr, setup_test, teardown_test), - cmocka_unit_test_setup_teardown(not_granted_to_granted, setup_test, teardown_test), - cmocka_unit_test_setup_teardown(granted_to_not_granted, setup_test, teardown_test)) + cmocka_unit_test_setup_teardown(not_granted_to_granted_without_force, setup_test, teardown_test), + cmocka_unit_test_setup_teardown(not_granted_to_granted_with_force, setup_test, teardown_test), + cmocka_unit_test_setup_teardown(granted_to_not_granted_without_force, setup_test, teardown_test), + cmocka_unit_test_setup_teardown(granted_to_not_granted_with_force, setup_test, teardown_test)) diff --git a/tools/crm_ticket.c b/tools/crm_ticket.c index 227c4ee6fd..4ed0c75ef8 100644 --- a/tools/crm_ticket.c +++ b/tools/crm_ticket.c @@ -1,683 +1,664 @@ /* * Copyright 2012-2024 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 GError *error = NULL; #define SUMMARY "Perform tasks related to cluster tickets\n\n" \ "Allows ticket attributes to be queried, modified and deleted." struct { gchar *attr_default; gchar *attr_id; char *attr_name; char *attr_value; gboolean force; char *get_attr_name; gboolean quiet; gchar *set_name; char ticket_cmd; gchar *ticket_id; gchar *xml_file; } options = { .ticket_cmd = 'S' }; GList *attr_delete; GHashTable *attr_set; bool modified = false; int cib_options = cib_sync_call; static pcmk__output_t *out = NULL; #define INDENT " " static pcmk__supported_format_t formats[] = { PCMK__SUPPORTED_FORMAT_NONE, PCMK__SUPPORTED_FORMAT_TEXT, PCMK__SUPPORTED_FORMAT_XML, { NULL, NULL, NULL } }; static gboolean attr_value_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { pcmk__str_update(&options.attr_value, optarg); if (!options.attr_name || !options.attr_value) { return TRUE; } pcmk__insert_dup(attr_set, options.attr_name, options.attr_value); pcmk__str_update(&options.attr_name, NULL); pcmk__str_update(&options.attr_value, NULL); modified = true; return TRUE; } static gboolean command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { if (pcmk__str_any_of(option_name, "--info", "-l", NULL)) { options.ticket_cmd = 'l'; } else if (pcmk__str_any_of(option_name, "--details", "-L", NULL)) { options.ticket_cmd = 'L'; } else if (pcmk__str_any_of(option_name, "--raw", "-w", NULL)) { options.ticket_cmd = 'w'; } else if (pcmk__str_any_of(option_name, "--query-xml", "-q", NULL)) { options.ticket_cmd = 'q'; } else if (pcmk__str_any_of(option_name, "--constraints", "-c", NULL)) { options.ticket_cmd = 'c'; } else if (pcmk__str_any_of(option_name, "--cleanup", "-C", NULL)) { options.ticket_cmd = 'C'; } return TRUE; } static gboolean delete_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { attr_delete = g_list_append(attr_delete, strdup(optarg)); modified = true; return TRUE; } static gboolean get_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { pcmk__str_update(&options.get_attr_name, optarg); options.ticket_cmd = 'G'; return TRUE; } static gboolean grant_standby_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { if (pcmk__str_any_of(option_name, "--grant", "-g", NULL)) { pcmk__insert_dup(attr_set, PCMK__XA_GRANTED, PCMK_VALUE_TRUE); modified = true; } else if (pcmk__str_any_of(option_name, "--revoke", "-r", NULL)) { pcmk__insert_dup(attr_set, PCMK__XA_GRANTED, PCMK_VALUE_FALSE); modified = true; } else if (pcmk__str_any_of(option_name, "--standby", "-s", NULL)) { pcmk__insert_dup(attr_set, PCMK_XA_STANDBY, PCMK_VALUE_TRUE); modified = true; } else if (pcmk__str_any_of(option_name, "--activate", "-a", NULL)) { pcmk__insert_dup(attr_set, PCMK_XA_STANDBY, PCMK_VALUE_FALSE); modified = true; } return TRUE; } static gboolean set_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { pcmk__str_update(&options.attr_name, optarg); if (!options.attr_name || !options.attr_value) { return TRUE; } pcmk__insert_dup(attr_set, options.attr_name, options.attr_value); pcmk__str_update(&options.attr_name, NULL); pcmk__str_update(&options.attr_value, NULL); modified = true; return TRUE; } static GOptionEntry query_entries[] = { { "info", 'l', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the information of ticket(s)", NULL }, { "details", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the details of ticket(s)", NULL }, { "raw", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the IDs of ticket(s)", NULL }, { "query-xml", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Query the XML of ticket(s)", NULL }, { "constraints", 'c', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the " PCMK_XE_RSC_TICKET " constraints that apply to ticket(s)", NULL }, { NULL } }; static GOptionEntry command_entries[] = { { "grant", 'g', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb, "Grant a ticket to this cluster site", NULL }, { "revoke", 'r', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb, "Revoke a ticket from this cluster site", NULL }, { "standby", 's', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb, "Tell this cluster site this ticket is standby", NULL }, { "activate", 'a', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb, "Tell this cluster site this ticket is active", NULL }, { NULL } }; static GOptionEntry advanced_entries[] = { { "get-attr", 'G', 0, G_OPTION_ARG_CALLBACK, get_attr_cb, "Display the named attribute for a ticket", "ATTRIBUTE" }, { "set-attr", 'S', 0, G_OPTION_ARG_CALLBACK, set_attr_cb, "Set the named attribute for a ticket", "ATTRIBUTE" }, { "delete-attr", 'D', 0, G_OPTION_ARG_CALLBACK, delete_attr_cb, "Delete the named attribute for a ticket", "ATTRIBUTE" }, { "cleanup", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Delete all state of a ticket at this cluster site", NULL }, { NULL} }; static GOptionEntry addl_entries[] = { { "attr-value", 'v', 0, G_OPTION_ARG_CALLBACK, attr_value_cb, "Attribute value to use with -S", "VALUE" }, { "default", 'd', 0, G_OPTION_ARG_STRING, &options.attr_default, "(Advanced) Default attribute value to display if none is found\n" INDENT "(for use with -G)", "VALUE" }, { "force", 'f', 0, G_OPTION_ARG_NONE, &options.force, "(Advanced) Force the action to be performed", NULL }, { "ticket", 't', 0, G_OPTION_ARG_STRING, &options.ticket_id, "Ticket ID", "ID" }, { "xml-file", 'x', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.xml_file, NULL, NULL }, { NULL } }; static GOptionEntry deprecated_entries[] = { { "set-name", 'n', 0, G_OPTION_ARG_STRING, &options.set_name, "(Advanced) ID of the " PCMK_XE_INSTANCE_ATTRIBUTES " object to change", "ID" }, { "nvpair", 'i', 0, G_OPTION_ARG_STRING, &options.attr_id, "(Advanced) ID of the nvpair object to change/delete", "ID" }, { "quiet", 'Q', 0, G_OPTION_ARG_NONE, &options.quiet, "Print only the value on stdout", NULL }, { NULL } }; static void ticket_grant_warning(gchar *ticket_id) { out->err(out, "This command cannot help you verify whether '%s' has " "been already granted elsewhere.\n" "If you really want to grant '%s' to this site now, and " "you know what you are doing,\n" "please specify --force.", ticket_id, ticket_id); } static void ticket_revoke_warning(gchar *ticket_id) { out->err(out, "Revoking '%s' can trigger the specified '" PCMK_XA_LOSS_POLICY "'(s) relating to '%s'.\n\n" "You can check that with:\n" "crm_ticket --ticket %s --constraints\n\n" "Otherwise before revoking '%s', you may want to make '%s'" "standby with:\n" "crm_ticket --ticket %s --standby\n\n" "If you really want to revoke '%s' from this site now, and " "you know what you are doing,\n" "please specify --force.", ticket_id, ticket_id, ticket_id, ticket_id, ticket_id, ticket_id, ticket_id); } -static bool -allow_modification(gchar *ticket_id) -{ - const char *value = NULL; - GList *list_iter = NULL; - - if (options.force) { - return true; - } - - if (g_hash_table_lookup_extended(attr_set, PCMK__XA_GRANTED, NULL, - (gpointer *) &value)) { - if (crm_is_true(value)) { - ticket_grant_warning(ticket_id); - return false; - - } else { - ticket_revoke_warning(ticket_id); - return false; - } - } - - for(list_iter = attr_delete; list_iter; list_iter = list_iter->next) { - const char *key = (const char *)list_iter->data; - - if (pcmk__str_eq(key, PCMK__XA_GRANTED, pcmk__str_none)) { - ticket_revoke_warning(ticket_id); - return false; - } - } - - return true; -} - static GOptionContext * build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) { GOptionContext *context = NULL; const char *description = "Examples:\n\n" "Display the info of tickets:\n\n" "\tcrm_ticket --info\n\n" "Display the detailed info of tickets:\n\n" "\tcrm_ticket --details\n\n" "Display the XML of 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --query-xml\n\n" "Display the " PCMK_XE_RSC_TICKET " constraints that apply to 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --constraints\n\n" "Grant 'ticketA' to this cluster site:\n\n" "\tcrm_ticket --ticket ticketA --grant\n\n" "Revoke 'ticketA' from this cluster site:\n\n" "\tcrm_ticket --ticket ticketA --revoke\n\n" "Make 'ticketA' standby (the cluster site will treat a granted\n" "'ticketA' as 'standby', and the dependent resources will be\n" "stopped or demoted gracefully without triggering loss-policies):\n\n" "\tcrm_ticket --ticket ticketA --standby\n\n" "Activate 'ticketA' from being standby:\n\n" "\tcrm_ticket --ticket ticketA --activate\n\n" "Get the value of the 'granted' attribute for 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --get-attr granted\n\n" "Set the value of the 'standby' attribute for 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --set-attr standby --attr-value true\n\n" "Delete the 'granted' attribute for 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --delete-attr granted\n\n" "Erase the operation history of 'ticketA' at this cluster site,\n" "causing the cluster site to 'forget' the existing ticket state:\n\n" "\tcrm_ticket --ticket ticketA --cleanup\n\n"; context = pcmk__build_arg_context(args, "text (default), xml", group, NULL); g_option_context_set_description(context, description); pcmk__add_arg_group(context, "queries", "Queries:", "Show queries", query_entries); pcmk__add_arg_group(context, "commands", "Commands:", "Show command options", command_entries); pcmk__add_arg_group(context, "advanced", "Advanced Options:", "Show advanced options", advanced_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; } int main(int argc, char **argv) { pcmk_scheduler_t *scheduler = NULL; xmlNode *cib_xml_copy = NULL; cib_t *cib_conn = NULL; crm_exit_t exit_code = CRM_EX_OK; int rc = pcmk_rc_ok; GOptionGroup *output_group = NULL; pcmk__common_args_t *args = NULL; GOptionContext *context = NULL; gchar **processed_args = NULL; attr_set = pcmk__strkey_table(free, free); attr_delete = NULL; args = pcmk__new_common_args(SUMMARY); context = build_arg_context(args, &output_group); processed_args = pcmk__cmdline_preproc(argv, "dintvxCDGS"); pcmk__register_formats(output_group, formats); if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; goto done; } pcmk__cli_init_logging("crm_ticket", args->verbosity); rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s", args->output_ty, pcmk_rc_str(rc)); goto done; } pe__register_messages(out); pcmk__register_lib_messages(out); if (args->version) { out->version(out, false); goto done; } scheduler = pe_new_working_set(); if (scheduler == NULL) { rc = errno; exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not allocate scheduler data: %s", pcmk_rc_str(rc)); goto done; } pcmk__set_scheduler_flags(scheduler, pcmk_sched_no_counts|pcmk_sched_no_compat); cib_conn = cib_new(); if (cib_conn == NULL) { exit_code = CRM_EX_DISCONNECT; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not connect to the CIB manager"); goto done; } rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not connect to the CIB: %s", pcmk_rc_str(rc)); goto done; } if (options.xml_file != NULL) { cib_xml_copy = pcmk__xml_read(options.xml_file); } else { rc = cib_conn->cmds->query(cib_conn, NULL, &cib_xml_copy, cib_scope_local | cib_sync_call); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not get local CIB: %s", pcmk_rc_str(rc)); goto done; } } if (!cli_config_update(&cib_xml_copy, NULL, FALSE)) { exit_code = CRM_EX_CONFIG; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not update local CIB to latest schema version"); goto done; } scheduler->input = cib_xml_copy; scheduler->now = crm_time_new(NULL); cluster_status(scheduler); /* For recording the tickets that are referenced in PCMK_XE_RSC_TICKET * constraints but have never been granted yet. */ pcmk__unpack_constraints(scheduler); if (options.ticket_cmd == 'l' || options.ticket_cmd == 'L' || options.ticket_cmd == 'w') { bool raw = false; bool details = false; if (options.ticket_cmd == 'L') { details = true; } else if (options.ticket_cmd == 'w') { raw = true; } rc = pcmk__ticket_info(out, scheduler, options.ticket_id, details, raw); exit_code = pcmk_rc2exitc(rc); if (rc == ENXIO) { g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "No such ticket '%s'", options.ticket_id); } else if (rc != pcmk_rc_ok) { g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not get ticket info: %s", pcmk_rc_str(rc)); } } else if (options.ticket_cmd == 'q') { rc = pcmk__ticket_state(out, cib_conn, options.ticket_id); if (rc != pcmk_rc_ok && rc != pcmk_rc_duplicate_id) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not query ticket XML: %s", pcmk_rc_str(rc)); } else { exit_code = CRM_EX_OK; } } else if (options.ticket_cmd == 'c') { rc = pcmk__ticket_constraints(out, cib_conn, options.ticket_id); exit_code = pcmk_rc2exitc(rc); if (rc != pcmk_rc_ok) { g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not show ticket constraints: %s", pcmk_rc_str(rc)); } } else if (options.ticket_cmd == 'G') { if (options.ticket_id == NULL) { exit_code = CRM_EX_NOSUCH; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply ticket ID with -t"); goto done; } rc = pcmk__ticket_get_attr(out, scheduler, options.ticket_id, options.get_attr_name, options.attr_default); exit_code = pcmk_rc2exitc(rc); } else if (options.ticket_cmd == 'C') { if (options.ticket_id == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply ticket ID with -t"); goto done; } rc = pcmk__ticket_delete(out, cib_conn, scheduler, options.ticket_id, options.force); exit_code = pcmk_rc2exitc(rc); switch (rc) { case ENXIO: g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "No such ticket '%s'", options.ticket_id); break; case EACCES: ticket_revoke_warning(options.ticket_id); break; case pcmk_rc_ok: case pcmk_rc_duplicate_id: break; default: g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not clean up ticket: %s", pcmk_rc_str(rc)); break; } } else if (modified) { if (options.ticket_id == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply ticket ID with -t"); goto done; } if (options.attr_value && (pcmk__str_empty(options.attr_name))) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply attribute name with -S for -v %s", options.attr_value); goto done; } if (options.attr_name && (pcmk__str_empty(options.attr_value))) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply attribute value with -v for -S %s", options.attr_value); goto done; } - if (!allow_modification(options.ticket_id)) { - exit_code = CRM_EX_INSUFFICIENT_PRIV; - g_set_error(&error, PCMK__EXITC_ERROR, exit_code, - "Ticket modification not allowed"); - goto done; - } - if (attr_delete != NULL) { rc = pcmk__ticket_remove_attr(out, cib_conn, scheduler, options.ticket_id, - attr_delete); + attr_delete, options.force); + + if (rc == EACCES) { + ticket_revoke_warning(options.ticket_id); + exit_code = pcmk_rc2exitc(rc); + g_set_error(&error, PCMK__EXITC_ERROR, exit_code, + "Ticket modification not allowed without --force"); + } } else { rc = pcmk__ticket_set_attr(out, cib_conn, scheduler, options.ticket_id, - attr_set); + attr_set, options.force); + + if (rc == EACCES) { + const char *value = NULL; + + value = g_hash_table_lookup(attr_set, PCMK__XA_GRANTED); + if (crm_is_true(value)) { + ticket_grant_warning(options.ticket_id); + } else { + ticket_revoke_warning(options.ticket_id); + } + + exit_code = pcmk_rc2exitc(rc); + g_set_error(&error, PCMK__EXITC_ERROR, exit_code, + "Ticket modification not allowed without --force"); + } } exit_code = pcmk_rc2exitc(rc); - if (rc != pcmk_rc_ok) { + if (rc != pcmk_rc_ok && error == NULL) { g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not modify ticket: %s", pcmk_rc_str(rc)); } } else if (options.ticket_cmd == 'S') { /* Correct usage was handled in the "if (modified)" block above, so * this is just for reporting usage errors */ if (pcmk__str_empty(options.attr_name)) { // We only get here if ticket_cmd was left as default exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply a command"); goto done; } if (options.ticket_id == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply ticket ID with -t"); goto done; } if (pcmk__str_empty(options.attr_value)) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply value with -v for -S %s", options.attr_name); goto done; } } else { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Unknown command: %c", options.ticket_cmd); } done: if (attr_set) { g_hash_table_destroy(attr_set); } attr_set = NULL; if (attr_delete) { g_list_free_full(attr_delete, free); } attr_delete = NULL; pe_free_working_set(scheduler); scheduler = NULL; cib__clean_up_connection(&cib_conn); g_strfreev(processed_args); pcmk__free_arg_context(context); g_free(options.attr_default); g_free(options.attr_id); free(options.attr_name); free(options.attr_value); free(options.get_attr_name); g_free(options.set_name); g_free(options.ticket_id); g_free(options.xml_file); pcmk__output_and_clear_error(&error, out); if (out != NULL) { out->finish(out, exit_code, true, NULL); pcmk__output_free(out); } pcmk__unregister_formats(); crm_exit(exit_code); }