Page MenuHomeClusterLabs Projects

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/daemons/based/based_callbacks.c b/daemons/based/based_callbacks.c
index d9f359b572..2b5be57f7d 100644
--- a/daemons/based/based_callbacks.c
+++ b/daemons/based/based_callbacks.c
@@ -1,1402 +1,1402 @@
/*
* 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/param.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdint.h> // uint32_t, uint64_t, UINT64_C()
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h> // PRIu64
#include <glib.h>
#include <libxml/tree.h>
#include <libxml/xpath.h> // xmlXPathObject, etc.
#include <crm/crm.h>
#include <crm/cib.h>
#include <crm/cluster/internal.h>
#include <crm/common/xml.h>
#include <crm/common/remote_internal.h>
#include <pacemaker-based.h>
#define EXIT_ESCALATION_MS 10000
qb_ipcs_service_t *ipcs_ro = NULL;
qb_ipcs_service_t *ipcs_rw = NULL;
qb_ipcs_service_t *ipcs_shm = NULL;
static int cib_process_command(xmlNode *request,
const cib__operation_t *operation,
cib__op_fn_t op_function, xmlNode **reply,
xmlNode **cib_diff, bool privileged);
static gboolean cib_common_callback(qb_ipcs_connection_t *c, void *data,
size_t size, gboolean privileged);
static int32_t
cib_ipc_accept(qb_ipcs_connection_t * c, uid_t uid, gid_t gid)
{
if (cib_shutdown_flag) {
crm_info("Ignoring new IPC client [%d] during shutdown",
pcmk__client_pid(c));
return -ECONNREFUSED;
}
if (pcmk__new_client(c, uid, gid) == NULL) {
return -ENOMEM;
}
return 0;
}
static int32_t
cib_ipc_dispatch_rw(qb_ipcs_connection_t * c, void *data, size_t size)
{
pcmk__client_t *client = pcmk__find_client(c);
crm_trace("%p message from %s", c, client->id);
return cib_common_callback(c, data, size, TRUE);
}
static int32_t
cib_ipc_dispatch_ro(qb_ipcs_connection_t * c, void *data, size_t size)
{
pcmk__client_t *client = pcmk__find_client(c);
crm_trace("%p message from %s", c, client->id);
return cib_common_callback(c, data, size, FALSE);
}
/* Error code means? */
static int32_t
cib_ipc_closed(qb_ipcs_connection_t * c)
{
pcmk__client_t *client = pcmk__find_client(c);
if (client == NULL) {
return 0;
}
crm_trace("Connection %p", c);
pcmk__free_client(client);
return 0;
}
static void
cib_ipc_destroy(qb_ipcs_connection_t * c)
{
crm_trace("Connection %p", c);
cib_ipc_closed(c);
if (cib_shutdown_flag) {
cib_shutdown(0);
}
}
struct qb_ipcs_service_handlers ipc_ro_callbacks = {
.connection_accept = cib_ipc_accept,
.connection_created = NULL,
.msg_process = cib_ipc_dispatch_ro,
.connection_closed = cib_ipc_closed,
.connection_destroyed = cib_ipc_destroy
};
struct qb_ipcs_service_handlers ipc_rw_callbacks = {
.connection_accept = cib_ipc_accept,
.connection_created = NULL,
.msg_process = cib_ipc_dispatch_rw,
.connection_closed = cib_ipc_closed,
.connection_destroyed = cib_ipc_destroy
};
/*!
* \internal
* \brief Create reply XML for a CIB request
*
* \param[in] op CIB operation type
* \param[in] call_id CIB call ID
* \param[in] client_id CIB client ID
* \param[in] call_options Group of <tt>enum cib_call_options</tt> flags
* \param[in] rc Request return code
* \param[in] call_data Request output data
*
* \return Reply XML (guaranteed not to be \c NULL)
*
* \note The caller is responsible for freeing the return value using
* \p pcmk__xml_free().
*/
static xmlNode *
create_cib_reply(const char *op, const char *call_id, const char *client_id,
uint32_t call_options, int rc, xmlNode *call_data)
{
xmlNode *reply = pcmk__xe_create(NULL, PCMK__XE_CIB_REPLY);
crm_xml_add(reply, PCMK__XA_T, PCMK__VALUE_CIB);
crm_xml_add(reply, PCMK__XA_CIB_OP, op);
crm_xml_add(reply, PCMK__XA_CIB_CALLID, call_id);
crm_xml_add(reply, PCMK__XA_CIB_CLIENTID, client_id);
crm_xml_add_int(reply, PCMK__XA_CIB_CALLOPT, call_options);
crm_xml_add_int(reply, PCMK__XA_CIB_RC, rc);
if (call_data != NULL) {
xmlNode *wrapper = pcmk__xe_create(reply, PCMK__XE_CIB_CALLDATA);
crm_trace("Attaching reply output");
pcmk__xml_copy(wrapper, call_data);
}
crm_log_xml_explicit(reply, "cib:reply");
return reply;
}
static void
do_local_notify(const xmlNode *notify_src, const char *client_id,
bool sync_reply, bool from_peer)
{
int msg_id = 0;
int rc = pcmk_rc_ok;
pcmk__client_t *client_obj = NULL;
uint32_t flags = crm_ipc_server_event;
CRM_CHECK((notify_src != NULL) && (client_id != NULL), return);
crm_element_value_int(notify_src, PCMK__XA_CIB_CALLID, &msg_id);
client_obj = pcmk__find_client_by_id(client_id);
if (client_obj == NULL) {
crm_debug("Could not notify client %s%s %s of call %d result: "
"client no longer exists", client_id,
(from_peer? " (originator of delegated request)" : ""),
(sync_reply? "synchronously" : "asynchronously"), msg_id);
return;
}
if (sync_reply) {
flags = crm_ipc_flags_none;
if (client_obj->ipcs != NULL) {
msg_id = client_obj->request_id;
client_obj->request_id = 0;
}
}
switch (PCMK__CLIENT_TYPE(client_obj)) {
case pcmk__client_ipc:
rc = pcmk__ipc_send_xml(client_obj, msg_id, notify_src, flags);
break;
case pcmk__client_tls:
case pcmk__client_tcp:
rc = pcmk__remote_send_xml(client_obj->remote, notify_src);
break;
default:
rc = EPROTONOSUPPORT;
break;
}
if (rc == pcmk_rc_ok) {
crm_trace("Notified %s client %s%s %s of call %d result",
pcmk__client_type_str(PCMK__CLIENT_TYPE(client_obj)),
pcmk__client_name(client_obj),
(from_peer? " (originator of delegated request)" : ""),
(sync_reply? "synchronously" : "asynchronously"), msg_id);
} else {
crm_warn("Could not notify %s client %s%s %s of call %d result: %s",
pcmk__client_type_str(PCMK__CLIENT_TYPE(client_obj)),
pcmk__client_name(client_obj),
(from_peer? " (originator of delegated request)" : ""),
(sync_reply? "synchronously" : "asynchronously"), msg_id,
pcmk_rc_str(rc));
}
}
void
cib_common_callback_worker(uint32_t id, uint32_t flags, xmlNode * op_request,
pcmk__client_t *cib_client, gboolean privileged)
{
const char *op = crm_element_value(op_request, PCMK__XA_CIB_OP);
uint32_t call_options = cib_none;
int rc = pcmk_rc_ok;
rc = pcmk__xe_get_flags(op_request, PCMK__XA_CIB_CALLOPT, &call_options,
cib_none);
if (rc != pcmk_rc_ok) {
crm_warn("Couldn't parse options from request: %s", pcmk_rc_str(rc));
}
/* Requests with cib_transaction set should not be sent to based directly
* (outside of a commit-transaction request)
*/
if (pcmk_is_set(call_options, cib_transaction)) {
return;
}
if (pcmk__str_eq(op, CRM_OP_REGISTER, pcmk__str_none)) {
if (flags & crm_ipc_client_response) {
xmlNode *ack = pcmk__xe_create(NULL, __func__);
crm_xml_add(ack, PCMK__XA_CIB_OP, CRM_OP_REGISTER);
crm_xml_add(ack, PCMK__XA_CIB_CLIENTID, cib_client->id);
pcmk__ipc_send_xml(cib_client, id, ack, flags);
cib_client->request_id = 0;
pcmk__xml_free(ack);
}
return;
} else if (pcmk__str_eq(op, PCMK__VALUE_CIB_NOTIFY, pcmk__str_none)) {
/* Update the notify filters for this client */
int on_off = 0;
crm_exit_t status = CRM_EX_OK;
uint64_t bit = UINT64_C(0);
const char *type = crm_element_value(op_request,
PCMK__XA_CIB_NOTIFY_TYPE);
crm_element_value_int(op_request, PCMK__XA_CIB_NOTIFY_ACTIVATE,
&on_off);
crm_debug("Setting %s callbacks %s for client %s",
type, (on_off? "on" : "off"), pcmk__client_name(cib_client));
if (pcmk__str_eq(type, PCMK__VALUE_CIB_POST_NOTIFY, pcmk__str_none)) {
bit = cib_notify_post;
} else if (pcmk__str_eq(type, PCMK__VALUE_CIB_PRE_NOTIFY,
pcmk__str_none)) {
bit = cib_notify_pre;
} else if (pcmk__str_eq(type, PCMK__VALUE_CIB_UPDATE_CONFIRMATION,
pcmk__str_none)) {
bit = cib_notify_confirm;
} else if (pcmk__str_eq(type, PCMK__VALUE_CIB_DIFF_NOTIFY,
pcmk__str_none)) {
bit = cib_notify_diff;
} else {
status = CRM_EX_INVALID_PARAM;
}
if (bit != 0) {
if (on_off) {
pcmk__set_client_flags(cib_client, bit);
} else {
pcmk__clear_client_flags(cib_client, bit);
}
}
pcmk__ipc_send_ack(cib_client, id, flags, PCMK__XE_ACK, NULL, status);
return;
}
cib_process_request(op_request, privileged, cib_client);
}
int32_t
cib_common_callback(qb_ipcs_connection_t * c, void *data, size_t size, gboolean privileged)
{
uint32_t id = 0;
uint32_t flags = 0;
uint32_t call_options = cib_none;
pcmk__client_t *cib_client = pcmk__find_client(c);
xmlNode *op_request = pcmk__client_data2xml(cib_client, data, &id, &flags);
if (op_request) {
int rc = pcmk_rc_ok;
rc = pcmk__xe_get_flags(op_request, PCMK__XA_CIB_CALLOPT, &call_options,
cib_none);
if (rc != pcmk_rc_ok) {
crm_warn("Couldn't parse options from request: %s",
pcmk_rc_str(rc));
}
}
if (op_request == NULL) {
crm_trace("Invalid message from %p", c);
pcmk__ipc_send_ack(cib_client, id, flags, PCMK__XE_NACK, NULL,
CRM_EX_PROTOCOL);
return 0;
} else if(cib_client == NULL) {
crm_trace("Invalid client %p", c);
return 0;
}
if (pcmk_is_set(call_options, cib_sync_call)) {
CRM_LOG_ASSERT(flags & crm_ipc_client_response);
CRM_LOG_ASSERT(cib_client->request_id == 0); /* This means the client has two synchronous events in-flight */
cib_client->request_id = id; /* Reply only to the last one */
}
if (cib_client->name == NULL) {
const char *value = crm_element_value(op_request,
PCMK__XA_CIB_CLIENTNAME);
if (value == NULL) {
cib_client->name = pcmk__itoa(cib_client->pid);
} else {
cib_client->name = pcmk__str_copy(value);
if (pcmk__parse_server(value) != pcmk_ipc_unknown) {
pcmk__set_client_flags(cib_client, cib_is_daemon);
}
}
}
/* Allow cluster daemons more leeway before being evicted */
if (pcmk_is_set(cib_client->flags, cib_is_daemon)) {
const char *qmax = cib_config_lookup(PCMK_OPT_CLUSTER_IPC_LIMIT);
pcmk__set_client_queue_max(cib_client, qmax);
}
crm_xml_add(op_request, PCMK__XA_CIB_CLIENTID, cib_client->id);
crm_xml_add(op_request, PCMK__XA_CIB_CLIENTNAME, cib_client->name);
CRM_LOG_ASSERT(cib_client->user != NULL);
pcmk__update_acl_user(op_request, PCMK__XA_CIB_USER, cib_client->user);
cib_common_callback_worker(id, flags, op_request, cib_client, privileged);
pcmk__xml_free(op_request);
return 0;
}
static uint64_t ping_seq = 0;
static char *ping_digest = NULL;
static bool ping_modified_since = FALSE;
static gboolean
cib_digester_cb(gpointer data)
{
if (based_is_primary) {
char buffer[32];
xmlNode *ping = pcmk__xe_create(NULL, PCMK__XE_PING);
ping_seq++;
free(ping_digest);
ping_digest = NULL;
ping_modified_since = FALSE;
snprintf(buffer, 32, "%" PRIu64, ping_seq);
crm_trace("Requesting peer digests (%s)", buffer);
crm_xml_add(ping, PCMK__XA_T, PCMK__VALUE_CIB);
crm_xml_add(ping, PCMK__XA_CIB_OP, CRM_OP_PING);
crm_xml_add(ping, PCMK__XA_CIB_PING_ID, buffer);
crm_xml_add(ping, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET);
pcmk__cluster_send_message(NULL, pcmk_ipc_based, ping);
pcmk__xml_free(ping);
}
return FALSE;
}
static void
process_ping_reply(xmlNode *reply)
{
uint64_t seq = 0;
const char *host = crm_element_value(reply, PCMK__XA_SRC);
xmlNode *wrapper = pcmk__xe_first_child(reply, PCMK__XE_CIB_CALLDATA, NULL,
NULL);
xmlNode *pong = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
const char *seq_s = crm_element_value(pong, PCMK__XA_CIB_PING_ID);
const char *digest = crm_element_value(pong, PCMK__XA_DIGEST);
if (seq_s == NULL) {
crm_debug("Ignoring ping reply with no " PCMK__XA_CIB_PING_ID);
return;
} else {
long long seq_ll;
int rc = pcmk__scan_ll(seq_s, &seq_ll, 0LL);
if (rc != pcmk_rc_ok) {
crm_debug("Ignoring ping reply with invalid " PCMK__XA_CIB_PING_ID
" '%s': %s", seq_s, pcmk_rc_str(rc));
return;
}
seq = (uint64_t) seq_ll;
}
if(digest == NULL) {
crm_trace("Ignoring ping reply %s from %s with no digest", seq_s, host);
} else if(seq != ping_seq) {
crm_trace("Ignoring out of sequence ping reply %s from %s", seq_s, host);
} else if(ping_modified_since) {
crm_trace("Ignoring ping reply %s from %s: cib updated since", seq_s, host);
} else {
if(ping_digest == NULL) {
crm_trace("Calculating new digest");
ping_digest = pcmk__digest_xml(the_cib, true);
}
crm_trace("Processing ping reply %s from %s (%s)", seq_s, host, digest);
if (!pcmk__str_eq(ping_digest, digest, pcmk__str_casei)) {
xmlNode *wrapper = pcmk__xe_first_child(pong, PCMK__XE_CIB_CALLDATA,
NULL, NULL);
xmlNode *remote_cib = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
const char *admin_epoch_s = NULL;
const char *epoch_s = NULL;
const char *num_updates_s = NULL;
if (remote_cib != NULL) {
admin_epoch_s = crm_element_value(remote_cib,
PCMK_XA_ADMIN_EPOCH);
epoch_s = crm_element_value(remote_cib, PCMK_XA_EPOCH);
num_updates_s = crm_element_value(remote_cib,
PCMK_XA_NUM_UPDATES);
}
crm_notice("Local CIB %s.%s.%s.%s differs from %s: %s.%s.%s.%s %p",
crm_element_value(the_cib, PCMK_XA_ADMIN_EPOCH),
crm_element_value(the_cib, PCMK_XA_EPOCH),
crm_element_value(the_cib, PCMK_XA_NUM_UPDATES),
ping_digest, host,
pcmk__s(admin_epoch_s, "_"),
pcmk__s(epoch_s, "_"),
pcmk__s(num_updates_s, "_"),
digest, remote_cib);
if(remote_cib && remote_cib->children) {
// Additional debug
- xml_calculate_changes(the_cib, remote_cib);
+ pcmk__xml_mark_changes(the_cib, remote_cib);
pcmk__log_xml_changes(LOG_INFO, remote_cib);
crm_trace("End of differences");
}
pcmk__xml_free(remote_cib);
sync_our_cib(reply, FALSE);
}
}
}
static void
parse_local_options(const pcmk__client_t *cib_client,
const cib__operation_t *operation,
const char *host, const char *op, gboolean *local_notify,
gboolean *needs_reply, gboolean *process,
gboolean *needs_forward)
{
// Process locally and notify local client
*process = TRUE;
*needs_reply = FALSE;
*local_notify = TRUE;
*needs_forward = FALSE;
if (pcmk_is_set(operation->flags, cib__op_attr_local)) {
/* Always process locally if cib__op_attr_local is set.
*
* @COMPAT: Currently host is ignored. At a compatibility break, throw
* an error (from cib_process_request() or earlier) if host is not NULL or
* OUR_NODENAME.
*/
crm_trace("Processing always-local %s op from client %s",
op, pcmk__client_name(cib_client));
if (!pcmk__str_eq(host, OUR_NODENAME,
pcmk__str_casei|pcmk__str_null_matches)) {
crm_warn("Operation '%s' is always local but its target host is "
"set to '%s'",
op, host);
}
return;
}
if (pcmk_is_set(operation->flags, cib__op_attr_modifies)
|| !pcmk__str_eq(host, OUR_NODENAME,
pcmk__str_casei|pcmk__str_null_matches)) {
// Forward modifying and non-local requests via cluster
*process = FALSE;
*needs_reply = FALSE;
*local_notify = FALSE;
*needs_forward = TRUE;
crm_trace("%s op from %s needs to be forwarded to %s",
op, pcmk__client_name(cib_client),
pcmk__s(host, "all nodes"));
return;
}
if (stand_alone) {
crm_trace("Processing %s op from client %s (stand-alone)",
op, pcmk__client_name(cib_client));
} else {
crm_trace("Processing %saddressed %s op from client %s",
((host != NULL)? "locally " : "un"),
op, pcmk__client_name(cib_client));
}
}
static gboolean
parse_peer_options(const cib__operation_t *operation, xmlNode *request,
gboolean *local_notify, gboolean *needs_reply,
gboolean *process)
{
/* TODO: What happens when an update comes in after node A
* requests the CIB from node B, but before it gets the reply (and
* sends out the replace operation)?
*
* (This may no longer be relevant since legacy mode was dropped; need to
* trace code more closely to check.)
*/
const char *host = NULL;
const char *delegated = crm_element_value(request,
PCMK__XA_CIB_DELEGATED_FROM);
const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
const char *originator = crm_element_value(request, PCMK__XA_SRC);
const char *reply_to = crm_element_value(request, PCMK__XA_CIB_ISREPLYTO);
gboolean is_reply = pcmk__str_eq(reply_to, OUR_NODENAME, pcmk__str_casei);
if (originator == NULL) { // Shouldn't be possible
originator = "peer";
}
if (pcmk__str_eq(op, PCMK__CIB_REQUEST_REPLACE, pcmk__str_none)) {
// sync_our_cib() sets PCMK__XA_CIB_ISREPLYTO
if (reply_to) {
delegated = reply_to;
}
goto skip_is_reply;
} else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_SYNC_TO_ALL,
pcmk__str_none)) {
// Nothing to do
} else if (is_reply && pcmk__str_eq(op, CRM_OP_PING, pcmk__str_casei)) {
process_ping_reply(request);
return FALSE;
} else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_UPGRADE, pcmk__str_none)) {
/* Only the DC (node with the oldest software) should process
* this operation if PCMK__XA_CIB_SCHEMA_MAX is unset.
*
* If the DC is happy it will then send out another
* PCMK__CIB_REQUEST_UPGRADE which will tell all nodes to do the actual
* upgrade.
*
* Except this time PCMK__XA_CIB_SCHEMA_MAX will be set which puts a
* limit on how far newer nodes will go
*/
const char *max = crm_element_value(request, PCMK__XA_CIB_SCHEMA_MAX);
const char *upgrade_rc = crm_element_value(request,
PCMK__XA_CIB_UPGRADE_RC);
crm_trace("Parsing upgrade %s for %s with max=%s and upgrade_rc=%s",
(is_reply? "reply" : "request"),
(based_is_primary? "primary" : "secondary"),
pcmk__s(max, "none"), pcmk__s(upgrade_rc, "none"));
if (upgrade_rc != NULL) {
// Our upgrade request was rejected by DC, notify clients of result
crm_xml_add(request, PCMK__XA_CIB_RC, upgrade_rc);
} else if ((max == NULL) && based_is_primary) {
/* We are the DC, check if this upgrade is allowed */
goto skip_is_reply;
} else if(max) {
/* Ok, go ahead and upgrade to 'max' */
goto skip_is_reply;
} else {
// Ignore broadcast client requests when we're not primary
return FALSE;
}
} else if (pcmk__xe_attr_is_true(request, PCMK__XA_CIB_UPDATE)) {
crm_info("Detected legacy %s global update from %s", op, originator);
send_sync_request(NULL);
return FALSE;
} else if (is_reply
&& pcmk_is_set(operation->flags, cib__op_attr_modifies)) {
crm_trace("Ignoring legacy %s reply sent from %s to local clients", op, originator);
return FALSE;
} else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_SHUTDOWN, pcmk__str_none)) {
*local_notify = FALSE;
if (reply_to == NULL) {
*process = TRUE;
} else { // Not possible?
crm_debug("Ignoring shutdown request from %s because reply_to=%s",
originator, reply_to);
}
return *process;
}
if (is_reply) {
crm_trace("Will notify local clients for %s reply from %s",
op, originator);
*process = FALSE;
*needs_reply = FALSE;
*local_notify = TRUE;
return TRUE;
}
skip_is_reply:
*process = TRUE;
*needs_reply = FALSE;
*local_notify = pcmk__str_eq(delegated, OUR_NODENAME, pcmk__str_casei);
host = crm_element_value(request, PCMK__XA_CIB_HOST);
if (pcmk__str_eq(host, OUR_NODENAME, pcmk__str_casei)) {
crm_trace("Processing %s request sent to us from %s", op, originator);
*needs_reply = TRUE;
return TRUE;
} else if (host != NULL) {
crm_trace("Ignoring %s request intended for CIB manager on %s",
op, host);
return FALSE;
} else if(is_reply == FALSE && pcmk__str_eq(op, CRM_OP_PING, pcmk__str_casei)) {
*needs_reply = TRUE;
}
crm_trace("Processing %s request broadcast by %s call %s on %s "
"(local clients will%s be notified)", op,
pcmk__s(crm_element_value(request, PCMK__XA_CIB_CLIENTNAME),
"client"),
pcmk__s(crm_element_value(request, PCMK__XA_CIB_CALLID),
"without ID"),
originator, (*local_notify? "" : "not"));
return TRUE;
}
/*!
* \internal
* \brief Forward a CIB request to the appropriate target host(s)
*
* \param[in] request CIB request to forward
*/
static void
forward_request(xmlNode *request)
{
const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
const char *section = crm_element_value(request, PCMK__XA_CIB_SECTION);
const char *host = crm_element_value(request, PCMK__XA_CIB_HOST);
const char *originator = crm_element_value(request, PCMK__XA_SRC);
const char *client_name = crm_element_value(request,
PCMK__XA_CIB_CLIENTNAME);
const char *call_id = crm_element_value(request, PCMK__XA_CIB_CALLID);
pcmk__node_status_t *peer = NULL;
int log_level = LOG_INFO;
if (pcmk__str_eq(op, PCMK__CIB_REQUEST_NOOP, pcmk__str_none)) {
log_level = LOG_DEBUG;
}
do_crm_log(log_level,
"Forwarding %s operation for section %s to %s (origin=%s/%s/%s)",
pcmk__s(op, "invalid"),
pcmk__s(section, "all"),
pcmk__s(host, "all"),
pcmk__s(originator, "local"),
pcmk__s(client_name, "unspecified"),
pcmk__s(call_id, "unspecified"));
crm_xml_add(request, PCMK__XA_CIB_DELEGATED_FROM, OUR_NODENAME);
if (host != NULL) {
peer = pcmk__get_node(0, host, NULL, pcmk__node_search_cluster_member);
}
pcmk__cluster_send_message(peer, pcmk_ipc_based, request);
// Return the request to its original state
pcmk__xe_remove_attr(request, PCMK__XA_CIB_DELEGATED_FROM);
}
static void
send_peer_reply(xmlNode *msg, const char *originator)
{
const pcmk__node_status_t *node = NULL;
if ((msg == NULL) || (originator == NULL)) {
return;
}
// Send reply via cluster to originating node
node = pcmk__get_node(0, originator, NULL,
pcmk__node_search_cluster_member);
crm_trace("Sending request result to %s only", originator);
crm_xml_add(msg, PCMK__XA_CIB_ISREPLYTO, originator);
pcmk__cluster_send_message(node, pcmk_ipc_based, msg);
}
/*!
* \internal
* \brief Handle an IPC or CPG message containing a request
*
* \param[in,out] request Request XML
* \param[in] privileged Whether privileged commands may be run
* (see cib_server_ops[] definition)
* \param[in] cib_client IPC client that sent request (or NULL if CPG)
*
* \return Legacy Pacemaker return code
*/
int
cib_process_request(xmlNode *request, gboolean privileged,
const pcmk__client_t *cib_client)
{
// @TODO: Break into multiple smaller functions
uint32_t call_options = cib_none;
gboolean process = TRUE; // Whether to process request locally now
gboolean is_update = TRUE; // Whether request would modify CIB
gboolean needs_reply = TRUE; // Whether to build a reply
gboolean local_notify = FALSE; // Whether to notify (local) requester
gboolean needs_forward = FALSE; // Whether to forward request somewhere else
xmlNode *op_reply = NULL;
xmlNode *result_diff = NULL;
int rc = pcmk_ok;
const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
const char *originator = crm_element_value(request, PCMK__XA_SRC);
const char *host = crm_element_value(request, PCMK__XA_CIB_HOST);
const char *call_id = crm_element_value(request, PCMK__XA_CIB_CALLID);
const char *client_id = crm_element_value(request, PCMK__XA_CIB_CLIENTID);
const char *client_name = crm_element_value(request,
PCMK__XA_CIB_CLIENTNAME);
const char *reply_to = crm_element_value(request, PCMK__XA_CIB_ISREPLYTO);
const cib__operation_t *operation = NULL;
cib__op_fn_t op_function = NULL;
rc = pcmk__xe_get_flags(request, PCMK__XA_CIB_CALLOPT, &call_options,
cib_none);
if (rc != pcmk_rc_ok) {
crm_warn("Couldn't parse options from request: %s", pcmk_rc_str(rc));
}
if ((host != NULL) && (*host == '\0')) {
host = NULL;
}
if (cib_client == NULL) {
crm_trace("Processing peer %s operation from %s/%s on %s intended for %s (reply=%s)",
op, pcmk__s(client_name, "client"), call_id, originator,
pcmk__s(host, "all"), reply_to);
} else {
crm_xml_add(request, PCMK__XA_SRC, OUR_NODENAME);
crm_trace("Processing local %s operation from %s/%s intended for %s",
op, pcmk__s(client_name, "client"), call_id,
pcmk__s(host, "all"));
}
rc = cib__get_operation(op, &operation);
rc = pcmk_rc2legacy(rc);
if (rc != pcmk_ok) {
/* TODO: construct error reply? */
crm_err("Pre-processing of command failed: %s", pcmk_strerror(rc));
return rc;
}
op_function = based_get_op_function(operation);
if (op_function == NULL) {
crm_err("Operation %s not supported by CIB manager", op);
return -EOPNOTSUPP;
}
if (cib_client != NULL) {
parse_local_options(cib_client, operation, host, op,
&local_notify, &needs_reply, &process,
&needs_forward);
} else if (!parse_peer_options(operation, request, &local_notify,
&needs_reply, &process)) {
return rc;
}
if (pcmk_is_set(call_options, cib_transaction)) {
/* All requests in a transaction are processed locally against a working
* CIB copy, and we don't notify for individual requests because the
* entire transaction is atomic.
*
* We still call the option parser functions above, for the sake of log
* messages and checking whether we're the target for peer requests.
*/
process = TRUE;
needs_reply = FALSE;
local_notify = FALSE;
needs_forward = FALSE;
}
is_update = pcmk_is_set(operation->flags, cib__op_attr_modifies);
if (pcmk_is_set(call_options, cib_discard_reply)) {
/* If the request will modify the CIB, and we are in legacy mode, we
* need to build a reply so we can broadcast a diff, even if the
* requester doesn't want one.
*/
needs_reply = FALSE;
local_notify = FALSE;
crm_trace("Client is not interested in the reply");
}
if (needs_forward) {
forward_request(request);
return rc;
}
if (cib_status != pcmk_ok) {
rc = cib_status;
crm_err("Ignoring request because cluster configuration is invalid "
"(please repair and restart): %s", pcmk_strerror(rc));
op_reply = create_cib_reply(op, call_id, client_id, call_options, rc,
the_cib);
} else if (process) {
time_t finished = 0;
time_t now = time(NULL);
int level = LOG_INFO;
const char *section = crm_element_value(request, PCMK__XA_CIB_SECTION);
const char *admin_epoch_s = NULL;
const char *epoch_s = NULL;
const char *num_updates_s = NULL;
rc = cib_process_command(request, operation, op_function, &op_reply,
&result_diff, privileged);
if (!is_update) {
level = LOG_TRACE;
} else if (pcmk__xe_attr_is_true(request, PCMK__XA_CIB_UPDATE)) {
switch (rc) {
case pcmk_ok:
level = LOG_INFO;
break;
case -pcmk_err_old_data:
case -pcmk_err_diff_resync:
case -pcmk_err_diff_failed:
level = LOG_TRACE;
break;
default:
level = LOG_ERR;
}
} else if (rc != pcmk_ok) {
level = LOG_WARNING;
}
if (the_cib != NULL) {
admin_epoch_s = crm_element_value(the_cib, PCMK_XA_ADMIN_EPOCH);
epoch_s = crm_element_value(the_cib, PCMK_XA_EPOCH);
num_updates_s = crm_element_value(the_cib, PCMK_XA_NUM_UPDATES);
}
do_crm_log(level,
"Completed %s operation for section %s: %s (rc=%d, origin=%s/%s/%s, version=%s.%s.%s)",
op, section ? section : "'all'", pcmk_strerror(rc), rc,
originator ? originator : "local",
pcmk__s(client_name, "client"), call_id,
pcmk__s(admin_epoch_s, "0"),
pcmk__s(epoch_s, "0"),
pcmk__s(num_updates_s, "0"));
finished = time(NULL);
if ((finished - now) > 3) {
crm_trace("%s operation took %lds to complete", op, (long)(finished - now));
crm_write_blackbox(0, NULL);
}
if (op_reply == NULL && (needs_reply || local_notify)) {
crm_err("Unexpected NULL reply to message");
crm_log_xml_err(request, "null reply");
needs_reply = FALSE;
local_notify = FALSE;
}
}
if (is_update) {
crm_trace("Completed pre-sync update from %s/%s/%s%s",
originator ? originator : "local",
pcmk__s(client_name, "client"), call_id,
local_notify?" with local notification":"");
} else if (!needs_reply || stand_alone) {
// This was a non-originating secondary update
crm_trace("Completed update as secondary");
} else if ((cib_client == NULL)
&& !pcmk_is_set(call_options, cib_discard_reply)) {
if (is_update == FALSE || result_diff == NULL) {
crm_trace("Request not broadcast: R/O call");
} else if (rc != pcmk_ok) {
crm_trace("Request not broadcast: call failed: %s", pcmk_strerror(rc));
} else {
crm_trace("Directing reply to %s", originator);
}
send_peer_reply(op_reply, originator);
}
if (local_notify && client_id) {
crm_trace("Performing local %ssync notification for %s",
(pcmk_is_set(call_options, cib_sync_call)? "" : "a"),
client_id);
if (process == FALSE) {
do_local_notify(request, client_id,
pcmk_is_set(call_options, cib_sync_call),
(cib_client == NULL));
} else {
do_local_notify(op_reply, client_id,
pcmk_is_set(call_options, cib_sync_call),
(cib_client == NULL));
}
}
pcmk__xml_free(op_reply);
pcmk__xml_free(result_diff);
return rc;
}
/*!
* \internal
* \brief Get a CIB operation's input from the request XML
*
* \param[in] request CIB request XML
* \param[in] type CIB operation type
* \param[out] section Where to store CIB section name
*
* \return Input XML for CIB operation
*
* \note If not \c NULL, the return value is a non-const pointer to part of
* \p request. The caller should not free it directly.
*/
static xmlNode *
prepare_input(const xmlNode *request, enum cib__op_type type,
const char **section)
{
xmlNode *wrapper = pcmk__xe_first_child(request, PCMK__XE_CIB_CALLDATA,
NULL, NULL);
xmlNode *input = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
if (type == cib__op_apply_patch) {
*section = NULL;
} else {
*section = crm_element_value(request, PCMK__XA_CIB_SECTION);
}
// Grab the specified section
if ((*section != NULL) && pcmk__xe_is(input, PCMK_XE_CIB)) {
input = pcmk_find_cib_element(input, *section);
}
return input;
}
#define XPATH_CONFIG_CHANGE \
"//" PCMK_XE_CHANGE \
"[contains(@" PCMK_XA_PATH ",'/" PCMK_XE_CRM_CONFIG "/')]"
static bool
contains_config_change(xmlNode *diff)
{
bool changed = false;
if (diff) {
xmlXPathObject *xpathObj = pcmk__xpath_search(diff->doc,
XPATH_CONFIG_CHANGE);
if (pcmk__xpath_num_results(xpathObj) > 0) {
changed = true;
}
xmlXPathFreeObject(xpathObj);
}
return changed;
}
static int
cib_process_command(xmlNode *request, const cib__operation_t *operation,
cib__op_fn_t op_function, xmlNode **reply,
xmlNode **cib_diff, bool privileged)
{
xmlNode *input = NULL;
xmlNode *output = NULL;
xmlNode *result_cib = NULL;
uint32_t call_options = cib_none;
const char *op = NULL;
const char *section = NULL;
const char *call_id = crm_element_value(request, PCMK__XA_CIB_CALLID);
const char *client_id = crm_element_value(request, PCMK__XA_CIB_CLIENTID);
const char *client_name = crm_element_value(request,
PCMK__XA_CIB_CLIENTNAME);
const char *originator = crm_element_value(request, PCMK__XA_SRC);
int rc = pcmk_ok;
bool config_changed = false;
bool manage_counters = true;
static mainloop_timer_t *digest_timer = NULL;
pcmk__assert(cib_status == pcmk_ok);
if(digest_timer == NULL) {
digest_timer = mainloop_timer_add("digester", 5000, FALSE, cib_digester_cb, NULL);
}
*reply = NULL;
*cib_diff = NULL;
/* Start processing the request... */
op = crm_element_value(request, PCMK__XA_CIB_OP);
rc = pcmk__xe_get_flags(request, PCMK__XA_CIB_CALLOPT, &call_options,
cib_none);
if (rc != pcmk_rc_ok) {
crm_warn("Couldn't parse options from request: %s", pcmk_rc_str(rc));
}
if (!privileged && pcmk_is_set(operation->flags, cib__op_attr_privileged)) {
rc = -EACCES;
crm_trace("Failed due to lack of privileges: %s", pcmk_strerror(rc));
goto done;
}
input = prepare_input(request, operation->type, &section);
if (!pcmk_is_set(operation->flags, cib__op_attr_modifies)) {
rc = cib_perform_op(NULL, op, call_options, op_function, true, section,
request, input, false, &config_changed, &the_cib,
&result_cib, NULL, &output);
CRM_CHECK(result_cib == NULL, pcmk__xml_free(result_cib));
goto done;
}
/* @COMPAT: Handle a valid write action (legacy)
*
* @TODO: Re-evaluate whether this is all truly legacy. The cib_force_diff
* portion is. However, PCMK__XA_CIB_UPDATE may be set by a sync operation
* even in non-legacy mode, and manage_counters tells xml_create_patchset()
* whether to update version/epoch info.
*/
if (pcmk__xe_attr_is_true(request, PCMK__XA_CIB_UPDATE)) {
manage_counters = false;
cib__set_call_options(call_options, "call", cib_force_diff);
crm_trace("Global update detected");
CRM_LOG_ASSERT(pcmk__str_any_of(op,
PCMK__CIB_REQUEST_APPLY_PATCH,
PCMK__CIB_REQUEST_REPLACE,
NULL));
}
ping_modified_since = TRUE;
// result_cib must not be modified after cib_perform_op() returns
rc = cib_perform_op(NULL, op, call_options, op_function, false, section,
request, input, manage_counters, &config_changed,
&the_cib, &result_cib, cib_diff, &output);
/* Always write to disk for successful ops with the flag set. This also
* negates the need to detect ordering changes.
*/
if ((rc == pcmk_ok)
&& pcmk_is_set(operation->flags, cib__op_attr_writes_through)) {
config_changed = true;
}
if ((rc == pcmk_ok)
&& !pcmk_any_flags_set(call_options, cib_dryrun|cib_transaction)) {
if (result_cib != the_cib) {
if (pcmk_is_set(operation->flags, cib__op_attr_writes_through)) {
config_changed = true;
}
crm_trace("Activating %s->%s%s",
crm_element_value(the_cib, PCMK_XA_NUM_UPDATES),
crm_element_value(result_cib, PCMK_XA_NUM_UPDATES),
(config_changed? " changed" : ""));
rc = activateCibXml(result_cib, config_changed, op);
if (rc != pcmk_ok) {
crm_err("Failed to activate new CIB: %s", pcmk_strerror(rc));
}
}
if ((rc == pcmk_ok) && contains_config_change(*cib_diff)) {
cib_read_config(config_hash, result_cib);
}
/* @COMPAT Nodes older than feature set 3.19.0 don't support
* transactions. In a mixed-version cluster with nodes <3.19.0, we must
* sync the updated CIB, so that the older nodes receive the changes.
* Any node that has already applied the transaction will ignore the
* synced CIB.
*
* To ensure the updated CIB is synced from only one node, we sync it
* from the originator.
*/
if ((operation->type == cib__op_commit_transact)
&& pcmk__str_eq(originator, OUR_NODENAME, pcmk__str_casei)
&& compare_version(crm_element_value(the_cib,
PCMK_XA_CRM_FEATURE_SET),
"3.19.0") < 0) {
sync_our_cib(request, TRUE);
}
mainloop_timer_stop(digest_timer);
mainloop_timer_start(digest_timer);
} else if (rc == -pcmk_err_schema_validation) {
pcmk__assert(result_cib != the_cib);
if (output != NULL) {
crm_log_xml_info(output, "cib:output");
pcmk__xml_free(output);
}
output = result_cib;
} else {
crm_trace("Not activating %d %d %s", rc,
pcmk_is_set(call_options, cib_dryrun),
crm_element_value(result_cib, PCMK_XA_NUM_UPDATES));
if (result_cib != the_cib) {
pcmk__xml_free(result_cib);
}
}
if (!pcmk_any_flags_set(call_options,
cib_dryrun|cib_inhibit_notify|cib_transaction)) {
crm_trace("Sending notifications %d",
pcmk_is_set(call_options, cib_dryrun));
cib_diff_notify(op, rc, call_id, client_id, client_name, originator,
input, *cib_diff);
}
pcmk__log_xml_patchset(LOG_TRACE, *cib_diff);
done:
if (!pcmk_is_set(call_options, cib_discard_reply)) {
*reply = create_cib_reply(op, call_id, client_id, call_options, rc,
output);
}
if (output != the_cib) {
pcmk__xml_free(output);
}
crm_trace("done");
return rc;
}
void
cib_peer_callback(xmlNode * msg, void *private_data)
{
const char *reason = NULL;
const char *originator = crm_element_value(msg, PCMK__XA_SRC);
if (pcmk__peer_cache == NULL) {
reason = "membership not established";
goto bail;
}
if (crm_element_value(msg, PCMK__XA_CIB_CLIENTNAME) == NULL) {
crm_xml_add(msg, PCMK__XA_CIB_CLIENTNAME, originator);
}
/* crm_log_xml_trace(msg, "Peer[inbound]"); */
cib_process_request(msg, TRUE, NULL);
return;
bail:
if (reason) {
const char *op = crm_element_value(msg, PCMK__XA_CIB_OP);
crm_warn("Discarding %s message from %s: %s", op, originator, reason);
}
}
static gboolean
cib_force_exit(gpointer data)
{
crm_notice("Exiting immediately after %s without shutdown acknowledgment",
pcmk__readable_interval(EXIT_ESCALATION_MS));
terminate_cib(CRM_EX_ERROR);
return FALSE;
}
static void
disconnect_remote_client(gpointer key, gpointer value, gpointer user_data)
{
pcmk__client_t *a_client = value;
crm_err("Can't disconnect client %s: Not implemented",
pcmk__client_name(a_client));
}
static void
initiate_exit(void)
{
int active = 0;
xmlNode *leaving = NULL;
active = pcmk__cluster_num_active_nodes();
if (active < 2) { // This is the last active node
crm_info("Exiting without sending shutdown request (no active peers)");
terminate_cib(CRM_EX_OK);
return;
}
crm_info("Sending shutdown request to %d peers", active);
leaving = pcmk__xe_create(NULL, PCMK__XE_EXIT_NOTIFICATION);
crm_xml_add(leaving, PCMK__XA_T, PCMK__VALUE_CIB);
crm_xml_add(leaving, PCMK__XA_CIB_OP, PCMK__CIB_REQUEST_SHUTDOWN);
pcmk__cluster_send_message(NULL, pcmk_ipc_based, leaving);
pcmk__xml_free(leaving);
pcmk__create_timer(EXIT_ESCALATION_MS, cib_force_exit, NULL);
}
void
cib_shutdown(int nsig)
{
struct qb_ipcs_stats srv_stats;
if (cib_shutdown_flag == FALSE) {
int disconnects = 0;
qb_ipcs_connection_t *c = NULL;
cib_shutdown_flag = TRUE;
c = qb_ipcs_connection_first_get(ipcs_rw);
while (c != NULL) {
qb_ipcs_connection_t *last = c;
c = qb_ipcs_connection_next_get(ipcs_rw, last);
crm_debug("Disconnecting r/w client %p...", last);
qb_ipcs_disconnect(last);
qb_ipcs_connection_unref(last);
disconnects++;
}
c = qb_ipcs_connection_first_get(ipcs_ro);
while (c != NULL) {
qb_ipcs_connection_t *last = c;
c = qb_ipcs_connection_next_get(ipcs_ro, last);
crm_debug("Disconnecting r/o client %p...", last);
qb_ipcs_disconnect(last);
qb_ipcs_connection_unref(last);
disconnects++;
}
c = qb_ipcs_connection_first_get(ipcs_shm);
while (c != NULL) {
qb_ipcs_connection_t *last = c;
c = qb_ipcs_connection_next_get(ipcs_shm, last);
crm_debug("Disconnecting non-blocking r/w client %p...", last);
qb_ipcs_disconnect(last);
qb_ipcs_connection_unref(last);
disconnects++;
}
disconnects += pcmk__ipc_client_count();
crm_debug("Disconnecting %d remote clients", pcmk__ipc_client_count());
pcmk__foreach_ipc_client(disconnect_remote_client, NULL);
crm_info("Disconnected %d clients", disconnects);
}
qb_ipcs_stats_get(ipcs_rw, &srv_stats, QB_FALSE);
if (pcmk__ipc_client_count() == 0) {
crm_info("All clients disconnected (%d)", srv_stats.active_connections);
initiate_exit();
} else {
crm_info("Waiting on %d clients to disconnect (%d)",
pcmk__ipc_client_count(), srv_stats.active_connections);
}
}
extern int remote_fd;
extern int remote_tls_fd;
/*!
* \internal
* \brief Close remote sockets, free the global CIB and quit
*
* \param[in] exit_status What exit status to use (if -1, use CRM_EX_OK, but
* skip disconnecting from the cluster layer)
*/
void
terminate_cib(int exit_status)
{
if (remote_fd > 0) {
close(remote_fd);
remote_fd = 0;
}
if (remote_tls_fd > 0) {
close(remote_tls_fd);
remote_tls_fd = 0;
}
uninitializeCib();
// Exit immediately on error
if (exit_status > CRM_EX_OK) {
pcmk__stop_based_ipc(ipcs_ro, ipcs_rw, ipcs_shm);
crm_exit(exit_status);
return;
}
if ((mainloop != NULL) && g_main_loop_is_running(mainloop)) {
/* Quit via returning from the main loop. If exit_status has the special
* value -1, we skip the disconnect here, and it will be done when the
* main loop returns (this allows the peer status callback to avoid
* messing with the peer caches).
*/
if (exit_status == CRM_EX_OK) {
pcmk_cluster_disconnect(crm_cluster);
}
g_main_loop_quit(mainloop);
return;
}
/* Exit cleanly. Even the peer status callback can disconnect here, because
* we're not returning control to the caller.
*/
pcmk_cluster_disconnect(crm_cluster);
pcmk__stop_based_ipc(ipcs_ro, ipcs_rw, ipcs_shm);
crm_exit(CRM_EX_OK);
}
diff --git a/include/crm/common/acl_internal.h b/include/crm/common/acl_internal.h
index a65be93c58..38d7f21e95 100644
--- a/include/crm/common/acl_internal.h
+++ b/include/crm/common/acl_internal.h
@@ -1,43 +1,43 @@
/*
* 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_ACL_INTERNAL__H
#define PCMK__CRM_COMMON_ACL_INTERNAL__H
#include <string.h> // strcmp()
#include <libxml/tree.h> // xmlNode
-#include <crm/common/xml_internal.h> // enum xml_private_flags
+#include <crm/common/xml_internal.h> // enum pcmk__xml_flags
#ifdef __cplusplus
extern "C" {
#endif
/* internal ACL-related utilities */
char *pcmk__uid2username(uid_t uid);
const char *pcmk__update_acl_user(xmlNode *request, const char *field,
const char *peer_user);
static inline bool
pcmk__is_privileged(const char *user)
{
return user && (!strcmp(user, CRM_DAEMON_USER) || !strcmp(user, "root"));
}
void pcmk__enable_acl(xmlNode *acl_source, xmlNode *target, const char *user);
bool pcmk__check_acl(xmlNode *xml, const char *attr_name,
- enum xml_private_flags mode);
+ enum pcmk__xml_flags mode);
#ifdef __cplusplus
}
#endif
#endif // PCMK__CRM_COMMON_INTERNAL__H
diff --git a/include/crm/common/xml.h b/include/crm/common/xml.h
index ac4c073926..1305ddb541 100644
--- a/include/crm/common/xml.h
+++ b/include/crm/common/xml.h
@@ -1,56 +1,52 @@
/*
* 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
*/
-void xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls);
-void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml);
-void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml);
-void xml_accept_changes(xmlNode * xml);
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 d575326e0a..dc3f924757 100644
--- a/include/crm/common/xml_compat.h
+++ b/include/crm/common/xml_compat.h
@@ -1,118 +1,131 @@
/*
* 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);
+
#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 4245985a74..b572155e9c 100644
--- a/include/crm/common/xml_internal.h
+++ b/include/crm/common/xml_internal.h
@@ -1,403 +1,457 @@
/*
* 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),
};
void pcmk__xml_init(void);
void pcmk__xml_cleanup(void);
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 */
-enum xml_private_flags {
- pcmk__xf_none = 0x0000,
- pcmk__xf_dirty = 0x0001,
- pcmk__xf_deleted = 0x0002,
- pcmk__xf_created = 0x0004,
- pcmk__xf_modified = 0x0008,
-
- pcmk__xf_tracking = 0x0010,
- pcmk__xf_processed = 0x0020,
- pcmk__xf_skip = 0x0040,
- pcmk__xf_moved = 0x0080,
-
- pcmk__xf_acl_enabled = 0x0100,
- pcmk__xf_acl_read = 0x0200,
- pcmk__xf_acl_write = 0x0400,
- pcmk__xf_acl_deny = 0x0800,
-
- pcmk__xf_acl_create = 0x1000,
- pcmk__xf_acl_denied = 0x2000,
- pcmk__xf_lazy = 0x4000,
+/*!
+ * \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
*/
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 2698194680..26a16a7f62 100644
--- a/lib/cib/cib_utils.c
+++ b/lib/cib/cib_utils.c
@@ -1,935 +1,954 @@
/*
* Original copyright 2004 International Business Machines
- * Later changes copyright 2008-2024 the Pacemaker project contributors
+ * 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);
*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 (cib_acl_enabled(cib_ro, user)
+ 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;
- xml_track_changes(scratch, user, NULL, cib_acl_enabled(scratch, user));
+ 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;
- xml_track_changes(scratch, user, NULL, cib_acl_enabled(scratch, user));
+ 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);
- xml_track_changes(scratch, user, *current_cib,
- cib_acl_enabled(*current_cib, user));
- xml_calculate_changes(*current_cib, scratch);
+ 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);
- xml_accept_changes(scratch);
+ pcmk__xml_commit_changes(scratch->doc);
if(local_diff) {
patchset_process_digest(local_diff, patchset_cib, scratch, with_digest);
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 9cdafa7016..e14d3f14c4 100644
--- a/lib/common/acl.c
+++ b/lib/common/acl.c
@@ -1,923 +1,922 @@
/*
* 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 xml_private_flags mode;
- gchar *xpath;
+ 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 xml_private_flags mode)
+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 xml_private_flags flags)
+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;
xmlXPathObject *xpathObj = NULL;
if (!xml_acl_enabled(xml)) {
crm_trace("Skipping ACLs for user '%s' because not enabled for this XML",
docpriv->acl_user);
return;
}
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 xml_private_flags allowed, enum xml_private_flags requested)
+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)) {
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
+ * \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 xml_private_flags mode)
+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)) {
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;
}
diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h
index 03a516f475..2a45e41c41 100644
--- a/lib/common/crmcommon_private.h
+++ b/lib/common/crmcommon_private.h
@@ -1,483 +1,478 @@
/*
* Copyright 2018-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__COMMON_CRMCOMMON_PRIVATE__H
#define PCMK__COMMON_CRMCOMMON_PRIVATE__H
/* This header is for the sole use of libcrmcommon, so that functions can be
* declared with G_GNUC_INTERNAL for efficiency.
*/
#include <stdint.h> // uint8_t, uint32_t
#include <stdbool.h> // bool
#include <sys/types.h> // size_t
#include <glib.h> // G_GNUC_INTERNAL, G_GNUC_PRINTF, gchar, etc.
#include <libxml/tree.h> // xmlNode, xmlAttr
#include <libxml/xmlstring.h> // xmlChar
#include <qb/qbipcc.h> // struct qb_ipc_response_header
#include <crm/common/ipc.h> // pcmk_ipc_api_t, crm_ipc_t, etc.
#include <crm/common/iso8601.h> // crm_time_t
#include <crm/common/logging.h> // LOG_NEVER
#include <crm/common/mainloop.h> // mainloop_io_t
#include <crm/common/output_internal.h> // pcmk__output_t
#include <crm/common/results.h> // crm_exit_t
#include <crm/common/rules.h> // pcmk_rule_input_t
-#include <crm/common/xml_internal.h> // enum xml_private_flags
+#include <crm/common/xml_internal.h> // enum pcmk__xml_flags
#ifdef __cplusplus
extern "C" {
#endif
// Decent chunk size for processing large amounts of data
#define PCMK__BUFFER_SIZE 4096
#if defined(PCMK__UNIT_TESTING)
#undef G_GNUC_INTERNAL
#define G_GNUC_INTERNAL
#endif
/*!
* \internal
* \brief Information about an XML node that was deleted
*
* When change tracking is enabled and we delete an XML node using
* \c pcmk__xml_free(), we free it and add its path and position to a list in
* its document's private data. This allows us to display changes, generate
* patchsets, etc.
*
* Note that this does not happen when deleting an XML attribute using
* \c pcmk__xa_remove(). In that case:
* * If \c force is \c true, we remove the attribute without any tracking.
* * If \c force is \c false, we mark the attribute as deleted but leave it in
* place until we commit changes.
*/
typedef struct pcmk__deleted_xml_s {
gchar *path; //!< XPath expression identifying the deleted node
int position; //!< Position of the deleted node among its siblings
} pcmk__deleted_xml_t;
/*!
* \internal
* \brief Private data for an XML node
*/
typedef struct xml_node_private_s {
uint32_t check; //!< Magic number for checking integrity
- uint32_t flags; //!< Group of <tt>enum xml_private_flags</tt>
+ uint32_t flags; //!< Group of <tt>enum pcmk__xml_flags</tt>
+ xmlNode *match; //!< Pointer to matching node (defined by caller)
} xml_node_private_t;
/*!
* \internal
* \brief Private data for an XML document
*/
typedef struct xml_doc_private_s {
uint32_t check; //!< Magic number for checking integrity
- uint32_t flags; //!< Group of <tt>enum xml_private_flags</tt>
+ uint32_t flags; //!< Group of <tt>enum pcmk__xml_flags</tt>
char *acl_user; //!< User affected by \c acls (for logging)
//! ACLs to check requested changes against (list of \c xml_acl_t)
GList *acls;
//! XML nodes marked as deleted (list of \c pcmk__deleted_xml_t)
GList *deleted_objs;
} xml_doc_private_t;
// XML private data magic numbers
#define PCMK__XML_DOC_PRIVATE_MAGIC 0x81726354UL
#define PCMK__XML_NODE_PRIVATE_MAGIC 0x54637281UL
// XML entity references
#define PCMK__XML_ENTITY_AMP "&amp;"
#define PCMK__XML_ENTITY_GT "&gt;"
#define PCMK__XML_ENTITY_LT "&lt;"
#define PCMK__XML_ENTITY_QUOT "&quot;"
#define pcmk__set_xml_flags(xml_priv, flags_to_set) do { \
(xml_priv)->flags = pcmk__set_flags_as(__func__, __LINE__, \
LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \
(flags_to_set), #flags_to_set); \
} while (0)
#define pcmk__clear_xml_flags(xml_priv, flags_to_clear) do { \
(xml_priv)->flags = pcmk__clear_flags_as(__func__, __LINE__, \
LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \
(flags_to_clear), #flags_to_clear); \
} while (0)
G_GNUC_INTERNAL
const char *pcmk__xml_element_type_text(xmlElementType type);
G_GNUC_INTERNAL
bool pcmk__xml_reset_node_flags(xmlNode *xml, void *user_data);
G_GNUC_INTERNAL
void pcmk__xml_set_parent_flags(xmlNode *xml, uint64_t flags);
G_GNUC_INTERNAL
void pcmk__xml_new_private_data(xmlNode *xml);
G_GNUC_INTERNAL
void pcmk__xml_free_private_data(xmlNode *xml);
G_GNUC_INTERNAL
void pcmk__xml_free_node(xmlNode *xml);
G_GNUC_INTERNAL
xmlDoc *pcmk__xml_new_doc(void);
G_GNUC_INTERNAL
-int pcmk__xml_position(const xmlNode *xml,
- enum xml_private_flags ignore_if_set);
+int pcmk__xml_position(const xmlNode *xml, enum pcmk__xml_flags ignore_if_set);
G_GNUC_INTERNAL
-xmlNode *pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle,
- bool exact);
-
-G_GNUC_INTERNAL
-xmlNode *pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment,
- bool exact);
+bool pcmk__xc_matches(const xmlNode *comment1, const xmlNode *comment2);
G_GNUC_INTERNAL
void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update);
G_GNUC_INTERNAL
void pcmk__free_acls(GList *acls);
G_GNUC_INTERNAL
void pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user);
G_GNUC_INTERNAL
bool pcmk__is_user_in_group(const char *user, const char *group);
G_GNUC_INTERNAL
void pcmk__apply_acl(xmlNode *xml);
G_GNUC_INTERNAL
void pcmk__apply_creation_acl(xmlNode *xml, bool check_top);
G_GNUC_INTERNAL
int pcmk__xa_remove(xmlAttr *attr, bool force);
G_GNUC_INTERNAL
void pcmk__mark_xml_attr_dirty(xmlAttr *a);
G_GNUC_INTERNAL
bool pcmk__xa_filterable(const char *name);
G_GNUC_INTERNAL
void pcmk__log_xmllib_err(void *ctx, const char *fmt, ...)
G_GNUC_PRINTF(2, 3);
G_GNUC_INTERNAL
void pcmk__mark_xml_node_dirty(xmlNode *xml);
G_GNUC_INTERNAL
bool pcmk__marked_as_deleted(xmlAttrPtr a, void *user_data);
G_GNUC_INTERNAL
void pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer);
G_GNUC_INTERNAL
int pcmk__xe_set_score(xmlNode *target, const char *name, const char *value);
G_GNUC_INTERNAL
bool pcmk__xml_is_name_start_char(const char *utf8, int *len);
G_GNUC_INTERNAL
bool pcmk__xml_is_name_char(const char *utf8, int *len);
/*
* Date/times
*/
// For use with pcmk__add_time_from_xml()
enum pcmk__time_component {
pcmk__time_unknown,
pcmk__time_years,
pcmk__time_months,
pcmk__time_weeks,
pcmk__time_days,
pcmk__time_hours,
pcmk__time_minutes,
pcmk__time_seconds,
};
G_GNUC_INTERNAL
const char *pcmk__time_component_attr(enum pcmk__time_component component);
G_GNUC_INTERNAL
int pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component,
const xmlNode *xml);
G_GNUC_INTERNAL
void pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source);
/*
* IPC
*/
#define PCMK__IPC_VERSION 1
#define PCMK__CONTROLD_API_MAJOR "1"
#define PCMK__CONTROLD_API_MINOR "0"
// IPC behavior that varies by daemon
typedef struct pcmk__ipc_methods_s {
/*!
* \internal
* \brief Allocate any private data needed by daemon IPC
*
* \param[in,out] api IPC API connection
*
* \return Standard Pacemaker return code
*/
int (*new_data)(pcmk_ipc_api_t *api);
/*!
* \internal
* \brief Free any private data used by daemon IPC
*
* \param[in,out] api_data Data allocated by new_data() method
*/
void (*free_data)(void *api_data);
/*!
* \internal
* \brief Perform daemon-specific handling after successful connection
*
* Some daemons require clients to register before sending any other
* commands. The controller requires a CRM_OP_HELLO (with no reply), and
* the CIB manager, executor, and fencer require a CRM_OP_REGISTER (with a
* reply). Ideally this would be consistent across all daemons, but for now
* this allows each to do its own authorization.
*
* \param[in,out] api IPC API connection
*
* \return Standard Pacemaker return code
*/
int (*post_connect)(pcmk_ipc_api_t *api);
/*!
* \internal
* \brief Check whether an IPC request results in a reply
*
* \param[in,out] api IPC API connection
* \param[in] request IPC request XML
*
* \return true if request would result in an IPC reply, false otherwise
*/
bool (*reply_expected)(pcmk_ipc_api_t *api, const xmlNode *request);
/*!
* \internal
* \brief Perform daemon-specific handling of an IPC message
*
* \param[in,out] api IPC API connection
* \param[in,out] msg Message read from IPC connection
*
* \return true if more IPC reply messages should be expected
*/
bool (*dispatch)(pcmk_ipc_api_t *api, xmlNode *msg);
/*!
* \internal
* \brief Perform daemon-specific handling of an IPC disconnect
*
* \param[in,out] api IPC API connection
*/
void (*post_disconnect)(pcmk_ipc_api_t *api);
} pcmk__ipc_methods_t;
// Implementation of pcmk_ipc_api_t
struct pcmk_ipc_api_s {
enum pcmk_ipc_server server; // Daemon this IPC API instance is for
enum pcmk_ipc_dispatch dispatch_type; // How replies should be dispatched
crm_ipc_t *ipc; // IPC connection
mainloop_io_t *mainloop_io; // If using mainloop, I/O source for IPC
bool free_on_disconnect; // Whether disconnect should free object
pcmk_ipc_callback_t cb; // Caller-registered callback (if any)
void *user_data; // Caller-registered data (if any)
void *api_data; // For daemon-specific use
pcmk__ipc_methods_t *cmds; // Behavior that varies by daemon
};
typedef struct pcmk__ipc_header_s {
struct qb_ipc_response_header qb;
uint32_t size;
uint32_t flags;
uint8_t version;
} pcmk__ipc_header_t;
G_GNUC_INTERNAL
int pcmk__send_ipc_request(pcmk_ipc_api_t *api, const xmlNode *request);
G_GNUC_INTERNAL
void pcmk__call_ipc_callback(pcmk_ipc_api_t *api,
enum pcmk_ipc_event event_type,
crm_exit_t status, void *event_data);
G_GNUC_INTERNAL
bool pcmk__valid_ipc_header(const pcmk__ipc_header_t *header);
G_GNUC_INTERNAL
pcmk__ipc_methods_t *pcmk__attrd_api_methods(void);
G_GNUC_INTERNAL
pcmk__ipc_methods_t *pcmk__controld_api_methods(void);
G_GNUC_INTERNAL
pcmk__ipc_methods_t *pcmk__pacemakerd_api_methods(void);
G_GNUC_INTERNAL
pcmk__ipc_methods_t *pcmk__schedulerd_api_methods(void);
/*
* Logging
*/
//! XML is newly created
#define PCMK__XML_PREFIX_CREATED "++"
//! XML has been deleted
#define PCMK__XML_PREFIX_DELETED "--"
//! XML has been modified
#define PCMK__XML_PREFIX_MODIFIED "+ "
//! XML has been moved
#define PCMK__XML_PREFIX_MOVED "+~"
/*
* Output
*/
G_GNUC_INTERNAL
int pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name,
const char *filename, char **argv);
G_GNUC_INTERNAL
void pcmk__register_option_messages(pcmk__output_t *out);
G_GNUC_INTERNAL
void pcmk__register_patchset_messages(pcmk__output_t *out);
G_GNUC_INTERNAL
bool pcmk__output_text_get_fancy(pcmk__output_t *out);
/*
* Rules
*/
// How node attribute values may be compared in rules
enum pcmk__comparison {
pcmk__comparison_unknown,
pcmk__comparison_defined,
pcmk__comparison_undefined,
pcmk__comparison_eq,
pcmk__comparison_ne,
pcmk__comparison_lt,
pcmk__comparison_lte,
pcmk__comparison_gt,
pcmk__comparison_gte,
};
// How node attribute values may be parsed in rules
enum pcmk__type {
pcmk__type_unknown,
pcmk__type_string,
pcmk__type_integer,
pcmk__type_number,
pcmk__type_version,
};
// Where to obtain reference value for a node attribute comparison
enum pcmk__reference_source {
pcmk__source_unknown,
pcmk__source_literal,
pcmk__source_instance_attrs,
pcmk__source_meta_attrs,
};
G_GNUC_INTERNAL
enum pcmk__comparison pcmk__parse_comparison(const char *op);
G_GNUC_INTERNAL
enum pcmk__type pcmk__parse_type(const char *type, enum pcmk__comparison op,
const char *value1, const char *value2);
G_GNUC_INTERNAL
enum pcmk__reference_source pcmk__parse_source(const char *source);
G_GNUC_INTERNAL
int pcmk__cmp_by_type(const char *value1, const char *value2,
enum pcmk__type type);
G_GNUC_INTERNAL
int pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start,
crm_time_t **end);
G_GNUC_INTERNAL
int pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now);
G_GNUC_INTERNAL
int pcmk__evaluate_attr_expression(const xmlNode *expression,
const pcmk_rule_input_t *rule_input);
G_GNUC_INTERNAL
int pcmk__evaluate_rsc_expression(const xmlNode *expr,
const pcmk_rule_input_t *rule_input);
G_GNUC_INTERNAL
int pcmk__evaluate_op_expression(const xmlNode *expr,
const pcmk_rule_input_t *rule_input);
/*
* Utils
*/
#define PCMK__PW_BUFFER_LEN 500
/*
* Schemas
*/
typedef struct {
unsigned char v[2];
} pcmk__schema_version_t;
enum pcmk__schema_validator {
pcmk__schema_validator_none,
pcmk__schema_validator_rng
};
typedef struct {
int schema_index;
char *name;
/*!
* List of XSLT stylesheets for upgrading from this schema version to the
* next one. Sorted by the order in which they should be applied to the CIB.
*/
GList *transforms;
void *cache;
enum pcmk__schema_validator validator;
pcmk__schema_version_t version;
} pcmk__schema_t;
G_GNUC_INTERNAL
GList *pcmk__find_x_0_schema(void);
#ifdef __cplusplus
}
#endif
#endif // PCMK__COMMON_CRMCOMMON_PRIVATE__H
diff --git a/lib/common/patchset.c b/lib/common/patchset.c
index 4f1fcbadf7..f4bda8dfc3 100644
--- a/lib/common/patchset.c
+++ b/lib/common/patchset.c
@@ -1,884 +1,885 @@
/*
* 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);
}
void
patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target,
bool with_digest)
{
char *digest = NULL;
if ((patch == NULL) || (source == NULL) || (target == NULL)
|| !with_digest) {
return;
}
- /* We should always call xml_accept_changes() before calculating a digest.
- * Otherwise, with an on-tracking dirty target, we could get a wrong digest.
+ /* 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.
*/
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;
}
// Get CIB versions used for additions and deletions in a patchset
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 -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]);
}
}
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 pcmk_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 };
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);
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;
}
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;
}
diff --git a/lib/common/tests/patchset/pcmk__cib_element_in_patchset_test.c b/lib/common/tests/patchset/pcmk__cib_element_in_patchset_test.c
index 79afff35a7..dfe9e859f9 100644
--- a/lib/common/tests/patchset/pcmk__cib_element_in_patchset_test.c
+++ b/lib/common/tests/patchset/pcmk__cib_element_in_patchset_test.c
@@ -1,243 +1,243 @@
/*
- * Copyright 2024 the Pacemaker project contributors
+ * Copyright 2024-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/common/unittest_internal.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
#define ORIG_CIB \
"<" PCMK_XE_CIB " " PCMK_XA_ADMIN_EPOCH "=\"0\"" \
" " PCMK_XA_EPOCH "=\"0\"" \
" " PCMK_XA_NUM_UPDATES "=\"0\">" \
"<" PCMK_XE_CONFIGURATION ">" \
"<" PCMK_XE_CRM_CONFIG "/>" \
"<" PCMK_XE_NODES ">" \
"<" PCMK_XE_NODE " " PCMK_XA_ID "=\"1\"" \
" " PCMK_XA_UNAME "=\"node-1\"/>" \
"</" PCMK_XE_NODES ">" \
"<" PCMK_XE_RESOURCES "/>" \
"<" PCMK_XE_CONSTRAINTS "/>" \
"</" PCMK_XE_CONFIGURATION ">" \
"<" PCMK_XE_STATUS "/>" \
"</" PCMK_XE_CIB ">"
static void
assert_in_patchset(const char *source_s, const char *target_s,
const char *element, bool reference)
{
xmlNode *source = pcmk__xml_parse(source_s);
xmlNode *target = pcmk__xml_parse(target_s);
xmlNode *patchset = NULL;
- xml_track_changes(target, NULL, NULL, false);
- xml_calculate_significant_changes(source, target);
+ pcmk__xml_doc_set_flags(target->doc, pcmk__xf_ignore_attr_pos);
+ pcmk__xml_mark_changes(source, target);
patchset = xml_create_patchset(2, source, target, NULL, false);
if (reference) {
assert_true(pcmk__cib_element_in_patchset(patchset, element));
} else {
assert_false(pcmk__cib_element_in_patchset(patchset, element));
}
pcmk__xml_free(source);
pcmk__xml_free(target);
pcmk__xml_free(patchset);
}
static void
null_patchset_asserts(void **state)
{
pcmk__assert_asserts(pcmk__cib_element_in_patchset(NULL, NULL));
pcmk__assert_asserts(pcmk__cib_element_in_patchset(NULL, PCMK_XE_NODES));
}
// PCMK_XE_ALERTS element has been created relative to ORIG_CIB
#define CREATE_CIB \
"<" PCMK_XE_CIB " " PCMK_XA_ADMIN_EPOCH "=\"0\"" \
" " PCMK_XA_EPOCH "=\"0\"" \
" " PCMK_XA_NUM_UPDATES "=\"0\">" \
"<" PCMK_XE_CONFIGURATION ">" \
"<" PCMK_XE_CRM_CONFIG "/>" \
"<" PCMK_XE_NODES ">" \
"<" PCMK_XE_NODE " " PCMK_XA_ID "=\"1\"" \
" " PCMK_XA_UNAME "=\"node-1\"/>" \
"</" PCMK_XE_NODES ">" \
"<" PCMK_XE_RESOURCES "/>" \
"<" PCMK_XE_CONSTRAINTS "/>" \
"<" PCMK_XE_ALERTS "/>" \
"</" PCMK_XE_CONFIGURATION ">" \
"<" PCMK_XE_STATUS "/>" \
"</" PCMK_XE_CIB ">"
static void
create_op(void **state)
{
// Requested element was created
assert_in_patchset(ORIG_CIB, CREATE_CIB, PCMK_XE_ALERTS, true);
// Requested element's descendant was created
assert_in_patchset(ORIG_CIB, CREATE_CIB, PCMK_XE_CONFIGURATION, true);
assert_in_patchset(ORIG_CIB, CREATE_CIB, NULL, true);
// Requested element was not changed
assert_in_patchset(ORIG_CIB, CREATE_CIB, PCMK_XE_STATUS, false);
}
static void
delete_op(void **state)
{
// Requested element was deleted
assert_in_patchset(CREATE_CIB, ORIG_CIB, PCMK_XE_ALERTS, true);
// Requested element's descendant was deleted
assert_in_patchset(CREATE_CIB, ORIG_CIB, PCMK_XE_CONFIGURATION, true);
assert_in_patchset(CREATE_CIB, ORIG_CIB, NULL, true);
// Requested element was not changed
assert_in_patchset(CREATE_CIB, ORIG_CIB, PCMK_XE_STATUS, false);
}
// PCMK_XE_CIB XML attribute was added relative to ORIG_CIB
#define MODIFY_ADD_CIB \
"<" PCMK_XE_CIB " " PCMK_XA_ADMIN_EPOCH "=\"0\"" \
" " PCMK_XA_EPOCH "=\"0\"" \
" " PCMK_XA_NUM_UPDATES "=\"0\"" \
" " PCMK_XA_CRM_FEATURE_SET "=\"3.19.7\">" \
"<" PCMK_XE_CONFIGURATION ">" \
"<" PCMK_XE_CRM_CONFIG "/>" \
"<" PCMK_XE_NODES ">" \
"<" PCMK_XE_NODE " " PCMK_XA_ID "=\"1\"" \
" " PCMK_XA_UNAME "=\"node-1\"/>" \
"</" PCMK_XE_NODES ">" \
"<" PCMK_XE_RESOURCES "/>" \
"<" PCMK_XE_CONSTRAINTS "/>" \
"</" PCMK_XE_CONFIGURATION ">" \
"<" PCMK_XE_STATUS "/>" \
"</" PCMK_XE_CIB ">"
// PCMK_XE_CIB XML attribute was updated relative to ORIG_CIB
#define MODIFY_UPDATE_CIB \
"<" PCMK_XE_CIB " " PCMK_XA_ADMIN_EPOCH "=\"0\"" \
" " PCMK_XA_EPOCH "=\"0\"" \
" " PCMK_XA_NUM_UPDATES "=\"1\">" \
"<" PCMK_XE_CONFIGURATION ">" \
"<" PCMK_XE_CRM_CONFIG "/>" \
"<" PCMK_XE_NODES ">" \
"<" PCMK_XE_NODE " " PCMK_XA_ID "=\"1\"" \
" " PCMK_XA_UNAME "=\"node-1\"/>" \
"</" PCMK_XE_NODES ">" \
"<" PCMK_XE_RESOURCES "/>" \
"<" PCMK_XE_CONSTRAINTS "/>" \
"</" PCMK_XE_CONFIGURATION ">" \
"<" PCMK_XE_STATUS "/>" \
"</" PCMK_XE_CIB ">"
// PCMK_XE_NODE XML attribute was added relative to ORIG_CIB
#define MODIFY_ADD_NODE_CIB \
"<" PCMK_XE_CIB " " PCMK_XA_ADMIN_EPOCH "=\"0\"" \
" " PCMK_XA_EPOCH "=\"0\"" \
" " PCMK_XA_NUM_UPDATES "=\"0\">" \
"<" PCMK_XE_CONFIGURATION ">" \
"<" PCMK_XE_CRM_CONFIG "/>" \
"<" PCMK_XE_NODES ">" \
"<" PCMK_XE_NODE " " PCMK_XA_ID "=\"1\"" \
" " PCMK_XA_UNAME "=\"node-1\"" \
" " PCMK_XA_TYPE "=\"member\"/>" \
"</" PCMK_XE_NODES ">" \
"<" PCMK_XE_RESOURCES "/>" \
"<" PCMK_XE_CONSTRAINTS "/>" \
"</" PCMK_XE_CONFIGURATION ">" \
"<" PCMK_XE_STATUS "/>" \
"</" PCMK_XE_CIB ">"
// PCMK_XE_NODE XML attribute was updated relative to ORIG_CIB
#define MODIFY_UPDATE_NODE_CIB \
"<" PCMK_XE_CIB " " PCMK_XA_ADMIN_EPOCH "=\"0\"" \
" " PCMK_XA_EPOCH "=\"0\"" \
" " PCMK_XA_NUM_UPDATES "=\"0\">" \
"<" PCMK_XE_CONFIGURATION ">" \
"<" PCMK_XE_CRM_CONFIG "/>" \
"<" PCMK_XE_NODES ">" \
"<" PCMK_XE_NODE " " PCMK_XA_ID "=\"1\"" \
" " PCMK_XA_UNAME "=\"node-2\"/>" \
"</" PCMK_XE_NODES ">" \
"<" PCMK_XE_RESOURCES "/>" \
"<" PCMK_XE_CONSTRAINTS "/>" \
"</" PCMK_XE_CONFIGURATION ">" \
"<" PCMK_XE_STATUS "/>" \
"</" PCMK_XE_CIB ">"
static void
modify_op(void **state)
{
// Requested element was modified (attribute added)
assert_in_patchset(ORIG_CIB, MODIFY_ADD_CIB, PCMK_XE_CIB, true);
// Requested element was modified (attribute updated)
assert_in_patchset(ORIG_CIB, MODIFY_UPDATE_CIB, PCMK_XE_CIB, true);
// Requested element was modified (attribute deleted)
assert_in_patchset(MODIFY_ADD_CIB, ORIG_CIB, PCMK_XE_CIB, true);
// Requested element's descendant was modified (attribute added)
assert_in_patchset(ORIG_CIB, MODIFY_ADD_NODE_CIB, PCMK_XE_CIB, true);
assert_in_patchset(ORIG_CIB, MODIFY_ADD_NODE_CIB, NULL, true);
// Requested element's descendant was modified (attribute updated)
assert_in_patchset(ORIG_CIB, MODIFY_UPDATE_NODE_CIB, PCMK_XE_CIB, true);
assert_in_patchset(ORIG_CIB, MODIFY_UPDATE_NODE_CIB, NULL, true);
// Requested element's descenant was modified (attribute deleted)
assert_in_patchset(MODIFY_ADD_NODE_CIB, ORIG_CIB, PCMK_XE_CIB, true);
assert_in_patchset(MODIFY_ADD_NODE_CIB, ORIG_CIB, NULL, true);
// Requested element was not changed
assert_in_patchset(ORIG_CIB, MODIFY_ADD_CIB, PCMK_XE_STATUS, false);
assert_in_patchset(ORIG_CIB, MODIFY_UPDATE_CIB, PCMK_XE_STATUS, false);
assert_in_patchset(ORIG_CIB, MODIFY_ADD_NODE_CIB, PCMK_XE_STATUS, false);
assert_in_patchset(ORIG_CIB, MODIFY_UPDATE_NODE_CIB, PCMK_XE_STATUS, false);
}
// PCMK_XE_RESOURCES and PCMK_XE_CONSTRAINTS are swapped relative to ORIG_CIB
#define MOVE_CIB \
"<" PCMK_XE_CIB " " PCMK_XA_ADMIN_EPOCH "=\"0\"" \
" " PCMK_XA_EPOCH "=\"0\"" \
" " PCMK_XA_NUM_UPDATES "=\"0\">" \
"<" PCMK_XE_CONFIGURATION ">" \
"<" PCMK_XE_CRM_CONFIG "/>" \
"<" PCMK_XE_NODES "/>" \
"<" PCMK_XE_CONSTRAINTS "/>" \
"<" PCMK_XE_RESOURCES "/>" \
"</" PCMK_XE_CONFIGURATION ">" \
"<" PCMK_XE_STATUS "/>" \
"</" PCMK_XE_CIB ">"
static void
move_op(void **state)
{
// Requested element was moved
assert_in_patchset(ORIG_CIB, MOVE_CIB, PCMK_XE_RESOURCES, true);
assert_in_patchset(ORIG_CIB, MOVE_CIB, PCMK_XE_CONSTRAINTS, true);
// Requested element's descendant was moved
assert_in_patchset(ORIG_CIB, MOVE_CIB, PCMK_XE_CONFIGURATION, true);
assert_in_patchset(ORIG_CIB, MOVE_CIB, NULL, true);
// Requested element was not changed
assert_in_patchset(ORIG_CIB, MOVE_CIB, PCMK_XE_STATUS, false);
}
PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group,
cmocka_unit_test(null_patchset_asserts),
cmocka_unit_test(create_op),
cmocka_unit_test(delete_op),
cmocka_unit_test(modify_op),
cmocka_unit_test(move_op))
diff --git a/lib/common/tests/xml/pcmk__xml_new_doc_test.c b/lib/common/tests/xml/pcmk__xml_new_doc_test.c
index 762aec4007..28b99b77e2 100644
--- a/lib/common/tests/xml/pcmk__xml_new_doc_test.c
+++ b/lib/common/tests/xml/pcmk__xml_new_doc_test.c
@@ -1,39 +1,38 @@
/*
- * Copyright 2024 the Pacemaker project contributors
+ * Copyright 2024-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/common/unittest_internal.h>
#include "crmcommon_private.h"
/* This tests new_private_data() indirectly for document nodes. Testing
* free_private_data() would be much less straightforward and is not worth the
* hassle.
*/
static void
create_document_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xmlDoc *doc = pcmk__xml_new_doc();
assert_non_null(doc);
assert_int_equal(doc->type, XML_DOCUMENT_NODE);
docpriv = doc->_private;
assert_non_null(docpriv);
assert_int_equal(docpriv->check, PCMK__XML_DOC_PRIVATE_MAGIC);
- assert_true(pcmk_all_flags_set(docpriv->flags,
- pcmk__xf_dirty|pcmk__xf_created));
+ assert_int_equal(docpriv->flags, pcmk__xf_none);
pcmk__xml_free_doc(doc);
}
PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group,
cmocka_unit_test(create_document_node))
diff --git a/lib/common/tests/xml_comment/pcmk__xc_create_test.c b/lib/common/tests/xml_comment/pcmk__xc_create_test.c
index 4e25adc130..a16a7da6f8 100644
--- a/lib/common/tests/xml_comment/pcmk__xc_create_test.c
+++ b/lib/common/tests/xml_comment/pcmk__xc_create_test.c
@@ -1,77 +1,71 @@
/*
- * Copyright 2024 the Pacemaker project contributors
+ * Copyright 2024-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/common/unittest_internal.h>
#include "crmcommon_private.h"
/* This tests new_private_data() indirectly for comment nodes. Testing
* free_private_data() would be much less straightforward and is not worth the
* hassle.
*/
static void
-assert_comment(xmlDoc *doc, const char *content)
+assert_comment(const char *content)
{
+ xmlDoc *doc = pcmk__xml_new_doc();
+ xml_doc_private_t *docpriv = doc->_private;
xmlNode *node = NULL;
xml_node_private_t *nodepriv = NULL;
- xml_doc_private_t *docpriv = doc->_private;
- // Also clears existing doc flags
- xml_track_changes((xmlNode *) doc, NULL, NULL, false);
+ pcmk__xml_doc_set_flags(doc, pcmk__xf_tracking);
node = pcmk__xc_create(doc, content);
assert_non_null(node);
assert_int_equal(node->type, XML_COMMENT_NODE);
assert_ptr_equal(node->doc, doc);
if (content == NULL) {
assert_null(node->content);
} else {
assert_non_null(node->content);
assert_string_equal((const char *) node->content, content);
}
nodepriv = node->_private;
assert_non_null(nodepriv);
assert_int_equal(nodepriv->check, PCMK__XML_NODE_PRIVATE_MAGIC);
assert_true(pcmk_all_flags_set(nodepriv->flags,
pcmk__xf_dirty|pcmk__xf_created));
assert_true(pcmk_is_set(docpriv->flags, pcmk__xf_dirty));
pcmk__xml_free(node);
+ pcmk__xml_free_doc(doc);
}
static void
null_doc(void **state)
{
pcmk__assert_asserts(pcmk__xc_create(NULL, NULL));
pcmk__assert_asserts(pcmk__xc_create(NULL, "some content"));
}
static void
with_doc(void **state)
{
- xmlDoc *doc = pcmk__xml_new_doc();
-
- assert_non_null(doc);
- assert_non_null(doc->_private);
-
- assert_comment(doc, NULL);
- assert_comment(doc, "some content");
-
- pcmk__xml_free_doc(doc);
+ assert_comment(NULL);
+ assert_comment("some content");
}
PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group,
cmocka_unit_test(null_doc),
cmocka_unit_test(with_doc));
diff --git a/lib/common/tests/xml_element/pcmk__xe_sort_attrs_test.c b/lib/common/tests/xml_element/pcmk__xe_sort_attrs_test.c
index b5265b1d49..106d5bc34a 100644
--- a/lib/common/tests/xml_element/pcmk__xe_sort_attrs_test.c
+++ b/lib/common/tests/xml_element/pcmk__xe_sort_attrs_test.c
@@ -1,203 +1,203 @@
/*
* Copyright 2024-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/common/unittest_internal.h>
#include <glib.h> // GHashTable, etc.
#include <libxml/tree.h> // xmlNode
#include <libxml/xmlstring.h> // xmlChar
#include "crmcommon_private.h" // xml_node_private_t
/*!
* \internal
* \brief Sort an XML element's attributes and compare against a reference
*
* This also verifies that any flags set on the original attributes are
* preserved.
*
* \param[in,out] test_xml XML whose attributes to sort
* \param[in] reference_xml XML whose attribute order to compare against
* (attributes must have the same values as in
* \p test_xml)
*/
static void
assert_order(xmlNode *test_xml, const xmlNode *reference_xml)
{
GHashTable *attr_flags = pcmk__strkey_table(free, NULL);
xmlAttr *test_attr = NULL;
xmlAttr *ref_attr = NULL;
// Save original flags
for (xmlAttr *attr = pcmk__xe_first_attr(test_xml); attr != NULL;
attr = attr->next) {
xml_node_private_t *nodepriv = attr->_private;
uint32_t flags = (nodepriv != NULL)? nodepriv->flags : pcmk__xf_none;
g_hash_table_insert(attr_flags,
pcmk__str_copy((const char *) attr->name),
GUINT_TO_POINTER((guint) flags));
}
pcmk__xe_sort_attrs(test_xml);
test_attr = pcmk__xe_first_attr(test_xml);
ref_attr = pcmk__xe_first_attr(reference_xml);
for (; (test_attr != NULL) && (ref_attr != NULL);
test_attr = test_attr->next, ref_attr = ref_attr->next) {
const char *test_name = (const char *) test_attr->name;
xml_node_private_t *nodepriv = test_attr->_private;
uint32_t flags = (nodepriv != NULL)? nodepriv->flags : pcmk__xf_none;
gpointer old_flags_ptr = g_hash_table_lookup(attr_flags, test_name);
uint32_t old_flags = pcmk__xf_none;
if (old_flags_ptr != NULL) {
old_flags = GPOINTER_TO_UINT(old_flags_ptr);
}
// Flags must not change
assert_true(flags == old_flags);
// Attributes must be in expected order with expected values
assert_string_equal(test_name, (const char *) ref_attr->name);
assert_string_equal(pcmk__xml_attr_value(test_attr),
pcmk__xml_attr_value(ref_attr));
}
// Attribute lists must be the same length
assert_null(test_attr);
assert_null(ref_attr);
g_hash_table_destroy(attr_flags);
}
static void
null_arg(void **state)
{
// Ensure it doesn't crash
pcmk__xe_sort_attrs(NULL);
}
static void
nothing_to_sort(void **state)
{
xmlNode *test_xml = pcmk__xe_create(NULL, "test");
xmlNode *reference_xml = NULL;
// No attributes
reference_xml = pcmk__xml_copy(NULL, test_xml);
assert_order(test_xml, reference_xml);
pcmk__xml_free(reference_xml);
// Only one attribute
crm_xml_add(test_xml, "name", "value");
reference_xml = pcmk__xml_copy(NULL, test_xml);
assert_order(test_xml, reference_xml);
pcmk__xml_free(reference_xml);
pcmk__xml_free(test_xml);
}
static void
already_sorted(void **state)
{
xmlNode *test_xml = pcmk__xe_create(NULL, "test");
xmlNode *reference_xml = pcmk__xe_create(NULL, "test");
xmlAttr *attr = NULL;
crm_xml_add(test_xml, "admin", "john");
crm_xml_add(test_xml, "dummy", "value");
crm_xml_add(test_xml, "location", "usa");
// Set flags in test_xml's attributes for testing flag preservation
attr = xmlHasProp(test_xml, (const xmlChar *) "admin");
if (attr != NULL) {
xml_node_private_t *nodepriv = attr->_private;
if (nodepriv != NULL) {
pcmk__clear_xml_flags(nodepriv, pcmk__xf_created|pcmk__xf_dirty);
}
}
attr = xmlHasProp(test_xml, (const xmlChar *) "location");
if (attr != NULL) {
xml_node_private_t *nodepriv = attr->_private;
if (nodepriv != NULL) {
- pcmk__set_xml_flags(nodepriv, pcmk__xf_lazy);
+ pcmk__set_xml_flags(nodepriv, pcmk__xf_ignore_attr_pos);
}
}
pcmk__xe_set_props(reference_xml,
"admin", "john",
"dummy", "value",
"location", "usa",
NULL);
assert_order(test_xml, reference_xml);
pcmk__xml_free(test_xml);
pcmk__xml_free(reference_xml);
}
static void
need_sort(void **state)
{
xmlNode *test_xml = pcmk__xe_create(NULL, "test");
xmlNode *reference_xml = pcmk__xe_create(NULL, "test");
xmlAttr *attr = NULL;
crm_xml_add(test_xml, "location", "usa");
crm_xml_add(test_xml, "admin", "john");
crm_xml_add(test_xml, "dummy", "value");
// Set flags in test_xml's attributes for testing flag preservation
attr = xmlHasProp(test_xml, (const xmlChar *) "location");
if (attr != NULL) {
xml_node_private_t *nodepriv = attr->_private;
if (nodepriv != NULL) {
- pcmk__set_xml_flags(nodepriv, pcmk__xf_lazy);
+ pcmk__set_xml_flags(nodepriv, pcmk__xf_ignore_attr_pos);
}
}
attr = xmlHasProp(test_xml, (const xmlChar *) "admin");
if (attr != NULL) {
xml_node_private_t *nodepriv = attr->_private;
if (nodepriv != NULL) {
pcmk__clear_xml_flags(nodepriv, pcmk__xf_created|pcmk__xf_dirty);
}
}
pcmk__xe_set_props(reference_xml,
"admin", "john",
"dummy", "value",
"location", "usa",
NULL);
assert_order(test_xml, reference_xml);
pcmk__xml_free(test_xml);
pcmk__xml_free(reference_xml);
}
PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group,
cmocka_unit_test(null_arg),
cmocka_unit_test(nothing_to_sort),
cmocka_unit_test(already_sorted),
cmocka_unit_test(need_sort))
diff --git a/lib/common/xml.c b/lib/common/xml.c
index 74bf14aa84..0c623b27ae 100644
--- a/lib/common/xml.c
+++ b/lib/common/xml.c
@@ -1,1660 +1,1904 @@
/*
* 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 <stdarg.h>
#include <stdint.h> // uint32_t
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h> // stat(), S_ISREG, etc.
#include <sys/types.h>
#include <glib.h> // gboolean, GString
#include <libxml/parser.h> // xmlCleanupParser()
#include <libxml/tree.h> // xmlNode, etc.
#include <libxml/xmlstring.h> // xmlChar, xmlGetUTF8Char()
#include <crm/crm.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc.
#include "crmcommon_private.h"
//! libxml2 supports only XML version 1.0, at least as of libxml2-2.12.5
#define XML_VERSION ((const xmlChar *) "1.0")
/*!
* \internal
* \brief Get a string representation of an XML element type for logging
*
* \param[in] type XML element type
*
* \return String representation of \p type
*/
const char *
pcmk__xml_element_type_text(xmlElementType type)
{
static const char *const element_type_names[] = {
[XML_ELEMENT_NODE] = "element",
[XML_ATTRIBUTE_NODE] = "attribute",
[XML_TEXT_NODE] = "text",
[XML_CDATA_SECTION_NODE] = "CDATA section",
[XML_ENTITY_REF_NODE] = "entity reference",
[XML_ENTITY_NODE] = "entity",
[XML_PI_NODE] = "PI",
[XML_COMMENT_NODE] = "comment",
[XML_DOCUMENT_NODE] = "document",
[XML_DOCUMENT_TYPE_NODE] = "document type",
[XML_DOCUMENT_FRAG_NODE] = "document fragment",
[XML_NOTATION_NODE] = "notation",
[XML_HTML_DOCUMENT_NODE] = "HTML document",
[XML_DTD_NODE] = "DTD",
[XML_ELEMENT_DECL] = "element declaration",
[XML_ATTRIBUTE_DECL] = "attribute declaration",
[XML_ENTITY_DECL] = "entity declaration",
[XML_NAMESPACE_DECL] = "namespace declaration",
[XML_XINCLUDE_START] = "XInclude start",
[XML_XINCLUDE_END] = "XInclude end",
};
// Assumes the numeric values of the indices are in ascending order
if ((type < XML_ELEMENT_NODE) || (type > XML_XINCLUDE_END)) {
return "unrecognized type";
}
return element_type_names[type];
}
/*!
* \internal
* \brief Apply a function to each XML node in a tree (pre-order, depth-first)
*
* \param[in,out] xml XML tree to traverse
* \param[in,out] fn Function to call for each node (returns \c true to
* continue traversing the tree or \c false to stop)
* \param[in,out] user_data Argument to \p fn
*
* \return \c false if any \p fn call returned \c false, or \c true otherwise
*
* \note This function is recursive.
*/
bool
pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *),
void *user_data)
{
if (xml == NULL) {
return true;
}
if (!fn(xml, user_data)) {
return false;
}
for (xml = pcmk__xml_first_child(xml); xml != NULL;
xml = pcmk__xml_next(xml)) {
if (!pcmk__xml_tree_foreach(xml, fn, user_data)) {
return false;
}
}
return true;
}
void
pcmk__xml_set_parent_flags(xmlNode *xml, uint64_t flags)
{
for (; xml != NULL; xml = xml->parent) {
xml_node_private_t *nodepriv = xml->_private;
if (nodepriv != NULL) {
pcmk__set_xml_flags(nodepriv, flags);
}
}
}
/*!
* \internal
* \brief Set flags for an XML document
*
* \param[in,out] doc XML document
- * \param[in] flags Group of <tt>enum xml_private_flags</tt>
+ * \param[in] flags Group of <tt>enum pcmk__xml_flags</tt>
*/
void
pcmk__xml_doc_set_flags(xmlDoc *doc, uint32_t flags)
{
if (doc != NULL) {
xml_doc_private_t *docpriv = doc->_private;
pcmk__set_xml_flags(docpriv, flags);
}
}
/*!
* \internal
* \brief Check whether the given flags are set for an XML document
*
* \param[in] doc XML document to check
- * \param[in] flags Group of <tt>enum xml_private_flags</tt>
+ * \param[in] flags Group of <tt>enum pcmk__xml_flags</tt>
*
* \return \c true if all of \p flags are set for \p doc, or \c false otherwise
*/
bool
pcmk__xml_doc_all_flags_set(const xmlDoc *doc, uint32_t flags)
{
if (doc != NULL) {
xml_doc_private_t *docpriv = doc->_private;
return (docpriv != NULL) && pcmk_all_flags_set(docpriv->flags, flags);
}
return false;
}
// Mark document, element, and all element's parents as changed
void
pcmk__mark_xml_node_dirty(xmlNode *xml)
{
if (xml == NULL) {
return;
}
pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_dirty);
pcmk__xml_set_parent_flags(xml, pcmk__xf_dirty);
}
/*!
* \internal
* \brief Clear flags on an XML node
*
* \param[in,out] xml XML node whose flags to reset
* \param[in,out] user_data Ignored
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
bool
pcmk__xml_reset_node_flags(xmlNode *xml, void *user_data)
{
xml_node_private_t *nodepriv = xml->_private;
if (nodepriv != NULL) {
nodepriv->flags = pcmk__xf_none;
}
return true;
}
/*!
* \internal
* \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node
*
* \param[in,out] xml Node whose flags to set
* \param[in] user_data Ignored
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
mark_xml_dirty_created(xmlNode *xml, void *user_data)
{
xml_node_private_t *nodepriv = xml->_private;
if (nodepriv != NULL) {
pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
}
return true;
}
/*!
* \internal
* \brief Mark an XML tree as dirty and created, and mark its parents dirty
*
* Also mark the document dirty.
*
* \param[in,out] xml Tree to mark as dirty and created
*/
static void
mark_xml_tree_dirty_created(xmlNode *xml)
{
pcmk__assert(xml != NULL);
if (!pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_tracking)) {
// Tracking is disabled for entire document
return;
}
// Mark all parents and document dirty
pcmk__mark_xml_node_dirty(xml);
pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL);
}
// Free an XML object previously marked as deleted
static void
free_deleted_object(void *data)
{
if(data) {
pcmk__deleted_xml_t *deleted_obj = data;
g_free(deleted_obj->path);
free(deleted_obj);
}
}
// Free and NULL user, ACLs, and deleted objects in an XML node's private data
static void
reset_xml_private_data(xml_doc_private_t *docpriv)
{
if (docpriv != NULL) {
pcmk__assert(docpriv->check == PCMK__XML_DOC_PRIVATE_MAGIC);
pcmk__str_update(&(docpriv->acl_user), NULL);
if (docpriv->acls != NULL) {
pcmk__free_acls(docpriv->acls);
docpriv->acls = NULL;
}
if(docpriv->deleted_objs) {
g_list_free_full(docpriv->deleted_objs, free_deleted_object);
docpriv->deleted_objs = NULL;
}
}
}
/*!
* \internal
* \brief Allocate and initialize private data for an XML node
*
* \param[in,out] node XML node whose private data to initialize
* \param[in] user_data Ignored
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
new_private_data(xmlNode *node, void *user_data)
{
+ bool tracking = false;
+
CRM_CHECK(node != NULL, return true);
if (node->_private != NULL) {
return true;
}
+ tracking = pcmk__xml_doc_all_flags_set(node->doc, pcmk__xf_tracking);
+
switch (node->type) {
case XML_DOCUMENT_NODE:
{
xml_doc_private_t *docpriv =
pcmk__assert_alloc(1, sizeof(xml_doc_private_t));
docpriv->check = PCMK__XML_DOC_PRIVATE_MAGIC;
node->_private = docpriv;
- pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created);
}
break;
case XML_ELEMENT_NODE:
case XML_ATTRIBUTE_NODE:
case XML_COMMENT_NODE:
{
xml_node_private_t *nodepriv =
pcmk__assert_alloc(1, sizeof(xml_node_private_t));
nodepriv->check = PCMK__XML_NODE_PRIVATE_MAGIC;
node->_private = nodepriv;
- pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
+ if (tracking) {
+ pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
+ }
for (xmlAttr *iter = pcmk__xe_first_attr(node); iter != NULL;
iter = iter->next) {
new_private_data((xmlNode *) iter, user_data);
}
}
break;
case XML_TEXT_NODE:
case XML_DTD_NODE:
case XML_CDATA_SECTION_NODE:
return true;
default:
CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE);
return true;
}
- if (pcmk__xml_doc_all_flags_set(node->doc, pcmk__xf_tracking)) {
+ if (tracking) {
pcmk__mark_xml_node_dirty(node);
}
return true;
}
/*!
* \internal
* \brief Free private data for an XML node
*
* \param[in,out] node XML node whose private data to free
* \param[in] user_data Ignored
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
free_private_data(xmlNode *node, void *user_data)
{
CRM_CHECK(node != NULL, return true);
if (node->_private == NULL) {
return true;
}
if (node->type == XML_DOCUMENT_NODE) {
reset_xml_private_data((xml_doc_private_t *) node->_private);
} else {
xml_node_private_t *nodepriv = node->_private;
pcmk__assert(nodepriv->check == PCMK__XML_NODE_PRIVATE_MAGIC);
for (xmlAttr *iter = pcmk__xe_first_attr(node); iter != NULL;
iter = iter->next) {
free_private_data((xmlNode *) iter, user_data);
}
}
free(node->_private);
node->_private = NULL;
return true;
}
/*!
* \internal
* \brief Allocate and initialize private data recursively for an XML tree
*
* \param[in,out] node XML node whose private data to initialize
*/
void
pcmk__xml_new_private_data(xmlNode *xml)
{
pcmk__xml_tree_foreach(xml, new_private_data, NULL);
}
/*!
* \internal
* \brief Free private data recursively for an XML tree
*
* \param[in,out] node XML node whose private data to free
*/
void
pcmk__xml_free_private_data(xmlNode *xml)
{
pcmk__xml_tree_foreach(xml, free_private_data, NULL);
}
-void
-xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls)
-{
- if (xml == NULL) {
- return;
- }
-
- xml_accept_changes(xml);
- crm_trace("Tracking changes%s to %p", enforce_acls?" with ACLs":"", xml);
- pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_tracking);
- if(enforce_acls) {
- if(acl_source == NULL) {
- acl_source = xml;
- }
- pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_acl_enabled);
- pcmk__unpack_acl(acl_source, xml, user);
- pcmk__apply_acl(xml);
- }
-}
-
/*!
* \internal
* \brief Return ordinal position of an XML node among its siblings
*
* \param[in] xml XML node to check
* \param[in] ignore_if_set Don't count siblings with this flag set
*
* \return Ordinal position of \p xml (starting with 0)
*/
int
-pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set)
+pcmk__xml_position(const xmlNode *xml, enum pcmk__xml_flags ignore_if_set)
{
int position = 0;
for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) {
xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private;
if (!pcmk_is_set(nodepriv->flags, ignore_if_set)) {
position++;
}
}
return position;
}
/*!
* \internal
* \brief Remove all attributes marked as deleted from an XML node
*
* \param[in,out] xml XML node whose deleted attributes to remove
* \param[in,out] user_data Ignored
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
-accept_attr_deletions(xmlNode *xml, void *user_data)
+commit_attr_deletions(xmlNode *xml, void *user_data)
{
pcmk__xml_reset_node_flags(xml, NULL);
pcmk__xe_remove_matching_attrs(xml, true, pcmk__marked_as_deleted, NULL);
return true;
}
/*!
* \internal
- * \brief Find first child XML node matching another given XML node
- *
- * \param[in] haystack XML whose children should be checked
- * \param[in] needle XML to match (comment content or element name and ID)
- * \param[in] exact If true and needle is a comment, position must match
+ * \brief Finalize all pending changes to an XML document and reset private data
+ *
+ * Clear the ACL user and all flags, unpacked ACLs, and deleted node records for
+ * the document; clear all flags on each node in the tree; and delete any
+ * attributes that are marked for deletion.
+ *
+ * \param[in,out] doc XML document
+ *
+ * \note When change tracking is enabled, "deleting" an attribute simply marks
+ * it for deletion (using \c pcmk__xf_deleted) until changes are
+ * committed. Freeing a node (using \c pcmk__xml_free()) adds a deleted
+ * node record (\c pcmk__deleted_xml_t) to the node's document before
+ * freeing it.
+ * \note This function clears all flags, not just flags that indicate changes.
+ * In particular, note that it clears the \c pcmk__xf_tracking flag, thus
+ * disabling tracking.
*/
-xmlNode *
-pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact)
-{
- CRM_CHECK(needle != NULL, return NULL);
-
- if (needle->type == XML_COMMENT_NODE) {
- return pcmk__xc_match(haystack, needle, exact);
-
- } else {
- const char *id = pcmk__xe_id(needle);
- const char *attr = (id == NULL)? NULL : PCMK_XA_ID;
-
- return pcmk__xe_first_child(haystack, (const char *) needle->name, attr,
- id);
- }
-}
-
void
-xml_accept_changes(xmlNode * xml)
+pcmk__xml_commit_changes(xmlDoc *doc)
{
- xmlNode *top = NULL;
xml_doc_private_t *docpriv = NULL;
- if(xml == NULL) {
+ if (doc == NULL) {
return;
}
- crm_trace("Accepting changes to %p", xml);
- docpriv = xml->doc->_private;
- top = xmlDocGetRootElement(xml->doc);
+ docpriv = doc->_private;
+ if (docpriv == NULL) {
+ return;
+ }
if (pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) {
- pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL);
+ pcmk__xml_tree_foreach(xmlDocGetRootElement(doc), commit_attr_deletions,
+ NULL);
}
- reset_xml_private_data(xml->doc->_private);
+ reset_xml_private_data(docpriv);
docpriv->flags = pcmk__xf_none;
}
/*!
* \internal
* \brief Create a new XML document
*
* \return Newly allocated XML document (guaranteed not to be \c NULL)
*
* \note The caller is responsible for freeing the return value using
* \c pcmk__xml_free_doc().
*/
xmlDoc *
pcmk__xml_new_doc(void)
{
xmlDoc *doc = xmlNewDoc(XML_VERSION);
pcmk__mem_assert(doc);
pcmk__xml_new_private_data((xmlNode *) doc);
return doc;
}
/*!
* \internal
* \brief Free a new XML document
*
* \param[in,out] doc XML document to free
*/
void
pcmk__xml_free_doc(xmlDoc *doc)
{
if (doc != NULL) {
pcmk__xml_free_private_data((xmlNode *) doc);
xmlFreeDoc(doc);
}
}
/*!
* \internal
* \brief Check whether the first character of a string is an XML NameStartChar
*
* See https://www.w3.org/TR/xml/#NT-NameStartChar.
*
* This is almost identical to libxml2's \c xmlIsDocNameStartChar(), but they
* don't expose it as part of the public API.
*
* \param[in] utf8 UTF-8 encoded string
* \param[out] len If not \c NULL, where to store size in bytes of first
* character in \p utf8
*
* \return \c true if \p utf8 begins with a valid XML NameStartChar, or \c false
* otherwise
*/
bool
pcmk__xml_is_name_start_char(const char *utf8, int *len)
{
int c = 0;
int local_len = 0;
if (len == NULL) {
len = &local_len;
}
/* xmlGetUTF8Char() abuses the len argument. At call time, it must be set to
* "the minimum number of bytes present in the sequence... to assure the
* next character is completely contained within the sequence." It's similar
* to the "n" in the strn*() functions. However, this doesn't make any sense
* for null-terminated strings, and there's no value that indicates "keep
* going until '\0'." So we set it to 4, the max number of bytes in a UTF-8
* character.
*
* At return, it's set to the actual number of bytes in the char, or 0 on
* error.
*/
*len = 4;
// Note: xmlGetUTF8Char() assumes a 32-bit int
c = xmlGetUTF8Char((const xmlChar *) utf8, len);
if (c < 0) {
GString *buf = g_string_sized_new(32);
for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) {
g_string_append_printf(buf, " 0x%.2X", utf8[i]);
}
crm_info("Invalid UTF-8 character (bytes:%s)",
(pcmk__str_empty(buf->str)? " <none>" : buf->str));
g_string_free(buf, TRUE);
return false;
}
return (c == '_')
|| (c == ':')
|| ((c >= 'a') && (c <= 'z'))
|| ((c >= 'A') && (c <= 'Z'))
|| ((c >= 0xC0) && (c <= 0xD6))
|| ((c >= 0xD8) && (c <= 0xF6))
|| ((c >= 0xF8) && (c <= 0x2FF))
|| ((c >= 0x370) && (c <= 0x37D))
|| ((c >= 0x37F) && (c <= 0x1FFF))
|| ((c >= 0x200C) && (c <= 0x200D))
|| ((c >= 0x2070) && (c <= 0x218F))
|| ((c >= 0x2C00) && (c <= 0x2FEF))
|| ((c >= 0x3001) && (c <= 0xD7FF))
|| ((c >= 0xF900) && (c <= 0xFDCF))
|| ((c >= 0xFDF0) && (c <= 0xFFFD))
|| ((c >= 0x10000) && (c <= 0xEFFFF));
}
/*!
* \internal
* \brief Check whether the first character of a string is an XML NameChar
*
* See https://www.w3.org/TR/xml/#NT-NameChar.
*
* This is almost identical to libxml2's \c xmlIsDocNameChar(), but they don't
* expose it as part of the public API.
*
* \param[in] utf8 UTF-8 encoded string
* \param[out] len If not \c NULL, where to store size in bytes of first
* character in \p utf8
*
* \return \c true if \p utf8 begins with a valid XML NameChar, or \c false
* otherwise
*/
bool
pcmk__xml_is_name_char(const char *utf8, int *len)
{
int c = 0;
int local_len = 0;
if (len == NULL) {
len = &local_len;
}
// See comment regarding len in pcmk__xml_is_name_start_char()
*len = 4;
// Note: xmlGetUTF8Char() assumes a 32-bit int
c = xmlGetUTF8Char((const xmlChar *) utf8, len);
if (c < 0) {
GString *buf = g_string_sized_new(32);
for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) {
g_string_append_printf(buf, " 0x%.2X", utf8[i]);
}
crm_info("Invalid UTF-8 character (bytes:%s)",
(pcmk__str_empty(buf->str)? " <none>" : buf->str));
g_string_free(buf, TRUE);
return false;
}
return ((c >= 'a') && (c <= 'z'))
|| ((c >= 'A') && (c <= 'Z'))
|| ((c >= '0') && (c <= '9'))
|| (c == '_')
|| (c == ':')
|| (c == '-')
|| (c == '.')
|| (c == 0xB7)
|| ((c >= 0xC0) && (c <= 0xD6))
|| ((c >= 0xD8) && (c <= 0xF6))
|| ((c >= 0xF8) && (c <= 0x2FF))
|| ((c >= 0x300) && (c <= 0x36F))
|| ((c >= 0x370) && (c <= 0x37D))
|| ((c >= 0x37F) && (c <= 0x1FFF))
|| ((c >= 0x200C) && (c <= 0x200D))
|| ((c >= 0x203F) && (c <= 0x2040))
|| ((c >= 0x2070) && (c <= 0x218F))
|| ((c >= 0x2C00) && (c <= 0x2FEF))
|| ((c >= 0x3001) && (c <= 0xD7FF))
|| ((c >= 0xF900) && (c <= 0xFDCF))
|| ((c >= 0xFDF0) && (c <= 0xFFFD))
|| ((c >= 0x10000) && (c <= 0xEFFFF));
}
/*!
* \internal
* \brief Sanitize a string so it is usable as an XML ID
*
* An ID must match the Name production as defined here:
* https://www.w3.org/TR/xml/#NT-Name.
*
* Convert an invalid start character to \c '_'. Convert an invalid character
* after the start character to \c '.'.
*
* \param[in,out] id String to sanitize
*/
void
pcmk__xml_sanitize_id(char *id)
{
bool valid = true;
int len = 0;
// If id is empty or NULL, there's no way to make it a valid XML ID
pcmk__assert(!pcmk__str_empty(id));
/* @TODO Suppose there are two strings and each has an invalid ID character
* in the same position. The strings are otherwise identical. Both strings
* will be sanitized to the same valid ID, which is incorrect.
*
* The caller is responsible for ensuring the sanitized ID does not already
* exist in a given XML document before using it, if uniqueness is desired.
*/
valid = pcmk__xml_is_name_start_char(id, &len);
CRM_CHECK(len > 0, return); // UTF-8 encoding error
if (!valid) {
*id = '_';
for (int i = 1; i < len; i++) {
id[i] = '.';
}
}
for (id += len; *id != '\0'; id += len) {
valid = pcmk__xml_is_name_char(id, &len);
CRM_CHECK(len > 0, return); // UTF-8 encoding error
if (!valid) {
for (int i = 0; i < len; i++) {
id[i] = '.';
}
}
}
}
/*!
* \internal
* \brief Free an XML tree without ACL checks or change tracking
*
* \param[in,out] xml XML node to free
*/
void
pcmk__xml_free_node(xmlNode *xml)
{
pcmk__xml_free_private_data(xml);
xmlUnlinkNode(xml);
xmlFreeNode(xml);
}
/*!
* \internal
* \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled
*
* If \p node is the root of its document, free the entire document.
*
* \param[in,out] node XML node to free
* \param[in] position Position of \p node among its siblings for change
* tracking (negative to calculate automatically if
* needed)
+ *
+ * \return Standard Pacemaker return code
*/
-static void
+static int
free_xml_with_position(xmlNode *node, int position)
{
xmlDoc *doc = NULL;
xml_node_private_t *nodepriv = NULL;
if (node == NULL) {
- return;
+ return pcmk_rc_ok;
}
doc = node->doc;
nodepriv = node->_private;
if ((doc != NULL) && (xmlDocGetRootElement(doc) == node)) {
/* @TODO Should we check ACLs first? Otherwise it seems like we could
* free the root element without write permission.
*/
pcmk__xml_free_doc(doc);
- return;
+ return pcmk_rc_ok;
}
if (!pcmk__check_acl(node, NULL, pcmk__xf_acl_write)) {
- GString *xpath = NULL;
-
- pcmk__if_tracing({}, return);
- xpath = pcmk__element_xpath(node);
- qb_log_from_external_source(__func__, __FILE__,
- "Cannot remove %s %x", LOG_TRACE,
- __LINE__, 0, xpath->str, nodepriv->flags);
- g_string_free(xpath, TRUE);
- return;
+ pcmk__if_tracing(
+ {
+ GString *xpath = pcmk__element_xpath(node);
+
+ qb_log_from_external_source(__func__, __FILE__,
+ "Cannot remove %s %x", LOG_TRACE,
+ __LINE__, 0, xpath->str,
+ nodepriv->flags);
+ g_string_free(xpath, TRUE);
+ },
+ {}
+ );
+ return EACCES;
}
if (pcmk__xml_doc_all_flags_set(node->doc, pcmk__xf_tracking)
&& !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
xml_doc_private_t *docpriv = doc->_private;
GString *xpath = pcmk__element_xpath(node);
if (xpath != NULL) {
pcmk__deleted_xml_t *deleted_obj = NULL;
crm_trace("Deleting %s %p from %p", xpath->str, node, doc);
deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t));
deleted_obj->path = g_string_free(xpath, FALSE);
deleted_obj->position = -1;
// Record the position only for XML comments for now
if (node->type == XML_COMMENT_NODE) {
if (position >= 0) {
deleted_obj->position = position;
} else {
deleted_obj->position = pcmk__xml_position(node,
pcmk__xf_skip);
}
}
docpriv->deleted_objs = g_list_append(docpriv->deleted_objs,
deleted_obj);
pcmk__xml_doc_set_flags(node->doc, pcmk__xf_dirty);
}
}
pcmk__xml_free_node(node);
+ return pcmk_rc_ok;
}
/*!
* \internal
* \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled
*
* If \p xml is the root of its document, free the entire document.
*
* \param[in,out] xml XML node to free
*/
void
pcmk__xml_free(xmlNode *xml)
{
free_xml_with_position(xml, -1);
}
/*!
* \internal
* \brief Make a deep copy of an XML node under a given parent
*
* \param[in,out] parent XML element that will be the copy's parent (\c NULL
* to create a new XML document with the copy as root)
* \param[in] src XML node to copy
*
* \return Deep copy of \p src, or \c NULL if \p src is \c NULL
*/
xmlNode *
pcmk__xml_copy(xmlNode *parent, xmlNode *src)
{
xmlNode *copy = NULL;
if (src == NULL) {
return NULL;
}
if (parent == NULL) {
xmlDoc *doc = NULL;
// The copy will be the root element of a new document
pcmk__assert(src->type == XML_ELEMENT_NODE);
doc = pcmk__xml_new_doc();
copy = xmlDocCopyNode(src, doc, 1);
pcmk__mem_assert(copy);
xmlDocSetRootElement(doc, copy);
} else {
copy = xmlDocCopyNode(src, parent->doc, 1);
pcmk__mem_assert(copy);
xmlAddChild(parent, copy);
}
pcmk__xml_new_private_data(copy);
return copy;
}
/*!
* \internal
* \brief Remove XML text nodes from specified XML and all its children
*
* \param[in,out] xml XML to strip text from
*/
void
pcmk__strip_xml_text(xmlNode *xml)
{
xmlNode *iter = xml->children;
while (iter) {
xmlNode *next = iter->next;
switch (iter->type) {
case XML_TEXT_NODE:
pcmk__xml_free_node(iter);
break;
case XML_ELEMENT_NODE:
/* Search it */
pcmk__strip_xml_text(iter);
break;
default:
/* Leave it */
break;
}
iter = next;
}
}
/*!
* \internal
* \brief Check whether a string has XML special characters that must be escaped
*
* See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details.
*
* \param[in] text String to check
* \param[in] type Type of escaping
*
* \return \c true if \p text has special characters that need to be escaped, or
* \c false otherwise
*/
bool
pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type)
{
if (text == NULL) {
return false;
}
while (*text != '\0') {
switch (type) {
case pcmk__xml_escape_text:
switch (*text) {
case '<':
case '>':
case '&':
return true;
case '\n':
case '\t':
break;
default:
if (g_ascii_iscntrl(*text)) {
return true;
}
break;
}
break;
case pcmk__xml_escape_attr:
switch (*text) {
case '<':
case '>':
case '&':
case '"':
return true;
default:
if (g_ascii_iscntrl(*text)) {
return true;
}
break;
}
break;
case pcmk__xml_escape_attr_pretty:
switch (*text) {
case '\n':
case '\r':
case '\t':
case '"':
return true;
default:
break;
}
break;
default: // Invalid enum value
pcmk__assert(false);
break;
}
text = g_utf8_next_char(text);
}
return false;
}
/*!
* \internal
* \brief Replace special characters with their XML escape sequences
*
* \param[in] text Text to escape
* \param[in] type Type of escaping
*
* \return Newly allocated string equivalent to \p text but with special
* characters replaced with XML escape sequences (or \c NULL if \p text
* is \c NULL). If \p text is not \c NULL, the return value is
* guaranteed not to be \c NULL.
*
* \note There are libxml functions that purport to do this:
* \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars().
* However, their escaping is incomplete. See:
* https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252
* \note The caller is responsible for freeing the return value using
* \c g_free().
*/
gchar *
pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type)
{
GString *copy = NULL;
if (text == NULL) {
return NULL;
}
copy = g_string_sized_new(strlen(text));
while (*text != '\0') {
// Don't escape any non-ASCII characters
if ((*text & 0x80) != 0) {
size_t bytes = g_utf8_next_char(text) - text;
g_string_append_len(copy, text, bytes);
text += bytes;
continue;
}
switch (type) {
case pcmk__xml_escape_text:
switch (*text) {
case '<':
g_string_append(copy, PCMK__XML_ENTITY_LT);
break;
case '>':
g_string_append(copy, PCMK__XML_ENTITY_GT);
break;
case '&':
g_string_append(copy, PCMK__XML_ENTITY_AMP);
break;
case '\n':
case '\t':
g_string_append_c(copy, *text);
break;
default:
if (g_ascii_iscntrl(*text)) {
g_string_append_printf(copy, "&#x%.2X;", *text);
} else {
g_string_append_c(copy, *text);
}
break;
}
break;
case pcmk__xml_escape_attr:
switch (*text) {
case '<':
g_string_append(copy, PCMK__XML_ENTITY_LT);
break;
case '>':
g_string_append(copy, PCMK__XML_ENTITY_GT);
break;
case '&':
g_string_append(copy, PCMK__XML_ENTITY_AMP);
break;
case '"':
g_string_append(copy, PCMK__XML_ENTITY_QUOT);
break;
default:
if (g_ascii_iscntrl(*text)) {
g_string_append_printf(copy, "&#x%.2X;", *text);
} else {
g_string_append_c(copy, *text);
}
break;
}
break;
case pcmk__xml_escape_attr_pretty:
switch (*text) {
case '"':
g_string_append(copy, "\\\"");
break;
case '\n':
g_string_append(copy, "\\n");
break;
case '\r':
g_string_append(copy, "\\r");
break;
case '\t':
g_string_append(copy, "\\t");
break;
default:
g_string_append_c(copy, *text);
break;
}
break;
default: // Invalid enum value
pcmk__assert(false);
break;
}
text = g_utf8_next_char(text);
}
return g_string_free(copy, FALSE);
}
-/*!
- * \internal
- * \brief Set a flag on all attributes of an XML element
- *
- * \param[in,out] xml XML node to set flags on
- * \param[in] flag XML private flag to set
- */
-static void
-set_attrs_flag(xmlNode *xml, enum xml_private_flags flag)
-{
- for (xmlAttr *attr = pcmk__xe_first_attr(xml); attr; attr = attr->next) {
- pcmk__set_xml_flags((xml_node_private_t *) (attr->_private), flag);
- }
-}
-
/*!
* \internal
* \brief Add an XML attribute to a node, marked as deleted
*
* When calculating XML changes, we need to know when an attribute has been
* deleted. Add the attribute back to the new XML, so that we can check the
* removal against ACLs, and mark it as deleted for later removal after
* differences have been calculated.
*
* \param[in,out] new_xml XML to modify
* \param[in] element Name of XML element that changed (for logging)
* \param[in] attr_name Name of attribute that was deleted
* \param[in] old_value Value of attribute that was deleted
*/
static void
mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name,
const char *old_value)
{
xml_doc_private_t *docpriv = new_xml->doc->_private;
xmlAttr *attr = NULL;
xml_node_private_t *nodepriv;
/* Restore the old value (without setting dirty flag recursively upwards or
* checking ACLs)
*/
pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking);
crm_xml_add(new_xml, attr_name, old_value);
pcmk__set_xml_flags(docpriv, pcmk__xf_tracking);
// Reset flags (so the attribute doesn't appear as newly created)
attr = xmlHasProp(new_xml, (const xmlChar *) attr_name);
nodepriv = attr->_private;
nodepriv->flags = 0;
// Check ACLs and mark restored value for later removal
pcmk__xa_remove(attr, false);
crm_trace("XML attribute %s=%s was removed from %s",
attr_name, old_value, element);
}
/*
* \internal
* \brief Check ACLs for a changed XML attribute
*/
static void
mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name,
const char *old_value)
{
xml_doc_private_t *docpriv = new_xml->doc->_private;
char *vcopy = crm_element_value_copy(new_xml, attr_name);
crm_trace("XML attribute %s was changed from '%s' to '%s' in %s",
attr_name, old_value, vcopy, element);
// Restore the original value (without checking ACLs)
pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking);
crm_xml_add(new_xml, attr_name, old_value);
pcmk__set_xml_flags(docpriv, pcmk__xf_tracking);
// Change it back to the new value, to check ACLs
crm_xml_add(new_xml, attr_name, vcopy);
free(vcopy);
}
/*!
* \internal
* \brief Mark an XML attribute as having changed position
*
* \param[in,out] new_xml XML to modify
* \param[in] element Name of XML element that changed (for logging)
* \param[in,out] old_attr Attribute that moved, in original XML
* \param[in,out] new_attr Attribute that moved, in \p new_xml
* \param[in] p_old Ordinal position of \p old_attr in original XML
* \param[in] p_new Ordinal position of \p new_attr in \p new_xml
*/
static void
mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr,
xmlAttr *new_attr, int p_old, int p_new)
{
xml_node_private_t *nodepriv = new_attr->_private;
crm_trace("XML attribute %s moved from position %d to %d in %s",
old_attr->name, p_old, p_new, element);
// Mark document, element, and all element's parents as changed
pcmk__mark_xml_node_dirty(new_xml);
// Mark attribute as changed
pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved);
nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private;
pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
}
/*!
* \internal
* \brief Calculate differences in all previously existing XML attributes
*
* \param[in,out] old_xml Original XML to compare
* \param[in,out] new_xml New XML to compare
*/
static void
xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml)
{
xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml);
while (attr_iter != NULL) {
const char *name = (const char *) attr_iter->name;
xmlAttr *old_attr = attr_iter;
xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name);
const char *old_value = pcmk__xml_attr_value(attr_iter);
attr_iter = attr_iter->next;
if (new_attr == NULL) {
mark_attr_deleted(new_xml, (const char *) old_xml->name, name,
old_value);
} else {
xml_node_private_t *nodepriv = new_attr->_private;
int new_pos = pcmk__xml_position((xmlNode*) new_attr,
pcmk__xf_skip);
int old_pos = pcmk__xml_position((xmlNode*) old_attr,
pcmk__xf_skip);
const char *new_value = crm_element_value(new_xml, name);
// This attribute isn't new
pcmk__clear_xml_flags(nodepriv, pcmk__xf_created);
if (strcmp(new_value, old_value) != 0) {
mark_attr_changed(new_xml, (const char *) old_xml->name, name,
old_value);
} else if ((old_pos != new_pos)
&& !pcmk__xml_doc_all_flags_set(new_xml->doc,
- pcmk__xf_lazy
+ pcmk__xf_ignore_attr_pos
|pcmk__xf_tracking)) {
- /* pcmk__xf_tracking is always set by xml_calculate_changes()
- * before this function is called, so only the pcmk__xf_lazy
- * check is truly relevant.
+ /* pcmk__xf_tracking is always set by pcmk__xml_mark_changes()
+ * before this function is called, so only the
+ * pcmk__xf_ignore_attr_pos check is truly relevant.
*/
mark_attr_moved(new_xml, (const char *) old_xml->name,
old_attr, new_attr, old_pos, new_pos);
}
}
}
}
/*!
* \internal
* \brief Check all attributes in new XML for creation
*
* For each of a given XML element's attributes marked as newly created, accept
* (and mark as dirty) or reject the creation according to ACLs.
*
* \param[in,out] new_xml XML to check
*/
static void
mark_created_attrs(xmlNode *new_xml)
{
xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml);
while (attr_iter != NULL) {
xmlAttr *new_attr = attr_iter;
xml_node_private_t *nodepriv = attr_iter->_private;
attr_iter = attr_iter->next;
if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
const char *attr_name = (const char *) new_attr->name;
crm_trace("Created new attribute %s=%s in %s",
attr_name, pcmk__xml_attr_value(new_attr),
new_xml->name);
/* Check ACLs (we can't use the remove-then-create trick because it
* would modify the attribute position).
*/
if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) {
pcmk__mark_xml_attr_dirty(new_attr);
} else {
// Creation was not allowed, so remove the attribute
pcmk__xa_remove(new_attr, true);
}
}
}
}
/*!
* \internal
* \brief Calculate differences in attributes between two XML nodes
*
* \param[in,out] old_xml Original XML to compare
* \param[in,out] new_xml New XML to compare
*/
static void
xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml)
{
- set_attrs_flag(new_xml, pcmk__xf_created); // cleared later if not really new
+ // Cleared later if attributes are not really new
+ for (xmlAttr *attr = pcmk__xe_first_attr(new_xml); attr != NULL;
+ attr = attr->next) {
+ xml_node_private_t *nodepriv = attr->_private;
+
+ pcmk__set_xml_flags(nodepriv, pcmk__xf_created);
+ }
+
xml_diff_old_attrs(old_xml, new_xml);
mark_created_attrs(new_xml);
}
/*!
* \internal
- * \brief Add an XML child element to a node, marked as deleted
+ * \brief Add a deleted object record for an old XML child if ACLs allow
*
- * When calculating XML changes, we need to know when a child element has been
- * deleted. Add the child back to the new XML, so that we can check the removal
- * against ACLs, and mark it as deleted for later removal after differences have
- * been calculated.
+ * This is intended to be called for a child of an old XML element that is not
+ * present as a child of a new XML element.
*
- * \param[in,out] old_child Child element from original XML
- * \param[in,out] new_parent New XML to add marked copy to
+ * Add a temporary copy of the old child to the new XML. Then check whether ACLs
+ * would have allowed the deletion of that element. If so, add a deleted object
+ * record for it to the new XML's document, and set the \c pcmk__xf_skip flag on
+ * the old child.
+ *
+ * The temporary copy is removed before returning. The new XML and all of its
+ * ancestors will have the \c pcmk__xf_dirty flag set because of the creation,
+ * however.
+ *
+ * \param[in,out] old_child Child of old XML
+ * \param[in,out] new_parent New XML that does not contain \p old_child
*/
static void
mark_child_deleted(xmlNode *old_child, xmlNode *new_parent)
{
+ int pos = pcmk__xml_position(old_child, pcmk__xf_skip);
+
// Re-create the child element so we can check ACLs
xmlNode *candidate = pcmk__xml_copy(new_parent, old_child);
// Clear flags on new child and its children
pcmk__xml_tree_foreach(candidate, pcmk__xml_reset_node_flags, NULL);
- // Check whether ACLs allow the deletion
+ // free_xml_with_position() will check whether ACLs allow the deletion
pcmk__apply_acl(xmlDocGetRootElement(candidate->doc));
- // Remove the child again (which will track it in document's deleted_objs)
- free_xml_with_position(candidate,
- pcmk__xml_position(old_child, pcmk__xf_skip));
-
- if (pcmk__xml_match(new_parent, old_child, true) == NULL) {
- pcmk__set_xml_flags((xml_node_private_t *) (old_child->_private),
- pcmk__xf_skip);
+ /* Try to remove the child again (which will track it in document's
+ * deleted_objs on success)
+ */
+ if (free_xml_with_position(candidate, pos) != pcmk_rc_ok) {
+ // ACLs denied deletion in free_xml_with_position. Free candidate here.
+ pcmk__xml_free_node(candidate);
}
+
+ pcmk__set_xml_flags((xml_node_private_t *) old_child->_private,
+ pcmk__xf_skip);
}
+/*!
+ * \internal
+ * \brief Mark a new child as moved and set \c pcmk__xf_skip as appropriate
+ *
+ * \param[in,out] old_child Child of old XML
+ * \param[in,out] new_child Child of new XML that matches \p old_child
+ * \param[in] old_pos Position of \p old_child among its siblings
+ * \param[in] new_pos Position of \p new_child among its siblings
+ */
static void
-mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child,
- int p_old, int p_new)
+mark_child_moved(xmlNode *old_child, xmlNode *new_child, int old_pos,
+ int new_pos)
{
+ const char *id_s = pcmk__s(pcmk__xe_id(new_child), "<no id>");
+ xmlNode *new_parent = new_child->parent;
xml_node_private_t *nodepriv = new_child->_private;
- crm_trace("Child element %s with "
- PCMK_XA_ID "='%s' moved from position %d to %d under %s",
- new_child->name, pcmk__s(pcmk__xe_id(new_child), "<no id>"),
- p_old, p_new, new_parent->name);
+ crm_trace("Child element %s with " PCMK_XA_ID "='%s' moved from position "
+ "%d to %d under %s",
+ new_child->name, id_s, old_pos, new_pos, new_parent->name);
pcmk__mark_xml_node_dirty(new_parent);
pcmk__set_xml_flags(nodepriv, pcmk__xf_moved);
- if (p_old > p_new) {
+ /* @TODO Figure out and document why we skip the old child in future
+ * position calculations if the old position is higher, and skip the new
+ * child in future position calculations if the new position is higher. This
+ * goes back to d028b52, and there's no explanation in the commit message.
+ */
+ if (old_pos > new_pos) {
nodepriv = old_child->_private;
- } else {
- nodepriv = new_child->_private;
}
pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
}
-// Given original and new XML, mark new XML portions that have changed
-static void
-mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top)
+/*!
+ * \internal
+ * \brief Check whether a new XML child comment matches an old XML child comment
+ *
+ * Two comments match if they have the same position among their siblings and
+ * the same contents.
+ *
+ * If \p new_comment has the \c pcmk__xf_skip flag set, then it is automatically
+ * considered not to match.
+ *
+ * \param[in] old_comment Old XML child element
+ * \param[in] new_comment New XML child element
+ *
+ * \retval \c true if \p new_comment matches \p old_comment
+ * \retval \c false otherwise
+ */
+static bool
+new_comment_matches(const xmlNode *old_comment, const xmlNode *new_comment)
{
- xmlNode *old_child = NULL;
- xmlNode *new_child = NULL;
- xml_node_private_t *nodepriv = NULL;
+ xml_node_private_t *nodepriv = new_comment->_private;
- CRM_CHECK(new_xml != NULL, return);
- if (old_xml == NULL) {
- mark_xml_tree_dirty_created(new_xml);
- pcmk__apply_creation_acl(new_xml, check_top);
- return;
+ if (pcmk_is_set(nodepriv->flags, pcmk__xf_skip)) {
+ /* @TODO Should we also return false if old_comment has pcmk__xf_skip
+ * set? This preserves existing behavior at time of writing.
+ */
+ return false;
}
+ if (pcmk__xml_position(old_comment, pcmk__xf_skip)
+ != pcmk__xml_position(new_comment, pcmk__xf_skip)) {
+ return false;
+ }
+ return pcmk__xc_matches(old_comment, new_comment);
+}
- nodepriv = new_xml->_private;
- CRM_CHECK(nodepriv != NULL, return);
+/*!
+ * \internal
+ * \brief Check whether a new XML child element matches an old XML child element
+ *
+ * Two elements match if they have the same name and, if \p match_ids is
+ * \c true, the same ID. (Both IDs can be \c NULL in this case.)
+ *
+ * \param[in] old_element Old XML child element
+ * \param[in] new_element New XML child element
+ * \param[in] match_ids If \c true, require IDs to match (or both to be
+ * \c NULL)
+ *
+ * \retval \c true if \p new_element matches \p old_element
+ * \retval \c false otherwise
+ */
+static bool
+new_element_matches(const xmlNode *old_element, const xmlNode *new_element,
+ bool match_ids)
+{
+ if (!pcmk__xe_is(new_element, (const char *) old_element->name)) {
+ return false;
+ }
+ return !match_ids
+ || pcmk__str_eq(pcmk__xe_id(old_element), pcmk__xe_id(new_element),
+ pcmk__str_none);
+}
- if(nodepriv->flags & pcmk__xf_processed) {
- /* Avoid re-comparing nodes */
- return;
+/*!
+ * \internal
+ * \brief Check whether a new XML child node matches an old XML child node
+ *
+ * Node types must be the same in order to match.
+ *
+ * For comments, a match is a comment at the same position with the same
+ * content.
+ *
+ * For elements, a match is an element with the same name and, if required, the
+ * same ID. (Both IDs can be \c NULL in this case.)
+ *
+ * For other node types, there is no match.
+ *
+ * \param[in] old_child Child of old XML
+ * \param[in] new_child Child of new XML
+ * \param[in] match_ids If \c true, require element IDs to match (or both to be
+ * \c NULL)
+ *
+ * \retval \c true if \p new_child matches \p old_child
+ * \retval \c false otherwise
+ */
+static bool
+new_child_matches(const xmlNode *old_child, const xmlNode *new_child,
+ bool match_ids)
+{
+ if (old_child->type != new_child->type) {
+ return false;
}
- pcmk__set_xml_flags(nodepriv, pcmk__xf_processed);
- xml_diff_attrs(old_xml, new_xml);
+ switch (old_child->type) {
+ case XML_COMMENT_NODE:
+ return new_comment_matches(old_child, new_child);
+ case XML_ELEMENT_NODE:
+ return new_element_matches(old_child, new_child, match_ids);
+ default:
+ return false;
+ }
+}
- // Check for differences in the original children
- for (old_child = pcmk__xml_first_child(old_xml); old_child != NULL;
+/*!
+ * \internal
+ * \brief Find matching XML node pairs between old and new XML's children
+ *
+ * A node that is part of a matching pair has its <tt>_private:match</tt> member
+ * set to the matching node.
+ *
+ * \param[in,out] old_xml Old XML
+ * \param[in,out] new_xml New XML
+ * \param[in] comments_ids If \c true, match comments and require element
+ * IDs to match; otherwise, skip comments and match
+ * elements by name only
+ */
+static void
+find_matching_children(xmlNode *old_xml, xmlNode *new_xml, bool comments_ids)
+{
+ for (xmlNode *old_child = pcmk__xml_first_child(old_xml); old_child != NULL;
old_child = pcmk__xml_next(old_child)) {
- new_child = pcmk__xml_match(new_xml, old_child, true);
-
- if (new_child != NULL) {
- mark_xml_changes(old_child, new_child, true);
+ xml_node_private_t *old_nodepriv = old_child->_private;
- } else {
- mark_child_deleted(old_child, new_xml);
+ if ((old_nodepriv == NULL) || (old_nodepriv->match != NULL)) {
+ // Can't process, or we already found a match for this old child
+ continue;
+ }
+ if (!comments_ids && (old_child->type != XML_ELEMENT_NODE)) {
+ /* We only match comments and elements, and we're not matching
+ * comments during this call
+ */
+ continue;
}
- }
-
- // Check for moved or created children
- new_child = pcmk__xml_first_child(new_xml);
- while (new_child != NULL) {
- xmlNode *next = pcmk__xml_next(new_child);
-
- old_child = pcmk__xml_match(old_xml, new_child, true);
- if (old_child == NULL) {
- // This is a newly created child
- nodepriv = new_child->_private;
- pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
+ for (xmlNode *new_child = pcmk__xml_first_child(new_xml);
+ new_child != NULL; new_child = pcmk__xml_next(new_child)) {
- // May free new_child
- mark_xml_changes(old_child, new_child, true);
+ xml_node_private_t *new_nodepriv = new_child->_private;
- } else {
- /* Check for movement, we already checked for differences */
- int p_new = pcmk__xml_position(new_child, pcmk__xf_skip);
- int p_old = pcmk__xml_position(old_child, pcmk__xf_skip);
+ if ((new_nodepriv == NULL) || (new_nodepriv->match != NULL)) {
+ /* Can't process, or this new child already matched some old
+ * child
+ */
+ continue;
+ }
- if(p_old != p_new) {
- mark_child_moved(old_child, new_xml, new_child, p_old, p_new);
+ if (new_child_matches(old_child, new_child, comments_ids)) {
+ old_nodepriv->match = new_child;
+ new_nodepriv->match = old_child;
+ break;
}
}
-
- new_child = next;
}
}
+/*!
+ * \internal
+ * \brief Mark changes between two XML trees
+ *
+ * Set flags in a new XML tree to indicate changes relative to an old XML tree.
+ *
+ * \param[in,out] old_xml XML before changes
+ * \param[in,out] new_xml XML after changes
+ *
+ * \note This may set \c pcmk__xf_skip on parts of \p old_xml.
+ */
void
-xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml)
+pcmk__xml_mark_changes(xmlNode *old_xml, xmlNode *new_xml)
{
- if (new_xml != NULL) {
- pcmk__xml_doc_set_flags(new_xml->doc, pcmk__xf_lazy);
+ /* This function may set the xml_node_private_t:match member on children of
+ * old_xml and new_xml, but it clears that member before returning.
+ *
+ * @TODO Ensure we handle (for example, by copying) or reject user-created
+ * XML that is missing xml_node_private_t at top level or in any children.
+ * Similarly, check handling of node types for which we don't create private
+ * data. For now, we'll skip them in the loops below.
+ */
+ CRM_CHECK((old_xml != NULL) && (new_xml != NULL), return);
+ if ((old_xml->_private == NULL) || (new_xml->_private == NULL)) {
+ return;
}
- xml_calculate_changes(old_xml, new_xml);
-}
-// Called functions may set the \p pcmk__xf_skip flag on parts of \p old_xml
-void
-xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml)
-{
- CRM_CHECK((old_xml != NULL) && (new_xml != NULL)
- && pcmk__xe_is(old_xml, (const char *) new_xml->name)
- && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml),
- pcmk__str_none),
- return);
+ pcmk__xml_doc_set_flags(new_xml->doc, pcmk__xf_tracking);
+ xml_diff_attrs(old_xml, new_xml);
- if (!pcmk__xml_doc_all_flags_set(new_xml->doc, pcmk__xf_tracking)) {
- xml_track_changes(new_xml, NULL, NULL, FALSE);
+ find_matching_children(old_xml, new_xml, true);
+ find_matching_children(old_xml, new_xml, false);
+
+ // Process matches (changed children) and deletions
+ for (xmlNode *old_child = pcmk__xml_first_child(old_xml); old_child != NULL;
+ old_child = pcmk__xml_next(old_child)) {
+
+ xml_node_private_t *nodepriv = old_child->_private;
+ xmlNode *new_child = NULL;
+
+ if (nodepriv == NULL) {
+ continue;
+ }
+
+ if (nodepriv->match == NULL) {
+ // No match in new XML means the old child was deleted
+ mark_child_deleted(old_child, new_xml);
+ continue;
+ }
+
+ /* Fetch the match and clear old_child->_private's match member.
+ * new_child->_private's match member is handled in the new_xml loop.
+ */
+ new_child = nodepriv->match;
+ nodepriv->match = NULL;
+
+ pcmk__assert(old_child->type == new_child->type);
+
+ if (old_child->type == XML_COMMENT_NODE) {
+ // Comments match only if their positions and contents match
+ continue;
+ }
+
+ pcmk__xml_mark_changes(old_child, new_child);
}
- mark_xml_changes(old_xml, new_xml, FALSE);
+ /* Mark unmatched new children as created, and mark matched new children as
+ * moved if their positions changed. Grab the next new child in advance,
+ * since new_child may get freed in the loop body.
+ */
+ for (xmlNode *new_child = pcmk__xml_first_child(new_xml),
+ *next = pcmk__xml_next(new_child);
+ new_child != NULL;
+ new_child = next, next = pcmk__xml_next(new_child)) {
+
+ xml_node_private_t *nodepriv = new_child->_private;
+
+ if (nodepriv == NULL) {
+ continue;
+ }
+
+ if (nodepriv->match != NULL) {
+ /* Fetch the match and clear new_child->_private's match member. Any
+ * changes were marked in the old_xml loop. Mark the move.
+ *
+ * We might be able to mark the move earlier, when we mark changes
+ * for matches in the old_xml loop, consolidating both actions. We'd
+ * have to think about whether the timing of setting the
+ * pcmk__xf_skip flag makes any difference.
+ */
+ xmlNode *old_child = nodepriv->match;
+ int old_pos = pcmk__xml_position(old_child, pcmk__xf_skip);
+ int new_pos = pcmk__xml_position(new_child, pcmk__xf_skip);
+
+ if (old_pos != new_pos) {
+ mark_child_moved(old_child, new_child, old_pos, new_pos);
+ }
+ nodepriv->match = NULL;
+ continue;
+ }
+
+ // No match in old XML means the new child is newly created
+ pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
+ mark_xml_tree_dirty_created(new_child);
+
+ // Check whether creation was allowed (may free new_child)
+ pcmk__apply_creation_acl(new_child, true);
+ }
}
/*!
* \internal
* \brief Initialize the Pacemaker XML environment
*
* Set an XML buffer allocation scheme, set XML node create and destroy
* callbacks, and load schemas into the cache.
*/
void
pcmk__xml_init(void)
{
// @TODO Try to find a better caller than crm_log_preinit()
static bool initialized = false;
if (!initialized) {
initialized = true;
/* Double the buffer size when the buffer needs to grow. The default
* allocator XML_BUFFER_ALLOC_EXACT was found to cause poor performance
* due to the number of reallocs.
*/
xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT);
// Load schemas into the cache
pcmk__schema_init();
}
}
/*!
* \internal
* \brief Tear down the Pacemaker XML environment
*
* Destroy schema cache and clean up memory allocated by libxml2.
*/
void
pcmk__xml_cleanup(void)
{
pcmk__schema_cleanup();
xmlCleanupParser();
}
char *
pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns)
{
static const char *base = NULL;
char *ret = NULL;
if (base == NULL) {
base = pcmk__env_option(PCMK__ENV_SCHEMA_DIRECTORY);
}
if (pcmk__str_empty(base)) {
base = PCMK_SCHEMA_DIR;
}
switch (ns) {
case pcmk__xml_artefact_ns_legacy_rng:
case pcmk__xml_artefact_ns_legacy_xslt:
ret = strdup(base);
break;
case pcmk__xml_artefact_ns_base_rng:
case pcmk__xml_artefact_ns_base_xslt:
ret = crm_strdup_printf("%s/base", base);
break;
default:
crm_err("XML artefact family specified as %u not recognized", ns);
}
return ret;
}
static char *
find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec)
{
char *ret = NULL;
switch (ns) {
case pcmk__xml_artefact_ns_legacy_rng:
case pcmk__xml_artefact_ns_base_rng:
if (pcmk__ends_with(filespec, ".rng")) {
ret = crm_strdup_printf("%s/%s", path, filespec);
} else {
ret = crm_strdup_printf("%s/%s.rng", path, filespec);
}
break;
case pcmk__xml_artefact_ns_legacy_xslt:
case pcmk__xml_artefact_ns_base_xslt:
if (pcmk__ends_with(filespec, ".xsl")) {
ret = crm_strdup_printf("%s/%s", path, filespec);
} else {
ret = crm_strdup_printf("%s/%s.xsl", path, filespec);
}
break;
default:
crm_err("XML artefact family specified as %u not recognized", ns);
}
return ret;
}
char *
pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec)
{
struct stat sb;
char *base = pcmk__xml_artefact_root(ns);
char *ret = NULL;
ret = find_artefact(ns, base, filespec);
free(base);
if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) {
const char *remote_schema_dir = pcmk__remote_schema_dir();
free(ret);
ret = find_artefact(ns, remote_schema_dir, filespec);
}
return ret;
}
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
#include <crm/common/xml_compat.h>
xmlNode *
copy_xml(xmlNode *src)
{
xmlDoc *doc = pcmk__xml_new_doc();
xmlNode *copy = NULL;
copy = xmlDocCopyNode(src, doc, 1);
pcmk__mem_assert(copy);
xmlDocSetRootElement(doc, copy);
pcmk__xml_new_private_data(copy);
return copy;
}
void
crm_xml_init(void)
{
pcmk__xml_init();
}
void
crm_xml_cleanup(void)
{
pcmk__xml_cleanup();
}
void
pcmk_free_xml_subtree(xmlNode *xml)
{
pcmk__xml_free_node(xml);
}
void
free_xml(xmlNode *child)
{
pcmk__xml_free(child);
}
void
crm_xml_sanitize_id(char *id)
{
char *c;
for (c = id; *c; ++c) {
switch (*c) {
case ':':
case '#':
*c = '.';
}
}
}
bool
xml_tracking_changes(xmlNode *xml)
{
return (xml != NULL)
&& pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_tracking);
}
bool
xml_document_dirty(xmlNode *xml)
{
return (xml != NULL)
&& pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_dirty);
}
+void
+xml_accept_changes(xmlNode *xml)
+{
+ if (xml != NULL) {
+ pcmk__xml_commit_changes(xml->doc);
+ }
+}
+
+void
+xml_track_changes(xmlNode *xml, const char *user, xmlNode *acl_source,
+ bool enforce_acls)
+{
+ if (xml == NULL) {
+ return;
+ }
+
+ pcmk__xml_commit_changes(xml->doc);
+ crm_trace("Tracking changes%s to %p",
+ (enforce_acls? " with ACLs" : ""), xml);
+ pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_tracking);
+ if (enforce_acls) {
+ if (acl_source == NULL) {
+ acl_source = xml;
+ }
+ pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_acl_enabled);
+ pcmk__unpack_acl(acl_source, xml, user);
+ pcmk__apply_acl(xml);
+ }
+}
+
+void
+xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml)
+{
+ CRM_CHECK((old_xml != NULL) && (new_xml != NULL)
+ && pcmk__xe_is(old_xml, (const char *) new_xml->name)
+ && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml),
+ pcmk__str_none),
+ return);
+
+ if (!pcmk__xml_doc_all_flags_set(new_xml->doc, pcmk__xf_tracking)) {
+ // Ensure tracking has a clean start (pcmk__xml_mark_changes() enables)
+ pcmk__xml_commit_changes(new_xml->doc);
+ }
+
+ pcmk__xml_mark_changes(old_xml, new_xml);
+}
+
+void
+xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml)
+{
+ CRM_CHECK((old_xml != NULL) && (new_xml != NULL)
+ && pcmk__xe_is(old_xml, (const char *) new_xml->name)
+ && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml),
+ pcmk__str_none),
+ return);
+
+ /* BUG: If pcmk__xf_tracking is not set for new_xml when this function is
+ * called, then we unset pcmk__xf_ignore_attr_pos via
+ * pcmk__xml_commit_changes(). Since this function is about to be
+ * deprecated, it's not worth fixing this and changing the user-facing
+ * behavior.
+ */
+ pcmk__xml_doc_set_flags(new_xml->doc, pcmk__xf_ignore_attr_pos);
+
+ if (!pcmk__xml_doc_all_flags_set(new_xml->doc, pcmk__xf_tracking)) {
+ // Ensure tracking has a clean start (pcmk__xml_mark_changes() enables)
+ pcmk__xml_commit_changes(new_xml->doc);
+ }
+
+ pcmk__xml_mark_changes(old_xml, new_xml);
+}
+
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/lib/common/xml_comment.c b/lib/common/xml_comment.c
index 2bc2848ef6..ae4443ca9f 100644
--- a/lib/common/xml_comment.c
+++ b/lib/common/xml_comment.c
@@ -1,116 +1,120 @@
/*
* Copyright 2024-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> // bool, false
#include <stdio.h> // NULL
#include <libxml/tree.h> // xmlDoc, xmlNode, etc.
#include <libxml/xmlstring.h> // xmlChar
#include "crmcommon_private.h"
/*!
* \internal
* \brief Create a new XML comment belonging to a given document
*
* \param[in] doc Document that new comment will belong to
* \param[in] content Comment content
*
* \return Newly created XML comment (guaranteed not to be \c NULL)
*/
xmlNode *
pcmk__xc_create(xmlDoc *doc, const char *content)
{
xmlNode *node = NULL;
// Pacemaker typically assumes every xmlNode has a doc
pcmk__assert(doc != NULL);
node = xmlNewDocComment(doc, (const xmlChar *) content);
pcmk__mem_assert(node);
pcmk__xml_new_private_data(node);
return node;
}
/*!
* \internal
- * \brief Find a comment with matching content in specified XML
+ * \brief Check whether two comments have matching content (case-insensitive)
*
- * \param[in] root XML to search
- * \param[in] search_comment Comment whose content should be searched for
- * \param[in] exact If true, comment must also be at same position
+ * \param[in] comment1 First comment node to compare
+ * \param[in] comment2 Second comment node to compare
+ *
+ * \return \c true if \p comment1 and \p comment2 have matching content (by
+ * case-insensitive string comparison), or \c false otherwise
*/
-xmlNode *
-pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact)
+bool
+pcmk__xc_matches(const xmlNode *comment1, const xmlNode *comment2)
{
- xmlNode *a_child = NULL;
- int search_offset = pcmk__xml_position(search_comment, pcmk__xf_skip);
-
- CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL);
+ pcmk__assert((comment1 != NULL) && (comment1->type == XML_COMMENT_NODE)
+ && (comment2 != NULL) && (comment2->type == XML_COMMENT_NODE));
- for (a_child = pcmk__xml_first_child(root); a_child != NULL;
- a_child = pcmk__xml_next(a_child)) {
- if (exact) {
- int offset = pcmk__xml_position(a_child, pcmk__xf_skip);
- xml_node_private_t *nodepriv = a_child->_private;
+ return pcmk__str_eq((const char *) comment1->content,
+ (const char *) comment2->content, pcmk__str_casei);
+}
- if (offset < search_offset) {
- continue;
+/*!
+ * \internal
+ * \brief Find a comment with matching content among children of specified XML
+ *
+ * \param[in] parent XML whose children to search
+ * \param[in] search Comment whose content should be searched for
+ *
+ * \return Matching comment, or \c NULL if no match is found
+ */
+static xmlNode *
+match_xc_child(const xmlNode *parent, const xmlNode *search)
+{
+ pcmk__assert((search != NULL) && (search->type == XML_COMMENT_NODE));
- } else if (offset > search_offset) {
- return NULL;
- }
+ for (xmlNode *child = pcmk__xml_first_child(parent); child != NULL;
+ child = pcmk__xml_next(child)) {
- if (pcmk_is_set(nodepriv->flags, pcmk__xf_skip)) {
- continue;
- }
+ if (child->type != XML_COMMENT_NODE) {
+ continue;
}
- if (a_child->type == XML_COMMENT_NODE
- && pcmk__str_eq((const char *)a_child->content, (const char *)search_comment->content, pcmk__str_casei)) {
- return a_child;
-
- } else if (exact) {
- return NULL;
+ if (pcmk__xc_matches(child, search)) {
+ return child;
}
}
return NULL;
}
/*!
* \internal
* \brief Make one XML comment match another (in content)
*
* \param[in,out] parent If \p target is NULL and this is not, add or update
* comment child of this XML node that matches \p update
* \param[in,out] target If not NULL, update this XML comment node
* \param[in] update Make comment content match this (must not be NULL)
*
* \note At least one of \parent and \target must be non-NULL
*/
void
pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update)
{
CRM_CHECK(update != NULL, return);
CRM_CHECK(update->type == XML_COMMENT_NODE, return);
if (target == NULL) {
- target = pcmk__xc_match(parent, update, false);
+ target = match_xc_child(parent, update);
}
if (target == NULL) {
pcmk__xml_copy(parent, update);
} else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) {
xmlFree(target->content);
target->content = xmlStrdup(update->content);
}
}
diff --git a/lib/common/xml_element.c b/lib/common/xml_element.c
index 2361f8ff64..426ef3c898 100644
--- a/lib/common/xml_element.c
+++ b/lib/common/xml_element.c
@@ -1,1609 +1,1609 @@
/*
* 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 <stdarg.h> // va_start(), etc.
#include <stdint.h> // uint32_t
#include <stdio.h> // NULL, etc.
#include <stdlib.h> // free(), etc.
#include <string.h> // strchr(), etc.
#include <sys/types.h> // time_t, etc.
#include <libxml/tree.h> // xmlNode, etc.
#include <libxml/valid.h> // xmlValidateNameValue()
#include <libxml/xmlstring.h> // xmlChar
#include <crm/crm.h>
#include <crm/common/nvpair.h> // crm_xml_add(), etc.
#include <crm/common/results.h> // pcmk_rc_ok, etc.
#include <crm/common/xml.h>
#include "crmcommon_private.h"
/*!
* \internal
* \brief Find first XML child element matching given criteria
*
* \param[in] parent XML element to search (can be \c NULL)
* \param[in] node_name If not \c NULL, only match children of this type
* \param[in] attr_n If not \c NULL, only match children with an attribute
* of this name.
* \param[in] attr_v If \p attr_n and this are not NULL, only match children
* with an attribute named \p attr_n and this value
*
* \return Matching XML child element, or \c NULL if none found
*/
xmlNode *
pcmk__xe_first_child(const xmlNode *parent, const char *node_name,
const char *attr_n, const char *attr_v)
{
xmlNode *child = NULL;
CRM_CHECK((attr_v == NULL) || (attr_n != NULL), return NULL);
if (parent == NULL) {
return NULL;
}
child = parent->children;
while ((child != NULL) && (child->type != XML_ELEMENT_NODE)) {
child = child->next;
}
for (; child != NULL; child = pcmk__xe_next(child, NULL)) {
const char *value = NULL;
if ((node_name != NULL) && !pcmk__xe_is(child, node_name)) {
// Node name mismatch
continue;
}
if (attr_n == NULL) {
// No attribute match needed
return child;
}
value = crm_element_value(child, attr_n);
if ((attr_v == NULL) && (value != NULL)) {
// attr_v == NULL: Attribute attr_n must be set (to any value)
return child;
}
if ((attr_v != NULL) && (pcmk__str_eq(value, attr_v, pcmk__str_none))) {
// attr_v != NULL: Attribute attr_n must be set to value attr_v
return child;
}
}
if (attr_n == NULL) {
crm_trace("%s XML has no child element of %s type",
(const char *) parent->name, pcmk__s(node_name, "any"));
} else {
crm_trace("%s XML has no child element of %s type with %s='%s'",
(const char *) parent->name, pcmk__s(node_name, "any"),
attr_n, attr_v);
}
return NULL;
}
/*!
* \internal
* \brief Return next sibling element of an XML element
*
* \param[in] xml XML element to check
* \param[in] element_name If not NULL, get next sibling with this element name
*
* \return Next desired sibling of \p xml (or NULL if none)
*/
xmlNode *
pcmk__xe_next(const xmlNode *xml, const char *element_name)
{
for (xmlNode *next = (xml == NULL)? NULL : xml->next;
next != NULL; next = next->next) {
if ((next->type == XML_ELEMENT_NODE)
&& ((element_name == NULL) || pcmk__xe_is(next, element_name))) {
return next;
}
}
return NULL;
}
/*!
* \internal
* \brief Parse an integer score from an XML attribute
*
* \param[in] xml XML element with attribute to parse
* \param[in] name Name of attribute to parse
* \param[out] score Where to store parsed score (can be NULL to
* just validate)
* \param[in] default_score What to return if the attribute value is not
* present or invalid
*
* \return Standard Pacemaker return code
*/
int
pcmk__xe_get_score(const xmlNode *xml, const char *name, int *score,
int default_score)
{
const char *value = NULL;
CRM_CHECK((xml != NULL) && (name != NULL), return EINVAL);
value = crm_element_value(xml, name);
return pcmk_parse_score(value, score, default_score);
}
/*!
* \internal
* \brief Set an XML attribute, expanding \c ++ and \c += where appropriate
*
* If \p target already has an attribute named \p name set to an integer value
* and \p value is an addition assignment expression on \p name, then expand
* \p value to an integer and set attribute \p name to the expanded value in
* \p target.
*
* Otherwise, set attribute \p name on \p target using the literal \p value.
*
* The original attribute value in \p target and the number in an assignment
* expression in \p value are parsed and added as scores (that is, their values
* are capped at \c INFINITY and \c -INFINITY). For more details, refer to
* \c pcmk_parse_score().
*
* For example, suppose \p target has an attribute named \c "X" with value
* \c "5", and that \p name is \c "X".
* * If \p value is \c "X++", the new value of \c "X" in \p target is \c "6".
* * If \p value is \c "X+=3", the new value of \c "X" in \p target is \c "8".
* * If \p value is \c "val", the new value of \c "X" in \p target is \c "val".
* * If \p value is \c "Y++", the new value of \c "X" in \p target is \c "Y++".
*
* \param[in,out] target XML node whose attribute to set
* \param[in] name Name of the attribute to set
* \param[in] value New value of attribute to set (if NULL, initial value
* will be left unchanged)
*
* \return Standard Pacemaker return code (specifically, \c EINVAL on invalid
* argument, or \c pcmk_rc_ok otherwise)
*/
int
pcmk__xe_set_score(xmlNode *target, const char *name, const char *value)
{
const char *old_value = NULL;
CRM_CHECK((target != NULL) && (name != NULL), return EINVAL);
if (value == NULL) {
// @TODO Maybe instead delete the attribute or set it to 0
return pcmk_rc_ok;
}
old_value = crm_element_value(target, name);
// If no previous value, skip to default case and set the value unexpanded.
if (old_value != NULL) {
const char *n = name;
const char *v = value;
// Stop at first character that differs between name and value
for (; (*n == *v) && (*n != '\0'); n++, v++);
// If value begins with name followed by a "++" or "+="
if ((*n == '\0')
&& (*v++ == '+')
&& ((*v == '+') || (*v == '='))) {
int add = 1;
int old_value_i = 0;
int rc = pcmk_rc_ok;
// If we're expanding ourselves, no previous value was set; use 0
if (old_value != value) {
rc = pcmk_parse_score(old_value, &old_value_i, 0);
if (rc != pcmk_rc_ok) {
// @TODO This is inconsistent with old_value==NULL
crm_trace("Using 0 before incrementing %s because '%s' "
"is not a score", name, old_value);
}
}
/* value="X++": new value of X is old_value + 1
* value="X+=Y": new value of X is old_value + Y (for some number Y)
*/
if (*v != '+') {
rc = pcmk_parse_score(++v, &add, 0);
if (rc != pcmk_rc_ok) {
// @TODO We should probably skip expansion instead
crm_trace("Not incrementing %s because '%s' does not have "
"a valid increment", name, value);
}
}
crm_xml_add_int(target, name, pcmk__add_scores(old_value_i, add));
return pcmk_rc_ok;
}
}
// Default case: set the attribute unexpanded (with value treated literally)
if (old_value != value) {
crm_xml_add(target, name, value);
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Copy XML attributes from a source element to a target element
*
* This is similar to \c xmlCopyPropList() except that attributes are marked
* as dirty for change tracking purposes.
*
* \param[in,out] target XML element to receive copied attributes from \p src
* \param[in] src XML element whose attributes to copy to \p target
* \param[in] flags Group of <tt>enum pcmk__xa_flags</tt>
*
* \return Standard Pacemaker return code
*/
int
pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags)
{
CRM_CHECK((src != NULL) && (target != NULL), return EINVAL);
for (xmlAttr *attr = pcmk__xe_first_attr(src); attr != NULL;
attr = attr->next) {
const char *name = (const char *) attr->name;
const char *value = pcmk__xml_attr_value(attr);
if (pcmk_is_set(flags, pcmk__xaf_no_overwrite)
&& (crm_element_value(target, name) != NULL)) {
continue;
}
if (pcmk_is_set(flags, pcmk__xaf_score_update)) {
pcmk__xe_set_score(target, name, value);
} else {
crm_xml_add(target, name, value);
}
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Compare two XML attributes by name
*
* \param[in] a First XML attribute to compare
* \param[in] b Second XML attribute to compare
*
* \retval negative \c a->name is \c NULL or comes before \c b->name
* lexicographically
* \retval 0 \c a->name and \c b->name are equal
* \retval positive \c b->name is \c NULL or comes before \c a->name
* lexicographically
*/
static gint
compare_xml_attr(gconstpointer a, gconstpointer b)
{
const xmlAttr *attr_a = a;
const xmlAttr *attr_b = b;
return pcmk__strcmp((const char *) attr_a->name,
(const char *) attr_b->name, pcmk__str_none);
}
/*!
* \internal
* \brief Sort an XML element's attributes by name
*
* This does not consider ACLs and does not mark the attributes as deleted or
* dirty. Upon return, all attributes still exist and are set to the same values
* as before the call. The only thing that may change is the order of the
* attribute list.
*
* \param[in,out] xml XML element whose attributes to sort
*/
void
pcmk__xe_sort_attrs(xmlNode *xml)
{
GSList *attr_list = NULL;
for (xmlAttr *iter = pcmk__xe_first_attr(xml); iter != NULL;
iter = iter->next) {
attr_list = g_slist_prepend(attr_list, iter);
}
attr_list = g_slist_sort(attr_list, compare_xml_attr);
for (GSList *iter = attr_list; iter != NULL; iter = iter->next) {
xmlNode *attr = iter->data;
xmlUnlinkNode(attr);
xmlAddChild(xml, attr);
}
g_slist_free(attr_list);
}
/*!
* \internal
* \brief Remove a named attribute from an XML element
*
* \param[in,out] element XML element to remove an attribute from
* \param[in] name Name of attribute to remove
*/
void
pcmk__xe_remove_attr(xmlNode *element, const char *name)
{
if (name != NULL) {
pcmk__xa_remove(xmlHasProp(element, (const xmlChar *) name), false);
}
}
/*!
* \internal
* \brief Remove a named attribute from an XML element
*
* This is a wrapper for \c pcmk__xe_remove_attr() for use with
* \c pcmk__xml_tree_foreach().
*
* \param[in,out] xml XML element to remove an attribute from
* \param[in] user_data Name of attribute to remove
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
bool
pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data)
{
const char *name = user_data;
pcmk__xe_remove_attr(xml, name);
return true;
}
/*!
* \internal
* \brief Remove an XML element's attributes that match some criteria
*
* \param[in,out] element XML element to modify
* \param[in] force If \c true, remove matching attributes immediately,
* ignoring ACLs and change tracking
* \param[in] match If not NULL, only remove attributes for which
* this function returns true
* \param[in,out] user_data Data to pass to \p match
*/
void
pcmk__xe_remove_matching_attrs(xmlNode *element, bool force,
bool (*match)(xmlAttrPtr, void *),
void *user_data)
{
xmlAttrPtr next = NULL;
for (xmlAttrPtr a = pcmk__xe_first_attr(element); a != NULL; a = next) {
next = a->next; // Grab now because attribute might get removed
if ((match == NULL) || match(a, user_data)) {
if (pcmk__xa_remove(a, force) != pcmk_rc_ok) {
return;
}
}
}
}
/*!
* \internal
* \brief Create a new XML element under a given parent
*
* \param[in,out] parent XML element that will be the new element's parent
* (\c NULL to create a new XML document with the new
* node as root)
* \param[in] name Name of new element
*
* \return Newly created XML element (guaranteed not to be \c NULL)
*/
xmlNode *
pcmk__xe_create(xmlNode *parent, const char *name)
{
xmlNode *node = NULL;
pcmk__assert(!pcmk__str_empty(name));
if (parent == NULL) {
xmlDoc *doc = pcmk__xml_new_doc();
node = xmlNewDocRawNode(doc, NULL, (const xmlChar *) name, NULL);
pcmk__mem_assert(node);
xmlDocSetRootElement(doc, node);
} else {
node = xmlNewChild(parent, NULL, (const xmlChar *) name, NULL);
pcmk__mem_assert(node);
}
pcmk__xml_new_private_data(node);
return node;
}
/*!
* \internal
* \brief Set a formatted string as an XML node's content
*
* \param[in,out] node Node whose content to set
* \param[in] format <tt>printf(3)</tt>-style format string
* \param[in] ... Arguments for \p format
*
* \note This function escapes special characters. \c xmlNodeSetContent() does
* not.
*/
G_GNUC_PRINTF(2, 3)
void
pcmk__xe_set_content(xmlNode *node, const char *format, ...)
{
if (node != NULL) {
const char *content = NULL;
char *buf = NULL;
/* xmlNodeSetContent() frees node->children and replaces it with new
* text. If this function is called for a node that already has a non-
* text child, it's a bug.
*/
CRM_CHECK((node->children == NULL)
|| (node->children->type == XML_TEXT_NODE),
return);
if (strchr(format, '%') == NULL) {
// Nothing to format
content = format;
} else {
va_list ap;
va_start(ap, format);
if (pcmk__str_eq(format, "%s", pcmk__str_none)) {
// No need to make a copy
content = va_arg(ap, const char *);
} else {
pcmk__assert(vasprintf(&buf, format, ap) >= 0);
content = buf;
}
va_end(ap);
}
xmlNodeSetContent(node, (const xmlChar *) content);
free(buf);
}
}
/*!
* \internal
* \brief Set a formatted string as an XML element's ID
*
* If the formatted string would not be a valid ID, it's first sanitized by
* \c pcmk__xml_sanitize_id().
*
* \param[in,out] node Node whose ID to set
* \param[in] format <tt>printf(3)</tt>-style format string
* \param[in] ... Arguments for \p format
*/
G_GNUC_PRINTF(2, 3)
void
pcmk__xe_set_id(xmlNode *node, const char *format, ...)
{
char *id = NULL;
va_list ap;
pcmk__assert(!pcmk__str_empty(format));
if (node == NULL) {
return;
}
va_start(ap, format);
pcmk__assert(vasprintf(&id, format, ap) >= 0);
va_end(ap);
if (!xmlValidateNameValue((const xmlChar *) id)) {
pcmk__xml_sanitize_id(id);
}
crm_xml_add(node, PCMK_XA_ID, id);
free(id);
}
/*!
* \internal
* \brief Add a "last written" attribute to an XML element, set to current time
*
* \param[in,out] xe XML element to add attribute to
*
* \return Value that was set, or NULL on error
*/
const char *
pcmk__xe_add_last_written(xmlNode *xe)
{
char *now_s = pcmk__epoch2str(NULL, 0);
const char *result = NULL;
result = crm_xml_add(xe, PCMK_XA_CIB_LAST_WRITTEN,
pcmk__s(now_s, "Could not determine current time"));
free(now_s);
return result;
}
/*!
* \internal
* \brief Merge one XML tree into another
*
* Here, "merge" means:
* 1. Copy attribute values from \p update to the target, overwriting in case of
* conflict.
* 2. Descend through \p update and the target in parallel. At each level, for
* each child of \p update, look for a matching child of the target.
* a. For each child, if a match is found, go to step 1, recursively merging
* the child of \p update into the child of the target.
* b. Otherwise, copy the child of \p update as a child of the target.
*
* A match is defined as the first child of the same type within the target,
* with:
* * the \c PCMK_XA_ID attribute matching, if set in \p update; otherwise,
* * the \c PCMK_XA_ID_REF attribute matching, if set in \p update
*
* This function does not delete any elements or attributes from the target. It
* may add elements or overwrite attributes, as described above.
*
* \param[in,out] parent If \p target is NULL and this is not, add or update
* child of this XML node that matches \p update
* \param[in,out] target If not NULL, update this XML
* \param[in] update Make the desired XML match this (must not be \c NULL)
* \param[in] flags Group of <tt>enum pcmk__xa_flags</tt>
*
* \note At least one of \p parent and \p target must be non-<tt>NULL</tt>.
* \note This function is recursive. For the top-level call, \p parent is
* \c NULL and \p target is not \c NULL. For recursive calls, \p target is
* \c NULL and \p parent is not \c NULL.
*/
static void
update_xe(xmlNode *parent, xmlNode *target, xmlNode *update, uint32_t flags)
{
// @TODO Try to refactor further, possibly using pcmk__xml_tree_foreach()
const char *update_name = NULL;
const char *update_id_attr = NULL;
const char *update_id_val = NULL;
char *trace_s = NULL;
crm_log_xml_trace(update, "update");
crm_log_xml_trace(target, "target");
CRM_CHECK(update != NULL, goto done);
if (update->type == XML_COMMENT_NODE) {
pcmk__xc_update(parent, target, update);
goto done;
}
update_name = (const char *) update->name;
CRM_CHECK(update_name != NULL, goto done);
CRM_CHECK((target != NULL) || (parent != NULL), goto done);
update_id_val = pcmk__xe_id(update);
if (update_id_val != NULL) {
update_id_attr = PCMK_XA_ID;
} else {
update_id_val = crm_element_value(update, PCMK_XA_ID_REF);
if (update_id_val != NULL) {
update_id_attr = PCMK_XA_ID_REF;
}
}
pcmk__if_tracing(
{
if (update_id_attr != NULL) {
trace_s = crm_strdup_printf("<%s %s=%s/>",
update_name, update_id_attr,
update_id_val);
} else {
trace_s = crm_strdup_printf("<%s/>", update_name);
}
},
{}
);
if (target == NULL) {
// Recursive call
target = pcmk__xe_first_child(parent, update_name, update_id_attr,
update_id_val);
}
if (target == NULL) {
// Recursive call with no existing matching child
target = pcmk__xe_create(parent, update_name);
crm_trace("Added %s", pcmk__s(trace_s, update_name));
} else {
// Either recursive call with match, or top-level call
crm_trace("Found node %s to update", pcmk__s(trace_s, update_name));
}
CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return);
pcmk__xe_copy_attrs(target, update, flags);
for (xmlNode *child = pcmk__xml_first_child(update); child != NULL;
child = pcmk__xml_next(child)) {
crm_trace("Updating child of %s", pcmk__s(trace_s, update_name));
update_xe(target, NULL, child, flags);
}
crm_trace("Finished with %s", pcmk__s(trace_s, update_name));
done:
free(trace_s);
}
/*!
* \internal
* \brief Delete an XML subtree if it matches a search element
*
* A match is defined as follows:
* * \p xml and \p user_data are both element nodes of the same type.
* * If \p user_data has attributes set, \p xml has those attributes set to the
* same values. (\p xml may have additional attributes set to arbitrary
* values.)
*
* \param[in,out] xml XML subtree to delete upon match
* \param[in] user_data Search element
*
* \return \c true to continue traversing the tree, or \c false to stop (because
* \p xml was deleted)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
delete_xe_if_matching(xmlNode *xml, void *user_data)
{
xmlNode *search = user_data;
if (!pcmk__xe_is(search, (const char *) xml->name)) {
// No match: either not both elements, or different element types
return true;
}
for (const xmlAttr *attr = pcmk__xe_first_attr(search); attr != NULL;
attr = attr->next) {
const char *search_val = pcmk__xml_attr_value(attr);
const char *xml_val = crm_element_value(xml, (const char *) attr->name);
if (!pcmk__str_eq(search_val, xml_val, pcmk__str_casei)) {
// No match: an attr in xml doesn't match the attr in search
return true;
}
}
crm_log_xml_trace(xml, "delete-match");
crm_log_xml_trace(search, "delete-search");
pcmk__xml_free(xml);
// Found a match and deleted it; stop traversing tree
return false;
}
/*!
* \internal
* \brief Search an XML tree depth-first and delete the first matching element
*
* This function does not attempt to match the tree root (\p xml).
*
* A match with a node \c node is defined as follows:
* * \c node and \p search are both element nodes of the same type.
* * If \p search has attributes set, \c node has those attributes set to the
* same values. (\c node may have additional attributes set to arbitrary
* values.)
*
* \param[in,out] xml XML subtree to search
* \param[in] search Element to match against
*
* \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
* successful deletion and an error code otherwise)
*/
int
pcmk__xe_delete_match(xmlNode *xml, xmlNode *search)
{
// See @COMPAT comment in pcmk__xe_replace_match()
CRM_CHECK((xml != NULL) && (search != NULL), return EINVAL);
for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL;
xml = pcmk__xe_next(xml, NULL)) {
if (!pcmk__xml_tree_foreach(xml, delete_xe_if_matching, search)) {
// Found and deleted an element
return pcmk_rc_ok;
}
}
// No match found in this subtree
return ENXIO;
}
/*!
* \internal
* \brief Replace one XML node with a copy of another XML node
*
* This function handles change tracking and applies ACLs.
*
* \param[in,out] old XML node to replace
* \param[in] new XML node to copy as replacement for \p old
*
* \note This frees \p old.
*/
static void
replace_node(xmlNode *old, xmlNode *new)
{
// Pass old for its doc; it won't remain the parent of new
new = pcmk__xml_copy(old, new);
old = xmlReplaceNode(old, new);
// old == NULL means memory allocation error
pcmk__assert(old != NULL);
// May be unnecessary but avoids slight changes to some test outputs
pcmk__xml_tree_foreach(new, pcmk__xml_reset_node_flags, NULL);
if (pcmk__xml_doc_all_flags_set(new->doc, pcmk__xf_tracking)) {
// Replaced sections may have included relevant ACLs
pcmk__apply_acl(new);
}
- xml_calculate_changes(old, new);
+ pcmk__xml_mark_changes(old, new);
pcmk__xml_free_node(old);
}
/*!
* \internal
* \brief Replace one XML subtree with a copy of another if the two match
*
* A match is defined as follows:
* * \p xml and \p user_data are both element nodes of the same type.
* * If \p user_data has the \c PCMK_XA_ID attribute set, then \p xml has
* \c PCMK_XA_ID set to the same value.
*
* \param[in,out] xml XML subtree to replace with \p user_data upon match
* \param[in] user_data XML to replace \p xml with a copy of upon match
*
* \return \c true to continue traversing the tree, or \c false to stop (because
* \p xml was replaced by \p user_data)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
replace_xe_if_matching(xmlNode *xml, void *user_data)
{
xmlNode *replace = user_data;
const char *xml_id = NULL;
const char *replace_id = NULL;
xml_id = pcmk__xe_id(xml);
replace_id = pcmk__xe_id(replace);
if (!pcmk__xe_is(replace, (const char *) xml->name)) {
// No match: either not both elements, or different element types
return true;
}
if ((replace_id != NULL)
&& !pcmk__str_eq(replace_id, xml_id, pcmk__str_none)) {
// No match: ID was provided in replace and doesn't match xml's ID
return true;
}
crm_log_xml_trace(xml, "replace-match");
crm_log_xml_trace(replace, "replace-with");
replace_node(xml, replace);
// Found a match and replaced it; stop traversing tree
return false;
}
/*!
* \internal
* \brief Search an XML tree depth-first and replace the first matching element
*
* This function does not attempt to match the tree root (\p xml).
*
* A match with a node \c node is defined as follows:
* * \c node and \p replace are both element nodes of the same type.
* * If \p replace has the \c PCMK_XA_ID attribute set, then \c node has
* \c PCMK_XA_ID set to the same value.
*
* \param[in,out] xml XML tree to search
* \param[in] replace XML to replace a matching element with a copy of
*
* \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
* successful replacement and an error code otherwise)
*/
int
pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace)
{
/* @COMPAT Some of this behavior (like not matching the tree root, which is
* allowed by pcmk__xe_update_match()) is questionable for general use but
* required for backward compatibility by cib_process_replace() and
* cib_process_delete(). Behavior can change at a major version release if
* desired.
*/
CRM_CHECK((xml != NULL) && (replace != NULL), return EINVAL);
for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL;
xml = pcmk__xe_next(xml, NULL)) {
if (!pcmk__xml_tree_foreach(xml, replace_xe_if_matching, replace)) {
// Found and replaced an element
return pcmk_rc_ok;
}
}
// No match found in this subtree
return ENXIO;
}
//! User data for \c update_xe_if_matching()
struct update_data {
xmlNode *update; //!< Update source
uint32_t flags; //!< Group of <tt>enum pcmk__xa_flags</tt>
};
/*!
* \internal
* \brief Update one XML subtree with another if the two match
*
* "Update" means to merge a source subtree into a target subtree (see
* \c update_xe()).
*
* A match is defined as follows:
* * \p xml and \p user_data->update are both element nodes of the same type.
* * \p xml and \p user_data->update have the same \c PCMK_XA_ID attribute
* value, or \c PCMK_XA_ID is unset in both
*
* \param[in,out] xml XML subtree to update with \p user_data->update
* upon match
* \param[in] user_data <tt>struct update_data</tt> object
*
* \return \c true to continue traversing the tree, or \c false to stop (because
* \p xml was updated by \p user_data->update)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
update_xe_if_matching(xmlNode *xml, void *user_data)
{
struct update_data *data = user_data;
xmlNode *update = data->update;
if (!pcmk__xe_is(update, (const char *) xml->name)) {
// No match: either not both elements, or different element types
return true;
}
if (!pcmk__str_eq(pcmk__xe_id(xml), pcmk__xe_id(update), pcmk__str_none)) {
// No match: ID mismatch
return true;
}
crm_log_xml_trace(xml, "update-match");
crm_log_xml_trace(update, "update-with");
update_xe(NULL, xml, update, data->flags);
// Found a match and replaced it; stop traversing tree
return false;
}
/*!
* \internal
* \brief Search an XML tree depth-first and update the first matching element
*
* "Update" means to merge a source subtree into a target subtree (see
* \c update_xe()).
*
* A match with a node \c node is defined as follows:
* * \c node and \p update are both element nodes of the same type.
* * \c node and \p update have the same \c PCMK_XA_ID attribute value, or
* \c PCMK_XA_ID is unset in both
*
* \param[in,out] xml XML tree to search
* \param[in] update XML to update a matching element with
* \param[in] flags Group of <tt>enum pcmk__xa_flags</tt>
*
* \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
* successful update and an error code otherwise)
*/
int
pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags)
{
/* @COMPAT In pcmk__xe_delete_match() and pcmk__xe_replace_match(), we
* compare IDs only if the equivalent of the update argument has an ID.
* Here, we're stricter: we consider it a mismatch if only one element has
* an ID attribute, or if both elements have IDs but they don't match.
*
* Perhaps we should align the behavior at a major version release.
*/
struct update_data data = {
.update = update,
.flags = flags,
};
CRM_CHECK((xml != NULL) && (update != NULL), return EINVAL);
if (!pcmk__xml_tree_foreach(xml, update_xe_if_matching, &data)) {
// Found and updated an element
return pcmk_rc_ok;
}
// No match found in this subtree
return ENXIO;
}
void
pcmk__xe_set_propv(xmlNodePtr node, va_list pairs)
{
while (true) {
const char *name, *value;
name = va_arg(pairs, const char *);
if (name == NULL) {
return;
}
value = va_arg(pairs, const char *);
if (value != NULL) {
crm_xml_add(node, name, value);
}
}
}
void
pcmk__xe_set_props(xmlNodePtr node, ...)
{
va_list pairs;
va_start(pairs, node);
pcmk__xe_set_propv(node, pairs);
va_end(pairs);
}
int
pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name,
int (*handler)(xmlNode *xml, void *userdata),
void *userdata)
{
xmlNode *children = (xml? xml->children : NULL);
pcmk__assert(handler != NULL);
for (xmlNode *node = children; node != NULL; node = node->next) {
if ((node->type == XML_ELEMENT_NODE)
&& ((child_element_name == NULL)
|| pcmk__xe_is(node, child_element_name))) {
int rc = handler(node, userdata);
if (rc != pcmk_rc_ok) {
return rc;
}
}
}
return pcmk_rc_ok;
}
// XML attribute handling
/*!
* \brief Create an XML attribute with specified name and value
*
* \param[in,out] node XML node to modify
* \param[in] name Attribute name to set
* \param[in] value Attribute value to set
*
* \return New value on success, \c NULL otherwise
* \note This does nothing if node, name, or value are \c NULL or empty.
*/
const char *
crm_xml_add(xmlNode *node, const char *name, const char *value)
{
// @TODO Replace with internal function that returns the new attribute
bool dirty = FALSE;
xmlAttr *attr = NULL;
CRM_CHECK(node != NULL, return NULL);
CRM_CHECK(name != NULL, return NULL);
if (value == NULL) {
return NULL;
}
if (pcmk__xml_doc_all_flags_set(node->doc, pcmk__xf_tracking)) {
const char *old = crm_element_value(node, name);
if (old == NULL || value == NULL || strcmp(old, value) != 0) {
dirty = TRUE;
}
}
if (dirty && (pcmk__check_acl(node, name, pcmk__xf_acl_create) == FALSE)) {
crm_trace("Cannot add %s=%s to %s", name, value, node->name);
return NULL;
}
attr = xmlSetProp(node, (const xmlChar *) name, (const xmlChar *) value);
/* If the attribute already exists, this does nothing. Attribute values
* don't get private data.
*/
pcmk__xml_new_private_data((xmlNode *) attr);
if (dirty) {
pcmk__mark_xml_attr_dirty(attr);
}
CRM_CHECK(attr && attr->children && attr->children->content, return NULL);
return (char *)attr->children->content;
}
/*!
* \brief Create an XML attribute with specified name and integer value
*
* This is like \c crm_xml_add() but taking an integer value.
*
* \param[in,out] node XML node to modify
* \param[in] name Attribute name to set
* \param[in] value Attribute value to set
*
* \return New value as string on success, \c NULL otherwise
* \note This does nothing if node or name are \c NULL or empty.
*/
const char *
crm_xml_add_int(xmlNode *node, const char *name, int value)
{
char *number = pcmk__itoa(value);
const char *added = crm_xml_add(node, name, number);
free(number);
return added;
}
/*!
* \brief Create an XML attribute with specified name and unsigned value
*
* This is like \c crm_xml_add() but taking a guint value.
*
* \param[in,out] node XML node to modify
* \param[in] name Attribute name to set
* \param[in] ms Attribute value to set
*
* \return New value as string on success, \c NULL otherwise
* \note This does nothing if node or name are \c NULL or empty.
*/
const char *
crm_xml_add_ms(xmlNode *node, const char *name, guint ms)
{
char *number = crm_strdup_printf("%u", ms);
const char *added = crm_xml_add(node, name, number);
free(number);
return added;
}
// Maximum size of null-terminated string representation of 64-bit integer
// -9223372036854775808
#define LLSTRSIZE 21
/*!
* \brief Create an XML attribute with specified name and long long int value
*
* This is like \c crm_xml_add() but taking a long long int value. It is a
* useful equivalent for defined types like time_t, etc.
*
* \param[in,out] xml XML node to modify
* \param[in] name Attribute name to set
* \param[in] value Attribute value to set
*
* \return New value as string on success, \c NULL otherwise
* \note This does nothing if xml or name are \c NULL or empty.
* This does not support greater than 64-bit values.
*/
const char *
crm_xml_add_ll(xmlNode *xml, const char *name, long long value)
{
char s[LLSTRSIZE] = { '\0', };
if (snprintf(s, LLSTRSIZE, "%lld", (long long) value) == LLSTRSIZE) {
return NULL;
}
return crm_xml_add(xml, name, s);
}
/*!
* \brief Create XML attributes for seconds and microseconds
*
* This is like \c crm_xml_add() but taking a struct timeval.
*
* \param[in,out] xml XML node to modify
* \param[in] name_sec Name of XML attribute for seconds
* \param[in] name_usec Name of XML attribute for microseconds (or NULL)
* \param[in] value Time value to set
*
* \return New seconds value as string on success, \c NULL otherwise
* \note This does nothing if xml, name_sec, or value is \c NULL.
*/
const char *
crm_xml_add_timeval(xmlNode *xml, const char *name_sec, const char *name_usec,
const struct timeval *value)
{
const char *added = NULL;
if (xml && name_sec && value) {
added = crm_xml_add_ll(xml, name_sec, (long long) value->tv_sec);
if (added && name_usec) {
// Any error is ignored (we successfully added seconds)
crm_xml_add_ll(xml, name_usec, (long long) value->tv_usec);
}
}
return added;
}
/*!
* \brief Retrieve the value of an XML attribute
*
* \param[in] data XML node to check
* \param[in] name Attribute name to check
*
* \return Value of specified attribute (may be \c NULL)
*/
const char *
crm_element_value(const xmlNode *data, const char *name)
{
xmlAttr *attr = NULL;
if (data == NULL) {
crm_err("Couldn't find %s in NULL", name ? name : "<null>");
CRM_LOG_ASSERT(data != NULL);
return NULL;
} else if (name == NULL) {
crm_err("Couldn't find NULL in %s", data->name);
return NULL;
}
attr = xmlHasProp(data, (const xmlChar *) name);
if (!attr || !attr->children) {
return NULL;
}
return (const char *) attr->children->content;
}
/*!
* \brief Retrieve the integer value of an XML attribute
*
* This is like \c crm_element_value() but getting the value as an integer.
*
* \param[in] data XML node to check
* \param[in] name Attribute name to check
* \param[out] dest Where to store element value
*
* \return 0 on success, -1 otherwise
*/
int
crm_element_value_int(const xmlNode *data, const char *name, int *dest)
{
const char *value = NULL;
CRM_CHECK(dest != NULL, return -1);
value = crm_element_value(data, name);
if (value) {
long long value_ll;
int rc = pcmk__scan_ll(value, &value_ll, 0LL);
*dest = PCMK__PARSE_INT_DEFAULT;
if (rc != pcmk_rc_ok) {
crm_warn("Using default for %s "
"because '%s' is not a valid integer: %s",
name, value, pcmk_rc_str(rc));
} else if ((value_ll < INT_MIN) || (value_ll > INT_MAX)) {
crm_warn("Using default for %s because '%s' is out of range",
name, value);
} else {
*dest = (int) value_ll;
return 0;
}
}
return -1;
}
/*!
* \internal
* \brief Retrieve a flag group from an XML attribute value
*
* This is like \c crm_element_value() except getting the value as a 32-bit
* unsigned integer.
*
* \param[in] xml XML node to check
* \param[in] name Attribute name to check (must not be NULL)
* \param[out] dest Where to store flags (may be NULL to just
* validate type)
* \param[in] default_value What to use for missing or invalid value
*
* \return Standard Pacemaker return code
*/
int
pcmk__xe_get_flags(const xmlNode *xml, const char *name, uint32_t *dest,
uint32_t default_value)
{
const char *value = NULL;
long long value_ll = 0LL;
int rc = pcmk_rc_ok;
if (dest != NULL) {
*dest = default_value;
}
if (name == NULL) {
return EINVAL;
}
if (xml == NULL) {
return pcmk_rc_ok;
}
value = crm_element_value(xml, name);
if (value == NULL) {
return pcmk_rc_ok;
}
rc = pcmk__scan_ll(value, &value_ll, default_value);
if ((value_ll < 0) || (value_ll > UINT32_MAX)) {
value_ll = default_value;
if (rc == pcmk_rc_ok) {
rc = pcmk_rc_bad_input;
}
}
if (dest != NULL) {
*dest = (uint32_t) value_ll;
}
return rc;
}
/*!
* \brief Retrieve the long long integer value of an XML attribute
*
* This is like \c crm_element_value() but getting the value as a long long int.
*
* \param[in] data XML node to check
* \param[in] name Attribute name to check
* \param[out] dest Where to store element value
*
* \return 0 on success, -1 otherwise
*/
int
crm_element_value_ll(const xmlNode *data, const char *name, long long *dest)
{
const char *value = NULL;
CRM_CHECK(dest != NULL, return -1);
value = crm_element_value(data, name);
if (value != NULL) {
int rc = pcmk__scan_ll(value, dest, PCMK__PARSE_INT_DEFAULT);
if (rc == pcmk_rc_ok) {
return 0;
}
crm_warn("Using default for %s "
"because '%s' is not a valid integer: %s",
name, value, pcmk_rc_str(rc));
}
return -1;
}
/*!
* \brief Retrieve the millisecond value of an XML attribute
*
* This is like \c crm_element_value() but returning the value as a guint.
*
* \param[in] data XML node to check
* \param[in] name Attribute name to check
* \param[out] dest Where to store attribute value
*
* \return \c pcmk_ok on success, -1 otherwise
*/
int
crm_element_value_ms(const xmlNode *data, const char *name, guint *dest)
{
const char *value = NULL;
long long value_ll;
int rc = pcmk_rc_ok;
CRM_CHECK(dest != NULL, return -1);
*dest = 0;
value = crm_element_value(data, name);
rc = pcmk__scan_ll(value, &value_ll, 0LL);
if (rc != pcmk_rc_ok) {
crm_warn("Using default for %s "
"because '%s' is not valid milliseconds: %s",
name, value, pcmk_rc_str(rc));
return -1;
}
if ((value_ll < 0) || (value_ll > G_MAXUINT)) {
crm_warn("Using default for %s because '%s' is out of range",
name, value);
return -1;
}
*dest = (guint) value_ll;
return pcmk_ok;
}
/*!
* \brief Retrieve the seconds-since-epoch value of an XML attribute
*
* This is like \c crm_element_value() but returning the value as a time_t.
*
* \param[in] xml XML node to check
* \param[in] name Attribute name to check
* \param[out] dest Where to store attribute value
*
* \return \c pcmk_ok on success, -1 otherwise
*/
int
crm_element_value_epoch(const xmlNode *xml, const char *name, time_t *dest)
{
long long value_ll = 0;
if (crm_element_value_ll(xml, name, &value_ll) < 0) {
return -1;
}
/* Unfortunately, we can't do any bounds checking, since time_t has neither
* standardized bounds nor constants defined for them.
*/
*dest = (time_t) value_ll;
return pcmk_ok;
}
/*!
* \brief Retrieve the value of XML second/microsecond attributes as time
*
* This is like \c crm_element_value() but returning value as a struct timeval.
*
* \param[in] xml XML to parse
* \param[in] name_sec Name of XML attribute for seconds
* \param[in] name_usec Name of XML attribute for microseconds
* \param[out] dest Where to store result
*
* \return \c pcmk_ok on success, -errno on error
* \note Values default to 0 if XML or XML attribute does not exist
*/
int
crm_element_value_timeval(const xmlNode *xml, const char *name_sec,
const char *name_usec, struct timeval *dest)
{
long long value_i = 0;
CRM_CHECK(dest != NULL, return -EINVAL);
dest->tv_sec = 0;
dest->tv_usec = 0;
if (xml == NULL) {
return pcmk_ok;
}
/* Unfortunately, we can't do any bounds checking, since there are no
* constants provided for the bounds of time_t and suseconds_t, and
* calculating them isn't worth the effort. If there are XML values
* beyond the native sizes, there will probably be worse problems anyway.
*/
// Parse seconds
errno = 0;
if (crm_element_value_ll(xml, name_sec, &value_i) < 0) {
return -errno;
}
dest->tv_sec = (time_t) value_i;
// Parse microseconds
if (crm_element_value_ll(xml, name_usec, &value_i) < 0) {
return -errno;
}
dest->tv_usec = (suseconds_t) value_i;
return pcmk_ok;
}
/*!
* \internal
* \brief Get a date/time object from an XML attribute value
*
* \param[in] xml XML with attribute to parse (from CIB)
* \param[in] attr Name of attribute to parse
* \param[out] t Where to create date/time object
* (\p *t must be NULL initially)
*
* \return Standard Pacemaker return code
* \note The caller is responsible for freeing \p *t using crm_time_free().
*/
int
pcmk__xe_get_datetime(const xmlNode *xml, const char *attr, crm_time_t **t)
{
const char *value = NULL;
if ((t == NULL) || (*t != NULL) || (xml == NULL) || (attr == NULL)) {
return EINVAL;
}
value = crm_element_value(xml, attr);
if (value != NULL) {
*t = crm_time_new(value);
if (*t == NULL) {
return pcmk_rc_unpack_error;
}
}
return pcmk_rc_ok;
}
/*!
* \brief Retrieve a copy of the value of an XML attribute
*
* This is like \c crm_element_value() but allocating new memory for the result.
*
* \param[in] data XML node to check
* \param[in] name Attribute name to check
*
* \return Value of specified attribute (may be \c NULL)
* \note The caller is responsible for freeing the result.
*/
char *
crm_element_value_copy(const xmlNode *data, const char *name)
{
return pcmk__str_copy(crm_element_value(data, name));
}
/*!
* \internal
* \brief Add a boolean attribute to an XML node.
*
* \param[in,out] node XML node to add attributes to
* \param[in] name XML attribute to create
* \param[in] value Value to give to the attribute
*/
void
pcmk__xe_set_bool_attr(xmlNodePtr node, const char *name, bool value)
{
crm_xml_add(node, name, pcmk__btoa(value));
}
/*!
* \internal
* \brief Extract a boolean attribute's value from an XML element, with
* error checking
*
* \param[in] node XML node to get attribute from
* \param[in] name XML attribute to get
* \param[out] value Destination for the value of the attribute
*
* \return EINVAL if \p name or \p value are NULL, ENODATA if \p node is
* NULL or the attribute does not exist, pcmk_rc_unknown_format
* if the attribute is not a boolean, and pcmk_rc_ok otherwise.
*
* \note \p value only has any meaning if the return value is pcmk_rc_ok.
*/
int
pcmk__xe_get_bool_attr(const xmlNode *node, const char *name, bool *value)
{
const char *xml_value = NULL;
int ret, rc;
if (node == NULL) {
return ENODATA;
} else if (name == NULL || value == NULL) {
return EINVAL;
}
xml_value = crm_element_value(node, name);
if (xml_value == NULL) {
return ENODATA;
}
rc = crm_str_to_boolean(xml_value, &ret);
if (rc == 1) {
*value = ret;
return pcmk_rc_ok;
} else {
return pcmk_rc_bad_input;
}
}
/*!
* \internal
* \brief Extract a boolean attribute's value from an XML element
*
* \param[in] node XML node to get attribute from
* \param[in] name XML attribute to get
*
* \return True if the given \p name is an attribute on \p node and has
* the value \c PCMK_VALUE_TRUE, False in all other cases
*/
bool
pcmk__xe_attr_is_true(const xmlNode *node, const char *name)
{
bool value = false;
int rc;
rc = pcmk__xe_get_bool_attr(node, name, &value);
return rc == pcmk_rc_ok && value == true;
}
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
#include <glib.h> // gboolean, GSList
#include <crm/common/nvpair_compat.h> // pcmk_xml_attrs2nvpairs(), etc.
#include <crm/common/xml_compat.h> // crm_xml_sanitize_id()
#include <crm/common/xml_element_compat.h>
xmlNode *
expand_idref(xmlNode *input, xmlNode *top)
{
return pcmk__xe_resolve_idref(input, top);
}
void
crm_xml_set_id(xmlNode *xml, const char *format, ...)
{
va_list ap;
int len = 0;
char *id = NULL;
/* equivalent to crm_strdup_printf() */
va_start(ap, format);
len = vasprintf(&id, format, ap);
va_end(ap);
pcmk__assert(len > 0);
crm_xml_sanitize_id(id);
crm_xml_add(xml, PCMK_XA_ID, id);
free(id);
}
xmlNode *
sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive)
{
xmlNode *child = NULL;
GSList *nvpairs = NULL;
xmlNode *result = NULL;
CRM_CHECK(input != NULL, return NULL);
result = pcmk__xe_create(parent, (const char *) input->name);
nvpairs = pcmk_xml_attrs2nvpairs(input);
nvpairs = pcmk_sort_nvpairs(nvpairs);
pcmk_nvpairs2xml_attrs(nvpairs, result);
pcmk_free_nvpairs(nvpairs);
for (child = pcmk__xe_first_child(input, NULL, NULL, NULL); child != NULL;
child = pcmk__xe_next(child, NULL)) {
if (recursive) {
sorted_xml(child, result, recursive);
} else {
pcmk__xml_copy(result, child);
}
}
return result;
}
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/tools/crm_diff.c b/tools/crm_diff.c
index 6fb5a1fb9f..27eb6d155c 100644
--- a/tools/crm_diff.c
+++ b/tools/crm_diff.c
@@ -1,338 +1,336 @@
/*
- * Copyright 2005-2024 the Pacemaker project contributors
+ * 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);
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]);
}
}
- xml_track_changes(object_new, NULL, object_new, FALSE);
- if(as_cib) {
- xml_calculate_significant_changes(object_original, object_new);
- } else {
- xml_calculate_changes(object_original, object_new);
+ 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);
- xml_accept_changes(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) {
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);
}
diff --git a/tools/crm_shadow.c b/tools/crm_shadow.c
index d28b2e7c83..823d3bc4b1 100644
--- a/tools/crm_shadow.c
+++ b/tools/crm_shadow.c
@@ -1,1308 +1,1307 @@
/*
* 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 <stdio.h>
#include <unistd.h>
#include <sys/param.h>
#include <crm/crm.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <crm/common/cmdline_internal.h>
#include <crm/common/ipc.h>
#include <crm/common/output_internal.h>
#include <crm/common/xml.h>
#include <crm/cib.h>
#include <crm/cib/internal.h>
#define SUMMARY "perform Pacemaker configuration changes in a sandbox\n\n" \
"This command sets up an environment in which " \
"configuration tools (cibadmin,\n" \
"crm_resource, etc.) work offline instead of against a " \
"live cluster, allowing\n" \
"changes to be previewed and tested for side effects."
#define INDENT " "
enum shadow_command {
shadow_cmd_none = 0,
shadow_cmd_which,
shadow_cmd_display,
shadow_cmd_diff,
shadow_cmd_file,
shadow_cmd_create,
shadow_cmd_create_empty,
shadow_cmd_commit,
shadow_cmd_delete,
shadow_cmd_edit,
shadow_cmd_reset,
shadow_cmd_switch,
};
/*!
* \internal
* \brief Bit flags to control which fields of shadow CIB info are displayed
*
* \note Ignored for XML output.
*/
enum shadow_disp_flags {
shadow_disp_instance = (1 << 0),
shadow_disp_file = (1 << 1),
shadow_disp_content = (1 << 2),
shadow_disp_diff = (1 << 3),
};
static crm_exit_t exit_code = CRM_EX_OK;
static struct {
enum shadow_command cmd;
int cmd_options;
char *instance;
gboolean force;
gboolean batch;
gboolean full_upload;
gchar *validate_with;
} options = {
.cmd_options = cib_sync_call,
};
/*!
* \internal
* \brief Display an instruction to the user
*
* \param[in,out] out Output object
* \param[in] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# Instructional message
*/
PCMK__OUTPUT_ARGS("instruction", "const char *")
static int
instruction_default(pcmk__output_t *out, va_list args)
{
const char *msg = va_arg(args, const char *);
if (msg == NULL) {
return pcmk_rc_no_output;
}
return out->info(out, "%s", msg);
}
/*!
* \internal
* \brief Display an instruction to the user
*
* \param[in,out] out Output object
* \param[in] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# Instructional message
*/
PCMK__OUTPUT_ARGS("instruction", "const char *")
static int
instruction_xml(pcmk__output_t *out, va_list args)
{
const char *msg = va_arg(args, const char *);
if (msg == NULL) {
return pcmk_rc_no_output;
}
pcmk__output_create_xml_text_node(out, "instruction", msg);
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Display information about a shadow CIB instance
*
* \param[in,out] out Output object
* \param[in] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# Instance name (can be \p NULL)
* -# Shadow file name (can be \p NULL)
* -# Shadow file content (can be \p NULL)
* -# Patchset containing the changes in the shadow CIB (can be \p NULL)
* -# Group of \p shadow_disp_flags indicating which fields to display
*/
PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
"const xmlNode *", "enum shadow_disp_flags")
static int
shadow_default(pcmk__output_t *out, va_list args)
{
const char *instance = va_arg(args, const char *);
const char *filename = va_arg(args, const char *);
const xmlNode *content = va_arg(args, const xmlNode *);
const xmlNode *diff = va_arg(args, const xmlNode *);
enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int);
int rc = pcmk_rc_no_output;
if (pcmk_is_set(flags, shadow_disp_instance)) {
rc = out->info(out, "Instance: %s", pcmk__s(instance, "<unknown>"));
}
if (pcmk_is_set(flags, shadow_disp_file)) {
rc = out->info(out, "File name: %s", pcmk__s(filename, "<unknown>"));
}
if (pcmk_is_set(flags, shadow_disp_content)) {
rc = out->info(out, "Content:");
if (content != NULL) {
GString *buf = g_string_sized_new(1024);
gchar *str = NULL;
pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
buf, 0);
str = g_string_free(buf, FALSE);
str = pcmk__trim(str);
if (!pcmk__str_empty(str)) {
out->info(out, "%s", str);
}
g_free(str);
} else {
out->info(out, "<unknown>");
}
}
if (pcmk_is_set(flags, shadow_disp_diff)) {
rc = out->info(out, "Diff:");
if (diff != NULL) {
out->message(out, "xml-patchset", diff);
} else {
out->info(out, "<empty>");
}
}
return rc;
}
/*!
* \internal
* \brief Display information about a shadow CIB instance
*
* \param[in,out] out Output object
* \param[in] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# Instance name (can be \p NULL)
* -# Shadow file name (can be \p NULL)
* -# Shadow file content (can be \p NULL)
* -# Patchset containing the changes in the shadow CIB (can be \p NULL)
* -# Group of \p shadow_disp_flags indicating which fields to display
*/
PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
"const xmlNode *", "enum shadow_disp_flags")
static int
shadow_text(pcmk__output_t *out, va_list args)
{
if (!out->is_quiet(out)) {
return shadow_default(out, args);
} else {
const char *instance = va_arg(args, const char *);
const char *filename = va_arg(args, const char *);
const xmlNode *content = va_arg(args, const xmlNode *);
const xmlNode *diff = va_arg(args, const xmlNode *);
enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int);
int rc = pcmk_rc_no_output;
bool quiet_orig = out->quiet;
/* We have to disable quiet mode for the "xml-patchset" message if we
* call it, so we might as well do so for this whole section.
*/
out->quiet = false;
if (pcmk_is_set(flags, shadow_disp_instance) && (instance != NULL)) {
rc = out->info(out, "%s", instance);
}
if (pcmk_is_set(flags, shadow_disp_file) && (filename != NULL)) {
rc = out->info(out, "%s", filename);
}
if (pcmk_is_set(flags, shadow_disp_content) && (content != NULL)) {
GString *buf = g_string_sized_new(1024);
gchar *str = NULL;
pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
buf, 0);
str = g_string_free(buf, FALSE);
str = pcmk__trim(str);
rc = out->info(out, "%s", str);
g_free(str);
}
if (pcmk_is_set(flags, shadow_disp_diff) && (diff != NULL)) {
rc = out->message(out, "xml-patchset", diff);
}
out->quiet = quiet_orig;
return rc;
}
}
/*!
* \internal
* \brief Display information about a shadow CIB instance
*
* \param[in,out] out Output object
* \param[in] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# Instance name (can be \p NULL)
* -# Shadow file name (can be \p NULL)
* -# Shadow file content (can be \p NULL)
* -# Patchset containing the changes in the shadow CIB (can be \p NULL)
* -# Group of \p shadow_disp_flags indicating which fields to display
* (ignored)
*/
PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
"const xmlNode *", "enum shadow_disp_flags")
static int
shadow_xml(pcmk__output_t *out, va_list args)
{
const char *instance = va_arg(args, const char *);
const char *filename = va_arg(args, const char *);
const xmlNode *content = va_arg(args, const xmlNode *);
const xmlNode *diff = va_arg(args, const xmlNode *);
enum shadow_disp_flags flags G_GNUC_UNUSED =
(enum shadow_disp_flags) va_arg(args, int);
pcmk__output_xml_create_parent(out, PCMK_XE_SHADOW,
PCMK_XA_INSTANCE, instance,
PCMK_XA_FILE, filename,
NULL);
if (content != NULL) {
GString *buf = g_string_sized_new(1024);
pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buf,
0);
out->output_xml(out, PCMK_XE_CONTENT, buf->str);
g_string_free(buf, TRUE);
}
if (diff != NULL) {
out->message(out, "xml-patchset", diff);
}
pcmk__output_xml_pop_parent(out);
return pcmk_rc_ok;
}
static const pcmk__supported_format_t formats[] = {
PCMK__SUPPORTED_FORMAT_NONE,
PCMK__SUPPORTED_FORMAT_TEXT,
PCMK__SUPPORTED_FORMAT_XML,
{ NULL, NULL, NULL }
};
static const pcmk__message_entry_t fmt_functions[] = {
{ "instruction", "default", instruction_default },
{ "instruction", "xml", instruction_xml },
{ "shadow", "default", shadow_default },
{ "shadow", "text", shadow_text },
{ "shadow", "xml", shadow_xml },
{ NULL, NULL, NULL }
};
/*!
* \internal
* \brief Set the error when \p --force is not passed with a dangerous command
*
* \param[in] reason Why command is dangerous
* \param[in] for_shadow If true, command is dangerous to the shadow file.
* Otherwise, command is dangerous to the active
* cluster.
* \param[in] show_mismatch If true and the supplied shadow instance is not
* the same as the active shadow instance, report
* this
* \param[out] error Where to store error
*/
static void
set_danger_error(const char *reason, bool for_shadow, bool show_mismatch,
GError **error)
{
const char *active = getenv("CIB_shadow");
char *full = NULL;
if (show_mismatch
&& !pcmk__str_eq(active, options.instance, pcmk__str_null_matches)) {
full = crm_strdup_printf("%s.\nAdditionally, the supplied shadow "
"instance (%s) is not the same as the active "
"one (%s)",
reason, options.instance, active);
reason = full;
}
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"%s%sTo prevent accidental destruction of the %s, the --force "
"flag is required in order to proceed.",
pcmk__s(reason, ""), ((reason != NULL)? ".\n" : ""),
(for_shadow? "shadow file" : "cluster"));
free(full);
}
/*!
* \internal
* \brief Get the active shadow instance from the environment
*
* This sets \p options.instance to the value of the \p CIB_shadow env variable.
*
* \param[out] error Where to store error
*/
static int
get_instance_from_env(GError **error)
{
int rc = pcmk_rc_ok;
pcmk__str_update(&options.instance, getenv("CIB_shadow"));
if (options.instance == NULL) {
rc = ENXIO;
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"No active shadow configuration defined");
}
return rc;
}
/*!
* \internal
* \brief Validate that the shadow file does or does not exist, as appropriate
*
* \param[in] filename Absolute path of shadow file
* \param[in] should_exist Whether the shadow file is expected to exist
* \param[out] error Where to store error
*
* \return Standard Pacemaker return code
*/
static int
check_file_exists(const char *filename, bool should_exist, GError **error)
{
struct stat buf;
if (!should_exist && (stat(filename, &buf) == 0)) {
char *reason = crm_strdup_printf("A shadow instance '%s' already "
"exists", options.instance);
exit_code = CRM_EX_CANTCREAT;
set_danger_error(reason, true, false, error);
free(reason);
return EEXIST;
}
if (should_exist && (stat(filename, &buf) < 0)) {
int rc = errno;
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not access shadow instance '%s': %s",
options.instance, strerror(rc));
return errno;
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Connect to the "real" (non-shadow) CIB
*
* \param[out] real_cib Where to store CIB connection
* \param[out] error Where to store error
*
* \return Standard Pacemaker return code
*/
static int
connect_real_cib(cib_t **real_cib, GError **error)
{
int rc = pcmk_rc_ok;
*real_cib = cib_new_no_shadow();
if (*real_cib == NULL) {
rc = ENOMEM;
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not create a CIB connection object");
return rc;
}
rc = cib__signon_attempts(*real_cib, cib_command, 5);
rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not connect to CIB: %s", pcmk_rc_str(rc));
}
return rc;
}
/*!
* \internal
* \brief Query the "real" (non-shadow) CIB and store the result
*
* \param[out] output Where to store query output
* \param[out] error Where to store error
*
* \return Standard Pacemaker return code
*/
static int
query_real_cib(xmlNode **output, GError **error)
{
cib_t *real_cib = NULL;
int rc = connect_real_cib(&real_cib, error);
if (rc != pcmk_rc_ok) {
goto done;
}
rc = real_cib->cmds->query(real_cib, NULL, output, options.cmd_options);
rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not query the non-shadow CIB: %s", pcmk_rc_str(rc));
}
done:
cib_delete(real_cib);
return rc;
}
/*!
* \internal
* \brief Read XML from the given file
*
* \param[in] filename Path of input file
* \param[out] output Where to store XML read from \p filename
* \param[out] error Where to store error
*
* \return Standard Pacemaker return code
*/
static int
read_xml(const char *filename, xmlNode **output, GError **error)
{
int rc = pcmk_rc_ok;
*output = pcmk__xml_read(filename);
if (*output == NULL) {
rc = pcmk_rc_no_input;
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not parse XML from input file '%s'", filename);
}
return rc;
}
/*!
* \internal
* \brief Write the shadow XML to a file
*
* \param[in] xml Shadow XML
* \param[in] filename Name of destination file
* \param[in] reset Whether the write is a reset (for logging only)
* \param[out] error Where to store error
*/
static int
write_shadow_file(const xmlNode *xml, const char *filename, bool reset,
GError **error)
{
int rc = pcmk__xml_write_file(xml, filename, false);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not %s the shadow instance '%s': %s",
reset? "reset" : "create", options.instance,
pcmk_rc_str(rc));
}
return rc;
}
/*!
* \internal
* \brief Create a shell prompt based on the given shadow instance name
*
* \return Newly created prompt
*
* \note The caller is responsible for freeing the return value using \p free().
*/
static inline char *
get_shadow_prompt(void)
{
return crm_strdup_printf("shadow[%.40s] # ", options.instance);
}
/*!
* \internal
* \brief Set up environment variables for a shadow instance
*
* \param[in,out] out Output object
* \param[in] do_switch If true, switch to an existing instance (logging
* only)
* \param[out] error Where to store error
*/
static void
shadow_setup(pcmk__output_t *out, bool do_switch, GError **error)
{
const char *active = getenv("CIB_shadow");
const char *prompt = getenv("PS1");
const char *shell = getenv("SHELL");
char *new_prompt = get_shadow_prompt();
if (pcmk__str_eq(active, options.instance, pcmk__str_none)
&& pcmk__str_eq(new_prompt, prompt, pcmk__str_none)) {
// CIB_shadow and prompt environment variables are already set up
goto done;
}
if (!options.batch && (shell != NULL)) {
out->info(out, "Setting up shadow instance");
setenv("PS1", new_prompt, 1);
setenv("CIB_shadow", options.instance, 1);
out->message(out, PCMK_XE_INSTRUCTION,
"Press Ctrl+D to exit the crm_shadow shell");
if (pcmk__str_eq(shell, "(^|/)bash$", pcmk__str_regex)) {
execl(shell, shell, "--norc", "--noprofile", NULL);
} else {
execl(shell, shell, NULL);
}
exit_code = pcmk_rc2exitc(errno);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Failed to launch shell '%s': %s",
shell, pcmk_rc_str(errno));
} else {
char *msg = NULL;
const char *prefix = "A new shadow instance was created. To begin "
"using it";
if (do_switch) {
prefix = "To switch to the named shadow instance";
}
msg = crm_strdup_printf("%s, enter the following into your shell:\n"
"\texport CIB_shadow=%s",
prefix, options.instance);
out->message(out, "instruction", msg);
free(msg);
}
done:
free(new_prompt);
}
/*!
* \internal
* \brief Remind the user to clean up the shadow environment
*
* \param[in,out] out Output object
*/
static void
shadow_teardown(pcmk__output_t *out)
{
const char *active = getenv("CIB_shadow");
const char *prompt = getenv("PS1");
if (pcmk__str_eq(active, options.instance, pcmk__str_none)) {
char *our_prompt = get_shadow_prompt();
if (pcmk__str_eq(prompt, our_prompt, pcmk__str_none)) {
out->message(out, "instruction",
"Press Ctrl+D to exit the crm_shadow shell");
} else {
out->message(out, "instruction",
"Remember to unset the CIB_shadow variable by "
"entering the following into your shell:\n"
"\tunset CIB_shadow");
}
free(our_prompt);
}
}
/*!
* \internal
* \brief Commit the shadow file contents to the active cluster
*
* \param[out] error Where to store error
*/
static void
commit_shadow_file(GError **error)
{
char *filename = NULL;
cib_t *real_cib = NULL;
xmlNodePtr input = NULL;
xmlNodePtr section_xml = NULL;
const char *section = NULL;
int rc = pcmk_rc_ok;
if (!options.force) {
const char *reason = "The commit command overwrites the active cluster "
"configuration";
exit_code = CRM_EX_USAGE;
set_danger_error(reason, false, true, error);
return;
}
filename = get_shadow_file(options.instance);
if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
goto done;
}
if (connect_real_cib(&real_cib, error) != pcmk_rc_ok) {
goto done;
}
if (read_xml(filename, &input, error) != pcmk_rc_ok) {
goto done;
}
section_xml = input;
if (!options.full_upload) {
section = PCMK_XE_CONFIGURATION;
section_xml = pcmk__xe_first_child(input, section, NULL, NULL);
}
rc = real_cib->cmds->replace(real_cib, section, section_xml,
options.cmd_options);
rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not commit shadow instance '%s' to the CIB: %s",
options.instance, pcmk_rc_str(rc));
}
done:
free(filename);
cib_delete(real_cib);
pcmk__xml_free(input);
}
/*!
* \internal
* \brief Create a new empty shadow instance
*
* \param[in,out] out Output object
* \param[out] error Where to store error
*
* \note If \p --force is given, we try to write the file regardless of whether
* it already exists.
*/
static void
create_shadow_empty(pcmk__output_t *out, GError **error)
{
char *filename = get_shadow_file(options.instance);
xmlNode *output = NULL;
if (!options.force
&& (check_file_exists(filename, false, error) != pcmk_rc_ok)) {
goto done;
}
output = createEmptyCib(0);
crm_xml_add(output, PCMK_XA_VALIDATE_WITH, options.validate_with);
out->info(out, "Created new %s configuration",
crm_element_value(output, PCMK_XA_VALIDATE_WITH));
if (write_shadow_file(output, filename, false, error) != pcmk_rc_ok) {
goto done;
}
shadow_setup(out, false, error);
done:
free(filename);
pcmk__xml_free(output);
}
/*!
* \internal
* \brief Create a shadow instance based on the active CIB
*
* \param[in,out] out Output object
* \param[in] reset If true, overwrite the given existing shadow instance.
* Otherwise, create a new shadow instance with the given
* name.
* \param[out] error Where to store error
*
* \note If \p --force is given, we try to write the file regardless of whether
* it already exists.
*/
static void
create_shadow_from_cib(pcmk__output_t *out, bool reset, GError **error)
{
char *filename = get_shadow_file(options.instance);
xmlNode *output = NULL;
if (!options.force) {
if (reset) {
const char *reason = "The reset command overwrites the active "
"shadow configuration";
exit_code = CRM_EX_USAGE;
set_danger_error(reason, true, true, error);
goto done;
}
if (check_file_exists(filename, reset, error) != pcmk_rc_ok) {
goto done;
}
}
if (query_real_cib(&output, error) != pcmk_rc_ok) {
goto done;
}
if (write_shadow_file(output, filename, reset, error) != pcmk_rc_ok) {
goto done;
}
shadow_setup(out, false, error);
done:
free(filename);
pcmk__xml_free(output);
}
/*!
* \internal
* \brief Delete the shadow file
*
* \param[in,out] out Output object
* \param[out] error Where to store error
*/
static void
delete_shadow_file(pcmk__output_t *out, GError **error)
{
char *filename = NULL;
if (!options.force) {
const char *reason = "The delete command removes the specified shadow "
"file";
exit_code = CRM_EX_USAGE;
set_danger_error(reason, true, true, error);
return;
}
filename = get_shadow_file(options.instance);
if ((unlink(filename) < 0) && (errno != ENOENT)) {
exit_code = pcmk_rc2exitc(errno);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not remove shadow instance '%s': %s",
options.instance, strerror(errno));
} else {
shadow_teardown(out);
}
free(filename);
}
/*!
* \internal
* \brief Open the shadow file in a text editor
*
* \param[out] error Where to store error
*
* \note The \p EDITOR environment variable must be set.
*/
static void
edit_shadow_file(GError **error)
{
char *filename = NULL;
const char *editor = NULL;
if (get_instance_from_env(error) != pcmk_rc_ok) {
return;
}
filename = get_shadow_file(options.instance);
if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
goto done;
}
editor = getenv("EDITOR");
if (editor == NULL) {
exit_code = CRM_EX_NOT_CONFIGURED;
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"No value for EDITOR defined");
goto done;
}
execlp(editor, "--", filename, NULL);
exit_code = CRM_EX_OSFILE;
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not invoke EDITOR (%s %s): %s",
editor, filename, strerror(errno));
done:
free(filename);
}
/*!
* \internal
* \brief Show the contents of the active shadow instance
*
* \param[in,out] out Output object
* \param[out] error Where to store error
*/
static void
show_shadow_contents(pcmk__output_t *out, GError **error)
{
char *filename = NULL;
if (get_instance_from_env(error) != pcmk_rc_ok) {
return;
}
filename = get_shadow_file(options.instance);
if (check_file_exists(filename, true, error) == pcmk_rc_ok) {
xmlNode *output = NULL;
bool quiet_orig = out->quiet;
if (read_xml(filename, &output, error) != pcmk_rc_ok) {
goto done;
}
out->quiet = true;
out->message(out, "shadow",
options.instance, NULL, output, NULL, shadow_disp_content);
out->quiet = quiet_orig;
pcmk__xml_free(output);
}
done:
free(filename);
}
/*!
* \internal
* \brief Show the changes in the active shadow instance
*
* \param[in,out] out Output object
* \param[out] error Where to store error
*/
static void
show_shadow_diff(pcmk__output_t *out, GError **error)
{
char *filename = NULL;
xmlNodePtr old_config = NULL;
xmlNodePtr new_config = NULL;
xmlNodePtr diff = NULL;
bool quiet_orig = out->quiet;
if (get_instance_from_env(error) != pcmk_rc_ok) {
return;
}
filename = get_shadow_file(options.instance);
if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
goto done;
}
if (query_real_cib(&old_config, error) != pcmk_rc_ok) {
goto done;
}
if (read_xml(filename, &new_config, error) != pcmk_rc_ok) {
goto done;
}
- xml_track_changes(new_config, NULL, new_config, false);
- xml_calculate_changes(old_config, new_config);
+ pcmk__xml_mark_changes(old_config, new_config);
diff = xml_create_patchset(0, old_config, new_config, NULL, false);
pcmk__log_xml_changes(LOG_INFO, new_config);
- xml_accept_changes(new_config);
+ pcmk__xml_commit_changes(new_config->doc);
out->quiet = true;
out->message(out, "shadow",
options.instance, NULL, NULL, diff, shadow_disp_diff);
out->quiet = quiet_orig;
if (diff != NULL) {
/* @COMPAT: Exit with CRM_EX_DIGEST? This is not really an error; we
* just want to indicate that there are differences (as the diff command
* does).
*/
exit_code = CRM_EX_ERROR;
}
done:
free(filename);
pcmk__xml_free(old_config);
pcmk__xml_free(new_config);
pcmk__xml_free(diff);
}
/*!
* \internal
* \brief Show the absolute path of the active shadow instance
*
* \param[in,out] out Output object
* \param[out] error Where to store error
*/
static void
show_shadow_filename(pcmk__output_t *out, GError **error)
{
if (get_instance_from_env(error) == pcmk_rc_ok) {
char *filename = get_shadow_file(options.instance);
bool quiet_orig = out->quiet;
out->quiet = true;
out->message(out, "shadow",
options.instance, filename, NULL, NULL, shadow_disp_file);
out->quiet = quiet_orig;
free(filename);
}
}
/*!
* \internal
* \brief Show the active shadow instance
*
* \param[in,out] out Output object
* \param[out] error Where to store error
*/
static void
show_shadow_instance(pcmk__output_t *out, GError **error)
{
if (get_instance_from_env(error) == pcmk_rc_ok) {
bool quiet_orig = out->quiet;
out->quiet = true;
out->message(out, "shadow",
options.instance, NULL, NULL, NULL, shadow_disp_instance);
out->quiet = quiet_orig;
}
}
/*!
* \internal
* \brief Switch to the given shadow instance
*
* \param[in,out] out Output object
* \param[out] error Where to store error
*/
static void
switch_shadow_instance(pcmk__output_t *out, GError **error)
{
char *filename = NULL;
filename = get_shadow_file(options.instance);
if (check_file_exists(filename, true, error) == pcmk_rc_ok) {
shadow_setup(out, true, error);
}
free(filename);
}
static gboolean
command_cb(const gchar *option_name, const gchar *optarg, gpointer data,
GError **error)
{
if (pcmk__str_any_of(option_name, "-w", "--which", NULL)) {
options.cmd = shadow_cmd_which;
} else if (pcmk__str_any_of(option_name, "-p", "--display", NULL)) {
options.cmd = shadow_cmd_display;
} else if (pcmk__str_any_of(option_name, "-d", "--diff", NULL)) {
options.cmd = shadow_cmd_diff;
} else if (pcmk__str_any_of(option_name, "-F", "--file", NULL)) {
options.cmd = shadow_cmd_file;
} else if (pcmk__str_any_of(option_name, "-c", "--create", NULL)) {
options.cmd = shadow_cmd_create;
} else if (pcmk__str_any_of(option_name, "-e", "--create-empty", NULL)) {
options.cmd = shadow_cmd_create_empty;
} else if (pcmk__str_any_of(option_name, "-C", "--commit", NULL)) {
options.cmd = shadow_cmd_commit;
} else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) {
options.cmd = shadow_cmd_delete;
} else if (pcmk__str_any_of(option_name, "-E", "--edit", NULL)) {
options.cmd = shadow_cmd_edit;
} else if (pcmk__str_any_of(option_name, "-r", "--reset", NULL)) {
options.cmd = shadow_cmd_reset;
} else if (pcmk__str_any_of(option_name, "-s", "--switch", NULL)) {
options.cmd = shadow_cmd_switch;
} else {
// Should be impossible
return FALSE;
}
// optarg may be NULL and that's okay
pcmk__str_update(&options.instance, optarg);
return TRUE;
}
static GOptionEntry query_entries[] = {
{ "which", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Indicate the active shadow copy", NULL },
{ "display", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display the contents of the active shadow copy", NULL },
{ "diff", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display the changes in the active shadow copy", NULL },
{ "file", 'F', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display the location of the active shadow copy file", NULL },
{ NULL }
};
static GOptionEntry command_entries[] = {
{ "create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
"Create the named shadow copy of the active cluster configuration",
"name" },
{ "create-empty", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
command_cb,
"Create the named shadow copy with an empty cluster configuration.\n"
INDENT "Optional: --validate-with", "name" },
{ "commit", 'C', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
"Upload the contents of the named shadow copy to the cluster", "name" },
{ "delete", 'D', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
"Delete the contents of the named shadow copy", "name" },
{ "edit", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Edit the contents of the active shadow copy with your favorite $EDITOR",
NULL },
{ "reset", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
"Recreate named shadow copy from the active cluster configuration",
"name. Required: --force." },
{ "switch", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
"(Advanced) Switch to the named shadow copy", "name" },
{ NULL }
};
static GOptionEntry addl_entries[] = {
{ "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force,
"(Advanced) Force the action to be performed", NULL },
{ "batch", 'b', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.batch,
"(Advanced) Don't spawn a new shell", NULL },
{ "all", 'a', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.full_upload,
"(Advanced) Upload entire CIB, including status, with --commit", NULL },
{ "validate-with", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
&options.validate_with,
"(Advanced) Create an older configuration version", NULL },
{ NULL }
};
static GOptionContext *
build_arg_context(pcmk__common_args_t *args, GOptionGroup **group)
{
const char *desc = NULL;
GOptionContext *context = NULL;
desc = "Examples:\n\n"
"Create a blank shadow configuration:\n\n"
"\t# crm_shadow --create-empty myShadow\n\n"
"Create a shadow configuration from the running cluster\n\n"
"\t# crm_shadow --create myShadow\n\n"
"Display the current shadow configuration:\n\n"
"\t# crm_shadow --display\n\n"
"Discard the current shadow configuration (named myShadow):\n\n"
"\t# crm_shadow --delete myShadow --force\n\n"
"Upload current shadow configuration (named myShadow) to running "
"cluster:\n\n"
"\t# crm_shadow --commit myShadow\n\n";
context = pcmk__build_arg_context(args, "text (default), xml", group,
"<query>|<command>");
g_option_context_set_description(context, desc);
pcmk__add_arg_group(context, "queries", "Queries:",
"Show query help", query_entries);
pcmk__add_arg_group(context, "commands", "Commands:",
"Show command help", command_entries);
pcmk__add_arg_group(context, "additional", "Additional Options:",
"Show additional options", addl_entries);
return context;
}
int
main(int argc, char **argv)
{
int rc = pcmk_rc_ok;
pcmk__output_t *out = NULL;
GError *error = NULL;
GOptionGroup *output_group = NULL;
pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
gchar **processed_args = pcmk__cmdline_preproc(argv, "CDcersv");
GOptionContext *context = build_arg_context(args, &output_group);
crm_log_preinit(NULL, argc, argv);
pcmk__register_formats(output_group, formats);
if (!g_option_context_parse_strv(context, &processed_args, &error)) {
exit_code = CRM_EX_USAGE;
goto done;
}
rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
if (rc != pcmk_rc_ok) {
exit_code = CRM_EX_ERROR;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Error creating output format %s: %s", args->output_ty,
pcmk_rc_str(rc));
goto done;
}
if (g_strv_length(processed_args) > 1) {
gchar *help = g_option_context_get_help(context, TRUE, NULL);
GString *extra = g_string_sized_new(128);
for (int lpc = 1; processed_args[lpc] != NULL; lpc++) {
if (extra->len > 0) {
g_string_append_c(extra, ' ');
}
g_string_append(extra, processed_args[lpc]);
}
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"non-option ARGV-elements: %s\n\n%s", extra->str, help);
g_free(help);
g_string_free(extra, TRUE);
goto done;
}
if (args->version) {
out->version(out, false);
goto done;
}
pcmk__register_messages(out, fmt_functions);
if (options.cmd == shadow_cmd_none) {
// @COMPAT: Create a default command if other tools have one
gchar *help = g_option_context_get_help(context, TRUE, NULL);
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Must specify a query or command option\n\n%s", help);
g_free(help);
goto done;
}
pcmk__cli_init_logging("crm_shadow", args->verbosity);
if (args->verbosity > 0) {
cib__set_call_options(options.cmd_options, crm_system_name,
cib_verbose);
}
// Run the command
switch (options.cmd) {
case shadow_cmd_commit:
commit_shadow_file(&error);
break;
case shadow_cmd_create:
create_shadow_from_cib(out, false, &error);
break;
case shadow_cmd_create_empty:
create_shadow_empty(out, &error);
break;
case shadow_cmd_reset:
create_shadow_from_cib(out, true, &error);
break;
case shadow_cmd_delete:
delete_shadow_file(out, &error);
break;
case shadow_cmd_diff:
show_shadow_diff(out, &error);
break;
case shadow_cmd_display:
show_shadow_contents(out, &error);
break;
case shadow_cmd_edit:
edit_shadow_file(&error);
break;
case shadow_cmd_file:
show_shadow_filename(out, &error);
break;
case shadow_cmd_switch:
switch_shadow_instance(out, &error);
break;
case shadow_cmd_which:
show_shadow_instance(out, &error);
break;
default:
// Should never reach this point
break;
}
done:
g_strfreev(processed_args);
pcmk__free_arg_context(context);
pcmk__output_and_clear_error(&error, out);
free(options.instance);
g_free(options.validate_with);
if (out != NULL) {
out->finish(out, exit_code, true, NULL);
pcmk__output_free(out);
}
crm_exit(exit_code);
}

File Metadata

Mime Type
text/x-diff
Expires
Wed, Jun 25, 3:22 AM (21 h, 14 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1945562
Default Alt Text
(373 KB)

Event Timeline