import Oracle_OSS resource-agents-4.16.0-53.el10_2.7

This commit is contained in:
AlmaLinux RelEng Bot 2026-07-17 17:33:49 -04:00
parent bf9b2b6a3e
commit a638e462be
56 changed files with 6926 additions and 626 deletions

View File

@ -0,0 +1,258 @@
From fc240bdff60aae7133a532c7752c6253ce8f65ca Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Mon, 4 Aug 2025 16:53:09 +0200
Subject: [PATCH 1/2] db2: add "skip_basic_sql_health_check" parameter to avoid
failing on systems with high load
---
heartbeat/db2 | 63 +++++++++++++++++++++++++++++++--------------------
1 file changed, 38 insertions(+), 25 deletions(-)
diff --git a/heartbeat/db2 b/heartbeat/db2
index 1cd66f15a..da6c9d5f1 100755
--- a/heartbeat/db2
+++ b/heartbeat/db2
@@ -40,10 +40,12 @@
# Parameter defaults
OCF_RESKEY_instance_default=""
+OCF_RESKEY_skip_basic_sql_health_check_default="false"
OCF_RESKEY_admin_default=""
OCF_RESKEY_dbpartitionnum_default="0"
: ${OCF_RESKEY_instance=${OCF_RESKEY_instance_default}}
+: ${OCF_RESKEY_skip_basic_sql_health_check=${OCF_RESKEY_skip_basic_sql_health_check_default}}
: ${OCF_RESKEY_admin=${OCF_RESKEY_admin_default}}
: ${OCF_RESKEY_dbpartitionnum=${OCF_RESKEY_dbpartitionnum_default}}
@@ -102,6 +104,15 @@ Defaults to all databases in the instance. Specify one db for HADR mode.
<shortdesc lang="en">List of databases to be managed</shortdesc>
<content type="string"/>
</parameter>
+<parameter name="skip_basic_sql_health_check" unique="0" required="0">
+<longdesc lang="en">
+Skip basic health check SQL query.
+
+Only set to "true" to avoid issues during high load.
+</longdesc>
+<shortdesc lang="en">Skip basic health check SQL query</shortdesc>
+<content type="boolean" default="${OCF_RESKEY_skip_basic_sql_health_check_default}" />
+</parameter>
<parameter name="admin" unique="0" required="0">
<longdesc lang="en">
DEPRECATED: The admin user of the instance.
@@ -695,31 +706,33 @@ db2_monitor() {
# set master preference accordingly
case "$hadr" in
PRIMARY/*|Primary/*|Standard/*)
- # perform a basic health check
- CMD="if db2 connect to $db;
- then
- db2 select \* from sysibm.sysversions ; rc=\$?;
- db2 terminate;
- else
- rc=\$?;
- fi;
- exit \$rc"
-
- if ! output=$(runasdb2 $CMD)
- then
- case "$output" in
- SQL1776N*)
- # can't connect/select on standby, may be spurious turing takeover
- ;;
-
- *)
- ocf_log err "DB2 database $instance($db2node)/$db is not working"
- ocf_log err "DB2 message: $output"
-
- # dead primary, remove master score
- master_score -D -l reboot
- return $OCF_ERR_GENERIC
- esac
+ if ! ocf_is_true "$OCF_RESKEY_skip_basic_sql_health_check"; then
+ # perform a basic health check
+ CMD="if db2 connect to $db;
+ then
+ db2 select \* from sysibm.sysversions ; rc=\$?;
+ db2 terminate;
+ else
+ rc=\$?;
+ fi;
+ exit \$rc"
+
+ if ! output=$(runasdb2 $CMD)
+ then
+ case "$output" in
+ SQL1776N*)
+ # can't connect/select on standby, may be spurious turing takeover
+ ;;
+
+ *)
+ ocf_log err "DB2 database $instance($db2node)/$db is not working"
+ ocf_log err "DB2 message: $output"
+
+ # dead primary, remove master score
+ master_score -D -l reboot
+ return $OCF_ERR_GENERIC
+ esac
+ fi
fi
ocf_log debug "DB2 database $instance($db2node)/$db appears to be working"
From ded016f84d3fb77dc0542e3f4226774526910d97 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Thu, 7 Aug 2025 13:55:11 +0200
Subject: [PATCH 2/2] db2: add "monitor_retries", "monitor_sleep", and
"monitor_retry_all_errors" parameters to be able to avoid failing on first
try
---
heartbeat/db2 | 80 +++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 72 insertions(+), 8 deletions(-)
diff --git a/heartbeat/db2 b/heartbeat/db2
index da6c9d5f1..fe1d9b892 100755
--- a/heartbeat/db2
+++ b/heartbeat/db2
@@ -41,11 +41,17 @@
OCF_RESKEY_instance_default=""
OCF_RESKEY_skip_basic_sql_health_check_default="false"
+OCF_RESKEY_monitor_retries_default="1"
+OCF_RESKEY_monitor_sleep_default="1"
+OCF_RESKEY_monitor_retry_all_errors_default="false"
OCF_RESKEY_admin_default=""
OCF_RESKEY_dbpartitionnum_default="0"
: ${OCF_RESKEY_instance=${OCF_RESKEY_instance_default}}
: ${OCF_RESKEY_skip_basic_sql_health_check=${OCF_RESKEY_skip_basic_sql_health_check_default}}
+: ${OCF_RESKEY_monitor_retries=${OCF_RESKEY_monitor_retries_default}}
+: ${OCF_RESKEY_monitor_sleep=${OCF_RESKEY_monitor_sleep_default}}
+: ${OCF_RESKEY_monitor_retry_all_errors=${OCF_RESKEY_monitor_retry_all_errors_default}}
: ${OCF_RESKEY_admin=${OCF_RESKEY_admin_default}}
: ${OCF_RESKEY_dbpartitionnum=${OCF_RESKEY_dbpartitionnum_default}}
@@ -108,11 +114,33 @@ Defaults to all databases in the instance. Specify one db for HADR mode.
<longdesc lang="en">
Skip basic health check SQL query.
-Only set to "true" to avoid issues during high load.
+Only set to "true" when the "monitor_retries" and "monitor_retry_all_errors" parameters arent
+enough to avoid issues under high load.
</longdesc>
<shortdesc lang="en">Skip basic health check SQL query</shortdesc>
<content type="boolean" default="${OCF_RESKEY_skip_basic_sql_health_check_default}" />
</parameter>
+<parameter name="monitor_retries" unique="0" required="0">
+<longdesc lang="en">
+Monitor retries before failing.
+</longdesc>
+<shortdesc lang="en">Monitor retries</shortdesc>
+<content type="string" default="${OCF_RESKEY_monitor_retries_default}" />
+</parameter>
+<parameter name="monitor_retries_sleep" unique="0" required="0">
+<longdesc lang="en">
+Monitor sleep between tries.
+</longdesc>
+<shortdesc lang="en">Monitor sleep</shortdesc>
+<content type="string" default="${OCF_RESKEY_monitor_sleep_default}" />
+</parameter>
+<parameter name="monitor_retry_all_errors" unique="0" required="0">
+<longdesc lang="en">
+Set to true to retry monitor-action for all errors instead of the default "db2pd" race conditions.
+</longdesc>
+<shortdesc lang="en">Retry monitor for all errors</shortdesc>
+<content type="string" default="${OCF_RESKEY_monitor_retry_all_errors_default}" />
+</parameter>
<parameter name="admin" unique="0" required="0">
<longdesc lang="en">
DEPRECATED: The admin user of the instance.
@@ -666,6 +694,7 @@ db2_hadr_status() {
local output
output=$(runasdb2 db2pd -hadr -db $db)
+ ocf_log debug "db2_hadr_status: $output"
if [ $? != 0 ]
then
echo "Down/Off"
@@ -676,7 +705,34 @@ db2_hadr_status() {
awk '/^\s+HADR_(ROLE|STATE) =/ {printf $3"/"}
/^\s+HADR_CONNECT_STATUS =/ {print $3; exit; }
/^HADR is not active/ {print "Standard/Standalone"; exit; }
- /^Role *State */ {getline; printf "%s/%s\n", $1, $2; exit; }'
+ /^Role *State */ {getline; printf "%s/%s\n", $1, $2; exit; }
+ /^Option -hadr requires -db <database> or -alldbs option and active database./ { exit 255 }
+ /^Another possibility of this failure is the Virtual Address Space Randomization is currently enabled on this system./ { exit 255 }
+ /^Changing data structure forced command termination./ { exit 255 }'
+}
+
+db2_monitor_retry() {
+ local tries=$(($OCF_RESKEY_monitor_retries + 1))
+
+ for try in $(seq $tries); do
+ ocf_log debug "monitor try $try of $tries"
+ db2_monitor
+ rc=$?
+ [ $rc -ne $OCF_SUCCESS ] && [ $rc -ne $OCF_RUNNING_MASTER ] && [ $rc -ne $OCF_NOT_RUNNING ] && ocf_log warn "Monitor failed with rc $rc."
+ if [ $rc -eq $OCF_SUCCESS ] || [ $rc -eq $OCF_RUNNING_MASTER ] || [ $rc -eq $OCF_NOT_RUNNING ] || { [ $rc -ne 255 ] && ! ocf_is_true "$OCF_RESKEY_monitor_retry_all_errors" ;} ;then
+ break
+ fi
+ [ $try -lt $tries ] && sleep $OCF_RESKEY_monitor_sleep
+ done
+
+ [ $rc -eq 255 ] && rc=$OCF_ERR_GENERIC
+
+ if [ $rc -ne $OCF_SUCCESS ] && [ $rc -ne $OCF_RUNNING_MASTER ]; then
+ # instance is dead remove master score
+ master_score -D -l reboot
+ fi
+
+ return $rc
}
#
@@ -690,9 +746,7 @@ db2_monitor() {
db2_instance_status
rc=$?
if [ $rc -ne $OCF_SUCCESS ]; then
- # instance is dead remove master score
- master_score -D -l reboot
- exit $rc
+ return $rc
fi
[ $db2node = 0 ] || return 0
@@ -700,8 +754,18 @@ db2_monitor() {
for db in $dblist
do
- hadr=$(db2_hadr_status $db) || return $OCF_ERR_GENERIC
+ hadr=$(db2_hadr_status $db)
+ rc=$?
ocf_log debug "Monitor: DB2 database $instance($db2node)/$db has HADR status $hadr"
+ if [ "$rc" -eq 255 ]; then
+ if [ "$__OCF_ACTION" = "monitor" ]; then
+ return $rc
+ else
+ return $OCF_ERR_GENERIC
+ fi
+ elif [ "$rc" -ne 0 ]; then
+ return $OCF_ERR_GENERIC
+ fi
# set master preference accordingly
case "$hadr" in
@@ -915,9 +979,9 @@ case "$__OCF_ACTION" in
exit $?
;;
- monitor)
+ monitor)
db2_validate
- db2_monitor
+ db2_monitor_retry
exit $?
;;

View File

@ -0,0 +1,49 @@
From 54714646c6e2c4ba851e366e63316adb1092af61 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Tue, 28 Oct 2025 16:34:54 +0100
Subject: [PATCH] db2: fix monitor_retries_sleep variable name
---
heartbeat/db2 | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/heartbeat/db2 b/heartbeat/db2
index 83020fc70..82f2f82c3 100755
--- a/heartbeat/db2
+++ b/heartbeat/db2
@@ -49,7 +49,7 @@ fi
OCF_RESKEY_instance_default=""
OCF_RESKEY_skip_basic_sql_health_check_default="false"
OCF_RESKEY_monitor_retries_default="1"
-OCF_RESKEY_monitor_sleep_default="1"
+OCF_RESKEY_monitor_retries_sleep_default="1"
OCF_RESKEY_monitor_retry_all_errors_default="false"
OCF_RESKEY_admin_default=""
OCF_RESKEY_dbpartitionnum_default="0"
@@ -57,7 +57,7 @@ OCF_RESKEY_dbpartitionnum_default="0"
: ${OCF_RESKEY_instance=${OCF_RESKEY_instance_default}}
: ${OCF_RESKEY_skip_basic_sql_health_check=${OCF_RESKEY_skip_basic_sql_health_check_default}}
: ${OCF_RESKEY_monitor_retries=${OCF_RESKEY_monitor_retries_default}}
-: ${OCF_RESKEY_monitor_sleep=${OCF_RESKEY_monitor_sleep_default}}
+: ${OCF_RESKEY_monitor_retries_sleep=${OCF_RESKEY_monitor_retries_sleep_default}}
: ${OCF_RESKEY_monitor_retry_all_errors=${OCF_RESKEY_monitor_retry_all_errors_default}}
: ${OCF_RESKEY_admin=${OCF_RESKEY_admin_default}}
: ${OCF_RESKEY_dbpartitionnum=${OCF_RESKEY_dbpartitionnum_default}}
@@ -140,7 +140,7 @@ Monitor retries before failing.
Monitor sleep between tries.
</longdesc>
<shortdesc lang="en">Monitor sleep</shortdesc>
-<content type="string" default="${OCF_RESKEY_monitor_sleep_default}" />
+<content type="string" default="${OCF_RESKEY_monitor_retries_sleep_default}" />
</parameter>
<parameter name="monitor_retry_all_errors" unique="0" required="0">
<longdesc lang="en">
@@ -776,7 +776,7 @@ db2_monitor_retry() {
if [ $rc -eq $OCF_SUCCESS ] || [ $rc -eq $OCF_RUNNING_MASTER ] || [ $rc -eq $OCF_NOT_RUNNING ] || { [ $rc -ne 255 ] && ! ocf_is_true "$OCF_RESKEY_monitor_retry_all_errors" ;} ;then
break
fi
- [ $try -lt $tries ] && sleep $OCF_RESKEY_monitor_sleep
+ [ $try -lt $tries ] && sleep $OCF_RESKEY_monitor_retries_sleep
done
[ $rc -eq 255 ] && rc=$OCF_ERR_GENERIC

View File

@ -0,0 +1,181 @@
From 443841ea27d61a2eedff4a0c4f18bb5771fb8d5e Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Tue, 8 Jul 2025 15:19:09 +0200
Subject: [PATCH] pgsqlms: improvements and fixes
- add support for promotable variables
- dont fail during validate-all action if notify != true (to avoid
error and future fails during `pcs resource create`)
- report NOT_RUNNING during probe-action when no database has been
created or postgresql is not installed
---
script/pgsqlms | 74 +++++++++++++++++++++++++++++++++-----------------
1 file changed, 49 insertions(+), 25 deletions(-)
diff --git a/heartbeat/pgsqlms b/heartbeat/pgsqlms
index 5ddd67a..1abffeb 100755
--- a/heartbeat/pgsqlms
+++ b/heartbeat/pgsqlms
@@ -485,7 +485,7 @@ sub _pg_isready {
# Add 60s to the timeout or use a 24h timeout fallback to make sure
# Pacemaker will give up before us and take decisions
my $timeout = ( _get_action_timeout() || 60*60*24 ) + 60;
- my $rc = _runas( $PGISREADY, '-h', $pghost, '-p', $pgport, '-d', 'postgres', '-t', $timeout );
+ my $rc = _runas( $PGISREADY, '-q', '-h', $pghost, '-p', $pgport, '-d', 'postgres', '-t', $timeout );
# Possible error codes:
# 1: ping rejected (usually when instance is in startup, in crash
@@ -624,14 +624,18 @@ sub _get_controldata {
and defined $controldata{'redo'}
and defined $controldata{'wal_level'};
- ocf_exit_reason( 'Could not read all datas from controldata file for "%s"',
- $datadir );
+ if ( ! ocf_is_probe() ) {
+ ocf_exit_reason( 'Could not read all datas from controldata file for "%s"',
+ $datadir );
- ocf_log( 'debug',
- "_get_controldata: controldata file: %s",
- Data::Dumper->new( [ \%controldata ] )->Terse(1)->Dump, $ans );
+ ocf_log( 'debug',
+ "_get_controldata: controldata file: %s",
+ Data::Dumper->new( [ \%controldata ] )->Terse(1)->Dump, $ans );
- exit $OCF_ERR_ARGS;
+ exit $OCF_ERR_ARGS;
+ }
+
+ return ();
}
# Pead major version from datadir/PG_VERSION and return it as numeric version
@@ -642,8 +646,12 @@ sub _get_pg_version {
# check PG_VERSION
if ( ! -s "$datadir/PG_VERSION" ) {
- ocf_exit_reason( 'PG_VERSION does not exist in "%s"', $datadir );
- exit $OCF_ERR_ARGS;
+ if ( ! ocf_is_probe() ) {
+ ocf_exit_reason( 'PG_VERSION does not exist in "%s"', $datadir );
+ exit $OCF_ERR_ARGS;
+ } else {
+ return -1;
+ }
}
unless ( open( $fh, '<', "$datadir/PG_VERSION" ) ) {
@@ -1324,22 +1332,34 @@ sub pgsql_validate_all {
}
# check notify=true
- unless ( defined $ENV{'OCF_RESKEY_CRM_meta_notify'}
- and lc($ENV{'OCF_RESKEY_CRM_meta_notify'}) =~ /^true$|^on$|^yes$|^y$|^1$/ ) {
+ unless ( $__OCF_ACTION eq 'validate-all'
+ or ( defined $ENV{'OCF_RESKEY_CRM_meta_notify'}
+ and lc($ENV{'OCF_RESKEY_CRM_meta_notify'}) =~ /^true$|^on$|^yes$|^y$|^1$/ ) ) {
ocf_exit_reason(
'You must set meta parameter notify=true for your "master" resource'
);
return $OCF_ERR_INSTALLED;
}
- # check master-max=1
+ # check promoted_max=1/master-max=1
unless (
- defined $ENV{'OCF_RESKEY_CRM_meta_master_max'}
- and $ENV{'OCF_RESKEY_CRM_meta_master_max'} eq '1'
+ $__OCF_ACTION eq 'validate-all'
+ or
+ ( defined $ENV{'OCF_RESKEY_CRM_meta_promoted_max'}
+ and $ENV{'OCF_RESKEY_CRM_meta_promoted_max'} eq '1' )
+ or
+ (defined $ENV{'OCF_RESKEY_CRM_meta_master_max'}
+ and $ENV{'OCF_RESKEY_CRM_meta_master_max'} eq '1')
) {
- ocf_exit_reason(
- 'You must set meta parameter master-max=1 for your "master" resource'
- );
+ if ( ocf_version_cmp( $ENV{"OCF_RESKEY_crm_feature_set"}, '3.1.0' ) =~ /^[21]$/ ) {
+ ocf_exit_reason(
+ 'You must set meta parameter promoted_max=1 for your "promotable" resource'
+ );
+ } else {
+ ocf_exit_reason(
+ 'You must set meta parameter master-max=1 for your "master" resource'
+ );
+ }
return $OCF_ERR_INSTALLED;
}
@@ -1366,14 +1386,14 @@ sub pgsql_validate_all {
}
$guc = qx{ $POSTGRES -C primary_conninfo -D "$pgdata" $start_opts};
- unless ($guc =~ /\bapplication_name='?$nodename'?\b/) {
+ unless ($guc =~ /\bapplication_name='?$nodename'?\b/ or $__OCF_ACTION eq 'validate-all') {
ocf_exit_reason(
q{Parameter "primary_conninfo" MUST contain 'application_name=%s'. }.
q{It is currently set to '%s'}, $nodename, $guc );
return $OCF_ERR_ARGS;
}
}
- else {
+ elsif ($PGVERNUM > -1 ) {
my @content;
# check recovery template
@@ -1428,14 +1448,14 @@ sub pgsql_validate_all {
}
# require 9.3 minimum
- if ( $PGVERNUM < $PGVER_93 ) {
+ if ( $PGVERNUM < $PGVER_93 && $PGVERNUM > -1 ) {
ocf_exit_reason( "Require 9.3 and more" );
return $OCF_ERR_INSTALLED;
}
# check binaries
- unless ( -x $PGCTL and -x $PGPSQL and -x $PGCTRLDATA and -x $PGISREADY
- and ( -x $PGWALDUMP or -x "$bindir/pg_xlogdump")
+ unless ( ( -x $PGCTL and -x $PGPSQL and -x $PGCTRLDATA and -x $PGISREADY
+ and ( -x $PGWALDUMP or -x "$bindir/pg_xlogdump") ) or ocf_is_probe()
) {
ocf_exit_reason(
"Missing one or more binary. Check following path: %s, %s, %s, %s, %s or %s",
@@ -1445,7 +1465,7 @@ sub pgsql_validate_all {
# require wal_level >= hot_standby
%cdata = _get_controldata();
- unless ( $cdata{'wal_level'} =~ m{hot_standby|logical|replica} ) {
+ unless ( (defined $cdata{'wal_level'} and $cdata{'wal_level'} =~ m{hot_standby|logical|replica}) or ocf_is_probe() ) {
ocf_exit_reason(
'wal_level must be one of "hot_standby", "logical" or "replica"' );
return $OCF_ERR_ARGS;
@@ -1599,6 +1619,10 @@ sub pgsql_monitor {
return _confirm_role();
}
+ if ( ocf_is_probe() ) {
+ return $OCF_NOT_RUNNING;
+ }
+
if ( $pgisready_rc == 1 ) {
# The attempt was rejected.
# This could happen in several cases:
@@ -2254,13 +2278,13 @@ chdir File::Spec->tmpdir();
# mandatory sanity checks
# check pgdata
-if ( ! -d $pgdata ) {
+if ( ! -d $pgdata and ! ocf_is_probe() ) {
ocf_exit_reason( 'PGDATA "%s" does not exist', $pgdata );
exit $OCF_ERR_ARGS;
}
# check datadir
-if ( ! -d $datadir ) {
+if ( ! -d $datadir and ! ocf_is_probe() ) {
ocf_exit_reason( 'data_directory "%s" does not exist', $datadir );
exit $OCF_ERR_ARGS;
}

View File

@ -0,0 +1,92 @@
From a4fd26a37b20e86e7c188b45d40e31d240f3decf Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Thu, 14 Aug 2025 09:33:17 +0200
Subject: [PATCH] nfsserver: add ability to set e.g.
"pipefs-directory=/run/nfs/rpc_pipefs" in /etc/nfs.conf to avoid issues with
non-clustered Kerberized mounts
---
heartbeat/nfsserver | 28 +++++++++++++++++-----------
1 file changed, 17 insertions(+), 11 deletions(-)
diff --git a/heartbeat/nfsserver b/heartbeat/nfsserver
index 5b02924a9..83f4bac51 100755
--- a/heartbeat/nfsserver
+++ b/heartbeat/nfsserver
@@ -264,7 +264,7 @@ set_exec_mode()
##
# If the user defined an init script, It must exist for us to continue
##
- if [ -n "$OCF_RESKEY_nfs_init_script" ]; then
+ if [ $systemd_running -ne 0 ] && [ -n "$OCF_RESKEY_nfs_init_script" ]; then
# check_binary will exit the process if init script does not exist
check_binary ${OCF_RESKEY_nfs_init_script}
EXEC_MODE=1
@@ -274,7 +274,7 @@ set_exec_mode()
##
# Check to see if the default init script exists, if so we'll use that.
##
- if which $DEFAULT_INIT_SCRIPT > /dev/null 2>&1; then
+ if [ $systemd_running -ne 0 ] && which $DEFAULT_INIT_SCRIPT > /dev/null 2>&1; then
OCF_RESKEY_nfs_init_script=$DEFAULT_INIT_SCRIPT
EXEC_MODE=1
return 0
@@ -780,7 +780,7 @@ nfsserver_start ()
# the uts namespace is useless in that case.
# If systemd is running, mangle the nfs-server.service unit,
# independent of the "EXEC_MODE" we detected.
- if $systemd_is_running ; then
+ if [ $systemd_running -eq 0 ]; then
if [ -z "$OCF_RESKEY_nfs_server_scope" ] ; then
remove_unshare_uts_dropins
else
@@ -789,7 +789,9 @@ nfsserver_start ()
fi
if ! `mount | grep -q " on $OCF_RESKEY_rpcpipefs_dir "`; then
- mount -t rpc_pipefs sunrpc $OCF_RESKEY_rpcpipefs_dir
+ if [ $systemd_running -ne 0 ] || { [ $systemd_running -eq 0 ] && systemctl -q is-enabled var-lib-nfs-rpc_pipefs.mount ;}; then
+ mount -t rpc_pipefs sunrpc $OCF_RESKEY_rpcpipefs_dir
+ fi
fi
# remove the sm-notify pid so sm-notify will be allowed to run again without requiring a reboot.
@@ -1003,11 +1005,15 @@ nfsserver_stop ()
fi
fi
- # systemd
- case $EXEC_MODE in
- [23]) nfs_exec stop rpc-gssd > /dev/null 2>&1
- ocf_log info "Stop: rpc-gssd"
- esac
+
+ if mount | grep -q " on $OCF_RESKEY_rpcpipefs_dir "; then
+ # systemd
+ case $EXEC_MODE in
+ [23])
+ nfs_exec stop rpc-gssd > /dev/null 2>&1
+ ocf_log info "Stop: rpc-gssd"
+ esac
+ fi
unbind_tree
rc=$?
@@ -1017,7 +1023,7 @@ nfsserver_stop ()
ocf_log info "NFS server stopped"
fi
- if $systemd_is_running; then
+ if [ $systemd_running -eq 0 ]; then
remove_unshare_uts_dropins
fi
@@ -1057,7 +1063,7 @@ nfsserver_validate ()
}
nfsserver_validate
-systemd_is_running && systemd_is_running=true || systemd_is_running=false
+systemd_is_running; systemd_running=$?
case $__OCF_ACTION in
start) nfsserver_start

View File

@ -0,0 +1,24 @@
From 72620db5b52c943358faaf77ce5a15fb41169fab Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Fri, 31 Oct 2025 11:22:46 +0100
Subject: [PATCH] nfsserver: set systemd_running before nfsserver_validate() to
avoid error message
---
heartbeat/nfsserver | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/heartbeat/nfsserver b/heartbeat/nfsserver
index 83f4bac51..71a711305 100755
--- a/heartbeat/nfsserver
+++ b/heartbeat/nfsserver
@@ -1062,8 +1062,8 @@ nfsserver_validate ()
return $OCF_SUCCESS
}
-nfsserver_validate
systemd_is_running; systemd_running=$?
+nfsserver_validate
case $__OCF_ACTION in
start) nfsserver_start

View File

@ -0,0 +1,686 @@
From 6e9200dc2ffc89382188794742361985309936b2 Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Wed, 23 Jul 2025 09:34:13 +0200
Subject: [PATCH] podman-etcd: preserve containers for debugging
This change modifies the agent to keep stopped containers for log
inspection and debugging, with supporting changes to enable this
behavior.
* Conditionally reuse existing containers when configuration unchanged
* Move etcd inline configuration flags to external file to allow
restarts without container recreation (mainly for the
force-new-cluster flag)
* Archive previous container renaming it into *-previous, and its
configuration files into /var/lib/etcd/config-previous.tar.gz archive.
The tar.gz archive consists in:
* the pod manifest created by CEO, used to generated the Etc
configuration file
* the Etcd configuration file
* the auth json file
Only one copy is maintained to limit disk usage.
* Both configuration and backup files location is configurable with 2
new input arguments.
Signed-off-by: Carlo Lobrano <c.lobrano@gmail.com>
---
heartbeat/podman-etcd | 438 ++++++++++++++++++++++++++++++++----------
1 file changed, 336 insertions(+), 102 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 4969fbaaf..33804414a 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -46,6 +46,8 @@ OCF_RESKEY_authfile_default="/var/lib/kubelet/config.json"
OCF_RESKEY_allow_pull_default="1"
OCF_RESKEY_reuse_default="0"
OCF_RESKEY_oom_default="-997"
+OCF_RESKEY_config_location_default="/var/lib/etcd"
+OCF_RESKEY_backup_location_default="/var/lib/etcd"
: ${OCF_RESKEY_image=${OCF_RESKEY_image_default}}
: ${OCF_RESKEY_pod_manifest=${OCF_RESKEY_pod_manifest_default}}
@@ -55,6 +57,9 @@ OCF_RESKEY_oom_default="-997"
: ${OCF_RESKEY_allow_pull=${OCF_RESKEY_allow_pull_default}}
: ${OCF_RESKEY_reuse=${OCF_RESKEY_reuse_default}}
: ${OCF_RESKEY_oom=${OCF_RESKEY_oom_default}}
+: ${OCF_RESKEY_config_location=${OCF_RESKEY_config_location_default}}
+: ${OCF_RESKEY_backup_location=${OCF_RESKEY_backup_location_default}}
+
#######################################################################
@@ -242,6 +247,23 @@ https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/#
<shortdesc lang="en">OOM for container</shortdesc>
<content type="integer" default="${OCF_RESKEY_oom_default}"/>
</parameter>
+
+<parameter name="config_location" required="0" unique="0">
+<longdesc lang="en">
+The directory where the resource agent stores its state files, such as the generated etcd configuration and a copy of the pod manifest.
+</longdesc>
+<shortdesc lang="en">Resource agent state directory</shortdesc>
+<content type="string" default="${OCF_RESKEY_config_location_default}"/>
+</parameter>
+
+<parameter name="backup_location" required="0" unique="0">
+<longdesc lang="en">
+The directory where the resource agent stores its backups.
+</longdesc>
+<shortdesc lang="en">Resource agent backup directory</shortdesc>
+<content type="string" default="${OCF_RESKEY_backup_location_default}"/>
+</parameter>
+
</parameters>
<actions>
@@ -309,42 +331,52 @@ container_exists()
return 1
}
-remove_container()
+# archive_current_container archives the current
+# podman etcd container and its configuration files.
+archive_current_container()
{
- local rc
- local execids
+ # don't attempt to archive a container that doesn't exist
+ if ! container_exists; then
+ return
+ fi
- if ocf_is_true "$OCF_RESKEY_reuse"; then
- # never remove the container if we have reuse enabled.
- return 0
+ # delete any container named "*-previous", or we won't be able to archive the current container.
+ if podman inspect "${CONTAINER}-previous" >/dev/null 2>&1; then
+ ocf_log info "removing old archived container '$CONTAINER-previous'"
+ if ! ocf_run podman rm --volumes --force "$CONTAINER-previous"; then
+ ocf_log warn "could not remove old archived container (podman rm failed, error code: $?). Won't be able to archive current container"
+ return
+ fi
fi
- if ! container_exists; then
- # don't attempt to remove a container that doesn't exist
- return 0
+ ocf_log info "archiving '$CONTAINER' container as '$CONTAINER-previous' for debugging purposes"
+ if ! ocf_run podman rename "$CONTAINER" "$CONTAINER-previous"; then
+ ocf_log err "could not archive container '$CONTAINER', error code: $?"
+ return
fi
- ocf_log notice "Cleaning up inactive container, ${CONTAINER}."
- ocf_run podman rm -v "$CONTAINER"
- rc=$?
- if [ $rc -ne 0 ]; then
- if [ $rc -eq 2 ]; then
- if podman inspect --format '{{.State.Status}}' "$CONTAINER" | grep -wq "stopping"; then
- ocf_log err "Inactive container ${CONTAINER} is stuck in 'stopping' state. Force-remove it."
- ocf_run podman rm -f "$CONTAINER"
- rc=$?
- fi
- fi
- # due to a podman bug (rhbz#1841485), sometimes a stopped
- # container can still be associated with Exec sessions, in
- # which case the "podman rm" has to be forced
- execids=$(podman inspect "$CONTAINER" --format '{{len .ExecIDs}}')
- if [ "$execids" -ne "0" ]; then
- ocf_log warn "Inactive container ${CONTAINER} has lingering exec sessions. Force-remove it."
- ocf_run podman rm -f "$CONTAINER"
- rc=$?
+
+ # archive corresponding etcd configuration files
+ local files_to_archive=""
+ for file in "$OCF_RESKEY_authfile" "$POD_MANIFEST_COPY" "$ETCD_CONFIGURATION_FILE"; do
+ if [ -f "$file" ]; then
+ files_to_archive="$files_to_archive $file"
+ else
+ ocf_log warn "file '$file' is missing and won't be archived"
fi
+ done
+
+ if [ -z "$files_to_archive" ]; then
+ ocf_log warn "could not find any file to archive."
+ return
+ fi
+
+ # NOTE: tar will override any existing archive as wanted
+ # shellcheck disable=SC2086
+ if ! ocf_run tar --create --verbose --gzip --file "$ETCD_BACKUP_FILE" $files_to_archive; then
+ ocf_log warn "container archived successfully, but configuration backup failed (error: $?). Container debugging available, but without matching configuration files"
+ else
+ ocf_log info "container configuration also archived in '$ETCD_BACKUP_FILE'"
fi
- return $rc
}
# Correctly wraps an ipv6 in [] for url otherwise use return normal ipv4 address.
@@ -365,6 +397,7 @@ attribute_node_ip()
local attribute="node_ip"
local ip_addr name
+ # TODO: We can retrieve both the local and peer IP addresses from this map, which eliminates the need to use CIB to share them between nodes
for node in $(echo "$OCF_RESKEY_node_ip_map" | sed "s/\s//g;s/;/ /g"); do
name=$(echo "$node" | cut -d: -f1)
# ignore other nodes
@@ -375,7 +408,7 @@ attribute_node_ip()
done
if [ -z "$ip_addr" ]; then
- ocf_log err "ip address was empty when querying (getent ahosts) for hostname: $(hostname -f)"
+ ocf_log err "could not get local ip address from node_ip_map: '$OCF_RESKEY_node_ip_map'"
return 1
fi
@@ -384,9 +417,9 @@ attribute_node_ip()
echo "$ip_addr"
;;
update)
- if ! crm_attribute --type nodes --node "$NODENAME" --name "$attribute" --update "$value"; then
+ if ! crm_attribute --type nodes --node "$NODENAME" --name "$attribute" --update "$ip_addr"; then
rc="$?"
- ocf_log err "could not set $attribute to $value, error code: $rc"
+ ocf_log err "could not set $attribute to $ip_addr, error code: $rc"
return "$rc"
fi
;;
@@ -428,6 +461,48 @@ get_env_from_manifest() {
echo "$env_var_value"
}
+# etcd configuration file expects duration to be expressed in nanoseconds
+convert_duration_in_nanoseconds() {
+ local duration=$1
+ local value unit nanoseconds
+
+ if [ -z "$duration" ]; then
+ ocf_log err "convert_duration_in_nanoseconds: no duration provided"
+ return 1
+ fi
+
+ if ! echo "$duration" | grep -qE '^[0-9]+[numµ]?s$'; then
+ ocf_log err "convert_duration_in_nanoseconds: invalid duration format \"$duration\". Expected format: <number><unit> where unit is one of s, ms, us, µs, ns"
+ return 1
+ fi
+
+ # Extract numeric value and unit from duration string
+ value=$(echo "$duration" | sed 's/[^0-9]*$//')
+ unit=$(echo "$duration" | sed 's/^[0-9]*//')
+
+ case "$unit" in
+ ns)
+ nanoseconds=$value
+ ;;
+ us|µs)
+ nanoseconds=$((value * 1000))
+ ;;
+ ms)
+ nanoseconds=$((value * 1000000))
+ ;;
+ s)
+ nanoseconds=$((value * 1000000000))
+ ;;
+ *)
+ # this should not happen as the input is already validated
+ ocf_log err "convert_duration_in_nanoseconds: unknown duration unit \"$unit\""
+ return 1
+ ;;
+ esac
+
+ echo "$nanoseconds"
+}
+
prepare_env() {
local name ip ipurl standalone_node
@@ -457,9 +532,14 @@ prepare_env() {
ETCDCTL_API=$(get_env_from_manifest "ETCDCTL_API")
ETCD_CIPHER_SUITES=$(get_env_from_manifest "ETCD_CIPHER_SUITES")
ETCD_DATA_DIR=$(get_env_from_manifest "ETCD_DATA_DIR")
+ if [ ! -d "$ETCD_DATA_DIR" ]; then
+ ocf_log err "could not find data-dir at path \"$ETCD_DATA_DIR\""
+ return "$OCF_ERR_ARGS"
+ else
+ ocf_log info "using data-dir: $ETCD_DATA_DIR"
+ fi
ETCD_ELECTION_TIMEOUT=$(get_env_from_manifest "ETCD_ELECTION_TIMEOUT")
ETCD_ENABLE_PPROF=$(get_env_from_manifest "ETCD_ENABLE_PPROF")
- ETCD_EXPERIMENTAL_MAX_LEARNERS=$(get_env_from_manifest "ETCD_EXPERIMENTAL_MAX_LEARNERS")
ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION=$(get_env_from_manifest "ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION")
ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERVAL=$(get_env_from_manifest "ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERVAL")
ETCD_HEARTBEAT_INTERVAL=$(get_env_from_manifest "ETCD_HEARTBEAT_INTERVAL")
@@ -475,6 +555,62 @@ prepare_env() {
LISTEN_METRICS_URLS="0.0.0.0"
}
+
+generate_etcd_configuration() {
+ if is_force_new_cluster; then
+ # The embedded newline is required for correct YAML formatting.
+ FORCE_NEW_CLUSTER_CONFIG="force-new-cluster: true
+force-new-cluster-bump-amount: 1000000000"
+ else
+ FORCE_NEW_CLUSTER_CONFIG="force-new-cluster: false"
+ fi
+
+ cat > "$ETCD_CONFIGURATION_FILE" << EOF
+logger: zap
+log-level: info
+snapshot-count: 10000
+name: $NODENAME
+data-dir: $ETCD_DATA_DIR
+$FORCE_NEW_CLUSTER_CONFIG
+socket-reuse-address: $ETCD_SOCKET_REUSE_ADDRESS
+election-timeout: $ETCD_ELECTION_TIMEOUT
+enable-pprof: $ETCD_ENABLE_PPROF
+heartbeat-interval: $ETCD_HEARTBEAT_INTERVAL
+quota-backend-bytes: $ETCD_QUOTA_BACKEND_BYTES
+initial-advertise-peer-urls: "$NODEIPURL:2380"
+listen-peer-urls: "$(ip_url ${LISTEN_PEER_URLS}):2380"
+listen-client-urls: "$(ip_url ${LISTEN_CLIENT_URLS}):2379,unixs://${NODEIP}:0"
+initial-cluster: $ETCD_INITIAL_CLUSTER
+initial-cluster-state: $ETCD_INITIAL_CLUSTER_STATE
+client-transport-security:
+ cert-file: /etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-serving-${NODENAME}.crt
+ key-file: /etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-serving-${NODENAME}.key
+ client-cert-auth: true
+ trusted-ca-file: $SERVER_CACERT
+peer-transport-security:
+ cert-file: $ETCD_PEER_CERT
+ key-file: $ETCD_PEER_KEY
+ client-cert-auth: true
+ trusted-ca-file: $SERVER_CACERT
+advertise-client-urls: "$NODEIPURL:2379"
+listen-metrics-urls: "$(ip_url ${LISTEN_METRICS_URLS}):9978"
+metrics: extensive
+experimental-initial-corrupt-check: true
+experimental-max-learners: 1
+experimental-warning-apply-duration: $(convert_duration_in_nanoseconds "$ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION")
+experimental-watch-progress-notify-interval: $(convert_duration_in_nanoseconds "$ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERVAL")
+EOF
+
+ {
+ if [ -n "$ETCD_CIPHER_SUITES" ]; then
+ echo "cipher-suites:"
+ echo "$ETCD_CIPHER_SUITES" | tr ',' '\n' | while read -r cipher; do
+ echo " - \"$cipher\""
+ done
+ fi
+ } >> "$ETCD_CONFIGURATION_FILE"
+}
+
archive_data_folder()
{
# TODO: use etcd snapshots
@@ -634,7 +770,7 @@ add_member_as_learner()
local endpoint_url=$(ip_url $(attribute_node_ip get))
local peer_url=$(ip_url $member_ip)
- ocf_log info "add $member_name ($member_ip) to the member list as learner"
+ ocf_log info "add $member_name ($member_ip, $endpoint_url) 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)
rc=$?
if [ $rc -ne 0 ]; then
@@ -1104,18 +1240,18 @@ compare_revision()
peer_revision=$(attribute_node_revision_peer)
if [ "$revision" = "" ] || [ "$revision" = "null" ] || [ "$peer_revision" = "" ] || [ "$peer_revision" = "null" ]; then
- ocf_log err "could not compare revisions: $NODENAME local revision: $revision, peer revision: $peer_revision"
+ ocf_log err "could not compare revisions: '$NODENAME' local revision='$revision', peer revision='$peer_revision'"
return "$OCF_ERR_GENERIC"
fi
if [ "$revision" -gt "$peer_revision" ]; then
- ocf_log info "$NODENAME revision: $revision is newer than peer revision: $peer_revision"
+ ocf_log info "$NODENAME revision: '$revision' is newer than peer revision: '$peer_revision'"
echo "newer"
elif [ "$revision" -eq "$peer_revision" ]; then
- ocf_log info "$NODENAME revision: $revision is equal to peer revision: $peer_revision"
+ ocf_log info "$NODENAME revision: '$revision' is equal to peer revision: '$peer_revision'"
echo "equal"
else
- ocf_log info "$NODENAME revision: $revision is older than peer revision: $peer_revision"
+ ocf_log info "$NODENAME revision: '$revision' is older than peer revision: '$peer_revision'"
echo "older"
fi
return "$OCF_SUCCESS"
@@ -1144,6 +1280,100 @@ ensure_pod_manifest_exists()
return "$OCF_SUCCESS"
}
+filter_pod_manifest() {
+ # Remove pod-version related fields from POD manifest
+ local pod_manifest="$1"
+ local temporary_file
+ local jq_filter='del(.metadata.labels.revision) | .spec.containers[] |= ( .env |= map(select( .name != "ETCD_STATIC_POD_VERSION" ))) | .spec.volumes |= map( select( .name != "resource-dir" ))'
+
+ if ! temporary_file=$(mktemp); then
+ ocf_log err "could not create temporary file for '$pod_manifest', error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
+ if ! jq "$jq_filter" "$pod_manifest" > "$temporary_file"; then
+ ocf_log err "could not remove pod version related data from '$pod_manifest', error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
+ echo "$temporary_file"
+}
+
+can_reuse_container() {
+ # Decide whether to reuse the existing container or create a new one based on etcd pod manifest changes.
+ # NOTE: explicitly ignore POD version and POD version related data, as the content might be the same even if the revision number has changed.
+ local cp_rc
+ local diff_rc
+ local filtered_original_pod_manifest
+ local filtered_copy_pod_manifest
+
+
+ # If the container does not exist it cannot be reused
+ if ! container_exists; then
+ OCF_RESKEY_reuse=0
+ return "$OCF_SUCCESS"
+ fi
+
+ # If the manifest copy doesn't exist, we need a new container.
+ if [ ! -f "$POD_MANIFEST_COPY" ]; then
+ ocf_log info "a working copy of $OCF_RESKEY_pod_manifest was not found. A new etcd container will be created."
+ OCF_RESKEY_reuse=0
+ return "$OCF_SUCCESS"
+ fi
+
+ if ! filtered_original_pod_manifest=$(filter_pod_manifest "$OCF_RESKEY_pod_manifest"); then
+ return $OCF_ERR_GENERIC
+ fi
+ if ! filtered_copy_pod_manifest=$(filter_pod_manifest "$POD_MANIFEST_COPY"); then
+ return $OCF_ERR_GENERIC
+ fi
+
+ ocf_log info "comparing $OCF_RESKEY_pod_manifest with local copy $POD_MANIFEST_COPY"
+ ocf_run diff -s "$filtered_original_pod_manifest" "$filtered_copy_pod_manifest"
+ diff_rc="$?"
+ # clean up temporary files
+ rm -f "$filtered_original_pod_manifest" "$filtered_copy_pod_manifest"
+ case "$diff_rc" in
+ 0)
+ ocf_log info "Reusing the existing etcd container"
+ OCF_RESKEY_reuse=1
+ ;;
+ 1)
+ ocf_log info "Etcd pod manifest changes detected: creating a new etcd container to apply the changes"
+ if ! ocf_run cp -p "$OCF_RESKEY_pod_manifest" "$POD_MANIFEST_COPY"; then
+ cp_rc="$?"
+ ocf_log err "Could not create a working copy of $OCF_RESKEY_pod_manifest, rc: $cp_rc"
+ return "$OCF_ERR_GENERIC"
+ fi
+ ocf_log info "A working copy of $OCF_RESKEY_pod_manifest was created"
+ OCF_RESKEY_reuse=0
+ ;;
+ *)
+ ocf_log err "Could not check if etcd pod manifest has changed, diff rc: $diff_rc"
+ return "$OCF_ERR_GENERIC"
+ ;;
+ esac
+
+ return "$OCF_SUCCESS"
+}
+
+ensure_pod_manifest_copy_exists() {
+ local cp_rc
+
+ if [ -f "$POD_MANIFEST_COPY" ]; then
+ return "$OCF_SUCCESS"
+ fi
+
+ # If the manifest copy doesn't exist, create it and ensure a new container.
+ if ! ocf_run cp -p "$OCF_RESKEY_pod_manifest" "$POD_MANIFEST_COPY"; then
+ cp_rc="$?"
+ ocf_log err "Could not create a working copy of $OCF_RESKEY_pod_manifest, rc: $cp_rc"
+ return "$OCF_ERR_GENERIC"
+ fi
+
+ ocf_log info "a new working copy of $OCF_RESKEY_pod_manifest was created"
+
+ return "$OCF_SUCCESS"
+}
+
podman_start()
{
local cid
@@ -1173,6 +1403,13 @@ podman_start()
return $OCF_ERR_GENERIC
fi
+ # check if the container has already started
+ podman_simple_status
+ if [ $? -eq $OCF_SUCCESS ]; then
+ ocf_log info "the '$CONTAINER' has already started. Nothing to do"
+ return "$OCF_SUCCESS"
+ fi
+
if ! ensure_pod_manifest_exists; then
ocf_exit_reason "could not find etcd pod manifest ($OCF_RESKEY_pod_manifest)"
return "$OCF_ERR_GENERIC"
@@ -1186,8 +1423,9 @@ podman_start()
ocf_log info "static pod was running: start normally"
else
if is_force_new_cluster; then
- ocf_log notice "$NODENAME marked to force-new-cluster"
+ ocf_log notice "'$NODENAME' marked to force-new-cluster"
else
+ ocf_log info "'$NODENAME' is not marked to force-new-cluster"
# When the local agent starts, we can infer the cluster state by counting
# how many agents are starting or already active:
# - 1 active agent: it's the peer (we are just starting)
@@ -1195,6 +1433,7 @@ podman_start()
# - 0 active agents, 2 starting: both agents are starting simultaneously
local active_resources_count
active_resources_count=$(echo "$OCF_RESKEY_CRM_meta_notify_active_resource" | wc -w)
+ ocf_log info "found '$active_resources_count' active etcd resources (meta notify environment variable: '$OCF_RESKEY_CRM_meta_notify_active_resource')"
case "$active_resources_count" in
1)
if [ "$(attribute_learner_node get)" = "$(get_peer_node_name)" ]; then
@@ -1205,17 +1444,17 @@ podman_start()
fi
;;
0)
+ # count how many agents are starting now
+ local start_resources_count
+ 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"
fi
-
- # count how many agents are starting now
- local start_resources_count
- start_resources_count=$(echo "$OCF_RESKEY_CRM_meta_notify_start_resource" | wc -w)
-
case "$start_resources_count" in
1)
ocf_log debug "peer not starting: ensure we can start a new cluster"
@@ -1231,6 +1470,7 @@ podman_start()
fi
;;
2)
+ # TODO: can we start "normally", regardless the revisions, if the container-id is the same on both nodes?
ocf_log info "peer starting"
if [ "$revision_compare_result" = "newer" ]; then
set_force_new_cluster
@@ -1263,7 +1503,7 @@ podman_start()
fi
podman_create_mounts
- local run_opts="--detach --name=${CONTAINER}"
+ local run_opts="--detach --name=${CONTAINER} --replace"
run_opts="$run_opts --oom-score-adj=${OCF_RESKEY_oom}"
@@ -1297,61 +1537,59 @@ podman_start()
archive_data_folder
fi
- prepare_env
+ ocf_log info "check for changes in pod manifest to decide if the container should be reused or replaced"
+ if ! can_reuse_container ; then
+ rc="$?"
+ ocf_log err "could not determine etcd container reuse strategy, rc: $rc"
+ return "$rc"
+ fi
+
+ # Archive current container and its configuration before creating
+ # new configuration files.
+ if ! ocf_is_true "$OCF_RESKEY_reuse"; then
+ # Log archive container failures but don't block, as the priority
+ # is ensuring the etcd container starts successfully.
+ archive_current_container
+ fi
+
+ if ! ensure_pod_manifest_copy_exists; then
+ return $OCF_ERR_GENERIC
+ fi
+
+ if ! prepare_env; then
+ ocf_log err "Could not prepare environment for podman, error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
+
+ if ! generate_etcd_configuration; then
+ ocf_log err "Could not generate etcd configuration, error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
- # add etcd-specific opts
run_opts="$run_opts \
- --network=host \
- -v /etc/kubernetes/static-pod-resources/etcd-certs:/etc/kubernetes/static-pod-certs \
- -v /var/lib/etcd:/var/lib/etcd \
- --env ALL_ETCD_ENDPOINTS=$ALL_ETCD_ENDPOINTS \
- --env ETCD_CIPHER_SUITES=$ETCD_CIPHER_SUITES \
- --env ETCD_DATA_DIR=$ETCD_DATA_DIR \
- --env ETCD_ELECTION_TIMEOUT=$ETCD_ELECTION_TIMEOUT \
- --env ETCD_ENABLE_PPROF=$ETCD_ENABLE_PPROF \
- --env ETCD_EXPERIMENTAL_MAX_LEARNERS=$ETCD_EXPERIMENTAL_MAX_LEARNERS \
- --env ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION=$ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION \
- --env ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERVAL=$ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERVAL \
- --env ETCD_HEARTBEAT_INTERVAL=$ETCD_HEARTBEAT_INTERVAL \
- --env ETCD_INITIAL_CLUSTER=$ETCD_INITIAL_CLUSTER \
- --env ETCD_INITIAL_CLUSTER_STATE=$ETCD_INITIAL_CLUSTER_STATE \
- --env ETCD_NAME=$NODENAME \
- --env ETCD_QUOTA_BACKEND_BYTES=$ETCD_QUOTA_BACKEND_BYTES \
- --env ETCD_SOCKET_REUSE_ADDRESS=$ETCD_SOCKET_REUSE_ADDRESS \
- --env ETCDCTL_API=$ETCDCTL_API \
- --env ETCDCTL_CACERT=$SERVER_CACERT \
- --env ETCDCTL_CERT=$ETCD_PEER_CERT \
- --env ETCDCTL_KEY=$ETCD_PEER_KEY \
- --authfile=$OCF_RESKEY_authfile \
- --security-opt label=disable"
+ --network=host \
+ -v /etc/kubernetes/static-pod-resources/etcd-certs:/etc/kubernetes/static-pod-certs \
+ -v /var/lib/etcd:/var/lib/etcd \
+ --env ETCDCTL_API=$ETCDCTL_API \
+ --env ETCDCTL_CACERT=$SERVER_CACERT \
+ --env ETCDCTL_CERT=$ETCD_PEER_CERT \
+ --env ETCDCTL_KEY=$ETCD_PEER_KEY \
+ --authfile=$OCF_RESKEY_authfile \
+ --security-opt label=disable"
if [ -n "$OCF_RESKEY_run_opts" ]; then
run_opts="$run_opts $OCF_RESKEY_run_opts"
fi
- OCF_RESKEY_run_cmd="$OCF_RESKEY_run_cmd --logger=zap \
- --log-level=info \
- --experimental-initial-corrupt-check=true \
- --snapshot-count=10000 \
- --initial-advertise-peer-urls=$NODEIPURL:2380 \
- --cert-file=/etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-serving-${NODENAME}.crt \
- --key-file=/etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-serving-${NODENAME}.key \
- --trusted-ca-file=$SERVER_CACERT \
- --client-cert-auth=true \
- --peer-cert-file=$ETCD_PEER_CERT \
- --peer-key-file=$ETCD_PEER_KEY \
- --peer-trusted-ca-file=$SERVER_CACERT \
- --peer-client-cert-auth=true \
- --advertise-client-urls=$NODEIPURL:2379 \
- --listen-client-urls=$(ip_url ${LISTEN_CLIENT_URLS}):2379,unixs://${NODEIP}:0 \
- --listen-peer-urls=$(ip_url ${LISTEN_PEER_URLS}):2380 \
- --metrics=extensive \
- --listen-metrics-urls=$(ip_url ${LISTEN_METRICS_URLS}):9978"
- if [ -n "$OCF_RESKEY_run_cmd_opts" ]; then
- OCF_RESKEY_run_cmd="$OCF_RESKEY_run_cmd $OCF_RESKEY_run_cmd_opts"
+ if [ -f "$ETCD_CONFIGURATION_FILE" ]; then
+ ocf_log info "using etcd configuration file: $ETCD_CONFIGURATION_FILE"
+ else
+ ocf_log err "could not find $ETCD_CONFIGURATION_FILE"
+ return "$OCF_ERR_GENERIC"
fi
- if is_force_new_cluster; then
- OCF_RESKEY_run_cmd="$OCF_RESKEY_run_cmd --force-new-cluster"
+ OCF_RESKEY_run_cmd="$OCF_RESKEY_run_cmd --config-file=$ETCD_CONFIGURATION_FILE"
+ if [ -n "$OCF_RESKEY_run_cmd_opts" ]; then
+ OCF_RESKEY_run_cmd="$OCF_RESKEY_run_cmd $OCF_RESKEY_run_cmd_opts"
fi
if [ "$OCF_RESKEY_image" = "$OCF_RESKEY_image_default" ]; then
@@ -1377,9 +1615,7 @@ podman_start()
ocf_log info "starting existing container $CONTAINER."
ocf_run podman start "$CONTAINER"
else
- # make sure any previous container matching our container name is cleaned up first.
- # we already know at this point it wouldn't be running
- remove_container
+ ocf_log info "starting new container $CONTAINER."
run_new_container "$run_opts" "$OCF_RESKEY_image" "$OCF_RESKEY_run_cmd"
if [ $? -eq 125 ]; then
return $OCF_ERR_GENERIC
@@ -1439,7 +1675,6 @@ podman_stop()
local rc
podman_simple_status
if [ $? -eq $OCF_NOT_RUNNING ]; then
- remove_container
ocf_log info "could not leave members list: etcd container not running"
return $OCF_SUCCESS
fi
@@ -1475,7 +1710,7 @@ podman_stop()
ocf_run podman kill "$CONTAINER"
rc=$?
else
- ocf_log debug "waiting $timeout second[s] before killing container"
+ ocf_log info "waiting $timeout second[s] before killing container"
ocf_run podman stop -t="$timeout" "$CONTAINER"
rc=$?
# on stop, systemd will automatically delete any transient
@@ -1496,11 +1731,6 @@ podman_stop()
fi
fi
- if ! remove_container; then
- ocf_exit_reason "Failed to remove stopped container, ${CONTAINER}, based on image, ${OCF_RESKEY_image}."
- return $OCF_ERR_GENERIC
- fi
-
return $OCF_SUCCESS
}
@@ -1532,6 +1762,7 @@ podman_validate()
check_binary oc
check_binary podman
check_binary jq
+ check_binary tar
if [ -z "$OCF_RESKEY_node_ip_map" ]; then
ocf_exit_reason "'node_ip_map' option is required"
@@ -1589,6 +1820,9 @@ else
fi
CONTAINER=$OCF_RESKEY_name
+POD_MANIFEST_COPY="${OCF_RESKEY_config_location}/pod.yaml"
+ETCD_CONFIGURATION_FILE="${OCF_RESKEY_config_location}/config.yaml"
+ETCD_BACKUP_FILE="${OCF_RESKEY_backup_location}/config-previous.tar.gz"
# 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,193 @@
From 11cdff8c886c72c83c26e48e46a8620c06e4c2f0 Mon Sep 17 00:00:00 2001
From: E Hila <ehila@redhat.com>
Date: Tue, 9 Sep 2025 06:06:12 -0400
Subject: [PATCH] OCPBUGS-60977: podman-etcd: wrap ipv6 address in brackets for
attribute_node_ip (#2068)
When trying to determine the node ip address we need to make sure we account for ipv6 and dualstack deployments, and accordingly wrap ipv6 in brackets so it correctly resolves. Since the node ip mapping is provided by the controller, we parse out the IP address of the node from there and use a helper function for building URLs with ports to correctly use brackets for ipv6 ip addresses.
Signed-off-by: ehila <ehila@redhat.com>
---
heartbeat/podman-etcd | 77 ++++++++++++++++++++++++++++---------------
1 file changed, 51 insertions(+), 26 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 884b7c579..4969fbaaf 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -347,21 +347,41 @@ remove_container()
return $rc
}
+# Correctly wraps an ipv6 in [] for url otherwise use return normal ipv4 address.
+ip_url() {
+ local ip_addr=$1
+ local value
+ if echo "$ip_addr" | grep -q ":" ; then
+ value="[$ip_addr]"
+ else
+ value="$ip_addr"
+ fi
+ echo "https://$value"
+}
+
attribute_node_ip()
{
local action="$1"
local attribute="node_ip"
- local value
+ local ip_addr name
- if ! value=$(ip -brief addr show "$OCF_RESKEY_nic" | awk '{gsub("/.*", "", $3); print $3}'); then
- rc=$?
- ocf_log err "could not get node ip, error code: $rc"
- return "$rc"
+ for node in $(echo "$OCF_RESKEY_node_ip_map" | sed "s/\s//g;s/;/ /g"); do
+ name=$(echo "$node" | cut -d: -f1)
+ # ignore other nodes
+ if [ "$name" != "$NODENAME" ]; then
+ continue
+ fi
+ ip_addr=$(echo "$node" | cut -d: -f2-) # Grab everything after the first : this covers ipv4/ipv6
+ done
+
+ if [ -z "$ip_addr" ]; then
+ ocf_log err "ip address was empty when querying (getent ahosts) for hostname: $(hostname -f)"
+ return 1
fi
case "$action" in
get)
- echo "$value"
+ echo "$ip_addr"
;;
update)
if ! crm_attribute --type nodes --node "$NODENAME" --name "$attribute" --update "$value"; then
@@ -409,26 +429,28 @@ get_env_from_manifest() {
}
prepare_env() {
- local name ip standalone_node
+ local name ip ipurl standalone_node
NODEIP="$(attribute_node_ip get)"
+ NODEIPURL=$(ip_url $NODEIP)
if is_force_new_cluster; then
- ALL_ETCD_ENDPOINTS="https://$NODEIP:2379"
+ ALL_ETCD_ENDPOINTS="$NODEIPURL:2379"
ETCD_INITIAL_CLUSTER_STATE="new"
- ETCD_INITIAL_CLUSTER="$NODENAME=https://$NODEIP:2380"
+ ETCD_INITIAL_CLUSTER="$NODENAME=$NODEIPURL:2380"
else
ETCD_INITIAL_CLUSTER_STATE="existing"
for node in $(echo "$OCF_RESKEY_node_ip_map" | sed "s/\s//g;s/;/ /g"); do
- name=$(echo "$node" | awk -F":" '{print $1}')
- ip=$(echo "$node" | awk -F":" '{print $2}')
+ name=$(echo "$node" | cut -d: -f1)
+ ip=$(echo "$node" | cut -d: -f2-) # Grab everything after the first : this covers ipv4/ipv6
+ ipurl="$(ip_url $ip)"
if [ -z "$name" ] || [ -z "$ip" ]; then
ocf_exit_reason "name or ip missing for 1 or more nodes"
exit $OCF_ERR_CONFIGURED
fi
- [ -z "$ALL_ETCD_ENDPOINTS" ] && ALL_ETCD_ENDPOINTS="https://$ip:2379" || ALL_ETCD_ENDPOINTS="$ALL_ETCD_ENDPOINTS,https://$ip:2379"
- [ -z "$ETCD_INITIAL_CLUSTER" ] && ETCD_INITIAL_CLUSTER="$name=https://$ip:2380" || ETCD_INITIAL_CLUSTER="$ETCD_INITIAL_CLUSTER,$name=https://$ip:2380"
+ [ -z "$ALL_ETCD_ENDPOINTS" ] && ALL_ETCD_ENDPOINTS="$ipurl:2379" || ALL_ETCD_ENDPOINTS="$ALL_ETCD_ENDPOINTS,$ipurl:2379"
+ [ -z "$ETCD_INITIAL_CLUSTER" ] && ETCD_INITIAL_CLUSTER="$name=$ipurl:2380" || ETCD_INITIAL_CLUSTER="$ETCD_INITIAL_CLUSTER,$name=$ipurl:2380"
done
fi
@@ -609,9 +631,11 @@ add_member_as_learner()
local rc
local member_name=$1
local member_ip=$2
+ local endpoint_url=$(ip_url $(attribute_node_ip get))
+ 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="https://$(attribute_node_ip get):2379" member add "$member_name" --peer-urls="https://$member_ip:2380" --learner)
+ out=$(podman exec "${CONTAINER}" etcdctl --endpoints="$endpoint_url:2379" member add "$member_name" --peer-urls="$peer_url:2380" --learner)
rc=$?
if [ $rc -ne 0 ]; then
ocf_log err "could not add $member_name as learner, error code: $rc"
@@ -806,14 +830,15 @@ get_peer_node_name() {
get_all_etcd_endpoints() {
for node in $(echo "$OCF_RESKEY_node_ip_map" | sed "s/\s//g;s/;/ /g"); do
- name=$(echo "$node" | awk -F":" '{print $1}')
- ip=$(echo "$node" | awk -F":" '{print $2}')
+ name=$(echo "$node" | cut -d: -f1)
+ ip=$(echo "$node" | cut -d: -f2-) # Grab everything after the first : this covers ipv4/ipv6
+ ipurl="$(ip_url $ip)"
if [ -z "$name" ] || [ -z "$ip" ]; then
ocf_exit_reason "name or ip missing for 1 or more nodes"
exit $OCF_ERR_CONFIGURED
fi
- [ -z "$ALL_ETCD_ENDPOINTS" ] && ALL_ETCD_ENDPOINTS="https://$ip:2379" || ALL_ETCD_ENDPOINTS="$ALL_ETCD_ENDPOINTS,https://$ip:2379"
+ [ -z "$ALL_ETCD_ENDPOINTS" ] && ALL_ETCD_ENDPOINTS="$ipurl:2379" || ALL_ETCD_ENDPOINTS="$ALL_ETCD_ENDPOINTS,$ipurl:2379"
done
echo "$ALL_ETCD_ENDPOINTS"
}
@@ -831,7 +856,7 @@ get_member_list_json() {
# Get the list of members visible to the current node
local this_node_endpoint
- this_node_endpoint="https://$(attribute_node_ip get):2379"
+ this_node_endpoint="$(ip_url $(attribute_node_ip get)):2379"
podman exec "${CONTAINER}" etcdctl member list --endpoints="$this_node_endpoint" -w json
}
@@ -886,14 +911,14 @@ check_peers()
# ]
# }
for node in $(echo "$OCF_RESKEY_node_ip_map" | sed "s/\s//g;s/;/ /g"); do
- name=$(echo "$node" | awk -F":" '{print $1}')
+ name=$(echo "$node" | cut -d: -f1)
# do not check itself
if [ "$name" = "$NODENAME" ]; then
continue
fi
# Check by IP instead of Name since "learner" members appear only in peerURLs, not by Name.
- ip=$(echo "$node" | awk -F":" '{print $2}')
+ ip=$(echo "$node" | cut -d: -f2-) # Grab everything after the first : this covers ipv4/ipv6
id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .peerURLs | map(test(\"$ip\")) | any).ID")
if [ -z "$id" ]; then
ocf_log info "$name is not in the members list"
@@ -1307,7 +1332,7 @@ podman_start()
--log-level=info \
--experimental-initial-corrupt-check=true \
--snapshot-count=10000 \
- --initial-advertise-peer-urls=https://${NODEIP}:2380 \
+ --initial-advertise-peer-urls=$NODEIPURL:2380 \
--cert-file=/etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-serving-${NODENAME}.crt \
--key-file=/etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-serving-${NODENAME}.key \
--trusted-ca-file=$SERVER_CACERT \
@@ -1316,11 +1341,11 @@ podman_start()
--peer-key-file=$ETCD_PEER_KEY \
--peer-trusted-ca-file=$SERVER_CACERT \
--peer-client-cert-auth=true \
- --advertise-client-urls=https://${NODEIP}:2379 \
- --listen-client-urls=https://${LISTEN_CLIENT_URLS}:2379,unixs://${NODEIP}:0 \
- --listen-peer-urls=https://${LISTEN_PEER_URLS}:2380 \
+ --advertise-client-urls=$NODEIPURL:2379 \
+ --listen-client-urls=$(ip_url ${LISTEN_CLIENT_URLS}):2379,unixs://${NODEIP}:0 \
+ --listen-peer-urls=$(ip_url ${LISTEN_PEER_URLS}):2380 \
--metrics=extensive \
- --listen-metrics-urls=https://${LISTEN_METRICS_URLS}:9978"
+ --listen-metrics-urls=$(ip_url ${LISTEN_METRICS_URLS}):9978"
if [ -n "$OCF_RESKEY_run_cmd_opts" ]; then
OCF_RESKEY_run_cmd="$OCF_RESKEY_run_cmd $OCF_RESKEY_run_cmd_opts"
fi
@@ -1430,7 +1455,7 @@ podman_stop()
ocf_log info "last member. Not leaving the member list"
else
ocf_log info "leaving members list as member with ID $member_id"
- endpoint="https://$(attribute_node_ip get):2379"
+ endpoint="$(ip_url $(attribute_node_ip get)):2379"
if ! ocf_run podman exec "$CONTAINER" etcdctl member remove "$member_id" --endpoints="$endpoint"; then
rc=$?
ocf_log err "error leaving members list, error code: $rc"

View File

@ -0,0 +1,481 @@
From dbc0d2647d73bed986bf7208df33f092f56e8523 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Thu, 25 Sep 2025 14:23:20 +0200
Subject: [PATCH] db2: use reintegration flag to avoid race condition on
cluster reintegration, and removed FAL, as it's no longer needed
---
heartbeat/db2 | 306 ++++++++++++++++++++++++++++++++------------------
1 file changed, 197 insertions(+), 109 deletions(-)
diff --git a/heartbeat/db2 b/heartbeat/db2
index fe1d9b892..83020fc70 100755
--- a/heartbeat/db2
+++ b/heartbeat/db2
@@ -37,6 +37,13 @@
: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat}
. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs
+# Use runuser if available for SELinux.
+if [ -x "/sbin/runuser" ]; then
+ SU="runuser"
+else
+ SU="su"
+fi
+
# Parameter defaults
OCF_RESKEY_instance_default=""
@@ -55,11 +62,12 @@ OCF_RESKEY_dbpartitionnum_default="0"
: ${OCF_RESKEY_admin=${OCF_RESKEY_admin_default}}
: ${OCF_RESKEY_dbpartitionnum=${OCF_RESKEY_dbpartitionnum_default}}
+POSIX_UNICODE_LOCALE="C.UTF-8"
#######################################################################
db2_usage() {
- echo "db2 start|stop|monitor|promote|demote|notify|validate-all|meta-data"
+ echo "db2 start|stop|monitor|promote|demote|validate-all|meta-data"
}
db2_meta_data() {
@@ -162,7 +170,6 @@ The number of the partition (DBPARTITIONNUM) to be managed.
<action name="stop" timeout="120s"/>
<action name="promote" timeout="120s"/>
<action name="demote" timeout="120s"/>
-<action name="notify" timeout="10s"/>
<action name="monitor" depth="0" timeout="60s" interval="20s"/>
<action name="monitor" depth="0" timeout="60s" role="Promoted" interval="22s"/>
<action name="validate-all" timeout="5s"/>
@@ -273,7 +280,18 @@ master_score()
# Run the given command as db2 instance user
#
runasdb2() {
- su $instance -c ". $db2profile; $*"
+ $SU $instance -c ". $db2profile; $*"
+}
+
+#
+# Run the given command as db2 instance user using $SU
+# We run this function as opposed to runasdb2 whenever we have to issue commands
+# that leave processes running on the system, such as db2start
+# We do not want these processes to hog the resources as they were run with elevated privileges
+#
+runasdb2_session() {
+ # Override db2profile with unicode locale is required to maintain compatibility with unicode CODEPAGE
+ $SU "$instance" -c "ksh -c '. $db2profile; export LC_ALL="$POSIX_UNICODE_LOCALE"; export LANG="$POSIX_UNICODE_LOCALE"; $*'"
}
#
@@ -294,48 +312,6 @@ logasdb2() {
}
-#
-# maintain the fal (first active log) attribute
-# db2_fal_attrib DB {set val|get}
-#
-db2_fal_attrib() {
- local db=$1
- local attr val rc id node member me
-
- attr=db2hadr_${instance}_${db}_fal
-
- case "$2" in
- set)
- me=$(ocf_local_nodename)
-
- # loop over all member nodes and set attribute
- crm_node -l |
- while read id node member
- do
- [ "$member" = member -a "$node" != "$me" ] || continue
- crm_attribute -l forever --node=$node -n $attr -v "$3"
- rc=$?
- ocf_log info "DB2 instance $instance($db2node/$db: setting attrib for FAL to $FIRST_ACTIVE_LOG @ $node"
- [ $rc != 0 ] && break
- done
- ;;
-
- get)
- crm_attribute -l forever -n $attr -G --quiet 2>&1
- rc=$?
- if ! ocf_is_true "$OCF_RESKEY_CRM_meta_notify" && [ $rc != 0 ]
- then
- ocf_log warn "DB2 instance $instance($db2node/$db: can't retrieve attribute $attr, are you sure notifications are enabled ?"
- fi
- ;;
-
- *)
- exit $OCF_ERR_CONFIGURED
- esac
-
- return $rc
-}
-
#
# unfortunately a first connect after a crash may need several minutes
# for some internal cleanup stuff in DB2.
@@ -429,6 +405,42 @@ db2_check_config_compatibility() {
}
+#
+# Start HADR as standby.
+#
+# Parameters
+# 1 - Calling function
+# 2 - Calling functions line number
+#
+# Return codes:
+# 0 - Start as standby successful
+# 1 - Start as standby failed
+#
+reintegrateAsStandby() {
+ db=$1
+ reint_attr="db2hadr-${inst1}_${inst2}_${db}_reint"
+ ocf_log info "$__OCF_ACTION: $LINENO: reintegrateAsStandby called by $2 at $3. Attempting to reintegrate $db as standby."
+ if output=$(runasdb2_session "db2 start hadr on db $db as standby"); then
+ rc=0
+ ocf_log info "$__OCF_ACTION: $LINENO: Db2 database $instance($db2node)/$db started/activated"
+ else
+ case $output in
+ SQL1777N*)
+ # SQL1777N: HADR is already started in given state.
+ ocf_log info "$__OCF_ACTION: $LINENO: $output"
+ rc=0
+ ;;
+
+ *)
+ rc=1
+ ocf_log err "$__OCF_ACTION: $LINENO: Unable to reintegrate Db2 database $instance($db2node)/$db. Please reintegrate manually: $output, return with rc=$rc"
+ ;;
+ esac
+ fi
+ crm_attribute -n "$reint_attr" -N "$local_host" -v "0" -l forever
+ return $rc
+}
+
#
# Start instance and DB.
# Standard mode is through "db2 activate" in order to start in previous
@@ -478,6 +490,8 @@ db2_start() {
for db in $dblist
do
+ reint_attr="db2hadr-${inst1}_${inst2}_${db}_reint"
+
# sets HADR_ROLE HADR_TIMEOUT HADR_PEER_WINDOW FIRST_ACTIVE_LOG
db2_get_cfg $db || return $?
@@ -488,20 +502,13 @@ db2_start() {
if [ $HADR_ROLE = PRIMARY ]
then
- local master_fal
-
- # communicate our FAL to other nodes the might start concurrently
- db2_fal_attrib $db set $FIRST_ACTIVE_LOG
-
- # ignore false positive:
- # error: Can't use > in [ ]. Escape it or use [[..]]. [SC2073]
- # see https://github.com/koalaman/shellcheck/issues/691
- # shellcheck disable=SC2073
- if master_fal=$(db2_fal_attrib $db get) && [ "$master_fal" '>' $FIRST_ACTIVE_LOG ]
- then
+ cib_value=$(crm_attribute -n "$reint_attr" -N "$local_host" -G | awk -v FS=' value=' '{print $2}')
+ ocf_log info "$__OCF_ACTION: $LINENO: CIB attribute $reint_attr is set to '$cib_value'"
+ if [ "$cib_value" = "1" ]; then
ocf_log info "DB2 database $instance($db2node)/$db is Primary and outdated, starting as secondary"
start_cmd="db2 start hadr on db $db as standby"
HADR_ROLE=STANDBY
+ standby_reintegration=1
fi
fi
@@ -511,27 +518,65 @@ db2_start() {
[ $HADR_ROLE != STANDBY ] && db2_run_connect $db &
else
case $output in
- SQL1490W*|SQL1494W*|SQL1497W*|SQL1777N*)
- ocf_log info "DB2 database $instance($db2node)/$db already activated: $output"
+ SQL1490W* | SQL1494W* | SQL1497W* | SQL1777N*)
+ # SQL1490W Activate database is successful, however, the database has already been activated on one or more nodes.
+ # SQL1494W Activate database is successful, however, there is already a connection to the database.
+ # SQL1497W Activate/Deactivate database was successful, however, an error occurred on some nodes.
+ # SQL1777N HADR is already started.
+
+ ocf_log info "$__OCF_ACTION: $LINENO: $instance: $db2node: $db: The database is already activated: $output"
;;
- SQL1768N*"Reason code = \"7\""*)
- ocf_log err "DB2 database $instance($db2node)/$db is a Primary and the Standby is down"
- ocf_log err "Possible split brain ! Manual intervention required."
+ SQL1768N*"Reason code = \"7\""*)
+ rc="$OCF_ERR_GENERIC"
+
+ ocf_log err "$__OCF_ACTION: $LINENO: $instance: $db2node: $db: The database is a Primary and the Standby is down"
+ ocf_log err "Possible split brain! Manual intervention required."
ocf_log err "If this DB is outdated use \"db2 start hadr on db $db as standby\""
- ocf_log err "If this DB is the surviving primary use \"db2 start hadr on db $db as primary by force\""
+ ocf_log err "If this DB is the surviving primary use \"db2 start hadr on db $db as primary by force\". db2_start() exit with rc=$rc."
- # might be the Standby is not yet there
- # might be a timing problem because "First active log" is delayed
- # on the next start attempt we might succeed when FAL was advanced
- # might be manual intervention is required
- # ... so let pacemaker give it another try and we will succeed then
- return $OCF_ERR_GENERIC
+ # let pacemaker give it another try and we will succeed then
+ return "$rc"
;;
- *)
- ocf_log err "DB2 database $instance($db2node)/$db didn't start: $output"
- return $OCF_ERR_GENERIC
+ SQL1776N*"Reason code = \"6\""*)
+ # SQL1776N The command cannot be issued on an HADR database.
+ # Reason code 6:
+ # This database is an old primary database. It cannot be started
+ # because the standby has become the new primary through forced
+ # takeover.
+
+ rc="$OCF_ERR_GENERIC"
+ ocf_log err "$__OCF_ACTION: $LINENO: Db2 database $instance($db2node)/$db didn't start: $output, return with rc=$rc"
+ ocf_log err "$__OCF_ACTION: $LINENO: This database is an old primary database. Trying start again as standby"
+
+ start_cmd="db2 start hadr on db $db as standby"
+ if output=$(runasdb2_session "$start_cmd"); then
+ rc="$OCF_SUCCESS"
+ ocf_log info "$__OCF_ACTION: $LINENO: Db2 database $instance($db2node)/$db started/activated"
+ else
+ case $output in
+ SQL1777N*)
+ # SQL1777N: HADR is already started.
+ ocf_log info "$__OCF_ACTION: $LINENO: $output"
+ rc="$OCF_SUCCESS"
+ ;;
+
+ *)
+ rc="$OCF_ERR_GENERIC"
+ ocf_log err "$__OCF_ACTION: $LINENO: Unable to reintegrate Db2 database $instance($db2node)/$db. Please reintegrate manually: $output, return with rc=$rc"
+ ;;
+ esac
+ fi
+
+ return "$rc"
+ ;;
+
+ *)
+ rc="$OCF_ERR_GENERIC"
+ ocf_log err "$__OCF_ACTION: $LINENO: $instance: $db2node: $db: The database didn't start: $output, db2_start() exit with rc=$rc."
+ return "$rc"
+ ;;
esac
fi
done
@@ -539,6 +584,15 @@ db2_start() {
# come here with success
# Even if we are a db2 Primary pacemaker requires start to end up in slave mode
echo SLAVE > $STATE_FILE
+
+ # Unset primary failover attribute as host was successfully reintegrated as standby
+ if [ "$standby_reintegration" = "1" ]; then
+ for db in $dblist; do
+ reint_attr="db2hadr-${inst1}_${inst2}_${db}_reint"
+ crm_attribute -n "$reint_attr" -N "$local_host" -v "0" -l forever
+ done
+ fi
+
return $OCF_SUCCESS
}
@@ -737,7 +791,7 @@ db2_monitor_retry() {
#
# Monitor the db
-# And as side effect set crm_master / FAL attribute
+# And as side effect set crm_master
#
db2_monitor() {
local CMD output hadr db
@@ -754,6 +808,22 @@ db2_monitor() {
for db in $dblist
do
+ reint_attr="db2hadr-${inst1}_${inst2}_${db}_reint"
+
+ #Check for the reintegration file, then set the flag if it exists and delete the file
+ if [ -e "/tmp/$reint_attr" ] && [ -n "$remote_host" ]; then
+ #The file exist, try to set the reintegration attribute
+ crm_attribute -n "$reint_attr" -N "$remote_host" -v "1" -l forever
+ cib_value=$(crm_attribute -n "$reint_attr" -N "$remote_host" -G | awk -v FS=' value=' '{print $2}')
+
+ if [ "$cib_value" = "1" ]; then
+ ocf_log info "$__OCF_ACTION: $LINENO: CIB attribute $reint_attr is set to '$cib_value', reintegration flag file will now be deleted."
+ rm -f "/tmp/$reint_attr"
+ else
+ ocf_log err "$__OCF_ACTION: $LINENO: $instance: $db2node: $db: The reintegration flag file exists, but its attribute failed to set."
+ fi
+ fi
+
hadr=$(db2_hadr_status $db)
rc=$?
ocf_log debug "Monitor: DB2 database $instance($db2node)/$db has HADR status $hadr"
@@ -804,6 +874,14 @@ db2_monitor() {
;;
STANDBY/*PEER/*|Standby/*Peer)
+ # If db is in standby peer, then it has already reintegrated.
+ # If the reintegrate flag is still set, remove it
+ cib_value=$(crm_attribute -n "$reint_attr" -N "$local_host" -G | awk -v FS=' value=' '{print $2}')
+ if [ "$cib_value" = "1" ]; then
+ ocf_log info "$__OCF_ACTION: $LINENO: Reintegrate flag detected for $db, but it has already reintegrated as standby. Removing reintegration flag."
+ crm_attribute -n "$reint_attr" -N "$local_host" -v "0" -l forever
+ fi
+
master_score -v 8000 -l reboot
;;
@@ -812,6 +890,34 @@ db2_monitor() {
master_score -D -l reboot
;;
+ Down/Off)
+ # If db is a deactivated primary and it has a reintegration flag, then reintegrate as standby.
+ cib_value=$(crm_attribute -n "$reint_attr" -N "$local_host" -G | awk -v FS=' value=' '{print $2}')
+ if [ "$cib_value" = "1" ]; then
+ output=$(runasdb2 "db2 get db cfg for $db" | grep 'HADR database role' | awk '{print $5}')
+ if [ "PRIMARY" = "$output" ]; then
+ ocf_log info "$__OCF_ACTION: $LINENO: $instance: $db2node: $db: Database is deactivated with Primary role and the reintegration flag is set. Role: $output, Reintegration flag: $reint_attr = $cib_value"
+ # Reintegrate as the standby database.
+ if reintegrateAsStandby "$db" 'db2_monitor' $LINENO; then
+ ocf_log info "$__OCF_ACTION: $LINENO: $instance: $db2node: $db: The database reintegration succeeded."
+ # Setting slave state here will cause rc to be OCF_SUCCESS below.
+ ocf_log info "$__OCF_ACTION: $LINENO: $instance: $db2node: $db: Echoing SLAVE into $STATE_FILE"
+ echo SLAVE >"$STATE_FILE"
+ # Update master score to reflect standby state.
+ master_score -v 8000 -l reboot
+ else
+ ocf_log err "$__OCF_ACTION: $LINENO: $instance: $db2node: $db: The database reintegration failed."
+ return "$OCF_ERR_GENERIC"
+ fi
+ fi
+ else
+ rc="$OCF_NOT_RUNNING"
+ ocf_log info "$__OCF_ACTION: $LINENO: $instance: $db2node: $db: The database has HADR status $hadr."
+ ocf_log info "$__OCF_ACTION: $LINENO: $instance: $db2node: $db: db2_monitor() exit with rc=$rc."
+ return "$rc"
+ fi
+ ;;
+
*)
return $OCF_ERR_GENERIC
esac
@@ -875,8 +981,6 @@ db2_promote() {
# update pacemaker's view
echo MASTER > $STATE_FILE
- # turn the log so we rapidly get a new FAL
- logasdb2 "db2 archive log for db $db"
return $OCF_SUCCESS
fi
@@ -914,26 +1018,6 @@ db2_demote() {
return $?
}
-#
-# handle pre start notification
-# We record our first active log on the other nodes.
-# If two primaries come up after a crash they can safely determine who is
-# the outdated one.
-#
-db2_notify() {
- local node
-
- # only interested in pre-start
- [ $OCF_RESKEY_CRM_meta_notify_type = pre \
- -a $OCF_RESKEY_CRM_meta_notify_operation = start ] || return $OCF_SUCCESS
-
- # gets FIRST_ACTIVE_LOG
- db2_get_cfg $dblist || return $?
-
- db2_fal_attrib $dblist set $FIRST_ACTIVE_LOG || return $OCF_ERR_GENERIC
- exit $OCF_SUCCESS
-}
-
########
# Main #
########
@@ -947,50 +1031,54 @@ case "$__OCF_ACTION" in
db2_usage
exit $OCF_SUCCESS
;;
+esac
+local_host=$(ocf_local_nodename)
+inst1=$(echo "$OCF_RESKEY_instance" | cut -d"," -f1)
+inst2=$(echo "$OCF_RESKEY_instance" | cut -d"," -f2)
+host1=$(crm_node -l | sort | awk '{print $2;}' | sed -n 1p)
+
+if [ "$host1" = "$local_host" ]; then
+ remote_host=$(crm_node -l | sort | awk '{print $2;}' | sed -n 2p)
+else
+ remote_host="$host1"
+fi
+
+db2_validate; validate_rc=$?
+
+case "$__OCF_ACTION" in
start)
- db2_validate
db2_start || exit $?
db2_monitor
- exit $?
;;
stop)
- db2_validate
db2_stop
- exit $?
;;
promote)
- db2_validate
db2_promote
- exit $?
;;
demote)
- db2_validate
db2_demote
- exit $?
;;
notify)
- db2_validate
- db2_notify
- exit $?
+ ocf_log debug "notify-action has been DEPRECATED, and should be removed"
;;
monitor)
- db2_validate
db2_monitor_retry
- exit $?
;;
validate-all)
- db2_validate
- exit $?
+ exit $validate_rc
;;
*)
db2_usage
exit $OCF_ERR_UNIMPLEMENTED
esac
+
+exit $?

View File

@ -0,0 +1,684 @@
From 90e4402ee81ee107d9e7b99e6908289b00a39a4c Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Tue, 20 May 2025 09:59:54 +0200
Subject: [PATCH] portblock: add nftables and multi-state support
---
heartbeat/ocf-binaries.in | 1 +
heartbeat/portblock | 389 ++++++++++++++++++++++++++++++--------
2 files changed, 307 insertions(+), 83 deletions(-)
diff --git a/heartbeat/ocf-binaries.in b/heartbeat/ocf-binaries.in
index e11ae1d6f..aed6eecae 100644
--- a/heartbeat/ocf-binaries.in
+++ b/heartbeat/ocf-binaries.in
@@ -26,6 +26,7 @@ export PATH
: ${GETENT:=getent}
: ${GREP:=grep}
: ${IFCONFIG:=ifconfig}
+: ${NFTABLES:=nft}
: ${IPTABLES:=iptables}
## for cases that are known not to be serviceable with iptables-nft impl.
: ${IPTABLES_LEGACY:=iptables-legacy}
diff --git a/heartbeat/portblock b/heartbeat/portblock
index 9b4f5db39..1eea28a6d 100755
--- a/heartbeat/portblock
+++ b/heartbeat/portblock
@@ -1,9 +1,10 @@
#!/bin/sh
#
-# portblock: iptables temporary portblocking control
+# portblock: iptables temporary portblocking control
#
# Author: Sun Jiang Dong (initial version)
# Philipp Reisner (per-IP filtering)
+# Sebastian Baszczyj (nftables code)
#
# License: GNU General Public License (GPL)
#
@@ -23,6 +24,7 @@
. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs
# Defaults
+OCF_RESKEY_firewall_default="auto"
OCF_RESKEY_protocol_default=""
OCF_RESKEY_portno_default=""
OCF_RESKEY_direction_default="in"
@@ -32,6 +34,7 @@ OCF_RESKEY_reset_local_on_unblock_stop_default="false"
OCF_RESKEY_tickle_dir_default=""
OCF_RESKEY_sync_script_default=""
+: ${OCF_RESKEY_firewall=${OCF_RESKEY_firewall_default}}
: ${OCF_RESKEY_protocol=${OCF_RESKEY_protocol_default}}
: ${OCF_RESKEY_portno=${OCF_RESKEY_portno_default}}
: ${OCF_RESKEY_direction=${OCF_RESKEY_direction_default}}
@@ -43,13 +46,17 @@ OCF_RESKEY_sync_script_default=""
#######################################################################
CMD=`basename $0`
TICKLETCP=$HA_BIN/tickle_tcp
+TABLE="portblock"
+# Promotion scores
+SCORE_UNPROMOTED=5
+SCORE_PROMOTED=10
usage()
{
cat <<END >&2
- usage: $CMD {start|stop|status|monitor|meta-data|validate-all}
+ usage: $CMD {start|stop|promote|demote|status|monitor|meta-data|validate-all}
- $CMD is used to temporarily block ports using iptables.
+ $CMD is used to temporarily block ports using nftables or iptables.
It can be used to blackhole a port before bringing
up an IP address, and enable it after a service is started.
@@ -86,8 +93,8 @@ usage()
NOTE: iptables is Linux-specific.
An additional feature in the portblock RA is the tickle ACK function
- enabled by specifying the tickle_dir parameter. The tickle ACK
- triggers the clients to faster reconnect their TCP connections to the
+ enabled by specifying the tickle_dir parameter. The tickle ACK
+ triggers the clients to faster reconnect their TCP connections to the
fail-overed server.
Please note that this feature is often used for the floating IP fail-
@@ -95,7 +102,7 @@ usage()
It doesn't support the cluster alias IP scenario.
When using the tickle ACK function, in addition to the normal usage
- of portblock RA, the parameter tickle_dir must be specified in the
+ of portblock RA, the parameter tickle_dir must be specified in the
action=unblock instance of the portblock resources.
For example, you may stack resources like below:
portblock action=block
@@ -103,18 +110,18 @@ usage()
portblock action=unblock tickle_dir=/tickle/state/dir
If you want to tickle all the TCP connections which connected to _one_
- floating IP but different ports, no matter how many portblock resources
- you have defined, you should enable tickles for _one_ portblock
+ floating IP but different ports, no matter how many portblock resources
+ you have defined, you should enable tickles for _one_ portblock
resource(action=unblock) only.
-
- The tickle_dir is a location which stores the established TCP
- connections. It can be a shared directory(which is cluster-visible to
+
+ The tickle_dir is a location which stores the established TCP
+ connections. It can be a shared directory(which is cluster-visible to
all nodes) or a local directory.
If you use the shared directory, you needn't do any other things.
If you use the local directory, you must also specify the sync_script
paramater. We recommend you to use csync2 as the sync_script.
- For example, if you use the local directory /tmp/tickle as tickle_dir,
- you could setup the csync2 as the csync2 documentation says and
+ For example, if you use the local directory /tmp/tickle as tickle_dir,
+ you could setup the csync2 as the csync2 documentation says and
configure your /etc/csync2/csync2.cfg like:
group ticklegroup {
host node1;
@@ -137,17 +144,29 @@ meta_data() {
<version>1.0</version>
<longdesc lang="en">
-Resource script for portblock. It is used to temporarily block ports
-using iptables. In addition, it may allow for faster TCP reconnects
-for clients on failover. Use that if there are long lived TCP
-connections to an HA service. This feature is enabled by setting the
-tickle_dir parameter and only in concert with action set to unblock.
+Resource script for portblock. It is used to block ports using nftables
+or iptables. In addition, it may allow for faster TCP reconnects for
+clients on failover. Use that if there are long lived TCP connections
+to an HA service. This feature is enabled by setting the tickle_dir
+parameter and only in concert with action set to unblock.
Note that the tickle ACK function is new as of version 3.0.2 and
hasn't yet seen widespread use.
+
+In Promotable mode, the promote action unblocks the ports on the Promoted node
+and blocks the ports on the Unpromoted node(s) when action=block, and vice versa
+when action=unblock.
</longdesc>
<shortdesc lang="en">Block and unblocks access to TCP and UDP ports</shortdesc>
<parameters>
+<parameter name="firewall" unique="0" required="0">
+<longdesc lang="en">
+Firewall to use, e.g. auto (default), nft, or iptables.
+</longdesc>
+<shortdesc lang="en">Firewall</shortdesc>
+<content type="string" default="${OCF_RESKEY_firewall_default}" />
+</parameter>
+
<parameter name="protocol" unique="0" required="1">
<longdesc lang="en">
The protocol used to be blocked/unblocked.
@@ -167,6 +186,9 @@ The port number used to be blocked/unblocked.
<parameter name="action" unique="0" required="1">
<longdesc lang="en">
The action (block/unblock) to be done on the protocol::portno.
+
+In Promotable mode it is the initial action for start/demote actions,
+and the promote action will change the state to the opposite.
</longdesc>
<shortdesc lang="en">action</shortdesc>
<content type="string" default="${OCF_RESKEY_action_default}" />
@@ -202,7 +224,7 @@ The IP address used to be blocked/unblocked.
<parameter name="tickle_dir" unique="0" required="0">
<longdesc lang="en">
-The shared or local directory (_must_ be absolute path) which
+The shared or local directory (_must_ be absolute path) which
stores the established TCP connections.
</longdesc>
<shortdesc lang="en">Tickle directory</shortdesc>
@@ -236,6 +258,8 @@ If "both" is used, both the incoming and outgoing ports are blocked.
<actions>
<action name="start" timeout="20s" />
<action name="stop" timeout="20s" />
+<action name="promote" timeout="10s"/>
+<action name="demote" timeout="10s"/>
<action name="status" depth="0" timeout="10s" interval="10s" />
<action name="monitor" depth="0" timeout="10s" interval="10s" />
<action name="meta-data" timeout="5s" />
@@ -269,11 +293,17 @@ active_grep_pat()
# iptables 1.8.9 briefly broke the output format, returning the
# numeric protocol value instead of a string. Support both variants.
if [ "$1" = "tcp" ]; then
- local prot="(tcp|6)"
+ local prot="\(tcp\|6\)"
else
- local prot="(udp|17)"
+ local prot="\(udp\|17\)"
+ fi
+ if [ "$FIREWALL" = "nft" ]; then
+ local ip
+ [ "$4" = "s" ] && ip=$src || ip=$dst
+ echo "^\s\+ip $4addr ${ip} $1 $4port $2 ct state { established, related, new } drop$"
+ else
+ echo "^DROP${w}${prot}${w}--${w}${src}${w}${dst}${w}multiport${w}${4}ports${w}${2}$"
fi
- echo "^DROP${w}${prot}${w}--${w}${src}${w}${dst}${w}multiport${w}${4}ports${w}${2}$"
}
#chain_isactive {udp|tcp} portno,portno ip chain
@@ -281,7 +311,11 @@ chain_isactive()
{
[ "$4" = "OUTPUT" ] && ds="s" || ds="d"
PAT=$(active_grep_pat "$1" "$2" "$3" "$ds")
- $IPTABLES $wait -n -L "$4" | grep -qE "$PAT"
+ if [ "$FIREWALL" = "nft" ]; then
+ $NFTABLES list chain inet $TABLE $4 2>&1 | grep -q "$PAT"
+ else
+ $IPTABLES $wait -n -L "$4" | grep -q "$PAT"
+ fi
}
# netstat -tn and ss -Htn, split on whitespace and colon,
@@ -372,8 +406,8 @@ SayInactive()
ocf_log debug "$CMD DROP rule [$*] is inactive"
}
-#IptablesStatus {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
-IptablesStatus() {
+#PortStatus {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
+PortStatus() {
local rc
rc=$OCF_ERR_GENERIC
is_active=0
@@ -397,6 +431,17 @@ IptablesStatus() {
rc=$OCF_NOT_RUNNING
;;
esac
+ elif ocf_is_ms; then
+ case $5 in
+ block)
+ SayInactive $*
+ rc=$OCF_NOT_RUNNING
+ ;;
+ *)
+ SayActive $*
+ rc=$OCF_SUCCESS
+ ;;
+ esac
else
case $5 in
block)
@@ -424,29 +469,56 @@ IptablesStatus() {
return $rc
}
-#DoIptables {-I|-D} {udp|tcp} portno,portno ip chain
-DoIptables()
+#NftDelete chain proto {d|s} ip ports
+NftDelete()
+{
+ local chain=$1 proto=$2 ds=$3 ip=$(echo "$4" | sed "s#/#\\\/#") ports=$5
+ # Try both single port and multi-port patterns for handle search
+ local handles=$($NFTABLES -a list chain inet $TABLE $chain 2>/dev/null | awk "/\s+ip ${ds}addr $ip $proto ${ds}port $ports/"' {printf "%d ", $NF}')
+ for handle in $handles; do
+ ocf_log debug "NftDelete: Deleting $chain rule with handle $handle"
+ nft delete rule inet $TABLE $chain handle $handle || {
+ ocf_exit_reason "NftDelete: Failed to delete $chain handle $handle."
+ return $OCF_ERR_GENERIC
+ }
+ done
+}
+
+#DoPort {-I|-D} {udp|tcp} portno,portno ip chain
+DoPort()
{
op=$1 proto=$2 ports=$3 ip=$4 chain=$5
active=0; chain_isactive "$proto" "$ports" "$ip" "$chain" && active=1
- want_active=0; [ "$op" = "-I" ] && want_active=1
+ want_active=0; { [ "$op" = "insert" ] || [ "$op" = "-I" ] && want_active=1; }
ocf_log debug "active: $active want_active: $want_active"
if [ $active -eq $want_active ] ; then
: Chain already in desired state
else
[ "$chain" = "OUTPUT" ] && ds="s" || ds="d"
- $IPTABLES $wait "$op" "$chain" -p "$proto" -${ds} "$ip" -m multiport --${ds}ports "$ports" -j DROP
+ case $FIREWALL in
+ nft)
+ if [ "$op" = "insert" ]; then
+ $NFTABLES $op rule inet $TABLE $chain ip ${ds}addr $ip $proto ${ds}port $ports ct state { established, related, new } drop
+ elif [ "$op" = "delete" ]; then
+ NftDelete "$chain" "$proto" "$ds" "$ip" "$ports"
+ fi
+ ;;
+ iptables)
+ $IPTABLES $wait "$op" "$chain" -p "$proto" -${ds} "$ip" -m multiport --${ds}ports "$ports" -j DROP
+ ;;
+ esac
fi
}
-#IptablesBLOCK {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
-IptablesBLOCK()
+#PortBLOCK {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
+PortBLOCK()
{
local rc_in=0
local rc_out=0
+ [ "$FIREWALL" = "nft" ] && action="insert" || action="-I"
if [ "$4" = "in" ] || [ "$4" = "both" ]; then
local try_reset=false
- if [ "$1/$5/$__OCF_ACTION" = tcp/unblock/stop ] &&
+ if [ "$1/$5/$__OCF_ACTION" = tcp/unblock/stop ] &&
ocf_is_true $reset_local_on_unblock_stop
then
try_reset=true
@@ -456,73 +528,168 @@ IptablesBLOCK()
then
: OK -- chain already active
else
- if $try_reset ; then
- $IPTABLES $wait -I OUTPUT -p "$1" -s "$3" -m multiport --sports "$2" -j REJECT --reject-with tcp-reset
- tickle_local
- fi
- $IPTABLES $wait -I INPUT -p "$1" -d "$3" -m multiport --dports "$2" -j DROP
- rc_in=$?
- if $try_reset ; then
- $IPTABLES $wait -D OUTPUT -p "$1" -s "$3" -m multiport --sports "$2" -j REJECT --reject-with tcp-reset
+ if [ "$FIREWALL" = "nft" ]; then
+ if $try_reset ; then
+ $NFTABLES insert rule inet $TABLE OUTPUT ip saddr $3 $1 sport $2 ct state { established, related, new } reject with tcp reset
+ tickle_local
+ fi
+ $NFTABLES insert rule inet $TABLE INPUT ip daddr $3 $1 dport $2 ct state { established, related, new } drop
+ rc_in=$?
+ if $try_reset ; then
+ NftDelete "OUTPUT" "$1" "s" "$ports"
+ fi
+ else
+ if $try_reset ; then
+ $IPTABLES $wait -I OUTPUT -p "$1" -s "$3" -m multiport --sports "$2" -j REJECT --reject-with tcp-reset
+ tickle_local
+ fi
+ $IPTABLES $wait -I INPUT -p "$1" -d "$3" -m multiport --dports "$2" -j DROP
+ rc_in=$?
+ if $try_reset ; then
+ $IPTABLES $wait -D OUTPUT -p "$1" -s "$3" -m multiport --sports "$2" -j REJECT --reject-with tcp-reset
+ fi
fi
fi
fi
if [ "$4" = "out" ] || [ "$4" = "both" ]; then
- DoIptables -I "$1" "$2" "$3" OUTPUT
+ DoPort "$action" "$1" "$2" "$3" OUTPUT
rc_out=$?
fi
[ $rc_in -gt $rc_out ] && return $rc_in || return $rc_out
}
-#IptablesUNBLOCK {udp|tcp} portno,portno ip {in|out|both}
-IptablesUNBLOCK()
+#PortUNBLOCK {udp|tcp} portno,portno ip {in|out|both}
+PortUNBLOCK()
{
+ local action
+ [ "$FIREWALL" = "nft" ] && action="delete" || action="-D"
if [ "$4" = "in" ] || [ "$4" = "both" ]; then
- DoIptables -D "$1" "$2" "$3" INPUT
+ DoPort "$action" "$1" "$2" "$3" INPUT
fi
if [ "$4" = "out" ] || [ "$4" = "both" ]; then
- DoIptables -D "$1" "$2" "$3" OUTPUT
+ DoPort "$action" "$1" "$2" "$3" OUTPUT
fi
return $?
}
-#IptablesStart {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
-IptablesStart()
+#PortStart {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
+PortStart()
{
ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" start
+
+ if [ "$FIREWALL" = "nft" ]; then
+ $NFTABLES add table inet $TABLE || {
+ ocf_exit_reason "Failed to create nftables table $TABLE"
+ return $OCF_ERR_GENERIC
+ }
+ ocf_log debug "Created nftables table $TABLE"
+
+ $NFTABLES add chain inet $TABLE INPUT { type filter hook input priority 0\; } || {
+ ocf_exit_reason "Failed to create INPUT chain"
+ return $OCF_ERR_GENERIC
+ }
+ ocf_log debug "Created INPUT chain"
+
+ $NFTABLES add chain inet $TABLE OUTPUT { type filter hook output priority 0\; } || {
+ ocf_exit_reason "Failed to create OUTPUT chain"
+ return $OCF_ERR_GENERIC
+ }
+ ocf_log debug "Created OUTPUT chain"
+ fi
+
case $5 in
- block) IptablesBLOCK "$@";;
+ block) PortBLOCK "$@"
+ rc=$?
+ ;;
unblock)
- IptablesUNBLOCK "$@"
+ PortUNBLOCK "$@"
rc=$?
tickle_remote
#ignore run_tickle_tcp exit code!
- return $rc
;;
- *) usage; return 1;
+ *) usage; return $OCF_ERR_CONFIGURED ;
esac
- return $?
+ ocf_is_ms && ocf_promotion_score -v $SCORE_UNPROMOTED -N $nodename
+
+ return $rc
}
-#IptablesStop {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
-IptablesStop()
+#PortStop {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
+PortStop()
{
ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" stop
+
case $5 in
- block) IptablesUNBLOCK "$@";;
+ block) PortUNBLOCK "$@"
+ rc=$?
+ ;;
unblock)
save_tcp_connections
- IptablesBLOCK "$@"
+ PortBLOCK "$@"
+ rc=$?
;;
- *) usage; return 1;;
+ *) usage; return $OCF_ERR_CONFIGURED ;;
esac
+ ocf_is_ms && ocf_promotion_score -D -N $nodename
+
+ return $rc
+}
+
+PortPromote() {
+ PortStatus "$@"
+ rc=$?
+ if [ $rc -eq $OCF_SUCCESS ] && [ $promotion_score -eq $SCORE_PROMOTED ]; then
+ ocf_log info "Promote: resource already promoted."
+ return $rc
+ elif [ $rc -ne $OCF_SUCCESS ] && [ $rc -ne $OCF_NOT_RUNNING ]; then
+ ocf_exit_reason "Promote: PortStatus failed with rc: $rc."
+ return $rc
+ fi
+ case $5 in
+ block) PortBLOCK "$@"
+ rc=$?
+ ;;
+ unblock)
+ PortUNBLOCK "$@"
+ rc=$?
+ tickle_remote
+ #ignore run_tickle_tcp exit code!
+ ;;
+ *) usage; return $OCF_ERR_CONFIGURED ;
+ esac
+ ocf_promotion_score -v $SCORE_PROMOTED -N $nodename
return $?
}
+PortDemote() {
+ PortStatus "$@"
+ rc=$?
+ if [ $rc -eq $OCF_SUCCESS ] && [ $promotion_score -eq $SCORE_UNPROMOTED ]; then
+ ocf_log info "Demote: resource already demoted."
+ return $rc
+ elif [ $rc -ne $OCF_SUCCESS ] && [ $rc -ne $OCF_NOT_RUNNING ]; then
+ ocf_exit_reason "Demote: PortStatus failed with rc: $rc."
+ return $rc
+ fi
+ case $5 in
+ block)
+ save_tcp_connections
+ PortBLOCK "$@"
+ rc=$?
+ ;;
+ unblock) PortUNBLOCK "$@"
+ rc=$?
+ ;;
+ *) usage; return $OCF_ERR_CONFIGURED ;;
+ esac
+ ocf_promotion_score -v $SCORE_UNPROMOTED -N $nodename
+ return $rc
+}
+
#
# Check if the port is valid, this function code is not decent, but works
#
@@ -532,9 +699,15 @@ CheckPort() {
echo $1 | $EGREP -qx '[0-9]+(:[0-9]+)?(,[0-9]+(:[0-9]+)?)*'
}
-IptablesValidateAll()
+PortValidateAll()
{
- check_binary $IPTABLES
+ case $FIREWALL in
+ nft)
+ check_binary $IPTABLES ;;
+ iptables)
+ check_binary $NFTABLES ;;
+ esac
+
case $protocol in
tcp|udp)
;;
@@ -558,17 +731,17 @@ IptablesValidateAll()
fi
if [ ! -d "$OCF_RESKEY_tickle_dir" ]; then
ocf_log err "The tickle dir doesn't exist!"
- exit $OCF_ERR_INSTALLED
+ exit $OCF_ERR_INSTALLED
fi
fi
case $action in
- block|unblock)
+ block|unblock)
;;
- *)
+ *)
ocf_log err "Invalid action $action!"
exit $OCF_ERR_CONFIGURED
- ;;
+ ;;
esac
if ocf_is_true $reset_local_on_unblock_stop; then
@@ -584,6 +757,20 @@ IptablesValidateAll()
return $OCF_SUCCESS
}
+# Detect firewall tool
+detect_firewall_tool() {
+ if have_binary nft; then
+ FIREWALL="nft"
+ ocf_log debug "Detected nftables"
+ elif have_binary iptables; then
+ FIREWALL="iptables"
+ ocf_log debug "Detected iptables"
+ else
+ ocf_exit_reason "No firewall tool available"
+ return $OCF_ERR_CONFIGURED
+ fi
+}
+
if
( [ $# -ne 1 ] )
then
@@ -591,7 +778,7 @@ then
exit $OCF_ERR_ARGS
fi
-case $1 in
+case $__OCF_ACTION in
meta-data) meta_data
exit $OCF_SUCCESS
;;
@@ -605,25 +792,16 @@ esac
if [ -z "$OCF_RESKEY_protocol" ]; then
ocf_log err "Please set OCF_RESKEY_protocol"
exit $OCF_ERR_CONFIGURED
-fi
+fi
if [ -z "$OCF_RESKEY_portno" ]; then
ocf_log err "Please set OCF_RESKEY_portno"
exit $OCF_ERR_CONFIGURED
-fi
+fi
if [ -z "$OCF_RESKEY_action" ]; then
ocf_log err "Please set OCF_RESKEY_action"
exit $OCF_ERR_CONFIGURED
-fi
-
-# iptables v1.4.20+ is required to use -w (wait)
-version=$(iptables -V | grep -oE '[0-9]+[\.0-9]+')
-ocf_version_cmp "$version" "1.4.19.1"
-if [ "$?" -eq "2" ]; then
- wait="-w"
-else
- wait=""
fi
protocol=$OCF_RESKEY_protocol
@@ -632,6 +810,7 @@ direction=$OCF_RESKEY_direction
action=$OCF_RESKEY_action
ip=$OCF_RESKEY_ip
reset_local_on_unblock_stop=$OCF_RESKEY_reset_local_on_unblock_stop
+nodename=$(ocf_local_nodename)
# If "tickle" is enabled, we need to record the list of currently established
@@ -647,21 +826,65 @@ if [ -n "$OCF_RESKEY_tickle_dir" ] ; then
fi
fi
-case $1 in
- start)
- IptablesStart $protocol $portno $ip $direction $action
+case $OCF_RESKEY_firewall in
+ auto)
+ detect_firewall_tool
+ ;;
+ nft|iptables)
+ FIREWALL="$OCF_RESKEY_firewall"
+ ;;
+ *)
+ ocf_exit_reason "Firewall '$OCF_RESKEY_firewall' not supported."
+ exit $OCF_ERR_CONFIGURED
+ ;;
+esac
+
+if [ "$FIREWALL" = "nft" ]; then
+ echo "$portno" | grep -q "," && portno="{ $(echo $portno | sed 's/,/, /g') }"
+elif [ "$FIREWALL" = "iptables" ]; then
+ # iptables v1.4.20+ is required to use -w (wait)
+ version=$(iptables -V | grep -oE '[0-9]+[\.0-9]+')
+ ocf_version_cmp "$version" "1.4.19.1"
+ if [ "$?" -eq "2" ]; then
+ wait="-w"
+ else
+ wait=""
+ fi
+fi
+
+if ocf_is_ms; then
+ promotion_score=$(ocf_promotion_score -G -N $nodename -q 2> /dev/null)
+ if { [ "$__OCF_ACTION" = "monitor" ] && [ "$promotion_score" = "$SCORE_UNPROMOTED" ]; } || [ "$__OCF_ACTION" = "demote" ] || [ "$__OCF_ACTION" = "start" ]; then
+ case $action in
+ block) action="unblock" ;;
+ unblock) action="block" ;;
+ esac
+ fi
+fi
+
+case $__OCF_ACTION in
+ start)
+ PortStart "$protocol" "$portno" "$ip" "$direction" "$action"
+ ;;
+
+ stop)
+ PortStop "$protocol" "$portno" "$ip" "$direction" "$action"
+ ;;
+
+ promote)
+ PortPromote $protocol "$portno" "$ip" "$direction" "$action"
;;
- stop)
- IptablesStop $protocol $portno $ip $direction $action
+ demote)
+ PortDemote "$protocol" "$portno" "$ip" "$direction" "$action"
;;
- status|monitor)
- IptablesStatus $protocol $portno $ip $direction $action
+ status|monitor)
+ PortStatus "$protocol" "$portno" "$ip" "$direction" "$action"
;;
validate-all)
- IptablesValidateAll
+ PortValidateAll
;;
*) usage

View File

@ -0,0 +1,50 @@
From ed2fdbb58d874d3a331425b360d7358b7a5b195e Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Mon, 29 Sep 2025 11:04:40 +0200
Subject: [PATCH] portblock: fix incorrect promotable description
---
heartbeat/portblock | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/heartbeat/portblock b/heartbeat/portblock
index 1eea28a6d..ff162c955 100755
--- a/heartbeat/portblock
+++ b/heartbeat/portblock
@@ -152,11 +152,11 @@ parameter and only in concert with action set to unblock.
Note that the tickle ACK function is new as of version 3.0.2 and
hasn't yet seen widespread use.
-In Promotable mode, the promote action unblocks the ports on the Promoted node
-and blocks the ports on the Unpromoted node(s) when action=block, and vice versa
-when action=unblock.
+In Promotable mode, the promote action unblocks the port(s) on the Promoted node
+and blocks the port(s) on the Unpromoted node(s) when action=unblock, and vice versa
+when action=block.
</longdesc>
-<shortdesc lang="en">Block and unblocks access to TCP and UDP ports</shortdesc>
+<shortdesc lang="en">Blocks and unblocks access to TCP and UDP ports</shortdesc>
<parameters>
<parameter name="firewall" unique="0" required="0">
@@ -187,8 +187,9 @@ The port number used to be blocked/unblocked.
<longdesc lang="en">
The action (block/unblock) to be done on the protocol::portno.
-In Promotable mode it is the initial action for start/demote actions,
-and the promote action will change the state to the opposite.
+In Promotable mode it is the action for the promote action,
+and the opposite action will be used for the start and demote
+actions.
</longdesc>
<shortdesc lang="en">action</shortdesc>
<content type="string" default="${OCF_RESKEY_action_default}" />
@@ -872,7 +873,7 @@ case $__OCF_ACTION in
;;
promote)
- PortPromote $protocol "$portno" "$ip" "$direction" "$action"
+ PortPromote "$protocol" "$portno" "$ip" "$direction" "$action"
;;
demote)

View File

@ -0,0 +1,239 @@
From 344beb18e41442f7af86fa585e4fb970452dc632 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Fri, 10 Oct 2025 16:31:00 +0200
Subject: [PATCH] portblock: add Promoted monitor op, validate-all checks, and
add "method" and "status_check" parameters
- add Promoted monitor op
- run validate-all to catch missing firewall binary and other issues for
non-metadata/usage actions
- add "method" parameter with reject alternative to be able to clear
connections when blocking
- add "status_check" parameter to allow user to specify rule or pseudo
check
---
heartbeat/portblock | 93 ++++++++++++++++++++++++++++++++++++++-------
1 file changed, 79 insertions(+), 14 deletions(-)
diff --git a/heartbeat/portblock b/heartbeat/portblock
index ff162c955..4fc9c2bb8 100755
--- a/heartbeat/portblock
+++ b/heartbeat/portblock
@@ -29,6 +29,8 @@ OCF_RESKEY_protocol_default=""
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=""
@@ -39,6 +41,8 @@ OCF_RESKEY_sync_script_default=""
: ${OCF_RESKEY_portno=${OCF_RESKEY_portno_default}}
: ${OCF_RESKEY_direction=${OCF_RESKEY_direction_default}}
: ${OCF_RESKEY_action=${OCF_RESKEY_action_default}}
+: ${OCF_RESKEY_method=${OCF_RESKEY_method_default}}
+: ${OCF_RESKEY_status_check=${OCF_RESKEY_status_check_default}}
: ${OCF_RESKEY_ip=${OCF_RESKEY_ip_default}}
: ${OCF_RESKEY_reset_local_on_unblock_stop=${OCF_RESKEY_reset_local_on_unblock_stop_default}}
: ${OCF_RESKEY_tickle_dir=${OCF_RESKEY_tickle_dir_default}}
@@ -195,6 +199,26 @@ actions.
<content type="string" default="${OCF_RESKEY_action_default}" />
</parameter>
+<parameter name="method" unique="0" required="0">
+<longdesc lang="en">
+Block method:
+drop: Use DROP rule.
+reject: Use REJECT rule w/conntrack to clear connections when blocking.
+</longdesc>
+<shortdesc lang="en">Block method</shortdesc>
+<content type="string" default="${OCF_RESKEY_method_default}" />
+</parameter>
+
+<parameter name="status_check" unique="0" required="0">
+<longdesc lang="en">
+Status check:
+rule: Check rule.
+pseudo: Check pseudo status when rule is absent.
+</longdesc>
+<shortdesc lang="en">Status check</shortdesc>
+<content type="string" default="${OCF_RESKEY_status_check_default}" />
+</parameter>
+
<parameter name="reset_local_on_unblock_stop" unique="0" required="0">
<longdesc lang="en">
If for some reason the long lived server side TCP sessions won't be cleaned up
@@ -263,6 +287,7 @@ If "both" is used, both the incoming and outgoing ports are blocked.
<action name="demote" timeout="10s"/>
<action name="status" depth="0" timeout="10s" interval="10s" />
<action name="monitor" depth="0" timeout="10s" interval="10s" />
+<action name="monitor" depth="0" timeout="10s" interval="9s" role="Promoted" />
<action name="meta-data" timeout="5s" />
<action name="validate-all" timeout="5s" />
</actions>
@@ -301,9 +326,17 @@ active_grep_pat()
if [ "$FIREWALL" = "nft" ]; then
local ip
[ "$4" = "s" ] && ip=$src || ip=$dst
- echo "^\s\+ip $4addr ${ip} $1 $4port $2 ct state { established, related, new } drop$"
+ if [ "$method" = "DROP" ]; then
+ echo "^\s\+ip${w}$4addr${w}${ip}${w}$1${w}$4port${w}$2${w}ct${w}state${w}{${w}established,${w}related,${w}new${w}}${w}drop$"
+ else
+ echo "^\s\+ip${w}$4addr${w}${ip}${w}$1${w}$4port${w}$2${w}ct${w}state${w}{${w}established,${w}related,${w}new${w}}${w}reject${w}with${w}tcp${w}reset$"
+ fi
else
- echo "^DROP${w}${prot}${w}--${w}${src}${w}${dst}${w}multiport${w}${4}ports${w}${2}$"
+ if [ "$method" = "DROP" ]; then
+ echo "^DROP${w}${prot}${w}--${w}${src}${w}${dst}${w}multiport${w}${4}ports${w}${2}$"
+ else
+ echo "^REJECT${w}${prot}${w}--${w}${src}${w}${dst}${w}multiport${w}${4}ports${w}${2}${w}ctstate${w}NEW,RELATED,ESTABLISHED${w}reject-with${w}tcp-reset$"
+ fi
fi
}
@@ -394,17 +427,17 @@ tickle_local()
SayActive()
{
- ocf_log debug "$CMD DROP rule [$*] is running (OK)"
+ ocf_log debug "$CMD $method rule [$*] is running (OK)"
}
SayConsideredActive()
{
- ocf_log debug "$CMD DROP rule [$*] considered to be running (OK)"
+ ocf_log debug "$CMD $method rule [$*] considered to be running (OK)"
}
SayInactive()
{
- ocf_log debug "$CMD DROP rule [$*] is inactive"
+ ocf_log debug "$CMD $method rule [$*] is inactive"
}
#PortStatus {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
@@ -425,14 +458,18 @@ PortStatus() {
case $5 in
block)
SayActive $*
- rc=$OCF_SUCCESS
+ if [ "$__OCF_ACTION" = "monitor" ] && [ "$promotion_score" = "$SCORE_PROMOTED" ]; then
+ rc=$OCF_RUNNING_MASTER
+ else
+ rc=$OCF_SUCCESS
+ fi
;;
*)
SayInactive $*
rc=$OCF_NOT_RUNNING
;;
esac
- elif ocf_is_ms; then
+ elif [ "$OCF_RESKEY_status_check" = "rule" ]; then
case $5 in
block)
SayInactive $*
@@ -440,7 +477,11 @@ PortStatus() {
;;
*)
SayActive $*
- rc=$OCF_SUCCESS
+ if [ "$__OCF_ACTION" = "monitor" ] && [ "$promotion_score" = "$SCORE_PROMOTED" ]; then
+ rc=$OCF_RUNNING_MASTER
+ else
+ rc=$OCF_SUCCESS
+ fi
;;
esac
else
@@ -499,13 +540,21 @@ DoPort()
case $FIREWALL in
nft)
if [ "$op" = "insert" ]; then
- $NFTABLES $op rule inet $TABLE $chain ip ${ds}addr $ip $proto ${ds}port $ports ct state { established, related, new } drop
+ if [ "$method" = "DROP" ]; then
+ $NFTABLES $op rule inet $TABLE $chain ip ${ds}addr $ip $proto ${ds}port $ports ct state { established, related, new } drop
+ else
+ $NFTABLES $op rule inet $TABLE $chain ip ${ds}addr $ip $proto ${ds}port $ports ct state { established, related, new } reject with tcp reset
+ fi
elif [ "$op" = "delete" ]; then
NftDelete "$chain" "$proto" "$ds" "$ip" "$ports"
fi
;;
iptables)
- $IPTABLES $wait "$op" "$chain" -p "$proto" -${ds} "$ip" -m multiport --${ds}ports "$ports" -j DROP
+ if [ "$method" = "DROP" ]; then
+ $IPTABLES $wait "$op" "$chain" -p "$proto" -${ds} "$ip" -m multiport --${ds}ports "$ports" -j DROP
+ else
+ $IPTABLES $wait "$op" "$chain" -p "$proto" -${ds} "$ip" -m multiport --${ds}ports "$ports" -m conntrack --ctstate NEW,ESTABLISHED,RELATED -j REJECT --reject-with tcp-reset
+ fi
;;
esac
fi
@@ -534,7 +583,11 @@ PortBLOCK()
$NFTABLES insert rule inet $TABLE OUTPUT ip saddr $3 $1 sport $2 ct state { established, related, new } reject with tcp reset
tickle_local
fi
- $NFTABLES insert rule inet $TABLE INPUT ip daddr $3 $1 dport $2 ct state { established, related, new } drop
+ if [ "$method" = "DROP" ]; then
+ $NFTABLES insert rule inet $TABLE INPUT ip daddr $3 $1 dport $2 ct state { established, related, new } drop
+ else
+ $NFTABLES insert rule inet $TABLE INPUT ip daddr $3 $1 dport $2 ct state { established, related, new } reject with tcp reset
+ fi
rc_in=$?
if $try_reset ; then
NftDelete "OUTPUT" "$1" "s" "$ports"
@@ -544,7 +597,11 @@ PortBLOCK()
$IPTABLES $wait -I OUTPUT -p "$1" -s "$3" -m multiport --sports "$2" -j REJECT --reject-with tcp-reset
tickle_local
fi
- $IPTABLES $wait -I INPUT -p "$1" -d "$3" -m multiport --dports "$2" -j DROP
+ if [ "$method" = "DROP" ]; then
+ $IPTABLES $wait -I INPUT -p "$1" -d "$3" -m multiport --dports "$2" -j DROP
+ else
+ $IPTABLES $wait -I INPUT -p "$1" -d "$3" -m multiport --dports "$2" -m conntrack --ctstate NEW,ESTABLISHED,RELATED -j REJECT --reject-with tcp-reset
+ fi
rc_in=$?
if $try_reset ; then
$IPTABLES $wait -D OUTPUT -p "$1" -s "$3" -m multiport --sports "$2" -j REJECT --reject-with tcp-reset
@@ -768,7 +825,7 @@ detect_firewall_tool() {
ocf_log debug "Detected iptables"
else
ocf_exit_reason "No firewall tool available"
- return $OCF_ERR_CONFIGURED
+ exit $OCF_ERR_CONFIGURED
fi
}
@@ -812,6 +869,13 @@ action=$OCF_RESKEY_action
ip=$OCF_RESKEY_ip
reset_local_on_unblock_stop=$OCF_RESKEY_reset_local_on_unblock_stop
nodename=$(ocf_local_nodename)
+case "$OCF_RESKEY_method" in
+ drop) method="DROP" ;;
+ reject) method="REJECT" ;;
+ *) ocf_log err "method: $OCF_RESKEY_method not supported"
+ exit $OCF_ERR_CONFIGURED
+ ;;
+esac
# If "tickle" is enabled, we need to record the list of currently established
@@ -863,6 +927,8 @@ if ocf_is_ms; then
fi
fi
+PortValidateAll
+
case $__OCF_ACTION in
start)
PortStart "$protocol" "$portno" "$ip" "$direction" "$action"
@@ -885,7 +951,6 @@ case $__OCF_ACTION in
;;
validate-all)
- PortValidateAll
;;
*) usage

View File

@ -0,0 +1,25 @@
From 14801ef5feeaa83c226bafd59061824d3f84924a Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Tue, 3 Feb 2026 10:47:23 +0100
Subject: [PATCH] portblock: check correct binary during validate-all
---
heartbeat/portblock | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/heartbeat/portblock b/heartbeat/portblock
index 4fc9c2bb8..20a84c21a 100755
--- a/heartbeat/portblock
+++ b/heartbeat/portblock
@@ -761,9 +761,9 @@ PortValidateAll()
{
case $FIREWALL in
nft)
- check_binary $IPTABLES ;;
- iptables)
check_binary $NFTABLES ;;
+ iptables)
+ check_binary $IPTABLES ;;
esac
case $protocol in

View File

@ -1,19 +0,0 @@
--- a/heartbeat/ocf-shellfuncs.in 2025-09-29 14:01:55.762931795 +0200
+++ b/heartbeat/ocf-shellfuncs.in 2025-09-29 14:09:28.651731793 +0200
@@ -1093,6 +1093,16 @@
echo $1
}
+ocf_promotion_score() {
+ ocf_version_cmp "$OCF_RESKEY_crm_feature_set" "3.10.0"
+ res=$?
+ if [ $res -eq 2 ] || [ $res -eq 1 ] || ! have_binary "crm_master"; then
+ ${HA_SBIN_DIR}/crm_attribute -p ${OCF_RESOURCE_INSTANCE} $@
+ else
+ ${HA_SBIN_DIR}/crm_master -l reboot $@
+ fi
+}
+
__ocf_set_defaults "$@"
: ${OCF_TRACE_RA:=$OCF_RESKEY_trace_ra}

View File

@ -1,362 +0,0 @@
--- a/heartbeat/portblock 2025-09-30 09:52:13.967530030 +0200
+++ b/heartbeat/portblock 2025-09-30 09:52:49.018382542 +0200
@@ -4,6 +4,7 @@
#
# Author: Sun Jiang Dong (initial version)
# Philipp Reisner (per-IP filtering)
+# Sebastian Baszczyj (nftables code)
#
# License: GNU General Public License (GPL)
#
@@ -43,11 +44,15 @@
#######################################################################
CMD=`basename $0`
TICKLETCP=$HA_BIN/tickle_tcp
+TABLE="portblock"
+# Promotion scores
+SCORE_UNPROMOTED=5
+SCORE_PROMOTED=10
usage()
{
cat <<END >&2
- usage: $CMD {start|stop|status|monitor|meta-data|validate-all}
+ usage: $CMD {start|stop|promote|demote|status|monitor|meta-data|validate-all}
$CMD is used to temporarily block ports using iptables.
@@ -86,8 +91,8 @@
NOTE: iptables is Linux-specific.
An additional feature in the portblock RA is the tickle ACK function
- enabled by specifying the tickle_dir parameter. The tickle ACK
- triggers the clients to faster reconnect their TCP connections to the
+ enabled by specifying the tickle_dir parameter. The tickle ACK
+ triggers the clients to faster reconnect their TCP connections to the
fail-overed server.
Please note that this feature is often used for the floating IP fail-
@@ -95,7 +100,7 @@
It doesn't support the cluster alias IP scenario.
When using the tickle ACK function, in addition to the normal usage
- of portblock RA, the parameter tickle_dir must be specified in the
+ of portblock RA, the parameter tickle_dir must be specified in the
action=unblock instance of the portblock resources.
For example, you may stack resources like below:
portblock action=block
@@ -103,18 +108,18 @@
portblock action=unblock tickle_dir=/tickle/state/dir
If you want to tickle all the TCP connections which connected to _one_
- floating IP but different ports, no matter how many portblock resources
- you have defined, you should enable tickles for _one_ portblock
+ floating IP but different ports, no matter how many portblock resources
+ you have defined, you should enable tickles for _one_ portblock
resource(action=unblock) only.
-
- The tickle_dir is a location which stores the established TCP
- connections. It can be a shared directory(which is cluster-visible to
+
+ The tickle_dir is a location which stores the established TCP
+ connections. It can be a shared directory(which is cluster-visible to
all nodes) or a local directory.
If you use the shared directory, you needn't do any other things.
If you use the local directory, you must also specify the sync_script
paramater. We recommend you to use csync2 as the sync_script.
- For example, if you use the local directory /tmp/tickle as tickle_dir,
- you could setup the csync2 as the csync2 documentation says and
+ For example, if you use the local directory /tmp/tickle as tickle_dir,
+ you could setup the csync2 as the csync2 documentation says and
configure your /etc/csync2/csync2.cfg like:
group ticklegroup {
host node1;
@@ -137,15 +142,19 @@
<version>1.0</version>
<longdesc lang="en">
-Resource script for portblock. It is used to temporarily block ports
+Resource script for portblock. It is used to block ports
using iptables. In addition, it may allow for faster TCP reconnects
for clients on failover. Use that if there are long lived TCP
connections to an HA service. This feature is enabled by setting the
tickle_dir parameter and only in concert with action set to unblock.
Note that the tickle ACK function is new as of version 3.0.2 and
hasn't yet seen widespread use.
+
+In Promotable mode, the promote action unblocks the port(s) on the Promoted node
+and blocks the port(s) on the Unpromoted node(s) when action=unblock, and vice versa
+when action=block.
</longdesc>
-<shortdesc lang="en">Block and unblocks access to TCP and UDP ports</shortdesc>
+<shortdesc lang="en">Blocks and unblocks access to TCP and UDP ports</shortdesc>
<parameters>
<parameter name="protocol" unique="0" required="1">
@@ -167,6 +176,10 @@
<parameter name="action" unique="0" required="1">
<longdesc lang="en">
The action (block/unblock) to be done on the protocol::portno.
+
+In Promotable mode it is the action for the promote action,
+and the opposite action will be used for the start and demote
+actions.
</longdesc>
<shortdesc lang="en">action</shortdesc>
<content type="string" default="${OCF_RESKEY_action_default}" />
@@ -202,7 +215,7 @@
<parameter name="tickle_dir" unique="0" required="0">
<longdesc lang="en">
-The shared or local directory (_must_ be absolute path) which
+The shared or local directory (_must_ be absolute path) which
stores the established TCP connections.
</longdesc>
<shortdesc lang="en">Tickle directory</shortdesc>
@@ -236,6 +249,8 @@
<actions>
<action name="start" timeout="20s" />
<action name="stop" timeout="20s" />
+<action name="promote" timeout="10s"/>
+<action name="demote" timeout="10s"/>
<action name="status" depth="0" timeout="10s" interval="10s" />
<action name="monitor" depth="0" timeout="10s" interval="10s" />
<action name="meta-data" timeout="5s" />
@@ -269,9 +284,9 @@
# iptables 1.8.9 briefly broke the output format, returning the
# numeric protocol value instead of a string. Support both variants.
if [ "$1" = "tcp" ]; then
- local prot="(tcp|6)"
+ local prot="\(tcp\|6\)"
else
- local prot="(udp|17)"
+ local prot="\(udp\|17\)"
fi
echo "^DROP${w}${prot}${w}--${w}${src}${w}${dst}${w}multiport${w}${4}ports${w}${2}$"
}
@@ -281,7 +296,7 @@
{
[ "$4" = "OUTPUT" ] && ds="s" || ds="d"
PAT=$(active_grep_pat "$1" "$2" "$3" "$ds")
- $IPTABLES $wait -n -L "$4" | grep -qE "$PAT"
+ $IPTABLES $wait -n -L "$4" | grep -q "$PAT"
}
# netstat -tn and ss -Htn, split on whitespace and colon,
@@ -397,6 +412,17 @@
rc=$OCF_NOT_RUNNING
;;
esac
+ elif ocf_is_ms; then
+ case $5 in
+ block)
+ SayInactive $*
+ rc=$OCF_NOT_RUNNING
+ ;;
+ *)
+ SayActive $*
+ rc=$OCF_SUCCESS
+ ;;
+ esac
else
case $5 in
block)
@@ -493,18 +519,21 @@
{
ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" start
case $5 in
- block) IptablesBLOCK "$@";;
+ block) IptablesBLOCK "$@"
+ rc=$?
+ ;;
unblock)
IptablesUNBLOCK "$@"
rc=$?
tickle_remote
#ignore run_tickle_tcp exit code!
- return $rc
;;
- *) usage; return 1;
+ *) usage; return $OCF_ERR_CONFIGURED ;
esac
- return $?
+ ocf_is_ms && ocf_promotion_score -v $SCORE_UNPROMOTED -N $nodename
+
+ return $rc
}
#IptablesStop {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
@@ -512,17 +541,73 @@
{
ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" stop
case $5 in
- block) IptablesUNBLOCK "$@";;
+ block) IptablesUNBLOCK "$@"
+ rc=$?
+ ;;
unblock)
save_tcp_connections
IptablesBLOCK "$@"
+ rc=$?
;;
- *) usage; return 1;;
+ *) usage; return $OCF_ERR_CONFIGURED ;;
esac
+ ocf_is_ms && ocf_promotion_score -D -N $nodename
+
+ return $rc
+}
+
+IptablesPromote() {
+ IptablesStatus "$@"
+ rc=$?
+ if [ $rc -eq $OCF_SUCCESS ] && [ $promotion_score -eq $SCORE_PROMOTED ]; then
+ ocf_log info "Promote: resource already promoted."
+ return $rc
+ elif [ $rc -ne $OCF_SUCCESS ] && [ $rc -ne $OCF_NOT_RUNNING ]; then
+ ocf_exit_reason "Promote: IptablesStatus failed with rc: $rc."
+ return $rc
+ fi
+ case $5 in
+ block) IptablesBLOCK "$@"
+ rc=$?
+ ;;
+ unblock)
+ IptablesUNBLOCK "$@"
+ rc=$?
+ tickle_remote
+ #ignore run_tickle_tcp exit code!
+ ;;
+ *) usage; return $OCF_ERR_CONFIGURED ;
+ esac
+ ocf_promotion_score -v $SCORE_PROMOTED -N $nodename
return $?
}
+IptablesDemote() {
+ IptablesStatus "$@"
+ rc=$?
+ if [ $rc -eq $OCF_SUCCESS ] && [ $promotion_score -eq $SCORE_UNPROMOTED ]; then
+ ocf_log info "Demote: resource already demoted."
+ return $rc
+ elif [ $rc -ne $OCF_SUCCESS ] && [ $rc -ne $OCF_NOT_RUNNING ]; then
+ ocf_exit_reason "Demote: IptablesStatus failed with rc: $rc."
+ return $rc
+ fi
+ case $5 in
+ block)
+ save_tcp_connections
+ IptablesBLOCK "$@"
+ rc=$?
+ ;;
+ unblock) IptablesUNBLOCK "$@"
+ rc=$?
+ ;;
+ *) usage; return $OCF_ERR_CONFIGURED ;;
+ esac
+ ocf_promotion_score -v $SCORE_UNPROMOTED -N $nodename
+ return $rc
+}
+
#
# Check if the port is valid, this function code is not decent, but works
#
@@ -558,17 +643,17 @@
fi
if [ ! -d "$OCF_RESKEY_tickle_dir" ]; then
ocf_log err "The tickle dir doesn't exist!"
- exit $OCF_ERR_INSTALLED
+ exit $OCF_ERR_INSTALLED
fi
fi
case $action in
- block|unblock)
+ block|unblock)
;;
- *)
+ *)
ocf_log err "Invalid action $action!"
exit $OCF_ERR_CONFIGURED
- ;;
+ ;;
esac
if ocf_is_true $reset_local_on_unblock_stop; then
@@ -591,7 +676,7 @@
exit $OCF_ERR_ARGS
fi
-case $1 in
+case $__OCF_ACTION in
meta-data) meta_data
exit $OCF_SUCCESS
;;
@@ -605,12 +690,12 @@
if [ -z "$OCF_RESKEY_protocol" ]; then
ocf_log err "Please set OCF_RESKEY_protocol"
exit $OCF_ERR_CONFIGURED
-fi
+fi
if [ -z "$OCF_RESKEY_portno" ]; then
ocf_log err "Please set OCF_RESKEY_portno"
exit $OCF_ERR_CONFIGURED
-fi
+fi
if [ -z "$OCF_RESKEY_action" ]; then
ocf_log err "Please set OCF_RESKEY_action"
@@ -632,6 +717,7 @@
action=$OCF_RESKEY_action
ip=$OCF_RESKEY_ip
reset_local_on_unblock_stop=$OCF_RESKEY_reset_local_on_unblock_stop
+nodename=$(ocf_local_nodename)
# If "tickle" is enabled, we need to record the list of currently established
@@ -647,17 +733,35 @@
fi
fi
-case $1 in
- start)
- IptablesStart $protocol $portno $ip $direction $action
+if ocf_is_ms; then
+ promotion_score=$(ocf_promotion_score -G -N $nodename -q 2> /dev/null)
+ if { [ "$__OCF_ACTION" = "monitor" ] && [ "$promotion_score" = "$SCORE_UNPROMOTED" ]; } || [ "$__OCF_ACTION" = "demote" ] || [ "$__OCF_ACTION" = "start" ]; then
+ case $action in
+ block) action="unblock" ;;
+ unblock) action="block" ;;
+ esac
+ fi
+fi
+
+case $__OCF_ACTION in
+ start)
+ IptablesStart "$protocol" "$portno" "$ip" "$direction" "$action"
+ ;;
+
+ stop)
+ IptablesStop "$protocol" "$portno" "$ip" "$direction" "$action"
+ ;;
+
+ promote)
+ IptablesPromote "$protocol" "$portno" "$ip" "$direction" "$action"
;;
- stop)
- IptablesStop $protocol $portno $ip $direction $action
+ demote)
+ IptablesDemote "$protocol" "$portno" "$ip" "$direction" "$action"
;;
- status|monitor)
- IptablesStatus $protocol $portno $ip $direction $action
+ status|monitor)
+ IptablesStatus "$protocol" "$portno" "$ip" "$direction" "$action"
;;
validate-all)

View File

@ -1,180 +0,0 @@
--- a/heartbeat/portblock 2025-10-21 09:27:41.753028260 +0200
+++ b/heartbeat/portblock 2025-10-21 09:28:55.573855995 +0200
@@ -28,6 +28,8 @@
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=""
@@ -37,6 +39,8 @@
: ${OCF_RESKEY_portno=${OCF_RESKEY_portno_default}}
: ${OCF_RESKEY_direction=${OCF_RESKEY_direction_default}}
: ${OCF_RESKEY_action=${OCF_RESKEY_action_default}}
+: ${OCF_RESKEY_method=${OCF_RESKEY_method_default}}
+: ${OCF_RESKEY_status_check=${OCF_RESKEY_status_check_default}}
: ${OCF_RESKEY_ip=${OCF_RESKEY_ip_default}}
: ${OCF_RESKEY_reset_local_on_unblock_stop=${OCF_RESKEY_reset_local_on_unblock_stop_default}}
: ${OCF_RESKEY_tickle_dir=${OCF_RESKEY_tickle_dir_default}}
@@ -185,6 +189,26 @@
<content type="string" default="${OCF_RESKEY_action_default}" />
</parameter>
+<parameter name="method" unique="0" required="0">
+<longdesc lang="en">
+Block method:
+drop: Use DROP rule.
+reject: Use REJECT rule w/conntrack to clear connections when blocking.
+</longdesc>
+<shortdesc lang="en">Block method</shortdesc>
+<content type="string" default="${OCF_RESKEY_method_default}" />
+</parameter>
+
+<parameter name="status_check" unique="0" required="0">
+<longdesc lang="en">
+Status check:
+rule: Check rule.
+pseudo: Check pseudo status when rule is absent.
+</longdesc>
+<shortdesc lang="en">Status check</shortdesc>
+<content type="string" default="${OCF_RESKEY_status_check_default}" />
+</parameter>
+
<parameter name="reset_local_on_unblock_stop" unique="0" required="0">
<longdesc lang="en">
If for some reason the long lived server side TCP sessions won't be cleaned up
@@ -253,6 +277,7 @@
<action name="demote" timeout="10s"/>
<action name="status" depth="0" timeout="10s" interval="10s" />
<action name="monitor" depth="0" timeout="10s" interval="10s" />
+<action name="monitor" depth="0" timeout="10s" interval="9s" role="Promoted" />
<action name="meta-data" timeout="5s" />
<action name="validate-all" timeout="5s" />
</actions>
@@ -288,7 +313,11 @@
else
local prot="\(udp\|17\)"
fi
- echo "^DROP${w}${prot}${w}--${w}${src}${w}${dst}${w}multiport${w}${4}ports${w}${2}$"
+ if [ "$method" = "DROP" ]; then
+ echo "^DROP${w}${prot}${w}--${w}${src}${w}${dst}${w}multiport${w}${4}ports${w}${2}$"
+ else
+ echo "^REJECT${w}${prot}${w}--${w}${src}${w}${dst}${w}multiport${w}${4}ports${w}${2}${w}ctstate${w}NEW,RELATED,ESTABLISHED${w}reject-with${w}tcp-reset$"
+ fi
}
#chain_isactive {udp|tcp} portno,portno ip chain
@@ -374,17 +403,17 @@
SayActive()
{
- ocf_log debug "$CMD DROP rule [$*] is running (OK)"
+ ocf_log debug "$CMD $method rule [$*] is running (OK)"
}
SayConsideredActive()
{
- ocf_log debug "$CMD DROP rule [$*] considered to be running (OK)"
+ ocf_log debug "$CMD $method rule [$*] considered to be running (OK)"
}
SayInactive()
{
- ocf_log debug "$CMD DROP rule [$*] is inactive"
+ ocf_log debug "$CMD $method rule [$*] is inactive"
}
#IptablesStatus {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
@@ -405,14 +434,18 @@
case $5 in
block)
SayActive $*
- rc=$OCF_SUCCESS
+ if [ "$__OCF_ACTION" = "monitor" ] && [ "$promotion_score" = "$SCORE_PROMOTED" ]; then
+ rc=$OCF_RUNNING_MASTER
+ else
+ rc=$OCF_SUCCESS
+ fi
;;
*)
SayInactive $*
rc=$OCF_NOT_RUNNING
;;
esac
- elif ocf_is_ms; then
+ elif [ "$OCF_RESKEY_status_check" = "rule" ]; then
case $5 in
block)
SayInactive $*
@@ -420,7 +453,11 @@
;;
*)
SayActive $*
- rc=$OCF_SUCCESS
+ if [ "$__OCF_ACTION" = "monitor" ] && [ "$promotion_score" = "$SCORE_PROMOTED" ]; then
+ rc=$OCF_RUNNING_MASTER
+ else
+ rc=$OCF_SUCCESS
+ fi
;;
esac
else
@@ -461,7 +498,11 @@
: Chain already in desired state
else
[ "$chain" = "OUTPUT" ] && ds="s" || ds="d"
- $IPTABLES $wait "$op" "$chain" -p "$proto" -${ds} "$ip" -m multiport --${ds}ports "$ports" -j DROP
+ if [ "$method" = "DROP" ]; then
+ $IPTABLES $wait "$op" "$chain" -p "$proto" -${ds} "$ip" -m multiport --${ds}ports "$ports" -j DROP
+ else
+ $IPTABLES $wait "$op" "$chain" -p "$proto" -${ds} "$ip" -m multiport --${ds}ports "$ports" -m conntrack --ctstate NEW,ESTABLISHED,RELATED -j REJECT --reject-with tcp-reset
+ fi
fi
}
@@ -486,7 +527,11 @@
$IPTABLES $wait -I OUTPUT -p "$1" -s "$3" -m multiport --sports "$2" -j REJECT --reject-with tcp-reset
tickle_local
fi
- $IPTABLES $wait -I INPUT -p "$1" -d "$3" -m multiport --dports "$2" -j DROP
+ if [ "$method" = "DROP" ]; then
+ $IPTABLES $wait -I INPUT -p "$1" -d "$3" -m multiport --dports "$2" -j DROP
+ else
+ $IPTABLES $wait -I INPUT -p "$1" -d "$3" -m multiport --dports "$2" -m conntrack --ctstate NEW,ESTABLISHED,RELATED -j REJECT --reject-with tcp-reset
+ fi
rc_in=$?
if $try_reset ; then
$IPTABLES $wait -D OUTPUT -p "$1" -s "$3" -m multiport --sports "$2" -j REJECT --reject-with tcp-reset
@@ -718,6 +763,13 @@
ip=$OCF_RESKEY_ip
reset_local_on_unblock_stop=$OCF_RESKEY_reset_local_on_unblock_stop
nodename=$(ocf_local_nodename)
+case "$OCF_RESKEY_method" in
+ drop) method="DROP" ;;
+ reject) method="REJECT" ;;
+ *) ocf_log err "method: $OCF_RESKEY_method not supported"
+ exit $OCF_ERR_CONFIGURED
+ ;;
+esac
# If "tickle" is enabled, we need to record the list of currently established
@@ -743,6 +795,8 @@
fi
fi
+IptablesValidateAll
+
case $__OCF_ACTION in
start)
IptablesStart "$protocol" "$portno" "$ip" "$direction" "$action"
@@ -765,7 +819,6 @@
;;
validate-all)
- IptablesValidateAll
;;
*) usage

View File

@ -0,0 +1,186 @@
From 1afdd91b2961061937fc802c575304ede8d79286 Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Wed, 10 Sep 2025 16:56:56 +0200
Subject: [PATCH] podman-etcd: Add cluster-wide force_new_cluster attribute
checking
Implement cluster-wide validation of force_new_cluster attribute to resolve
race conditions during automated cluster recovery. The enhancement ensures
agents check for the cluster-wide attribute before falling back to local
etcd revision comparison.
Key changes:
- Enhanced get_force_new_cluster() to query all cluster nodes
- Ensure force_new_cluster is not set in both nodes to prevent
conflicting recovery attempts
- Updated startup logic to prioritize cluster-wide attribute checking
fixes OCPBUGS-61117
---
heartbeat/podman-etcd | 107 ++++++++++++++++++++++++++++--------------
1 file changed, 72 insertions(+), 35 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 33804414a..f3a6da5e2 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -794,54 +794,72 @@ set_force_new_cluster()
return $rc
}
+# get_force_new_cluster returns a space-separated list of nodes that have the force_new_cluster attribute set.
+# Return values:
+# - Exit code 0 with non-empty output: One or more nodes have the force_new_cluster attribute set
+# - Exit code 0 with empty output: No nodes have the force_new_cluster attribute set
+# - Exit code 1 with empty output: Error occurred while querying the cluster nodes
get_force_new_cluster()
{
- crm_attribute --lifetime reboot --query --name "force_new_cluster" | awk -F"value=" '{print $2}'
+ local node nodes value
+ local holders=""
+
+ if ! nodes=$(crm_node -l | awk '{print $2}'); then
+ ocf_log err "could not get force_new_cluster attribute, crm_node error code: $?"
+ return 1
+ fi
+ if [ -z "$nodes" ]; then
+ ocf_log err "could not get force_new_cluster attribute, the list of nodes is empty"
+ return 1
+ fi
+
+ for node in $nodes; do
+ if ! value=$(crm_attribute --query --lifetime reboot --name "force_new_cluster" --node "$node" 2>/dev/null | awk -F'value=' '{print $2}' | tr -d "'"); then
+ ocf_log err "could not get force_new_cluster attribute, crm_attribut error code: $?"
+ return 1
+ fi
+ if [ -n "$value" ]; then
+ holders="$holders$node "
+ fi
+ done
+ echo "$holders"
}
+
clear_force_new_cluster()
{
- local force_new_cluster_node
-
- force_new_cluster_node=$(get_force_new_cluster)
- if [ -z "$force_new_cluster_node" ]; then
- ocf_log info "$NODENAME: force_new_cluster attribute not set"
+ # only the holder of "force_new_cluster" attribute can delete it
+ if ! is_force_new_cluster; then
+ ocf_log info "force_new_cluster unset or not owned by $NODENAME"
return $OCF_SUCCESS
fi
- # only the holder of "force_new_cluster" attribute can delete it
- if [ "$NODENAME" = "$force_new_cluster_node" ]; then
- crm_attribute --lifetime reboot --name "force_new_cluster" --delete
- rc=$?
- if [ $rc -ne 0 ]; then
- ocf_log err "could not clear force_new_cluster attribute, error code: $rc"
- else
- ocf_log info "$NODENAME: force_new_cluster attribute cleared"
- fi
- return $rc
- else
- ocf_log info "$NODENAME does not hold force_new_cluster ($force_new_cluster_node has it)"
- return $OCF_SUCCESS
+ if ! crm_attribute --delete --lifetime reboot --node "$NODENAME" --name "force_new_cluster"; then
+ ocf_log err "could not clear force_new_cluster attribute, error code: $?"
+ return $OCF_ERR_GENERIC
fi
+
+ ocf_log info "$NODENAME: force_new_cluster attribute cleared"
+ return $OCF_SUCCESS
}
+
is_force_new_cluster()
{
- # Return 0 if 'force_new_cluster' is set and the value matches the current node name, 1 otherwise.
- local value
+ # Return 0 if 'force_new_cluster' is set on the current node, 1 otherwise.
+ local fnc_holders
- value=$(get_force_new_cluster)
- if [ -z "$value" ]; then
- ocf_log debug "force_new_cluster attribute is not set"
- return 1
+ if ! fnc_holders=$(get_force_new_cluster); then
+ ocf_exit_reason "is_force_new_cluster: Failed to get force_new_cluster node holders"
+ exit $OCF_ERR_GENERIC
fi
- if [ "$value" = "$NODENAME" ]; then
+ if echo "$fnc_holders" | grep -q -w "$NODENAME"; then
ocf_log debug "$NODENAME has force_new_cluster set"
return 0
fi
- ocf_log info "force_new_cluster attribute set on peer node $value"
+ ocf_log debug "force_new_cluster attribute is not set on $NODENAME"
return 1
}
@@ -1415,17 +1433,34 @@ podman_start()
return "$OCF_ERR_GENERIC"
fi
- # force-new-cluster property is a runtime-scoped flag that instructs the agent to force a new cluster-of-1.
- # Since this attribute is configured with a reboot-lifetime, it is automatically cleared when the machine reboots.
- # If the agent detects during its start that this property is set, it indicates that the flag was explicitly set
- # during the current node boot session, implying a deliberate request to recover the cluster.
if ocf_is_true "$pod_was_running"; then
ocf_log info "static pod was running: start normally"
else
- if is_force_new_cluster; then
- ocf_log notice "'$NODENAME' marked to force-new-cluster"
+ local fnc_holders
+ if ! fnc_holders=$(get_force_new_cluster); then
+ ocf_exit_reason "Failed to get force_new_cluster node holders"
+ return "$OCF_ERR_GENERIC"
+ fi
+
+ local fnc_holder_count
+ fnc_holder_count=$(echo "$fnc_holders" | wc -w)
+ if [ "$fnc_holder_count" -gt 1 ]; then
+ ocf_exit_reason "force_new_cluster attribute is set on multiple nodes ($fnc_holders)"
+ return "$OCF_ERR_GENERIC"
+ fi
+
+ if [ "$fnc_holder_count" -eq 1 ]; then
+ if echo "$fnc_holders" | grep -q -w "$NODENAME"; then
+ # Attribute is set on the local node.
+ ocf_log notice "$NODENAME marked to force-new-cluster"
+ JOIN_AS_LEARNER=false
+ else
+ # Attribute is set on a peer node.
+ ocf_log info "$NODENAME shall join as learner because force_new_cluster is set on peer $fnc_holders"
+ JOIN_AS_LEARNER=true
+ fi
else
- ocf_log info "'$NODENAME' is not marked to force-new-cluster"
+ ocf_log info "no node is marked to force-new-cluster"
# When the local agent starts, we can infer the cluster state by counting
# how many agents are starting or already active:
# - 1 active agent: it's the peer (we are just starting)
@@ -1522,7 +1557,7 @@ podman_start()
for try in $(seq $retries); do
learner_node=$(attribute_learner_node get)
if [ "$NODENAME" != "$learner_node" ]; then
- ocf_log info "$learner_node is not in the member list yet. Retry in $poll_interval_sec seconds."
+ ocf_log info "$NODENAME is not in the member list yet. Retry in $poll_interval_sec seconds."
sleep $poll_interval_sec
continue
fi
@@ -1673,6 +1708,8 @@ podman_stop()
{
local timeout=60
local rc
+
+ ocf_log notice "podman-etcd stop"
podman_simple_status
if [ $? -eq $OCF_NOT_RUNNING ]; then
ocf_log info "could not leave members list: etcd container not running"

View File

@ -0,0 +1,321 @@
From a31f15104fc712cd25f8a59d49f1bbcdbbbc5434 Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Tue, 30 Sep 2025 11:54:44 +0200
Subject: [PATCH 1/2] Refactor(podman-etcd): improve peer checking and
leadership loss detection
The check_peers function is broken up into smaller, more manageable
functions. This refactoring separates the logic for detecting a loss of
cluster leadership from the logic for managing peer membership.
The main function is renamed to check_peer as there is only 1 peer to
check (it was check_peers).
---
heartbeat/podman-etcd | 78 +++++++++++++++++++++++++------------------
1 file changed, 45 insertions(+), 33 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index f3a6da5e2..3d1e4c520 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -1014,42 +1014,35 @@ get_member_list_json() {
podman exec "${CONTAINER}" etcdctl member list --endpoints="$this_node_endpoint" -w json
}
-check_peers()
+detect_cluster_leadership_loss()
{
- # Check peers endpoint status and locally accessible member list
- local member_list_json
-
- if ! container_exists; then
- # we need a running container to execute etcdctl.
- return $OCF_SUCCESS
+ endpoint_status_json=$(get_endpoint_status_json)
+ ocf_log info "endpoint status: $endpoint_status_json"
+
+ count_endpoints=$(printf "%s" "$endpoint_status_json" | jq -r ".[].Endpoint" | wc -l)
+ if [ "$count_endpoints" -eq 1 ]; then
+ ocf_log info "one endpoint only: checking status errors"
+ endpoint_status_errors=$(printf "%s" "$endpoint_status_json" | jq -r ".[0].Status.errors")
+ if echo "$endpoint_status_errors" | grep -q "no leader"; then
+ set_force_new_cluster
+ set_standalone_node
+ ocf_exit_reason "$NODENAME must force a new cluster"
+ return $OCF_ERR_GENERIC
+ fi
+ if [ "$endpoint_status_errors" != "null" ]; then
+ ocf_log err "unmanaged endpoint status error: $endpoint_status_errors"
+ fi
fi
- member_list_json=$(get_member_list_json)
- rc=$?
- ocf_log debug "member list: $member_list_json"
- if [ $rc -ne 0 ]; then
- ocf_log info "podman failed to get member list, error code: $rc"
-
- endpoint_status_json=$(get_endpoint_status_json)
- ocf_log info "endpoint status: $endpoint_status_json"
-
- count_endpoints=$(printf "%s" "$endpoint_status_json" | jq -r ".[].Endpoint" | wc -l)
- if [ "$count_endpoints" -eq 1 ]; then
- ocf_log info "one endpoint only: checking status errors"
- endpoint_status_errors=$(printf "%s" "$endpoint_status_json" | jq -r ".[0].Status.errors")
- if echo "$endpoint_status_errors" | grep -q "no leader"; then
- set_force_new_cluster
- set_standalone_node
- ocf_exit_reason "$NODENAME must force a new cluster"
- return $OCF_ERR_GENERIC
- fi
- if [ "$endpoint_status_errors" != "null" ]; then
- ocf_log err "unmanaged endpoint status error: $endpoint_status_errors"
- fi
- fi
+ return $OCF_SUCCESS
+}
- return $OCF_SUCCESS
- fi
+manage_peer_membership()
+{
+ # Read etcd member list to detect the status of the peer member.
+ # If the peer is missing from the member list, it will be added back as learner
+ # If the peer is back in the member list, we ensure that the related CIB attributes (standalone and learner_node) are reset
+ local member_list_json="$1"
# 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
@@ -1083,6 +1076,25 @@ check_peers()
clear_standalone_and_learner_if_not_learners "$member_list_json"
fi
done
+}
+
+check_peer()
+{
+ # Check peers endpoint status and locally accessible member list
+ local member_list_json
+
+ # we need a running container to execute etcdctl.
+ if ! container_exists; then
+ return $OCF_SUCCESS
+ fi
+
+ if ! member_list_json=$(get_member_list_json); then
+ ocf_log info "podman failed to get member list, error code: $?"
+ detect_cluster_leadership_loss
+ return $?
+ fi
+
+ manage_peer_membership "$member_list_json"
return $OCF_SUCCESS
}
@@ -1124,7 +1136,7 @@ podman_monitor()
# monitor operation to fail.
# TODO: move this inside check_peers where we already query member list json
attribute_node_member_id update
- if ! check_peers; then
+ if ! check_peer; then
return $OCF_ERR_GENERIC
fi
From de7c73a933cefb8f7b9e810bd23c3d12f6d6f29a Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Tue, 30 Sep 2025 18:38:06 +0200
Subject: [PATCH 2/2] OCPBUGS-42808: podman-etcd: add automatic learner member
promotion
Automatically promote etcd learner members to voting members when detected.
Includes refactored member management functions and improved validation.
---
heartbeat/podman-etcd | 108 ++++++++++++++++++++++++++++++------------
1 file changed, 79 insertions(+), 29 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 3d1e4c520..e1425ec02 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -712,6 +712,22 @@ attribute_node_revision_peer()
crm_attribute --query --type nodes --node "$nodename" --name "revision" | awk -F"value=" '{print $2}'
}
+# Converts a decimal number to hexadecimal format with validation
+# Args: $1 - decimal number (test for non-negative integer too)
+# Returns: 0 on success, OCF_ERR_GENERIC on invalid input
+# Outputs: hexadecimal representation to stdout
+decimal_to_hex() {
+ local dec=$1
+
+ if ! echo "$dec" | grep -q "^[1-9][0-9]*$"; then
+ ocf_log err "Invalid member ID format: '$dec' (expected decimal number)"
+ return $OCF_ERR_GENERIC
+ fi
+
+ printf "%x" "$dec"
+ return $OCF_SUCCESS
+}
+
attribute_node_member_id()
{
local action="$1"
@@ -737,7 +753,7 @@ attribute_node_member_id()
return "$rc"
fi
- local value
+ local value value_hex
if ! value=$(echo -n "$member_list_json" | jq -r ".header.member_id"); then
rc=$?
ocf_log err "could not get $attribute from member list JSON, error code: $rc"
@@ -745,8 +761,11 @@ attribute_node_member_id()
fi
# JSON member_id is decimal, while etcdctl command needs the hex version
- value=$(printf "%x" "$value")
- if ! crm_attribute --type nodes --node "$NODENAME" --name "$attribute" --update "$value"; then
+ if ! value_hex=$(decimal_to_hex "$value"); then
+ ocf_log err "could not convert decimal member_id '$value' to hex, error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
+ if ! crm_attribute --type nodes --node "$NODENAME" --name "$attribute" --update "$value_hex"; then
rc=$?
ocf_log err "could not update etcd $attribute, error code: $rc"
return "$rc"
@@ -905,42 +924,70 @@ clear_standalone_node()
crm_attribute --name "standalone_node" --delete
}
-clear_standalone_and_learner_if_not_learners()
+
+# Promotes an etcd learner member to a voting member
+# Args: $1 - learner member ID in decimal format
+# Returns: OCF_SUCCESS (even on expected promotion failures), OCF_ERR_GENERIC on conversion errors
+# Note: Promotion failures are expected and logged as info (peer may not be up-to-date)
+promote_learner_member()
+{
+ local learner_member_id=$1
+
+ # JSON member_id is decimal, while etcdctl command needs the hex version
+ if ! learner_member_id_hex=$(decimal_to_hex "$learner_member_id"); then
+ ocf_log err "could not convert decimal member_id '$learner_member_id' to hex, error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
+ if ! ocf_run podman exec "${CONTAINER}" etcdctl member promote "$learner_member_id_hex" 2>&1; then
+ # promotion is expected to fail if the peer is not yet up-to-date
+ ocf_log info "could not promote member $learner_member_id_hex, error code: $?"
+ return $OCF_SUCCESS
+ fi
+ ocf_log info "successfully promoted member '$learner_member_id_hex'"
+ return $OCF_SUCCESS
+}
+
+# Reconciles etcd cluster member states
+# Promotes learner members or clears standalone/learner attributes as needed
+# Args: $1 - member list JSON from etcdctl
+# Returns: OCF_SUCCESS on completion, OCF_ERR_GENERIC on errors
+# Note: Only operates when exactly 2 started members are present
+reconcile_member_state()
{
local rc
local member_list_json="$1"
- number_of_members=$(printf "%s" "$member_list_json" | jq -r ".members[].ID" | wc -l)
- if [ "$number_of_members" -ne 2 ]; then
- ocf_log info "could not clear standalone_node, nor learner_node properties: found $number_of_members members, need 2"
+ # count only the started members, which have the ".name" JSON field
+ number_of_started_members=$(printf "%s" "$member_list_json" | jq -r ".members[].name | select(. != null)" | wc -l)
+ if [ "$number_of_started_members" -ne 2 ]; then
+ ocf_log info "could not clear standalone_node, nor learner_node properties: found $number_of_started_members members, need 2"
return $OCF_SUCCESS
fi
- id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .isLearner==true ).ID")
+ learner_member_id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .isLearner==true ).ID")
rc=$?
if [ $rc -ne 0 ]; then
ocf_log err "could not get isLearner field from member list, error code: $rc"
return $rc
fi
- if [ -z "$id" ]; then
- clear_standalone_node
- rc=$?
- if [ $rc -ne 0 ]; then
- ocf_og error "could not clear standalone_node attribute, error code: $rc"
- return $rc
- fi
+ if [ -n "$learner_member_id" ]; then
+ promote_learner_member "$learner_member_id"
+ return $?
fi
- if [ -z "$id" ]; then
- attribute_learner_node clear
- rc=$?
- if [ $rc -ne 0 ]; then
- ocf_og error "could not clear learner_node attribute, error code: $rc"
- return $rc
+
+ if [ -z "$learner_member_id" ]; then
+ if ! clear_standalone_node; then
+ ocf_log error "could not clear standalone_node attribute, error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
+ if ! attribute_learner_node clear; then
+ ocf_log error "could not clear learner_node attribute, error code: $?"
+ return $OCF_ERR_GENERIC
fi
fi
- return $rc
+ return $OCF_SUCCESS
}
attribute_learner_node()
@@ -1019,7 +1066,7 @@ detect_cluster_leadership_loss()
endpoint_status_json=$(get_endpoint_status_json)
ocf_log info "endpoint status: $endpoint_status_json"
- count_endpoints=$(printf "%s" "$endpoint_status_json" | jq -r ".[].Endpoint" | wc -l)
+ count_endpoints=$(printf "%s" "$endpoint_status_json" | jq -r ".[].Endpoint" | wc -l)
if [ "$count_endpoints" -eq 1 ]; then
ocf_log info "one endpoint only: checking status errors"
endpoint_status_errors=$(printf "%s" "$endpoint_status_json" | jq -r ".[0].Status.errors")
@@ -1037,11 +1084,14 @@ detect_cluster_leadership_loss()
return $OCF_SUCCESS
}
+
+# 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
+# Returns: OCF_SUCCESS on completion, OCF_ERR_GENERIC on errors
+# Note: Iterates through all peer nodes to ensure proper cluster membership
manage_peer_membership()
{
- # Read etcd member list to detect the status of the peer member.
- # If the peer is missing from the member list, it will be added back as learner
- # If the peer is back in the member list, we ensure that the related CIB attributes (standalone and learner_node) are reset
local member_list_json="$1"
# Example of .members[] instance fields in member list json format:
@@ -1066,14 +1116,14 @@ manage_peer_membership()
# 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
- id=$(printf "%s" "$member_list_json" | jq -r ".members[] | select( .peerURLs | map(test(\"$ip\")) | any).ID")
- if [ -z "$id" ]; then
+ 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"
- clear_standalone_and_learner_if_not_learners "$member_list_json"
+ reconcile_member_state "$member_list_json"
fi
done
}

View File

@ -0,0 +1,135 @@
From 93729d83fa5bf15f4ec694e08e9777bde858fb41 Mon Sep 17 00:00:00 2001
From: Lars Ellenberg <lars.ellenberg@linbit.com>
Date: Thu, 16 Oct 2025 10:58:37 +0200
Subject: [PATCH 1/2] Filesystem: speed up get_pids
With force_umount=safe, we "manually" scan the /proc/ file system.
We look for symlinks pointing into the path we are interested in.
Specifically, we are interested in
/proc/<pid>/{root,exe,cwd}
/proc/<pid>/fd/<fd>
We also look for relevant memory mappings in /proc/<pid>/maps
All these are per process, not per "task" or "thread".
see procfs(5) and pthreads(7).
Still, we currently also scan /proc/<pid>/task/<tid>/
for all the same things.
With a large system with many heavily threaded processes,
this can significantly slow down this scanning,
without gaining new information.
Adding -maxdepth to the find command line avoids this useless work,
potentially reducing the scanning time by orders of magnitute
on systems with many heavily threaded processes.
We could also write a dedicated helper in C to do the very same thing,
with the option to "short circuit" and proceed with the next pid
as soon as the first "match" is found for the currently inspected pid.
That could further reduce the scanning time
by about an additional factor of 10.
---
heartbeat/Filesystem | 25 +++++++++++++++++++++----
1 file changed, 21 insertions(+), 4 deletions(-)
diff --git a/heartbeat/Filesystem b/heartbeat/Filesystem
index 6d3960162..f76339fd6 100755
--- a/heartbeat/Filesystem
+++ b/heartbeat/Filesystem
@@ -680,14 +680,31 @@ get_pids()
# -path "/proc/[!0-9]*" -prune -o ...
# -path "/proc/[0-9]*" -a ...
# the latter seemd to be significantly faster for this one in my naive test.
+
+ # root, cwd, exe, maps, fd: all per process, not per task ("thread").
+ # -maxdepth to avoid repeatedly scanning the same thing
+ # for all threads of a heavily threaded process.
+ #
+ # Adding -maxdepth reduced scanning from > 16 seconds to < 2 seconds
+ # on a mostly idle system that happened to run a few java processes.
+ #
+ # We can also add a dedicated helper in C do twhat is done below,
+ # which would reduce the scanning time by an
+ # additional factor of 10 again.
+ #
+ # Or trust that fuser (above) learned something in the last 15 years
+ # and avoids blocking operations meanwhile?
procs=$(exec 2>/dev/null;
- find /proc -path "/proc/[0-9]*" -type l \( -lname "${dir}/*" -o -lname "${dir}" \) -print |
+ find /proc -mindepth 1 -maxdepth 3 \
+ -path "/proc/[0-9]*" \
+ -type l \( -lname "${dir}/*" -o -lname "${dir}" \) -print |
awk -F/ '{print $3}' | uniq)
- # This finds both /proc/<pid>/maps and /proc/<pid>/task/<tid>/maps;
- # if you don't want the latter, add -maxdepth.
+ # memory mappings are also per process, not per task.
+ # This finds only /proc/<pid>/maps, and not /proc/<pid>/task/<tid>/maps;
+ # if you also want the latter, drop -maxdepth.
mmap_procs=$(exec 2>/dev/null;
- find /proc -path "/proc/[0-9]*/maps" -print |
+ find /proc -mindepth 2 -maxdepth 2 -path "/proc/[0-9]*/maps" -print |
xargs -r grep -l " ${dir}/" | awk -F/ '{print $3}' | uniq)
printf "${procs}\n${mmap_procs}" | sort -u
fi
From 3d34db0c60a125126361b45ff8303358b6275298 Mon Sep 17 00:00:00 2001
From: Lars Ellenberg <lars.ellenberg@linbit.com>
Date: Thu, 16 Oct 2025 11:31:00 +0200
Subject: [PATCH 2/2] Filesystem: futher speed up get_pids
If we have /proc/<pid>/map_files/* symlinks,
we don't need to additionally grep /proc/<pid>/maps.
Also don't first collect output of commands into variables
just to pipe them to sort -u later,
just pipe the output of the commands through sort -u directly.
---
heartbeat/Filesystem | 31 +++++++++++++++++++------------
1 file changed, 19 insertions(+), 12 deletions(-)
diff --git a/heartbeat/Filesystem b/heartbeat/Filesystem
index f76339fd6..7021f13da 100755
--- a/heartbeat/Filesystem
+++ b/heartbeat/Filesystem
@@ -694,19 +694,26 @@ get_pids()
#
# Or trust that fuser (above) learned something in the last 15 years
# and avoids blocking operations meanwhile?
- procs=$(exec 2>/dev/null;
- find /proc -mindepth 1 -maxdepth 3 \
- -path "/proc/[0-9]*" \
- -type l \( -lname "${dir}/*" -o -lname "${dir}" \) -print |
- awk -F/ '{print $3}' | uniq)
-
- # memory mappings are also per process, not per task.
- # This finds only /proc/<pid>/maps, and not /proc/<pid>/task/<tid>/maps;
- # if you also want the latter, drop -maxdepth.
- mmap_procs=$(exec 2>/dev/null;
+ (
+ # If you want to debug this, drop this redirection.
+ # But it producess too much "No such file" noise for kernel
+ # threads or due to races with exiting processes or closing fds.
+ exec 2>/dev/null;
+ find /proc -mindepth 1 -maxdepth 3 \
+ -path "/proc/[0-9]*" \
+ -type l \( -lname "${dir}/*" -o -lname "${dir}" \) -print |
+ awk -F/ '{print $3}' | uniq
+
+ # If we have "map_files/", "find" above already found the
+ # relevant symlinks, and we don't need to grep "maps" below.
+ # Available since kernel 3.3, respectively 4.3.
+ test -d /proc/$$/map_files ||
+ # memory mappings are also per process, not per task.
+ # This finds only /proc/<pid>/maps, and not /proc/<pid>/task/<tid>/maps;
+ # if you also want the latter, drop -maxdepth.
find /proc -mindepth 2 -maxdepth 2 -path "/proc/[0-9]*/maps" -print |
- xargs -r grep -l " ${dir}/" | awk -F/ '{print $3}' | uniq)
- printf "${procs}\n${mmap_procs}" | sort -u
+ xargs -r grep -l " ${dir}/" | awk -F/ '{print $3}' | uniq
+ ) | sort -u
fi
}

View File

@ -0,0 +1,166 @@
From 6bfbe1dc3a0dad234decd77330ca6189e932bb89 Mon Sep 17 00:00:00 2001
From: ehila <ehila@redhat.com>
Date: Thu, 16 Oct 2025 23:39:32 -0400
Subject: [PATCH] feat: add support for podman-etcd cert rotation
added a cert check function to the monitor call to force a restart of etcd when the certs have been changed
Signed-off-by: ehila <ehila@redhat.com>
---
heartbeat/podman-etcd | 87 ++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 86 insertions(+), 1 deletion(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index e1425ec02..b8dfb2f9e 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -40,6 +40,7 @@
# Parameter defaults
OCF_RESKEY_image_default="default"
OCF_RESKEY_pod_manifest_default="/etc/kubernetes/static-pod-resources/etcd-certs/configmaps/external-etcd-pod/pod.yaml"
+OCF_RESKEY_etcd_certs_dir_default="/etc/kubernetes/static-pod-resources/etcd-certs"
OCF_RESKEY_name_default="etcd"
OCF_RESKEY_nic_default="br-ex"
OCF_RESKEY_authfile_default="/var/lib/kubelet/config.json"
@@ -51,6 +52,7 @@ OCF_RESKEY_backup_location_default="/var/lib/etcd"
: ${OCF_RESKEY_image=${OCF_RESKEY_image_default}}
: ${OCF_RESKEY_pod_manifest=${OCF_RESKEY_pod_manifest_default}}
+: ${OCF_RESKEY_etcd_certs_dir=${OCF_RESKEY_etcd_certs_dir_default}}
: ${OCF_RESKEY_name=${OCF_RESKEY_name_default}}
: ${OCF_RESKEY_nic=${OCF_RESKEY_nic_default}}
: ${OCF_RESKEY_authfile=${OCF_RESKEY_authfile_default}}
@@ -88,6 +90,15 @@ The Pod manifest with the configuration for Etcd.
<content type="string" default="${OCF_RESKEY_pod_manifest_default}"/>
</parameter>
+<parameter name="etcd_certs_dir" required="0" unique="0">
+<longdesc lang="en">
+The Etcd certificates directory mounted into the etcd container.
+The agent will monitor this directory for changes and restart the etcd container if the certificates have changed.
+</longdesc>
+<shortdesc lang="en">Etcd certificates directory</shortdesc>
+<content type="string" default="${OCF_RESKEY_etcd_certs_dir_default}"/>
+</parameter>
+
<parameter name="image" required="0" unique="0">
<longdesc lang="en">
The podman image to base this container off of.
@@ -289,6 +300,59 @@ Expects to have a fully populated OCF RA-compliant environment set.
END
}
+etcd_certificates_hash_manager()
+{
+ local action="$1"
+ local current_hash
+ local stored_hash
+
+ # If the certs directory doesn't exist, consider it unchanged
+ if [ ! -d "$OCF_RESKEY_etcd_certs_dir" ]; then
+ ocf_log warn "certificates directory $OCF_RESKEY_etcd_certs_dir does not exist, skipping certificate monitoring"
+ return $OCF_SUCCESS
+ fi
+
+ # Calculate hash of all certificate files, ignore key files to avoid accidental disclosure of sensitive information
+ # we only need to monitor the certificate files to detect changes.
+ if ! current_hash=$(find "$OCF_RESKEY_etcd_certs_dir" -type f \( -name "*.crt" \) -exec sha256sum {} \; | sort | sha256sum | cut -d' ' -f1); then
+ ocf_log err "failed to calculate certificate files hash"
+ return $OCF_ERR_GENERIC
+ fi
+
+ # If no stored hash exists, create one and return success
+ if [ ! -f "$ETCD_CERTS_HASH_FILE" ]; then
+ echo "$current_hash" > "$ETCD_CERTS_HASH_FILE"
+ ocf_log info "created initial certificate hash: $current_hash"
+ return $OCF_SUCCESS
+ fi
+
+ case "$action" in
+ "update")
+ if ! echo "$current_hash" > "$ETCD_CERTS_HASH_FILE"; then
+ ocf_log err "failed to update certificate hash file $ETCD_CERTS_HASH_FILE"
+ fi
+ ocf_log info "updated certificate hash: $current_hash"
+ ;;
+ "check")
+ if ! stored_hash=$(cat "$ETCD_CERTS_HASH_FILE"); then
+ ocf_log err "failed to read stored certificate hash from $ETCD_CERTS_HASH_FILE"
+ # This should not happen but if for some reason we can not read the stored hash,
+ # use the current hash and log the error but allow etcd to run as long as possible.
+ stored_hash="$current_hash"
+ fi
+ if [ "$current_hash" != "$stored_hash" ]; then
+ ocf_exit_reason "$NODENAME etcd certificate files have changed (stored: $stored_hash, current: $current_hash)"
+ return $OCF_ERR_GENERIC
+ fi
+ ;;
+ *)
+ ocf_log err "unsupported action: $action"
+ return $OCF_ERR_GENERIC
+ ;;
+ esac
+
+ return $OCF_SUCCESS
+}
monitor_cmd_exec()
{
@@ -357,7 +421,7 @@ archive_current_container()
# archive corresponding etcd configuration files
local files_to_archive=""
- for file in "$OCF_RESKEY_authfile" "$POD_MANIFEST_COPY" "$ETCD_CONFIGURATION_FILE"; do
+ for file in "$OCF_RESKEY_authfile" "$POD_MANIFEST_COPY" "$ETCD_CONFIGURATION_FILE" "$ETCD_CERTS_HASH_FILE"; do
if [ -f "$file" ]; then
files_to_archive="$files_to_archive $file"
else
@@ -1178,6 +1242,11 @@ podman_monitor()
return $rc
fi
+ # Check if certificate files have changed, if they have, etcd needs to be restarted
+ if ! etcd_certificates_hash_manager "check"; then
+ return $OCF_ERR_GENERIC
+ fi
+
if is_learner; then
ocf_log info "$NODENAME is learner. Cannot get member id"
return "$OCF_SUCCESS"
@@ -1483,6 +1552,14 @@ podman_start()
return $OCF_ERR_GENERIC
fi
+ # Update the certificate hash after the container has started successfully
+ # this is to ensure that the certificate hash is updated after a restart is initiated
+ # by a cert rotation event from the monitor command.
+ if ! etcd_certificates_hash_manager "update"; then
+ ocf_exit_reason "etcd certificate hash manager failed to update the certificate hash"
+ return $OCF_ERR_GENERIC
+ fi
+
# check if the container has already started
podman_simple_status
if [ $? -eq $OCF_SUCCESS ]; then
@@ -1888,6 +1965,13 @@ podman_validate()
exit $OCF_ERR_CONFIGURED
fi
+ if ! echo "validation test" > "$ETCD_CERTS_HASH_FILE" \
+ || ! cat "$ETCD_CERTS_HASH_FILE" >/dev/null 2>&1 \
+ || ! rm "$ETCD_CERTS_HASH_FILE"; then
+ ocf_exit_reason "cannot read/write to certificate hash file $ETCD_CERTS_HASH_FILE"
+ exit $OCF_ERR_GENERIC
+ fi
+
return $OCF_SUCCESS
}
@@ -1922,6 +2006,7 @@ CONTAINER=$OCF_RESKEY_name
POD_MANIFEST_COPY="${OCF_RESKEY_config_location}/pod.yaml"
ETCD_CONFIGURATION_FILE="${OCF_RESKEY_config_location}/config.yaml"
ETCD_BACKUP_FILE="${OCF_RESKEY_backup_location}/config-previous.tar.gz"
+ETCD_CERTS_HASH_FILE="${OCF_RESKEY_config_location}/certs.hash"
# 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,115 @@
From 6a5608f02a657cf006b6d44d31200342c4bd19b9 Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Tue, 28 Oct 2025 12:47:10 +0100
Subject: [PATCH] podman-etcd: compute dynamic revision bump from maxRaftIndex
(#2087)
Replace hardcoded 1 billion revision bump with dynamic calculation based
on 20% of the last known maxRaftIndex from revision.json.
This aligns with the logic used by cluster-etcd-operator's
quorum-restore-pod utility and ensures the bump amount is proportional
to the cluster's actual revision state.
The implementation:
- Adds compute_bump_revision() function with safe fallback to 1bn
default
- Extracts magic values to named constants
(ETCD_REVISION_BUMP_PERCENTAGE, ETCD_BUMP_REV_DEFAULT,
ETCD_REVISION_JSON)
- Validates computed values (non-zero, not exceeding default)
- Logs computation results for debugging
Reference:
https://github.com/openshift/cluster-etcd-operator/blob/215998939f5223da9166
22c71fd07d17656faf6b/bindata/etcd/quorum-restore-pod.yaml#L26-L34
---
heartbeat/podman-etcd | 38 ++++++++++++++++++++++++++++++++++----
1 file changed, 34 insertions(+), 4 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index b8dfb2f9e..551d37a20 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -619,16 +619,43 @@ prepare_env() {
LISTEN_METRICS_URLS="0.0.0.0"
}
+compute_bump_revision() {
+ # Same logic used by cluster-etcd-operator quorum-restore-pod utility.
+ # see https://github.com/openshift/cluster-etcd-operator/blob/215998939f5223da916622c71fd07d17656faf6b/bindata/etcd/quorum-restore-pod.yaml#L26-L34
+ # set a default value: 1bn would be an etcd running at 1000 writes/s for about eleven days.
+ BUMP_REV=$ETCD_BUMP_REV_DEFAULT
+ if [ ! -f "${ETCD_REVISION_JSON}" ]; then
+ ocf_log err "could not compute bump revision: ${ETCD_REVISION_JSON} not found. Defaulting to ${ETCD_BUMP_REV_DEFAULT} revision bump"
+ return
+ fi
+
+ # this will bump by the amount of 20% of the last known live revision.
+ if ! COMPUTED_BUMP=$(jq -r "(.maxRaftIndex*${ETCD_REVISION_BUMP_PERCENTAGE}|floor)" "${ETCD_REVISION_JSON}"); then
+ ocf_log err "could not compute maxRaftIndex for bump revision, jq error code: $?. Defaulting to ${ETCD_BUMP_REV_DEFAULT} revision bump"
+ return
+ fi
+
+ if [ -z "${COMPUTED_BUMP}" ] || [ "${COMPUTED_BUMP}" -le 0 ] || [ "${COMPUTED_BUMP}" -gt "${ETCD_BUMP_REV_DEFAULT}" ]; then
+ ocf_log err "computed bump revision (${COMPUTED_BUMP}) is invalid. Defaulting to ${ETCD_BUMP_REV_DEFAULT} revision bump"
+ return
+ fi
+
+ BUMP_REV="${COMPUTED_BUMP}"
+ ocf_log info "bumping etcd revisions by ${BUMP_REV}"
+}
generate_etcd_configuration() {
if is_force_new_cluster; then
+ compute_bump_revision
# The embedded newline is required for correct YAML formatting.
FORCE_NEW_CLUSTER_CONFIG="force-new-cluster: true
-force-new-cluster-bump-amount: 1000000000"
+force-new-cluster-bump-amount: $BUMP_REV"
else
FORCE_NEW_CLUSTER_CONFIG="force-new-cluster: false"
fi
+ # the space indentation for client-transport-security and peer-transport-security
+ # is required for correct YAML formatting.
cat > "$ETCD_CONFIGURATION_FILE" << EOF
logger: zap
log-level: info
@@ -707,7 +734,7 @@ attribute_node_cluster_id()
{
local action="$1"
local value
- if ! value=$(jq -r ".clusterId" /var/lib/etcd/revision.json); then
+ if ! value=$(jq -r ".clusterId" "$ETCD_REVISION_JSON"); then
rc=$?
ocf_log err "could not get cluster_id, error code: $rc"
return "$rc"
@@ -745,7 +772,7 @@ attribute_node_revision()
local value
local attribute="revision"
- if ! value=$(jq -r ".maxRaftIndex" /var/lib/etcd/revision.json); then
+ if ! value=$(jq -r ".maxRaftIndex" "$ETCD_REVISION_JSON"); then
rc=$?
ocf_log err "could not get $attribute, error code: $rc"
return "$rc"
@@ -1456,7 +1483,7 @@ can_reuse_container() {
# If the container does not exist it cannot be reused
- if ! container_exists; then
+ if ! container_exists; then
OCF_RESKEY_reuse=0
return "$OCF_SUCCESS"
fi
@@ -2006,6 +2033,9 @@ CONTAINER=$OCF_RESKEY_name
POD_MANIFEST_COPY="${OCF_RESKEY_config_location}/pod.yaml"
ETCD_CONFIGURATION_FILE="${OCF_RESKEY_config_location}/config.yaml"
ETCD_BACKUP_FILE="${OCF_RESKEY_backup_location}/config-previous.tar.gz"
+ETCD_REVISION_JSON="/var/lib/etcd/revision.json"
+ETCD_REVISION_BUMP_PERCENTAGE=0.2
+ETCD_BUMP_REV_DEFAULT=1000000000
ETCD_CERTS_HASH_FILE="${OCF_RESKEY_config_location}/certs.hash"
# Note: we currently monitor podman containers by with the "podman exec"

View File

@ -0,0 +1,23 @@
From eac983c14f4695f491fe430a78d8d18a1481c60c Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Wed, 29 Oct 2025 15:15:54 +0100
Subject: [PATCH] oracle: improve monpassword description
---
heartbeat/oracle | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/heartbeat/oracle b/heartbeat/oracle
index 8cf4e3649c..c85e499833 100755
--- a/heartbeat/oracle
+++ b/heartbeat/oracle
@@ -132,8 +132,7 @@ that the password for this user does not expire.
<longdesc lang="en">
Password for the monitoring user. Make sure
that the password for this user does not expire.
-Need to explicitly set a password to a new monitor
-user for the security reason.
+Set to avoid using the agents default password for "monuser".
</longdesc>
<shortdesc lang="en">monpassword</shortdesc>
<content type="string" default="$OCF_RESKEY_monpassword_default" />

View File

@ -0,0 +1,222 @@
From e8fb2ad9cc14e91b74b5cde1e012d92afcddb1a5 Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Sat, 25 Oct 2025 17:27:42 +0200
Subject: [PATCH] podman-etcd: add container crash detection with coordinated
recovery
This change prevents the agent from starting prematurely when the etcd
container has failed. Previously, an early start would cause the agent
to block while waiting for peer-initiated recovery. This blocking
prevented Pacemaker from allowing the surviving agent to stop and
properly recover the cluster.
The change introduces `container_health_check` function to monitor the
container's state and catch etcd failures. This check uses a state file
to distinguish between a planned shutdown and an unexpected failure:
* Container Running: The state file is created or updated with the
current epoch (timestamp). Returns: "healthy".
* Container Not Running + No State File: It's the first check. Returns:
"not-running".
* Container Not Running + State File: An unexpected failure is detected.
* If force_new_cluster is set, the status is: "failed-restart-now".
* Otherwise, the status is: "failed-wait-for-peer".
The state file is written in a temporary directory (HA_RSCTMP) to ensure
automatic cleanup on reboot. It is also explicitly removed in
`podman_start` and `podman_stop` to mark planned transitions.
A new helper function `get_time_since_last_heartbeat()` calculates
elapsed time since the last healthy check for diagnostic logging.
Monitor behavior changes:
* failed-wait-for-peer: Returns OCF_SUCCESS to keep resource running
while waiting for peer-initiated recovery, as the agent is not able
to recover the cluster from a failed state.
* failed-restart-now: Returns OCF_ERR_GENERIC to trigger restart once
peer has set force_new_cluster
---
heartbeat/podman-etcd | 133 +++++++++++++++++++++++++++++++++++++++---
1 file changed, 124 insertions(+), 9 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index b8dfb2f9e..d596c6f2a 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -1226,22 +1226,122 @@ podman_simple_status()
return $rc
}
-podman_monitor()
+# get_time_since_last_heartbeat returns the time in seconds since the heartbeat file was last updated.
+#
+# Returns: time in seconds since last heartbeat, or empty string if file doesn't exist
+get_time_since_last_heartbeat()
{
+ local last_heartbeat
+
+ if [ ! -f "$CONTAINER_HEARTBEAT_FILE" ]; then
+ return
+ fi
+
+ last_heartbeat=$(cat "$CONTAINER_HEARTBEAT_FILE")
+ echo $(($(date +%s) - last_heartbeat))
+}
+
+# container_health_check performs comprehensive health monitoring for the container.
+# This function allows coordinated failure handling where the agent waits for
+# peer-initiated cluster recovery in case of container failure.
+#
+# Uses a state file to track container state:
+# - Container running: Update state file with current epoch, return "healthy"
+# - Container not running + no state file: Return "not-running" (never checked before)
+# - Container not running + state file: Failure detected, check force_new_cluster
+# - If force_new_cluster set: Return "failed-restart-now"
+# - Otherwise: Return "failed-wait-for-peer"
+#
+# Returns: healthy, not-running, failed-restart-now, failed-wait-for-peer
+
+container_health_check()
+{
+ local rc
+
# We rely on running podman exec to monitor the container
# state because that command seems to be less prone to
# performance issue under IO load.
#
# For probes to work, we expect cmd_exec to be able to report
- # when a container is not running. Here, we're not interested
- # in distinguishing whether it's stopped or non existing
- # (there's function container_exists for that)
+ # when a container is not running. Here, we're not interested
+ # in distinguishing whether it's stopped or non existing
+ # (there's function container_exists for that)
+ # For monitor, however, we still need to know if it has stopped
+ # recently (i.e. a failure), or not (fresh start)
monitor_cmd_exec
rc=$?
- if [ $rc -ne 0 ]; then
- return $rc
+ if [ "$rc" -eq 0 ]; then
+ # Container is running - update state file with current epoch
+ local current_epoch
+ current_epoch=$(date +%s)
+ if ! echo "$current_epoch" > "$CONTAINER_HEARTBEAT_FILE"; then
+ ocf_log warn "Failed to update container heartbeat file, error code: $?"
+ # wait for peer to detect any real issue with the etcd cluster or wait for the
+ # next monitor interval
+ echo "failed-wait-for-peer"
+ return
+ fi
+ echo "healthy"
+ return
fi
+ # Check if state file exists (was container running on last check?)
+ if [ ! -f "$CONTAINER_HEARTBEAT_FILE" ]; then
+ # No state file - container was never checked before
+ ocf_log debug "Container ${CONTAINER} has no previous state"
+ echo "not-running"
+ # NOTE: this is where the probe is expected to exit, keeping the logic
+ # quick and less prone to performance issue under IO load.
+ return
+ fi
+
+ # State file exists - the container failed, check recovery status in this lifecycle
+ local time_since_heartbeat
+ time_since_heartbeat=$(get_time_since_last_heartbeat)
+ ocf_log err "Container ${CONTAINER} failed (last healthy: ${time_since_heartbeat}s ago)"
+
+ # Check if peer has set force_new_cluster for recovery
+ local fnc_holders
+ if ! fnc_holders=$(get_force_new_cluster); then
+ ocf_log err "Could not detect peer-initiated recovery. Checking again in the next monitor cycle"
+ echo "failed-wait-for-peer"
+ return
+ fi
+
+ if [ -n "$fnc_holders" ]; then
+ ocf_log debug "force_new_cluster detected (set by: $fnc_holders), triggering restart"
+ echo "failed-restart-now"
+ return
+ fi
+
+ echo "failed-wait-for-peer"
+}
+
+podman_monitor()
+{
+ local container_health_state
+
+ container_health_state=$(container_health_check)
+ case "$container_health_state" in
+ healthy)
+ # Continue with normal monitoring
+ ;;
+ not-running)
+ return $OCF_NOT_RUNNING
+ ;;
+ failed-restart-now)
+ return $OCF_ERR_GENERIC
+ ;;
+ failed-wait-for-peer)
+ # Continue running, waiting for peer recovery
+ return $OCF_SUCCESS
+ ;;
+ *)
+ ocf_log err "Unknown health state: $container_health_state"
+ return $OCF_ERR_GENERIC
+ ;;
+ esac
+
# Check if certificate files have changed, if they have, etcd needs to be restarted
if ! etcd_certificates_hash_manager "check"; then
return $OCF_ERR_GENERIC
@@ -1533,6 +1633,12 @@ podman_start()
local pod_was_running=false
ocf_log notice "podman-etcd start"
+
+ # Clear container health check state file
+ if ! rm -f "$CONTAINER_HEARTBEAT_FILE"; then
+ ocf_log err "could not delete container health check state file"
+ fi
+
attribute_node_ip update
attribute_node_cluster_id update
attribute_node_revision update
@@ -1849,15 +1955,21 @@ podman_stop()
local rc
ocf_log notice "podman-etcd stop"
+
+ # Clear container health check state file
+ if ! rm -f "$CONTAINER_HEARTBEAT_FILE"; then
+ ocf_log err "could not delete container health check state file"
+ fi
+
+ 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"
return $OCF_SUCCESS
fi
- attribute_node_revision update
- attribute_node_cluster_id update
-
if ! member_id=$(attribute_node_member_id get); then
ocf_log err "error leaving members list: could not get member-id"
else
@@ -2007,6 +2119,9 @@ POD_MANIFEST_COPY="${OCF_RESKEY_config_location}/pod.yaml"
ETCD_CONFIGURATION_FILE="${OCF_RESKEY_config_location}/config.yaml"
ETCD_BACKUP_FILE="${OCF_RESKEY_backup_location}/config-previous.tar.gz"
ETCD_CERTS_HASH_FILE="${OCF_RESKEY_config_location}/certs.hash"
+# State file location: Uses HA_RSCTMP to ensure automatic cleanup on reboot.
+# This is intentional - reboots are controlled stops, not failures requiring detection.
+CONTAINER_HEARTBEAT_FILE=${HA_RSCTMP}/podman-container-last-running
# 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,47 @@
From a155018f6d65edf99493804dad99412b50d13e6c Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Wed, 5 Nov 2025 13:48:38 +0100
Subject: [PATCH] podman-etcd: fix count of fnc holders in
container_health_check
The variable `fnc_holders` (a list of nodes that have force_new_cluster
CIB attribute set) can contain empty spaces. Because of this, the
shell's simple `-n` test is not enough to establish if there are no
`fnc_holders`.
Fixed counting the number of words inside the variable.
Moreover
* Enhanced comment for clarity.
* Log level changed to `info`. We want visibility when the monitor
detects the peer node is ready for recovery, and this is rare enough
not to flood the logs.
---
heartbeat/podman-etcd | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 5bdc6d184..7795130a6 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -1366,7 +1366,7 @@ container_health_check()
return
fi
- # State file exists - the container failed, check recovery status in this lifecycle
+ # Could not execute monitor check command and state file exists - the container failed, check recovery status in this lifecycle
local time_since_heartbeat
time_since_heartbeat=$(get_time_since_last_heartbeat)
ocf_log err "Container ${CONTAINER} failed (last healthy: ${time_since_heartbeat}s ago)"
@@ -1379,8 +1379,9 @@ container_health_check()
return
fi
- if [ -n "$fnc_holders" ]; then
- ocf_log debug "force_new_cluster detected (set by: $fnc_holders), triggering restart"
+ local fnc_holder_count=$(echo "$fnc_holders" | wc -w)
+ if [ "$fnc_holder_count" -gt 0 ]; then
+ ocf_log info "force_new_cluster detected (set by: $fnc_holders), triggering restart"
echo "failed-restart-now"
return
fi

View File

@ -0,0 +1,158 @@
From 48455cb6cef9c5b849045bc838bc2b5ccd01b0fe Mon Sep 17 00:00:00 2001
From: Klaus Wenninger <klaus.wenninger@aon.at>
Date: Fri, 7 Nov 2025 17:06:57 +0100
Subject: [PATCH 1/3] storage_mon: refactor removing basically duplicate code
---
tools/storage_mon.c | 45 ++++++++++++++++-----------------------------
1 file changed, 16 insertions(+), 29 deletions(-)
diff --git a/tools/storage_mon.c b/tools/storage_mon.c
index 27d2ff1d1..fa9bd0cbc 100644
--- a/tools/storage_mon.c
+++ b/tools/storage_mon.c
@@ -119,6 +119,8 @@ static void *test_device(const char *device, int verbose, int inject_error_perce
int device_fd;
int res;
off_t seek_spot;
+ int sec_size = 512;
+ void *buffer;
if (verbose) {
printf("Testing device %s\n", device);
@@ -164,9 +166,6 @@ static void *test_device(const char *device, int verbose, int inject_error_perce
}
if (flags & O_DIRECT) {
- int sec_size = 0;
- void *buffer;
-
#ifdef __FreeBSD__
res = ioctl(device_fd, DIOCGSECTORSIZE, &sec_size);
#else
@@ -176,33 +175,21 @@ static void *test_device(const char *device, int verbose, int inject_error_perce
PRINT_STORAGE_MON_ERR("Failed to get block device sector size for %s: %s", device, strerror(errno));
goto error;
}
+ }
- if (posix_memalign(&buffer, sysconf(_SC_PAGESIZE), sec_size) != 0) {
- PRINT_STORAGE_MON_ERR("Failed to allocate aligned memory: %s", strerror(errno));
- goto error;
- }
- res = read(device_fd, buffer, sec_size);
- free(buffer);
- if (res < 0) {
- PRINT_STORAGE_MON_ERR("Failed to read %s: %s", device, strerror(errno));
- goto error;
- }
- if (res < sec_size) {
- PRINT_STORAGE_MON_ERR("Failed to read %d bytes from %s, got %d", sec_size, device, res);
- goto error;
- }
- } else {
- char buffer[512];
-
- res = read(device_fd, buffer, sizeof(buffer));
- if (res < 0) {
- PRINT_STORAGE_MON_ERR("Failed to read %s: %s", device, strerror(errno));
- goto error;
- }
- if (res < (int)sizeof(buffer)) {
- PRINT_STORAGE_MON_ERR("Failed to read %ld bytes from %s, got %d", sizeof(buffer), device, res);
- goto error;
- }
+ if (posix_memalign(&buffer, sysconf(_SC_PAGESIZE), sec_size) != 0) {
+ PRINT_STORAGE_MON_ERR("Failed to allocate aligned memory: %s", strerror(errno));
+ goto error;
+ }
+ res = read(device_fd, buffer, sec_size);
+ free(buffer);
+ if (res < 0) {
+ PRINT_STORAGE_MON_ERR("Failed to read %s: %s", device, strerror(errno));
+ goto error;
+ }
+ if (res < sec_size) {
+ PRINT_STORAGE_MON_ERR("Failed to read %d bytes from %s, got %d", sec_size, device, res);
+ goto error;
}
/* Fake an error */
From 310f224fc7d9a6f4fca234f10696e6049c8f2666 Mon Sep 17 00:00:00 2001
From: Klaus Wenninger <klaus.wenninger@aon.at>
Date: Fri, 7 Nov 2025 17:14:06 +0100
Subject: [PATCH 2/3] storage_mon.c: refactor moving up getting blocksize
if that fails we can bail out without unnecessary seek
---
tools/storage_mon.c | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/tools/storage_mon.c b/tools/storage_mon.c
index fa9bd0cbc..960266a74 100644
--- a/tools/storage_mon.c
+++ b/tools/storage_mon.c
@@ -152,6 +152,18 @@ static void *test_device(const char *device, int verbose, int inject_error_perce
PRINT_STORAGE_MON_INFO("%s: opened %s O_DIRECT, size=%zu", device, (flags & O_DIRECT)?"with":"without", devsize);
}
+ if (flags & O_DIRECT) {
+#ifdef __FreeBSD__
+ res = ioctl(device_fd, DIOCGSECTORSIZE, &sec_size);
+#else
+ res = ioctl(device_fd, BLKSSZGET, &sec_size);
+#endif
+ if (res < 0) {
+ PRINT_STORAGE_MON_ERR("Failed to get block device sector size for %s: %s", device, strerror(errno));
+ goto error;
+ }
+ }
+
/* Don't fret about real randomness */
srand(time(NULL) + getpid());
/* Pick a random place on the device - sector aligned */
@@ -165,18 +177,6 @@ static void *test_device(const char *device, int verbose, int inject_error_perce
PRINT_STORAGE_MON_INFO("%s: reading from pos %ld", device, seek_spot);
}
- if (flags & O_DIRECT) {
-#ifdef __FreeBSD__
- res = ioctl(device_fd, DIOCGSECTORSIZE, &sec_size);
-#else
- res = ioctl(device_fd, BLKSSZGET, &sec_size);
-#endif
- if (res < 0) {
- PRINT_STORAGE_MON_ERR("Failed to get block device sector size for %s: %s", device, strerror(errno));
- goto error;
- }
- }
-
if (posix_memalign(&buffer, sysconf(_SC_PAGESIZE), sec_size) != 0) {
PRINT_STORAGE_MON_ERR("Failed to allocate aligned memory: %s", strerror(errno));
goto error;
From ac19911ce550d5eca42be6cb44632384bdf8e1c9 Mon Sep 17 00:00:00 2001
From: Klaus Wenninger <klaus.wenninger@aon.at>
Date: Fri, 7 Nov 2025 17:18:45 +0100
Subject: [PATCH 3/3] storage_mon.c: fix block-seek mask deriving it from the
block-size
now this is as well working for e.g. 4K block-devices
---
tools/storage_mon.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/storage_mon.c b/tools/storage_mon.c
index 960266a74..6c4555f04 100644
--- a/tools/storage_mon.c
+++ b/tools/storage_mon.c
@@ -167,7 +167,7 @@ static void *test_device(const char *device, int verbose, int inject_error_perce
/* Don't fret about real randomness */
srand(time(NULL) + getpid());
/* Pick a random place on the device - sector aligned */
- seek_spot = (rand() % (devsize-1024)) & 0xFFFFFFFFFFFFFE00;
+ seek_spot = (rand() % (devsize-sec_size)) & ~(((off_t) sec_size)-1);
res = lseek(device_fd, seek_spot, SEEK_SET);
if (res < 0) {
PRINT_STORAGE_MON_ERR("Failed to seek %s: %s", device, strerror(errno));

View File

@ -0,0 +1,106 @@
From d5b4428e6cd66fd47680531ff0244d9b56e4e4c2 Mon Sep 17 00:00:00 2001
From: Pablo Fontanilla <pfontani@redhat.com>
Date: Tue, 14 Oct 2025 11:57:09 +0200
Subject: [PATCH 1/2] Redo counting of active_resources
---
heartbeat/podman-etcd | 46 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 44 insertions(+), 2 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index e1425ec02..dbf16918d 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -1029,6 +1029,48 @@ get_peer_node_name() {
crm_node -l | awk '{print $2}' | grep -v "$NODENAME"
}
+# Calculate the count of truly active resources by excluding those being stopped.
+# According to Pacemaker documentation, during "Post-notification (stop) /
+# Pre-notification (start)" transitions, the true active resource count should be:
+# Active resources = $OCF_RESKEY_CRM_meta_notify_active_resource
+# minus $OCF_RESKEY_CRM_meta_notify_stop_resource
+# This handles the case where a resource appears in both the active and stop lists
+# during rapid restart scenarios (e.g., process crash recovery).
+get_truly_active_resources_count() {
+ local active_list="$OCF_RESKEY_CRM_meta_notify_active_resource"
+ local stop_list="$OCF_RESKEY_CRM_meta_notify_stop_resource"
+ local truly_active=""
+
+ # If no active resources, return 0
+ if [ -z "$active_list" ]; then
+ echo "0"
+ return
+ fi
+
+ # If no resources being stopped, return count of active resources
+ if [ -z "$stop_list" ]; then
+ echo "$active_list" | wc -w
+ return
+ fi
+
+ # Filter out resources that are being stopped from the active list
+ for resource in $active_list; do
+ local is_stopping=0
+ for stop_resource in $stop_list; do
+ if [ "$resource" = "$stop_resource" ]; then
+ is_stopping=1
+ break
+ fi
+ done
+ if [ $is_stopping -eq 0 ]; then
+ truly_active="$truly_active $resource"
+ fi
+ done
+
+ # Count the truly active resources (trim leading space and count words)
+ echo "$truly_active" | wc -w
+}
+
get_all_etcd_endpoints() {
for node in $(echo "$OCF_RESKEY_node_ip_map" | sed "s/\s//g;s/;/ /g"); do
name=$(echo "$node" | cut -d: -f1)
@@ -1529,8 +1571,8 @@ podman_start()
# - 0 active agents, 1 starting: we are starting; the peer is not starting
# - 0 active agents, 2 starting: both agents are starting simultaneously
local active_resources_count
- active_resources_count=$(echo "$OCF_RESKEY_CRM_meta_notify_active_resource" | wc -w)
- ocf_log info "found '$active_resources_count' active etcd resources (meta notify environment variable: '$OCF_RESKEY_CRM_meta_notify_active_resource')"
+ active_resources_count=$(get_truly_active_resources_count)
+ 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
From 0114ddf83c95122a7f9fe9f704f864242cdb284a Mon Sep 17 00:00:00 2001
From: Pablo Fontanilla <pfontani@redhat.com>
Date: Wed, 29 Oct 2025 12:49:17 +0100
Subject: [PATCH 2/2] Update truly active resources count with safer empty
calculation
---
heartbeat/podman-etcd | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index dbf16918d..8fc92a537 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -1042,13 +1042,15 @@ get_truly_active_resources_count() {
local truly_active=""
# If no active resources, return 0
- if [ -z "$active_list" ]; then
+ # Use word count to handle whitespace-only values
+ if [ "$(echo "$active_list" | wc -w)" -eq 0 ]; then
echo "0"
return
fi
# If no resources being stopped, return count of active resources
- if [ -z "$stop_list" ]; then
+ # Use word count to handle whitespace-only values
+ if [ "$(echo "$stop_list" | wc -w)" -eq 0 ]; then
echo "$active_list" | wc -w
return
fi

View File

@ -0,0 +1,161 @@
From 578e6d982e5ab705dac216cecf85c50fe3842af5 Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Sun, 16 Nov 2025 19:40:30 +0100
Subject: [PATCH] OCPBUGS-60098: podman-etcd: prevent last active member from
leaving the etcd member list
When stopping etcd instances, simultaneous member removal from both
nodes can corrupt the etcd Write-Ahead Log (WAL). This change implements
a two-part solution:
1. Concurrent stop protection: When multiple nodes are stopping, the
alphabetically second node delays its member removal by 10
seconds. This prevents simultaneous member list updates that can
corrupt WAL.
2. Last member detection: Checks active resource count after any
delay. If this is the last active member, skips member removal to
avoid leaving an empty cluster.
Additionally, reorders podman_stop() to clear the member_id attribute
after leaving the member list, ensuring the attribute reflects actual
cluster state during shutdown.
---
heartbeat/podman-etcd | 86 ++++++++++++++++++++++++++++++++++---------
1 file changed, 69 insertions(+), 17 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 7795130a6..7b6e08f11 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -1341,6 +1341,11 @@ container_health_check()
# recently (i.e. a failure), or not (fresh start)
monitor_cmd_exec
rc=$?
+ if [ "$rc" -ne 0 ]; then
+ ocf_log info "Container ${CONTAINER} not-running"
+ echo "not-running"
+ return
+ fi
if [ "$rc" -eq 0 ]; then
# Container is running - update state file with current epoch
local current_epoch
@@ -1639,7 +1644,7 @@ can_reuse_container() {
OCF_RESKEY_reuse=0
return "$OCF_SUCCESS"
fi
-
+
if ! filtered_original_pod_manifest=$(filter_pod_manifest "$OCF_RESKEY_pod_manifest"); then
return $OCF_ERR_GENERIC
fi
@@ -1866,7 +1871,7 @@ podman_start()
fi
if ocf_is_true "$JOIN_AS_LEARNER"; then
- local wait_timeout_sec=$((10*60))
+ local wait_timeout_sec=60
local poll_interval_sec=5
local retries=$(( wait_timeout_sec / poll_interval_sec ))
@@ -2021,6 +2026,64 @@ podman_start()
done
}
+# leave_etcd_member_list removes the current node from the etcd member list during
+# shutdown to ensure clean cluster state.
+#
+# Skips removal if this is the standalone (last) node. When both nodes are stopping
+# concurrently, delays the second node to prevent simultaneous member removal that
+# could corrupt the etcd WAL.
+leave_etcd_member_list()
+{
+ if ! member_id=$(attribute_node_member_id get); then
+ ocf_log err "error leaving members list: could not get member-id"
+ return
+ fi
+
+ if is_standalone; then
+ ocf_log info "last member. Not leaving the member list"
+ return
+ fi
+
+ local stopping_resources_count
+ stopping_resources_count=$(echo "$OCF_RESKEY_CRM_meta_notify_stop_resource" | wc -w)
+ ocf_log info "found '$stopping_resources_count' stopping etcd resources (stop: '$OCF_RESKEY_CRM_meta_notify_stop_resource')"
+ if [ "$stopping_resources_count" -gt 1 ]; then
+ # Prevent WAL corruption by delaying the alphabetically second node's member
+ # removal when both nodes are stopping concurrently.
+ local delayed_node
+
+ node_names_sorted=$(echo "$OCF_RESKEY_node_ip_map" | sed 's/:[^;]*//g; s/;/ /g' | tr ' ' '\n' | sort | tr '\n' ' ')
+ delayed_node="$(echo "$node_names_sorted" | cut -d' ' -f2)"
+
+ if [ -z "$delayed_node" ]; then
+ ocf_log warn "could not determine node to be delayed: not leaving the member list"
+ return
+ fi
+
+ if [ "$NODENAME" = "$delayed_node" ]; then
+ ocf_log info "delaying stop for ${DELAY_SECOND_NODE_LEAVE_SEC}s to prevent simultaneous etcd member removal"
+ sleep $DELAY_SECOND_NODE_LEAVE_SEC
+ fi
+ fi
+
+ # Ensure we're not the last active resource before leaving. The `standalone_node` property
+ # may not be set if stop was called before monitor check, or after the delayed node waited.
+ local active_resources_count
+ active_resources_count=$(get_truly_active_resources_count)
+ if [ "$active_resources_count" -lt 1 ]; then
+ ocf_log info "last member. Not leaving the member list"
+ return
+ fi
+
+ 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_log err "error leaving members list, error code: $rc"
+ fi
+}
+
podman_stop()
{
local timeout=60
@@ -2039,24 +2102,12 @@ podman_stop()
podman_simple_status
if [ $? -eq $OCF_NOT_RUNNING ]; then
ocf_log info "could not leave members list: etcd container not running"
+ attribute_node_member_id clear
return $OCF_SUCCESS
fi
- if ! member_id=$(attribute_node_member_id get); then
- ocf_log err "error leaving members list: could not get member-id"
- else
- # TODO: is it worth/possible to check the current status instead than relying on cached attributes?
- if is_standalone; then
- ocf_log info "last member. Not leaving the member list"
- else
- ocf_log info "leaving members list as member with ID $member_id"
- endpoint="$(ip_url $(attribute_node_ip get)):2379"
- if ! ocf_run podman exec "$CONTAINER" etcdctl member remove "$member_id" --endpoints="$endpoint"; then
- rc=$?
- ocf_log err "error leaving members list, error code: $rc"
- fi
- fi
- fi
+ leave_etcd_member_list
+ # clear node_member_id CIB attribute only after leaving the member list
attribute_node_member_id clear
if [ -n "$OCF_RESKEY_CRM_meta_timeout" ]; then
@@ -2197,6 +2248,7 @@ ETCD_CERTS_HASH_FILE="${OCF_RESKEY_config_location}/certs.hash"
# State file location: Uses HA_RSCTMP to ensure automatic cleanup on reboot.
# 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
# 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,42 @@
From 29df4255c5f65ea94fb6de997805dca65e31071c Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Mon, 24 Nov 2025 12:21:55 +0100
Subject: [PATCH] podman-etcd: remove test code (#2103)
---
heartbeat/podman-etcd | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 7b6e08f11..b1f52cd5c 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -1341,11 +1341,6 @@ container_health_check()
# recently (i.e. a failure), or not (fresh start)
monitor_cmd_exec
rc=$?
- if [ "$rc" -ne 0 ]; then
- ocf_log info "Container ${CONTAINER} not-running"
- echo "not-running"
- return
- fi
if [ "$rc" -eq 0 ]; then
# Container is running - update state file with current epoch
local current_epoch
@@ -1644,7 +1639,6 @@ can_reuse_container() {
OCF_RESKEY_reuse=0
return "$OCF_SUCCESS"
fi
-
if ! filtered_original_pod_manifest=$(filter_pod_manifest "$OCF_RESKEY_pod_manifest"); then
return $OCF_ERR_GENERIC
fi
@@ -1871,7 +1865,7 @@ podman_start()
fi
if ocf_is_true "$JOIN_AS_LEARNER"; then
- local wait_timeout_sec=60
+ local wait_timeout_sec=$((10*60))
local poll_interval_sec=5
local retries=$(( wait_timeout_sec / poll_interval_sec ))

View File

@ -0,0 +1,107 @@
From 5cc74acd67c294da36b3f40e44842a82aa7d0957 Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Wed, 26 Nov 2025 11:43:25 +0100
Subject: [PATCH] OCPEDGE-2213: podman-etcd: fix to prevent learner from
starting before cluster is ready (#2098)
* OCPEDGE-2213: fix(podman-etcd): prevent learner from starting before cluster is ready
Clear stale learner_node attribute during stop and on restart when no
active resources exist, ensuring learner always waits for peer
availability.
* fix: podman-etcd should cleanup standalone/learner attributes when promotion succeeds
* fix: remove misleading endpoint IP from log
---
heartbeat/podman-etcd | 33 +++++++++++++++++++--------------
1 file changed, 19 insertions(+), 14 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index b1f52cd5c..3e3f1d60e 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -880,7 +880,7 @@ add_member_as_learner()
local endpoint_url=$(ip_url $(attribute_node_ip get))
local peer_url=$(ip_url $member_ip)
- ocf_log info "add $member_name ($member_ip, $endpoint_url) to the member list as learner"
+ 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)
rc=$?
if [ $rc -ne 0 ]; then
@@ -1032,7 +1032,7 @@ promote_learner_member()
if ! ocf_run podman exec "${CONTAINER}" etcdctl member promote "$learner_member_id_hex" 2>&1; then
# promotion is expected to fail if the peer is not yet up-to-date
ocf_log info "could not promote member $learner_member_id_hex, error code: $?"
- return $OCF_SUCCESS
+ return $OCF_ERR_GENERIC
fi
ocf_log info "successfully promoted member '$learner_member_id_hex'"
return $OCF_SUCCESS
@@ -1063,19 +1063,19 @@ reconcile_member_state()
fi
if [ -n "$learner_member_id" ]; then
- promote_learner_member "$learner_member_id"
- return $?
- fi
-
- if [ -z "$learner_member_id" ]; then
- if ! clear_standalone_node; then
- ocf_log error "could not clear standalone_node attribute, error code: $?"
- return $OCF_ERR_GENERIC
- fi
- if ! attribute_learner_node clear; then
- ocf_log error "could not clear learner_node attribute, error code: $?"
+ if ! promote_learner_member "$learner_member_id"; then
return $OCF_ERR_GENERIC
fi
+ # promotion succeded: continue to clear standalone_node and learner_node
+ fi
+
+ if ! clear_standalone_node; then
+ ocf_log error "could not clear standalone_node attribute, error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
+ if ! attribute_learner_node clear; then
+ ocf_log error "could not clear learner_node attribute, error code: $?"
+ return $OCF_ERR_GENERIC
fi
return $OCF_SUCCESS
@@ -1258,6 +1258,7 @@ manage_peer_membership()
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
@@ -1369,7 +1370,7 @@ container_health_check()
# Could not execute monitor check command and state file exists - the container failed, check recovery status in this lifecycle
local time_since_heartbeat
time_since_heartbeat=$(get_time_since_last_heartbeat)
- ocf_log err "Container ${CONTAINER} failed (last healthy: ${time_since_heartbeat}s ago)"
+ ocf_log err "Container ${CONTAINER} failed (last healthy: ${time_since_heartbeat}s ago, error code: $rc)"
# Check if peer has set force_new_cluster for recovery
local fnc_holders
@@ -1795,6 +1796,9 @@ podman_start()
fi
;;
0)
+ # No active resources: clear any stale learner_node attribute from previous failed session
+ ocf_log debug "clearing stale learner_node attribute (safe when active_resources_count=0)"
+ attribute_learner_node clear
# count how many agents are starting now
local start_resources_count
start_resources_count=$(echo "$OCF_RESKEY_CRM_meta_notify_start_resource" | wc -w)
@@ -2090,6 +2094,7 @@ podman_stop()
ocf_log err "could not delete container health check state file"
fi
+ attribute_learner_node clear
attribute_node_revision update
attribute_node_cluster_id update

View File

@ -0,0 +1,146 @@
From 192b0ecbe015e8b8a4d32f8b066ead3a6dba0589 Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Tue, 2 Dec 2025 10:01:01 +0100
Subject: [PATCH] OCPEDGE-2231: podman-etcd: improve error handling to support
retry on start errors (#2105)
* podman-etcd: improve add_member_as_learner error log
Improving add_member_as_learner error log to better debug rare issue
when the podman exec command returns error, but the etcd member is added
to the list anyway. This is critical as the `learner_node` attribute
won't be cleaned up anymore.
Signed-off-by: Carlo Lobrano <c.lobrano@gmail.com>
* podman-etcd: remove duplicated check for container already started
* podman-etcd: improve error return codes to support start retries
Improved and/or changed some returns code to allow or forbid retry in
case of start errors.
see: OCPEDGE-2231
---------
Signed-off-by: Carlo Lobrano <c.lobrano@gmail.com>
---
heartbeat/podman-etcd | 40 +++++++++++++++++++++++++---------------
1 file changed, 25 insertions(+), 15 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 3e3f1d60e..242226bb1 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -617,9 +617,13 @@ prepare_env() {
LISTEN_CLIENT_URLS="0.0.0.0"
LISTEN_PEER_URLS="0.0.0.0"
LISTEN_METRICS_URLS="0.0.0.0"
+
+ return $OCF_SUCCESS
}
compute_bump_revision() {
+ local rc
+
# Same logic used by cluster-etcd-operator quorum-restore-pod utility.
# see https://github.com/openshift/cluster-etcd-operator/blob/215998939f5223da916622c71fd07d17656faf6b/bindata/etcd/quorum-restore-pod.yaml#L26-L34
# set a default value: 1bn would be an etcd running at 1000 writes/s for about eleven days.
@@ -691,7 +695,13 @@ experimental-max-learners: 1
experimental-warning-apply-duration: $(convert_duration_in_nanoseconds "$ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION")
experimental-watch-progress-notify-interval: $(convert_duration_in_nanoseconds "$ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERVAL")
EOF
+ rc=$?
+ if [ $rc -ne 0 ]; then
+ ocf_log err "could not create etcd configuration, 'cat' error code: $rc"
+ return $OCF_ERR_CONFIGURED
+ fi
+ # Append cipher suites from the env variable where the entries are comma separated.
{
if [ -n "$ETCD_CIPHER_SUITES" ]; then
echo "cipher-suites:"
@@ -700,6 +710,13 @@ EOF
done
fi
} >> "$ETCD_CONFIGURATION_FILE"
+ rc=$?
+ if [ $rc -ne 0 ]; then
+ ocf_log err "could not append cipher suites to etcd configuration, error code: $rc"
+ return $OCF_ERR_CONFIGURED
+ fi
+
+ return $OCF_SUCCESS
}
archive_data_folder()
@@ -884,7 +901,7 @@ add_member_as_learner()
out=$(podman exec "${CONTAINER}" etcdctl --endpoints="$endpoint_url:2379" member add "$member_name" --peer-urls="$peer_url:2380" --learner)
rc=$?
if [ $rc -ne 0 ]; then
- ocf_log err "could not add $member_name as learner, error code: $rc"
+ ocf_log err "could not add $member_name as learner, error code $rc, etcdctl output: $out"
return $rc
fi
ocf_log info "$out"
@@ -1763,7 +1780,7 @@ podman_start()
fnc_holder_count=$(echo "$fnc_holders" | wc -w)
if [ "$fnc_holder_count" -gt 1 ]; then
ocf_exit_reason "force_new_cluster attribute is set on multiple nodes ($fnc_holders)"
- return "$OCF_ERR_GENERIC"
+ return "$OCF_ERR_CONFIGURED"
fi
if [ "$fnc_holder_count" -eq 1 ]; then
@@ -1837,7 +1854,7 @@ podman_start()
ocf_log info "same cluster_id and revision: start normal"
else
ocf_exit_reason "same revision but different cluster id"
- return "$OCF_ERR_GENERIC"
+ return "$OCF_ERR_CONFIGURED"
fi
fi
;;
@@ -1862,12 +1879,6 @@ podman_start()
run_opts="$run_opts --oom-score-adj=${OCF_RESKEY_oom}"
- # check to see if the container has already started
- podman_simple_status
- if [ $? -eq $OCF_SUCCESS ]; then
- return "$OCF_SUCCESS"
- fi
-
if ocf_is_true "$JOIN_AS_LEARNER"; then
local wait_timeout_sec=$((10*60))
local poll_interval_sec=5
@@ -1894,9 +1905,8 @@ podman_start()
ocf_log info "check for changes in pod manifest to decide if the container should be reused or replaced"
if ! can_reuse_container ; then
- rc="$?"
- ocf_log err "could not determine etcd container reuse strategy, rc: $rc"
- return "$rc"
+ ocf_log err "could not determine etcd container reuse strategy"
+ return $OCF_ERR_GENERIC
fi
# Archive current container and its configuration before creating
@@ -1912,13 +1922,13 @@ podman_start()
fi
if ! prepare_env; then
- ocf_log err "Could not prepare environment for podman, error code: $?"
+ ocf_log err "Could not prepare environment for podman"
return $OCF_ERR_GENERIC
fi
if ! generate_etcd_configuration; then
- ocf_log err "Could not generate etcd configuration, error code: $?"
- return $OCF_ERR_GENERIC
+ ocf_log err "Could not generate etcd configuration"
+ return $OCF_ERR_CONFIGURED
fi
run_opts="$run_opts \

View File

@ -0,0 +1,52 @@
From 8b70d5026fee0910a52f0fdefcaf930b2c0a3909 Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Wed, 3 Dec 2025 11:38:25 +0100
Subject: [PATCH] podman-etcd: sync environment variables with Pod manifest
The EXPERIMENTAL substring was removed from
ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION and
ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERNAL in the Pod
manifest. This change aligns our config with those updates.
NOTE: Some Etcd flags deprecated in v3.6 will be replaced in a future
change.
See: https://github.com/openshift/cluster-etcd-operator/pull/1507
---
heartbeat/podman-etcd | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 242226bb1..bb2900536 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -604,8 +604,8 @@ prepare_env() {
fi
ETCD_ELECTION_TIMEOUT=$(get_env_from_manifest "ETCD_ELECTION_TIMEOUT")
ETCD_ENABLE_PPROF=$(get_env_from_manifest "ETCD_ENABLE_PPROF")
- ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION=$(get_env_from_manifest "ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION")
- ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERVAL=$(get_env_from_manifest "ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERVAL")
+ ETCD_WARNING_APPLY_DURATION=$(get_env_from_manifest "ETCD_WARNING_APPLY_DURATION")
+ ETCD_WATCH_PROGRESS_NOTIFY_INTERVAL=$(get_env_from_manifest "ETCD_WATCH_PROGRESS_NOTIFY_INTERVAL")
ETCD_HEARTBEAT_INTERVAL=$(get_env_from_manifest "ETCD_HEARTBEAT_INTERVAL")
ETCD_QUOTA_BACKEND_BYTES=$(get_env_from_manifest "ETCD_QUOTA_BACKEND_BYTES")
ETCD_SOCKET_REUSE_ADDRESS=$(get_env_from_manifest "ETCD_SOCKET_REUSE_ADDRESS")
@@ -660,6 +660,7 @@ force-new-cluster-bump-amount: $BUMP_REV"
# the space indentation for client-transport-security and peer-transport-security
# is required for correct YAML formatting.
+ # TODO: replace flags deprecated in Etcd v3.6
cat > "$ETCD_CONFIGURATION_FILE" << EOF
logger: zap
log-level: info
@@ -692,8 +693,8 @@ listen-metrics-urls: "$(ip_url ${LISTEN_METRICS_URLS}):9978"
metrics: extensive
experimental-initial-corrupt-check: true
experimental-max-learners: 1
-experimental-warning-apply-duration: $(convert_duration_in_nanoseconds "$ETCD_EXPERIMENTAL_WARNING_APPLY_DURATION")
-experimental-watch-progress-notify-interval: $(convert_duration_in_nanoseconds "$ETCD_EXPERIMENTAL_WATCH_PROGRESS_NOTIFY_INTERVAL")
+experimental-warning-apply-duration: $(convert_duration_in_nanoseconds "$ETCD_WARNING_APPLY_DURATION")
+experimental-watch-progress-notify-interval: $(convert_duration_in_nanoseconds "$ETCD_WATCH_PROGRESS_NOTIFY_INTERVAL")
EOF
rc=$?
if [ $rc -ne 0 ]; then

View File

@ -0,0 +1,25 @@
From 7449fd88d21650db1eaafdc7ef85bf3553f6ac7f Mon Sep 17 00:00:00 2001
From: Pablo Fontanilla <pfontani@redhat.com>
Date: Thu, 8 Jan 2026 09:42:42 +0100
Subject: [PATCH] OCPBUGS-64765: podman-etcd: add -a option to crictl ps
(#2112)
---
heartbeat/podman-etcd | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index bb2900536..591a663bf 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -738,8 +738,8 @@ archive_data_folder()
etcd_pod_container_exists() {
local count_matches
- # Check whether the etcd pod exists on the same node (header line included)
- count_matches=$(crictl pods --label app=etcd -q | xargs -I {} crictl ps --pod {} -o json | jq -r '.containers[].metadata | select ( .name == "etcd" ).name' | wc -l)
+ # 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
# etcd pod found
return 0

View File

@ -0,0 +1,278 @@
From 8df1e4dfdee960b971fb598c043b4ccb2b9fefca Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Mon, 3 Nov 2025 12:34:29 +0100
Subject: [PATCH] podman-etcd: enhance etcd data backup with snapshots and
retention
Replace basic data directory backup with proper etcd database snapshot
functionality. The new implementation:
- Creates timestamped snapshot files instead of moving the entire data directory
- Stores backups in a non-volatile location (backup_location parameter) instead
of the previous volatile HA_RSCTMP directory
- Validates backup file existence and size after creation
- Implements configurable retention policy via max_backup_snapshots parameter
- Automatically cleans up old snapshots to control storage usage
Default retention is set to 3 snapshots, with backups stored in /var/lib/etcd
by default. This provides better backup reliability, persistence across reboots,
and storage management for etcd databases.
---
heartbeat/podman-etcd | 205 ++++++++++++++++++++++++++++++++++++++++--
1 file changed, 196 insertions(+), 9 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index bb2900536..1d717ec00 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -49,6 +49,7 @@ OCF_RESKEY_reuse_default="0"
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_image=${OCF_RESKEY_image_default}}
: ${OCF_RESKEY_pod_manifest=${OCF_RESKEY_pod_manifest_default}}
@@ -61,6 +62,7 @@ OCF_RESKEY_backup_location_default="/var/lib/etcd"
: ${OCF_RESKEY_oom=${OCF_RESKEY_oom_default}}
: ${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}}
#######################################################################
@@ -275,6 +277,17 @@ The directory where the resource agent stores its backups.
<content type="string" default="${OCF_RESKEY_backup_location_default}"/>
</parameter>
+<parameter name="max_backup_snapshots" required="0" unique="0">
+<longdesc lang="en">
+Maximum number of etcd database snapshots to retain. When a new snapshot is created,
+older snapshots will be automatically removed to maintain this limit. This helps
+control storage usage while ensuring recent backups are available for recovery.
+Set max_backup_snapshots=0 to disable backups.
+</longdesc>
+<shortdesc lang="en">Maximum number of backup snapshots to retain</shortdesc>
+<content type="integer" default="${OCF_RESKEY_max_backup_snapshots_default}"/>
+</parameter>
+
</parameters>
<actions>
@@ -720,20 +733,190 @@ EOF
return $OCF_SUCCESS
}
+# Remove etcd member directory to allow the node to rejoin the cluster as a learner.
+#
+# When a node rejoins an etcd cluster, it must start fresh as a learner to prevent
+# data inconsistencies. This function removes the member directory and syncs to disk.
+#
+# Returns:
+# OCF_SUCCESS - Member directory successfully removed
+# OCF_ERR_GENERIC - Failed to remove member directory (critical error)
+wipe_data_folder_for_learner()
+{
+ ocf_log info "deleting etcd member directory ($ETCD_MEMBER_DIR) to enable learner rejoin"
+ if ! rm -rf "$ETCD_MEMBER_DIR"; then
+ ocf_log err "could not delete etcd member directory ($ETCD_MEMBER_DIR), error code: $?"
+ return $OCF_ERR_GENERIC
+ fi
+ sync
+ return $OCF_SUCCESS
+}
+
+
+# Calculate available disk space in bytes for a given directory.
+#
+# This function queries the filesystem and returns available space in bytes.
+# It converts df output (KB) to bytes for consistent size comparisons.
+#
+# Arguments:
+# $1 - Target directory path to check
+#
+# Returns:
+# OCF_SUCCESS - Available space in bytes (via stdout)
+# OCF_ERR_GENERIC - Failed to determine available space (error message via stdout)
+get_available_space_in_directory()
+{
+ local target_dir=$1
+ local available_space_kb
+ local available_space_bytes
+
+ available_space_kb=$(df -P "$target_dir" | awk 'NR==2 {print $4}' 2>&1)
+
+ # Validate output is numeric
+ if ! echo "$available_space_kb" | grep -q '^[0-9]\+$'; then
+ echo "df command failed or returned invalid value: $available_space_kb"
+ return $OCF_ERR_GENERIC
+ fi
+
+ available_space_bytes=$((available_space_kb*1024))
+ echo "$available_space_bytes"
+ return $OCF_SUCCESS
+}
+
+# Archive etcd database with backup and cleanup
+#
+# This function creates a backup copy of the etcd database, validates it, and
+# removes old backups according to the retention policy. Backups are optional
+# and can be disabled by setting max_backup_snapshots=0.
+#
+# Error handling strategy:
+# All backup failures return OCF_SUCCESS to prevent blocking cluster recovery.
+# Backups are beneficial but not critical for recovery operations.
+#
+# NOTE: This function cannot use etcdctl/etcdutl utilities because the etcd
+# server is not running when this backup is performed.
archive_data_folder()
{
- # TODO: use etcd snapshots
- local dest_dir_name
- local data_dir="/var/lib/etcd/member"
+ local backup_dir="$OCF_RESKEY_backup_location"
+ local etcd_db_path="$ETCD_MEMBER_DIR/snap/db"
- dest_dir_name="members-snapshot-$(date +%Y%M%d%H%M%S)"
- if [ ! -d $data_dir ]; then
- ocf_log info "no data dir to backup"
+ if [ "$OCF_RESKEY_max_backup_snapshots" -eq 0 ]; then
+ ocf_log debug "etcd backup disabled (max_backup_snapshots=0)"
return $OCF_SUCCESS
fi
- ocf_log info "backing up $data_dir under $HA_RSCTMP/$dest_dir_name"
- mv "$data_dir" "$HA_RSCTMP/$dest_dir_name"
- sync
+
+ # Check if the etcd database file exists
+ if [ ! -f "$etcd_db_path" ]; then
+ ocf_log warn "backup skipped: etcd database file not found at '$etcd_db_path'"
+ return $OCF_SUCCESS
+ fi
+
+ # Ensure backup directory exists
+ if [ ! -d "$backup_dir" ]; then
+ ocf_log debug "creating backup directory: '$backup_dir'"
+ if ! mkdir -p "$backup_dir"; then
+ ocf_log warn "backup skipped: failed to create backup directory '$backup_dir'"
+ return $OCF_SUCCESS
+ fi
+ fi
+
+ ocf_log debug "checking disk space: backup_dir=$backup_dir"
+ local available_space_bytes
+ if ! available_space_bytes=$(get_available_space_in_directory "$backup_dir"); then
+ ocf_log warn "backup skipped: could not compute available disk space in '$backup_dir', error msg: $available_space_bytes"
+ return $OCF_SUCCESS
+ fi
+
+ local required_space_bytes
+ required_space_bytes=$(stat -c %s "$etcd_db_path" 2>&1)
+ if ! echo "$required_space_bytes" | grep -q '^[0-9]\+$'; then
+ ocf_log warn "backup skipped: could not compute etcd database size at '$etcd_db_path', error msg: $required_space_bytes"
+ return $OCF_SUCCESS
+ fi
+
+ if [ "$required_space_bytes" -gt "$available_space_bytes" ]; then
+ ocf_log warn "backup skipped: insufficient disk space (required: ${required_space_bytes}B, available: ${available_space_bytes}B)"
+ return $OCF_SUCCESS
+ fi
+
+ # Generate timestamp and backup filename
+ local timestamp
+ timestamp=$(date +%Y%m%d-%H%M%S)
+
+ local backup_file
+ backup_file="$backup_dir/snapshot-$timestamp.db"
+
+ ocf_log info "creating etcd database backup: '$backup_file'"
+
+ # Create the backup by copying the database file (enable Copy-on-Write copy)
+ if ! cp --reflink=auto "$etcd_db_path" "$backup_file"; then
+ ocf_log warn "backup creation failed: could not copy '$etcd_db_path' to '$backup_file', error code: $?"
+ return $OCF_SUCCESS
+ fi
+
+ # Validate the backup file exists and has the expected size
+ if [ ! -f "$backup_file" ]; then
+ ocf_log warn "backup validation failed: snapshot file '$backup_file' does not exist"
+ return $OCF_SUCCESS
+ fi
+
+ local backup_size_bytes
+ backup_size_bytes=$(stat -c %s "$backup_file" 2>/dev/null || echo "0")
+ if [ "$backup_size_bytes" -ne "$required_space_bytes" ]; then
+ ocf_log warn "backup validation failed: size mismatch (expected: ${required_space_bytes}B, got: ${backup_size_bytes}B)"
+ rm -f "$backup_file"
+ return $OCF_SUCCESS
+ fi
+
+ ocf_log info "backup created successfully: $backup_file (${backup_size_bytes}B)"
+
+ # Cleanup old backups based on retention policy
+ cleanup_old_backups "$backup_dir"
+
+ return $OCF_SUCCESS
+}
+
+cleanup_old_backups()
+{
+ local backup_dir="$1"
+ local max_snapshots="$OCF_RESKEY_max_backup_snapshots"
+ local backup_count
+ local backups_to_remove
+ local old_backups
+
+ # Validate max_snapshots is a positive integer
+ if ! echo "$max_snapshots" | grep -q '^[1-9][0-9]*$'; then
+ ocf_log warn "invalid max_backup_snapshots value. Positive integer expected, got '$max_snapshots' instead, skipping cleanup"
+ return $OCF_SUCCESS
+ fi
+
+ # Count existing backup files
+ backup_count=$(find "$backup_dir" -maxdepth 1 -name "snapshot-*.db" -type f 2>/dev/null | wc -l)
+
+ if [ "$backup_count" -le "$max_snapshots" ]; then
+ ocf_log info "backup count ($backup_count) is within retention limit ($max_snapshots), no cleanup needed"
+ return $OCF_SUCCESS
+ fi
+
+ # Calculate how many backups to remove
+ backups_to_remove=$((backup_count - max_snapshots))
+ ocf_log info "removing $backups_to_remove old backup(s) to maintain retention limit of $max_snapshots"
+
+ # Find oldest backups sorted by modification time
+ # -t sorts by modification time, -r reverses (oldest first)
+ # -print0 and -0 handle filenames with spaces/special characters
+ old_backups=$(find "$backup_dir" -maxdepth 1 -name "snapshot-*.db" -type f -print0 2>/dev/null | \
+ xargs -0 -r ls -tr | \
+ head -n "$backups_to_remove")
+
+ if [ -n "$old_backups" ]; then
+ ocf_log info "removing old backups: $old_backups"
+ if ! echo "$old_backups" | xargs -r rm -f; then
+ ocf_log warn "failed to remove some old backups, error code: $?"
+ fi
+ fi
+
+ return $OCF_SUCCESS
}
etcd_pod_container_exists() {
@@ -1902,6 +2085,9 @@ podman_start()
fi
archive_data_folder
+ if ! wipe_data_folder_for_learner; then
+ return "$OCF_ERR_GENERIC"
+ fi
fi
ocf_log info "check for changes in pod manifest to decide if the container should be reused or replaced"
@@ -2251,6 +2437,7 @@ CONTAINER=$OCF_RESKEY_name
POD_MANIFEST_COPY="${OCF_RESKEY_config_location}/pod.yaml"
ETCD_CONFIGURATION_FILE="${OCF_RESKEY_config_location}/config.yaml"
ETCD_BACKUP_FILE="${OCF_RESKEY_backup_location}/config-previous.tar.gz"
+ETCD_MEMBER_DIR="/var/lib/etcd/member"
ETCD_REVISION_JSON="/var/lib/etcd/revision.json"
ETCD_REVISION_BUMP_PERCENTAGE=0.2
ETCD_BUMP_REV_DEFAULT=1000000000

View File

@ -0,0 +1,23 @@
From fcefa56677aac98a20c741698887446d51a0c9ce Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Mon, 16 Feb 2026 14:27:14 +0100
Subject: [PATCH] IPsrcaddr: fix grep expression, so it doesnt log "stray \
before white space" with newer versions of grep
---
heartbeat/IPsrcaddr | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/heartbeat/IPsrcaddr b/heartbeat/IPsrcaddr
index 58d89a280..31c9f2663 100755
--- a/heartbeat/IPsrcaddr
+++ b/heartbeat/IPsrcaddr
@@ -434,7 +434,7 @@ find_interface_solaris() {
# is an (aliased) interface name (e.g., "eth0" and "eth0:0").
#
find_interface_generic() {
- local iface=`$IP2UTIL -o -f $FAMILY addr show | grep "\ $BASEIP" \
+ local iface=`$IP2UTIL -o -f $FAMILY addr show | grep " $BASEIP" \
| cut -d ' ' -f2 | grep -v '^ipsec[0-9][0-9]*$'`
if [ -z "$iface" ]; then
return $OCF_ERR_GENERIC

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

@ -1,6 +1,36 @@
--- 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 @@
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"
@ -16,10 +46,10 @@
+ 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}}
: ${OCF_RESKEY_direction=${OCF_RESKEY_direction_default}}
@@ -401,6 +406,10 @@
@@ -425,6 +430,10 @@ tickle_local()
done
}
@ -30,7 +60,7 @@
SayActive()
{
ocf_log debug "$CMD $method rule [$*] is running (OK)"
@@ -416,6 +425,11 @@
@@ -440,6 +449,11 @@ SayInactive()
ocf_log debug "$CMD $method rule [$*] is inactive"
}
@ -39,10 +69,10 @@
+ ocf_log debug "$CMD $method rule [$*] considered to be inactive"
+}
+
#IptablesStatus {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
IptablesStatus() {
#PortStatus {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
PortStatus() {
local rc
@@ -441,8 +455,17 @@
@@ -465,8 +479,17 @@ PortStatus() {
fi
;;
*)
@ -62,7 +92,7 @@
;;
esac
elif [ "$OCF_RESKEY_status_check" = "rule" ]; then
@@ -454,6 +477,7 @@
@@ -478,6 +501,7 @@ PortStatus() {
*)
SayActive $*
if [ "$__OCF_ACTION" = "monitor" ] && [ "$promotion_score" = "$SCORE_PROMOTED" ]; then
@ -70,7 +100,7 @@
rc=$OCF_RUNNING_MASTER
else
rc=$OCF_SUCCESS
@@ -463,7 +487,10 @@
@@ -487,7 +511,10 @@ PortStatus() {
else
case $5 in
block)
@ -82,7 +112,7 @@
SayConsideredActive $*
rc=$OCF_SUCCESS
else
@@ -472,13 +499,15 @@
@@ -496,13 +523,15 @@ PortStatus() {
fi
;;
*)
@ -101,28 +131,27 @@
rc=$OCF_NOT_RUNNING
fi
;;
@@ -562,7 +591,7 @@
#IptablesStart {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
IptablesStart()
@@ -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"
case $5 in
block) IptablesBLOCK "$@"
rc=$?
@@ -584,7 +613,8 @@
#IptablesStop {udp|tcp} portno,portno ip {in|out|both} {block|unblock}
IptablesStop()
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) IptablesUNBLOCK "$@"
rc=$?
@@ -797,6 +827,33 @@
IptablesValidateAll
case $5 in
block) PortUNBLOCK "$@"
@@ -929,6 +958,33 @@ fi
PortValidateAll
+# State file name for ha_pseudo_resource
+#
@ -153,4 +182,4 @@
+
case $__OCF_ACTION in
start)
IptablesStart "$protocol" "$portno" "$ip" "$direction" "$action"
PortStart "$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,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

@ -0,0 +1,183 @@
From 5709a5e92e0b375642f74fe053ed86a619c69c5f Mon Sep 17 00:00:00 2001
From: Pablo Fontanilla <pfontani@redhat.com>
Date: Mon, 15 Jun 2026 15:14:00 +0200
Subject: [PATCH] podman-etcd: remove cert monitoring infrastructure
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
etcd 3.6+ hot-reloads TLS certificates on each new TLS handshake via
Go's GetCertificate callback (etcd PR #7829). The cert hash monitor
that ran every 10s cycle is therefore dead code — it can never trigger
a useful action.
Remove: etcd_certificates_hash_manager() function, etcd_certs_dir OCF
parameter, ETCD_CERTS_HASH_FILE variable, and all four call sites
(monitor, start, validate, archive). 86 lines deleted.
This also eliminates the root cause of OCPBUGS-86897: the monitor
detected cert changes and returned OCF_ERR_GENERIC, which triggered
Pacemaker to stop etcd, call leave_etcd_member_list(), and force the
node to rejoin as a learner with a new member ID — on every cert
rotation, 10 times per CI run.
Signed-off-by: Pablo Fontanilla <pfontani@redhat.com>
---
heartbeat/podman-etcd | 88 +------------------------------------------
1 file changed, 1 insertion(+), 87 deletions(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index d0fc22e1f1..0195633af4 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -40,7 +40,6 @@
# Parameter defaults
OCF_RESKEY_image_default="default"
OCF_RESKEY_pod_manifest_default="/etc/kubernetes/static-pod-resources/etcd-certs/configmaps/external-etcd-pod/pod.yaml"
-OCF_RESKEY_etcd_certs_dir_default="/etc/kubernetes/static-pod-resources/etcd-certs"
OCF_RESKEY_name_default="etcd"
OCF_RESKEY_nic_default="br-ex"
OCF_RESKEY_authfile_default="/var/lib/kubelet/config.json"
@@ -54,7 +53,6 @@ OCF_RESKEY_kubeconfig_default="/etc/kubernetes/static-pod-resources/kube-apiserv
: ${OCF_RESKEY_image=${OCF_RESKEY_image_default}}
: ${OCF_RESKEY_pod_manifest=${OCF_RESKEY_pod_manifest_default}}
-: ${OCF_RESKEY_etcd_certs_dir=${OCF_RESKEY_etcd_certs_dir_default}}
: ${OCF_RESKEY_name=${OCF_RESKEY_name_default}}
: ${OCF_RESKEY_nic=${OCF_RESKEY_nic_default}}
: ${OCF_RESKEY_authfile=${OCF_RESKEY_authfile_default}}
@@ -94,15 +92,6 @@ The Pod manifest with the configuration for Etcd.
<content type="string" default="${OCF_RESKEY_pod_manifest_default}"/>
</parameter>
-<parameter name="etcd_certs_dir" required="0" unique="0">
-<longdesc lang="en">
-The Etcd certificates directory mounted into the etcd container.
-The agent will monitor this directory for changes and restart the etcd container if the certificates have changed.
-</longdesc>
-<shortdesc lang="en">Etcd certificates directory</shortdesc>
-<content type="string" default="${OCF_RESKEY_etcd_certs_dir_default}"/>
-</parameter>
-
<parameter name="image" required="0" unique="0">
<longdesc lang="en">
The podman image to base this container off of.
@@ -326,60 +315,6 @@ Expects to have a fully populated OCF RA-compliant environment set.
END
}
-etcd_certificates_hash_manager()
-{
- local action="$1"
- local current_hash
- local stored_hash
-
- # If the certs directory doesn't exist, consider it unchanged
- if [ ! -d "$OCF_RESKEY_etcd_certs_dir" ]; then
- ocf_log warn "certificates directory $OCF_RESKEY_etcd_certs_dir does not exist, skipping certificate monitoring"
- return $OCF_SUCCESS
- fi
-
- # Calculate hash of all certificate files, ignore key files to avoid accidental disclosure of sensitive information
- # we only need to monitor the certificate files to detect changes.
- if ! current_hash=$(find "$OCF_RESKEY_etcd_certs_dir" -type f \( -name "*.crt" \) -exec sha256sum {} \; | sort | sha256sum | cut -d' ' -f1); then
- ocf_log err "failed to calculate certificate files hash"
- return $OCF_ERR_GENERIC
- fi
-
- # If no stored hash exists, create one and return success
- if [ ! -f "$ETCD_CERTS_HASH_FILE" ]; then
- echo "$current_hash" > "$ETCD_CERTS_HASH_FILE"
- ocf_log info "created initial certificate hash: $current_hash"
- return $OCF_SUCCESS
- fi
-
- case "$action" in
- "update")
- if ! echo "$current_hash" > "$ETCD_CERTS_HASH_FILE"; then
- ocf_log err "failed to update certificate hash file $ETCD_CERTS_HASH_FILE"
- fi
- ocf_log info "updated certificate hash: $current_hash"
- ;;
- "check")
- if ! stored_hash=$(cat "$ETCD_CERTS_HASH_FILE"); then
- ocf_log err "failed to read stored certificate hash from $ETCD_CERTS_HASH_FILE"
- # This should not happen but if for some reason we can not read the stored hash,
- # use the current hash and log the error but allow etcd to run as long as possible.
- stored_hash="$current_hash"
- fi
- if [ "$current_hash" != "$stored_hash" ]; then
- ocf_exit_reason "$NODENAME etcd certificate files have changed (stored: $stored_hash, current: $current_hash)"
- return $OCF_ERR_GENERIC
- fi
- ;;
- *)
- ocf_log err "unsupported action: $action"
- return $OCF_ERR_GENERIC
- ;;
- esac
-
- return $OCF_SUCCESS
-}
-
monitor_cmd_exec()
{
local rc=$OCF_SUCCESS
@@ -456,7 +391,7 @@ archive_current_container()
# archive corresponding etcd configuration files
local files_to_archive=""
- for file in "$OCF_RESKEY_authfile" "$POD_MANIFEST_COPY" "$ETCD_CONFIGURATION_FILE" "$ETCD_CERTS_HASH_FILE"; do
+ for file in "$OCF_RESKEY_authfile" "$POD_MANIFEST_COPY" "$ETCD_CONFIGURATION_FILE"; do
if [ -f "$file" ]; then
files_to_archive="$files_to_archive $file"
else
@@ -1826,11 +1761,6 @@ podman_monitor()
;;
esac
- # Check if certificate files have changed, if they have, etcd needs to be restarted
- if ! etcd_certificates_hash_manager "check"; then
- return $OCF_ERR_GENERIC
- fi
-
if is_learner; then
ocf_log info "$NODENAME is learner. Cannot get member id"
return "$OCF_SUCCESS"
@@ -2141,14 +2071,6 @@ podman_start()
return $OCF_ERR_GENERIC
fi
- # Update the certificate hash after the container has started successfully
- # this is to ensure that the certificate hash is updated after a restart is initiated
- # by a cert rotation event from the monitor command.
- if ! etcd_certificates_hash_manager "update"; then
- ocf_exit_reason "etcd certificate hash manager failed to update the certificate hash"
- return $OCF_ERR_GENERIC
- fi
-
# check if the container has already started
podman_simple_status
if [ $? -eq $OCF_SUCCESS ]; then
@@ -2628,13 +2550,6 @@ podman_validate()
exit $OCF_ERR_CONFIGURED
fi
- if ! echo "validation test" > "$ETCD_CERTS_HASH_FILE" \
- || ! cat "$ETCD_CERTS_HASH_FILE" >/dev/null 2>&1 \
- || ! rm "$ETCD_CERTS_HASH_FILE"; then
- ocf_exit_reason "cannot read/write to certificate hash file $ETCD_CERTS_HASH_FILE"
- exit $OCF_ERR_GENERIC
- fi
-
return $OCF_SUCCESS
}
@@ -2687,7 +2602,6 @@ ETCD_MEMBER_DIR="/var/lib/etcd/member"
ETCD_REVISION_JSON="/var/lib/etcd/revision.json"
ETCD_REVISION_BUMP_PERCENTAGE=0.2
ETCD_BUMP_REV_DEFAULT=1000000000
-ETCD_CERTS_HASH_FILE="${OCF_RESKEY_config_location}/certs.hash"
# State file location: Uses HA_RSCTMP to ensure automatic cleanup on reboot.
# This is intentional - reboots are controlled stops, not failures requiring detection.
CONTAINER_HEARTBEAT_FILE=${HA_RSCTMP}/podman-container-last-running

View File

@ -0,0 +1,36 @@
From 1e546b85010e5fdbf7a0f31207dce144c14c50ec Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Wed, 29 Oct 2025 15:17:30 +0100
Subject: [PATCH] MailTo: add s-nail support for multiple recipients
---
heartbeat/MailTo | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/heartbeat/MailTo b/heartbeat/MailTo
index 56940bafaa..a3ee6a04c8 100755
--- a/heartbeat/MailTo
+++ b/heartbeat/MailTo
@@ -92,12 +92,16 @@ END
}
MailProgram() {
- $MAILCMD -s "$1" "$email" <<EOF
- $Subject
-
- Command line was:
- $ARGS
-EOF
+ local body="\
+$Subject
+
+Command line was:
+$ARGS"
+ if $MAILCMD -V | grep -q "^s-nail"; then
+ printf "$body" | $MAILCMD -s "$1" $(echo $email | sed "s/,\s*/ /g")
+ else
+ printf "$body" | $MAILCMD -s "$1" "$email"
+ fi
return $?
}

View File

@ -45,7 +45,7 @@
Name: resource-agents
Summary: Open Source HA Reusable Cluster Resource Scripts
Version: 4.16.0
Release: 22%{?rcver:%{rcver}}%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty}}%{?dist}.10
Release: 53%{?rcver:%{rcver}}%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty}}%{?dist}.7
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
@ -77,21 +77,58 @@ Patch24: RHEL-85014-IPaddr2-add-link-status-DOWN-LOWERLAYERDOWN-check.patch
Patch25: RHEL-99743-Filesystem-remove-validate-all-fstype-check.patch
Patch26: RHEL-97216-Filesystem-fix-issue-with-Vormetric-mounts.patch
Patch27: RHEL-102728-ocf-shellfuncs-remove-extra-sleep-from-curl_retry.patch
Patch28: RHEL-113751-1-podman-etcd-new-ra.patch
Patch29: RHEL-113751-2-podman-etcd-remove-unused-actions-from-metadata.patch
Patch30: RHEL-113751-3-podman-etcd-fix-listen-peer-urls-binding.patch
Patch31: RHEL-113106-podman-etcd-add-oom-parameter.patch
Patch32: RHEL-126121-1-nginx-fix-validate-warnings.patch
Patch33: RHEL-126121-2-nginx-restore-selinux-context-for-pid-file-during-validate-all-action.patch
Patch34: RHEL-114494-1-powervs-move-ip-new-ra.patch
Patch35: RHEL-114494-2-powervs-move-ip-add-iflabel-parameter.patch
Patch36: RHEL-81238-powervs-subnet-wait-for-IP.patch
Patch37: RHEL-143528-powervs-move-ip-powervs-subnet-fix-error-logging.patch
Patch38: RHEL-116197-1-ocf-shellfuncs-add-ocf_promotion_score.patch
Patch39: RHEL-116197-2-portblock-add-promotable-support.patch
Patch40: RHEL-116197-3-portblock-fixes-add-method-and-status_check-parameters.patch
Patch41: RHEL-116197-4-portblock-check-inverse-action.patch
Patch42: RHEL-166745-db2-do-not-use-db2stop-to-avoid-divergence-in-the-log.patch
Patch28: RHEL-88431-1-podman-etcd-new-ra.patch
Patch29: RHEL-88431-2-podman-etcd-remove-unused-actions-from-metadata.patch
Patch30: RHEL-88431-3-podman-etcd-fix-listen-peer-urls-binding.patch
Patch31: RHEL-113104-podman-etcd-add-oom-parameter.patch
Patch32: RHEL-109013-1-powervs-move-ip-new-ra.patch
Patch33: RHEL-102255-RHEL-102319-1-db2-add-skip_basic_sql_health_check-and-monitor-parameters.patch
Patch34: RHEL-113817-podman-etcd-wrap-ipv6-address-in-brackets.patch
Patch35: RHEL-113816-podman-etcd-preserve-containers-for-debugging.patch
Patch36: RHEL-116205-podman-etcd-add-cluster-wide-force_new_cluster-attribute-check.patch
Patch37: RHEL-116149-RHEL-116152-1-portblock-add-promotable-and-nftables-support.patch
Patch38: RHEL-116149-RHEL-116152-2-portblock-fix-incorrect-promotable-description.patch
Patch39: RHEL-116149-RHEL-116152-3-portblock-fixes-add-method-and-status_check-parameters.patch
Patch40: RHEL-119504-podman-etcd-add-automatic-learner-member-promotion.patch
Patch41: RHEL-115495-db2-use-reintegration-flag-to-avoid-race-condition-on-cluster-reintegration.patch
Patch42: RHEL-124203-podman-etcd-certificate-rotation.patch
Patch43: RHEL-124206-podman-etcd-compute-dynamic-revision-bump-from-maxRaftIndex.patch
Patch44: RHEL-102255-RHEL-102319-2-db2-fix-variable-name.patch
Patch45: RHEL-92707-MailTo-add-s-nail-support-for-multiple-recipients.patch
Patch46: RHEL-124881-oracle-improve-monpassword-description.patch
Patch47: RHEL-109486-1-nfsserver-support-non-clustered-kerberized-mounts.patch
Patch48: RHEL-109486-2-nfsserver-fix-error-message.patch
Patch49: RHEL-109013-2-powervs-move-ip-add-iflabel-parameter.patch
Patch50: RHEL-102779-pgsqlms-fix-validate-warnings.patch
Patch51: RHEL-112443-1-nginx-fix-validate-warnings.patch
Patch52: RHEL-121985-Filesystem-speed-up-get-PIDs.patch
Patch53: RHEL-126791-storage_mon-fix-handling-of-4k-block-devices.patch
Patch54: RHEL-127840-podman-etcd-exclude-stopping-resources-from-active-count.patch
Patch55: RHEL-126083-1-podman-etcd-add-container-crash-detection-with-coordinated-recovery.patch
Patch56: RHEL-112443-2-nginx-restore-selinux-context-for-pid-file-during-validate-all-action.patch
Patch57: RHEL-130576-1-podman-etcd-prevent-last-active-member-from-leaving.patch
Patch58: RHEL-130576-2-podman-etcd-remove-test-code.patch
Patch59: RHEL-126083-2-podman-etcd-fix-count-of-fnc-holders-in-container_health_check.patch
Patch60: RHEL-131181-podman-etcd-prevent-learner-from-starting-before-cluster-is-ready.patch
Patch61: RHEL-132047-podman-etcd-prevent-retries-on-fatal-errors.patch
Patch62: RHEL-133928-podman-etcd-align-variable-names-with-etcd-3.6-pod-manifest.patch
Patch63: RHEL-139066-podman-etcd-verify-no-containers-running-or-being-deleted.patch
Patch64: RHEL-50380-powervs-subnet-wait-for-IP.patch
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-151827-portblock-check-inverse-action.patch
Patch70: RHEL-150701-podman-etcd-set-attributes-if-they-fail-during-force-new-cluster.patch
Patch71: RHEL-156710-podman-etcd-ignore-learners-when-considering-which-node-has-higher-revision.patch
Patch72: RHEL-157147-podman-etcd-handle-existing-peer-URLs-gracefully-during-force_new_cluster-recovery.patch
Patch73: RHEL-157303-db2-set-reintegration-when-promotion-is-successful.patch
Patch74: RHEL-159205-podman-etcd-hardened-monitor-stop-actions.patch
Patch75: RHEL-166746-db2-do-not-use-db2stop-to-avoid-divergence-in-the-log.patch
Patch76: RHEL-177851-podman-etcd-fix-port-2380-binding-race.patch
Patch77: RHEL-177842-podman-etcd-fix-machine-deletion-deadlock.patch
Patch78: RHEL-177846-podman-etcd-fix-learner-start-deadlock.patch
Patch79: RHEL-188109-podman-etcd-remove-cert-monitoring.patch
# bundled ha-cloud-support libs
Patch500: ha-cloud-support-aliyun.patch
@ -161,10 +198,14 @@ Requires: /sbin/fsck
Requires: /usr/sbin/fsck.ext2 /usr/sbin/fsck.ext3 /usr/sbin/fsck.ext4
Requires: /usr/sbin/fsck.xfs
%if 0%{?fedora} > 40 || 0%{?rhel} > 9 || 0%{?suse_version}
Requires: /usr/sbin/mount.nfs /usr/sbin/mount.nfs4
Recommends: /usr/sbin/mount.nfs /usr/sbin/mount.nfs4
%else
%if 0%{?rhel} > 8
Recommends: /sbin/mount.nfs /sbin/mount.nfs4
%else
Requires: /sbin/mount.nfs /sbin/mount.nfs4
%endif
%endif
%if (0%{?fedora} && 0%{?fedora} < 33) || (0%{?rhel} && 0%{?rhel} < 9) || (0%{?centos} && 0%{?centos} < 9) || 0%{?suse_version}
%if (0%{?rhel} && 0%{?rhel} < 8) || (0%{?centos} && 0%{?centos} < 8)
Requires: /usr/sbin/mount.cifs
@ -181,11 +222,19 @@ Requires: /usr/sbin/lvm
# nfsserver / netfs.sh
%if 0%{?fedora} > 40 || 0%{?rhel} > 9 || 0%{?suse_version}
Requires: /usr/sbin/rpc.statd
Recommends: /usr/sbin/rpc.statd
%else
%if 0%{?rhel} > 8
Recommends: /sbin/rpc.statd
%else
Requires: /sbin/rpc.statd
%endif
%endif
%if 0%{?fedora} > 40 || 0%{?rhel} > 8 || 0%{?suse_version}
Recommends: /usr/sbin/rpc.nfsd /usr/sbin/rpc.mountd
%else
Requires: /usr/sbin/rpc.nfsd /usr/sbin/rpc.mountd
%endif
# ocf.py
Requires: python3
@ -288,11 +337,48 @@ exit 1
%patch -p1 -P 35
%patch -p1 -P 36
%patch -p1 -P 37
%patch -p1 -P 38 -F1
%patch -p1 -P 38
%patch -p1 -P 39
%patch -p1 -P 40
%patch -p1 -P 41
%patch -p1 -P 42
%patch -p1 -P 43
%patch -p1 -P 44
%patch -p1 -P 45
%patch -p1 -P 46
%patch -p1 -P 47
%patch -p1 -P 48
%patch -p1 -P 49
%patch -p1 -P 50
%patch -p1 -P 51
%patch -p1 -P 52
%patch -p1 -P 53
%patch -p1 -P 54
%patch -p1 -P 55 -F2
%patch -p1 -P 56
%patch -p1 -P 57
%patch -p1 -P 58
%patch -p1 -P 59
%patch -p1 -P 60
%patch -p1 -P 61
%patch -p1 -P 62
%patch -p1 -P 63
%patch -p1 -P 64
%patch -p1 -P 65
%patch -p1 -P 66
%patch -p1 -P 67
%patch -p1 -P 68
%patch -p1 -P 69
%patch -p1 -P 70
%patch -p1 -P 71
%patch -p1 -P 72
%patch -p1 -P 73
%patch -p1 -P 74
%patch -p1 -P 75
%patch -p1 -P 76
%patch -p1 -P 77
%patch -p1 -P 78
%patch -p1 -P 79
# bundled ha-cloud-support libs
%patch -p1 -P 500
@ -420,8 +506,8 @@ rm -rf %{buildroot}/usr/share/doc/resource-agents
%exclude %{_mandir}/man7/*aliyun-vpc-move-ip*
%exclude /usr/lib/ocf/resource.d/heartbeat/gcp*
%exclude %{_mandir}/man7/*gcp*
%exclude /usr/lib/ocf/resource.d/heartbeat/powervs-subnet
%exclude %{_mandir}/man7/*powervs-subnet*
%exclude /usr/lib/ocf/resource.d/heartbeat/powervs-*
%exclude %{_mandir}/man7/*powervs-*
%exclude /usr/lib/ocf/resource.d/heartbeat/pgsqlms
%exclude %{_mandir}/man7/*pgsqlms*
%exclude %{_usr}/lib/ocf/lib/heartbeat/OCF_*.pm
@ -477,6 +563,7 @@ rm -rf %{buildroot}/usr/share/doc/resource-agents
%exclude %{_usr}/lib/ocf/resource.d/heartbeat/jira
%exclude %{_usr}/lib/ocf/resource.d/heartbeat/kamailio
%exclude %{_usr}/lib/ocf/resource.d/heartbeat/ldirectord
%exclude %{_usr}/lib/ocf/resource.d/heartbeat/lvmlockd
%exclude %{_usr}/lib/ocf/resource.d/heartbeat/lxc
%exclude %{_usr}/lib/ocf/resource.d/heartbeat/lxd-info
%exclude %{_usr}/lib/ocf/resource.d/heartbeat/machine-info
@ -545,6 +632,7 @@ rm -rf %{buildroot}/usr/share/doc/resource-agents
%exclude %{_mandir}/man7/ocf_heartbeat_jboss.7.gz
%exclude %{_mandir}/man7/ocf_heartbeat_jira.7.gz
%exclude %{_mandir}/man7/ocf_heartbeat_kamailio.7.gz
%exclude %{_mandir}/man7/ocf_heartbeat_lvmlockd.7.gz
%exclude %{_mandir}/man7/ocf_heartbeat_lxc.7.gz
%exclude %{_mandir}/man7/ocf_heartbeat_lxd-info.7.gz
%exclude %{_mandir}/man7/ocf_heartbeat_machine-info.7.gz
@ -623,36 +711,186 @@ rm -rf %{buildroot}/usr/share/doc/resource-agents
%{_usr}/lib/ocf/lib/heartbeat/OCF_*.pm
%changelog
* Wed Apr 29 2026 Arslan Ahmad <arahmad@redhat.com> - 4.16.0-22.10
* Thu Jun 25 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-53.7
- podman-etcd: remove cert monitoring
Resolves: RHEL-188109
* Wed May 20 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-53.6
- podman-etcd: fix port 2380 binding race
- podman-etcd: fix machine deletion deadlock
- podman-etcd: fix learner start deadlock
Resolves: RHEL-177851, RHEL-177842, RHEL-177846
* Wed Apr 29 2026 Arslan Ahmad <arahmad@redhat.com> - 4.16.0-53.5
- db2: do not use db2stop to avoid divergence in the log
Resolves: RHEL-166745
Resolves: RHEL-166746
* Fri Feb 27 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-22.9
- portblock: add promotable support, and method and status_check
parameters
* Fri Mar 27 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-53.4
- podman-etcd: hardened monitor/stop actions
Resolves: RHEL-116197
Resolves: RHEL-159205
* Mon Jan 26 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-22.8
- powervs-move-ip: new resource agent
- powervs-subnet: new resource agent
* Thu Mar 19 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-53.3
- podman etcd: ignore learners when considering which node has higher revision
- podman etcd: handle existing peer URLs gracefully during force_new_cluster recovery
- db2: set reintegration when promotion is successful
Resolves: RHEL-156710, RHEL-157147, RHEL-157303
* Thu Feb 26 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-53.2
- 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-151827, RHEL-150701
* 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
Resolves: RHEL-149738
* Wed Feb 4 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-52
- podman-etcd: enhance etcd data backup with snapshots and retention
Resolves: RHEL-145622
* Tue Feb 3 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-51
- portblock: add promotable and nftables support, and method and
status_check parameters
Resolves: RHEL-116149, RHEL-116152
* Thu Jan 22 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-50
- powervs-move-ip/powervs-subnet: fix error logging
Resolves: RHEL-114494, RHEL-81238, RHEL-143528
Resolves: RHEL-143524
* Fri Dec 5 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-22.7
* Mon Jan 12 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-49
- powervs-subnet: new resource agent
Resolves: RHEL-50380
* Thu Jan 8 2026 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-48
- podman-etcd: verify that no static pod containers are running or
being deleted before starting
Resolves: RHEL-139066
* Mon Dec 8 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-47
- podman-etcd: align environment variable names with Etcd v3.6 Pod
manifest
Resolves: RHEL-133928
* Tue Dec 2 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-46
- podman-etcd: prevent retries on fatal errors
Resolves: RHEL-132047
* Thu Nov 27 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-45
- podman-etcd: prevent learner from starting before cluster is ready
Resolves: RHEL-131181
* Mon Nov 24 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-44
- podman-etcd: add container crash detection with coordinated recovery
Resolves: RHEL-126083
* Mon Nov 24 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-43
- podman-etcd: prevent last active member from leaving the etcd member
list
Resolves: RHEL-130576
* Tue Nov 18 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-42
- nginx: fix validate warnings, and restore SELinux context for
pid-file during validate-all action
Resolves: RHEL-126121
Resolves: RHEL-112443
* Tue Sep 9 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-22.2
* Thu Nov 13 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-41
- podman-etcd: exclude stopping resources from active count
Resolves: RHEL-127840
* Mon Nov 10 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-40
- storage_mon: fix handling of 4k block devices
Resolves: RHEL-126791
* Tue Nov 4 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-39
- pgsqlms: fix validate warnings
- Filesystem: speed up get PIDs
Resolves: RHEL-102779, RHEL-121985
* Mon Nov 3 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-38
- powervs-move-ip: new resource agent
Resolves: RHEL-109013
* Fri Oct 31 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-37
- nfsserver: add ability to set e.g. "pipefs-directory=/run/nfs/rpc_pipefs"
in /etc/nfs.conf to avoid issues with non-clustered Kerberized mounts
Resolves: RHEL-109486
* Wed Oct 29 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-35
- MailTo: add s-nail support for multiple recipients
- oracle: improve monpassword description
Resolves: RHEL-92707, RHEL-124881
* Wed Oct 29 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-34
- db2: add "skip_basic_sql_health_check" parameter to avoid failing on
systems with high load
- db2: add "monitor_retries", "monitor_sleep", and "monitor_retry_all_errors"
parameters to be able to avoid failing on first try
Resolves: RHEL-102255, RHEL-102319
* Tue Oct 28 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-33
- podman-etcd: add support for cert rotation
- podman-etcd: compute dynamic revision bump from maxRaftIndex
Resolves: RHEL-124203, RHEL-124206
* Wed Oct 22 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-32
- db2: use reintegration flag to avoid race condition on cluster
reintegration
Resolves: RHEL-115495
* Fri Oct 10 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-31
- podman-etcd: add automatic learner member promotion
Resolves: RHEL-119504
* Wed Oct 8 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-30
- build: make nfs-utils a weak dependency
Resolves: RHEL-113500
* Mon Sep 22 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-27
- podman-etcd: wrap ipv6 address in brackets
- podman-etcd: preserve containers for debugging
- podman-etcd: add cluster-wide force_new_cluster attribute check
- lvmlockd: remove unsupported agent
Resolves: RHEL-113817, RHEL-113816, RHEL-116205, RHEL-116202
* Tue Sep 9 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-23
- podman-etcd: new resource agent
- podman-etcd: add oom parameter to be able to tune the Out-Of-Memory (OOM)
score for etcd containers
Resolves: RHEL-113751, RHEL-113106
Resolves: RHEL-88431, RHEL-113104
* Tue Jul 15 2025 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.16.0-22
- ocf-shellfuncs/AWS agents: dont sleep after the final try in
@ -738,9 +976,8 @@ rm -rf %{buildroot}/usr/share/doc/resource-agents
- Rebase to resource-agents 4.15.1 upstream release
- IPaddr2: change default for lvs_ipv6_addrlabel to true to avoid
last added IP becoming src IP
- powervs-subnet: new resource agent
Resolves: RHEL-50378, RHEL-46557, RHEL-50380
Resolves: RHEL-50378, RHEL-46557
* Thu Jun 27 2024 Oyvind Albrigtsen <oalbrigt@redhat.com> - 4.13.0-6
- apache: prefer curl due to wget2 issues, and dont use -L for wget2