Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ab65cc7
Refactor: libcrmcommon: Use g_strsplit() in add_possible_values_* funcs
nrwahl2 Sep 17, 2025
000e151
Refactor: libcrmcommon: Use g_strsplit() in add_desc_xml()
nrwahl2 Sep 17, 2025
18da485
Refactor: libcrmservice: Rename services_os_get_single_directory_list()
nrwahl2 Sep 17, 2025
a3f881c
Low: libcrmservice: Fix memory leaks when listing directory contents
nrwahl2 Sep 17, 2025
486e90d
Refactor: libcrmservice: Assert on memory error in services__list_dir()
nrwahl2 Sep 17, 2025
5e3ad4a
Refactor: libcrmservice: Use pcmk__any_flags_set() in services__list_dir
nrwahl2 Sep 17, 2025
f52beb7
Refactor: libcrmservice: Rename services_os_get_directory_list()
nrwahl2 Sep 17, 2025
901382b
Refactor: libcrmservice: Use g_strsplit() for getting directory list
nrwahl2 Sep 17, 2025
02a141f
Low: build, libcrmservice: initdir must be a single directory
nrwahl2 Sep 17, 2025
5ddfe9c
Refactor: libcrmservice: Drop services__list_dirs()
nrwahl2 Sep 17, 2025
5247e71
Refactor: libcrmservice: Drop internal call to get_directory_list()
nrwahl2 Sep 17, 2025
d56111f
API: libcrmservice: Deprecate get_directory_list()
nrwahl2 Sep 17, 2025
e6c1700
Refactor: libcrmservice: Clean services_os_get_directory_list_provider
nrwahl2 Sep 17, 2025
853d999
Refactor: libcrmservice: Simplify resources_os_list_ocf_agents()
nrwahl2 Sep 18, 2025
0a1889d
Refactor: libcrmservice: Avoid a services__list_dir() call
nrwahl2 Sep 18, 2025
eadac78
Refactor: libcrmservice: Drop third argument of services__list_dir()
nrwahl2 Sep 18, 2025
9f0953b
Low: libcrmservice: List only the requested directory contents
nrwahl2 Sep 18, 2025
dffb052
Refactor: libcrmservice: Use scandir() filters for services__list_dir()
nrwahl2 Sep 18, 2025
5ac8c25
Refactor: libcrmservice: Rename resources_os_list_ocf_providers()
nrwahl2 Sep 18, 2025
1723718
Refactor: libcrmservice: Rename resources_os_list_ocf_agents()
nrwahl2 Sep 18, 2025
6d6cfd6
Refactor: libcrmservice: Simplify services__ocf_agent_exists() somewhat
nrwahl2 Sep 18, 2025
e30d524
Refactor: libcrmservice: Use g_strsplit() in services__ocf_agent_exists
nrwahl2 Sep 18, 2025
20e0bf4
Refactor: libcrmservice: Use g_strsplit() in services__ocf_prepare()
nrwahl2 Sep 18, 2025
7641bbe
Refactor: libcrmservice: Return path from services__ocf_agent_exists()
nrwahl2 Sep 18, 2025
0229b55
Refactor: libcrmservice: Reduce duplication in services__ocf_prepare()
nrwahl2 Sep 18, 2025
b550685
Refactor: fencer: Reduce nesting in get_action_delay_base()
nrwahl2 Sep 18, 2025
dfc256f
Log: fencer: Fix a format string
nrwahl2 Sep 18, 2025
1d583ae
Log: fencer: Log an error for empty pcmk_delay_base mapping key
nrwahl2 Sep 18, 2025
36c8129
Refactor: fencer: Use g_strsplit() for pcmk_delay_base mappings
nrwahl2 Sep 18, 2025
a17c005
Refactor: fencer: Use g_strsplit_set() in get_action_delay_base()
nrwahl2 Sep 18, 2025
d20a904
Refactor: fencer: Functionize part of get_action_delay_base()
nrwahl2 Sep 18, 2025
d966425
Refactor: fencer: Avoid some more nesting in get_action_delay_base()
nrwahl2 Sep 18, 2025
e01a014
Refactor: fencer: Functionize getting delay base for target
nrwahl2 Sep 18, 2025
5c590f6
Low: fencer: Fix ISO 8601 interval parsing in pcmk_delay_base
nrwahl2 Sep 18, 2025
0f71e08
Test: cts-fencing: Test pcmk_delay_base/pcmk_delay_max properly
nrwahl2 Sep 18, 2025
a5bd5c8
Refactor: fencer: Make build_port_aliases():lpc loop-scope and rename
nrwahl2 Sep 19, 2025
3719667
Refactor: fencer: Assume build_port_aliases():targets is non-NULL
nrwahl2 Sep 19, 2025
65c7d99
Log: fencer: Drop unhelpful message from build_port_aliases()
nrwahl2 Sep 19, 2025
406369c
Refactor: fencer: Don't set build_port_aliases():value to NULL
nrwahl2 Sep 19, 2025
d57c00d
Refactor: various: Use correct pcmk__assert_alloc() arg order
nrwahl2 Sep 19, 2025
2ebf66b
Low: fencer: Drop support for escaped characters in pcmk_host_map
nrwahl2 Sep 19, 2025
ec51073
Feature: fencer: Improve validation of pcmk_host_map
nrwahl2 Sep 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions cts/cts-fencing.in
Original file line number Diff line number Diff line change
Expand Up @@ -510,21 +510,28 @@ class FenceTests(Tests):
args='--output-as=xml -R true1 -a fence_dummy -o mode=pass -o "pcmk_host_list=node1 node2 node3" -o pcmk_delay_base=1')
test.add_cmd("stonith_admin",
args='--output-as=xml -R false1 -a fence_dummy -o mode=fail -o "pcmk_host_list=node1 node2 node3" -o pcmk_delay_base=1')
# Resulting "random" delay will always be 1 since (rand() % (delay_max - delay_base)) is always 0 here

# Resulting "random" delay will be either 1 or 2 seconds
test.add_cmd("stonith_admin",
args='--output-as=xml -R true2 -a fence_dummy -o mode=pass -o "pcmk_host_list=node1 node2 node3" -o pcmk_delay_base=1 -o pcmk_delay_max=2')

# Resulting delay will be 1 second (capped by pcmk_delay_max)
test.add_cmd("stonith_admin",
args='--output-as=xml -R true3 -a fence_dummy -o mode=pass -o "pcmk_host_list=node1 node2 node3"')
args='--output-as=xml -R true3 -a fence_dummy -o mode=pass -o "pcmk_host_list=node1 node2 node3" -o pcmk_delay_base=2 -o pcmk_delay_max=1')

test.add_cmd("stonith_admin",
args='--output-as=xml -R true4 -a fence_dummy -o mode=pass -o "pcmk_host_list=node1 node2 node3"')

test.add_cmd("stonith_admin", args="--output-as=xml -r node3 -i 1 -v true1")
test.add_cmd("stonith_admin", args="--output-as=xml -r node3 -i 1 -v false1")
test.add_cmd("stonith_admin", args="--output-as=xml -r node3 -i 2 -v true2")
test.add_cmd("stonith_admin", args="--output-as=xml -r node3 -i 2 -v true3")
test.add_cmd("stonith_admin", args="--output-as=xml -r node3 -i 2 -v true4")

test.add_cmd("stonith_admin", args="--output-as=xml -F node3 --delay 1")

# Total fencing timeout takes all fencing delays into account
test.add_log_pattern("Total timeout set to 582s")
test.add_log_pattern("Total timeout set to 727s")

# Fencing timeout for the first device takes the requested fencing delay
# and pcmk_delay_base into account
Expand All @@ -543,9 +550,15 @@ class FenceTests(Tests):
# Fencing timeout takes pcmk_delay_max into account
test.add_log_pattern(r"Requesting that .* perform 'off' action targeting node3 using true2 .*146s.*",
regex=True)
test.add_log_pattern("Delaying 'off' action targeting node3 using true2 for 1s | timeout=120s requested_delay=0s base=1s max=2s")
test.add_log_pattern(r"Delaying 'off' action targeting node3 using true2 for [12]s "
"| timeout=120s requested_delay=0s base=1s max=2s",
regex=True)
test.add_log_pattern(r"Requesting that .* perform 'off' action targeting node3 using true3 .*145s.*",
regex=True)
test.add_log_pattern("Delaying 'off' action targeting node3 using true3 for 1s "
"| timeout=120s requested_delay=0s base=1s max=1s")

test.add_log_pattern("Delaying 'off' action targeting node3 using true3",
test.add_log_pattern("Delaying 'off' action targeting node3 using true4",
negative=True)

def build_unfence_tests(self):
Expand Down
205 changes: 114 additions & 91 deletions daemons/fenced/fenced_commands.c
Original file line number Diff line number Diff line change
Expand Up @@ -201,48 +201,110 @@ get_action_delay_max(const fenced_device_t *device, const char *action)
return (int) delay_max;
}

static gchar *
get_value_if_matching(const char *mapping, const char *target)
{
gchar **nvpair = NULL;
gchar *value = NULL;

if (pcmk__str_empty(mapping)) {
goto done;
}

nvpair = g_strsplit(mapping, ":", 2);

if (pcmk__str_empty(nvpair[0]) || pcmk__str_empty(nvpair[1])) {
crm_err(PCMK_FENCING_DELAY_BASE ": Malformed mapping '%s'", mapping);
goto done;
}

if (!pcmk__str_eq(target, nvpair[0], pcmk__str_casei)) {
goto done;
}

// Take ownership so that we don't free nvpair[1] with nvpair
value = nvpair[1];
nvpair[1] = NULL;

crm_debug(PCMK_FENCING_DELAY_BASE " mapped to %s for %s", value, target);

done:
g_strfreev(nvpair);
return value;
}

static gchar *
get_value_for_target(const char *target, const char *values)
{
gchar *value = NULL;
gchar **mappings = g_strsplit_set(values, "; \t", 0);

/* If there are no delimiters after stripping leading and trailing
* whitespace, then we want to parse the string as a single interval, rather
* than as a delimited list of mappings. Short-circuiting here when we split
* into fewer than two mappings avoids a "Malformed mapping" error message
* below.
*/
if (g_strv_length(mappings) < 2) {
Copy link
Contributor Author

@nrwahl2 nrwahl2 Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong. There could be a single mapping -- though if there are no delimiters, we don't want to throw an error if it's an interval instead of a mapping.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this still need to be addressed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

goto done;
}

for (gchar **mapping = mappings; *mapping != NULL; mapping++) {
value = get_value_if_matching(*mapping, target);
if (value != NULL) {
break;
}
}

done:
g_strfreev(mappings);
return value;
}

/* @TODO Consolidate some of this with build_port_aliases(). But keep in mind
* that build_port_aliases()/pcmk__host_map supports either '=' or ':' as a
* mapping separator, while pcmk_delay_base supports only ':'.
*/
static int
get_action_delay_base(const fenced_device_t *device, const char *action,
const char *target)
{
char *hash_value = NULL;
const char *param = NULL;
gchar *stripped = NULL;
gchar *delay_base_s = NULL;
guint delay_base = 0U;

if (!pcmk__is_fencing_action(action)) {
return 0;
}

hash_value = g_hash_table_lookup(device->params, PCMK_FENCING_DELAY_BASE);

if (hash_value) {
char *value = pcmk__str_copy(hash_value);
char *valptr = value;

if (target != NULL) {
for (char *val = strtok(value, "; \t"); val != NULL; val = strtok(NULL, "; \t")) {
char *mapval = strchr(val, ':');
param = g_hash_table_lookup(device->params, PCMK_FENCING_DELAY_BASE);
if (param == NULL) {
return 0;
}

if (mapval == NULL || mapval[1] == 0) {
crm_err("pcmk_delay_base: empty value in mapping", val);
continue;
}
stripped = g_strstrip(g_strdup(param));
if (target != NULL) {
delay_base_s = get_value_for_target(target, stripped);
}

if (mapval != val && strncasecmp(target, val, (size_t)(mapval - val)) == 0) {
value = mapval + 1;
crm_debug("pcmk_delay_base mapped to %s for %s",
value, target);
break;
}
}
}
if (delay_base_s == NULL) {
/* Either target is NULL or we didn't find a mapping. Try to parse the
* stripped value itself. Take ownership so that we don't free stripped
* twice.
*/
delay_base_s = stripped;
stripped = NULL;
}

if (strchr(value, ':') == 0) {
pcmk_parse_interval_spec(value, &delay_base);
delay_base /= 1000;
}
/* @TODO Should we accept only a simple time+units string, rather than an
* ISO 8601 interval?
*/
pcmk_parse_interval_spec(delay_base_s, &delay_base);
delay_base /= 1000;

free(valptr);
}
g_free(stripped);
g_free(delay_base_s);

return (int) delay_base;
}
Expand Down Expand Up @@ -839,82 +901,43 @@ fenced_free_device_table(void)
}

static GHashTable *
build_port_aliases(const char *hostmap, GList ** targets)
build_port_aliases(const char *hostmap, GList **targets)
{
char *name = NULL;
int last = 0, lpc = 0, max = 0, added = 0;
GHashTable *aliases = pcmk__strikey_table(free, free);
gchar *stripped = NULL;
gchar **mappings = NULL;

if (hostmap == NULL) {
return aliases;
if (pcmk__str_empty(hostmap)) {
goto done;
}

max = strlen(hostmap);
for (; lpc <= max; lpc++) {
switch (hostmap[lpc]) {
/* Skip escaped chars */
case '\\':
lpc++;
break;
stripped = g_strstrip(g_strdup(hostmap));
mappings = g_strsplit_set(stripped, "; \t", 0);

/* Assignment chars */
case '=':
case ':':
if (lpc > last) {
free(name);
name = pcmk__assert_alloc(1, 1 + lpc - last);
memcpy(name, hostmap + last, lpc - last);
}
last = lpc + 1;
break;
for (gchar **mapping = mappings; *mapping != NULL; mapping++) {
gchar **nvpair = NULL;

/* Delimeter chars */
/* case ',': Potentially used to specify multiple ports */
case 0:
case ';':
case ' ':
case '\t':
if (name) {
char *value = NULL;
int k = 0;

value = pcmk__assert_alloc(1, 1 + lpc - last);
memcpy(value, hostmap + last, lpc - last);

for (int i = 0; value[i] != '\0'; i++) {
if (value[i] != '\\') {
value[k++] = value[i];
}
}
value[k] = '\0';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs (doc/sphinx/Pacemaker_Explained/fencing.rst) say this about pcmk_host_map:

     - A mapping of node names to ports for devices that do not understand the
       node names. For example, ``node1:1;node2:2,3`` tells the cluster to use
       port 1 for ``node1`` and ports 2 and 3 for ``node2``. If
       ``pcmk_host_check`` is explicitly set to ``static-list``, either this or
       ``pcmk_host_list`` must be set. The port portion of the map may contain
       special characters such as spaces if preceded by a backslash *(since 2.1.2)*.

I'm unclear on what a "port" is in this context, and what characters are valid in the definition of a port. I'd like to make sure that if we are expected to support characters such as spaces in port names, that the new parsing code handles that without backslashes. Also, since this has been present since 2.1.2, I'm a little concerned about breaking whoever is using this right now regardless of how little sense it makes or how it is currently mangled in the XML.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Port" comes from a time when it was more common for fencing to target a shared power supply or network switch. For example:

the nodes each have two independent Power Supply Units (PSUs) connected to two independent Power Distribution Units (PDUs) reachable at 198.51.100.1 (port 10 and port 11) and 203.0.113.1 (port 10 and port 11)

Ref https://clusterlabs.org/projects/pacemaker/doc/3.0/Pacemaker_Explained/singlehtml/#example-dual-layer-dual-device-fencing-topologies

It has since been overloaded -- for example, fence_vmware_rest uses it for the VM name. Ultimately, the meaning of a port, and thus the characters that make sense, are up to the fence agent. If the fence agent advertises support for a --plug or --port option, and pcmk_host_map is configured, then the "ports" get passed as that option by default (or as pcmk_host_argument if set).


I'd like to make sure that if we are expected to support characters such as spaces in port names, that the new parsing code handles that without backslashes

Previously, the backslash was just getting ignored. Wouldn't hurt to do some testing (or re-testing... not sure what I did, if anything) to be sure. But the reason I felt comfortable making this change in the first place, is that it doesn't appear to change the end result; we simply no longer skip a backslash if present.

FWIW Oyvind has no idea why e95198f was done.


Also, since this has been present since 2.1.2, I'm a little concerned about breaking whoever is using this right now regardless of how little sense it makes or how it is currently mangled in the XML.

Hopefully no one... but I understand your concern. This is a case where I'd be willing to take my chances in order to make the code look sane. But I'm not the one who has to do the z-stream builds.

crm_debug("Adding alias '%s'='%s'", name, value);
g_hash_table_replace(aliases, name, value);
if (targets) {
*targets = g_list_append(*targets, pcmk__str_copy(value));
}
value = NULL;
name = NULL;
added++;
if (pcmk__str_empty(*mapping)) {
continue;
}

} else if (lpc > last) {
crm_debug("Parse error at offset %d near '%s'", lpc - last, hostmap + last);
}
// @COMPAT Drop support for '=' as delimiter
nvpair = g_strsplit_set(*mapping, ":=", 2);

last = lpc + 1;
break;
}
if (pcmk__str_empty(nvpair[0]) || pcmk__str_empty(nvpair[1])) {
crm_err(PCMK_FENCING_HOST_MAP ": Malformed mapping '%s'", *mapping);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're changing behavior... I wonder if it makes sense to return an empty mapping in the case where it's malformed. I can see going either way with it. If there's a parse error, who knows what they screwed up so maybe the entire thing is invalid. On the other hand, maybe we should try to use what we can from before the error occurred (what we're doing right now) to implement the user's configuration as much as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not even just "before the error occurred," but also after the error occurred. This is a sanity check to catch mappings with an empty name (node) or value (port). Note that this is happening after splitting on semicolons and whitespace. So consider the following (colons are equivalent to equal signs):

node1:port1 node2: node3:port3 :port4 node5:port5 :

The mappings for node1, node3, and node5 would get used. The mappings node2:, :port4, and : would get ignored because either the name or value was empty. I lean against throwing away the entire set of mappings in that case. But I am open to it.

if (hostmap[lpc] == 0) {
break;
} else {
crm_debug("Adding alias '%s'='%s'", nvpair[0], nvpair[1]);
pcmk__insert_dup(aliases, nvpair[0], nvpair[1]);
*targets = g_list_append(*targets, pcmk__str_copy(nvpair[1]));
}
g_strfreev(nvpair);
}

if (added == 0) {
crm_info("No host mappings detected in '%s'", hostmap);
}

free(name);
done:
g_free(stripped);
g_strfreev(mappings);
return aliases;
}

Expand Down
16 changes: 1 addition & 15 deletions include/crm/services.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2024 the Pacemaker project contributors
* Copyright 2010-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
Expand Down Expand Up @@ -159,20 +159,6 @@ typedef struct svc_action_s {
svc_action_private_t *opaque;
} svc_action_t;

/*!
* \brief Get a list of files or directories in a given path
*
* \param[in] root Full path to a directory to read
* \param[in] files Return list of files if TRUE or directories if FALSE
* \param[in] executable If TRUE and files is TRUE, only return executable files
*
* \return List of what was found as char * items.
* \note The caller is responsibile for freeing the result with
* g_list_free_full(list, free).
*/
GList *get_directory_list(const char *root, gboolean files,
gboolean executable);

/*!
* \brief Get a list of providers
*
Expand Down
6 changes: 6 additions & 0 deletions include/crm/services_compat.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
#ifndef PCMK__CRM_SERVICES_COMPAT__H
#define PCMK__CRM_SERVICES_COMPAT__H

#include <glib.h> // GList, gboolean

#include <crm/common/results.h> // enum ocf_exitcode, PCMK_OCF_OK, etc.

#ifdef __cplusplus
Expand Down Expand Up @@ -50,6 +52,10 @@ services_ocf_exitcode_str(enum ocf_exitcode code)
}
}

//! \deprecated Do not use
GList *get_directory_list(const char *root, gboolean files,
gboolean executable);

# ifdef __cplusplus
}
# endif
Expand Down
2 changes: 1 addition & 1 deletion lib/cluster/cpg.c
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ pcmk__cpg_message_data(cpg_handle_t handle, uint32_t sender_id, uint32_t pid,
if (msg->is_compressed && (msg->size > 0)) {
int rc = BZ_OK;
unsigned int new_size = msg->size + 1;
char *uncompressed = pcmk__assert_alloc(1, new_size);
char *uncompressed = pcmk__assert_alloc(new_size, sizeof(char));

rc = BZ2_bzBuffToBuffDecompress(uncompressed, &new_size, msg->data,
msg->compressed_size, 1, 0);
Expand Down
2 changes: 1 addition & 1 deletion lib/common/actions.c
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ decode_transition_magic(const char *magic, char **uuid, int *transition_id, int
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);
key = pcmk__assert_alloc(strlen(magic) - 3, sizeof(char));
res = sscanf(magic, "%d:%d;%s", &local_op_status, &local_op_rc, key);
#endif
if (res == EOF) {
Expand Down
4 changes: 2 additions & 2 deletions lib/common/fuzzers/iso8601_fuzzer.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 the Pacemaker project contributors
* Copyright 2024-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
Expand Down Expand Up @@ -27,7 +27,7 @@ LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
if (size < 10) {
return -1; // Do not add input to testing corpus
}
ns = pcmk__assert_alloc(1, size + 1);
ns = pcmk__assert_alloc(size + 1, sizeof(char));
memcpy(ns, data, size);

period = crm_time_parse_period(ns);
Expand Down
4 changes: 2 additions & 2 deletions lib/common/fuzzers/scores_fuzzer.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 the Pacemaker project contributors
* Copyright 2024-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
Expand All @@ -21,7 +21,7 @@ LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
guint result = 0U;

if (size > 0) {
ns = pcmk__assert_alloc(1, size + 1);
ns = pcmk__assert_alloc(size + 1, sizeof(char));
memcpy(ns, data, size);
ns[size] = '\0';
}
Expand Down
Loading