diff --git a/cts/cli/regression.upgrade.exp b/cts/cli/regression.upgrade.exp
index 00ee754e75..dfb8775be0 100644
--- a/cts/cli/regression.upgrade.exp
+++ b/cts/cli/regression.upgrade.exp
@@ -1,145 +1,145 @@
Created new pacemaker configuration
A new shadow instance was created. To begin using it, enter the following into your shell:
export CIB_shadow=cts-cli
=#=#=#= Begin test: Set stonith-enabled=false =#=#=#=
=#=#=#= Current cib after: Set stonith-enabled=false =#=#=#=
=#=#=#= End test: Set stonith-enabled=false - OK (0) =#=#=#=
* Passed: crm_attribute - Set stonith-enabled=false
=#=#=#= Begin test: Configure the initial resource =#=#=#=
=#=#=#= Current cib after: Configure the initial resource =#=#=#=
=#=#=#= End test: Configure the initial resource - OK (0) =#=#=#=
* Passed: cibadmin - Configure the initial resource
=#=#=#= Begin test: Upgrade to latest CIB schema (trigger 2.10.xsl + the wrapping) =#=#=#=
pcmk__update_schema debug: Schema pacemaker-2.10 validates
-apply_upgrade debug: Upgrading schema from pacemaker-2.10 to pacemaker-3.0: applying pre-upgrade XSL transform upgrade-2.10-enter
-apply_upgrade debug: Upgrading schema from pacemaker-2.10 to pacemaker-3.0: applying upgrade XSL transform upgrade-2.10
+apply_upgrade debug: Upgrading schema from pacemaker-2.10 to pacemaker-3.0: applying XSL transform upgrade-2.10-0.xsl
+apply_upgrade debug: Upgrading schema from pacemaker-2.10 to pacemaker-3.0: applying XSL transform upgrade-2.10-1.xsl
INFO: Resources-operation instance_attributes: mySmartFuse-monitor-inputpower (rsc=mySmartFuse, meta=mySmartFuse-inputpower-instanceparams): dropping requires
INFO: Resources-operation instance_attributes: ... only start/promote operation taken into account
INFO: Resources-operation instance_attributes: mySmartFuse-monitor-outputpower (rsc=mySmartFuse, meta=mySmartFuse-outputpower-instanceparams): dropping requires
INFO: Resources-operation instance_attributes: ... only start/promote operation taken into account
-apply_upgrade debug: Upgrading schema from pacemaker-2.10 to pacemaker-3.0: applying post-upgrade XSL transform upgrade-2.10-leave
+apply_upgrade debug: Upgrading schema from pacemaker-2.10 to pacemaker-3.0: applying XSL transform upgrade-2.10-2.xsl
DEBUG: instance_attributes: original element pointed to with @id-ref (mySmartFuse-outputpower-instanceparams) disappeared during upgrade
apply_upgrade info: Schema upgrade from pacemaker-2.10 to pacemaker-3.0 succeeded
pcmk__update_schema debug: Schema pacemaker-3.0 validates
pcmk__update_schema debug: Schema pacemaker-3.1 validates
pcmk__update_schema debug: Schema pacemaker-3.2 validates
pcmk__update_schema debug: Schema pacemaker-3.3 validates
pcmk__update_schema debug: Schema pacemaker-3.4 validates
pcmk__update_schema debug: Schema pacemaker-3.5 validates
pcmk__update_schema debug: Schema pacemaker-3.6 validates
pcmk__update_schema debug: Schema pacemaker-3.7 validates
pcmk__update_schema debug: Schema pacemaker-3.8 validates
pcmk__update_schema debug: Schema pacemaker-3.9 validates
pcmk__update_schema debug: Schema pacemaker-3.10 validates
pcmk__update_schema info: Transformed the configuration schema to pacemaker-3.10
=#=#=#= Current cib after: Upgrade to latest CIB schema (trigger 2.10.xsl + the wrapping) =#=#=#=
=#=#=#= End test: Upgrade to latest CIB schema (trigger 2.10.xsl + the wrapping) - OK (0) =#=#=#=
* Passed: cibadmin - Upgrade to latest CIB schema (trigger 2.10.xsl + the wrapping)
=#=#=#= Begin test: Query a resource instance attribute (shall survive) =#=#=#=
outputpower
=#=#=#= Current cib after: Query a resource instance attribute (shall survive) =#=#=#=
=#=#=#= End test: Query a resource instance attribute (shall survive) - OK (0) =#=#=#=
* Passed: crm_resource - Query a resource instance attribute (shall survive)
diff --git a/lib/cib/cib_native.c b/lib/cib/cib_native.c
index 98f7866c36..4f6f755e4a 100644
--- a/lib/cib/cib_native.c
+++ b/lib/cib/cib_native.c
@@ -1,511 +1,507 @@
/*
* Copyright 2004 International Business Machines
* Later changes copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
-#ifndef _GNU_SOURCE
-# define _GNU_SOURCE
-#endif
-
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
typedef struct cib_native_opaque_s {
char *token;
crm_ipc_t *ipc;
void (*dnotify_fn) (gpointer user_data);
mainloop_io_t *source;
} cib_native_opaque_t;
static int
cib_native_perform_op_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)
{
int rc = pcmk_ok;
int reply_id = 0;
enum crm_ipc_flags ipc_flags = crm_ipc_flags_none;
xmlNode *op_msg = NULL;
xmlNode *op_reply = NULL;
cib_native_opaque_t *native = cib->variant_opaque;
if (cib->state == cib_disconnected) {
return -ENOTCONN;
}
if (output_data != NULL) {
*output_data = NULL;
}
if (op == NULL) {
crm_err("No operation specified");
return -EINVAL;
}
if (call_options & cib_sync_call) {
pcmk__set_ipc_flags(ipc_flags, "client", crm_ipc_client_response);
}
rc = cib__create_op(cib, op, host, section, data, call_options, user_name,
NULL, &op_msg);
if (rc != pcmk_ok) {
return rc;
}
if (pcmk_is_set(call_options, cib_transaction)) {
rc = cib__extend_transaction(cib, op_msg);
goto done;
}
crm_trace("Sending %s message to the CIB manager (timeout=%ds)", op, cib->call_timeout);
rc = crm_ipc_send(native->ipc, op_msg, ipc_flags, cib->call_timeout * 1000, &op_reply);
if (rc < 0) {
crm_err("Couldn't perform %s operation (timeout=%ds): %s (%d)", op,
cib->call_timeout, pcmk_strerror(rc), rc);
rc = -ECOMM;
goto done;
}
crm_log_xml_trace(op_reply, "Reply");
if (!(call_options & cib_sync_call)) {
crm_trace("Async call, returning %d", cib->call_id);
CRM_CHECK(cib->call_id != 0,
rc = -ENOMSG; goto done);
rc = cib->call_id;
goto done;
}
rc = pcmk_ok;
crm_element_value_int(op_reply, PCMK__XA_CIB_CALLID, &reply_id);
if (reply_id == cib->call_id) {
xmlNode *wrapper = pcmk__xe_first_child(op_reply, PCMK__XE_CIB_CALLDATA,
NULL, NULL);
xmlNode *tmp = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
crm_trace("Synchronous reply %d received", reply_id);
if (crm_element_value_int(op_reply, PCMK__XA_CIB_RC, &rc) != 0) {
rc = -EPROTO;
}
if (output_data == NULL || (call_options & cib_discard_reply)) {
crm_trace("Discarding reply");
} else {
*output_data = pcmk__xml_copy(NULL, tmp);
}
} else if (reply_id <= 0) {
crm_err("Received bad reply: No id set");
crm_log_xml_err(op_reply, "Bad reply");
rc = -ENOMSG;
goto done;
} else {
crm_err("Received bad reply: %d (wanted %d)", reply_id, cib->call_id);
crm_log_xml_err(op_reply, "Old reply");
rc = -ENOMSG;
goto done;
}
if (op_reply == NULL && cib->state == cib_disconnected) {
rc = -ENOTCONN;
} else if (rc == pcmk_ok && op_reply == NULL) {
rc = -ETIME;
}
switch (rc) {
case pcmk_ok:
case -EPERM:
break;
/* This is an internal value that clients do not and should not care about */
case -pcmk_err_diff_resync:
rc = pcmk_ok;
break;
/* These indicate internal problems */
case -EPROTO:
case -ENOMSG:
crm_err("Call failed: %s", pcmk_strerror(rc));
if (op_reply) {
crm_log_xml_err(op_reply, "Invalid reply");
}
break;
default:
if (!pcmk__str_eq(op, PCMK__CIB_REQUEST_QUERY, pcmk__str_none)) {
crm_warn("Call failed: %s", pcmk_strerror(rc));
}
}
done:
if (!crm_ipc_connected(native->ipc)) {
crm_err("The CIB manager disconnected");
cib->state = cib_disconnected;
}
pcmk__xml_free(op_msg);
pcmk__xml_free(op_reply);
return rc;
}
static int
cib_native_dispatch_internal(const char *buffer, ssize_t length,
gpointer userdata)
{
const char *type = NULL;
xmlNode *msg = NULL;
cib_t *cib = userdata;
crm_trace("dispatching %p", userdata);
if (cib == NULL) {
crm_err("No CIB!");
return 0;
}
msg = pcmk__xml_parse(buffer);
if (msg == NULL) {
crm_warn("Received a NULL message from the CIB manager");
return 0;
}
/* do callbacks */
type = crm_element_value(msg, PCMK__XA_T);
crm_trace("Activating %s callbacks...", type);
crm_log_xml_explicit(msg, "cib-reply");
if (pcmk__str_eq(type, PCMK__VALUE_CIB, pcmk__str_none)) {
cib_native_callback(cib, msg, 0, 0);
} else if (pcmk__str_eq(type, PCMK__VALUE_CIB_NOTIFY, pcmk__str_none)) {
g_list_foreach(cib->notify_list, cib_native_notify, msg);
} else {
crm_err("Unknown message type: %s", type);
}
pcmk__xml_free(msg);
return 0;
}
static void
cib_native_destroy(void *userdata)
{
cib_t *cib = userdata;
cib_native_opaque_t *native = cib->variant_opaque;
crm_trace("destroying %p", userdata);
cib->state = cib_disconnected;
native->source = NULL;
native->ipc = NULL;
if (native->dnotify_fn) {
native->dnotify_fn(userdata);
}
}
static int
cib_native_signoff(cib_t *cib)
{
cib_native_opaque_t *native = cib->variant_opaque;
crm_debug("Disconnecting from the CIB manager");
cib_free_notify(cib);
remove_cib_op_callback(0, TRUE);
if (native->source != NULL) {
/* Attached to mainloop */
mainloop_del_ipc_client(native->source);
native->source = NULL;
native->ipc = NULL;
} else if (native->ipc) {
/* Not attached to mainloop */
crm_ipc_t *ipc = native->ipc;
native->ipc = NULL;
crm_ipc_close(ipc);
crm_ipc_destroy(ipc);
}
cib->cmds->end_transaction(cib, false, cib_none);
cib->state = cib_disconnected;
cib->type = cib_no_connection;
return pcmk_ok;
}
static int
cib_native_signon_raw(cib_t *cib, const char *name, enum cib_conn_type type,
int *async_fd)
{
int rc = pcmk_ok;
const char *channel = NULL;
cib_native_opaque_t *native = cib->variant_opaque;
xmlNode *hello = NULL;
struct ipc_client_callbacks cib_callbacks = {
.dispatch = cib_native_dispatch_internal,
.destroy = cib_native_destroy
};
cib->call_timeout = PCMK__IPC_TIMEOUT;
if (type == cib_command) {
cib->state = cib_connected_command;
channel = PCMK__SERVER_BASED_RW;
} else if (type == cib_command_nonblocking) {
cib->state = cib_connected_command;
channel = PCMK__SERVER_BASED_SHM;
} else if (type == cib_query) {
cib->state = cib_connected_query;
channel = PCMK__SERVER_BASED_RO;
} else {
return -ENOTCONN;
}
crm_trace("Connecting %s channel", channel);
if (async_fd != NULL) {
native->ipc = crm_ipc_new(channel, 0);
if (native->ipc != NULL) {
rc = pcmk__connect_generic_ipc(native->ipc);
if (rc == pcmk_rc_ok) {
rc = pcmk__ipc_fd(native->ipc, async_fd);
if (rc != pcmk_rc_ok) {
crm_info("Couldn't get file descriptor for %s IPC",
channel);
}
}
rc = pcmk_rc2legacy(rc);
}
} else {
native->source =
mainloop_add_ipc_client(channel, G_PRIORITY_HIGH, 512 * 1024 /* 512k */ , cib,
&cib_callbacks);
native->ipc = mainloop_get_ipc_client(native->source);
}
if (rc != pcmk_ok || native->ipc == NULL || !crm_ipc_connected(native->ipc)) {
crm_info("Could not connect to CIB manager for %s", name);
rc = -ENOTCONN;
}
if (rc == pcmk_ok) {
rc = cib__create_op(cib, CRM_OP_REGISTER, NULL, NULL, NULL,
cib_sync_call, NULL, name, &hello);
}
if (rc == pcmk_ok) {
xmlNode *reply = NULL;
if (crm_ipc_send(native->ipc, hello, crm_ipc_client_response, -1,
&reply) > 0) {
const char *msg_type = crm_element_value(reply, PCMK__XA_CIB_OP);
crm_log_xml_trace(reply, "reg-reply");
if (!pcmk__str_eq(msg_type, CRM_OP_REGISTER, pcmk__str_casei)) {
crm_info("Reply to CIB registration message has unknown type "
"'%s'",
msg_type);
rc = -EPROTO;
} else {
native->token = crm_element_value_copy(reply,
PCMK__XA_CIB_CLIENTID);
if (native->token == NULL) {
rc = -EPROTO;
}
}
pcmk__xml_free(reply);
} else {
rc = -ECOMM;
}
pcmk__xml_free(hello);
}
if (rc == pcmk_ok) {
crm_info("Successfully connected to CIB manager for %s", name);
return pcmk_ok;
}
crm_info("Connection to CIB manager for %s failed: %s",
name, pcmk_strerror(rc));
cib_native_signoff(cib);
return rc;
}
static int
cib_native_signon(cib_t *cib, const char *name, enum cib_conn_type type)
{
return cib_native_signon_raw(cib, name, type, NULL);
}
static int
cib_native_free(cib_t *cib)
{
int rc = pcmk_ok;
if (cib->state != cib_disconnected) {
rc = cib_native_signoff(cib);
}
if (cib->state == cib_disconnected) {
cib_native_opaque_t *native = cib->variant_opaque;
free(native->token);
free(cib->variant_opaque);
free(cib->cmds);
free(cib->user);
free(cib);
}
return rc;
}
static int
cib_native_register_notification(cib_t *cib, const char *callback, int enabled)
{
int rc = pcmk_ok;
xmlNode *notify_msg = pcmk__xe_create(NULL, PCMK__XE_CIB_CALLBACK);
cib_native_opaque_t *native = cib->variant_opaque;
if (cib->state != cib_disconnected) {
crm_xml_add(notify_msg, PCMK__XA_CIB_OP, PCMK__VALUE_CIB_NOTIFY);
crm_xml_add(notify_msg, PCMK__XA_CIB_NOTIFY_TYPE, callback);
crm_xml_add_int(notify_msg, PCMK__XA_CIB_NOTIFY_ACTIVATE, enabled);
rc = crm_ipc_send(native->ipc, notify_msg, crm_ipc_client_response,
1000 * cib->call_timeout, NULL);
if (rc <= 0) {
crm_trace("Notification not registered: %d", rc);
rc = -ECOMM;
}
}
pcmk__xml_free(notify_msg);
return rc;
}
static int
cib_native_set_connection_dnotify(cib_t *cib,
void (*dnotify) (gpointer user_data))
{
cib_native_opaque_t *native = NULL;
if (cib == NULL) {
crm_err("No CIB!");
return FALSE;
}
native = cib->variant_opaque;
native->dnotify_fn = dnotify;
return pcmk_ok;
}
/*!
* \internal
* \brief Get the given CIB connection's unique client identifier
*
* These can be used to check whether this client requested the action that
* triggered a CIB notification.
*
* \param[in] cib CIB connection
* \param[out] async_id If not \p NULL, where to store asynchronous client ID
* \param[out] sync_id If not \p NULL, where to store synchronous client ID
*
* \return Legacy Pacemaker return code (specifically, \p pcmk_ok)
*
* \note This is the \p cib_native variant implementation of
* \p cib_api_operations_t:client_id().
* \note For \p cib_native objects, \p async_id and \p sync_id are the same.
* \note The client ID is assigned during CIB sign-on.
*/
static int
cib_native_client_id(const cib_t *cib, const char **async_id,
const char **sync_id)
{
cib_native_opaque_t *native = cib->variant_opaque;
if (async_id != NULL) {
*async_id = native->token;
}
if (sync_id != NULL) {
*sync_id = native->token;
}
return pcmk_ok;
}
cib_t *
cib_native_new(void)
{
cib_native_opaque_t *native = NULL;
cib_t *cib = cib_new_variant();
if (cib == NULL) {
return NULL;
}
native = calloc(1, sizeof(cib_native_opaque_t));
if (native == NULL) {
free(cib);
return NULL;
}
cib->variant = cib_native;
cib->variant_opaque = native;
native->ipc = NULL;
native->source = NULL;
native->dnotify_fn = NULL;
/* assign variant specific ops */
cib->delegate_fn = cib_native_perform_op_delegate;
cib->cmds->signon = cib_native_signon;
cib->cmds->signon_raw = cib_native_signon_raw;
cib->cmds->signoff = cib_native_signoff;
cib->cmds->free = cib_native_free;
cib->cmds->register_notification = cib_native_register_notification;
cib->cmds->set_connection_dnotify = cib_native_set_connection_dnotify;
cib->cmds->client_id = cib_native_client_id;
return cib;
}
diff --git a/lib/cluster/membership.c b/lib/cluster/membership.c
index cfc861367b..813941e6b4 100644
--- a/lib/cluster/membership.c
+++ b/lib/cluster/membership.c
@@ -1,1505 +1,1501 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
-#ifndef _GNU_SOURCE
-# define _GNU_SOURCE
-#endif
-
#include // PRIu32
#include // bool
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "crmcluster_private.h"
/* The peer cache remembers cluster nodes that have been seen. This is managed
* mostly automatically by libcrmcluster, based on cluster membership events.
*
* Because cluster nodes can have conflicting names or UUIDs, the hash table key
* is a uniquely generated ID.
*
* @TODO Move caches to pcmk_cluster_t
*/
GHashTable *pcmk__peer_cache = NULL;
/* The remote peer cache tracks pacemaker_remote nodes. While the
* value has the same type as the peer cache's, it is tracked separately for
* three reasons: pacemaker_remote nodes can't have conflicting names or UUIDs,
* so the name (which is also the UUID) is used as the hash table key; there
* is no equivalent of membership events, so management is not automatic; and
* most users of the peer cache need to exclude pacemaker_remote nodes.
*
* @TODO That said, using a single cache would be more logical and less
* error-prone, so it would be a good idea to merge them one day.
*
* libcrmcluster provides two avenues for populating the cache:
* pcmk__cluster_lookup_remote_node() and pcmk__cluster_forget_remote_node()
* directly manage it, while refresh_remote_nodes() populates it via the CIB.
*
* @TODO Move caches to pcmk_cluster_t
*/
GHashTable *pcmk__remote_peer_cache = NULL;
/*
* The CIB cluster node cache tracks cluster nodes that have been seen in
* the CIB. It is useful mainly when a caller needs to know about a node that
* may no longer be in the membership, but doesn't want to add the node to the
* main peer cache tables.
*/
static GHashTable *cluster_node_cib_cache = NULL;
static bool autoreap = true;
static bool has_quorum = false;
// Flag setting and clearing for pcmk__node_status_t:flags
#define set_peer_flags(peer, flags_to_set) do { \
(peer)->flags = pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE, \
"Peer", (peer)->name, \
(peer)->flags, (flags_to_set), \
#flags_to_set); \
} while (0)
#define clear_peer_flags(peer, flags_to_clear) do { \
(peer)->flags = pcmk__clear_flags_as(__func__, __LINE__, \
LOG_TRACE, \
"Peer", (peer)->name, \
(peer)->flags, (flags_to_clear), \
#flags_to_clear); \
} while (0)
static void update_peer_uname(pcmk__node_status_t *node, const char *uname);
static pcmk__node_status_t *find_cib_cluster_node(const char *id,
const char *uname);
/*!
* \internal
* \brief Check whether the cluster currently has quorum
*
* \return \c true if the cluster has quorum, or \c false otherwise
*/
bool
pcmk__cluster_has_quorum(void)
{
return has_quorum;
}
/*!
* \internal
* \brief Set whether the cluster currently has quorum
*
* \param[in] quorate \c true if the cluster has quorum, or \c false otherwise
*/
void
pcmk__cluster_set_quorum(bool quorate)
{
has_quorum = quorate;
}
/*!
* \internal
* \brief Get the number of Pacemaker Remote nodes that have been seen
*
* \return Number of cached Pacemaker Remote nodes
*/
unsigned int
pcmk__cluster_num_remote_nodes(void)
{
if (pcmk__remote_peer_cache == NULL) {
return 0U;
}
return g_hash_table_size(pcmk__remote_peer_cache);
}
/*!
* \internal
* \brief Get a remote node cache entry, creating it if necessary
*
* \param[in] node_name Name of remote node
*
* \return Cache entry for node on success, or \c NULL (and set \c errno)
* otherwise
*
* \note When creating a new entry, this will leave the node state undetermined.
* The caller should also call \c pcmk__update_peer_state() if the state
* is known.
* \note Because this can add and remove cache entries, callers should not
* assume any previously obtained cache entry pointers remain valid.
*/
pcmk__node_status_t *
pcmk__cluster_lookup_remote_node(const char *node_name)
{
pcmk__node_status_t *node = NULL;
char *node_name_copy = NULL;
if (node_name == NULL) {
errno = EINVAL;
return NULL;
}
/* It's theoretically possible that the node was added to the cluster peer
* cache before it was known to be a Pacemaker Remote node. Remove that
* entry unless it has a node ID, which means the name actually is
* associated with a cluster node. (@TODO return an error in that case?)
*/
node = pcmk__search_node_caches(0, node_name,
pcmk__node_search_cluster_member);
if ((node != NULL) && (node->xml_id == NULL)) {
/* node_name could be a pointer into the cache entry being removed, so
* reassign it to a copy before the original gets freed
*/
node_name_copy = strdup(node_name);
if (node_name_copy == NULL) {
errno = ENOMEM;
return NULL;
}
node_name = node_name_copy;
pcmk__cluster_forget_cluster_node(0, node_name);
}
/* Return existing cache entry if one exists */
node = g_hash_table_lookup(pcmk__remote_peer_cache, node_name);
if (node) {
free(node_name_copy);
return node;
}
/* Allocate a new entry */
node = calloc(1, sizeof(pcmk__node_status_t));
if (node == NULL) {
free(node_name_copy);
return NULL;
}
/* Populate the essential information */
set_peer_flags(node, pcmk__node_status_remote);
node->xml_id = strdup(node_name);
if (node->xml_id == NULL) {
free(node);
errno = ENOMEM;
free(node_name_copy);
return NULL;
}
/* Add the new entry to the cache */
g_hash_table_replace(pcmk__remote_peer_cache, node->xml_id, node);
crm_trace("added %s to remote cache", node_name);
/* Update the entry's uname, ensuring peer status callbacks are called */
update_peer_uname(node, node_name);
free(node_name_copy);
return node;
}
/*!
* \internal
* \brief Remove a node from the Pacemaker Remote node cache
*
* \param[in] node_name Name of node to remove from cache
*
* \note The caller must be careful not to use \p node_name after calling this
* function if it might be a pointer into the cache entry being removed.
*/
void
pcmk__cluster_forget_remote_node(const char *node_name)
{
/* Do a lookup first, because node_name could be a pointer within the entry
* being removed -- we can't log it *after* removing it.
*/
if (g_hash_table_lookup(pcmk__remote_peer_cache, node_name) != NULL) {
crm_trace("Removing %s from Pacemaker Remote node cache", node_name);
g_hash_table_remove(pcmk__remote_peer_cache, node_name);
}
}
/*!
* \internal
* \brief Return node status based on a CIB status entry
*
* \param[in] node_state XML of node state
*
* \return \c PCMK_VALUE_MEMBER if \c PCMK__XA_IN_CCM is true in
* \c PCMK__XE_NODE_STATE, or \c PCMK__VALUE_LOST otherwise
*/
static const char *
remote_state_from_cib(const xmlNode *node_state)
{
bool in_ccm = false;
if ((pcmk__xe_get_bool_attr(node_state, PCMK__XA_IN_CCM,
&in_ccm) == pcmk_rc_ok) && in_ccm) {
return PCMK_VALUE_MEMBER;
}
return PCMK__VALUE_LOST;
}
/* user data for looping through remote node xpath searches */
struct refresh_data {
const char *field; /* XML attribute to check for node name */
gboolean has_state; /* whether to update node state based on XML */
};
/*!
* \internal
* \brief Process one pacemaker_remote node xpath search result
*
* \param[in] result XML search result
* \param[in] user_data what to look for in the XML
*/
static void
remote_cache_refresh_helper(xmlNode *result, void *user_data)
{
const struct refresh_data *data = user_data;
const char *remote = crm_element_value(result, data->field);
const char *state = NULL;
pcmk__node_status_t *node;
CRM_CHECK(remote != NULL, return);
/* Determine node's state, if the result has it */
if (data->has_state) {
state = remote_state_from_cib(result);
}
/* Check whether cache already has entry for node */
node = g_hash_table_lookup(pcmk__remote_peer_cache, remote);
if (node == NULL) {
/* Node is not in cache, so add a new entry for it */
node = pcmk__cluster_lookup_remote_node(remote);
CRM_ASSERT(node);
if (state) {
pcmk__update_peer_state(__func__, node, state, 0);
}
} else if (pcmk_is_set(node->flags, pcmk__node_status_dirty)) {
/* Node is in cache and hasn't been updated already, so mark it clean */
clear_peer_flags(node, pcmk__node_status_dirty);
if (state) {
pcmk__update_peer_state(__func__, node, state, 0);
}
}
}
static void
mark_dirty(gpointer key, gpointer value, gpointer user_data)
{
set_peer_flags((pcmk__node_status_t *) value, pcmk__node_status_dirty);
}
static gboolean
is_dirty(gpointer key, gpointer value, gpointer user_data)
{
const pcmk__node_status_t *node = value;
return pcmk_is_set(node->flags, pcmk__node_status_dirty);
}
/*!
* \internal
* \brief Repopulate the remote node cache based on CIB XML
*
* \param[in] cib CIB XML to parse
*/
static void
refresh_remote_nodes(xmlNode *cib)
{
struct refresh_data data;
pcmk__cluster_init_node_caches();
/* First, we mark all existing cache entries as dirty,
* so that later we can remove any that weren't in the CIB.
* We don't empty the cache, because we need to detect changes in state.
*/
g_hash_table_foreach(pcmk__remote_peer_cache, mark_dirty, NULL);
/* Look for guest nodes and remote nodes in the status section */
data.field = PCMK_XA_ID;
data.has_state = TRUE;
crm_foreach_xpath_result(cib, PCMK__XP_REMOTE_NODE_STATUS,
remote_cache_refresh_helper, &data);
/* Look for guest nodes and remote nodes in the configuration section,
* because they may have just been added and not have a status entry yet.
* In that case, the cached node state will be left NULL, so that the
* peer status callback isn't called until we're sure the node started
* successfully.
*/
data.field = PCMK_XA_VALUE;
data.has_state = FALSE;
crm_foreach_xpath_result(cib, PCMK__XP_GUEST_NODE_CONFIG,
remote_cache_refresh_helper, &data);
data.field = PCMK_XA_ID;
data.has_state = FALSE;
crm_foreach_xpath_result(cib, PCMK__XP_REMOTE_NODE_CONFIG,
remote_cache_refresh_helper, &data);
/* Remove all old cache entries that weren't seen in the CIB */
g_hash_table_foreach_remove(pcmk__remote_peer_cache, is_dirty, NULL);
}
/*!
* \internal
* \brief Check whether a node is an active cluster node
*
* Remote nodes are never considered active. This guarantees that they can never
* become DC.
*
* \param[in] node Node to check
*
* \return \c true if the node is an active cluster node, or \c false otherwise
*/
bool
pcmk__cluster_is_node_active(const pcmk__node_status_t *node)
{
const enum pcmk_cluster_layer cluster_layer = pcmk_get_cluster_layer();
if ((node == NULL) || pcmk_is_set(node->flags, pcmk__node_status_remote)) {
return false;
}
switch (cluster_layer) {
case pcmk_cluster_layer_corosync:
#if SUPPORT_COROSYNC
return pcmk__corosync_is_peer_active(node);
#else
break;
#endif // SUPPORT_COROSYNC
default:
break;
}
crm_err("Unhandled cluster layer: %s",
pcmk_cluster_layer_text(cluster_layer));
return false;
}
/*!
* \internal
* \brief Check if a node's entry should be removed from the cluster node cache
*
* A node should be removed from the cache if it's inactive and matches another
* \c pcmk__node_status_t (the search object). The node is considered a
* mismatch if any of the following are true:
* * The search object is \c NULL.
* * The search object has an ID set and the cached node's ID does not match it.
* * The search object does not have an ID set, and the cached node's name does
* not match the search node's name. (If both names are \c NULL, it's a
* match.)
*
* Otherwise, the node is considered a match.
*
* Note that if the search object has both an ID and a name set, the name is
* ignored for matching purposes.
*
* \param[in] key Ignored
* \param[in] value \c pcmk__node_status_t object from cluster node cache
* \param[in] user_data \c pcmk__node_status_t object to match against (search
* object)
*
* \return \c TRUE if the node entry should be removed from \c pcmk__peer_cache,
* or \c FALSE otherwise
*/
static gboolean
should_forget_cluster_node(gpointer key, gpointer value, gpointer user_data)
{
pcmk__node_status_t *node = value;
pcmk__node_status_t *search = user_data;
if (search == NULL) {
return FALSE;
}
if ((search->cluster_layer_id != 0)
&& (node->cluster_layer_id != search->cluster_layer_id)) {
return FALSE;
}
if ((search->cluster_layer_id == 0)
&& !pcmk__str_eq(node->name, search->name, pcmk__str_casei)) {
// @TODO Consider name even if ID is set?
return FALSE;
}
if (pcmk__cluster_is_node_active(value)) {
return FALSE;
}
crm_info("Removing node with name %s and cluster layer ID " PRIu32
" from membership cache",
pcmk__s(node->name, "(unknown)"), node->cluster_layer_id);
return TRUE;
}
/*!
* \internal
* \brief Remove one or more inactive nodes from the cluster node cache
*
* All inactive nodes matching \p id and \p node_name as described in
* \c should_forget_cluster_node documentation are removed from the cache.
*
* If \p id is 0 and \p node_name is \c NULL, all inactive nodes are removed
* from the cache regardless of ID and name. This differs from clearing the
* cache, in that entries for active nodes are preserved.
*
* \param[in] id ID of node to remove from cache (0 to ignore)
* \param[in] node_name Name of node to remove from cache (ignored if \p id is
* nonzero)
*
* \note \p node_name is not modified directly, but it will be freed if it's a
* pointer into a cache entry that is removed.
*/
void
pcmk__cluster_forget_cluster_node(uint32_t id, const char *node_name)
{
pcmk__node_status_t search = { 0, };
char *criterion = NULL; // For logging
guint matches = 0;
if (pcmk__peer_cache == NULL) {
crm_trace("Membership cache not initialized, ignoring removal request");
return;
}
search.cluster_layer_id = id;
search.name = pcmk__str_copy(node_name); // May log after original freed
if (id > 0) {
criterion = crm_strdup_printf("cluster layer ID %" PRIu32, id);
} else if (node_name != NULL) {
criterion = crm_strdup_printf("name %s", node_name);
}
matches = g_hash_table_foreach_remove(pcmk__peer_cache,
should_forget_cluster_node, &search);
if (matches > 0) {
if (criterion != NULL) {
crm_notice("Removed %u inactive node%s with %s from the membership "
"cache",
matches, pcmk__plural_s(matches), criterion);
} else {
crm_notice("Removed all (%u) inactive cluster nodes from the "
"membership cache",
matches);
}
} else {
crm_info("No inactive cluster nodes%s%s to remove from the membership "
"cache",
((criterion != NULL)? " with " : ""), pcmk__s(criterion, ""));
}
free(search.name);
free(criterion);
}
static void
count_peer(gpointer key, gpointer value, gpointer user_data)
{
unsigned int *count = user_data;
pcmk__node_status_t *node = value;
if (pcmk__cluster_is_node_active(node)) {
*count = *count + 1;
}
}
/*!
* \internal
* \brief Get the number of active cluster nodes that have been seen
*
* Remote nodes are never considered active. This guarantees that they can never
* become DC.
*
* \return Number of active nodes in the cluster node cache
*/
unsigned int
pcmk__cluster_num_active_nodes(void)
{
unsigned int count = 0;
if (pcmk__peer_cache != NULL) {
g_hash_table_foreach(pcmk__peer_cache, count_peer, &count);
}
return count;
}
static void
destroy_crm_node(gpointer data)
{
pcmk__node_status_t *node = data;
crm_trace("Destroying entry for node %" PRIu32 ": %s",
node->cluster_layer_id, node->name);
free(node->name);
free(node->state);
free(node->xml_id);
free(node->user_data);
free(node->expected);
free(node->conn_host);
free(node);
}
/*!
* \internal
* \brief Initialize node caches
*/
void
pcmk__cluster_init_node_caches(void)
{
if (pcmk__peer_cache == NULL) {
pcmk__peer_cache = pcmk__strikey_table(free, destroy_crm_node);
}
if (pcmk__remote_peer_cache == NULL) {
pcmk__remote_peer_cache = pcmk__strikey_table(NULL, destroy_crm_node);
}
if (cluster_node_cib_cache == NULL) {
cluster_node_cib_cache = pcmk__strikey_table(free, destroy_crm_node);
}
}
/*!
* \internal
* \brief Initialize node caches
*/
void
pcmk__cluster_destroy_node_caches(void)
{
if (pcmk__peer_cache != NULL) {
crm_trace("Destroying peer cache with %d members",
g_hash_table_size(pcmk__peer_cache));
g_hash_table_destroy(pcmk__peer_cache);
pcmk__peer_cache = NULL;
}
if (pcmk__remote_peer_cache != NULL) {
crm_trace("Destroying remote peer cache with %d members",
pcmk__cluster_num_remote_nodes());
g_hash_table_destroy(pcmk__remote_peer_cache);
pcmk__remote_peer_cache = NULL;
}
if (cluster_node_cib_cache != NULL) {
crm_trace("Destroying configured cluster node cache with %d members",
g_hash_table_size(cluster_node_cib_cache));
g_hash_table_destroy(cluster_node_cib_cache);
cluster_node_cib_cache = NULL;
}
}
static void (*peer_status_callback)(enum pcmk__node_update,
pcmk__node_status_t *,
const void *) = NULL;
/*!
* \internal
* \brief Set a client function that will be called after peer status changes
*
* \param[in] dispatch Pointer to function to use as callback
*
* \note Client callbacks should do only client-specific handling. Callbacks
* must not add or remove entries in the peer caches.
*/
void
pcmk__cluster_set_status_callback(void (*dispatch)(enum pcmk__node_update,
pcmk__node_status_t *,
const void *))
{
// @TODO Improve documentation of peer_status_callback
peer_status_callback = dispatch;
}
/*!
* \internal
* \brief Tell the library whether to automatically reap lost nodes
*
* If \c true (the default), calling \c crm_update_peer_proc() will also update
* the peer state to \c PCMK_VALUE_MEMBER or \c PCMK__VALUE_LOST, and updating
* the peer state will reap peers whose state changes to anything other than
* \c PCMK_VALUE_MEMBER.
*
* Callers should leave this enabled unless they plan to manage the cache
* separately on their own.
*
* \param[in] enable \c true to enable automatic reaping, \c false to disable
*/
void
pcmk__cluster_set_autoreap(bool enable)
{
autoreap = enable;
}
static void
dump_peer_hash(int level, const char *caller)
{
GHashTableIter iter;
const char *id = NULL;
pcmk__node_status_t *node = NULL;
g_hash_table_iter_init(&iter, pcmk__peer_cache);
while (g_hash_table_iter_next(&iter, (gpointer *) &id, (gpointer *) &node)) {
do_crm_log(level, "%s: Node %" PRIu32 "/%s = %p - %s",
caller, node->cluster_layer_id, node->name, node, id);
}
}
static gboolean
hash_find_by_data(gpointer key, gpointer value, gpointer user_data)
{
return value == user_data;
}
/*!
* \internal
* \brief Search cluster member node cache
*
* \param[in] id If not 0, cluster node ID to search for
* \param[in] uname If not NULL, node name to search for
* \param[in] uuid If not NULL while id is 0, node UUID instead of cluster
* node ID to search for
*
* \return Cluster node cache entry if found, otherwise NULL
*/
static pcmk__node_status_t *
search_cluster_member_cache(unsigned int id, const char *uname,
const char *uuid)
{
GHashTableIter iter;
pcmk__node_status_t *node = NULL;
pcmk__node_status_t *by_id = NULL;
pcmk__node_status_t *by_name = NULL;
CRM_ASSERT(id > 0 || uname != NULL);
pcmk__cluster_init_node_caches();
if (uname != NULL) {
g_hash_table_iter_init(&iter, pcmk__peer_cache);
while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) {
if (pcmk__str_eq(node->name, uname, pcmk__str_casei)) {
crm_trace("Name match: %s", node->name);
by_name = node;
break;
}
}
}
if (id > 0) {
g_hash_table_iter_init(&iter, pcmk__peer_cache);
while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) {
if (node->cluster_layer_id == id) {
crm_trace("ID match: %" PRIu32, node->cluster_layer_id);
by_id = node;
break;
}
}
} else if (uuid != NULL) {
g_hash_table_iter_init(&iter, pcmk__peer_cache);
while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) {
if (pcmk__str_eq(node->xml_id, uuid, pcmk__str_casei)) {
crm_trace("UUID match: %s", node->xml_id);
by_id = node;
break;
}
}
}
node = by_id; /* Good default */
if(by_id == by_name) {
/* Nothing to do if they match (both NULL counts) */
crm_trace("Consistent: %p for %u/%s", by_id, id, uname);
} else if(by_id == NULL && by_name) {
crm_trace("Only one: %p for %u/%s", by_name, id, uname);
if (id && by_name->cluster_layer_id) {
dump_peer_hash(LOG_WARNING, __func__);
crm_crit("Nodes %u and %" PRIu32 " share the same name '%s'",
id, by_name->cluster_layer_id, uname);
node = NULL; /* Create a new one */
} else {
node = by_name;
}
} else if(by_name == NULL && by_id) {
crm_trace("Only one: %p for %u/%s", by_id, id, uname);
if ((uname != NULL) && (by_id->name != NULL)) {
dump_peer_hash(LOG_WARNING, __func__);
crm_crit("Nodes '%s' and '%s' share the same cluster nodeid %u: "
"assuming '%s' is correct",
uname, by_id->name, id, uname);
}
} else if ((uname != NULL) && (by_id->name != NULL)) {
if (pcmk__str_eq(uname, by_id->name, pcmk__str_casei)) {
crm_notice("Node '%s' has changed its cluster layer ID "
"from %" PRIu32 " to %" PRIu32,
by_id->name, by_name->cluster_layer_id,
by_id->cluster_layer_id);
g_hash_table_foreach_remove(pcmk__peer_cache, hash_find_by_data,
by_name);
} else {
crm_warn("Nodes '%s' and '%s' share the same cluster nodeid: %u %s",
by_id->name, by_name->name, id, uname);
dump_peer_hash(LOG_INFO, __func__);
crm_abort(__FILE__, __func__, __LINE__, "member weirdness", TRUE,
TRUE);
}
} else if ((id > 0) && (by_name->cluster_layer_id > 0)) {
crm_warn("Nodes %" PRIu32 " and %" PRIu32 " share the same name: '%s'",
by_id->cluster_layer_id, by_name->cluster_layer_id, uname);
} else {
/* Simple merge */
/* Only corosync-based clusters use node IDs. The functions that call
* pcmk__update_peer_state() and crm_update_peer_proc() only know
* nodeid, so 'by_id' is authoritative when merging.
*/
dump_peer_hash(LOG_DEBUG, __func__);
crm_info("Merging %p into %p", by_name, by_id);
g_hash_table_foreach_remove(pcmk__peer_cache, hash_find_by_data,
by_name);
}
return node;
}
/*!
* \internal
* \brief Search caches for a node (cluster or Pacemaker Remote)
*
* \param[in] id If not 0, cluster node ID to search for
* \param[in] uname If not NULL, node name to search for
* \param[in] flags Group of enum pcmk__node_search_flags
*
* \return Node cache entry if found, otherwise NULL
*/
pcmk__node_status_t *
pcmk__search_node_caches(unsigned int id, const char *uname, uint32_t flags)
{
pcmk__node_status_t *node = NULL;
CRM_ASSERT(id > 0 || uname != NULL);
pcmk__cluster_init_node_caches();
if ((uname != NULL) && pcmk_is_set(flags, pcmk__node_search_remote)) {
node = g_hash_table_lookup(pcmk__remote_peer_cache, uname);
}
if ((node == NULL)
&& pcmk_is_set(flags, pcmk__node_search_cluster_member)) {
node = search_cluster_member_cache(id, uname, NULL);
}
if ((node == NULL) && pcmk_is_set(flags, pcmk__node_search_cluster_cib)) {
char *id_str = (id == 0)? NULL : crm_strdup_printf("%u", id);
node = find_cib_cluster_node(id_str, uname);
free(id_str);
}
return node;
}
/*!
* \internal
* \brief Purge a node from cache (both cluster and Pacemaker Remote)
*
* \param[in] node_name If not NULL, purge only nodes with this name
* \param[in] node_id If not 0, purge cluster nodes only if they have this ID
*
* \note If \p node_name is NULL and \p node_id is 0, no nodes will be purged.
* If \p node_name is not NULL and \p node_id is not 0, Pacemaker Remote
* nodes that match \p node_name will be purged, and cluster nodes that
* match both \p node_name and \p node_id will be purged.
* \note The caller must be careful not to use \p node_name after calling this
* function if it might be a pointer into a cache entry being removed.
*/
void
pcmk__purge_node_from_cache(const char *node_name, uint32_t node_id)
{
char *node_name_copy = NULL;
if ((node_name == NULL) && (node_id == 0U)) {
return;
}
// Purge from Pacemaker Remote node cache
if ((node_name != NULL)
&& (g_hash_table_lookup(pcmk__remote_peer_cache, node_name) != NULL)) {
/* node_name could be a pointer into the cache entry being purged,
* so reassign it to a copy before the original gets freed
*/
node_name_copy = pcmk__str_copy(node_name);
node_name = node_name_copy;
crm_trace("Purging %s from Pacemaker Remote node cache", node_name);
g_hash_table_remove(pcmk__remote_peer_cache, node_name);
}
pcmk__cluster_forget_cluster_node(node_id, node_name);
free(node_name_copy);
}
#if SUPPORT_COROSYNC
static guint
remove_conflicting_peer(pcmk__node_status_t *node)
{
int matches = 0;
GHashTableIter iter;
pcmk__node_status_t *existing_node = NULL;
if ((node->cluster_layer_id == 0) || (node->name == NULL)) {
return 0;
}
if (!pcmk__corosync_has_nodelist()) {
return 0;
}
g_hash_table_iter_init(&iter, pcmk__peer_cache);
while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &existing_node)) {
if ((existing_node->cluster_layer_id > 0)
&& (existing_node->cluster_layer_id != node->cluster_layer_id)
&& pcmk__str_eq(existing_node->name, node->name, pcmk__str_casei)) {
if (pcmk__cluster_is_node_active(existing_node)) {
continue;
}
crm_warn("Removing cached offline node %" PRIu32 "/%s which has "
"conflicting name with %" PRIu32,
existing_node->cluster_layer_id, existing_node->name,
node->cluster_layer_id);
g_hash_table_iter_remove(&iter);
matches++;
}
}
return matches;
}
#endif
/*!
* \internal
* \brief Get a cluster node cache entry, possibly creating one if not found
*
* If \c pcmk__node_search_cluster_member is set in \p flags, the return value
* is guaranteed not to be \c NULL. A new cache entry is created if one does not
* already exist.
*
* \param[in] id If not 0, cluster node ID to search for
* \param[in] uname If not NULL, node name to search for
* \param[in] uuid If not NULL while id is 0, node UUID instead of cluster
* node ID to search for
* \param[in] flags Group of enum pcmk__node_search_flags
*
* \return (Possibly newly created) cluster node cache entry
*/
/* coverity[-alloc] Memory is referenced in one or both hashtables */
pcmk__node_status_t *
pcmk__get_node(unsigned int id, const char *uname, const char *uuid,
uint32_t flags)
{
pcmk__node_status_t *node = NULL;
char *uname_lookup = NULL;
CRM_ASSERT(id > 0 || uname != NULL);
pcmk__cluster_init_node_caches();
// Check the Pacemaker Remote node cache first
if (pcmk_is_set(flags, pcmk__node_search_remote)) {
node = g_hash_table_lookup(pcmk__remote_peer_cache, uname);
if (node != NULL) {
return node;
}
}
if (!pcmk_is_set(flags, pcmk__node_search_cluster_member)) {
return NULL;
}
node = search_cluster_member_cache(id, uname, uuid);
/* if uname wasn't provided, and find_peer did not turn up a uname based on id.
* we need to do a lookup of the node name using the id in the cluster membership. */
if ((uname == NULL) && ((node == NULL) || (node->name == NULL))) {
uname_lookup = pcmk__cluster_node_name(id);
}
if (uname_lookup) {
uname = uname_lookup;
crm_trace("Inferred a name of '%s' for node %u", uname, id);
/* try to turn up the node one more time now that we know the uname. */
if (node == NULL) {
node = search_cluster_member_cache(id, uname, uuid);
}
}
if (node == NULL) {
char *uniqueid = crm_generate_uuid();
node = pcmk__assert_alloc(1, sizeof(pcmk__node_status_t));
crm_info("Created entry %s/%p for node %s/%u (%d total)",
uniqueid, node, uname, id,
1 + g_hash_table_size(pcmk__peer_cache));
g_hash_table_replace(pcmk__peer_cache, uniqueid, node);
}
if ((id > 0) && (uname != NULL)
&& ((node->cluster_layer_id == 0) || (node->name == NULL))) {
crm_info("Node %u is now known as %s", id, uname);
}
if ((id > 0) && (node->cluster_layer_id == 0)) {
node->cluster_layer_id = id;
}
if ((uname != NULL) && (node->name == NULL)) {
update_peer_uname(node, uname);
}
if (node->xml_id == NULL) {
if (uuid == NULL) {
uuid = pcmk__cluster_node_uuid(node);
}
if (uuid) {
crm_info("Node %u has uuid %s", id, uuid);
} else {
crm_info("Cannot obtain a UUID for node %u/%s", id, node->name);
}
}
free(uname_lookup);
return node;
}
/*!
* \internal
* \brief Update a node's uname
*
* \param[in,out] node Node object to update
* \param[in] uname New name to set
*
* \note This function should not be called within a peer cache iteration,
* because in some cases it can remove conflicting cache entries,
* which would invalidate the iterator.
*/
static void
update_peer_uname(pcmk__node_status_t *node, const char *uname)
{
CRM_CHECK(uname != NULL,
crm_err("Bug: can't update node name without name"); return);
CRM_CHECK(node != NULL,
crm_err("Bug: can't update node name to %s without node", uname);
return);
if (pcmk__str_eq(uname, node->name, pcmk__str_casei)) {
crm_debug("Node name '%s' did not change", uname);
return;
}
for (const char *c = uname; *c; ++c) {
if ((*c >= 'A') && (*c <= 'Z')) {
crm_warn("Node names with capitals are discouraged, consider changing '%s'",
uname);
break;
}
}
pcmk__str_update(&node->name, uname);
if (peer_status_callback != NULL) {
peer_status_callback(pcmk__node_update_name, node, NULL);
}
#if SUPPORT_COROSYNC
if ((pcmk_get_cluster_layer() == pcmk_cluster_layer_corosync)
&& !pcmk_is_set(node->flags, pcmk__node_status_remote)) {
remove_conflicting_peer(node);
}
#endif
}
/*!
* \internal
* \brief Get log-friendly string equivalent of a process flag
*
* \param[in] proc Process flag
*
* \return Log-friendly string equivalent of \p proc
*/
static inline const char *
proc2text(enum crm_proc_flag proc)
{
const char *text = "unknown";
switch (proc) {
case crm_proc_none:
text = "none";
break;
case crm_proc_cpg:
text = "corosync-cpg";
break;
}
return text;
}
/*!
* \internal
* \brief Update a node's process information (and potentially state)
*
* \param[in] source Caller's function name (for log messages)
* \param[in,out] node Node object to update
* \param[in] flag Bitmask of new process information
* \param[in] status node status (online, offline, etc.)
*
* \return NULL if any node was reaped from peer caches, value of node otherwise
*
* \note If this function returns NULL, the supplied node object was likely
* freed and should not be used again. This function should not be
* called within a cache iteration if reaping is possible, otherwise
* reaping could invalidate the iterator.
*/
pcmk__node_status_t *
crm_update_peer_proc(const char *source, pcmk__node_status_t *node,
uint32_t flag, const char *status)
{
uint32_t last = 0;
gboolean changed = FALSE;
CRM_CHECK(node != NULL, crm_err("%s: Could not set %s to %s for NULL",
source, proc2text(flag), status);
return NULL);
/* Pacemaker doesn't spawn processes on remote nodes */
if (pcmk_is_set(node->flags, pcmk__node_status_remote)) {
return node;
}
last = node->processes;
if (status == NULL) {
node->processes = flag;
if (node->processes != last) {
changed = TRUE;
}
} else if (pcmk__str_eq(status, PCMK_VALUE_ONLINE, pcmk__str_casei)) {
if ((node->processes & flag) != flag) {
node->processes = pcmk__set_flags_as(__func__, __LINE__,
LOG_TRACE, "Peer process",
node->name, node->processes,
flag, "processes");
changed = TRUE;
}
} else if (node->processes & flag) {
node->processes = pcmk__clear_flags_as(__func__, __LINE__,
LOG_TRACE, "Peer process",
node->name, node->processes,
flag, "processes");
changed = TRUE;
}
if (changed) {
if (status == NULL && flag <= crm_proc_none) {
crm_info("%s: Node %s[%" PRIu32 "] - all processes are now offline",
source, node->name, node->cluster_layer_id);
} else {
crm_info("%s: Node %s[%" PRIu32 "] - %s is now %s",
source, node->name, node->cluster_layer_id,
proc2text(flag), status);
}
if (pcmk_is_set(node->processes, crm_get_cluster_proc())) {
node->when_online = time(NULL);
} else {
node->when_online = 0;
}
/* Call the client callback first, then update the peer state,
* in case the node will be reaped
*/
if (peer_status_callback != NULL) {
peer_status_callback(pcmk__node_update_processes, node, &last);
}
/* The client callback shouldn't touch the peer caches,
* but as a safety net, bail if the peer cache was destroyed.
*/
if (pcmk__peer_cache == NULL) {
return NULL;
}
if (autoreap) {
const char *peer_state = NULL;
if (pcmk_is_set(node->processes, crm_get_cluster_proc())) {
peer_state = PCMK_VALUE_MEMBER;
} else {
peer_state = PCMK__VALUE_LOST;
}
node = pcmk__update_peer_state(__func__, node, peer_state, 0);
}
} else {
crm_trace("%s: Node %s[%" PRIu32 "] - %s is unchanged (%s)",
source, node->name, node->cluster_layer_id, proc2text(flag),
status);
}
return node;
}
/*!
* \internal
* \brief Update a cluster node cache entry's expected join state
*
* \param[in] source Caller's function name (for logging)
* \param[in,out] node Node to update
* \param[in] expected Node's new join state
*/
void
pcmk__update_peer_expected(const char *source, pcmk__node_status_t *node,
const char *expected)
{
char *last = NULL;
gboolean changed = FALSE;
CRM_CHECK(node != NULL, crm_err("%s: Could not set 'expected' to %s", source, expected);
return);
/* Remote nodes don't participate in joins */
if (pcmk_is_set(node->flags, pcmk__node_status_remote)) {
return;
}
last = node->expected;
if (expected != NULL && !pcmk__str_eq(node->expected, expected, pcmk__str_casei)) {
node->expected = strdup(expected);
changed = TRUE;
}
if (changed) {
crm_info("%s: Node %s[%" PRIu32 "] - expected state is now %s (was %s)",
source, node->name, node->cluster_layer_id, expected, last);
free(last);
} else {
crm_trace("%s: Node %s[%" PRIu32 "] - expected state is unchanged (%s)",
source, node->name, node->cluster_layer_id, expected);
}
}
/*!
* \internal
* \brief Update a node's state and membership information
*
* \param[in] source Caller's function name (for log messages)
* \param[in,out] node Node object to update
* \param[in] state Node's new state
* \param[in] membership Node's new membership ID
* \param[in,out] iter If not NULL, pointer to node's peer cache iterator
*
* \return NULL if any node was reaped, value of node otherwise
*
* \note If this function returns NULL, the supplied node object was likely
* freed and should not be used again. This function may be called from
* within a peer cache iteration if the iterator is supplied.
*/
static pcmk__node_status_t *
update_peer_state_iter(const char *source, pcmk__node_status_t *node,
const char *state, uint64_t membership,
GHashTableIter *iter)
{
gboolean is_member;
CRM_CHECK(node != NULL,
crm_err("Could not set state for unknown host to %s"
QB_XS " source=%s", state, source);
return NULL);
is_member = pcmk__str_eq(state, PCMK_VALUE_MEMBER, pcmk__str_none);
if (is_member) {
node->when_lost = 0;
if (membership) {
node->membership_id = membership;
}
}
if (state && !pcmk__str_eq(node->state, state, pcmk__str_casei)) {
char *last = node->state;
if (is_member) {
node->when_member = time(NULL);
} else {
node->when_member = 0;
}
node->state = strdup(state);
crm_notice("Node %s state is now %s " QB_XS
" nodeid=%" PRIu32 " previous=%s source=%s",
node->name, state, node->cluster_layer_id,
pcmk__s(last, "unknown"), source);
if (peer_status_callback != NULL) {
peer_status_callback(pcmk__node_update_state, node, last);
}
free(last);
if (autoreap && !is_member
&& !pcmk_is_set(node->flags, pcmk__node_status_remote)) {
/* We only autoreap from the peer cache, not the remote peer cache,
* because the latter should be managed only by
* refresh_remote_nodes().
*/
if(iter) {
crm_notice("Purged 1 peer with cluster layer ID=" PRIu32
"and/or name=%s from the membership cache",
node->cluster_layer_id, node->name);
g_hash_table_iter_remove(iter);
} else {
pcmk__cluster_forget_cluster_node(node->cluster_layer_id,
node->name);
}
node = NULL;
}
} else {
crm_trace("Node %s state is unchanged (%s) " QB_XS
" nodeid=%" PRIu32 " source=%s",
node->name, state, node->cluster_layer_id, source);
}
return node;
}
/*!
* \brief Update a node's state and membership information
*
* \param[in] source Caller's function name (for log messages)
* \param[in,out] node Node object to update
* \param[in] state Node's new state
* \param[in] membership Node's new membership ID
*
* \return NULL if any node was reaped, value of node otherwise
*
* \note If this function returns NULL, the supplied node object was likely
* freed and should not be used again. This function should not be
* called within a cache iteration if reaping is possible,
* otherwise reaping could invalidate the iterator.
*/
pcmk__node_status_t *
pcmk__update_peer_state(const char *source, pcmk__node_status_t *node,
const char *state, uint64_t membership)
{
return update_peer_state_iter(source, node, state, membership, NULL);
}
/*!
* \internal
* \brief Reap all nodes from cache whose membership information does not match
*
* \param[in] membership Membership ID of nodes to keep
*/
void
pcmk__reap_unseen_nodes(uint64_t membership)
{
GHashTableIter iter;
pcmk__node_status_t *node = NULL;
crm_trace("Reaping unseen nodes...");
g_hash_table_iter_init(&iter, pcmk__peer_cache);
while (g_hash_table_iter_next(&iter, NULL, (gpointer *)&node)) {
if (node->membership_id != membership) {
if (node->state) {
/* Calling update_peer_state_iter() allows us to remove the node
* from pcmk__peer_cache without invalidating our iterator
*/
update_peer_state_iter(__func__, node, PCMK__VALUE_LOST,
membership, &iter);
} else {
crm_info("State of node %s[%" PRIu32 "] is still unknown",
node->name, node->cluster_layer_id);
}
}
}
}
static pcmk__node_status_t *
find_cib_cluster_node(const char *id, const char *uname)
{
GHashTableIter iter;
pcmk__node_status_t *node = NULL;
pcmk__node_status_t *by_id = NULL;
pcmk__node_status_t *by_name = NULL;
if (uname) {
g_hash_table_iter_init(&iter, cluster_node_cib_cache);
while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) {
if (pcmk__str_eq(node->name, uname, pcmk__str_casei)) {
crm_trace("Name match: %s = %p", node->name, node);
by_name = node;
break;
}
}
}
if (id) {
g_hash_table_iter_init(&iter, cluster_node_cib_cache);
while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) {
if (pcmk__str_eq(node->xml_id, id, pcmk__str_casei)) {
crm_trace("ID match: %s= %p", id, node);
by_id = node;
break;
}
}
}
node = by_id; /* Good default */
if (by_id == by_name) {
/* Nothing to do if they match (both NULL counts) */
crm_trace("Consistent: %p for %s/%s", by_id, id, uname);
} else if (by_id == NULL && by_name) {
crm_trace("Only one: %p for %s/%s", by_name, id, uname);
if (id) {
node = NULL;
} else {
node = by_name;
}
} else if (by_name == NULL && by_id) {
crm_trace("Only one: %p for %s/%s", by_id, id, uname);
if (uname) {
node = NULL;
}
} else if ((uname != NULL) && (by_id->name != NULL)
&& pcmk__str_eq(uname, by_id->name, pcmk__str_casei)) {
/* Multiple nodes have the same uname in the CIB.
* Return by_id. */
} else if ((id != NULL) && (by_name->xml_id != NULL)
&& pcmk__str_eq(id, by_name->xml_id, pcmk__str_casei)) {
/* Multiple nodes have the same id in the CIB.
* Return by_name. */
node = by_name;
} else {
node = NULL;
}
if (node == NULL) {
crm_debug("Couldn't find node%s%s%s%s",
id? " " : "",
id? id : "",
uname? " with name " : "",
uname? uname : "");
}
return node;
}
static void
cluster_node_cib_cache_refresh_helper(xmlNode *xml_node, void *user_data)
{
const char *id = crm_element_value(xml_node, PCMK_XA_ID);
const char *uname = crm_element_value(xml_node, PCMK_XA_UNAME);
pcmk__node_status_t * node = NULL;
CRM_CHECK(id != NULL && uname !=NULL, return);
node = find_cib_cluster_node(id, uname);
if (node == NULL) {
char *uniqueid = crm_generate_uuid();
node = pcmk__assert_alloc(1, sizeof(pcmk__node_status_t));
node->name = pcmk__str_copy(uname);
node->xml_id = pcmk__str_copy(id);
g_hash_table_replace(cluster_node_cib_cache, uniqueid, node);
} else if (pcmk_is_set(node->flags, pcmk__node_status_dirty)) {
pcmk__str_update(&node->name, uname);
/* Node is in cache and hasn't been updated already, so mark it clean */
clear_peer_flags(node, pcmk__node_status_dirty);
}
}
static void
refresh_cluster_node_cib_cache(xmlNode *cib)
{
pcmk__cluster_init_node_caches();
g_hash_table_foreach(cluster_node_cib_cache, mark_dirty, NULL);
crm_foreach_xpath_result(cib, PCMK__XP_MEMBER_NODE_CONFIG,
cluster_node_cib_cache_refresh_helper, NULL);
// Remove all old cache entries that weren't seen in the CIB
g_hash_table_foreach_remove(cluster_node_cib_cache, is_dirty, NULL);
}
void
pcmk__refresh_node_caches_from_cib(xmlNode *cib)
{
refresh_remote_nodes(cib);
refresh_cluster_node_cib_cache(cib);
}
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
#include
void
crm_peer_init(void)
{
pcmk__cluster_init_node_caches();
}
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/lib/common/actions.c b/lib/common/actions.c
index 815bc5b3ea..ecba4765b5 100644
--- a/lib/common/actions.c
+++ b/lib/common/actions.c
@@ -1,590 +1,586 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
-#ifndef _GNU_SOURCE
-# define _GNU_SOURCE
-#endif
-
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*!
* \internal
* \brief Get string equivalent of an action type
*
* \param[in] action Action type
*
* \return Static string describing \p action
*/
const char *
pcmk__action_text(enum pcmk__action_type action)
{
switch (action) {
case pcmk__action_stop:
return PCMK_ACTION_STOP;
case pcmk__action_stopped:
return PCMK_ACTION_STOPPED;
case pcmk__action_start:
return PCMK_ACTION_START;
case pcmk__action_started:
return PCMK_ACTION_RUNNING;
case pcmk__action_shutdown:
return PCMK_ACTION_DO_SHUTDOWN;
case pcmk__action_fence:
return PCMK_ACTION_STONITH;
case pcmk__action_monitor:
return PCMK_ACTION_MONITOR;
case pcmk__action_notify:
return PCMK_ACTION_NOTIFY;
case pcmk__action_notified:
return PCMK_ACTION_NOTIFIED;
case pcmk__action_promote:
return PCMK_ACTION_PROMOTE;
case pcmk__action_promoted:
return PCMK_ACTION_PROMOTED;
case pcmk__action_demote:
return PCMK_ACTION_DEMOTE;
case pcmk__action_demoted:
return PCMK_ACTION_DEMOTED;
default: // pcmk__action_unspecified or invalid
return "no_action";
}
}
/*!
* \internal
* \brief Parse an action type from an action name
*
* \param[in] action_name Action name
*
* \return Action type corresponding to \p action_name
*/
enum pcmk__action_type
pcmk__parse_action(const char *action_name)
{
if (pcmk__str_eq(action_name, PCMK_ACTION_STOP, pcmk__str_none)) {
return pcmk__action_stop;
} else if (pcmk__str_eq(action_name, PCMK_ACTION_STOPPED, pcmk__str_none)) {
return pcmk__action_stopped;
} else if (pcmk__str_eq(action_name, PCMK_ACTION_START, pcmk__str_none)) {
return pcmk__action_start;
} else if (pcmk__str_eq(action_name, PCMK_ACTION_RUNNING, pcmk__str_none)) {
return pcmk__action_started;
} else if (pcmk__str_eq(action_name, PCMK_ACTION_DO_SHUTDOWN,
pcmk__str_none)) {
return pcmk__action_shutdown;
} else if (pcmk__str_eq(action_name, PCMK_ACTION_STONITH, pcmk__str_none)) {
return pcmk__action_fence;
} else if (pcmk__str_eq(action_name, PCMK_ACTION_MONITOR, pcmk__str_none)) {
return pcmk__action_monitor;
} else if (pcmk__str_eq(action_name, PCMK_ACTION_NOTIFY, pcmk__str_none)) {
return pcmk__action_notify;
} else if (pcmk__str_eq(action_name, PCMK_ACTION_NOTIFIED,
pcmk__str_none)) {
return pcmk__action_notified;
} else if (pcmk__str_eq(action_name, PCMK_ACTION_PROMOTE, pcmk__str_none)) {
return pcmk__action_promote;
} else if (pcmk__str_eq(action_name, PCMK_ACTION_DEMOTE, pcmk__str_none)) {
return pcmk__action_demote;
} else if (pcmk__str_eq(action_name, PCMK_ACTION_PROMOTED,
pcmk__str_none)) {
return pcmk__action_promoted;
} else if (pcmk__str_eq(action_name, PCMK_ACTION_DEMOTED, pcmk__str_none)) {
return pcmk__action_demoted;
}
return pcmk__action_unspecified;
}
/*!
* \internal
* \brief Get string equivalent of a failure handling type
*
* \param[in] on_fail Failure handling type
*
* \return Static string describing \p on_fail
*/
const char *
pcmk__on_fail_text(enum pcmk__on_fail on_fail)
{
switch (on_fail) {
case pcmk__on_fail_ignore:
return "ignore";
case pcmk__on_fail_demote:
return "demote";
case pcmk__on_fail_block:
return "block";
case pcmk__on_fail_restart:
return "recover";
case pcmk__on_fail_ban:
return "migrate";
case pcmk__on_fail_stop:
return "stop";
case pcmk__on_fail_fence_node:
return "fence";
case pcmk__on_fail_standby_node:
return "standby";
case pcmk__on_fail_restart_container:
return "restart-container";
case pcmk__on_fail_reset_remote:
return "reset-remote";
}
return "";
}
/*!
* \brief Generate an operation key (RESOURCE_ACTION_INTERVAL)
*
* \param[in] rsc_id ID of resource being operated on
* \param[in] op_type Operation name
* \param[in] interval_ms Operation interval
*
* \return Newly allocated memory containing operation key as string
*
* \note This function asserts on errors, so it will never return NULL.
* The caller is responsible for freeing the result with free().
*/
char *
pcmk__op_key(const char *rsc_id, const char *op_type, guint interval_ms)
{
CRM_ASSERT(rsc_id != NULL);
CRM_ASSERT(op_type != NULL);
return crm_strdup_printf(PCMK__OP_FMT, rsc_id, op_type, interval_ms);
}
static inline gboolean
convert_interval(const char *s, guint *interval_ms)
{
unsigned long l;
errno = 0;
l = strtoul(s, NULL, 10);
if (errno != 0) {
return FALSE;
}
*interval_ms = (guint) l;
return TRUE;
}
/*!
* \internal
* \brief Check for underbar-separated substring match
*
* \param[in] key Overall string being checked
* \param[in] position Match before underbar at this \p key index
* \param[in] matches Substrings to match (may contain underbars)
*
* \return \p key index of underbar before any matching substring,
* or 0 if none
*/
static size_t
match_before(const char *key, size_t position, const char **matches)
{
for (int i = 0; matches[i] != NULL; ++i) {
const size_t match_len = strlen(matches[i]);
// Must have at least X_MATCH before position
if (position > (match_len + 1)) {
const size_t possible = position - match_len - 1;
if ((key[possible] == '_')
&& (strncmp(key + possible + 1, matches[i], match_len) == 0)) {
return possible;
}
}
}
return 0;
}
gboolean
parse_op_key(const char *key, char **rsc_id, char **op_type, guint *interval_ms)
{
guint local_interval_ms = 0;
const size_t key_len = (key == NULL)? 0 : strlen(key);
// Operation keys must be formatted as RSC_ACTION_INTERVAL
size_t action_underbar = 0; // Index in key of underbar before ACTION
size_t interval_underbar = 0; // Index in key of underbar before INTERVAL
size_t possible = 0;
/* Underbar was a poor choice of separator since both RSC and ACTION can
* contain underbars. Here, list action names and name prefixes that can.
*/
const char *actions_with_underbars[] = {
PCMK_ACTION_MIGRATE_FROM,
PCMK_ACTION_MIGRATE_TO,
NULL
};
const char *action_prefixes_with_underbars[] = {
"pre_" PCMK_ACTION_NOTIFY,
"post_" PCMK_ACTION_NOTIFY,
"confirmed-pre_" PCMK_ACTION_NOTIFY,
"confirmed-post_" PCMK_ACTION_NOTIFY,
NULL,
};
// Initialize output variables in case of early return
if (rsc_id) {
*rsc_id = NULL;
}
if (op_type) {
*op_type = NULL;
}
if (interval_ms) {
*interval_ms = 0;
}
// RSC_ACTION_INTERVAL implies a minimum of 5 characters
if (key_len < 5) {
return FALSE;
}
// Find, parse, and validate interval
interval_underbar = key_len - 2;
while ((interval_underbar > 2) && (key[interval_underbar] != '_')) {
--interval_underbar;
}
if ((interval_underbar == 2)
|| !convert_interval(key + interval_underbar + 1, &local_interval_ms)) {
return FALSE;
}
// Find the base (OCF) action name, disregarding prefixes
action_underbar = match_before(key, interval_underbar,
actions_with_underbars);
if (action_underbar == 0) {
action_underbar = interval_underbar - 2;
while ((action_underbar > 0) && (key[action_underbar] != '_')) {
--action_underbar;
}
if (action_underbar == 0) {
return FALSE;
}
}
possible = match_before(key, action_underbar,
action_prefixes_with_underbars);
if (possible != 0) {
action_underbar = possible;
}
// Set output variables
if (rsc_id != NULL) {
*rsc_id = strndup(key, action_underbar);
pcmk__mem_assert(*rsc_id);
}
if (op_type != NULL) {
*op_type = strndup(key + action_underbar + 1,
interval_underbar - action_underbar - 1);
pcmk__mem_assert(*op_type);
}
if (interval_ms != NULL) {
*interval_ms = local_interval_ms;
}
return TRUE;
}
char *
pcmk__notify_key(const char *rsc_id, const char *notify_type,
const char *op_type)
{
CRM_CHECK(rsc_id != NULL, return NULL);
CRM_CHECK(op_type != NULL, return NULL);
CRM_CHECK(notify_type != NULL, return NULL);
return crm_strdup_printf("%s_%s_notify_%s_0",
rsc_id, notify_type, op_type);
}
/*!
* \brief Parse a transition magic string into its constituent parts
*
* \param[in] magic Magic string to parse (must be non-NULL)
* \param[out] uuid If non-NULL, where to store copy of parsed UUID
* \param[out] transition_id If non-NULL, where to store parsed transition ID
* \param[out] action_id If non-NULL, where to store parsed action ID
* \param[out] op_status If non-NULL, where to store parsed result status
* \param[out] op_rc If non-NULL, where to store parsed actual rc
* \param[out] target_rc If non-NULL, where to stored parsed target rc
*
* \return TRUE if key was valid, FALSE otherwise
* \note If uuid is supplied and this returns TRUE, the caller is responsible
* for freeing the memory for *uuid using free().
*/
gboolean
decode_transition_magic(const char *magic, char **uuid, int *transition_id, int *action_id,
int *op_status, int *op_rc, int *target_rc)
{
int res = 0;
char *key = NULL;
gboolean result = TRUE;
int local_op_status = -1;
int local_op_rc = -1;
CRM_CHECK(magic != NULL, return FALSE);
#ifdef HAVE_SSCANF_M
res = sscanf(magic, "%d:%d;%ms", &local_op_status, &local_op_rc, &key);
#else
// magic must have >=4 other characters
key = pcmk__assert_alloc(1, strlen(magic) - 3);
res = sscanf(magic, "%d:%d;%s", &local_op_status, &local_op_rc, key);
#endif
if (res == EOF) {
crm_err("Could not decode transition information '%s': %s",
magic, pcmk_rc_str(errno));
result = FALSE;
} else if (res < 3) {
crm_warn("Transition information '%s' incomplete (%d of 3 expected items)",
magic, res);
result = FALSE;
} else {
if (op_status) {
*op_status = local_op_status;
}
if (op_rc) {
*op_rc = local_op_rc;
}
result = decode_transition_key(key, uuid, transition_id, action_id,
target_rc);
}
free(key);
return result;
}
char *
pcmk__transition_key(int transition_id, int action_id, int target_rc,
const char *node)
{
CRM_CHECK(node != NULL, return NULL);
return crm_strdup_printf("%d:%d:%d:%-*s",
action_id, transition_id, target_rc, 36, node);
}
/*!
* \brief Parse a transition key into its constituent parts
*
* \param[in] key Transition key to parse (must be non-NULL)
* \param[out] uuid If non-NULL, where to store copy of parsed UUID
* \param[out] transition_id If non-NULL, where to store parsed transition ID
* \param[out] action_id If non-NULL, where to store parsed action ID
* \param[out] target_rc If non-NULL, where to stored parsed target rc
*
* \return TRUE if key was valid, FALSE otherwise
* \note If uuid is supplied and this returns TRUE, the caller is responsible
* for freeing the memory for *uuid using free().
*/
gboolean
decode_transition_key(const char *key, char **uuid, int *transition_id, int *action_id,
int *target_rc)
{
int local_transition_id = -1;
int local_action_id = -1;
int local_target_rc = -1;
char local_uuid[37] = { '\0' };
// Initialize any supplied output arguments
if (uuid) {
*uuid = NULL;
}
if (transition_id) {
*transition_id = -1;
}
if (action_id) {
*action_id = -1;
}
if (target_rc) {
*target_rc = -1;
}
CRM_CHECK(key != NULL, return FALSE);
if (sscanf(key, "%d:%d:%d:%36s", &local_action_id, &local_transition_id,
&local_target_rc, local_uuid) != 4) {
crm_err("Invalid transition key '%s'", key);
return FALSE;
}
if (strlen(local_uuid) != 36) {
crm_warn("Invalid UUID '%s' in transition key '%s'", local_uuid, key);
}
if (uuid) {
*uuid = pcmk__str_copy(local_uuid);
}
if (transition_id) {
*transition_id = local_transition_id;
}
if (action_id) {
*action_id = local_action_id;
}
if (target_rc) {
*target_rc = local_target_rc;
}
return TRUE;
}
int
rsc_op_expected_rc(const lrmd_event_data_t *op)
{
int rc = 0;
if (op && op->user_data) {
decode_transition_key(op->user_data, NULL, NULL, NULL, &rc);
}
return rc;
}
gboolean
did_rsc_op_fail(lrmd_event_data_t * op, int target_rc)
{
switch (op->op_status) {
case PCMK_EXEC_CANCELLED:
case PCMK_EXEC_PENDING:
return FALSE;
case PCMK_EXEC_NOT_SUPPORTED:
case PCMK_EXEC_TIMEOUT:
case PCMK_EXEC_ERROR:
case PCMK_EXEC_NOT_CONNECTED:
case PCMK_EXEC_NO_FENCE_DEVICE:
case PCMK_EXEC_NO_SECRETS:
case PCMK_EXEC_INVALID:
return TRUE;
default:
if (target_rc != op->rc) {
return TRUE;
}
}
return FALSE;
}
/*!
* \brief Create a CIB XML element for an operation
*
* \param[in,out] parent If not NULL, make new XML node a child of this
* \param[in] prefix Generate an ID using this prefix
* \param[in] task Operation task to set
* \param[in] interval_spec Operation interval to set
* \param[in] timeout If not NULL, operation timeout to set
*
* \return New XML object on success, NULL otherwise
*/
xmlNode *
crm_create_op_xml(xmlNode *parent, const char *prefix, const char *task,
const char *interval_spec, const char *timeout)
{
xmlNode *xml_op;
CRM_CHECK(prefix && task && interval_spec, return NULL);
xml_op = pcmk__xe_create(parent, PCMK_XE_OP);
pcmk__xe_set_id(xml_op, "%s-%s-%s", prefix, task, interval_spec);
crm_xml_add(xml_op, PCMK_META_INTERVAL, interval_spec);
crm_xml_add(xml_op, PCMK_XA_NAME, task);
if (timeout) {
crm_xml_add(xml_op, PCMK_META_TIMEOUT, timeout);
}
return xml_op;
}
/*!
* \brief Check whether an operation requires resource agent meta-data
*
* \param[in] rsc_class Resource agent class (or NULL to skip class check)
* \param[in] op Operation action (or NULL to skip op check)
*
* \return true if operation needs meta-data, false otherwise
* \note At least one of rsc_class and op must be specified.
*/
bool
crm_op_needs_metadata(const char *rsc_class, const char *op)
{
/* Agent metadata is used to determine whether an agent reload is possible,
* so if this op is not relevant to that feature, we don't need metadata.
*/
CRM_CHECK((rsc_class != NULL) || (op != NULL), return false);
if ((rsc_class != NULL)
&& !pcmk_is_set(pcmk_get_ra_caps(rsc_class), pcmk_ra_cap_params)) {
// Metadata is needed only for resource classes that use parameters
return false;
}
if (op == NULL) {
return true;
}
// Metadata is needed only for these actions
return pcmk__str_any_of(op, PCMK_ACTION_START, PCMK_ACTION_MONITOR,
PCMK_ACTION_PROMOTE, PCMK_ACTION_DEMOTE,
PCMK_ACTION_RELOAD, PCMK_ACTION_RELOAD_AGENT,
PCMK_ACTION_MIGRATE_TO, PCMK_ACTION_MIGRATE_FROM,
PCMK_ACTION_NOTIFY, NULL);
}
/*!
* \internal
* \brief Check whether an action name is for a fencing action
*
* \param[in] action Action name to check
*
* \return \c true if \p action is \c PCMK_ACTION_OFF, \c PCMK_ACTION_REBOOT,
* or \c PCMK__ACTION_POWEROFF, otherwise \c false
*/
bool
pcmk__is_fencing_action(const char *action)
{
return pcmk__str_any_of(action, PCMK_ACTION_OFF, PCMK_ACTION_REBOOT,
PCMK__ACTION_POWEROFF, NULL);
}
diff --git a/lib/common/agents.c b/lib/common/agents.c
index 16e064fb24..9a47619959 100644
--- a/lib/common/agents.c
+++ b/lib/common/agents.c
@@ -1,200 +1,196 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
-#ifndef _GNU_SOURCE
-# define _GNU_SOURCE
-#endif
-
#include
#include
#include
#include
#include
/*!
* \brief Get capabilities of a resource agent standard
*
* \param[in] standard Standard name
*
* \return Bitmask of enum pcmk_ra_caps values
*/
uint32_t
pcmk_get_ra_caps(const char *standard)
{
/* @COMPAT This should probably be case-sensitive, but isn't,
* for backward compatibility.
*/
if (standard == NULL) {
return pcmk_ra_cap_none;
} else if (!strcasecmp(standard, PCMK_RESOURCE_CLASS_OCF)) {
return pcmk_ra_cap_provider | pcmk_ra_cap_params
| pcmk_ra_cap_unique | pcmk_ra_cap_promotable;
} else if (!strcasecmp(standard, PCMK_RESOURCE_CLASS_STONITH)) {
/* @COMPAT Stonith resources can't really be unique clones, but we've
* allowed it in the past and have it in some scheduler regression tests
* (which were likely never used as real configurations).
*
* @TODO Remove pcmk_ra_cap_unique at the next major schema version
* bump, with a transform to remove PCMK_META_GLOBALLY_UNIQUE from the
* config.
*/
return pcmk_ra_cap_params | pcmk_ra_cap_unique | pcmk_ra_cap_stdin
| pcmk_ra_cap_fence_params;
} else if (!strcasecmp(standard, PCMK_RESOURCE_CLASS_SYSTEMD)
|| !strcasecmp(standard, PCMK_RESOURCE_CLASS_SERVICE)
|| !strcasecmp(standard, PCMK_RESOURCE_CLASS_LSB)
|| !strcasecmp(standard, PCMK_RESOURCE_CLASS_UPSTART)) {
/* Since service can map to LSB, systemd, or upstart, these should
* have identical capabilities
*/
return pcmk_ra_cap_status;
} else if (!strcasecmp(standard, PCMK_RESOURCE_CLASS_NAGIOS)) {
return pcmk_ra_cap_params;
}
return pcmk_ra_cap_none;
}
int
pcmk__effective_rc(int rc)
{
int remapped_rc = rc;
switch (rc) {
case PCMK_OCF_DEGRADED:
remapped_rc = PCMK_OCF_OK;
break;
case PCMK_OCF_DEGRADED_PROMOTED:
remapped_rc = PCMK_OCF_RUNNING_PROMOTED;
break;
default:
break;
}
return remapped_rc;
}
char *
crm_generate_ra_key(const char *standard, const char *provider,
const char *type)
{
bool std_empty = pcmk__str_empty(standard);
bool prov_empty = pcmk__str_empty(provider);
bool ty_empty = pcmk__str_empty(type);
if (std_empty || ty_empty) {
return NULL;
}
return crm_strdup_printf("%s%s%s:%s",
standard,
(prov_empty ? "" : ":"), (prov_empty ? "" : provider),
type);
}
/*!
* \brief Parse a "standard[:provider]:type" agent specification
*
* \param[in] spec Agent specification
* \param[out] standard Newly allocated memory containing agent standard (or NULL)
* \param[out] provider Newly allocated memory containing agent provider (or NULL)
* \param[put] type Newly allocated memory containing agent type (or NULL)
*
* \return pcmk_ok if the string could be parsed, -EINVAL otherwise
*
* \note It is acceptable for the type to contain a ':' if the standard supports
* that. For example, systemd supports the form "systemd:UNIT@A:B".
* \note It is the caller's responsibility to free the returned values.
*/
int
crm_parse_agent_spec(const char *spec, char **standard, char **provider,
char **type)
{
char *colon;
CRM_CHECK(spec && standard && provider && type, return -EINVAL);
*standard = NULL;
*provider = NULL;
*type = NULL;
colon = strchr(spec, ':');
if ((colon == NULL) || (colon == spec)) {
return -EINVAL;
}
*standard = strndup(spec, colon - spec);
spec = colon + 1;
if (pcmk_is_set(pcmk_get_ra_caps(*standard), pcmk_ra_cap_provider)) {
colon = strchr(spec, ':');
if ((colon == NULL) || (colon == spec)) {
free(*standard);
return -EINVAL;
}
*provider = strndup(spec, colon - spec);
spec = colon + 1;
}
if (*spec == '\0') {
free(*standard);
free(*provider);
return -EINVAL;
}
*type = strdup(spec);
return pcmk_ok;
}
/*!
* \brief Check whether a given stonith parameter is handled by Pacemaker
*
* Return true if a given string is the name of one of the special resource
* instance attributes interpreted directly by Pacemaker for stonith-class
* resources.
*
* \param[in] param Parameter name to check
*
* \return true if \p param is a special fencing parameter
*/
bool
pcmk_stonith_param(const char *param)
{
if (param == NULL) {
return false;
}
if (pcmk__str_any_of(param, PCMK_STONITH_PROVIDES,
PCMK_STONITH_STONITH_TIMEOUT, NULL)) {
return true;
}
if (!pcmk__starts_with(param, "pcmk_")) { // Short-circuit common case
return false;
}
if (pcmk__str_any_of(param,
PCMK_STONITH_ACTION_LIMIT,
PCMK_STONITH_DELAY_BASE,
PCMK_STONITH_DELAY_MAX,
PCMK_STONITH_HOST_ARGUMENT,
PCMK_STONITH_HOST_CHECK,
PCMK_STONITH_HOST_LIST,
PCMK_STONITH_HOST_MAP,
NULL)) {
return true;
}
param = strchr(param + 5, '_'); // Skip past "pcmk_ACTION"
return pcmk__str_any_of(param, "_action", "_timeout", "_retries", NULL);
}
diff --git a/lib/common/attrs.c b/lib/common/attrs.c
index cc8f42df30..7d313d1e1b 100644
--- a/lib/common/attrs.c
+++ b/lib/common/attrs.c
@@ -1,183 +1,179 @@
/*
* Copyright 2011-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
-#ifndef _GNU_SOURCE
-# define _GNU_SOURCE
-#endif
-
#include
#include
#include
#include
#include
#define OCF_RESKEY_PREFIX "OCF_RESKEY_"
#define LRM_TARGET_ENV OCF_RESKEY_PREFIX CRM_META "_" PCMK__META_ON_NODE
/*!
* \internal
* \brief Get the node name that should be used to set node attributes
*
* If given NULL, "auto", or "localhost" as an argument, check the environment
* to detect the node name that should be used to set node attributes. (The
* caller might not know the correct name, for example if the target is part of
* a bundle with \c PCMK_META_CONTAINER_ATTRIBUTE_TARGET set to
* \c PCMK_VALUE_HOST.)
*
* \param[in] name NULL, "auto" or "localhost" to check environment variables,
* or anything else to return NULL
*
* \return Node name that should be used for node attributes based on the
* environment if known, otherwise NULL
*/
const char *
pcmk__node_attr_target(const char *name)
{
if (name == NULL || pcmk__strcase_any_of(name, "auto", "localhost", NULL)) {
char buf[128] = OCF_RESKEY_PREFIX;
size_t offset = sizeof(OCF_RESKEY_PREFIX) - 1;
char *target_var = crm_meta_name(PCMK_META_CONTAINER_ATTRIBUTE_TARGET);
char *phys_var = crm_meta_name(PCMK__META_PHYSICAL_HOST);
const char *target = NULL;
const char *host_physical = NULL;
snprintf(buf + offset, sizeof(buf) - offset, "%s", target_var);
target = getenv(buf);
snprintf(buf + offset, sizeof(buf) - offset, "%s", phys_var);
host_physical = getenv(buf);
// It is important to use the name by which the scheduler knows us
if (host_physical
&& pcmk__str_eq(target, PCMK_VALUE_HOST, pcmk__str_casei)) {
name = host_physical;
} else {
const char *host_pcmk = getenv(LRM_TARGET_ENV);
if (host_pcmk) {
name = host_pcmk;
}
}
free(target_var);
free(phys_var);
// TODO? Call pcmk__cluster_local_node_name() if name == NULL
// (currently would require linkage against libcrmcluster)
return name;
} else {
return NULL;
}
}
/*!
* \brief Return the name of the node attribute used as a promotion score
*
* \param[in] rsc_id Resource ID that promotion score is for (or NULL to
* check the OCF_RESOURCE_INSTANCE environment variable)
*
* \return Newly allocated string with the node attribute name (or NULL on
* error, including no ID or environment variable specified)
* \note It is the caller's responsibility to free() the result.
*/
char *
pcmk_promotion_score_name(const char *rsc_id)
{
if (pcmk__str_empty(rsc_id)) {
rsc_id = getenv("OCF_RESOURCE_INSTANCE");
if (pcmk__str_empty(rsc_id)) {
return NULL;
}
}
return crm_strdup_printf("master-%s", rsc_id);
}
/*!
* \internal
* \brief Get the value of a node attribute
*
* \param[in] node Node to get attribute for
* \param[in] name Name of node attribute to get
* \param[in] target If this is \c PCMK_VALUE_HOST and \p node is a guest
* (bundle) node, get the value from the guest's host,
* otherwise get the value from \p node itself
* \param[in] node_type If getting the value from \p node's host, this
* indicates whether to check the current or assigned host
*
* \return Value of \p name attribute for \p node
*/
const char *
pcmk__node_attr(const pcmk_node_t *node, const char *name, const char *target,
enum pcmk__rsc_node node_type)
{
const char *value = NULL; // Attribute value to return
const char *node_type_s = NULL; // Readable equivalent of node_type
const pcmk_node_t *host = NULL;
const pcmk_resource_t *container = NULL;
if ((node == NULL) || (name == NULL)) {
return NULL;
}
/* Check the node's own attributes unless this is a guest (bundle) node with
* the container host as the attribute target.
*/
if (!pcmk__is_guest_or_bundle_node(node)
|| !pcmk__str_eq(target, PCMK_VALUE_HOST, pcmk__str_casei)) {
value = g_hash_table_lookup(node->priv->attrs, name);
crm_trace("%s='%s' on %s",
name, pcmk__s(value, ""), pcmk__node_name(node));
return value;
}
/* This resource needs attributes set for the container's host instead of
* for the container itself (useful when the container uses the host's
* storage).
*/
container = node->priv->remote->priv->launcher;
switch (node_type) {
case pcmk__rsc_node_assigned:
host = container->priv->assigned_node;
if (host == NULL) {
crm_trace("Skipping %s lookup for %s because "
"its container %s is unassigned",
name, pcmk__node_name(node), container->id);
return NULL;
}
node_type_s = "assigned";
break;
case pcmk__rsc_node_current:
if (container->priv->active_nodes != NULL) {
host = container->priv->active_nodes->data;
}
if (host == NULL) {
crm_trace("Skipping %s lookup for %s because "
"its container %s is inactive",
name, pcmk__node_name(node), container->id);
return NULL;
}
node_type_s = "current";
break;
default:
// Add support for other enum pcmk__rsc_node values if needed
CRM_ASSERT(false);
break;
}
value = g_hash_table_lookup(host->priv->attrs, name);
crm_trace("%s='%s' for %s on %s container host %s",
name, pcmk__s(value, ""), pcmk__node_name(node), node_type_s,
pcmk__node_name(host));
return value;
}
diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h
index 74b44f61a0..0b7def3174 100644
--- a/lib/common/crmcommon_private.h
+++ b/lib/common/crmcommon_private.h
@@ -1,446 +1,450 @@
/*
* Copyright 2018-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#ifndef PCMK__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 // uint8_t, uint32_t
#include // bool
#include // size_t
#include // G_GNUC_INTERNAL, G_GNUC_PRINTF, gchar, etc.
#include // xmlNode, xmlAttr
#include // struct qb_ipc_response_header
#include // pcmk_ipc_api_t, crm_ipc_t, etc.
#include // crm_time_t
#include // LOG_NEVER
#include // mainloop_io_t
#include // pcmk__output_t
#include // crm_exit_t
#include // pcmk_rule_input_t
#include // pcmkXmlStr
#include // enum xml_private_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
/* When deleting portions of an XML tree, we keep a record so we can know later
* (e.g. when checking differences) that something was deleted.
*/
typedef struct pcmk__deleted_xml_s {
gchar *path;
int position;
} pcmk__deleted_xml_t;
typedef struct xml_node_private_s {
uint32_t check;
uint32_t flags;
} xml_node_private_t;
typedef struct xml_doc_private_s {
uint32_t check;
uint32_t flags;
char *user;
GList *acls;
GList *deleted_objs; // List of pcmk__deleted_xml_t
} xml_doc_private_t;
// XML entity references
#define PCMK__XML_ENTITY_AMP "&"
#define PCMK__XML_ENTITY_GT ">"
#define PCMK__XML_ENTITY_LT "<"
#define PCMK__XML_ENTITY_QUOT """
//! libxml2 supports only XML version 1.0, at least as of libxml2-2.12.5
#define PCMK__XML_VERSION ((pcmkXmlStr) "1.0")
#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
bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy);
G_GNUC_INTERNAL
void pcmk__xml_mark_created(xmlNode *xml);
G_GNUC_INTERNAL
int pcmk__xml_position(const xmlNode *xml,
enum xml_private_flags ignore_if_set);
G_GNUC_INTERNAL
xmlNode *pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle,
bool exact);
G_GNUC_INTERNAL
void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update,
uint32_t flags, bool as_diff);
G_GNUC_INTERNAL
xmlNode *pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment,
bool exact);
G_GNUC_INTERNAL
void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update);
G_GNUC_INTERNAL
void pcmk__free_acls(GList *acls);
G_GNUC_INTERNAL
void pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user);
G_GNUC_INTERNAL
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
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
size_t ipc_size_max; // maximum IPC buffer size
crm_ipc_t *ipc; // IPC connection
mainloop_io_t *mainloop_io; // If using mainloop, I/O source for IPC
bool free_on_disconnect; // Whether disconnect should free object
pcmk_ipc_callback_t cb; // Caller-registered callback (if any)
void *user_data; // Caller-registered data (if any)
void *api_data; // For daemon-specific use
pcmk__ipc_methods_t *cmds; // Behavior that varies by daemon
};
typedef struct pcmk__ipc_header_s {
struct qb_ipc_response_header qb;
uint32_t size_uncompressed;
uint32_t size_compressed;
uint32_t flags;
uint8_t version;
} pcmk__ipc_header_t;
G_GNUC_INTERNAL
int pcmk__send_ipc_request(pcmk_ipc_api_t *api, 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
unsigned int pcmk__ipc_buffer_size(unsigned int max);
G_GNUC_INTERNAL
bool pcmk__valid_ipc_header(const pcmk__ipc_header_t *header);
G_GNUC_INTERNAL
pcmk__ipc_methods_t *pcmk__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;
- char *transform;
+
+ /*!
+ * 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;
- char *transform_enter;
- bool transform_onleave;
} 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/io.c b/lib/common/io.c
index 4c034bf439..0e556378a1 100644
--- a/lib/common/io.c
+++ b/lib/common/io.c
@@ -1,643 +1,639 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
-#ifndef _GNU_SOURCE
-# define _GNU_SOURCE
-#endif
-
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*!
* \internal
* \brief Create a directory, including any parent directories needed
*
* \param[in] path_c Pathname of the directory to create
* \param[in] mode Permissions to be used (with current umask) when creating
*
* \return Standard Pacemaker return code
*/
int
pcmk__build_path(const char *path_c, mode_t mode)
{
int offset = 1, len = 0;
int rc = pcmk_rc_ok;
char *path = strdup(path_c);
// cppcheck seems not to understand the abort logic in CRM_CHECK
// cppcheck-suppress memleak
CRM_CHECK(path != NULL, return -ENOMEM);
for (len = strlen(path); offset < len; offset++) {
if (path[offset] == '/') {
path[offset] = 0;
if ((mkdir(path, mode) < 0) && (errno != EEXIST)) {
rc = errno;
goto done;
}
path[offset] = '/';
}
}
if ((mkdir(path, mode) < 0) && (errno != EEXIST)) {
rc = errno;
}
done:
free(path);
return rc;
}
/*!
* \internal
* \brief Return canonicalized form of a path name
*
* \param[in] path Pathname to canonicalize
* \param[out] resolved_path Where to store canonicalized pathname
*
* \return Standard Pacemaker return code
* \note The caller is responsible for freeing \p resolved_path on success.
* \note This function exists because not all C library versions of
* realpath(path, resolved_path) support a NULL resolved_path.
*/
int
pcmk__real_path(const char *path, char **resolved_path)
{
CRM_CHECK((path != NULL) && (resolved_path != NULL), return EINVAL);
#if _POSIX_VERSION >= 200809L
/* Recent C libraries can dynamically allocate memory as needed */
*resolved_path = realpath(path, NULL);
return (*resolved_path == NULL)? errno : pcmk_rc_ok;
#elif defined(PATH_MAX)
/* Older implementations require pre-allocated memory */
/* (this is less desirable because PATH_MAX may be huge or not defined) */
*resolved_path = malloc(PATH_MAX);
if ((*resolved_path == NULL) || (realpath(path, *resolved_path) == NULL)) {
return errno;
}
return pcmk_rc_ok;
#else
*resolved_path = NULL;
return ENOTSUP;
#endif
}
/*!
* \internal
* \brief Create a file name using a sequence number
*
* \param[in] directory Directory that contains the file series
* \param[in] series Start of file name
* \param[in] sequence Sequence number
* \param[in] bzip Whether to use ".bz2" instead of ".raw" as extension
*
* \return Newly allocated file path (asserts on error, so always non-NULL)
* \note The caller is responsible for freeing the return value.
*/
char *
pcmk__series_filename(const char *directory, const char *series, int sequence,
bool bzip)
{
CRM_ASSERT((directory != NULL) && (series != NULL));
return crm_strdup_printf("%s/%s-%d.%s", directory, series, sequence,
(bzip? "bz2" : "raw"));
}
/*!
* \internal
* \brief Read sequence number stored in a file series' .last file
*
* \param[in] directory Directory that contains the file series
* \param[in] series Start of file name
* \param[out] seq Where to store the sequence number
*
* \return Standard Pacemaker return code
*/
int
pcmk__read_series_sequence(const char *directory, const char *series,
unsigned int *seq)
{
int rc;
FILE *fp = NULL;
char *series_file = NULL;
if ((directory == NULL) || (series == NULL) || (seq == NULL)) {
return EINVAL;
}
series_file = crm_strdup_printf("%s/%s.last", directory, series);
fp = fopen(series_file, "r");
if (fp == NULL) {
rc = errno;
crm_debug("Could not open series file %s: %s",
series_file, strerror(rc));
free(series_file);
return rc;
}
errno = 0;
if (fscanf(fp, "%u", seq) != 1) {
rc = (errno == 0)? ENODATA : errno;
crm_debug("Could not read sequence number from series file %s: %s",
series_file, pcmk_rc_str(rc));
fclose(fp);
return rc;
}
fclose(fp);
crm_trace("Found last sequence number %u in series file %s",
*seq, series_file);
free(series_file);
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Write sequence number to a file series' .last file
*
* \param[in] directory Directory that contains the file series
* \param[in] series Start of file name
* \param[in] sequence Sequence number to write
* \param[in] max Maximum sequence value, after which it is reset to 0
*
* \note This function logs some errors but does not return any to the caller
*/
void
pcmk__write_series_sequence(const char *directory, const char *series,
unsigned int sequence, int max)
{
int rc = 0;
FILE *file_strm = NULL;
char *series_file = NULL;
CRM_CHECK(directory != NULL, return);
CRM_CHECK(series != NULL, return);
if (max == 0) {
return;
}
if (max > 0 && sequence >= max) {
sequence = 0;
}
series_file = crm_strdup_printf("%s/%s.last", directory, series);
file_strm = fopen(series_file, "w");
if (file_strm != NULL) {
rc = fprintf(file_strm, "%u", sequence);
if (rc < 0) {
crm_perror(LOG_ERR, "Cannot write to series file %s", series_file);
}
} else {
crm_err("Cannot open series file %s for writing", series_file);
}
if (file_strm != NULL) {
fflush(file_strm);
fclose(file_strm);
}
crm_trace("Wrote %d to %s", sequence, series_file);
free(series_file);
}
/*!
* \internal
* \brief Change the owner and group of a file series' .last file
*
* \param[in] directory Directory that contains series
* \param[in] series Series to change
* \param[in] uid User ID of desired file owner
* \param[in] gid Group ID of desired file group
*
* \return Standard Pacemaker return code
* \note The caller must have the appropriate privileges.
*/
int
pcmk__chown_series_sequence(const char *directory, const char *series,
uid_t uid, gid_t gid)
{
char *series_file = NULL;
int rc = pcmk_rc_ok;
if ((directory == NULL) || (series == NULL)) {
return EINVAL;
}
series_file = crm_strdup_printf("%s/%s.last", directory, series);
if (chown(series_file, uid, gid) < 0) {
rc = errno;
}
free(series_file);
return rc;
}
static bool
pcmk__daemon_user_can_write(const char *target_name, struct stat *target_stat)
{
struct passwd *sys_user = NULL;
errno = 0;
sys_user = getpwnam(CRM_DAEMON_USER);
if (sys_user == NULL) {
crm_notice("Could not find user %s: %s",
CRM_DAEMON_USER, pcmk_rc_str(errno));
return FALSE;
}
if (target_stat->st_uid != sys_user->pw_uid) {
crm_notice("%s is not owned by user %s " QB_XS " uid %d != %d",
target_name, CRM_DAEMON_USER, sys_user->pw_uid,
target_stat->st_uid);
return FALSE;
}
if ((target_stat->st_mode & (S_IRUSR | S_IWUSR)) == 0) {
crm_notice("%s is not readable and writable by user %s "
QB_XS " st_mode=0%lo",
target_name, CRM_DAEMON_USER,
(unsigned long) target_stat->st_mode);
return FALSE;
}
return TRUE;
}
static bool
pcmk__daemon_group_can_write(const char *target_name, struct stat *target_stat)
{
struct group *sys_grp = NULL;
errno = 0;
sys_grp = getgrnam(CRM_DAEMON_GROUP);
if (sys_grp == NULL) {
crm_notice("Could not find group %s: %s",
CRM_DAEMON_GROUP, pcmk_rc_str(errno));
return FALSE;
}
if (target_stat->st_gid != sys_grp->gr_gid) {
crm_notice("%s is not owned by group %s " QB_XS " uid %d != %d",
target_name, CRM_DAEMON_GROUP,
sys_grp->gr_gid, target_stat->st_gid);
return FALSE;
}
if ((target_stat->st_mode & (S_IRGRP | S_IWGRP)) == 0) {
crm_notice("%s is not readable and writable by group %s "
QB_XS " st_mode=0%lo",
target_name, CRM_DAEMON_GROUP,
(unsigned long) target_stat->st_mode);
return FALSE;
}
return TRUE;
}
/*!
* \internal
* \brief Check whether a directory or file is writable by the cluster daemon
*
* Return true if either the cluster daemon user or cluster daemon group has
* write permission on a specified file or directory.
*
* \param[in] dir Directory to check (this argument must be specified, and
* the directory must exist)
* \param[in] file File to check (only the directory will be checked if this
* argument is not specified or the file does not exist)
*
* \return true if target is writable by cluster daemon, false otherwise
*/
bool
pcmk__daemon_can_write(const char *dir, const char *file)
{
int s_res = 0;
struct stat buf;
char *full_file = NULL;
const char *target = NULL;
// Caller must supply directory
CRM_ASSERT(dir != NULL);
// If file is given, check whether it exists as a regular file
if (file != NULL) {
full_file = crm_strdup_printf("%s/%s", dir, file);
target = full_file;
s_res = stat(full_file, &buf);
if (s_res < 0) {
crm_notice("%s not found: %s", target, pcmk_rc_str(errno));
free(full_file);
full_file = NULL;
target = NULL;
} else if (S_ISREG(buf.st_mode) == FALSE) {
crm_err("%s must be a regular file " QB_XS " st_mode=0%lo",
target, (unsigned long) buf.st_mode);
free(full_file);
return false;
}
}
// If file is not given, ensure dir exists as directory
if (target == NULL) {
target = dir;
s_res = stat(dir, &buf);
if (s_res < 0) {
crm_err("%s not found: %s", dir, pcmk_rc_str(errno));
return false;
} else if (S_ISDIR(buf.st_mode) == FALSE) {
crm_err("%s must be a directory " QB_XS " st_mode=0%lo",
dir, (unsigned long) buf.st_mode);
return false;
}
}
if (!pcmk__daemon_user_can_write(target, &buf)
&& !pcmk__daemon_group_can_write(target, &buf)) {
crm_err("%s must be owned and writable by either user %s or group %s "
QB_XS " st_mode=0%lo",
target, CRM_DAEMON_USER, CRM_DAEMON_GROUP,
(unsigned long) buf.st_mode);
free(full_file);
return false;
}
free(full_file);
return true;
}
/*!
* \internal
* \brief Flush and sync a directory to disk
*
* \param[in] name Directory to flush and sync
* \note This function logs errors but does not return them to the caller
*/
void
pcmk__sync_directory(const char *name)
{
int fd;
DIR *directory;
directory = opendir(name);
if (directory == NULL) {
crm_perror(LOG_ERR, "Could not open %s for syncing", name);
return;
}
fd = dirfd(directory);
if (fd < 0) {
crm_perror(LOG_ERR, "Could not obtain file descriptor for %s", name);
return;
}
if (fsync(fd) < 0) {
crm_perror(LOG_ERR, "Could not sync %s", name);
}
if (closedir(directory) < 0) {
crm_perror(LOG_ERR, "Could not close %s after fsync", name);
}
}
/*!
* \internal
* \brief Read the contents of a file
*
* \param[in] filename Name of file to read
* \param[out] contents Where to store file contents
*
* \return Standard Pacemaker return code
* \note On success, the caller is responsible for freeing contents.
*/
int
pcmk__file_contents(const char *filename, char **contents)
{
FILE *fp;
int length, read_len;
int rc = pcmk_rc_ok;
if ((filename == NULL) || (contents == NULL)) {
return EINVAL;
}
fp = fopen(filename, "r");
if ((fp == NULL) || (fseek(fp, 0L, SEEK_END) < 0)) {
rc = errno;
goto bail;
}
length = ftell(fp);
if (length < 0) {
rc = errno;
goto bail;
}
if (length == 0) {
*contents = NULL;
} else {
*contents = calloc(length + 1, sizeof(char));
if (*contents == NULL) {
rc = errno;
goto bail;
}
rewind(fp);
read_len = fread(*contents, 1, length, fp);
if (read_len != length) {
free(*contents);
*contents = NULL;
rc = EIO;
} else {
/* Coverity thinks *contents isn't null-terminated. It doesn't
* understand calloc().
*/
(*contents)[length] = '\0';
}
}
bail:
if (fp != NULL) {
fclose(fp);
}
return rc;
}
/*!
* \internal
* \brief Write text to a file, flush and sync it to disk, then close the file
*
* \param[in] fd File descriptor opened for writing
* \param[in] contents String to write to file
*
* \return Standard Pacemaker return code
*/
int
pcmk__write_sync(int fd, const char *contents)
{
int rc = 0;
FILE *fp = fdopen(fd, "w");
if (fp == NULL) {
return errno;
}
if ((contents != NULL) && (fprintf(fp, "%s", contents) < 0)) {
rc = EIO;
}
if (fflush(fp) != 0) {
rc = errno;
}
if (fsync(fileno(fp)) < 0) {
rc = errno;
}
fclose(fp);
return rc;
}
/*!
* \internal
* \brief Set a file descriptor to non-blocking
*
* \param[in] fd File descriptor to use
*
* \return Standard Pacemaker return code
*/
int
pcmk__set_nonblocking(int fd)
{
int flag = fcntl(fd, F_GETFL);
if (flag < 0) {
return errno;
}
if (fcntl(fd, F_SETFL, flag | O_NONBLOCK) < 0) {
return errno;
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Get directory name for temporary files
*
* Return the value of the TMPDIR environment variable if it is set to a
* full path, otherwise return "/tmp".
*
* \return Name of directory to be used for temporary files
*/
const char *
pcmk__get_tmpdir(void)
{
const char *dir = getenv("TMPDIR");
return (dir && (*dir == '/'))? dir : "/tmp";
}
/*!
* \internal
* \brief Close open file descriptors
*
* Close all file descriptors (except optionally stdin, stdout, and stderr),
* which is a best practice for a new child process forked for the purpose of
* executing an external program.
*
* \param[in] bool If true, close stdin, stdout, and stderr as well
*/
void
pcmk__close_fds_in_child(bool all)
{
DIR *dir;
struct rlimit rlim;
rlim_t max_fd;
int min_fd = (all? 0 : (STDERR_FILENO + 1));
/* Find the current process's (soft) limit for open files. getrlimit()
* should always work, but have a fallback just in case.
*/
if (getrlimit(RLIMIT_NOFILE, &rlim) == 0) {
max_fd = rlim.rlim_cur - 1;
} else {
long conf_max = sysconf(_SC_OPEN_MAX);
max_fd = (conf_max > 0)? conf_max : 1024;
}
/* /proc/self/fd (on Linux) or /dev/fd (on most OSes) contains symlinks to
* all open files for the current process, named as the file descriptor.
* Use this if available, because it's more efficient than a shotgun
* approach to closing descriptors.
*/
#if HAVE_LINUX_PROCFS
dir = opendir("/proc/self/fd");
if (dir == NULL) {
dir = opendir("/dev/fd");
}
#else
dir = opendir("/dev/fd");
#endif // HAVE_LINUX_PROCFS
if (dir != NULL) {
struct dirent *entry;
int dir_fd = dirfd(dir);
while ((entry = readdir(dir)) != NULL) {
int lpc = atoi(entry->d_name);
/* How could one of these entries be higher than max_fd, you ask?
* It isn't possible in normal operation, but when run under
* valgrind, valgrind can open high-numbered file descriptors for
* its own use that are higher than the process's soft limit.
* These will show up in the fd directory but aren't closable.
*/
if ((lpc >= min_fd) && (lpc <= max_fd) && (lpc != dir_fd)) {
close(lpc);
}
}
closedir(dir);
return;
}
/* If no fd directory is available, iterate over all possible descriptors.
* This is less efficient due to the overhead of many system calls.
*/
for (int lpc = max_fd; lpc >= min_fd; lpc--) {
close(lpc);
}
}
/*!
* \brief Duplicate a file path, inserting a prefix if not absolute
*
* \param[in] filename File path to duplicate
* \param[in] dirname If filename is not absolute, prefix to add
*
* \return Newly allocated memory with full path (guaranteed non-NULL)
*/
char *
pcmk__full_path(const char *filename, const char *dirname)
{
CRM_ASSERT(filename != NULL);
if (filename[0] == '/') {
return pcmk__str_copy(filename);
}
CRM_ASSERT(dirname != NULL);
return crm_strdup_printf("%s/%s", dirname, filename);
}
diff --git a/lib/common/ipc_attrd.c b/lib/common/ipc_attrd.c
index da54440d34..6e91819c19 100644
--- a/lib/common/ipc_attrd.c
+++ b/lib/common/ipc_attrd.c
@@ -1,485 +1,481 @@
/*
* Copyright 2011-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
-#ifndef _GNU_SOURCE
-# define _GNU_SOURCE
-#endif
-
#include
#include
#include
#include
#include
#include
#include
#include "crmcommon_private.h"
static void
set_pairs_data(pcmk__attrd_api_reply_t *data, xmlNode *msg_data)
{
const char *name = NULL;
pcmk__attrd_query_pair_t *pair;
name = crm_element_value(msg_data, PCMK__XA_ATTR_NAME);
for (xmlNode *node = pcmk__xe_first_child(msg_data, PCMK_XE_NODE, NULL,
NULL);
node != NULL; node = pcmk__xe_next_same(node)) {
pair = pcmk__assert_alloc(1, sizeof(pcmk__attrd_query_pair_t));
pair->node = crm_element_value(node, PCMK__XA_ATTR_HOST);
pair->name = name;
pair->value = crm_element_value(node, PCMK__XA_ATTR_VALUE);
data->data.pairs = g_list_prepend(data->data.pairs, pair);
}
}
static bool
reply_expected(pcmk_ipc_api_t *api, const xmlNode *request)
{
const char *command = crm_element_value(request, PCMK_XA_TASK);
return pcmk__str_any_of(command,
PCMK__ATTRD_CMD_CLEAR_FAILURE,
PCMK__ATTRD_CMD_QUERY,
PCMK__ATTRD_CMD_REFRESH,
PCMK__ATTRD_CMD_UPDATE,
PCMK__ATTRD_CMD_UPDATE_BOTH,
PCMK__ATTRD_CMD_UPDATE_DELAY,
NULL);
}
static bool
dispatch(pcmk_ipc_api_t *api, xmlNode *reply)
{
const char *value = NULL;
crm_exit_t status = CRM_EX_OK;
pcmk__attrd_api_reply_t reply_data = {
pcmk__attrd_reply_unknown
};
if (pcmk__xe_is(reply, PCMK__XE_ACK)) {
return false;
}
/* Do some basic validation of the reply */
value = crm_element_value(reply, PCMK__XA_T);
if (pcmk__str_empty(value)
|| !pcmk__str_eq(value, PCMK__VALUE_ATTRD, pcmk__str_none)) {
crm_info("Unrecognizable message from attribute manager: "
"message type '%s' not '" PCMK__VALUE_ATTRD "'",
pcmk__s(value, ""));
status = CRM_EX_PROTOCOL;
goto done;
}
value = crm_element_value(reply, PCMK__XA_SUBT);
/* Only the query command gets a reply for now. NULL counts as query for
* backward compatibility with attribute managers <2.1.3 that didn't set it.
*/
if (pcmk__str_eq(value, PCMK__ATTRD_CMD_QUERY, pcmk__str_null_matches)) {
if (!xmlHasProp(reply, (pcmkXmlStr) PCMK__XA_ATTR_NAME)) {
status = ENXIO; // Most likely, the attribute doesn't exist
goto done;
}
reply_data.reply_type = pcmk__attrd_reply_query;
set_pairs_data(&reply_data, reply);
} else {
crm_info("Unrecognizable message from attribute manager: "
"message subtype '%s' unknown", pcmk__s(value, ""));
status = CRM_EX_PROTOCOL;
goto done;
}
done:
pcmk__call_ipc_callback(api, pcmk_ipc_event_reply, status, &reply_data);
/* Free any reply data that was allocated */
if (reply_data.data.pairs) {
g_list_free_full(reply_data.data.pairs, free);
}
return false;
}
pcmk__ipc_methods_t *
pcmk__attrd_api_methods(void)
{
pcmk__ipc_methods_t *cmds = calloc(1, sizeof(pcmk__ipc_methods_t));
if (cmds != NULL) {
cmds->new_data = NULL;
cmds->free_data = NULL;
cmds->post_connect = NULL;
cmds->reply_expected = reply_expected;
cmds->dispatch = dispatch;
}
return cmds;
}
/*!
* \internal
* \brief Create a generic pacemaker-attrd operation
*
* \param[in] user_name If not NULL, ACL user to set for operation
*
* \return XML of pacemaker-attrd operation
*/
static xmlNode *
create_attrd_op(const char *user_name)
{
xmlNode *attrd_op = pcmk__xe_create(NULL, __func__);
crm_xml_add(attrd_op, PCMK__XA_T, PCMK__VALUE_ATTRD);
crm_xml_add(attrd_op, PCMK__XA_SRC, pcmk__s(crm_system_name, "unknown"));
crm_xml_add(attrd_op, PCMK__XA_ATTR_USER, user_name);
return attrd_op;
}
static int
connect_and_send_attrd_request(pcmk_ipc_api_t *api, const xmlNode *request)
{
int rc = pcmk_rc_ok;
bool created_api = false;
if (api == NULL) {
rc = pcmk_new_ipc_api(&api, pcmk_ipc_attrd);
if (rc != pcmk_rc_ok) {
return rc;
}
created_api = true;
}
rc = pcmk__connect_ipc(api, pcmk_ipc_dispatch_sync, 5);
if (rc == pcmk_rc_ok) {
rc = pcmk__send_ipc_request(api, request);
}
if (created_api) {
pcmk_free_ipc_api(api);
}
return rc;
}
int
pcmk__attrd_api_clear_failures(pcmk_ipc_api_t *api, const char *node,
const char *resource, const char *operation,
const char *interval_spec, const char *user_name,
uint32_t options)
{
int rc = pcmk_rc_ok;
xmlNode *request = create_attrd_op(user_name);
const char *interval_desc = NULL;
const char *op_desc = NULL;
const char *target = pcmk__node_attr_target(node);
if (target != NULL) {
node = target;
}
if (operation) {
interval_desc = pcmk__s(interval_spec, "nonrecurring");
op_desc = operation;
} else {
interval_desc = "all";
op_desc = "operations";
}
crm_debug("Asking %s to clear failure of %s %s for %s on %s",
pcmk_ipc_name(api, true), interval_desc, op_desc,
pcmk__s(resource, "all resources"), pcmk__s(node, "all nodes"));
crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_CLEAR_FAILURE);
pcmk__xe_add_node(request, node, 0);
crm_xml_add(request, PCMK__XA_ATTR_RESOURCE, resource);
crm_xml_add(request, PCMK__XA_ATTR_CLEAR_OPERATION, operation);
crm_xml_add(request, PCMK__XA_ATTR_CLEAR_INTERVAL, interval_spec);
crm_xml_add_int(request, PCMK__XA_ATTR_IS_REMOTE,
pcmk_is_set(options, pcmk__node_attr_remote));
rc = connect_and_send_attrd_request(api, request);
pcmk__xml_free(request);
return rc;
}
int
pcmk__attrd_api_delete(pcmk_ipc_api_t *api, const char *node, const char *name,
uint32_t options)
{
const char *target = NULL;
if (name == NULL) {
return EINVAL;
}
target = pcmk__node_attr_target(node);
if (target != NULL) {
node = target;
}
/* Make sure the right update option is set. */
options &= ~pcmk__node_attr_delay;
options |= pcmk__node_attr_value;
return pcmk__attrd_api_update(api, node, name, NULL, NULL, NULL, NULL, options);
}
int
pcmk__attrd_api_purge(pcmk_ipc_api_t *api, const char *node, bool reap)
{
int rc = pcmk_rc_ok;
xmlNode *request = NULL;
const char *target = pcmk__node_attr_target(node);
if (target != NULL) {
node = target;
}
crm_debug("Asking %s to purge transient attributes%s for %s",
pcmk_ipc_name(api, true),
(reap? " and node cache entries" : ""),
pcmk__s(node, "local node"));
request = create_attrd_op(NULL);
crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE);
pcmk__xe_set_bool_attr(request, PCMK__XA_REAP, reap);
pcmk__xe_add_node(request, node, 0);
rc = connect_and_send_attrd_request(api, request);
pcmk__xml_free(request);
return rc;
}
int
pcmk__attrd_api_query(pcmk_ipc_api_t *api, const char *node, const char *name,
uint32_t options)
{
int rc = pcmk_rc_ok;
xmlNode *request = NULL;
const char *target = NULL;
if (name == NULL) {
return EINVAL;
}
if (pcmk_is_set(options, pcmk__node_attr_query_all)) {
node = NULL;
} else {
target = pcmk__node_attr_target(node);
if (target != NULL) {
node = target;
}
}
crm_debug("Querying %s for value of '%s'%s%s",
pcmk_ipc_name(api, true), name,
((node == NULL)? "" : " on "), pcmk__s(node, ""));
request = create_attrd_op(NULL);
crm_xml_add(request, PCMK__XA_ATTR_NAME, name);
crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_QUERY);
pcmk__xe_add_node(request, node, 0);
rc = connect_and_send_attrd_request(api, request);
pcmk__xml_free(request);
return rc;
}
int
pcmk__attrd_api_refresh(pcmk_ipc_api_t *api, const char *node)
{
int rc = pcmk_rc_ok;
xmlNode *request = NULL;
const char *target = pcmk__node_attr_target(node);
if (target != NULL) {
node = target;
}
crm_debug("Asking %s to write all transient attributes for %s to CIB",
pcmk_ipc_name(api, true), pcmk__s(node, "local node"));
request = create_attrd_op(NULL);
crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_REFRESH);
pcmk__xe_add_node(request, node, 0);
rc = connect_and_send_attrd_request(api, request);
pcmk__xml_free(request);
return rc;
}
static void
add_op_attr(xmlNode *op, uint32_t options)
{
if (pcmk_all_flags_set(options, pcmk__node_attr_value | pcmk__node_attr_delay)) {
crm_xml_add(op, PCMK_XA_TASK, PCMK__ATTRD_CMD_UPDATE_BOTH);
} else if (pcmk_is_set(options, pcmk__node_attr_value)) {
crm_xml_add(op, PCMK_XA_TASK, PCMK__ATTRD_CMD_UPDATE);
} else if (pcmk_is_set(options, pcmk__node_attr_delay)) {
crm_xml_add(op, PCMK_XA_TASK, PCMK__ATTRD_CMD_UPDATE_DELAY);
}
}
static void
populate_update_op(xmlNode *op, const char *node, const char *name, const char *value,
const char *dampen, const char *set, uint32_t options)
{
if (pcmk_is_set(options, pcmk__node_attr_pattern)) {
crm_xml_add(op, PCMK__XA_ATTR_REGEX, name);
} else {
crm_xml_add(op, PCMK__XA_ATTR_NAME, name);
}
if (pcmk_is_set(options, pcmk__node_attr_utilization)) {
crm_xml_add(op, PCMK__XA_ATTR_SET_TYPE, PCMK_XE_UTILIZATION);
} else {
crm_xml_add(op, PCMK__XA_ATTR_SET_TYPE, PCMK_XE_INSTANCE_ATTRIBUTES);
}
add_op_attr(op, options);
crm_xml_add(op, PCMK__XA_ATTR_VALUE, value);
crm_xml_add(op, PCMK__XA_ATTR_DAMPENING, dampen);
pcmk__xe_add_node(op, node, 0);
crm_xml_add(op, PCMK__XA_ATTR_SET, set);
crm_xml_add_int(op, PCMK__XA_ATTR_IS_REMOTE,
pcmk_is_set(options, pcmk__node_attr_remote));
crm_xml_add_int(op, PCMK__XA_ATTR_IS_PRIVATE,
pcmk_is_set(options, pcmk__node_attr_private));
if (pcmk_is_set(options, pcmk__node_attr_sync_local)) {
crm_xml_add(op, PCMK__XA_ATTR_SYNC_POINT, PCMK__VALUE_LOCAL);
} else if (pcmk_is_set(options, pcmk__node_attr_sync_cluster)) {
crm_xml_add(op, PCMK__XA_ATTR_SYNC_POINT, PCMK__VALUE_CLUSTER);
}
}
int
pcmk__attrd_api_update(pcmk_ipc_api_t *api, const char *node, const char *name,
const char *value, const char *dampen, const char *set,
const char *user_name, uint32_t options)
{
int rc = pcmk_rc_ok;
xmlNode *request = NULL;
const char *target = NULL;
if (name == NULL) {
return EINVAL;
}
target = pcmk__node_attr_target(node);
if (target != NULL) {
node = target;
}
crm_debug("Asking %s to update '%s' to '%s' for %s",
pcmk_ipc_name(api, true), name, pcmk__s(value, "(null)"),
pcmk__s(node, "local node"));
request = create_attrd_op(user_name);
populate_update_op(request, node, name, value, dampen, set, options);
rc = connect_and_send_attrd_request(api, request);
pcmk__xml_free(request);
return rc;
}
int
pcmk__attrd_api_update_list(pcmk_ipc_api_t *api, GList *attrs, const char *dampen,
const char *set, const char *user_name,
uint32_t options)
{
int rc = pcmk_rc_ok;
xmlNode *request = NULL;
if (attrs == NULL) {
return EINVAL;
}
/* There are two different ways of handling a list of attributes:
*
* (1) For messages originating from some command line tool, we have to send
* them one at a time. In this loop, we just call pcmk__attrd_api_update
* for each, letting it deal with creating the API object if it doesn't
* already exist.
*
* The reason we can't use a single message in this case is that we can't
* trust that the server supports it. Remote nodes could be involved
* here, and there's no guarantee that a newer client running on a remote
* node is talking to (or proxied through) a cluster node with a newer
* attrd. We also can't just try sending a single message and then falling
* back on multiple. There's no handshake with the attrd server to
* determine its version. And then we would need to do that fallback in the
* dispatch function for this to work for all connection types (mainloop in
* particular), and at that point we won't know what the original message
* was in order to break it apart and resend as individual messages.
*
* (2) For messages between daemons, we can be assured that the local attrd
* will support the new message and that it can send to the other attrds
* as one request or split up according to the minimum supported version.
*/
for (GList *iter = attrs; iter != NULL; iter = iter->next) {
pcmk__attrd_query_pair_t *pair = (pcmk__attrd_query_pair_t *) iter->data;
if (pcmk__is_daemon) {
const char *target = NULL;
xmlNode *child = NULL;
/* First time through this loop - create the basic request. */
if (request == NULL) {
request = create_attrd_op(user_name);
add_op_attr(request, options);
}
/* Add a child node for this operation. We add the task to the top
* level XML node so attrd_ipc_dispatch doesn't need changes. And
* then we also add the task to each child node in populate_update_op
* so attrd_client_update knows what form of update is taking place.
*/
child = pcmk__xe_create(request, PCMK_XE_OP);
target = pcmk__node_attr_target(pair->node);
if (target != NULL) {
pair->node = target;
}
populate_update_op(child, pair->node, pair->name, pair->value, dampen,
set, options);
} else {
rc = pcmk__attrd_api_update(api, pair->node, pair->name, pair->value,
dampen, set, user_name, options);
}
}
/* If we were doing multiple attributes at once, we still need to send the
* request. Do that now, creating and destroying the API object if needed.
*/
if (pcmk__is_daemon) {
rc = connect_and_send_attrd_request(api, request);
pcmk__xml_free(request);
}
return rc;
}
diff --git a/lib/common/ipc_client.c b/lib/common/ipc_client.c
index a0a78ff09a..3fd1e69b37 100644
--- a/lib/common/ipc_client.c
+++ b/lib/common/ipc_client.c
@@ -1,1694 +1,1689 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
#if defined(HAVE_UCRED) || defined(HAVE_SOCKPEERCRED)
-# ifdef HAVE_UCRED
-# ifndef _GNU_SOURCE
-# define _GNU_SOURCE
-# endif
-# endif
-# include
+#include
#elif defined(HAVE_GETPEERUCRED)
-# include
+#include
#endif
#include
#include
#include
#include
#include /* indirectly: pcmk_err_generic */
#include
#include
#include
#include "crmcommon_private.h"
static int is_ipc_provider_expected(qb_ipcc_connection_t *qb_ipc, int sock,
uid_t refuid, gid_t refgid, pid_t *gotpid,
uid_t *gotuid, gid_t *gotgid);
/*!
* \brief Create a new object for using Pacemaker daemon IPC
*
* \param[out] api Where to store new IPC object
* \param[in] server Which Pacemaker daemon the object is for
*
* \return Standard Pacemaker result code
*
* \note The caller is responsible for freeing *api using pcmk_free_ipc_api().
* \note This is intended to supersede crm_ipc_new() but currently only
* supports the controller, pacemakerd, and schedulerd IPC API.
*/
int
pcmk_new_ipc_api(pcmk_ipc_api_t **api, enum pcmk_ipc_server server)
{
if (api == NULL) {
return EINVAL;
}
*api = calloc(1, sizeof(pcmk_ipc_api_t));
if (*api == NULL) {
return errno;
}
(*api)->server = server;
if (pcmk_ipc_name(*api, false) == NULL) {
pcmk_free_ipc_api(*api);
*api = NULL;
return EOPNOTSUPP;
}
(*api)->ipc_size_max = 0;
// Set server methods and max_size (if not default)
switch (server) {
case pcmk_ipc_attrd:
(*api)->cmds = pcmk__attrd_api_methods();
break;
case pcmk_ipc_based:
(*api)->ipc_size_max = 512 * 1024; // 512KB
break;
case pcmk_ipc_controld:
(*api)->cmds = pcmk__controld_api_methods();
break;
case pcmk_ipc_execd:
break;
case pcmk_ipc_fenced:
break;
case pcmk_ipc_pacemakerd:
(*api)->cmds = pcmk__pacemakerd_api_methods();
break;
case pcmk_ipc_schedulerd:
(*api)->cmds = pcmk__schedulerd_api_methods();
// @TODO max_size could vary by client, maybe take as argument?
(*api)->ipc_size_max = 5 * 1024 * 1024; // 5MB
break;
}
if ((*api)->cmds == NULL) {
pcmk_free_ipc_api(*api);
*api = NULL;
return ENOMEM;
}
(*api)->ipc = crm_ipc_new(pcmk_ipc_name(*api, false),
(*api)->ipc_size_max);
if ((*api)->ipc == NULL) {
pcmk_free_ipc_api(*api);
*api = NULL;
return ENOMEM;
}
// If daemon API has its own data to track, allocate it
if ((*api)->cmds->new_data != NULL) {
if ((*api)->cmds->new_data(*api) != pcmk_rc_ok) {
pcmk_free_ipc_api(*api);
*api = NULL;
return ENOMEM;
}
}
crm_trace("Created %s API IPC object", pcmk_ipc_name(*api, true));
return pcmk_rc_ok;
}
static void
free_daemon_specific_data(pcmk_ipc_api_t *api)
{
if ((api != NULL) && (api->cmds != NULL)) {
if ((api->cmds->free_data != NULL) && (api->api_data != NULL)) {
api->cmds->free_data(api->api_data);
api->api_data = NULL;
}
free(api->cmds);
api->cmds = NULL;
}
}
/*!
* \internal
* \brief Call an IPC API event callback, if one is registed
*
* \param[in,out] api IPC API connection
* \param[in] event_type The type of event that occurred
* \param[in] status Event status
* \param[in,out] event_data Event-specific data
*/
void
pcmk__call_ipc_callback(pcmk_ipc_api_t *api, enum pcmk_ipc_event event_type,
crm_exit_t status, void *event_data)
{
if ((api != NULL) && (api->cb != NULL)) {
api->cb(api, event_type, status, event_data, api->user_data);
}
}
/*!
* \internal
* \brief Clean up after an IPC disconnect
*
* \param[in,out] user_data IPC API connection that disconnected
*
* \note This function can be used as a main loop IPC destroy callback.
*/
static void
ipc_post_disconnect(gpointer user_data)
{
pcmk_ipc_api_t *api = user_data;
crm_info("Disconnected from %s", pcmk_ipc_name(api, true));
// Perform any daemon-specific handling needed
if ((api->cmds != NULL) && (api->cmds->post_disconnect != NULL)) {
api->cmds->post_disconnect(api);
}
// Call client's registered event callback
pcmk__call_ipc_callback(api, pcmk_ipc_event_disconnect, CRM_EX_DISCONNECT,
NULL);
/* If this is being called from a running main loop, mainloop_gio_destroy()
* will free ipc and mainloop_io immediately after calling this function.
* If this is called from a stopped main loop, these will leak, so the best
* practice is to close the connection before stopping the main loop.
*/
api->ipc = NULL;
api->mainloop_io = NULL;
if (api->free_on_disconnect) {
/* pcmk_free_ipc_api() has already been called, but did not free api
* or api->cmds because this function needed them. Do that now.
*/
free_daemon_specific_data(api);
crm_trace("Freeing IPC API object after disconnect");
free(api);
}
}
/*!
* \brief Free the contents of an IPC API object
*
* \param[in,out] api IPC API object to free
*/
void
pcmk_free_ipc_api(pcmk_ipc_api_t *api)
{
bool free_on_disconnect = false;
if (api == NULL) {
return;
}
crm_debug("Releasing %s IPC API", pcmk_ipc_name(api, true));
if (api->ipc != NULL) {
if (api->mainloop_io != NULL) {
/* We need to keep the api pointer itself around, because it is the
* user data for the IPC client destroy callback. That will be
* triggered by the pcmk_disconnect_ipc() call below, but it might
* happen later in the main loop (if still running).
*
* This flag tells the destroy callback to free the object. It can't
* do that unconditionally, because the application might call this
* function after a disconnect that happened by other means.
*/
free_on_disconnect = api->free_on_disconnect = true;
}
pcmk_disconnect_ipc(api); // Frees api if free_on_disconnect is true
}
if (!free_on_disconnect) {
free_daemon_specific_data(api);
crm_trace("Freeing IPC API object");
free(api);
}
}
/*!
* \brief Get the IPC name used with an IPC API connection
*
* \param[in] api IPC API connection
* \param[in] for_log If true, return human-friendly name instead of IPC name
*
* \return IPC API's human-friendly or connection name, or if none is available,
* "Pacemaker" if for_log is true and NULL if for_log is false
*/
const char *
pcmk_ipc_name(const pcmk_ipc_api_t *api, bool for_log)
{
if (api == NULL) {
return for_log? "Pacemaker" : NULL;
}
switch (api->server) {
case pcmk_ipc_attrd:
return for_log? "attribute manager" : PCMK__VALUE_ATTRD;
case pcmk_ipc_based:
return for_log? "CIB manager" : NULL /* PCMK__SERVER_BASED_RW */;
case pcmk_ipc_controld:
return for_log? "controller" : CRM_SYSTEM_CRMD;
case pcmk_ipc_execd:
return for_log? "executor" : NULL /* CRM_SYSTEM_LRMD */;
case pcmk_ipc_fenced:
return for_log? "fencer" : NULL /* "stonith-ng" */;
case pcmk_ipc_pacemakerd:
return for_log? "launcher" : CRM_SYSTEM_MCP;
case pcmk_ipc_schedulerd:
return for_log? "scheduler" : CRM_SYSTEM_PENGINE;
default:
return for_log? "Pacemaker" : NULL;
}
}
/*!
* \brief Check whether an IPC API connection is active
*
* \param[in,out] api IPC API connection
*
* \return true if IPC is connected, false otherwise
*/
bool
pcmk_ipc_is_connected(pcmk_ipc_api_t *api)
{
return (api != NULL) && crm_ipc_connected(api->ipc);
}
/*!
* \internal
* \brief Call the daemon-specific API's dispatch function
*
* Perform daemon-specific handling of IPC reply dispatch. It is the daemon
* method's responsibility to call the client's registered event callback, as
* well as allocate and free any event data.
*
* \param[in,out] api IPC API connection
* \param[in,out] message IPC reply XML to dispatch
*/
static bool
call_api_dispatch(pcmk_ipc_api_t *api, xmlNode *message)
{
crm_log_xml_trace(message, "ipc-received");
if ((api->cmds != NULL) && (api->cmds->dispatch != NULL)) {
return api->cmds->dispatch(api, message);
}
return false;
}
/*!
* \internal
* \brief Dispatch previously read IPC data
*
* \param[in] buffer Data read from IPC
* \param[in,out] api IPC object
*
* \return Standard Pacemaker return code. In particular:
*
* pcmk_rc_ok: There are no more messages expected from the server. Quit
* reading.
* EINPROGRESS: There are more messages expected from the server. Keep reading.
*
* All other values indicate an error.
*/
static int
dispatch_ipc_data(const char *buffer, pcmk_ipc_api_t *api)
{
bool more = false;
xmlNode *msg;
if (buffer == NULL) {
crm_warn("Empty message received from %s IPC",
pcmk_ipc_name(api, true));
return ENOMSG;
}
msg = pcmk__xml_parse(buffer);
if (msg == NULL) {
crm_warn("Malformed message received from %s IPC",
pcmk_ipc_name(api, true));
return EPROTO;
}
more = call_api_dispatch(api, msg);
pcmk__xml_free(msg);
if (more) {
return EINPROGRESS;
} else {
return pcmk_rc_ok;
}
}
/*!
* \internal
* \brief Dispatch data read from IPC source
*
* \param[in] buffer Data read from IPC
* \param[in] length Number of bytes of data in buffer (ignored)
* \param[in,out] user_data IPC object
*
* \return Always 0 (meaning connection is still required)
*
* \note This function can be used as a main loop IPC dispatch callback.
*/
static int
dispatch_ipc_source_data(const char *buffer, ssize_t length, gpointer user_data)
{
pcmk_ipc_api_t *api = user_data;
CRM_CHECK(api != NULL, return 0);
dispatch_ipc_data(buffer, api);
return 0;
}
/*!
* \brief Check whether an IPC connection has data available (without main loop)
*
* \param[in] api IPC API connection
* \param[in] timeout_ms If less than 0, poll indefinitely; if 0, poll once
* and return immediately; otherwise, poll for up to
* this many milliseconds
*
* \return Standard Pacemaker return code
*
* \note Callers of pcmk_connect_ipc() using pcmk_ipc_dispatch_poll should call
* this function to check whether IPC data is available. Return values of
* interest include pcmk_rc_ok meaning data is available, and EAGAIN
* meaning no data is available; all other values indicate errors.
* \todo This does not allow the caller to poll multiple file descriptors at
* once. If there is demand for that, we could add a wrapper for
* pcmk__ipc_fd(api->ipc), so the caller can call poll() themselves.
*/
int
pcmk_poll_ipc(const pcmk_ipc_api_t *api, int timeout_ms)
{
int rc;
struct pollfd pollfd = { 0, };
if ((api == NULL) || (api->dispatch_type != pcmk_ipc_dispatch_poll)) {
return EINVAL;
}
rc = pcmk__ipc_fd(api->ipc, &(pollfd.fd));
if (rc != pcmk_rc_ok) {
crm_debug("Could not obtain file descriptor for %s IPC: %s",
pcmk_ipc_name(api, true), pcmk_rc_str(rc));
return rc;
}
pollfd.events = POLLIN;
rc = poll(&pollfd, 1, timeout_ms);
if (rc < 0) {
/* Some UNIX systems return negative and set EAGAIN for failure to
* allocate memory; standardize the return code in that case
*/
return (errno == EAGAIN)? ENOMEM : errno;
} else if (rc == 0) {
return EAGAIN;
}
return pcmk_rc_ok;
}
/*!
* \brief Dispatch available messages on an IPC connection (without main loop)
*
* \param[in,out] api IPC API connection
*
* \return Standard Pacemaker return code
*
* \note Callers of pcmk_connect_ipc() using pcmk_ipc_dispatch_poll should call
* this function when IPC data is available.
*/
void
pcmk_dispatch_ipc(pcmk_ipc_api_t *api)
{
if (api == NULL) {
return;
}
while (crm_ipc_ready(api->ipc) > 0) {
if (crm_ipc_read(api->ipc) > 0) {
dispatch_ipc_data(crm_ipc_buffer(api->ipc), api);
}
}
}
// \return Standard Pacemaker return code
static int
connect_with_main_loop(pcmk_ipc_api_t *api)
{
int rc;
struct ipc_client_callbacks callbacks = {
.dispatch = dispatch_ipc_source_data,
.destroy = ipc_post_disconnect,
};
rc = pcmk__add_mainloop_ipc(api->ipc, G_PRIORITY_DEFAULT, api,
&callbacks, &(api->mainloop_io));
if (rc != pcmk_rc_ok) {
return rc;
}
crm_debug("Connected to %s IPC (attached to main loop)",
pcmk_ipc_name(api, true));
/* After this point, api->mainloop_io owns api->ipc, so api->ipc
* should not be explicitly freed.
*/
return pcmk_rc_ok;
}
// \return Standard Pacemaker return code
static int
connect_without_main_loop(pcmk_ipc_api_t *api)
{
int rc = pcmk__connect_generic_ipc(api->ipc);
if (rc != pcmk_rc_ok) {
crm_ipc_close(api->ipc);
} else {
crm_debug("Connected to %s IPC (without main loop)",
pcmk_ipc_name(api, true));
}
return rc;
}
/*!
* \internal
* \brief Connect to a Pacemaker daemon via IPC (retrying after soft errors)
*
* \param[in,out] api IPC API instance
* \param[in] dispatch_type How IPC replies should be dispatched
* \param[in] attempts How many times to try (in case of soft error)
*
* \return Standard Pacemaker return code
*/
int
pcmk__connect_ipc(pcmk_ipc_api_t *api, enum pcmk_ipc_dispatch dispatch_type,
int attempts)
{
int rc = pcmk_rc_ok;
if ((api == NULL) || (attempts < 1)) {
return EINVAL;
}
if (api->ipc == NULL) {
api->ipc = crm_ipc_new(pcmk_ipc_name(api, false), api->ipc_size_max);
if (api->ipc == NULL) {
return ENOMEM;
}
}
if (crm_ipc_connected(api->ipc)) {
crm_trace("Already connected to %s", pcmk_ipc_name(api, true));
return pcmk_rc_ok;
}
api->dispatch_type = dispatch_type;
crm_debug("Attempting connection to %s (up to %d time%s)",
pcmk_ipc_name(api, true), attempts, pcmk__plural_s(attempts));
for (int remaining = attempts - 1; remaining >= 0; --remaining) {
switch (dispatch_type) {
case pcmk_ipc_dispatch_main:
rc = connect_with_main_loop(api);
break;
case pcmk_ipc_dispatch_sync:
case pcmk_ipc_dispatch_poll:
rc = connect_without_main_loop(api);
break;
}
if ((remaining == 0) || ((rc != EAGAIN) && (rc != EALREADY))) {
break; // Result is final
}
// Retry after soft error (interrupted by signal, etc.)
pcmk__sleep_ms((attempts - remaining) * 500);
crm_debug("Re-attempting connection to %s (%d attempt%s remaining)",
pcmk_ipc_name(api, true), remaining,
pcmk__plural_s(remaining));
}
if (rc != pcmk_rc_ok) {
return rc;
}
if ((api->cmds != NULL) && (api->cmds->post_connect != NULL)) {
rc = api->cmds->post_connect(api);
if (rc != pcmk_rc_ok) {
crm_ipc_close(api->ipc);
}
}
return rc;
}
/*!
* \brief Connect to a Pacemaker daemon via IPC
*
* \param[in,out] api IPC API instance
* \param[in] dispatch_type How IPC replies should be dispatched
*
* \return Standard Pacemaker return code
*/
int
pcmk_connect_ipc(pcmk_ipc_api_t *api, enum pcmk_ipc_dispatch dispatch_type)
{
int rc = pcmk__connect_ipc(api, dispatch_type, 2);
if (rc != pcmk_rc_ok) {
crm_err("Connection to %s failed: %s",
pcmk_ipc_name(api, true), pcmk_rc_str(rc));
}
return rc;
}
/*!
* \brief Disconnect an IPC API instance
*
* \param[in,out] api IPC API connection
*
* \return Standard Pacemaker return code
*
* \note If the connection is attached to a main loop, this function should be
* called before quitting the main loop, to ensure that all memory is
* freed.
*/
void
pcmk_disconnect_ipc(pcmk_ipc_api_t *api)
{
if ((api == NULL) || (api->ipc == NULL)) {
return;
}
switch (api->dispatch_type) {
case pcmk_ipc_dispatch_main:
{
mainloop_io_t *mainloop_io = api->mainloop_io;
// Make sure no code with access to api can use these again
api->mainloop_io = NULL;
api->ipc = NULL;
mainloop_del_ipc_client(mainloop_io);
// After this point api might have already been freed
}
break;
case pcmk_ipc_dispatch_poll:
case pcmk_ipc_dispatch_sync:
{
crm_ipc_t *ipc = api->ipc;
// Make sure no code with access to api can use ipc again
api->ipc = NULL;
// This should always be the case already, but to be safe
api->free_on_disconnect = false;
crm_ipc_close(ipc);
crm_ipc_destroy(ipc);
ipc_post_disconnect(api);
}
break;
}
}
/*!
* \brief Register a callback for IPC API events
*
* \param[in,out] api IPC API connection
* \param[in] callback Callback to register
* \param[in] userdata Caller data to pass to callback
*
* \note This function may be called multiple times to update the callback
* and/or user data. The caller remains responsible for freeing
* userdata in any case (after the IPC is disconnected, if the
* user data is still registered with the IPC).
*/
void
pcmk_register_ipc_callback(pcmk_ipc_api_t *api, pcmk_ipc_callback_t cb,
void *user_data)
{
if (api == NULL) {
return;
}
api->cb = cb;
api->user_data = user_data;
}
/*!
* \internal
* \brief Send an XML request across an IPC API connection
*
* \param[in,out] api IPC API connection
* \param[in] request XML request to send
*
* \return Standard Pacemaker return code
*
* \note Daemon-specific IPC API functions should call this function to send
* requests, because it handles different dispatch types appropriately.
*/
int
pcmk__send_ipc_request(pcmk_ipc_api_t *api, const xmlNode *request)
{
int rc;
xmlNode *reply = NULL;
enum crm_ipc_flags flags = crm_ipc_flags_none;
if ((api == NULL) || (api->ipc == NULL) || (request == NULL)) {
return EINVAL;
}
crm_log_xml_trace(request, "ipc-sent");
// Synchronous dispatch requires waiting for a reply
if ((api->dispatch_type == pcmk_ipc_dispatch_sync)
&& (api->cmds != NULL)
&& (api->cmds->reply_expected != NULL)
&& (api->cmds->reply_expected(api, request))) {
flags = crm_ipc_client_response;
}
// The 0 here means a default timeout of 5 seconds
rc = crm_ipc_send(api->ipc, request, flags, 0, &reply);
if (rc < 0) {
return pcmk_legacy2rc(rc);
} else if (rc == 0) {
return ENODATA;
}
// With synchronous dispatch, we dispatch any reply now
if (reply != NULL) {
bool more = call_api_dispatch(api, reply);
pcmk__xml_free(reply);
while (more) {
rc = crm_ipc_read(api->ipc);
if (rc == -EAGAIN) {
continue;
} else if (rc == -ENOMSG || rc == pcmk_ok) {
return pcmk_rc_ok;
} else if (rc < 0) {
return -rc;
}
rc = dispatch_ipc_data(crm_ipc_buffer(api->ipc), api);
if (rc == pcmk_rc_ok) {
more = false;
} else if (rc == EINPROGRESS) {
more = true;
} else {
continue;
}
}
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Create the XML for an IPC request to purge a node from the peer cache
*
* \param[in] api IPC API connection
* \param[in] node_name If not NULL, name of node to purge
* \param[in] nodeid If not 0, node ID of node to purge
*
* \return Newly allocated IPC request XML
*
* \note The controller, fencer, and pacemakerd use the same request syntax, but
* the attribute manager uses a different one. The CIB manager doesn't
* have any syntax for it. The executor and scheduler don't connect to the
* cluster layer and thus don't have or need any syntax for it.
*
* \todo Modify the attribute manager to accept the common syntax (as well
* as its current one, for compatibility with older clients). Modify
* the CIB manager to accept and honor the common syntax. Modify the
* executor and scheduler to accept the syntax (immediately returning
* success), just for consistency. Modify this function to use the
* common syntax with all daemons if their version supports it.
*/
static xmlNode *
create_purge_node_request(const pcmk_ipc_api_t *api, const char *node_name,
uint32_t nodeid)
{
xmlNode *request = NULL;
const char *client = crm_system_name? crm_system_name : "client";
switch (api->server) {
case pcmk_ipc_attrd:
request = pcmk__xe_create(NULL, __func__);
crm_xml_add(request, PCMK__XA_T, PCMK__VALUE_ATTRD);
crm_xml_add(request, PCMK__XA_SRC, crm_system_name);
crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE);
pcmk__xe_set_bool_attr(request, PCMK__XA_REAP, true);
pcmk__xe_add_node(request, node_name, nodeid);
break;
case pcmk_ipc_controld:
case pcmk_ipc_fenced:
case pcmk_ipc_pacemakerd:
request = create_request(CRM_OP_RM_NODE_CACHE, NULL, NULL,
pcmk_ipc_name(api, false), client, NULL);
if (nodeid > 0) {
pcmk__xe_set_id(request, "%lu", (unsigned long) nodeid);
}
crm_xml_add(request, PCMK_XA_UNAME, node_name);
break;
case pcmk_ipc_based:
case pcmk_ipc_execd:
case pcmk_ipc_schedulerd:
break;
}
return request;
}
/*!
* \brief Ask a Pacemaker daemon to purge a node from its peer cache
*
* \param[in,out] api IPC API connection
* \param[in] node_name If not NULL, name of node to purge
* \param[in] nodeid If not 0, node ID of node to purge
*
* \return Standard Pacemaker return code
*
* \note At least one of node_name or nodeid must be specified.
*/
int
pcmk_ipc_purge_node(pcmk_ipc_api_t *api, const char *node_name, uint32_t nodeid)
{
int rc = 0;
xmlNode *request = NULL;
if (api == NULL) {
return EINVAL;
}
if ((node_name == NULL) && (nodeid == 0)) {
return EINVAL;
}
request = create_purge_node_request(api, node_name, nodeid);
if (request == NULL) {
return EOPNOTSUPP;
}
rc = pcmk__send_ipc_request(api, request);
pcmk__xml_free(request);
crm_debug("%s peer cache purge of node %s[%lu]: rc=%d",
pcmk_ipc_name(api, true), node_name, (unsigned long) nodeid, rc);
return rc;
}
/*
* Generic IPC API (to eventually be deprecated as public API and made internal)
*/
struct crm_ipc_s {
struct pollfd pfd;
unsigned int max_buf_size; // maximum bytes we can send or receive over IPC
unsigned int buf_size; // size of allocated buffer
int msg_size;
int need_reply;
char *buffer;
char *server_name; // server IPC name being connected to
qb_ipcc_connection_t *ipc;
};
/*!
* \brief Create a new (legacy) object for using Pacemaker daemon IPC
*
* \param[in] name IPC system name to connect to
* \param[in] max_size Use a maximum IPC buffer size of at least this size
*
* \return Newly allocated IPC object on success, NULL otherwise
*
* \note The caller is responsible for freeing the result using
* crm_ipc_destroy().
* \note This should be considered deprecated for use with daemons supported by
* pcmk_new_ipc_api().
*/
crm_ipc_t *
crm_ipc_new(const char *name, size_t max_size)
{
crm_ipc_t *client = NULL;
client = calloc(1, sizeof(crm_ipc_t));
if (client == NULL) {
crm_err("Could not create IPC connection: %s", strerror(errno));
return NULL;
}
client->server_name = strdup(name);
if (client->server_name == NULL) {
crm_err("Could not create %s IPC connection: %s",
name, strerror(errno));
free(client);
return NULL;
}
client->buf_size = pcmk__ipc_buffer_size(max_size);
client->buffer = malloc(client->buf_size);
if (client->buffer == NULL) {
crm_err("Could not create %s IPC connection: %s",
name, strerror(errno));
free(client->server_name);
free(client);
return NULL;
}
/* Clients initiating connection pick the max buf size */
client->max_buf_size = client->buf_size;
client->pfd.fd = -1;
client->pfd.events = POLLIN;
client->pfd.revents = 0;
return client;
}
/*!
* \internal
* \brief Connect a generic (not daemon-specific) IPC object
*
* \param[in,out] ipc Generic IPC object to connect
*
* \return Standard Pacemaker return code
*/
int
pcmk__connect_generic_ipc(crm_ipc_t *ipc)
{
uid_t cl_uid = 0;
gid_t cl_gid = 0;
pid_t found_pid = 0;
uid_t found_uid = 0;
gid_t found_gid = 0;
int rc = pcmk_rc_ok;
if (ipc == NULL) {
return EINVAL;
}
ipc->need_reply = FALSE;
ipc->ipc = qb_ipcc_connect(ipc->server_name, ipc->buf_size);
if (ipc->ipc == NULL) {
return errno;
}
rc = qb_ipcc_fd_get(ipc->ipc, &ipc->pfd.fd);
if (rc < 0) { // -errno
crm_ipc_close(ipc);
return -rc;
}
rc = pcmk_daemon_user(&cl_uid, &cl_gid);
rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
crm_ipc_close(ipc);
return rc;
}
rc = is_ipc_provider_expected(ipc->ipc, ipc->pfd.fd, cl_uid, cl_gid,
&found_pid, &found_uid, &found_gid);
if (rc != pcmk_rc_ok) {
if (rc == pcmk_rc_ipc_unauthorized) {
crm_info("%s IPC provider authentication failed: process %lld has "
"uid %lld (expected %lld) and gid %lld (expected %lld)",
ipc->server_name,
(long long) PCMK__SPECIAL_PID_AS_0(found_pid),
(long long) found_uid, (long long) cl_uid,
(long long) found_gid, (long long) cl_gid);
}
crm_ipc_close(ipc);
return rc;
}
ipc->max_buf_size = qb_ipcc_get_buffer_size(ipc->ipc);
if (ipc->max_buf_size > ipc->buf_size) {
free(ipc->buffer);
ipc->buffer = calloc(ipc->max_buf_size, sizeof(char));
if (ipc->buffer == NULL) {
rc = errno;
crm_ipc_close(ipc);
return rc;
}
ipc->buf_size = ipc->max_buf_size;
}
return pcmk_rc_ok;
}
/*!
* \brief Establish an IPC connection to a Pacemaker component
*
* \param[in,out] client Connection instance obtained from crm_ipc_new()
*
* \return true on success, false otherwise (in which case errno will be set;
* specifically, in case of discovering the remote side is not
* authentic, its value is set to ECONNABORTED).
*/
bool
crm_ipc_connect(crm_ipc_t *client)
{
int rc = pcmk__connect_generic_ipc(client);
if (rc == pcmk_rc_ok) {
return true;
}
if ((client != NULL) && (client->ipc == NULL)) {
errno = (rc > 0)? rc : ENOTCONN;
crm_debug("Could not establish %s IPC connection: %s (%d)",
client->server_name, pcmk_rc_str(errno), errno);
} else if (rc == pcmk_rc_ipc_unauthorized) {
crm_err("%s IPC provider authentication failed",
(client == NULL)? "Pacemaker" : client->server_name);
errno = ECONNABORTED;
} else {
crm_perror(LOG_ERR,
"Could not verify authenticity of %s IPC provider",
(client == NULL)? "Pacemaker" : client->server_name);
errno = ENOTCONN;
}
return false;
}
void
crm_ipc_close(crm_ipc_t * client)
{
if (client) {
if (client->ipc) {
qb_ipcc_connection_t *ipc = client->ipc;
client->ipc = NULL;
qb_ipcc_disconnect(ipc);
}
}
}
void
crm_ipc_destroy(crm_ipc_t * client)
{
if (client) {
if (client->ipc && qb_ipcc_is_connected(client->ipc)) {
crm_notice("Destroying active %s IPC connection",
client->server_name);
/* The next line is basically unsafe
*
* If this connection was attached to mainloop and mainloop is active,
* the 'disconnected' callback will end up back here and we'll end
* up free'ing the memory twice - something that can still happen
* even without this if we destroy a connection and it closes before
* we call exit
*/
/* crm_ipc_close(client); */
} else {
crm_trace("Destroying inactive %s IPC connection",
client->server_name);
}
free(client->buffer);
free(client->server_name);
free(client);
}
}
/*!
* \internal
* \brief Get the file descriptor for a generic IPC object
*
* \param[in,out] ipc Generic IPC object to get file descriptor for
* \param[out] fd Where to store file descriptor
*
* \return Standard Pacemaker return code
*/
int
pcmk__ipc_fd(crm_ipc_t *ipc, int *fd)
{
if ((ipc == NULL) || (fd == NULL)) {
return EINVAL;
}
if ((ipc->ipc == NULL) || (ipc->pfd.fd < 0)) {
return ENOTCONN;
}
*fd = ipc->pfd.fd;
return pcmk_rc_ok;
}
int
crm_ipc_get_fd(crm_ipc_t * client)
{
int fd = -1;
if (pcmk__ipc_fd(client, &fd) != pcmk_rc_ok) {
crm_err("Could not obtain file descriptor for %s IPC",
((client == NULL)? "unspecified" : client->server_name));
errno = EINVAL;
return -EINVAL;
}
return fd;
}
bool
crm_ipc_connected(crm_ipc_t * client)
{
bool rc = FALSE;
if (client == NULL) {
crm_trace("No client");
return FALSE;
} else if (client->ipc == NULL) {
crm_trace("No connection");
return FALSE;
} else if (client->pfd.fd < 0) {
crm_trace("Bad descriptor");
return FALSE;
}
rc = qb_ipcc_is_connected(client->ipc);
if (rc == FALSE) {
client->pfd.fd = -EINVAL;
}
return rc;
}
/*!
* \brief Check whether an IPC connection is ready to be read
*
* \param[in,out] client Connection to check
*
* \return Positive value if ready to be read, 0 if not ready, -errno on error
*/
int
crm_ipc_ready(crm_ipc_t *client)
{
int rc;
CRM_ASSERT(client != NULL);
if (!crm_ipc_connected(client)) {
return -ENOTCONN;
}
client->pfd.revents = 0;
rc = poll(&(client->pfd), 1, 0);
return (rc < 0)? -errno : rc;
}
// \return Standard Pacemaker return code
static int
crm_ipc_decompress(crm_ipc_t * client)
{
pcmk__ipc_header_t *header = (pcmk__ipc_header_t *)(void*)client->buffer;
if (header->size_compressed) {
int rc = 0;
unsigned int size_u = 1 + header->size_uncompressed;
/* never let buf size fall below our max size required for ipc reads. */
unsigned int new_buf_size = QB_MAX((sizeof(pcmk__ipc_header_t) + size_u), client->max_buf_size);
char *uncompressed = pcmk__assert_alloc(1, new_buf_size);
crm_trace("Decompressing message data %u bytes into %u bytes",
header->size_compressed, size_u);
rc = BZ2_bzBuffToBuffDecompress(uncompressed + sizeof(pcmk__ipc_header_t), &size_u,
client->buffer + sizeof(pcmk__ipc_header_t), header->size_compressed, 1, 0);
rc = pcmk__bzlib2rc(rc);
if (rc != pcmk_rc_ok) {
crm_err("Decompression failed: %s " QB_XS " rc=%d",
pcmk_rc_str(rc), rc);
free(uncompressed);
return rc;
}
/*
* This assert no longer holds true. For an identical msg, some clients may
* require compression, and others may not. If that same msg (event) is sent
* to multiple clients, it could result in some clients receiving a compressed
* msg even though compression was not explicitly required for them.
*
* CRM_ASSERT((header->size_uncompressed + sizeof(pcmk__ipc_header_t)) >= ipc_buffer_max);
*/
CRM_ASSERT(size_u == header->size_uncompressed);
memcpy(uncompressed, client->buffer, sizeof(pcmk__ipc_header_t)); /* Preserve the header */
header = (pcmk__ipc_header_t *)(void*)uncompressed;
free(client->buffer);
client->buf_size = new_buf_size;
client->buffer = uncompressed;
}
CRM_ASSERT(client->buffer[sizeof(pcmk__ipc_header_t) + header->size_uncompressed - 1] == 0);
return pcmk_rc_ok;
}
long
crm_ipc_read(crm_ipc_t * client)
{
pcmk__ipc_header_t *header = NULL;
CRM_ASSERT(client != NULL);
CRM_ASSERT(client->ipc != NULL);
CRM_ASSERT(client->buffer != NULL);
client->buffer[0] = 0;
client->msg_size = qb_ipcc_event_recv(client->ipc, client->buffer,
client->buf_size, 0);
if (client->msg_size >= 0) {
int rc = crm_ipc_decompress(client);
if (rc != pcmk_rc_ok) {
return pcmk_rc2legacy(rc);
}
header = (pcmk__ipc_header_t *)(void*)client->buffer;
if (!pcmk__valid_ipc_header(header)) {
return -EBADMSG;
}
crm_trace("Received %s IPC event %d size=%u rc=%d text='%.100s'",
client->server_name, header->qb.id, header->qb.size,
client->msg_size,
client->buffer + sizeof(pcmk__ipc_header_t));
} else {
crm_trace("No message received from %s IPC: %s",
client->server_name, pcmk_strerror(client->msg_size));
if (client->msg_size == -EAGAIN) {
return -EAGAIN;
}
}
if (!crm_ipc_connected(client) || client->msg_size == -ENOTCONN) {
crm_err("Connection to %s IPC failed", client->server_name);
}
if (header) {
/* Data excluding the header */
return header->size_uncompressed;
}
return -ENOMSG;
}
const char *
crm_ipc_buffer(crm_ipc_t * client)
{
CRM_ASSERT(client != NULL);
return client->buffer + sizeof(pcmk__ipc_header_t);
}
uint32_t
crm_ipc_buffer_flags(crm_ipc_t * client)
{
pcmk__ipc_header_t *header = NULL;
CRM_ASSERT(client != NULL);
if (client->buffer == NULL) {
return 0;
}
header = (pcmk__ipc_header_t *)(void*)client->buffer;
return header->flags;
}
const char *
crm_ipc_name(crm_ipc_t * client)
{
CRM_ASSERT(client != NULL);
return client->server_name;
}
// \return Standard Pacemaker return code
static int
internal_ipc_get_reply(crm_ipc_t *client, int request_id, int ms_timeout,
ssize_t *bytes)
{
time_t timeout = time(NULL) + 1 + (ms_timeout / 1000);
int rc = pcmk_rc_ok;
/* get the reply */
crm_trace("Waiting on reply to %s IPC message %d",
client->server_name, request_id);
do {
*bytes = qb_ipcc_recv(client->ipc, client->buffer, client->buf_size, 1000);
if (*bytes > 0) {
pcmk__ipc_header_t *hdr = NULL;
rc = crm_ipc_decompress(client);
if (rc != pcmk_rc_ok) {
return rc;
}
hdr = (pcmk__ipc_header_t *)(void*)client->buffer;
if (hdr->qb.id == request_id) {
/* Got it */
break;
} else if (hdr->qb.id < request_id) {
xmlNode *bad = pcmk__xml_parse(crm_ipc_buffer(client));
crm_err("Discarding old reply %d (need %d)", hdr->qb.id, request_id);
crm_log_xml_notice(bad, "OldIpcReply");
} else {
xmlNode *bad = pcmk__xml_parse(crm_ipc_buffer(client));
crm_err("Discarding newer reply %d (need %d)", hdr->qb.id, request_id);
crm_log_xml_notice(bad, "ImpossibleReply");
CRM_ASSERT(hdr->qb.id <= request_id);
}
} else if (!crm_ipc_connected(client)) {
crm_err("%s IPC provider disconnected while waiting for message %d",
client->server_name, request_id);
break;
}
} while (time(NULL) < timeout);
if (*bytes < 0) {
rc = (int) -*bytes; // System errno
}
return rc;
}
/*!
* \brief Send an IPC XML message
*
* \param[in,out] client Connection to IPC server
* \param[in] message XML message to send
* \param[in] flags Bitmask of crm_ipc_flags
* \param[in] ms_timeout Give up if not sent within this much time
* (5 seconds if 0, or no timeout if negative)
* \param[out] reply Reply from server (or NULL if none)
*
* \return Negative errno on error, otherwise size of reply received in bytes
* if reply was needed, otherwise number of bytes sent
*/
int
crm_ipc_send(crm_ipc_t *client, const xmlNode *message,
enum crm_ipc_flags flags, int32_t ms_timeout, xmlNode **reply)
{
int rc = 0;
ssize_t qb_rc = 0;
ssize_t bytes = 0;
struct iovec *iov;
static uint32_t id = 0;
static int factor = 8;
pcmk__ipc_header_t *header;
if (client == NULL) {
crm_notice("Can't send IPC request without connection (bug?): %.100s",
message);
return -ENOTCONN;
} else if (!crm_ipc_connected(client)) {
/* Don't even bother */
crm_notice("Can't send %s IPC requests: Connection closed",
client->server_name);
return -ENOTCONN;
}
if (ms_timeout == 0) {
ms_timeout = 5000;
}
if (client->need_reply) {
qb_rc = qb_ipcc_recv(client->ipc, client->buffer, client->buf_size, ms_timeout);
if (qb_rc < 0) {
crm_warn("Sending %s IPC disabled until pending reply received",
client->server_name);
return -EALREADY;
} else {
crm_notice("Sending %s IPC re-enabled after pending reply received",
client->server_name);
client->need_reply = FALSE;
}
}
id++;
CRM_LOG_ASSERT(id != 0); /* Crude wrap-around detection */
rc = pcmk__ipc_prepare_iov(id, message, client->max_buf_size, &iov, &bytes);
if (rc != pcmk_rc_ok) {
crm_warn("Couldn't prepare %s IPC request: %s " QB_XS " rc=%d",
client->server_name, pcmk_rc_str(rc), rc);
return pcmk_rc2legacy(rc);
}
header = iov[0].iov_base;
pcmk__set_ipc_flags(header->flags, client->server_name, flags);
if (pcmk_is_set(flags, crm_ipc_proxied)) {
/* Don't look for a synchronous response */
pcmk__clear_ipc_flags(flags, "client", crm_ipc_client_response);
}
if(header->size_compressed) {
if(factor < 10 && (client->max_buf_size / 10) < (bytes / factor)) {
crm_notice("Compressed message exceeds %d0%% of configured IPC "
"limit (%u bytes); consider setting PCMK_ipc_buffer to "
"%u or higher",
factor, client->max_buf_size, 2 * client->max_buf_size);
factor++;
}
}
crm_trace("Sending %s IPC request %d of %u bytes using %dms timeout",
client->server_name, header->qb.id, header->qb.size, ms_timeout);
if ((ms_timeout > 0) || !pcmk_is_set(flags, crm_ipc_client_response)) {
time_t timeout = time(NULL) + 1 + (ms_timeout / 1000);
do {
/* @TODO Is this check really needed? Won't qb_ipcc_sendv() return
* an error if it's not connected?
*/
if (!crm_ipc_connected(client)) {
goto send_cleanup;
}
qb_rc = qb_ipcc_sendv(client->ipc, iov, 2);
} while ((qb_rc == -EAGAIN) && (time(NULL) < timeout));
rc = (int) qb_rc; // Negative of system errno, or bytes sent
if (qb_rc <= 0) {
goto send_cleanup;
} else if (!pcmk_is_set(flags, crm_ipc_client_response)) {
crm_trace("Not waiting for reply to %s IPC request %d",
client->server_name, header->qb.id);
goto send_cleanup;
}
rc = internal_ipc_get_reply(client, header->qb.id, ms_timeout, &bytes);
if (rc != pcmk_rc_ok) {
/* We didn't get the reply in time, so disable future sends for now.
* The only alternative would be to close the connection since we
* don't know how to detect and discard out-of-sequence replies.
*
* @TODO Implement out-of-sequence detection
*/
client->need_reply = TRUE;
}
rc = (int) bytes; // Negative system errno, or size of reply received
} else {
// No timeout, and client response needed
do {
qb_rc = qb_ipcc_sendv_recv(client->ipc, iov, 2, client->buffer,
client->buf_size, -1);
} while ((qb_rc == -EAGAIN) && crm_ipc_connected(client));
rc = (int) qb_rc; // Negative system errno, or size of reply received
}
if (rc > 0) {
pcmk__ipc_header_t *hdr = (pcmk__ipc_header_t *)(void*)client->buffer;
crm_trace("Received %d-byte reply %d to %s IPC %d: %.100s",
rc, hdr->qb.id, client->server_name, header->qb.id,
crm_ipc_buffer(client));
if (reply) {
*reply = pcmk__xml_parse(crm_ipc_buffer(client));
}
} else {
crm_trace("No reply to %s IPC %d: rc=%d",
client->server_name, header->qb.id, rc);
}
send_cleanup:
if (!crm_ipc_connected(client)) {
crm_notice("Couldn't send %s IPC request %d: Connection closed "
QB_XS " rc=%d", client->server_name, header->qb.id, rc);
} else if (rc == -ETIMEDOUT) {
crm_warn("%s IPC request %d failed: %s after %dms " QB_XS " rc=%d",
client->server_name, header->qb.id, pcmk_strerror(rc),
ms_timeout, rc);
crm_write_blackbox(0, NULL);
} else if (rc <= 0) {
crm_warn("%s IPC request %d failed: %s " QB_XS " rc=%d",
client->server_name, header->qb.id,
((rc == 0)? "No bytes sent" : pcmk_strerror(rc)), rc);
}
pcmk_free_ipc_event(iov);
return rc;
}
/*!
* \brief Ensure an IPC provider has expected user or group
*
* \param[in] qb_ipc libqb client connection if available
* \param[in] sock Connected Unix socket for IPC
* \param[in] refuid Expected user ID
* \param[in] refgid Expected group ID
* \param[out] gotpid If not NULL, where to store provider's actual process ID
* (or 1 on platforms where ID is not available)
* \param[out] gotuid If not NULL, where to store provider's actual user ID
* \param[out] gotgid If not NULL, where to store provider's actual group ID
*
* \return Standard Pacemaker return code
* \note An actual user ID of 0 (root) will always be considered authorized,
* regardless of the expected values provided. The caller can use the
* output arguments to be stricter than this function.
*/
static int
is_ipc_provider_expected(qb_ipcc_connection_t *qb_ipc, int sock,
uid_t refuid, gid_t refgid,
pid_t *gotpid, uid_t *gotuid, gid_t *gotgid)
{
int rc = EOPNOTSUPP;
pid_t found_pid = 0;
uid_t found_uid = 0;
gid_t found_gid = 0;
#ifdef HAVE_QB_IPCC_AUTH_GET
if (qb_ipc != NULL) {
rc = qb_ipcc_auth_get(qb_ipc, &found_pid, &found_uid, &found_gid);
rc = -rc; // libqb returns 0 or -errno
if (rc == pcmk_rc_ok) {
goto found;
}
}
#endif
#ifdef HAVE_UCRED
{
struct ucred ucred;
socklen_t ucred_len = sizeof(ucred);
if (getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &ucred, &ucred_len) < 0) {
rc = errno;
} else if (ucred_len != sizeof(ucred)) {
rc = EOPNOTSUPP;
} else {
found_pid = ucred.pid;
found_uid = ucred.uid;
found_gid = ucred.gid;
goto found;
}
}
#endif
#ifdef HAVE_SOCKPEERCRED
{
struct sockpeercred sockpeercred;
socklen_t sockpeercred_len = sizeof(sockpeercred);
if (getsockopt(sock, SOL_SOCKET, SO_PEERCRED,
&sockpeercred, &sockpeercred_len) < 0) {
rc = errno;
} else if (sockpeercred_len != sizeof(sockpeercred)) {
rc = EOPNOTSUPP;
} else {
found_pid = sockpeercred.pid;
found_uid = sockpeercred.uid;
found_gid = sockpeercred.gid;
goto found;
}
}
#endif
#ifdef HAVE_GETPEEREID // For example, FreeBSD
if (getpeereid(sock, &found_uid, &found_gid) < 0) {
rc = errno;
} else {
found_pid = PCMK__SPECIAL_PID;
goto found;
}
#endif
#ifdef HAVE_GETPEERUCRED
{
ucred_t *ucred = NULL;
if (getpeerucred(sock, &ucred) < 0) {
rc = errno;
} else {
found_pid = ucred_getpid(ucred);
found_uid = ucred_geteuid(ucred);
found_gid = ucred_getegid(ucred);
ucred_free(ucred);
goto found;
}
}
#endif
return rc; // If we get here, nothing succeeded
found:
if (gotpid != NULL) {
*gotpid = found_pid;
}
if (gotuid != NULL) {
*gotuid = found_uid;
}
if (gotgid != NULL) {
*gotgid = found_gid;
}
if ((found_uid != 0) && (found_uid != refuid) && (found_gid != refgid)) {
return pcmk_rc_ipc_unauthorized;
}
return pcmk_rc_ok;
}
int
crm_ipc_is_authentic_process(int sock, uid_t refuid, gid_t refgid,
pid_t *gotpid, uid_t *gotuid, gid_t *gotgid)
{
int ret = is_ipc_provider_expected(NULL, sock, refuid, refgid,
gotpid, gotuid, gotgid);
/* The old function had some very odd return codes*/
if (ret == 0) {
return 1;
} else if (ret == pcmk_rc_ipc_unauthorized) {
return 0;
} else {
return pcmk_rc2legacy(ret);
}
}
int
pcmk__ipc_is_authentic_process_active(const char *name, uid_t refuid,
gid_t refgid, pid_t *gotpid)
{
static char last_asked_name[PATH_MAX / 2] = ""; /* log spam prevention */
int fd;
int rc = pcmk_rc_ipc_unresponsive;
int auth_rc = 0;
int32_t qb_rc;
pid_t found_pid = 0; uid_t found_uid = 0; gid_t found_gid = 0;
qb_ipcc_connection_t *c;
#ifdef HAVE_QB_IPCC_CONNECT_ASYNC
struct pollfd pollfd = { 0, };
int poll_rc;
c = qb_ipcc_connect_async(name, 0,
&(pollfd.fd));
#else
c = qb_ipcc_connect(name, 0);
#endif
if (c == NULL) {
crm_info("Could not connect to %s IPC: %s", name, strerror(errno));
rc = pcmk_rc_ipc_unresponsive;
goto bail;
}
#ifdef HAVE_QB_IPCC_CONNECT_ASYNC
pollfd.events = POLLIN;
do {
poll_rc = poll(&pollfd, 1, 2000);
} while ((poll_rc == -1) && (errno == EINTR));
/* If poll() failed, given that disconnect function is not registered yet,
* qb_ipcc_disconnect() won't clean up the socket. In any case, call
* qb_ipcc_connect_continue() here so that it may fail and do the cleanup
* for us.
*/
if (qb_ipcc_connect_continue(c) != 0) {
crm_info("Could not connect to %s IPC: %s", name,
(poll_rc == 0)?"timeout":strerror(errno));
rc = pcmk_rc_ipc_unresponsive;
c = NULL; // qb_ipcc_connect_continue cleaned up for us
goto bail;
}
#endif
qb_rc = qb_ipcc_fd_get(c, &fd);
if (qb_rc != 0) {
rc = (int) -qb_rc; // System errno
crm_err("Could not get fd from %s IPC: %s " QB_XS " rc=%d",
name, pcmk_rc_str(rc), rc);
goto bail;
}
auth_rc = is_ipc_provider_expected(c, fd, refuid, refgid,
&found_pid, &found_uid, &found_gid);
if (auth_rc == pcmk_rc_ipc_unauthorized) {
crm_err("Daemon (IPC %s) effectively blocked with unauthorized"
" process %lld (uid: %lld, gid: %lld)",
name, (long long) PCMK__SPECIAL_PID_AS_0(found_pid),
(long long) found_uid, (long long) found_gid);
rc = pcmk_rc_ipc_unauthorized;
goto bail;
}
if (auth_rc != pcmk_rc_ok) {
rc = auth_rc;
crm_err("Could not get peer credentials from %s IPC: %s "
QB_XS " rc=%d", name, pcmk_rc_str(rc), rc);
goto bail;
}
if (gotpid != NULL) {
*gotpid = found_pid;
}
rc = pcmk_rc_ok;
if ((found_uid != refuid || found_gid != refgid)
&& strncmp(last_asked_name, name, sizeof(last_asked_name))) {
if ((found_uid == 0) && (refuid != 0)) {
crm_warn("Daemon (IPC %s) runs as root, whereas the expected"
" credentials are %lld:%lld, hazard of violating"
" the least privilege principle",
name, (long long) refuid, (long long) refgid);
} else {
crm_notice("Daemon (IPC %s) runs as %lld:%lld, whereas the"
" expected credentials are %lld:%lld, which may"
" mean a different set of privileges than expected",
name, (long long) found_uid, (long long) found_gid,
(long long) refuid, (long long) refgid);
}
memccpy(last_asked_name, name, '\0', sizeof(last_asked_name));
}
bail:
if (c != NULL) {
qb_ipcc_disconnect(c);
}
return rc;
}
diff --git a/lib/common/mainloop.c b/lib/common/mainloop.c
index b06a31366e..e53b0b1c1b 100644
--- a/lib/common/mainloop.c
+++ b/lib/common/mainloop.c
@@ -1,1467 +1,1463 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
-#ifndef _GNU_SOURCE
-# define _GNU_SOURCE
-#endif
-
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
struct mainloop_child_s {
pid_t pid;
char *desc;
unsigned timerid;
gboolean timeout;
void *privatedata;
enum mainloop_child_flags flags;
/* Called when a process dies */
void (*callback) (mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode);
};
struct trigger_s {
GSource source;
gboolean running;
gboolean trigger;
void *user_data;
guint id;
};
struct mainloop_timer_s {
guint id;
guint period_ms;
bool repeat;
char *name;
GSourceFunc cb;
void *userdata;
};
static gboolean
crm_trigger_prepare(GSource * source, gint * timeout)
{
crm_trigger_t *trig = (crm_trigger_t *) source;
/* cluster-glue's FD and IPC related sources make use of
* g_source_add_poll() but do not set a timeout in their prepare
* functions
*
* This means mainloop's poll() will block until an event for one
* of these sources occurs - any /other/ type of source, such as
* this one or g_idle_*, that doesn't use g_source_add_poll() is
* S-O-L and won't be processed until there is something fd-based
* happens.
*
* Luckily the timeout we can set here affects all sources and
* puts an upper limit on how long poll() can take.
*
* So unconditionally set a small-ish timeout, not too small that
* we're in constant motion, which will act as an upper bound on
* how long the signal handling might be delayed for.
*/
*timeout = 500; /* Timeout in ms */
return trig->trigger;
}
static gboolean
crm_trigger_check(GSource * source)
{
crm_trigger_t *trig = (crm_trigger_t *) source;
return trig->trigger;
}
/*!
* \internal
* \brief GSource dispatch function for crm_trigger_t
*
* \param[in] source crm_trigger_t being dispatched
* \param[in] callback Callback passed at source creation
* \param[in,out] userdata User data passed at source creation
*
* \return G_SOURCE_REMOVE to remove source, G_SOURCE_CONTINUE to keep it
*/
static gboolean
crm_trigger_dispatch(GSource *source, GSourceFunc callback, gpointer userdata)
{
gboolean rc = G_SOURCE_CONTINUE;
crm_trigger_t *trig = (crm_trigger_t *) source;
if (trig->running) {
/* Wait until the existing job is complete before starting the next one */
return G_SOURCE_CONTINUE;
}
trig->trigger = FALSE;
if (callback) {
int callback_rc = callback(trig->user_data);
if (callback_rc < 0) {
crm_trace("Trigger handler %p not yet complete", trig);
trig->running = TRUE;
} else if (callback_rc == 0) {
rc = G_SOURCE_REMOVE;
}
}
return rc;
}
static void
crm_trigger_finalize(GSource * source)
{
crm_trace("Trigger %p destroyed", source);
}
static GSourceFuncs crm_trigger_funcs = {
crm_trigger_prepare,
crm_trigger_check,
crm_trigger_dispatch,
crm_trigger_finalize,
};
static crm_trigger_t *
mainloop_setup_trigger(GSource * source, int priority, int (*dispatch) (gpointer user_data),
gpointer userdata)
{
crm_trigger_t *trigger = NULL;
trigger = (crm_trigger_t *) source;
trigger->id = 0;
trigger->trigger = FALSE;
trigger->user_data = userdata;
if (dispatch) {
g_source_set_callback(source, dispatch, trigger, NULL);
}
g_source_set_priority(source, priority);
g_source_set_can_recurse(source, FALSE);
trigger->id = g_source_attach(source, NULL);
return trigger;
}
void
mainloop_trigger_complete(crm_trigger_t * trig)
{
crm_trace("Trigger handler %p complete", trig);
trig->running = FALSE;
}
/*!
* \brief Create a trigger to be used as a mainloop source
*
* \param[in] priority Relative priority of source (lower number is higher priority)
* \param[in] dispatch Trigger dispatch function (should return 0 to remove the
* trigger from the mainloop, -1 if the trigger should be
* kept but the job is still running and not complete, and
* 1 if the trigger should be kept and the job is complete)
* \param[in] userdata Pointer to pass to \p dispatch
*
* \return Newly allocated mainloop source for trigger
*/
crm_trigger_t *
mainloop_add_trigger(int priority, int (*dispatch) (gpointer user_data),
gpointer userdata)
{
GSource *source = NULL;
CRM_ASSERT(sizeof(crm_trigger_t) > sizeof(GSource));
source = g_source_new(&crm_trigger_funcs, sizeof(crm_trigger_t));
return mainloop_setup_trigger(source, priority, dispatch, userdata);
}
void
mainloop_set_trigger(crm_trigger_t * source)
{
if(source) {
source->trigger = TRUE;
}
}
gboolean
mainloop_destroy_trigger(crm_trigger_t * source)
{
GSource *gs = NULL;
if(source == NULL) {
return TRUE;
}
gs = (GSource *)source;
g_source_destroy(gs); /* Remove from mainloop, ref_count-- */
g_source_unref(gs); /* The caller no longer carries a reference to source
*
* At this point the source should be free'd,
* unless we're currently processing said
* source, in which case mainloop holds an
* additional reference and it will be free'd
* once our processing completes
*/
return TRUE;
}
// Define a custom glib source for signal handling
// Data structure for custom glib source
typedef struct signal_s {
crm_trigger_t trigger; // trigger that invoked source (must be first)
void (*handler) (int sig); // signal handler
int signal; // signal that was received
} crm_signal_t;
// Table to associate signal handlers with signal numbers
static crm_signal_t *crm_signals[NSIG];
/*!
* \internal
* \brief Dispatch an event from custom glib source for signals
*
* Given an signal event, clear the event trigger and call any registered
* signal handler.
*
* \param[in] source glib source that triggered this dispatch
* \param[in] callback (ignored)
* \param[in] userdata (ignored)
*/
static gboolean
crm_signal_dispatch(GSource *source, GSourceFunc callback, gpointer userdata)
{
crm_signal_t *sig = (crm_signal_t *) source;
if(sig->signal != SIGCHLD) {
crm_notice("Caught '%s' signal " QB_XS " %d (%s handler)",
strsignal(sig->signal), sig->signal,
(sig->handler? "invoking" : "no"));
}
sig->trigger.trigger = FALSE;
if (sig->handler) {
sig->handler(sig->signal);
}
return TRUE;
}
/*!
* \internal
* \brief Handle a signal by setting a trigger for signal source
*
* \param[in] sig Signal number that was received
*
* \note This is the true signal handler for the mainloop signal source, and
* must be async-safe.
*/
static void
mainloop_signal_handler(int sig)
{
if (sig > 0 && sig < NSIG && crm_signals[sig] != NULL) {
mainloop_set_trigger((crm_trigger_t *) crm_signals[sig]);
}
}
// Functions implementing our custom glib source for signal handling
static GSourceFuncs crm_signal_funcs = {
crm_trigger_prepare,
crm_trigger_check,
crm_signal_dispatch,
crm_trigger_finalize,
};
/*!
* \internal
* \brief Set a true signal handler
*
* signal()-like interface to sigaction()
*
* \param[in] sig Signal number to register handler for
* \param[in] dispatch Signal handler
*
* \return The previous value of the signal handler, or SIG_ERR on error
* \note The dispatch function must be async-safe.
*/
sighandler_t
crm_signal_handler(int sig, sighandler_t dispatch)
{
sigset_t mask;
struct sigaction sa;
struct sigaction old;
if (sigemptyset(&mask) < 0) {
crm_err("Could not set handler for signal %d: %s",
sig, pcmk_rc_str(errno));
return SIG_ERR;
}
memset(&sa, 0, sizeof(struct sigaction));
sa.sa_handler = dispatch;
sa.sa_flags = SA_RESTART;
sa.sa_mask = mask;
if (sigaction(sig, &sa, &old) < 0) {
crm_err("Could not set handler for signal %d: %s",
sig, pcmk_rc_str(errno));
return SIG_ERR;
}
return old.sa_handler;
}
static void
mainloop_destroy_signal_entry(int sig)
{
crm_signal_t *tmp = crm_signals[sig];
crm_signals[sig] = NULL;
crm_trace("Destroying signal %d", sig);
mainloop_destroy_trigger((crm_trigger_t *) tmp);
}
/*!
* \internal
* \brief Add a signal handler to a mainloop
*
* \param[in] sig Signal number to handle
* \param[in] dispatch Signal handler function
*
* \note The true signal handler merely sets a mainloop trigger to call this
* dispatch function via the mainloop. Therefore, the dispatch function
* does not need to be async-safe.
*/
gboolean
mainloop_add_signal(int sig, void (*dispatch) (int sig))
{
GSource *source = NULL;
int priority = G_PRIORITY_HIGH - 1;
if (sig == SIGTERM) {
/* TERM is higher priority than other signals,
* signals are higher priority than other ipc.
* Yes, minus: smaller is "higher"
*/
priority--;
}
if (sig >= NSIG || sig < 0) {
crm_err("Signal %d is out of range", sig);
return FALSE;
} else if (crm_signals[sig] != NULL && crm_signals[sig]->handler == dispatch) {
crm_trace("Signal handler for %d is already installed", sig);
return TRUE;
} else if (crm_signals[sig] != NULL) {
crm_err("Different signal handler for %d is already installed", sig);
return FALSE;
}
CRM_ASSERT(sizeof(crm_signal_t) > sizeof(GSource));
source = g_source_new(&crm_signal_funcs, sizeof(crm_signal_t));
crm_signals[sig] = (crm_signal_t *) mainloop_setup_trigger(source, priority, NULL, NULL);
CRM_ASSERT(crm_signals[sig] != NULL);
crm_signals[sig]->handler = dispatch;
crm_signals[sig]->signal = sig;
if (crm_signal_handler(sig, mainloop_signal_handler) == SIG_ERR) {
mainloop_destroy_signal_entry(sig);
return FALSE;
}
return TRUE;
}
gboolean
mainloop_destroy_signal(int sig)
{
if (sig >= NSIG || sig < 0) {
crm_err("Signal %d is out of range", sig);
return FALSE;
} else if (crm_signal_handler(sig, NULL) == SIG_ERR) {
crm_perror(LOG_ERR, "Could not uninstall signal handler for signal %d", sig);
return FALSE;
} else if (crm_signals[sig] == NULL) {
return TRUE;
}
mainloop_destroy_signal_entry(sig);
return TRUE;
}
static qb_array_t *gio_map = NULL;
void
mainloop_cleanup(void)
{
if (gio_map) {
qb_array_free(gio_map);
}
for (int sig = 0; sig < NSIG; ++sig) {
mainloop_destroy_signal_entry(sig);
}
}
/*
* libqb...
*/
struct gio_to_qb_poll {
int32_t is_used;
guint source;
int32_t events;
void *data;
qb_ipcs_dispatch_fn_t fn;
enum qb_loop_priority p;
};
static gboolean
gio_read_socket(GIOChannel * gio, GIOCondition condition, gpointer data)
{
struct gio_to_qb_poll *adaptor = (struct gio_to_qb_poll *)data;
gint fd = g_io_channel_unix_get_fd(gio);
crm_trace("%p.%d %d", data, fd, condition);
/* if this assert get's hit, then there is a race condition between
* when we destroy a fd and when mainloop actually gives it up */
CRM_ASSERT(adaptor->is_used > 0);
return (adaptor->fn(fd, condition, adaptor->data) == 0);
}
static void
gio_poll_destroy(gpointer data)
{
struct gio_to_qb_poll *adaptor = (struct gio_to_qb_poll *)data;
adaptor->is_used--;
CRM_ASSERT(adaptor->is_used >= 0);
if (adaptor->is_used == 0) {
crm_trace("Marking adaptor %p unused", adaptor);
adaptor->source = 0;
}
}
/*!
* \internal
* \brief Convert libqb's poll priority into GLib's one
*
* \param[in] prio libqb's poll priority (#QB_LOOP_MED assumed as fallback)
*
* \return best matching GLib's priority
*/
static gint
conv_prio_libqb2glib(enum qb_loop_priority prio)
{
switch (prio) {
case QB_LOOP_LOW: return G_PRIORITY_LOW;
case QB_LOOP_HIGH: return G_PRIORITY_HIGH;
default: return G_PRIORITY_DEFAULT; // QB_LOOP_MED
}
}
/*!
* \internal
* \brief Convert libqb's poll priority to rate limiting spec
*
* \param[in] prio libqb's poll priority (#QB_LOOP_MED assumed as fallback)
*
* \return best matching rate limiting spec
* \note This is the inverse of libqb's qb_ipcs_request_rate_limit().
*/
static enum qb_ipcs_rate_limit
conv_libqb_prio2ratelimit(enum qb_loop_priority prio)
{
switch (prio) {
case QB_LOOP_LOW: return QB_IPCS_RATE_SLOW;
case QB_LOOP_HIGH: return QB_IPCS_RATE_FAST;
default: return QB_IPCS_RATE_NORMAL; // QB_LOOP_MED
}
}
static int32_t
gio_poll_dispatch_update(enum qb_loop_priority p, int32_t fd, int32_t evts,
void *data, qb_ipcs_dispatch_fn_t fn, int32_t add)
{
struct gio_to_qb_poll *adaptor;
GIOChannel *channel;
int32_t res = 0;
res = qb_array_index(gio_map, fd, (void **)&adaptor);
if (res < 0) {
crm_err("Array lookup failed for fd=%d: %d", fd, res);
return res;
}
crm_trace("Adding fd=%d to mainloop as adaptor %p", fd, adaptor);
if (add && adaptor->source) {
crm_err("Adaptor for descriptor %d is still in-use", fd);
return -EEXIST;
}
if (!add && !adaptor->is_used) {
crm_err("Adaptor for descriptor %d is not in-use", fd);
return -ENOENT;
}
/* channel is created with ref_count = 1 */
channel = g_io_channel_unix_new(fd);
if (!channel) {
crm_err("No memory left to add fd=%d", fd);
return -ENOMEM;
}
if (adaptor->source) {
g_source_remove(adaptor->source);
adaptor->source = 0;
}
/* Because unlike the poll() API, glib doesn't tell us about HUPs by default */
evts |= (G_IO_HUP | G_IO_NVAL | G_IO_ERR);
adaptor->fn = fn;
adaptor->events = evts;
adaptor->data = data;
adaptor->p = p;
adaptor->is_used++;
adaptor->source =
g_io_add_watch_full(channel, conv_prio_libqb2glib(p), evts,
gio_read_socket, adaptor, gio_poll_destroy);
/* Now that mainloop now holds a reference to channel,
* thanks to g_io_add_watch_full(), drop ours from g_io_channel_unix_new().
*
* This means that channel will be free'd by:
* g_main_context_dispatch()
* -> g_source_destroy_internal()
* -> g_source_callback_unref()
* shortly after gio_poll_destroy() completes
*/
g_io_channel_unref(channel);
crm_trace("Added to mainloop with gsource id=%d", adaptor->source);
if (adaptor->source > 0) {
return 0;
}
return -EINVAL;
}
static int32_t
gio_poll_dispatch_add(enum qb_loop_priority p, int32_t fd, int32_t evts,
void *data, qb_ipcs_dispatch_fn_t fn)
{
return gio_poll_dispatch_update(p, fd, evts, data, fn, QB_TRUE);
}
static int32_t
gio_poll_dispatch_mod(enum qb_loop_priority p, int32_t fd, int32_t evts,
void *data, qb_ipcs_dispatch_fn_t fn)
{
return gio_poll_dispatch_update(p, fd, evts, data, fn, QB_FALSE);
}
static int32_t
gio_poll_dispatch_del(int32_t fd)
{
struct gio_to_qb_poll *adaptor;
crm_trace("Looking for fd=%d", fd);
if (qb_array_index(gio_map, fd, (void **)&adaptor) == 0) {
if (adaptor->source) {
g_source_remove(adaptor->source);
adaptor->source = 0;
}
}
return 0;
}
struct qb_ipcs_poll_handlers gio_poll_funcs = {
.job_add = NULL,
.dispatch_add = gio_poll_dispatch_add,
.dispatch_mod = gio_poll_dispatch_mod,
.dispatch_del = gio_poll_dispatch_del,
};
static enum qb_ipc_type
pick_ipc_type(enum qb_ipc_type requested)
{
const char *env = pcmk__env_option(PCMK__ENV_IPC_TYPE);
if (env && strcmp("shared-mem", env) == 0) {
return QB_IPC_SHM;
} else if (env && strcmp("socket", env) == 0) {
return QB_IPC_SOCKET;
} else if (env && strcmp("posix", env) == 0) {
return QB_IPC_POSIX_MQ;
} else if (env && strcmp("sysv", env) == 0) {
return QB_IPC_SYSV_MQ;
} else if (requested == QB_IPC_NATIVE) {
/* We prefer shared memory because the server never blocks on
* send. If part of a message fits into the socket, libqb
* needs to block until the remainder can be sent also.
* Otherwise the client will wait forever for the remaining
* bytes.
*/
return QB_IPC_SHM;
}
return requested;
}
qb_ipcs_service_t *
mainloop_add_ipc_server(const char *name, enum qb_ipc_type type,
struct qb_ipcs_service_handlers *callbacks)
{
return mainloop_add_ipc_server_with_prio(name, type, callbacks, QB_LOOP_MED);
}
qb_ipcs_service_t *
mainloop_add_ipc_server_with_prio(const char *name, enum qb_ipc_type type,
struct qb_ipcs_service_handlers *callbacks,
enum qb_loop_priority prio)
{
int rc = 0;
qb_ipcs_service_t *server = NULL;
if (gio_map == NULL) {
gio_map = qb_array_create_2(64, sizeof(struct gio_to_qb_poll), 1);
}
server = qb_ipcs_create(name, 0, pick_ipc_type(type), callbacks);
if (server == NULL) {
crm_err("Could not create %s IPC server: %s (%d)",
name, pcmk_rc_str(errno), errno);
return NULL;
}
if (prio != QB_LOOP_MED) {
qb_ipcs_request_rate_limit(server, conv_libqb_prio2ratelimit(prio));
}
/* All clients should use at least ipc_buffer_max as their buffer size */
qb_ipcs_enforce_buffer_size(server, crm_ipc_default_buffer_size());
qb_ipcs_poll_handlers_set(server, &gio_poll_funcs);
rc = qb_ipcs_run(server);
if (rc < 0) {
crm_err("Could not start %s IPC server: %s (%d)", name, pcmk_strerror(rc), rc);
return NULL; // qb_ipcs_run() destroys server on failure
}
return server;
}
void
mainloop_del_ipc_server(qb_ipcs_service_t * server)
{
if (server) {
qb_ipcs_destroy(server);
}
}
struct mainloop_io_s {
char *name;
void *userdata;
int fd;
guint source;
crm_ipc_t *ipc;
GIOChannel *channel;
int (*dispatch_fn_ipc) (const char *buffer, ssize_t length, gpointer userdata);
int (*dispatch_fn_io) (gpointer userdata);
void (*destroy_fn) (gpointer userdata);
};
/*!
* \internal
* \brief I/O watch callback function (GIOFunc)
*
* \param[in] gio I/O channel being watched
* \param[in] condition I/O condition satisfied
* \param[in] data User data passed when source was created
*
* \return G_SOURCE_REMOVE to remove source, G_SOURCE_CONTINUE to keep it
*/
static gboolean
mainloop_gio_callback(GIOChannel *gio, GIOCondition condition, gpointer data)
{
gboolean rc = G_SOURCE_CONTINUE;
mainloop_io_t *client = data;
CRM_ASSERT(client->fd == g_io_channel_unix_get_fd(gio));
if (condition & G_IO_IN) {
if (client->ipc) {
long read_rc = 0L;
int max = 10;
do {
read_rc = crm_ipc_read(client->ipc);
if (read_rc <= 0) {
crm_trace("Could not read IPC message from %s: %s (%ld)",
client->name, pcmk_strerror(read_rc), read_rc);
} else if (client->dispatch_fn_ipc) {
const char *buffer = crm_ipc_buffer(client->ipc);
crm_trace("New %ld-byte IPC message from %s "
"after I/O condition %d",
read_rc, client->name, (int) condition);
if (client->dispatch_fn_ipc(buffer, read_rc, client->userdata) < 0) {
crm_trace("Connection to %s no longer required", client->name);
rc = G_SOURCE_REMOVE;
}
}
} while ((rc == G_SOURCE_CONTINUE) && (read_rc > 0) && --max > 0);
} else {
crm_trace("New I/O event for %s after I/O condition %d",
client->name, (int) condition);
if (client->dispatch_fn_io) {
if (client->dispatch_fn_io(client->userdata) < 0) {
crm_trace("Connection to %s no longer required", client->name);
rc = G_SOURCE_REMOVE;
}
}
}
}
if (client->ipc && !crm_ipc_connected(client->ipc)) {
crm_err("Connection to %s closed " QB_XS "client=%p condition=%d",
client->name, client, condition);
rc = G_SOURCE_REMOVE;
} else if (condition & (G_IO_HUP | G_IO_NVAL | G_IO_ERR)) {
crm_trace("The connection %s[%p] has been closed (I/O condition=%d)",
client->name, client, condition);
rc = G_SOURCE_REMOVE;
} else if ((condition & G_IO_IN) == 0) {
/*
#define GLIB_SYSDEF_POLLIN =1
#define GLIB_SYSDEF_POLLPRI =2
#define GLIB_SYSDEF_POLLOUT =4
#define GLIB_SYSDEF_POLLERR =8
#define GLIB_SYSDEF_POLLHUP =16
#define GLIB_SYSDEF_POLLNVAL =32
typedef enum
{
G_IO_IN GLIB_SYSDEF_POLLIN,
G_IO_OUT GLIB_SYSDEF_POLLOUT,
G_IO_PRI GLIB_SYSDEF_POLLPRI,
G_IO_ERR GLIB_SYSDEF_POLLERR,
G_IO_HUP GLIB_SYSDEF_POLLHUP,
G_IO_NVAL GLIB_SYSDEF_POLLNVAL
} GIOCondition;
A bitwise combination representing a condition to watch for on an event source.
G_IO_IN There is data to read.
G_IO_OUT Data can be written (without blocking).
G_IO_PRI There is urgent data to read.
G_IO_ERR Error condition.
G_IO_HUP Hung up (the connection has been broken, usually for pipes and sockets).
G_IO_NVAL Invalid request. The file descriptor is not open.
*/
crm_err("Strange condition: %d", condition);
}
/* G_SOURCE_REMOVE results in mainloop_gio_destroy() being called
* just before the source is removed from mainloop
*/
return rc;
}
static void
mainloop_gio_destroy(gpointer c)
{
mainloop_io_t *client = c;
char *c_name = strdup(client->name);
/* client->source is valid but about to be destroyed (ref_count == 0) in gmain.c
* client->channel will still have ref_count > 0... should be == 1
*/
crm_trace("Destroying client %s[%p]", c_name, c);
if (client->ipc) {
crm_ipc_close(client->ipc);
}
if (client->destroy_fn) {
void (*destroy_fn) (gpointer userdata) = client->destroy_fn;
client->destroy_fn = NULL;
destroy_fn(client->userdata);
}
if (client->ipc) {
crm_ipc_t *ipc = client->ipc;
client->ipc = NULL;
crm_ipc_destroy(ipc);
}
crm_trace("Destroyed client %s[%p]", c_name, c);
free(client->name); client->name = NULL;
free(client);
free(c_name);
}
/*!
* \brief Connect to IPC and add it as a main loop source
*
* \param[in,out] ipc IPC connection to add
* \param[in] priority Event source priority to use for connection
* \param[in] userdata Data to register with callbacks
* \param[in] callbacks Dispatch and destroy callbacks for connection
* \param[out] source Newly allocated event source
*
* \return Standard Pacemaker return code
*
* \note On failure, the caller is still responsible for ipc. On success, the
* caller should call mainloop_del_ipc_client() when source is no longer
* needed, which will lead to the disconnection of the IPC later in the
* main loop if it is connected. However the IPC disconnects,
* mainloop_gio_destroy() will free ipc and source after calling the
* destroy callback.
*/
int
pcmk__add_mainloop_ipc(crm_ipc_t *ipc, int priority, void *userdata,
const struct ipc_client_callbacks *callbacks,
mainloop_io_t **source)
{
int rc = pcmk_rc_ok;
int fd = -1;
const char *ipc_name = NULL;
CRM_CHECK((ipc != NULL) && (callbacks != NULL), return EINVAL);
ipc_name = pcmk__s(crm_ipc_name(ipc), "Pacemaker");
rc = pcmk__connect_generic_ipc(ipc);
if (rc != pcmk_rc_ok) {
crm_debug("Connection to %s failed: %s", ipc_name, pcmk_rc_str(rc));
return rc;
}
rc = pcmk__ipc_fd(ipc, &fd);
if (rc != pcmk_rc_ok) {
crm_debug("Could not obtain file descriptor for %s IPC: %s",
ipc_name, pcmk_rc_str(rc));
crm_ipc_close(ipc);
return rc;
}
*source = mainloop_add_fd(ipc_name, priority, fd, userdata, NULL);
if (*source == NULL) {
rc = errno;
crm_ipc_close(ipc);
return rc;
}
(*source)->ipc = ipc;
(*source)->destroy_fn = callbacks->destroy;
(*source)->dispatch_fn_ipc = callbacks->dispatch;
return pcmk_rc_ok;
}
/*!
* \brief Get period for mainloop timer
*
* \param[in] timer Timer
*
* \return Period in ms
*/
guint
pcmk__mainloop_timer_get_period(const mainloop_timer_t *timer)
{
if (timer) {
return timer->period_ms;
}
return 0;
}
mainloop_io_t *
mainloop_add_ipc_client(const char *name, int priority, size_t max_size,
void *userdata, struct ipc_client_callbacks *callbacks)
{
crm_ipc_t *ipc = crm_ipc_new(name, max_size);
mainloop_io_t *source = NULL;
int rc = pcmk__add_mainloop_ipc(ipc, priority, userdata, callbacks,
&source);
if (rc != pcmk_rc_ok) {
if (crm_log_level == LOG_STDOUT) {
fprintf(stderr, "Connection to %s failed: %s",
name, pcmk_rc_str(rc));
}
crm_ipc_destroy(ipc);
if (rc > 0) {
errno = rc;
} else {
errno = ENOTCONN;
}
return NULL;
}
return source;
}
void
mainloop_del_ipc_client(mainloop_io_t * client)
{
mainloop_del_fd(client);
}
crm_ipc_t *
mainloop_get_ipc_client(mainloop_io_t * client)
{
if (client) {
return client->ipc;
}
return NULL;
}
mainloop_io_t *
mainloop_add_fd(const char *name, int priority, int fd, void *userdata,
struct mainloop_fd_callbacks * callbacks)
{
mainloop_io_t *client = NULL;
if (fd >= 0) {
client = calloc(1, sizeof(mainloop_io_t));
if (client == NULL) {
return NULL;
}
client->name = strdup(name);
client->userdata = userdata;
if (callbacks) {
client->destroy_fn = callbacks->destroy;
client->dispatch_fn_io = callbacks->dispatch;
}
client->fd = fd;
client->channel = g_io_channel_unix_new(fd);
client->source =
g_io_add_watch_full(client->channel, priority,
(G_IO_IN | G_IO_HUP | G_IO_NVAL | G_IO_ERR), mainloop_gio_callback,
client, mainloop_gio_destroy);
/* Now that mainloop now holds a reference to channel,
* thanks to g_io_add_watch_full(), drop ours from g_io_channel_unix_new().
*
* This means that channel will be free'd by:
* g_main_context_dispatch() or g_source_remove()
* -> g_source_destroy_internal()
* -> g_source_callback_unref()
* shortly after mainloop_gio_destroy() completes
*/
g_io_channel_unref(client->channel);
crm_trace("Added connection %d for %s[%p].%d", client->source, client->name, client, fd);
} else {
errno = EINVAL;
}
return client;
}
void
mainloop_del_fd(mainloop_io_t * client)
{
if (client != NULL) {
crm_trace("Removing client %s[%p]", client->name, client);
if (client->source) {
/* Results in mainloop_gio_destroy() being called just
* before the source is removed from mainloop
*/
g_source_remove(client->source);
}
}
}
static GList *child_list = NULL;
pid_t
mainloop_child_pid(mainloop_child_t * child)
{
return child->pid;
}
const char *
mainloop_child_name(mainloop_child_t * child)
{
return child->desc;
}
int
mainloop_child_timeout(mainloop_child_t * child)
{
return child->timeout;
}
void *
mainloop_child_userdata(mainloop_child_t * child)
{
return child->privatedata;
}
void
mainloop_clear_child_userdata(mainloop_child_t * child)
{
child->privatedata = NULL;
}
/* good function name */
static void
child_free(mainloop_child_t *child)
{
if (child->timerid != 0) {
crm_trace("Removing timer %d", child->timerid);
g_source_remove(child->timerid);
child->timerid = 0;
}
free(child->desc);
free(child);
}
/* terrible function name */
static int
child_kill_helper(mainloop_child_t *child)
{
int rc;
if (child->flags & mainloop_leave_pid_group) {
crm_debug("Kill pid %d only. leave group intact.", child->pid);
rc = kill(child->pid, SIGKILL);
} else {
crm_debug("Kill pid %d's group", child->pid);
rc = kill(-child->pid, SIGKILL);
}
if (rc < 0) {
if (errno != ESRCH) {
crm_perror(LOG_ERR, "kill(%d, KILL) failed", child->pid);
}
return -errno;
}
return 0;
}
static gboolean
child_timeout_callback(gpointer p)
{
mainloop_child_t *child = p;
int rc = 0;
child->timerid = 0;
if (child->timeout) {
crm_warn("%s process (PID %d) will not die!", child->desc, (int)child->pid);
return FALSE;
}
rc = child_kill_helper(child);
if (rc == -ESRCH) {
/* Nothing left to do. pid doesn't exist */
return FALSE;
}
child->timeout = TRUE;
crm_debug("%s process (PID %d) timed out", child->desc, (int)child->pid);
child->timerid = g_timeout_add(5000, child_timeout_callback, child);
return FALSE;
}
static bool
child_waitpid(mainloop_child_t *child, int flags)
{
int rc = 0;
int core = 0;
int signo = 0;
int status = 0;
int exitcode = 0;
bool callback_needed = true;
rc = waitpid(child->pid, &status, flags);
if (rc == 0) { // WNOHANG in flags, and child status is not available
crm_trace("Child process %d (%s) still active",
child->pid, child->desc);
callback_needed = false;
} else if (rc != child->pid) {
/* According to POSIX, possible conditions:
* - child->pid was non-positive (process group or any child),
* and rc is specific child
* - errno ECHILD (pid does not exist or is not child)
* - errno EINVAL (invalid flags)
* - errno EINTR (caller interrupted by signal)
*
* @TODO Handle these cases more specifically.
*/
signo = SIGCHLD;
exitcode = 1;
crm_notice("Wait for child process %d (%s) interrupted: %s",
child->pid, child->desc, pcmk_rc_str(errno));
} else if (WIFEXITED(status)) {
exitcode = WEXITSTATUS(status);
crm_trace("Child process %d (%s) exited with status %d",
child->pid, child->desc, exitcode);
} else if (WIFSIGNALED(status)) {
signo = WTERMSIG(status);
crm_trace("Child process %d (%s) exited with signal %d (%s)",
child->pid, child->desc, signo, strsignal(signo));
#ifdef WCOREDUMP // AIX, SunOS, maybe others
} else if (WCOREDUMP(status)) {
core = 1;
crm_err("Child process %d (%s) dumped core",
child->pid, child->desc);
#endif
} else { // flags must contain WUNTRACED and/or WCONTINUED to reach this
crm_trace("Child process %d (%s) stopped or continued",
child->pid, child->desc);
callback_needed = false;
}
if (callback_needed && child->callback) {
child->callback(child, child->pid, core, signo, exitcode);
}
return callback_needed;
}
static void
child_death_dispatch(int signal)
{
for (GList *iter = child_list; iter; ) {
GList *saved = iter;
mainloop_child_t *child = iter->data;
iter = iter->next;
if (child_waitpid(child, WNOHANG)) {
crm_trace("Removing completed process %d from child list",
child->pid);
child_list = g_list_remove_link(child_list, saved);
g_list_free(saved);
child_free(child);
}
}
}
static gboolean
child_signal_init(gpointer p)
{
crm_trace("Installed SIGCHLD handler");
/* Do NOT use g_child_watch_add() and friends, they rely on pthreads */
mainloop_add_signal(SIGCHLD, child_death_dispatch);
/* In case they terminated before the signal handler was installed */
child_death_dispatch(SIGCHLD);
return FALSE;
}
gboolean
mainloop_child_kill(pid_t pid)
{
GList *iter;
mainloop_child_t *child = NULL;
mainloop_child_t *match = NULL;
/* It is impossible to block SIGKILL, this allows us to
* call waitpid without WNOHANG flag.*/
int waitflags = 0, rc = 0;
for (iter = child_list; iter != NULL && match == NULL; iter = iter->next) {
child = iter->data;
if (pid == child->pid) {
match = child;
}
}
if (match == NULL) {
return FALSE;
}
rc = child_kill_helper(match);
if(rc == -ESRCH) {
/* It's gone, but hasn't shown up in waitpid() yet. Wait until we get
* SIGCHLD and let handler clean it up as normal (so we get the correct
* return code/status). The blocking alternative would be to call
* child_waitpid(match, 0).
*/
crm_trace("Waiting for signal that child process %d completed",
match->pid);
return TRUE;
} else if(rc != 0) {
/* If KILL for some other reason set the WNOHANG flag since we
* can't be certain what happened.
*/
waitflags = WNOHANG;
}
if (!child_waitpid(match, waitflags)) {
/* not much we can do if this occurs */
return FALSE;
}
child_list = g_list_remove(child_list, match);
child_free(match);
return TRUE;
}
/* Create/Log a new tracked process
* To track a process group, use -pid
*
* @TODO Using a non-positive pid (i.e. any child, or process group) would
* likely not be useful since we will free the child after the first
* completed process.
*/
void
mainloop_child_add_with_flags(pid_t pid, int timeout, const char *desc, void *privatedata, enum mainloop_child_flags flags,
void (*callback) (mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode))
{
static bool need_init = TRUE;
mainloop_child_t *child = pcmk__assert_alloc(1, sizeof(mainloop_child_t));
child->pid = pid;
child->timerid = 0;
child->timeout = FALSE;
child->privatedata = privatedata;
child->callback = callback;
child->flags = flags;
child->desc = pcmk__str_copy(desc);
if (timeout) {
child->timerid = g_timeout_add(timeout, child_timeout_callback, child);
}
child_list = g_list_append(child_list, child);
if(need_init) {
need_init = FALSE;
/* SIGCHLD processing has to be invoked from mainloop.
* We do not want it to be possible to both add a child pid
* to mainloop, and have the pid's exit callback invoked within
* the same callstack. */
g_timeout_add(1, child_signal_init, NULL);
}
}
void
mainloop_child_add(pid_t pid, int timeout, const char *desc, void *privatedata,
void (*callback) (mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode))
{
mainloop_child_add_with_flags(pid, timeout, desc, privatedata, 0, callback);
}
static gboolean
mainloop_timer_cb(gpointer user_data)
{
int id = 0;
bool repeat = FALSE;
struct mainloop_timer_s *t = user_data;
CRM_ASSERT(t != NULL);
id = t->id;
t->id = 0; /* Ensure it's unset during callbacks so that
* mainloop_timer_running() works as expected
*/
if(t->cb) {
crm_trace("Invoking callbacks for timer %s", t->name);
repeat = t->repeat;
if(t->cb(t->userdata) == FALSE) {
crm_trace("Timer %s complete", t->name);
repeat = FALSE;
}
}
if(repeat) {
/* Restore if repeating */
t->id = id;
}
return repeat;
}
bool
mainloop_timer_running(mainloop_timer_t *t)
{
if(t && t->id != 0) {
return TRUE;
}
return FALSE;
}
void
mainloop_timer_start(mainloop_timer_t *t)
{
mainloop_timer_stop(t);
if(t && t->period_ms > 0) {
crm_trace("Starting timer %s", t->name);
t->id = g_timeout_add(t->period_ms, mainloop_timer_cb, t);
}
}
void
mainloop_timer_stop(mainloop_timer_t *t)
{
if(t && t->id != 0) {
crm_trace("Stopping timer %s", t->name);
g_source_remove(t->id);
t->id = 0;
}
}
guint
mainloop_timer_set_period(mainloop_timer_t *t, guint period_ms)
{
guint last = 0;
if(t) {
last = t->period_ms;
t->period_ms = period_ms;
}
if(t && t->id != 0 && last != t->period_ms) {
mainloop_timer_start(t);
}
return last;
}
mainloop_timer_t *
mainloop_timer_add(const char *name, guint period_ms, bool repeat, GSourceFunc cb, void *userdata)
{
mainloop_timer_t *t = pcmk__assert_alloc(1, sizeof(mainloop_timer_t));
if (name != NULL) {
t->name = crm_strdup_printf("%s-%u-%d", name, period_ms, repeat);
} else {
t->name = crm_strdup_printf("%p-%u-%d", t, period_ms, repeat);
}
t->id = 0;
t->period_ms = period_ms;
t->repeat = repeat;
t->cb = cb;
t->userdata = userdata;
crm_trace("Created timer %s with %p %p", t->name, userdata, t->userdata);
return t;
}
void
mainloop_timer_del(mainloop_timer_t *t)
{
if(t) {
crm_trace("Destroying timer %s", t->name);
mainloop_timer_stop(t);
free(t->name);
free(t);
}
}
/*
* Helpers to make sure certain events aren't lost at shutdown
*/
static gboolean
drain_timeout_cb(gpointer user_data)
{
bool *timeout_popped = (bool*) user_data;
*timeout_popped = TRUE;
return FALSE;
}
/*!
* \brief Drain some remaining main loop events then quit it
*
* \param[in,out] mloop Main loop to drain and quit
* \param[in] n Drain up to this many pending events
*/
void
pcmk_quit_main_loop(GMainLoop *mloop, unsigned int n)
{
if ((mloop != NULL) && g_main_loop_is_running(mloop)) {
GMainContext *ctx = g_main_loop_get_context(mloop);
/* Drain up to n events in case some memory clean-up is pending
* (helpful to reduce noise in valgrind output).
*/
for (int i = 0; (i < n) && g_main_context_pending(ctx); ++i) {
g_main_context_dispatch(ctx);
}
g_main_loop_quit(mloop);
}
}
/*!
* \brief Process main loop events while a certain condition is met
*
* \param[in,out] mloop Main loop to process
* \param[in] timer_ms Don't process longer than this amount of time
* \param[in] check Function that returns true if events should be
* processed
*
* \note This function is intended to be called at shutdown if certain important
* events should not be missed. The caller would likely quit the main loop
* or exit after calling this function. The check() function will be
* passed the remaining timeout in milliseconds.
*/
void
pcmk_drain_main_loop(GMainLoop *mloop, guint timer_ms, bool (*check)(guint))
{
bool timeout_popped = FALSE;
guint timer = 0;
GMainContext *ctx = NULL;
CRM_CHECK(mloop && check, return);
ctx = g_main_loop_get_context(mloop);
if (ctx) {
time_t start_time = time(NULL);
timer = g_timeout_add(timer_ms, drain_timeout_cb, &timeout_popped);
while (!timeout_popped
&& check(timer_ms - (time(NULL) - start_time) * 1000)) {
g_main_context_iteration(ctx, TRUE);
}
}
if (!timeout_popped && (timer > 0)) {
g_source_remove(timer);
}
}
diff --git a/lib/common/options.c b/lib/common/options.c
index ee1486f8a8..d26e7807e1 100644
--- a/lib/common/options.c
+++ b/lib/common/options.c
@@ -1,1565 +1,1561 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
-#ifndef _GNU_SOURCE
-# define _GNU_SOURCE
-#endif
-
#include
#include
#include
#include
#include
#include
#include
#include
void
pcmk__cli_help(char cmd)
{
if (cmd == 'v' || cmd == '$') {
printf("Pacemaker %s\n", PACEMAKER_VERSION);
printf("Written by Andrew Beekhof and "
"the Pacemaker project contributors\n");
} else if (cmd == '!') {
printf("Pacemaker %s (Build: %s): %s\n", PACEMAKER_VERSION, BUILD_VERSION, CRM_FEATURES);
}
crm_exit(CRM_EX_OK);
while(1); // above does not return
}
/*
* Option metadata
*/
static const pcmk__cluster_option_t cluster_options[] = {
/* name, old name, type, allowed values,
* default value, validator,
* flags,
* short description,
* long description
*/
{
PCMK_OPT_DC_VERSION, NULL, PCMK_VALUE_VERSION, NULL,
NULL, NULL,
pcmk__opt_controld|pcmk__opt_generated,
N_("Pacemaker version on cluster node elected Designated Controller "
"(DC)"),
N_("Includes a hash which identifies the exact revision the code was "
"built from. Used for diagnostic purposes."),
},
{
PCMK_OPT_CLUSTER_INFRASTRUCTURE, NULL, PCMK_VALUE_STRING, NULL,
NULL, NULL,
pcmk__opt_controld|pcmk__opt_generated,
N_("The messaging layer on which Pacemaker is currently running"),
N_("Used for informational and diagnostic purposes."),
},
{
PCMK_OPT_CLUSTER_NAME, NULL, PCMK_VALUE_STRING, NULL,
NULL, NULL,
pcmk__opt_controld,
N_("An arbitrary name for the cluster"),
N_("This optional value is mostly for users' convenience as desired "
"in administration, but may also be used in Pacemaker "
"configuration rules via the #cluster-name node attribute, and "
"by higher-level tools and resource agents."),
},
{
PCMK_OPT_DC_DEADTIME, NULL, PCMK_VALUE_DURATION, NULL,
"20s", pcmk__valid_interval_spec,
pcmk__opt_controld,
N_("How long to wait for a response from other nodes during start-up"),
N_("The optimal value will depend on the speed and load of your "
"network and the type of switches used."),
},
{
PCMK_OPT_CLUSTER_RECHECK_INTERVAL, NULL, PCMK_VALUE_DURATION, NULL,
"15min", pcmk__valid_interval_spec,
pcmk__opt_controld,
N_("Polling interval to recheck cluster state and evaluate rules "
"with date specifications"),
N_("Pacemaker is primarily event-driven, and looks ahead to know when "
"to recheck cluster state for failure-timeout settings and most "
"time-based rules. However, it will also recheck the cluster after "
"this amount of inactivity, to evaluate rules with date "
"specifications and serve as a fail-safe for certain types of "
"scheduler bugs. A value of 0 disables polling. A positive value "
"sets an interval in seconds, unless other units are specified "
"(for example, \"5min\")."),
},
{
PCMK_OPT_FENCE_REACTION, NULL, PCMK_VALUE_SELECT,
PCMK_VALUE_STOP ", " PCMK_VALUE_PANIC,
PCMK_VALUE_STOP, NULL,
pcmk__opt_controld,
N_("How a cluster node should react if notified of its own fencing"),
N_("A cluster node may receive notification of a \"succeeded\" "
"fencing that targeted it if fencing is misconfigured, or if "
"fabric fencing is in use that doesn't cut cluster communication. "
"Use \"stop\" to attempt to immediately stop Pacemaker and stay "
"stopped, or \"panic\" to attempt to immediately reboot the local "
"node, falling back to stop on failure."),
},
{
PCMK_OPT_ELECTION_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL,
"2min", pcmk__valid_interval_spec,
pcmk__opt_controld|pcmk__opt_advanced,
N_("Declare an election failed if it is not decided within this much "
"time. If you need to adjust this value, it probably indicates "
"the presence of a bug."),
NULL,
},
{
PCMK_OPT_SHUTDOWN_ESCALATION, NULL, PCMK_VALUE_DURATION, NULL,
"20min", pcmk__valid_interval_spec,
pcmk__opt_controld|pcmk__opt_advanced,
N_("Exit immediately if shutdown does not complete within this much "
"time. If you need to adjust this value, it probably indicates "
"the presence of a bug."),
NULL,
},
{
PCMK_OPT_JOIN_INTEGRATION_TIMEOUT, "crmd-integration-timeout",
PCMK_VALUE_DURATION, NULL,
"3min", pcmk__valid_interval_spec,
pcmk__opt_controld|pcmk__opt_advanced,
N_("If you need to adjust this value, it probably indicates "
"the presence of a bug."),
NULL,
},
{
PCMK_OPT_JOIN_FINALIZATION_TIMEOUT, "crmd-finalization-timeout",
PCMK_VALUE_DURATION, NULL,
"30min", pcmk__valid_interval_spec,
pcmk__opt_controld|pcmk__opt_advanced,
N_("If you need to adjust this value, it probably indicates "
"the presence of a bug."),
NULL,
},
{
PCMK_OPT_TRANSITION_DELAY, "crmd-transition-delay", PCMK_VALUE_DURATION,
NULL,
"0s", pcmk__valid_interval_spec,
pcmk__opt_controld|pcmk__opt_advanced,
N_("Enabling this option will slow down cluster recovery under all "
"conditions"),
N_("Delay cluster recovery for this much time to allow for additional "
"events to occur. Useful if your configuration is sensitive to "
"the order in which ping updates arrive."),
},
{
PCMK_OPT_NO_QUORUM_POLICY, NULL, PCMK_VALUE_SELECT,
PCMK_VALUE_STOP ", " PCMK_VALUE_FREEZE ", " PCMK_VALUE_IGNORE
", " PCMK_VALUE_DEMOTE ", " PCMK_VALUE_FENCE_LEGACY,
PCMK_VALUE_STOP, pcmk__valid_no_quorum_policy,
pcmk__opt_schedulerd,
N_("What to do when the cluster does not have quorum"),
NULL,
},
{
PCMK_OPT_SHUTDOWN_LOCK, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_FALSE, pcmk__valid_boolean,
pcmk__opt_schedulerd,
N_("Whether to lock resources to a cleanly shut down node"),
N_("When true, resources active on a node when it is cleanly shut down "
"are kept \"locked\" to that node (not allowed to run elsewhere) "
"until they start again on that node after it rejoins (or for at "
"most shutdown-lock-limit, if set). Stonith resources and "
"Pacemaker Remote connections are never locked. Clone and bundle "
"instances and the promoted role of promotable clones are "
"currently never locked, though support could be added in a future "
"release."),
},
{
PCMK_OPT_SHUTDOWN_LOCK_LIMIT, NULL, PCMK_VALUE_DURATION, NULL,
"0", pcmk__valid_interval_spec,
pcmk__opt_schedulerd,
N_("Do not lock resources to a cleanly shut down node longer than "
"this"),
N_("If shutdown-lock is true and this is set to a nonzero time "
"duration, shutdown locks will expire after this much time has "
"passed since the shutdown was initiated, even if the node has not "
"rejoined."),
},
{
PCMK_OPT_ENABLE_ACL, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_FALSE, pcmk__valid_boolean,
pcmk__opt_based,
N_("Enable Access Control Lists (ACLs) for the CIB"),
NULL,
},
{
PCMK_OPT_SYMMETRIC_CLUSTER, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_TRUE, pcmk__valid_boolean,
pcmk__opt_schedulerd,
N_("Whether resources can run on any node by default"),
NULL,
},
{
PCMK_OPT_MAINTENANCE_MODE, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_FALSE, pcmk__valid_boolean,
pcmk__opt_schedulerd,
N_("Whether the cluster should refrain from monitoring, starting, and "
"stopping resources"),
NULL,
},
{
PCMK_OPT_START_FAILURE_IS_FATAL, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_TRUE, pcmk__valid_boolean,
pcmk__opt_schedulerd,
N_("Whether a start failure should prevent a resource from being "
"recovered on the same node"),
N_("When true, the cluster will immediately ban a resource from a node "
"if it fails to start there. When false, the cluster will instead "
"check the resource's fail count against its migration-threshold.")
},
{
PCMK_OPT_ENABLE_STARTUP_PROBES, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_TRUE, pcmk__valid_boolean,
pcmk__opt_schedulerd,
N_("Whether the cluster should check for active resources during "
"start-up"),
NULL,
},
// Fencing-related options
{
PCMK_OPT_STONITH_ENABLED, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_TRUE, pcmk__valid_boolean,
pcmk__opt_schedulerd|pcmk__opt_advanced,
N_("Whether nodes may be fenced as part of recovery"),
N_("If false, unresponsive nodes are immediately assumed to be "
"harmless, and resources that were active on them may be recovered "
"elsewhere. This can result in a \"split-brain\" situation, "
"potentially leading to data loss and/or service unavailability."),
},
{
PCMK_OPT_STONITH_ACTION, NULL, PCMK_VALUE_SELECT,
PCMK_ACTION_REBOOT ", " PCMK_ACTION_OFF ", " PCMK__ACTION_POWEROFF,
PCMK_ACTION_REBOOT, pcmk__is_fencing_action,
pcmk__opt_schedulerd,
N_("Action to send to fence device when a node needs to be fenced "
"(\"poweroff\" is a deprecated alias for \"off\")"),
NULL,
},
{
PCMK_OPT_STONITH_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL,
"60s", pcmk__valid_interval_spec,
pcmk__opt_schedulerd,
N_("How long to wait for on, off, and reboot fence actions to complete "
"by default"),
NULL,
},
{
PCMK_OPT_HAVE_WATCHDOG, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_FALSE, pcmk__valid_boolean,
pcmk__opt_schedulerd|pcmk__opt_generated,
N_("Whether watchdog integration is enabled"),
N_("This is set automatically by the cluster according to whether SBD "
"is detected to be in use. User-configured values are ignored. "
"The value `true` is meaningful if diskless SBD is used and "
"`stonith-watchdog-timeout` is nonzero. In that case, if fencing "
"is required, watchdog-based self-fencing will be performed via "
"SBD without requiring a fencing resource explicitly configured."),
},
{
/* @COMPAT Currently, unparsable values default to -1 (auto-calculate),
* while missing values default to 0 (disable). All values are accepted
* (unless the controller finds that the value conflicts with the
* SBD_WATCHDOG_TIMEOUT).
*
* At a compatibility break: properly validate as a timeout, let
* either negative values or a particular string like "auto" mean auto-
* calculate, and use 0 as the single default for when the option either
* is unset or fails to validate.
*/
PCMK_OPT_STONITH_WATCHDOG_TIMEOUT, NULL, PCMK_VALUE_TIMEOUT, NULL,
"0", NULL,
pcmk__opt_controld,
N_("How long before nodes can be assumed to be safely down when "
"watchdog-based self-fencing via SBD is in use"),
N_("If this is set to a positive value, lost nodes are assumed to "
"achieve self-fencing using watchdog-based SBD within this much "
"time. This does not require a fencing resource to be explicitly "
"configured, though a fence_watchdog resource can be configured, to "
"limit use to specific nodes. If this is set to 0 (the default), "
"the cluster will never assume watchdog-based self-fencing. If this "
"is set to a negative value, the cluster will use twice the local "
"value of the `SBD_WATCHDOG_TIMEOUT` environment variable if that "
"is positive, or otherwise treat this as 0. WARNING: When used, "
"this timeout must be larger than `SBD_WATCHDOG_TIMEOUT` on all "
"nodes that use watchdog-based SBD, and Pacemaker will refuse to "
"start on any of those nodes where this is not true for the local "
"value or SBD is not active. When this is set to a negative value, "
"`SBD_WATCHDOG_TIMEOUT` must be set to the same value on all nodes "
"that use SBD, otherwise data corruption or loss could occur."),
},
{
PCMK_OPT_STONITH_MAX_ATTEMPTS, NULL, PCMK_VALUE_SCORE, NULL,
"10", pcmk__valid_positive_int,
pcmk__opt_controld,
N_("How many times fencing can fail before it will no longer be "
"immediately re-attempted on a target"),
NULL,
},
{
PCMK_OPT_CONCURRENT_FENCING, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK__CONCURRENT_FENCING_DEFAULT, pcmk__valid_boolean,
pcmk__opt_schedulerd,
N_("Allow performing fencing operations in parallel"),
NULL,
},
{
PCMK_OPT_STARTUP_FENCING, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_TRUE, pcmk__valid_boolean,
pcmk__opt_schedulerd|pcmk__opt_advanced,
N_("Whether to fence unseen nodes at start-up"),
N_("Setting this to false may lead to a \"split-brain\" situation, "
"potentially leading to data loss and/or service unavailability."),
},
{
PCMK_OPT_PRIORITY_FENCING_DELAY, NULL, PCMK_VALUE_DURATION, NULL,
"0", pcmk__valid_interval_spec,
pcmk__opt_schedulerd,
N_("Apply fencing delay targeting the lost nodes with the highest "
"total resource priority"),
N_("Apply specified delay for the fencings that are targeting the lost "
"nodes with the highest total resource priority in case we don't "
"have the majority of the nodes in our cluster partition, so that "
"the more significant nodes potentially win any fencing match, "
"which is especially meaningful under split-brain of 2-node "
"cluster. A promoted resource instance takes the base priority + 1 "
"on calculation if the base priority is not 0. Any static/random "
"delays that are introduced by `pcmk_delay_base/max` configured "
"for the corresponding fencing resources will be added to this "
"delay. This delay should be significantly greater than, safely "
"twice, the maximum `pcmk_delay_base/max`. By default, priority "
"fencing delay is disabled."),
},
{
PCMK_OPT_NODE_PENDING_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL,
"0", pcmk__valid_interval_spec,
pcmk__opt_schedulerd,
N_("How long to wait for a node that has joined the cluster to join "
"the controller process group"),
N_("Fence nodes that do not join the controller process group within "
"this much time after joining the cluster, to allow the cluster "
"to continue managing resources. A value of 0 means never fence "
"pending nodes. Setting the value to 2h means fence nodes after "
"2 hours."),
},
{
PCMK_OPT_CLUSTER_DELAY, NULL, PCMK_VALUE_DURATION, NULL,
"60s", pcmk__valid_interval_spec,
pcmk__opt_schedulerd,
N_("Maximum time for node-to-node communication"),
N_("The node elected Designated Controller (DC) will consider an action "
"failed if it does not get a response from the node executing the "
"action within this time (after considering the action's own "
"timeout). The \"correct\" value will depend on the speed and "
"load of your network and cluster nodes.")
},
// Limits
{
PCMK_OPT_LOAD_THRESHOLD, NULL, PCMK_VALUE_PERCENTAGE, NULL,
"80%", pcmk__valid_percentage,
pcmk__opt_controld,
N_("Maximum amount of system load that should be used by cluster "
"nodes"),
N_("The cluster will slow down its recovery process when the amount of "
"system resources used (currently CPU) approaches this limit"),
},
{
PCMK_OPT_NODE_ACTION_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL,
"0", pcmk__valid_int,
pcmk__opt_controld,
N_("Maximum number of jobs that can be scheduled per node (defaults to "
"2x cores)"),
NULL,
},
{
PCMK_OPT_BATCH_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL,
"0", pcmk__valid_int,
pcmk__opt_schedulerd,
N_("Maximum number of jobs that the cluster may execute in parallel "
"across all nodes"),
N_("The \"correct\" value will depend on the speed and load of your "
"network and cluster nodes. If set to 0, the cluster will "
"impose a dynamically calculated limit when any node has a "
"high load."),
},
{
PCMK_OPT_MIGRATION_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL,
"-1", pcmk__valid_int,
pcmk__opt_schedulerd,
N_("The number of live migration actions that the cluster is allowed "
"to execute in parallel on a node (-1 means no limit)"),
NULL,
},
{
/* @TODO This is actually ignored if not strictly positive. We should
* overhaul value types in Pacemaker Explained. There are lots of
* inaccurate ranges (assumptions of 32-bit width, "nonnegative" when
* positive is required, etc.).
*
* Maybe a single integer type with the allowed range specified would be
* better.
*
* Drop the PCMK_VALUE_NONNEGATIVE_INTEGER constant if we do this before
* a release.
*/
PCMK_OPT_CLUSTER_IPC_LIMIT, NULL, PCMK_VALUE_NONNEGATIVE_INTEGER, NULL,
"500", pcmk__valid_positive_int,
pcmk__opt_based,
N_("Maximum IPC message backlog before disconnecting a cluster daemon"),
N_("Raise this if log has \"Evicting client\" messages for cluster "
"daemon PIDs (a good value is the number of resources in the "
"cluster multiplied by the number of nodes)."),
},
// Orphans and stopping
{
PCMK_OPT_STOP_ALL_RESOURCES, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_FALSE, pcmk__valid_boolean,
pcmk__opt_schedulerd,
N_("Whether the cluster should stop all active resources"),
NULL,
},
{
PCMK_OPT_STOP_ORPHAN_RESOURCES, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_TRUE, pcmk__valid_boolean,
pcmk__opt_schedulerd,
N_("Whether to stop resources that were removed from the "
"configuration"),
NULL,
},
{
PCMK_OPT_STOP_ORPHAN_ACTIONS, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_TRUE, pcmk__valid_boolean,
pcmk__opt_schedulerd,
N_("Whether to cancel recurring actions removed from the "
"configuration"),
NULL,
},
{
PCMK__OPT_REMOVE_AFTER_STOP, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_FALSE, pcmk__valid_boolean,
pcmk__opt_schedulerd|pcmk__opt_deprecated,
N_("Whether to remove stopped resources from the executor"),
N_("Values other than default are poorly tested and potentially "
"dangerous."),
},
// Storing inputs
{
PCMK_OPT_PE_ERROR_SERIES_MAX, NULL, PCMK_VALUE_INTEGER, NULL,
"-1", pcmk__valid_int,
pcmk__opt_schedulerd,
N_("The number of scheduler inputs resulting in errors to save"),
N_("Zero to disable, -1 to store unlimited."),
},
{
PCMK_OPT_PE_WARN_SERIES_MAX, NULL, PCMK_VALUE_INTEGER, NULL,
"5000", pcmk__valid_int,
pcmk__opt_schedulerd,
N_("The number of scheduler inputs resulting in warnings to save"),
N_("Zero to disable, -1 to store unlimited."),
},
{
PCMK_OPT_PE_INPUT_SERIES_MAX, NULL, PCMK_VALUE_INTEGER, NULL,
"4000", pcmk__valid_int,
pcmk__opt_schedulerd,
N_("The number of scheduler inputs without errors or warnings to save"),
N_("Zero to disable, -1 to store unlimited."),
},
// Node health
{
PCMK_OPT_NODE_HEALTH_STRATEGY, NULL, PCMK_VALUE_SELECT,
PCMK_VALUE_NONE ", " PCMK_VALUE_MIGRATE_ON_RED ", "
PCMK_VALUE_ONLY_GREEN ", " PCMK_VALUE_PROGRESSIVE ", "
PCMK_VALUE_CUSTOM,
PCMK_VALUE_NONE, pcmk__validate_health_strategy,
pcmk__opt_schedulerd,
N_("How cluster should react to node health attributes"),
N_("Requires external entities to create node attributes (named with "
"the prefix \"#health\") with values \"red\", \"yellow\", or "
"\"green\".")
},
{
PCMK_OPT_NODE_HEALTH_BASE, NULL, PCMK_VALUE_SCORE, NULL,
"0", pcmk__valid_int,
pcmk__opt_schedulerd,
N_("Base health score assigned to a node"),
N_("Only used when \"node-health-strategy\" is set to "
"\"progressive\"."),
},
{
PCMK_OPT_NODE_HEALTH_GREEN, NULL, PCMK_VALUE_SCORE, NULL,
"0", pcmk__valid_int,
pcmk__opt_schedulerd,
N_("The score to use for a node health attribute whose value is "
"\"green\""),
N_("Only used when \"node-health-strategy\" is set to \"custom\" or "
"\"progressive\"."),
},
{
PCMK_OPT_NODE_HEALTH_YELLOW, NULL, PCMK_VALUE_SCORE, NULL,
"0", pcmk__valid_int,
pcmk__opt_schedulerd,
N_("The score to use for a node health attribute whose value is "
"\"yellow\""),
N_("Only used when \"node-health-strategy\" is set to \"custom\" or "
"\"progressive\"."),
},
{
PCMK_OPT_NODE_HEALTH_RED, NULL, PCMK_VALUE_SCORE, NULL,
"-INFINITY", pcmk__valid_int,
pcmk__opt_schedulerd,
N_("The score to use for a node health attribute whose value is "
"\"red\""),
N_("Only used when \"node-health-strategy\" is set to \"custom\" or "
"\"progressive\".")
},
// Placement strategy
{
PCMK_OPT_PLACEMENT_STRATEGY, NULL, PCMK_VALUE_SELECT,
PCMK_VALUE_DEFAULT ", " PCMK_VALUE_UTILIZATION ", "
PCMK_VALUE_MINIMAL ", " PCMK_VALUE_BALANCED,
PCMK_VALUE_DEFAULT, pcmk__valid_placement_strategy,
pcmk__opt_schedulerd,
N_("How the cluster should allocate resources to nodes"),
NULL,
},
{ NULL, },
};
static const pcmk__cluster_option_t fencing_params[] = {
/* name, old name, type, allowed values,
* default value, validator,
* flags,
* short description,
* long description
*/
{
PCMK_STONITH_HOST_ARGUMENT, NULL, PCMK_VALUE_STRING, NULL,
"port", NULL,
pcmk__opt_advanced,
N_("An alternate parameter to supply instead of 'port'"),
N_("Some devices do not support the standard 'port' parameter or may "
"provide additional ones. Use this to specify an alternate, device-"
"specific, parameter that should indicate the machine to be "
"fenced. A value of \"none\" can be used to tell the cluster not "
"to supply any additional parameters."),
},
{
PCMK_STONITH_HOST_MAP, NULL, PCMK_VALUE_STRING, NULL,
NULL, NULL,
pcmk__opt_none,
N_("A mapping of node names to port numbers for devices that do not "
"support node names."),
N_("For example, \"node1:1;node2:2,3\" would tell the cluster to use "
"port 1 for node1 and ports 2 and 3 for node2."),
},
{
PCMK_STONITH_HOST_LIST, NULL, PCMK_VALUE_STRING, NULL,
NULL, NULL,
pcmk__opt_none,
N_("Nodes targeted by this device"),
N_("Comma-separated list of nodes that can be targeted by this device "
"(for example, \"node1,node2,node3\"). If pcmk_host_check is "
"\"static-list\", either this or pcmk_host_map must be set."),
},
{
PCMK_STONITH_HOST_CHECK, NULL, PCMK_VALUE_SELECT,
PCMK_VALUE_DYNAMIC_LIST ", " PCMK_VALUE_STATIC_LIST ", "
PCMK_VALUE_STATUS ", " PCMK_VALUE_NONE,
NULL, NULL,
pcmk__opt_none,
N_("How to determine which nodes can be targeted by the device"),
N_("Use \"dynamic-list\" to query the device via the 'list' command; "
"\"static-list\" to check the pcmk_host_list attribute; "
"\"status\" to query the device via the 'status' command; or "
"\"none\" to assume every device can fence every node. "
"The default value is \"static-list\" if pcmk_host_map or "
"pcmk_host_list is set; otherwise \"dynamic-list\" if the device "
"supports the list operation; otherwise \"status\" if the device "
"supports the status operation; otherwise \"none\""),
},
{
PCMK_STONITH_DELAY_MAX, NULL, PCMK_VALUE_DURATION, NULL,
"0s", NULL,
pcmk__opt_none,
N_("Enable a delay of no more than the time specified before executing "
"fencing actions."),
N_("Enable a delay of no more than the time specified before executing "
"fencing actions. Pacemaker derives the overall delay by taking "
"the value of pcmk_delay_base and adding a random delay value such "
"that the sum is kept below this maximum."),
},
{
PCMK_STONITH_DELAY_BASE, NULL, PCMK_VALUE_STRING, NULL,
"0s", NULL,
pcmk__opt_none,
N_("Enable a base delay for fencing actions and specify base delay "
"value."),
N_("This enables a static delay for fencing actions, which can help "
"avoid \"death matches\" where two nodes try to fence each other "
"at the same time. If pcmk_delay_max is also used, a random delay "
"will be added such that the total delay is kept below that value. "
"This can be set to a single time value to apply to any node "
"targeted by this device (useful if a separate device is "
"configured for each target), or to a node map (for example, "
"\"node1:1s;node2:5\") to set a different value for each target."),
},
{
PCMK_STONITH_ACTION_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL,
"1", NULL,
pcmk__opt_none,
N_("The maximum number of actions can be performed in parallel on this "
"device"),
N_("Cluster property concurrent-fencing=\"true\" needs to be "
"configured first. Then use this to specify the maximum number of "
"actions can be performed in parallel on this device. A value of "
"-1 means an unlimited number of actions can be performed in "
"parallel."),
},
{
"pcmk_reboot_action", NULL, PCMK_VALUE_STRING, NULL,
PCMK_ACTION_REBOOT, NULL,
pcmk__opt_advanced,
N_("An alternate command to run instead of 'reboot'"),
N_("Some devices do not support the standard commands or may provide "
"additional ones. Use this to specify an alternate, device-"
"specific, command that implements the 'reboot' action."),
},
{
"pcmk_reboot_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL,
"60s", NULL,
pcmk__opt_advanced,
N_("Specify an alternate timeout to use for 'reboot' actions instead "
"of stonith-timeout"),
N_("Some devices need much more/less time to complete than normal. "
"Use this to specify an alternate, device-specific, timeout for "
"'reboot' actions."),
},
{
"pcmk_reboot_retries", NULL, PCMK_VALUE_INTEGER, NULL,
"2", NULL,
pcmk__opt_advanced,
N_("The maximum number of times to try the 'reboot' command within the "
"timeout period"),
N_("Some devices do not support multiple connections. Operations may "
"\"fail\" if the device is busy with another task. In that case, "
"Pacemaker will automatically retry the operation if there is time "
"remaining. Use this option to alter the number of times Pacemaker "
"tries a 'reboot' action before giving up."),
},
{
"pcmk_off_action", NULL, PCMK_VALUE_STRING, NULL,
PCMK_ACTION_OFF, NULL,
pcmk__opt_advanced,
N_("An alternate command to run instead of 'off'"),
N_("Some devices do not support the standard commands or may provide "
"additional ones. Use this to specify an alternate, device-"
"specific, command that implements the 'off' action."),
},
{
"pcmk_off_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL,
"60s", NULL,
pcmk__opt_advanced,
N_("Specify an alternate timeout to use for 'off' actions instead of "
"stonith-timeout"),
N_("Some devices need much more/less time to complete than normal. "
"Use this to specify an alternate, device-specific, timeout for "
"'off' actions."),
},
{
"pcmk_off_retries", NULL, PCMK_VALUE_INTEGER, NULL,
"2", NULL,
pcmk__opt_advanced,
N_("The maximum number of times to try the 'off' command within the "
"timeout period"),
N_("Some devices do not support multiple connections. Operations may "
"\"fail\" if the device is busy with another task. In that case, "
"Pacemaker will automatically retry the operation if there is time "
"remaining. Use this option to alter the number of times Pacemaker "
"tries a 'off' action before giving up."),
},
{
"pcmk_on_action", NULL, PCMK_VALUE_STRING, NULL,
PCMK_ACTION_ON, NULL,
pcmk__opt_advanced,
N_("An alternate command to run instead of 'on'"),
N_("Some devices do not support the standard commands or may provide "
"additional ones. Use this to specify an alternate, device-"
"specific, command that implements the 'on' action."),
},
{
"pcmk_on_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL,
"60s", NULL,
pcmk__opt_advanced,
N_("Specify an alternate timeout to use for 'on' actions instead of "
"stonith-timeout"),
N_("Some devices need much more/less time to complete than normal. "
"Use this to specify an alternate, device-specific, timeout for "
"'on' actions."),
},
{
"pcmk_on_retries", NULL, PCMK_VALUE_INTEGER, NULL,
"2", NULL,
pcmk__opt_advanced,
N_("The maximum number of times to try the 'on' command within the "
"timeout period"),
N_("Some devices do not support multiple connections. Operations may "
"\"fail\" if the device is busy with another task. In that case, "
"Pacemaker will automatically retry the operation if there is time "
"remaining. Use this option to alter the number of times Pacemaker "
"tries a 'on' action before giving up."),
},
{
"pcmk_list_action", NULL, PCMK_VALUE_STRING, NULL,
PCMK_ACTION_LIST, NULL,
pcmk__opt_advanced,
N_("An alternate command to run instead of 'list'"),
N_("Some devices do not support the standard commands or may provide "
"additional ones. Use this to specify an alternate, device-"
"specific, command that implements the 'list' action."),
},
{
"pcmk_list_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL,
"60s", NULL,
pcmk__opt_advanced,
N_("Specify an alternate timeout to use for 'list' actions instead of "
"stonith-timeout"),
N_("Some devices need much more/less time to complete than normal. "
"Use this to specify an alternate, device-specific, timeout for "
"'list' actions."),
},
{
"pcmk_list_retries", NULL, PCMK_VALUE_INTEGER, NULL,
"2", NULL,
pcmk__opt_advanced,
N_("The maximum number of times to try the 'list' command within the "
"timeout period"),
N_("Some devices do not support multiple connections. Operations may "
"\"fail\" if the device is busy with another task. In that case, "
"Pacemaker will automatically retry the operation if there is time "
"remaining. Use this option to alter the number of times Pacemaker "
"tries a 'list' action before giving up."),
},
{
"pcmk_monitor_action", NULL, PCMK_VALUE_STRING, NULL,
PCMK_ACTION_MONITOR, NULL,
pcmk__opt_advanced,
N_("An alternate command to run instead of 'monitor'"),
N_("Some devices do not support the standard commands or may provide "
"additional ones. Use this to specify an alternate, device-"
"specific, command that implements the 'monitor' action."),
},
{
"pcmk_monitor_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL,
"60s", NULL,
pcmk__opt_advanced,
N_("Specify an alternate timeout to use for 'monitor' actions instead "
"of stonith-timeout"),
N_("Some devices need much more/less time to complete than normal. "
"Use this to specify an alternate, device-specific, timeout for "
"'monitor' actions."),
},
{
"pcmk_monitor_retries", NULL, PCMK_VALUE_INTEGER, NULL,
"2", NULL,
pcmk__opt_advanced,
N_("The maximum number of times to try the 'monitor' command within "
"the timeout period"),
N_("Some devices do not support multiple connections. Operations may "
"\"fail\" if the device is busy with another task. In that case, "
"Pacemaker will automatically retry the operation if there is time "
"remaining. Use this option to alter the number of times Pacemaker "
"tries a 'monitor' action before giving up."),
},
{
"pcmk_status_action", NULL, PCMK_VALUE_STRING, NULL,
PCMK_ACTION_STATUS, NULL,
pcmk__opt_advanced,
N_("An alternate command to run instead of 'status'"),
N_("Some devices do not support the standard commands or may provide "
"additional ones. Use this to specify an alternate, device-"
"specific, command that implements the 'status' action."),
},
{
"pcmk_status_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL,
"60s", NULL,
pcmk__opt_advanced,
N_("Specify an alternate timeout to use for 'status' actions instead "
"of stonith-timeout"),
N_("Some devices need much more/less time to complete than normal. "
"Use this to specify an alternate, device-specific, timeout for "
"'status' actions."),
},
{
"pcmk_status_retries", NULL, PCMK_VALUE_INTEGER, NULL,
"2", NULL,
pcmk__opt_advanced,
N_("The maximum number of times to try the 'status' command within "
"the timeout period"),
N_("Some devices do not support multiple connections. Operations may "
"\"fail\" if the device is busy with another task. In that case, "
"Pacemaker will automatically retry the operation if there is time "
"remaining. Use this option to alter the number of times Pacemaker "
"tries a 'status' action before giving up."),
},
{ NULL, },
};
static const pcmk__cluster_option_t primitive_meta[] = {
/* name, old name, type, allowed values,
* default value, validator,
* flags,
* short description,
* long description
*/
{
PCMK_META_PRIORITY, NULL, PCMK_VALUE_SCORE, NULL,
"0", NULL,
pcmk__opt_none,
N_("Resource assignment priority"),
N_("If not all resources can be active, the cluster will stop "
"lower-priority resources in order to keep higher-priority ones "
"active."),
},
{
PCMK_META_CRITICAL, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_TRUE, NULL,
pcmk__opt_none,
N_("Default value for influence in colocation constraints"),
N_("Use this value as the default for influence in all colocation "
"constraints involving this resource, as well as in the implicit "
"colocation constraints created if this resource is in a group."),
},
{
PCMK_META_TARGET_ROLE, NULL, PCMK_VALUE_SELECT,
PCMK_ROLE_STOPPED ", " PCMK_ROLE_STARTED ", "
PCMK_ROLE_UNPROMOTED ", " PCMK_ROLE_PROMOTED,
PCMK_ROLE_STARTED, NULL,
pcmk__opt_none,
N_("State the cluster should attempt to keep this resource in"),
N_("\"Stopped\" forces the resource to be stopped. "
"\"Started\" allows the resource to be started (and in the case of "
"promotable clone resources, promoted if appropriate). "
"\"Unpromoted\" allows the resource to be started, but only in the "
"unpromoted role if the resource is promotable. "
"\"Promoted\" is equivalent to \"Started\"."),
},
{
PCMK_META_IS_MANAGED, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_TRUE, NULL,
pcmk__opt_none,
N_("Whether the cluster is allowed to actively change the resource's "
"state"),
N_("If false, the cluster will not start, stop, promote, or demote the "
"resource on any node. Recurring actions for the resource are "
"unaffected. If true, a true value for the maintenance-mode "
"cluster option, the maintenance node attribute, or the "
"maintenance resource meta-attribute overrides this."),
},
{
PCMK_META_MAINTENANCE, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_FALSE, NULL,
pcmk__opt_none,
N_("If true, the cluster will not schedule any actions involving the "
"resource"),
N_("If true, the cluster will not start, stop, promote, or demote the "
"resource on any node, and will pause any recurring monitors "
"(except those specifying role as \"Stopped\"). If false, a true "
"value for the maintenance-mode cluster option or maintenance node "
"attribute overrides this."),
},
{
PCMK_META_RESOURCE_STICKINESS, NULL, PCMK_VALUE_SCORE, NULL,
NULL, NULL,
pcmk__opt_none,
N_("Score to add to the current node when a resource is already "
"active"),
N_("Score to add to the current node when a resource is already "
"active. This allows running resources to stay where they are, "
"even if they would be placed elsewhere if they were being started "
"from a stopped state. "
"The default is 1 for individual clone instances, and 0 for all "
"other resources."),
},
{
PCMK_META_REQUIRES, NULL, PCMK_VALUE_SELECT,
PCMK_VALUE_NOTHING ", " PCMK_VALUE_QUORUM ", "
PCMK_VALUE_FENCING ", " PCMK_VALUE_UNFENCING,
NULL, NULL,
pcmk__opt_none,
N_("Conditions under which the resource can be started"),
N_("Conditions under which the resource can be started. "
"\"nothing\" means the cluster can always start this resource. "
"\"quorum\" means the cluster can start this resource only if a "
"majority of the configured nodes are active. "
"\"fencing\" means the cluster can start this resource only if a "
"majority of the configured nodes are active and any failed or "
"unknown nodes have been fenced. "
"\"unfencing\" means the cluster can start this resource only if "
"a majority of the configured nodes are active and any failed or "
"unknown nodes have been fenced, and only on nodes that have been "
"unfenced. "
"The default is \"quorum\" for resources with a class of stonith; "
"otherwise, \"unfencing\" if unfencing is active in the cluster; "
"otherwise, \"fencing\" if the stonith-enabled cluster option is "
"true; "
"otherwise, \"quorum\"."),
},
{
PCMK_META_MIGRATION_THRESHOLD, NULL, PCMK_VALUE_SCORE, NULL,
PCMK_VALUE_INFINITY, NULL,
pcmk__opt_none,
N_("Number of failures on a node before the resource becomes "
"ineligible to run there."),
N_("Number of failures that may occur for this resource on a node, "
"before that node is marked ineligible to host this resource. A "
"value of 0 indicates that this feature is disabled (the node will "
"never be marked ineligible). By contrast, the cluster treats "
"\"INFINITY\" (the default) as a very large but finite number. "
"This option has an effect only if the failed operation specifies "
"its on-fail attribute as \"restart\" (the default), and "
"additionally for failed start operations, if the "
"start-failure-is-fatal cluster property is set to false."),
},
{
PCMK_META_FAILURE_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL,
"0", NULL,
pcmk__opt_none,
N_("Number of seconds before acting as if a failure had not occurred"),
N_("Number of seconds after a failed action for this resource before "
"acting as if the failure had not occurred, and potentially "
"allowing the resource back to the node on which it failed. "
"A value of 0 indicates that this feature is disabled."),
},
{
PCMK_META_MULTIPLE_ACTIVE, NULL, PCMK_VALUE_SELECT,
PCMK_VALUE_BLOCK ", " PCMK_VALUE_STOP_ONLY ", "
PCMK_VALUE_STOP_START ", " PCMK_VALUE_STOP_UNEXPECTED,
PCMK_VALUE_STOP_START, NULL,
pcmk__opt_none,
N_("What to do if the cluster finds the resource active on more than "
"one node"),
N_("What to do if the cluster finds the resource active on more than "
"one node. "
"\"block\" means to mark the resource as unmanaged. "
"\"stop_only\" means to stop all active instances of this resource "
"and leave them stopped. "
"\"stop_start\" means to stop all active instances of this "
"resource and start the resource in one location only. "
"\"stop_unexpected\" means to stop all active instances of this "
"resource except where the resource should be active. (This should "
"be used only when extra instances are not expected to disrupt "
"existing instances, and the resource agent's monitor of an "
"existing instance is capable of detecting any problems that could "
"be caused. Note that any resources ordered after this one will "
"still need to be restarted.)"),
},
{
PCMK_META_ALLOW_MIGRATE, NULL, PCMK_VALUE_BOOLEAN, NULL,
NULL, NULL,
pcmk__opt_none,
N_("Whether the cluster should try to \"live migrate\" this resource "
"when it needs to be moved"),
N_("Whether the cluster should try to \"live migrate\" this resource "
"when it needs to be moved. "
"The default is true for ocf:pacemaker:remote resources, and false "
"otherwise."),
},
{
PCMK_META_ALLOW_UNHEALTHY_NODES, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_FALSE, NULL,
pcmk__opt_none,
N_("Whether the resource should be allowed to run on a node even if "
"the node's health score would otherwise prevent it"),
NULL,
},
{
PCMK_META_CONTAINER_ATTRIBUTE_TARGET, NULL, PCMK_VALUE_STRING, NULL,
NULL, NULL,
pcmk__opt_none,
N_("Where to check user-defined node attributes"),
N_("Whether to check user-defined node attributes on the physical host "
"where a container is running or on the local node. This is "
"usually set for a bundle resource and inherited by the bundle's "
"primitive resource. "
"A value of \"host\" means to check user-defined node attributes "
"on the underlying physical host. Any other value means to check "
"user-defined node attributes on the local node (for a bundled "
"primitive resource, this is the bundle node)."),
},
{
PCMK_META_REMOTE_NODE, NULL, PCMK_VALUE_STRING, NULL,
NULL, NULL,
pcmk__opt_none,
N_("Name of the Pacemaker Remote guest node this resource is "
"associated with, if any"),
N_("Name of the Pacemaker Remote guest node this resource is "
"associated with, if any. If specified, this both enables the "
"resource as a guest node and defines the unique name used to "
"identify the guest node. The guest must be configured to run the "
"Pacemaker Remote daemon when it is started. "
"WARNING: This value cannot overlap with any resource or node "
"IDs."),
},
{
PCMK_META_REMOTE_ADDR, NULL, PCMK_VALUE_STRING, NULL,
NULL, NULL,
pcmk__opt_none,
N_("If remote-node is specified, the IP address or hostname used to "
"connect to the guest via Pacemaker Remote"),
N_("If remote-node is specified, the IP address or hostname used to "
"connect to the guest via Pacemaker Remote. The Pacemaker Remote "
"daemon on the guest must be configured to accept connections on "
"this address. "
"The default is the value of the remote-node meta-attribute."),
},
{
PCMK_META_REMOTE_PORT, NULL, PCMK_VALUE_PORT, NULL,
"3121", NULL,
pcmk__opt_none,
N_("If remote-node is specified, port on the guest used for its "
"Pacemaker Remote connection"),
N_("If remote-node is specified, the port on the guest used for its "
"Pacemaker Remote connection. The Pacemaker Remote daemon on the "
"guest must be configured to listen on this port."),
},
{
PCMK_META_REMOTE_CONNECT_TIMEOUT, NULL, PCMK_VALUE_TIMEOUT, NULL,
"60s", NULL,
pcmk__opt_none,
N_("If remote-node is specified, how long before a pending Pacemaker "
"Remote guest connection times out."),
NULL,
},
{
PCMK_META_REMOTE_ALLOW_MIGRATE, NULL, PCMK_VALUE_BOOLEAN, NULL,
PCMK_VALUE_TRUE, NULL,
pcmk__opt_none,
N_("If remote-node is specified, this acts as the allow-migrate "
"meta-attribute for the implicit remote connection resource "
"(ocf:pacemaker:remote)."),
NULL,
},
{ NULL, },
};
/*
* Environment variable option handling
*/
/*!
* \internal
* \brief Get the value of a Pacemaker environment variable option
*
* If an environment variable option is set, with either a PCMK_ or (for
* backward compatibility) HA_ prefix, log and return the value.
*
* \param[in] option Environment variable name (without prefix)
*
* \return Value of environment variable option, or NULL in case of
* option name too long or value not found
*/
const char *
pcmk__env_option(const char *option)
{
const char *const prefixes[] = {"PCMK_", "HA_"};
char env_name[NAME_MAX];
const char *value = NULL;
CRM_CHECK(!pcmk__str_empty(option), return NULL);
for (int i = 0; i < PCMK__NELEM(prefixes); i++) {
int rv = snprintf(env_name, NAME_MAX, "%s%s", prefixes[i], option);
if (rv < 0) {
crm_err("Failed to write %s%s to buffer: %s", prefixes[i], option,
strerror(errno));
return NULL;
}
if (rv >= sizeof(env_name)) {
crm_trace("\"%s%s\" is too long", prefixes[i], option);
continue;
}
value = getenv(env_name);
if (value != NULL) {
crm_trace("Found %s = %s", env_name, value);
return value;
}
}
crm_trace("Nothing found for %s", option);
return NULL;
}
/*!
* \brief Set or unset a Pacemaker environment variable option
*
* Set an environment variable option with a \c "PCMK_" prefix and optionally
* an \c "HA_" prefix for backward compatibility.
*
* \param[in] option Environment variable name (without prefix)
* \param[in] value New value (or NULL to unset)
* \param[in] compat If false and \p value is not \c NULL, set only
* \c "PCMK_