import OL resource-agents-4.10.0-71.el9_6.11

This commit is contained in:
eabdullin 2025-10-21 06:50:05 +00:00
parent a451362f03
commit 961cde1291
12 changed files with 1520 additions and 16 deletions

View File

@ -22,4 +22,3 @@ index d10e5a714..515a3919d 100755
}
determine_blockdevice() {

View File

@ -0,0 +1,85 @@
From d08a7f74427ea2cf7d355a0f7f6d8f583e2d0cba Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Thu, 3 Jul 2025 12:22:12 +0200
Subject: [PATCH] OCPBUGS-58324: podman-etcd Add OOM score adjustment for etcd
containers
This change introduces a new `oom` parameter to the `podman-etcd` OCF
agent. This allows tuning the Out-Of-Memory (OOM) score adjustment for
the etcd container.
The `oom` parameter accepts integer values from -1000 to 1000,
defaulting to -997 (system-node-critical equivalent).
see https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/#node-out-of-memory-behavior
Key changes:
- Added `OCF_RESKEY_oom` parameter to agent definition (`content type="integer"`).
- Integrated `--oom-score-adj` option into `podman_start()`.
- Implemented input validation for `oom` in `podman_validate()`,
ensuring values are within the [-1000:1000] range.
---
heartbeat/podman-etcd | 22 +++++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 6762112ec..884b7c579 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -45,6 +45,7 @@ OCF_RESKEY_nic_default="br-ex"
OCF_RESKEY_authfile_default="/var/lib/kubelet/config.json"
OCF_RESKEY_allow_pull_default="1"
OCF_RESKEY_reuse_default="0"
+OCF_RESKEY_oom_default="-997"
: ${OCF_RESKEY_image=${OCF_RESKEY_image_default}}
: ${OCF_RESKEY_pod_manifest=${OCF_RESKEY_pod_manifest_default}}
@@ -53,6 +54,7 @@ OCF_RESKEY_reuse_default="0"
: ${OCF_RESKEY_authfile=${OCF_RESKEY_authfile_default}}
: ${OCF_RESKEY_allow_pull=${OCF_RESKEY_allow_pull_default}}
: ${OCF_RESKEY_reuse=${OCF_RESKEY_reuse_default}}
+: ${OCF_RESKEY_oom=${OCF_RESKEY_oom_default}}
#######################################################################
@@ -230,6 +232,16 @@ to stop the container before pacemaker.
<shortdesc lang="en">drop-in dependency</shortdesc>
<content type="boolean"/>
</parameter>
+
+<parameter name="oom" required="0" unique="0">
+<longdesc lang="en">
+Tune the host's Out-Of-Memory (OOM) preferences for containers (accepts values from -1000 to 1000).
+Default to same OOM score as system-node-critical
+https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/#node-out-of-memory-behavior
+</longdesc>
+<shortdesc lang="en">OOM for container</shortdesc>
+<content type="integer" default="${OCF_RESKEY_oom_default}"/>
+</parameter>
</parameters>
<actions>
@@ -1226,7 +1238,10 @@ podman_start()
fi
podman_create_mounts
- local run_opts="-d --name=${CONTAINER}"
+ local run_opts="--detach --name=${CONTAINER}"
+
+ run_opts="$run_opts --oom-score-adj=${OCF_RESKEY_oom}"
+
# check to see if the container has already started
podman_simple_status
if [ $? -eq $OCF_SUCCESS ]; then
@@ -1513,6 +1528,11 @@ podman_validate()
exit $OCF_ERR_CONFIGURED
fi
+ if [ "$OCF_RESKEY_oom" -lt -1000 ] || [ "$OCF_RESKEY_oom" -gt 1000 ]; then
+ ocf_exit_reason "'oom' value ${OCF_RESKEY_oom} is out of range [-1000:1000]"
+ exit $OCF_ERR_CONFIGURED
+ fi
+
return $OCF_SUCCESS
}

View File

@ -0,0 +1,686 @@
From 6e9200dc2ffc89382188794742361985309936b2 Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Wed, 23 Jul 2025 09:34:13 +0200
Subject: [PATCH] podman-etcd: preserve containers for debugging
This change modifies the agent to keep stopped containers for log
inspection and debugging, with supporting changes to enable this
behavior.
* Conditionally reuse existing containers when configuration unchanged
* Move etcd inline configuration flags to external file to allow
restarts without container recreation (mainly for the
force-new-cluster flag)
* Archive previous container renaming it into *-previous, and its
configuration files into /var/lib/etcd/config-previous.tar.gz archive.
The tar.gz archive consists in:
* the pod manifest created by CEO, used to generated the Etc
configuration file
* the Etcd configuration file
* the auth json file
Only one copy is maintained to limit disk usage.
* Both configuration and backup files location is configurable with 2
new input arguments.
Signed-off-by: Carlo Lobrano <c.lobrano@gmail.com>
---
heartbeat/podman-etcd | 438 ++++++++++++++++++++++++++++++++----------
1 file changed, 336 insertions(+), 102 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 4969fbaaf..33804414a 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -46,6 +46,8 @@ OCF_RESKEY_authfile_default="/var/lib/kubelet/config.json"
OCF_RESKEY_allow_pull_default="1"
OCF_RESKEY_reuse_default="0"
OCF_RESKEY_oom_default="-997"
+OCF_RESKEY_config_location_default="/var/lib/etcd"
+OCF_RESKEY_backup_location_default="/var/lib/etcd"
: ${OCF_RESKEY_image=${OCF_RESKEY_image_default}}
: ${OCF_RESKEY_pod_manifest=${OCF_RESKEY_pod_manifest_default}}
@@ -55,6 +57,9 @@ OCF_RESKEY_oom_default="-997"
: ${OCF_RESKEY_allow_pull=${OCF_RESKEY_allow_pull_default}}
: ${OCF_RESKEY_reuse=${OCF_RESKEY_reuse_default}}
: ${OCF_RESKEY_oom=${OCF_RESKEY_oom_default}}
+: ${OCF_RESKEY_config_location=${OCF_RESKEY_config_location_default}}
+: ${OCF_RESKEY_backup_location=${OCF_RESKEY_backup_location_default}}
+
#######################################################################
@@ -242,6 +247,23 @@ https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/#
<shortdesc lang="en">OOM for container</shortdesc>
<content type="integer" default="${OCF_RESKEY_oom_default}"/>
</parameter>
+
+<parameter name="config_location" required="0" unique="0">
+<longdesc lang="en">
+The directory where the resource agent stores its state files, such as the generated etcd configuration and a copy of the pod manifest.
+</longdesc>
+<shortdesc lang="en">Resource agent state directory</shortdesc>
+<content type="string" default="${OCF_RESKEY_config_location_default}"/>
+</parameter>
+
+<parameter name="backup_location" required="0" unique="0">
+<longdesc lang="en">
+The directory where the resource agent stores its backups.
+</longdesc>
+<shortdesc lang="en">Resource agent backup directory</shortdesc>
+<content type="string" default="${OCF_RESKEY_backup_location_default}"/>
+</parameter>
+
</parameters>
<actions>
@@ -309,42 +331,52 @@ container_exists()
return 1
}
-remove_container()
+# archive_current_container archives the current
+# podman etcd container and its configuration files.
+archive_current_container()
{
- local rc
- local execids
+ # don't attempt to archive a container that doesn't exist
+ if ! container_exists; then
+ return
+ fi
- if ocf_is_true "$OCF_RESKEY_reuse"; then
- # never remove the container if we have reuse enabled.
- return 0
+ # delete any container named "*-previous", or we won't be able to archive the current container.
+ if podman inspect "${CONTAINER}-previous" >/dev/null 2>&1; then
+ ocf_log info "removing old archived container '$CONTAINER-previous'"
+ if ! ocf_run podman rm --volumes --force "$CONTAINER-previous"; then
+ ocf_log warn "could not remove old archived container (podman rm failed, error code: $?). Won't be able to archive current container"
+ return
+ fi
fi
- if ! container_exists; then
- # don't attempt to remove a container that doesn't exist
- return 0
+ ocf_log info "archiving '$CONTAINER' container as '$CONTAINER-previous' for debugging purposes"
+ if ! ocf_run podman rename "$CONTAINER" "$CONTAINER-previous"; then
+ ocf_log err "could not archive container '$CONTAINER', error code: $?"
+ return
fi
- ocf_log notice "Cleaning up inactive container, ${CONTAINER}."
- ocf_run podman rm -v "$CONTAINER"
- rc=$?
- if [ $rc -ne 0 ]; then
- if [ $rc -eq 2 ]; then
- if podman inspect --format '{{.State.Status}}' "$CONTAINER" | grep -wq "stopping"; then
- ocf_log err "Inactive container ${CONTAINER} is stuck in 'stopping' state. Force-remove it."
- ocf_run podman rm -f "$CONTAINER"
- rc=$?
- fi
- fi
- # due to a podman bug (rhbz#1841485), sometimes a stopped
- # container can still be associated with Exec sessions, in
- # which case the "podman rm" has to be forced
- execids=$(podman inspect "$CONTAINER" --format '{{len .ExecIDs}}')
- if [ "$execids" -ne "0" ]; then
- ocf_log warn "Inactive container ${CONTAINER} has lingering exec sessions. Force-remove it."
- ocf_run podman rm -f "$CONTAINER"
- rc=$?
+
+ # archive corresponding etcd configuration files
+ local files_to_archive=""
+ for file in "$OCF_RESKEY_authfile" "$POD_MANIFEST_COPY" "$ETCD_CONFIGURATION_FILE"; do
+ if [ -f "$file" ]; then
+ files_to_archive="$files_to_archive $file"
+ else
+ ocf_log warn "file '$file' is missing and won't be archived"
fi
+ done
+
+ if [ -z "$files_to_archive" ]; then
+ ocf_log warn "could not find any file to archive."
+ return
+ fi
+
+ # NOTE: tar will override any existing archive as wanted
+ # shellcheck disable=SC2086
+ if ! ocf_run tar --create --verbose --gzip --file "$ETCD_BACKUP_FILE" $files_to_archive; then
+ ocf_log warn "container archived successfully, but configuration backup failed (error: $?). Container debugging available, but without matching configuration files"
+ else
+ ocf_log info "container configuration also archived in '$ETCD_BACKUP_FILE'"
fi
- return $rc
}
# Correctly wraps an ipv6 in [] for url otherwise use return normal ipv4 address.
@@ -365,6 +397,7 @@ attribute_node_ip()
local attribute="node_ip"
local ip_addr name
+ # TODO: We can retrieve both the local and peer IP addresses from this map, which eliminates the need to use CIB to share them between nodes
for node in $(echo "$OCF_RESKEY_node_ip_map" | sed "s/\s//g;s/;/ /g"); do
name=$(echo "$node" | cut -d: -f1)
# ignore other nodes
@@ -375,7 +408,7 @@ attribute_node_ip()
done
if [ -z "$ip_addr" ]; then
- ocf_log err "ip address was empty when querying (getent ahosts) for hostname: $(hostname -f)"
+ ocf_log err "could not get local ip address from node_ip_map: '$OCF_RESKEY_node_ip_map'"
return 1
fi
@@ -384,9 +417,9 @@ attribute_node_ip()
echo "$ip_addr"
;;
update)
- if ! crm_attribute --type nodes --node "$NODENAME" --name "$attribute" --update "$value"; then
+ if ! crm_attribute --type nodes --node "$NODENAME" --name "$attribute" --update "$ip_addr"; then
rc="$?"
- ocf_log err "could not set $attribute to $value, error code: $rc"
+ ocf_log err "could not set $attribute to $ip_addr, error code: $rc"
return "$rc"
fi
;;
@@ -428,6 +461,48 @@ get_env_from_manifest() {
echo "$env_var_value"
}
+# etcd configuration file expects duration to be expressed in nanoseconds
+convert_duration_in_nanoseconds() {
+ local duration=$1
+ local value unit nanoseconds
+
+ if [ -z "$duration" ]; then
+ ocf_log err "convert_duration_in_nanoseconds: no duration provided"
+ return 1
+ fi
+
+ if ! echo "$duration" | grep -qE '^[0-9]+[numµ]?s$'; then
+ ocf_log err "convert_duration_in_nanoseconds: invalid duration format \"$duration\". Expected format: <number><unit> where unit is one of s, ms, us, µs, ns"
+ return 1
+ fi
+
+ # Extract numeric value and unit from duration string
+ value=$(echo "$duration" | sed 's/[^0-9]*$//')
+ unit=$(echo "$duration" | sed 's/^[0-9]*//')
+
+ case "$unit" in
+ ns)
+ nanoseconds=$value
+ ;;
+ us|µs)
+ nanoseconds=$((value * 1000))
+ ;;
+ ms)
+ nanoseconds=$((value * 1000000))
+ ;;
+ s)
+ nanoseconds=$((value * 1000000000))
+ ;;
+ *)
+ # this should not happen as the input is already validated
+ ocf_log err "convert_duration_in_nanoseconds: unknown duration unit \"$unit\""
+ return 1
+ ;;
+ esac
+
+ echo "$nanoseconds"
+}
+
prepare_env() {
local name ip ipurl standalone_node
@@ -457,9 +532,14 @@ prepare_env() {
ETCDCTL_API=$(get_env_from_manifest "ETCDCTL_API")
ETCD_CIPHER_SUITES=$(get_env_from_manifest "ETCD_CIPHER_SUITES")
ETCD_DATA_DIR=$(get_env_from_manifest "ETCD_DATA_DIR")
+ if [ ! -d "$ETCD_DATA_DIR" ]; then
+ ocf_log err "could not find data-dir at path \"$ETCD_DATA_DIR\""
+ return "$OCF_ERR_ARGS"
+ else
+ ocf_log info "using data-dir: $ETCD_DATA_DIR"
+ fi
ETCD_ELECTION_TIMEOUT=$(get_env_from_manifest "ETCD_ELECTION_TIMEOUT")
ETCD_ENABLE_PPROF=$(get_env_from_manifest "ETCD_ENABLE_PPROF")
- ETCD_EXPERIMENTAL_MAX_LEARNERS=$(get_env_from_manifest "ETCD_EXPERIMENTAL_MAX_LEARNERS")
ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION=$(get_env_from_manifest "ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION")
ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERVAL=$(get_env_from_manifest "ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERVAL")
ETCD_HEARTBEAT_INTERVAL=$(get_env_from_manifest "ETCD_HEARTBEAT_INTERVAL")
@@ -475,6 +555,62 @@ prepare_env() {
LISTEN_METRICS_URLS="0.0.0.0"
}
+
+generate_etcd_configuration() {
+ if is_force_new_cluster; then
+ # The embedded newline is required for correct YAML formatting.
+ FORCE_NEW_CLUSTER_CONFIG="force-new-cluster: true
+force-new-cluster-bump-amount: 1000000000"
+ else
+ FORCE_NEW_CLUSTER_CONFIG="force-new-cluster: false"
+ fi
+
+ cat > "$ETCD_CONFIGURATION_FILE" << EOF
+logger: zap
+log-level: info
+snapshot-count: 10000
+name: $NODENAME
+data-dir: $ETCD_DATA_DIR
+$FORCE_NEW_CLUSTER_CONFIG
+socket-reuse-address: $ETCD_SOCKET_REUSE_ADDRESS
+election-timeout: $ETCD_ELECTION_TIMEOUT
+enable-pprof: $ETCD_ENABLE_PPROF
+heartbeat-interval: $ETCD_HEARTBEAT_INTERVAL
+quota-backend-bytes: $ETCD_QUOTA_BACKEND_BYTES
+initial-advertise-peer-urls: "$NODEIPURL:2380"
+listen-peer-urls: "$(ip_url ${LISTEN_PEER_URLS}):2380"
+listen-client-urls: "$(ip_url ${LISTEN_CLIENT_URLS}):2379,unixs://${NODEIP}:0"
+initial-cluster: $ETCD_INITIAL_CLUSTER
+initial-cluster-state: $ETCD_INITIAL_CLUSTER_STATE
+client-transport-security:
+ cert-file: /etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-serving-${NODENAME}.crt
+ key-file: /etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-serving-${NODENAME}.key
+ client-cert-auth: true
+ trusted-ca-file: $SERVER_CACERT
+peer-transport-security:
+ cert-file: $ETCD_PEER_CERT
+ key-file: $ETCD_PEER_KEY
+ client-cert-auth: true
+ trusted-ca-file: $SERVER_CACERT
+advertise-client-urls: "$NODEIPURL:2379"
+listen-metrics-urls: "$(ip_url ${LISTEN_METRICS_URLS}):9978"
+metrics: extensive
+experimental-initial-corrupt-check: true
+experimental-max-learners: 1
+experimental-warning-apply-duration: $(convert_duration_in_nanoseconds "$ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION")
+experimental-watch-progress-notify-interval: $(convert_duration_in_nanoseconds "$ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERVAL")
+EOF
+
+ {
+ if [ -n "$ETCD_CIPHER_SUITES" ]; then
+ echo "cipher-suites:"
+ echo "$ETCD_CIPHER_SUITES" | tr ',' '\n' | while read -r cipher; do
+ echo " - \"$cipher\""
+ done
+ fi
+ } >> "$ETCD_CONFIGURATION_FILE"
+}
+
archive_data_folder()
{
# TODO: use etcd snapshots
@@ -634,7 +770,7 @@ add_member_as_learner()
local endpoint_url=$(ip_url $(attribute_node_ip get))
local peer_url=$(ip_url $member_ip)
- ocf_log info "add $member_name ($member_ip) to the member list as learner"
+ ocf_log info "add $member_name ($member_ip, $endpoint_url) to the member list as learner"
out=$(podman exec "${CONTAINER}" etcdctl --endpoints="$endpoint_url:2379" member add "$member_name" --peer-urls="$peer_url:2380" --learner)
rc=$?
if [ $rc -ne 0 ]; then
@@ -1104,18 +1240,18 @@ compare_revision()
peer_revision=$(attribute_node_revision_peer)
if [ "$revision" = "" ] || [ "$revision" = "null" ] || [ "$peer_revision" = "" ] || [ "$peer_revision" = "null" ]; then
- ocf_log err "could not compare revisions: $NODENAME local revision: $revision, peer revision: $peer_revision"
+ ocf_log err "could not compare revisions: '$NODENAME' local revision='$revision', peer revision='$peer_revision'"
return "$OCF_ERR_GENERIC"
fi
if [ "$revision" -gt "$peer_revision" ]; then
- ocf_log info "$NODENAME revision: $revision is newer than peer revision: $peer_revision"
+ ocf_log info "$NODENAME revision: '$revision' is newer than peer revision: '$peer_revision'"
echo "newer"
elif [ "$revision" -eq "$peer_revision" ]; then
- ocf_log info "$NODENAME revision: $revision is equal to peer revision: $peer_revision"
+ ocf_log info "$NODENAME revision: '$revision' is equal to peer revision: '$peer_revision'"
echo "equal"
else
- ocf_log info "$NODENAME revision: $revision is older than peer revision: $peer_revision"
+ ocf_log info "$NODENAME revision: '$revision' is older than peer revision: '$peer_revision'"
echo "older"
fi
return "$OCF_SUCCESS"
@@ -1144,6 +1280,100 @@ ensure_pod_manifest_exists()
return "$OCF_SUCCESS"
}
+filter_pod_manifest() {
+ # Remove pod-version related fields from POD manifest
+ local pod_manifest="$1"
+ local temporary_file
+ local jq_filter='del(.metadata.labels.revision) | .spec.containers[] |= ( .env |= map(select( .name != "ETCD_STATIC_POD_VERSION" ))) | .spec.volumes |= map( select( .name != "resource-dir" ))'
+
+ if ! temporary_file=$(mktemp); then
+ ocf_log err "could not create temporary file for '$pod_manifest', error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
+ if ! jq "$jq_filter" "$pod_manifest" > "$temporary_file"; then
+ ocf_log err "could not remove pod version related data from '$pod_manifest', error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
+ echo "$temporary_file"
+}
+
+can_reuse_container() {
+ # Decide whether to reuse the existing container or create a new one based on etcd pod manifest changes.
+ # NOTE: explicitly ignore POD version and POD version related data, as the content might be the same even if the revision number has changed.
+ local cp_rc
+ local diff_rc
+ local filtered_original_pod_manifest
+ local filtered_copy_pod_manifest
+
+
+ # If the container does not exist it cannot be reused
+ if ! container_exists; then
+ OCF_RESKEY_reuse=0
+ return "$OCF_SUCCESS"
+ fi
+
+ # If the manifest copy doesn't exist, we need a new container.
+ if [ ! -f "$POD_MANIFEST_COPY" ]; then
+ ocf_log info "a working copy of $OCF_RESKEY_pod_manifest was not found. A new etcd container will be created."
+ OCF_RESKEY_reuse=0
+ return "$OCF_SUCCESS"
+ fi
+
+ if ! filtered_original_pod_manifest=$(filter_pod_manifest "$OCF_RESKEY_pod_manifest"); then
+ return $OCF_ERR_GENERIC
+ fi
+ if ! filtered_copy_pod_manifest=$(filter_pod_manifest "$POD_MANIFEST_COPY"); then
+ return $OCF_ERR_GENERIC
+ fi
+
+ ocf_log info "comparing $OCF_RESKEY_pod_manifest with local copy $POD_MANIFEST_COPY"
+ ocf_run diff -s "$filtered_original_pod_manifest" "$filtered_copy_pod_manifest"
+ diff_rc="$?"
+ # clean up temporary files
+ rm -f "$filtered_original_pod_manifest" "$filtered_copy_pod_manifest"
+ case "$diff_rc" in
+ 0)
+ ocf_log info "Reusing the existing etcd container"
+ OCF_RESKEY_reuse=1
+ ;;
+ 1)
+ ocf_log info "Etcd pod manifest changes detected: creating a new etcd container to apply the changes"
+ if ! ocf_run cp -p "$OCF_RESKEY_pod_manifest" "$POD_MANIFEST_COPY"; then
+ cp_rc="$?"
+ ocf_log err "Could not create a working copy of $OCF_RESKEY_pod_manifest, rc: $cp_rc"
+ return "$OCF_ERR_GENERIC"
+ fi
+ ocf_log info "A working copy of $OCF_RESKEY_pod_manifest was created"
+ OCF_RESKEY_reuse=0
+ ;;
+ *)
+ ocf_log err "Could not check if etcd pod manifest has changed, diff rc: $diff_rc"
+ return "$OCF_ERR_GENERIC"
+ ;;
+ esac
+
+ return "$OCF_SUCCESS"
+}
+
+ensure_pod_manifest_copy_exists() {
+ local cp_rc
+
+ if [ -f "$POD_MANIFEST_COPY" ]; then
+ return "$OCF_SUCCESS"
+ fi
+
+ # If the manifest copy doesn't exist, create it and ensure a new container.
+ if ! ocf_run cp -p "$OCF_RESKEY_pod_manifest" "$POD_MANIFEST_COPY"; then
+ cp_rc="$?"
+ ocf_log err "Could not create a working copy of $OCF_RESKEY_pod_manifest, rc: $cp_rc"
+ return "$OCF_ERR_GENERIC"
+ fi
+
+ ocf_log info "a new working copy of $OCF_RESKEY_pod_manifest was created"
+
+ return "$OCF_SUCCESS"
+}
+
podman_start()
{
local cid
@@ -1173,6 +1403,13 @@ podman_start()
return $OCF_ERR_GENERIC
fi
+ # check if the container has already started
+ podman_simple_status
+ if [ $? -eq $OCF_SUCCESS ]; then
+ ocf_log info "the '$CONTAINER' has already started. Nothing to do"
+ return "$OCF_SUCCESS"
+ fi
+
if ! ensure_pod_manifest_exists; then
ocf_exit_reason "could not find etcd pod manifest ($OCF_RESKEY_pod_manifest)"
return "$OCF_ERR_GENERIC"
@@ -1186,8 +1423,9 @@ podman_start()
ocf_log info "static pod was running: start normally"
else
if is_force_new_cluster; then
- ocf_log notice "$NODENAME marked to force-new-cluster"
+ ocf_log notice "'$NODENAME' marked to force-new-cluster"
else
+ ocf_log info "'$NODENAME' is not marked to force-new-cluster"
# When the local agent starts, we can infer the cluster state by counting
# how many agents are starting or already active:
# - 1 active agent: it's the peer (we are just starting)
@@ -1195,6 +1433,7 @@ podman_start()
# - 0 active agents, 2 starting: both agents are starting simultaneously
local active_resources_count
active_resources_count=$(echo "$OCF_RESKEY_CRM_meta_notify_active_resource" | wc -w)
+ ocf_log info "found '$active_resources_count' active etcd resources (meta notify environment variable: '$OCF_RESKEY_CRM_meta_notify_active_resource')"
case "$active_resources_count" in
1)
if [ "$(attribute_learner_node get)" = "$(get_peer_node_name)" ]; then
@@ -1205,17 +1444,17 @@ podman_start()
fi
;;
0)
+ # count how many agents are starting now
+ local start_resources_count
+ start_resources_count=$(echo "$OCF_RESKEY_CRM_meta_notify_start_resource" | wc -w)
+ ocf_log info "found '$start_resources_count' starting etcd resources (meta notify environment variable: '$OCF_RESKEY_CRM_meta_notify_start_resource')"
+
# we need to compare the revisions in any of the following branches
# so call the function only once here
if ! revision_compare_result=$(compare_revision); then
ocf_log err "could not compare revisions, error code: $?"
return "$OCF_ERR_GENERIC"
fi
-
- # count how many agents are starting now
- local start_resources_count
- start_resources_count=$(echo "$OCF_RESKEY_CRM_meta_notify_start_resource" | wc -w)
-
case "$start_resources_count" in
1)
ocf_log debug "peer not starting: ensure we can start a new cluster"
@@ -1231,6 +1470,7 @@ podman_start()
fi
;;
2)
+ # TODO: can we start "normally", regardless the revisions, if the container-id is the same on both nodes?
ocf_log info "peer starting"
if [ "$revision_compare_result" = "newer" ]; then
set_force_new_cluster
@@ -1263,7 +1503,7 @@ podman_start()
fi
podman_create_mounts
- local run_opts="--detach --name=${CONTAINER}"
+ local run_opts="--detach --name=${CONTAINER} --replace"
run_opts="$run_opts --oom-score-adj=${OCF_RESKEY_oom}"
@@ -1297,61 +1537,59 @@ podman_start()
archive_data_folder
fi
- prepare_env
+ ocf_log info "check for changes in pod manifest to decide if the container should be reused or replaced"
+ if ! can_reuse_container ; then
+ rc="$?"
+ ocf_log err "could not determine etcd container reuse strategy, rc: $rc"
+ return "$rc"
+ fi
+
+ # Archive current container and its configuration before creating
+ # new configuration files.
+ if ! ocf_is_true "$OCF_RESKEY_reuse"; then
+ # Log archive container failures but don't block, as the priority
+ # is ensuring the etcd container starts successfully.
+ archive_current_container
+ fi
+
+ if ! ensure_pod_manifest_copy_exists; then
+ return $OCF_ERR_GENERIC
+ fi
+
+ if ! prepare_env; then
+ ocf_log err "Could not prepare environment for podman, error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
+
+ if ! generate_etcd_configuration; then
+ ocf_log err "Could not generate etcd configuration, error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
- # add etcd-specific opts
run_opts="$run_opts \
- --network=host \
- -v /etc/kubernetes/static-pod-resources/etcd-certs:/etc/kubernetes/static-pod-certs \
- -v /var/lib/etcd:/var/lib/etcd \
- --env ALL_ETCD_ENDPOINTS=$ALL_ETCD_ENDPOINTS \
- --env ETCD_CIPHER_SUITES=$ETCD_CIPHER_SUITES \
- --env ETCD_DATA_DIR=$ETCD_DATA_DIR \
- --env ETCD_ELECTION_TIMEOUT=$ETCD_ELECTION_TIMEOUT \
- --env ETCD_ENABLE_PPROF=$ETCD_ENABLE_PPROF \
- --env ETCD_EXPERIMENTAL_MAX_LEARNERS=$ETCD_EXPERIMENTAL_MAX_LEARNERS \
- --env ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION=$ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION \
- --env ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERVAL=$ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERVAL \
- --env ETCD_HEARTBEAT_INTERVAL=$ETCD_HEARTBEAT_INTERVAL \
- --env ETCD_INITIAL_CLUSTER=$ETCD_INITIAL_CLUSTER \
- --env ETCD_INITIAL_CLUSTER_STATE=$ETCD_INITIAL_CLUSTER_STATE \
- --env ETCD_NAME=$NODENAME \
- --env ETCD_QUOTA_BACKEND_BYTES=$ETCD_QUOTA_BACKEND_BYTES \
- --env ETCD_SOCKET_REUSE_ADDRESS=$ETCD_SOCKET_REUSE_ADDRESS \
- --env ETCDCTL_API=$ETCDCTL_API \
- --env ETCDCTL_CACERT=$SERVER_CACERT \
- --env ETCDCTL_CERT=$ETCD_PEER_CERT \
- --env ETCDCTL_KEY=$ETCD_PEER_KEY \
- --authfile=$OCF_RESKEY_authfile \
- --security-opt label=disable"
+ --network=host \
+ -v /etc/kubernetes/static-pod-resources/etcd-certs:/etc/kubernetes/static-pod-certs \
+ -v /var/lib/etcd:/var/lib/etcd \
+ --env ETCDCTL_API=$ETCDCTL_API \
+ --env ETCDCTL_CACERT=$SERVER_CACERT \
+ --env ETCDCTL_CERT=$ETCD_PEER_CERT \
+ --env ETCDCTL_KEY=$ETCD_PEER_KEY \
+ --authfile=$OCF_RESKEY_authfile \
+ --security-opt label=disable"
if [ -n "$OCF_RESKEY_run_opts" ]; then
run_opts="$run_opts $OCF_RESKEY_run_opts"
fi
- OCF_RESKEY_run_cmd="$OCF_RESKEY_run_cmd --logger=zap \
- --log-level=info \
- --experimental-initial-corrupt-check=true \
- --snapshot-count=10000 \
- --initial-advertise-peer-urls=$NODEIPURL:2380 \
- --cert-file=/etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-serving-${NODENAME}.crt \
- --key-file=/etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-serving-${NODENAME}.key \
- --trusted-ca-file=$SERVER_CACERT \
- --client-cert-auth=true \
- --peer-cert-file=$ETCD_PEER_CERT \
- --peer-key-file=$ETCD_PEER_KEY \
- --peer-trusted-ca-file=$SERVER_CACERT \
- --peer-client-cert-auth=true \
- --advertise-client-urls=$NODEIPURL:2379 \
- --listen-client-urls=$(ip_url ${LISTEN_CLIENT_URLS}):2379,unixs://${NODEIP}:0 \
- --listen-peer-urls=$(ip_url ${LISTEN_PEER_URLS}):2380 \
- --metrics=extensive \
- --listen-metrics-urls=$(ip_url ${LISTEN_METRICS_URLS}):9978"
- if [ -n "$OCF_RESKEY_run_cmd_opts" ]; then
- OCF_RESKEY_run_cmd="$OCF_RESKEY_run_cmd $OCF_RESKEY_run_cmd_opts"
+ if [ -f "$ETCD_CONFIGURATION_FILE" ]; then
+ ocf_log info "using etcd configuration file: $ETCD_CONFIGURATION_FILE"
+ else
+ ocf_log err "could not find $ETCD_CONFIGURATION_FILE"
+ return "$OCF_ERR_GENERIC"
fi
- if is_force_new_cluster; then
- OCF_RESKEY_run_cmd="$OCF_RESKEY_run_cmd --force-new-cluster"
+ OCF_RESKEY_run_cmd="$OCF_RESKEY_run_cmd --config-file=$ETCD_CONFIGURATION_FILE"
+ if [ -n "$OCF_RESKEY_run_cmd_opts" ]; then
+ OCF_RESKEY_run_cmd="$OCF_RESKEY_run_cmd $OCF_RESKEY_run_cmd_opts"
fi
if [ "$OCF_RESKEY_image" = "$OCF_RESKEY_image_default" ]; then
@@ -1377,9 +1615,7 @@ podman_start()
ocf_log info "starting existing container $CONTAINER."
ocf_run podman start "$CONTAINER"
else
- # make sure any previous container matching our container name is cleaned up first.
- # we already know at this point it wouldn't be running
- remove_container
+ ocf_log info "starting new container $CONTAINER."
run_new_container "$run_opts" "$OCF_RESKEY_image" "$OCF_RESKEY_run_cmd"
if [ $? -eq 125 ]; then
return $OCF_ERR_GENERIC
@@ -1439,7 +1675,6 @@ podman_stop()
local rc
podman_simple_status
if [ $? -eq $OCF_NOT_RUNNING ]; then
- remove_container
ocf_log info "could not leave members list: etcd container not running"
return $OCF_SUCCESS
fi
@@ -1475,7 +1710,7 @@ podman_stop()
ocf_run podman kill "$CONTAINER"
rc=$?
else
- ocf_log debug "waiting $timeout second[s] before killing container"
+ ocf_log info "waiting $timeout second[s] before killing container"
ocf_run podman stop -t="$timeout" "$CONTAINER"
rc=$?
# on stop, systemd will automatically delete any transient
@@ -1496,11 +1731,6 @@ podman_stop()
fi
fi
- if ! remove_container; then
- ocf_exit_reason "Failed to remove stopped container, ${CONTAINER}, based on image, ${OCF_RESKEY_image}."
- return $OCF_ERR_GENERIC
- fi
-
return $OCF_SUCCESS
}
@@ -1532,6 +1762,7 @@ podman_validate()
check_binary oc
check_binary podman
check_binary jq
+ check_binary tar
if [ -z "$OCF_RESKEY_node_ip_map" ]; then
ocf_exit_reason "'node_ip_map' option is required"
@@ -1589,6 +1820,9 @@ else
fi
CONTAINER=$OCF_RESKEY_name
+POD_MANIFEST_COPY="${OCF_RESKEY_config_location}/pod.yaml"
+ETCD_CONFIGURATION_FILE="${OCF_RESKEY_config_location}/config.yaml"
+ETCD_BACKUP_FILE="${OCF_RESKEY_backup_location}/config-previous.tar.gz"
# Note: we currently monitor podman containers by with the "podman exec"
# command, so make sure that invocation is always valid by enforcing the

View File

@ -0,0 +1,193 @@
From 11cdff8c886c72c83c26e48e46a8620c06e4c2f0 Mon Sep 17 00:00:00 2001
From: E Hila <ehila@redhat.com>
Date: Tue, 9 Sep 2025 06:06:12 -0400
Subject: [PATCH] OCPBUGS-60977: podman-etcd: wrap ipv6 address in brackets for
attribute_node_ip (#2068)
When trying to determine the node ip address we need to make sure we account for ipv6 and dualstack deployments, and accordingly wrap ipv6 in brackets so it correctly resolves. Since the node ip mapping is provided by the controller, we parse out the IP address of the node from there and use a helper function for building URLs with ports to correctly use brackets for ipv6 ip addresses.
Signed-off-by: ehila <ehila@redhat.com>
---
heartbeat/podman-etcd | 77 ++++++++++++++++++++++++++++---------------
1 file changed, 51 insertions(+), 26 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 884b7c579..4969fbaaf 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -347,21 +347,41 @@ remove_container()
return $rc
}
+# Correctly wraps an ipv6 in [] for url otherwise use return normal ipv4 address.
+ip_url() {
+ local ip_addr=$1
+ local value
+ if echo "$ip_addr" | grep -q ":" ; then
+ value="[$ip_addr]"
+ else
+ value="$ip_addr"
+ fi
+ echo "https://$value"
+}
+
attribute_node_ip()
{
local action="$1"
local attribute="node_ip"
- local value
+ local ip_addr name
- if ! value=$(ip -brief addr show "$OCF_RESKEY_nic" | awk '{gsub("/.*", "", $3); print $3}'); then
- rc=$?
- ocf_log err "could not get node ip, error code: $rc"
- return "$rc"
+ for node in $(echo "$OCF_RESKEY_node_ip_map" | sed "s/\s//g;s/;/ /g"); do
+ name=$(echo "$node" | cut -d: -f1)
+ # ignore other nodes
+ if [ "$name" != "$NODENAME" ]; then
+ continue
+ fi
+ ip_addr=$(echo "$node" | cut -d: -f2-) # Grab everything after the first : this covers ipv4/ipv6
+ done
+
+ if [ -z "$ip_addr" ]; then
+ ocf_log err "ip address was empty when querying (getent ahosts) for hostname: $(hostname -f)"
+ return 1
fi
case "$action" in
get)
- echo "$value"
+ echo "$ip_addr"
;;
update)
if ! crm_attribute --type nodes --node "$NODENAME" --name "$attribute" --update "$value"; then
@@ -409,26 +429,28 @@ get_env_from_manifest() {
}
prepare_env() {
- local name ip standalone_node
+ local name ip ipurl standalone_node
NODEIP="$(attribute_node_ip get)"
+ NODEIPURL=$(ip_url $NODEIP)
if is_force_new_cluster; then
- ALL_ETCD_ENDPOINTS="https://$NODEIP:2379"
+ ALL_ETCD_ENDPOINTS="$NODEIPURL:2379"
ETCD_INITIAL_CLUSTER_STATE="new"
- ETCD_INITIAL_CLUSTER="$NODENAME=https://$NODEIP:2380"
+ ETCD_INITIAL_CLUSTER="$NODENAME=$NODEIPURL:2380"
else
ETCD_INITIAL_CLUSTER_STATE="existing"
for node in $(echo "$OCF_RESKEY_node_ip_map" | sed "s/\s//g;s/;/ /g"); do
- name=$(echo "$node" | awk -F":" '{print $1}')
- ip=$(echo "$node" | awk -F":" '{print $2}')
+ name=$(echo "$node" | cut -d: -f1)
+ ip=$(echo "$node" | cut -d: -f2-) # Grab everything after the first : this covers ipv4/ipv6
+ ipurl="$(ip_url $ip)"
if [ -z "$name" ] || [ -z "$ip" ]; then
ocf_exit_reason "name or ip missing for 1 or more nodes"
exit $OCF_ERR_CONFIGURED
fi
- [ -z "$ALL_ETCD_ENDPOINTS" ] && ALL_ETCD_ENDPOINTS="https://$ip:2379" || ALL_ETCD_ENDPOINTS="$ALL_ETCD_ENDPOINTS,https://$ip:2379"
- [ -z "$ETCD_INITIAL_CLUSTER" ] && ETCD_INITIAL_CLUSTER="$name=https://$ip:2380" || ETCD_INITIAL_CLUSTER="$ETCD_INITIAL_CLUSTER,$name=https://$ip:2380"
+ [ -z "$ALL_ETCD_ENDPOINTS" ] && ALL_ETCD_ENDPOINTS="$ipurl:2379" || ALL_ETCD_ENDPOINTS="$ALL_ETCD_ENDPOINTS,$ipurl:2379"
+ [ -z "$ETCD_INITIAL_CLUSTER" ] && ETCD_INITIAL_CLUSTER="$name=$ipurl:2380" || ETCD_INITIAL_CLUSTER="$ETCD_INITIAL_CLUSTER,$name=$ipurl:2380"
done
fi
@@ -609,9 +631,11 @@ add_member_as_learner()
local rc
local member_name=$1
local member_ip=$2
+ local endpoint_url=$(ip_url $(attribute_node_ip get))
+ local peer_url=$(ip_url $member_ip)
ocf_log info "add $member_name ($member_ip) to the member list as learner"
- out=$(podman exec "${CONTAINER}" etcdctl --endpoints="https://$(attribute_node_ip get):2379" member add "$member_name" --peer-urls="https://$member_ip:2380" --learner)
+ out=$(podman exec "${CONTAINER}" etcdctl --endpoints="$endpoint_url:2379" member add "$member_name" --peer-urls="$peer_url:2380" --learner)
rc=$?
if [ $rc -ne 0 ]; then
ocf_log err "could not add $member_name as learner, error code: $rc"
@@ -806,14 +830,15 @@ get_peer_node_name() {
get_all_etcd_endpoints() {
for node in $(echo "$OCF_RESKEY_node_ip_map" | sed "s/\s//g;s/;/ /g"); do
- name=$(echo "$node" | awk -F":" '{print $1}')
- ip=$(echo "$node" | awk -F":" '{print $2}')
+ name=$(echo "$node" | cut -d: -f1)
+ ip=$(echo "$node" | cut -d: -f2-) # Grab everything after the first : this covers ipv4/ipv6
+ ipurl="$(ip_url $ip)"
if [ -z "$name" ] || [ -z "$ip" ]; then
ocf_exit_reason "name or ip missing for 1 or more nodes"
exit $OCF_ERR_CONFIGURED
fi
- [ -z "$ALL_ETCD_ENDPOINTS" ] && ALL_ETCD_ENDPOINTS="https://$ip:2379" || ALL_ETCD_ENDPOINTS="$ALL_ETCD_ENDPOINTS,https://$ip:2379"
+ [ -z "$ALL_ETCD_ENDPOINTS" ] && ALL_ETCD_ENDPOINTS="$ipurl:2379" || ALL_ETCD_ENDPOINTS="$ALL_ETCD_ENDPOINTS,$ipurl:2379"
done
echo "$ALL_ETCD_ENDPOINTS"
}
@@ -831,7 +856,7 @@ get_member_list_json() {
# Get the list of members visible to the current node
local this_node_endpoint
- this_node_endpoint="https://$(attribute_node_ip get):2379"
+ this_node_endpoint="$(ip_url $(attribute_node_ip get)):2379"
podman exec "${CONTAINER}" etcdctl member list --endpoints="$this_node_endpoint" -w json
}
@@ -886,14 +911,14 @@ check_peers()
# ]
# }
for node in $(echo "$OCF_RESKEY_node_ip_map" | sed "s/\s//g;s/;/ /g"); do
- name=$(echo "$node" | awk -F":" '{print $1}')
+ name=$(echo "$node" | cut -d: -f1)
# do not check itself
if [ "$name" = "$NODENAME" ]; then
continue
fi
# Check by IP instead of Name since "learner" members appear only in peerURLs, not by Name.
- ip=$(echo "$node" | awk -F":" '{print $2}')
+ ip=$(echo "$node" | cut -d: -f2-) # Grab everything after the first : this covers ipv4/ipv6
id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .peerURLs | map(test(\"$ip\")) | any).ID")
if [ -z "$id" ]; then
ocf_log info "$name is not in the members list"
@@ -1307,7 +1332,7 @@ podman_start()
--log-level=info \
--experimental-initial-corrupt-check=true \
--snapshot-count=10000 \
- --initial-advertise-peer-urls=https://${NODEIP}:2380 \
+ --initial-advertise-peer-urls=$NODEIPURL:2380 \
--cert-file=/etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-serving-${NODENAME}.crt \
--key-file=/etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-serving-${NODENAME}.key \
--trusted-ca-file=$SERVER_CACERT \
@@ -1316,11 +1341,11 @@ podman_start()
--peer-key-file=$ETCD_PEER_KEY \
--peer-trusted-ca-file=$SERVER_CACERT \
--peer-client-cert-auth=true \
- --advertise-client-urls=https://${NODEIP}:2379 \
- --listen-client-urls=https://${LISTEN_CLIENT_URLS}:2379,unixs://${NODEIP}:0 \
- --listen-peer-urls=https://${LISTEN_PEER_URLS}:2380 \
+ --advertise-client-urls=$NODEIPURL:2379 \
+ --listen-client-urls=$(ip_url ${LISTEN_CLIENT_URLS}):2379,unixs://${NODEIP}:0 \
+ --listen-peer-urls=$(ip_url ${LISTEN_PEER_URLS}):2380 \
--metrics=extensive \
- --listen-metrics-urls=https://${LISTEN_METRICS_URLS}:9978"
+ --listen-metrics-urls=$(ip_url ${LISTEN_METRICS_URLS}):9978"
if [ -n "$OCF_RESKEY_run_cmd_opts" ]; then
OCF_RESKEY_run_cmd="$OCF_RESKEY_run_cmd $OCF_RESKEY_run_cmd_opts"
fi
@@ -1430,7 +1455,7 @@ podman_stop()
ocf_log info "last member. Not leaving the member list"
else
ocf_log info "leaving members list as member with ID $member_id"
- endpoint="https://$(attribute_node_ip get):2379"
+ endpoint="$(ip_url $(attribute_node_ip get)):2379"
if ! ocf_run podman exec "$CONTAINER" etcdctl member remove "$member_id" --endpoints="$endpoint"; then
rc=$?
ocf_log err "error leaving members list, error code: $rc"

View File

@ -0,0 +1,186 @@
From 1afdd91b2961061937fc802c575304ede8d79286 Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Wed, 10 Sep 2025 16:56:56 +0200
Subject: [PATCH] podman-etcd: Add cluster-wide force_new_cluster attribute
checking
Implement cluster-wide validation of force_new_cluster attribute to resolve
race conditions during automated cluster recovery. The enhancement ensures
agents check for the cluster-wide attribute before falling back to local
etcd revision comparison.
Key changes:
- Enhanced get_force_new_cluster() to query all cluster nodes
- Ensure force_new_cluster is not set in both nodes to prevent
conflicting recovery attempts
- Updated startup logic to prioritize cluster-wide attribute checking
fixes OCPBUGS-61117
---
heartbeat/podman-etcd | 107 ++++++++++++++++++++++++++++--------------
1 file changed, 72 insertions(+), 35 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 33804414a..f3a6da5e2 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -794,54 +794,72 @@ set_force_new_cluster()
return $rc
}
+# get_force_new_cluster returns a space-separated list of nodes that have the force_new_cluster attribute set.
+# Return values:
+# - Exit code 0 with non-empty output: One or more nodes have the force_new_cluster attribute set
+# - Exit code 0 with empty output: No nodes have the force_new_cluster attribute set
+# - Exit code 1 with empty output: Error occurred while querying the cluster nodes
get_force_new_cluster()
{
- crm_attribute --lifetime reboot --query --name "force_new_cluster" | awk -F"value=" '{print $2}'
+ local node nodes value
+ local holders=""
+
+ if ! nodes=$(crm_node -l | awk '{print $2}'); then
+ ocf_log err "could not get force_new_cluster attribute, crm_node error code: $?"
+ return 1
+ fi
+ if [ -z "$nodes" ]; then
+ ocf_log err "could not get force_new_cluster attribute, the list of nodes is empty"
+ return 1
+ fi
+
+ for node in $nodes; do
+ if ! value=$(crm_attribute --query --lifetime reboot --name "force_new_cluster" --node "$node" 2>/dev/null | awk -F'value=' '{print $2}' | tr -d "'"); then
+ ocf_log err "could not get force_new_cluster attribute, crm_attribut error code: $?"
+ return 1
+ fi
+ if [ -n "$value" ]; then
+ holders="$holders$node "
+ fi
+ done
+ echo "$holders"
}
+
clear_force_new_cluster()
{
- local force_new_cluster_node
-
- force_new_cluster_node=$(get_force_new_cluster)
- if [ -z "$force_new_cluster_node" ]; then
- ocf_log info "$NODENAME: force_new_cluster attribute not set"
+ # only the holder of "force_new_cluster" attribute can delete it
+ if ! is_force_new_cluster; then
+ ocf_log info "force_new_cluster unset or not owned by $NODENAME"
return $OCF_SUCCESS
fi
- # only the holder of "force_new_cluster" attribute can delete it
- if [ "$NODENAME" = "$force_new_cluster_node" ]; then
- crm_attribute --lifetime reboot --name "force_new_cluster" --delete
- rc=$?
- if [ $rc -ne 0 ]; then
- ocf_log err "could not clear force_new_cluster attribute, error code: $rc"
- else
- ocf_log info "$NODENAME: force_new_cluster attribute cleared"
- fi
- return $rc
- else
- ocf_log info "$NODENAME does not hold force_new_cluster ($force_new_cluster_node has it)"
- return $OCF_SUCCESS
+ if ! crm_attribute --delete --lifetime reboot --node "$NODENAME" --name "force_new_cluster"; then
+ ocf_log err "could not clear force_new_cluster attribute, error code: $?"
+ return $OCF_ERR_GENERIC
fi
+
+ ocf_log info "$NODENAME: force_new_cluster attribute cleared"
+ return $OCF_SUCCESS
}
+
is_force_new_cluster()
{
- # Return 0 if 'force_new_cluster' is set and the value matches the current node name, 1 otherwise.
- local value
+ # Return 0 if 'force_new_cluster' is set on the current node, 1 otherwise.
+ local fnc_holders
- value=$(get_force_new_cluster)
- if [ -z "$value" ]; then
- ocf_log debug "force_new_cluster attribute is not set"
- return 1
+ if ! fnc_holders=$(get_force_new_cluster); then
+ ocf_exit_reason "is_force_new_cluster: Failed to get force_new_cluster node holders"
+ exit $OCF_ERR_GENERIC
fi
- if [ "$value" = "$NODENAME" ]; then
+ if echo "$fnc_holders" | grep -q -w "$NODENAME"; then
ocf_log debug "$NODENAME has force_new_cluster set"
return 0
fi
- ocf_log info "force_new_cluster attribute set on peer node $value"
+ ocf_log debug "force_new_cluster attribute is not set on $NODENAME"
return 1
}
@@ -1415,17 +1433,34 @@ podman_start()
return "$OCF_ERR_GENERIC"
fi
- # force-new-cluster property is a runtime-scoped flag that instructs the agent to force a new cluster-of-1.
- # Since this attribute is configured with a reboot-lifetime, it is automatically cleared when the machine reboots.
- # If the agent detects during its start that this property is set, it indicates that the flag was explicitly set
- # during the current node boot session, implying a deliberate request to recover the cluster.
if ocf_is_true "$pod_was_running"; then
ocf_log info "static pod was running: start normally"
else
- if is_force_new_cluster; then
- ocf_log notice "'$NODENAME' marked to force-new-cluster"
+ local fnc_holders
+ if ! fnc_holders=$(get_force_new_cluster); then
+ ocf_exit_reason "Failed to get force_new_cluster node holders"
+ return "$OCF_ERR_GENERIC"
+ fi
+
+ local fnc_holder_count
+ fnc_holder_count=$(echo "$fnc_holders" | wc -w)
+ if [ "$fnc_holder_count" -gt 1 ]; then
+ ocf_exit_reason "force_new_cluster attribute is set on multiple nodes ($fnc_holders)"
+ return "$OCF_ERR_GENERIC"
+ fi
+
+ if [ "$fnc_holder_count" -eq 1 ]; then
+ if echo "$fnc_holders" | grep -q -w "$NODENAME"; then
+ # Attribute is set on the local node.
+ ocf_log notice "$NODENAME marked to force-new-cluster"
+ JOIN_AS_LEARNER=false
+ else
+ # Attribute is set on a peer node.
+ ocf_log info "$NODENAME shall join as learner because force_new_cluster is set on peer $fnc_holders"
+ JOIN_AS_LEARNER=true
+ fi
else
- ocf_log info "'$NODENAME' is not marked to force-new-cluster"
+ ocf_log info "no node is marked to force-new-cluster"
# When the local agent starts, we can infer the cluster state by counting
# how many agents are starting or already active:
# - 1 active agent: it's the peer (we are just starting)
@@ -1522,7 +1557,7 @@ podman_start()
for try in $(seq $retries); do
learner_node=$(attribute_learner_node get)
if [ "$NODENAME" != "$learner_node" ]; then
- ocf_log info "$learner_node is not in the member list yet. Retry in $poll_interval_sec seconds."
+ ocf_log info "$NODENAME is not in the member list yet. Retry in $poll_interval_sec seconds."
sleep $poll_interval_sec
continue
fi
@@ -1673,6 +1708,8 @@ podman_stop()
{
local timeout=60
local rc
+
+ ocf_log notice "podman-etcd stop"
podman_simple_status
if [ $? -eq $OCF_NOT_RUNNING ]; then
ocf_log info "could not leave members list: etcd container not running"

View File

@ -0,0 +1,321 @@
From a31f15104fc712cd25f8a59d49f1bbcdbbbc5434 Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Tue, 30 Sep 2025 11:54:44 +0200
Subject: [PATCH 1/2] Refactor(podman-etcd): improve peer checking and
leadership loss detection
The check_peers function is broken up into smaller, more manageable
functions. This refactoring separates the logic for detecting a loss of
cluster leadership from the logic for managing peer membership.
The main function is renamed to check_peer as there is only 1 peer to
check (it was check_peers).
---
heartbeat/podman-etcd | 78 +++++++++++++++++++++++++------------------
1 file changed, 45 insertions(+), 33 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index f3a6da5e2..3d1e4c520 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -1014,42 +1014,35 @@ get_member_list_json() {
podman exec "${CONTAINER}" etcdctl member list --endpoints="$this_node_endpoint" -w json
}
-check_peers()
+detect_cluster_leadership_loss()
{
- # Check peers endpoint status and locally accessible member list
- local member_list_json
-
- if ! container_exists; then
- # we need a running container to execute etcdctl.
- return $OCF_SUCCESS
+ endpoint_status_json=$(get_endpoint_status_json)
+ ocf_log info "endpoint status: $endpoint_status_json"
+
+ count_endpoints=$(printf "%s" "$endpoint_status_json" | jq -r ".[].Endpoint" | wc -l)
+ if [ "$count_endpoints" -eq 1 ]; then
+ ocf_log info "one endpoint only: checking status errors"
+ endpoint_status_errors=$(printf "%s" "$endpoint_status_json" | jq -r ".[0].Status.errors")
+ if echo "$endpoint_status_errors" | grep -q "no leader"; then
+ set_force_new_cluster
+ set_standalone_node
+ ocf_exit_reason "$NODENAME must force a new cluster"
+ return $OCF_ERR_GENERIC
+ fi
+ if [ "$endpoint_status_errors" != "null" ]; then
+ ocf_log err "unmanaged endpoint status error: $endpoint_status_errors"
+ fi
fi
- member_list_json=$(get_member_list_json)
- rc=$?
- ocf_log debug "member list: $member_list_json"
- if [ $rc -ne 0 ]; then
- ocf_log info "podman failed to get member list, error code: $rc"
-
- endpoint_status_json=$(get_endpoint_status_json)
- ocf_log info "endpoint status: $endpoint_status_json"
-
- count_endpoints=$(printf "%s" "$endpoint_status_json" | jq -r ".[].Endpoint" | wc -l)
- if [ "$count_endpoints" -eq 1 ]; then
- ocf_log info "one endpoint only: checking status errors"
- endpoint_status_errors=$(printf "%s" "$endpoint_status_json" | jq -r ".[0].Status.errors")
- if echo "$endpoint_status_errors" | grep -q "no leader"; then
- set_force_new_cluster
- set_standalone_node
- ocf_exit_reason "$NODENAME must force a new cluster"
- return $OCF_ERR_GENERIC
- fi
- if [ "$endpoint_status_errors" != "null" ]; then
- ocf_log err "unmanaged endpoint status error: $endpoint_status_errors"
- fi
- fi
+ return $OCF_SUCCESS
+}
- return $OCF_SUCCESS
- fi
+manage_peer_membership()
+{
+ # Read etcd member list to detect the status of the peer member.
+ # If the peer is missing from the member list, it will be added back as learner
+ # If the peer is back in the member list, we ensure that the related CIB attributes (standalone and learner_node) are reset
+ local member_list_json="$1"
# Example of .members[] instance fields in member list json format:
# NOTE that "name" is present in voting members only, while "isLearner" in learner members only
@@ -1083,6 +1076,25 @@ check_peers()
clear_standalone_and_learner_if_not_learners "$member_list_json"
fi
done
+}
+
+check_peer()
+{
+ # Check peers endpoint status and locally accessible member list
+ local member_list_json
+
+ # we need a running container to execute etcdctl.
+ if ! container_exists; then
+ return $OCF_SUCCESS
+ fi
+
+ if ! member_list_json=$(get_member_list_json); then
+ ocf_log info "podman failed to get member list, error code: $?"
+ detect_cluster_leadership_loss
+ return $?
+ fi
+
+ manage_peer_membership "$member_list_json"
return $OCF_SUCCESS
}
@@ -1124,7 +1136,7 @@ podman_monitor()
# monitor operation to fail.
# TODO: move this inside check_peers where we already query member list json
attribute_node_member_id update
- if ! check_peers; then
+ if ! check_peer; then
return $OCF_ERR_GENERIC
fi
From de7c73a933cefb8f7b9e810bd23c3d12f6d6f29a Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Tue, 30 Sep 2025 18:38:06 +0200
Subject: [PATCH 2/2] OCPBUGS-42808: podman-etcd: add automatic learner member
promotion
Automatically promote etcd learner members to voting members when detected.
Includes refactored member management functions and improved validation.
---
heartbeat/podman-etcd | 108 ++++++++++++++++++++++++++++++------------
1 file changed, 79 insertions(+), 29 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 3d1e4c520..e1425ec02 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -712,6 +712,22 @@ attribute_node_revision_peer()
crm_attribute --query --type nodes --node "$nodename" --name "revision" | awk -F"value=" '{print $2}'
}
+# Converts a decimal number to hexadecimal format with validation
+# Args: $1 - decimal number (test for non-negative integer too)
+# Returns: 0 on success, OCF_ERR_GENERIC on invalid input
+# Outputs: hexadecimal representation to stdout
+decimal_to_hex() {
+ local dec=$1
+
+ if ! echo "$dec" | grep -q "^[1-9][0-9]*$"; then
+ ocf_log err "Invalid member ID format: '$dec' (expected decimal number)"
+ return $OCF_ERR_GENERIC
+ fi
+
+ printf "%x" "$dec"
+ return $OCF_SUCCESS
+}
+
attribute_node_member_id()
{
local action="$1"
@@ -737,7 +753,7 @@ attribute_node_member_id()
return "$rc"
fi
- local value
+ local value value_hex
if ! value=$(echo -n "$member_list_json" | jq -r ".header.member_id"); then
rc=$?
ocf_log err "could not get $attribute from member list JSON, error code: $rc"
@@ -745,8 +761,11 @@ attribute_node_member_id()
fi
# JSON member_id is decimal, while etcdctl command needs the hex version
- value=$(printf "%x" "$value")
- if ! crm_attribute --type nodes --node "$NODENAME" --name "$attribute" --update "$value"; then
+ if ! value_hex=$(decimal_to_hex "$value"); then
+ ocf_log err "could not convert decimal member_id '$value' to hex, error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
+ if ! crm_attribute --type nodes --node "$NODENAME" --name "$attribute" --update "$value_hex"; then
rc=$?
ocf_log err "could not update etcd $attribute, error code: $rc"
return "$rc"
@@ -905,42 +924,70 @@ clear_standalone_node()
crm_attribute --name "standalone_node" --delete
}
-clear_standalone_and_learner_if_not_learners()
+
+# Promotes an etcd learner member to a voting member
+# Args: $1 - learner member ID in decimal format
+# Returns: OCF_SUCCESS (even on expected promotion failures), OCF_ERR_GENERIC on conversion errors
+# Note: Promotion failures are expected and logged as info (peer may not be up-to-date)
+promote_learner_member()
+{
+ local learner_member_id=$1
+
+ # JSON member_id is decimal, while etcdctl command needs the hex version
+ if ! learner_member_id_hex=$(decimal_to_hex "$learner_member_id"); then
+ ocf_log err "could not convert decimal member_id '$learner_member_id' to hex, error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
+ if ! ocf_run podman exec "${CONTAINER}" etcdctl member promote "$learner_member_id_hex" 2>&1; then
+ # promotion is expected to fail if the peer is not yet up-to-date
+ ocf_log info "could not promote member $learner_member_id_hex, error code: $?"
+ return $OCF_SUCCESS
+ fi
+ ocf_log info "successfully promoted member '$learner_member_id_hex'"
+ return $OCF_SUCCESS
+}
+
+# Reconciles etcd cluster member states
+# Promotes learner members or clears standalone/learner attributes as needed
+# Args: $1 - member list JSON from etcdctl
+# Returns: OCF_SUCCESS on completion, OCF_ERR_GENERIC on errors
+# Note: Only operates when exactly 2 started members are present
+reconcile_member_state()
{
local rc
local member_list_json="$1"
- number_of_members=$(printf "%s" "$member_list_json" | jq -r ".members[].ID" | wc -l)
- if [ "$number_of_members" -ne 2 ]; then
- ocf_log info "could not clear standalone_node, nor learner_node properties: found $number_of_members members, need 2"
+ # count only the started members, which have the ".name" JSON field
+ number_of_started_members=$(printf "%s" "$member_list_json" | jq -r ".members[].name | select(. != null)" | wc -l)
+ if [ "$number_of_started_members" -ne 2 ]; then
+ ocf_log info "could not clear standalone_node, nor learner_node properties: found $number_of_started_members members, need 2"
return $OCF_SUCCESS
fi
- id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .isLearner==true ).ID")
+ learner_member_id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .isLearner==true ).ID")
rc=$?
if [ $rc -ne 0 ]; then
ocf_log err "could not get isLearner field from member list, error code: $rc"
return $rc
fi
- if [ -z "$id" ]; then
- clear_standalone_node
- rc=$?
- if [ $rc -ne 0 ]; then
- ocf_og error "could not clear standalone_node attribute, error code: $rc"
- return $rc
- fi
+ if [ -n "$learner_member_id" ]; then
+ promote_learner_member "$learner_member_id"
+ return $?
fi
- if [ -z "$id" ]; then
- attribute_learner_node clear
- rc=$?
- if [ $rc -ne 0 ]; then
- ocf_og error "could not clear learner_node attribute, error code: $rc"
- return $rc
+
+ if [ -z "$learner_member_id" ]; then
+ if ! clear_standalone_node; then
+ ocf_log error "could not clear standalone_node attribute, error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
+ if ! attribute_learner_node clear; then
+ ocf_log error "could not clear learner_node attribute, error code: $?"
+ return $OCF_ERR_GENERIC
fi
fi
- return $rc
+ return $OCF_SUCCESS
}
attribute_learner_node()
@@ -1019,7 +1066,7 @@ detect_cluster_leadership_loss()
endpoint_status_json=$(get_endpoint_status_json)
ocf_log info "endpoint status: $endpoint_status_json"
- count_endpoints=$(printf "%s" "$endpoint_status_json" | jq -r ".[].Endpoint" | wc -l)
+ count_endpoints=$(printf "%s" "$endpoint_status_json" | jq -r ".[].Endpoint" | wc -l)
if [ "$count_endpoints" -eq 1 ]; then
ocf_log info "one endpoint only: checking status errors"
endpoint_status_errors=$(printf "%s" "$endpoint_status_json" | jq -r ".[0].Status.errors")
@@ -1037,11 +1084,14 @@ detect_cluster_leadership_loss()
return $OCF_SUCCESS
}
+
+# Manages etcd peer membership by detecting and handling missing or rejoining peers
+# Adds missing peers as learners and reconciles member states when peers rejoin
+# Args: $1 - member list JSON from etcdctl
+# Returns: OCF_SUCCESS on completion, OCF_ERR_GENERIC on errors
+# Note: Iterates through all peer nodes to ensure proper cluster membership
manage_peer_membership()
{
- # Read etcd member list to detect the status of the peer member.
- # If the peer is missing from the member list, it will be added back as learner
- # If the peer is back in the member list, we ensure that the related CIB attributes (standalone and learner_node) are reset
local member_list_json="$1"
# Example of .members[] instance fields in member list json format:
@@ -1066,14 +1116,14 @@ manage_peer_membership()
# Check by IP instead of Name since "learner" members appear only in peerURLs, not by Name.
ip=$(echo "$node" | cut -d: -f2-) # Grab everything after the first : this covers ipv4/ipv6
- id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .peerURLs | map(test(\"$ip\")) | any).ID")
- if [ -z "$id" ]; then
+ peer_member_id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .peerURLs | map(test(\"$ip\")) | any).ID")
+ if [ -z "$peer_member_id" ]; then
ocf_log info "$name is not in the members list"
add_member_as_learner "$name" "$ip"
set_standalone_node
else
ocf_log debug "$name is in the members list by IP: $ip"
- clear_standalone_and_learner_if_not_learners "$member_list_json"
+ reconcile_member_state "$member_list_json"
fi
done
}

View File

@ -19,7 +19,7 @@ index bc8935782..0d34c7c65 100644
--- a/doc/man/Makefile.am
+++ b/doc/man/Makefile.am
@@ -187,6 +187,7 @@ man_MANS = ocf_heartbeat_AoEtarget.7 \
ocf_heartbeat_pgsqlms.7 \
ocf_heartbeat_pgsql.7 \
ocf_heartbeat_pingd.7 \
ocf_heartbeat_podman.7 \
+ ocf_heartbeat_podman-etcd.7 \
@ -31,7 +31,7 @@ index 5c41e0038..839505af9 100644
--- a/heartbeat/Makefile.am
+++ b/heartbeat/Makefile.am
@@ -159,6 +159,7 @@ ocf_SCRIPTS = AoEtarget \
pgsqlms \
pgsql \
pingd \
podman \
+ podman-etcd \

View File

@ -45,7 +45,7 @@
Name: resource-agents
Summary: Open Source HA Reusable Cluster Resource Scripts
Version: 4.10.0
Release: 71%{?rcver:%{rcver}}%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty}}%{?dist}.6
Release: 71%{?rcver:%{rcver}}%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty}}%{?dist}.11
License: GPLv2+ and LGPLv2+
URL: https://github.com/ClusterLabs/resource-agents
Source0: %{upstream_prefix}-%{upstream_version}.tar.gz
@ -148,12 +148,17 @@ Patch95: RHEL-66292-2-aws-agents-reuse-imds-token-improvements.patch
Patch96: RHEL-68739-awsvip-add-interface-parameter.patch
Patch97: RHEL-69734-1-openstack-cinder-volume-wait-for-volume-to-be-available.patch
Patch98: RHEL-69734-2-openstack-cinder-volume-fix-detach-not-working-during-start-action.patch
Patch105: RHEL-79819-portblock-fix-version-detection.patch
Patch106: RHEL-88035-Filesystem-add-support-for-aznfs.patch
Patch107: RHEL-88429-1-podman-etcd-new-ra.patch
Patch108: RHEL-88429-2-podman-etcd-remove-unused-actions-from-metadata.patch
Patch109: RHEL-88429-3-podman-etcd-fix-listen-peer-urls-binding.patch
Patch110: RHEL-101705-Filesystem-fix-issue-with-Vormetric-mounts.patch
Patch99: RHEL-86602-portblock-fix-version-detection.patch
Patch100: RHEL-88430-1-podman-etcd-new-ra.patch
Patch101: RHEL-88430-2-podman-etcd-remove-unused-actions-from-metadata.patch
Patch102: RHEL-88430-3-podman-etcd-fix-listen-peer-urls-binding.patch
Patch103: RHEL-92481-Filesystem-add-support-for-aznfs.patch
Patch104: RHEL-101705-Filesystem-fix-issue-with-Vormetric-mounts.patch
Patch105: RHEL-113108-podman-etcd-add-oom-parameter.patch
Patch106: RHEL-113813-podman-etcd-wrap-ipv6-address-in-brackets.patch
Patch107: RHEL-113810-podman-etcd-preserve-containers-for-debugging.patch
Patch108: RHEL-116208-podman-etcd-add-cluster-wide-force_new_cluster-attribute-check.patch
Patch109: RHEL-119502-podman-etcd-add-automatic-learner-member-promotion.patch
# bundled ha-cloud-support libs
Patch500: ha-cloud-support-aliyun.patch
@ -376,12 +381,17 @@ exit 1
%patch -p1 -P 96
%patch -p1 -P 97
%patch -p1 -P 98
%patch -p1 -P 99
%patch -p1 -P 100 -F1
%patch -p1 -P 101
%patch -p1 -P 102
%patch -p1 -P 103
%patch -p1 -P 104
%patch -p1 -P 105
%patch -p1 -P 106
%patch -p1 -P 107
%patch -p1 -P 108
%patch -p1 -P 109
%patch -p1 -P 110
# bundled ha-cloud-support libs
%patch -p1 -P 500
@ -711,19 +721,43 @@ rm -rf %{buildroot}/usr/share/doc/resource-agents
%{_usr}/lib/ocf/lib/heartbeat/OCF_*.pm
%changelog
* Tue Aug 05 2025 Alan Steinberg <alan.steinberg@oracle.com> - 4.10.0-71.6
* Wed Oct 15 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-71.11
- podman-etcd: add automatic learner member promotion
Resolves: RHEL-119502
* Mon Sep 22 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-71.8
- podman-etcd: wrap ipv6 address in brackets
- podman-etcd: preserve containers for debugging
- podman-etcd: add cluster-wide force_new_cluster attribute check
Resolves: RHEL-113813, RHEL-113810, RHEL-116208
* Mon Sep 8 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-71.7
- podman-etcd: add oom parameter to be able to tune the Out-Of-Memory (OOM)
score for etcd containers
Resolves: RHEL-113108
* Thu Jul 3 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-71.6
- Filesystem: fix issue with Vormetric mounts
Resolves: RHEL-101705
* Mon Apr 28 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-71.5
* Tue May 20 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-71.5
- podman-etcd: new resource agent
Resolves: RHEL-88430
* Mon May 19 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-71.4
- Filesystem: add support for aznfs
Resolves: RHEL-92481
* Wed Apr 9 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-71.1
- portblock: fix iptables version detection
Resolves: RHEL-88429
Resolves: RHEL-88035
Resolves: HEL-79819
Resolves: RHEL-86602
* Fri Jan 10 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-71
- openstack-cinder-volume: wait for volume to be available