- 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:
parent
d9d35bb587
commit
ed7e1ddd3e
185
RHEL-116149-portblock-check-inverse-action.patch
Normal file
185
RHEL-116149-portblock-check-inverse-action.patch
Normal 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"
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user