From 21d0c040533a07e8c82e6c4780984aa2bc39121c Mon Sep 17 00:00:00 2001 From: AlmaLinux RelEng Bot Date: Mon, 20 Jul 2026 19:59:18 -0400 Subject: [PATCH] import Oracle_OSS resource-agents-4.10.0-111.el9_8.7 --- ...dd-support-for-single-node-operation.patch | 214 ++++++++++++++++++ ...move-ip-add-awscli_timeout-parameter.patch | 81 +++++++ SPECS/resource-agents.spec | 16 +- 3 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 SOURCES/RHEL-193691-podman-etcd-add-support-for-single-node-operation.patch create mode 100644 SOURCES/RHEL-193966-aws-vpc-move-ip-add-awscli_timeout-parameter.patch diff --git a/SOURCES/RHEL-193691-podman-etcd-add-support-for-single-node-operation.patch b/SOURCES/RHEL-193691-podman-etcd-add-support-for-single-node-operation.patch new file mode 100644 index 0000000..47bf30e --- /dev/null +++ b/SOURCES/RHEL-193691-podman-etcd-add-support-for-single-node-operation.patch @@ -0,0 +1,214 @@ +From 1ce0c683b5eee74d1aee4a7d6f27ee9cb9e322df Mon Sep 17 00:00:00 2001 +From: Jeremy Poulin +Date: Tue, 16 Jun 2026 11:06:17 -0400 +Subject: [PATCH] OCPBUGS-84695: fix(podman-etcd): single-node crash recovery + and hardened detection + +Fix single-node etcd containers staying down permanently after crashes by +adding recovery logic and hardening cluster topology detection. + +Problem: +When a single-node etcd container crashes, container_health_check() returns +"failed-wait-for-peer" which maps to OCF_SUCCESS in monitor. Pacemaker thinks +the resource is healthy while etcd remains down indefinitely because there is +no peer to initiate recovery via force_new_cluster. + +Changes: + +Single-node crash recovery: +- Detect single-node topology during container failure in container_health_check() +- Set standalone_node attribute to ensure proper single-node startup on restart +- Return "failed-restart-now" to trigger immediate Pacemaker restart +- Fixes PR review feedback: ClusterLabs/resource-agents#2155 (#r3466461583, #r3473146886) + +Hardened topology detection: +- Add is_single_node() helper to detect cluster topology from node_ip_map +- Use exact field matching to avoid false matches in node names +- Add defensive checks for invalid node_ip_map states (empty, malformed) +- Fail-safe behavior: ambiguous states default to multi-node mode +- Add verbose logging for all topology decisions + +Enhanced single-node support: +- Handle single-node clusters in peer management (skip peer operations, clean up stale learners) +- Skip revision comparison during single-node startup (no peer to compare with) +- Maintain standalone_node attribute correctly across cluster topology changes +- Document standalone_node behavior as persistent marker for force_new_cluster decisions + +Improved observability: +- Log topology detection decisions with context (node_ip_map contents, peer counts) +- Log invalid states with details for troubleshooting +- Add comments explaining standalone_node and force_new_cluster interaction + +Fixes: single-node container crash recovery, single-node cluster startup and operation + +Co-Authored-By: Claude Sonnet 4.5 +--- + heartbeat/podman-etcd | 123 +++++++++++++++++++++++++++++++++++++++--- + 1 file changed, 116 insertions(+), 7 deletions(-) + +diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd +index 0195633af..bad94aa44 100755 +--- a/heartbeat/podman-etcd ++++ b/heartbeat/podman-etcd +@@ -1267,6 +1267,41 @@ clear_standalone_node() + crm_attribute --name "standalone_node" --delete + } + ++is_single_node() ++{ ++ # Check if this is a single-node cluster by examining node_ip_map ++ # Returns 0 (true) if only one node is configured, 1 (false) otherwise ++ # When state is ambiguous or invalid, falls back to multi-node mode (safer) ++ local peer_count node_count ++ ++ # Empty node_ip_map - cannot determine topology, fall back to multi-node ++ if [ -z "$OCF_RESKEY_node_ip_map" ]; then ++ ocf_log warn "node_ip_map is empty - cannot determine cluster topology, assuming multi-node operation" ++ return 1 ++ fi ++ ++ # Check if our node is in the map - if not, we're in an invalid state ++ # Use awk to match node name field exactly (before first colon) to avoid substring matches ++ if ! printf "%s\n" "$OCF_RESKEY_node_ip_map" | tr ';' '\n' | awk -F: -v node="$NODENAME" '$1 == node { found=1 } END { exit !found }'; then ++ ocf_log warn "node $NODENAME not found in node_ip_map ('$OCF_RESKEY_node_ip_map') - invalid state, assuming multi-node operation" ++ return 1 ++ fi ++ ++ # Count total nodes and peers ++ # Use awk to count entries and match node name field exactly (prevents substring matches like "node1" matching "node10") ++ node_count=$(printf "%s\n" "$OCF_RESKEY_node_ip_map" | tr ';' '\n' | awk 'NF { count++ } END { print count+0 }') ++ peer_count=$(printf "%s\n" "$OCF_RESKEY_node_ip_map" | tr ';' '\n' | awk -F: -v node="$NODENAME" 'NF && $1 != node { count++ } END { print count+0 }') ++ ++ # Determine topology based on peer count ++ if [ "$peer_count" -eq 0 ]; then ++ ocf_log debug "Single-node cluster detected (node_count: $node_count, node_ip_map: '$OCF_RESKEY_node_ip_map')" ++ return 0 ++ else ++ ocf_log debug "Multi-node cluster detected (node_count: $node_count, peers: $peer_count, node_ip_map: '$OCF_RESKEY_node_ip_map')" ++ return 1 ++ fi ++} ++ + + # Promotes an etcd learner member to a voting member + # Args: $1 - learner member ID in decimal format +@@ -1538,6 +1573,39 @@ manage_peer_membership() + local peer_member_ip + local peer_member_id + ++ # Check if this is a single-node cluster ++ if is_single_node; then ++ # Single-node cluster - no peer to manage ++ ocf_log info "Single-node cluster detected (node_ip_map: '$OCF_RESKEY_node_ip_map')" ++ ++ # Clean up any learners from the member list (could be left over from 2→1 transition) ++ local learner_member_id ++ learner_member_id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .isLearner==true ).ID") ++ if [ -n "$learner_member_id" ]; then ++ ocf_log warn "Single-node cluster has learner member $learner_member_id - removing to ensure clean state" ++ local learner_member_id_hex ++ if learner_member_id_hex=$(decimal_to_hex "$learner_member_id"); then ++ local endpoint_url ++ endpoint_url=$(ip_url $(attribute_node_ip get)) ++ if podman exec "${CONTAINER}" etcdctl --endpoints="$endpoint_url:2379" member remove "$learner_member_id_hex" 2>&1; then ++ ocf_log info "Successfully removed learner member $learner_member_id_hex" ++ attribute_learner_node clear ++ else ++ ocf_log warn "Failed to remove learner member $learner_member_id_hex" ++ fi ++ else ++ ocf_log err "Could not convert learner member ID to hex: $learner_member_id" ++ fi ++ fi ++ ++ # Ensure standalone_node is set (may have been cleared during 2-node operation) ++ if ! is_standalone; then ++ ocf_log info "Setting standalone_node for single-node cluster" ++ set_standalone_node ++ fi ++ return $OCF_SUCCESS ++ fi ++ + # Get peer node name and IP + peer_ip_map_entry=$(echo "$OCF_RESKEY_node_ip_map" | tr ';' '\n' | grep -vF "$NODENAME") + if [ -z "$peer_ip_map_entry" ]; then +@@ -1733,6 +1801,18 @@ container_health_check() + return + fi + ++ # Check if this is a single-node cluster - no peer available for recovery ++ if is_single_node; then ++ ocf_log warn "Single-node cluster detected with failed container - no peer available for recovery" ++ # Set standalone_node to ensure proper single-node startup on restart ++ if ! set_standalone_node; then ++ ocf_log err "Failed to set standalone_node attribute, error code: $?" ++ fi ++ # Trigger immediate restart instead of waiting for non-existent peer ++ echo "failed-restart-now" ++ return ++ fi ++ + echo "failed-wait-for-peer" + } + +@@ -2144,16 +2224,39 @@ podman_start() + 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" ++ # Standalone node doesn't need peer comparison (matches active_resources_count=1 logic) ++ # Check this before is_single_node to handle drift reconciliation where standalone_node ++ # is set but node_ip_map may already include the peer being added ++ if is_standalone; then ++ ocf_log info "standalone node: skip peer comparison during recovery" ++ # Skip peer comparison - standalone_node is a persistent marker indicating we should ++ # force a new cluster on restart (set during quorum loss or single-node operation). ++ # revision_compare_result is intentionally left unset here - the is_single_node check ++ # in the start_resources_count case below will differentiate: ++ # - True single-node: start normally without force_new_cluster ++ # - Multi-node with standalone marker: set force_new_cluster (cold start after quorum loss) ++ # Note: This only forces new cluster when start_resources_count=1 (cold start). ++ # If active_resources_count=1 (peer already running), the new node joins as learner ++ # without force_new_cluster (handled in active_resources_count=1 case above). ++ elif is_single_node; then ++ ocf_log info "Single-node cluster: starting normally without revision comparison" ++ # No need to set force_new_cluster or compare revisions ++ else ++ # 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 + fi + case "$start_resources_count" in + 1) + ocf_log debug "peer not starting: ensure we can start a new cluster" +- if [ "$revision_compare_result" != "older" ]; then ++ if is_single_node; then ++ # Single-node cluster: start normally without revision comparison ++ ocf_log info "Single-node cluster: starting normally" ++ set_standalone_node ++ elif [ "$revision_compare_result" != "older" ]; then + # If our revision is the same as or newer than the peer's last saved + # revision, and the peer agent isn't currently starting, we can + # restore e-quorum by forcing a new cluster. +@@ -2358,7 +2461,13 @@ podman_start() + 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})" ++ # Check if this is a single-node cluster ++ if is_single_node; then ++ ocf_log info "Single-node cluster: setting standalone_node" ++ 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})" ++ fi + fi + fi + return $OCF_SUCCESS diff --git a/SOURCES/RHEL-193966-aws-vpc-move-ip-add-awscli_timeout-parameter.patch b/SOURCES/RHEL-193966-aws-vpc-move-ip-add-awscli_timeout-parameter.patch new file mode 100644 index 0000000..e2177f8 --- /dev/null +++ b/SOURCES/RHEL-193966-aws-vpc-move-ip-add-awscli_timeout-parameter.patch @@ -0,0 +1,81 @@ +From fd1c9a1cfd4463dad843f666ad951e6616f1ccf0 Mon Sep 17 00:00:00 2001 +From: Guilherme Felix +Date: Mon, 23 Mar 2026 19:22:49 +0000 +Subject: [PATCH 1/2] aws-vpc-move-ip: Add awscli_timeout option + +--- + heartbeat/aws-vpc-move-ip | 16 +++++++++++++++- + 1 file changed, 15 insertions(+), 1 deletion(-) + +diff --git a/heartbeat/aws-vpc-move-ip b/heartbeat/aws-vpc-move-ip +index 2afc0ba537..3e798173f5 100755 +--- a/heartbeat/aws-vpc-move-ip ++++ b/heartbeat/aws-vpc-move-ip +@@ -48,6 +48,7 @@ OCF_RESKEY_interface_default="eth0" + OCF_RESKEY_iflabel_default="" + OCF_RESKEY_monapi_default="false" + OCF_RESKEY_lookup_type_default="InstanceId" ++OCF_RESKEY_awscli_timeout_default="" + + : ${OCF_RESKEY_awscli=${OCF_RESKEY_awscli_default}} + : ${OCF_RESKEY_auth_type=${OCF_RESKEY_auth_type_default}} +@@ -61,6 +62,7 @@ OCF_RESKEY_lookup_type_default="InstanceId" + : ${OCF_RESKEY_iflabel=${OCF_RESKEY_iflabel_default}} + : ${OCF_RESKEY_monapi=${OCF_RESKEY_monapi_default}} + : ${OCF_RESKEY_lookup_type=${OCF_RESKEY_lookup_type_default}} ++: ${OCF_RESKEY_awscli_timeout=${OCF_RESKEY_awscli_timeout_default}} + ####################################################################### + + +@@ -211,6 +213,14 @@ curl sleep between tries + + + ++ ++ ++awscli cli-connect-timeout value ++ ++awscli cli-connect-timeout ++ ++ ++ + + + +@@ -490,7 +500,11 @@ if ! ocf_is_root; then + exit $OCF_ERR_PERM + fi + +-AWSCLI_CMD="${OCF_RESKEY_awscli}" ++if [ -n "${OCF_RESKEY_awscli_timeout}" ]; then ++ AWSCLI_CMD="${OCF_RESKEY_awscli} --cli-connect-timeout ${OCF_RESKEY_awscli_timeout}" ++else ++ AWSCLI_CMD="${OCF_RESKEY_awscli}" ++fi + if [ "x${OCF_RESKEY_auth_type}" = "xkey" ]; then + AWSCLI_CMD="$AWSCLI_CMD --profile ${OCF_RESKEY_profile}" + elif [ "x${OCF_RESKEY_auth_type}" = "xrole" ]; then + +From be4ad4dc4978875717b84b847bcd39386405bf51 Mon Sep 17 00:00:00 2001 +From: Guilherme Felix +Date: Tue, 24 Mar 2026 16:08:41 +0000 +Subject: [PATCH 2/2] aws-vpc-move-ip: Fix error in default value for + awscli_timeout + +--- + heartbeat/aws-vpc-move-ip | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/heartbeat/aws-vpc-move-ip b/heartbeat/aws-vpc-move-ip +index 3e798173f5..1348c59085 100755 +--- a/heartbeat/aws-vpc-move-ip ++++ b/heartbeat/aws-vpc-move-ip +@@ -218,7 +218,7 @@ curl sleep between tries + awscli cli-connect-timeout value + + awscli cli-connect-timeout +- ++ + + + diff --git a/SPECS/resource-agents.spec b/SPECS/resource-agents.spec index 0be3887..de7ec87 100644 --- a/SPECS/resource-agents.spec +++ b/SPECS/resource-agents.spec @@ -45,7 +45,7 @@ Name: resource-agents Summary: Open Source HA Reusable Cluster Resource Scripts Version: 4.10.0 -Release: 111%{?rcver:%{rcver}}%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty}}%{?dist}.5 +Release: 111%{?rcver:%{rcver}}%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty}}%{?dist}.7 License: GPLv2+ and LGPLv2+ URL: https://github.com/ClusterLabs/resource-agents Source0: %{upstream_prefix}-%{upstream_version}.tar.gz @@ -210,6 +210,8 @@ 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 Patch160: RHEL-188107-podman-etcd-remove-cert-monitoring.patch +Patch161: RHEL-193966-aws-vpc-move-ip-add-awscli_timeout-parameter.patch +Patch162: RHEL-193691-podman-etcd-add-support-for-single-node-operation.patch # bundled ha-cloud-support libs Patch500: ha-cloud-support-aliyun.patch @@ -515,6 +517,8 @@ exit 1 %patch -p1 -P 158 %patch -p1 -P 159 %patch -p1 -P 160 +%patch -p1 -P 161 +%patch -p1 -P 162 # bundled ha-cloud-support libs %patch -p1 -P 500 @@ -849,6 +853,16 @@ rm -rf %{buildroot}/usr/share/doc/resource-agents %{_usr}/lib/ocf/lib/heartbeat/OCF_*.pm %changelog +* Wed Jul 15 2026 Oyvind Albrigtsen - 4.10.0-111.7 +- podman-etcd: add support for single-node operation + + Resolves: RHEL-193691 + +* Fri Jul 10 2026 Arslan Ahmad - 4.10.0-111.6 +- aws-vpc-move-ip: add awscli_timeout parameter + + Resolves: RHEL-193966 + * Thu Jun 25 2026 Oyvind Albrigtsen - 4.10.0-111.5 - podman-etcd: remove cert monitoring