import Oracle_OSS resource-agents-4.10.0-111.el9_8.4

This commit is contained in:
AlmaLinux RelEng Bot 2026-06-26 09:23:59 -04:00
parent 0a163b6838
commit 2fe04c28a5
15 changed files with 1381 additions and 10 deletions

View File

@ -8,7 +8,7 @@
-import requests.adapters
-import urllib3.util
+try:
+ sys.path.insert(0, '/usr/lib/fence-agents/support/ibm')
+ sys.path.insert(0, '/usr/lib/fence-agents/support/ibm/lib/python#PYTHON3_VERSION#/site-packages')
+ import requests
+ import requests.adapters
+ import urllib3.util

View File

@ -0,0 +1,156 @@
--- a/heartbeat/portblock 2026-02-27 08:43:50.813925268 +0100
+++ b/heartbeat/portblock 2026-02-27 08:44:40.481824601 +0100
@@ -29,12 +29,17 @@
OCF_RESKEY_direction_default="in"
OCF_RESKEY_action_default=""
OCF_RESKEY_method_default="drop"
-OCF_RESKEY_status_check_default="rule"
OCF_RESKEY_ip_default="0.0.0.0/0"
OCF_RESKEY_reset_local_on_unblock_stop_default="false"
OCF_RESKEY_tickle_dir_default=""
OCF_RESKEY_sync_script_default=""
+if ocf_is_ms; then
+ OCF_RESKEY_status_check_default="rule"
+else
+ OCF_RESKEY_status_check_default="pseudo"
+fi
+
: ${OCF_RESKEY_protocol=${OCF_RESKEY_protocol_default}}
: ${OCF_RESKEY_portno=${OCF_RESKEY_portno_default}}
: ${OCF_RESKEY_direction=${OCF_RESKEY_direction_default}}
@@ -401,6 +406,10 @@
done
}
+# A long time ago, these messages needed to go to stdout,
+# "running" / "OK" being the trigger string
+# for heartbeat in haresources mode.
+# Now they are still useful for debugging.
SayActive()
{
ocf_log debug "$CMD $method rule [$*] is running (OK)"
@@ -416,6 +425,11 @@
ocf_log debug "$CMD $method rule [$*] is inactive"
}
+SayConsideredInactive()
+{
+ ocf_log debug "$CMD $method rule [$*] considered to be inactive"
+}
+
#IptablesStatus {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
IptablesStatus() {
local rc
@@ -441,8 +455,17 @@
fi
;;
*)
- SayInactive $*
- rc=$OCF_NOT_RUNNING
+ if [ "$OCF_RESKEY_status_check" != "rule" ] \
+ && test -e "$state_file" && test "$inverse_state_file" -nt "$state_file"; then
+ # rule present, action=unblock, unblock statefile present,
+ # block state file more recent.
+ # apparently an unusual setup: unblock first, block later
+ SayConsideredActive $*
+ rc=$OCF_SUCCESS
+ else
+ SayInactive $*
+ rc=$OCF_NOT_RUNNING
+ fi
;;
esac
elif [ "$OCF_RESKEY_status_check" = "rule" ]; then
@@ -454,6 +477,7 @@
*)
SayActive $*
if [ "$__OCF_ACTION" = "monitor" ] && [ "$promotion_score" = "$SCORE_PROMOTED" ]; then
+ save_tcp_connections
rc=$OCF_RUNNING_MASTER
else
rc=$OCF_SUCCESS
@@ -463,7 +487,10 @@
else
case $5 in
block)
- if ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" status; then
+ if test -e "$state_file" && test "$inverse_state_file" -nt "$state_file"; then
+ # rule NOT present, action=block, block state file present,
+ # unblock state file more recent.
+ # expected setup: block first, unblock later
SayConsideredActive $*
rc=$OCF_SUCCESS
else
@@ -472,13 +499,15 @@
fi
;;
*)
- if ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" status; then
+ if test -e "$state_file" ; then
+ # rule NOT present, action=unblock, unblock state file present
SayActive $*
- #This is only run on real monitor events.
+ # This is only run on real monitor events (state file present).
save_tcp_connections
rc=$OCF_SUCCESS
else
- SayInactive $*
+ # rule NOT present, action=unblock, unblock state file NOT present
+ SayConsideredInactive $*
rc=$OCF_NOT_RUNNING
fi
;;
@@ -562,7 +591,7 @@
#IptablesStart {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
IptablesStart()
{
- ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" start
+ ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" start "$state_file"
case $5 in
block) IptablesBLOCK "$@"
rc=$?
@@ -584,7 +613,8 @@
#IptablesStop {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
IptablesStop()
{
- ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" stop
+ ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" stop "$state_file"
+
case $5 in
block) IptablesUNBLOCK "$@"
rc=$?
@@ -797,6 +827,33 @@
IptablesValidateAll
+# State file name for ha_pseudo_resource
+#
+# The expected usage of this agent is to pair a "block" with an "unblock",
+# and order startup and configuration of some service between these.
+#
+# The established idiom is to have two separate instances with inverse actions.
+# To "reliably" report the status of "block" during a monitor action,
+# it is not sufficient to check the existence of the blocking rule.
+#
+# It is also insufficient to rely on the pseudo resource state file
+# of this instance only.
+#
+# To know our actual expectation, we need to check the state file of the
+# "inverse" instance as well.
+#
+# Because we don't know the OCF_RESOURCE_INSTANCE value of the other instance,
+# we override the state file name for both instances to something derived from
+# our parameters.
+#
+# This should give use the same "global state" view as the "promotion score"
+# does for the promotable clone variant of this agent.
+#
+[ "$action" = block ] && inverse_action=unblock || inverse_action=block
+state_file_base=$(echo "portblock_${protocol}_${portno}_${ip}_${direction}" | tr -c '0-9a-zA-Z._' _)
+state_file=${HA_RSCTMP}/${state_file_base}_${action}
+inverse_state_file=${HA_RSCTMP}/${state_file_base}_${inverse_action}
+
case $__OCF_ACTION in
start)
IptablesStart "$protocol" "$portno" "$ip" "$direction" "$action"

View File

@ -0,0 +1,40 @@
From 5890f47bc61703130cd27d767118367f03bca95f Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Tue, 10 Mar 2026 17:26:04 +0100
Subject: [PATCH] podman-etcd: Preserve standalone voter identity during
restart
If the standalone voter restarts before the peer is added to the member
list, the learner_node attribute may not be set yet. Without checking
is_standalone, the voter incorrectly joins as a learner, causing both
nodes to become learners and creating an unrecoverable deadlock.
Check is_standalone to ensure the voter restarts in the same role it
had before the restart.
---
heartbeat/podman-etcd | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 539ad33b2..2f8aa122f 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -2002,9 +2002,16 @@ podman_start()
ocf_log info "found '$active_resources_count' active etcd resources (active: '$OCF_RESKEY_CRM_meta_notify_active_resource', stop: '$OCF_RESKEY_CRM_meta_notify_stop_resource')"
case "$active_resources_count" in
1)
- if [ "$(attribute_learner_node get)" = "$(get_peer_node_name)" ]; then
- ocf_log info "peer active but in learner mode: start normally"
+ # is_standalone may return true here due to a restart: in the previous run,
+ # this agent was the sole voter and the peer had not yet joined the member
+ # list (learner_node unset). Since standalone_node was not cleared before
+ # the restart, start normally to recover the previous cluster state.
+ if is_standalone; then
+ ocf_log info "peer active but not a voter: start normally to recover"
+ elif [ "$(attribute_learner_node get)" = "$(get_peer_node_name)" ]; then
+ ocf_log info "peer active but in learner mode: start normally to recover"
else
+ # If (A) we must join the peer's new cluster
ocf_log info "peer is active standalone: joining as learner"
JOIN_AS_LEARNER=true
fi

View File

@ -0,0 +1,130 @@
From 83d16b59a354a20bf7679af45a9acfa9f344959a Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Wed, 18 Mar 2026 11:30:31 +0100
Subject: [PATCH] OCPBUGS-78482: podman-etcd: fix "Peer URLs already exists" in
add_member_as_learner (#2136)
* podman-etcd: handle "Peer URLs already exists" in add_member_as_learner
When etcdctl member add fails with "Peer URLs already exists", the stale
member entry is removed and the add is retried.
Without this fix, add_member_as_learner returns early without setting
the learner_node attribute, causing the peer node to time out waiting
for it.
---
heartbeat/podman-etcd | 85 +++++++++++++++++++++++++++++++++++++------
1 file changed, 73 insertions(+), 12 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 2f8aa122f..860aca817 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -1072,6 +1072,49 @@ attribute_node_member_id()
esac
}
+# remove an etcd member identified by its IP from the member list
+remove_etcd_member_by_ip()
+{
+ local rc
+ local out
+ local member_ip
+ local endpoint_url
+ local member_list_json
+ local stale_member_id
+ local stale_member_id_hex
+
+ member_ip=$1
+ member_list_json=$(get_member_list_json)
+ rc=$?
+ if [ "$rc" -ne 0 ] ; then
+ ocf_log err "could not remove etcd member. Failed to get member list, error code: $rc"
+ return $OCF_ERR_GENERIC
+ fi
+
+ stale_member_id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .peerURLs | map(test(\"$member_ip\")) | any).ID")
+ if [ -z "$stale_member_id" ]; then
+ ocf_log err "could not remove etcd member. Failed to find member ID"
+ return $OCF_ERR_GENERIC
+ fi
+
+ # JSON member_id is decimal, while etcdctl command needs the hex version
+ if ! stale_member_id_hex=$(decimal_to_hex "$stale_member_id"); then
+ ocf_log err "could not remove etcd member. Failed to convert member_id '$stale_member_id' into hex format"
+ return $OCF_ERR_GENERIC
+ fi
+
+ endpoint_url=$(ip_url $(attribute_node_ip get))
+ out=$(podman exec "${CONTAINER}" etcdctl --endpoints="$endpoint_url:2379" member remove "$stale_member_id_hex" 2>&1)
+ rc=$?
+ if [ "$rc" -ne 0 ] ; then
+ ocf_log err "could not remove etcd member. etcdctl member remove command failed, error code: $rc, output: $out"
+ return $OCF_ERR_GENERIC
+ fi
+
+ ocf_log info "$out"
+ return $OCF_SUCCESS
+}
+
add_member_as_learner()
{
local rc
@@ -1079,18 +1122,36 @@ add_member_as_learner()
local member_ip=$2
local endpoint_url=$(ip_url $(attribute_node_ip get))
local peer_url=$(ip_url $member_ip)
+ local i
+ local max_retries
+ local out
- ocf_log info "add $member_name ($member_ip) 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 2>&1)
- rc=$?
- if [ $rc -ne 0 ]; then
- ocf_log err "could not add $member_name as learner, error code $rc, etcdctl output: $out"
- return $rc
- fi
- ocf_log info "$out"
+ i=0
+ max_retries=3
+ while [ "$i" -lt "$max_retries" ]; do
+ i=$((i + 1))
+ ocf_log info "adding $member_name ($member_ip) to the member list as learner (attempt $i of $max_retries)"
+ out=$(podman exec "${CONTAINER}" etcdctl --endpoints="$endpoint_url:2379" \
+ member add "$member_name" \
+ --peer-urls="$peer_url:2380" \
+ --learner 2>&1)
+ rc=$?
+ if [ "$rc" -eq 0 ]; then
+ ocf_log info "$out"
+ attribute_learner_node update "$member_name"
+ return $OCF_SUCCESS
+ fi
+ if echo "$out" | grep -q "Peer URLs already exists"; then
+ # etcd data might have stale membership data
+ ocf_log warn "could not add member: Peer URLs already exists"
+ remove_etcd_member_by_ip "$member_ip"
+ else
+ ocf_log warn "could not add member: $out"
+ fi
+ done
- attribute_learner_node update "$member_name"
- return $?
+ ocf_log err "could not add $member_name as learner, error code $rc, etcdctl output: $out"
+ return $OCF_ERR_GENERIC
}
set_force_new_cluster()
@@ -1454,8 +1515,8 @@ manage_peer_membership()
# "https://<node IP>:2379"
# ]
# }
- # NOTE that the "name" field is present in voting members only, while "isLearner"
- # field in learner members only and the value is always true (not a string) in that case.
+ # NOTE: voting members have a "name" field but no "isLearner" field,
+ # while learner members have "isLearner": true (boolean) but no "name" field, so we search for peerURLs matching.
peer_member_id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .peerURLs | map(test(\"$peer_member_ip\")) | any).ID")
if [ -z "$peer_member_id" ]; then
ocf_log info "$peer_member_name is not in the members list"

View File

@ -0,0 +1,46 @@
From 66885ea0227e847b571608015b150d391a6234d7 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Mon, 23 Feb 2026 13:35:58 +0100
Subject: [PATCH] db2: set reintegration when promotion is successful
---
heartbeat/db2 | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/heartbeat/db2 b/heartbeat/db2
index 82f2f82c3..4420b9989 100755
--- a/heartbeat/db2
+++ b/heartbeat/db2
@@ -955,6 +955,16 @@ db2_promote() {
PRIMARY/PEER/*|PRIMARY/REMOTE_CATCHUP/*|PRIMARY/REMOTE_CATCHUP_PENDING/CONNECTED|Primary/Peer)
# nothing to do, only update pacemaker's view
echo MASTER > $STATE_FILE
+
+ if [ -n "$remote_host" ]; then
+ for db in $dblist
+ do
+ reint_attr="db2hadr-${inst1}_${inst2}_${db}_reint"
+ ocf_log debug "Promotion succeeded, setting $reint_attr = 1"
+ crm_attribute -n "$reint_attr" -N "$remote_host" -v "1" -l forever
+ done
+ fi
+
return $OCF_SUCCESS
;;
@@ -981,6 +991,15 @@ db2_promote() {
# update pacemaker's view
echo MASTER > $STATE_FILE
+ if [ -n "$remote_host" ]; then
+ for db in $dblist
+ do
+ reint_attr="db2hadr-${inst1}_${inst2}_${db}_reint"
+ ocf_log debug "Promotion succeeded, setting $reint_attr = 1"
+ crm_attribute -n "$reint_attr" -N "$remote_host" -v "1" -l forever
+ done
+ fi
+
return $OCF_SUCCESS
fi

View File

@ -0,0 +1,265 @@
From c909003639ef36f995f855f5b954a5ae2132f19c Mon Sep 17 00:00:00 2001
From: Vincenzo Mauro <43814449+vimauro@users.noreply.github.com>
Date: Mon, 23 Mar 2026 11:54:51 +0100
Subject: [PATCH] OCPBUGS-76538: podman-etcd: monitor/stop hardening (#2130)
* monitor/stop hardening
* removed noisy log
* Improved return code handling + PR comments
* reduced wait_timeout_sec in podman_start
* enriched log line on container_running
* Fixed detect_cluster_leadership_loss in case podman exec fails
* Reverted detect_cluster_leadership_loss changes
* Updated return code in attribute_node_revision
* restored initial attribute_node_revision logic and updated comment
* restored original log line in attribute_node_revision
* updated return codes on changed code path that reaches pacemaker
* updated log line in check_peer
---
heartbeat/podman-etcd | 133 ++++++++++++++++++++++++++----------------
1 file changed, 83 insertions(+), 50 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 860aca817..4c9bbd4fa 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -371,25 +371,34 @@ monitor_cmd_exec()
{
local rc=$OCF_SUCCESS
local out
-
- out=$(podman exec ${CONTAINER} $OCF_RESKEY_monitor_cmd 2>&1)
- rc=$?
- # 125: no container with name or ID ${CONTAINER} found
- # 126: container state improper (not running)
- # 127: any other error
- # 255: podman 2+: container not running
- case "$rc" in
- 125|126|255)
- rc=$OCF_NOT_RUNNING
- ;;
- 0)
- ocf_log debug "monitor cmd passed: exit code = $rc"
- ;;
- *)
- ocf_exit_reason "monitor cmd failed (rc=$rc), output: $out"
- rc=$OCF_ERR_GENERIC
- ;;
- esac
+ local attempt
+ # 3 attempts × 5s = 15s worst case, fits within the 25s monitor timeout.
+ # The health check normally completes in <1s; the 5s per-attempt timeout
+ # is a safety net for when the container's process namespace is slow.
+ local max_attempts=3
+ local attempt_timeout=5
+
+ for attempt in $(seq 1 $max_attempts); do
+ out=$(timeout $attempt_timeout podman exec ${CONTAINER} $OCF_RESKEY_monitor_cmd 2>&1)
+ rc=$?
+ # 125: no container with name or ID ${CONTAINER} found
+ # 126: container state improper (not running)
+ # 127: any other error
+ # 255: podman 2+: container not running
+ case "$rc" in
+ 125|126|255)
+ return $OCF_NOT_RUNNING
+ ;;
+ 0)
+ ocf_log debug "monitor cmd passed: exit code = $rc"
+ return $OCF_SUCCESS
+ ;;
+ *)
+ ocf_log warn "monitor cmd failed (rc=$rc), output: $out"
+ rc=$OCF_ERR_GENERIC
+ ;;
+ esac
+ done
return $rc
}
@@ -527,8 +536,9 @@ get_env_from_manifest() {
exit "$OCF_ERR_INSTALLED"
fi
- if ! env_var_value=$(jq -r ".spec.containers[].env[] | select( .name == \"$env_var_name\" ).value" "$OCF_RESKEY_pod_manifest"); then
- rc=$?
+ env_var_value=$(jq -r ".spec.containers[].env[] | select( .name == \"$env_var_name\" ).value" "$OCF_RESKEY_pod_manifest")
+ rc=$?
+ if [ $rc -ne 0 ]; then
ocf_log err "could not find environment variable $env_var_name in etcd pod manifest, error code: $rc"
exit "$OCF_ERR_INSTALLED"
fi
@@ -934,10 +944,14 @@ attribute_node_cluster_id()
{
local action="$1"
local value
- if ! value=$(jq -r ".clusterId" "$ETCD_REVISION_JSON"); then
- rc=$?
+ local rc
+ value=$(jq -r ".clusterId" "$ETCD_REVISION_JSON")
+ rc=$?
+ if [ $rc -ne 0 ]; then
+ # Log the error but return success to avoid monitor failure if the file is not available yet.
+ #This should not block cluster recovery.
ocf_log err "could not get cluster_id, error code: $rc"
- return "$rc"
+ return $OCF_SUCCESS
fi
case "$action" in
@@ -945,10 +959,12 @@ attribute_node_cluster_id()
echo "$value"
;;
update)
- if ! crm_attribute --type nodes --node "$NODENAME" --name "cluster_id" --update "$value"; then
- rc=$?
+ crm_attribute --type nodes --node "$NODENAME" --name "cluster_id" --update "$value"
+ rc=$?
+ if [ $rc -ne 0 ]; then
+ # Log the error but return success to avoid monitor failure if we can not update the attribute.
ocf_log err "could not update cluster_id, error code: $rc"
- return "$rc"
+ return $OCF_SUCCESS
fi
;;
*)
@@ -983,10 +999,12 @@ attribute_node_revision()
echo "$value"
;;
update)
- if ! crm_attribute --type nodes --node "$NODENAME" --name "$attribute" --update "$value"; then
- rc=$?
- ocf_log err "could not update etcd $revision, error code: $rc"
- return "$rc"
+ crm_attribute --type nodes --node "$NODENAME" --name "$attribute" --update "$value"
+ rc=$?
+ if [ $rc -ne 0 ]; then
+ # Log the error but return success to avoid monitor failure if we can not update the attribute.
+ ocf_log err "could not update etcd $attribute, error code: $rc"
+ return $OCF_SUCCESS
fi
;;
*)
@@ -1041,25 +1059,31 @@ attribute_node_member_id()
ocf_log info "member list: $member_list_json"
if [ -z "$member_list_json" ] ; then
ocf_log err "could not get $attribute: could not get member list JSON"
- return "$rc"
+ return $OCF_ERR_GENERIC
fi
- local value value_hex
- if ! value=$(echo -n "$member_list_json" | jq -r ".header.member_id"); then
- rc=$?
+ local value value_hex rc
+ value=$(echo -n "$member_list_json" | jq -r ".header.member_id")
+ rc=$?
+ if [ $rc -ne 0 ]; then
+ # Log the error but return success to avoid monitor failure if the file is not available yet.
ocf_log err "could not get $attribute from member list JSON, error code: $rc"
- return "$rc"
+ return $OCF_SUCCESS
fi
# JSON member_id is decimal, while etcdctl command needs the hex version
- if ! value_hex=$(decimal_to_hex "$value"); then
- ocf_log err "could not convert decimal member_id '$value' to hex, error code: $?"
+ value_hex=$(decimal_to_hex "$value")
+ rc=$?
+ if [ $rc -ne 0 ]; then
+ ocf_log err "could not convert decimal member_id '$value' to hex, error code: $rc"
return $OCF_ERR_GENERIC
fi
- if ! crm_attribute --type nodes --node "$NODENAME" --name "$attribute" --update "$value_hex"; then
- rc=$?
+ crm_attribute --type nodes --node "$NODENAME" --name "$attribute" --update "$value_hex"
+ rc=$?
+ if [ $rc -ne 0 ]; then
+ # Log the error but return success to avoid monitor failure if we can not update the attribute.
ocf_log err "could not update etcd $attribute, error code: $rc"
- return "$rc"
+ return $OCF_SUCCESS
fi
;;
clear)
@@ -1446,7 +1470,7 @@ get_endpoint_status_json()
local all_etcd_endpoints
all_etcd_endpoints=$(get_all_etcd_endpoints)
- podman exec "${CONTAINER}" etcdctl endpoint status --endpoints="$all_etcd_endpoints" -w json
+ podman exec "${CONTAINER}" etcdctl endpoint status --command-timeout="$MONITOR_ETCDCTL_TIMEOUT" --endpoints="$all_etcd_endpoints" -w json
}
get_member_list_json() {
@@ -1454,7 +1478,7 @@ get_member_list_json() {
local this_node_endpoint
this_node_endpoint="$(ip_url $(attribute_node_ip get)):2379"
- podman exec "${CONTAINER}" etcdctl member list --endpoints="$this_node_endpoint" -w json
+ podman exec "${CONTAINER}" etcdctl member list --command-timeout="$MONITOR_ETCDCTL_TIMEOUT" --endpoints="$this_node_endpoint" -w json
}
detect_cluster_leadership_loss()
@@ -1550,7 +1574,7 @@ check_peer()
fi
if ! member_list_json=$(get_member_list_json); then
- ocf_log info "podman failed to get member list, error code: $?"
+ ocf_log info "podman failed to get member list"
detect_cluster_leadership_loss
return $?
fi
@@ -2145,7 +2169,7 @@ podman_start()
run_opts="$run_opts --oom-score-adj=${OCF_RESKEY_oom}"
if ocf_is_true "$JOIN_AS_LEARNER"; then
- local wait_timeout_sec=$((10*60))
+ local wait_timeout_sec=$((2*60))
local poll_interval_sec=5
local retries=$(( wait_timeout_sec / poll_interval_sec ))
@@ -2354,8 +2378,9 @@ leave_etcd_member_list()
ocf_log info "leaving members list as member with ID $member_id"
local endpoint
endpoint="$(ip_url $(attribute_node_ip get)):2379"
- if ! ocf_run podman exec "$CONTAINER" etcdctl member remove "$member_id" --endpoints="$endpoint"; then
- rc=$?
+ ocf_run timeout 30 podman exec "$CONTAINER" etcdctl member remove "$member_id" --endpoints="$endpoint"
+ rc=$?
+ if [ $rc -ne 0 ]; then
ocf_log err "error leaving members list, error code: $rc"
fi
}
@@ -2376,14 +2401,19 @@ podman_stop()
attribute_node_revision update
attribute_node_cluster_id update
- podman_simple_status
- if [ $? -eq $OCF_NOT_RUNNING ]; then
- ocf_log info "could not leave members list: etcd container not running"
+ # Use podman inspect instead of podman exec (podman_simple_status) to check
+ # container state. podman exec enters the container's process namespace and
+ # hangs when etcd is unresponsive — the typical scenario that triggers a stop.
+ local container_running
+ container_running=$(podman inspect --format '{{.State.Running}}' "$CONTAINER" 2>/dev/null)
+ if [ "$container_running" != "true" ]; then
+ ocf_log info "could not leave members list: $CONTAINER container not running, running state: ${container_running}"
attribute_node_member_id clear
return $OCF_SUCCESS
fi
leave_etcd_member_list
+
# clear node_member_id CIB attribute only after leaving the member list
attribute_node_member_id clear
@@ -2527,6 +2557,9 @@ ETCD_CERTS_HASH_FILE="${OCF_RESKEY_config_location}/certs.hash"
# This is intentional - reboots are controlled stops, not failures requiring detection.
CONTAINER_HEARTBEAT_FILE=${HA_RSCTMP}/podman-container-last-running
DELAY_SECOND_NODE_LEAVE_SEC=10
+# Shorter etcdctl command-timeout for monitor-path calls to prevent
+# consuming the 25s monitor budget. Non-monitor callers use the default 5s.
+MONITOR_ETCDCTL_TIMEOUT="3s"
# 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,32 @@
From 3712b1f52bccddc767ad6f16ec67d6c8c29f1f71 Mon Sep 17 00:00:00 2001
From: Valentin Vidic <vvidic@valentin-vidic.from.hr>
Date: Sun, 3 Apr 2022 20:39:01 +0200
Subject: [PATCH] db2: fix bashism
dash only allows -a as AND operator.
---
heartbeat/db2 | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/heartbeat/db2 b/heartbeat/db2
index ea24d33fc8..4a4b2f477f 100755
--- a/heartbeat/db2
+++ b/heartbeat/db2
@@ -407,7 +407,7 @@ db2_start() {
# partition is explicitly specified, activate without
# partition information. This allows db2 instances without
# partition support to be managed.
- if [ -z "$OCF_RESKEY_dbpartitionnum" ] && ! [ -a "$db2sql/db2nodes.cfg" ]; then
+ if [ -z "$OCF_RESKEY_dbpartitionnum" ] && ! [ -e "$db2sql/db2nodes.cfg" ]; then
start_opts=""
fi
@@ -511,7 +511,7 @@ db2_stop_bg() {
rc=$OCF_SUCCESS
- if [ -z "$OCF_RESKEY_dbpartitionnum" ] && ! [ -a "$db2sql/db2nodes.cfg" ]; then
+ if [ -z "$OCF_RESKEY_dbpartitionnum" ] && ! [ -e "$db2sql/db2nodes.cfg" ]; then
stop_opts=""
fi

View File

@ -0,0 +1,143 @@
From 26c0d48bc69da1859f1ce5205a8bb6eaf6297b81 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Wed, 25 Mar 2026 10:46:09 +0100
Subject: [PATCH] db2: do not use db2stop, as it sends truncation messages,
which in some cases are not delivered
This caused divergence in the log, and the user would have to manually rebuild
the DB to recover from it.
---
heartbeat/db2 | 104 +++++++-------------------------------------------
1 file changed, 13 insertions(+), 91 deletions(-)
diff --git a/heartbeat/db2 b/heartbeat/db2
index 4420b9989..9de18639d 100755
--- a/heartbeat/db2
+++ b/heartbeat/db2
@@ -596,45 +596,10 @@ db2_start() {
return $OCF_SUCCESS
}
-#
-# helper function to be spawned
-# so we can detect a hang of the db2stop command
-#
-db2_stop_bg() {
- local rc output
- local stop_opts="dbpartitionnum $db2node"
-
- rc=$OCF_SUCCESS
-
- if [ -z "$OCF_RESKEY_dbpartitionnum" ] && ! [ -e "$db2sql/db2nodes.cfg" ]; then
- stop_opts=""
- fi
-
- if output=$(runasdb2 db2stop force $stop_opts)
- then
- ocf_log info "DB2 instance $instance($db2node) stopped: $output"
- else
- case $output in
- *SQL1032N*)
- #SQL1032N No start database manager command was issued
- ocf_log info "$output"
- ;;
-
- *)
- ocf_log err "DB2 instance $instance($db2node) stop failed: $output"
- rc=$OCF_ERR_GENERIC
- esac
- fi
-
- return $rc
-}
-
#
# Stop the given db2 database instance
#
db2_stop() {
- local stop_timeout grace_timeout stop_bg_pid i must_kill
-
# remove master score
master_score -D -l reboot
@@ -647,67 +612,24 @@ db2_stop() {
return $OCF_SUCCESS
fi
- stop_timeout=${OCF_RESKEY_CRM_meta_timeout:-20000}
-
- # grace_time is 4/5 (unit is ms)
- grace_timeout=$((stop_timeout/1250))
-
- # start db2stop in background as this may hang
- db2_stop_bg &
- stop_bg_pid=$!
-
- # wait for grace_timeout
- i=0
- while [ $i -lt $grace_timeout ]
- do
- kill -0 $stop_bg_pid 2>/dev/null || break;
- sleep 1
- i=$((i+1))
- done
-
- # collect exit status but don't hang
- if kill -0 $stop_bg_pid 2>/dev/null
- then
- stoprc=1
- kill -9 $stop_bg_pid 2>/dev/null
- else
- wait $stop_bg_pid
- stoprc=$?
- fi
-
- must_kill=0
-
- if [ $stoprc -ne 0 ]
+ # db2nkill kills *all* partitions on the node
+ if [ -x $db2bin/db2nkill ]
then
- ocf_log warn "DB2 instance $instance($db2node): db2stop failed, using db2nkill"
- must_kill=1
- elif ! db2_instance_dead
+ logasdb2 $db2bin/db2nkill $db2node
+ elif [ -x $db2bin/db2_kill ]
then
- ocf_log warn "DB2 instance $instance($db2node): db2stop indicated success but there a still processes, using db2nkill"
- must_kill=1
+ logasdb2 $db2bin/db2_kill
fi
- if [ $must_kill -eq 1 ]
- then
- # db2nkill kills *all* partitions on the node
- if [ -x $db2bin/db2nkill ]
- then
- logasdb2 $db2bin/db2nkill $db2node
- elif [ -x $db2bin/db2_kill ]
- then
- logasdb2 $db2bin/db2_kill
- fi
-
- # loop forever (or lrmd kills us due to timeout) until the
- # instance is dead
- while ! db2_instance_dead
- do
- ocf_log info "DB2 instance $instance($db2node): waiting for processes to exit"
- sleep 1
- done
+ # loop forever (or lrmd kills us due to timeout) until the
+ # instance is dead
+ while ! db2_instance_dead
+ do
+ ocf_log info "DB2 instance $instance($db2node): waiting for processes to exit"
+ sleep 1
+ done
- ocf_log info "DB2 instance $instance($db2node) is now dead"
- fi
+ ocf_log info "DB2 instance $instance($db2node) is now dead"
return $OCF_SUCCESS
}

View File

@ -0,0 +1,283 @@
From 9ba19a62543de4d7365fc711b908a2759f811af9 Mon Sep 17 00:00:00 2001
From: Vincenzo Mauro <vmauro@redhat.com>
Date: Tue, 5 May 2026 14:24:43 +0200
Subject: [PATCH 1/4] fix: fixed etcd learner deadlock
---
heartbeat/podman-etcd | 79 +++++++++++++++++++++++++++++++++++++++----
1 file changed, 73 insertions(+), 6 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 4c9bbd4fa..5bb3b2897 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -50,6 +50,7 @@ OCF_RESKEY_oom_default="-997"
OCF_RESKEY_config_location_default="/var/lib/etcd"
OCF_RESKEY_backup_location_default="/var/lib/etcd"
OCF_RESKEY_max_backup_snapshots_default="3"
+OCF_RESKEY_kubeconfig_default="/etc/kubernetes/static-pod-resources/kube-apiserver-certs/secrets/node-kubeconfigs/localhost.kubeconfig"
: ${OCF_RESKEY_image=${OCF_RESKEY_image_default}}
: ${OCF_RESKEY_pod_manifest=${OCF_RESKEY_pod_manifest_default}}
@@ -63,6 +64,7 @@ OCF_RESKEY_max_backup_snapshots_default="3"
: ${OCF_RESKEY_config_location=${OCF_RESKEY_config_location_default}}
: ${OCF_RESKEY_backup_location=${OCF_RESKEY_backup_location_default}}
: ${OCF_RESKEY_max_backup_snapshots=${OCF_RESKEY_max_backup_snapshots_default}}
+: ${OCF_RESKEY_kubeconfig=${OCF_RESKEY_kubeconfig_default}}
#######################################################################
@@ -288,6 +290,16 @@ Set max_backup_snapshots=0 to disable backups.
<content type="integer" default="${OCF_RESKEY_max_backup_snapshots_default}"/>
</parameter>
+<parameter name="kubeconfig" required="0" unique="0">
+<longdesc lang="en">
+Path to a kubeconfig file for querying Machine API objects. Used to detect
+whether a peer node's Machine is being deleted, preventing the resource agent
+from re-adding it as an etcd learner during Machine deletion flows.
+</longdesc>
+<shortdesc lang="en">Kubeconfig for Machine API queries</shortdesc>
+<content type="string" default="${OCF_RESKEY_kubeconfig_default}"/>
+</parameter>
+
</parameters>
<actions>
@@ -1505,6 +1517,34 @@ detect_cluster_leadership_loss()
}
+# Checks whether the Machine object for a given node is being deleted.
+# Returns 0 (true) if the Machine has a deletionTimestamp set, 1 (false) otherwise.
+# Fails open: returns 1 on API errors to preserve current learner-addition behavior.
+is_peer_machine_deleting()
+{
+ local node_name="$1"
+ local out
+ local deletion_ts
+
+ out=$(timeout 10 oc --kubeconfig="$OCF_RESKEY_kubeconfig" get machines \
+ -n openshift-machine-api -o json 2>&1)
+ if [ $? -ne 0 ]; then
+ ocf_log warn "could not query Machine API for node $node_name (fail-open): $out"
+ return 1
+ fi
+
+ # Select the Machine object for the given node and extract its deletionTimestamp if present
+ deletion_ts=$(printf "%s" "$out" | jq -r --arg name "$node_name" \
+ '.items[] | select(.status.nodeRef.name == $name) | .metadata.deletionTimestamp // empty')
+
+ if [ -n "$deletion_ts" ]; then
+ ocf_log info "Machine for node $node_name is being deleted (deletionTimestamp: $deletion_ts)"
+ return 0
+ fi
+
+ return 1
+}
+
# 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
@@ -1542,9 +1582,21 @@ manage_peer_membership()
# NOTE: voting members have a "name" field but no "isLearner" field,
# while learner members have "isLearner": true (boolean) but no "name" field, so we search for peerURLs matching.
peer_member_id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .peerURLs | map(test(\"$peer_member_ip\")) | any).ID")
+ # During Machine deletion, CEO's MachineDeletionHooksController
+ # keeps the EtcdQuorumOperator preDrain hook as long as the peer IP appears in the etcd
+ # member list (learners included). If we add or keep a learner for a peer whose Machine
+ # is being deleted, CEO never clears the hook, MAO never drains, and the Machine hangs
+ # in Deleting. Two safeguards cover the race:
+ # A (below): peer is not yet in the member list — skip adding it as a learner if machine is deleting
+ # B (learner exists): a prior monitor cycle added the learner before the Machine
+ # deletion started — remove it so CEO can clear the hook.
if [ -z "$peer_member_id" ]; then
ocf_log info "$peer_member_name is not in the members list"
- add_member_as_learner "$peer_member_name" "$peer_member_ip"
+ if ! is_peer_machine_deleting "$peer_member_name"; then
+ add_member_as_learner "$peer_member_name" "$peer_member_ip"
+ else
+ ocf_log info "peer Machine is being deleted, skipping learner addition for $peer_member_name"
+ fi
set_standalone_node
return
fi
@@ -1552,10 +1604,21 @@ manage_peer_membership()
# Ensure learner_node attribute is always set when we have a learner member
local learner_member_id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .isLearner==true ).ID")
local current_learner_node=$(attribute_learner_node get)
- if [ -n "$learner_member_id" ] && [ -z "$current_learner_node" ]; then
- ocf_log debug "$peer_member_name found as learner in member list, but learner_node attribute was not set. Updating"
- attribute_learner_node update "$peer_member_name"
- return
+
+ if [ -n "$learner_member_id" ]; then
+ # Clean up a learner added before the Machine deletion started
+ if is_peer_machine_deleting "$peer_member_name"; then
+ ocf_log info "peer Machine is being deleted, removing learner $peer_member_name from member list"
+ remove_etcd_member_by_ip "$peer_member_ip"
+ attribute_learner_node clear
+ set_standalone_node
+ return
+ fi
+ if [ -z "$current_learner_node" ]; then
+ ocf_log debug "$peer_member_name found as learner in member list, but learner_node attribute was not set. Updating"
+ attribute_learner_node update "$peer_member_name"
+ return
+ fi
fi
ocf_log debug "$peer_member_name is in the members list by IP: $peer_member_ip"
@@ -2312,7 +2375,11 @@ podman_start()
peer_node_name="$(get_peer_node_name)"
peer_node_ip="$(attribute_node_ip_peer)"
if [ -n "$peer_node_name" ] && [ -n "$peer_node_ip" ]; then
- add_member_as_learner "$peer_node_name" "$peer_node_ip"
+ if is_peer_machine_deleting "$peer_node_name"; then
+ ocf_log info "peer Machine is being deleted, skipping learner addition for $peer_node_name"
+ else
+ add_member_as_learner "$peer_node_name" "$peer_node_ip"
+ fi
set_standalone_node
else
ocf_log err "could not add peer as learner (peer node name: ${peer_node_name:-unknown}, peer ip: ${peer_node_ip:-unknown})"
From 56d9754311ab0595dea1c47e26eca85bbcfb049c Mon Sep 17 00:00:00 2001
From: Vincenzo Mauro <vmauro@redhat.com>
Date: Wed, 6 May 2026 15:15:27 +0200
Subject: [PATCH 2/4] fix: added support for both MAPI and CAPI
---
heartbeat/podman-etcd | 42 +++++++++++++++++++++++++++++++++++-------
1 file changed, 35 insertions(+), 7 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 5bb3b2897..ad9804c1d 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -295,6 +295,7 @@ Set max_backup_snapshots=0 to disable backups.
Path to a kubeconfig file for querying Machine API objects. Used to detect
whether a peer node's Machine is being deleted, preventing the resource agent
from re-adding it as an etcd learner during Machine deletion flows.
+Supports both MAPI (machine.openshift.io) and CAPI (cluster.x-k8s.io) Machine resources.
</longdesc>
<shortdesc lang="en">Kubeconfig for Machine API queries</shortdesc>
<content type="string" default="${OCF_RESKEY_kubeconfig_default}"/>
@@ -1525,15 +1526,39 @@ is_peer_machine_deleting()
local node_name="$1"
local out
local deletion_ts
+ local oc_rc
+ local item_count
- out=$(timeout 10 oc --kubeconfig="$OCF_RESKEY_kubeconfig" get machines \
+ # Try MAPI first (machine.openshift.io), fall back to CAPI (cluster.x-k8s.io)
+ out=$(timeout 10 oc --kubeconfig="$OCF_RESKEY_kubeconfig" get machines.machine.openshift.io \
-n openshift-machine-api -o json 2>&1)
- if [ $? -ne 0 ]; then
- ocf_log warn "could not query Machine API for node $node_name (fail-open): $out"
- return 1
+ oc_rc=$?
+
+ if [ $oc_rc -eq 0 ]; then
+ item_count=$(printf "%s" "$out" | jq '.items | length' 2>/dev/null)
+ fi
+
+ # MAPI CRD missing, namespace absent, or no Machine objects — try CAPI
+ if [ $oc_rc -ne 0 ] || [ "${item_count:-0}" -eq 0 ]; then
+ ocf_log info "MAPI returned no machines, trying CAPI for node $node_name"
+ out=$(timeout 10 oc --kubeconfig="$OCF_RESKEY_kubeconfig" get machines.cluster.x-k8s.io \
+ -n openshift-cluster-api -o json 2>&1)
+ if [ $? -ne 0 ]; then
+ ocf_log warn "could not query Machine API (MAPI or CAPI) for node $node_name (fail-open): $out"
+ return 1
+ fi
fi
# Select the Machine object for the given node and extract its deletionTimestamp if present
+ local machine_count
+ machine_count=$(printf "%s" "$out" | jq -r --arg name "$node_name" \
+ '[.items[] | select(.status.nodeRef.name == $name)] | length' 2>/dev/null)
+
+ if [ "$machine_count" = "0" ] || [ -z "$machine_count" ]; then
+ ocf_log warn "No Machine object found for node $node_name (fail-open): nodeRef may not be populated yet"
+ return 1
+ fi
+
deletion_ts=$(printf "%s" "$out" | jq -r --arg name "$node_name" \
'.items[] | select(.status.nodeRef.name == $name) | .metadata.deletionTimestamp // empty')
@@ -1609,9 +1634,12 @@ manage_peer_membership()
# Clean up a learner added before the Machine deletion started
if is_peer_machine_deleting "$peer_member_name"; then
ocf_log info "peer Machine is being deleted, removing learner $peer_member_name from member list"
- remove_etcd_member_by_ip "$peer_member_ip"
- attribute_learner_node clear
- set_standalone_node
+ if remove_etcd_member_by_ip "$peer_member_ip"; then
+ attribute_learner_node clear
+ set_standalone_node
+ else
+ ocf_log err "failed to remove learner for deleting Machine $peer_member_name; will retry next monitor cycle"
+ fi
return
fi
if [ -z "$current_learner_node" ]; then
From beab70c7acd4f6ccc33c4dbcb3d72f94fc560812 Mon Sep 17 00:00:00 2001
From: Vincenzo Mauro <vmauro@redhat.com>
Date: Tue, 12 May 2026 09:54:33 +0200
Subject: [PATCH 3/4] reduced timeout to 5 and fixed MCAPI return code
---
heartbeat/podman-etcd | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index ad9804c1d..e022869a8 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -1530,7 +1530,7 @@ is_peer_machine_deleting()
local item_count
# Try MAPI first (machine.openshift.io), fall back to CAPI (cluster.x-k8s.io)
- out=$(timeout 10 oc --kubeconfig="$OCF_RESKEY_kubeconfig" get machines.machine.openshift.io \
+ out=$(timeout 5 oc --kubeconfig="$OCF_RESKEY_kubeconfig" get machines.machine.openshift.io \
-n openshift-machine-api -o json 2>&1)
oc_rc=$?
@@ -1607,6 +1607,7 @@ manage_peer_membership()
# NOTE: voting members have a "name" field but no "isLearner" field,
# while learner members have "isLearner": true (boolean) but no "name" field, so we search for peerURLs matching.
peer_member_id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .peerURLs | map(test(\"$peer_member_ip\")) | any).ID")
+
# During Machine deletion, CEO's MachineDeletionHooksController
# keeps the EtcdQuorumOperator preDrain hook as long as the peer IP appears in the etcd
# member list (learners included). If we add or keep a learner for a peer whose Machine
From 2b06ed31bda015543a365e02e1bc5a47b3fa0439 Mon Sep 17 00:00:00 2001
From: Vincenzo Mauro <vmauro@redhat.com>
Date: Tue, 12 May 2026 10:30:48 +0200
Subject: [PATCH 4/4] fixed return code for CAPI
---
heartbeat/podman-etcd | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index e022869a8..2dbaf9991 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -1541,9 +1541,10 @@ is_peer_machine_deleting()
# MAPI CRD missing, namespace absent, or no Machine objects — try CAPI
if [ $oc_rc -ne 0 ] || [ "${item_count:-0}" -eq 0 ]; then
ocf_log info "MAPI returned no machines, trying CAPI for node $node_name"
- out=$(timeout 10 oc --kubeconfig="$OCF_RESKEY_kubeconfig" get machines.cluster.x-k8s.io \
+ out=$(timeout 5 oc --kubeconfig="$OCF_RESKEY_kubeconfig" get machines.cluster.x-k8s.io \
-n openshift-cluster-api -o json 2>&1)
- if [ $? -ne 0 ]; then
+ oc_rc=$?
+ if [ $oc_rc -ne 0 ]; then
ocf_log warn "could not query Machine API (MAPI or CAPI) for node $node_name (fail-open): $out"
return 1
fi

View File

@ -0,0 +1,130 @@
From db041869f4b8612e44561f4ba4a46ed09d18e24e Mon Sep 17 00:00:00 2001
From: Vincenzo Mauro <vmauro@redhat.com>
Date: Thu, 7 May 2026 18:14:04 +0200
Subject: [PATCH 1/4] fixed OCPBUGS-83333
---
heartbeat/podman-etcd | 22 +++++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 4c9bbd4fa..d96c055e3 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -2519,7 +2519,27 @@ podman_validate()
podman_notify()
{
- ocf_log info "notify: type=${OCF_RESKEY_CRM_meta_notify_type}, operation=${OCF_RESKEY_CRM_meta_notify_operation}, nodes { active=[${OCF_RESKEY_CRM_meta_notify_active_uname}], start=[${OCF_RESKEY_CRM_meta_notify_start_uname}], stop=[${OCF_RESKEY_CRM_meta_notify_stop_uname}] }, resources { active=[${OCF_RESKEY_CRM_meta_notify_active_resource}], start =[${OCF_RESKEY_CRM_meta_notify_start_resource}], stop=[${OCF_RESKEY_CRM_meta_notify_stop_resource}] }"
+ local notify_type="${OCF_RESKEY_CRM_meta_notify_type}"
+ local notify_operation="${OCF_RESKEY_CRM_meta_notify_operation}"
+
+ ocf_log info "notify: type=${notify_type}, operation=${notify_operation}, nodes { active=[${OCF_RESKEY_CRM_meta_notify_active_uname}], start=[${OCF_RESKEY_CRM_meta_notify_start_uname}], stop=[${OCF_RESKEY_CRM_meta_notify_stop_uname}] }, resources { active=[${OCF_RESKEY_CRM_meta_notify_active_resource}], start =[${OCF_RESKEY_CRM_meta_notify_start_resource}], stop=[${OCF_RESKEY_CRM_meta_notify_stop_resource}] }"
+
+ # Pacemaker serializes operations per resource per node. The start sequence
+ # with notifications is:
+ # pre-notify(start) on peer → start on joiner → post-notify(start) on peer
+ # Between pre-notify and post-notify, the peer's recurring monitor is
+ # queued — Pacemaker won't overlap operations for the same resource on the
+ # same node. The monitor path (check_peer → manage_peer_membership →
+ # add_member_as_learner) is the primary way a running peer adds the
+ # starting node to the etcd member list. Without handling it here, the
+ # starting node's podman_start poll loop (waiting for learner_node attribute)
+ # deadlocks: start waits for learner_node, monitor waits for start to finish.
+ # pre_notify_start fires before the start action, giving us the window to
+ # add the learner so the joiner's poll loop finds it immediately.
+ if [ "$notify_type" = "pre" ] && [ "$notify_operation" = "start" ]; then
+ ocf_log info "pre_notify_start: running peer membership check for starting node"
+ check_peer
+ fi
}
# TODO :
From d1c817108276ee3019a20164fca0646985d99cde Mon Sep 17 00:00:00 2001
From: Vincenzo Mauro <vmauro@redhat.com>
Date: Fri, 8 May 2026 10:38:12 +0200
Subject: [PATCH 2/4] Updated deadlock comment
---
heartbeat/podman-etcd | 18 ++++++------------
1 file changed, 6 insertions(+), 12 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index d96c055e3..21a5e01e1 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -2524,18 +2524,12 @@ podman_notify()
ocf_log info "notify: type=${notify_type}, operation=${notify_operation}, nodes { active=[${OCF_RESKEY_CRM_meta_notify_active_uname}], start=[${OCF_RESKEY_CRM_meta_notify_start_uname}], stop=[${OCF_RESKEY_CRM_meta_notify_stop_uname}] }, resources { active=[${OCF_RESKEY_CRM_meta_notify_active_resource}], start =[${OCF_RESKEY_CRM_meta_notify_start_resource}], stop=[${OCF_RESKEY_CRM_meta_notify_stop_resource}] }"
- # Pacemaker serializes operations per resource per node. The start sequence
- # with notifications is:
- # pre-notify(start) on peer → start on joiner → post-notify(start) on peer
- # Between pre-notify and post-notify, the peer's recurring monitor is
- # queued — Pacemaker won't overlap operations for the same resource on the
- # same node. The monitor path (check_peer → manage_peer_membership →
- # add_member_as_learner) is the primary way a running peer adds the
- # starting node to the etcd member list. Without handling it here, the
- # starting node's podman_start poll loop (waiting for learner_node attribute)
- # deadlocks: start waits for learner_node, monitor waits for start to finish.
- # pre_notify_start fires before the start action, giving us the window to
- # add the learner so the joiner's poll loop finds it immediately.
+ # Pacemaker suppresses the peer's monitor during an
+ # active start/notify cycle. Since monitor is the only path that calls
+ # add_member_as_learner (outside force_new_cluster), the joiner's
+ # podman_start poll loop deadlocks; it waits for learner_node, but
+ # no monitor runs to set it. pre_notify_start fires before start,
+ # so we add the learner here to break the deadlock
if [ "$notify_type" = "pre" ] && [ "$notify_operation" = "start" ]; then
ocf_log info "pre_notify_start: running peer membership check for starting node"
check_peer
From 0fafab701878ce4b8c7413610e41dce3e69447aa Mon Sep 17 00:00:00 2001
From: Vincenzo Mauro <vmauro@redhat.com>
Date: Tue, 12 May 2026 10:24:26 +0200
Subject: [PATCH 3/4] improved logging
---
heartbeat/podman-etcd | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 21a5e01e1..41ce84ff1 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -2531,7 +2531,7 @@ podman_notify()
# no monitor runs to set it. pre_notify_start fires before start,
# so we add the learner here to break the deadlock
if [ "$notify_type" = "pre" ] && [ "$notify_operation" = "start" ]; then
- ocf_log info "pre_notify_start: running peer membership check for starting node"
+ ocf_log info "pre_notify_start: running peer membership check for ${OCF_RESKEY_CRM_meta_notify_start_uname}"
check_peer
fi
}
@@ -2616,4 +2616,4 @@ validate-all) podman_validate;;
esac
rc=$?
ocf_log debug "${OCF_RESOURCE_INSTANCE} $__OCF_ACTION : $rc"
-exit $rc
+exc
From bdce9048b4fc2c38255d36e73a1f73a7d72b7471 Mon Sep 17 00:00:00 2001
From: Vincenzo Mauro <vmauro@redhat.com>
Date: Tue, 12 May 2026 10:46:49 +0200
Subject: [PATCH 4/4] fixed typo
---
heartbeat/podman-etcd | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 41ce84ff1..740e2edb4 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -2616,4 +2616,4 @@ validate-all) podman_validate;;
esac
rc=$?
ocf_log debug "${OCF_RESOURCE_INSTANCE} $__OCF_ACTION : $rc"
-exc
+exit $rc

View File

@ -0,0 +1,96 @@
From 42dfef941ed80d6073022141fa1cad513e8dae4f Mon Sep 17 00:00:00 2001
From: Pablo Fontanilla <pfontani@redhat.com>
Date: Wed, 22 Apr 2026 12:54:58 +0200
Subject: [PATCH 1/2] fix(podman-etcd): use -ge 1 in
etcd_pod_container_exists()
PR #2112 added -a to crictl ps to include exited containers, but
did not update the count check from -eq 1 to -ge 1. During install,
etcd container crashes create exited containers that inflate the
count past 1, causing the guard to report 'pod not found' despite
the pod running.
Fixes: OCPBUGS-83742
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
heartbeat/podman-etcd | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 4c9bbd4fa..52b2a1386 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -932,7 +932,7 @@ etcd_pod_container_exists() {
local count_matches
# Check whether the etcd pod exists on the same node (including stopped/exited containers)
count_matches=$(crictl pods --label app=etcd -q | xargs -I {} crictl ps -a --pod {} -o json | jq -r '.containers[].metadata | select ( .name == "etcd" ).name' | wc -l)
- if [ "$count_matches" -eq 1 ]; then
+ if [ "$count_matches" -ge 1 ]; then
# etcd pod found
return 0
fi
From 30d20f6b99ae9898bf801c0a5e690b81fc928faa Mon Sep 17 00:00:00 2001
From: Pablo Fontanilla <pfontani@redhat.com>
Date: Wed, 22 Apr 2026 12:57:46 +0200
Subject: [PATCH 2/2] fix(podman-etcd): wait for etcd ports before starting
container
During the static-pod to podman-etcd transition, the old etcd process
may still hold ports 2379/2380 when the RA tries to start its container.
This causes 'bind: address already in use' errors and eventual fallback
to standalone mode.
Add a 60-second wait loop (modeled on CEO's pod.gotpl.yaml port check)
that blocks until the ports are free before calling podman run/start.
Fixes: OCPBUGS-83742
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
heartbeat/podman-etcd | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 52b2a1386..9a960914b 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -940,6 +940,25 @@ etcd_pod_container_exists() {
return 1
}
+wait_for_etcd_ports_release() {
+ local timeout=${1:-60}
+ local elapsed=0
+ if [ -z "$(ss -Htan '( sport = 2379 or sport = 2380 )')" ]; then
+ return 0
+ fi
+ ocf_log info "waiting for etcd ports 2379/2380 to be released (timeout: ${timeout}s)"
+ while [ -n "$(ss -Htan '( sport = 2379 or sport = 2380 )')" ]; do
+ if [ "$elapsed" -ge "$timeout" ]; then
+ ocf_log err "etcd ports still in use after ${timeout}s"
+ return 1
+ fi
+ sleep 1
+ elapsed=$((elapsed + 1))
+ done
+ ocf_log info "etcd ports released after ${elapsed}s"
+ return 0
+}
+
attribute_node_cluster_id()
{
local action="$1"
@@ -2267,6 +2286,11 @@ podman_start()
ocf_log notice "Pull image not required, ${OCF_RESKEY_image}"
fi
+ if ! wait_for_etcd_ports_release 60; then
+ ocf_exit_reason "etcd ports 2379/2380 still bound — cannot start container"
+ return $OCF_ERR_GENERIC
+ fi
+
if ocf_is_true "$OCF_RESKEY_reuse" && container_exists; then
ocf_log info "starting existing container $CONTAINER."
ocf_run podman start "$CONTAINER"

View File

View File

@ -5,7 +5,7 @@ diff --color -uNr a/heartbeat/gcp-pd-move.in b/heartbeat/gcp-pd-move.in
from ocf import logger
try:
+ sys.path.insert(0, '/usr/lib/fence-agents/support/google')
+ sys.path.insert(0, '/usr/lib/fence-agents/support/google/lib/python#PYTHON3_VERSION#/site-packages')
import googleapiclient.discovery
except ImportError:
pass
@ -16,7 +16,7 @@ diff --color -uNr a/heartbeat/gcp-vpc-move-route.in b/heartbeat/gcp-vpc-move-rou
from ocf import *
try:
+ sys.path.insert(0, '/usr/lib/fence-agents/support/google')
+ sys.path.insert(0, '/usr/lib/fence-agents/support/google/lib/python#PYTHON3_VERSION#/site-packages')
import googleapiclient.discovery
import pyroute2
try:
@ -27,7 +27,7 @@ diff --color -uNr a/heartbeat/gcp-vpc-move-vip.in b/heartbeat/gcp-vpc-move-vip.i
from ocf import *
try:
+ sys.path.insert(0, '/usr/lib/fence-agents/support/google')
+ sys.path.insert(0, '/usr/lib/fence-agents/support/google/lib/python#PYTHON3_VERSION#/site-packages')
import googleapiclient.discovery
try:
from google.oauth2.service_account import Credentials as ServiceAccountCredentials

View File

@ -8,7 +8,7 @@
-import requests.adapters
-import urllib3.util
+try:
+ sys.path.insert(0, '/usr/lib/fence-agents/support/ibm')
+ sys.path.insert(0, '/usr/lib/fence-agents/support/ibm/lib/python#PYTHON3_VERSION#/site-packages')
+ import requests
+ import requests.adapters
+ import urllib3.util

View File

@ -45,7 +45,7 @@
Name: resource-agents
Summary: Open Source HA Reusable Cluster Resource Scripts
Version: 4.10.0
Release: 108%{?rcver:%{rcver}}%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty}}%{?dist}
Release: 111%{?rcver:%{rcver}}%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty}}%{?dist}.4
License: GPLv2+ and LGPLv2+
URL: https://github.com/ClusterLabs/resource-agents
Source0: %{upstream_prefix}-%{upstream_version}.tar.gz
@ -199,6 +199,16 @@ Patch146: RHEL-42513-powervs-subnet-wait-for-IP.patch
Patch147: RHEL-143527-powervs-move-ip-powervs-subnet-fix-error-logging.patch
Patch148: RHEL-145628-podman-etcd-enhance-etcd-data-backup-with-snapshots-and-retention.patch
Patch149: RHEL-150700-podman-etcd-set-attributes-if-they-fail-during-force-new-cluster.patch
Patch150: RHEL-151828-portblock-check-inverse-action.patch
Patch151: RHEL-156808-podman-etcd-ignore-learners-when-considering-which-node-has-higher-revision.patch
Patch152: RHEL-157145-podman-etcd-handle-existing-peer-URLs-gracefully-during-force_new_cluster-recovery.patch
Patch153: RHEL-159202-podman-etcd-hardened-monitor-stop-actions.patch
Patch154: RHEL-157273-db2-set-reintegration-when-promotion-is-successful.patch
Patch155: RHEL-166183-1-db2-fix-bashism.patch
Patch156: RHEL-166183-2-db2-do-not-use-db2stop-to-avoid-divergence-in-the-log.patch
Patch157: RHEL-177849-podman-etcd-fix-port-2380-binding-race.patch
Patch158: RHEL-177838-podman-etcd-fix-machine-deletion-deadlock.patch
Patch159: RHEL-177843-podman-etcd-fix-learner-start-deadlock.patch
# bundled ha-cloud-support libs
Patch500: ha-cloud-support-aliyun.patch
@ -315,7 +325,7 @@ service managers.
License: GPLv2+ and LGPLv2+
Summary: Cloud resource agents
Requires: %{name} = %{version}-%{release}
Requires: ha-cloud-support >= 4.10.0-63
Requires: ha-cloud-support >= 4.10.0-110.el9_8.1
Requires: socat
Provides: resource-agents-aliyun
Obsoletes: resource-agents-aliyun <= %{version}
@ -493,6 +503,16 @@ exit 1
%patch -p1 -P 147
%patch -p1 -P 148
%patch -p1 -P 149
%patch -p1 -P 150
%patch -p1 -P 151
%patch -p1 -P 152
%patch -p1 -P 153
%patch -p1 -P 154
%patch -p1 -P 155
%patch -p1 -P 156
%patch -p1 -P 157
%patch -p1 -P 158
%patch -p1 -P 159
# bundled ha-cloud-support libs
%patch -p1 -P 500
@ -504,6 +524,8 @@ chmod 755 heartbeat/NovaEvacuate
chmod 755 heartbeat/pgsqlms
%build
sed -i -e "s/#PYTHON3_VERSION#/%{python3_version}/" heartbeat/*.in
if [ ! -f configure ]; then
./autogen.sh
fi
@ -533,10 +555,10 @@ export CFLAGS
PYTHON="%{__python3}" \
%endif
%ifarch x86_64
PYTHONPATH="%{_usr}/lib/fence-agents/support/google" \
PYTHONPATH="%{_usr}/lib/fence-agents/support/google/lib/python%{python3_version}/site-packages" \
%endif
%ifarch ppc64le
PYTHONPATH="%{_usr}/lib/fence-agents/support/ibm" \
PYTHONPATH="%{_usr}/lib/fence-agents/support/ibm/lib/python%{python3_version}/site-packages" \
%endif
%{conf_opt_fatal} \
%if %{defined _unitdir}
@ -825,7 +847,35 @@ rm -rf %{buildroot}/usr/share/doc/resource-agents
%{_usr}/lib/ocf/lib/heartbeat/OCF_*.pm
%changelog
* Wed Feb 18 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-108
* Wed May 20 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-111.4
- podman-etcd: fix port 2380 binding race
- podman-etcd: fix machine deletion deadlock
- podman-etcd: fix learner start deadlock
Resolves: RHEL-177849, RHEL-177838, RHEL-177843
* Mon Apr 20 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-111.3
- Cloud agents: change bundled lib paths to match changes in
ha-cloud-support
Resolves: RHEL-168565
* Fri Apr 10 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-111.2
- db2: do not use db2stop to avoid divergence in the log
Resolves: RHEL-166183
* Wed Apr 8 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-111.1
- portblock: check inverse action state file for non-promotable
resources to avoid issues when doing e.g. block followed by unblock
- podman-etcd: ignore learners when considering which node has higher revision
- podman-etcd: handle existing peer URLs gracefully during force_new_cluster recovery
- podman-etcd: hardened monitor/stop actions
- db2: set reintegration when promotion is successful
Resolves: RHEL-151828, RHEL-156808, RHEL-157145, RHEL-159202, RHEL-157273
* Fri Mar 6 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.10.0-108
- podman-etcd: set attributes if they fail during force-new-cluster
Resolves: RHEL-150700