diff --git a/RHEL-116149-portblock-check-inverse-action.patch b/RHEL-116149-portblock-check-inverse-action.patch new file mode 100644 index 0000000..d050b5e --- /dev/null +++ b/RHEL-116149-portblock-check-inverse-action.patch @@ -0,0 +1,185 @@ +From ea0932c30c72a2667177de10c906c157cabd55d2 Mon Sep 17 00:00:00 2001 +From: Lars Ellenberg +Date: Wed, 25 Feb 2026 13:48:02 +0100 +Subject: [PATCH] portblock: monitor needs to also check state file of inverse + action (#2108) + +portblock: derive state file from parameters instead of instance + +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. + +Fixes regression introduced with 344beb1 status_check=rule (and set that as default), which breaks existing setups requiring user interaction. See also #2099. Alternative to #2107. + +Also: add save_tcp_connection to monnitor for promoted action=unblock instance +--- + heartbeat/portblock | 74 +++++++++++++++++++++++++++++++++++++++------ + 1 file changed, 65 insertions(+), 9 deletions(-) + +diff --git a/heartbeat/portblock b/heartbeat/portblock +index 20a84c21a..803eea55f 100755 +--- a/heartbeat/portblock ++++ b/heartbeat/portblock +@@ -30,12 +30,17 @@ OCF_RESKEY_portno_default="" + 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_firewall=${OCF_RESKEY_firewall_default}} + : ${OCF_RESKEY_protocol=${OCF_RESKEY_protocol_default}} + : ${OCF_RESKEY_portno=${OCF_RESKEY_portno_default}} +@@ -425,6 +430,10 @@ tickle_local() + 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)" +@@ -440,6 +449,11 @@ SayInactive() + ocf_log debug "$CMD $method rule [$*] is inactive" + } + ++SayConsideredInactive() ++{ ++ ocf_log debug "$CMD $method rule [$*] considered to be inactive" ++} ++ + #PortStatus {udp|tcp} portno,portno ip {in|out|both} {block|unblock} + PortStatus() { + local rc +@@ -465,8 +479,17 @@ PortStatus() { + 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 +@@ -478,6 +501,7 @@ PortStatus() { + *) + SayActive $* + if [ "$__OCF_ACTION" = "monitor" ] && [ "$promotion_score" = "$SCORE_PROMOTED" ]; then ++ save_tcp_connections + rc=$OCF_RUNNING_MASTER + else + rc=$OCF_SUCCESS +@@ -487,7 +511,10 @@ PortStatus() { + 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 +@@ -496,13 +523,15 @@ PortStatus() { + 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 + ;; +@@ -635,7 +664,7 @@ PortUNBLOCK() + #PortStart {udp|tcp} portno,portno ip {in|out|both} {block|unblock} + PortStart() + { +- ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" start ++ ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" start "$state_file" + + if [ "$FIREWALL" = "nft" ]; then + $NFTABLES add table inet $TABLE || { +@@ -678,7 +707,7 @@ PortStart() + #PortStop {udp|tcp} portno,portno ip {in|out|both} {block|unblock} + PortStop() + { +- ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" stop ++ ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" stop "$state_file" + + case $5 in + block) PortUNBLOCK "$@" +@@ -929,6 +958,33 @@ fi + + PortValidateAll + ++# 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) + PortStart "$protocol" "$portno" "$ip" "$direction" "$action" diff --git a/RHEL-150697-podman-etcd-set-attributes-if-they-fail-during-force-new-cluster.patch b/RHEL-150697-podman-etcd-set-attributes-if-they-fail-during-force-new-cluster.patch new file mode 100644 index 0000000..cd458ea --- /dev/null +++ b/RHEL-150697-podman-etcd-set-attributes-if-they-fail-during-force-new-cluster.patch @@ -0,0 +1,111 @@ +From e4d311b40d8ded2a1921a0e5c01cb49a07c9fb35 Mon Sep 17 00:00:00 2001 +From: Carlo Lobrano +Date: Thu, 5 Feb 2026 19:31:42 +0100 +Subject: [PATCH] podman-etcd: fix learner node attribute not set after etcdctl + failure + +Ensure that learner_node attribute is always set when the member list +contains one learner member. + +Moreover: +* Ensure set_standalone_node is called after adding a learner member. +* Capture stderr from etcdctl for better error logging. +--- + heartbeat/podman-etcd | 61 +++++++++++++++++++++++++++---------------- + 1 file changed, 38 insertions(+), 23 deletions(-) + +diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd +index 77525ddb7..06814ad89 100755 +--- a/heartbeat/podman-etcd ++++ b/heartbeat/podman-etcd +@@ -1082,7 +1082,7 @@ add_member_as_learner() + 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="$endpoint_url:2379" member add "$member_name" --peer-urls="$peer_url:2380" --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" +@@ -1429,10 +1429,22 @@ detect_cluster_leadership_loss() + manage_peer_membership() + { + local member_list_json="$1" ++ local peer_ip_map_entry ++ local peer_member_name ++ local peer_member_ip ++ local peer_member_id ++ ++ # 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 ++ ocf_exit_reason "manage_peer_membership: could not parse node_ip_map: '$OCF_RESKEY_node_ip_map'" ++ exit $OCF_ERR_CONFIGURED ++ fi ++ peer_member_name=$(echo "$peer_ip_map_entry" | cut -d: -f1) ++ peer_member_ip=$(echo "$peer_ip_map_entry" | cut -d: -f2-) + +- # 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 +- # and the value is always true (not a string) in that case. ++ # Parsing the member list's json output to find a "learner" member. ++ # Example of .members[] instance fields in member list json format: + # { + # "ID": , + # "name": "", +@@ -1443,26 +1455,28 @@ manage_peer_membership() + # "https://:2379" + # ] + # } +- for node in $(echo "$OCF_RESKEY_node_ip_map" | sed "s/\s//g;s/;/ /g"); do +- name=$(echo "$node" | cut -d: -f1) +- # do not check itself +- if [ "$name" = "$NODENAME" ]; then +- continue +- fi ++ # 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. ++ 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" ++ add_member_as_learner "$peer_member_name" "$peer_member_ip" ++ set_standalone_node ++ return ++ fi + +- # 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 +- 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" +- # Errors from reconcile_member_state are logged internally. Ignoring them here prevents stopping a healthy voter agent; critical local failures are caught by detect_cluster_leadership_loss. +- reconcile_member_state "$member_list_json" +- fi +- done ++ # 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 ++ fi ++ ++ ocf_log debug "$peer_member_name is in the members list by IP: $peer_member_ip" ++ # Errors from reconcile_member_state are logged internally. Ignoring them here prevents stopping a healthy voter agent; critical local failures are caught by detect_cluster_leadership_loss. ++ reconcile_member_state "$member_list_json" + } + + check_peer() +@@ -2209,6 +2223,7 @@ podman_start() + 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" ++ 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 diff --git a/resource-agents.spec b/resource-agents.spec index ed11c50..b458f1f 100644 --- a/resource-agents.spec +++ b/resource-agents.spec @@ -45,7 +45,7 @@ Name: resource-agents Summary: Open Source HA Reusable Cluster Resource Scripts Version: 4.16.0 -Release: 53%{?rcver:%{rcver}}%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty}}%{?dist} +Release: 54%{?rcver:%{rcver}}%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty}}%{?dist} License: GPL-2.0-or-later AND LGPL-2.1-or-later URL: https://github.com/ClusterLabs/resource-agents Source0: %{upstream_prefix}-%{upstream_version}.tar.gz @@ -118,6 +118,8 @@ Patch65: RHEL-143524-powervs-move-ip-powervs-subnet-fix-error-logging.patch Patch66: RHEL-116149-RHEL-116152-4-check-correct-binary-during-validate-all.patch Patch67: RHEL-145622-podman-etcd-enhance-etcd-data-backup-with-snapshots-and-retention.patch Patch68: RHEL-149738-IPsrcaddr-fix-grep-expression-for-newer-grep-versions.patch +Patch69: RHEL-116149-portblock-check-inverse-action.patch +Patch70: RHEL-150697-podman-etcd-set-attributes-if-they-fail-during-force-new-cluster.patch # bundled ha-cloud-support libs Patch500: ha-cloud-support-aliyun.patch @@ -357,6 +359,8 @@ exit 1 %patch -p1 -P 66 %patch -p1 -P 67 %patch -p1 -P 68 +%patch -p1 -P 69 +%patch -p1 -P 70 # bundled ha-cloud-support libs %patch -p1 -P 500 @@ -689,6 +693,13 @@ rm -rf %{buildroot}/usr/share/doc/resource-agents %{_usr}/lib/ocf/lib/heartbeat/OCF_*.pm %changelog +* Thu Feb 26 2026 Oyvind Albrigtsen - 4.16.0-54 +- portblock: check inverse action state file for non-promotable + resources to avoid issues when doing e.g. block followed by unblock +- podman-etcd: set attributes if they fail during force-new-cluster + + Resolves: RHEL-116149, RHEL-150697 + * Mon Feb 16 2026 Oyvind Albrigtsen - 4.16.0-53 - IPsrcaddr: fix grep expression, so it doesnt log "stray \ before white space" with newer versions of grep