Page MenuHomeClusterLabs Projects

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/daemons/controld/controld_cib.c b/daemons/controld/controld_cib.c
index e2a0d50179..47c6d6eb79 100644
--- a/daemons/controld/controld_cib.c
+++ b/daemons/controld/controld_cib.c
@@ -1,1061 +1,1061 @@
/*
* Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <unistd.h> /* sleep */
#include <crm/common/alerts_internal.h>
#include <crm/common/xml.h>
#include <crm/crm.h>
#include <crm/lrmd_internal.h>
#include <pacemaker-controld.h>
// Call ID of the most recent in-progress CIB resource update (or 0 if none)
static int pending_rsc_update = 0;
/*!
* \internal
* \brief Respond to a dropped CIB connection
*
* \param[in] user_data CIB connection that dropped
*/
static void
handle_cib_disconnect(gpointer user_data)
{
CRM_LOG_ASSERT(user_data == controld_globals.cib_conn);
controld_trigger_fsa();
controld_globals.cib_conn->state = cib_disconnected;
if (pcmk_is_set(controld_globals.fsa_input_register, R_CIB_CONNECTED)) {
// @TODO This should trigger a reconnect, not a shutdown
crm_crit("Lost connection to the CIB manager, shutting down");
register_fsa_input(C_FSA_INTERNAL, I_ERROR, NULL);
controld_clear_fsa_input_flags(R_CIB_CONNECTED);
} else { // Expected
crm_info("Disconnected from the CIB manager");
}
}
static void
do_cib_updated(const char *event, xmlNode * msg)
{
const xmlNode *patchset = NULL;
const char *client_name = NULL;
crm_debug("Received CIB diff notification: DC=%s", pcmk__btoa(AM_I_DC));
if (cib__get_notify_patchset(msg, &patchset) != pcmk_rc_ok) {
return;
}
if (pcmk__cib_element_in_patchset(patchset, PCMK_XE_ALERTS)
|| pcmk__cib_element_in_patchset(patchset, PCMK_XE_CRM_CONFIG)) {
controld_trigger_config();
}
if (!AM_I_DC) {
// We're not in control of the join sequence
return;
}
client_name = crm_element_value(msg, PCMK__XA_CIB_CLIENTNAME);
if (!cib__client_triggers_refresh(client_name)) {
// The CIB is still accurate
return;
}
if (pcmk__cib_element_in_patchset(patchset, PCMK_XE_NODES)
|| pcmk__cib_element_in_patchset(patchset, PCMK_XE_STATUS)) {
/* An unsafe client modified the PCMK_XE_NODES or PCMK_XE_STATUS
* section. Ensure the node list is up-to-date, and start the join
* process again so we get everyone's current resource history.
*/
if (client_name == NULL) {
client_name = crm_element_value(msg, PCMK__XA_CIB_CLIENTID);
}
crm_notice("Populating nodes and starting an election after %s event "
"triggered by %s",
event, pcmk__s(client_name, "(unidentified client)"));
populate_cib_nodes(controld_node_update_quick|controld_node_update_all,
__func__);
register_fsa_input(C_FSA_INTERNAL, I_ELECTION, NULL);
}
}
void
controld_disconnect_cib_manager(void)
{
cib_t *cib_conn = controld_globals.cib_conn;
pcmk__assert(cib_conn != NULL);
crm_debug("Disconnecting from the CIB manager");
controld_clear_fsa_input_flags(R_CIB_CONNECTED);
cib_conn->cmds->del_notify_callback(cib_conn, PCMK__VALUE_CIB_DIFF_NOTIFY,
do_cib_updated);
cib_free_callbacks(cib_conn);
if (cib_conn->state != cib_disconnected) {
cib_conn->cmds->set_secondary(cib_conn, cib_discard_reply);
cib_conn->cmds->signoff(cib_conn);
}
}
/* A_CIB_STOP, A_CIB_START, O_CIB_RESTART */
void
do_cib_control(long long action,
enum crmd_fsa_cause cause,
enum crmd_fsa_state cur_state,
enum crmd_fsa_input current_input, fsa_data_t * msg_data)
{
static int cib_retries = 0;
cib_t *cib_conn = controld_globals.cib_conn;
void (*dnotify_fn) (gpointer user_data) = handle_cib_disconnect;
void (*update_cb) (const char *event, xmlNodePtr msg) = do_cib_updated;
int rc = pcmk_ok;
pcmk__assert(cib_conn != NULL);
if (pcmk_is_set(action, A_CIB_STOP)) {
if ((cib_conn->state != cib_disconnected)
&& (pending_rsc_update != 0)) {
crm_info("Waiting for resource update %d to complete",
pending_rsc_update);
crmd_fsa_stall(FALSE);
return;
}
controld_disconnect_cib_manager();
}
if (!pcmk_is_set(action, A_CIB_START)) {
return;
}
if (cur_state == S_STOPPING) {
crm_err("Ignoring request to connect to the CIB manager after "
"shutdown");
return;
}
rc = cib_conn->cmds->signon(cib_conn, crm_system_name,
cib_command_nonblocking);
if (rc != pcmk_ok) {
// A short wait that usually avoids stalling the FSA
sleep(1);
rc = cib_conn->cmds->signon(cib_conn, crm_system_name,
cib_command_nonblocking);
}
if (rc != pcmk_ok) {
crm_info("Could not connect to the CIB manager: %s", pcmk_strerror(rc));
} else if (cib_conn->cmds->set_connection_dnotify(cib_conn,
dnotify_fn) != pcmk_ok) {
crm_err("Could not set dnotify callback");
} else if (cib_conn->cmds->add_notify_callback(cib_conn,
PCMK__VALUE_CIB_DIFF_NOTIFY,
update_cb) != pcmk_ok) {
crm_err("Could not set CIB notification callback (update)");
} else {
controld_set_fsa_input_flags(R_CIB_CONNECTED);
cib_retries = 0;
}
if (!pcmk_is_set(controld_globals.fsa_input_register, R_CIB_CONNECTED)) {
cib_retries++;
if (cib_retries < 30) {
crm_warn("Couldn't complete CIB registration %d times... "
"pause and retry", cib_retries);
controld_start_wait_timer();
crmd_fsa_stall(FALSE);
} else {
crm_err("Could not complete CIB registration %d times... "
"hard error", cib_retries);
register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL);
}
}
}
#define MIN_CIB_OP_TIMEOUT (30)
/*!
* \internal
* \brief Get the timeout (in seconds) that should be used with CIB operations
*
* \return The maximum of 30 seconds, the value of the PCMK_cib_timeout
* environment variable, or 10 seconds times one more than the number of
* nodes in the cluster.
*/
unsigned int
cib_op_timeout(void)
{
unsigned int calculated_timeout = 10U * (pcmk__cluster_num_active_nodes()
+ pcmk__cluster_num_remote_nodes()
+ 1U);
calculated_timeout = QB_MAX(calculated_timeout, MIN_CIB_OP_TIMEOUT);
crm_trace("Calculated timeout: %s",
pcmk__readable_interval(calculated_timeout * 1000));
if (controld_globals.cib_conn) {
controld_globals.cib_conn->call_timeout = calculated_timeout;
}
return calculated_timeout;
}
/*!
* \internal
* \brief Get CIB call options to use local scope if primary is unavailable
*
* \return CIB call options
*/
int
crmd_cib_smart_opt(void)
{
int call_opt = cib_none;
if ((controld_globals.fsa_state == S_ELECTION)
|| (controld_globals.fsa_state == S_PENDING)) {
crm_info("Sending update to local CIB in state: %s",
fsa_state2string(controld_globals.fsa_state));
cib__set_call_options(call_opt, "update", cib_none);
}
return call_opt;
}
static void
cib_delete_callback(xmlNode *msg, int call_id, int rc, xmlNode *output,
void *user_data)
{
char *desc = user_data;
if (rc == 0) {
crm_debug("Deletion of %s (via CIB call %d) succeeded", desc, call_id);
} else {
crm_warn("Deletion of %s (via CIB call %d) failed: %s " QB_XS " rc=%d",
desc, call_id, pcmk_strerror(rc), rc);
}
}
// Searches for various portions of PCMK__XE_NODE_STATE to delete
// Match a particular node's PCMK__XE_NODE_STATE (takes node name 1x)
#define XPATH_NODE_STATE "//" PCMK__XE_NODE_STATE "[@" PCMK_XA_UNAME "='%s']"
// Node's lrm section (name 1x)
#define XPATH_NODE_LRM XPATH_NODE_STATE "/" PCMK__XE_LRM
/* Node's PCMK__XE_LRM_RSC_OP entries and PCMK__XE_LRM_RESOURCE entries without
* unexpired lock
* (name 2x, (seconds_since_epoch - PCMK_OPT_SHUTDOWN_LOCK_LIMIT) 1x)
*/
#define XPATH_NODE_LRM_UNLOCKED XPATH_NODE_STATE "//" PCMK__XE_LRM_RSC_OP \
"|" XPATH_NODE_STATE \
"//" PCMK__XE_LRM_RESOURCE \
"[not(@" PCMK_OPT_SHUTDOWN_LOCK ") " \
"or " PCMK_OPT_SHUTDOWN_LOCK "<%lld]"
// Node's PCMK__XE_TRANSIENT_ATTRIBUTES section (name 1x)
#define XPATH_NODE_ATTRS XPATH_NODE_STATE "/" PCMK__XE_TRANSIENT_ATTRIBUTES
// Everything under PCMK__XE_NODE_STATE (name 1x)
#define XPATH_NODE_ALL XPATH_NODE_STATE "/*"
/* Unlocked history + transient attributes
* (name 2x, (seconds_since_epoch - PCMK_OPT_SHUTDOWN_LOCK_LIMIT) 1x, name 1x)
*/
#define XPATH_NODE_ALL_UNLOCKED XPATH_NODE_LRM_UNLOCKED "|" XPATH_NODE_ATTRS
/*!
* \internal
* \brief Get the XPath and description of a node state section to be deleted
*
* \param[in] uname Desired node
* \param[in] section Subsection of \c PCMK__XE_NODE_STATE to be deleted
* \param[out] xpath Where to store XPath of \p section
* \param[out] desc If not \c NULL, where to store description of \p section
*/
void
controld_node_state_deletion_strings(const char *uname,
enum controld_section_e section,
char **xpath, char **desc)
{
const char *desc_pre = NULL;
// Shutdown locks that started before this time are expired
long long expire = (long long) time(NULL)
- controld_globals.shutdown_lock_limit;
switch (section) {
case controld_section_lrm:
*xpath = crm_strdup_printf(XPATH_NODE_LRM, uname);
desc_pre = "resource history";
break;
case controld_section_lrm_unlocked:
*xpath = crm_strdup_printf(XPATH_NODE_LRM_UNLOCKED,
uname, uname, expire);
desc_pre = "resource history (other than shutdown locks)";
break;
case controld_section_attrs:
*xpath = crm_strdup_printf(XPATH_NODE_ATTRS, uname);
desc_pre = "transient attributes";
break;
case controld_section_all:
*xpath = crm_strdup_printf(XPATH_NODE_ALL, uname);
desc_pre = "all state";
break;
case controld_section_all_unlocked:
*xpath = crm_strdup_printf(XPATH_NODE_ALL_UNLOCKED,
uname, uname, expire, uname);
desc_pre = "all state (other than shutdown locks)";
break;
default:
// We called this function incorrectly
pcmk__assert(false);
break;
}
if (desc != NULL) {
*desc = crm_strdup_printf("%s for node %s", desc_pre, uname);
}
}
/*!
* \internal
* \brief Delete subsection of a node's CIB \c PCMK__XE_NODE_STATE
*
* \param[in] uname Desired node
* \param[in] section Subsection of \c PCMK__XE_NODE_STATE to delete
* \param[in] options CIB call options to use
*/
void
controld_delete_node_state(const char *uname, enum controld_section_e section,
int options)
{
cib_t *cib = controld_globals.cib_conn;
char *xpath = NULL;
char *desc = NULL;
int cib_rc = pcmk_ok;
pcmk__assert((uname != NULL) && (cib != NULL));
controld_node_state_deletion_strings(uname, section, &xpath, &desc);
cib__set_call_options(options, "node state deletion",
cib_xpath|cib_multiple);
cib_rc = cib->cmds->remove(cib, xpath, NULL, options);
fsa_register_cib_callback(cib_rc, desc, cib_delete_callback);
crm_info("Deleting %s (via CIB call %d) " QB_XS " xpath=%s",
desc, cib_rc, xpath);
// CIB library handles freeing desc
free(xpath);
}
// Takes node name and resource ID
#define XPATH_RESOURCE_HISTORY "//" PCMK__XE_NODE_STATE \
"[@" PCMK_XA_UNAME "='%s']/" \
PCMK__XE_LRM "/" PCMK__XE_LRM_RESOURCES \
"/" PCMK__XE_LRM_RESOURCE \
"[@" PCMK_XA_ID "='%s']"
// @TODO could add "and @PCMK_OPT_SHUTDOWN_LOCK" to limit to locks
/*!
* \internal
* \brief Clear resource history from CIB for a given resource and node
*
* \param[in] rsc_id ID of resource to be cleared
* \param[in] node Node whose resource history should be cleared
* \param[in] user_name ACL user name to use
* \param[in] call_options CIB call options
*
* \return Standard Pacemaker return code
*/
int
controld_delete_resource_history(const char *rsc_id, const char *node,
const char *user_name, int call_options)
{
char *desc = NULL;
char *xpath = NULL;
int rc = pcmk_rc_ok;
cib_t *cib = controld_globals.cib_conn;
CRM_CHECK((rsc_id != NULL) && (node != NULL), return EINVAL);
desc = crm_strdup_printf("resource history for %s on %s", rsc_id, node);
if (cib == NULL) {
crm_err("Unable to clear %s: no CIB connection", desc);
free(desc);
return ENOTCONN;
}
// Ask CIB to delete the entry
xpath = crm_strdup_printf(XPATH_RESOURCE_HISTORY, node, rsc_id);
cib->cmds->set_user(cib, user_name);
rc = cib->cmds->remove(cib, xpath, NULL, call_options|cib_xpath);
cib->cmds->set_user(cib, NULL);
if (rc < 0) {
rc = pcmk_legacy2rc(rc);
crm_err("Could not delete resource status of %s on %s%s%s: %s "
QB_XS " rc=%d", rsc_id, node,
(user_name? " for user " : ""), (user_name? user_name : ""),
pcmk_rc_str(rc), rc);
free(desc);
free(xpath);
return rc;
}
if (pcmk_is_set(call_options, cib_sync_call)) {
if (pcmk_is_set(call_options, cib_dryrun)) {
crm_debug("Deletion of %s would succeed", desc);
} else {
crm_debug("Deletion of %s succeeded", desc);
}
free(desc);
} else {
crm_info("Clearing %s (via CIB call %d) " QB_XS " xpath=%s",
desc, rc, xpath);
fsa_register_cib_callback(rc, desc, cib_delete_callback);
// CIB library handles freeing desc
}
free(xpath);
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Build XML and string of parameters meeting some criteria, for digest
*
* \param[in] op Executor event with parameter table to use
* \param[in] metadata Parsed meta-data for executed resource agent
* \param[in] param_type Flag used for selection criteria
* \param[out] result Will be set to newly created XML with selected
* parameters as attributes
*
* \return Newly allocated space-separated string of parameter names
* \note Selection criteria varies by param_type: for the restart digest, we
* want parameters that are *not* marked reloadable (OCF 1.1) or that
* *are* marked unique (pre-1.1), for both string and XML results; for the
* secure digest, we want parameters that *are* marked private for the
* string, but parameters that are *not* marked private for the XML.
* \note It is the caller's responsibility to free the string return value with
* \p g_string_free() and the XML result with \p pcmk__xml_free().
*/
static GString *
build_parameter_list(const lrmd_event_data_t *op,
const struct ra_metadata_s *metadata,
enum ra_param_flags_e param_type, xmlNode **result)
{
GString *list = NULL;
*result = pcmk__xe_create(NULL, PCMK_XE_PARAMETERS);
/* Consider all parameters only except private ones to be consistent with
* what scheduler does with calculate_secure_digest().
*/
if (param_type == ra_param_private
&& compare_version(controld_globals.dc_version, "3.16.0") >= 0) {
g_hash_table_foreach(op->params, hash2field, *result);
pcmk__filter_op_for_digest(*result);
}
for (GList *iter = metadata->ra_params; iter != NULL; iter = iter->next) {
struct ra_param_s *param = (struct ra_param_s *) iter->data;
bool accept_for_list = false;
bool accept_for_xml = false;
switch (param_type) {
case ra_param_reloadable:
accept_for_list = !pcmk_is_set(param->rap_flags, param_type);
accept_for_xml = accept_for_list;
break;
case ra_param_unique:
accept_for_list = pcmk_is_set(param->rap_flags, param_type);
accept_for_xml = accept_for_list;
break;
case ra_param_private:
accept_for_list = pcmk_is_set(param->rap_flags, param_type);
accept_for_xml = !accept_for_list;
break;
}
if (accept_for_list) {
crm_trace("Attr %s is %s", param->rap_name, ra_param_flag2text(param_type));
if (list == NULL) {
// We will later search for " WORD ", so start list with a space
pcmk__add_word(&list, 256, " ");
}
pcmk__add_word(&list, 0, param->rap_name);
} else {
crm_trace("Rejecting %s for %s", param->rap_name, ra_param_flag2text(param_type));
}
if (accept_for_xml) {
const char *v = g_hash_table_lookup(op->params, param->rap_name);
if (v != NULL) {
crm_trace("Adding attr %s=%s to the xml result", param->rap_name, v);
crm_xml_add(*result, param->rap_name, v);
}
} else {
crm_trace("Removing attr %s from the xml result", param->rap_name);
pcmk__xe_remove_attr(*result, param->rap_name);
}
}
if (list != NULL) {
// We will later search for " WORD ", so end list with a space
pcmk__add_word(&list, 0, " ");
}
return list;
}
static void
append_restart_list(lrmd_event_data_t *op, struct ra_metadata_s *metadata,
xmlNode *update, const char *version)
{
GString *list = NULL;
char *digest = NULL;
xmlNode *restart = NULL;
CRM_LOG_ASSERT(op->params != NULL);
if (op->interval_ms > 0) {
/* monitors are not reloadable */
return;
}
if (pcmk_is_set(metadata->ra_flags, ra_supports_reload_agent)) {
/* Add parameters not marked reloadable to the PCMK__XA_OP_FORCE_RESTART
* list
*/
list = build_parameter_list(op, metadata, ra_param_reloadable,
&restart);
} else if (pcmk_is_set(metadata->ra_flags, ra_supports_legacy_reload)) {
/* @COMPAT pre-OCF-1.1 resource agents
*
* Before OCF 1.1, Pacemaker abused "unique=0" to indicate
* reloadability. Add any parameters with unique="1" to the
* PCMK__XA_OP_FORCE_RESTART list.
*/
list = build_parameter_list(op, metadata, ra_param_unique, &restart);
} else {
// Resource does not support agent reloads
return;
}
- digest = pcmk__digest_operation(restart);
+ digest = pcmk__digest_op_params(restart);
/* Add PCMK__XA_OP_FORCE_RESTART and PCMK__XA_OP_RESTART_DIGEST to indicate
* the resource supports reload, no matter if it actually supports any
* reloadable parameters
*/
crm_xml_add(update, PCMK__XA_OP_FORCE_RESTART,
(list == NULL)? "" : (const char *) list->str);
crm_xml_add(update, PCMK__XA_OP_RESTART_DIGEST, digest);
if ((list != NULL) && (list->len > 0)) {
crm_trace("%s: %s, %s", op->rsc_id, digest, (const char *) list->str);
} else {
crm_trace("%s: %s", op->rsc_id, digest);
}
if (list != NULL) {
g_string_free(list, TRUE);
}
pcmk__xml_free(restart);
free(digest);
}
static void
append_secure_list(lrmd_event_data_t *op, struct ra_metadata_s *metadata,
xmlNode *update, const char *version)
{
GString *list = NULL;
char *digest = NULL;
xmlNode *secure = NULL;
CRM_LOG_ASSERT(op->params != NULL);
/* To keep PCMK__XA_OP_SECURE_PARAMS short, we want it to contain the secure
* parameters but PCMK__XA_OP_SECURE_DIGEST to be based on the insecure ones
*/
list = build_parameter_list(op, metadata, ra_param_private, &secure);
if (list != NULL) {
- digest = pcmk__digest_operation(secure);
+ digest = pcmk__digest_op_params(secure);
crm_xml_add(update, PCMK__XA_OP_SECURE_PARAMS,
(const char *) list->str);
crm_xml_add(update, PCMK__XA_OP_SECURE_DIGEST, digest);
crm_trace("%s: %s, %s", op->rsc_id, digest, (const char *) list->str);
g_string_free(list, TRUE);
} else {
crm_trace("%s: no secure parameters", op->rsc_id);
}
pcmk__xml_free(secure);
free(digest);
}
/*!
* \internal
* \brief Create XML for a resource history entry
*
* \param[in] func Function name of caller
* \param[in,out] parent XML to add entry to
* \param[in] rsc Affected resource
* \param[in,out] op Action to add an entry for (or NULL to do nothing)
* \param[in] node_name Node where action occurred
*/
void
controld_add_resource_history_xml_as(const char *func, xmlNode *parent,
const lrmd_rsc_info_t *rsc,
lrmd_event_data_t *op,
const char *node_name)
{
int target_rc = 0;
xmlNode *xml_op = NULL;
struct ra_metadata_s *metadata = NULL;
const char *caller_version = NULL;
lrm_state_t *lrm_state = NULL;
if (op == NULL) {
return;
}
target_rc = rsc_op_expected_rc(op);
caller_version = g_hash_table_lookup(op->params, PCMK_XA_CRM_FEATURE_SET);
CRM_CHECK(caller_version != NULL, caller_version = CRM_FEATURE_SET);
xml_op = pcmk__create_history_xml(parent, op, caller_version, target_rc,
controld_globals.cluster->priv->node_name,
func);
if (xml_op == NULL) {
return;
}
if ((rsc == NULL) || (op->params == NULL)
|| !crm_op_needs_metadata(rsc->standard, op->op_type)) {
crm_trace("No digests needed for %s action on %s (params=%p rsc=%p)",
op->op_type, op->rsc_id, op->params, rsc);
return;
}
lrm_state = controld_get_executor_state(node_name, false);
if (lrm_state == NULL) {
crm_warn("Cannot calculate digests for operation " PCMK__OP_FMT
" because we have no connection to executor for %s",
op->rsc_id, op->op_type, op->interval_ms, node_name);
return;
}
/* Ideally the metadata is cached, and the agent is just a fallback.
*
* @TODO Go through all callers and ensure they get metadata asynchronously
* first.
*/
metadata = controld_get_rsc_metadata(lrm_state, rsc,
controld_metadata_from_agent
|controld_metadata_from_cache);
if (metadata == NULL) {
return;
}
crm_trace("Including additional digests for %s:%s:%s",
rsc->standard, rsc->provider, rsc->type);
append_restart_list(op, metadata, xml_op, caller_version);
append_secure_list(op, metadata, xml_op, caller_version);
return;
}
/*!
* \internal
* \brief Record an action as pending in the CIB, if appropriate
*
* \param[in] node_name Node where the action is pending
* \param[in] rsc Resource that action is for
* \param[in,out] op Pending action
*
* \return true if action was recorded in CIB, otherwise false
*/
bool
controld_record_pending_op(const char *node_name, const lrmd_rsc_info_t *rsc,
lrmd_event_data_t *op)
{
const char *record_pending = NULL;
CRM_CHECK((node_name != NULL) && (rsc != NULL) && (op != NULL),
return false);
// Never record certain operation types as pending
if ((op->op_type == NULL) || (op->params == NULL)
|| !controld_action_is_recordable(op->op_type)) {
return false;
}
// Check action's PCMK_META_RECORD_PENDING meta-attribute (defaults to true)
record_pending = crm_meta_value(op->params, PCMK_META_RECORD_PENDING);
if ((record_pending != NULL) && !crm_is_true(record_pending)) {
pcmk__warn_once(pcmk__wo_record_pending,
"The " PCMK_META_RECORD_PENDING " option (for example, "
"for the %s resource's %s operation) is deprecated and "
"will be removed in a future release",
rsc->id, op->op_type);
return false;
}
op->call_id = -1;
op->t_run = time(NULL);
op->t_rcchange = op->t_run;
lrmd__set_result(op, PCMK_OCF_UNKNOWN, PCMK_EXEC_PENDING, NULL);
crm_debug("Recording pending %s-interval %s for %s on %s in the CIB",
pcmk__readable_interval(op->interval_ms), op->op_type, op->rsc_id,
node_name);
controld_update_resource_history(node_name, rsc, op, 0);
return true;
}
static void
cib_rsc_callback(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data)
{
switch (rc) {
case pcmk_ok:
case -pcmk_err_diff_failed:
case -pcmk_err_diff_resync:
crm_trace("Resource history update completed (call=%d rc=%d)",
call_id, rc);
break;
default:
if (call_id > 0) {
crm_warn("Resource history update %d failed: %s "
QB_XS " rc=%d", call_id, pcmk_strerror(rc), rc);
} else {
crm_warn("Resource history update failed: %s " QB_XS " rc=%d",
pcmk_strerror(rc), rc);
}
}
if (call_id == pending_rsc_update) {
pending_rsc_update = 0;
controld_trigger_fsa();
}
}
/* Only successful stops, and probes that found the resource inactive, get locks
* recorded in the history. This ensures the resource stays locked to the node
* until it is active there again after the node comes back up.
*/
static bool
should_preserve_lock(lrmd_event_data_t *op)
{
if (!pcmk_is_set(controld_globals.flags, controld_shutdown_lock_enabled)) {
return false;
}
if (!strcmp(op->op_type, PCMK_ACTION_STOP) && (op->rc == PCMK_OCF_OK)) {
return true;
}
if (!strcmp(op->op_type, PCMK_ACTION_MONITOR)
&& (op->rc == PCMK_OCF_NOT_RUNNING)) {
return true;
}
return false;
}
/*!
* \internal
* \brief Request a CIB update
*
* \param[in] section Section of CIB to update
* \param[in] data New XML of CIB section to update
* \param[in] options CIB call options
* \param[in] callback If not \c NULL, set this as the operation callback
*
* \return Standard Pacemaker return code
*
* \note If \p callback is \p cib_rsc_callback(), the CIB update's call ID is
* stored in \p pending_rsc_update on success.
*/
int
controld_update_cib(const char *section, xmlNode *data, int options,
void (*callback)(xmlNode *, int, int, xmlNode *, void *))
{
cib_t *cib = controld_globals.cib_conn;
int cib_rc = -ENOTCONN;
pcmk__assert(data != NULL);
if (cib != NULL) {
cib_rc = cib->cmds->modify(cib, section, data, options);
if (cib_rc >= 0) {
crm_debug("Submitted CIB update %d for %s section",
cib_rc, section);
}
}
if (callback == NULL) {
if (cib_rc < 0) {
crm_err("Failed to update CIB %s section: %s",
section, pcmk_rc_str(pcmk_legacy2rc(cib_rc)));
}
} else {
if ((cib_rc >= 0) && (callback == cib_rsc_callback)) {
/* Checking for a particular callback is a little hacky, but it
* didn't seem worth adding an output argument for cib_rc for just
* one use case.
*/
pending_rsc_update = cib_rc;
}
fsa_register_cib_callback(cib_rc, NULL, callback);
}
return (cib_rc >= 0)? pcmk_rc_ok : pcmk_legacy2rc(cib_rc);
}
/*!
* \internal
* \brief Update resource history entry in CIB
*
* \param[in] node_name Node where action occurred
* \param[in] rsc Resource that action is for
* \param[in,out] op Action to record
* \param[in] lock_time If nonzero, when resource was locked to node
*
* \note On success, the CIB update's call ID will be stored in
* pending_rsc_update.
*/
void
controld_update_resource_history(const char *node_name,
const lrmd_rsc_info_t *rsc,
lrmd_event_data_t *op, time_t lock_time)
{
xmlNode *update = NULL;
xmlNode *xml = NULL;
int call_opt = crmd_cib_smart_opt();
const char *node_id = NULL;
const char *container = NULL;
CRM_CHECK((node_name != NULL) && (op != NULL), return);
if (rsc == NULL) {
crm_warn("Resource %s no longer exists in the executor", op->rsc_id);
controld_ack_event_directly(NULL, NULL, rsc, op, op->rsc_id);
return;
}
// <status>
update = pcmk__xe_create(NULL, PCMK_XE_STATUS);
// <node_state ...>
xml = pcmk__xe_create(update, PCMK__XE_NODE_STATE);
if (controld_is_local_node(node_name)) {
node_id = controld_globals.our_uuid;
} else {
node_id = node_name;
pcmk__xe_set_bool_attr(xml, PCMK_XA_REMOTE_NODE, true);
}
crm_xml_add(xml, PCMK_XA_ID, node_id);
crm_xml_add(xml, PCMK_XA_UNAME, node_name);
crm_xml_add(xml, PCMK_XA_CRM_DEBUG_ORIGIN, __func__);
// <lrm ...>
xml = pcmk__xe_create(xml, PCMK__XE_LRM);
crm_xml_add(xml, PCMK_XA_ID, node_id);
// <lrm_resources>
xml = pcmk__xe_create(xml, PCMK__XE_LRM_RESOURCES);
// <lrm_resource ...>
xml = pcmk__xe_create(xml, PCMK__XE_LRM_RESOURCE);
crm_xml_add(xml, PCMK_XA_ID, op->rsc_id);
crm_xml_add(xml, PCMK_XA_CLASS, rsc->standard);
crm_xml_add(xml, PCMK_XA_PROVIDER, rsc->provider);
crm_xml_add(xml, PCMK_XA_TYPE, rsc->type);
if (lock_time != 0) {
/* Actions on a locked resource should either preserve the lock by
* recording it with the action result, or clear it.
*/
if (!should_preserve_lock(op)) {
lock_time = 0;
}
crm_xml_add_ll(xml, PCMK_OPT_SHUTDOWN_LOCK, (long long) lock_time);
}
if (op->params != NULL) {
container = g_hash_table_lookup(op->params,
CRM_META "_" PCMK__META_CONTAINER);
if (container != NULL) {
crm_trace("Resource %s is a part of container resource %s",
op->rsc_id, container);
crm_xml_add(xml, PCMK__META_CONTAINER, container);
}
}
// <lrm_resource_op ...> (possibly more than one)
controld_add_resource_history_xml(xml, rsc, op, node_name);
/* Update CIB asynchronously. Even if it fails, the resource state should be
* discovered during the next election. Worst case, the node is wrongly
* fenced for running a resource it isn't.
*/
crm_log_xml_trace(update, __func__);
controld_update_cib(PCMK_XE_STATUS, update, call_opt, cib_rsc_callback);
pcmk__xml_free(update);
}
/*!
* \internal
* \brief Erase an LRM history entry from the CIB, given the operation data
*
* \param[in] op Operation whose history should be deleted
*/
void
controld_delete_action_history(const lrmd_event_data_t *op)
{
xmlNode *xml_top = NULL;
CRM_CHECK(op != NULL, return);
xml_top = pcmk__xe_create(NULL, PCMK__XE_LRM_RSC_OP);
crm_xml_add_int(xml_top, PCMK__XA_CALL_ID, op->call_id);
crm_xml_add(xml_top, PCMK__XA_TRANSITION_KEY, op->user_data);
if (op->interval_ms > 0) {
char *op_id = pcmk__op_key(op->rsc_id, op->op_type, op->interval_ms);
/* Avoid deleting last_failure too (if it was a result of this recurring op failing) */
crm_xml_add(xml_top, PCMK_XA_ID, op_id);
free(op_id);
}
crm_debug("Erasing resource operation history for " PCMK__OP_FMT " (call=%d)",
op->rsc_id, op->op_type, op->interval_ms, op->call_id);
controld_globals.cib_conn->cmds->remove(controld_globals.cib_conn,
PCMK_XE_STATUS, xml_top, cib_none);
crm_log_xml_trace(xml_top, "op:cancel");
pcmk__xml_free(xml_top);
}
/* Define xpath to find LRM resource history entry by node and resource */
#define XPATH_HISTORY \
"/" PCMK_XE_CIB "/" PCMK_XE_STATUS \
"/" PCMK__XE_NODE_STATE "[@" PCMK_XA_UNAME "='%s']" \
"/" PCMK__XE_LRM "/" PCMK__XE_LRM_RESOURCES \
"/" PCMK__XE_LRM_RESOURCE "[@" PCMK_XA_ID "='%s']" \
"/" PCMK__XE_LRM_RSC_OP
/* ... and also by operation key */
#define XPATH_HISTORY_ID XPATH_HISTORY "[@" PCMK_XA_ID "='%s']"
/* ... and also by operation key and operation call ID */
#define XPATH_HISTORY_CALL XPATH_HISTORY \
"[@" PCMK_XA_ID "='%s' and @" PCMK__XA_CALL_ID "='%d']"
/* ... and also by operation key and original operation key */
#define XPATH_HISTORY_ORIG XPATH_HISTORY \
"[@" PCMK_XA_ID "='%s' and @" PCMK__XA_OPERATION_KEY "='%s']"
/*!
* \internal
* \brief Delete a last_failure resource history entry from the CIB
*
* \param[in] rsc_id Name of resource to clear history for
* \param[in] node Name of node to clear history for
* \param[in] action If specified, delete only if this was failed action
* \param[in] interval_ms If \p action is specified, it has this interval
*/
void
controld_cib_delete_last_failure(const char *rsc_id, const char *node,
const char *action, guint interval_ms)
{
char *xpath = NULL;
char *last_failure_key = NULL;
CRM_CHECK((rsc_id != NULL) && (node != NULL), return);
// Generate XPath to match desired entry
last_failure_key = pcmk__op_key(rsc_id, "last_failure", 0);
if (action == NULL) {
xpath = crm_strdup_printf(XPATH_HISTORY_ID, node, rsc_id,
last_failure_key);
} else {
char *action_key = pcmk__op_key(rsc_id, action, interval_ms);
xpath = crm_strdup_printf(XPATH_HISTORY_ORIG, node, rsc_id,
last_failure_key, action_key);
free(action_key);
}
free(last_failure_key);
controld_globals.cib_conn->cmds->remove(controld_globals.cib_conn, xpath,
NULL, cib_xpath);
free(xpath);
}
/*!
* \internal
* \brief Delete resource history entry from the CIB, given operation key
*
* \param[in] rsc_id Name of resource to clear history for
* \param[in] node Name of node to clear history for
* \param[in] key Operation key of operation to clear history for
* \param[in] call_id If specified, delete entry only if it has this call ID
*/
void
controld_delete_action_history_by_key(const char *rsc_id, const char *node,
const char *key, int call_id)
{
char *xpath = NULL;
CRM_CHECK((rsc_id != NULL) && (node != NULL) && (key != NULL), return);
if (call_id > 0) {
xpath = crm_strdup_printf(XPATH_HISTORY_CALL, node, rsc_id, key,
call_id);
} else {
xpath = crm_strdup_printf(XPATH_HISTORY_ID, node, rsc_id, key);
}
controld_globals.cib_conn->cmds->remove(controld_globals.cib_conn, xpath,
NULL, cib_xpath);
free(xpath);
}
diff --git a/daemons/controld/controld_te_callbacks.c b/daemons/controld/controld_te_callbacks.c
index b74da0ffb5..c78ec37037 100644
--- a/daemons/controld/controld_te_callbacks.c
+++ b/daemons/controld/controld_te_callbacks.c
@@ -1,527 +1,527 @@
/*
* Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <sys/stat.h>
#include <crm/crm.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
#include <libxml/xpath.h> // xmlXPathObject, etc.
#include <pacemaker-controld.h>
// An explicit PCMK_OPT_SHUTDOWN_LOCK of 0 means the lock has been cleared
static bool
shutdown_lock_cleared(xmlNode *lrm_resource)
{
time_t shutdown_lock = 0;
return (crm_element_value_epoch(lrm_resource, PCMK_OPT_SHUTDOWN_LOCK,
&shutdown_lock) == pcmk_ok)
&& (shutdown_lock == 0);
}
static void
process_lrm_resource_diff(xmlNode *lrm_resource, const char *node)
{
for (xmlNode *rsc_op = pcmk__xe_first_child(lrm_resource, NULL, NULL, NULL);
rsc_op != NULL; rsc_op = pcmk__xe_next(rsc_op, NULL)) {
process_graph_event(rsc_op, node);
}
if (shutdown_lock_cleared(lrm_resource)) {
// @TODO would be more efficient to abort once after transition done
abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart,
"Shutdown lock cleared", lrm_resource);
}
}
static void
process_resource_updates(const char *node, xmlNode *xml, xmlNode *change,
const char *op, const char *xpath)
{
xmlNode *rsc = NULL;
if (xml == NULL) {
return;
}
if (pcmk__xe_is(xml, PCMK__XE_LRM)) {
xml = pcmk__xe_first_child(xml, PCMK__XE_LRM_RESOURCES, NULL, NULL);
CRM_CHECK(xml != NULL, return);
}
CRM_CHECK(pcmk__xe_is(xml, PCMK__XE_LRM_RESOURCES), return);
/*
* Updates by, or in response to, TE actions will never contain updates
* for more than one resource at a time, so such updates indicate an
* LRM refresh.
*
* In that case, start a new transition rather than check each result
* individually, which can result in _huge_ speedups in large clusters.
*
* Unfortunately, we can only do so when there are no pending actions.
* Otherwise, we could mistakenly throw away those results here, and
* the cluster will stall waiting for them and time out the operation.
*/
if ((controld_globals.transition_graph->pending == 0)
&& (xml->children != NULL) && (xml->children->next != NULL)) {
crm_log_xml_trace(change, "lrm-refresh");
abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart,
"History refresh", NULL);
return;
}
for (rsc = pcmk__xe_first_child(xml, NULL, NULL, NULL); rsc != NULL;
rsc = pcmk__xe_next(rsc, NULL)) {
crm_trace("Processing %s", pcmk__xe_id(rsc));
process_lrm_resource_diff(rsc, node);
}
}
static char *extract_node_uuid(const char *xpath)
{
char *mutable_path = pcmk__str_copy(xpath);
char *node_uuid = NULL;
char *search = NULL;
char *match = NULL;
match = strstr(mutable_path, PCMK__XE_NODE_STATE "[@" PCMK_XA_ID "=\'");
if (match == NULL) {
free(mutable_path);
return NULL;
}
match += strlen(PCMK__XE_NODE_STATE "[@" PCMK_XA_ID "=\'");
search = strchr(match, '\'');
if (search == NULL) {
free(mutable_path);
return NULL;
}
search[0] = 0;
node_uuid = pcmk__str_copy(match);
free(mutable_path);
return node_uuid;
}
static void
abort_unless_down(const char *xpath, const char *op, xmlNode *change,
const char *reason)
{
char *node_uuid = NULL;
pcmk__graph_action_t *down = NULL;
if (!pcmk__str_eq(op, PCMK_VALUE_DELETE, pcmk__str_none)) {
abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart, reason,
change);
return;
}
node_uuid = extract_node_uuid(xpath);
if(node_uuid == NULL) {
crm_err("Could not extract node ID from %s", xpath);
abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart, reason,
change);
return;
}
down = match_down_event(node_uuid);
if (down == NULL) {
crm_trace("Not expecting %s to be down (%s)", node_uuid, xpath);
abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart, reason,
change);
} else {
crm_trace("Expecting changes to %s (%s)", node_uuid, xpath);
}
free(node_uuid);
}
static void
process_op_deletion(const char *xpath, xmlNode *change)
{
char *mutable_key = pcmk__str_copy(xpath);
char *key;
char *node_uuid;
// Extract the part of xpath between last pair of single quotes
key = strrchr(mutable_key, '\'');
if (key != NULL) {
*key = '\0';
key = strrchr(mutable_key, '\'');
}
if (key == NULL) {
crm_warn("Ignoring malformed CIB update (resource deletion of %s)",
xpath);
free(mutable_key);
return;
}
++key;
node_uuid = extract_node_uuid(xpath);
if (confirm_cancel_action(key, node_uuid) == FALSE) {
abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart,
"Resource operation removal", change);
}
free(mutable_key);
free(node_uuid);
}
static void
process_delete_diff(const char *xpath, const char *op, xmlNode *change)
{
if (strstr(xpath, "/" PCMK__XE_LRM_RSC_OP "[")) {
process_op_deletion(xpath, change);
} else if (strstr(xpath, "/" PCMK__XE_LRM "[")) {
abort_unless_down(xpath, op, change, "Resource state removal");
} else if (strstr(xpath, "/" PCMK__XE_NODE_STATE "[")) {
abort_unless_down(xpath, op, change, "Node state removal");
} else {
crm_trace("Ignoring delete of %s", xpath);
}
}
static void
process_node_state_diff(xmlNode *state, xmlNode *change, const char *op,
const char *xpath)
{
xmlNode *lrm = pcmk__xe_first_child(state, PCMK__XE_LRM, NULL, NULL);
process_resource_updates(pcmk__xe_id(state), lrm, change, op, xpath);
}
static void
process_status_diff(xmlNode *status, xmlNode *change, const char *op,
const char *xpath)
{
for (xmlNode *state = pcmk__xe_first_child(status, NULL, NULL, NULL);
state != NULL; state = pcmk__xe_next(state, NULL)) {
process_node_state_diff(state, change, op, xpath);
}
}
static void
process_cib_diff(xmlNode *cib, xmlNode *change, const char *op,
const char *xpath)
{
xmlNode *status = pcmk__xe_first_child(cib, PCMK_XE_STATUS, NULL, NULL);
xmlNode *config = pcmk__xe_first_child(cib, PCMK_XE_CONFIGURATION, NULL,
NULL);
if (status) {
process_status_diff(status, change, op, xpath);
}
if (config) {
abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart,
"Non-status-only change", change);
}
}
static int
te_update_diff_element(xmlNode *change, void *userdata)
{
xmlNode *match = NULL;
const char *name = NULL;
const char *xpath = crm_element_value(change, PCMK_XA_PATH);
// Possible ops: create, modify, delete, move
const char *op = crm_element_value(change, PCMK_XA_OPERATION);
// Ignore uninteresting updates
if (op == NULL) {
return pcmk_rc_ok;
} else if (xpath == NULL) {
crm_trace("Ignoring %s change for version field", op);
return pcmk_rc_ok;
} else if ((strcmp(op, PCMK_VALUE_MOVE) == 0)
&& (strstr(xpath,
"/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION
"/" PCMK_XE_RESOURCES) == NULL)) {
/* We still need to consider moves within the resources section,
* since they affect placement order.
*/
crm_trace("Ignoring move change at %s", xpath);
return pcmk_rc_ok;
}
// Find the result of create/modify ops
if (strcmp(op, PCMK_VALUE_CREATE) == 0) {
match = change->children;
} else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) {
match = pcmk__xe_first_child(change, PCMK_XE_CHANGE_RESULT, NULL, NULL);
if(match) {
match = match->children;
}
} else if (!pcmk__str_any_of(op,
PCMK_VALUE_DELETE, PCMK_VALUE_MOVE,
NULL)) {
crm_warn("Ignoring malformed CIB update (%s operation on %s is unrecognized)",
op, xpath);
return pcmk_rc_ok;
}
if (match) {
if (match->type == XML_COMMENT_NODE) {
crm_trace("Ignoring %s operation for comment at %s", op, xpath);
return pcmk_rc_ok;
}
name = (const char *)match->name;
}
crm_trace("Handling %s operation for %s%s%s",
op, (xpath? xpath : "CIB"),
(name? " matched by " : ""), (name? name : ""));
if (strstr(xpath, "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION)) {
abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart,
"Configuration change", change);
return pcmk_rc_cib_modified; // Won't be packaged with operation results we may be waiting for
} else if (strstr(xpath, "/" PCMK_XE_TICKETS)
|| pcmk__str_eq(name, PCMK_XE_TICKETS, pcmk__str_none)) {
abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart,
"Ticket attribute change", change);
return pcmk_rc_cib_modified; // Won't be packaged with operation results we may be waiting for
} else if (strstr(xpath, "/" PCMK__XE_TRANSIENT_ATTRIBUTES "[")
|| pcmk__str_eq(name, PCMK__XE_TRANSIENT_ATTRIBUTES,
pcmk__str_none)) {
abort_unless_down(xpath, op, change, "Transient attribute change");
return pcmk_rc_cib_modified; // Won't be packaged with operation results we may be waiting for
} else if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
process_delete_diff(xpath, op, change);
} else if (name == NULL) {
crm_warn("Ignoring malformed CIB update (%s at %s has no result)",
op, xpath);
} else if (strcmp(name, PCMK_XE_CIB) == 0) {
process_cib_diff(match, change, op, xpath);
} else if (strcmp(name, PCMK_XE_STATUS) == 0) {
process_status_diff(match, change, op, xpath);
} else if (strcmp(name, PCMK__XE_NODE_STATE) == 0) {
process_node_state_diff(match, change, op, xpath);
} else if (strcmp(name, PCMK__XE_LRM) == 0) {
process_resource_updates(pcmk__xe_id(match), match, change, op,
xpath);
} else if (strcmp(name, PCMK__XE_LRM_RESOURCES) == 0) {
char *local_node = pcmk__xpath_node_id(xpath, PCMK__XE_LRM);
process_resource_updates(local_node, match, change, op, xpath);
free(local_node);
} else if (strcmp(name, PCMK__XE_LRM_RESOURCE) == 0) {
char *local_node = pcmk__xpath_node_id(xpath, PCMK__XE_LRM);
process_lrm_resource_diff(match, local_node);
free(local_node);
} else if (strcmp(name, PCMK__XE_LRM_RSC_OP) == 0) {
char *local_node = pcmk__xpath_node_id(xpath, PCMK__XE_LRM);
process_graph_event(match, local_node);
free(local_node);
} else {
crm_warn("Ignoring malformed CIB update (%s at %s has unrecognized result %s)",
op, xpath, name);
}
return pcmk_rc_ok;
}
void
te_update_diff(const char *event, xmlNode * msg)
{
xmlNode *wrapper = NULL;
xmlNode *diff = NULL;
const char *op = NULL;
int rc = -EINVAL;
int format = 1;
int p_add[] = { 0, 0, 0 };
int p_del[] = { 0, 0, 0 };
CRM_CHECK(msg != NULL, return);
crm_element_value_int(msg, PCMK__XA_CIB_RC, &rc);
if (controld_globals.transition_graph == NULL) {
crm_trace("No graph");
return;
} else if (rc < pcmk_ok) {
crm_trace("Filter rc=%d (%s)", rc, pcmk_strerror(rc));
return;
} else if (controld_globals.transition_graph->complete
&& (controld_globals.fsa_state != S_IDLE)
&& (controld_globals.fsa_state != S_TRANSITION_ENGINE)
&& (controld_globals.fsa_state != S_POLICY_ENGINE)) {
crm_trace("Filter state=%s (complete)",
fsa_state2string(controld_globals.fsa_state));
return;
}
op = crm_element_value(msg, PCMK__XA_CIB_OP);
wrapper = pcmk__xe_first_child(msg, PCMK__XE_CIB_UPDATE_RESULT, NULL, NULL);
diff = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
- xml_patch_versions(diff, p_add, p_del);
+ pcmk__xml_patchset_versions(diff, p_del, p_add);
crm_debug("Processing (%s) diff: %d.%d.%d -> %d.%d.%d (%s)", op,
p_del[0], p_del[1], p_del[2], p_add[0], p_add[1], p_add[2],
fsa_state2string(controld_globals.fsa_state));
crm_element_value_int(diff, PCMK_XA_FORMAT, &format);
if (format == 2) {
crm_log_xml_trace(diff, "patch");
pcmk__xe_foreach_child(diff, NULL, te_update_diff_element, NULL);
} else {
crm_warn("Ignoring malformed CIB update (unknown patch format %d)",
format);
}
controld_remove_all_outside_events();
}
void
process_te_message(xmlNode * msg, xmlNode * xml_data)
{
const char *value = NULL;
xmlXPathObject *xpathObj = NULL;
int nmatches = 0;
CRM_CHECK(msg != NULL, return);
// Transition requests must specify transition engine as subsystem
value = crm_element_value(msg, PCMK__XA_CRM_SYS_TO);
if (pcmk__str_empty(value)
|| !pcmk__str_eq(value, CRM_SYSTEM_TENGINE, pcmk__str_none)) {
crm_info("Received invalid transition request: subsystem '%s' not '"
CRM_SYSTEM_TENGINE "'", pcmk__s(value, ""));
return;
}
// Only the lrm_invoke command is supported as a transition request
value = crm_element_value(msg, PCMK__XA_CRM_TASK);
if (!pcmk__str_eq(value, CRM_OP_INVOKE_LRM, pcmk__str_none)) {
crm_info("Received invalid transition request: command '%s' not '"
CRM_OP_INVOKE_LRM "'", pcmk__s(value, ""));
return;
}
// Transition requests must be marked as coming from the executor
value = crm_element_value(msg, PCMK__XA_CRM_SYS_FROM);
if (!pcmk__str_eq(value, CRM_SYSTEM_LRMD, pcmk__str_none)) {
crm_info("Received invalid transition request: from '%s' not '"
CRM_SYSTEM_LRMD "'", pcmk__s(value, ""));
return;
}
crm_debug("Processing transition request with ref='%s' origin='%s'",
pcmk__s(crm_element_value(msg, PCMK_XA_REFERENCE), ""),
pcmk__s(crm_element_value(msg, PCMK__XA_SRC), ""));
xpathObj = pcmk__xpath_search(xml_data->doc, "//" PCMK__XE_LRM_RSC_OP);
nmatches = pcmk__xpath_num_results(xpathObj);
if (nmatches == 0) {
crm_err("Received transition request with no results (bug?)");
} else {
for (int lpc = 0; lpc < nmatches; lpc++) {
xmlNode *rsc_op = pcmk__xpath_result(xpathObj, lpc);
if (rsc_op != NULL) {
const char *node = get_node_id(rsc_op);
process_graph_event(rsc_op, node);
}
}
}
xmlXPathFreeObject(xpathObj);
}
void
cib_action_updated(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data)
{
if (rc < pcmk_ok) {
crm_err("Update %d FAILED: %s", call_id, pcmk_strerror(rc));
}
}
/*!
* \brief Handle a timeout in node-to-node communication
*
* \param[in,out] data Pointer to graph action
*
* \return FALSE (indicating that source should be not be re-added)
*/
gboolean
action_timer_callback(gpointer data)
{
pcmk__graph_action_t *action = (pcmk__graph_action_t *) data;
const char *task = NULL;
const char *on_node = NULL;
const char *via_node = NULL;
CRM_CHECK(data != NULL, return FALSE);
stop_te_timer(action);
task = crm_element_value(action->xml, PCMK_XA_OPERATION);
on_node = crm_element_value(action->xml, PCMK__META_ON_NODE);
via_node = crm_element_value(action->xml, PCMK__XA_ROUTER_NODE);
if (controld_globals.transition_graph->complete) {
crm_notice("Node %s did not send %s result (via %s) within %dms "
"(ignoring because transition not in progress)",
(on_node? on_node : ""), (task? task : "unknown action"),
(via_node? via_node : "controller"), action->timeout);
} else {
/* fail the action */
crm_err("Node %s did not send %s result (via %s) within %dms "
"(action timeout plus " PCMK_OPT_CLUSTER_DELAY ")",
(on_node? on_node : ""), (task? task : "unknown action"),
(via_node? via_node : "controller"),
(action->timeout
+ controld_globals.transition_graph->network_delay));
pcmk__log_graph_action(LOG_ERR, action);
pcmk__set_graph_action_flags(action, pcmk__graph_action_failed);
te_action_confirmed(action, controld_globals.transition_graph);
abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart,
"Action lost", NULL);
// Record timeout in the CIB if appropriate
if ((action->type == pcmk__rsc_graph_action)
&& controld_action_is_recordable(task)) {
controld_record_action_timeout(action);
}
}
return FALSE;
}
diff --git a/daemons/controld/controld_te_utils.c b/daemons/controld/controld_te_utils.c
index 80d3c9477a..289aa21b46 100644
--- a/daemons/controld/controld_te_utils.c
+++ b/daemons/controld/controld_te_utils.c
@@ -1,508 +1,508 @@
/*
- * Copyright 2004-2024 the Pacemaker project contributors
+ * Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <crm/crm.h>
#include <crm/common/xml.h>
#include <pacemaker-controld.h>
//! Triggers transition graph processing
static crm_trigger_t *transition_trigger = NULL;
static GHashTable *node_pending_timers = NULL;
gboolean
stop_te_timer(pcmk__graph_action_t *action)
{
if (action == NULL) {
return FALSE;
}
if (action->timer != 0) {
crm_trace("Stopping action timer");
g_source_remove(action->timer);
action->timer = 0;
} else {
crm_trace("Action timer was already stopped");
return FALSE;
}
return TRUE;
}
static gboolean
te_graph_trigger(gpointer user_data)
{
if (controld_globals.transition_graph == NULL) {
crm_debug("Nothing to do");
return TRUE;
}
crm_trace("Invoking graph %d in state %s",
controld_globals.transition_graph->id,
fsa_state2string(controld_globals.fsa_state));
switch (controld_globals.fsa_state) {
case S_STARTING:
case S_PENDING:
case S_NOT_DC:
case S_HALT:
case S_ILLEGAL:
case S_STOPPING:
case S_TERMINATE:
return TRUE;
default:
break;
}
if (!controld_globals.transition_graph->complete) {
enum pcmk__graph_status graph_rc;
int orig_limit = controld_globals.transition_graph->batch_limit;
int throttled_limit = throttle_get_total_job_limit(orig_limit);
controld_globals.transition_graph->batch_limit = throttled_limit;
graph_rc = pcmk__execute_graph(controld_globals.transition_graph);
controld_globals.transition_graph->batch_limit = orig_limit;
if (graph_rc == pcmk__graph_active) {
crm_trace("Transition not yet complete");
return TRUE;
} else if (graph_rc == pcmk__graph_pending) {
crm_trace("Transition not yet complete - no actions fired");
return TRUE;
}
if (graph_rc != pcmk__graph_complete) {
crm_warn("Transition failed: %s",
pcmk__graph_status2text(graph_rc));
pcmk__log_graph(LOG_NOTICE, controld_globals.transition_graph);
}
}
crm_debug("Transition %d is now complete",
controld_globals.transition_graph->id);
controld_globals.transition_graph->complete = true;
notify_crmd(controld_globals.transition_graph);
return TRUE;
}
/*!
* \internal
* \brief Initialize transition trigger
*/
void
controld_init_transition_trigger(void)
{
transition_trigger = mainloop_add_trigger(G_PRIORITY_LOW, te_graph_trigger,
NULL);
}
/*!
* \internal
* \brief Destroy transition trigger
*/
void
controld_destroy_transition_trigger(void)
{
mainloop_destroy_trigger(transition_trigger);
transition_trigger = NULL;
}
void
controld_trigger_graph_as(const char *fn, int line)
{
crm_trace("%s:%d - Triggered graph processing", fn, line);
mainloop_set_trigger(transition_trigger);
}
static struct abort_timer_s {
bool aborted;
guint id;
int priority;
enum pcmk__graph_next action;
const char *text;
} abort_timer = { 0, };
static gboolean
abort_timer_popped(gpointer data)
{
struct abort_timer_s *abort_timer = (struct abort_timer_s *) data;
if (AM_I_DC && (abort_timer->aborted == FALSE)) {
abort_transition(abort_timer->priority, abort_timer->action,
abort_timer->text, NULL);
}
abort_timer->id = 0;
return FALSE; // do not immediately reschedule timer
}
/*!
* \internal
* \brief Abort transition after delay, if not already aborted in that time
*
* \param[in] abort_text Must be literal string
*/
void
abort_after_delay(int abort_priority, enum pcmk__graph_next abort_action,
const char *abort_text, guint delay_ms)
{
if (abort_timer.id) {
// Timer already in progress, stop and reschedule
g_source_remove(abort_timer.id);
}
abort_timer.aborted = FALSE;
abort_timer.priority = abort_priority;
abort_timer.action = abort_action;
abort_timer.text = abort_text;
abort_timer.id = pcmk__create_timer(delay_ms, abort_timer_popped, &abort_timer);
}
static void
free_node_pending_timer(gpointer data)
{
struct abort_timer_s *node_pending_timer = (struct abort_timer_s *) data;
if (node_pending_timer->id != 0) {
g_source_remove(node_pending_timer->id);
node_pending_timer->id = 0;
}
free(node_pending_timer);
}
static gboolean
node_pending_timer_popped(gpointer key)
{
struct abort_timer_s *node_pending_timer = NULL;
if (node_pending_timers == NULL) {
return FALSE;
}
node_pending_timer = g_hash_table_lookup(node_pending_timers, key);
if (node_pending_timer == NULL) {
return FALSE;
}
crm_warn("Node with " PCMK_XA_ID " '%s' pending timed out (%us) "
"on joining the process group",
(const char *) key, controld_globals.node_pending_timeout);
if (controld_globals.node_pending_timeout > 0) {
abort_timer_popped(node_pending_timer);
}
g_hash_table_remove(node_pending_timers, key);
return FALSE; // do not reschedule timer
}
static void
init_node_pending_timer(const pcmk__node_status_t *node, guint timeout)
{
struct abort_timer_s *node_pending_timer = NULL;
char *key = NULL;
if (node->xml_id == NULL) {
return;
}
if (node_pending_timers == NULL) {
node_pending_timers = pcmk__strikey_table(free,
free_node_pending_timer);
// The timer is somehow already existing
} else if (g_hash_table_lookup(node_pending_timers, node->xml_id) != NULL) {
return;
}
crm_notice("Waiting for pending %s with " PCMK_XA_ID " '%s' "
"to join the process group (timeout=%us)",
pcmk__s(node->name, "node"), node->xml_id,
controld_globals.node_pending_timeout);
key = pcmk__str_copy(node->xml_id);
node_pending_timer = pcmk__assert_alloc(1, sizeof(struct abort_timer_s));
node_pending_timer->aborted = FALSE;
node_pending_timer->priority = PCMK_SCORE_INFINITY;
node_pending_timer->action = pcmk__graph_restart;
node_pending_timer->text = "Node pending timed out";
g_hash_table_replace(node_pending_timers, key, node_pending_timer);
node_pending_timer->id = pcmk__create_timer(timeout * 1000,
node_pending_timer_popped,
key);
pcmk__assert(node_pending_timer->id != 0);
}
static void
remove_node_pending_timer(const char *node_uuid)
{
if (node_pending_timers == NULL) {
return;
}
g_hash_table_remove(node_pending_timers, node_uuid);
}
void
controld_node_pending_timer(const pcmk__node_status_t *node)
{
long long remaining_timeout = 0;
/* If the node is not an active cluster node, is leaving the cluster, or is
* already part of CPG, or PCMK_OPT_NODE_PENDING_TIMEOUT is disabled, free
* any node pending timer for it.
*/
if (pcmk_is_set(node->flags, pcmk__node_status_remote)
|| (node->when_member <= 1) || (node->when_online > 0)
|| (controld_globals.node_pending_timeout == 0)) {
remove_node_pending_timer(node->xml_id);
return;
}
// Node is a cluster member but offline in CPG
remaining_timeout = node->when_member - time(NULL)
+ controld_globals.node_pending_timeout;
/* It already passed node pending timeout somehow.
* Free any node pending timer of it.
*/
if (remaining_timeout <= 0) {
remove_node_pending_timer(node->xml_id);
return;
}
init_node_pending_timer(node, remaining_timeout);
}
void
controld_free_node_pending_timers(void)
{
if (node_pending_timers == NULL) {
return;
}
g_hash_table_destroy(node_pending_timers);
node_pending_timers = NULL;
}
static const char *
abort2text(enum pcmk__graph_next abort_action)
{
switch (abort_action) {
case pcmk__graph_done: return "done";
case pcmk__graph_wait: return "stop";
case pcmk__graph_restart: return "restart";
case pcmk__graph_shutdown: return "shutdown";
}
return "unknown";
}
static bool
update_abort_priority(pcmk__graph_t *graph, int priority,
enum pcmk__graph_next action, const char *abort_reason)
{
bool change = FALSE;
if (graph == NULL) {
return change;
}
if (graph->abort_priority < priority) {
crm_debug("Abort priority upgraded from %d to %d", graph->abort_priority, priority);
graph->abort_priority = priority;
if (graph->abort_reason != NULL) {
crm_debug("'%s' abort superseded by %s", graph->abort_reason, abort_reason);
}
graph->abort_reason = abort_reason;
change = TRUE;
}
if (graph->completion_action < action) {
crm_debug("Abort action %s superseded by %s: %s",
abort2text(graph->completion_action), abort2text(action), abort_reason);
graph->completion_action = action;
change = TRUE;
}
return change;
}
void
abort_transition_graph(int abort_priority, enum pcmk__graph_next abort_action,
const char *abort_text, const xmlNode *reason,
const char *fn, int line)
{
int add[] = { 0, 0, 0 };
int del[] = { 0, 0, 0 };
int level = LOG_INFO;
const xmlNode *diff = NULL;
const xmlNode *change = NULL;
CRM_CHECK(controld_globals.transition_graph != NULL, return);
switch (controld_globals.fsa_state) {
case S_STARTING:
case S_PENDING:
case S_NOT_DC:
case S_HALT:
case S_ILLEGAL:
case S_STOPPING:
case S_TERMINATE:
crm_info("Abort %s suppressed: state=%s (%scomplete)",
abort_text, fsa_state2string(controld_globals.fsa_state),
(controld_globals.transition_graph->complete? "" : "in"));
return;
default:
break;
}
abort_timer.aborted = TRUE;
controld_expect_sched_reply(NULL);
if (!controld_globals.transition_graph->complete
&& update_abort_priority(controld_globals.transition_graph,
abort_priority, abort_action,
abort_text)) {
level = LOG_NOTICE;
}
if (reason != NULL) {
const xmlNode *search = NULL;
for(search = reason; search; search = search->parent) {
if (pcmk__xe_is(search, PCMK_XE_DIFF)) {
diff = search;
break;
}
}
if(diff) {
- xml_patch_versions(diff, add, del);
+ pcmk__xml_patchset_versions(diff, del, add);
for(search = reason; search; search = search->parent) {
if (pcmk__xe_is(search, PCMK_XE_CHANGE)) {
change = search;
break;
}
}
}
}
if (reason == NULL) {
do_crm_log(level,
"Transition %d aborted: %s " QB_XS " source=%s:%d "
"complete=%s", controld_globals.transition_graph->id,
abort_text, fn, line,
pcmk__btoa(controld_globals.transition_graph->complete));
} else if(change == NULL) {
GString *local_path = pcmk__element_xpath(reason);
pcmk__assert(local_path != NULL);
do_crm_log(level, "Transition %d aborted by %s.%s: %s "
QB_XS " cib=%d.%d.%d source=%s:%d path=%s complete=%s",
controld_globals.transition_graph->id, reason->name,
pcmk__xe_id(reason), abort_text, add[0], add[1], add[2], fn,
line, (const char *) local_path->str,
pcmk__btoa(controld_globals.transition_graph->complete));
g_string_free(local_path, TRUE);
} else {
const char *op = crm_element_value(change, PCMK_XA_OPERATION);
const char *path = crm_element_value(change, PCMK_XA_PATH);
if(change == reason) {
if (strcmp(op, PCMK_VALUE_CREATE) == 0) {
reason = reason->children;
} else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) {
reason = pcmk__xe_first_child(reason, PCMK_XE_CHANGE_RESULT,
NULL, NULL);
if(reason) {
reason = reason->children;
}
}
CRM_CHECK(reason != NULL, goto done);
}
if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
const char *shortpath = strrchr(path, '/');
do_crm_log(level, "Transition %d aborted by deletion of %s: %s "
QB_XS " cib=%d.%d.%d source=%s:%d path=%s complete=%s",
controld_globals.transition_graph->id,
(shortpath? (shortpath + 1) : path), abort_text,
add[0], add[1], add[2], fn, line, path,
pcmk__btoa(controld_globals.transition_graph->complete));
} else if (pcmk__xe_is(reason, PCMK_XE_NVPAIR)) {
do_crm_log(level, "Transition %d aborted by %s doing %s %s=%s: %s "
QB_XS " cib=%d.%d.%d source=%s:%d path=%s complete=%s",
controld_globals.transition_graph->id,
crm_element_value(reason, PCMK_XA_ID), op,
crm_element_value(reason, PCMK_XA_NAME),
crm_element_value(reason, PCMK_XA_VALUE),
abort_text, add[0], add[1], add[2], fn, line, path,
pcmk__btoa(controld_globals.transition_graph->complete));
} else if (pcmk__xe_is(reason, PCMK__XE_LRM_RSC_OP)) {
const char *magic = crm_element_value(reason,
PCMK__XA_TRANSITION_MAGIC);
do_crm_log(level, "Transition %d aborted by operation %s '%s' on %s: %s "
QB_XS " magic=%s cib=%d.%d.%d source=%s:%d complete=%s",
controld_globals.transition_graph->id,
crm_element_value(reason, PCMK__XA_OPERATION_KEY), op,
crm_element_value(reason, PCMK__META_ON_NODE),
abort_text,
magic, add[0], add[1], add[2], fn, line,
pcmk__btoa(controld_globals.transition_graph->complete));
} else if (pcmk__str_any_of((const char *) reason->name,
PCMK__XE_NODE_STATE, PCMK_XE_NODE, NULL)) {
const char *uname = pcmk__node_name_from_uuid(pcmk__xe_id(reason));
do_crm_log(level, "Transition %d aborted by %s '%s' on %s: %s "
QB_XS " cib=%d.%d.%d source=%s:%d complete=%s",
controld_globals.transition_graph->id,
reason->name, op, pcmk__s(uname, pcmk__xe_id(reason)),
abort_text, add[0], add[1], add[2], fn, line,
pcmk__btoa(controld_globals.transition_graph->complete));
} else {
const char *id = pcmk__xe_id(reason);
do_crm_log(level, "Transition %d aborted by %s.%s '%s': %s "
QB_XS " cib=%d.%d.%d source=%s:%d path=%s complete=%s",
controld_globals.transition_graph->id,
reason->name, pcmk__s(id, ""), pcmk__s(op, "change"),
abort_text, add[0], add[1], add[2], fn, line, path,
pcmk__btoa(controld_globals.transition_graph->complete));
}
}
done:
if (controld_globals.transition_graph->complete) {
if (controld_get_period_transition_timer() > 0) {
controld_stop_transition_timer();
controld_start_transition_timer();
} else {
register_fsa_input(C_FSA_INTERNAL, I_PE_CALC, NULL);
}
return;
}
trigger_graph();
}
diff --git a/daemons/fenced/fenced_cib.c b/daemons/fenced/fenced_cib.c
index e76b25aeef..9fdd7d9e35 100644
--- a/daemons/fenced/fenced_cib.c
+++ b/daemons/fenced/fenced_cib.c
@@ -1,636 +1,636 @@
/*
* Copyright 2009-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <stdbool.h>
#include <stdio.h>
#include <libxml/tree.h> // xmlNode
#include <libxml/xpath.h> // xmlXPathObject, etc.
#include <crm/crm.h>
#include <crm/common/xml.h>
#include <crm/cluster/internal.h>
#include <crm/cib.h>
#include <crm/cib/internal.h>
#include <pacemaker-fenced.h>
static xmlNode *local_cib = NULL;
static cib_t *cib_api = NULL;
static bool have_cib_devices = FALSE;
/*!
* \internal
* \brief Check whether a node has a specific attribute name/value
*
* \param[in] node Name of node to check
* \param[in] name Name of an attribute to look for
* \param[in] value The value the named attribute needs to be set to in order to be considered a match
*
* \return TRUE if the locally cached CIB has the specified node attribute
*/
gboolean
node_has_attr(const char *node, const char *name, const char *value)
{
GString *xpath = NULL;
xmlNode *match;
CRM_CHECK((local_cib != NULL) && (node != NULL) && (name != NULL)
&& (value != NULL), return FALSE);
/* Search for the node's attributes in the CIB. While the schema allows
* multiple sets of instance attributes, and allows instance attributes to
* use id-ref to reference values elsewhere, that is intended for resources,
* so we ignore that here.
*/
xpath = g_string_sized_new(256);
pcmk__g_strcat(xpath,
"//" PCMK_XE_NODES "/" PCMK_XE_NODE
"[@" PCMK_XA_UNAME "='", node, "']"
"/" PCMK_XE_INSTANCE_ATTRIBUTES
"/" PCMK_XE_NVPAIR
"[@" PCMK_XA_NAME "='", name, "' "
"and @" PCMK_XA_VALUE "='", value, "']", NULL);
match = pcmk__xpath_find_one(local_cib->doc, xpath->str, LOG_NEVER);
g_string_free(xpath, TRUE);
return (match != NULL);
}
static void
remove_topology_level(xmlNode *match)
{
int index = 0;
char *key = NULL;
xmlNode *data = NULL;
CRM_CHECK(match != NULL, return);
key = stonith_level_key(match, fenced_target_by_unknown);
crm_element_value_int(match, PCMK_XA_INDEX, &index);
data = pcmk__xe_create(NULL, PCMK_XE_FENCING_LEVEL);
crm_xml_add(data, PCMK__XA_ST_ORIGIN, __func__);
crm_xml_add(data, PCMK_XA_TARGET, key);
crm_xml_add_int(data, PCMK_XA_INDEX, index);
fenced_unregister_level(data, NULL);
free(key);
pcmk__xml_free(data);
}
static void
register_fencing_topology(xmlXPathObjectPtr xpathObj)
{
int max = pcmk__xpath_num_results(xpathObj);
for (int lpc = 0; lpc < max; lpc++) {
xmlNode *match = pcmk__xpath_result(xpathObj, lpc);
if (match == NULL) {
continue;
}
remove_topology_level(match);
fenced_register_level(match, NULL);
}
}
/* Fencing
<diff crm_feature_set="3.0.6">
<diff-removed>
<fencing-topology>
<fencing-level id="f-p1.1" target="pcmk-1" index="1" devices="poison-pill" __crm_diff_marker__="removed:top"/>
<fencing-level id="f-p1.2" target="pcmk-1" index="2" devices="power" __crm_diff_marker__="removed:top"/>
<fencing-level devices="disk,network" id="f-p2.1"/>
</fencing-topology>
</diff-removed>
<diff-added>
<fencing-topology>
<fencing-level id="f-p.1" target="pcmk-1" index="1" devices="poison-pill" __crm_diff_marker__="added:top"/>
<fencing-level id="f-p2.1" target="pcmk-2" index="1" devices="disk,something"/>
<fencing-level id="f-p3.1" target="pcmk-2" index="2" devices="power" __crm_diff_marker__="added:top"/>
</fencing-topology>
</diff-added>
</diff>
*/
void
fencing_topology_init(void)
{
xmlXPathObject *xpathObj = NULL;
const char *xpath = "//" PCMK_XE_FENCING_LEVEL;
crm_trace("Full topology refresh");
free_topology_list();
init_topology_list();
/* Grab everything */
xpathObj = pcmk__xpath_search(local_cib->doc, xpath);
register_fencing_topology(xpathObj);
xmlXPathFreeObject(xpathObj);
}
#define XPATH_WATCHDOG_TIMEOUT "//" PCMK_XE_NVPAIR \
"[@" PCMK_XA_NAME "='" \
PCMK_OPT_STONITH_WATCHDOG_TIMEOUT "']"
static void
update_stonith_watchdog_timeout_ms(xmlNode *cib)
{
long long timeout_ms = 0;
xmlNode *stonith_watchdog_xml = NULL;
const char *value = NULL;
// @TODO An XPath search can't handle multiple instances or rules
stonith_watchdog_xml = pcmk__xpath_find_one(cib->doc,
XPATH_WATCHDOG_TIMEOUT,
LOG_NEVER);
if (stonith_watchdog_xml) {
value = crm_element_value(stonith_watchdog_xml, PCMK_XA_VALUE);
}
if (value) {
timeout_ms = crm_get_msec(value);
}
if (timeout_ms < 0) {
timeout_ms = pcmk__auto_stonith_watchdog_timeout();
}
stonith_watchdog_timeout_ms = timeout_ms;
}
/*!
* \internal
* \brief Mark a fence device dirty if its \c fenced_df_cib_registered flag is
* set
*
* \param[in] key Ignored
* \param[in,out] value Fence device (<tt>fenced_device_t *</tt>)
* \param[in] user_data Ignored
*
* \note This function is suitable for use with \c g_hash_table_foreach().
*/
static void
mark_dirty_if_cib_registered(gpointer key, gpointer value, gpointer user_data)
{
fenced_device_t *device = value;
if (pcmk_is_set(device->flags, fenced_df_cib_registered)) {
fenced_device_set_flags(device, fenced_df_dirty);
}
}
/*!
* \internal
* \brief Return the value of a fence device's \c dirty flag
*
* \param[in] key Ignored
* \param[in] value Fence device (<tt>fenced_device_t *</tt>)
* \param[in] user_data Ignored
*
* \return \c dirty flag of \p value
*
* \note This function is suitable for use with
* \c g_hash_table_foreach_remove().
*/
static gboolean
device_is_dirty(gpointer key, gpointer value, gpointer user_data)
{
fenced_device_t *device = value;
return pcmk_is_set(device->flags, fenced_df_dirty);
}
/*!
* \internal
* \brief Update all STONITH device definitions based on current CIB
*/
static void
cib_devices_update(void)
{
crm_info("Updating devices to version %s.%s.%s",
crm_element_value(local_cib, PCMK_XA_ADMIN_EPOCH),
crm_element_value(local_cib, PCMK_XA_EPOCH),
crm_element_value(local_cib, PCMK_XA_NUM_UPDATES));
fenced_foreach_device(mark_dirty_if_cib_registered, NULL);
/* have list repopulated if cib has a watchdog-fencing-resource
TODO: keep a cached list for queries happening while we are refreshing
*/
g_list_free_full(stonith_watchdog_targets, free);
stonith_watchdog_targets = NULL;
fenced_scheduler_run(local_cib);
fenced_foreach_device_remove(device_is_dirty);
}
#define PRIMITIVE_ID_XP_FRAGMENT "/" PCMK_XE_PRIMITIVE "[@" PCMK_XA_ID "='"
static void
update_cib_stonith_devices(const xmlNode *patchset)
{
char *reason = NULL;
for (const xmlNode *change = pcmk__xe_first_child(patchset, NULL, NULL,
NULL);
change != NULL; change = pcmk__xe_next(change, NULL)) {
const char *op = crm_element_value(change, PCMK_XA_OPERATION);
const char *xpath = crm_element_value(change, PCMK_XA_PATH);
const char *primitive_xpath = NULL;
if (pcmk__str_eq(op, PCMK_VALUE_MOVE, pcmk__str_null_matches)
|| (strstr(xpath, "/" PCMK_XE_STATUS) != NULL)) {
continue;
}
primitive_xpath = strstr(xpath, PRIMITIVE_ID_XP_FRAGMENT);
if ((primitive_xpath != NULL)
&& pcmk__str_eq(op, PCMK_VALUE_DELETE, pcmk__str_none)) {
const char *rsc_id = NULL;
const char *end_quote = NULL;
if ((strstr(primitive_xpath, PCMK_XE_INSTANCE_ATTRIBUTES) != NULL)
|| (strstr(primitive_xpath, PCMK_XE_META_ATTRIBUTES) != NULL)) {
reason = pcmk__str_copy("(meta) attribute deleted from "
"resource");
break;
}
rsc_id = primitive_xpath + sizeof(PRIMITIVE_ID_XP_FRAGMENT) - 1;
end_quote = strchr(rsc_id, '\'');
CRM_LOG_ASSERT(end_quote != NULL);
if (end_quote == NULL) {
crm_err("Bug: Malformed item in Pacemaker-generated patchset");
continue;
}
if (strchr(end_quote, '/') == NULL) {
/* The primitive element itself was deleted. If this was a
* fencing resource, it's faster to remove it directly than to
* run the scheduler and update all device registrations.
*/
char *copy = strndup(rsc_id, end_quote - rsc_id);
pcmk__assert(copy != NULL);
stonith_device_remove(copy, true);
/* watchdog_device_update called afterwards
to fall back to implicit definition if needed */
free(copy);
continue;
}
}
if (strstr(xpath, "/" PCMK_XE_RESOURCES)
|| strstr(xpath, "/" PCMK_XE_CONSTRAINTS)
|| strstr(xpath, "/" PCMK_XE_RSC_DEFAULTS)) {
const char *shortpath = strrchr(xpath, '/');
reason = crm_strdup_printf("%s %s", op, shortpath + 1);
break;
}
}
if (reason != NULL) {
crm_info("Updating device list from CIB: %s", reason);
cib_devices_update();
free(reason);
} else {
crm_trace("No updates for device list found in CIB");
}
}
static void
watchdog_device_update(void)
{
if (stonith_watchdog_timeout_ms > 0) {
if (!fenced_has_watchdog_device()
&& (stonith_watchdog_targets == NULL)) {
/* getting here watchdog-fencing enabled, no device there yet
and reason isn't stonith_watchdog_targets preventing that
*/
int rc;
xmlNode *xml;
xml = create_device_registration_xml(
STONITH_WATCHDOG_ID,
st_namespace_internal,
STONITH_WATCHDOG_AGENT,
NULL, /* fenced_device_register() will add our
own name as PCMK_STONITH_HOST_LIST param
so we can skip that here
*/
NULL);
rc = fenced_device_register(xml, true);
pcmk__xml_free(xml);
if (rc != pcmk_rc_ok) {
exit_code = CRM_EX_FATAL;
crm_crit("Cannot register watchdog pseudo fence agent: %s",
pcmk_rc_str(rc));
stonith_shutdown(0);
}
}
} else if (fenced_has_watchdog_device()) {
/* be silent if no device - todo parameter to stonith_device_remove */
stonith_device_remove(STONITH_WATCHDOG_ID, true);
}
}
/*!
* \internal
* \brief Query the full CIB
*
* \return Standard Pacemaker return code
*/
static int
fenced_query_cib(void)
{
int rc = pcmk_ok;
crm_trace("Re-requesting full CIB");
rc = cib_api->cmds->query(cib_api, NULL, &local_cib, cib_sync_call);
rc = pcmk_legacy2rc(rc);
if (rc == pcmk_rc_ok) {
pcmk__assert(local_cib != NULL);
} else {
crm_err("Couldn't retrieve the CIB: %s " QB_XS " rc=%d",
pcmk_rc_str(rc), rc);
}
return rc;
}
static void
update_fencing_topology(const char *event, xmlNode *msg)
{
xmlNode *wrapper = pcmk__xe_first_child(msg, PCMK__XE_CIB_UPDATE_RESULT,
NULL, NULL);
xmlNode *patchset = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
int format = 1;
int add[] = { 0, 0, 0 };
int del[] = { 0, 0, 0 };
CRM_CHECK(patchset != NULL, return);
crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
if (format != 2) {
crm_warn("Unknown patch format: %d", format);
return;
}
- xml_patch_versions(patchset, add, del);
+ pcmk__xml_patchset_versions(patchset, del, add);
for (xmlNode *change = pcmk__xe_first_child(patchset, NULL, NULL, NULL);
change != NULL; change = pcmk__xe_next(change, NULL)) {
const char *op = crm_element_value(change, PCMK_XA_OPERATION);
const char *xpath = crm_element_value(change, PCMK_XA_PATH);
if (op == NULL) {
continue;
}
if (strstr(xpath, "/" PCMK_XE_FENCING_LEVEL) != NULL) {
// Change to a specific entry
crm_trace("Handling %s operation %d.%d.%d for %s",
op, add[0], add[1], add[2], xpath);
if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
/* We have only path and ID, which is not enough info to remove
* a specific entry. Re-initialize the whole topology.
*/
crm_info("Re-initializing fencing topology after %s operation "
"%d.%d.%d for %s",
op, add[0], add[1], add[2], xpath);
fencing_topology_init();
return;
}
if (strcmp(op, PCMK_VALUE_CREATE) == 0) {
fenced_register_level(change->children, NULL);
} else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) {
xmlNode *match = pcmk__xe_first_child(change,
PCMK_XE_CHANGE_RESULT,
NULL, NULL);
if (match != NULL) {
remove_topology_level(match->children);
fenced_register_level(match->children, NULL);
}
}
continue;
}
if (strstr(xpath, "/" PCMK_XE_FENCING_TOPOLOGY) != NULL) {
// Change to the topology in general
crm_info("Re-initializing fencing topology after top-level "
"%s operation %d.%d.%d for %s",
op, add[0], add[1], add[2], xpath);
fencing_topology_init();
return;
}
if ((strstr(xpath, "/" PCMK_XE_CONFIGURATION) != NULL)
&& (pcmk__xe_first_child(change, PCMK_XE_FENCING_TOPOLOGY, NULL,
NULL) != NULL)
&& pcmk__str_any_of(op, PCMK_VALUE_CREATE, PCMK_VALUE_DELETE,
NULL)) {
// Topology was created or entire configuration section was deleted
crm_info("Re-initializing fencing topology after top-level "
"%s operation %d.%d.%d for %s",
op, add[0], add[1], add[2], xpath);
fencing_topology_init();
return;
}
crm_trace("Nothing for us in %s operation %d.%d.%d for %s",
op, add[0], add[1], add[2], xpath);
}
}
static void
update_cib_cache_cb(const char *event, xmlNode * msg)
{
xmlNode *patchset = NULL;
long long timeout_ms_saved = stonith_watchdog_timeout_ms;
bool need_full_refresh = false;
if(!have_cib_devices) {
crm_trace("Skipping updates until we get a full dump");
return;
} else if(msg == NULL) {
crm_trace("Missing %s update", event);
return;
}
/* Maintain a local copy of the CIB so that we have full access
* to device definitions, location constraints, and node attributes
*/
if (local_cib != NULL) {
int rc = pcmk_ok;
xmlNode *wrapper = NULL;
crm_element_value_int(msg, PCMK__XA_CIB_RC, &rc);
if (rc != pcmk_ok) {
return;
}
wrapper = pcmk__xe_first_child(msg, PCMK__XE_CIB_UPDATE_RESULT, NULL,
NULL);
patchset = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
rc = xml_apply_patchset(local_cib, patchset, TRUE);
switch (rc) {
case pcmk_ok:
case -pcmk_err_old_data:
/* @TODO Full refresh (with or without query) in case of
* -pcmk_err_old_data? It seems wrong to call
* stonith_device_remove() based on primitive deletion in an
* old diff.
*/
break;
case -pcmk_err_diff_resync:
case -pcmk_err_diff_failed:
crm_notice("[%s] Patch aborted: %s (%d)", event, pcmk_strerror(rc), rc);
pcmk__xml_free(local_cib);
local_cib = NULL;
break;
default:
crm_warn("[%s] ABORTED: %s (%d)", event, pcmk_strerror(rc), rc);
pcmk__xml_free(local_cib);
local_cib = NULL;
}
}
if (local_cib == NULL) {
if (fenced_query_cib() != pcmk_rc_ok) {
return;
}
need_full_refresh = true;
}
pcmk__refresh_node_caches_from_cib(local_cib);
update_stonith_watchdog_timeout_ms(local_cib);
if (timeout_ms_saved != stonith_watchdog_timeout_ms) {
need_full_refresh = true;
}
if (need_full_refresh) {
fencing_topology_init();
cib_devices_update();
} else {
// Partial refresh
update_fencing_topology(event, msg);
update_cib_stonith_devices(patchset);
}
watchdog_device_update();
}
static void
init_cib_cache_cb(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data)
{
crm_info("Updating device list from CIB");
have_cib_devices = TRUE;
local_cib = pcmk__xml_copy(NULL, output);
pcmk__refresh_node_caches_from_cib(local_cib);
update_stonith_watchdog_timeout_ms(local_cib);
fencing_topology_init();
cib_devices_update();
watchdog_device_update();
}
static void
cib_connection_destroy(gpointer user_data)
{
if (stonith_shutdown_flag) {
crm_info("Connection to the CIB manager closed");
return;
} else {
crm_crit("Lost connection to the CIB manager, shutting down");
}
if (cib_api) {
cib_api->cmds->signoff(cib_api);
}
stonith_shutdown(0);
}
/*!
* \internal
* \brief Disconnect from CIB manager
*/
void
fenced_cib_cleanup(void)
{
if (cib_api != NULL) {
cib_api->cmds->del_notify_callback(cib_api, PCMK__VALUE_CIB_DIFF_NOTIFY,
update_cib_cache_cb);
cib__clean_up_connection(&cib_api);
}
pcmk__xml_free(local_cib);
local_cib = NULL;
}
void
setup_cib(void)
{
int rc, retries = 0;
cib_api = cib_new();
if (cib_api == NULL) {
crm_err("No connection to the CIB manager");
return;
}
do {
sleep(retries);
rc = cib_api->cmds->signon(cib_api, crm_system_name, cib_command);
} while (rc == -ENOTCONN && ++retries < 5);
if (rc != pcmk_ok) {
crm_err("Could not connect to the CIB manager: %s (%d)", pcmk_strerror(rc), rc);
return;
}
rc = cib_api->cmds->add_notify_callback(cib_api,
PCMK__VALUE_CIB_DIFF_NOTIFY,
update_cib_cache_cb);
if (rc != pcmk_ok) {
crm_err("Could not set CIB notification callback");
return;
}
rc = cib_api->cmds->query(cib_api, NULL, NULL, cib_none);
cib_api->cmds->register_callback(cib_api, rc, 120, FALSE, NULL,
"init_cib_cache_cb", init_cib_cache_cb);
cib_api->cmds->set_connection_dnotify(cib_api, cib_connection_destroy);
crm_info("Watching for fencing topology changes");
}
diff --git a/daemons/fenced/fenced_commands.c b/daemons/fenced/fenced_commands.c
index ebbdea8660..68c9a6df59 100644
--- a/daemons/fenced/fenced_commands.c
+++ b/daemons/fenced/fenced_commands.c
@@ -1,3651 +1,3651 @@
/*
* Copyright 2009-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <sys/param.h>
#include <stdbool.h> // bool
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/utsname.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <libxml/tree.h> // xmlNode
#include <libxml/xpath.h> // xmlXPathObject, etc.
#include <crm/crm.h>
#include <crm/common/ipc.h>
#include <crm/common/ipc_internal.h>
#include <crm/cluster/internal.h>
#include <crm/common/mainloop.h>
#include <crm/stonith-ng.h>
#include <crm/fencing/internal.h>
#include <crm/common/xml.h>
#include <pacemaker-fenced.h>
static GHashTable *device_table = NULL;
GHashTable *topology = NULL;
static GList *cmd_list = NULL;
static GHashTable *fenced_handlers = NULL;
struct device_search_s {
/* target of fence action */
char *host;
/* requested fence action */
char *action;
/* timeout to use if a device is queried dynamically for possible targets */
// @TODO This name is misleading now, it's the value of stonith-timeout
int per_device_timeout;
/* number of registered fencing devices at time of request */
int replies_needed;
/* number of device replies received so far */
int replies_received;
/* whether the target is eligible to perform requested action (or off) */
bool allow_self;
/* private data to pass to search callback function */
void *user_data;
/* function to call when all replies have been received */
void (*callback) (GList * devices, void *user_data);
/* devices capable of performing requested action (or off if remapping) */
GList *capable;
/* Whether to perform searches that support the action */
uint32_t support_action_only;
};
static gboolean stonith_device_dispatch(gpointer user_data);
static void st_child_done(int pid, const pcmk__action_result_t *result,
void *user_data);
static void search_devices_record_result(struct device_search_s *search, const char *device,
gboolean can_fence);
static int get_agent_metadata(const char *agent, xmlNode **metadata);
static void read_action_metadata(fenced_device_t *device);
static enum fenced_target_by unpack_level_kind(const xmlNode *level);
typedef struct {
int id;
uint32_t options;
int default_timeout; /* seconds */
int timeout; /* seconds */
int start_delay; // seconds (-1 means disable static/random fencing delays)
int delay_id;
char *op;
char *origin;
char *client;
char *client_name;
char *remote_op_id;
char *target;
char *action;
char *device;
//! Head of device list (used only for freeing list with command object)
GList *device_list;
//! Next item to process in \c device_list
GList *next_device_iter;
void *internal_user_data;
void (*done_cb) (int pid, const pcmk__action_result_t *result,
void *user_data);
fenced_device_t *active_on;
fenced_device_t *activating_on;
} async_command_t;
static xmlNode *construct_async_reply(const async_command_t *cmd,
const pcmk__action_result_t *result);
/*!
* \internal
* \brief Set a bad fencer API request error in a result object
*
* \param[out] result Result to set
*/
static inline void
set_bad_request_result(pcmk__action_result_t *result)
{
pcmk__set_result(result, CRM_EX_PROTOCOL, PCMK_EXEC_INVALID,
"Fencer API request missing required information (bug?)");
}
/*!
* \internal
* \brief Check whether the fencer's device table contains a watchdog device
*
* \retval \c true If the device table contains a watchdog device
* \retval \c false Otherwise
*/
bool
fenced_has_watchdog_device(void)
{
return (device_table != NULL)
&& (g_hash_table_lookup(device_table, STONITH_WATCHDOG_ID) != NULL);
}
/*!
* \internal
* \brief Call a function for each known fence device
*
* \param[in] fn Function to call for each device
* \param[in,out] user_data User data
*/
void
fenced_foreach_device(GHFunc fn, gpointer user_data)
{
if (device_table != NULL) {
g_hash_table_foreach(device_table, fn, user_data);
}
}
/*!
* \internal
* \brief Remove each known fence device matching a given predicate
*
* \param[in] fn Function that returns \c TRUE to remove a fence device or
* \c FALSE to keep it
*/
void
fenced_foreach_device_remove(GHRFunc fn)
{
if (device_table != NULL) {
g_hash_table_foreach_remove(device_table, fn, NULL);
}
}
static gboolean
is_action_required(const char *action, const fenced_device_t *device)
{
return (device != NULL)
&& pcmk_is_set(device->flags, fenced_df_auto_unfence)
&& pcmk__str_eq(action, PCMK_ACTION_ON, pcmk__str_none);
}
static int
get_action_delay_max(const fenced_device_t *device, const char *action)
{
const char *value = NULL;
guint delay_max = 0U;
if (!pcmk__is_fencing_action(action)) {
return 0;
}
value = g_hash_table_lookup(device->params, PCMK_STONITH_DELAY_MAX);
if (value) {
pcmk_parse_interval_spec(value, &delay_max);
delay_max /= 1000;
}
return (int) delay_max;
}
static int
get_action_delay_base(const fenced_device_t *device, const char *action,
const char *target)
{
char *hash_value = NULL;
guint delay_base = 0U;
if (!pcmk__is_fencing_action(action)) {
return 0;
}
hash_value = g_hash_table_lookup(device->params, PCMK_STONITH_DELAY_BASE);
if (hash_value) {
char *value = pcmk__str_copy(hash_value);
char *valptr = value;
if (target != NULL) {
for (char *val = strtok(value, "; \t"); val != NULL; val = strtok(NULL, "; \t")) {
char *mapval = strchr(val, ':');
if (mapval == NULL || mapval[1] == 0) {
crm_err("pcmk_delay_base: empty value in mapping", val);
continue;
}
if (mapval != val && strncasecmp(target, val, (size_t)(mapval - val)) == 0) {
value = mapval + 1;
crm_debug("pcmk_delay_base mapped to %s for %s",
value, target);
break;
}
}
}
if (strchr(value, ':') == 0) {
pcmk_parse_interval_spec(value, &delay_base);
delay_base /= 1000;
}
free(valptr);
}
return (int) delay_base;
}
/*!
* \internal
* \brief Override STONITH timeout with pcmk_*_timeout if available
*
* \param[in] device STONITH device to use
* \param[in] action STONITH action name
* \param[in] default_timeout Timeout to use if device does not have
* a pcmk_*_timeout parameter for action
*
* \return Value of pcmk_(action)_timeout if available, otherwise default_timeout
* \note For consistency, it would be nice if reboot/off/on timeouts could be
* set the same way as start/stop/monitor timeouts, i.e. with an
* <operation> entry in the fencing resource configuration. However that
* is insufficient because fencing devices may be registered directly via
* the fencer's register_device() API instead of going through the CIB
* (e.g. stonith_admin uses it for its -R option, and the executor uses it
* to ensure a device is registered when a command is issued). As device
* properties, pcmk_*_timeout parameters can be grabbed by the fencer when
* the device is registered, whether by CIB change or API call.
*/
static int
get_action_timeout(const fenced_device_t *device, const char *action,
int default_timeout)
{
if (action && device && device->params) {
char buffer[64] = { 0, };
const char *value = NULL;
/* If "reboot" was requested but the device does not support it,
* we will remap to "off", so check timeout for "off" instead
*/
if (pcmk__str_eq(action, PCMK_ACTION_REBOOT, pcmk__str_none)
&& !pcmk_is_set(device->flags, fenced_df_supports_reboot)) {
crm_trace("%s doesn't support reboot, using timeout for off instead",
device->id);
action = PCMK_ACTION_OFF;
}
/* If the device config specified an action-specific timeout, use it */
snprintf(buffer, sizeof(buffer), "pcmk_%s_timeout", action);
value = g_hash_table_lookup(device->params, buffer);
if (value) {
long long timeout_ms = crm_get_msec(value);
return (int) QB_MIN(pcmk__timeout_ms2s(timeout_ms), INT_MAX);
}
}
return default_timeout;
}
/*!
* \internal
* \brief Get the currently executing device for a fencing operation
*
* \param[in] cmd Fencing operation to check
*
* \return Currently executing device for \p cmd if any, otherwise NULL
*/
static fenced_device_t *
cmd_device(const async_command_t *cmd)
{
if ((cmd == NULL) || (cmd->device == NULL) || (device_table == NULL)) {
return NULL;
}
return g_hash_table_lookup(device_table, cmd->device);
}
/*!
* \internal
* \brief Return the configured reboot action for a given device
*
* \param[in] device_id Device ID
*
* \return Configured reboot action for \p device_id
*/
const char *
fenced_device_reboot_action(const char *device_id)
{
const char *action = NULL;
if ((device_table != NULL) && (device_id != NULL)) {
fenced_device_t *device = g_hash_table_lookup(device_table, device_id);
if ((device != NULL) && (device->params != NULL)) {
action = g_hash_table_lookup(device->params, "pcmk_reboot_action");
}
}
return pcmk__s(action, PCMK_ACTION_REBOOT);
}
/*!
* \internal
* \brief Check whether a given device supports the "on" action
*
* \param[in] device_id Device ID
*
* \return true if \p device_id supports "on", otherwise false
*/
bool
fenced_device_supports_on(const char *device_id)
{
if ((device_table != NULL) && (device_id != NULL)) {
fenced_device_t *device = g_hash_table_lookup(device_table, device_id);
if (device != NULL) {
return pcmk_is_set(device->flags, fenced_df_supports_on);
}
}
return false;
}
static void
free_async_command(async_command_t * cmd)
{
if (!cmd) {
return;
}
if (cmd->delay_id) {
g_source_remove(cmd->delay_id);
}
cmd_list = g_list_remove(cmd_list, cmd);
g_list_free_full(cmd->device_list, free);
free(cmd->device);
free(cmd->action);
free(cmd->target);
free(cmd->remote_op_id);
free(cmd->client);
free(cmd->client_name);
free(cmd->origin);
free(cmd->op);
free(cmd);
}
/*!
* \internal
* \brief Create a new asynchronous fencing operation from request XML
*
* \param[in] msg Fencing request XML (from IPC or CPG)
*
* \return Newly allocated fencing operation on success, otherwise NULL
*
* \note This asserts on memory errors, so a NULL return indicates an
* unparseable message.
*/
static async_command_t *
create_async_command(xmlNode *msg)
{
xmlNode *op = NULL;
async_command_t *cmd = NULL;
int rc = pcmk_rc_ok;
if (msg == NULL) {
return NULL;
}
op = pcmk__xpath_find_one(msg->doc, "//*[@" PCMK__XA_ST_DEVICE_ACTION "]",
LOG_ERR);
if (op == NULL) {
return NULL;
}
cmd = pcmk__assert_alloc(1, sizeof(async_command_t));
// All messages must include these
cmd->action = crm_element_value_copy(op, PCMK__XA_ST_DEVICE_ACTION);
cmd->op = crm_element_value_copy(msg, PCMK__XA_ST_OP);
cmd->client = crm_element_value_copy(msg, PCMK__XA_ST_CLIENTID);
if ((cmd->action == NULL) || (cmd->op == NULL) || (cmd->client == NULL)) {
free_async_command(cmd);
return NULL;
}
crm_element_value_int(msg, PCMK__XA_ST_CALLID, &(cmd->id));
crm_element_value_int(msg, PCMK__XA_ST_DELAY, &(cmd->start_delay));
crm_element_value_int(msg, PCMK__XA_ST_TIMEOUT, &(cmd->default_timeout));
cmd->timeout = cmd->default_timeout;
rc = pcmk__xe_get_flags(msg, PCMK__XA_ST_CALLOPT, &(cmd->options),
st_opt_none);
if (rc != pcmk_rc_ok) {
crm_warn("Couldn't parse options from request: %s", pcmk_rc_str(rc));
}
cmd->origin = crm_element_value_copy(msg, PCMK__XA_SRC);
cmd->remote_op_id = crm_element_value_copy(msg, PCMK__XA_ST_REMOTE_OP);
cmd->client_name = crm_element_value_copy(msg, PCMK__XA_ST_CLIENTNAME);
cmd->target = crm_element_value_copy(op, PCMK__XA_ST_TARGET);
cmd->device = crm_element_value_copy(op, PCMK__XA_ST_DEVICE_ID);
cmd->done_cb = st_child_done;
// Track in global command list
cmd_list = g_list_append(cmd_list, cmd);
return cmd;
}
static int
get_action_limit(fenced_device_t *device)
{
const char *value = NULL;
int action_limit = 1;
value = g_hash_table_lookup(device->params, PCMK_STONITH_ACTION_LIMIT);
if ((value == NULL)
|| (pcmk__scan_min_int(value, &action_limit, INT_MIN) != pcmk_rc_ok)
|| (action_limit == 0)) {
action_limit = 1;
}
return action_limit;
}
static int
get_active_cmds(fenced_device_t *device)
{
int counter = 0;
GList *gIter = NULL;
GList *gIterNext = NULL;
CRM_CHECK(device != NULL, return 0);
for (gIter = cmd_list; gIter != NULL; gIter = gIterNext) {
async_command_t *cmd = gIter->data;
gIterNext = gIter->next;
if (cmd->active_on == device) {
counter++;
}
}
return counter;
}
static void
fork_cb(int pid, void *user_data)
{
async_command_t *cmd = (async_command_t *) user_data;
fenced_device_t *device = cmd->activating_on;
if (device == NULL) {
/* In case of a retry, we've done the move from activating_on to
* active_on already
*/
device = cmd->active_on;
}
pcmk__assert(device != NULL);
crm_debug("Operation '%s' [%d]%s%s using %s now running with %ds timeout",
cmd->action, pid,
((cmd->target == NULL)? "" : " targeting "),
pcmk__s(cmd->target, ""), device->id, cmd->timeout);
cmd->active_on = device;
cmd->activating_on = NULL;
}
static int
get_agent_metadata_cb(gpointer data) {
fenced_device_t *device = data;
guint period_ms;
switch (get_agent_metadata(device->agent, &device->agent_metadata)) {
case pcmk_rc_ok:
if (device->agent_metadata) {
read_action_metadata(device);
device->default_host_arg =
stonith__default_host_arg(device->agent_metadata);
}
return G_SOURCE_REMOVE;
case EAGAIN:
period_ms = pcmk__mainloop_timer_get_period(device->timer);
if (period_ms < 160 * 1000) {
mainloop_timer_set_period(device->timer, 2 * period_ms);
}
return G_SOURCE_CONTINUE;
default:
return G_SOURCE_REMOVE;
}
}
/*!
* \internal
* \brief Call a command's action callback for an internal (not library) result
*
* \param[in,out] cmd Command to report result for
* \param[in] execution_status Execution status to use for result
* \param[in] exit_status Exit status to use for result
* \param[in] exit_reason Exit reason to use for result
*/
static void
report_internal_result(async_command_t *cmd, int exit_status,
int execution_status, const char *exit_reason)
{
pcmk__action_result_t result = PCMK__UNKNOWN_RESULT;
pcmk__set_result(&result, exit_status, execution_status, exit_reason);
cmd->done_cb(0, &result, cmd);
pcmk__reset_result(&result);
}
static gboolean
stonith_device_execute(fenced_device_t *device)
{
int exec_rc = 0;
const char *action_str = NULL;
async_command_t *cmd = NULL;
stonith_action_t *action = NULL;
int active_cmds = 0;
int action_limit = 0;
GList *gIter = NULL;
GList *gIterNext = NULL;
CRM_CHECK(device != NULL, return FALSE);
active_cmds = get_active_cmds(device);
action_limit = get_action_limit(device);
if (action_limit > -1 && active_cmds >= action_limit) {
crm_trace("%s is over its action limit of %d (%u active action%s)",
device->id, action_limit, active_cmds,
pcmk__plural_s(active_cmds));
return TRUE;
}
for (gIter = device->pending_ops; gIter != NULL; gIter = gIterNext) {
async_command_t *pending_op = gIter->data;
gIterNext = gIter->next;
if (pending_op && pending_op->delay_id) {
crm_trace("Operation '%s'%s%s using %s was asked to run too early, "
"waiting for start delay of %ds",
pending_op->action,
((pending_op->target == NULL)? "" : " targeting "),
pcmk__s(pending_op->target, ""),
device->id, pending_op->start_delay);
continue;
}
device->pending_ops = g_list_remove_link(device->pending_ops, gIter);
g_list_free_1(gIter);
cmd = pending_op;
break;
}
if (cmd == NULL) {
crm_trace("No actions using %s are needed", device->id);
return TRUE;
}
if (pcmk__str_any_of(device->agent, STONITH_WATCHDOG_AGENT,
STONITH_WATCHDOG_AGENT_INTERNAL, NULL)) {
if (pcmk__is_fencing_action(cmd->action)) {
if (node_does_watchdog_fencing(fenced_get_local_node())) {
pcmk__panic("Watchdog self-fencing required");
goto done;
}
} else {
crm_info("Faking success for %s watchdog operation", cmd->action);
report_internal_result(cmd, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
goto done;
}
}
#if PCMK__ENABLE_CIBSECRETS
exec_rc = pcmk__substitute_secrets(device->id, device->params);
if (exec_rc != pcmk_rc_ok) {
if (pcmk__str_eq(cmd->action, PCMK_ACTION_STOP, pcmk__str_none)) {
crm_info("Proceeding with stop operation for %s "
"despite being unable to load CIB secrets (%s)",
device->id, pcmk_rc_str(exec_rc));
} else {
crm_err("Considering %s unconfigured "
"because unable to load CIB secrets: %s",
device->id, pcmk_rc_str(exec_rc));
report_internal_result(cmd, CRM_EX_ERROR, PCMK_EXEC_NO_SECRETS,
"Failed to get CIB secrets");
goto done;
}
}
#endif
action_str = cmd->action;
if (pcmk__str_eq(cmd->action, PCMK_ACTION_REBOOT, pcmk__str_none)
&& !pcmk_is_set(device->flags, fenced_df_supports_reboot)) {
crm_notice("Remapping 'reboot' action%s%s using %s to 'off' "
"because agent '%s' does not support reboot",
((cmd->target == NULL)? "" : " targeting "),
pcmk__s(cmd->target, ""), device->id, device->agent);
action_str = PCMK_ACTION_OFF;
}
action = stonith__action_create(device->agent, action_str, cmd->target,
cmd->timeout, device->params,
device->aliases, device->default_host_arg);
/* for async exec, exec_rc is negative for early error exit
otherwise handling of success/errors is done via callbacks */
cmd->activating_on = device;
exec_rc = stonith__execute_async(action, (void *)cmd, cmd->done_cb,
fork_cb);
if (exec_rc < 0) {
cmd->activating_on = NULL;
cmd->done_cb(0, stonith__action_result(action), cmd);
stonith__destroy_action(action);
}
done:
/* Device might get triggered to work by multiple fencing commands
* simultaneously. Trigger the device again to make sure any
* remaining concurrent commands get executed. */
if (device->pending_ops) {
mainloop_set_trigger(device->work);
}
return TRUE;
}
static gboolean
stonith_device_dispatch(gpointer user_data)
{
return stonith_device_execute(user_data);
}
static gboolean
start_delay_helper(gpointer data)
{
async_command_t *cmd = data;
fenced_device_t *device = cmd_device(cmd);
cmd->delay_id = 0;
if (device) {
mainloop_set_trigger(device->work);
}
return FALSE;
}
static void
schedule_stonith_command(async_command_t *cmd, fenced_device_t *device)
{
int delay_max = 0;
int delay_base = 0;
int requested_delay = cmd->start_delay;
CRM_CHECK(cmd != NULL, return);
CRM_CHECK(device != NULL, return);
if (cmd->device) {
free(cmd->device);
}
cmd->device = pcmk__str_copy(device->id);
cmd->timeout = get_action_timeout(device, cmd->action, cmd->default_timeout);
if (cmd->remote_op_id) {
crm_debug("Scheduling '%s' action%s%s using %s for remote peer %s "
"with op id %.8s and timeout %ds",
cmd->action,
(cmd->target == NULL)? "" : " targeting ",
pcmk__s(cmd->target, ""),
device->id, cmd->origin, cmd->remote_op_id, cmd->timeout);
} else {
crm_debug("Scheduling '%s' action%s%s using %s for %s with timeout %ds",
cmd->action,
(cmd->target == NULL)? "" : " targeting ",
pcmk__s(cmd->target, ""),
device->id, cmd->client, cmd->timeout);
}
device->pending_ops = g_list_append(device->pending_ops, cmd);
mainloop_set_trigger(device->work);
// Value -1 means disable any static/random fencing delays
if (requested_delay < 0) {
return;
}
delay_max = get_action_delay_max(device, cmd->action);
delay_base = get_action_delay_base(device, cmd->action, cmd->target);
if (delay_max == 0) {
delay_max = delay_base;
}
if (delay_max < delay_base) {
crm_warn(PCMK_STONITH_DELAY_BASE " (%ds) is larger than "
PCMK_STONITH_DELAY_MAX " (%ds) for %s using %s "
"(limiting to maximum delay)",
delay_base, delay_max, cmd->action, device->id);
delay_base = delay_max;
}
if (delay_max > 0) {
cmd->start_delay +=
// coverity[dont_call] It doesn't matter here if rand() is predictable
((delay_max != delay_base)?(rand() % (delay_max - delay_base)):0)
+ delay_base;
}
if (cmd->start_delay > 0) {
crm_notice("Delaying '%s' action%s%s using %s for %ds " QB_XS
" timeout=%ds requested_delay=%ds base=%ds max=%ds",
cmd->action,
(cmd->target == NULL)? "" : " targeting ",
pcmk__s(cmd->target, ""),
device->id, cmd->start_delay, cmd->timeout,
requested_delay, delay_base, delay_max);
cmd->delay_id =
pcmk__create_timer(cmd->start_delay * 1000, start_delay_helper, cmd);
}
}
static void
free_device(gpointer data)
{
GList *gIter = NULL;
fenced_device_t *device = data;
g_hash_table_destroy(device->params);
g_hash_table_destroy(device->aliases);
for (gIter = device->pending_ops; gIter != NULL; gIter = gIter->next) {
async_command_t *cmd = gIter->data;
crm_warn("Removal of device '%s' purged operation '%s'", device->id, cmd->action);
report_internal_result(cmd, CRM_EX_ERROR, PCMK_EXEC_NO_FENCE_DEVICE,
"Device was removed before action could be executed");
}
g_list_free(device->pending_ops);
g_list_free_full(device->targets, free);
if (device->timer) {
mainloop_timer_stop(device->timer);
mainloop_timer_del(device->timer);
}
mainloop_destroy_trigger(device->work);
pcmk__xml_free(device->agent_metadata);
free(device->namespace);
if (device->on_target_actions != NULL) {
g_string_free(device->on_target_actions, TRUE);
}
free(device->agent);
free(device->id);
free(device);
}
/*!
* \internal
* \brief Initialize the table of known fence devices
*/
void
fenced_init_device_table(void)
{
if (device_table == NULL) {
device_table = pcmk__strkey_table(NULL, free_device);
}
}
/*!
* \internal
* \brief Free the table of known fence devices
*/
void
fenced_free_device_table(void)
{
if (device_table != NULL) {
g_hash_table_destroy(device_table);
device_table = NULL;
}
}
static GHashTable *
build_port_aliases(const char *hostmap, GList ** targets)
{
char *name = NULL;
int last = 0, lpc = 0, max = 0, added = 0;
GHashTable *aliases = pcmk__strikey_table(free, free);
if (hostmap == NULL) {
return aliases;
}
max = strlen(hostmap);
for (; lpc <= max; lpc++) {
switch (hostmap[lpc]) {
/* Skip escaped chars */
case '\\':
lpc++;
break;
/* Assignment chars */
case '=':
case ':':
if (lpc > last) {
free(name);
name = pcmk__assert_alloc(1, 1 + lpc - last);
memcpy(name, hostmap + last, lpc - last);
}
last = lpc + 1;
break;
/* Delimeter chars */
/* case ',': Potentially used to specify multiple ports */
case 0:
case ';':
case ' ':
case '\t':
if (name) {
char *value = NULL;
int k = 0;
value = pcmk__assert_alloc(1, 1 + lpc - last);
memcpy(value, hostmap + last, lpc - last);
for (int i = 0; value[i] != '\0'; i++) {
if (value[i] != '\\') {
value[k++] = value[i];
}
}
value[k] = '\0';
crm_debug("Adding alias '%s'='%s'", name, value);
g_hash_table_replace(aliases, name, value);
if (targets) {
*targets = g_list_append(*targets, pcmk__str_copy(value));
}
value = NULL;
name = NULL;
added++;
} else if (lpc > last) {
crm_debug("Parse error at offset %d near '%s'", lpc - last, hostmap + last);
}
last = lpc + 1;
break;
}
if (hostmap[lpc] == 0) {
break;
}
}
if (added == 0) {
crm_info("No host mappings detected in '%s'", hostmap);
}
free(name);
return aliases;
}
GHashTable *metadata_cache = NULL;
void
free_metadata_cache(void) {
if (metadata_cache != NULL) {
g_hash_table_destroy(metadata_cache);
metadata_cache = NULL;
}
}
static void
init_metadata_cache(void) {
if (metadata_cache == NULL) {
metadata_cache = pcmk__strkey_table(free, free);
}
}
int
get_agent_metadata(const char *agent, xmlNode ** metadata)
{
char *buffer = NULL;
if (metadata == NULL) {
return EINVAL;
}
*metadata = NULL;
if (pcmk__str_eq(agent, STONITH_WATCHDOG_AGENT_INTERNAL, pcmk__str_none)) {
return pcmk_rc_ok;
}
init_metadata_cache();
buffer = g_hash_table_lookup(metadata_cache, agent);
if (buffer == NULL) {
stonith_t *st = stonith__api_new();
int rc;
if (st == NULL) {
crm_warn("Could not get agent meta-data: "
"API memory allocation failed");
return EAGAIN;
}
rc = st->cmds->metadata(st, st_opt_sync_call, agent,
NULL, &buffer, 10);
stonith__api_free(st);
if (rc || !buffer) {
crm_err("Could not retrieve metadata for fencing agent %s", agent);
return EAGAIN;
}
g_hash_table_replace(metadata_cache, pcmk__str_copy(agent), buffer);
}
*metadata = pcmk__xml_parse(buffer);
return pcmk_rc_ok;
}
static void
read_action_metadata(fenced_device_t *device)
{
xmlXPathObject *xpath = NULL;
int max = 0;
int lpc = 0;
if (device->agent_metadata == NULL) {
return;
}
xpath = pcmk__xpath_search(device->agent_metadata->doc,
"//" PCMK_XE_ACTION);
max = pcmk__xpath_num_results(xpath);
if (max == 0) {
xmlXPathFreeObject(xpath);
return;
}
for (lpc = 0; lpc < max; lpc++) {
const char *action = NULL;
xmlNode *match = pcmk__xpath_result(xpath, lpc);
CRM_LOG_ASSERT(match != NULL);
if(match == NULL) { continue; };
action = crm_element_value(match, PCMK_XA_NAME);
if (pcmk__str_eq(action, PCMK_ACTION_LIST, pcmk__str_none)) {
fenced_device_set_flags(device, fenced_df_supports_list);
} else if (pcmk__str_eq(action, PCMK_ACTION_STATUS, pcmk__str_none)) {
fenced_device_set_flags(device, fenced_df_supports_status);
} else if (pcmk__str_eq(action, PCMK_ACTION_REBOOT, pcmk__str_none)) {
fenced_device_set_flags(device, fenced_df_supports_reboot);
} else if (pcmk__str_eq(action, PCMK_ACTION_ON, pcmk__str_none)) {
/* PCMK_XA_AUTOMATIC means the cluster will unfence a node when it
* joins.
*
* @COMPAT PCMK__XA_REQUIRED is a deprecated synonym for
* PCMK_XA_AUTOMATIC.
*/
if (pcmk__xe_attr_is_true(match, PCMK_XA_AUTOMATIC)
|| pcmk__xe_attr_is_true(match, PCMK__XA_REQUIRED)) {
fenced_device_set_flags(device, fenced_df_auto_unfence);
}
fenced_device_set_flags(device, fenced_df_supports_on);
}
if ((action != NULL)
&& pcmk__xe_attr_is_true(match, PCMK_XA_ON_TARGET)) {
pcmk__add_word(&(device->on_target_actions), 64, action);
}
}
xmlXPathFreeObject(xpath);
}
static const char *
target_list_type(fenced_device_t *dev)
{
const char *check_type = NULL;
check_type = g_hash_table_lookup(dev->params, PCMK_STONITH_HOST_CHECK);
if (check_type == NULL) {
if (g_hash_table_lookup(dev->params, PCMK_STONITH_HOST_LIST)) {
check_type = PCMK_VALUE_STATIC_LIST;
} else if (g_hash_table_lookup(dev->params, PCMK_STONITH_HOST_MAP)) {
check_type = PCMK_VALUE_STATIC_LIST;
} else if (pcmk_is_set(dev->flags, fenced_df_supports_list)) {
check_type = PCMK_VALUE_DYNAMIC_LIST;
} else if (pcmk_is_set(dev->flags, fenced_df_supports_status)) {
check_type = PCMK_VALUE_STATUS;
} else {
check_type = PCMK_VALUE_NONE;
}
}
return check_type;
}
static fenced_device_t *
build_device_from_xml(const xmlNode *dev)
{
const char *value;
fenced_device_t *device = NULL;
char *agent = crm_element_value_copy(dev, PCMK_XA_AGENT);
CRM_CHECK(agent != NULL, return device);
device = pcmk__assert_alloc(1, sizeof(fenced_device_t));
device->id = crm_element_value_copy(dev, PCMK_XA_ID);
device->agent = agent;
device->namespace = crm_element_value_copy(dev, PCMK__XA_NAMESPACE);
device->params = xml2list(dev);
value = g_hash_table_lookup(device->params, PCMK_STONITH_HOST_LIST);
if (value) {
device->targets = stonith__parse_targets(value);
}
value = g_hash_table_lookup(device->params, PCMK_STONITH_HOST_MAP);
device->aliases = build_port_aliases(value, &(device->targets));
value = target_list_type(device);
if (!pcmk__str_eq(value, PCMK_VALUE_STATIC_LIST, pcmk__str_casei)
&& (device->targets != NULL)) {
// device->targets is necessary only with PCMK_VALUE_STATIC_LIST
g_list_free_full(device->targets, free);
device->targets = NULL;
}
switch (get_agent_metadata(device->agent, &device->agent_metadata)) {
case pcmk_rc_ok:
if (device->agent_metadata) {
read_action_metadata(device);
device->default_host_arg =
stonith__default_host_arg(device->agent_metadata);
}
break;
case EAGAIN:
if (device->timer == NULL) {
device->timer = mainloop_timer_add("get_agent_metadata", 10 * 1000,
TRUE, get_agent_metadata_cb, device);
}
if (!mainloop_timer_running(device->timer)) {
mainloop_timer_start(device->timer);
}
break;
default:
break;
}
value = crm_element_value(dev, PCMK__XA_RSC_PROVIDES);
if (pcmk__str_eq(value, PCMK_VALUE_UNFENCING, pcmk__str_casei)) {
fenced_device_set_flags(device, fenced_df_auto_unfence);
}
if (is_action_required(PCMK_ACTION_ON, device)) {
crm_info("Fencing device '%s' requires unfencing", device->id);
}
if (device->on_target_actions != NULL) {
crm_info("Fencing device '%s' requires actions (%s) to be executed "
"on target", device->id,
(const char *) device->on_target_actions->str);
}
device->work = mainloop_add_trigger(G_PRIORITY_HIGH, stonith_device_dispatch, device);
return device;
}
static void
schedule_internal_command(const char *origin, fenced_device_t *device,
const char *action, const char *target, int timeout,
void *internal_user_data,
void (*done_cb) (int pid,
const pcmk__action_result_t *result,
void *user_data))
{
async_command_t *cmd = NULL;
cmd = pcmk__assert_alloc(1, sizeof(async_command_t));
cmd->id = -1;
cmd->default_timeout = timeout ? timeout : 60;
cmd->timeout = cmd->default_timeout;
cmd->action = pcmk__str_copy(action);
cmd->target = pcmk__str_copy(target);
cmd->device = pcmk__str_copy(device->id);
cmd->origin = pcmk__str_copy(origin);
cmd->client = pcmk__str_copy(crm_system_name);
cmd->client_name = pcmk__str_copy(crm_system_name);
cmd->internal_user_data = internal_user_data;
cmd->done_cb = done_cb; /* cmd, not internal_user_data, is passed to 'done_cb' as the userdata */
schedule_stonith_command(cmd, device);
}
// Fence agent status commands use custom exit status codes
enum fence_status_code {
fence_status_invalid = -1,
fence_status_active = 0,
fence_status_unknown = 1,
fence_status_inactive = 2,
};
static void
status_search_cb(int pid, const pcmk__action_result_t *result, void *user_data)
{
async_command_t *cmd = user_data;
struct device_search_s *search = cmd->internal_user_data;
fenced_device_t *dev = cmd_device(cmd);
gboolean can = FALSE;
free_async_command(cmd);
if (!dev) {
search_devices_record_result(search, NULL, FALSE);
return;
}
mainloop_set_trigger(dev->work);
if (result->execution_status != PCMK_EXEC_DONE) {
crm_warn("Assuming %s cannot fence %s "
"because status could not be executed: %s%s%s%s",
dev->id, search->host,
pcmk_exec_status_str(result->execution_status),
((result->exit_reason == NULL)? "" : " ("),
((result->exit_reason == NULL)? "" : result->exit_reason),
((result->exit_reason == NULL)? "" : ")"));
search_devices_record_result(search, dev->id, FALSE);
return;
}
switch (result->exit_status) {
case fence_status_unknown:
crm_trace("%s reported it cannot fence %s", dev->id, search->host);
break;
case fence_status_active:
case fence_status_inactive:
crm_trace("%s reported it can fence %s", dev->id, search->host);
can = TRUE;
break;
default:
crm_warn("Assuming %s cannot fence %s "
"(status returned unknown code %d)",
dev->id, search->host, result->exit_status);
break;
}
search_devices_record_result(search, dev->id, can);
}
static void
dynamic_list_search_cb(int pid, const pcmk__action_result_t *result,
void *user_data)
{
async_command_t *cmd = user_data;
struct device_search_s *search = cmd->internal_user_data;
fenced_device_t *dev = cmd_device(cmd);
gboolean can_fence = FALSE;
free_async_command(cmd);
/* Host/alias must be in the list output to be eligible to be fenced
*
* Will cause problems if down'd nodes aren't listed or (for virtual nodes)
* if the guest is still listed despite being moved to another machine
*/
if (!dev) {
search_devices_record_result(search, NULL, FALSE);
return;
}
mainloop_set_trigger(dev->work);
if (pcmk__result_ok(result)) {
crm_info("Refreshing target list for %s", dev->id);
g_list_free_full(dev->targets, free);
dev->targets = stonith__parse_targets(result->action_stdout);
dev->targets_age = time(NULL);
} else if (dev->targets != NULL) {
if (result->execution_status == PCMK_EXEC_DONE) {
crm_info("Reusing most recent target list for %s "
"because list returned error code %d",
dev->id, result->exit_status);
} else {
crm_info("Reusing most recent target list for %s "
"because list could not be executed: %s%s%s%s",
dev->id, pcmk_exec_status_str(result->execution_status),
((result->exit_reason == NULL)? "" : " ("),
((result->exit_reason == NULL)? "" : result->exit_reason),
((result->exit_reason == NULL)? "" : ")"));
}
} else { // We have never successfully executed list
if (result->execution_status == PCMK_EXEC_DONE) {
crm_warn("Assuming %s cannot fence %s "
"because list returned error code %d",
dev->id, search->host, result->exit_status);
} else {
crm_warn("Assuming %s cannot fence %s "
"because list could not be executed: %s%s%s%s",
dev->id, search->host,
pcmk_exec_status_str(result->execution_status),
((result->exit_reason == NULL)? "" : " ("),
((result->exit_reason == NULL)? "" : result->exit_reason),
((result->exit_reason == NULL)? "" : ")"));
}
/* Fall back to pcmk_host_check=PCMK_VALUE_STATUS if the user didn't
* explicitly specify PCMK_VALUE_DYNAMIC_LIST
*/
if (g_hash_table_lookup(dev->params, PCMK_STONITH_HOST_CHECK) == NULL) {
crm_notice("Switching to pcmk_host_check='status' for %s", dev->id);
pcmk__insert_dup(dev->params, PCMK_STONITH_HOST_CHECK,
PCMK_VALUE_STATUS);
}
}
if (dev->targets) {
const char *alias = g_hash_table_lookup(dev->aliases, search->host);
if (!alias) {
alias = search->host;
}
if (pcmk__str_in_list(alias, dev->targets, pcmk__str_casei)) {
can_fence = TRUE;
}
}
search_devices_record_result(search, dev->id, can_fence);
}
/*!
* \internal
* \brief Returns true if any key in first is not in second or second has a different value for key
*/
static int
device_params_diff(GHashTable *first, GHashTable *second) {
char *key = NULL;
char *value = NULL;
GHashTableIter gIter;
g_hash_table_iter_init(&gIter, first);
while (g_hash_table_iter_next(&gIter, (void **)&key, (void **)&value)) {
if(strstr(key, "CRM_meta") == key) {
continue;
} else if (strcmp(key, PCMK_XA_CRM_FEATURE_SET) == 0) {
continue;
} else {
char *other_value = g_hash_table_lookup(second, key);
if (!other_value || !pcmk__str_eq(other_value, value, pcmk__str_casei)) {
crm_trace("Different value for %s: %s != %s", key, other_value, value);
return 1;
}
}
}
return 0;
}
/*!
* \internal
* \brief Checks to see if an identical device already exists in the table
*/
static fenced_device_t *
device_has_duplicate(const fenced_device_t *device)
{
fenced_device_t *dup = g_hash_table_lookup(device_table, device->id);
if (!dup) {
crm_trace("No match for %s", device->id);
return NULL;
} else if (!pcmk__str_eq(dup->agent, device->agent, pcmk__str_casei)) {
crm_trace("Different agent: %s != %s", dup->agent, device->agent);
return NULL;
}
- // Use pcmk__digest_operation() here?
+ // Find a way to share logic with pcmk__digest_op_params() here?
if (device_params_diff(device->params, dup->params) ||
device_params_diff(dup->params, device->params)) {
return NULL;
}
crm_trace("Match");
return dup;
}
int
fenced_device_register(const xmlNode *dev, bool from_cib)
{
const char *local_node_name = fenced_get_local_node();
fenced_device_t *dup = NULL;
fenced_device_t *device = build_device_from_xml(dev);
int rc = pcmk_rc_ok;
CRM_CHECK(device != NULL, return ENOMEM);
/* do we have a watchdog-device? */
if (pcmk__str_eq(device->id, STONITH_WATCHDOG_ID, pcmk__str_none)
|| pcmk__str_any_of(device->agent, STONITH_WATCHDOG_AGENT,
STONITH_WATCHDOG_AGENT_INTERNAL, NULL)) {
if (stonith_watchdog_timeout_ms <= 0) {
crm_err("Ignoring watchdog fence device without "
PCMK_OPT_STONITH_WATCHDOG_TIMEOUT " set");
rc = ENODEV;
goto done;
}
if (!pcmk__str_any_of(device->agent, STONITH_WATCHDOG_AGENT,
STONITH_WATCHDOG_AGENT_INTERNAL, NULL)) {
crm_err("Ignoring watchdog fence device with unknown agent '%s' "
"rather than '" STONITH_WATCHDOG_AGENT "'",
pcmk__s(device->agent, ""));
rc = ENODEV;
goto done;
}
if (!pcmk__str_eq(device->id, STONITH_WATCHDOG_ID, pcmk__str_none)) {
crm_err("Ignoring watchdog fence device named '%s' rather than "
"'" STONITH_WATCHDOG_ID "'",
pcmk__s(device->id, ""));
rc = ENODEV;
goto done;
}
if (pcmk__str_eq(device->agent, STONITH_WATCHDOG_AGENT,
pcmk__str_none)) {
/* This has either an empty list or the targets configured for
* watchdog fencing
*/
g_list_free_full(stonith_watchdog_targets, free);
stonith_watchdog_targets = device->targets;
device->targets = NULL;
}
if (!node_does_watchdog_fencing(local_node_name)) {
crm_debug("Skip registration of watchdog fence device on node not "
"in host list");
device->targets = NULL;
stonith_device_remove(device->id, from_cib);
goto done;
}
// Proceed as with any other fencing device
g_list_free_full(device->targets, free);
device->targets = stonith__parse_targets(local_node_name);
pcmk__insert_dup(device->params, PCMK_STONITH_HOST_LIST,
local_node_name);
}
dup = device_has_duplicate(device);
if (dup != NULL) {
guint ndevices = g_hash_table_size(device_table);
crm_debug("Device '%s' already in device list (%d active device%s)",
device->id, ndevices, pcmk__plural_s(ndevices));
free_device(device);
device = dup;
fenced_device_clear_flags(device, fenced_df_dirty);
} else {
guint ndevices = 0;
fenced_device_t *old = g_hash_table_lookup(device_table, device->id);
if (from_cib && (old != NULL)
&& pcmk_is_set(old->flags, fenced_df_api_registered)) {
/* If the CIB is writing over an entry that is shared with a stonith
* client, copy any pending ops that currently exist on the old
* entry to the new one. Otherwise the pending ops will be reported
* as failures.
*/
crm_info("Overwriting existing entry for %s from CIB", device->id);
device->pending_ops = old->pending_ops;
fenced_device_set_flags(device, fenced_df_api_registered);
old->pending_ops = NULL;
if (device->pending_ops != NULL) {
mainloop_set_trigger(device->work);
}
}
g_hash_table_replace(device_table, device->id, device);
ndevices = g_hash_table_size(device_table);
crm_notice("Added '%s' to device list (%d active device%s)",
device->id, ndevices, pcmk__plural_s(ndevices));
}
if (from_cib) {
fenced_device_set_flags(device, fenced_df_cib_registered);
} else {
fenced_device_set_flags(device, fenced_df_api_registered);
}
done:
if (rc != pcmk_rc_ok) {
free_device(device);
}
return rc;
}
void
stonith_device_remove(const char *id, bool from_cib)
{
fenced_device_t *device = g_hash_table_lookup(device_table, id);
guint ndevices = 0;
if (device == NULL) {
ndevices = g_hash_table_size(device_table);
crm_info("Device '%s' not found (%u active device%s)", id, ndevices,
pcmk__plural_s(ndevices));
return;
}
if (from_cib) {
fenced_device_clear_flags(device, fenced_df_cib_registered);
} else {
fenced_device_clear_flags(device,
fenced_df_api_registered|fenced_df_verified);
}
if (!pcmk_any_flags_set(device->flags,
fenced_df_api_registered
|fenced_df_cib_registered)) {
g_hash_table_remove(device_table, id);
ndevices = g_hash_table_size(device_table);
crm_info("Removed '%s' from device list (%u active device%s)",
id, ndevices, pcmk__plural_s(ndevices));
} else {
// Exactly one is true at this point
const bool cib_registered = pcmk_is_set(device->flags,
fenced_df_cib_registered);
crm_trace("Not removing '%s' from device list (%u active) because "
"still registered via %s",
id, g_hash_table_size(device_table),
(cib_registered? "CIB" : "API"));
}
}
/*!
* \internal
* \brief Return the number of stonith levels registered for a node
*
* \param[in] tp Node's topology table entry
*
* \return Number of non-NULL levels in topology entry
* \note This function is used only for log messages.
*/
static int
count_active_levels(const stonith_topology_t *tp)
{
int lpc = 0;
int count = 0;
for (lpc = 0; lpc < ST__LEVEL_COUNT; lpc++) {
if (tp->levels[lpc] != NULL) {
count++;
}
}
return count;
}
static void
free_topology_entry(gpointer data)
{
stonith_topology_t *tp = data;
int lpc = 0;
for (lpc = 0; lpc < ST__LEVEL_COUNT; lpc++) {
if (tp->levels[lpc] != NULL) {
g_list_free_full(tp->levels[lpc], free);
}
}
free(tp->target);
free(tp->target_value);
free(tp->target_pattern);
free(tp->target_attribute);
free(tp);
}
void
free_topology_list(void)
{
if (topology != NULL) {
g_hash_table_destroy(topology);
topology = NULL;
}
}
void
init_topology_list(void)
{
if (topology == NULL) {
topology = pcmk__strkey_table(NULL, free_topology_entry);
}
}
char *
stonith_level_key(const xmlNode *level, enum fenced_target_by mode)
{
if (mode == fenced_target_by_unknown) {
mode = unpack_level_kind(level);
}
switch (mode) {
case fenced_target_by_name:
return crm_element_value_copy(level, PCMK_XA_TARGET);
case fenced_target_by_pattern:
return crm_element_value_copy(level, PCMK_XA_TARGET_PATTERN);
case fenced_target_by_attribute:
return crm_strdup_printf("%s=%s",
crm_element_value(level, PCMK_XA_TARGET_ATTRIBUTE),
crm_element_value(level, PCMK_XA_TARGET_VALUE));
default:
return crm_strdup_printf("unknown-%s", pcmk__xe_id(level));
}
}
/*!
* \internal
* \brief Parse target identification from topology level XML
*
* \param[in] level Topology level XML to parse
*
* \return How to identify target of \p level
*/
static enum fenced_target_by
unpack_level_kind(const xmlNode *level)
{
if (crm_element_value(level, PCMK_XA_TARGET) != NULL) {
return fenced_target_by_name;
}
if (crm_element_value(level, PCMK_XA_TARGET_PATTERN) != NULL) {
return fenced_target_by_pattern;
}
if ((crm_element_value(level, PCMK_XA_TARGET_ATTRIBUTE) != NULL)
&& (crm_element_value(level, PCMK_XA_TARGET_VALUE) != NULL)) {
return fenced_target_by_attribute;
}
return fenced_target_by_unknown;
}
/*!
* \internal
* \brief Unpack essential information from topology request XML
*
* \param[in] xml Request XML to search
* \param[out] mode If not NULL, where to store level kind
* \param[out] target If not NULL, where to store representation of target
* \param[out] id If not NULL, where to store level number
*
* \return Topology level XML from within \p xml, or NULL if not found
* \note The caller is responsible for freeing \p *target if set.
*/
static xmlNode *
unpack_level_request(xmlNode *xml, enum fenced_target_by *mode, char **target,
int *id)
{
enum fenced_target_by local_mode = fenced_target_by_unknown;
char *local_target = NULL;
int local_id = 0;
/* The level element can be the top element or lower. If top level, don't
* search by xpath, because it might give multiple hits if the XML is the
* CIB.
*/
if ((xml != NULL) && !pcmk__xe_is(xml, PCMK_XE_FENCING_LEVEL)) {
xml = pcmk__xpath_find_one(xml->doc, "//" PCMK_XE_FENCING_LEVEL,
LOG_WARNING);
}
if (xml != NULL) {
local_mode = unpack_level_kind(xml);
local_target = stonith_level_key(xml, local_mode);
crm_element_value_int(xml, PCMK_XA_INDEX, &local_id);
}
if (mode != NULL) {
*mode = local_mode;
}
if (id != NULL) {
*id = local_id;
}
if (target != NULL) {
*target = local_target;
} else {
free(local_target);
}
return xml;
}
/*!
* \internal
* \brief Register a fencing topology level for a target
*
* Given an XML request specifying the target name, level index, and device IDs
* for the level, this will create an entry for the target in the global topology
* table if one does not already exist, then append the specified device IDs to
* the entry's device list for the specified level.
*
* \param[in] msg XML request for STONITH level registration
* \param[out] result Where to set result of registration (can be \c NULL)
*/
void
fenced_register_level(xmlNode *msg, pcmk__action_result_t *result)
{
int id = 0;
xmlNode *level;
enum fenced_target_by mode;
char *target;
stonith_topology_t *tp;
const char *value = NULL;
CRM_CHECK(msg != NULL, return);
level = unpack_level_request(msg, &mode, &target, &id);
if (level == NULL) {
set_bad_request_result(result);
return;
}
// Ensure an ID was given (even the client API adds an ID)
if (pcmk__str_empty(pcmk__xe_id(level))) {
crm_warn("Ignoring registration for topology level without ID");
free(target);
crm_log_xml_trace(level, "Bad level");
pcmk__format_result(result, CRM_EX_INVALID_PARAM, PCMK_EXEC_INVALID,
"Topology level is invalid without ID");
return;
}
// Ensure a valid target was specified
if (mode == fenced_target_by_unknown) {
crm_warn("Ignoring registration for topology level '%s' "
"without valid target", pcmk__xe_id(level));
free(target);
crm_log_xml_trace(level, "Bad level");
pcmk__format_result(result, CRM_EX_INVALID_PARAM, PCMK_EXEC_INVALID,
"Invalid target for topology level '%s'",
pcmk__xe_id(level));
return;
}
// Ensure level ID is in allowed range
if ((id < ST__LEVEL_MIN) || (id > ST__LEVEL_MAX)) {
crm_warn("Ignoring topology registration for %s with invalid level %d",
target, id);
free(target);
crm_log_xml_trace(level, "Bad level");
pcmk__format_result(result, CRM_EX_INVALID_PARAM, PCMK_EXEC_INVALID,
"Invalid level number '%s' for topology level '%s'",
pcmk__s(crm_element_value(level, PCMK_XA_INDEX),
""),
pcmk__xe_id(level));
return;
}
/* Find or create topology table entry */
tp = g_hash_table_lookup(topology, target);
if (tp == NULL) {
tp = pcmk__assert_alloc(1, sizeof(stonith_topology_t));
tp->kind = mode;
tp->target = target;
tp->target_value = crm_element_value_copy(level, PCMK_XA_TARGET_VALUE);
tp->target_pattern = crm_element_value_copy(level,
PCMK_XA_TARGET_PATTERN);
tp->target_attribute = crm_element_value_copy(level,
PCMK_XA_TARGET_ATTRIBUTE);
g_hash_table_replace(topology, tp->target, tp);
crm_trace("Added %s (%d) to the topology (%d active entries)",
target, (int) mode, g_hash_table_size(topology));
} else {
free(target);
}
if (tp->levels[id] != NULL) {
crm_info("Adding to the existing %s[%d] topology entry",
tp->target, id);
}
value = crm_element_value(level, PCMK_XA_DEVICES);
if (value != NULL) {
/* Empty string and whitespace are not possible with schema validation
* enabled. Don't bother handling them specially here.
*/
gchar **devices = g_strsplit(value, ",", 0);
for (char **dev = devices; (dev != NULL) && (*dev != NULL); dev++) {
crm_trace("Adding device '%s' for %s[%d]", *dev, tp->target, id);
tp->levels[id] = g_list_append(tp->levels[id],
pcmk__str_copy(*dev));
}
g_strfreev(devices);
}
{
int nlevels = count_active_levels(tp);
crm_info("Target %s has %d active fencing level%s",
tp->target, nlevels, pcmk__plural_s(nlevels));
}
pcmk__set_result(result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
}
/*!
* \internal
* \brief Unregister a fencing topology level for a target
*
* Given an XML request specifying the target name and level index (or 0 for all
* levels), this will remove any corresponding entry for the target from the
* global topology table.
*
* \param[in] msg XML request for STONITH level registration
* \param[out] result Where to set result of unregistration (can be \c NULL)
*/
void
fenced_unregister_level(xmlNode *msg, pcmk__action_result_t *result)
{
int id = -1;
stonith_topology_t *tp;
char *target;
xmlNode *level = NULL;
level = unpack_level_request(msg, NULL, &target, &id);
if (level == NULL) {
set_bad_request_result(result);
return;
}
// Ensure level ID is in allowed range
if ((id < 0) || (id >= ST__LEVEL_COUNT)) {
crm_warn("Ignoring topology unregistration for %s with invalid level %d",
target, id);
free(target);
crm_log_xml_trace(level, "Bad level");
pcmk__format_result(result, CRM_EX_INVALID_PARAM, PCMK_EXEC_INVALID,
"Invalid level number '%s' for topology level %s",
pcmk__s(crm_element_value(level, PCMK_XA_INDEX),
"<null>"),
// Client API doesn't add ID to unregistration XML
pcmk__s(pcmk__xe_id(level), ""));
return;
}
tp = g_hash_table_lookup(topology, target);
if (tp == NULL) {
guint nentries = g_hash_table_size(topology);
crm_info("No fencing topology found for %s (%d active %s)",
target, nentries,
pcmk__plural_alt(nentries, "entry", "entries"));
} else if (id == 0 && g_hash_table_remove(topology, target)) {
guint nentries = g_hash_table_size(topology);
crm_info("Removed all fencing topology entries related to %s "
"(%d active %s remaining)", target, nentries,
pcmk__plural_alt(nentries, "entry", "entries"));
} else if (tp->levels[id] != NULL) {
guint nlevels;
g_list_free_full(tp->levels[id], free);
tp->levels[id] = NULL;
nlevels = count_active_levels(tp);
crm_info("Removed level %d from fencing topology for %s "
"(%d active level%s remaining)",
id, target, nlevels, pcmk__plural_s(nlevels));
}
free(target);
pcmk__set_result(result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
}
static char *
list_to_string(GList *list, const char *delim, gboolean terminate_with_delim)
{
int max = g_list_length(list);
size_t delim_len = delim?strlen(delim):0;
size_t alloc_size = 1 + (max?((max-1+(terminate_with_delim?1:0))*delim_len):0);
char *rv;
GList *gIter;
char *pos = NULL;
const char *lead_delim = "";
for (gIter = list; gIter != NULL; gIter = gIter->next) {
const char *value = (const char *) gIter->data;
alloc_size += strlen(value);
}
rv = pcmk__assert_alloc(alloc_size, sizeof(char));
pos = rv;
for (gIter = list; gIter != NULL; gIter = gIter->next) {
const char *value = (const char *) gIter->data;
pos = &pos[sprintf(pos, "%s%s", lead_delim, value)];
lead_delim = delim;
}
if (max && terminate_with_delim) {
sprintf(pos, "%s", delim);
}
return rv;
}
/*!
* \internal
* \brief Execute a fence agent action directly (and asynchronously)
*
* Handle a STONITH_OP_EXEC API message by scheduling a requested agent action
* directly on a specified device. Only list, monitor, and status actions are
* expected to use this call, though it should work with any agent command.
*
* \param[in] msg Request XML specifying action
* \param[out] result Where to store result of action
*
* \note If the action is monitor, the device must be registered via the API
* (CIB registration is not sufficient), because monitor should not be
* possible unless the device is "started" (API registered).
*/
static void
execute_agent_action(xmlNode *msg, pcmk__action_result_t *result)
{
xmlNode *dev = pcmk__xpath_find_one(msg->doc, "//" PCMK__XE_ST_DEVICE_ID,
LOG_ERR);
xmlNode *op = pcmk__xpath_find_one(msg->doc,
"//*[@" PCMK__XA_ST_DEVICE_ACTION "]",
LOG_ERR);
const char *id = crm_element_value(dev, PCMK__XA_ST_DEVICE_ID);
const char *action = crm_element_value(op, PCMK__XA_ST_DEVICE_ACTION);
async_command_t *cmd = NULL;
fenced_device_t *device = NULL;
if ((id == NULL) || (action == NULL)) {
crm_info("Malformed API action request: device %s, action %s",
(id? id : "not specified"),
(action? action : "not specified"));
set_bad_request_result(result);
return;
}
if (pcmk__str_eq(id, STONITH_WATCHDOG_ID, pcmk__str_none)) {
// Watchdog agent actions are implemented internally
if (stonith_watchdog_timeout_ms <= 0) {
pcmk__set_result(result, CRM_EX_ERROR, PCMK_EXEC_NO_FENCE_DEVICE,
"Watchdog fence device not configured");
return;
} else if (pcmk__str_eq(action, PCMK_ACTION_LIST, pcmk__str_none)) {
pcmk__set_result(result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
pcmk__set_result_output(result,
list_to_string(stonith_watchdog_targets,
"\n", TRUE),
NULL);
return;
} else if (pcmk__str_eq(action, PCMK_ACTION_MONITOR, pcmk__str_none)) {
pcmk__set_result(result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
return;
}
}
device = g_hash_table_lookup(device_table, id);
if (device == NULL) {
crm_info("Ignoring API '%s' action request because device %s not found",
action, id);
pcmk__format_result(result, CRM_EX_ERROR, PCMK_EXEC_NO_FENCE_DEVICE,
"'%s' not found", id);
return;
} else if (!pcmk_is_set(device->flags, fenced_df_api_registered)
&& (strcmp(action, PCMK_ACTION_MONITOR) == 0)) {
// Monitors may run only on "started" (API-registered) devices
crm_info("Ignoring API '%s' action request because device %s not active",
action, id);
pcmk__format_result(result, CRM_EX_ERROR, PCMK_EXEC_NO_FENCE_DEVICE,
"'%s' not active", id);
return;
}
cmd = create_async_command(msg);
if (cmd == NULL) {
crm_log_xml_warn(msg, "invalid");
set_bad_request_result(result);
return;
}
schedule_stonith_command(cmd, device);
pcmk__set_result(result, CRM_EX_OK, PCMK_EXEC_PENDING, NULL);
}
static void
search_devices_record_result(struct device_search_s *search, const char *device, gboolean can_fence)
{
search->replies_received++;
if (can_fence && device) {
if (search->support_action_only != fenced_df_none) {
fenced_device_t *dev = g_hash_table_lookup(device_table, device);
if (dev && !pcmk_is_set(dev->flags, search->support_action_only)) {
return;
}
}
search->capable = g_list_append(search->capable,
pcmk__str_copy(device));
}
if (search->replies_needed == search->replies_received) {
guint ndevices = g_list_length(search->capable);
crm_debug("Search found %d device%s that can perform '%s' targeting %s",
ndevices, pcmk__plural_s(ndevices),
(search->action? search->action : "unknown action"),
(search->host? search->host : "any node"));
search->callback(search->capable, search->user_data);
free(search->host);
free(search->action);
free(search);
}
}
/*!
* \internal
* \brief Check whether the local host is allowed to execute a fencing action
*
* \param[in] device Fence device to check
* \param[in] action Fence action to check
* \param[in] target Hostname of fence target
* \param[in] allow_self Whether self-fencing is allowed for this operation
*
* \return TRUE if local host is allowed to execute action, FALSE otherwise
*/
static gboolean
localhost_is_eligible(const fenced_device_t *device, const char *action,
const char *target, gboolean allow_self)
{
gboolean localhost_is_target = pcmk__str_eq(target, fenced_get_local_node(),
pcmk__str_casei);
if ((device != NULL) && (action != NULL)
&& (device->on_target_actions != NULL)
&& (strstr((const char*) device->on_target_actions->str,
action) != NULL)) {
if (!localhost_is_target) {
crm_trace("Operation '%s' using %s can only be executed for local "
"host, not %s", action, device->id, target);
return FALSE;
}
} else if (localhost_is_target && !allow_self) {
crm_trace("'%s' operation does not support self-fencing", action);
return FALSE;
}
return TRUE;
}
/*!
* \internal
* \brief Check if local node is allowed to execute (possibly remapped) action
*
* \param[in] device Fence device to check
* \param[in] action Fence action to check
* \param[in] target Node name of fence target
* \param[in] allow_self Whether self-fencing is allowed for this operation
*
* \return true if local node is allowed to execute \p action or any actions it
* might be remapped to, otherwise false
*/
static bool
localhost_is_eligible_with_remap(const fenced_device_t *device,
const char *action, const char *target,
gboolean allow_self)
{
// Check exact action
if (localhost_is_eligible(device, action, target, allow_self)) {
return true;
}
// Check potential remaps
if (pcmk__str_eq(action, PCMK_ACTION_REBOOT, pcmk__str_none)) {
/* "reboot" might get remapped to "off" then "on", so even if reboot is
* disallowed, return true if either of those is allowed. We'll report
* the disallowed actions with the results. We never allow self-fencing
* for remapped "on" actions because the target is off at that point.
*/
if (localhost_is_eligible(device, PCMK_ACTION_OFF, target, allow_self)
|| localhost_is_eligible(device, PCMK_ACTION_ON, target, FALSE)) {
return true;
}
}
return false;
}
/*!
* \internal
* \brief Check whether we can use a device's cached target list
*
* \param[in] dev Fencing device to check
*
* \return \c true if \p dev cached its targets less than a minute ago,
* otherwise \c false
*/
static inline bool
can_use_target_cache(const fenced_device_t *dev)
{
return (dev->targets != NULL) && (time(NULL) < (dev->targets_age + 60));
}
static void
can_fence_host_with_device(fenced_device_t *dev,
struct device_search_s *search)
{
gboolean can = FALSE;
const char *check_type = "Internal bug";
const char *target = NULL;
const char *alias = NULL;
const char *dev_id = "Unspecified device";
const char *action = (search == NULL)? NULL : search->action;
CRM_CHECK((dev != NULL) && (action != NULL), goto search_report_results);
if (dev->id != NULL) {
dev_id = dev->id;
}
target = search->host;
if (target == NULL) {
can = TRUE;
check_type = "No target";
goto search_report_results;
}
/* Answer immediately if the device does not support the action
* or the local node is not allowed to perform it
*/
if (pcmk__str_eq(action, PCMK_ACTION_ON, pcmk__str_none)
&& !pcmk_is_set(dev->flags, fenced_df_supports_on)) {
check_type = "Agent does not support 'on'";
goto search_report_results;
} else if (!localhost_is_eligible_with_remap(dev, action, target,
search->allow_self)) {
check_type = "This node is not allowed to execute action";
goto search_report_results;
}
// Check eligibility as specified by pcmk_host_check
check_type = target_list_type(dev);
alias = g_hash_table_lookup(dev->aliases, target);
if (pcmk__str_eq(check_type, PCMK_VALUE_NONE, pcmk__str_casei)) {
can = TRUE;
} else if (pcmk__str_eq(check_type, PCMK_VALUE_STATIC_LIST,
pcmk__str_casei)) {
if (pcmk__str_in_list(target, dev->targets, pcmk__str_casei)) {
can = TRUE;
} else if (g_hash_table_lookup(dev->params, PCMK_STONITH_HOST_MAP)
&& g_hash_table_lookup(dev->aliases, target)) {
can = TRUE;
}
} else if (pcmk__str_eq(check_type, PCMK_VALUE_DYNAMIC_LIST,
pcmk__str_casei)) {
if (!can_use_target_cache(dev)) {
int device_timeout = get_action_timeout(dev, PCMK_ACTION_LIST,
search->per_device_timeout);
if (device_timeout > search->per_device_timeout) {
crm_notice("Since the pcmk_list_timeout (%ds) parameter of %s "
"is larger than " PCMK_OPT_STONITH_TIMEOUT
" (%ds), timeout may occur",
device_timeout, dev_id, search->per_device_timeout);
}
crm_trace("Running '%s' to check whether %s is eligible to fence %s (%s)",
check_type, dev_id, target, action);
schedule_internal_command(__func__, dev, PCMK_ACTION_LIST, NULL,
search->per_device_timeout, search, dynamic_list_search_cb);
/* we'll respond to this search request async in the cb */
return;
}
if (pcmk__str_in_list(((alias == NULL)? target : alias), dev->targets,
pcmk__str_casei)) {
can = TRUE;
}
} else if (pcmk__str_eq(check_type, PCMK_VALUE_STATUS, pcmk__str_casei)) {
int device_timeout = get_action_timeout(dev, check_type, search->per_device_timeout);
if (device_timeout > search->per_device_timeout) {
crm_notice("Since the pcmk_status_timeout (%ds) parameter of %s is "
"larger than " PCMK_OPT_STONITH_TIMEOUT " (%ds), "
"timeout may occur",
device_timeout, dev_id, search->per_device_timeout);
}
crm_trace("Running '%s' to check whether %s is eligible to fence %s (%s)",
check_type, dev_id, target, action);
schedule_internal_command(__func__, dev, PCMK_ACTION_STATUS, target,
search->per_device_timeout, search, status_search_cb);
/* we'll respond to this search request async in the cb */
return;
} else {
crm_err("Invalid value for " PCMK_STONITH_HOST_CHECK ": %s", check_type);
check_type = "Invalid " PCMK_STONITH_HOST_CHECK;
}
search_report_results:
crm_info("%s is%s eligible to fence (%s) %s%s%s%s: %s",
dev_id, (can? "" : " not"), pcmk__s(action, "unspecified action"),
pcmk__s(target, "unspecified target"),
(alias == NULL)? "" : " (as '", pcmk__s(alias, ""),
(alias == NULL)? "" : "')", check_type);
search_devices_record_result(search, ((dev == NULL)? NULL : dev_id), can);
}
static void
search_devices(gpointer key, gpointer value, gpointer user_data)
{
fenced_device_t *dev = value;
struct device_search_s *search = user_data;
can_fence_host_with_device(dev, search);
}
#define DEFAULT_QUERY_TIMEOUT 20
static void
get_capable_devices(const char *host, const char *action, int timeout,
bool allow_self, void *user_data,
void (*callback) (GList * devices, void *user_data),
uint32_t support_action_only)
{
struct device_search_s *search;
guint ndevices = g_hash_table_size(device_table);
if (ndevices == 0) {
callback(NULL, user_data);
return;
}
search = pcmk__assert_alloc(1, sizeof(struct device_search_s));
search->host = pcmk__str_copy(host);
search->action = pcmk__str_copy(action);
search->per_device_timeout = timeout;
search->allow_self = allow_self;
search->callback = callback;
search->user_data = user_data;
search->support_action_only = support_action_only;
/* We are guaranteed this many replies, even if a device is
* unregistered while the search is in progress.
*/
search->replies_needed = ndevices;
crm_debug("Searching %d device%s to see which can execute '%s' targeting %s",
ndevices, pcmk__plural_s(ndevices),
(search->action? search->action : "unknown action"),
(search->host? search->host : "any node"));
fenced_foreach_device(search_devices, search);
}
struct st_query_data {
xmlNode *reply;
char *remote_peer;
char *client_id;
char *target;
char *action;
int call_options;
};
/*!
* \internal
* \brief Add action-specific attributes to query reply XML
*
* \param[in,out] xml XML to add attributes to
* \param[in] action Fence action
* \param[in] device Fence device
* \param[in] target Fence target
*/
static void
add_action_specific_attributes(xmlNode *xml, const char *action,
const fenced_device_t *device,
const char *target)
{
int action_specific_timeout;
int delay_max;
int delay_base;
CRM_CHECK(xml && action && device, return);
// PCMK__XA_ST_REQUIRED is currently used only for unfencing
if (is_action_required(action, device)) {
crm_trace("Action '%s' is required using %s", action, device->id);
crm_xml_add_int(xml, PCMK__XA_ST_REQUIRED, 1);
}
// pcmk_<action>_timeout if configured
action_specific_timeout = get_action_timeout(device, action, 0);
if (action_specific_timeout) {
crm_trace("Action '%s' has timeout %ds using %s",
action, action_specific_timeout, device->id);
crm_xml_add_int(xml, PCMK__XA_ST_ACTION_TIMEOUT,
action_specific_timeout);
}
delay_max = get_action_delay_max(device, action);
if (delay_max > 0) {
crm_trace("Action '%s' has maximum random delay %ds using %s",
action, delay_max, device->id);
crm_xml_add_int(xml, PCMK__XA_ST_DELAY_MAX, delay_max);
}
delay_base = get_action_delay_base(device, action, target);
if (delay_base > 0) {
crm_xml_add_int(xml, PCMK__XA_ST_DELAY_BASE, delay_base);
}
if ((delay_max > 0) && (delay_base == 0)) {
crm_trace("Action '%s' has maximum random delay %ds using %s",
action, delay_max, device->id);
} else if ((delay_max == 0) && (delay_base > 0)) {
crm_trace("Action '%s' has a static delay of %ds using %s",
action, delay_base, device->id);
} else if ((delay_max > 0) && (delay_base > 0)) {
crm_trace("Action '%s' has a minimum delay of %ds and a randomly chosen "
"maximum delay of %ds using %s",
action, delay_base, delay_max, device->id);
}
}
/*!
* \internal
* \brief Add "disallowed" attribute to query reply XML if appropriate
*
* \param[in,out] xml XML to add attribute to
* \param[in] action Fence action
* \param[in] device Fence device
* \param[in] target Fence target
* \param[in] allow_self Whether self-fencing is allowed
*/
static void
add_disallowed(xmlNode *xml, const char *action, const fenced_device_t *device,
const char *target, gboolean allow_self)
{
if (!localhost_is_eligible(device, action, target, allow_self)) {
crm_trace("Action '%s' using %s is disallowed for local host",
action, device->id);
pcmk__xe_set_bool_attr(xml, PCMK__XA_ST_ACTION_DISALLOWED, true);
}
}
/*!
* \internal
* \brief Add child element with action-specific values to query reply XML
*
* \param[in,out] xml XML to add attribute to
* \param[in] action Fence action
* \param[in] device Fence device
* \param[in] target Fence target
* \param[in] allow_self Whether self-fencing is allowed
*/
static void
add_action_reply(xmlNode *xml, const char *action,
const fenced_device_t *device, const char *target,
gboolean allow_self)
{
xmlNode *child = pcmk__xe_create(xml, PCMK__XE_ST_DEVICE_ACTION);
crm_xml_add(child, PCMK_XA_ID, action);
add_action_specific_attributes(child, action, device, target);
add_disallowed(child, action, device, target, allow_self);
}
/*!
* \internal
* \brief Send a reply to a CPG peer or IPC client
*
* \param[in] reply XML reply to send
* \param[in] call_options Send synchronously if st_opt_sync_call is set
* \param[in] remote_peer If not NULL, name of peer node to send CPG reply
* \param[in,out] client If not NULL, client to send IPC reply
*/
static void
stonith_send_reply(const xmlNode *reply, int call_options,
const char *remote_peer, pcmk__client_t *client)
{
CRM_CHECK((reply != NULL) && ((remote_peer != NULL) || (client != NULL)),
return);
if (remote_peer == NULL) {
do_local_reply(reply, client, call_options);
} else {
const pcmk__node_status_t *node =
pcmk__get_node(0, remote_peer, NULL,
pcmk__node_search_cluster_member);
pcmk__cluster_send_message(node, pcmk_ipc_fenced, reply);
}
}
static void
stonith_query_capable_device_cb(GList * devices, void *user_data)
{
struct st_query_data *query = user_data;
int available_devices = 0;
xmlNode *wrapper = NULL;
xmlNode *list = NULL;
GList *lpc = NULL;
pcmk__client_t *client = NULL;
if (query->client_id != NULL) {
client = pcmk__find_client_by_id(query->client_id);
if ((client == NULL) && (query->remote_peer == NULL)) {
crm_trace("Skipping reply to %s: no longer a client",
query->client_id);
goto done;
}
}
// Pack the results into XML
wrapper = pcmk__xe_create(query->reply, PCMK__XE_ST_CALLDATA);
list = pcmk__xe_create(wrapper, __func__);
crm_xml_add(list, PCMK__XA_ST_TARGET, query->target);
for (lpc = devices; lpc != NULL; lpc = lpc->next) {
fenced_device_t *device = g_hash_table_lookup(device_table, lpc->data);
const char *action = query->action;
xmlNode *dev = NULL;
if (!device) {
/* It is possible the device got unregistered while
* determining who can fence the target */
continue;
}
available_devices++;
dev = pcmk__xe_create(list, PCMK__XE_ST_DEVICE_ID);
crm_xml_add(dev, PCMK_XA_ID, device->id);
crm_xml_add(dev, PCMK__XA_NAMESPACE, device->namespace);
crm_xml_add(dev, PCMK_XA_AGENT, device->agent);
// Has had successful monitor, list, or status on this node
crm_xml_add_int(dev, PCMK__XA_ST_MONITOR_VERIFIED,
pcmk_is_set(device->flags, fenced_df_verified));
crm_xml_add_int(dev, PCMK__XA_ST_DEVICE_SUPPORT_FLAGS, device->flags);
/* If the originating fencer wants to reboot the node, and we have a
* capable device that doesn't support "reboot", remap to "off" instead.
*/
if (!pcmk_is_set(device->flags, fenced_df_supports_reboot)
&& pcmk__str_eq(query->action, PCMK_ACTION_REBOOT,
pcmk__str_none)) {
crm_trace("%s doesn't support reboot, using values for off instead",
device->id);
action = PCMK_ACTION_OFF;
}
/* Add action-specific values if available */
add_action_specific_attributes(dev, action, device, query->target);
if (pcmk__str_eq(query->action, PCMK_ACTION_REBOOT, pcmk__str_none)) {
/* A "reboot" *might* get remapped to "off" then "on", so after
* sending the "reboot"-specific values in the main element, we add
* sub-elements for "off" and "on" values.
*
* We short-circuited earlier if "reboot", "off" and "on" are all
* disallowed for the local host. However if only one or two are
* disallowed, we send back the results and mark which ones are
* disallowed. If "reboot" is disallowed, this might cause problems
* with older fencer versions, which won't check for it. Older
* versions will ignore "off" and "on", so they are not a problem.
*/
add_disallowed(dev, action, device, query->target,
pcmk_is_set(query->call_options,
st_opt_allow_self_fencing));
add_action_reply(dev, PCMK_ACTION_OFF, device, query->target,
pcmk_is_set(query->call_options,
st_opt_allow_self_fencing));
add_action_reply(dev, PCMK_ACTION_ON, device, query->target, FALSE);
}
/* A query without a target wants device parameters */
if (query->target == NULL) {
xmlNode *attrs = pcmk__xe_create(dev, PCMK__XE_ATTRIBUTES);
g_hash_table_foreach(device->params, hash2field, attrs);
}
}
crm_xml_add_int(list, PCMK__XA_ST_AVAILABLE_DEVICES, available_devices);
if (query->target) {
crm_debug("Found %d matching device%s for target '%s'",
available_devices, pcmk__plural_s(available_devices),
query->target);
} else {
crm_debug("%d device%s installed",
available_devices, pcmk__plural_s(available_devices));
}
crm_log_xml_trace(list, "query-result");
stonith_send_reply(query->reply, query->call_options, query->remote_peer,
client);
done:
pcmk__xml_free(query->reply);
free(query->remote_peer);
free(query->client_id);
free(query->target);
free(query->action);
free(query);
g_list_free_full(devices, free);
}
/*!
* \internal
* \brief Log the result of an asynchronous command
*
* \param[in] cmd Command the result is for
* \param[in] result Result of command
* \param[in] pid Process ID of command, if available
* \param[in] next Alternate device that will be tried if command failed
* \param[in] op_merged Whether this command was merged with an earlier one
*/
static void
log_async_result(const async_command_t *cmd,
const pcmk__action_result_t *result,
int pid, const char *next, bool op_merged)
{
int log_level = LOG_ERR;
int output_log_level = LOG_NEVER;
guint devices_remaining = g_list_length(cmd->next_device_iter);
GString *msg = g_string_sized_new(80); // Reasonable starting size
// Choose log levels appropriately if we have a result
if (pcmk__result_ok(result)) {
log_level = (cmd->target == NULL)? LOG_DEBUG : LOG_NOTICE;
if ((result->action_stdout != NULL)
&& !pcmk__str_eq(cmd->action, PCMK_ACTION_METADATA,
pcmk__str_none)) {
output_log_level = LOG_DEBUG;
}
next = NULL;
} else {
log_level = (cmd->target == NULL)? LOG_NOTICE : LOG_ERR;
if ((result->action_stdout != NULL)
&& !pcmk__str_eq(cmd->action, PCMK_ACTION_METADATA,
pcmk__str_none)) {
output_log_level = LOG_WARNING;
}
}
// Build the log message piece by piece
pcmk__g_strcat(msg, "Operation '", cmd->action, "' ", NULL);
if (pid != 0) {
g_string_append_printf(msg, "[%d] ", pid);
}
if (cmd->target != NULL) {
pcmk__g_strcat(msg, "targeting ", cmd->target, " ", NULL);
}
if (cmd->device != NULL) {
pcmk__g_strcat(msg, "using ", cmd->device, " ", NULL);
}
// Add exit status or execution status as appropriate
if (result->execution_status == PCMK_EXEC_DONE) {
g_string_append_printf(msg, "returned %d", result->exit_status);
} else {
pcmk__g_strcat(msg, "could not be executed: ",
pcmk_exec_status_str(result->execution_status), NULL);
}
// Add exit reason and next device if appropriate
if (result->exit_reason != NULL) {
pcmk__g_strcat(msg, " (", result->exit_reason, ")", NULL);
}
if (next != NULL) {
pcmk__g_strcat(msg, ", retrying with ", next, NULL);
}
if (devices_remaining > 0) {
g_string_append_printf(msg, " (%u device%s remaining)",
(unsigned int) devices_remaining,
pcmk__plural_s(devices_remaining));
}
g_string_append_printf(msg, " " QB_XS " %scall %d from %s",
(op_merged? "merged " : ""), cmd->id,
cmd->client_name);
// Log the result
do_crm_log(log_level, "%s", msg->str);
g_string_free(msg, TRUE);
// Log the output (which may have multiple lines), if appropriate
if (output_log_level != LOG_NEVER) {
char *prefix = crm_strdup_printf("%s[%d]", cmd->device, pid);
crm_log_output(output_log_level, prefix, result->action_stdout);
free(prefix);
}
}
/*!
* \internal
* \brief Reply to requester after asynchronous command completion
*
* \param[in] cmd Command that completed
* \param[in] result Result of command
* \param[in] pid Process ID of command, if available
* \param[in] merged If true, command was merged with another, not executed
*/
static void
send_async_reply(const async_command_t *cmd, const pcmk__action_result_t *result,
int pid, bool merged)
{
xmlNode *reply = NULL;
pcmk__client_t *client = NULL;
CRM_CHECK((cmd != NULL) && (result != NULL), return);
log_async_result(cmd, result, pid, NULL, merged);
if (cmd->client != NULL) {
client = pcmk__find_client_by_id(cmd->client);
if ((client == NULL) && (cmd->origin == NULL)) {
crm_trace("Skipping reply to %s: no longer a client", cmd->client);
return;
}
}
reply = construct_async_reply(cmd, result);
if (merged) {
pcmk__xe_set_bool_attr(reply, PCMK__XA_ST_OP_MERGED, true);
}
if (pcmk__is_fencing_action(cmd->action)
&& pcmk__str_eq(cmd->origin, cmd->target, pcmk__str_casei)) {
/* The target was also the originator, so broadcast the result on its
* behalf (since it will be unable to).
*/
crm_trace("Broadcast '%s' result for %s (target was also originator)",
cmd->action, cmd->target);
crm_xml_add(reply, PCMK__XA_SUBT, PCMK__VALUE_BROADCAST);
crm_xml_add(reply, PCMK__XA_ST_OP, STONITH_OP_NOTIFY);
pcmk__cluster_send_message(NULL, pcmk_ipc_fenced, reply);
} else {
// Reply only to the originator
stonith_send_reply(reply, cmd->options, cmd->origin, client);
}
crm_log_xml_trace(reply, "Reply");
pcmk__xml_free(reply);
}
static void
cancel_stonith_command(async_command_t * cmd)
{
fenced_device_t *device = cmd_device(cmd);
if (device) {
crm_trace("Cancel scheduled '%s' action using %s",
cmd->action, device->id);
device->pending_ops = g_list_remove(device->pending_ops, cmd);
}
}
/*!
* \internal
* \brief Cancel and reply to any duplicates of a just-completed operation
*
* Check whether any fencing operations are scheduled to do the same thing as
* one that just succeeded. If so, rather than performing the same operation
* twice, return the result of this operation for all matching pending commands.
*
* \param[in,out] cmd Fencing operation that just succeeded
* \param[in] result Result of \p cmd
* \param[in] pid If nonzero, process ID of agent invocation (for logs)
*
* \note Duplicate merging will do the right thing for either type of remapped
* reboot. If the executing fencer remapped an unsupported reboot to off,
* then cmd->action will be "reboot" and will be merged with any other
* reboot requests. If the originating fencer remapped a topology reboot
* to off then on, we will get here once with cmd->action "off" and once
* with "on", and they will be merged separately with similar requests.
*/
static void
reply_to_duplicates(async_command_t *cmd, const pcmk__action_result_t *result,
int pid)
{
GList *next = NULL;
for (GList *iter = cmd_list; iter != NULL; iter = next) {
async_command_t *cmd_other = iter->data;
next = iter->next; // We might delete this entry, so grab next now
if (cmd == cmd_other) {
continue;
}
/* A pending operation matches if:
* 1. The client connections are different.
* 2. The target is the same.
* 3. The fencing action is the same.
* 4. The device scheduled to execute the action is the same.
*/
if (pcmk__str_eq(cmd->client, cmd_other->client, pcmk__str_casei) ||
!pcmk__str_eq(cmd->target, cmd_other->target, pcmk__str_casei) ||
!pcmk__str_eq(cmd->action, cmd_other->action, pcmk__str_none) ||
!pcmk__str_eq(cmd->device, cmd_other->device, pcmk__str_casei)) {
continue;
}
crm_notice("Merging fencing action '%s'%s%s originating from "
"client %s with identical fencing request from client %s",
cmd_other->action,
(cmd_other->target == NULL)? "" : " targeting ",
pcmk__s(cmd_other->target, ""), cmd_other->client_name,
cmd->client_name);
// Stop tracking the duplicate, send its result, and cancel it
cmd_list = g_list_remove_link(cmd_list, iter);
send_async_reply(cmd_other, result, pid, true);
cancel_stonith_command(cmd_other);
free_async_command(cmd_other);
g_list_free_1(iter);
}
}
/*!
* \internal
* \brief Return the next required device (if any) for an operation
*
* \param[in,out] cmd Fencing operation that just succeeded
*
* \return Next device required for action if any, otherwise NULL
*/
static fenced_device_t *
next_required_device(async_command_t *cmd)
{
for (GList *iter = cmd->next_device_iter; iter != NULL; iter = iter->next) {
fenced_device_t *next_device = g_hash_table_lookup(device_table,
iter->data);
if (is_action_required(cmd->action, next_device)) {
/* This is only called for successful actions, so it's OK to skip
* non-required devices.
*/
cmd->next_device_iter = iter->next;
return next_device;
}
}
return NULL;
}
static void
st_child_done(int pid, const pcmk__action_result_t *result, void *user_data)
{
async_command_t *cmd = user_data;
fenced_device_t *device = NULL;
fenced_device_t *next_device = NULL;
CRM_CHECK(cmd != NULL, return);
device = cmd_device(cmd);
cmd->active_on = NULL;
/* The device is ready to do something else now */
if (device) {
if (!pcmk_is_set(device->flags, fenced_df_verified)
&& pcmk__result_ok(result)
&& pcmk__strcase_any_of(cmd->action, PCMK_ACTION_LIST,
PCMK_ACTION_MONITOR, PCMK_ACTION_STATUS,
NULL)) {
fenced_device_set_flags(device, fenced_df_verified);
}
mainloop_set_trigger(device->work);
}
if (pcmk__result_ok(result)) {
next_device = next_required_device(cmd);
} else if ((cmd->next_device_iter != NULL)
&& !is_action_required(cmd->action, device)) {
/* if this device didn't work out, see if there are any others we can try.
* if the failed device was 'required', we can't pick another device. */
next_device = g_hash_table_lookup(device_table,
cmd->next_device_iter->data);
cmd->next_device_iter = cmd->next_device_iter->next;
}
if (next_device == NULL) {
send_async_reply(cmd, result, pid, false);
if (pcmk__result_ok(result)) {
reply_to_duplicates(cmd, result, pid);
}
free_async_command(cmd);
} else { // This operation requires more fencing
log_async_result(cmd, result, pid, next_device->id, false);
schedule_stonith_command(cmd, next_device);
}
}
static void
stonith_fence_get_devices_cb(GList * devices, void *user_data)
{
async_command_t *cmd = user_data;
fenced_device_t *device = NULL;
guint ndevices = g_list_length(devices);
crm_info("Found %d matching device%s for target '%s'",
ndevices, pcmk__plural_s(ndevices), cmd->target);
if (devices != NULL) {
device = g_hash_table_lookup(device_table, devices->data);
}
if (device == NULL) { // No device found
pcmk__action_result_t result = PCMK__UNKNOWN_RESULT;
pcmk__format_result(&result, CRM_EX_ERROR, PCMK_EXEC_NO_FENCE_DEVICE,
"No device configured for target '%s'",
cmd->target);
send_async_reply(cmd, &result, 0, false);
pcmk__reset_result(&result);
free_async_command(cmd);
g_list_free_full(devices, free);
} else {
/* Device found. Schedule a fencing command for it.
*
* Assign devices to device_list so that it will be freed with cmd.
*/
cmd->device_list = devices;
cmd->next_device_iter = devices->next;
schedule_stonith_command(cmd, device);
}
}
/*!
* \internal
* \brief Execute a fence action via the local node
*
* \param[in] msg Fencing request
* \param[out] result Where to store result of fence action
*/
static void
fence_locally(xmlNode *msg, pcmk__action_result_t *result)
{
const char *device_id = NULL;
fenced_device_t *device = NULL;
async_command_t *cmd = NULL;
xmlNode *dev = NULL;
CRM_CHECK((msg != NULL) && (result != NULL), return);
dev = pcmk__xpath_find_one(msg->doc, "//*[@" PCMK__XA_ST_TARGET "]",
LOG_ERR);
cmd = create_async_command(msg);
if (cmd == NULL) {
crm_log_xml_warn(msg, "invalid");
set_bad_request_result(result);
return;
}
device_id = crm_element_value(dev, PCMK__XA_ST_DEVICE_ID);
if (device_id != NULL) {
device = g_hash_table_lookup(device_table, device_id);
if (device == NULL) {
crm_err("Requested device '%s' is not available", device_id);
pcmk__format_result(result, CRM_EX_ERROR, PCMK_EXEC_NO_FENCE_DEVICE,
"Requested device '%s' not found", device_id);
return;
}
schedule_stonith_command(cmd, device);
} else {
const char *host = crm_element_value(dev, PCMK__XA_ST_TARGET);
if (pcmk_is_set(cmd->options, st_opt_cs_nodeid)) {
int nodeid = 0;
pcmk__node_status_t *node = NULL;
pcmk__scan_min_int(host, &nodeid, 0);
node = pcmk__search_node_caches(nodeid, NULL, NULL,
pcmk__node_search_any
|pcmk__node_search_cluster_cib);
if (node != NULL) {
host = node->name;
}
}
/* If we get to here, then self-fencing is implicitly allowed */
get_capable_devices(host, cmd->action, cmd->default_timeout,
TRUE, cmd, stonith_fence_get_devices_cb,
fenced_support_flag(cmd->action));
}
pcmk__set_result(result, CRM_EX_OK, PCMK_EXEC_PENDING, NULL);
}
/*!
* \internal
* \brief Build an XML reply for a fencing operation
*
* \param[in] request Request that reply is for
* \param[in] data If not NULL, add to reply as call data
* \param[in] result Full result of fencing operation
*
* \return Newly created XML reply
* \note The caller is responsible for freeing the result.
* \note This has some overlap with construct_async_reply(), but that copies
* values from an async_command_t, whereas this one copies them from the
* request.
*/
xmlNode *
fenced_construct_reply(const xmlNode *request, xmlNode *data,
const pcmk__action_result_t *result)
{
xmlNode *reply = NULL;
reply = pcmk__xe_create(NULL, PCMK__XE_ST_REPLY);
crm_xml_add(reply, PCMK__XA_ST_ORIGIN, __func__);
crm_xml_add(reply, PCMK__XA_T, PCMK__VALUE_STONITH_NG);
stonith__xe_set_result(reply, result);
if (request == NULL) {
/* Most likely, this is the result of a stonith operation that was
* initiated before we came up. Unfortunately that means we lack enough
* information to provide clients with a full result.
*
* @TODO Maybe synchronize this information at start-up?
*/
crm_warn("Missing request information for client notifications for "
"operation with result '%s' (initiated before we came up?)",
pcmk_exec_status_str(result->execution_status));
} else {
const char *name = NULL;
const char *value = NULL;
// Attributes to copy from request to reply
const char *names[] = {
PCMK__XA_ST_OP,
PCMK__XA_ST_CALLID,
PCMK__XA_ST_CLIENTID,
PCMK__XA_ST_CLIENTNAME,
PCMK__XA_ST_REMOTE_OP,
PCMK__XA_ST_CALLOPT,
};
for (int lpc = 0; lpc < PCMK__NELEM(names); lpc++) {
name = names[lpc];
value = crm_element_value(request, name);
crm_xml_add(reply, name, value);
}
if (data != NULL) {
xmlNode *wrapper = pcmk__xe_create(reply, PCMK__XE_ST_CALLDATA);
pcmk__xml_copy(wrapper, data);
}
}
return reply;
}
/*!
* \internal
* \brief Build an XML reply to an asynchronous fencing command
*
* \param[in] cmd Fencing command that reply is for
* \param[in] result Command result
*/
static xmlNode *
construct_async_reply(const async_command_t *cmd,
const pcmk__action_result_t *result)
{
xmlNode *reply = pcmk__xe_create(NULL, PCMK__XE_ST_REPLY);
crm_xml_add(reply, PCMK__XA_ST_ORIGIN, __func__);
crm_xml_add(reply, PCMK__XA_T, PCMK__VALUE_STONITH_NG);
crm_xml_add(reply, PCMK__XA_ST_OP, cmd->op);
crm_xml_add(reply, PCMK__XA_ST_DEVICE_ID, cmd->device);
crm_xml_add(reply, PCMK__XA_ST_REMOTE_OP, cmd->remote_op_id);
crm_xml_add(reply, PCMK__XA_ST_CLIENTID, cmd->client);
crm_xml_add(reply, PCMK__XA_ST_CLIENTNAME, cmd->client_name);
crm_xml_add(reply, PCMK__XA_ST_TARGET, cmd->target);
crm_xml_add(reply, PCMK__XA_ST_DEVICE_ACTION, cmd->op);
crm_xml_add(reply, PCMK__XA_ST_ORIGIN, cmd->origin);
crm_xml_add_int(reply, PCMK__XA_ST_CALLID, cmd->id);
crm_xml_add_int(reply, PCMK__XA_ST_CALLOPT, cmd->options);
stonith__xe_set_result(reply, result);
return reply;
}
bool
fencing_peer_active(pcmk__node_status_t *peer)
{
return (peer != NULL) && (peer->name != NULL)
&& pcmk_is_set(peer->processes, crm_get_cluster_proc());
}
void
set_fencing_completed(remote_fencing_op_t *op)
{
struct timespec tv;
qb_util_timespec_from_epoch_get(&tv);
op->completed = tv.tv_sec;
op->completed_nsec = tv.tv_nsec;
}
/*!
* \internal
* \brief Look for alternate node needed if local node shouldn't fence target
*
* \param[in] target Node that must be fenced
*
* \return Name of an alternate node that should fence \p target if any,
* or NULL otherwise
*/
static const char *
check_alternate_host(const char *target)
{
if (pcmk__str_eq(target, fenced_get_local_node(), pcmk__str_casei)) {
GHashTableIter gIter;
pcmk__node_status_t *entry = NULL;
g_hash_table_iter_init(&gIter, pcmk__peer_cache);
while (g_hash_table_iter_next(&gIter, NULL, (void **)&entry)) {
if (fencing_peer_active(entry)
&& !pcmk__str_eq(entry->name, target, pcmk__str_casei)) {
crm_notice("Forwarding self-fencing request to %s",
entry->name);
return entry->name;
}
}
crm_warn("Will handle own fencing because no peer can");
}
return NULL;
}
static void
remove_relay_op(xmlNode * request)
{
xmlNode *dev = pcmk__xpath_find_one(request->doc,
"//*[@" PCMK__XA_ST_DEVICE_ACTION "]",
LOG_TRACE);
const char *relay_op_id = NULL;
const char *op_id = NULL;
const char *client_name = NULL;
const char *target = NULL;
remote_fencing_op_t *relay_op = NULL;
if (dev) {
target = crm_element_value(dev, PCMK__XA_ST_TARGET);
}
relay_op_id = crm_element_value(request, PCMK__XA_ST_REMOTE_OP_RELAY);
op_id = crm_element_value(request, PCMK__XA_ST_REMOTE_OP);
client_name = crm_element_value(request, PCMK__XA_ST_CLIENTNAME);
/* Delete RELAY operation. */
if ((relay_op_id != NULL) && (target != NULL)
&& pcmk__str_eq(target, fenced_get_local_node(), pcmk__str_casei)) {
relay_op = g_hash_table_lookup(stonith_remote_op_list, relay_op_id);
if (relay_op) {
GHashTableIter iter;
remote_fencing_op_t *list_op = NULL;
g_hash_table_iter_init(&iter, stonith_remote_op_list);
/* If the operation to be deleted is registered as a duplicate, delete the registration. */
while (g_hash_table_iter_next(&iter, NULL, (void **)&list_op)) {
GList *dup_iter = NULL;
if (list_op != relay_op) {
for (dup_iter = list_op->duplicates; dup_iter != NULL; dup_iter = dup_iter->next) {
remote_fencing_op_t *other = dup_iter->data;
if (other == relay_op) {
other->duplicates = g_list_remove(other->duplicates, relay_op);
break;
}
}
}
}
crm_debug("Deleting relay op %s ('%s'%s%s for %s), "
"replaced by op %s ('%s'%s%s for %s)",
relay_op->id, relay_op->action,
(relay_op->target == NULL)? "" : " targeting ",
pcmk__s(relay_op->target, ""),
relay_op->client_name, op_id, relay_op->action,
(target == NULL)? "" : " targeting ", pcmk__s(target, ""),
client_name);
g_hash_table_remove(stonith_remote_op_list, relay_op_id);
}
}
}
/*!
* \internal
* \brief Check whether an API request was sent by a privileged user
*
* API commands related to fencing configuration may be done only by privileged
* IPC users (i.e. root or hacluster), because all other users should go through
* the CIB to have ACLs applied. If no client was given, this is a peer request,
* which is always allowed.
*
* \param[in] c IPC client that sent request (or NULL if sent by CPG peer)
* \param[in] op Requested API operation (for logging only)
*
* \return true if sender is peer or privileged client, otherwise false
*/
static inline bool
is_privileged(const pcmk__client_t *c, const char *op)
{
if ((c == NULL) || pcmk_is_set(c->flags, pcmk__client_privileged)) {
return true;
} else {
crm_warn("Rejecting IPC request '%s' from unprivileged client %s",
pcmk__s(op, ""), pcmk__client_name(c));
return false;
}
}
// CRM_OP_REGISTER
static xmlNode *
handle_register_request(pcmk__request_t *request)
{
xmlNode *reply = pcmk__xe_create(NULL, "reply");
pcmk__assert(request->ipc_client != NULL);
crm_xml_add(reply, PCMK__XA_ST_OP, CRM_OP_REGISTER);
crm_xml_add(reply, PCMK__XA_ST_CLIENTID, request->ipc_client->id);
pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
pcmk__set_request_flags(request, pcmk__request_reuse_options);
return reply;
}
// STONITH_OP_EXEC
static xmlNode *
handle_agent_request(pcmk__request_t *request)
{
execute_agent_action(request->xml, &request->result);
if (request->result.execution_status == PCMK_EXEC_PENDING) {
return NULL;
}
return fenced_construct_reply(request->xml, NULL, &request->result);
}
// STONITH_OP_TIMEOUT_UPDATE
static xmlNode *
handle_update_timeout_request(pcmk__request_t *request)
{
const char *call_id = crm_element_value(request->xml, PCMK__XA_ST_CALLID);
const char *client_id = crm_element_value(request->xml,
PCMK__XA_ST_CLIENTID);
int op_timeout = 0;
crm_element_value_int(request->xml, PCMK__XA_ST_TIMEOUT, &op_timeout);
do_stonith_async_timeout_update(client_id, call_id, op_timeout);
pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
return NULL;
}
// STONITH_OP_QUERY
static xmlNode *
handle_query_request(pcmk__request_t *request)
{
int timeout = 0;
xmlNode *dev = NULL;
const char *action = NULL;
const char *target = NULL;
const char *client_id = crm_element_value(request->xml,
PCMK__XA_ST_CLIENTID);
struct st_query_data *query = NULL;
if (request->peer != NULL) {
// Record it for the future notification
create_remote_stonith_op(client_id, request->xml, TRUE);
}
/* Delete the DC node RELAY operation. */
remove_relay_op(request->xml);
pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
dev = pcmk__xpath_find_one(request->xml->doc,
"//*[@" PCMK__XA_ST_DEVICE_ACTION "]",
LOG_NEVER);
if (dev != NULL) {
const char *device = crm_element_value(dev, PCMK__XA_ST_DEVICE_ID);
if (pcmk__str_eq(device, "manual_ack", pcmk__str_casei)) {
return NULL; // No query or reply necessary
}
target = crm_element_value(dev, PCMK__XA_ST_TARGET);
action = crm_element_value(dev, PCMK__XA_ST_DEVICE_ACTION);
}
crm_log_xml_trace(request->xml, "Query");
query = pcmk__assert_alloc(1, sizeof(struct st_query_data));
query->reply = fenced_construct_reply(request->xml, NULL, &request->result);
query->remote_peer = pcmk__str_copy(request->peer);
query->client_id = pcmk__str_copy(client_id);
query->target = pcmk__str_copy(target);
query->action = pcmk__str_copy(action);
query->call_options = request->call_options;
crm_element_value_int(request->xml, PCMK__XA_ST_TIMEOUT, &timeout);
get_capable_devices(target, action, timeout,
pcmk_is_set(query->call_options,
st_opt_allow_self_fencing),
query, stonith_query_capable_device_cb, fenced_df_none);
return NULL;
}
// STONITH_OP_NOTIFY
static xmlNode *
handle_notify_request(pcmk__request_t *request)
{
const char *flag_name = NULL;
pcmk__assert(request->ipc_client != NULL);
flag_name = crm_element_value(request->xml, PCMK__XA_ST_NOTIFY_ACTIVATE);
if (flag_name != NULL) {
crm_debug("Enabling %s callbacks for client %s",
flag_name, pcmk__request_origin(request));
pcmk__set_client_flags(request->ipc_client,
fenced_parse_notify_flag(flag_name));
}
flag_name = crm_element_value(request->xml, PCMK__XA_ST_NOTIFY_DEACTIVATE);
if (flag_name != NULL) {
crm_debug("Disabling %s callbacks for client %s",
flag_name, pcmk__request_origin(request));
pcmk__clear_client_flags(request->ipc_client,
fenced_parse_notify_flag(flag_name));
}
pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
pcmk__set_request_flags(request, pcmk__request_reuse_options);
return pcmk__ipc_create_ack(request->ipc_flags, PCMK__XE_ACK, NULL,
CRM_EX_OK);
}
// STONITH_OP_RELAY
static xmlNode *
handle_relay_request(pcmk__request_t *request)
{
xmlNode *dev = pcmk__xpath_find_one(request->xml->doc,
"//*[@" PCMK__XA_ST_TARGET "]",
LOG_TRACE);
crm_notice("Received forwarded fencing request from "
"%s %s to fence (%s) peer %s",
pcmk__request_origin_type(request),
pcmk__request_origin(request),
crm_element_value(dev, PCMK__XA_ST_DEVICE_ACTION),
crm_element_value(dev, PCMK__XA_ST_TARGET));
if (initiate_remote_stonith_op(NULL, request->xml, FALSE) == NULL) {
set_bad_request_result(&request->result);
return fenced_construct_reply(request->xml, NULL, &request->result);
}
pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_PENDING, NULL);
return NULL;
}
// STONITH_OP_FENCE
static xmlNode *
handle_fence_request(pcmk__request_t *request)
{
if (request->peer != NULL) {
fence_locally(request->xml, &request->result);
} else if (pcmk_is_set(request->call_options, st_opt_manual_ack)) {
switch (fenced_handle_manual_confirmation(request->ipc_client,
request->xml)) {
case pcmk_rc_ok:
pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE,
NULL);
break;
case EINPROGRESS:
pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_PENDING,
NULL);
break;
default:
set_bad_request_result(&request->result);
break;
}
} else {
const char *alternate_host = NULL;
xmlNode *dev = pcmk__xpath_find_one(request->xml->doc,
"//*[@" PCMK__XA_ST_TARGET "]",
LOG_TRACE);
const char *target = crm_element_value(dev, PCMK__XA_ST_TARGET);
const char *action = crm_element_value(dev, PCMK__XA_ST_DEVICE_ACTION);
const char *device = crm_element_value(dev, PCMK__XA_ST_DEVICE_ID);
if (request->ipc_client != NULL) {
int tolerance = 0;
crm_notice("Client %s wants to fence (%s) %s using %s",
pcmk__request_origin(request), action,
target, (device? device : "any device"));
crm_element_value_int(dev, PCMK__XA_ST_TOLERANCE, &tolerance);
if (stonith_check_fence_tolerance(tolerance, target, action)) {
pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE,
NULL);
return fenced_construct_reply(request->xml, NULL,
&request->result);
}
alternate_host = check_alternate_host(target);
} else {
crm_notice("Peer %s wants to fence (%s) '%s' with device '%s'",
request->peer, action, target,
(device == NULL)? "(any)" : device);
}
if (alternate_host != NULL) {
const char *client_id = NULL;
remote_fencing_op_t *op = NULL;
pcmk__node_status_t *node =
pcmk__get_node(0, alternate_host, NULL,
pcmk__node_search_cluster_member);
if (request->ipc_client->id == 0) {
client_id = crm_element_value(request->xml,
PCMK__XA_ST_CLIENTID);
} else {
client_id = request->ipc_client->id;
}
/* Create a duplicate fencing operation to relay with the client ID.
* When a query response is received, this operation should be
* deleted to avoid keeping the duplicate around.
*/
op = create_remote_stonith_op(client_id, request->xml, FALSE);
crm_xml_add(request->xml, PCMK__XA_ST_OP, STONITH_OP_RELAY);
crm_xml_add(request->xml, PCMK__XA_ST_CLIENTID,
request->ipc_client->id);
crm_xml_add(request->xml, PCMK__XA_ST_REMOTE_OP, op->id);
// @TODO On failure, fail request immediately, or maybe panic
pcmk__cluster_send_message(node, pcmk_ipc_fenced, request->xml);
pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_PENDING,
NULL);
} else if (initiate_remote_stonith_op(request->ipc_client, request->xml,
FALSE) == NULL) {
set_bad_request_result(&request->result);
} else {
pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_PENDING,
NULL);
}
}
if (request->result.execution_status == PCMK_EXEC_PENDING) {
return NULL;
}
return fenced_construct_reply(request->xml, NULL, &request->result);
}
// STONITH_OP_FENCE_HISTORY
static xmlNode *
handle_history_request(pcmk__request_t *request)
{
xmlNode *reply = NULL;
xmlNode *data = NULL;
stonith_fence_history(request->xml, &data, request->peer,
request->call_options);
pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
if (!pcmk_is_set(request->call_options, st_opt_discard_reply)) {
/* When the local node broadcasts its history, it sets
* st_opt_discard_reply and doesn't need a reply.
*/
reply = fenced_construct_reply(request->xml, data, &request->result);
}
pcmk__xml_free(data);
return reply;
}
// STONITH_OP_DEVICE_ADD
static xmlNode *
handle_device_add_request(pcmk__request_t *request)
{
const char *op = crm_element_value(request->xml, PCMK__XA_ST_OP);
xmlNode *dev = pcmk__xpath_find_one(request->xml->doc,
"//" PCMK__XE_ST_DEVICE_ID, LOG_ERR);
if (is_privileged(request->ipc_client, op)) {
int rc = fenced_device_register(dev, false);
rc = pcmk_rc2legacy(rc);
pcmk__set_result(&request->result,
((rc == pcmk_ok)? CRM_EX_OK : CRM_EX_ERROR),
stonith__legacy2status(rc),
((rc == pcmk_ok)? NULL : pcmk_strerror(rc)));
} else {
pcmk__set_result(&request->result, CRM_EX_INSUFFICIENT_PRIV,
PCMK_EXEC_INVALID,
"Unprivileged users must register device via CIB");
}
fenced_send_config_notification(op, &request->result,
(dev == NULL)? NULL : pcmk__xe_id(dev));
return fenced_construct_reply(request->xml, NULL, &request->result);
}
// STONITH_OP_DEVICE_DEL
static xmlNode *
handle_device_delete_request(pcmk__request_t *request)
{
xmlNode *dev = pcmk__xpath_find_one(request->xml->doc,
"//" PCMK__XE_ST_DEVICE_ID, LOG_ERR);
const char *device_id = crm_element_value(dev, PCMK_XA_ID);
const char *op = crm_element_value(request->xml, PCMK__XA_ST_OP);
if (is_privileged(request->ipc_client, op)) {
stonith_device_remove(device_id, false);
pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
} else {
pcmk__set_result(&request->result, CRM_EX_INSUFFICIENT_PRIV,
PCMK_EXEC_INVALID,
"Unprivileged users must delete device via CIB");
}
fenced_send_config_notification(op, &request->result, device_id);
return fenced_construct_reply(request->xml, NULL, &request->result);
}
// STONITH_OP_LEVEL_ADD
static xmlNode *
handle_level_add_request(pcmk__request_t *request)
{
const char *op = crm_element_value(request->xml, PCMK__XA_ST_OP);
if (is_privileged(request->ipc_client, op)) {
fenced_register_level(request->xml, &request->result);
} else {
unpack_level_request(request->xml, NULL, NULL, NULL);
pcmk__set_result(&request->result, CRM_EX_INSUFFICIENT_PRIV,
PCMK_EXEC_INVALID,
"Unprivileged users must add level via CIB");
}
return fenced_construct_reply(request->xml, NULL, &request->result);
}
// STONITH_OP_LEVEL_DEL
static xmlNode *
handle_level_delete_request(pcmk__request_t *request)
{
const char *op = crm_element_value(request->xml, PCMK__XA_ST_OP);
if (is_privileged(request->ipc_client, op)) {
fenced_unregister_level(request->xml, &request->result);
} else {
unpack_level_request(request->xml, NULL, NULL, NULL);
pcmk__set_result(&request->result, CRM_EX_INSUFFICIENT_PRIV,
PCMK_EXEC_INVALID,
"Unprivileged users must delete level via CIB");
}
return fenced_construct_reply(request->xml, NULL, &request->result);
}
// CRM_OP_RM_NODE_CACHE
static xmlNode *
handle_cache_request(pcmk__request_t *request)
{
int node_id = 0;
const char *name = NULL;
crm_element_value_int(request->xml, PCMK_XA_ID, &node_id);
name = crm_element_value(request->xml, PCMK_XA_UNAME);
pcmk__cluster_forget_cluster_node(node_id, name);
pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
return NULL;
}
static xmlNode *
handle_unknown_request(pcmk__request_t *request)
{
crm_err("Unknown IPC request %s from %s %s",
request->op, pcmk__request_origin_type(request),
pcmk__request_origin(request));
pcmk__format_result(&request->result, CRM_EX_PROTOCOL, PCMK_EXEC_INVALID,
"Unknown IPC request type '%s' (bug?)", request->op);
return fenced_construct_reply(request->xml, NULL, &request->result);
}
static void
fenced_register_handlers(void)
{
pcmk__server_command_t handlers[] = {
{ CRM_OP_REGISTER, handle_register_request },
{ STONITH_OP_EXEC, handle_agent_request },
{ STONITH_OP_TIMEOUT_UPDATE, handle_update_timeout_request },
{ STONITH_OP_QUERY, handle_query_request },
{ STONITH_OP_NOTIFY, handle_notify_request },
{ STONITH_OP_RELAY, handle_relay_request },
{ STONITH_OP_FENCE, handle_fence_request },
{ STONITH_OP_FENCE_HISTORY, handle_history_request },
{ STONITH_OP_DEVICE_ADD, handle_device_add_request },
{ STONITH_OP_DEVICE_DEL, handle_device_delete_request },
{ STONITH_OP_LEVEL_ADD, handle_level_add_request },
{ STONITH_OP_LEVEL_DEL, handle_level_delete_request },
{ CRM_OP_RM_NODE_CACHE, handle_cache_request },
{ NULL, handle_unknown_request },
};
fenced_handlers = pcmk__register_handlers(handlers);
}
void
fenced_unregister_handlers(void)
{
if (fenced_handlers != NULL) {
g_hash_table_destroy(fenced_handlers);
fenced_handlers = NULL;
}
}
static void
handle_request(pcmk__request_t *request)
{
xmlNode *reply = NULL;
const char *reason = NULL;
if (fenced_handlers == NULL) {
fenced_register_handlers();
}
reply = pcmk__process_request(request, fenced_handlers);
if (reply != NULL) {
if (pcmk_is_set(request->flags, pcmk__request_reuse_options)
&& (request->ipc_client != NULL)) {
/* Certain IPC-only commands must reuse the call options from the
* original request rather than the ones set by stonith_send_reply()
* -> do_local_reply().
*/
pcmk__ipc_send_xml(request->ipc_client, request->ipc_id, reply,
request->ipc_flags);
request->ipc_client->request_id = 0;
} else {
stonith_send_reply(reply, request->call_options,
request->peer, request->ipc_client);
}
pcmk__xml_free(reply);
}
reason = request->result.exit_reason;
crm_debug("Processed %s request from %s %s: %s%s%s%s",
request->op, pcmk__request_origin_type(request),
pcmk__request_origin(request),
pcmk_exec_status_str(request->result.execution_status),
(reason == NULL)? "" : " (",
(reason == NULL)? "" : reason,
(reason == NULL)? "" : ")");
}
static void
handle_reply(pcmk__client_t *client, xmlNode *request, const char *remote_peer)
{
// Copy, because request might be freed before we want to log this
char *op = crm_element_value_copy(request, PCMK__XA_ST_OP);
if (pcmk__str_eq(op, STONITH_OP_QUERY, pcmk__str_none)) {
process_remote_stonith_query(request);
} else if (pcmk__str_any_of(op, STONITH_OP_NOTIFY, STONITH_OP_FENCE,
NULL)) {
fenced_process_fencing_reply(request);
} else {
crm_err("Ignoring unknown %s reply from %s %s",
pcmk__s(op, "untyped"), ((client == NULL)? "peer" : "client"),
((client == NULL)? remote_peer : pcmk__client_name(client)));
crm_log_xml_warn(request, "UnknownOp");
free(op);
return;
}
crm_debug("Processed %s reply from %s %s",
op, ((client == NULL)? "peer" : "client"),
((client == NULL)? remote_peer : pcmk__client_name(client)));
free(op);
}
/*!
* \internal
* \brief Handle a message from an IPC client or CPG peer
*
* \param[in,out] client If not NULL, IPC client that sent message
* \param[in] id If from IPC client, IPC message ID
* \param[in] flags Message flags
* \param[in,out] message Message XML
* \param[in] remote_peer If not NULL, CPG peer that sent message
*/
void
stonith_command(pcmk__client_t *client, uint32_t id, uint32_t flags,
xmlNode *message, const char *remote_peer)
{
uint32_t call_options = st_opt_none;
int rc = pcmk_rc_ok;
bool is_reply = false;
CRM_CHECK(message != NULL, return);
if (pcmk__xpath_find_one(message->doc, "//" PCMK__XE_ST_REPLY,
LOG_NEVER) != NULL) {
is_reply = true;
}
rc = pcmk__xe_get_flags(message, PCMK__XA_ST_CALLOPT, &call_options,
st_opt_none);
if (rc != pcmk_rc_ok) {
crm_warn("Couldn't parse options from message: %s", pcmk_rc_str(rc));
}
crm_debug("Processing %ssynchronous %s %s %u from %s %s",
pcmk_is_set(call_options, st_opt_sync_call)? "" : "a",
crm_element_value(message, PCMK__XA_ST_OP),
(is_reply? "reply" : "request"), id,
((client == NULL)? "peer" : "client"),
((client == NULL)? remote_peer : pcmk__client_name(client)));
if (pcmk_is_set(call_options, st_opt_sync_call)) {
pcmk__assert((client == NULL) || (client->request_id == id));
}
if (is_reply) {
handle_reply(client, message, remote_peer);
} else {
pcmk__request_t request = {
.ipc_client = client,
.ipc_id = id,
.ipc_flags = flags,
.peer = remote_peer,
.xml = message,
.call_options = call_options,
.result = PCMK__UNKNOWN_RESULT,
};
request.op = crm_element_value_copy(request.xml, PCMK__XA_ST_OP);
CRM_CHECK(request.op != NULL, return);
if (pcmk_is_set(request.call_options, st_opt_sync_call)) {
pcmk__set_request_flags(&request, pcmk__request_sync);
}
handle_request(&request);
pcmk__reset_request(&request);
}
}
diff --git a/include/crm/common/Makefile.am b/include/crm/common/Makefile.am
index 7b9a676ffa..a928273781 100644
--- a/include/crm/common/Makefile.am
+++ b/include/crm/common/Makefile.am
@@ -1,53 +1,54 @@
#
-# Copyright 2004-2024 the Pacemaker project contributors
+# Copyright 2004-2025 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 $(top_srcdir)/mk/common.mk
headerdir=$(pkgincludedir)/crm/common
header_HEADERS = acl.h \
+ acl_compat.h \
actions.h \
agents.h \
cib.h \
ipc.h \
ipc_client_compat.h \
ipc_controld.h \
ipc_pacemakerd.h \
ipc_schedulerd.h \
iso8601.h \
logging.h \
logging_compat.h \
mainloop.h \
nodes.h \
nvpair.h \
nvpair_compat.h \
options.h \
output.h \
probes.h \
resources.h \
results.h \
results_compat.h \
roles.h \
rules.h \
scheduler.h \
scheduler_types.h \
schemas.h \
scores.h \
scores_compat.h \
strings.h \
util.h \
util_compat.h \
xml.h \
xml_compat.h \
xml_element.h \
xml_element_compat.h \
xml_io.h \
xml_names.h
noinst_HEADERS = $(wildcard *internal.h)
diff --git a/include/crm/common/acl.h b/include/crm/common/acl.h
index f8f4b688d4..0933a4554c 100644
--- a/include/crm/common/acl.h
+++ b/include/crm/common/acl.h
@@ -1,38 +1,41 @@
/*
- * Copyright 2004-2024 the Pacemaker project contributors
+ * Copyright 2004-2025 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__CRM_COMMON_ACL__H
#define PCMK__CRM_COMMON_ACL__H
#include <libxml/tree.h> // xmlNode
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* \file
* \brief Low-level API for XML Access Control Lists (ACLs)
* \ingroup core
*/
-bool xml_acl_enabled(const xmlNode *xml);
void xml_acl_disable(xmlNode *xml);
bool xml_acl_denied(const xmlNode *xml);
bool xml_acl_filtered_copy(const char *user, xmlNode* acl_source, xmlNode *xml,
xmlNode **result);
bool pcmk_acl_required(const char *user);
+#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
+#include <crm/common/acl_compat.h>
+#endif // !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
+
#ifdef __cplusplus
}
#endif
#endif // PCMK__CRM_COMMON_ACL__H
diff --git a/include/crm/common/acl_compat.h b/include/crm/common/acl_compat.h
new file mode 100644
index 0000000000..a87db5ca26
--- /dev/null
+++ b/include/crm/common/acl_compat.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2004-2025 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__CRM_COMMON_ACL_COMPAT__H
+#define PCMK__CRM_COMMON_ACL_COMPAT__H
+
+#include <stdbool.h> // bool
+
+#include <libxml/tree.h> // xmlNode
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \file
+ * \brief Deprecated Pacemaker ACL API
+ * \ingroup core
+ * \deprecated Do not include this header directly. The XML APIs in this
+ * header, and the header itself, will be removed in a future
+ * release.
+ */
+
+//! \deprecated Do not use
+bool xml_acl_enabled(const xmlNode *xml);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // PCMK__CRM_COMMON_ACL_COMPAT__H
diff --git a/include/crm/common/digest_internal.h b/include/crm/common/digest_internal.h
index 5c49c1d09e..45be1cf8b7 100644
--- a/include/crm/common/digest_internal.h
+++ b/include/crm/common/digest_internal.h
@@ -1,54 +1,54 @@
/*
- * Copyright 2015-2024 the Pacemaker project contributors
+ * Copyright 2015-2025 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__CRM_COMMON_DIGEST_INTERNAL__H
#define PCMK__CRM_COMMON_DIGEST_INTERNAL__H
/*
* Internal-only functions to create digest strings from XML
*/
#include <stdbool.h>
#include <libxml/tree.h> // xmlNode
#ifdef __cplusplus
extern "C" {
#endif
// Digest comparison results
enum pcmk__digest_result {
pcmk__digest_unknown, // No digest available for comparison
pcmk__digest_match, // Digests match
pcmk__digest_mismatch, // Any parameter changed (potentially reloadable)
pcmk__digest_restart, // Parameters that require a restart changed
};
// Information needed to compare operation digests
typedef struct {
enum pcmk__digest_result rc; // Result of digest comparison
xmlNode *params_all; // All operation parameters
xmlNode *params_secure; // Parameters marked private
xmlNode *params_restart; // Parameters marked not reloadable
char *digest_all_calc; // Digest of params_all
char *digest_secure_calc; // Digest of params_secure
char *digest_restart_calc; // Digest of params_restart
} pcmk__op_digest_t;
-char *pcmk__digest_on_disk_cib(xmlNode *input);
-char *pcmk__digest_operation(xmlNode *input);
-char *pcmk__digest_xml(xmlNode *input, bool filter);
+char *pcmk__digest_on_disk_cib(const xmlNode *input);
+char *pcmk__digest_op_params(const xmlNode *input);
+char *pcmk__digest_xml(const xmlNode *input, bool filter);
-bool pcmk__verify_digest(xmlNode *input, const char *expected);
+bool pcmk__verify_digest(const xmlNode *input, const char *expected);
#ifdef __cplusplus
}
#endif
#endif // PCMK__CRM_COMMON_DIGEST_INTERNAL__H
diff --git a/include/crm/common/xml.h b/include/crm/common/xml.h
index 1305ddb541..a4cdcde69d 100644
--- a/include/crm/common/xml.h
+++ b/include/crm/common/xml.h
@@ -1,52 +1,48 @@
/*
* Copyright 2004-2025 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__CRM_COMMON_XML__H
#define PCMK__CRM_COMMON_XML__H
#include <stdbool.h> // bool
#include <libxml/tree.h> // xmlNode
// xml.h is a wrapper for the following headers
#include <crm/common/xml_element.h>
#include <crm/common/xml_io.h>
#include <crm/common/xml_names.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* \file
* \brief Wrappers for and extensions to libxml2
* \ingroup core
*/
-/*
- * Searching & Modifying
+/* @COMPAT Create and apply patchset functions must remain public and
+ * undeprecated until we create replacements
*/
-bool xml_patch_versions(const xmlNode *patchset, int add[3], int del[3]);
-
xmlNode *xml_create_patchset(
int format, xmlNode *source, xmlNode *target, bool *config, bool manage_version);
int xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version);
-void patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, bool with_digest);
-
#ifdef __cplusplus
}
#endif
#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
#include <crm/common/xml_compat.h>
#endif
#endif
diff --git a/include/crm/common/xml_compat.h b/include/crm/common/xml_compat.h
index dc3f924757..76323d1230 100644
--- a/include/crm/common/xml_compat.h
+++ b/include/crm/common/xml_compat.h
@@ -1,131 +1,138 @@
/*
* Copyright 2004-2025 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__CRM_COMMON_XML_COMPAT__H
#define PCMK__CRM_COMMON_XML_COMPAT__H
#include <glib.h> // gboolean
#include <libxml/tree.h> // xmlNode
#include <libxml/xpath.h> // xmlXPathObject
#include <crm/common/nvpair.h> // crm_xml_add()
#include <crm/common/xml_names.h> // PCMK_XE_CLONE
#ifdef __cplusplus
extern "C" {
#endif
/**
* \file
* \brief Deprecated Pacemaker XML API
* \ingroup core
* \deprecated Do not include this header directly. The XML APIs in this
* header, and the header itself, will be removed in a future
* release.
*/
// NOTE: sbd (as of at least 1.5.2) uses this
//! \deprecated Use name member directly
static inline const char *
crm_element_name(const xmlNode *xml)
{
return (xml == NULL)? NULL : (const char *) xml->name;
}
// NOTE: sbd (as of at least 1.5.2) uses this
//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
xmlNode *copy_xml(xmlNode *src_node);
// NOTE: sbd (as of at least 1.5.2) uses this
//! \deprecated Do not use
gboolean cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs);
// NOTE: sbd (as of at least 1.5.2) uses this
//! \deprecated Call \c crm_log_init() or \c crm_log_cli_init() instead
void crm_xml_init(void);
//! \deprecated Exit with \c crm_exit() instead
void crm_xml_cleanup(void);
//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
void pcmk_free_xml_subtree(xmlNode *xml);
// NOTE: sbd (as of at least 1.5.2) uses this
//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
void free_xml(xmlNode *child);
//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
void crm_xml_sanitize_id(char *id);
//! \deprecated Do not use
char *calculate_on_disk_digest(xmlNode *input);
//! \deprecated Do not use
char *calculate_operation_digest(xmlNode *input, const char *version);
//! \deprecated Do not use
char *calculate_xml_versioned_digest(xmlNode *input, gboolean sort,
gboolean do_filter, const char *version);
//! \deprecated Do not use
xmlXPathObjectPtr xpath_search(const xmlNode *xml_top, const char *path);
//! \deprecated Do not use
static inline int numXpathResults(xmlXPathObjectPtr xpathObj)
{
if ((xpathObj == NULL) || (xpathObj->nodesetval == NULL)) {
return 0;
}
return xpathObj->nodesetval->nodeNr;
}
//! \deprecated Do not use
xmlNode *getXpathResult(xmlXPathObjectPtr xpathObj, int index);
//! \deprecated Do not use
void freeXpathObject(xmlXPathObjectPtr xpathObj);
//! \deprecated Do not use
void dedupXpathResults(xmlXPathObjectPtr xpathObj);
//! \deprecated Do not use
void crm_foreach_xpath_result(xmlNode *xml, const char *xpath,
void (*helper)(xmlNode*, void*), void *user_data);
// NOTE: sbd (as of at least 1.5.2) uses this
//! \deprecated Do not use
xmlNode *get_xpath_object(const char *xpath, xmlNode *xml_obj, int error_level);
//! \deprecated Do not use
typedef const xmlChar *pcmkXmlStr;
//! \deprecated Do not use
bool xml_tracking_changes(xmlNode *xml);
//! \deprecated Do not use
bool xml_document_dirty(xmlNode *xml);
//! \deprecated Do not use
void xml_accept_changes(xmlNode *xml);
//! \deprecated Do not use
void xml_track_changes(xmlNode *xml, const char *user, xmlNode *acl_source,
bool enforce_acls);
//! \deprecated Do not use
void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml);
//! \deprecated Do not use
void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml);
+//! \deprecated Do not use
+bool xml_patch_versions(const xmlNode *patchset, int add[3], int del[3]);
+
+//! \deprecated Do not use
+void patchset_process_digest(xmlNode *patch, const xmlNode *source,
+ const xmlNode *target, bool with_digest);
+
#ifdef __cplusplus
}
#endif
#endif // PCMK__CRM_COMMON_XML_COMPAT__H
diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h
index e0bf139476..ed71737113 100644
--- a/include/crm/common/xml_internal.h
+++ b/include/crm/common/xml_internal.h
@@ -1,454 +1,447 @@
/*
* Copyright 2017-2025 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__CRM_COMMON_XML_INTERNAL__H
#define PCMK__CRM_COMMON_XML_INTERNAL__H
/*
* Internal-only wrappers for and extensions to libxml2 (libxslt)
*/
#include <stdlib.h>
#include <stdint.h> // uint32_t
#include <stdio.h>
#include <crm/crm.h> /* transitively imports qblog.h */
#include <crm/common/output_internal.h>
#include <crm/common/xml_names.h> // PCMK_XA_ID, PCMK_XE_CLONE
// This file is a wrapper for other {xml_*,xpath}_internal.h headers
#include <crm/common/xml_comment_internal.h>
#include <crm/common/xml_element_internal.h>
#include <crm/common/xml_idref_internal.h>
#include <crm/common/xml_io_internal.h>
#include <crm/common/xml_names_internal.h>
#include <crm/common/xpath_internal.h>
#include <libxml/relaxng.h>
#ifdef __cplusplus
extern "C" {
#endif
/*!
* \brief Base for directing lib{xml2,xslt} log into standard libqb backend
*
* This macro implements the core of what can be needed for directing
* libxml2 or libxslt error messaging into standard, preconfigured
* libqb-backed log stream.
*
* It's a bit unfortunate that libxml2 (and more sparsely, also libxslt)
* emits a single message by chunks (location is emitted separatedly from
* the message itself), so we have to take the effort to combine these
* chunks back to single message. Whether to do this or not is driven
* with \p dechunk toggle.
*
* The form of a macro was chosen for implicit deriving of __FILE__, etc.
* and also because static dechunking buffer should be differentiated per
* library (here we assume different functions referring to this macro
* will not ever be using both at once), preferably also per-library
* context of use to avoid clashes altogether.
*
* Note that we cannot use qb_logt, because callsite data have to be known
* at the moment of compilation, which it is not always the case -- xml_log
* (and unfortunately there's no clear explanation of the fail to compile).
*
* Also note that there's no explicit guard against said libraries producing
* never-newline-terminated chunks (which would just keep consuming memory),
* as it's quite improbable. Termination of the program in between the
* same-message chunks will raise a flag with valgrind and the likes, though.
*
* And lastly, regarding how dechunking combines with other non-message
* parameters -- for \p priority, most important running specification
* wins (possibly elevated to LOG_ERR in case of nonconformance with the
* newline-termination "protocol"), \p dechunk is expected to always be
* on once it was at the start, and the rest (\p postemit and \p prefix)
* are picked directly from the last chunk entry finalizing the message
* (also reasonable to always have it the same with all related entries).
*
* \param[in] priority Syslog priority for the message to be logged
* \param[in] dechunk Whether to dechunk new-line terminated message
* \param[in] postemit Code to be executed once message is sent out
* \param[in] prefix How to prefix the message or NULL for raw passing
* \param[in] fmt Format string as with printf-like functions
* \param[in] ap Variable argument list to supplement \p fmt format string
*/
#define PCMK__XML_LOG_BASE(priority, dechunk, postemit, prefix, fmt, ap) \
do { \
if (!(dechunk) && (prefix) == NULL) { /* quick pass */ \
qb_log_from_external_source_va(__func__, __FILE__, (fmt), \
(priority), __LINE__, 0, (ap)); \
(void) (postemit); \
} else { \
int CXLB_len = 0; \
char *CXLB_buf = NULL; \
static int CXLB_buffer_len = 0; \
static char *CXLB_buffer = NULL; \
static uint8_t CXLB_priority = 0; \
\
CXLB_len = vasprintf(&CXLB_buf, (fmt), (ap)); \
\
if (CXLB_len <= 0 || CXLB_buf[CXLB_len - 1] == '\n' || !(dechunk)) { \
if (CXLB_len < 0) { \
CXLB_buf = (char *) "LOG CORRUPTION HAZARD"; /*we don't modify*/\
CXLB_priority = QB_MIN(CXLB_priority, LOG_ERR); \
} else if (CXLB_len > 0 /* && (dechunk) */ \
&& CXLB_buf[CXLB_len - 1] == '\n') { \
CXLB_buf[CXLB_len - 1] = '\0'; \
} \
if (CXLB_buffer) { \
qb_log_from_external_source(__func__, __FILE__, "%s%s%s", \
CXLB_priority, __LINE__, 0, \
(prefix) != NULL ? (prefix) : "", \
CXLB_buffer, CXLB_buf); \
free(CXLB_buffer); \
} else { \
qb_log_from_external_source(__func__, __FILE__, "%s%s", \
(priority), __LINE__, 0, \
(prefix) != NULL ? (prefix) : "", \
CXLB_buf); \
} \
if (CXLB_len < 0) { \
CXLB_buf = NULL; /* restore temporary override */ \
} \
CXLB_buffer = NULL; \
CXLB_buffer_len = 0; \
(void) (postemit); \
\
} else if (CXLB_buffer == NULL) { \
CXLB_buffer_len = CXLB_len; \
CXLB_buffer = CXLB_buf; \
CXLB_buf = NULL; \
CXLB_priority = (priority); /* remember as a running severest */ \
\
} else { \
CXLB_buffer = realloc(CXLB_buffer, 1 + CXLB_buffer_len + CXLB_len); \
memcpy(CXLB_buffer + CXLB_buffer_len, CXLB_buf, CXLB_len); \
CXLB_buffer_len += CXLB_len; \
CXLB_buffer[CXLB_buffer_len] = '\0'; \
CXLB_priority = QB_MIN(CXLB_priority, (priority)); /* severest? */ \
} \
free(CXLB_buf); \
} \
} while (0)
/*!
* \internal
* \brief Bit flags to control format in XML logs and dumps
*/
enum pcmk__xml_fmt_options {
//! Exclude certain XML attributes (for calculating digests)
pcmk__xml_fmt_filtered = (1 << 0),
//! Include indentation and newlines
pcmk__xml_fmt_pretty = (1 << 1),
//! Include the opening tag of an XML element, and include XML comments
pcmk__xml_fmt_open = (1 << 3),
//! Include the children of an XML element
pcmk__xml_fmt_children = (1 << 4),
//! Include the closing tag of an XML element
pcmk__xml_fmt_close = (1 << 5),
// @COMPAT Can we start including text nodes unconditionally?
//! Include XML text nodes
pcmk__xml_fmt_text = (1 << 6),
};
int pcmk__xml_show(pcmk__output_t *out, const char *prefix, const xmlNode *data,
int depth, uint32_t options);
int pcmk__xml_show_changes(pcmk__output_t *out, const xmlNode *xml);
enum pcmk__xml_artefact_ns {
pcmk__xml_artefact_ns_legacy_rng = 1,
pcmk__xml_artefact_ns_legacy_xslt,
pcmk__xml_artefact_ns_base_rng,
pcmk__xml_artefact_ns_base_xslt,
};
void pcmk__strip_xml_text(xmlNode *xml);
/*!
* \internal
* \brief Indicators of which XML characters to escape
*
* XML allows the escaping of special characters by replacing them with entity
* references (for example, <tt>"&quot;"</tt>) or character references (for
* example, <tt>"&#13;"</tt>).
*
* The special characters <tt>'&'</tt> (except as the beginning of an entity
* reference) and <tt>'<'</tt> are not allowed in their literal forms in XML
* character data. Character data is non-markup text (for example, the content
* of a text node). <tt>'>'</tt> is allowed under most circumstances; we escape
* it for safety and symmetry.
*
* For more details, see the "Character Data and Markup" section of the XML
* spec, currently section 2.4:
* https://www.w3.org/TR/xml/#dt-markup
*
* Attribute values are handled specially.
* * If an attribute value is delimited by single quotes, then single quotes
* must be escaped within the value.
* * Similarly, if an attribute value is delimited by double quotes, then double
* quotes must be escaped within the value.
* * A conformant XML processor replaces a literal whitespace character (tab,
* newline, carriage return, space) in an attribute value with a space
* (\c '#x20') character. However, a reference to a whitespace character (for
* example, \c "&#x0A;" for \c '\n') does not get replaced.
* * For more details, see the "Attribute-Value Normalization" section of the
* XML spec, currently section 3.3.3. Note that the default attribute type
* is CDATA; we don't deal with NMTOKENS, etc.:
* https://www.w3.org/TR/xml/#AVNormalize
*
* Pacemaker always delimits attribute values with double quotes, so there's no
* need to escape single quotes.
*
* Newlines and tabs should be escaped in attribute values when XML is
* serialized to text, so that future parsing preserves them rather than
* normalizing them to spaces.
*
* We always escape carriage returns, so that they're not converted to spaces
* during attribute-value normalization and because displaying them as literals
* is messy.
*/
enum pcmk__xml_escape_type {
/*!
* For text nodes.
* * Escape \c '<', \c '>', and \c '&' using entity references.
* * Do not escape \c '\n' and \c '\t'.
* * Escape other non-printing characters using character references.
*/
pcmk__xml_escape_text,
/*!
* For attribute values.
* * Escape \c '<', \c '>', \c '&', and \c '"' using entity references.
* * Escape \c '\n', \c '\t', and other non-printing characters using
* character references.
*/
pcmk__xml_escape_attr,
/* @COMPAT Drop escaping of at least '\n' and '\t' for
* pcmk__xml_escape_attr_pretty when openstack-info, openstack-floating-ip,
* and openstack-virtual-ip resource agents no longer depend on it.
*
* At time of writing, openstack-info may set a multiline value for the
* openstack_ports node attribute. The other two agents query the value and
* require it to be on one line with no spaces.
*/
/*!
* For attribute values displayed in text output delimited by double quotes.
* * Escape \c '\n' as \c "\\n"
* * Escape \c '\r' as \c "\\r"
* * Escape \c '\t' as \c "\\t"
* * Escape \c '"' as \c "\\""
*/
pcmk__xml_escape_attr_pretty,
};
bool pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type);
char *pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type);
/*!
* \internal
* \brief Get the root directory to scan XML artefacts of given kind for
*
* \param[in] ns governs the hierarchy nesting against the inherent root dir
*
* \return root directory to scan XML artefacts of given kind for
*/
char *
pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns);
/*!
* \internal
* \brief Get the fully unwrapped path to particular XML artifact (RNG/XSLT)
*
* \param[in] ns denotes path forming details (parent dir, suffix)
* \param[in] filespec symbolic file specification to be combined with
* #artefact_ns to form the final path
* \return unwrapped path to particular XML artifact (RNG/XSLT)
*/
char *pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns,
const char *filespec);
/*!
* \internal
* \brief Return first non-text child node of an XML node
*
* \param[in] parent XML node to check
*
* \return First non-text child node of \p parent (or NULL if none)
*/
static inline xmlNode *
pcmk__xml_first_child(const xmlNode *parent)
{
xmlNode *child = (parent? parent->children : NULL);
while (child && (child->type == XML_TEXT_NODE)) {
child = child->next;
}
return child;
}
/*!
* \internal
* \brief Return next non-text sibling node of an XML node
*
* \param[in] child XML node to check
*
* \return Next non-text sibling of \p child (or NULL if none)
*/
static inline xmlNode *
pcmk__xml_next(const xmlNode *child)
{
xmlNode *next = (child? child->next : NULL);
while (next && (next->type == XML_TEXT_NODE)) {
next = next->next;
}
return next;
}
void pcmk__xml_free(xmlNode *xml);
void pcmk__xml_free_doc(xmlDoc *doc);
xmlNode *pcmk__xml_copy(xmlNode *parent, xmlNode *src);
/*!
* \internal
* \brief Flags for operations affecting XML attributes
*/
enum pcmk__xa_flags {
//! Flag has no effect
pcmk__xaf_none = 0U,
//! Don't overwrite existing values
pcmk__xaf_no_overwrite = (1U << 0),
/*!
* Treat values as score updates where possible (see
* \c pcmk__xe_set_score())
*/
pcmk__xaf_score_update = (1U << 1),
};
void pcmk__xml_sanitize_id(char *id);
/* internal XML-related utilities */
/*!
* \internal
* \brief Flags related to XML change tracking and ACLs
*/
enum pcmk__xml_flags {
//! This flag has no effect
pcmk__xf_none = UINT32_C(0),
/*!
* Node was created or modified, or one of its descendants was created,
* modified, moved, or deleted.
*/
pcmk__xf_dirty = (UINT32_C(1) << 0),
//! Node was deleted (set for attribute only)
pcmk__xf_deleted = (UINT32_C(1) << 1),
//! Node was created
pcmk__xf_created = (UINT32_C(1) << 2),
//! Node was modified
pcmk__xf_modified = (UINT32_C(1) << 3),
/*!
* \brief Tracking is enabled (set for document only)
*
* Call \c pcmk__xml_commit_changes() before setting this flag if a clean
* start for tracking is needed.
*/
pcmk__xf_tracking = (UINT32_C(1) << 4),
//! Skip counting this node when getting a node's position among siblings
pcmk__xf_skip = (UINT32_C(1) << 6),
//! Node was moved
pcmk__xf_moved = (UINT32_C(1) << 7),
//! ACLs are enabled (set for document only)
pcmk__xf_acl_enabled = (UINT32_C(1) << 8),
/* @TODO Consider splitting the ACL permission flags (pcmk__xf_acl_read,
* pcmk__xf_acl_write, pcmk__xf_acl_write, and pcmk__xf_acl_create) into a
* separate enum and reserving this enum for tracking-related flags.
*
* The ACL permission flags have various meanings in different contexts (for
* example, what permission an ACL grants or denies; what permissions the
* current ACL user has for a given XML node; and possibly others). And
* for xml_acl_t objects, they're used in exclusive mode (exactly one is
* set), rather than as flags.
*/
//! ACL read permission
pcmk__xf_acl_read = (UINT32_C(1) << 9),
//! ACL write permission (implies read permission in most or all contexts)
pcmk__xf_acl_write = (UINT32_C(1) << 10),
//! ACL deny permission (that is, no permission)
pcmk__xf_acl_deny = (UINT32_C(1) << 11),
/*!
* ACL create permission for attributes (if attribute exists, this is mapped
* to \c pcmk__xf_acl_write)
*/
pcmk__xf_acl_create = (UINT32_C(1) << 12),
//! ACLs deny the user access (set for document only)
pcmk__xf_acl_denied = (UINT32_C(1) << 13),
//! Ignore attribute moves within an element (set for document only)
pcmk__xf_ignore_attr_pos = (UINT32_C(1) << 14),
};
void pcmk__xml_doc_set_flags(xmlDoc *doc, uint32_t flags);
bool pcmk__xml_doc_all_flags_set(const xmlDoc *xml, uint32_t flags);
void pcmk__xml_commit_changes(xmlDoc *doc);
void pcmk__xml_mark_changes(xmlNode *old_xml, xmlNode *new_xml);
bool pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *),
void *user_data);
static inline const char *
pcmk__xml_attr_value(const xmlAttr *attr)
{
return ((attr == NULL) || (attr->children == NULL))? NULL
: (const char *) attr->children->content;
}
-/*!
- * \internal
- * \brief Check whether a given CIB element was modified in a CIB patchset
- *
- * \param[in] patchset CIB XML patchset
- * \param[in] element XML tag of CIB element to check (\c NULL is equivalent
- * to \c PCMK_XE_CIB). Supported values include any CIB
- * element supported by \c pcmk__cib_abs_xpath_for().
- *
- * \return \c true if \p element was modified, or \c false otherwise
- */
+void pcmk__xml_patchset_add_digest(xmlNode *patchset, const xmlNode *target);
+int pcmk__xml_patchset_versions(const xmlNode *patchset, int source[3],
+ int target[3]);
+
bool pcmk__cib_element_in_patchset(const xmlNode *patchset,
const char *element);
#ifdef __cplusplus
}
#endif
#endif // PCMK__CRM_COMMON_XML_INTERNAL__H
diff --git a/lib/cib/cib_utils.c b/lib/cib/cib_utils.c
index 26a16a7f62..e591550d0f 100644
--- a/lib/cib/cib_utils.c
+++ b/lib/cib/cib_utils.c
@@ -1,954 +1,956 @@
/*
* Original copyright 2004 International Business Machines
* Later changes copyright 2008-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <sys/utsname.h>
#include <glib.h>
#include <crm/crm.h>
#include <crm/cib/internal.h>
#include <crm/common/cib_internal.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
gboolean
cib_version_details(xmlNode * cib, int *admin_epoch, int *epoch, int *updates)
{
*epoch = -1;
*updates = -1;
*admin_epoch = -1;
if (cib == NULL) {
return FALSE;
} else {
crm_element_value_int(cib, PCMK_XA_EPOCH, epoch);
crm_element_value_int(cib, PCMK_XA_NUM_UPDATES, updates);
crm_element_value_int(cib, PCMK_XA_ADMIN_EPOCH, admin_epoch);
}
return TRUE;
}
gboolean
cib_diff_version_details(xmlNode * diff, int *admin_epoch, int *epoch, int *updates,
int *_admin_epoch, int *_epoch, int *_updates)
{
int add[] = { 0, 0, 0 };
int del[] = { 0, 0, 0 };
- xml_patch_versions(diff, add, del);
+ pcmk__xml_patchset_versions(diff, del, add);
*admin_epoch = add[0];
*epoch = add[1];
*updates = add[2];
*_admin_epoch = del[0];
*_epoch = del[1];
*_updates = del[2];
return TRUE;
}
/*!
* \internal
* \brief Get the XML patchset from a CIB diff notification
*
* \param[in] msg CIB diff notification
* \param[out] patchset Where to store XML patchset
*
* \return Standard Pacemaker return code
*/
int
cib__get_notify_patchset(const xmlNode *msg, const xmlNode **patchset)
{
int rc = pcmk_err_generic;
xmlNode *wrapper = NULL;
pcmk__assert(patchset != NULL);
*patchset = NULL;
if (msg == NULL) {
crm_err("CIB diff notification received with no XML");
return ENOMSG;
}
if ((crm_element_value_int(msg, PCMK__XA_CIB_RC, &rc) != 0)
|| (rc != pcmk_ok)) {
crm_warn("Ignore failed CIB update: %s " QB_XS " rc=%d",
pcmk_strerror(rc), rc);
crm_log_xml_debug(msg, "failed");
return pcmk_legacy2rc(rc);
}
wrapper = pcmk__xe_first_child(msg, PCMK__XE_CIB_UPDATE_RESULT, NULL, NULL);
*patchset = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
if (*patchset == NULL) {
crm_err("CIB diff notification received with no patchset");
return ENOMSG;
}
return pcmk_rc_ok;
}
/*!
* \brief Create XML for a new (empty) CIB
*
* \param[in] cib_epoch What to use as \c PCMK_XA_EPOCH CIB attribute
*
* \return Newly created XML for empty CIB
*
* \note It is the caller's responsibility to free the result with
* \c pcmk__xml_free().
*/
xmlNode *
createEmptyCib(int cib_epoch)
{
xmlNode *cib_root = NULL, *config = NULL;
cib_root = pcmk__xe_create(NULL, PCMK_XE_CIB);
crm_xml_add(cib_root, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET);
crm_xml_add(cib_root, PCMK_XA_VALIDATE_WITH, pcmk__highest_schema_name());
crm_xml_add_int(cib_root, PCMK_XA_EPOCH, cib_epoch);
crm_xml_add_int(cib_root, PCMK_XA_NUM_UPDATES, 0);
crm_xml_add_int(cib_root, PCMK_XA_ADMIN_EPOCH, 0);
config = pcmk__xe_create(cib_root, PCMK_XE_CONFIGURATION);
pcmk__xe_create(cib_root, PCMK_XE_STATUS);
pcmk__xe_create(config, PCMK_XE_CRM_CONFIG);
pcmk__xe_create(config, PCMK_XE_NODES);
pcmk__xe_create(config, PCMK_XE_RESOURCES);
pcmk__xe_create(config, PCMK_XE_CONSTRAINTS);
#if PCMK__RESOURCE_STICKINESS_DEFAULT != 0
{
xmlNode *rsc_defaults = pcmk__xe_create(config, PCMK_XE_RSC_DEFAULTS);
xmlNode *meta = pcmk__xe_create(rsc_defaults, PCMK_XE_META_ATTRIBUTES);
xmlNode *nvpair = pcmk__xe_create(meta, PCMK_XE_NVPAIR);
crm_xml_add(meta, PCMK_XA_ID, "build-resource-defaults");
crm_xml_add(nvpair, PCMK_XA_ID, "build-" PCMK_META_RESOURCE_STICKINESS);
crm_xml_add(nvpair, PCMK_XA_NAME, PCMK_META_RESOURCE_STICKINESS);
crm_xml_add_int(nvpair, PCMK_XA_VALUE,
PCMK__RESOURCE_STICKINESS_DEFAULT);
}
#endif
return cib_root;
}
static bool
cib_acl_enabled(xmlNode *xml, const char *user)
{
bool rc = FALSE;
if(pcmk_acl_required(user)) {
const char *value = NULL;
GHashTable *options = pcmk__strkey_table(free, free);
cib_read_config(options, xml);
value = pcmk__cluster_option(options, PCMK_OPT_ENABLE_ACL);
rc = crm_is_true(value);
g_hash_table_destroy(options);
}
crm_trace("CIB ACL is %s", rc ? "enabled" : "disabled");
return rc;
}
/*!
* \internal
* \brief Determine whether to perform operations on a scratch copy of the CIB
*
* \param[in] op CIB operation
* \param[in] section CIB section
* \param[in] call_options CIB call options
*
* \return \p true if we should make a copy of the CIB, or \p false otherwise
*/
static bool
should_copy_cib(const char *op, const char *section, int call_options)
{
if (pcmk_is_set(call_options, cib_dryrun)) {
// cib_dryrun implies a scratch copy by definition; no side effects
return true;
}
if (pcmk__str_eq(op, PCMK__CIB_REQUEST_COMMIT_TRANSACT, pcmk__str_none)) {
/* Commit-transaction must make a copy for atomicity. We must revert to
* the original CIB if the entire transaction cannot be applied
* successfully.
*/
return true;
}
if (pcmk_is_set(call_options, cib_transaction)) {
/* If cib_transaction is set, then we're in the process of committing a
* transaction. The commit-transaction request already made a scratch
* copy, and we're accumulating changes in that copy.
*/
return false;
}
if (pcmk__str_eq(section, PCMK_XE_STATUS, pcmk__str_none)) {
/* Copying large CIBs accounts for a huge percentage of our CIB usage,
* and this avoids some of it.
*
* @TODO: Is this safe? See discussion at
* https://github.com/ClusterLabs/pacemaker/pull/3094#discussion_r1211400690.
*/
return false;
}
// Default behavior is to operate on a scratch copy
return true;
}
int
cib_perform_op(cib_t *cib, const char *op, uint32_t call_options,
cib__op_fn_t fn, bool is_query, const char *section,
xmlNode *req, xmlNode *input, bool manage_counters,
bool *config_changed, xmlNode **current_cib,
xmlNode **result_cib, xmlNode **diff, xmlNode **output)
{
int rc = pcmk_ok;
bool check_schema = true;
bool make_copy = true;
xmlNode *top = NULL;
xmlNode *scratch = NULL;
xmlNode *patchset_cib = NULL;
xmlNode *local_diff = NULL;
const char *user = crm_element_value(req, PCMK__XA_CIB_USER);
const bool enable_acl = cib_acl_enabled(*current_cib, user);
bool with_digest = false;
crm_trace("Begin %s%s%s op",
(pcmk_is_set(call_options, cib_dryrun)? "dry run of " : ""),
(is_query? "read-only " : ""), op);
CRM_CHECK(output != NULL, return -ENOMSG);
CRM_CHECK(current_cib != NULL, return -ENOMSG);
CRM_CHECK(result_cib != NULL, return -ENOMSG);
CRM_CHECK(config_changed != NULL, return -ENOMSG);
if(output) {
*output = NULL;
}
*result_cib = NULL;
*config_changed = false;
if (fn == NULL) {
return -EINVAL;
}
if (is_query) {
xmlNode *cib_ro = *current_cib;
xmlNode *cib_filtered = NULL;
if (enable_acl
&& xml_acl_filtered_copy(user, *current_cib, *current_cib,
&cib_filtered)) {
if (cib_filtered == NULL) {
crm_debug("Pre-filtered the entire cib");
return -EACCES;
}
cib_ro = cib_filtered;
crm_log_xml_trace(cib_ro, "filtered");
}
rc = (*fn) (op, call_options, section, req, input, cib_ro, result_cib, output);
if(output == NULL || *output == NULL) {
/* nothing */
} else if(cib_filtered == *output) {
cib_filtered = NULL; /* Let them have this copy */
} else if (*output == *current_cib) {
/* They already know not to free it */
} else if(cib_filtered && (*output)->doc == cib_filtered->doc) {
/* We're about to free the document of which *output is a part */
*output = pcmk__xml_copy(NULL, *output);
} else if ((*output)->doc == (*current_cib)->doc) {
/* Give them a copy they can free */
*output = pcmk__xml_copy(NULL, *output);
}
pcmk__xml_free(cib_filtered);
return rc;
}
make_copy = should_copy_cib(op, section, call_options);
if (!make_copy) {
/* Conditional on v2 patch style */
scratch = *current_cib;
// Make a copy of the top-level element to store version details
top = pcmk__xe_create(NULL, (const char *) scratch->name);
pcmk__xe_copy_attrs(top, scratch, pcmk__xaf_none);
patchset_cib = top;
pcmk__xml_commit_changes(scratch->doc);
pcmk__xml_doc_set_flags(scratch->doc, pcmk__xf_tracking);
if (enable_acl) {
pcmk__enable_acl(*current_cib, scratch, user);
}
rc = (*fn) (op, call_options, section, req, input, scratch, &scratch, output);
/* If scratch points to a new object now (for example, after an erase
* operation), then *current_cib should point to the same object.
*
* @TODO Enable tracking and ACLs and calculate changes? Change tracking
* and unpacked ACLs didn't carry over to new object.
*/
*current_cib = scratch;
} else {
scratch = pcmk__xml_copy(NULL, *current_cib);
patchset_cib = *current_cib;
pcmk__xml_doc_set_flags(scratch->doc, pcmk__xf_tracking);
if (enable_acl) {
pcmk__enable_acl(*current_cib, scratch, user);
}
rc = (*fn) (op, call_options, section, req, input, *current_cib,
&scratch, output);
/* @TODO This appears to be a hack to determine whether scratch points
* to a new object now, without saving the old pointer (which may be
* invalid now) for comparison. Confirm this, and check more clearly.
*/
if (!pcmk__xml_doc_all_flags_set(scratch->doc, pcmk__xf_tracking)) {
crm_trace("Inferring changes after %s op", op);
pcmk__xml_commit_changes(scratch->doc);
if (enable_acl) {
pcmk__enable_acl(*current_cib, scratch, user);
}
pcmk__xml_mark_changes(*current_cib, scratch);
}
CRM_CHECK(*current_cib != scratch, return -EINVAL);
}
xml_acl_disable(scratch); /* Allow the system to make any additional changes */
if (rc == pcmk_ok && scratch == NULL) {
rc = -EINVAL;
goto done;
} else if(rc == pcmk_ok && xml_acl_denied(scratch)) {
crm_trace("ACL rejected part or all of the proposed changes");
rc = -EACCES;
goto done;
} else if (rc != pcmk_ok) {
goto done;
}
/* If the CIB is from a file, we don't need to check that the feature set is
* supported. All we care about in that case is the schema version, which
* is checked elsewhere.
*/
if (scratch && (cib == NULL || cib->variant != cib_file)) {
const char *new_version = crm_element_value(scratch, PCMK_XA_CRM_FEATURE_SET);
rc = pcmk__check_feature_set(new_version);
if (rc != pcmk_rc_ok) {
crm_err("Discarding update with feature set '%s' greater than "
"our own '%s'", new_version, CRM_FEATURE_SET);
rc = pcmk_rc2legacy(rc);
goto done;
}
}
if (patchset_cib != NULL) {
int old = 0;
int new = 0;
crm_element_value_int(scratch, PCMK_XA_ADMIN_EPOCH, &new);
crm_element_value_int(patchset_cib, PCMK_XA_ADMIN_EPOCH, &old);
if (old > new) {
crm_err("%s went backwards: %d -> %d (Opts: %#x)",
PCMK_XA_ADMIN_EPOCH, old, new, call_options);
crm_log_xml_warn(req, "Bad Op");
crm_log_xml_warn(input, "Bad Data");
rc = -pcmk_err_old_data;
} else if (old == new) {
crm_element_value_int(scratch, PCMK_XA_EPOCH, &new);
crm_element_value_int(patchset_cib, PCMK_XA_EPOCH, &old);
if (old > new) {
crm_err("%s went backwards: %d -> %d (Opts: %#x)",
PCMK_XA_EPOCH, old, new, call_options);
crm_log_xml_warn(req, "Bad Op");
crm_log_xml_warn(input, "Bad Data");
rc = -pcmk_err_old_data;
}
}
}
crm_trace("Massaging CIB contents");
pcmk__strip_xml_text(scratch);
if (make_copy) {
static time_t expires = 0;
time_t tm_now = time(NULL);
if (expires < tm_now) {
expires = tm_now + 60; /* Validate clients are correctly applying v2-style diffs at most once a minute */
with_digest = true;
}
}
local_diff = xml_create_patchset(0, patchset_cib, scratch,
config_changed, manage_counters);
pcmk__log_xml_changes(LOG_TRACE, scratch);
pcmk__xml_commit_changes(scratch->doc);
if(local_diff) {
- patchset_process_digest(local_diff, patchset_cib, scratch, with_digest);
+ if (with_digest) {
+ pcmk__xml_patchset_add_digest(local_diff, scratch);
+ }
pcmk__log_xml_patchset(LOG_INFO, local_diff);
crm_log_xml_trace(local_diff, "raw patch");
}
if (make_copy && (local_diff != NULL)) {
// Original to compare against doesn't exist
pcmk__if_tracing(
{
// Validate the calculated patch set
int test_rc = pcmk_ok;
int format = 1;
xmlNode *cib_copy = pcmk__xml_copy(NULL, patchset_cib);
crm_element_value_int(local_diff, PCMK_XA_FORMAT, &format);
test_rc = xml_apply_patchset(cib_copy, local_diff,
manage_counters);
if (test_rc != pcmk_ok) {
save_xml_to_file(cib_copy, "PatchApply:calculated", NULL);
save_xml_to_file(patchset_cib, "PatchApply:input", NULL);
save_xml_to_file(scratch, "PatchApply:actual", NULL);
save_xml_to_file(local_diff, "PatchApply:diff", NULL);
crm_err("v%d patchset error, patch failed to apply: %s "
"(%d)",
format, pcmk_rc_str(pcmk_legacy2rc(test_rc)),
test_rc);
}
pcmk__xml_free(cib_copy);
},
{}
);
}
if (pcmk__str_eq(section, PCMK_XE_STATUS, pcmk__str_casei)) {
/* Throttle the amount of costly validation we perform due to status updates
* a) we don't really care whats in the status section
* b) we don't validate any of its contents at the moment anyway
*/
check_schema = false;
}
/* === scratch must not be modified after this point ===
* Exceptions, anything in:
static filter_t filter[] = {
{ 0, PCMK_XA_CRM_DEBUG_ORIGIN },
{ 0, PCMK_XA_CIB_LAST_WRITTEN },
{ 0, PCMK_XA_UPDATE_ORIGIN },
{ 0, PCMK_XA_UPDATE_CLIENT },
{ 0, PCMK_XA_UPDATE_USER },
};
*/
if (*config_changed && !pcmk_is_set(call_options, cib_no_mtime)) {
const char *schema = crm_element_value(scratch, PCMK_XA_VALIDATE_WITH);
if (schema == NULL) {
rc = -pcmk_err_cib_corrupt;
}
pcmk__xe_add_last_written(scratch);
pcmk__warn_if_schema_deprecated(schema);
/* Make values of origin, client, and user in scratch match
* the ones in req (if the schema allows the attributes)
*/
if (pcmk__cmp_schemas_by_name(schema, "pacemaker-1.2") >= 0) {
const char *origin = crm_element_value(req, PCMK__XA_SRC);
const char *client = crm_element_value(req,
PCMK__XA_CIB_CLIENTNAME);
if (origin != NULL) {
crm_xml_add(scratch, PCMK_XA_UPDATE_ORIGIN, origin);
} else {
pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_ORIGIN);
}
if (client != NULL) {
crm_xml_add(scratch, PCMK_XA_UPDATE_CLIENT, user);
} else {
pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_CLIENT);
}
if (user != NULL) {
crm_xml_add(scratch, PCMK_XA_UPDATE_USER, user);
} else {
pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_USER);
}
}
}
crm_trace("Perform validation: %s", pcmk__btoa(check_schema));
if ((rc == pcmk_ok) && check_schema
&& !pcmk__configured_schema_validates(scratch)) {
rc = -pcmk_err_schema_validation;
}
done:
*result_cib = scratch;
/* @TODO: This may not work correctly with !make_copy, since we don't
* keep the original CIB.
*/
if ((rc != pcmk_ok) && cib_acl_enabled(patchset_cib, user)
&& xml_acl_filtered_copy(user, patchset_cib, scratch, result_cib)) {
if (*result_cib == NULL) {
crm_debug("Pre-filtered the entire cib result");
}
pcmk__xml_free(scratch);
}
if(diff) {
*diff = local_diff;
} else {
pcmk__xml_free(local_diff);
}
pcmk__xml_free(top);
crm_trace("Done");
return rc;
}
int
cib__create_op(cib_t *cib, const char *op, const char *host,
const char *section, xmlNode *data, int call_options,
const char *user_name, const char *client_name,
xmlNode **op_msg)
{
CRM_CHECK((cib != NULL) && (op_msg != NULL), return -EPROTO);
*op_msg = pcmk__xe_create(NULL, PCMK__XE_CIB_COMMAND);
cib->call_id++;
if (cib->call_id < 1) {
cib->call_id = 1;
}
crm_xml_add(*op_msg, PCMK__XA_T, PCMK__VALUE_CIB);
crm_xml_add(*op_msg, PCMK__XA_CIB_OP, op);
crm_xml_add(*op_msg, PCMK__XA_CIB_HOST, host);
crm_xml_add(*op_msg, PCMK__XA_CIB_SECTION, section);
crm_xml_add(*op_msg, PCMK__XA_CIB_USER, user_name);
crm_xml_add(*op_msg, PCMK__XA_CIB_CLIENTNAME, client_name);
crm_xml_add_int(*op_msg, PCMK__XA_CIB_CALLID, cib->call_id);
crm_trace("Sending call options: %.8lx, %d", (long)call_options, call_options);
crm_xml_add_int(*op_msg, PCMK__XA_CIB_CALLOPT, call_options);
if (data != NULL) {
xmlNode *wrapper = pcmk__xe_create(*op_msg, PCMK__XE_CIB_CALLDATA);
pcmk__xml_copy(wrapper, data);
}
return pcmk_ok;
}
/*!
* \internal
* \brief Check whether a CIB request is supported in a transaction
*
* \param[in] request CIB request
*
* \return Standard Pacemaker return code
*/
static int
validate_transaction_request(const xmlNode *request)
{
const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
const char *host = crm_element_value(request, PCMK__XA_CIB_HOST);
const cib__operation_t *operation = NULL;
int rc = cib__get_operation(op, &operation);
if (rc != pcmk_rc_ok) {
// cib__get_operation() logs error
return rc;
}
if (!pcmk_is_set(operation->flags, cib__op_attr_transaction)) {
crm_err("Operation %s is not supported in CIB transactions", op);
return EOPNOTSUPP;
}
if (host != NULL) {
crm_err("Operation targeting a specific node (%s) is not supported in "
"a CIB transaction",
host);
return EOPNOTSUPP;
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Append a CIB request to a CIB transaction
*
* \param[in,out] cib CIB client whose transaction to extend
* \param[in,out] request Request to add to transaction
*
* \return Legacy Pacemaker return code
*/
int
cib__extend_transaction(cib_t *cib, xmlNode *request)
{
int rc = pcmk_rc_ok;
pcmk__assert((cib != NULL) && (request != NULL));
rc = validate_transaction_request(request);
if ((rc == pcmk_rc_ok) && (cib->transaction == NULL)) {
rc = pcmk_rc_no_transaction;
}
if (rc == pcmk_rc_ok) {
pcmk__xml_copy(cib->transaction, request);
} else {
const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
const char *client_id = NULL;
cib->cmds->client_id(cib, NULL, &client_id);
crm_err("Failed to add '%s' operation to transaction for client %s: %s",
op, pcmk__s(client_id, "(unidentified)"), pcmk_rc_str(rc));
crm_log_xml_info(request, "failed");
}
return pcmk_rc2legacy(rc);
}
void
cib_native_callback(cib_t * cib, xmlNode * msg, int call_id, int rc)
{
xmlNode *output = NULL;
cib_callback_client_t *blob = NULL;
if (msg != NULL) {
xmlNode *wrapper = NULL;
crm_element_value_int(msg, PCMK__XA_CIB_RC, &rc);
crm_element_value_int(msg, PCMK__XA_CIB_CALLID, &call_id);
wrapper = pcmk__xe_first_child(msg, PCMK__XE_CIB_CALLDATA, NULL, NULL);
output = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
}
blob = cib__lookup_id(call_id);
if (blob == NULL) {
crm_trace("No callback found for call %d", call_id);
}
if (cib == NULL) {
crm_debug("No cib object supplied");
}
if (rc == -pcmk_err_diff_resync) {
/* This is an internal value that clients do not and should not care about */
rc = pcmk_ok;
}
if (blob && blob->callback && (rc == pcmk_ok || blob->only_success == FALSE)) {
crm_trace("Invoking callback %s for call %d",
pcmk__s(blob->id, "without ID"), call_id);
blob->callback(msg, call_id, rc, output, blob->user_data);
} else if ((cib != NULL) && (rc != pcmk_ok)) {
crm_warn("CIB command failed: %s", pcmk_strerror(rc));
crm_log_xml_debug(msg, "Failed CIB Update");
}
/* This may free user_data, so do it after the callback */
if (blob) {
remove_cib_op_callback(call_id, FALSE);
}
crm_trace("OP callback activated for %d", call_id);
}
void
cib_native_notify(gpointer data, gpointer user_data)
{
xmlNode *msg = user_data;
cib_notify_client_t *entry = data;
const char *event = NULL;
if (msg == NULL) {
crm_warn("Skipping callback - NULL message");
return;
}
event = crm_element_value(msg, PCMK__XA_SUBT);
if (entry == NULL) {
crm_warn("Skipping callback - NULL callback client");
return;
} else if (entry->callback == NULL) {
crm_warn("Skipping callback - NULL callback");
return;
} else if (!pcmk__str_eq(entry->event, event, pcmk__str_casei)) {
crm_trace("Skipping callback - event mismatch %p/%s vs. %s", entry, entry->event, event);
return;
}
crm_trace("Invoking callback for %p/%s event...", entry, event);
entry->callback(event, msg);
crm_trace("Callback invoked...");
}
gboolean
cib_read_config(GHashTable * options, xmlNode * current_cib)
{
xmlNode *config = NULL;
crm_time_t *now = NULL;
if (options == NULL || current_cib == NULL) {
return FALSE;
}
now = crm_time_new(NULL);
g_hash_table_remove_all(options);
config = pcmk_find_cib_element(current_cib, PCMK_XE_CRM_CONFIG);
if (config) {
pcmk_rule_input_t rule_input = {
.now = now,
};
pcmk_unpack_nvpair_blocks(config, PCMK_XE_CLUSTER_PROPERTY_SET,
PCMK_VALUE_CIB_BOOTSTRAP_OPTIONS, &rule_input,
options, NULL);
}
pcmk__validate_cluster_options(options);
crm_time_free(now);
return TRUE;
}
int
cib_internal_op(cib_t * cib, const char *op, const char *host,
const char *section, xmlNode * data,
xmlNode ** output_data, int call_options, const char *user_name)
{
int (*delegate)(cib_t *cib, const char *op, const char *host,
const char *section, xmlNode *data, xmlNode **output_data,
int call_options, const char *user_name) = NULL;
if (cib == NULL) {
return -EINVAL;
}
delegate = cib->delegate_fn;
if (delegate == NULL) {
return -EPROTONOSUPPORT;
}
if (user_name == NULL) {
user_name = getenv("CIB_user");
}
return delegate(cib, op, host, section, data, output_data, call_options, user_name);
}
/*!
* \brief Apply a CIB update patch to a given CIB
*
* \param[in] event CIB update patch
* \param[in] input CIB to patch
* \param[out] output Resulting CIB after patch
* \param[in] level Log the patch at this log level (unless LOG_CRIT)
*
* \return Legacy Pacemaker return code
* \note sbd calls this function
*/
int
cib_apply_patch_event(xmlNode *event, xmlNode *input, xmlNode **output,
int level)
{
int rc = pcmk_err_generic;
xmlNode *wrapper = NULL;
xmlNode *diff = NULL;
pcmk__assert((event != NULL) && (input != NULL) && (output != NULL));
crm_element_value_int(event, PCMK__XA_CIB_RC, &rc);
wrapper = pcmk__xe_first_child(event, PCMK__XE_CIB_UPDATE_RESULT, NULL,
NULL);
diff = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
if (rc < pcmk_ok || diff == NULL) {
return rc;
}
if (level > LOG_CRIT) {
pcmk__log_xml_patchset(level, diff);
}
if (input != NULL) {
rc = cib_process_diff(NULL, cib_none, NULL, event, diff, input, output,
NULL);
if (rc != pcmk_ok) {
crm_debug("Update didn't apply: %s (%d) %p",
pcmk_strerror(rc), rc, *output);
if (rc == -pcmk_err_old_data) {
crm_trace("Masking error, we already have the supplied update");
return pcmk_ok;
}
pcmk__xml_free(*output);
*output = NULL;
return rc;
}
}
return rc;
}
#define log_signon_query_err(out, fmt, args...) do { \
if (out != NULL) { \
out->err(out, fmt, ##args); \
} else { \
crm_err(fmt, ##args); \
} \
} while (0)
int
cib__signon_query(pcmk__output_t *out, cib_t **cib, xmlNode **cib_object)
{
int rc = pcmk_rc_ok;
cib_t *cib_conn = NULL;
pcmk__assert(cib_object != NULL);
if (cib == NULL) {
cib_conn = cib_new();
} else {
if (*cib == NULL) {
*cib = cib_new();
}
cib_conn = *cib;
}
if (cib_conn == NULL) {
return ENOMEM;
}
if (cib_conn->state == cib_disconnected) {
rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command);
rc = pcmk_legacy2rc(rc);
}
if (rc != pcmk_rc_ok) {
log_signon_query_err(out, "Could not connect to the CIB: %s",
pcmk_rc_str(rc));
goto done;
}
if (out != NULL) {
out->transient(out, "Querying CIB...");
}
rc = cib_conn->cmds->query(cib_conn, NULL, cib_object, cib_sync_call);
rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
log_signon_query_err(out, "CIB query failed: %s", pcmk_rc_str(rc));
}
done:
if (cib == NULL) {
cib__clean_up_connection(&cib_conn);
}
if ((rc == pcmk_rc_ok) && (*cib_object == NULL)) {
return pcmk_rc_no_input;
}
return rc;
}
int
cib__signon_attempts(cib_t *cib, enum cib_conn_type type, int attempts)
{
int rc = pcmk_rc_ok;
crm_trace("Attempting connection to CIB manager (up to %d time%s)",
attempts, pcmk__plural_s(attempts));
for (int remaining = attempts - 1; remaining >= 0; --remaining) {
rc = cib->cmds->signon(cib, crm_system_name, type);
if ((rc == pcmk_rc_ok)
|| (remaining == 0)
|| ((errno != EAGAIN) && (errno != EALREADY))) {
break;
}
// Retry after soft error (interrupted by signal, etc.)
pcmk__sleep_ms((attempts - remaining) * 500);
crm_debug("Re-attempting connection to CIB manager (%d attempt%s remaining)",
remaining, pcmk__plural_s(remaining));
}
return rc;
}
int
cib__clean_up_connection(cib_t **cib)
{
int rc;
if (*cib == NULL) {
return pcmk_rc_ok;
}
rc = (*cib)->cmds->signoff(*cib);
cib_delete(*cib);
*cib = NULL;
return pcmk_legacy2rc(rc);
}
diff --git a/lib/common/acl.c b/lib/common/acl.c
index e14d3f14c4..12c741e0e5 100644
--- a/lib/common/acl.c
+++ b/lib/common/acl.c
@@ -1,922 +1,930 @@
/*
* Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <stdio.h>
#include <sys/types.h>
#include <pwd.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <libxml/tree.h> // xmlNode, etc.
#include <libxml/xmlstring.h> // xmlChar
#include <libxml/xpath.h> // xmlXPathObject, etc.
#include <crm/crm.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
#include "crmcommon_private.h"
typedef struct xml_acl_s {
enum pcmk__xml_flags mode;
gchar *xpath;
} xml_acl_t;
static void
free_acl(void *data)
{
if (data) {
xml_acl_t *acl = data;
g_free(acl->xpath);
free(acl);
}
}
void
pcmk__free_acls(GList *acls)
{
g_list_free_full(acls, free_acl);
}
static GList *
create_acl(const xmlNode *xml, GList *acls, enum pcmk__xml_flags mode)
{
xml_acl_t *acl = NULL;
const char *tag = crm_element_value(xml, PCMK_XA_OBJECT_TYPE);
const char *ref = crm_element_value(xml, PCMK_XA_REFERENCE);
const char *xpath = crm_element_value(xml, PCMK_XA_XPATH);
const char *attr = crm_element_value(xml, PCMK_XA_ATTRIBUTE);
if ((tag == NULL) && (ref == NULL) && (xpath == NULL)) {
// Schema should prevent this, but to be safe ...
crm_trace("Ignoring ACL <%s> element without selection criteria",
xml->name);
return NULL;
}
acl = pcmk__assert_alloc(1, sizeof (xml_acl_t));
acl->mode = mode;
if (xpath) {
acl->xpath = g_strdup(xpath);
crm_trace("Unpacked ACL <%s> element using xpath: %s",
xml->name, acl->xpath);
} else {
GString *buf = g_string_sized_new(128);
if ((ref != NULL) && (attr != NULL)) {
// NOTE: schema currently does not allow this
pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" PCMK_XA_ID "='",
ref, "' and @", attr, "]", NULL);
} else if (ref != NULL) {
pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" PCMK_XA_ID "='",
ref, "']", NULL);
} else if (attr != NULL) {
pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@", attr, "]", NULL);
} else {
pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), NULL);
}
acl->xpath = buf->str;
g_string_free(buf, FALSE);
crm_trace("Unpacked ACL <%s> element as xpath: %s",
xml->name, acl->xpath);
}
return g_list_append(acls, acl);
}
/*!
* \internal
* \brief Unpack a user, group, or role subtree of the ACLs section
*
* \param[in] acl_top XML of entire ACLs section
* \param[in] acl_entry XML of ACL element being unpacked
* \param[in,out] acls List of ACLs unpacked so far
*
* \return New head of (possibly modified) acls
*
* \note This function is recursive
*/
static GList *
parse_acl_entry(const xmlNode *acl_top, const xmlNode *acl_entry, GList *acls)
{
for (const xmlNode *child = pcmk__xe_first_child(acl_entry, NULL, NULL,
NULL);
child != NULL; child = pcmk__xe_next(child, NULL)) {
if (pcmk__xe_is(child, PCMK_XE_ACL_PERMISSION)) {
const char *kind = crm_element_value(child, PCMK_XA_KIND);
pcmk__assert(kind != NULL);
crm_trace("Unpacking <" PCMK_XE_ACL_PERMISSION "> element of "
"kind '%s'",
kind);
if (pcmk__str_eq(kind, PCMK_VALUE_READ, pcmk__str_none)) {
acls = create_acl(child, acls, pcmk__xf_acl_read);
} else if (pcmk__str_eq(kind, PCMK_VALUE_WRITE, pcmk__str_none)) {
acls = create_acl(child, acls, pcmk__xf_acl_write);
} else if (pcmk__str_eq(kind, PCMK_VALUE_DENY, pcmk__str_none)) {
acls = create_acl(child, acls, pcmk__xf_acl_deny);
} else {
crm_warn("Ignoring unknown ACL kind '%s'", kind);
}
} else if (pcmk__xe_is(child, PCMK_XE_ROLE)) {
const char *ref_role = crm_element_value(child, PCMK_XA_ID);
crm_trace("Unpacking <" PCMK_XE_ROLE "> element");
if (ref_role == NULL) {
continue;
}
for (xmlNode *role = pcmk__xe_first_child(acl_top, NULL, NULL,
NULL);
role != NULL; role = pcmk__xe_next(role, NULL)) {
const char *role_id = NULL;
if (!pcmk__xe_is(role, PCMK_XE_ACL_ROLE)) {
continue;
}
role_id = crm_element_value(role, PCMK_XA_ID);
if (pcmk__str_eq(ref_role, role_id, pcmk__str_none)) {
crm_trace("Unpacking referenced role '%s' in <%s> element",
role_id, acl_entry->name);
acls = parse_acl_entry(acl_top, role, acls);
break;
}
}
}
}
return acls;
}
/*
<acls>
<acl_target id="l33t-haxor"><role id="auto-l33t-haxor"/></acl_target>
<acl_role id="auto-l33t-haxor">
<acl_permission id="crook-nothing" kind="deny" xpath="/cib"/>
</acl_role>
<acl_target id="niceguy">
<role id="observer"/>
</acl_target>
<acl_role id="observer">
<acl_permission id="observer-read-1" kind="read" xpath="/cib"/>
<acl_permission id="observer-write-1" kind="write" xpath="//nvpair[@name='stonith-enabled']"/>
<acl_permission id="observer-write-2" kind="write" xpath="//nvpair[@name='target-role']"/>
</acl_role>
<acl_target id="badidea"><role id="auto-badidea"/></acl_target>
<acl_role id="auto-badidea">
<acl_permission id="badidea-resources" kind="read" xpath="//meta_attributes"/>
<acl_permission id="badidea-resources-2" kind="deny" reference="dummy-meta_attributes"/>
</acl_role>
</acls>
*/
static const char *
acl_to_text(enum pcmk__xml_flags flags)
{
if (pcmk_is_set(flags, pcmk__xf_acl_deny)) {
return "deny";
} else if (pcmk_any_flags_set(flags, pcmk__xf_acl_write|pcmk__xf_acl_create)) {
return "read/write";
} else if (pcmk_is_set(flags, pcmk__xf_acl_read)) {
return "read";
}
return "none";
}
void
pcmk__apply_acl(xmlNode *xml)
{
GList *aIter = NULL;
- xml_doc_private_t *docpriv = xml->doc->_private;
- xml_node_private_t *nodepriv;
+ xml_doc_private_t *docpriv = NULL;
+ xml_node_private_t *nodepriv = NULL;
xmlXPathObject *xpathObj = NULL;
- if (!xml_acl_enabled(xml)) {
+ pcmk__assert(xml != NULL);
+
+ if (!pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_acl_enabled)) {
crm_trace("Skipping ACLs for user '%s' because not enabled for this XML",
docpriv->acl_user);
return;
}
+ docpriv = xml->doc->_private;
+
for (aIter = docpriv->acls; aIter != NULL; aIter = aIter->next) {
int max = 0, lpc = 0;
xml_acl_t *acl = aIter->data;
xpathObj = pcmk__xpath_search(xml->doc, acl->xpath);
max = pcmk__xpath_num_results(xpathObj);
for (lpc = 0; lpc < max; lpc++) {
xmlNode *match = pcmk__xpath_result(xpathObj, lpc);
if (match == NULL) {
continue;
}
/* @COMPAT If the ACL's XPath matches a node that is neither an
* element nor a document, we apply the ACL to the parent element
* rather than to the matched node. For example, if the XPath
* matches a "score" attribute, then it applies to every element
* that contains a "score" attribute. That is, the XPath expression
* "//@score" matches all attributes named "score", but we apply the
* ACL to all elements containing such an attribute.
*
* This behavior is incorrect from an XPath standpoint and is thus
* confusing and counterintuitive. The correct way to match all
* elements containing a "score" attribute is to use an XPath
* predicate: "// *[@score]". (Space inserted after slashes so that
* GCC doesn't throw an error about nested comments.)
*
* Additionally, if an XPath expression matches the entire document
* (for example, "/"), then the ACL applies to the document's root
* element if it exists.
*
* These behaviors should be changed so that the ACL applies to the
* nodes matched by the XPath expression, or so that it doesn't
* apply at all if applying an ACL to an attribute doesn't make
* sense.
*
* Unfortunately, we document in Pacemaker Explained that matching
* attributes is a valid way to match elements: "Attributes may be
* specified in the XPath to select particular elements, but the
* permissions apply to the entire element."
*
* So we have to keep this behavior at least until a compatibility
* break. Even then, it's not feasible in the general case to
* transform such XPath expressions using XSLT.
*/
match = pcmk__xpath_match_element(match);
if (match == NULL) {
continue;
}
nodepriv = match->_private;
pcmk__set_xml_flags(nodepriv, acl->mode);
// Build a GString only if tracing is enabled
pcmk__if_tracing(
{
GString *path = pcmk__element_xpath(match);
crm_trace("Applying %s ACL to %s matched by %s",
acl_to_text(acl->mode), path->str, acl->xpath);
g_string_free(path, TRUE);
},
{}
);
}
crm_trace("Applied %s ACL %s (%d match%s)",
acl_to_text(acl->mode), acl->xpath, max,
((max == 1)? "" : "es"));
xmlXPathFreeObject(xpathObj);
}
}
/*!
* \internal
* \brief Unpack ACLs for a given user into the
* metadata of the target XML tree
*
* Taking the description of ACLs from the source XML tree and
* marking up the target XML tree with access information for the
* given user by tacking it onto the relevant nodes
*
* \param[in] source XML with ACL definitions
* \param[in,out] target XML that ACLs will be applied to
* \param[in] user Username whose ACLs need to be unpacked
*/
void
pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user)
{
xml_doc_private_t *docpriv = NULL;
if ((target == NULL) || (target->doc == NULL)
|| (target->doc->_private == NULL)) {
return;
}
docpriv = target->doc->_private;
if (!pcmk_acl_required(user)) {
crm_trace("Not unpacking ACLs because not required for user '%s'",
user);
} else if (docpriv->acls == NULL) {
xmlNode *acls = pcmk__xpath_find_one(source->doc, "//" PCMK_XE_ACLS,
LOG_NEVER);
pcmk__str_update(&(docpriv->acl_user), user);
if (acls) {
xmlNode *child = NULL;
for (child = pcmk__xe_first_child(acls, NULL, NULL, NULL);
child != NULL; child = pcmk__xe_next(child, NULL)) {
if (pcmk__xe_is(child, PCMK_XE_ACL_TARGET)) {
const char *id = crm_element_value(child, PCMK_XA_NAME);
if (id == NULL) {
id = crm_element_value(child, PCMK_XA_ID);
}
if (id && strcmp(id, user) == 0) {
crm_debug("Unpacking ACLs for user '%s'", id);
docpriv->acls = parse_acl_entry(acls, child, docpriv->acls);
}
} else if (pcmk__xe_is(child, PCMK_XE_ACL_GROUP)) {
const char *id = crm_element_value(child, PCMK_XA_NAME);
if (id == NULL) {
id = crm_element_value(child, PCMK_XA_ID);
}
if (id && pcmk__is_user_in_group(user,id)) {
crm_debug("Unpacking ACLs for group '%s'", id);
docpriv->acls = parse_acl_entry(acls, child, docpriv->acls);
}
}
}
}
}
}
/*!
* \internal
* \brief Copy source to target and set xf_acl_enabled flag in target
*
* \param[in] acl_source XML with ACL definitions
* \param[in,out] target XML that ACLs will be applied to
* \param[in] user Username whose ACLs need to be set
*/
void
pcmk__enable_acl(xmlNode *acl_source, xmlNode *target, const char *user)
{
if (target == NULL) {
return;
}
pcmk__unpack_acl(acl_source, target, user);
pcmk__xml_doc_set_flags(target->doc, pcmk__xf_acl_enabled);
pcmk__apply_acl(target);
}
static inline bool
test_acl_mode(enum pcmk__xml_flags allowed, enum pcmk__xml_flags requested)
{
if (pcmk_is_set(allowed, pcmk__xf_acl_deny)) {
return false;
} else if (pcmk_all_flags_set(allowed, requested)) {
return true;
} else if (pcmk_is_set(requested, pcmk__xf_acl_read)
&& pcmk_is_set(allowed, pcmk__xf_acl_write)) {
return true;
} else if (pcmk_is_set(requested, pcmk__xf_acl_create)
&& pcmk_any_flags_set(allowed, pcmk__xf_acl_write|pcmk__xf_created)) {
return true;
}
return false;
}
/*!
* \internal
* \brief Rid XML tree of all unreadable nodes and node properties
*
* \param[in,out] xml Root XML node to be purged of attributes
*
* \return true if this node or any of its children are readable
* if false is returned, xml will be freed
*
* \note This function is recursive
*/
static bool
purge_xml_attributes(xmlNode *xml)
{
xmlNode *child = NULL;
xmlAttr *xIter = NULL;
bool readable_children = false;
xml_node_private_t *nodepriv = xml->_private;
if (test_acl_mode(nodepriv->flags, pcmk__xf_acl_read)) {
crm_trace("%s[@" PCMK_XA_ID "=%s] is readable",
xml->name, pcmk__xe_id(xml));
return true;
}
xIter = xml->properties;
while (xIter != NULL) {
xmlAttr *tmp = xIter;
const char *prop_name = (const char *)xIter->name;
xIter = xIter->next;
if (strcmp(prop_name, PCMK_XA_ID) == 0) {
continue;
}
pcmk__xa_remove(tmp, true);
}
child = pcmk__xml_first_child(xml);
while ( child != NULL ) {
xmlNode *tmp = child;
child = pcmk__xml_next(child);
readable_children |= purge_xml_attributes(tmp);
}
if (!readable_children) {
// Nothing readable under here, so purge completely
pcmk__xml_free(xml);
}
return readable_children;
}
/*!
* \brief Copy ACL-allowed portions of specified XML
*
* \param[in] user Username whose ACLs should be used
* \param[in] acl_source XML containing ACLs
* \param[in] xml XML to be copied
* \param[out] result Copy of XML portions readable via ACLs
*
* \return true if xml exists and ACLs are required for user, false otherwise
* \note If this returns true, caller should use \p result rather than \p xml
*/
bool
xml_acl_filtered_copy(const char *user, xmlNode *acl_source, xmlNode *xml,
xmlNode **result)
{
GList *aIter = NULL;
xmlNode *target = NULL;
xml_doc_private_t *docpriv = NULL;
*result = NULL;
if ((xml == NULL) || !pcmk_acl_required(user)) {
crm_trace("Not filtering XML because ACLs not required for user '%s'",
user);
return false;
}
crm_trace("Filtering XML copy using user '%s' ACLs", user);
target = pcmk__xml_copy(NULL, xml);
if (target == NULL) {
return true;
}
pcmk__enable_acl(acl_source, target, user);
docpriv = target->doc->_private;
for(aIter = docpriv->acls; aIter != NULL && target; aIter = aIter->next) {
int max = 0;
xml_acl_t *acl = aIter->data;
if (acl->mode != pcmk__xf_acl_deny) {
/* Nothing to do */
} else if (acl->xpath) {
int lpc = 0;
xmlXPathObject *xpathObj = pcmk__xpath_search(target->doc,
acl->xpath);
max = pcmk__xpath_num_results(xpathObj);
for(lpc = 0; lpc < max; lpc++) {
xmlNode *match = pcmk__xpath_result(xpathObj, lpc);
if (match == NULL) {
continue;
}
// @COMPAT See COMPAT comment in pcmk__apply_acl()
match = pcmk__xpath_match_element(match);
if (match == NULL) {
continue;
}
if (!purge_xml_attributes(match) && (match == target)) {
crm_trace("ACLs deny user '%s' access to entire XML document",
user);
xmlXPathFreeObject(xpathObj);
return true;
}
}
crm_trace("ACLs deny user '%s' access to %s (%d %s)",
user, acl->xpath, max,
pcmk__plural_alt(max, "match", "matches"));
xmlXPathFreeObject(xpathObj);
}
}
if (!purge_xml_attributes(target)) {
crm_trace("ACLs deny user '%s' access to entire XML document", user);
return true;
}
if (docpriv->acls) {
g_list_free_full(docpriv->acls, free_acl);
docpriv->acls = NULL;
} else {
crm_trace("User '%s' without ACLs denied access to entire XML document",
user);
pcmk__xml_free(target);
target = NULL;
}
if (target) {
*result = target;
}
return true;
}
/*!
* \internal
* \brief Check whether creation of an XML element is implicitly allowed
*
* Check whether XML is a "scaffolding" element whose creation is implicitly
* allowed regardless of ACLs (that is, it is not in the ACL section and has
* no attributes other than \c PCMK_XA_ID).
*
* \param[in] xml XML element to check
*
* \return true if XML element is implicitly allowed, false otherwise
*/
static bool
implicitly_allowed(const xmlNode *xml)
{
GString *path = NULL;
for (xmlAttr *prop = xml->properties; prop != NULL; prop = prop->next) {
if (strcmp((const char *) prop->name, PCMK_XA_ID) != 0) {
return false;
}
}
path = pcmk__element_xpath(xml);
pcmk__assert(path != NULL);
if (strstr((const char *) path->str, "/" PCMK_XE_ACLS "/") != NULL) {
g_string_free(path, TRUE);
return false;
}
g_string_free(path, TRUE);
return true;
}
#define display_id(xml) pcmk__s(pcmk__xe_id(xml), "<unset>")
/*!
* \internal
* \brief Drop XML nodes created in violation of ACLs
*
* Given an XML element, free all of its descendant nodes created in violation
* of ACLs, with the exception of allowing "scaffolding" elements (i.e. those
* that aren't in the ACL section and don't have any attributes other than
* \c PCMK_XA_ID).
*
* \param[in,out] xml XML to check
* \param[in] check_top Whether to apply checks to argument itself
* (if true, xml might get freed)
*
* \note This function is recursive
*/
void
pcmk__apply_creation_acl(xmlNode *xml, bool check_top)
{
xml_node_private_t *nodepriv = xml->_private;
if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
if (implicitly_allowed(xml)) {
crm_trace("Creation of <%s> scaffolding with " PCMK_XA_ID "=\"%s\""
" is implicitly allowed",
xml->name, display_id(xml));
} else if (pcmk__check_acl(xml, NULL, pcmk__xf_acl_write)) {
crm_trace("ACLs allow creation of <%s> with " PCMK_XA_ID "=\"%s\"",
xml->name, display_id(xml));
} else if (check_top) {
/* is_root=true should be impossible with check_top=true, but check
* for sanity
*/
bool is_root = (xmlDocGetRootElement(xml->doc) == xml);
xml_doc_private_t *docpriv = xml->doc->_private;
crm_trace("ACLs disallow creation of %s<%s> with "
PCMK_XA_ID "=\"%s\"",
(is_root? "root element " : ""), xml->name,
display_id(xml));
// pcmk__xml_free() checks ACLs if enabled, which would fail
pcmk__clear_xml_flags(docpriv, pcmk__xf_acl_enabled);
pcmk__xml_free(xml);
if (!is_root) {
// If root, the document was freed. Otherwise re-enable ACLs.
pcmk__set_xml_flags(docpriv, pcmk__xf_acl_enabled);
}
return;
} else {
crm_notice("ACLs would disallow creation of %s<%s> with "
PCMK_XA_ID "=\"%s\"",
((xml == xmlDocGetRootElement(xml->doc))? "root element " : ""),
xml->name, display_id(xml));
}
}
for (xmlNode *cIter = pcmk__xml_first_child(xml); cIter != NULL; ) {
xmlNode *child = cIter;
cIter = pcmk__xml_next(cIter); /* In case it is free'd */
pcmk__apply_creation_acl(child, true);
}
}
/*!
* \brief Check whether or not an XML node is ACL-denied
*
* \param[in] xml node to check
*
* \return true if XML node exists and is ACL-denied, false otherwise
*/
bool
xml_acl_denied(const xmlNode *xml)
{
if (xml && xml->doc && xml->doc->_private){
xml_doc_private_t *docpriv = xml->doc->_private;
return pcmk_is_set(docpriv->flags, pcmk__xf_acl_denied);
}
return false;
}
void
xml_acl_disable(xmlNode *xml)
{
- if (xml_acl_enabled(xml)) {
+ if ((xml != NULL)
+ && pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_acl_enabled)) {
+
xml_doc_private_t *docpriv = xml->doc->_private;
/* Catch anything that was created but shouldn't have been */
pcmk__apply_acl(xml);
pcmk__apply_creation_acl(xml, false);
pcmk__clear_xml_flags(docpriv, pcmk__xf_acl_enabled);
}
}
-/*!
- * \brief Check whether or not an XML node is ACL-enabled
- *
- * \param[in] xml node to check
- *
- * \return true if XML node exists and is ACL-enabled, false otherwise
- */
-bool
-xml_acl_enabled(const xmlNode *xml)
-{
- if (xml && xml->doc && xml->doc->_private){
- xml_doc_private_t *docpriv = xml->doc->_private;
-
- return pcmk_is_set(docpriv->flags, pcmk__xf_acl_enabled);
- }
- return false;
-}
-
/*!
* \internal
* \brief Deny access to an XML tree's document based on ACLs
*
* \param[in,out] xml XML tree
* \param[in] attr_name Name of attribute being accessed in \p xml (for
* logging only)
* \param[in] prefix Prefix describing ACL that denied access (for
* logging only)
* \param[in] user User accessing \p xml (for logging only)
* \param[in] mode Access mode (for logging only)
*/
#define check_acl_deny(xml, attr_name, prefix, user, mode) do { \
xmlNode *tree = xml; \
\
pcmk__xml_doc_set_flags(tree->doc, pcmk__xf_acl_denied); \
pcmk__if_tracing( \
{ \
GString *xpath = pcmk__element_xpath(tree); \
\
if ((attr_name) != NULL) { \
pcmk__g_strcat(xpath, "[@", attr_name, "]", NULL); \
} \
qb_log_from_external_source(__func__, __FILE__, \
"%sACL denies user '%s' %s " \
"access to %s", \
LOG_TRACE, __LINE__, 0 , \
prefix, user, \
acl_to_text(mode), xpath->str); \
g_string_free(xpath, TRUE); \
}, \
{} \
); \
} while (false);
bool
pcmk__check_acl(xmlNode *xml, const char *attr_name, enum pcmk__xml_flags mode)
{
xml_doc_private_t *docpriv = NULL;
pcmk__assert((xml != NULL) && (xml->doc->_private != NULL));
- if (!pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_tracking)
- || !xml_acl_enabled(xml)) {
+ if (!pcmk__xml_doc_all_flags_set(xml->doc,
+ pcmk__xf_tracking|pcmk__xf_acl_enabled)) {
return true;
}
docpriv = xml->doc->_private;
if (docpriv->acls == NULL) {
check_acl_deny(xml, attr_name, "Lack of ", docpriv->acl_user, mode);
return false;
}
/* Walk the tree upwards looking for xml_acl_* flags
* - Creating an attribute requires write permissions for the node
* - Creating a child requires write permissions for the parent
*/
if (attr_name != NULL) {
xmlAttr *attr = xmlHasProp(xml, (const xmlChar *) attr_name);
if ((attr != NULL) && (mode == pcmk__xf_acl_create)) {
mode = pcmk__xf_acl_write;
}
}
for (const xmlNode *parent = xml;
(parent != NULL) && (parent->_private != NULL);
parent = parent->parent) {
const xml_node_private_t *nodepriv = parent->_private;
if (test_acl_mode(nodepriv->flags, mode)) {
return true;
}
if (pcmk_is_set(nodepriv->flags, pcmk__xf_acl_deny)) {
const char *pfx = (parent != xml)? "Parent " : "";
check_acl_deny(xml, attr_name, pfx, docpriv->acl_user, mode);
return false;
}
}
check_acl_deny(xml, attr_name, "Default ", docpriv->acl_user, mode);
return false;
}
/*!
* \brief Check whether ACLs are required for a given user
*
* \param[in] User name to check
*
* \return true if the user requires ACLs, false otherwise
*/
bool
pcmk_acl_required(const char *user)
{
if (pcmk__str_empty(user)) {
crm_trace("ACLs not required because no user set");
return false;
} else if (!strcmp(user, CRM_DAEMON_USER) || !strcmp(user, "root")) {
crm_trace("ACLs not required for privileged user %s", user);
return false;
}
crm_trace("ACLs required for %s", user);
return true;
}
char *
pcmk__uid2username(uid_t uid)
{
struct passwd *pwent = getpwuid(uid);
if (pwent == NULL) {
crm_perror(LOG_INFO, "Cannot get user details for user ID %d", uid);
return NULL;
}
return pcmk__str_copy(pwent->pw_name);
}
/*!
* \internal
* \brief Set the ACL user field properly on an XML request
*
* Multiple user names are potentially involved in an XML request: the effective
* user of the current process; the user name known from an IPC client
* connection; and the user name obtained from the request itself, whether by
* the current standard XML attribute name or an older legacy attribute name.
* This function chooses the appropriate one that should be used for ACLs, sets
* it in the request (using the standard attribute name, and the legacy name if
* given), and returns it.
*
* \param[in,out] request XML request to update
* \param[in] field Alternate name for ACL user name XML attribute
* \param[in] peer_user User name as known from IPC connection
*
* \return ACL user name actually used
*/
const char *
pcmk__update_acl_user(xmlNode *request, const char *field,
const char *peer_user)
{
static const char *effective_user = NULL;
const char *requested_user = NULL;
const char *user = NULL;
if (effective_user == NULL) {
effective_user = pcmk__uid2username(geteuid());
if (effective_user == NULL) {
effective_user = pcmk__str_copy("#unprivileged");
crm_err("Unable to determine effective user, assuming unprivileged for ACLs");
}
}
requested_user = crm_element_value(request, PCMK__XA_ACL_TARGET);
if (requested_user == NULL) {
/* Currently, different XML attribute names are used for the ACL user in
* different contexts (PCMK__XA_ATTR_USER, PCMK__XA_CIB_USER, etc.).
* The caller may specify that name as the field argument.
*
* @TODO Standardize on PCMK__XA_ACL_TARGET and eventually drop the
* others once rolling upgrades from versions older than that are no
* longer supported.
*/
requested_user = crm_element_value(request, field);
}
if (!pcmk__is_privileged(effective_user)) {
/* We're not running as a privileged user, set or overwrite any existing
* value for PCMK__XA_ACL_TARGET
*/
user = effective_user;
} else if (peer_user == NULL && requested_user == NULL) {
/* No user known or requested, use 'effective_user' and make sure one is
* set for the request
*/
user = effective_user;
} else if (peer_user == NULL) {
/* No user known, trusting 'requested_user' */
user = requested_user;
} else if (!pcmk__is_privileged(peer_user)) {
/* The peer is not a privileged user, set or overwrite any existing
* value for PCMK__XA_ACL_TARGET
*/
user = peer_user;
} else if (requested_user == NULL) {
/* Even if we're privileged, make sure there is always a value set */
user = peer_user;
} else {
/* Legal delegation to 'requested_user' */
user = requested_user;
}
// This requires pointer comparison, not string comparison
if (user != crm_element_value(request, PCMK__XA_ACL_TARGET)) {
crm_xml_add(request, PCMK__XA_ACL_TARGET, user);
}
if (field != NULL && user != crm_element_value(request, field)) {
crm_xml_add(request, field, user);
}
return requested_user;
}
+
+// Deprecated functions kept only for backward API compatibility
+// LCOV_EXCL_START
+
+#include <crm/common/acl_compat.h>
+#include <crm/common/xml_compat.h>
+
+bool
+xml_acl_enabled(const xmlNode *xml)
+{
+ if (xml && xml->doc && xml->doc->_private){
+ xml_doc_private_t *docpriv = xml->doc->_private;
+
+ return pcmk_is_set(docpriv->flags, pcmk__xf_acl_enabled);
+ }
+ return false;
+}
+
+// LCOV_EXCL_STOP
+// End deprecated API
diff --git a/lib/common/digest.c b/lib/common/digest.c
index e7d23f7088..3479f8a425 100644
--- a/lib/common/digest.c
+++ b/lib/common/digest.c
@@ -1,384 +1,393 @@
/*
* Copyright 2015-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <glib.h> // GString, etc.
#include <gnutls/crypto.h> // gnutls_hash_fast(), gnutls_hash_get_len()
#include <gnutls/gnutls.h> // gnutls_strerror()
#include <crm/crm.h>
#include <crm/common/xml.h>
#include "crmcommon_private.h"
#define BEST_EFFORT_STATUS 0
/*
* Pacemaker uses digests (MD5 hashes) of stringified XML to detect changes in
* the CIB as a whole, a particular resource's agent parameters, and the device
* parameters last used to unfence a particular node.
*
* "v2" digests hash pcmk__xml_string() directly, while less efficient "v1"
* digests do the same with a prefixed space, suffixed newline, and optional
* pre-sorting.
*
* On-disk CIB digests use v1 without sorting.
*
* Operation digests use v1 with sorting, and are stored in a resource's
* operation history in the CIB status section. They come in three flavors:
* - a digest of (nearly) all resource parameters and options, used to detect
* any resource configuration change;
* - a digest of resource parameters marked as nonreloadable, used to decide
* whether a reload or full restart is needed after a configuration change;
* - and a digest of resource parameters not marked as private, used in
* simulations where private parameters have been removed from the input.
*
* Unfencing digests are set as node attributes, and are used to require
* that nodes be unfenced again after a device's configuration changes.
*/
/*!
* \internal
* \brief Dump XML in a format used with v1 digests
*
* \param[in] xml Root of XML to dump
*
* \return Newly allocated buffer containing dumped XML
*/
static GString *
-dump_xml_for_digest(xmlNodePtr xml)
+dump_xml_for_digest(const xmlNode *xml)
{
GString *buffer = g_string_sized_new(1024);
/* for compatibility with the old result which is used for v1 digests */
g_string_append_c(buffer, ' ');
pcmk__xml_string(xml, 0, buffer, 0);
g_string_append_c(buffer, '\n');
return buffer;
}
/*!
* \internal
* \brief Calculate and return v1 digest of XML tree
*
* \param[in] input Root of XML to digest
*
* \return Newly allocated string containing digest
*
* \note Example return value: "c048eae664dba840e1d2060f00299e9d"
*/
static char *
-calculate_xml_digest_v1(xmlNode *input)
+calculate_xml_digest_v1(const xmlNode *input)
{
GString *buffer = dump_xml_for_digest(input);
char *digest = NULL;
// buffer->len > 2 for initial space and trailing newline
CRM_CHECK(buffer->len > 2,
g_string_free(buffer, TRUE);
return NULL);
digest = crm_md5sum((const char *) buffer->str);
crm_log_xml_trace(input, "digest:source");
g_string_free(buffer, TRUE);
return digest;
}
/*!
* \internal
* \brief Calculate and return the digest of a CIB, suitable for storing on disk
*
* \param[in] input Root of XML to digest
*
* \return Newly allocated string containing digest
*/
char *
-pcmk__digest_on_disk_cib(xmlNode *input)
+pcmk__digest_on_disk_cib(const xmlNode *input)
{
/* Always use the v1 format for on-disk digests.
* * Switching to v2 affects even full-restart upgrades, so it would be a
* compatibility nightmare.
* * We only use this once at startup. All other invocations are in a
* separate child process.
*/
return calculate_xml_digest_v1(input);
}
/*!
* \internal
- * \brief Calculate and return digest of an operation XML element
+ * \brief Calculate and return digest of a \c PCMK_XE_PARAMETERS element
*
- * The digest is invariant to changes in the order of XML attributes, provided
- * that \p input has no children.
+ * This is intended for parameters of a resource operation (also known as
+ * resource action). A \c PCMK_XE_PARAMETERS element from a different source
+ * (for example, resource agent metadata) may have child elements, which are not
+ * allowed here.
*
- * \param[in] input Root of XML to digest
+ * The digest is invariant to changes in the order of XML attributes.
+ *
+ * \param[in] input XML element to digest (must have no children)
*
* \return Newly allocated string containing digest
*/
char *
-pcmk__digest_operation(xmlNode *input)
+pcmk__digest_op_params(const xmlNode *input)
{
/* Switching to v2 digests would likely cause restarts during rolling
* upgrades.
*
* @TODO Confirm this. Switch to v2 if safe, or drop this TODO otherwise.
*/
- xmlNode *sorted = pcmk__xml_copy(NULL, input);
char *digest = NULL;
+ xmlNode *sorted = NULL;
+ pcmk__assert(input->children == NULL);
+
+ sorted = pcmk__xe_create(NULL, (const char *) input->name);
+ pcmk__xe_copy_attrs(sorted, input, pcmk__xaf_none);
pcmk__xe_sort_attrs(sorted);
+
digest = calculate_xml_digest_v1(sorted);
pcmk__xml_free(sorted);
return digest;
}
/*!
* \internal
* \brief Calculate and return the digest of an XML tree
*
* \param[in] xml XML tree to digest
* \param[in] filter Whether to filter certain XML attributes
*
* \return Newly allocated string containing digest
*/
char *
-pcmk__digest_xml(xmlNode *xml, bool filter)
+pcmk__digest_xml(const xmlNode *xml, bool filter)
{
/* @TODO Filtering accounts for significant CPU usage. Consider removing if
* possible.
*/
char *digest = NULL;
GString *buf = g_string_sized_new(1024);
pcmk__xml_string(xml, (filter? pcmk__xml_fmt_filtered : 0), buf, 0);
digest = crm_md5sum(buf->str);
pcmk__if_tracing(
{
char *trace_file = crm_strdup_printf("%s/digest-%s",
pcmk__get_tmpdir(), digest);
crm_trace("Saving %s.%s.%s to %s",
crm_element_value(xml, PCMK_XA_ADMIN_EPOCH),
crm_element_value(xml, PCMK_XA_EPOCH),
crm_element_value(xml, PCMK_XA_NUM_UPDATES),
trace_file);
save_xml_to_file(xml, "digest input", trace_file);
free(trace_file);
},
{}
);
g_string_free(buf, TRUE);
return digest;
}
/*!
* \internal
* \brief Check whether calculated digest of given XML matches expected digest
*
* \param[in] input Root of XML tree to digest
* \param[in] expected Expected digest in on-disk format
*
* \return true if digests match, false on mismatch or error
*/
bool
-pcmk__verify_digest(xmlNode *input, const char *expected)
+pcmk__verify_digest(const xmlNode *input, const char *expected)
{
char *calculated = NULL;
bool passed;
if (input != NULL) {
calculated = pcmk__digest_on_disk_cib(input);
if (calculated == NULL) {
crm_perror(LOG_ERR, "Could not calculate digest for comparison");
return false;
}
}
passed = pcmk__str_eq(expected, calculated, pcmk__str_casei);
if (passed) {
crm_trace("Digest comparison passed: %s", calculated);
} else {
crm_err("Digest comparison failed: expected %s, calculated %s",
expected, calculated);
}
free(calculated);
return passed;
}
/*!
* \internal
* \brief Check whether an XML attribute should be excluded from CIB digests
*
* \param[in] name XML attribute name
*
* \return true if XML attribute should be excluded from CIB digest calculation
*/
bool
pcmk__xa_filterable(const char *name)
{
static const char *filter[] = {
PCMK_XA_CRM_DEBUG_ORIGIN,
PCMK_XA_CIB_LAST_WRITTEN,
PCMK_XA_UPDATE_ORIGIN,
PCMK_XA_UPDATE_CLIENT,
PCMK_XA_UPDATE_USER,
};
for (int i = 0; i < PCMK__NELEM(filter); i++) {
if (strcmp(name, filter[i]) == 0) {
return true;
}
}
return false;
}
char *
crm_md5sum(const char *buffer)
{
char *digest = NULL;
gchar *raw_digest = NULL;
if (buffer == NULL) {
return NULL;
}
raw_digest = g_compute_checksum_for_string(G_CHECKSUM_MD5, buffer, -1);
if (raw_digest == NULL) {
crm_err("Failed to calculate hash");
return NULL;
}
digest = pcmk__str_copy(raw_digest);
g_free(raw_digest);
crm_trace("Digest %s.", digest);
return digest;
}
// Return true if a is an attribute that should be filtered
static bool
should_filter_for_digest(xmlAttrPtr a, void *user_data)
{
if (strncmp((const char *) a->name, CRM_META "_",
sizeof(CRM_META " ") - 1) == 0) {
return true;
}
return pcmk__str_any_of((const char *) a->name,
PCMK_XA_ID,
PCMK_XA_CRM_FEATURE_SET,
PCMK__XA_OP_DIGEST,
PCMK__META_ON_NODE,
PCMK__META_ON_NODE_UUID,
"pcmk_external_ip",
NULL);
}
/*!
* \internal
* \brief Remove XML attributes not needed for operation digest
*
* \param[in,out] param_set XML with operation parameters
*/
void
pcmk__filter_op_for_digest(xmlNode *param_set)
{
char *key = NULL;
char *timeout = NULL;
guint interval_ms = 0;
if (param_set == NULL) {
return;
}
/* Timeout is useful for recurring operation digests, so grab it before
* removing meta-attributes
*/
key = crm_meta_name(PCMK_META_INTERVAL);
if (crm_element_value_ms(param_set, key, &interval_ms) != pcmk_ok) {
interval_ms = 0;
}
free(key);
key = NULL;
if (interval_ms != 0) {
key = crm_meta_name(PCMK_META_TIMEOUT);
timeout = crm_element_value_copy(param_set, key);
}
// Remove all CRM_meta_* attributes and certain other attributes
pcmk__xe_remove_matching_attrs(param_set, false, should_filter_for_digest,
NULL);
// Add timeout back for recurring operation digests
if (timeout != NULL) {
crm_xml_add(param_set, key, timeout);
}
free(timeout);
free(key);
}
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
#include <crm/common/xml_compat.h>
#include <crm/common/xml_element_compat.h>
char *
calculate_on_disk_digest(xmlNode *input)
{
return calculate_xml_digest_v1(input);
}
char *
calculate_operation_digest(xmlNode *input, const char *version)
{
xmlNode *sorted = sorted_xml(input, NULL, true);
char *digest = calculate_xml_digest_v1(sorted);
pcmk__xml_free(sorted);
return digest;
}
char *
calculate_xml_versioned_digest(xmlNode *input, gboolean sort,
gboolean do_filter, const char *version)
{
if ((version == NULL) || (compare_version("3.0.5", version) > 0)) {
xmlNode *sorted = NULL;
char *digest = NULL;
if (sort) {
xmlNode *sorted = sorted_xml(input, NULL, true);
input = sorted;
}
crm_trace("Using v1 digest algorithm for %s",
pcmk__s(version, "unknown feature set"));
digest = calculate_xml_digest_v1(input);
pcmk__xml_free(sorted);
return digest;
}
crm_trace("Using v2 digest algorithm for %s", version);
return pcmk__digest_xml(input, do_filter);
}
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/lib/common/patchset.c b/lib/common/patchset.c
index f4bda8dfc3..9a1c1bd82f 100644
--- a/lib/common/patchset.c
+++ b/lib/common/patchset.c
@@ -1,885 +1,994 @@
/*
* Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <bzlib.h>
#include <libxml/tree.h> // xmlNode
#include <crm/crm.h>
#include <crm/common/cib_internal.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h> // CRM_XML_LOG_BASE, etc.
#include "crmcommon_private.h"
/* Add changes for specified XML to patchset.
* For patchset format, refer to diff schema.
*/
static void
add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset)
{
xmlNode *cIter = NULL;
xmlAttr *pIter = NULL;
xmlNode *change = NULL;
xml_node_private_t *nodepriv = xml->_private;
const char *value = NULL;
if (nodepriv == NULL) {
/* Elements that shouldn't occur in a CIB don't have _private set. They
* should be stripped out, ignored, or have an error thrown by any code
* that processes their parent, so we ignore any changes to them.
*/
return;
}
// If this XML node is new, just report that
if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
GString *xpath = pcmk__element_xpath(xml->parent);
if (xpath != NULL) {
int position = pcmk__xml_position(xml, pcmk__xf_deleted);
change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_CREATE);
crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str);
crm_xml_add_int(change, PCMK_XE_POSITION, position);
pcmk__xml_copy(change, xml);
g_string_free(xpath, TRUE);
}
return;
}
// Check each of the XML node's attributes for changes
for (pIter = pcmk__xe_first_attr(xml); pIter != NULL;
pIter = pIter->next) {
xmlNode *attr = NULL;
nodepriv = pIter->_private;
if (!pcmk_any_flags_set(nodepriv->flags, pcmk__xf_deleted|pcmk__xf_dirty)) {
continue;
}
if (change == NULL) {
GString *xpath = pcmk__element_xpath(xml);
if (xpath != NULL) {
change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_MODIFY);
crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str);
change = pcmk__xe_create(change, PCMK_XE_CHANGE_LIST);
g_string_free(xpath, TRUE);
}
}
attr = pcmk__xe_create(change, PCMK_XE_CHANGE_ATTR);
crm_xml_add(attr, PCMK_XA_NAME, (const char *) pIter->name);
if (nodepriv->flags & pcmk__xf_deleted) {
crm_xml_add(attr, PCMK_XA_OPERATION, "unset");
} else {
crm_xml_add(attr, PCMK_XA_OPERATION, "set");
value = pcmk__xml_attr_value(pIter);
crm_xml_add(attr, PCMK_XA_VALUE, value);
}
}
if (change) {
xmlNode *result = NULL;
change = pcmk__xe_create(change->parent, PCMK_XE_CHANGE_RESULT);
result = pcmk__xe_create(change, (const char *)xml->name);
for (pIter = pcmk__xe_first_attr(xml); pIter != NULL;
pIter = pIter->next) {
nodepriv = pIter->_private;
if (!pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) {
value = crm_element_value(xml, (const char *) pIter->name);
crm_xml_add(result, (const char *)pIter->name, value);
}
}
}
// Now recursively do the same for each child node of this node
for (cIter = pcmk__xml_first_child(xml); cIter != NULL;
cIter = pcmk__xml_next(cIter)) {
add_xml_changes_to_patchset(cIter, patchset);
}
nodepriv = xml->_private;
if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) {
GString *xpath = pcmk__element_xpath(xml);
crm_trace("%s.%s moved to position %d",
xml->name, pcmk__xe_id(xml),
pcmk__xml_position(xml, pcmk__xf_skip));
if (xpath != NULL) {
change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_MOVE);
crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str);
crm_xml_add_int(change, PCMK_XE_POSITION,
pcmk__xml_position(xml, pcmk__xf_deleted));
g_string_free(xpath, TRUE);
}
}
}
static bool
is_config_change(xmlNode *xml)
{
GList *gIter = NULL;
xml_node_private_t *nodepriv = NULL;
xml_doc_private_t *docpriv;
xmlNode *config = pcmk__xe_first_child(xml, PCMK_XE_CONFIGURATION, NULL,
NULL);
if (config) {
nodepriv = config->_private;
}
if ((nodepriv != NULL) && pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) {
return TRUE;
}
if ((xml->doc != NULL) && (xml->doc->_private != NULL)) {
docpriv = xml->doc->_private;
for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) {
pcmk__deleted_xml_t *deleted_obj = gIter->data;
if (strstr(deleted_obj->path,
"/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION) != NULL) {
return TRUE;
}
}
}
return FALSE;
}
static xmlNode *
xml_create_patchset_v2(xmlNode *source, xmlNode *target)
{
int lpc = 0;
GList *gIter = NULL;
xml_doc_private_t *docpriv;
xmlNode *v = NULL;
xmlNode *version = NULL;
xmlNode *patchset = NULL;
const char *vfields[] = {
PCMK_XA_ADMIN_EPOCH,
PCMK_XA_EPOCH,
PCMK_XA_NUM_UPDATES,
};
pcmk__assert(target != NULL);
if (!pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty)) {
return NULL;
}
pcmk__assert(target->doc != NULL);
docpriv = target->doc->_private;
patchset = pcmk__xe_create(NULL, PCMK_XE_DIFF);
crm_xml_add_int(patchset, PCMK_XA_FORMAT, 2);
version = pcmk__xe_create(patchset, PCMK_XE_VERSION);
v = pcmk__xe_create(version, PCMK_XE_SOURCE);
for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
const char *value = crm_element_value(source, vfields[lpc]);
if (value == NULL) {
value = "1";
}
crm_xml_add(v, vfields[lpc], value);
}
v = pcmk__xe_create(version, PCMK_XE_TARGET);
for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
const char *value = crm_element_value(target, vfields[lpc]);
if (value == NULL) {
value = "1";
}
crm_xml_add(v, vfields[lpc], value);
}
for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) {
pcmk__deleted_xml_t *deleted_obj = gIter->data;
xmlNode *change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_DELETE);
crm_xml_add(change, PCMK_XA_PATH, deleted_obj->path);
if (deleted_obj->position >= 0) {
crm_xml_add_int(change, PCMK_XE_POSITION, deleted_obj->position);
}
}
add_xml_changes_to_patchset(target, patchset);
return patchset;
}
xmlNode *
xml_create_patchset(int format, xmlNode *source, xmlNode *target,
bool *config_changed, bool manage_version)
{
bool local_config_changed = false;
if (format == 0) {
format = 2;
}
if (format != 2) {
crm_err("Unknown patch format: %d", format);
return NULL;
}
xml_acl_disable(target);
if ((target == NULL)
|| !pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty)) {
crm_trace("No change %d", format);
return NULL;
}
if (config_changed == NULL) {
config_changed = &local_config_changed;
}
*config_changed = is_config_change(target);
if (manage_version) {
int counter = 0;
if (*config_changed) {
crm_xml_add(target, PCMK_XA_NUM_UPDATES, "0");
crm_element_value_int(target, PCMK_XA_EPOCH, &counter);
crm_xml_add_int(target, PCMK_XA_EPOCH, counter + 1);
} else {
crm_element_value_int(target, PCMK_XA_NUM_UPDATES, &counter);
crm_xml_add_int(target, PCMK_XA_NUM_UPDATES, counter + 1);
}
}
return xml_create_patchset_v2(source, target);
}
+/*!
+ * \internal
+ * \brief Add a digest of a patchset's target XML to the patchset
+ *
+ * \param[in,out] patchset XML patchset
+ * \param[in] target Target XML
+ */
void
-patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target,
- bool with_digest)
+pcmk__xml_patchset_add_digest(xmlNode *patchset, const xmlNode *target)
{
char *digest = NULL;
- if ((patch == NULL) || (source == NULL) || (target == NULL)
- || !with_digest) {
- return;
- }
+ CRM_CHECK((patchset != NULL) && (target != NULL), return);
- /* We should always call pcmk__xml_commit_changes() before calculating a
- * digest. Otherwise, with an on-tracking dirty target, we could get a wrong
- * digest.
+ /* If tracking is enabled and the document is dirty, we could get an
+ * incorrect digest. Call pcmk__xml_commit_changes() before calling this.
*/
- CRM_LOG_ASSERT(!pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty));
+ CRM_CHECK(!pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty),
+ return);
digest = pcmk__digest_xml(target, true);
- crm_xml_add(patch, PCMK__XA_DIGEST, digest);
+ crm_xml_add(patchset, PCMK__XA_DIGEST, digest);
free(digest);
-
- return;
}
-// Get CIB versions used for additions and deletions in a patchset
-bool
-xml_patch_versions(const xmlNode *patchset, int add[3], int del[3])
+/*!
+ * \internal
+ * \brief Get the source and target CIB versions from an XML patchset
+ *
+ * Each output object will contain, in order, the following version fields from
+ * the source and target, respectively:
+ * * \c PCMK_XA_ADMIN_EPOCH
+ * * \c PCMK_XA_EPOCH
+ * * \c PCMK_XA_NUM_UPDATES
+ *
+ * \param[in] patchset XML patchset
+ * \param[out] source Where to store versions from source CIB
+ * \param[out] target Where to store versions from target CIB
+ *
+ * \return Standard Pacemaker return code
+ */
+int
+pcmk__xml_patchset_versions(const xmlNode *patchset, int source[3],
+ int target[3])
{
static const char *const vfields[] = {
PCMK_XA_ADMIN_EPOCH,
PCMK_XA_EPOCH,
PCMK_XA_NUM_UPDATES,
};
- const xmlNode *version = pcmk__xe_first_child(patchset, PCMK_XE_VERSION,
- NULL, NULL);
- const xmlNode *source = pcmk__xe_first_child(version, PCMK_XE_SOURCE, NULL,
- NULL);
- const xmlNode *target = pcmk__xe_first_child(version, PCMK_XE_TARGET, NULL,
- NULL);
- int format = 1;
+ int format = 0;
+ const xmlNode *version = NULL;
+ const xmlNode *source_xml = NULL;
+ const xmlNode *target_xml = NULL;
+
+ CRM_CHECK((patchset != NULL) && (source != NULL) && (target != NULL),
+ return EINVAL);
crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
if (format != 2) {
crm_err("Unknown patch format: %d", format);
- return -EINVAL;
+ return EINVAL;
}
- if (source != NULL) {
- for (int i = 0; i < PCMK__NELEM(vfields); i++) {
- crm_element_value_int(source, vfields[i], &(del[i]));
- crm_trace("Got %d for del[%s]", del[i], vfields[i]);
- }
+ version = pcmk__xe_first_child(patchset, PCMK_XE_VERSION, NULL, NULL);
+ source_xml = pcmk__xe_first_child(version, PCMK_XE_SOURCE, NULL, NULL);
+ target_xml = pcmk__xe_first_child(version, PCMK_XE_TARGET, NULL, NULL);
+
+ if ((source_xml == NULL) || (target_xml == NULL)) {
+ return EINVAL;
}
- if (target != NULL) {
- for (int i = 0; i < PCMK__NELEM(vfields); i++) {
- crm_element_value_int(target, vfields[i], &(add[i]));
- crm_trace("Got %d for add[%s]", add[i], vfields[i]);
+ for (int i = 0; i < PCMK__NELEM(vfields); i++) {
+ if (crm_element_value_int(source_xml, vfields[i], &(source[i])) != 0) {
+ return EINVAL;
+ }
+ crm_trace("Got %d for source[%s]", source[i], vfields[i]);
+
+ if (crm_element_value_int(target_xml, vfields[i], &(target[i])) != 0) {
+ return EINVAL;
}
+ crm_trace("Got %d for target[%s]", target[i], vfields[i]);
}
- return pcmk_ok;
+
+ return pcmk_rc_ok;
}
/*!
* \internal
* \brief Check whether patchset can be applied to current CIB
*
* \param[in] xml Root of current CIB
* \param[in] patchset Patchset to check
*
* \return Standard Pacemaker return code
*/
static int
xml_patch_version_check(const xmlNode *xml, const xmlNode *patchset)
{
int lpc = 0;
bool changed = FALSE;
int this[] = { 0, 0, 0 };
int add[] = { 0, 0, 0 };
int del[] = { 0, 0, 0 };
+ int rc = pcmk_rc_ok;
const char *vfields[] = {
PCMK_XA_ADMIN_EPOCH,
PCMK_XA_EPOCH,
PCMK_XA_NUM_UPDATES,
};
for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
crm_element_value_int(xml, vfields[lpc], &(this[lpc]));
crm_trace("Got %d for this[%s]", this[lpc], vfields[lpc]);
if (this[lpc] < 0) {
this[lpc] = 0;
}
}
/* Set some defaults in case nothing is present */
add[0] = this[0];
add[1] = this[1];
add[2] = this[2] + 1;
for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
del[lpc] = this[lpc];
}
- xml_patch_versions(patchset, add, del);
+ rc = pcmk__xml_patchset_versions(patchset, del, add);
+ if (rc != pcmk_rc_ok) {
+ return rc;
+ }
for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
if (this[lpc] < del[lpc]) {
crm_debug("Current %s is too low (%d.%d.%d < %d.%d.%d --> %d.%d.%d)",
vfields[lpc], this[0], this[1], this[2],
del[0], del[1], del[2], add[0], add[1], add[2]);
return pcmk_rc_diff_resync;
} else if (this[lpc] > del[lpc]) {
crm_info("Current %s is too high (%d.%d.%d > %d.%d.%d --> %d.%d.%d) %p",
vfields[lpc], this[0], this[1], this[2],
del[0], del[1], del[2], add[0], add[1], add[2], patchset);
crm_log_xml_info(patchset, "OldPatch");
return pcmk_rc_old_data;
}
}
for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
if (add[lpc] > del[lpc]) {
changed = TRUE;
}
}
if (!changed) {
crm_notice("Versions did not change in patch %d.%d.%d",
add[0], add[1], add[2]);
return pcmk_rc_old_data;
}
crm_debug("Can apply patch %d.%d.%d to %d.%d.%d",
add[0], add[1], add[2], this[0], this[1], this[2]);
return pcmk_rc_ok;
}
// Return first child matching element name and optionally id or position
static xmlNode *
first_matching_xml_child(const xmlNode *parent, const char *name,
const char *id, int position)
{
xmlNode *cIter = NULL;
for (cIter = pcmk__xml_first_child(parent); cIter != NULL;
cIter = pcmk__xml_next(cIter)) {
if (strcmp((const char *) cIter->name, name) != 0) {
continue;
} else if (id) {
const char *cid = pcmk__xe_id(cIter);
if ((cid == NULL) || (strcmp(cid, id) != 0)) {
continue;
}
}
// "position" makes sense only for XML comments for now
if ((cIter->type == XML_COMMENT_NODE)
&& (position >= 0)
&& (pcmk__xml_position(cIter, pcmk__xf_skip) != position)) {
continue;
}
return cIter;
}
return NULL;
}
/*!
* \internal
* \brief Simplified, more efficient alternative to pcmk__xpath_find_one()
*
* \param[in] top Root of XML to search
* \param[in] key Search xpath
* \param[in] target_position If deleting, where to delete
*
* \return XML child matching xpath if found, NULL otherwise
*
* \note This only works on simplified xpaths found in v2 patchset diffs,
* i.e. the only allowed search predicate is [@id='XXX'].
*/
static xmlNode *
search_v2_xpath(const xmlNode *top, const char *key, int target_position)
{
xmlNode *target = (xmlNode *) top->doc;
const char *current = key;
char *section;
char *remainder;
char *id;
char *tag;
char *path = NULL;
int rc;
size_t key_len;
CRM_CHECK(key != NULL, return NULL);
key_len = strlen(key);
/* These are scanned from key after a slash, so they can't be bigger
* than key_len - 1 characters plus a null terminator.
*/
remainder = pcmk__assert_alloc(key_len, sizeof(char));
section = pcmk__assert_alloc(key_len, sizeof(char));
id = pcmk__assert_alloc(key_len, sizeof(char));
tag = pcmk__assert_alloc(key_len, sizeof(char));
do {
// Look for /NEXT_COMPONENT/REMAINING_COMPONENTS
rc = sscanf(current, "/%[^/]%s", section, remainder);
if (rc > 0) {
// Separate FIRST_COMPONENT into TAG[@id='ID']
int f = sscanf(section, "%[^[][@" PCMK_XA_ID "='%[^']", tag, id);
int current_position = -1;
/* The target position is for the final component tag, so only use
* it if there is nothing left to search after this component.
*/
if ((rc == 1) && (target_position >= 0)) {
current_position = target_position;
}
switch (f) {
case 1:
target = first_matching_xml_child(target, tag, NULL,
current_position);
break;
case 2:
target = first_matching_xml_child(target, tag, id,
current_position);
break;
default:
// This should not be possible
target = NULL;
break;
}
current = remainder;
}
// Continue if something remains to search, and we've matched so far
} while ((rc == 2) && target);
if (target) {
crm_trace("Found %s for %s",
(path = (char *) xmlGetNodePath(target)), key);
free(path);
} else {
crm_debug("No match for %s", key);
}
free(remainder);
free(section);
free(tag);
free(id);
return target;
}
typedef struct xml_change_obj_s {
const xmlNode *change;
xmlNode *match;
} xml_change_obj_t;
static gint
sort_change_obj_by_position(gconstpointer a, gconstpointer b)
{
const xml_change_obj_t *change_obj_a = a;
const xml_change_obj_t *change_obj_b = b;
int position_a = -1;
int position_b = -1;
crm_element_value_int(change_obj_a->change, PCMK_XE_POSITION, &position_a);
crm_element_value_int(change_obj_b->change, PCMK_XE_POSITION, &position_b);
if (position_a < position_b) {
return -1;
} else if (position_a > position_b) {
return 1;
}
return 0;
}
/*!
* \internal
* \brief Apply a version 2 patchset to an XML node
*
* \param[in,out] xml XML to apply patchset to
* \param[in] patchset Patchset to apply
*
* \return Standard Pacemaker return code
*/
static int
apply_v2_patchset(xmlNode *xml, const xmlNode *patchset)
{
int rc = pcmk_rc_ok;
const xmlNode *change = NULL;
GList *change_objs = NULL;
GList *gIter = NULL;
for (change = pcmk__xml_first_child(patchset); change != NULL;
change = pcmk__xml_next(change)) {
xmlNode *match = NULL;
const char *op = crm_element_value(change, PCMK_XA_OPERATION);
const char *xpath = crm_element_value(change, PCMK_XA_PATH);
int position = -1;
if (op == NULL) {
continue;
}
crm_trace("Processing %s %s", change->name, op);
/* PCMK_VALUE_DELETE changes for XML comments are generated with
* PCMK_XE_POSITION
*/
if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
crm_element_value_int(change, PCMK_XE_POSITION, &position);
}
match = search_v2_xpath(xml, xpath, position);
crm_trace("Performing %s on %s with %p", op, xpath, match);
if ((match == NULL) && (strcmp(op, PCMK_VALUE_DELETE) == 0)) {
crm_debug("No %s match for %s in %p", op, xpath, xml->doc);
continue;
} else if (match == NULL) {
crm_err("No %s match for %s in %p", op, xpath, xml->doc);
rc = pcmk_rc_diff_failed;
continue;
} else if (pcmk__str_any_of(op,
PCMK_VALUE_CREATE, PCMK_VALUE_MOVE, NULL)) {
// Delay the adding of a PCMK_VALUE_CREATE object
xml_change_obj_t *change_obj =
pcmk__assert_alloc(1, sizeof(xml_change_obj_t));
change_obj->change = change;
change_obj->match = match;
change_objs = g_list_append(change_objs, change_obj);
if (strcmp(op, PCMK_VALUE_MOVE) == 0) {
// Temporarily put the PCMK_VALUE_MOVE object after the last sibling
if ((match->parent != NULL) && (match->parent->last != NULL)) {
xmlAddNextSibling(match->parent->last, match);
}
}
} else if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
pcmk__xml_free(match);
} else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) {
const xmlNode *child = pcmk__xe_first_child(change,
PCMK_XE_CHANGE_RESULT,
NULL, NULL);
const xmlNode *attrs = pcmk__xml_first_child(child);
if (attrs == NULL) {
rc = ENOMSG;
continue;
}
// Remove all attributes
pcmk__xe_remove_matching_attrs(match, false, NULL, NULL);
for (xmlAttrPtr pIter = pcmk__xe_first_attr(attrs); pIter != NULL;
pIter = pIter->next) {
const char *name = (const char *) pIter->name;
const char *value = pcmk__xml_attr_value(pIter);
crm_xml_add(match, name, value);
}
} else {
crm_err("Unknown operation: %s", op);
rc = pcmk_rc_diff_failed;
}
}
// Changes should be generated in the right order. Double checking.
change_objs = g_list_sort(change_objs, sort_change_obj_by_position);
for (gIter = change_objs; gIter; gIter = gIter->next) {
xml_change_obj_t *change_obj = gIter->data;
xmlNode *match = change_obj->match;
const char *op = NULL;
const char *xpath = NULL;
change = change_obj->change;
op = crm_element_value(change, PCMK_XA_OPERATION);
xpath = crm_element_value(change, PCMK_XA_PATH);
crm_trace("Continue performing %s on %s with %p", op, xpath, match);
if (strcmp(op, PCMK_VALUE_CREATE) == 0) {
int position = 0;
xmlNode *child = NULL;
xmlNode *match_child = NULL;
match_child = match->children;
crm_element_value_int(change, PCMK_XE_POSITION, &position);
while ((match_child != NULL)
&& (position != pcmk__xml_position(match_child, pcmk__xf_skip))) {
match_child = match_child->next;
}
child = pcmk__xml_copy(match, change->children);
if (match_child != NULL) {
crm_trace("Adding %s at position %d", child->name, position);
xmlAddPrevSibling(match_child, child);
} else {
crm_trace("Adding %s at position %d (end)",
child->name, position);
}
} else if (strcmp(op, PCMK_VALUE_MOVE) == 0) {
int position = 0;
crm_element_value_int(change, PCMK_XE_POSITION, &position);
if (position != pcmk__xml_position(match, pcmk__xf_skip)) {
xmlNode *match_child = NULL;
int p = position;
if (p > pcmk__xml_position(match, pcmk__xf_skip)) {
p++; // Skip ourselves
}
pcmk__assert(match->parent != NULL);
match_child = match->parent->children;
while ((match_child != NULL)
&& (p != pcmk__xml_position(match_child, pcmk__xf_skip))) {
match_child = match_child->next;
}
crm_trace("Moving %s to position %d (was %d, prev %p, %s %p)",
match->name, position,
pcmk__xml_position(match, pcmk__xf_skip),
match->prev, (match_child? "next":"last"),
(match_child? match_child : match->parent->last));
if (match_child) {
xmlAddPrevSibling(match_child, match);
} else {
pcmk__assert(match->parent->last != NULL);
xmlAddNextSibling(match->parent->last, match);
}
} else {
crm_trace("%s is already in position %d",
match->name, position);
}
if (position != pcmk__xml_position(match, pcmk__xf_skip)) {
crm_err("Moved %s.%s to position %d instead of %d (%p)",
match->name, pcmk__xe_id(match),
pcmk__xml_position(match, pcmk__xf_skip),
position, match->prev);
rc = pcmk_rc_diff_failed;
}
}
}
g_list_free_full(change_objs, free);
return rc;
}
int
xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version)
{
int format = 1;
int rc = pcmk_ok;
xmlNode *old = NULL;
const char *digest = NULL;
if (patchset == NULL) {
return rc;
}
pcmk__log_xml_patchset(LOG_TRACE, patchset);
if (check_version) {
rc = pcmk_rc2legacy(xml_patch_version_check(xml, patchset));
if (rc != pcmk_ok) {
return rc;
}
}
digest = crm_element_value(patchset, PCMK__XA_DIGEST);
if (digest != NULL) {
/* Make original XML available for logging in case result doesn't have
* expected digest
*/
pcmk__if_tracing(old = pcmk__xml_copy(NULL, xml), {});
}
if (rc == pcmk_ok) {
crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
if (format != 2) {
crm_err("Unknown patch format: %d", format);
rc = -EINVAL;
} else {
rc = pcmk_rc2legacy(apply_v2_patchset(xml, patchset));
}
}
if ((rc == pcmk_ok) && (digest != NULL)) {
char *new_digest = NULL;
new_digest = pcmk__digest_xml(xml, true);
if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) {
crm_info("v%d digest mis-match: expected %s, calculated %s",
format, digest, new_digest);
rc = -pcmk_err_diff_failed;
pcmk__if_tracing(
{
save_xml_to_file(old, "PatchDigest:input", NULL);
save_xml_to_file(xml, "PatchDigest:result", NULL);
save_xml_to_file(patchset, "PatchDigest:diff", NULL);
},
{}
);
} else {
crm_trace("v%d digest matched: expected %s, calculated %s",
format, digest, new_digest);
}
free(new_digest);
}
pcmk__xml_free(old);
return rc;
}
+/*!
+ * \internal
+ * \brief Check whether a given CIB element was modified in a CIB patchset
+ *
+ * \param[in] patchset CIB XML patchset
+ * \param[in] element XML tag of CIB element to check (\c NULL is equivalent
+ * to \c PCMK_XE_CIB). Supported values include any CIB
+ * element supported by \c pcmk__cib_abs_xpath_for().
+ *
+ * \retval \c true if \p element was modified
+ * \retval \c false otherwise
+ */
bool
pcmk__cib_element_in_patchset(const xmlNode *patchset, const char *element)
{
const char *element_xpath = pcmk__cib_abs_xpath_for(element);
const char *parent_xpath = pcmk_cib_parent_name_for(element);
char *element_regex = NULL;
bool rc = false;
int format = 1;
pcmk__assert(patchset != NULL);
crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
if (format != 2) {
crm_warn("Unknown patch format: %d", format);
return false;
}
CRM_CHECK(element_xpath != NULL, return false); // Unsupported element
/* Matches if and only if element_xpath is part of a changed path
* (supported values for element never contain XML IDs with schema
* validation enabled)
*
* @TODO Use POSIX word boundary instead of (/|$), if it works:
* https://www.regular-expressions.info/wordboundaries.html.
*/
element_regex = crm_strdup_printf("^%s(/|$)", element_xpath);
for (const xmlNode *change = pcmk__xe_first_child(patchset, PCMK_XE_CHANGE,
NULL, NULL);
change != NULL; change = pcmk__xe_next(change, PCMK_XE_CHANGE)) {
const char *op = crm_element_value(change, PCMK_XA_OPERATION);
const char *diff_xpath = crm_element_value(change, PCMK_XA_PATH);
if (pcmk__str_eq(diff_xpath, element_regex, pcmk__str_regex)) {
// Change to an existing element
rc = true;
break;
}
if (pcmk__str_eq(op, PCMK_VALUE_CREATE, pcmk__str_none)
&& pcmk__str_eq(diff_xpath, parent_xpath, pcmk__str_none)
&& pcmk__xe_is(pcmk__xe_first_child(change, NULL, NULL, NULL),
element)) {
// Newly added element
rc = true;
break;
}
}
free(element_regex);
return rc;
}
+
+// Deprecated functions kept only for backward API compatibility
+// LCOV_EXCL_START
+
+#include <crm/common/xml_compat.h>
+
+// Return value of true means failure; false means success
+bool
+xml_patch_versions(const xmlNode *patchset, int add[3], int del[3])
+{
+ static const char *const vfields[] = {
+ PCMK_XA_ADMIN_EPOCH,
+ PCMK_XA_EPOCH,
+ PCMK_XA_NUM_UPDATES,
+ };
+
+ const xmlNode *version = pcmk__xe_first_child(patchset, PCMK_XE_VERSION,
+ NULL, NULL);
+ const xmlNode *source = pcmk__xe_first_child(version, PCMK_XE_SOURCE, NULL,
+ NULL);
+ const xmlNode *target = pcmk__xe_first_child(version, PCMK_XE_TARGET, NULL,
+ NULL);
+ int format = 1;
+
+ crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
+ if (format != 2) {
+ crm_err("Unknown patch format: %d", format);
+ return true;
+ }
+
+ if (source != NULL) {
+ for (int i = 0; i < PCMK__NELEM(vfields); i++) {
+ crm_element_value_int(source, vfields[i], &(del[i]));
+ crm_trace("Got %d for del[%s]", del[i], vfields[i]);
+ }
+ }
+
+ if (target != NULL) {
+ for (int i = 0; i < PCMK__NELEM(vfields); i++) {
+ crm_element_value_int(target, vfields[i], &(add[i]));
+ crm_trace("Got %d for add[%s]", add[i], vfields[i]);
+ }
+ }
+ return false;
+}
+
+void
+patchset_process_digest(xmlNode *patch, const xmlNode *source,
+ const xmlNode *target, bool with_digest)
+{
+ char *digest = NULL;
+
+ if ((patch == NULL) || (source == NULL) || (target == NULL)
+ || !with_digest) {
+ return;
+ }
+
+ CRM_LOG_ASSERT(!pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty));
+
+ digest = pcmk__digest_xml(target, true);
+
+ crm_xml_add(patch, PCMK__XA_DIGEST, digest);
+ free(digest);
+
+ return;
+}
+
+// LCOV_EXCL_STOP
+// End deprecated API
diff --git a/lib/common/patchset_display.c b/lib/common/patchset_display.c
index 5ddbdbc533..f3dabb43dd 100644
--- a/lib/common/patchset_display.c
+++ b/lib/common/patchset_display.c
@@ -1,326 +1,326 @@
/*
- * Copyright 2004-2024 the Pacemaker project contributors
+ * Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <crm/common/xml.h>
#include "crmcommon_private.h"
/*!
* \internal
* \brief Output an XML patchset header
*
* This function parses a header from an XML patchset (a \c PCMK_XE_DIFF element
* and its children).
*
* All header lines contain three integers separated by dots, of the form
* <tt>{0}.{1}.{2}</tt>:
* * \p {0}: \c PCMK_XA_ADMIN_EPOCH
* * \p {1}: \c PCMK_XA_EPOCH
* * \p {2}: \c PCMK_XA_NUM_UPDATES
*
* Lines containing \p "---" describe removals and end with the patch format
* number. Lines containing \p "+++" describe additions and end with the patch
* digest.
*
* \param[in,out] out Output object
* \param[in] patchset XML patchset to output
*
* \return Standard Pacemaker return code
*
* \note This function produces output only for text-like formats.
*/
static int
xml_show_patchset_header(pcmk__output_t *out, const xmlNode *patchset)
{
int rc = pcmk_rc_no_output;
int add[] = { 0, 0, 0 };
int del[] = { 0, 0, 0 };
- xml_patch_versions(patchset, add, del);
+ pcmk__xml_patchset_versions(patchset, del, add);
if ((add[0] != del[0]) || (add[1] != del[1]) || (add[2] != del[2])) {
const char *fmt = crm_element_value(patchset, PCMK_XA_FORMAT);
const char *digest = crm_element_value(patchset, PCMK__XA_DIGEST);
out->info(out, "Diff: --- %d.%d.%d %s", del[0], del[1], del[2], fmt);
rc = out->info(out, "Diff: +++ %d.%d.%d %s",
add[0], add[1], add[2], digest);
} else if ((add[0] != 0) || (add[1] != 0) || (add[2] != 0)) {
rc = out->info(out, "Local-only Change: %d.%d.%d",
add[0], add[1], add[2]);
}
return rc;
}
/*!
* \internal
* \brief Output a user-friendly form of an XML patchset
*
* This function parses an XML patchset (a \c PCMK_XE_DIFF element and its
* children) into a user-friendly combined diff output.
*
* \param[in,out] out Output object
* \param[in] patchset XML patchset to output
*
* \return Standard Pacemaker return code
*
* \note This function produces output only for text-like formats.
*/
static int
xml_show_patchset(pcmk__output_t *out, const xmlNode *patchset)
{
int rc = xml_show_patchset_header(out, patchset);
int temp_rc = pcmk_rc_no_output;
for (const xmlNode *change = pcmk__xe_first_child(patchset, NULL, NULL,
NULL);
change != NULL; change = pcmk__xe_next(change, NULL)) {
const char *op = crm_element_value(change, PCMK_XA_OPERATION);
const char *xpath = crm_element_value(change, PCMK_XA_PATH);
if (op == NULL) {
continue;
}
if (strcmp(op, PCMK_VALUE_CREATE) == 0) {
char *prefix = crm_strdup_printf(PCMK__XML_PREFIX_CREATED " %s: ",
xpath);
temp_rc = pcmk__xml_show(out, prefix, change->children, 0,
pcmk__xml_fmt_pretty|pcmk__xml_fmt_open);
rc = pcmk__output_select_rc(rc, temp_rc);
// Overwrite all except the first two characters with spaces
for (char *ch = prefix + 2; *ch != '\0'; ch++) {
*ch = ' ';
}
temp_rc = pcmk__xml_show(out, prefix, change->children, 0,
pcmk__xml_fmt_pretty
|pcmk__xml_fmt_children
|pcmk__xml_fmt_close);
rc = pcmk__output_select_rc(rc, temp_rc);
free(prefix);
} else if (strcmp(op, PCMK_VALUE_MOVE) == 0) {
const char *position = crm_element_value(change, PCMK_XE_POSITION);
temp_rc = out->info(out,
PCMK__XML_PREFIX_MOVED " %s moved to offset %s",
xpath, position);
rc = pcmk__output_select_rc(rc, temp_rc);
} else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) {
xmlNode *clist = pcmk__xe_first_child(change, PCMK_XE_CHANGE_LIST,
NULL, NULL);
GString *buffer_set = NULL;
GString *buffer_unset = NULL;
for (const xmlNode *child = pcmk__xe_first_child(clist, NULL, NULL,
NULL);
child != NULL; child = pcmk__xe_next(child, NULL)) {
const char *name = crm_element_value(child, PCMK_XA_NAME);
op = crm_element_value(child, PCMK_XA_OPERATION);
if (op == NULL) {
continue;
}
if (strcmp(op, "set") == 0) {
const char *value = crm_element_value(child, PCMK_XA_VALUE);
pcmk__add_separated_word(&buffer_set, 256, "@", ", ");
pcmk__g_strcat(buffer_set, name, "=", value, NULL);
} else if (strcmp(op, "unset") == 0) {
pcmk__add_separated_word(&buffer_unset, 256, "@", ", ");
g_string_append(buffer_unset, name);
}
}
if (buffer_set != NULL) {
temp_rc = out->info(out, "+ %s: %s", xpath, buffer_set->str);
rc = pcmk__output_select_rc(rc, temp_rc);
g_string_free(buffer_set, TRUE);
}
if (buffer_unset != NULL) {
temp_rc = out->info(out, "-- %s: %s",
xpath, buffer_unset->str);
rc = pcmk__output_select_rc(rc, temp_rc);
g_string_free(buffer_unset, TRUE);
}
} else if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
int position = -1;
crm_element_value_int(change, PCMK_XE_POSITION, &position);
if (position >= 0) {
temp_rc = out->info(out, "-- %s (%d)", xpath, position);
} else {
temp_rc = out->info(out, "-- %s", xpath);
}
rc = pcmk__output_select_rc(rc, temp_rc);
}
}
return rc;
}
/*!
* \internal
* \brief Output a user-friendly form of an XML patchset
*
* This function parses an XML patchset (a \c PCMK_XE_DIFF element and its
* children) into a user-friendly combined diff output.
*
* \param[in,out] out Output object
* \param[in] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# XML patchset
*/
PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
static int
xml_patchset_default(pcmk__output_t *out, va_list args)
{
const xmlNode *patchset = va_arg(args, const xmlNode *);
int format = 1;
if (patchset == NULL) {
crm_trace("Empty patch");
return pcmk_rc_no_output;
}
crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
if (format != 2) {
crm_err("Unknown patch format: %d", format);
return pcmk_rc_bad_xml_patch;
}
return xml_show_patchset(out, patchset);
}
/*!
* \internal
* \brief Output a user-friendly form of an XML patchset
*
* This function parses an XML patchset (a \c PCMK_XE_DIFF element and its
* children) into a user-friendly combined diff output.
*
* \param[in,out] out Output object
* \param[in] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# XML patchset
*/
PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
static int
xml_patchset_log(pcmk__output_t *out, va_list args)
{
static struct qb_log_callsite *patchset_cs = NULL;
const xmlNode *patchset = va_arg(args, const xmlNode *);
uint8_t log_level = pcmk__output_get_log_level(out);
int format = 1;
if (log_level == LOG_NEVER) {
return pcmk_rc_no_output;
}
if (patchset == NULL) {
crm_trace("Empty patch");
return pcmk_rc_no_output;
}
if (patchset_cs == NULL) {
patchset_cs = qb_log_callsite_get(__func__, __FILE__, "xml-patchset",
log_level, __LINE__,
crm_trace_nonlog);
}
if (!crm_is_callsite_active(patchset_cs, log_level, crm_trace_nonlog)) {
// Nothing would be logged, so skip all the work
return pcmk_rc_no_output;
}
crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
if (format != 2) {
crm_err("Unknown patch format: %d", format);
return pcmk_rc_bad_xml_patch;
}
return xml_show_patchset(out, patchset);
}
/*!
* \internal
* \brief Output an XML patchset
*
* This function outputs an XML patchset (a \c PCMK_XE_DIFF element and its
* children) without modification, as a CDATA block.
*
* \param[in,out] out Output object
* \param[in] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# XML patchset
*/
PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
static int
xml_patchset_xml(pcmk__output_t *out, va_list args)
{
const xmlNode *patchset = va_arg(args, const xmlNode *);
if (patchset != NULL) {
GString *buf = g_string_sized_new(1024);
pcmk__xml_string(patchset, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buf,
0);
out->output_xml(out, PCMK_XE_XML_PATCHSET, buf->str);
g_string_free(buf, TRUE);
return pcmk_rc_ok;
}
crm_trace("Empty patch");
return pcmk_rc_no_output;
}
static pcmk__message_entry_t fmt_functions[] = {
{ "xml-patchset", "default", xml_patchset_default },
{ "xml-patchset", "log", xml_patchset_log },
{ "xml-patchset", "xml", xml_patchset_xml },
{ NULL, NULL, NULL }
};
/*!
* \internal
* \brief Register the formatting functions for XML patchsets
*
* \param[in,out] out Output object
*/
void
pcmk__register_patchset_messages(pcmk__output_t *out) {
pcmk__register_messages(out, fmt_functions);
}
diff --git a/lib/common/tests/acl/Makefile.am b/lib/common/tests/acl/Makefile.am
index d8b8b06cbe..e9e001283f 100644
--- a/lib/common/tests/acl/Makefile.am
+++ b/lib/common/tests/acl/Makefile.am
@@ -1,21 +1,20 @@
#
-# Copyright 2021-2024 the Pacemaker project contributors
+# Copyright 2021-2025 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 $(top_srcdir)/mk/common.mk
include $(top_srcdir)/mk/tap.mk
include $(top_srcdir)/mk/unittest.mk
# Add "_test" to the end of all test program names to simplify .gitignore.
check_PROGRAMS = pcmk__is_user_in_group_test \
pcmk_acl_required_test \
- xml_acl_denied_test \
- xml_acl_enabled_test
+ xml_acl_denied_test
TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/acl/xml_acl_enabled_test.c b/lib/common/tests/acl/xml_acl_enabled_test.c
deleted file mode 100644
index 6a920a3c73..0000000000
--- a/lib/common/tests/acl/xml_acl_enabled_test.c
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright 2020-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 <crm_internal.h>
-
-#include <crm/common/unittest_internal.h>
-#include <crm/common/acl.h>
-
-#include "../../crmcommon_private.h"
-
-static void
-is_xml_acl_enabled_without_node(void **state)
-{
- xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml");
- assert_false(xml_acl_enabled(test_xml));
-
- test_xml->doc->_private = NULL;
- assert_false(xml_acl_enabled(test_xml));
-
- test_xml->doc = NULL;
- assert_false(xml_acl_enabled(test_xml));
-
- test_xml = NULL;
- assert_false(xml_acl_enabled(test_xml));
-}
-
-static void
-is_xml_acl_enabled_with_node(void **state)
-{
- xml_doc_private_t *docpriv;
-
- xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml");
-
- // allocate memory for _private, which is NULL by default
- test_xml->doc->_private = pcmk__assert_alloc(1, sizeof(xml_doc_private_t));
-
- assert_false(xml_acl_enabled(test_xml));
-
- // cast _private from void* to xml_doc_private_t*
- docpriv = test_xml->doc->_private;
-
- // enable an irrelevant flag
- docpriv->flags |= pcmk__xf_acl_denied;
-
- assert_false(xml_acl_enabled(test_xml));
-
- // enable pcmk__xf_acl_enabled
- docpriv->flags |= pcmk__xf_acl_enabled;
-
- assert_true(xml_acl_enabled(test_xml));
-}
-
-PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group,
- cmocka_unit_test(is_xml_acl_enabled_without_node),
- cmocka_unit_test(is_xml_acl_enabled_with_node))
diff --git a/lib/pacemaker/pcmk_sched_actions.c b/lib/pacemaker/pcmk_sched_actions.c
index c7b7c62fbc..8a45c15f99 100644
--- a/lib/pacemaker/pcmk_sched_actions.c
+++ b/lib/pacemaker/pcmk_sched_actions.c
@@ -1,1959 +1,1959 @@
/*
* Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <stdbool.h> // bool, true, false
#include <stdio.h>
#include <sys/param.h>
#include <glib.h>
#include <libxml/tree.h> // xmlNode
#include <crm/lrmd_internal.h>
#include <crm/common/scheduler_internal.h>
#include <pacemaker-internal.h>
#include "libpacemaker_private.h"
/*!
* \internal
* \brief Get the action flags relevant to ordering constraints
*
* \param[in,out] action Action to check
* \param[in] node Node that *other* action in the ordering is on
* (used only for clone resource actions)
*
* \return Action flags that should be used for orderings
*/
static uint32_t
action_flags_for_ordering(pcmk_action_t *action, const pcmk_node_t *node)
{
bool runnable = false;
uint32_t flags;
// For non-resource actions, return the action flags
if (action->rsc == NULL) {
return action->flags;
}
/* For non-clone resources, or a clone action not assigned to a node,
* return the flags as determined by the resource method without a node
* specified.
*/
flags = action->rsc->priv->cmds->action_flags(action, NULL);
if ((node == NULL) || !pcmk__is_clone(action->rsc)) {
return flags;
}
/* Otherwise (i.e., for clone resource actions on a specific node), first
* remember whether the non-node-specific action is runnable.
*/
runnable = pcmk_is_set(flags, pcmk__action_runnable);
// Then recheck the resource method with the node
flags = action->rsc->priv->cmds->action_flags(action, node);
/* For clones in ordering constraints, the node-specific "runnable" doesn't
* matter, just the non-node-specific setting (i.e., is the action runnable
* anywhere).
*
* This applies only to runnable, and only for ordering constraints. This
* function shouldn't be used for other types of constraints without
* changes. Not very satisfying, but it's logical and appears to work well.
*/
if (runnable && !pcmk_is_set(flags, pcmk__action_runnable)) {
pcmk__set_raw_action_flags(flags, action->rsc->id,
pcmk__action_runnable);
}
return flags;
}
/*!
* \internal
* \brief Get action UUID that should be used with a resource ordering
*
* When an action is ordered relative to an action for a collective resource
* (clone, group, or bundle), it actually needs to be ordered after all
* instances of the collective have completed the relevant action (for example,
* given "start CLONE then start RSC", RSC must wait until all instances of
* CLONE have started). Given the UUID and resource of the first action in an
* ordering, this returns the UUID of the action that should actually be used
* for ordering (for example, "CLONE_started_0" instead of "CLONE_start_0").
*
* \param[in] first_uuid UUID of first action in ordering
* \param[in] first_rsc Resource of first action in ordering
*
* \return Newly allocated copy of UUID to use with ordering
* \note It is the caller's responsibility to free the return value.
*/
static char *
action_uuid_for_ordering(const char *first_uuid,
const pcmk_resource_t *first_rsc)
{
guint interval_ms = 0;
char *uuid = NULL;
char *rid = NULL;
char *first_task_str = NULL;
enum pcmk__action_type first_task = pcmk__action_unspecified;
enum pcmk__action_type remapped_task = pcmk__action_unspecified;
// Only non-notify actions for collective resources need remapping
if ((strstr(first_uuid, PCMK_ACTION_NOTIFY) != NULL)
|| (first_rsc->priv->variant < pcmk__rsc_variant_group)) {
goto done;
}
// Only non-recurring actions need remapping
pcmk__assert(parse_op_key(first_uuid, &rid, &first_task_str, &interval_ms));
if (interval_ms > 0) {
goto done;
}
first_task = pcmk__parse_action(first_task_str);
switch (first_task) {
case pcmk__action_stop:
case pcmk__action_start:
case pcmk__action_notify:
case pcmk__action_promote:
case pcmk__action_demote:
remapped_task = first_task + 1;
break;
case pcmk__action_stopped:
case pcmk__action_started:
case pcmk__action_notified:
case pcmk__action_promoted:
case pcmk__action_demoted:
remapped_task = first_task;
break;
case pcmk__action_monitor:
case pcmk__action_shutdown:
case pcmk__action_fence:
break;
default:
crm_err("Unknown action '%s' in ordering", first_task_str);
break;
}
if (remapped_task != pcmk__action_unspecified) {
/* If a clone or bundle has notifications enabled, the ordering will be
* relative to when notifications have been sent for the remapped task.
*/
if (pcmk_is_set(first_rsc->flags, pcmk__rsc_notify)
&& (pcmk__is_clone(first_rsc) || pcmk__is_bundled(first_rsc))) {
uuid = pcmk__notify_key(rid, "confirmed-post",
pcmk__action_text(remapped_task));
} else {
uuid = pcmk__op_key(rid, pcmk__action_text(remapped_task), 0);
}
pcmk__rsc_trace(first_rsc,
"Remapped action UUID %s to %s for ordering purposes",
first_uuid, uuid);
}
done:
free(first_task_str);
free(rid);
return (uuid != NULL)? uuid : pcmk__str_copy(first_uuid);
}
/*!
* \internal
* \brief Get actual action that should be used with an ordering
*
* When an action is ordered relative to an action for a collective resource
* (clone, group, or bundle), it actually needs to be ordered after all
* instances of the collective have completed the relevant action (for example,
* given "start CLONE then start RSC", RSC must wait until all instances of
* CLONE have started). Given the first action in an ordering, this returns the
* the action that should actually be used for ordering (for example, the
* started action instead of the start action).
*
* \param[in] action First action in an ordering
*
* \return Actual action that should be used for the ordering
*/
static pcmk_action_t *
action_for_ordering(pcmk_action_t *action)
{
pcmk_action_t *result = action;
pcmk_resource_t *rsc = action->rsc;
if (rsc == NULL) {
return result;
}
if ((rsc->priv->variant >= pcmk__rsc_variant_group)
&& (action->uuid != NULL)) {
char *uuid = action_uuid_for_ordering(action->uuid, rsc);
result = find_first_action(rsc->priv->actions, uuid, NULL, NULL);
if (result == NULL) {
crm_warn("Not remapping %s to %s because %s does not have "
"remapped action", action->uuid, uuid, rsc->id);
result = action;
}
free(uuid);
}
return result;
}
/*!
* \internal
* \brief Wrapper for update_ordered_actions() method for readability
*
* \param[in,out] rsc Resource to call method for
* \param[in,out] first 'First' action in an ordering
* \param[in,out] then 'Then' action in an ordering
* \param[in] node If not NULL, limit scope of ordering to this
* node (only used when interleaving instances)
* \param[in] flags Action flags for \p first for ordering purposes
* \param[in] filter Action flags to limit scope of certain updates
* (may include pcmk__action_optional to affect only
* mandatory actions, and pe_action_runnable to
* affect only runnable actions)
* \param[in] type Group of enum pcmk__action_relation_flags to apply
* \param[in,out] scheduler Scheduler data
*
* \return Group of enum pcmk__updated flags indicating what was updated
*/
static inline uint32_t
update(pcmk_resource_t *rsc, pcmk_action_t *first, pcmk_action_t *then,
const pcmk_node_t *node, uint32_t flags, uint32_t filter, uint32_t type,
pcmk_scheduler_t *scheduler)
{
return rsc->priv->cmds->update_ordered_actions(first, then, node, flags,
filter, type, scheduler);
}
/*!
* \internal
* \brief Update flags for ordering's actions appropriately for ordering's flags
*
* \param[in,out] first First action in an ordering
* \param[in,out] then Then action in an ordering
* \param[in] first_flags Action flags for \p first for ordering purposes
* \param[in] then_flags Action flags for \p then for ordering purposes
* \param[in,out] order Action wrapper for \p first in ordering
* \param[in,out] scheduler Scheduler data
*
* \return Group of enum pcmk__updated flags
*/
static uint32_t
update_action_for_ordering_flags(pcmk_action_t *first, pcmk_action_t *then,
uint32_t first_flags, uint32_t then_flags,
pcmk__related_action_t *order,
pcmk_scheduler_t *scheduler)
{
uint32_t changed = pcmk__updated_none;
/* The node will only be used for clones. If interleaved, node will be NULL,
* otherwise the ordering scope will be limited to the node. Normally, the
* whole 'then' clone should restart if 'first' is restarted, so then->node
* is needed.
*/
pcmk_node_t *node = then->node;
if (pcmk_is_set(order->flags, pcmk__ar_first_implies_same_node_then)) {
/* For unfencing, only instances of 'then' on the same node as 'first'
* (the unfencing operation) should restart, so reset node to
* first->node, at which point this case is handled like a normal
* pcmk__ar_first_implies_then.
*/
pcmk__clear_relation_flags(order->flags,
pcmk__ar_first_implies_same_node_then);
pcmk__set_relation_flags(order->flags, pcmk__ar_first_implies_then);
node = first->node;
pcmk__rsc_trace(then->rsc,
"%s then %s: mapped "
"pcmk__ar_first_implies_same_node_then to "
"pcmk__ar_first_implies_then on %s",
first->uuid, then->uuid, pcmk__node_name(node));
}
if (pcmk_is_set(order->flags, pcmk__ar_first_implies_then)) {
if (then->rsc != NULL) {
changed |= update(then->rsc, first, then, node,
first_flags & pcmk__action_optional,
pcmk__action_optional,
pcmk__ar_first_implies_then, scheduler);
} else if (!pcmk_is_set(first_flags, pcmk__action_optional)
&& pcmk_is_set(then->flags, pcmk__action_optional)) {
pcmk__clear_action_flags(then, pcmk__action_optional);
pcmk__set_updated_flags(changed, first, pcmk__updated_then);
}
pcmk__rsc_trace(then->rsc,
"%s then %s: %s after pcmk__ar_first_implies_then",
first->uuid, then->uuid,
(changed? "changed" : "unchanged"));
}
if (pcmk_is_set(order->flags, pcmk__ar_intermediate_stop)
&& (then->rsc != NULL)) {
enum pcmk__action_flags restart = pcmk__action_optional
|pcmk__action_runnable;
changed |= update(then->rsc, first, then, node, first_flags, restart,
pcmk__ar_intermediate_stop, scheduler);
pcmk__rsc_trace(then->rsc,
"%s then %s: %s after pcmk__ar_intermediate_stop",
first->uuid, then->uuid,
(changed? "changed" : "unchanged"));
}
if (pcmk_is_set(order->flags, pcmk__ar_then_implies_first)) {
if (first->rsc != NULL) {
changed |= update(first->rsc, first, then, node, first_flags,
pcmk__action_optional,
pcmk__ar_then_implies_first, scheduler);
} else if (!pcmk_is_set(first_flags, pcmk__action_optional)
&& pcmk_is_set(first->flags, pcmk__action_runnable)) {
pcmk__clear_action_flags(first, pcmk__action_runnable);
pcmk__set_updated_flags(changed, first, pcmk__updated_first);
}
pcmk__rsc_trace(then->rsc,
"%s then %s: %s after pcmk__ar_then_implies_first",
first->uuid, then->uuid,
(changed? "changed" : "unchanged"));
}
if (pcmk_is_set(order->flags, pcmk__ar_promoted_then_implies_first)) {
if (then->rsc != NULL) {
changed |= update(then->rsc, first, then, node,
first_flags & pcmk__action_optional,
pcmk__action_optional,
pcmk__ar_promoted_then_implies_first, scheduler);
}
pcmk__rsc_trace(then->rsc,
"%s then %s: %s after "
"pcmk__ar_promoted_then_implies_first",
first->uuid, then->uuid,
(changed? "changed" : "unchanged"));
}
if (pcmk_is_set(order->flags, pcmk__ar_min_runnable)) {
if (then->rsc != NULL) {
changed |= update(then->rsc, first, then, node, first_flags,
pcmk__action_runnable, pcmk__ar_min_runnable,
scheduler);
} else if (pcmk_is_set(first_flags, pcmk__action_runnable)) {
// We have another runnable instance of "first"
then->runnable_before++;
/* Mark "then" as runnable if it requires a certain number of
* "before" instances to be runnable, and they now are.
*/
if ((then->runnable_before >= then->required_runnable_before)
&& !pcmk_is_set(then->flags, pcmk__action_runnable)) {
pcmk__set_action_flags(then, pcmk__action_runnable);
pcmk__set_updated_flags(changed, first, pcmk__updated_then);
}
}
pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_min_runnable",
first->uuid, then->uuid,
(changed? "changed" : "unchanged"));
}
if (pcmk_is_set(order->flags, pcmk__ar_nested_remote_probe)
&& (then->rsc != NULL)) {
if (!pcmk_is_set(first_flags, pcmk__action_runnable)
&& (first->rsc != NULL)
&& (first->rsc->priv->active_nodes != NULL)) {
pcmk__rsc_trace(then->rsc,
"%s then %s: ignoring because first is stopping",
first->uuid, then->uuid);
order->flags = pcmk__ar_none;
} else {
changed |= update(then->rsc, first, then, node, first_flags,
pcmk__action_runnable,
pcmk__ar_unrunnable_first_blocks, scheduler);
}
pcmk__rsc_trace(then->rsc,
"%s then %s: %s after pcmk__ar_nested_remote_probe",
first->uuid, then->uuid,
(changed? "changed" : "unchanged"));
}
if (pcmk_is_set(order->flags, pcmk__ar_unrunnable_first_blocks)) {
if (then->rsc != NULL) {
changed |= update(then->rsc, first, then, node, first_flags,
pcmk__action_runnable,
pcmk__ar_unrunnable_first_blocks, scheduler);
} else if (!pcmk_is_set(first_flags, pcmk__action_runnable)
&& pcmk_is_set(then->flags, pcmk__action_runnable)) {
pcmk__clear_action_flags(then, pcmk__action_runnable);
pcmk__set_updated_flags(changed, first, pcmk__updated_then);
}
pcmk__rsc_trace(then->rsc,
"%s then %s: %s after pcmk__ar_unrunnable_first_blocks",
first->uuid, then->uuid,
(changed? "changed" : "unchanged"));
}
if (pcmk_is_set(order->flags, pcmk__ar_unmigratable_then_blocks)) {
if (then->rsc != NULL) {
changed |= update(then->rsc, first, then, node, first_flags,
pcmk__action_optional,
pcmk__ar_unmigratable_then_blocks, scheduler);
}
pcmk__rsc_trace(then->rsc,
"%s then %s: %s after "
"pcmk__ar_unmigratable_then_blocks",
first->uuid, then->uuid,
(changed? "changed" : "unchanged"));
}
if (pcmk_is_set(order->flags, pcmk__ar_first_else_then)) {
if (then->rsc != NULL) {
changed |= update(then->rsc, first, then, node, first_flags,
pcmk__action_optional, pcmk__ar_first_else_then,
scheduler);
}
pcmk__rsc_trace(then->rsc,
"%s then %s: %s after pcmk__ar_first_else_then",
first->uuid, then->uuid,
(changed? "changed" : "unchanged"));
}
if (pcmk_is_set(order->flags, pcmk__ar_ordered)) {
if (then->rsc != NULL) {
changed |= update(then->rsc, first, then, node, first_flags,
pcmk__action_runnable, pcmk__ar_ordered,
scheduler);
}
pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_ordered",
first->uuid, then->uuid,
(changed? "changed" : "unchanged"));
}
if (pcmk_is_set(order->flags, pcmk__ar_asymmetric)) {
if (then->rsc != NULL) {
changed |= update(then->rsc, first, then, node, first_flags,
pcmk__action_runnable, pcmk__ar_asymmetric,
scheduler);
}
pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_asymmetric",
first->uuid, then->uuid,
(changed? "changed" : "unchanged"));
}
if (pcmk_is_set(first->flags, pcmk__action_runnable)
&& pcmk_is_set(order->flags, pcmk__ar_first_implies_then_graphed)
&& !pcmk_is_set(first_flags, pcmk__action_optional)) {
pcmk__rsc_trace(then->rsc, "%s will be in graph because %s is required",
then->uuid, first->uuid);
pcmk__set_action_flags(then, pcmk__action_always_in_graph);
// Don't bother marking 'then' as changed just for this
}
if (pcmk_is_set(order->flags, pcmk__ar_then_implies_first_graphed)
&& !pcmk_is_set(then_flags, pcmk__action_optional)) {
pcmk__rsc_trace(then->rsc, "%s will be in graph because %s is required",
first->uuid, then->uuid);
pcmk__set_action_flags(first, pcmk__action_always_in_graph);
// Don't bother marking 'first' as changed just for this
}
if (pcmk_any_flags_set(order->flags, pcmk__ar_first_implies_then
|pcmk__ar_then_implies_first
|pcmk__ar_intermediate_stop)
&& (first->rsc != NULL)
&& !pcmk_is_set(first->rsc->flags, pcmk__rsc_managed)
&& pcmk_is_set(first->rsc->flags, pcmk__rsc_blocked)
&& !pcmk_is_set(first->flags, pcmk__action_runnable)
&& pcmk__str_eq(first->task, PCMK_ACTION_STOP, pcmk__str_none)) {
/* @TODO This seems odd; why wouldn't an unrunnable "first" already
* block "then" before this? Note that the unmanaged-stop-{1,2}
* scheduler regression tests and the test CIB for T209 have tests for
* "stop then stop" relations that would be good for checking any
* changes.
*/
if (pcmk_is_set(then->flags, pcmk__action_runnable)) {
pcmk__clear_action_flags(then, pcmk__action_runnable);
pcmk__set_updated_flags(changed, first, pcmk__updated_then);
}
pcmk__rsc_trace(then->rsc,
"%s then %s: %s after checking whether first "
"is blocked, unmanaged, unrunnable stop",
first->uuid, then->uuid,
(changed? "changed" : "unchanged"));
}
return changed;
}
// Convenience macros for logging action properties
#define action_type_str(flags) \
(pcmk_is_set((flags), pcmk__action_pseudo)? "pseudo-action" : "action")
#define action_optional_str(flags) \
(pcmk_is_set((flags), pcmk__action_optional)? "optional" : "required")
#define action_runnable_str(flags) \
(pcmk_is_set((flags), pcmk__action_runnable)? "runnable" : "unrunnable")
#define action_node_str(a) \
(((a)->node == NULL)? "no node" : (a)->node->priv->name)
/*!
* \internal
* \brief Update an action's flags for all orderings where it is "then"
*
* \param[in,out] then Action to update
* \param[in,out] scheduler Scheduler data
*/
void
pcmk__update_action_for_orderings(pcmk_action_t *then,
pcmk_scheduler_t *scheduler)
{
GList *lpc = NULL;
uint32_t changed = pcmk__updated_none;
int last_flags = then->flags;
pcmk__rsc_trace(then->rsc, "Updating %s %s (%s %s) on %s",
action_type_str(then->flags), then->uuid,
action_optional_str(then->flags),
action_runnable_str(then->flags), action_node_str(then));
if (then->required_runnable_before > 0) {
/* Initialize current known "runnable before" actions. As
* update_action_for_ordering_flags() is called for each of then's
* before actions, this number will increment as runnable 'first'
* actions are encountered.
*/
then->runnable_before = 0;
/* The pcmk__ar_min_runnable clause of
* update_action_for_ordering_flags() (called below)
* will reset runnable if appropriate.
*/
pcmk__clear_action_flags(then, pcmk__action_runnable);
}
for (lpc = then->actions_before; lpc != NULL; lpc = lpc->next) {
pcmk__related_action_t *other = lpc->data;
pcmk_action_t *first = other->action;
pcmk_node_t *then_node = then->node;
pcmk_node_t *first_node = first->node;
const uint32_t target = pcmk__rsc_node_assigned;
if ((first->rsc != NULL)
&& pcmk__is_group(first->rsc)
&& pcmk__str_eq(first->task, PCMK_ACTION_START, pcmk__str_none)) {
first_node = first->rsc->priv->fns->location(first->rsc, NULL,
target);
if (first_node != NULL) {
pcmk__rsc_trace(first->rsc, "Found %s for 'first' %s",
pcmk__node_name(first_node), first->uuid);
}
}
if (pcmk__is_group(then->rsc)
&& pcmk__str_eq(then->task, PCMK_ACTION_START, pcmk__str_none)) {
then_node = then->rsc->priv->fns->location(then->rsc, NULL, target);
if (then_node != NULL) {
pcmk__rsc_trace(then->rsc, "Found %s for 'then' %s",
pcmk__node_name(then_node), then->uuid);
}
}
// Disable constraint if it only applies when on same node, but isn't
if (pcmk_is_set(other->flags, pcmk__ar_if_on_same_node)
&& (first_node != NULL) && (then_node != NULL)
&& !pcmk__same_node(first_node, then_node)) {
pcmk__rsc_trace(then->rsc,
"Disabled ordering %s on %s then %s on %s: "
"not same node",
other->action->uuid, pcmk__node_name(first_node),
then->uuid, pcmk__node_name(then_node));
other->flags = pcmk__ar_none;
continue;
}
pcmk__clear_updated_flags(changed, then, pcmk__updated_first);
if ((first->rsc != NULL)
&& pcmk_is_set(other->flags, pcmk__ar_then_cancels_first)
&& !pcmk_is_set(then->flags, pcmk__action_optional)) {
/* 'then' is required, so we must abandon 'first'
* (e.g. a required stop cancels any agent reload).
*/
pcmk__set_action_flags(other->action, pcmk__action_optional);
if (!strcmp(first->task, PCMK_ACTION_RELOAD_AGENT)) {
pcmk__clear_rsc_flags(first->rsc, pcmk__rsc_reload);
}
}
if ((first->rsc != NULL) && (then->rsc != NULL)
&& (first->rsc != then->rsc) && !is_parent(then->rsc, first->rsc)) {
first = action_for_ordering(first);
}
if (first != other->action) {
pcmk__rsc_trace(then->rsc, "Ordering %s after %s instead of %s",
then->uuid, first->uuid, other->action->uuid);
}
pcmk__rsc_trace(then->rsc,
"%s (%#.6x) then %s (%#.6x): type=%#.6x node=%s",
first->uuid, first->flags, then->uuid, then->flags,
other->flags, action_node_str(first));
if (first == other->action) {
/* 'first' was not remapped (e.g. from 'start' to 'running'), which
* could mean it is a non-resource action, a primitive resource
* action, or already expanded.
*/
uint32_t first_flags, then_flags;
first_flags = action_flags_for_ordering(first, then_node);
then_flags = action_flags_for_ordering(then, first_node);
changed |= update_action_for_ordering_flags(first, then,
first_flags, then_flags,
other, scheduler);
/* 'first' was for a complex resource (clone, group, etc),
* create a new dependency if necessary
*/
} else if (order_actions(first, then, other->flags)) {
/* This was the first time 'first' and 'then' were associated,
* start again to get the new actions_before list
*/
pcmk__set_updated_flags(changed, then, pcmk__updated_then);
pcmk__rsc_trace(then->rsc,
"Disabled ordering %s then %s in favor of %s "
"then %s",
other->action->uuid, then->uuid, first->uuid,
then->uuid);
other->flags = pcmk__ar_none;
}
if (pcmk_is_set(changed, pcmk__updated_first)) {
crm_trace("Re-processing %s and its 'after' actions "
"because it changed", first->uuid);
for (GList *lpc2 = first->actions_after; lpc2 != NULL;
lpc2 = lpc2->next) {
pcmk__related_action_t *other = lpc2->data;
pcmk__update_action_for_orderings(other->action, scheduler);
}
pcmk__update_action_for_orderings(first, scheduler);
}
}
if (then->required_runnable_before > 0) {
if (last_flags == then->flags) {
pcmk__clear_updated_flags(changed, then, pcmk__updated_then);
} else {
pcmk__set_updated_flags(changed, then, pcmk__updated_then);
}
}
if (pcmk_is_set(changed, pcmk__updated_then)) {
crm_trace("Re-processing %s and its 'after' actions because it changed",
then->uuid);
if (pcmk_is_set(last_flags, pcmk__action_runnable)
&& !pcmk_is_set(then->flags, pcmk__action_runnable)) {
pcmk__block_colocation_dependents(then);
}
pcmk__update_action_for_orderings(then, scheduler);
for (lpc = then->actions_after; lpc != NULL; lpc = lpc->next) {
pcmk__related_action_t *other = lpc->data;
pcmk__update_action_for_orderings(other->action, scheduler);
}
}
}
static inline bool
is_primitive_action(const pcmk_action_t *action)
{
return (action != NULL) && pcmk__is_primitive(action->rsc);
}
/*!
* \internal
* \brief Clear a single action flag and set reason text
*
* \param[in,out] action Action whose flag should be cleared
* \param[in] flag Action flag that should be cleared
* \param[in] reason Action that is the reason why flag is being cleared
*/
#define clear_action_flag_because(action, flag, reason) do { \
if (pcmk_is_set((action)->flags, (flag))) { \
pcmk__clear_action_flags(action, flag); \
if ((action)->rsc != (reason)->rsc) { \
char *reason_text = pe__action2reason((reason), (flag)); \
pe_action_set_reason((action), reason_text, false); \
free(reason_text); \
} \
} \
} while (0)
/*!
* \internal
* \brief Update actions in an asymmetric ordering
*
* If the "first" action in an asymmetric ordering is unrunnable, make the
* "second" action unrunnable as well, if appropriate.
*
* \param[in] first 'First' action in an asymmetric ordering
* \param[in,out] then 'Then' action in an asymmetric ordering
*/
static void
handle_asymmetric_ordering(const pcmk_action_t *first, pcmk_action_t *then)
{
/* Only resource actions after an unrunnable 'first' action need updates for
* asymmetric ordering.
*/
if ((then->rsc == NULL)
|| pcmk_is_set(first->flags, pcmk__action_runnable)) {
return;
}
// Certain optional 'then' actions are unaffected by unrunnable 'first'
if (pcmk_is_set(then->flags, pcmk__action_optional)) {
enum rsc_role_e then_rsc_role;
then_rsc_role = then->rsc->priv->fns->state(then->rsc, true);
if ((then_rsc_role == pcmk_role_stopped)
&& pcmk__str_eq(then->task, PCMK_ACTION_STOP, pcmk__str_none)) {
/* If 'then' should stop after 'first' but is already stopped, the
* ordering is irrelevant.
*/
return;
} else if ((then_rsc_role >= pcmk_role_started)
&& pcmk__str_eq(then->task, PCMK_ACTION_START, pcmk__str_none)
&& pe__rsc_running_on_only(then->rsc, then->node)) {
/* Similarly if 'then' should start after 'first' but is already
* started on a single node.
*/
return;
}
}
// 'First' can't run, so 'then' can't either
clear_action_flag_because(then, pcmk__action_optional, first);
clear_action_flag_because(then, pcmk__action_runnable, first);
}
/*!
* \internal
* \brief Set action bits appropriately when pcmk__ar_intermediate_stop is used
*
* \param[in,out] first 'First' action in ordering
* \param[in,out] then 'Then' action in ordering
* \param[in] filter What action flags to care about
*
* \note pcmk__ar_intermediate_stop is set for "stop resource before starting
* it" and "stop later group member before stopping earlier group member"
*/
static void
handle_restart_ordering(pcmk_action_t *first, pcmk_action_t *then,
uint32_t filter)
{
const char *reason = NULL;
pcmk__assert(is_primitive_action(first) && is_primitive_action(then));
// We need to update the action in two cases:
// ... if 'then' is required
if (pcmk_is_set(filter, pcmk__action_optional)
&& !pcmk_is_set(then->flags, pcmk__action_optional)) {
reason = "restart";
}
/* ... if 'then' is unrunnable action on same resource (if a resource
* should restart but can't start, we still want to stop)
*/
if (pcmk_is_set(filter, pcmk__action_runnable)
&& !pcmk_is_set(then->flags, pcmk__action_runnable)
&& pcmk_is_set(then->rsc->flags, pcmk__rsc_managed)
&& (first->rsc == then->rsc)) {
reason = "stop";
}
if (reason == NULL) {
return;
}
pcmk__rsc_trace(first->rsc, "Handling %s -> %s for %s",
first->uuid, then->uuid, reason);
// Make 'first' required if it is runnable
if (pcmk_is_set(first->flags, pcmk__action_runnable)) {
clear_action_flag_because(first, pcmk__action_optional, then);
}
// Make 'first' required if 'then' is required
if (!pcmk_is_set(then->flags, pcmk__action_optional)) {
clear_action_flag_because(first, pcmk__action_optional, then);
}
// Make 'first' unmigratable if 'then' is unmigratable
if (!pcmk_is_set(then->flags, pcmk__action_migratable)) {
clear_action_flag_because(first, pcmk__action_migratable, then);
}
// Make 'then' unrunnable if 'first' is required but unrunnable
if (!pcmk_is_set(first->flags, pcmk__action_optional)
&& !pcmk_is_set(first->flags, pcmk__action_runnable)) {
clear_action_flag_because(then, pcmk__action_runnable, first);
}
}
/*!
* \internal
* \brief Update two actions according to an ordering between them
*
* Given information about an ordering of two actions, update the actions' flags
* (and runnable_before members if appropriate) as appropriate for the ordering.
* Effects may cascade to other orderings involving the actions as well.
*
* \param[in,out] first 'First' action in an ordering
* \param[in,out] then 'Then' action in an ordering
* \param[in] node If not NULL, limit scope of ordering to this node
* (ignored)
* \param[in] flags Action flags for \p first for ordering purposes
* \param[in] filter Action flags to limit scope of certain updates (may
* include pcmk__action_optional to affect only
* mandatory actions, and pcmk__action_runnable to
* affect only runnable actions)
* \param[in] type Group of enum pcmk__action_relation_flags to apply
* \param[in,out] scheduler Scheduler data
*
* \return Group of enum pcmk__updated flags indicating what was updated
*/
uint32_t
pcmk__update_ordered_actions(pcmk_action_t *first, pcmk_action_t *then,
const pcmk_node_t *node, uint32_t flags,
uint32_t filter, uint32_t type,
pcmk_scheduler_t *scheduler)
{
uint32_t changed = pcmk__updated_none;
uint32_t then_flags = 0U;
uint32_t first_flags = 0U;
pcmk__assert((first != NULL) && (then != NULL) && (scheduler != NULL));
then_flags = then->flags;
first_flags = first->flags;
if (pcmk_is_set(type, pcmk__ar_asymmetric)) {
handle_asymmetric_ordering(first, then);
}
if (pcmk_is_set(type, pcmk__ar_then_implies_first)
&& !pcmk_is_set(then_flags, pcmk__action_optional)) {
// Then is required, and implies first should be, too
if (pcmk_is_set(filter, pcmk__action_optional)
&& !pcmk_is_set(flags, pcmk__action_optional)
&& pcmk_is_set(first_flags, pcmk__action_optional)) {
clear_action_flag_because(first, pcmk__action_optional, then);
}
if (pcmk_is_set(flags, pcmk__action_migratable)
&& !pcmk_is_set(then->flags, pcmk__action_migratable)) {
clear_action_flag_because(first, pcmk__action_migratable, then);
}
}
if (pcmk_is_set(type, pcmk__ar_promoted_then_implies_first)
&& (then->rsc != NULL)
&& (then->rsc->priv->orig_role == pcmk_role_promoted)
&& pcmk_is_set(filter, pcmk__action_optional)
&& !pcmk_is_set(then->flags, pcmk__action_optional)) {
clear_action_flag_because(first, pcmk__action_optional, then);
if (pcmk_is_set(first->flags, pcmk__action_migratable)
&& !pcmk_is_set(then->flags, pcmk__action_migratable)) {
clear_action_flag_because(first, pcmk__action_migratable, then);
}
}
if (pcmk_is_set(type, pcmk__ar_unmigratable_then_blocks)
&& pcmk_is_set(filter, pcmk__action_optional)) {
if (!pcmk_all_flags_set(then->flags, pcmk__action_migratable
|pcmk__action_runnable)) {
clear_action_flag_because(first, pcmk__action_runnable, then);
}
if (!pcmk_is_set(then->flags, pcmk__action_optional)) {
clear_action_flag_because(first, pcmk__action_optional, then);
}
}
if (pcmk_is_set(type, pcmk__ar_first_else_then)
&& pcmk_is_set(filter, pcmk__action_optional)
&& !pcmk_is_set(first->flags, pcmk__action_runnable)) {
clear_action_flag_because(then, pcmk__action_migratable, first);
pcmk__clear_action_flags(then, pcmk__action_pseudo);
}
if (pcmk_is_set(type, pcmk__ar_unrunnable_first_blocks)
&& pcmk_is_set(filter, pcmk__action_runnable)
&& pcmk_is_set(then->flags, pcmk__action_runnable)
&& !pcmk_is_set(flags, pcmk__action_runnable)) {
clear_action_flag_because(then, pcmk__action_runnable, first);
clear_action_flag_because(then, pcmk__action_migratable, first);
}
if (pcmk_is_set(type, pcmk__ar_first_implies_then)
&& pcmk_is_set(filter, pcmk__action_optional)
&& pcmk_is_set(then->flags, pcmk__action_optional)
&& !pcmk_is_set(flags, pcmk__action_optional)
&& !pcmk_is_set(first->flags, pcmk__action_migratable)) {
clear_action_flag_because(then, pcmk__action_optional, first);
}
if (pcmk_is_set(type, pcmk__ar_intermediate_stop)) {
handle_restart_ordering(first, then, filter);
}
if (then_flags != then->flags) {
pcmk__set_updated_flags(changed, first, pcmk__updated_then);
pcmk__rsc_trace(then->rsc,
"%s on %s: flags are now %#.6x (was %#.6x) "
"because of 'first' %s (%#.6x)",
then->uuid, pcmk__node_name(then->node),
then->flags, then_flags, first->uuid, first->flags);
if ((then->rsc != NULL) && (then->rsc->priv->parent != NULL)) {
// Required to handle "X_stop then X_start" for cloned groups
pcmk__update_action_for_orderings(then, scheduler);
}
}
if (first_flags != first->flags) {
pcmk__set_updated_flags(changed, first, pcmk__updated_first);
pcmk__rsc_trace(first->rsc,
"%s on %s: flags are now %#.6x (was %#.6x) "
"because of 'then' %s (%#.6x)",
first->uuid, pcmk__node_name(first->node),
first->flags, first_flags, then->uuid, then->flags);
}
return changed;
}
/*!
* \internal
* \brief Trace-log an action (optionally with its dependent actions)
*
* \param[in] pre_text If not NULL, prefix the log with this plus ": "
* \param[in] action Action to log
* \param[in] details If true, recursively log dependent actions
*/
void
pcmk__log_action(const char *pre_text, const pcmk_action_t *action,
bool details)
{
const char *node_uname = NULL;
const char *node_uuid = NULL;
const char *desc = NULL;
CRM_CHECK(action != NULL, return);
if (!pcmk_is_set(action->flags, pcmk__action_pseudo)) {
if (action->node != NULL) {
node_uname = action->node->priv->name;
node_uuid = action->node->priv->id;
} else {
node_uname = "<none>";
}
}
switch (pcmk__parse_action(action->task)) {
case pcmk__action_fence:
case pcmk__action_shutdown:
if (pcmk_is_set(action->flags, pcmk__action_pseudo)) {
desc = "Pseudo ";
} else if (pcmk_is_set(action->flags, pcmk__action_optional)) {
desc = "Optional ";
} else if (!pcmk_is_set(action->flags, pcmk__action_runnable)) {
desc = "!!Non-Startable!! ";
} else {
desc = "(Provisional) ";
}
crm_trace("%s%s%sAction %d: %s%s%s%s%s%s",
((pre_text == NULL)? "" : pre_text),
((pre_text == NULL)? "" : ": "),
desc, action->id, action->uuid,
(node_uname? "\ton " : ""), (node_uname? node_uname : ""),
(node_uuid? "\t\t(" : ""), (node_uuid? node_uuid : ""),
(node_uuid? ")" : ""));
break;
default:
if (pcmk_is_set(action->flags, pcmk__action_optional)) {
desc = "Optional ";
} else if (pcmk_is_set(action->flags, pcmk__action_pseudo)) {
desc = "Pseudo ";
} else if (!pcmk_is_set(action->flags, pcmk__action_runnable)) {
desc = "!!Non-Startable!! ";
} else {
desc = "(Provisional) ";
}
crm_trace("%s%s%sAction %d: %s %s%s%s%s%s%s",
((pre_text == NULL)? "" : pre_text),
((pre_text == NULL)? "" : ": "),
desc, action->id, action->uuid,
(action->rsc? action->rsc->id : "<none>"),
(node_uname? "\ton " : ""), (node_uname? node_uname : ""),
(node_uuid? "\t\t(" : ""), (node_uuid? node_uuid : ""),
(node_uuid? ")" : ""));
break;
}
if (details) {
const GList *iter = NULL;
const pcmk__related_action_t *other = NULL;
crm_trace("\t\t====== Preceding Actions");
for (iter = action->actions_before; iter != NULL; iter = iter->next) {
other = (const pcmk__related_action_t *) iter->data;
pcmk__log_action("\t\t", other->action, false);
}
crm_trace("\t\t====== Subsequent Actions");
for (iter = action->actions_after; iter != NULL; iter = iter->next) {
other = (const pcmk__related_action_t *) iter->data;
pcmk__log_action("\t\t", other->action, false);
}
crm_trace("\t\t====== End");
} else {
crm_trace("\t\t(before=%d, after=%d)",
g_list_length(action->actions_before),
g_list_length(action->actions_after));
}
}
/*!
* \internal
* \brief Create a new shutdown action for a node
*
* \param[in,out] node Node being shut down
*
* \return Newly created shutdown action for \p node
*/
pcmk_action_t *
pcmk__new_shutdown_action(pcmk_node_t *node)
{
char *shutdown_id = NULL;
pcmk_action_t *shutdown_op = NULL;
pcmk__assert(node != NULL);
shutdown_id = crm_strdup_printf("%s-%s", PCMK_ACTION_DO_SHUTDOWN,
node->priv->name);
shutdown_op = custom_action(NULL, shutdown_id, PCMK_ACTION_DO_SHUTDOWN,
node, FALSE, node->priv->scheduler);
pcmk__order_stops_before_shutdown(node, shutdown_op);
pcmk__insert_meta(shutdown_op, PCMK__META_OP_NO_WAIT, PCMK_VALUE_TRUE);
return shutdown_op;
}
/*!
* \internal
* \brief Calculate and add an operation digest to XML
*
* Calculate an operation digest, which enables us to later determine when a
* restart is needed due to the resource's parameters being changed, and add it
* to given XML.
*
* \param[in] op Operation result from executor
* \param[in,out] update XML to add digest to
*/
static void
add_op_digest_to_xml(const lrmd_event_data_t *op, xmlNode *update)
{
char *digest = NULL;
xmlNode *args_xml = NULL;
if (op->params == NULL) {
return;
}
args_xml = pcmk__xe_create(NULL, PCMK_XE_PARAMETERS);
g_hash_table_foreach(op->params, hash2field, args_xml);
pcmk__filter_op_for_digest(args_xml);
- digest = pcmk__digest_operation(args_xml);
+ digest = pcmk__digest_op_params(args_xml);
crm_xml_add(update, PCMK__XA_OP_DIGEST, digest);
pcmk__xml_free(args_xml);
free(digest);
}
#define FAKE_TE_ID "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
/*!
* \internal
* \brief Create XML for resource operation history update
*
* \param[in,out] parent Parent XML node to add to
* \param[in,out] op Operation event data
* \param[in] caller_version DC feature set
* \param[in] target_rc Expected result of operation
* \param[in] node Name of node on which operation was performed
* \param[in] origin Arbitrary description of update source
*
* \return Newly created XML node for history update
*/
xmlNode *
pcmk__create_history_xml(xmlNode *parent, lrmd_event_data_t *op,
const char *caller_version, int target_rc,
const char *node, const char *origin)
{
char *key = NULL;
char *magic = NULL;
char *op_id = NULL;
char *op_id_additional = NULL;
char *local_user_data = NULL;
const char *exit_reason = NULL;
xmlNode *xml_op = NULL;
const char *task = NULL;
CRM_CHECK(op != NULL, return NULL);
crm_trace("Creating history XML for %s-interval %s action for %s on %s "
"(DC version: %s, origin: %s)",
pcmk__readable_interval(op->interval_ms), op->op_type, op->rsc_id,
((node == NULL)? "no node" : node), caller_version, origin);
task = op->op_type;
/* Record a successful agent reload as a start, and a failed one as a
* monitor, to make life easier for the scheduler when determining the
* current state.
*
* @COMPAT We should check "reload" here only if the operation was for a
* pre-OCF-1.1 resource agent, but we don't know that here, and we should
* only ever get results for actions scheduled by us, so we can reasonably
* assume any "reload" is actually a pre-1.1 agent reload.
*
* @TODO This remapping can make log messages with task confusing for users
* (for example, an "Initiating reload ..." followed by "... start ...
* confirmed"). Either do this remapping in the scheduler if possible, or
* store the original task in a new XML attribute for later logging.
*/
if (pcmk__str_any_of(task, PCMK_ACTION_RELOAD, PCMK_ACTION_RELOAD_AGENT,
NULL)) {
if (op->op_status == PCMK_EXEC_DONE) {
task = PCMK_ACTION_START;
} else {
task = PCMK_ACTION_MONITOR;
}
}
key = pcmk__op_key(op->rsc_id, task, op->interval_ms);
if (pcmk__str_eq(task, PCMK_ACTION_NOTIFY, pcmk__str_none)) {
const char *n_type = crm_meta_value(op->params, "notify_type");
const char *n_task = crm_meta_value(op->params, "notify_operation");
CRM_LOG_ASSERT(n_type != NULL);
CRM_LOG_ASSERT(n_task != NULL);
op_id = pcmk__notify_key(op->rsc_id, n_type, n_task);
if (op->op_status != PCMK_EXEC_PENDING) {
/* Ignore notify errors.
*
* @TODO It might be better to keep the correct result here, and
* ignore it in process_graph_event().
*/
lrmd__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL);
}
/* Migration history is preserved separately, which usually matters for
* multiple nodes and is important for future cluster transitions.
*/
} else if (pcmk__str_any_of(op->op_type, PCMK_ACTION_MIGRATE_TO,
PCMK_ACTION_MIGRATE_FROM, NULL)) {
op_id = strdup(key);
} else if (did_rsc_op_fail(op, target_rc)) {
op_id = pcmk__op_key(op->rsc_id, "last_failure", 0);
if (op->interval_ms == 0) {
/* Ensure 'last' gets updated, in case PCMK_META_RECORD_PENDING is
* true
*/
op_id_additional = pcmk__op_key(op->rsc_id, "last", 0);
}
exit_reason = op->exit_reason;
} else if (op->interval_ms > 0) {
op_id = strdup(key);
} else {
op_id = pcmk__op_key(op->rsc_id, "last", 0);
}
again:
xml_op = pcmk__xe_first_child(parent, PCMK__XE_LRM_RSC_OP, PCMK_XA_ID,
op_id);
if (xml_op == NULL) {
xml_op = pcmk__xe_create(parent, PCMK__XE_LRM_RSC_OP);
}
if (op->user_data == NULL) {
crm_debug("Generating fake transition key for: " PCMK__OP_FMT
" %d from %s", op->rsc_id, op->op_type, op->interval_ms,
op->call_id, origin);
local_user_data = pcmk__transition_key(-1, op->call_id, target_rc,
FAKE_TE_ID);
op->user_data = local_user_data;
}
if (magic == NULL) {
magic = crm_strdup_printf("%d:%d;%s", op->op_status, op->rc,
(const char *) op->user_data);
}
crm_xml_add(xml_op, PCMK_XA_ID, op_id);
crm_xml_add(xml_op, PCMK__XA_OPERATION_KEY, key);
crm_xml_add(xml_op, PCMK_XA_OPERATION, task);
crm_xml_add(xml_op, PCMK_XA_CRM_DEBUG_ORIGIN, origin);
crm_xml_add(xml_op, PCMK_XA_CRM_FEATURE_SET, caller_version);
crm_xml_add(xml_op, PCMK__XA_TRANSITION_KEY, op->user_data);
crm_xml_add(xml_op, PCMK__XA_TRANSITION_MAGIC, magic);
crm_xml_add(xml_op, PCMK_XA_EXIT_REASON, pcmk__s(exit_reason, ""));
crm_xml_add(xml_op, PCMK__META_ON_NODE, node); // For context during triage
crm_xml_add_int(xml_op, PCMK__XA_CALL_ID, op->call_id);
crm_xml_add_int(xml_op, PCMK__XA_RC_CODE, op->rc);
crm_xml_add_int(xml_op, PCMK__XA_OP_STATUS, op->op_status);
crm_xml_add_ms(xml_op, PCMK_META_INTERVAL, op->interval_ms);
if ((op->t_run > 0) || (op->t_rcchange > 0) || (op->exec_time > 0)
|| (op->queue_time > 0)) {
crm_trace("Timing data (" PCMK__OP_FMT "): "
"last=%lld change=%lld exec=%u queue=%u",
op->rsc_id, op->op_type, op->interval_ms,
(long long) op->t_run, (long long) op->t_rcchange,
op->exec_time, op->queue_time);
if ((op->interval_ms > 0) && (op->t_rcchange > 0)) {
// Recurring ops may have changed rc after initial run
crm_xml_add_ll(xml_op, PCMK_XA_LAST_RC_CHANGE,
(long long) op->t_rcchange);
} else {
crm_xml_add_ll(xml_op, PCMK_XA_LAST_RC_CHANGE,
(long long) op->t_run);
}
crm_xml_add_int(xml_op, PCMK_XA_EXEC_TIME, op->exec_time);
crm_xml_add_int(xml_op, PCMK_XA_QUEUE_TIME, op->queue_time);
}
if (pcmk__str_any_of(op->op_type, PCMK_ACTION_MIGRATE_TO,
PCMK_ACTION_MIGRATE_FROM, NULL)) {
/* Record PCMK__META_MIGRATE_SOURCE and PCMK__META_MIGRATE_TARGET always
* for migrate ops.
*/
const char *name = PCMK__META_MIGRATE_SOURCE;
crm_xml_add(xml_op, name, crm_meta_value(op->params, name));
name = PCMK__META_MIGRATE_TARGET;
crm_xml_add(xml_op, name, crm_meta_value(op->params, name));
}
add_op_digest_to_xml(op, xml_op);
if (op_id_additional) {
free(op_id);
op_id = op_id_additional;
op_id_additional = NULL;
goto again;
}
if (local_user_data) {
free(local_user_data);
op->user_data = NULL;
}
free(magic);
free(op_id);
free(key);
return xml_op;
}
/*!
* \internal
* \brief Check whether an action shutdown-locks a resource to a node
*
* If the PCMK_OPT_SHUTDOWN_LOCK cluster property is set, resources will not be
* recovered on a different node if cleanly stopped, and may start only on that
* same node. This function checks whether that applies to a given action, so
* that the transition graph can be marked appropriately.
*
* \param[in] action Action to check
*
* \return true if \p action locks its resource to the action's node,
* otherwise false
*/
bool
pcmk__action_locks_rsc_to_node(const pcmk_action_t *action)
{
// Only resource actions taking place on resource's lock node are locked
if ((action == NULL) || (action->rsc == NULL)
|| !pcmk__same_node(action->node, action->rsc->priv->lock_node)) {
return false;
}
/* During shutdown, only stops are locked (otherwise, another action such as
* a demote would cause the controller to clear the lock)
*/
if (action->node->details->shutdown && (action->task != NULL)
&& (strcmp(action->task, PCMK_ACTION_STOP) != 0)) {
return false;
}
return true;
}
/* lowest to highest */
static gint
sort_action_id(gconstpointer a, gconstpointer b)
{
const pcmk__related_action_t *action_wrapper2 = a;
const pcmk__related_action_t *action_wrapper1 = b;
if (a == NULL) {
return 1;
}
if (b == NULL) {
return -1;
}
if (action_wrapper1->action->id < action_wrapper2->action->id) {
return 1;
}
if (action_wrapper1->action->id > action_wrapper2->action->id) {
return -1;
}
return 0;
}
/*!
* \internal
* \brief Remove any duplicate action inputs, merging action flags
*
* \param[in,out] action Action whose inputs should be checked
*/
void
pcmk__deduplicate_action_inputs(pcmk_action_t *action)
{
GList *item = NULL;
GList *next = NULL;
pcmk__related_action_t *last_input = NULL;
action->actions_before = g_list_sort(action->actions_before,
sort_action_id);
for (item = action->actions_before; item != NULL; item = next) {
pcmk__related_action_t *input = item->data;
next = item->next;
if ((last_input != NULL)
&& (input->action->id == last_input->action->id)) {
crm_trace("Input %s (%d) duplicate skipped for action %s (%d)",
input->action->uuid, input->action->id,
action->uuid, action->id);
/* For the purposes of scheduling, the ordering flags no longer
* matter, but crm_simulate looks at certain ones when creating a
* dot graph. Combining the flags is sufficient for that purpose.
*/
pcmk__set_relation_flags(last_input->flags, input->flags);
if (input->graphed) {
last_input->graphed = true;
}
free(item->data);
action->actions_before = g_list_delete_link(action->actions_before,
item);
} else {
last_input = input;
input->graphed = false;
}
}
}
/*!
* \internal
* \brief Output all scheduled actions
*
* \param[in,out] scheduler Scheduler data
*/
void
pcmk__output_actions(pcmk_scheduler_t *scheduler)
{
pcmk__output_t *out = scheduler->priv->out;
// Output node (non-resource) actions
for (GList *iter = scheduler->priv->actions;
iter != NULL; iter = iter->next) {
char *node_name = NULL;
char *task = NULL;
pcmk_action_t *action = (pcmk_action_t *) iter->data;
if (action->rsc != NULL) {
continue; // Resource actions will be output later
} else if (pcmk_is_set(action->flags, pcmk__action_optional)) {
continue; // This action was not scheduled
}
if (pcmk__str_eq(action->task, PCMK_ACTION_DO_SHUTDOWN,
pcmk__str_none)) {
task = strdup("Shutdown");
} else if (pcmk__str_eq(action->task, PCMK_ACTION_STONITH,
pcmk__str_none)) {
const char *op = g_hash_table_lookup(action->meta,
PCMK__META_STONITH_ACTION);
task = crm_strdup_printf("Fence (%s)", op);
} else {
continue; // Don't display other node action types
}
if (pcmk__is_guest_or_bundle_node(action->node)) {
const pcmk_resource_t *remote = action->node->priv->remote;
node_name = crm_strdup_printf("%s (resource: %s)",
pcmk__node_name(action->node),
remote->priv->launcher->id);
} else if (action->node != NULL) {
node_name = crm_strdup_printf("%s", pcmk__node_name(action->node));
}
out->message(out, "node-action", task, node_name, action->reason);
free(node_name);
free(task);
}
// Output resource actions
for (GList *iter = scheduler->priv->resources;
iter != NULL; iter = iter->next) {
pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
rsc->priv->cmds->output_actions(rsc);
}
}
/*!
* \internal
* \brief Get action name needed to compare digest for configuration changes
*
* \param[in] task Action name from history
* \param[in] interval_ms Action interval (in milliseconds)
*
* \return Action name whose digest should be compared
*/
static const char *
task_for_digest(const char *task, guint interval_ms)
{
/* Certain actions need to be compared against the parameters used to start
* the resource.
*/
if ((interval_ms == 0)
&& pcmk__str_any_of(task, PCMK_ACTION_MONITOR, PCMK_ACTION_MIGRATE_FROM,
PCMK_ACTION_PROMOTE, NULL)) {
task = PCMK_ACTION_START;
}
return task;
}
/*!
* \internal
* \brief Check whether only sanitized parameters to an action changed
*
* When collecting CIB files for troubleshooting, crm_report will mask
* sensitive resource parameters. If simulations were run using that, affected
* resources would appear to need a restart, which would complicate
* troubleshooting. To avoid that, we save a "secure digest" of non-sensitive
* parameters. This function used that digest to check whether only masked
* parameters are different.
*
* \param[in] xml_op Resource history entry with secure digest
* \param[in] digest_data Operation digest information being compared
* \param[in] scheduler Scheduler data
*
* \return true if only sanitized parameters changed, otherwise false
*/
static bool
only_sanitized_changed(const xmlNode *xml_op,
const pcmk__op_digest_t *digest_data,
const pcmk_scheduler_t *scheduler)
{
const char *digest_secure = NULL;
if (!pcmk_is_set(scheduler->flags, pcmk__sched_sanitized)) {
// The scheduler is not being run as a simulation
return false;
}
digest_secure = crm_element_value(xml_op, PCMK__XA_OP_SECURE_DIGEST);
return (digest_data->rc != pcmk__digest_match) && (digest_secure != NULL)
&& (digest_data->digest_secure_calc != NULL)
&& (strcmp(digest_data->digest_secure_calc, digest_secure) == 0);
}
/*!
* \internal
* \brief Force a restart due to a configuration change
*
* \param[in,out] rsc Resource that action is for
* \param[in] task Name of action whose configuration changed
* \param[in] interval_ms Action interval (in milliseconds)
* \param[in,out] node Node where resource should be restarted
*/
static void
force_restart(pcmk_resource_t *rsc, const char *task, guint interval_ms,
pcmk_node_t *node)
{
char *key = pcmk__op_key(rsc->id, task, interval_ms);
pcmk_action_t *required = custom_action(rsc, key, task, NULL, FALSE,
rsc->priv->scheduler);
pe_action_set_reason(required, "resource definition change", true);
trigger_unfencing(rsc, node, "Device parameters changed", NULL,
rsc->priv->scheduler);
}
/*!
* \internal
* \brief Schedule a reload of a resource on a node
*
* \param[in,out] data Resource to reload
* \param[in] user_data Where resource should be reloaded
*/
static void
schedule_reload(gpointer data, gpointer user_data)
{
pcmk_resource_t *rsc = data;
const pcmk_node_t *node = user_data;
pcmk_action_t *reload = NULL;
// For collective resources, just call recursively for children
if (rsc->priv->variant > pcmk__rsc_variant_primitive) {
g_list_foreach(rsc->priv->children, schedule_reload, user_data);
return;
}
// Skip the reload in certain situations
if ((node == NULL)
|| !pcmk_is_set(rsc->flags, pcmk__rsc_managed)
|| pcmk_is_set(rsc->flags, pcmk__rsc_failed)) {
pcmk__rsc_trace(rsc, "Skip reload of %s:%s%s %s",
rsc->id,
pcmk_is_set(rsc->flags, pcmk__rsc_managed)? "" : " unmanaged",
pcmk_is_set(rsc->flags, pcmk__rsc_failed)? " failed" : "",
(node == NULL)? "inactive" : node->priv->name);
return;
}
/* If a resource's configuration changed while a start was pending,
* force a full restart instead of a reload.
*/
if (pcmk_is_set(rsc->flags, pcmk__rsc_start_pending)) {
pcmk__rsc_trace(rsc,
"%s: preventing agent reload because start pending",
rsc->id);
custom_action(rsc, stop_key(rsc), PCMK_ACTION_STOP, node, FALSE,
rsc->priv->scheduler);
return;
}
// Schedule the reload
pcmk__set_rsc_flags(rsc, pcmk__rsc_reload);
reload = custom_action(rsc, reload_key(rsc), PCMK_ACTION_RELOAD_AGENT, node,
FALSE, rsc->priv->scheduler);
pe_action_set_reason(reload, "resource definition change", FALSE);
// Set orderings so that a required stop or demote cancels the reload
pcmk__new_ordering(NULL, NULL, reload, rsc, stop_key(rsc), NULL,
pcmk__ar_ordered|pcmk__ar_then_cancels_first,
rsc->priv->scheduler);
pcmk__new_ordering(NULL, NULL, reload, rsc, demote_key(rsc), NULL,
pcmk__ar_ordered|pcmk__ar_then_cancels_first,
rsc->priv->scheduler);
}
/*!
* \internal
* \brief Handle any configuration change for an action
*
* Given an action from resource history, if the resource's configuration
* changed since the action was done, schedule any actions needed (restart,
* reload, unfencing, rescheduling recurring actions, etc.).
*
* \param[in,out] rsc Resource that action is for
* \param[in,out] node Node that action was on
* \param[in] xml_op Action XML from resource history
*
* \return true if action configuration changed, otherwise false
*/
bool
pcmk__check_action_config(pcmk_resource_t *rsc, pcmk_node_t *node,
const xmlNode *xml_op)
{
guint interval_ms = 0;
const char *task = NULL;
const pcmk__op_digest_t *digest_data = NULL;
CRM_CHECK((rsc != NULL) && (node != NULL) && (xml_op != NULL),
return false);
task = crm_element_value(xml_op, PCMK_XA_OPERATION);
CRM_CHECK(task != NULL, return false);
crm_element_value_ms(xml_op, PCMK_META_INTERVAL, &interval_ms);
// If this is a recurring action, check whether it has been orphaned
if (interval_ms > 0) {
if (pcmk__find_action_config(rsc, task, interval_ms, false) != NULL) {
pcmk__rsc_trace(rsc,
"%s-interval %s for %s on %s is in configuration",
pcmk__readable_interval(interval_ms), task, rsc->id,
pcmk__node_name(node));
} else if (pcmk_is_set(rsc->priv->scheduler->flags,
pcmk__sched_cancel_removed_actions)) {
pcmk__schedule_cancel(rsc,
crm_element_value(xml_op, PCMK__XA_CALL_ID),
task, interval_ms, node, "orphan");
return true;
} else {
pcmk__rsc_debug(rsc, "%s-interval %s for %s on %s is orphaned",
pcmk__readable_interval(interval_ms), task, rsc->id,
pcmk__node_name(node));
return true;
}
}
crm_trace("Checking %s-interval %s for %s on %s for configuration changes",
pcmk__readable_interval(interval_ms), task, rsc->id,
pcmk__node_name(node));
task = task_for_digest(task, interval_ms);
digest_data = rsc_action_digest_cmp(rsc, xml_op, node,
rsc->priv->scheduler);
if (only_sanitized_changed(xml_op, digest_data, rsc->priv->scheduler)) {
if (!pcmk__is_daemon && (rsc->priv->scheduler->priv->out != NULL)) {
pcmk__output_t *out = rsc->priv->scheduler->priv->out;
out->info(out,
"Only 'private' parameters to %s-interval %s for %s "
"on %s changed: %s",
pcmk__readable_interval(interval_ms), task, rsc->id,
pcmk__node_name(node),
crm_element_value(xml_op, PCMK__XA_TRANSITION_MAGIC));
}
return false;
}
switch (digest_data->rc) {
case pcmk__digest_restart:
crm_log_xml_debug(digest_data->params_restart, "params:restart");
force_restart(rsc, task, interval_ms, node);
return true;
case pcmk__digest_unknown:
case pcmk__digest_mismatch:
// Changes that can potentially be handled by an agent reload
if (interval_ms > 0) {
/* Recurring actions aren't reloaded per se, they are just
* re-scheduled so the next run uses the new parameters.
* The old instance will be cancelled automatically.
*/
crm_log_xml_debug(digest_data->params_all, "params:reschedule");
pcmk__reschedule_recurring(rsc, task, interval_ms, node);
} else if (crm_element_value(xml_op,
PCMK__XA_OP_RESTART_DIGEST) != NULL) {
// Agent supports reload, so use it
trigger_unfencing(rsc, node,
"Device parameters changed (reload)", NULL,
rsc->priv->scheduler);
crm_log_xml_debug(digest_data->params_all, "params:reload");
schedule_reload((gpointer) rsc, (gpointer) node);
} else {
pcmk__rsc_trace(rsc,
"Restarting %s "
"because agent doesn't support reload",
rsc->id);
crm_log_xml_debug(digest_data->params_restart,
"params:restart");
force_restart(rsc, task, interval_ms, node);
}
return true;
default:
break;
}
return false;
}
/*!
* \internal
* \brief Create a list of resource's action history entries, sorted by call ID
*
* \param[in] rsc_entry Resource's \c PCMK__XE_LRM_RSC_OP status XML
* \param[out] start_index Where to store index of start-like action, if any
* \param[out] stop_index Where to store index of stop action, if any
*/
static GList *
rsc_history_as_list(const xmlNode *rsc_entry, int *start_index, int *stop_index)
{
GList *ops = NULL;
for (xmlNode *rsc_op = pcmk__xe_first_child(rsc_entry, PCMK__XE_LRM_RSC_OP,
NULL, NULL);
rsc_op != NULL; rsc_op = pcmk__xe_next(rsc_op, PCMK__XE_LRM_RSC_OP)) {
ops = g_list_prepend(ops, rsc_op);
}
ops = g_list_sort(ops, sort_op_by_callid);
calculate_active_ops(ops, start_index, stop_index);
return ops;
}
/*!
* \internal
* \brief Process a resource's action history from the CIB status
*
* Given a resource's action history, if the resource's configuration
* changed since the actions were done, schedule any actions needed (restart,
* reload, unfencing, rescheduling recurring actions, clean-up, etc.).
* (This also cancels recurring actions for maintenance mode, which is not
* entirely related but convenient to do here.)
*
* \param[in] rsc_entry Resource's \c PCMK__XE_LRM_RSC_OP status XML
* \param[in,out] rsc Resource whose history is being processed
* \param[in,out] node Node whose history is being processed
*/
static void
process_rsc_history(const xmlNode *rsc_entry, pcmk_resource_t *rsc,
pcmk_node_t *node)
{
int offset = -1;
int stop_index = 0;
int start_index = 0;
GList *sorted_op_list = NULL;
if (pcmk_is_set(rsc->flags, pcmk__rsc_removed)) {
if (pcmk__is_anonymous_clone(pe__const_top_resource(rsc, false))) {
/* @TODO Should this be done for bundled primitives as well? Added
* by 2ac43ae31
*/
pcmk__rsc_trace(rsc,
"Skipping configuration check "
"for orphaned clone instance %s",
rsc->id);
} else {
pcmk__rsc_trace(rsc,
"Skipping configuration check and scheduling "
"clean-up for orphaned resource %s", rsc->id);
pcmk__schedule_cleanup(rsc, node, false);
}
return;
}
if (pe_find_node_id(rsc->priv->active_nodes,
node->priv->id) == NULL) {
if (pcmk__rsc_agent_changed(rsc, node, rsc_entry, false)) {
pcmk__schedule_cleanup(rsc, node, false);
}
pcmk__rsc_trace(rsc,
"Skipping configuration check for %s "
"because no longer active on %s",
rsc->id, pcmk__node_name(node));
return;
}
pcmk__rsc_trace(rsc, "Checking for configuration changes for %s on %s",
rsc->id, pcmk__node_name(node));
if (pcmk__rsc_agent_changed(rsc, node, rsc_entry, true)) {
pcmk__schedule_cleanup(rsc, node, false);
}
sorted_op_list = rsc_history_as_list(rsc_entry, &start_index, &stop_index);
if (start_index < stop_index) {
return; // Resource is stopped
}
for (GList *iter = sorted_op_list; iter != NULL; iter = iter->next) {
xmlNode *rsc_op = (xmlNode *) iter->data;
const char *task = NULL;
guint interval_ms = 0;
if (++offset < start_index) {
// Skip actions that happened before a start
continue;
}
task = crm_element_value(rsc_op, PCMK_XA_OPERATION);
crm_element_value_ms(rsc_op, PCMK_META_INTERVAL, &interval_ms);
if ((interval_ms > 0)
&& (pcmk_is_set(rsc->flags, pcmk__rsc_maintenance)
|| node->details->maintenance)) {
// Maintenance mode cancels recurring operations
pcmk__schedule_cancel(rsc,
crm_element_value(rsc_op, PCMK__XA_CALL_ID),
task, interval_ms, node, "maintenance mode");
} else if ((interval_ms > 0)
|| pcmk__strcase_any_of(task, PCMK_ACTION_MONITOR,
PCMK_ACTION_START,
PCMK_ACTION_PROMOTE,
PCMK_ACTION_MIGRATE_FROM, NULL)) {
/* If a resource operation failed, and the operation's definition
* has changed, clear any fail count so they can be retried fresh.
*/
if (pe__bundle_needs_remote_name(rsc)) {
/* We haven't assigned resources to nodes yet, so if the
* REMOTE_CONTAINER_HACK is used, we may calculate the digest
* based on the literal "#uname" value rather than the properly
* substituted value. That would mistakenly make the action
* definition appear to have been changed. Defer the check until
* later in this case.
*/
pcmk__add_param_check(rsc_op, rsc, node, pcmk__check_active);
} else if (pcmk__check_action_config(rsc, node, rsc_op)
&& (pe_get_failcount(node, rsc, NULL, pcmk__fc_effective,
NULL) != 0)) {
pe__clear_failcount(rsc, node, "action definition changed",
rsc->priv->scheduler);
}
}
}
g_list_free(sorted_op_list);
}
/*!
* \internal
* \brief Process a node's action history from the CIB status
*
* Given a node's resource history, if the resource's configuration changed
* since the actions were done, schedule any actions needed (restart,
* reload, unfencing, rescheduling recurring actions, clean-up, etc.).
* (This also cancels recurring actions for maintenance mode, which is not
* entirely related but convenient to do here.)
*
* \param[in,out] node Node whose history is being processed
* \param[in] lrm_rscs Node's \c PCMK__XE_LRM_RESOURCES from CIB status XML
*/
static void
process_node_history(pcmk_node_t *node, const xmlNode *lrm_rscs)
{
crm_trace("Processing node history for %s", pcmk__node_name(node));
for (const xmlNode *rsc_entry = pcmk__xe_first_child(lrm_rscs,
PCMK__XE_LRM_RESOURCE,
NULL, NULL);
rsc_entry != NULL;
rsc_entry = pcmk__xe_next(rsc_entry, PCMK__XE_LRM_RESOURCE)) {
if (rsc_entry->children != NULL) {
GList *result = pcmk__rscs_matching_id(pcmk__xe_id(rsc_entry),
node->priv->scheduler);
for (GList *iter = result; iter != NULL; iter = iter->next) {
pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
if (pcmk__is_primitive(rsc)) {
process_rsc_history(rsc_entry, rsc, node);
}
}
g_list_free(result);
}
}
}
// XPath to find a node's resource history
#define XPATH_NODE_HISTORY "/" PCMK_XE_CIB "/" PCMK_XE_STATUS \
"/" PCMK__XE_NODE_STATE \
"[@" PCMK_XA_UNAME "='%s']" \
"/" PCMK__XE_LRM "/" PCMK__XE_LRM_RESOURCES
/*!
* \internal
* \brief Process any resource configuration changes in the CIB status
*
* Go through all nodes' resource history, and if a resource's configuration
* changed since its actions were done, schedule any actions needed (restart,
* reload, unfencing, rescheduling recurring actions, clean-up, etc.).
* (This also cancels recurring actions for maintenance mode, which is not
* entirely related but convenient to do here.)
*
* \param[in,out] scheduler Scheduler data
*/
void
pcmk__handle_rsc_config_changes(pcmk_scheduler_t *scheduler)
{
crm_trace("Check resource and action configuration for changes");
/* Rather than iterate through the status section, iterate through the nodes
* and search for the appropriate status subsection for each. This skips
* orphaned nodes and lets us eliminate some cases before searching the XML.
*/
for (GList *iter = scheduler->nodes; iter != NULL; iter = iter->next) {
pcmk_node_t *node = (pcmk_node_t *) iter->data;
/* Don't bother checking actions for a node that can't run actions ...
* unless it's in maintenance mode, in which case we still need to
* cancel any existing recurring monitors.
*/
if (node->details->maintenance
|| pcmk__node_available(node, false, false)) {
char *xpath = NULL;
xmlNode *history = NULL;
xpath = crm_strdup_printf(XPATH_NODE_HISTORY, node->priv->name);
history = pcmk__xpath_find_one(scheduler->input->doc, xpath,
LOG_NEVER);
free(xpath);
process_node_history(node, history);
}
}
}
diff --git a/lib/pengine/pe_digest.c b/lib/pengine/pe_digest.c
index a4c603831c..b61b44b689 100644
--- a/lib/pengine/pe_digest.c
+++ b/lib/pengine/pe_digest.c
@@ -1,614 +1,614 @@
/*
* Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <glib.h>
#include <stdbool.h>
#include <crm/crm.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
#include <crm/pengine/internal.h>
#include "pe_status_private.h"
extern bool pcmk__is_daemon;
/*!
* \internal
* \brief Free an operation digest cache entry
*
* \param[in,out] ptr Pointer to cache entry to free
*
* \note The argument is a gpointer so this can be used as a hash table
* free function.
*/
void
pe__free_digests(gpointer ptr)
{
pcmk__op_digest_t *data = ptr;
if (data != NULL) {
pcmk__xml_free(data->params_all);
pcmk__xml_free(data->params_secure);
pcmk__xml_free(data->params_restart);
free(data->digest_all_calc);
free(data->digest_restart_calc);
free(data->digest_secure_calc);
free(data);
}
}
// Return true if XML attribute name is not substring of a given string
static bool
attr_not_in_string(xmlAttrPtr a, void *user_data)
{
bool filter = false;
char *name = crm_strdup_printf(" %s ", (const char *) a->name);
if (strstr((const char *) user_data, name) == NULL) {
crm_trace("Filtering %s (not found in '%s')",
(const char *) a->name, (const char *) user_data);
filter = true;
}
free(name);
return filter;
}
// Return true if XML attribute name is substring of a given string
static bool
attr_in_string(xmlAttrPtr a, void *user_data)
{
bool filter = false;
char *name = crm_strdup_printf(" %s ", (const char *) a->name);
if (strstr((const char *) user_data, name) != NULL) {
crm_trace("Filtering %s (found in '%s')",
(const char *) a->name, (const char *) user_data);
filter = true;
}
free(name);
return filter;
}
/*!
* \internal
* \brief Add digest of all parameters to a digest cache entry
*
* \param[out] data Digest cache entry to modify
* \param[in,out] rsc Resource that action was for
* \param[in] node Node action was performed on
* \param[in] params Resource parameters evaluated for node
* \param[in] task Name of action performed
* \param[in,out] interval_ms Action's interval (will be reset if in overrides)
* \param[in] xml_op Unused
* \param[in] op_version CRM feature set to use for digest calculation
* \param[in] overrides Key/value table to override resource parameters
* \param[in,out] scheduler Scheduler data
*/
static void
calculate_main_digest(pcmk__op_digest_t *data, pcmk_resource_t *rsc,
const pcmk_node_t *node, GHashTable *params,
const char *task, guint *interval_ms,
const xmlNode *xml_op, const char *op_version,
GHashTable *overrides, pcmk_scheduler_t *scheduler)
{
xmlNode *action_config = NULL;
data->params_all = pcmk__xe_create(NULL, PCMK_XE_PARAMETERS);
/* REMOTE_CONTAINER_HACK: Allow Pacemaker Remote nodes to run containers
* that themselves are Pacemaker Remote nodes
*/
(void) pe__add_bundle_remote_name(rsc, data->params_all,
PCMK_REMOTE_RA_ADDR);
if (overrides != NULL) {
// If interval was overridden, reset it
const char *meta_name = CRM_META "_" PCMK_META_INTERVAL;
const char *interval_s = g_hash_table_lookup(overrides, meta_name);
if (interval_s != NULL) {
long long value_ll;
if ((pcmk__scan_ll(interval_s, &value_ll, 0LL) == pcmk_rc_ok)
&& (value_ll >= 0) && (value_ll <= G_MAXUINT)) {
*interval_ms = (guint) value_ll;
}
}
// Add overrides to list of all parameters
g_hash_table_foreach(overrides, hash2field, data->params_all);
}
// Add provided instance parameters
g_hash_table_foreach(params, hash2field, data->params_all);
// Find action configuration XML in CIB
action_config = pcmk__find_action_config(rsc, task, *interval_ms, true);
/* Add action-specific resource instance attributes to the digest list.
*
* If this is a one-time action with action-specific instance attributes,
* enforce a restart instead of reload-agent in case the main digest doesn't
* match, even if the restart digest does. This ensures any changes of the
* action-specific parameters get applied for this specific action, and
* digests calculated for the resulting history will be correct. Default the
* result to RSC_DIGEST_RESTART for the case where the main digest doesn't
* match.
*/
params = pcmk__unpack_action_rsc_params(action_config, node->priv->attrs,
scheduler);
if ((*interval_ms == 0) && (g_hash_table_size(params) > 0)) {
data->rc = pcmk__digest_restart;
}
g_hash_table_foreach(params, hash2field, data->params_all);
g_hash_table_destroy(params);
// Add action meta-attributes
params = pcmk__unpack_action_meta(rsc, node, task, *interval_ms,
action_config);
g_hash_table_foreach(params, hash2metafield, data->params_all);
g_hash_table_destroy(params);
pcmk__filter_op_for_digest(data->params_all);
- data->digest_all_calc = pcmk__digest_operation(data->params_all);
+ data->digest_all_calc = pcmk__digest_op_params(data->params_all);
}
// Return true if XML attribute name is a Pacemaker-defined fencing parameter
static bool
is_fence_param(xmlAttrPtr attr, void *user_data)
{
return pcmk_stonith_param((const char *) attr->name);
}
/*!
* \internal
* \brief Add secure digest to a digest cache entry
*
* \param[out] data Digest cache entry to modify
* \param[in] rsc Resource that action was for
* \param[in] params Resource parameters evaluated for node
* \param[in] xml_op XML of operation in CIB status (if available)
* \param[in] op_version CRM feature set to use for digest calculation
* \param[in] overrides Key/value hash table to override resource parameters
*/
static void
calculate_secure_digest(pcmk__op_digest_t *data, const pcmk_resource_t *rsc,
GHashTable *params, const xmlNode *xml_op,
const char *op_version, GHashTable *overrides)
{
const char *class = crm_element_value(rsc->priv->xml, PCMK_XA_CLASS);
const char *secure_list = NULL;
bool old_version = (compare_version(op_version, "3.16.0") < 0);
if (xml_op == NULL) {
secure_list = " passwd password user ";
} else {
secure_list = crm_element_value(xml_op, PCMK__XA_OP_SECURE_PARAMS);
}
if (old_version) {
data->params_secure = pcmk__xe_create(NULL, PCMK_XE_PARAMETERS);
if (overrides != NULL) {
g_hash_table_foreach(overrides, hash2field, data->params_secure);
}
g_hash_table_foreach(params, hash2field, data->params_secure);
} else {
// Start with a copy of all parameters
data->params_secure = pcmk__xml_copy(NULL, data->params_all);
}
if (secure_list != NULL) {
pcmk__xe_remove_matching_attrs(data->params_secure, false,
attr_in_string, (void *) secure_list);
}
if (old_version
&& pcmk_is_set(pcmk_get_ra_caps(class),
pcmk_ra_cap_fence_params)) {
/* For stonith resources, Pacemaker adds special parameters,
* but these are not listed in fence agent meta-data, so with older
* versions of DC, the controller will not hash them. That means we have
* to filter them out before calculating our hash for comparison.
*/
pcmk__xe_remove_matching_attrs(data->params_secure, false,
is_fence_param, NULL);
}
pcmk__filter_op_for_digest(data->params_secure);
/* CRM_meta_timeout *should* be part of a digest for recurring operations.
* However, with older versions of DC, the controller does not add timeout
* to secure digests, because it only includes parameters declared by the
* resource agent.
* Remove any timeout that made it this far, to match.
*/
if (old_version) {
pcmk__xe_remove_attr(data->params_secure,
CRM_META "_" PCMK_META_TIMEOUT);
}
- data->digest_secure_calc = pcmk__digest_operation(data->params_secure);
+ data->digest_secure_calc = pcmk__digest_op_params(data->params_secure);
}
/*!
* \internal
* \brief Add restart digest to a digest cache entry
*
* \param[out] data Digest cache entry to modify
* \param[in] xml_op XML of operation in CIB status (if available)
* \param[in] op_version CRM feature set to use for digest calculation
*
* \note This function doesn't need to handle overrides because it starts with
* data->params_all, which already has overrides applied.
*/
static void
calculate_restart_digest(pcmk__op_digest_t *data, const xmlNode *xml_op,
const char *op_version)
{
const char *value = NULL;
// We must have XML of resource operation history
if (xml_op == NULL) {
return;
}
// And the history must have a restart digest to compare against
if (crm_element_value(xml_op, PCMK__XA_OP_RESTART_DIGEST) == NULL) {
return;
}
// Start with a copy of all parameters
data->params_restart = pcmk__xml_copy(NULL, data->params_all);
// Then filter out reloadable parameters, if any
value = crm_element_value(xml_op, PCMK__XA_OP_FORCE_RESTART);
if (value != NULL) {
pcmk__xe_remove_matching_attrs(data->params_restart, false,
attr_not_in_string, (void *) value);
}
- data->digest_restart_calc = pcmk__digest_operation(data->params_restart);
+ data->digest_restart_calc = pcmk__digest_op_params(data->params_restart);
}
/*!
* \internal
* \brief Create a new digest cache entry with calculated digests
*
* \param[in,out] rsc Resource that action was for
* \param[in] task Name of action performed
* \param[in,out] interval_ms Action's interval (will be reset if in overrides)
* \param[in] node Node action was performed on
* \param[in] xml_op XML of operation in CIB status (if available)
* \param[in] overrides Key/value table to override resource parameters
* \param[in] calc_secure Whether to calculate secure digest
* \param[in,out] scheduler Scheduler data
*
* \return Pointer to new digest cache entry (or NULL on memory error)
* \note It is the caller's responsibility to free the result using
* pe__free_digests().
*/
pcmk__op_digest_t *
pe__calculate_digests(pcmk_resource_t *rsc, const char *task,
guint *interval_ms, const pcmk_node_t *node,
const xmlNode *xml_op, GHashTable *overrides,
bool calc_secure, pcmk_scheduler_t *scheduler)
{
pcmk__op_digest_t *data = NULL;
const char *op_version = NULL;
GHashTable *params = NULL;
CRM_CHECK(scheduler != NULL, return NULL);
data = calloc(1, sizeof(pcmk__op_digest_t));
if (data == NULL) {
pcmk__sched_err(scheduler,
"Could not allocate memory for operation digest");
return NULL;
}
data->rc = pcmk__digest_match;
if (xml_op != NULL) {
op_version = crm_element_value(xml_op, PCMK_XA_CRM_FEATURE_SET);
}
if ((op_version == NULL) && (scheduler->input != NULL)) {
op_version = crm_element_value(scheduler->input,
PCMK_XA_CRM_FEATURE_SET);
}
if (op_version == NULL) {
op_version = CRM_FEATURE_SET;
}
params = pe_rsc_params(rsc, node, scheduler);
calculate_main_digest(data, rsc, node, params, task, interval_ms, xml_op,
op_version, overrides, scheduler);
if (calc_secure) {
calculate_secure_digest(data, rsc, params, xml_op, op_version,
overrides);
}
calculate_restart_digest(data, xml_op, op_version);
return data;
}
/*!
* \internal
* \brief Calculate action digests and store in node's digest cache
*
* \param[in,out] rsc Resource that action was for
* \param[in] task Name of action performed
* \param[in] interval_ms Action's interval
* \param[in,out] node Node action was performed on
* \param[in] xml_op XML of operation in CIB status (if available)
* \param[in] calc_secure Whether to calculate secure digest
* \param[in,out] scheduler Scheduler data
*
* \return Pointer to node's digest cache entry
*/
static pcmk__op_digest_t *
rsc_action_digest(pcmk_resource_t *rsc, const char *task, guint interval_ms,
pcmk_node_t *node, const xmlNode *xml_op,
bool calc_secure, pcmk_scheduler_t *scheduler)
{
pcmk__op_digest_t *data = NULL;
char *key = pcmk__op_key(rsc->id, task, interval_ms);
data = g_hash_table_lookup(node->priv->digest_cache, key);
if (data == NULL) {
data = pe__calculate_digests(rsc, task, &interval_ms, node, xml_op,
NULL, calc_secure, scheduler);
pcmk__assert(data != NULL);
g_hash_table_insert(node->priv->digest_cache, strdup(key), data);
}
free(key);
return data;
}
/*!
* \internal
* \brief Calculate operation digests and compare against an XML history entry
*
* \param[in,out] rsc Resource to check
* \param[in] xml_op Resource history XML
* \param[in,out] node Node to use for digest calculation
* \param[in,out] scheduler Scheduler data
*
* \return Pointer to node's digest cache entry, with comparison result set
*/
pcmk__op_digest_t *
rsc_action_digest_cmp(pcmk_resource_t *rsc, const xmlNode *xml_op,
pcmk_node_t *node, pcmk_scheduler_t *scheduler)
{
pcmk__op_digest_t *data = NULL;
guint interval_ms = 0;
const char *op_version;
const char *task = crm_element_value(xml_op, PCMK_XA_OPERATION);
const char *digest_all;
const char *digest_restart;
pcmk__assert(node != NULL);
op_version = crm_element_value(xml_op, PCMK_XA_CRM_FEATURE_SET);
digest_all = crm_element_value(xml_op, PCMK__XA_OP_DIGEST);
digest_restart = crm_element_value(xml_op, PCMK__XA_OP_RESTART_DIGEST);
crm_element_value_ms(xml_op, PCMK_META_INTERVAL, &interval_ms);
data = rsc_action_digest(rsc, task, interval_ms, node, xml_op,
pcmk_is_set(scheduler->flags,
pcmk__sched_sanitized),
scheduler);
if (!pcmk__str_eq(data->digest_restart_calc, digest_restart,
pcmk__str_null_matches)) {
pcmk__rsc_info(rsc,
"Parameter change for %s-interval %s of %s on %s "
"requires restart (hash now %s vs. %s "
"with op feature set %s for transition %s)",
pcmk__readable_interval(interval_ms), task, rsc->id,
pcmk__node_name(node), data->digest_restart_calc,
pcmk__s(digest_restart, "missing"), op_version,
crm_element_value(xml_op, PCMK__XA_TRANSITION_MAGIC));
data->rc = pcmk__digest_restart;
} else if (digest_all == NULL) {
/* it is unknown what the previous op digest was */
data->rc = pcmk__digest_unknown;
} else if (strcmp(digest_all, data->digest_all_calc) != 0) {
/* Given a non-recurring operation with extra parameters configured,
* in case that the main digest doesn't match, even if the restart
* digest matches, enforce a restart rather than a reload-agent anyway.
* So that it ensures any changes of the extra parameters get applied
* for this specific operation, and the digests calculated for the
* resulting PCMK__XE_LRM_RSC_OP will be correct.
* Preserve the implied rc pcmk__digest_restart for the case that the
* main digest doesn't match.
*/
if ((interval_ms == 0) && (data->rc == pcmk__digest_restart)) {
pcmk__rsc_info(rsc,
"Parameters containing extra ones to %ums-interval"
" %s action for %s on %s "
"changed: hash was %s vs. now %s (restart:%s) %s",
interval_ms, task, rsc->id, pcmk__node_name(node),
pcmk__s(digest_all, "missing"),
data->digest_all_calc, op_version,
crm_element_value(xml_op,
PCMK__XA_TRANSITION_MAGIC));
} else {
pcmk__rsc_info(rsc,
"Parameters to %ums-interval %s action for %s on %s "
"changed: hash was %s vs. now %s (%s:%s) %s",
interval_ms, task, rsc->id, pcmk__node_name(node),
pcmk__s(digest_all, "missing"),
data->digest_all_calc,
(interval_ms > 0)? "reschedule" : "reload",
op_version,
crm_element_value(xml_op,
PCMK__XA_TRANSITION_MAGIC));
data->rc = pcmk__digest_mismatch;
}
} else {
data->rc = pcmk__digest_match;
}
return data;
}
/*!
* \internal
* \brief Create an unfencing summary for use in special node attribute
*
* Create a string combining a fence device's resource ID, agent type, and
* parameter digest (whether for all parameters or just non-private parameters).
* This can be stored in a special node attribute, allowing us to detect changes
* in either the agent type or parameters, to know whether unfencing must be
* redone or can be safely skipped when the device's history is cleaned.
*
* \param[in] rsc_id Fence device resource ID
* \param[in] agent_type Fence device agent
* \param[in] param_digest Fence device parameter digest
*
* \return Newly allocated string with unfencing digest
* \note The caller is responsible for freeing the result.
*/
static inline char *
create_unfencing_summary(const char *rsc_id, const char *agent_type,
const char *param_digest)
{
return crm_strdup_printf("%s:%s:%s", rsc_id, agent_type, param_digest);
}
/*!
* \internal
* \brief Check whether a node can skip unfencing
*
* Check whether a fence device's current definition matches a node's
* stored summary of when it was last unfenced by the device.
*
* \param[in] rsc_id Fence device's resource ID
* \param[in] agent Fence device's agent type
* \param[in] digest_calc Fence device's current parameter digest
* \param[in] node_summary Value of node's special unfencing node attribute
* (a comma-separated list of unfencing summaries for
* all devices that have unfenced this node)
*
* \return TRUE if digest matches, FALSE otherwise
*/
static bool
unfencing_digest_matches(const char *rsc_id, const char *agent,
const char *digest_calc, const char *node_summary)
{
bool matches = FALSE;
if (rsc_id && agent && digest_calc && node_summary) {
char *search_secure = create_unfencing_summary(rsc_id, agent,
digest_calc);
/* The digest was calculated including the device ID and agent,
* so there is no risk of collision using strstr().
*/
matches = (strstr(node_summary, search_secure) != NULL);
crm_trace("Calculated unfencing digest '%s' %sfound in '%s'",
search_secure, matches? "" : "not ", node_summary);
free(search_secure);
}
return matches;
}
/* Magic string to use as action name for digest cache entries used for
* unfencing checks. This is not a real action name (i.e. "on"), so
* pcmk__check_action_config() won't confuse these entries with real actions.
*/
#define STONITH_DIGEST_TASK "stonith-on"
/*!
* \internal
* \brief Calculate fence device digests and digest comparison result
*
* \param[in,out] rsc Fence device resource
* \param[in] agent Fence device's agent type
* \param[in,out] node Node with digest cache to use
* \param[in,out] scheduler Scheduler data
*
* \return Node's digest cache entry
*/
pcmk__op_digest_t *
pe__compare_fencing_digest(pcmk_resource_t *rsc, const char *agent,
pcmk_node_t *node, pcmk_scheduler_t *scheduler)
{
const char *node_summary = NULL;
// Calculate device's current parameter digests
pcmk__op_digest_t *data = rsc_action_digest(rsc, STONITH_DIGEST_TASK, 0U,
node, NULL, TRUE, scheduler);
// Check whether node has special unfencing summary node attribute
node_summary = pcmk__node_attr(node, CRM_ATTR_DIGESTS_ALL, NULL,
pcmk__rsc_node_current);
if (node_summary == NULL) {
data->rc = pcmk__digest_unknown;
return data;
}
// Check whether full parameter digest matches
if (unfencing_digest_matches(rsc->id, agent, data->digest_all_calc,
node_summary)) {
data->rc = pcmk__digest_match;
return data;
}
// Check whether secure parameter digest matches
node_summary = pcmk__node_attr(node, CRM_ATTR_DIGESTS_SECURE, NULL,
pcmk__rsc_node_current);
if (unfencing_digest_matches(rsc->id, agent, data->digest_secure_calc,
node_summary)) {
data->rc = pcmk__digest_match;
if (!pcmk__is_daemon && (scheduler->priv->out != NULL)) {
pcmk__output_t *out = scheduler->priv->out;
out->info(out, "Only 'private' parameters to %s "
"for unfencing %s changed", rsc->id,
pcmk__node_name(node));
}
return data;
}
// Parameters don't match
data->rc = pcmk__digest_mismatch;
if (pcmk_is_set(scheduler->flags, pcmk__sched_sanitized)
&& (data->digest_secure_calc != NULL)) {
if (scheduler->priv->out != NULL) {
pcmk__output_t *out = scheduler->priv->out;
char *digest = create_unfencing_summary(rsc->id, agent,
data->digest_secure_calc);
out->info(out, "Parameters to %s for unfencing "
"%s changed, try '%s'", rsc->id,
pcmk__node_name(node), digest);
free(digest);
} else if (!pcmk__is_daemon) {
char *digest = create_unfencing_summary(rsc->id, agent,
data->digest_secure_calc);
printf("Parameters to %s for unfencing %s changed, try '%s'\n",
rsc->id, pcmk__node_name(node), digest);
free(digest);
}
}
return data;
}
diff --git a/tools/crm_diff.c b/tools/crm_diff.c
index 27eb6d155c..09aa272ae1 100644
--- a/tools/crm_diff.c
+++ b/tools/crm_diff.c
@@ -1,336 +1,335 @@
/*
* Copyright 2005-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/param.h>
#include <sys/types.h>
#include <crm/crm.h>
#include <crm/common/cmdline_internal.h>
#include <crm/common/output_internal.h>
#include <crm/common/xml.h>
#include <crm/common/ipc.h>
#include <crm/cib.h>
#define SUMMARY "Compare two Pacemaker configurations (in XML format) to produce a custom diff-like output, " \
"or apply such an output as a patch"
struct {
gboolean apply;
gboolean as_cib;
gboolean no_version;
gboolean raw_original;
gboolean raw_new;
gboolean use_stdin;
char *xml_file_original;
char *xml_file_new;
} options;
gboolean new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
gboolean original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
gboolean patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
static GOptionEntry original_xml_entries[] = {
{ "original", 'o', 0, G_OPTION_ARG_STRING, &options.xml_file_original,
"XML is contained in the named file",
"FILE" },
{ "original-string", 'O', 0, G_OPTION_ARG_CALLBACK, original_string_cb,
"XML is contained in the supplied string",
"STRING" },
{ NULL }
};
static GOptionEntry operation_entries[] = {
{ "new", 'n', 0, G_OPTION_ARG_STRING, &options.xml_file_new,
"Compare the original XML to the contents of the named file",
"FILE" },
{ "new-string", 'N', 0, G_OPTION_ARG_CALLBACK, new_string_cb,
"Compare the original XML with the contents of the supplied string",
"STRING" },
{ "patch", 'p', 0, G_OPTION_ARG_CALLBACK, patch_cb,
"Patch the original XML with the contents of the named file",
"FILE" },
{ NULL }
};
static GOptionEntry addl_entries[] = {
{ "cib", 'c', 0, G_OPTION_ARG_NONE, &options.as_cib,
"Compare/patch the inputs as a CIB (includes versions details)",
NULL },
{ "stdin", 's', 0, G_OPTION_ARG_NONE, &options.use_stdin,
"",
NULL },
{ "no-version", 'u', 0, G_OPTION_ARG_NONE, &options.no_version,
"Generate the difference without versions details",
NULL },
{ NULL }
};
gboolean
new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
options.raw_new = TRUE;
pcmk__str_update(&options.xml_file_new, optarg);
return TRUE;
}
gboolean
original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
options.raw_original = TRUE;
pcmk__str_update(&options.xml_file_original, optarg);
return TRUE;
}
gboolean
patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
options.apply = TRUE;
pcmk__str_update(&options.xml_file_new, optarg);
return TRUE;
}
static void
print_patch(xmlNode *patch)
{
GString *buffer = g_string_sized_new(1024);
pcmk__xml_string(patch, pcmk__xml_fmt_pretty, buffer, 0);
printf("%s", buffer->str);
g_string_free(buffer, TRUE);
fflush(stdout);
}
// \return Standard Pacemaker return code
static int
apply_patch(xmlNode *input, xmlNode *patch, gboolean as_cib)
{
xmlNode *output = pcmk__xml_copy(NULL, input);
int rc = xml_apply_patchset(output, patch, as_cib);
rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
fprintf(stderr, "Could not apply patch: %s\n", pcmk_rc_str(rc));
pcmk__xml_free(output);
return rc;
}
if (output != NULL) {
char *buffer;
print_patch(output);
buffer = pcmk__digest_xml(output, true);
crm_trace("Digest: %s", pcmk__s(buffer, "<null>\n"));
free(buffer);
pcmk__xml_free(output);
}
return pcmk_rc_ok;
}
static void
log_patch_cib_versions(xmlNode *patch)
{
int add[] = { 0, 0, 0 };
int del[] = { 0, 0, 0 };
const char *fmt = NULL;
const char *digest = NULL;
- xml_patch_versions(patch, add, del);
+ pcmk__xml_patchset_versions(patch, del, add);
fmt = crm_element_value(patch, PCMK_XA_FORMAT);
digest = crm_element_value(patch, PCMK__XA_DIGEST);
if (add[2] != del[2] || add[1] != del[1] || add[0] != del[0]) {
crm_info("Patch: --- %d.%d.%d %s", del[0], del[1], del[2], fmt);
crm_info("Patch: +++ %d.%d.%d %s", add[0], add[1], add[2], digest);
}
}
// \return Standard Pacemaker return code
static int
generate_patch(xmlNode *object_original, xmlNode *object_new, const char *xml_file_new,
gboolean as_cib, gboolean no_version)
{
const char *vfields[] = {
PCMK_XA_ADMIN_EPOCH,
PCMK_XA_EPOCH,
PCMK_XA_NUM_UPDATES,
};
xmlNode *output = NULL;
/* If we're ignoring the version, make the version information
* identical, so it isn't detected as a change. */
if (no_version) {
int lpc;
for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
crm_copy_xml_element(object_original, object_new, vfields[lpc]);
}
}
if (as_cib) {
pcmk__xml_doc_set_flags(object_new->doc, pcmk__xf_ignore_attr_pos);
}
pcmk__xml_mark_changes(object_original, object_new);
crm_log_xml_debug(object_new, (xml_file_new? xml_file_new: "target"));
output = xml_create_patchset(0, object_original, object_new, NULL, FALSE);
pcmk__log_xml_changes(LOG_INFO, object_new);
pcmk__xml_commit_changes(object_new->doc);
if (output == NULL) {
return pcmk_rc_ok; // No changes
}
- patchset_process_digest(output, object_original, object_new, as_cib);
-
if (as_cib) {
+ pcmk__xml_patchset_add_digest(output, object_new);
log_patch_cib_versions(output);
} else if (no_version) {
pcmk__xml_free(pcmk__xe_first_child(output, PCMK_XE_VERSION, NULL,
NULL));
}
pcmk__log_xml_patchset(LOG_NOTICE, output);
print_patch(output);
pcmk__xml_free(output);
/* pcmk_rc_error means there's a non-empty diff.
* @COMPAT Choose a more descriptive return code, like one that maps to
* CRM_EX_DIGEST?
*/
return pcmk_rc_error;
}
static GOptionContext *
build_arg_context(pcmk__common_args_t *args) {
GOptionContext *context = NULL;
const char *description = "Examples:\n\n"
"Obtain the two different configuration files by running cibadmin on the two cluster setups to compare:\n\n"
"\t# cibadmin --query > cib-old.xml\n\n"
"\t# cibadmin --query > cib-new.xml\n\n"
"Calculate and save the difference between the two files:\n\n"
"\t# crm_diff --original cib-old.xml --new cib-new.xml > patch.xml\n\n"
"Apply the patch to the original file:\n\n"
"\t# crm_diff --original cib-old.xml --patch patch.xml > updated.xml\n\n"
"Apply the patch to the running cluster:\n\n"
"\t# cibadmin --patch -x patch.xml\n";
context = pcmk__build_arg_context(args, NULL, NULL, NULL);
g_option_context_set_description(context, description);
pcmk__add_arg_group(context, "xml", "Original XML:",
"Show original XML options", original_xml_entries);
pcmk__add_arg_group(context, "operation", "Operation:",
"Show operation options", operation_entries);
pcmk__add_arg_group(context, "additional", "Additional Options:",
"Show additional options", addl_entries);
return context;
}
int
main(int argc, char **argv)
{
xmlNode *object_original = NULL;
xmlNode *object_new = NULL;
crm_exit_t exit_code = CRM_EX_OK;
GError *error = NULL;
pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
gchar **processed_args = pcmk__cmdline_preproc(argv, "nopNO");
GOptionContext *context = build_arg_context(args);
int rc = pcmk_rc_ok;
if (!g_option_context_parse_strv(context, &processed_args, &error)) {
exit_code = CRM_EX_USAGE;
goto done;
}
pcmk__cli_init_logging("crm_diff", args->verbosity);
if (args->version) {
g_strfreev(processed_args);
pcmk__free_arg_context(context);
/* FIXME: When crm_diff is converted to use formatted output, this can go. */
pcmk__cli_help('v');
}
if (options.apply && options.no_version) {
fprintf(stderr, "warning: -u/--no-version ignored with -p/--patch\n");
} else if (options.as_cib && options.no_version) {
fprintf(stderr, "error: -u/--no-version incompatible with -c/--cib\n");
exit_code = CRM_EX_USAGE;
goto done;
}
if (options.raw_original) {
object_original = pcmk__xml_parse(options.xml_file_original);
} else if (options.use_stdin) {
fprintf(stderr, "Input first XML fragment:");
object_original = pcmk__xml_read(NULL);
} else if (options.xml_file_original != NULL) {
object_original = pcmk__xml_read(options.xml_file_original);
}
if (options.raw_new) {
object_new = pcmk__xml_parse(options.xml_file_new);
} else if (options.use_stdin) {
fprintf(stderr, "Input second XML fragment:");
object_new = pcmk__xml_read(NULL);
} else if (options.xml_file_new != NULL) {
object_new = pcmk__xml_read(options.xml_file_new);
}
if (object_original == NULL) {
fprintf(stderr, "Could not parse the first XML fragment\n");
exit_code = CRM_EX_DATAERR;
goto done;
}
if (object_new == NULL) {
fprintf(stderr, "Could not parse the second XML fragment\n");
exit_code = CRM_EX_DATAERR;
goto done;
}
if (options.apply) {
rc = apply_patch(object_original, object_new, options.as_cib);
} else {
rc = generate_patch(object_original, object_new, options.xml_file_new, options.as_cib, options.no_version);
}
exit_code = pcmk_rc2exitc(rc);
done:
g_strfreev(processed_args);
pcmk__free_arg_context(context);
free(options.xml_file_original);
free(options.xml_file_new);
pcmk__xml_free(object_original);
pcmk__xml_free(object_new);
pcmk__output_and_clear_error(&error, NULL);
crm_exit(exit_code);
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Jul 10, 3:09 AM (23 h, 34 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2008608
Default Alt Text
(480 KB)

Event Timeline