- 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
This commit is contained in:
Oyvind Albrigtsen 2026-02-26 11:47:42 +01:00
parent d9d35bb587
commit ed7e1ddd3e
3 changed files with 308 additions and 1 deletions

View File

@ -0,0 +1,185 @@
From ea0932c30c72a2667177de10c906c157cabd55d2 Mon Sep 17 00:00:00 2001
From: Lars Ellenberg <lars.ellenberg@linbit.com>
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"

View File

@ -0,0 +1,111 @@
From e4d311b40d8ded2a1921a0e5c01cb49a07c9fb35 Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
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": <member ID>,
# "name": "<node hostname>",
@@ -1443,26 +1455,28 @@ manage_peer_membership()
# "https://<node IP>: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

View File

@ -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 <oalbrigt@redhat.com> - 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 <oalbrigt@redhat.com> - 4.16.0-53
- IPsrcaddr: fix grep expression, so it doesnt log "stray \ before
white space" with newer versions of grep