Compare commits

...

No commits in common. "a9-beta" and "c8" have entirely different histories.
a9-beta ... c8

213 changed files with 6676 additions and 16387 deletions

13
.gitignore vendored
View File

@ -1 +1,12 @@
SOURCES/ClusterLabs-resource-agents-fd0720f7.tar.gz
SOURCES/ClusterLabs-resource-agents-55a4e2c9.tar.gz
SOURCES/aliyun-cli-2.1.10.tar.gz
SOURCES/aliyun-python-sdk-core-2.13.1.tar.gz
SOURCES/aliyun-python-sdk-ecs-4.9.3.tar.gz
SOURCES/aliyun-python-sdk-vpc-3.0.2.tar.gz
SOURCES/colorama-0.3.3.tar.gz
SOURCES/google-cloud-sdk-360.0.0-linux-x86_64.tar.gz
SOURCES/httplib2-0.20.4.tar.gz
SOURCES/pycryptodome-3.20.0.tar.gz
SOURCES/pyparsing-2.4.7-py2.py3-none-any.whl
SOURCES/pyroute2-0.4.13.tar.gz
SOURCES/urllib3-1.26.18.tar.gz

View File

@ -1 +1,12 @@
3b517ecdbe2103df77813050e5c998e102c5de7e SOURCES/ClusterLabs-resource-agents-fd0720f7.tar.gz
dfc65f4cac3f95026b2f5674019814a527333004 SOURCES/ClusterLabs-resource-agents-55a4e2c9.tar.gz
306e131d8908ca794276bfe3a0b55ccc3bbd482f SOURCES/aliyun-cli-2.1.10.tar.gz
0a56f6d9ed2014a363486d33b63eca094379be06 SOURCES/aliyun-python-sdk-core-2.13.1.tar.gz
c2a98b9a1562d223a76514f05028488ca000c395 SOURCES/aliyun-python-sdk-ecs-4.9.3.tar.gz
f14647a4d37a9a254c4e711b95a7654fc418e41e SOURCES/aliyun-python-sdk-vpc-3.0.2.tar.gz
0fe5bd8bca54dd71223778a1e0bcca9af324abb1 SOURCES/colorama-0.3.3.tar.gz
81f039cf075e9c8b70d5af99c189296a9e031de3 SOURCES/google-cloud-sdk-360.0.0-linux-x86_64.tar.gz
7caf4412d9473bf17352316249a8133fa70b7e37 SOURCES/httplib2-0.20.4.tar.gz
c55d177e9484d974c95078d4ae945f89ba2c7251 SOURCES/pycryptodome-3.20.0.tar.gz
c8307f47e3b75a2d02af72982a2dfefa3f56e407 SOURCES/pyparsing-2.4.7-py2.py3-none-any.whl
147149db11104c06d405fd077dcd2aa1c345f109 SOURCES/pyroute2-0.4.13.tar.gz
84e2852d8da1655373f7ce5e7d5d3e256b62b4e4 SOURCES/urllib3-1.26.18.tar.gz

View File

@ -0,0 +1,25 @@
diff -uNr a/bundled/gcp/google-cloud-sdk/lib/googlecloudsdk/calliope/usage_text.py b/bundled/gcp/google-cloud-sdk/lib/googlecloudsdk/calliope/usage_text.py
--- a/bundled/gcp/google-cloud-sdk/lib/googlecloudsdk/calliope/usage_text.py 1980-01-01 09:00:00.000000000 +0100
+++ b/bundled/gcp/google-cloud-sdk/lib/googlecloudsdk/calliope/usage_text.py 2019-04-04 11:59:47.592768577 +0200
@@ -900,6 +900,9 @@
return """\
For detailed information on this command and its flags, run:
{command_path} --help
+
+WARNING: {command_path} is only supported for "{command_path} init" and for use
+with the agents in resource-agents.
""".format(command_path=' '.join(command.GetPath()))
diff -uNr a/bundled/gcp/google-cloud-sdk/lib/googlecloudsdk/gcloud_main.py b/bundled/gcp/google-cloud-sdk/lib/googlecloudsdk/gcloud_main.py
--- a/bundled/gcp/google-cloud-sdk/lib/googlecloudsdk/gcloud_main.py 1980-01-01 09:00:00.000000000 +0100
+++ b/bundled/gcp/google-cloud-sdk/lib/googlecloudsdk/gcloud_main.py 2019-04-04 12:00:23.991142694 +0200
@@ -84,7 +84,7 @@
pkg_root = os.path.dirname(os.path.dirname(surface.__file__))
loader = cli.CLILoader(
- name='gcloud',
+ name='gcloud-ra',
command_root_directory=os.path.join(pkg_root, 'surface'),
allow_non_existing_modules=True,
version_func=VersionFunc,

View File

@ -1,49 +0,0 @@
From 6cde49d0000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: AlmaLinux <packager@almalinux.org>
Date: Sun, 25 May 2026 00:00:00 +0000
Subject: [PATCH] ocf-distro: add AlmaLinux to RHEL-based distro detection
Partial backport of upstream commit 6cde49d (PR #1756) which adds
AlmaLinux to the Red Hat-based distro detection in ocf-distro.
Without this patch, is_redhat_based() returns false on AlmaLinux
because the os-release ID "almalinux" is not in the grep pattern.
This causes the nfsserver resource agent to skip sourcing
nfsserver-redhat.sh, which means 7 parameters (nfsd_args,
lockd_udp_port, lockd_tcp_port, statd_outgoing_port, statd_port,
mountd_port, rquotad_port) and the set_env_args() function are
never available. The same gate affects Filesystem and IPaddr2 agents.
Upstream: https://github.com/ClusterLabs/resource-agents/pull/1756
Upstream-commit: 6cde49d
---
heartbeat/ocf-distro | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/heartbeat/ocf-distro b/heartbeat/ocf-distro
index abcdef1..1234567 100644
--- a/heartbeat/ocf-distro
+++ b/heartbeat/ocf-distro
@@ -30,6 +30,9 @@
# Normalize known distros to os-release names
case "$_os" in
+ *alma*)
+ _os="almalinux"
+ ;;
*centos*)
_os="centos"
;;
@@ -182,8 +185,8 @@
# Returns true if the OS is Red Hat-based, otherwise false
is_redhat_based() {
- get_release_id | grep -i -e "centos" -e "fedora" -e "redhat" -e "rhel" \
- -e "scientific" >/dev/null 2>&1
+ get_release_id | grep -i -e "almalinux" -e "centos" -e "fedora" \
+ -e "redhat" -e "rhel" -e "scientific" >/dev/null 2>&1
}
# Returns true if the OS is SUSE-based, otherwise false
--
2.43.0

View File

@ -0,0 +1,45 @@
diff --color -uNr a/heartbeat/gcp-pd-move.in b/heartbeat/gcp-pd-move.in
--- a/heartbeat/gcp-pd-move.in 2024-07-22 10:59:42.170483160 +0200
+++ b/heartbeat/gcp-pd-move.in 2024-07-22 11:01:51.455543850 +0200
@@ -32,6 +32,7 @@
from ocf import logger
try:
+ sys.path.insert(0, '/usr/lib/resource-agents/bundled/gcp')
import googleapiclient.discovery
except ImportError:
pass
diff --color -uNr a/heartbeat/gcp-vpc-move-ip.in b/heartbeat/gcp-vpc-move-ip.in
--- a/heartbeat/gcp-vpc-move-ip.in 2024-07-22 10:59:42.170483160 +0200
+++ b/heartbeat/gcp-vpc-move-ip.in 2024-07-22 11:01:18.010752081 +0200
@@ -36,7 +36,7 @@
. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs
# Defaults
-OCF_RESKEY_gcloud_default="/usr/bin/gcloud"
+OCF_RESKEY_gcloud_default="/usr/bin/gcloud-ra"
OCF_RESKEY_configuration_default="default"
OCF_RESKEY_vpc_network_default="default"
OCF_RESKEY_interface_default="eth0"
diff --color -uNr a/heartbeat/gcp-vpc-move-route.in b/heartbeat/gcp-vpc-move-route.in
--- a/heartbeat/gcp-vpc-move-route.in 2024-07-22 10:59:42.170483160 +0200
+++ b/heartbeat/gcp-vpc-move-route.in 2024-07-22 11:01:18.011752105 +0200
@@ -45,6 +45,7 @@
from ocf import *
try:
+ sys.path.insert(0, '/usr/lib/resource-agents/bundled/gcp')
import googleapiclient.discovery
import pyroute2
try:
diff --color -uNr a/heartbeat/gcp-vpc-move-vip.in b/heartbeat/gcp-vpc-move-vip.in
--- a/heartbeat/gcp-vpc-move-vip.in 2024-07-22 10:59:42.170483160 +0200
+++ b/heartbeat/gcp-vpc-move-vip.in 2024-07-22 11:01:18.012752128 +0200
@@ -29,6 +29,7 @@
from ocf import *
try:
+ sys.path.insert(0, '/usr/lib/resource-agents/bundled/gcp')
import googleapiclient.discovery
try:
from google.oauth2.service_account import Credentials as ServiceAccountCredentials

View File

@ -0,0 +1,129 @@
diff -uNr a/bundled/gcp/google-cloud-sdk/lib/third_party/oauth2client/_pure_python_crypt.py b/bundled/gcp/google-cloud-sdk/lib/third_party/oauth2client/_pure_python_crypt.py
--- a/bundled/gcp/google-cloud-sdk/lib/third_party/oauth2client/_pure_python_crypt.py 1980-01-01 09:00:00.000000000 +0100
+++ b/bundled/gcp/google-cloud-sdk/lib/third_party/oauth2client/_pure_python_crypt.py 2019-04-04 11:56:00.292677044 +0200
@@ -19,8 +19,14 @@
certificates.
"""
+from pyasn1.codec.der import decoder
from pyasn1_modules import pem
-import rsa
+from pyasn1_modules.rfc2459 import Certificate
+from pyasn1_modules.rfc5208 import PrivateKeyInfo
+from cryptography.hazmat.primitives import serialization, hashes
+from cryptography.hazmat.primitives.asymmetric import padding
+from cryptography import x509
+from cryptography.hazmat.backends import default_backend
import six
from oauth2client import _helpers
@@ -40,7 +46,7 @@
'-----END RSA PRIVATE KEY-----')
_PKCS8_MARKER = ('-----BEGIN PRIVATE KEY-----',
'-----END PRIVATE KEY-----')
-_PKCS8_SPEC = None
+_PKCS8_SPEC = PrivateKeyInfo()
def _bit_list_to_bytes(bit_list):
@@ -67,7 +73,8 @@
"""
def __init__(self, pubkey):
- self._pubkey = pubkey
+ self._pubkey = serialization.load_pem_public_key(pubkey,
+ backend=default_backend())
def verify(self, message, signature):
"""Verifies a message against a signature.
@@ -84,8 +91,9 @@
"""
message = _helpers._to_bytes(message, encoding='utf-8')
try:
- return rsa.pkcs1.verify(message, signature, self._pubkey)
- except (ValueError, rsa.pkcs1.VerificationError):
+ return self._pubkey.verify(signature, message, padding.PKCS1v15(),
+ hashes.SHA256())
+ except (ValueError, TypeError, InvalidSignature):
return False
@classmethod
@@ -109,19 +117,18 @@
"""
key_pem = _helpers._to_bytes(key_pem)
if is_x509_cert:
- from pyasn1.codec.der import decoder
- from pyasn1_modules import rfc2459
-
- der = rsa.pem.load_pem(key_pem, 'CERTIFICATE')
- asn1_cert, remaining = decoder.decode(der, asn1Spec=rfc2459.Certificate())
+ der = x509.load_pem_x509_certificate(pem_data, default_backend())
+ asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate())
if remaining != b'':
raise ValueError('Unused bytes', remaining)
cert_info = asn1_cert['tbsCertificate']['subjectPublicKeyInfo']
key_bytes = _bit_list_to_bytes(cert_info['subjectPublicKey'])
- pubkey = rsa.PublicKey.load_pkcs1(key_bytes, 'DER')
+ pubkey = serialization.load_der_public_key(decoded_key,
+ backend=default_backend())
else:
- pubkey = rsa.PublicKey.load_pkcs1(key_pem, 'PEM')
+ pubkey = serialization.load_pem_public_key(decoded_key,
+ backend=default_backend())
return cls(pubkey)
@@ -134,6 +141,8 @@
def __init__(self, pkey):
self._key = pkey
+ self._pubkey = serialization.load_pem_private_key(pkey,
+ backend=default_backend())
def sign(self, message):
"""Signs a message.
@@ -145,7 +154,7 @@
string, The signature of the message for the given key.
"""
message = _helpers._to_bytes(message, encoding='utf-8')
- return rsa.pkcs1.sign(message, self._key, 'SHA-256')
+ return self._key.sign(message, padding.PKCS1v15(), hashes.SHA256())
@classmethod
def from_string(cls, key, password='notasecret'):
@@ -163,27 +172,24 @@
ValueError if the key cannot be parsed as PKCS#1 or PKCS#8 in
PEM format.
"""
- global _PKCS8_SPEC
key = _helpers._from_bytes(key) # pem expects str in Py3
marker_id, key_bytes = pem.readPemBlocksFromFile(
six.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER)
if marker_id == 0:
- pkey = rsa.key.PrivateKey.load_pkcs1(key_bytes,
- format='DER')
- elif marker_id == 1:
- from pyasn1.codec.der import decoder
- from pyasn1_modules import rfc5208
+ pkey = serialization.load_der_private_key(
+ key_bytes, password=None,
+ backend=default_backend())
- if _PKCS8_SPEC is None:
- _PKCS8_SPEC = rfc5208.PrivateKeyInfo()
+ elif marker_id == 1:
key_info, remaining = decoder.decode(
key_bytes, asn1Spec=_PKCS8_SPEC)
if remaining != b'':
raise ValueError('Unused bytes', remaining)
pkey_info = key_info.getComponentByName('privateKey')
- pkey = rsa.key.PrivateKey.load_pkcs1(pkey_info.asOctets(),
- format='DER')
+ pkey = serialization.load_der_private_key(
+ pkey_info.asOctets(), password=None,
+ backend=default_backend())
else:
raise ValueError('No key could be detected.')

View File

@ -1,85 +0,0 @@
From d08a7f74427ea2cf7d355a0f7f6d8f583e2d0cba Mon Sep 17 00:00:00 2001
From: Carlo Lobrano <c.lobrano@gmail.com>
Date: Thu, 3 Jul 2025 12:22:12 +0200
Subject: [PATCH] OCPBUGS-58324: podman-etcd Add OOM score adjustment for etcd
containers
This change introduces a new `oom` parameter to the `podman-etcd` OCF
agent. This allows tuning the Out-Of-Memory (OOM) score adjustment for
the etcd container.
The `oom` parameter accepts integer values from -1000 to 1000,
defaulting to -997 (system-node-critical equivalent).
see https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/#node-out-of-memory-behavior
Key changes:
- Added `OCF_RESKEY_oom` parameter to agent definition (`content type="integer"`).
- Integrated `--oom-score-adj` option into `podman_start()`.
- Implemented input validation for `oom` in `podman_validate()`,
ensuring values are within the [-1000:1000] range.
---
heartbeat/podman-etcd | 22 +++++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)
diff --git a/heartbeat/podman-etcd b/heartbeat/podman-etcd
index 6762112ec..884b7c579 100755
--- a/heartbeat/podman-etcd
+++ b/heartbeat/podman-etcd
@@ -45,6 +45,7 @@ OCF_RESKEY_nic_default="br-ex"
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_image=${OCF_RESKEY_image_default}}
: ${OCF_RESKEY_pod_manifest=${OCF_RESKEY_pod_manifest_default}}
@@ -53,6 +54,7 @@ OCF_RESKEY_reuse_default="0"
: ${OCF_RESKEY_authfile=${OCF_RESKEY_authfile_default}}
: ${OCF_RESKEY_allow_pull=${OCF_RESKEY_allow_pull_default}}
: ${OCF_RESKEY_reuse=${OCF_RESKEY_reuse_default}}
+: ${OCF_RESKEY_oom=${OCF_RESKEY_oom_default}}
#######################################################################
@@ -230,6 +232,16 @@ to stop the container before pacemaker.
<shortdesc lang="en">drop-in dependency</shortdesc>
<content type="boolean"/>
</parameter>
+
+<parameter name="oom" required="0" unique="0">
+<longdesc lang="en">
+Tune the host's Out-Of-Memory (OOM) preferences for containers (accepts values from -1000 to 1000).
+Default to same OOM score as system-node-critical
+https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/#node-out-of-memory-behavior
+</longdesc>
+<shortdesc lang="en">OOM for container</shortdesc>
+<content type="integer" default="${OCF_RESKEY_oom_default}"/>
+</parameter>
</parameters>
<actions>
@@ -1226,7 +1238,10 @@ podman_start()
fi
podman_create_mounts
- local run_opts="-d --name=${CONTAINER}"
+ local run_opts="--detach --name=${CONTAINER}"
+
+ 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
@@ -1513,6 +1528,11 @@ podman_validate()
exit $OCF_ERR_CONFIGURED
fi
+ if [ "$OCF_RESKEY_oom" -lt -1000 ] || [ "$OCF_RESKEY_oom" -gt 1000 ]; then
+ ocf_exit_reason "'oom' value ${OCF_RESKEY_oom} is out of range [-1000:1000]"
+ exit $OCF_ERR_CONFIGURED
+ fi
+
return $OCF_SUCCESS
}

View File

@ -0,0 +1,47 @@
From 57acb7c26d809cf864ec439b8bcd6364702022d5 Mon Sep 17 00:00:00 2001
From: Nate Prewitt <nate.prewitt@gmail.com>
Date: Wed, 25 Sep 2024 08:03:20 -0700
Subject: [PATCH] Only use hostname to do netrc lookup instead of netloc
---
src/requests/utils.py | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/aliyun/aliyunsdkcore/vendored/requests/utils.py b/aliyun/aliyunsdkcore/vendored/requests/utils.py
index 699683e5d9..8a307ca8a0 100644
--- a/aliyun/aliyunsdkcore/vendored/requests/utils.py
+++ b/aliyun/aliyunsdkcore/vendored/requests/utils.py
@@ -182,13 +182,7 @@
return
ri = urlparse(url)
-
- # Strip port numbers from netloc. This weird `if...encode`` dance is
- # used for Python 3.2, which doesn't support unicode literals.
- splitstr = b':'
- if isinstance(url, str):
- splitstr = splitstr.decode('ascii')
- host = ri.netloc.split(splitstr)[0]
+ host = ri.hostname
try:
_netrc = netrc(netrc_path).authenticators(host)
diff --git a/gcp/google-cloud-sdk/lib/third_party/requests/utils.py b/gcp/google-cloud-sdk/lib/third_party/requests/utils.py
index 699683e5d9..8a307ca8a0 100644
--- a/gcp/google-cloud-sdk/lib/third_party/requests/utils.py
+++ b/gcp/google-cloud-sdk/lib/third_party/requests/utils.py
@@ -236,13 +236,7 @@ def get_netrc_auth(url, raise_errors=False):
return
ri = urlparse(url)
-
- # Strip port numbers from netloc. This weird `if...encode`` dance is
- # used for Python 3.2, which doesn't support unicode literals.
- splitstr = b':'
- if isinstance(url, str):
- splitstr = splitstr.decode('ascii')
- host = ri.netloc.split(splitstr)[0]
+ host = ri.hostname
try:
_netrc = netrc(netrc_path).authenticators(host)

View File

@ -1,686 +0,0 @@
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

@ -1,193 +0,0 @@
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"

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +0,0 @@
--- a/heartbeat/powervs-move-ip.in 2025-09-15 16:13:34.225046827 +0200
+++ b/heartbeat/powervs-move-ip.in 2025-09-15 17:39:02.746258434 +0200
@@ -33,9 +33,13 @@
from pathlib import Path
from urllib.parse import urlparse
-import requests
-import requests.adapters
-import urllib3.util
+try:
+ sys.path.insert(0, '/usr/lib/fence-agents/support/ibm/lib/python#PYTHON3_VERSION#/site-packages')
+ import requests
+ import requests.adapters
+ import urllib3.util
+except ImportError:
+ pass
# Constants
OCF_FUNCTIONS_DIR = os.environ.get(

View File

@ -1,197 +0,0 @@
From a4e496e5e6d9abde1b071fa2dfa1c6e7ba899cf1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Edmund=20H=C3=A4fele?= <edmund.haefele@de.ibm.com>
Date: Thu, 30 Oct 2025 13:03:22 +0100
Subject: [PATCH] Update powervs-move-ip
- Add `iflabel` argument.
- Increase maximum number of retries for HTTP requests to four.
---
heartbeat/powervs-move-ip.in | 66 +++++++++++++++++++++++++-----------
1 file changed, 47 insertions(+), 19 deletions(-)
diff --git a/heartbeat/powervs-move-ip.in b/heartbeat/powervs-move-ip.in
index d55979e52..e2250c998 100755
--- a/heartbeat/powervs-move-ip.in
+++ b/heartbeat/powervs-move-ip.in
@@ -50,11 +50,13 @@ RESOURCE_OPTIONS = (
"use_token_cache",
"monitor_api",
"device",
+ "iflabel",
"proxy",
)
IP_CMD = "/usr/sbin/ip"
+IFLABEL_MAX_LEN = 15 # Maximum character limit for interface labels
REQUESTS_TIMEOUT = 5 # Timeout for requests calls
-HTTP_MAX_RETRIES = 3 # Maximum number of retries for HTTP requests
+HTTP_MAX_RETRIES = 4 # Maximum number of retries for HTTP requests
HTTP_BACKOFF_FACTOR = 0.3 # Sleep (factor * (2^number of previous retries)) secs
HTTP_STATUS_FORCE_RETRIES = (500, 502, 503, 504) # HTTP status codes to retry on
HTTP_RETRY_ALLOWED_METHODS = frozenset({"GET", "POST", "PUT", "DELETE"})
@@ -154,13 +156,13 @@ def ip_check_device(device):
return False
-def ip_alias_add(ip, device):
+def ip_alias_add(ip, device, label=None):
"""Add an IP alias to the given device."""
ip_cidr = f"{ip}/{CIDR_NETMASK}"
ocf.logger.debug(
- f"[ip_alias_add]: adding IP alias '{ip_cidr}' to interface '{device}'"
+ f"[ip_alias_add]: adding IP alias '{ip_cidr}' with label '{label}' to interface '{device}'"
)
- _ = ip_address_add(ip_cidr, device)
+ _ = ip_address_add(ip_cidr, device, label)
def ip_alias_remove(ip):
@@ -522,6 +524,7 @@ class PowerCloudRoute(PowerCloudAPI):
region="",
route_host_map="",
device="",
+ iflabel="",
proxy="",
monitor_api="",
use_token_cache="",
@@ -543,6 +546,7 @@ class PowerCloudRoute(PowerCloudAPI):
self.route_info = self._get_route_info()
self.route_name = self.route_info["name"]
self.device = self._get_device_name(device)
+ self.iflabel = self._make_iflabel(iflabel)
def _get_ip_info(self, ip):
"""Validate the given IP address and return its standard form."""
@@ -588,7 +592,7 @@ class PowerCloudRoute(PowerCloudAPI):
nodename = (
hostname
if not self._is_remote_route
- else next((h for h in route_map if h != hostname), None)
+ else next((host for host in route_map if host != hostname), None)
)
if not nodename or nodename not in route_map:
@@ -646,6 +650,21 @@ class PowerCloudRoute(PowerCloudAPI):
ocf.OCF_ERR_CONFIGURED,
)
+ def _make_iflabel(self, label=None):
+ """Constructs an interface label in the format 'device:label' if both are provided."""
+ if not label or self._is_remote_route:
+ return None
+
+ iflabel = f"{self.device}:{label}"
+
+ if len(iflabel) > IFLABEL_MAX_LEN:
+ raise PowerCloudRouteError(
+ f"_make_iflabel: interface label '{iflabel}' exceeds limit of {IFLABEL_MAX_LEN} characters",
+ ocf.OCF_ERR_CONFIGURED,
+ )
+
+ return iflabel
+
def _set_route_enabled(self, enabled: bool):
"""Enable or disable the PowerVS network route."""
resource = f"/v1/routes/{self.route_id}"
@@ -706,6 +725,7 @@ def start_action(
use_token_cache="",
monitor_api="",
device="",
+ iflabel="",
proxy="",
):
"""Assign the service IP.
@@ -730,7 +750,7 @@ def start_action(
local_route = create_route_instance(resource_options)
# Add IP alias
- ip_alias_add(ip, local_route.device)
+ ip_alias_add(ip, local_route.device, local_route.iflabel)
# Enable local route
ocf.logger.debug(f"[start_action]: enabling local route '{local_route.route_name}'")
@@ -758,6 +778,7 @@ def stop_action(
use_token_cache="",
monitor_api="",
device="",
+ iflabel="",
proxy="",
):
"""Remove the service IP.
@@ -810,6 +831,7 @@ def monitor_action(
use_token_cache="",
monitor_api="",
device="",
+ iflabel="",
proxy="",
):
"""Monitor the service IP.
@@ -829,15 +851,11 @@ def monitor_action(
interface_name = ip_find_device(ip)
if not use_extended_monitor:
- if interface_name:
- ocf.logger.debug(
- f"[monitor_action]: IP alias '{ip}' is active'"
- )
+ if interface_name:
+ ocf.logger.debug(f"[monitor_action]: IP alias '{ip}' is active'")
return ocf.OCF_SUCCESS
- else:
- ocf.logger.debug(
- f"[monitor_action]: IP alias '{ip}' is not active"
- )
+ else:
+ ocf.logger.debug(f"[monitor_action]: IP alias '{ip}' is not active")
return ocf.OCF_NOT_RUNNING
remote_route = create_route_instance(
@@ -893,6 +911,7 @@ def validate_all_action(
use_token_cache="",
monitor_api="",
device="",
+ iflabel="",
proxy="",
):
"""Validate resource agent parameters.
@@ -914,12 +933,10 @@ def main():
Resource Agent to move an IP address from one Power Virtual Server instance to another.
Prerequisites:
- 1. Red Hat Enterprise Linux 9.4 or higher
-
- 2. Two-node cluster
+ 1. Two-node cluster
- Distributed across two PowerVS workspaces in separate data centers within the same region.
- 3. IBM Cloud API Key:
+ 2. IBM Cloud API Key:
- Create a service API key with privileges for both workspaces.
- Save the key in a file and copy it to both cluster nodes using the same path and filename.
- Reference the key file path in the resource definition.
@@ -932,7 +949,7 @@ def main():
"powervs-move-ip",
shortdesc="Manages Power Virtual Server overlay IP routes.",
longdesc=agent_description,
- version=1.00,
+ version=1.01,
)
agent.add_parameter(
@@ -1011,6 +1028,17 @@ def main():
default="",
required=False,
)
+ agent.add_parameter(
+ "iflabel",
+ shortdesc="Network interface label",
+ longdesc=(
+ "A custom suffix for the IP address label. "
+ "It is appended to the interface name in the format device:label. "
+ "The full label must not exceed 15 characters. "
+ ),
+ content_type="string",
+ required=False,
+ )
agent.add_parameter(
"proxy",
shortdesc="Proxy",

View File

@ -1,186 +0,0 @@
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

@ -1,36 +0,0 @@
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

@ -1,321 +0,0 @@
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

@ -1,135 +0,0 @@
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

@ -1,166 +0,0 @@
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

@ -1,115 +0,0 @@
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

@ -1,222 +0,0 @@
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

@ -1,47 +0,0 @@
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

@ -1,158 +0,0 @@
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

@ -1,106 +0,0 @@
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

@ -1,161 +0,0 @@
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

@ -1,42 +0,0 @@
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

@ -1,107 +0,0 @@
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

@ -1,146 +0,0 @@
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

@ -1,52 +0,0 @@
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,45 @@
--- a/aliyun/aliyunsdkcore/vendored/requests/packages/urllib3/response.py 2023-10-17 19:42:56.000000000 +0200
+++ b/aliyun/aliyunsdkcore/vendored/requests/packages/urllib3/response.py 2026-01-02 11:19:25.583808492 +0100
@@ -135,8 +135,18 @@
they were applied.
"""
+ # Maximum allowed number of chained HTTP encodings in the
+ # Content-Encoding header.
+ max_decode_links = 5
+
def __init__(self, modes):
- self._decoders = [_get_decoder(m.strip()) for m in modes.split(",")]
+ encodings = [m.strip() for m in modes.split(",")]
+ if len(encodings) > self.max_decode_links:
+ raise DecodeError(
+ "Too many content encodings in the chain: "
+ f"{len(encodings)} > {self.max_decode_links}"
+ )
+ self._decoders = [_get_decoder(e) for e in encodings]
def flush(self):
return self._decoders[0].flush()
--- a/gcp/google-cloud-sdk/lib/third_party/urllib3/response.py 2023-10-17 19:42:56.000000000 +0200
+++ b/gcp/google-cloud-sdk/lib/third_party/urllib3/response.py 2026-01-02 11:19:25.583808492 +0100
@@ -135,8 +135,18 @@
they were applied.
"""
+ # Maximum allowed number of chained HTTP encodings in the
+ # Content-Encoding header.
+ max_decode_links = 5
+
def __init__(self, modes):
- self._decoders = [_get_decoder(m.strip()) for m in modes.split(",")]
+ encodings = [m.strip() for m in modes.split(",")]
+ if len(encodings) > self.max_decode_links:
+ raise DecodeError(
+ "Too many content encodings in the chain: "
+ f"{len(encodings)} > {self.max_decode_links}"
+ )
+ self._decoders = [_get_decoder(e) for e in encodings]
def flush(self):
return self._decoders[0].flush()

View File

@ -1,25 +0,0 @@
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,563 @@
--- a/aliyun/aliyunsdkcore/vendored/requests/packages/urllib3/response.py 2026-01-20 10:46:57.006470161 +0100
+++ b/aliyun/aliyunsdkcore/vendored/requests/packages/urllib3/response.py 2026-01-20 10:55:44.090084896 +0100
@@ -23,6 +23,7 @@
from .exceptions import (
BodyNotHttplibCompatible,
DecodeError,
+ DependencyWarning,
HTTPError,
IncompleteRead,
InvalidChunkLength,
@@ -41,34 +42,60 @@
class DeflateDecoder(object):
def __init__(self):
self._first_try = True
- self._data = b""
+ self._first_try_data = b""
+ self._unfed_data = b""
self._obj = zlib.decompressobj()
def __getattr__(self, name):
return getattr(self._obj, name)
- def decompress(self, data):
- if not data:
+ def decompress(self, data: bytes, max_length: int = -1) -> bytes:
+ data = self._unfed_data + data
+ self._unfed_data = b""
+ if not data and not self._obj.unconsumed_tail:
return data
+ original_max_length = max_length
+ if original_max_length < 0:
+ max_length = 0
+ elif original_max_length == 0:
+ # We should not pass 0 to the zlib decompressor because 0 is
+ # the default value that will make zlib decompress without a
+ # length limit.
+ # Data should be stored for subsequent calls.
+ self._unfed_data = data
+ return b""
+ # Subsequent calls always reuse `self._obj`. zlib requires
+ # passing the unconsumed tail if decompression is to continue.
if not self._first_try:
- return self._obj.decompress(data)
+ return self._obj.decompress(
+ self._obj.unconsumed_tail + data, max_length=max_length
+ )
- self._data += data
+ # First call tries with RFC 1950 ZLIB format.
+ self._first_try_data += data
try:
- decompressed = self._obj.decompress(data)
+ decompressed = self._obj.decompress(data, max_length=max_length)
if decompressed:
self._first_try = False
- self._data = None
+ self._first_try_data = b""
return decompressed
+ # On failure, it falls back to RFC 1951 DEFLATE format.
except zlib.error:
self._first_try = False
self._obj = zlib.decompressobj(-zlib.MAX_WBITS)
try:
- return self.decompress(self._data)
+ return self.decompress(
+ self._first_try_data, max_length=original_max_length
+ )
finally:
- self._data = None
+ self._first_try_data = b""
+ @property
+ def has_unconsumed_tail(self) -> bool:
+ return bool(self._unfed_data) or (
+ bool(self._obj.unconsumed_tail) and not self._first_try
+ )
class GzipDecoderState(object):
@@ -81,30 +108,64 @@
def __init__(self):
self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
self._state = GzipDecoderState.FIRST_MEMBER
+ self._unconsumed_tail = b""
def __getattr__(self, name):
return getattr(self._obj, name)
- def decompress(self, data):
+ def decompress(self, data: bytes, max_length: int = -1) -> bytes:
ret = bytearray()
- if self._state == GzipDecoderState.SWALLOW_DATA or not data:
+ if self._state == GzipDecoderState.SWALLOW_DATA:
+ return bytes(ret)
+
+ if max_length == 0:
+ # We should not pass 0 to the zlib decompressor because 0 is
+ # the default value that will make zlib decompress without a
+ # length limit.
+ # Data should be stored for subsequent calls.
+ self._unconsumed_tail += data
+ return b""
+
+ # zlib requires passing the unconsumed tail to the subsequent
+ # call if decompression is to continue.
+ data = self._unconsumed_tail + data
+ if not data and self._obj.eof:
return bytes(ret)
+
while True:
try:
- ret += self._obj.decompress(data)
+ ret += self._obj.decompress(
+ data, max_length=max(max_length - len(ret), 0)
+ )
except zlib.error:
previous_state = self._state
# Ignore data after the first error
self._state = GzipDecoderState.SWALLOW_DATA
+ self._unconsumed_tail = b""
if previous_state == GzipDecoderState.OTHER_MEMBERS:
# Allow trailing garbage acceptable in other gzip clients
return bytes(ret)
raise
- data = self._obj.unused_data
+
+ self._unconsumed_tail = data = (
+ self._obj.unconsumed_tail or self._obj.unused_data
+ )
+ if max_length > 0 and len(ret) >= max_length:
+ break
+
if not data:
return bytes(ret)
- self._state = GzipDecoderState.OTHER_MEMBERS
- self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
+ # When the end of a gzip member is reached, a new decompressor
+ # must be created for unused (possibly future) data.
+ if self._obj.eof:
+ self._state = GzipDecoderState.OTHER_MEMBERS
+ self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
+
+ return bytes(ret)
+
+ @property
+ def has_unconsumed_tail(self) -> bool:
+ return bool(self._unconsumed_tail)
if brotli is not None:
@@ -116,9 +177,35 @@
def __init__(self):
self._obj = brotli.Decompressor()
if hasattr(self._obj, "decompress"):
- self.decompress = self._obj.decompress
+ setattr(self, "_decompress", self._obj.decompress)
else:
- self.decompress = self._obj.process
+ setattr(self, "_decompress", self._obj.process)
+
+ # Requires Brotli >= 1.2.0 for `output_buffer_limit`.
+ def _decompress(self, data: bytes, output_buffer_limit: int = -1) -> bytes:
+ raise NotImplementedError()
+
+ def decompress(self, data: bytes, max_length: int = -1) -> bytes:
+ try:
+ if max_length > 0:
+ return self._decompress(data, output_buffer_limit=max_length)
+ else:
+ return self._decompress(data)
+ except TypeError:
+ # Fallback for Brotli/brotlicffi/brotlipy versions without
+ # the `output_buffer_limit` parameter.
+ warnings.warn(
+ "Brotli >= 1.2.0 is required to prevent decompression bombs.",
+ DependencyWarning,
+ )
+ return self._decompress(data)
+
+ @property
+ def has_unconsumed_tail(self) -> bool:
+ try:
+ return not self._obj.can_accept_more_data()
+ except AttributeError:
+ return False
def flush(self):
if hasattr(self._obj, "flush"):
@@ -151,10 +238,35 @@
def flush(self):
return self._decoders[0].flush()
- def decompress(self, data):
- for d in reversed(self._decoders):
- data = d.decompress(data)
- return data
+ def decompress(self, data: bytes, max_length: int = -1) -> bytes:
+ if max_length <= 0:
+ for d in reversed(self._decoders):
+ data = d.decompress(data)
+ return data
+
+ ret = bytearray()
+ # Every while loop iteration goes through all decoders once.
+ # It exits when enough data is read or no more data can be read.
+ # It is possible that the while loop iteration does not produce
+ # any data because we retrieve up to `max_length` from every
+ # decoder, and the amount of bytes may be insufficient for the
+ # next decoder to produce enough/any output.
+ while True:
+ any_data = False
+ for d in reversed(self._decoders):
+ data = d.decompress(data, max_length=max_length - len(ret))
+ if data:
+ any_data = True
+ # We should not break when no data is returned because
+ # next decoders may produce data even with empty input.
+ ret += data
+ if not any_data or len(ret) >= max_length:
+ return bytes(ret)
+ data = b""
+
+ @property
+ def has_unconsumed_tail(self) -> bool:
+ return any(d.has_unconsumed_tail for d in self._decoders)
def _get_decoder(mode):
@@ -405,16 +517,25 @@
if brotli is not None:
DECODER_ERROR_CLASSES += (brotli.error,)
- def _decode(self, data, decode_content, flush_decoder):
+ def _decode(
+ self,
+ data: bytes,
+ decode_content: bool,
+ flush_decoder: bool,
+ max_length: int = None,
+ ) -> bytes:
"""
Decode the data passed in and potentially flush the decoder.
"""
if not decode_content:
return data
+ if max_length is None or flush_decoder:
+ max_length = -1
+
try:
if self._decoder:
- data = self._decoder.decompress(data)
+ data = self._decoder.decompress(data, max_length=max_length)
except self.DECODER_ERROR_CLASSES as e:
content_encoding = self.headers.get("content-encoding", "").lower()
raise DecodeError(
@@ -634,7 +755,10 @@
for line in self.read_chunked(amt, decode_content=decode_content):
yield line
else:
- while not is_fp_closed(self._fp):
+ while (
+ not is_fp_closed(self._fp)
+ or (self._decoder and self._decoder.has_unconsumed_tail)
+ ):
data = self.read(amt=amt, decode_content=decode_content)
if data:
@@ -840,7 +964,10 @@
break
chunk = self._handle_chunk(amt)
decoded = self._decode(
- chunk, decode_content=decode_content, flush_decoder=False
+ chunk,
+ decode_content=decode_content,
+ flush_decoder=False,
+ max_length=amt,
)
if decoded:
yield decoded
--- a/gcp/google-cloud-sdk/lib/third_party/urllib3/response.py 2026-01-20 10:46:57.006470161 +0100
+++ b/gcp/google-cloud-sdk/lib/third_party/urllib3/response.py 2026-01-20 10:55:44.090084896 +0100
@@ -23,6 +23,7 @@
from .exceptions import (
BodyNotHttplibCompatible,
DecodeError,
+ DependencyWarning,
HTTPError,
IncompleteRead,
InvalidChunkLength,
@@ -41,34 +42,60 @@
class DeflateDecoder(object):
def __init__(self):
self._first_try = True
- self._data = b""
+ self._first_try_data = b""
+ self._unfed_data = b""
self._obj = zlib.decompressobj()
def __getattr__(self, name):
return getattr(self._obj, name)
- def decompress(self, data):
- if not data:
+ def decompress(self, data: bytes, max_length: int = -1) -> bytes:
+ data = self._unfed_data + data
+ self._unfed_data = b""
+ if not data and not self._obj.unconsumed_tail:
return data
+ original_max_length = max_length
+ if original_max_length < 0:
+ max_length = 0
+ elif original_max_length == 0:
+ # We should not pass 0 to the zlib decompressor because 0 is
+ # the default value that will make zlib decompress without a
+ # length limit.
+ # Data should be stored for subsequent calls.
+ self._unfed_data = data
+ return b""
+ # Subsequent calls always reuse `self._obj`. zlib requires
+ # passing the unconsumed tail if decompression is to continue.
if not self._first_try:
- return self._obj.decompress(data)
+ return self._obj.decompress(
+ self._obj.unconsumed_tail + data, max_length=max_length
+ )
- self._data += data
+ # First call tries with RFC 1950 ZLIB format.
+ self._first_try_data += data
try:
- decompressed = self._obj.decompress(data)
+ decompressed = self._obj.decompress(data, max_length=max_length)
if decompressed:
self._first_try = False
- self._data = None
+ self._first_try_data = b""
return decompressed
+ # On failure, it falls back to RFC 1951 DEFLATE format.
except zlib.error:
self._first_try = False
self._obj = zlib.decompressobj(-zlib.MAX_WBITS)
try:
- return self.decompress(self._data)
+ return self.decompress(
+ self._first_try_data, max_length=original_max_length
+ )
finally:
- self._data = None
+ self._first_try_data = b""
+ @property
+ def has_unconsumed_tail(self) -> bool:
+ return bool(self._unfed_data) or (
+ bool(self._obj.unconsumed_tail) and not self._first_try
+ )
class GzipDecoderState(object):
@@ -81,30 +108,64 @@
def __init__(self):
self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
self._state = GzipDecoderState.FIRST_MEMBER
+ self._unconsumed_tail = b""
def __getattr__(self, name):
return getattr(self._obj, name)
- def decompress(self, data):
+ def decompress(self, data: bytes, max_length: int = -1) -> bytes:
ret = bytearray()
- if self._state == GzipDecoderState.SWALLOW_DATA or not data:
+ if self._state == GzipDecoderState.SWALLOW_DATA:
+ return bytes(ret)
+
+ if max_length == 0:
+ # We should not pass 0 to the zlib decompressor because 0 is
+ # the default value that will make zlib decompress without a
+ # length limit.
+ # Data should be stored for subsequent calls.
+ self._unconsumed_tail += data
+ return b""
+
+ # zlib requires passing the unconsumed tail to the subsequent
+ # call if decompression is to continue.
+ data = self._unconsumed_tail + data
+ if not data and self._obj.eof:
return bytes(ret)
+
while True:
try:
- ret += self._obj.decompress(data)
+ ret += self._obj.decompress(
+ data, max_length=max(max_length - len(ret), 0)
+ )
except zlib.error:
previous_state = self._state
# Ignore data after the first error
self._state = GzipDecoderState.SWALLOW_DATA
+ self._unconsumed_tail = b""
if previous_state == GzipDecoderState.OTHER_MEMBERS:
# Allow trailing garbage acceptable in other gzip clients
return bytes(ret)
raise
- data = self._obj.unused_data
+
+ self._unconsumed_tail = data = (
+ self._obj.unconsumed_tail or self._obj.unused_data
+ )
+ if max_length > 0 and len(ret) >= max_length:
+ break
+
if not data:
return bytes(ret)
- self._state = GzipDecoderState.OTHER_MEMBERS
- self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
+ # When the end of a gzip member is reached, a new decompressor
+ # must be created for unused (possibly future) data.
+ if self._obj.eof:
+ self._state = GzipDecoderState.OTHER_MEMBERS
+ self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
+
+ return bytes(ret)
+
+ @property
+ def has_unconsumed_tail(self) -> bool:
+ return bool(self._unconsumed_tail)
if brotli is not None:
@@ -116,9 +177,35 @@
def __init__(self):
self._obj = brotli.Decompressor()
if hasattr(self._obj, "decompress"):
- self.decompress = self._obj.decompress
+ setattr(self, "_decompress", self._obj.decompress)
else:
- self.decompress = self._obj.process
+ setattr(self, "_decompress", self._obj.process)
+
+ # Requires Brotli >= 1.2.0 for `output_buffer_limit`.
+ def _decompress(self, data: bytes, output_buffer_limit: int = -1) -> bytes:
+ raise NotImplementedError()
+
+ def decompress(self, data: bytes, max_length: int = -1) -> bytes:
+ try:
+ if max_length > 0:
+ return self._decompress(data, output_buffer_limit=max_length)
+ else:
+ return self._decompress(data)
+ except TypeError:
+ # Fallback for Brotli/brotlicffi/brotlipy versions without
+ # the `output_buffer_limit` parameter.
+ warnings.warn(
+ "Brotli >= 1.2.0 is required to prevent decompression bombs.",
+ DependencyWarning,
+ )
+ return self._decompress(data)
+
+ @property
+ def has_unconsumed_tail(self) -> bool:
+ try:
+ return not self._obj.can_accept_more_data()
+ except AttributeError:
+ return False
def flush(self):
if hasattr(self._obj, "flush"):
@@ -151,10 +238,35 @@
def flush(self):
return self._decoders[0].flush()
- def decompress(self, data):
- for d in reversed(self._decoders):
- data = d.decompress(data)
- return data
+ def decompress(self, data: bytes, max_length: int = -1) -> bytes:
+ if max_length <= 0:
+ for d in reversed(self._decoders):
+ data = d.decompress(data)
+ return data
+
+ ret = bytearray()
+ # Every while loop iteration goes through all decoders once.
+ # It exits when enough data is read or no more data can be read.
+ # It is possible that the while loop iteration does not produce
+ # any data because we retrieve up to `max_length` from every
+ # decoder, and the amount of bytes may be insufficient for the
+ # next decoder to produce enough/any output.
+ while True:
+ any_data = False
+ for d in reversed(self._decoders):
+ data = d.decompress(data, max_length=max_length - len(ret))
+ if data:
+ any_data = True
+ # We should not break when no data is returned because
+ # next decoders may produce data even with empty input.
+ ret += data
+ if not any_data or len(ret) >= max_length:
+ return bytes(ret)
+ data = b""
+
+ @property
+ def has_unconsumed_tail(self) -> bool:
+ return any(d.has_unconsumed_tail for d in self._decoders)
def _get_decoder(mode):
@@ -405,16 +517,25 @@
if brotli is not None:
DECODER_ERROR_CLASSES += (brotli.error,)
- def _decode(self, data, decode_content, flush_decoder):
+ def _decode(
+ self,
+ data: bytes,
+ decode_content: bool,
+ flush_decoder: bool,
+ max_length: int = None,
+ ) -> bytes:
"""
Decode the data passed in and potentially flush the decoder.
"""
if not decode_content:
return data
+ if max_length is None or flush_decoder:
+ max_length = -1
+
try:
if self._decoder:
- data = self._decoder.decompress(data)
+ data = self._decoder.decompress(data, max_length=max_length)
except self.DECODER_ERROR_CLASSES as e:
content_encoding = self.headers.get("content-encoding", "").lower()
raise DecodeError(
@@ -634,7 +755,10 @@
for line in self.read_chunked(amt, decode_content=decode_content):
yield line
else:
- while not is_fp_closed(self._fp):
+ while (
+ not is_fp_closed(self._fp)
+ or (self._decoder and self._decoder.has_unconsumed_tail)
+ ):
data = self.read(amt=amt, decode_content=decode_content)
if data:
@@ -840,7 +964,10 @@
break
chunk = self._handle_chunk(amt)
decoded = self._decode(
- chunk, decode_content=decode_content, flush_decoder=False
+ chunk,
+ decode_content=decode_content,
+ flush_decoder=False,
+ max_length=amt,
)
if decoded:
yield decoded

View File

@ -0,0 +1,63 @@
--- a/aliyun/aliyunsdkcore/vendored/requests/packages/urllib3/response.py 2023-10-17 19:42:56.000000000 +0200
+++ b/aliyun/aliyunsdkcore/vendored/requests/packages/urllib3/response.py 2026-01-13 14:17:48.477104360 +0100
@@ -350,6 +350,7 @@
self.reason = reason
self.strict = strict
self.decode_content = decode_content
+ self._has_decoded_content = False
self.retries = retries
self.enforce_content_length = enforce_content_length
self.auto_close = auto_close
@@ -414,7 +415,11 @@
Unread data in the HTTPResponse connection blocks the connection from being released back to the pool.
"""
try:
- self.read()
+ self.read(
+ # Do not spend resources decoding the content unless
+ # decoding has already been initiated.
+ decode_content=self._has_decoded_content,
+ )
except (HTTPError, SocketError, BaseSSLError, HTTPException):
pass
@@ -536,6 +541,7 @@
try:
if self._decoder:
data = self._decoder.decompress(data, max_length=max_length)
+ self._has_decoded_content = True
except self.DECODER_ERROR_CLASSES as e:
content_encoding = self.headers.get("content-encoding", "").lower()
raise DecodeError(
--- a/gcp/google-cloud-sdk/lib/third_party/urllib3/response.py 2023-10-17 19:42:56.000000000 +0200
+++ b/gcp/google-cloud-sdk/lib/third_party/urllib3/response.py 2026-01-13 14:17:48.477104360 +0100
@@ -350,6 +350,7 @@
self.reason = reason
self.strict = strict
self.decode_content = decode_content
+ self._has_decoded_content = False
self.retries = retries
self.enforce_content_length = enforce_content_length
self.auto_close = auto_close
@@ -414,7 +415,11 @@
Unread data in the HTTPResponse connection blocks the connection from being released back to the pool.
"""
try:
- self.read()
+ self.read(
+ # Do not spend resources decoding the content unless
+ # decoding has already been initiated.
+ decode_content=self._has_decoded_content,
+ )
except (HTTPError, SocketError, BaseSSLError, HTTPException):
pass
@@ -536,6 +541,7 @@
try:
if self._decoder:
data = self._decoder.decompress(data, max_length=max_length)
+ self._has_decoded_content = True
except self.DECODER_ERROR_CLASSES as e:
content_encoding = self.headers.get("content-encoding", "").lower()
raise DecodeError(

View File

@ -0,0 +1,28 @@
--- a/gcp/google-cloud-sdk/lib/third_party/pyasn1/codec/ber/decoder.py 2019-10-17 07:00:19.000000000 +0200
+++ b/gcp/google-cloud-sdk/lib/third_party/pyasn1/codec/ber/decoder.py 2026-01-27 10:43:12.757563432 +0100
@@ -22,6 +22,10 @@
noValue = base.noValue
+# Maximum number of continuation octets (high-bit set) allowed per OID arc.
+# 20 octets allows up to 140-bit integers, supporting UUID-based OIDs
+MAX_OID_ARC_CONTINUATION_OCTETS = 20
+
class AbstractDecoder(object):
protoComponent = None
@@ -342,7 +346,14 @@
# Construct subid from a number of octets
nextSubId = subId
subId = 0
+ continuationOctetCount = 0
while nextSubId >= 128:
+ continuationOctetCount += 1
+ if continuationOctetCount > MAX_OID_ARC_CONTINUATION_OCTETS:
+ raise error.PyAsn1Error(
+ 'OID arc exceeds maximum continuation octets limit (%d) '
+ 'at position %d' % (MAX_OID_ARC_CONTINUATION_OCTETS, index)
+ )
subId = (subId << 7) + (nextSubId & 0x7F)
if index >= substrateLen:
raise error.SubstrateUnderrunError(

View File

@ -1,54 +0,0 @@
From 8f5c5a2a472ab404b6fd15ff492e72904dc8ac20 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Thu, 22 Jan 2026 07:37:40 +0100
Subject: [PATCH] powervs-move-ip/powervs-subnet: fix error logging
---
heartbeat/powervs-move-ip.in | 4 ++--
heartbeat/powervs-subnet.in | 10 ++++++----
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/heartbeat/powervs-move-ip.in b/heartbeat/powervs-move-ip.in
index e2250c998..0eea89f1d 100755
--- a/heartbeat/powervs-move-ip.in
+++ b/heartbeat/powervs-move-ip.in
@@ -310,9 +310,9 @@ class PowerCloudTokenManager:
return json.load(f)
finally:
fcntl.flock(f, fcntl.LOCK_UN)
- except (json.JSONDecodeError, FileNotFoundError, PermissionError):
+ except (json.JSONDecodeError, FileNotFoundError, PermissionError) as e:
ocf.logger.warning(
- "[PowerCloudTokenManager] _read_cache: failed to read token cache read due to missing file or malformed JSON."
+ f"[PowerCloudTokenManager] _read_cache: failed to read token cache read due to missing file or malformed JSON: '{e}'"
)
return {}
diff --git a/heartbeat/powervs-subnet.in b/heartbeat/powervs-subnet.in
index 062b1235e..b8f3864e9 100755
--- a/heartbeat/powervs-subnet.in
+++ b/heartbeat/powervs-subnet.in
@@ -837,8 +837,9 @@ def start_action(
if rc != ocf.OCF_SUCCESS:
return rc
- if monitor_action(**res_options) != ocf.OCF_SUCCESS:
- raise PowerCloudAPIError(f"start_action: start subnet: {ws.subnet_name} failed")
+ rc = monitor_action(**res_options)
+ if rc != ocf.OCF_SUCCESS:
+ raise PowerCloudAPIError(f"start_action: start subnet: {ws.subnet_name} failed", rc)
ocf.logger.info(
f"start_action: finished, added connection {conn_name} for subnet {ws.subnet_name}"
@@ -872,8 +873,9 @@ def stop_action(
ws.subnet_remove()
- if monitor_action(**res_options) != ocf.OCF_NOT_RUNNING:
- raise PowerCloudAPIError(f"stop_action: stop subnet {ws.subnet_name} failed")
+ rc = monitor_action(**res_options)
+ if rc != ocf.OCF_NOT_RUNNING:
+ raise PowerCloudAPIError(f"stop_action: stop subnet {ws.subnet_name} failed", rc)
ocf.logger.info(
f"stop_action: finished, deleted connection for subnet {ws.subnet_name}"

View File

@ -1,278 +0,0 @@
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

@ -1,111 +0,0 @@
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,153 +0,0 @@
--- a/heartbeat/pgsqlms
+++ b/heartbeat/pgsqlms
@@ -48,6 +48,8 @@
# Default parameters values
my $system_user_default = "postgres";
+my $monitor_user_default = "";
+my $monitor_password_default = "";
my $bindir_default = "/usr/bin";
my $pgdata_default = "/var/lib/pgsql/data";
my $pghost_default = "/tmp";
@@ -57,6 +59,8 @@
# Set default values if not found in environment
my $system_user = $ENV{'OCF_RESKEY_system_user'} || $system_user_default;
+my $monitor_user = $ENV{'OCF_RESKEY_monitor_user'} || $monitor_user_default;
+my $monitor_password = $ENV{'OCF_RESKEY_monitor_password'} || $monitor_password_default;
my $bindir = $ENV{'OCF_RESKEY_bindir'} || $bindir_default;
my $pgdata = $ENV{'OCF_RESKEY_pgdata'} || $pgdata_default;
my $datadir = $ENV{'OCF_RESKEY_datadir'} || $pgdata;
@@ -98,6 +102,7 @@
# the result as second one.
#
sub _query {
+ my $user = shift if @_ == 3;
my $query = shift;
my $res = shift;
my $connstr = "dbname=postgres";
@@ -110,6 +115,7 @@
my $ans;
my $pid;
my $rc;
+ my @psql_args;
unless ( defined $res and defined $query and $query ne '' ) {
ocf_log( 'debug', '_query: wrong parameters!' );
@@ -138,9 +144,19 @@
$pid = open(my $KID, "-|");
if ( $pid == 0 ) { # child
- exec $PGPSQL, '--set', 'ON_ERROR_STOP=1', '-qXAtf', $tmpfile,
- '-R', $RS, '-F', $FS, '--port', $pgport, '--host', $pghost,
- $connstr;
+ # Build psql arguments
+ @psql_args = ('--set', 'ON_ERROR_STOP=1', '-qXAtf', $tmpfile,
+ '-R', $RS, '-F', $FS, '--port', $pgport, '--host', $pghost);
+
+ # Add username and password for non-system user
+ if ( $user ) {
+ push @psql_args, '-U', $user;
+ $ENV{'PGPASSWORD'} = $monitor_password if $monitor_password ne '';
+ }
+
+ push @psql_args, $connstr;
+
+ exec $PGPSQL, @psql_args;
}
# parent
@@ -259,7 +275,7 @@
ORDER BY priority DESC
};
- $rc = _query( $query, \@rs );
+ $rc = _query( $monitor_user, $query, \@rs );
if ( $rc != 0 ) {
ocf_exit_reason( 'Query to get standby locations failed (%d)', $rc );
@@ -898,7 +914,7 @@
my $rc;
my @rs;
- $rc = _query( "SELECT pg_is_in_recovery()", \@rs );
+ $rc = _query( $monitor_user, "SELECT pg_is_in_recovery()", \@rs );
$is_in_recovery = $rs[0][0];
@@ -1050,6 +1066,18 @@
(optional, string, default "postgres")
+=item B<monitor_user>
+
+PostgreSQL user for monitor operations
+
+(optional, string, default "")
+
+=item B<monitor_password>
+
+PostgreSQL password for monitor user
+
+(optional, string, default "")
+
=item B<recovery_template>
B<ONLY> for PostgreSQL 11 and bellow.
@@ -1114,6 +1142,22 @@
<content type="string" default="$system_user_default" />
</parameter>
+ <parameter name="monitor_user" unique="0" required="0">
+ <longdesc lang="en">
+ PostgreSQL user that pgsql RA will use for monitor operations
+ </longdesc>
+ <shortdesc lang="en">PostgreSQL monitor User</shortdesc>
+ <content type="string" default="$monitor_user_default" />
+ </parameter>
+
+ <parameter name="monitor_password" unique="0" required="0">
+ <longdesc lang="en">
+ PostgreSQL password for monitor user
+ </longdesc>
+ <shortdesc lang="en">PostgreSQL monitor Password</shortdesc>
+ <content type="string" default="$monitor_password_default" />
+ </parameter>
+
<parameter name="bindir" unique="0" required="0">
<longdesc lang="en">
Path to the directory storing the PostgreSQL binaries. The agent uses psql, pg_isready, pg_controldata and pg_ctl.
@@ -1523,6 +1567,7 @@
my $rc;
my $state;
my $pidfile = "$datadir/postmaster.pid";
+ my $pgisready_rc;
# 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;
@@ -1536,9 +1581,22 @@
return $OCF_SUCCESS;
}
elsif ( $rc != $OCF_SUCCESS and $rc != $OCF_RUNNING_MASTER ) {
- ocf_exit_reason( 'Unexpected state for instance "%s" (returned %d)',
- $OCF_RESOURCE_INSTANCE, $rc );
- return $OCF_ERR_GENERIC;
+ ocf_log( 'debug', 'pgsql_stop: monitor failed (rc=%d), checking with pg_isready', $rc );
+
+ $pgisready_rc = _pg_isready();
+ if ( $pgisready_rc == 0 or $pgisready_rc == 1 ) {
+ ocf_log( 'info', 'Instance "%s" is running (pg_isready=%d), proceeding with stop despite monitor failure',
+ $OCF_RESOURCE_INSTANCE, $pgisready_rc );
+ }
+ elsif ( $pgisready_rc == 2 ) {
+ ocf_log( 'debug', 'pgsql_stop: pg_isready indicates instance is not listening' );
+ return _confirm_stopped();
+ }
+ else {
+ ocf_exit_reason( 'Unexpected state for instance "%s" (monitor returned %d, pg_isready returned %d)',
+ $OCF_RESOURCE_INSTANCE, $rc, $pgisready_rc );
+ return $OCF_ERR_GENERIC;
+ }
}
#

View File

@ -1,149 +0,0 @@
From f6a0ed4681f6fb528219dfaf7a9b5a5617fda8e9 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Wed, 11 Mar 2026 14:02:33 +0100
Subject: [PATCH] pgsql: use monitor_user for monitor-calls and use .pgpass
when monitor_password is not specified
---
heartbeat/pgsql | 40 ++++++++++++++++------------------------
1 file changed, 16 insertions(+), 24 deletions(-)
diff --git a/heartbeat/pgsql b/heartbeat/pgsql
index 69384f155..9c474007c 100755
--- a/heartbeat/pgsql
+++ b/heartbeat/pgsql
@@ -216,7 +216,7 @@ will be used.
<parameter name="monitor_user" unique="0" required="0">
<longdesc lang="en">
-PostgreSQL user that pgsql RA will user for monitor operations. If it's not set
+PostgreSQL user that pgsql RA will use for monitor operations. If it's not set
pgdba user will be used.
</longdesc>
<shortdesc lang="en">monitor_user</shortdesc>
@@ -526,12 +526,13 @@ EOF
# Execulte SQL and return the result.
exec_sql() {
- local sql="$1"
+ local user="$1"
+ local sql="$2"
local output
local rc
output=`$SU $OCF_RESKEY_pgdba -c "cd $OCF_RESKEY_pgdata; \
- $OCF_RESKEY_psql $psql_options -U $OCF_RESKEY_pgdba \
+ $OCF_RESKEY_psql $psql_options -U $user \
-Atc \"$sql\""`
rc=$?
@@ -661,7 +662,7 @@ pgsql_replication_start() {
return $OCF_ERR_GENERIC
fi
- synchronous_standby_names=$(exec_sql "${CHECK_SYNCHRONOUS_STANDBY_NAMES_SQL}")
+ synchronous_standby_names=$(exec_sql "$OCF_RESKEY_monitor_user" "${CHECK_SYNCHRONOUS_STANDBY_NAMES_SQL}")
if [ -n "${synchronous_standby_names}" ]; then
ocf_exit_reason "Invalid synchronous_standby_names is set in postgresql.conf."
return $OCF_ERR_CONFIGURED
@@ -693,7 +694,7 @@ pgsql_promote() {
return $OCF_ERR_CONFIGURED
fi
- output=`exec_sql "${CHECK_MS_SQL}"`
+ output=`exec_sql "$OCF_RESKEY_monitor_user" "${CHECK_MS_SQL}"`
if [ $? -ne 0 ]; then
report_psql_error $rc $loglevel "Can't get PostgreSQL recovery status on promote."
return $OCF_ERR_GENERIC
@@ -971,7 +972,7 @@ pgsql_real_monitor() {
if is_replication; then
#Check replication state
- output=`exec_sql "${CHECK_MS_SQL}"`
+ output=`exec_sql "$OCF_RESKEY_monitor_user" "${CHECK_MS_SQL}"`
rc=$?
if [ $rc -ne 0 ]; then
@@ -1003,7 +1004,7 @@ pgsql_real_monitor() {
fi
OCF_RESKEY_monitor_sql=`escape_string "$OCF_RESKEY_monitor_sql"`
- runasowner -q $loglevel "$OCF_RESKEY_psql $psql_options \
+ runasowner -q $loglevel "$OCF_RESKEY_psql $psql_options -U "$OCF_RESKEY_monitor_user" \
-c '$OCF_RESKEY_monitor_sql'"
rc=$?
if [ $rc -ne 0 ]; then
@@ -1183,7 +1184,7 @@ control_slave_status() {
local tmp_data_status
local number_of_nodes
- all_data_status=`exec_sql "${CHECK_REPLICATION_STATE_SQL}"`
+ all_data_status=`exec_sql "$OCF_RESKEY_monitor_user" "${CHECK_REPLICATION_STATE_SQL}"`
rc=$?
if [ $rc -eq 0 ]; then
@@ -1402,7 +1403,7 @@ create_replication_slot_name() {
delete_replication_slot(){
DELETE_REPLICATION_SLOT_sql="SELECT pg_drop_replication_slot('$1');"
- output=`exec_sql "$DELETE_REPLICATION_SLOT_sql"`
+ output=`exec_sql "$OCF_RESKEY_pgdba" "$DELETE_REPLICATION_SLOT_sql"`
return $?
}
@@ -1452,7 +1453,7 @@ create_replication_slots() {
fi
CREATE_REPLICATION_SLOT_sql="SELECT pg_create_physical_replication_slot('$replication_slot_name');"
- output=`exec_sql "$CREATE_REPLICATION_SLOT_sql"`
+ output=`exec_sql "$OCF_RESKEY_pgdba" "$CREATE_REPLICATION_SLOT_sql"`
rc=$?
if [ $rc -eq 0 ]; then
@@ -1472,7 +1473,7 @@ check_replication_slot(){
local output
local CHECK_REPLICATION_SLOT_sql="SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$replication_slot_name'"
- output=`exec_sql "$CHECK_REPLICATION_SLOT_sql"`
+ output=`exec_sql "$OCF_RESKEY_monitor_user" "$CHECK_REPLICATION_SLOT_sql"`
echo "$output"
}
@@ -1488,7 +1489,7 @@ get_my_location() {
local log2
local newer_location
- output=`exec_sql "$CHECK_XLOG_LOC_SQL"`
+ output=`exec_sql "$OCF_RESKEY_monitor_user" "$CHECK_XLOG_LOC_SQL"`
rc=$?
if [ $rc -ne 0 ]; then
@@ -2018,11 +2019,6 @@ pgsql_validate_all() {
return $OCF_ERR_INSTALLED;
fi
- if [ -n "$OCF_RESKEY_monitor_user" ] && [ -z "$OCF_RESKEY_monitor_password" ]; then
- ocf_exit_reason "monitor password can't be empty"
- return $OCF_ERR_CONFIGURED
- fi
-
if [ -z "$OCF_RESKEY_monitor_user" ] && [ -n "$OCF_RESKEY_monitor_password" ]; then
ocf_exit_reason "monitor_user has to be set if monitor_password is set"
return $OCF_ERR_CONFIGURED
@@ -2201,13 +2197,9 @@ then
fi
# make psql command options
-if [ -n "$OCF_RESKEY_monitor_user" ]; then
- PGUSER=$OCF_RESKEY_monitor_user; export PGUSER
- PGPASSWORD=$OCF_RESKEY_monitor_password; export PGPASSWORD
- psql_options="-p $OCF_RESKEY_pgport $OCF_RESKEY_pgdb"
-else
- psql_options="-p $OCF_RESKEY_pgport -U $OCF_RESKEY_pgdba $OCF_RESKEY_pgdb"
-fi
+[ -z "$OCF_RESKEY_monitor_user" ] && OCF_RESKEY_monitor_user="$OCF_RESKEY_pgdba"
+[ -n "$OCF_RESKEY_monitor_password" ] && export PGPASSWORD="$OCF_RESKEY_monitor_password"
+psql_options="-p $OCF_RESKEY_pgport $OCF_RESKEY_pgdb"
if [ -n "$OCF_RESKEY_pghost" ]; then
psql_options="$psql_options -h $OCF_RESKEY_pghost"

View File

@ -1,40 +0,0 @@
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

@ -1,130 +0,0 @@
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,40 @@
From 25ad481c19fdb006e20485ef3fc2e5b3eff30ef0 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <simon.pichugin@gmail.com>
Date: Mon, 16 Mar 2026 17:23:11 -0700
Subject: [PATCH] Merge commit from fork
---
pyasn1/codec/ber/decoder.py | 10 +++
tests/codec/ber/test_decoder.py | 116 ++++++++++++++++++++++++++++++++
tests/codec/cer/test_decoder.py | 24 +++++++
tests/codec/der/test_decoder.py | 42 ++++++++++++
4 files changed, 192 insertions(+)
diff --git a/gcp/google-cloud-sdk/lib/third_party/pyasn1/codec/ber/decoder.py b/gcp/google-cloud-sdk/lib/third_party/pyasn1/codec/ber/decoder.py
index 50b14e98..2ea0be13 100644
--- a/gcp/google-cloud-sdk/lib/third_party/pyasn1/codec/ber/decoder.py
+++ b/gcp/google-cloud-sdk/lib/third_party/pyasn1/codec/ber/decoder.py
@@ -36,6 +36,7 @@
# Maximum number of continuation octets (high-bit set) allowed per OID arc.
# 20 octets allows up to 140-bit integers, supporting UUID-based OIDs
MAX_OID_ARC_CONTINUATION_OCTETS = 20
+MAX_NESTING_DEPTH = 100
# Maximum number of bytes in a BER length field (8 bytes = up to 2^64-1)
MAX_LENGTH_OCTETS = 8
@@ -1568,6 +1569,15 @@ def __call__(self, substrate, asn1Spec=None,
decodeFun=None, substrateFun=None,
**options):
+ _nestingLevel = options.get('_nestingLevel', 0)
+
+ if _nestingLevel > MAX_NESTING_DEPTH:
+ raise error.PyAsn1Error(
+ 'ASN.1 structure nesting depth exceeds limit (%d)' % MAX_NESTING_DEPTH
+ )
+
+ options['_nestingLevel'] = _nestingLevel + 1
+
allowEoo = options.pop('allowEoo', False)
if LOG:

View File

@ -1,265 +0,0 @@
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

@ -1,27 +0,0 @@
From 46e305ab4cb80fb6e28d2f8a1d90587d1cb1b549 Mon Sep 17 00:00:00 2001
From: SatomiOSAWA <satomi.t.osawa@gmail.com>
Date: Wed, 18 Mar 2026 09:32:05 +0900
Subject: [PATCH] Modify the return code when a mismatch between mount state
and configuration is detected in monitor operation.
---
heartbeat/Filesystem | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/heartbeat/Filesystem b/heartbeat/Filesystem
index dda8e66e5..b4f16f17f 100755
--- a/heartbeat/Filesystem
+++ b/heartbeat/Filesystem
@@ -1029,7 +1029,11 @@ Filesystem_status()
rc=$OCF_NOT_RUNNING
else
ocf_exit_reason "Another device ($mounted_device) is already mounted on $MOUNTPOINT"
- rc=$OCF_ERR_CONFIGURED
+ if [ "$__OCF_ACTION" = "monitor" ]; then
+ rc=$OCF_ERR_GENERIC
+ else
+ rc=$OCF_ERR_CONFIGURED
+ fi
fi
fi
else

View File

@ -1,28 +0,0 @@
From f2cfc150da843a6c57bb6f6eebb4e44eb9d23213 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Wed, 13 May 2026 09:28:59 +0200
Subject: [PATCH] Filesystem: always return OCF_ERR_GENERIC when another device
is mounted on mountpoint to ensure relocation after trying to restart
(default behaviour)
---
heartbeat/Filesystem | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/heartbeat/Filesystem b/heartbeat/Filesystem
index b4f16f17f..f7d2aa34b 100755
--- a/heartbeat/Filesystem
+++ b/heartbeat/Filesystem
@@ -1029,11 +1029,7 @@ Filesystem_status()
rc=$OCF_NOT_RUNNING
else
ocf_exit_reason "Another device ($mounted_device) is already mounted on $MOUNTPOINT"
- if [ "$__OCF_ACTION" = "monitor" ]; then
- rc=$OCF_ERR_GENERIC
- else
- rc=$OCF_ERR_CONFIGURED
- fi
+ rc=$OCF_ERR_GENERIC
fi
fi
else

View File

@ -1,81 +0,0 @@
From fd1c9a1cfd4463dad843f666ad951e6616f1ccf0 Mon Sep 17 00:00:00 2001
From: Guilherme Felix <fguilher@amazon.com>
Date: Mon, 23 Mar 2026 19:22:49 +0000
Subject: [PATCH 1/2] aws-vpc-move-ip: Add awscli_timeout option
---
heartbeat/aws-vpc-move-ip | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/heartbeat/aws-vpc-move-ip b/heartbeat/aws-vpc-move-ip
index 2afc0ba53..3e798173f 100755
--- a/heartbeat/aws-vpc-move-ip
+++ b/heartbeat/aws-vpc-move-ip
@@ -48,6 +48,7 @@ OCF_RESKEY_interface_default="eth0"
OCF_RESKEY_iflabel_default=""
OCF_RESKEY_monapi_default="false"
OCF_RESKEY_lookup_type_default="InstanceId"
+OCF_RESKEY_awscli_timeout_default=""
: ${OCF_RESKEY_awscli=${OCF_RESKEY_awscli_default}}
: ${OCF_RESKEY_auth_type=${OCF_RESKEY_auth_type_default}}
@@ -61,6 +62,7 @@ OCF_RESKEY_lookup_type_default="InstanceId"
: ${OCF_RESKEY_iflabel=${OCF_RESKEY_iflabel_default}}
: ${OCF_RESKEY_monapi=${OCF_RESKEY_monapi_default}}
: ${OCF_RESKEY_lookup_type=${OCF_RESKEY_lookup_type_default}}
+: ${OCF_RESKEY_awscli_timeout=${OCF_RESKEY_awscli_timeout_default}}
#######################################################################
@@ -211,6 +213,14 @@ curl sleep between tries
<content type="integer" default="${OCF_RESKEY_curl_sleep_default}" />
</parameter>
+<parameter name="awscli_timeout" unique="0">
+<longdesc lang="en">
+awscli cli-connect-timeout value
+</longdesc>
+<shortdesc lang="en">awscli cli-connect-timeout</shortdesc>
+<content type="integer" default="${OCF_RESKEY_curl_sleep_default}" />
+</parameter>
+
</parameters>
<actions>
@@ -490,7 +500,11 @@ if ! ocf_is_root; then
exit $OCF_ERR_PERM
fi
-AWSCLI_CMD="${OCF_RESKEY_awscli}"
+if [ -n "${OCF_RESKEY_awscli_timeout}" ]; then
+ AWSCLI_CMD="${OCF_RESKEY_awscli} --cli-connect-timeout ${OCF_RESKEY_awscli_timeout}"
+else
+ AWSCLI_CMD="${OCF_RESKEY_awscli}"
+fi
if [ "x${OCF_RESKEY_auth_type}" = "xkey" ]; then
AWSCLI_CMD="$AWSCLI_CMD --profile ${OCF_RESKEY_profile}"
elif [ "x${OCF_RESKEY_auth_type}" = "xrole" ]; then
From be4ad4dc4978875717b84b847bcd39386405bf51 Mon Sep 17 00:00:00 2001
From: Guilherme Felix <fguilher@amazon.com>
Date: Tue, 24 Mar 2026 16:08:41 +0000
Subject: [PATCH 2/2] aws-vpc-move-ip: Fix error in default value for
awscli_timeout
---
heartbeat/aws-vpc-move-ip | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/heartbeat/aws-vpc-move-ip b/heartbeat/aws-vpc-move-ip
index 3e798173f..1348c5908 100755
--- a/heartbeat/aws-vpc-move-ip
+++ b/heartbeat/aws-vpc-move-ip
@@ -218,7 +218,7 @@ curl sleep between tries
awscli cli-connect-timeout value
</longdesc>
<shortdesc lang="en">awscli cli-connect-timeout</shortdesc>
-<content type="integer" default="${OCF_RESKEY_curl_sleep_default}" />
+<content type="integer" default="${OCF_RESKEY_awscli_timeout_default}" />
</parameter>
</parameters>

View File

@ -1,71 +0,0 @@
From 54fa7a59c36697cd8df5b619fff0b50af00df76e Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Mon, 20 Nov 2023 16:35:52 +0100
Subject: [PATCH 1/2] storage_mon: fix file handler out of scope leak and
uninitialized values
---
tools/storage_mon.c | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/tools/storage_mon.c b/tools/storage_mon.c
index 1aae29e58..cc415e97f 100644
--- a/tools/storage_mon.c
+++ b/tools/storage_mon.c
@@ -382,9 +382,11 @@ static int write_pid_file(const char *pidfile)
syslog(LOG_ERR, "Failed to write '%s' to %s: %s", pid, pidfile, strerror(errno));
goto done;
}
- close(fd);
rc = 0;
done:
+ if (fd != -1) {
+ close(fd);
+ }
if (pid != NULL) {
free(pid);
}
@@ -663,6 +665,7 @@ storage_mon_client(void)
snprintf(request.message, SMON_MAX_MSGSIZE, "%s", SMON_GET_RESULT_COMMAND);
request.hdr.id = 0;
request.hdr.size = sizeof(struct storage_mon_check_value_req);
+ response.hdr.id = 0;
rc = qb_ipcc_send(conn, &request, request.hdr.size);
if (rc < 0) {
syslog(LOG_ERR, "qb_ipcc_send error : %d\n", rc);
@@ -683,7 +686,11 @@ storage_mon_client(void)
/* greater than 0 : monitoring error. */
/* -1 : communication system error. */
/* -2 : Not all checks completed for first device in daemon mode. */
- rc = atoi(response.message);
+ if (strnlen(response.message, 1)) {
+ rc = atoi(response.message);
+ } else {
+ rc = -1;
+ }
syslog(LOG_DEBUG, "daemon response[%d]: %s \n", response.hdr.id, response.message);
From b23ba4eaefb500199c4845751f4c5545c81f42f1 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Mon, 20 Nov 2023 16:37:37 +0100
Subject: [PATCH 2/2] findif: also check that netmaskbits != EOS
---
tools/findif.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/findif.c b/tools/findif.c
index a25395fec..ab108a3c4 100644
--- a/tools/findif.c
+++ b/tools/findif.c
@@ -669,7 +669,7 @@ main(int argc, char ** argv) {
}
}
- if (netmaskbits) {
+ if (netmaskbits != NULL && *netmaskbits != EOS) {
best_netmask = netmask;
}else if (best_netmask == 0L) {
/*

View File

@ -1,23 +0,0 @@
From cb968378959b8aa334e98daf62a1b08ef6525fb4 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Wed, 22 Nov 2023 10:32:31 +0100
Subject: [PATCH] storage_mon: use memset() to fix "uninitialized value"
covscan error, as qb_ipcc_recv() will always set a message (according to
honzaf)
---
tools/storage_mon.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/tools/storage_mon.c b/tools/storage_mon.c
index cc415e97f..a9227ef90 100644
--- a/tools/storage_mon.c
+++ b/tools/storage_mon.c
@@ -655,6 +655,7 @@ storage_mon_client(void)
int32_t rc;
+ memset(&response, 0, sizeof(response));
snprintf(ipcs_name, SMON_MAX_IPCSNAME, "storage_mon_%s", attrname);
conn = qb_ipcc_connect(ipcs_name, 0);
if (conn == NULL) {

View File

@ -0,0 +1,22 @@
From b23ba4eaefb500199c4845751f4c5545c81f42f1 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Mon, 20 Nov 2023 16:37:37 +0100
Subject: [PATCH 2/2] findif: also check that netmaskbits != EOS
---
tools/findif.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/findif.c b/tools/findif.c
index a25395fec..ab108a3c4 100644
--- a/tools/findif.c
+++ b/tools/findif.c
@@ -669,7 +669,7 @@ main(int argc, char ** argv) {
}
}
- if (netmaskbits) {
+ if (netmaskbits != NULL && *netmaskbits != EOS) {
best_netmask = netmask;
}else if (best_netmask == 0L) {
/*

View File

@ -1,26 +0,0 @@
--- a/heartbeat/portblock 2026-06-19 09:34:28.950367810 +0200
+++ b/heartbeat/portblock 2026-06-19 10:06:09.851173500 +0200
@@ -47,6 +47,14 @@
: ${OCF_RESKEY_method=${OCF_RESKEY_method_default}}
: ${OCF_RESKEY_status_check=${OCF_RESKEY_status_check_default}}
: ${OCF_RESKEY_ip=${OCF_RESKEY_ip_default}}
+
+# IPv6 support: detect family from service IP and parameterise both backends.
+case "$OCF_RESKEY_ip" in
+ *:*) ip_family=ip6 ;;
+ *) ip_family=ip ;;
+esac
+IPTABLES=${ip_family}tables
+
: ${OCF_RESKEY_reset_local_on_unblock_stop=${OCF_RESKEY_reset_local_on_unblock_stop_default}}
: ${OCF_RESKEY_tickle_dir=${OCF_RESKEY_tickle_dir_default}}
: ${OCF_RESKEY_sync_script=${OCF_RESKEY_sync_script_default}}
@@ -778,7 +786,7 @@
fi
# iptables v1.4.20+ is required to use -w (wait)
-version=$(iptables -V | grep -oE '[0-9]+[\.0-9]+')
+version=$($IPTABLES -V | grep -oE '[0-9]+[\.0-9]+')
ocf_version_cmp "$version" "1.4.19.1"
if [ "$?" -eq "2" ]; then
wait="-w"

View File

@ -1,160 +0,0 @@
--- a/heartbeat/iSCSILogicalUnit.in 2021-11-03 10:12:01.000000000 +0100
+++ b/heartbeat/iSCSILogicalUnit.in 2026-05-28 14:25:31.007210780 +0200
@@ -67,6 +67,8 @@
# Set LIO-T backend default as 'block'
OCF_RESKEY_liot_bstype_default="block"
: ${OCF_RESKEY_liot_bstype=${OCF_RESKEY_liot_bstype_default}}
+OCF_RESKEY_block_size_default=""
+: ${OCF_RESKEY_block_size=${OCF_RESKEY_block_size_default}}
## tgt specifics
# tgt has "backing store type" and "backing store open flags",
@@ -186,6 +188,19 @@
<content type="integer" />
</parameter>
+<parameter name="emulate_write_cache" required="0" unique="0">
+<longdesc lang="en">
+Enable or disable write-back caching for this Logical Unit.
+Setting to 1 enables write-back caching (the default for fileio
+backstores). Setting to 0 disables it, enabling write-through mode.
+Write-through mode is recommended for HA configurations to ensure
+data is flushed to disk before the iSCSI write is acknowledged.
+Requires the lio-t implementation.
+</longdesc>
+<shortdesc lang="en">Write-back cache (0 or 1)</shortdesc>
+<content type="integer" />
+</parameter>
+
<parameter name="vendor_id" required="0" unique="0">
<longdesc lang="en">
The SCSI vendor ID to be configured for this Logical Unit.
@@ -292,6 +307,19 @@
<content type="string" default="${OCF_RESKEY_liot_bstype_default}"/>
</parameter>
+<parameter name="block_size" required="0" unique="0">
+<longdesc lang="en">
+Override the block size presented to initiators for this
+Logical Unit. Accepted values are 512, 1024, 2048, or 4096.
+This sets the block_size attribute in the LIO kernel target
+(configfs) or the blocksize attribute in SCST.
+Requires the lio-t implementation.
+If unset, the kernel default (typically 512) is used.
+</longdesc>
+<shortdesc lang="en">Block size (512, 1024, 2048, or 4096)</shortdesc>
+<content type="integer" default="${OCF_RESKEY_block_size_default}"/>
+</parameter>
+
</parameters>
<actions>
@@ -428,7 +456,13 @@
lio-t)
ocf_take_lock $TARGETLOCKFILE
ocf_release_lock_on_exit $TARGETLOCKFILE
- iblock_attrib_path="/sys/kernel/config/target/core/iblock_*/${OCF_RESOURCE_INSTANCE}/attrib"
+ # Map liot_bstype to the configfs core directory name
+ case "${OCF_RESKEY_liot_bstype}" in
+ block) liot_configfs_core="iblock_*" ;;
+ fileio) liot_configfs_core="fileio_*" ;;
+ pscsi) liot_configfs_core="pscsi_*" ;;
+ esac
+ liot_core_path="/sys/kernel/config/target/core/${liot_configfs_core}/${OCF_RESOURCE_INSTANCE}"
# For lio, we first have to create a target device, then
# add it to the Target Portal Group as an LU.
# Handle differently 'block', 'fileio' and 'pscsi'
@@ -445,10 +479,13 @@
ocf_run targetcli /backstores/${OCF_RESKEY_liot_bstype} create ${OCF_RESOURCE_INSTANCE} ${OCF_RESKEY_path} || exit $OCF_ERR_GENERIC
fi
if [ -n "${OCF_RESKEY_scsi_sn}" ]; then
- echo ${OCF_RESKEY_scsi_sn} > /sys/kernel/config/target/core/iblock_*/${OCF_RESOURCE_INSTANCE}/wwn/vpd_unit_serial
+ echo ${OCF_RESKEY_scsi_sn} > ${liot_core_path}/wwn/vpd_unit_serial
fi
if [ -n "${OCF_RESKEY_product_id}" ]; then
- echo "${OCF_RESKEY_product_id}" > /sys/kernel/config/target/core/iblock_*/${OCF_RESOURCE_INSTANCE}/wwn/product_id
+ echo "${OCF_RESKEY_product_id}" > ${liot_core_path}/wwn/product_id
+ fi
+ if [ -n "${OCF_RESKEY_block_size}" ]; then
+ echo ${OCF_RESKEY_block_size} > ${liot_core_path}/attrib/block_size || exit $OCF_ERR_GENERIC
fi
ocf_run targetcli /iscsi/${OCF_RESKEY_target_iqn}/tpg1/luns create /backstores/${OCF_RESKEY_liot_bstype}/${OCF_RESOURCE_INSTANCE} ${OCF_RESKEY_lun} || exit $OCF_ERR_GENERIC
@@ -470,13 +507,16 @@
fi
if [ -n "${OCF_RESKEY_emulate_tpu}" ]; then
- echo ${OCF_RESKEY_emulate_tpu} > ${iblock_attrib_path}/emulate_tpu || exit $OCF_ERR_GENERIC
+ echo ${OCF_RESKEY_emulate_tpu} > ${liot_core_path}/attrib/emulate_tpu || exit $OCF_ERR_GENERIC
fi
if [ -n "${OCF_RESKEY_emulate_3pc}" ]; then
- echo ${OCF_RESKEY_emulate_3pc} > ${iblock_attrib_path}/emulate_3pc || exit $OCF_ERR_GENERIC
+ echo ${OCF_RESKEY_emulate_3pc} > ${liot_core_path}/attrib/emulate_3pc || exit $OCF_ERR_GENERIC
fi
if [ -n "${OCF_RESKEY_emulate_caw}" ]; then
- echo ${OCF_RESKEY_emulate_caw} > ${iblock_attrib_path}/emulate_caw || exit $OCF_ERR_GENERIC
+ echo ${OCF_RESKEY_emulate_caw} > ${liot_core_path}/attrib/emulate_caw || exit $OCF_ERR_GENERIC
+ fi
+ if [ -n "${OCF_RESKEY_emulate_write_cache}" ]; then
+ echo ${OCF_RESKEY_emulate_write_cache} > ${liot_core_path}/attrib/emulate_write_cache || exit $OCF_ERR_GENERIC
fi
;;
esac
@@ -604,9 +644,14 @@
configfs_path="/sys/kernel/config/target/iscsi/${OCF_RESKEY_target_iqn}/tpgt_1/lun/lun_${OCF_RESKEY_lun}/*/udev_path"
[ -e ${configfs_path} ] && [ `cat ${configfs_path}` = "${OCF_RESKEY_path}" ] && return $OCF_SUCCESS
- # if we aren't activated, is a block device still left over?
- block_configfs_path="/sys/kernel/config/target/core/iblock_*/${OCF_RESOURCE_INSTANCE}/udev_path"
- [ -e ${block_configfs_path} ] && ocf_log warn "existing block without an active lun: ${block_configfs_path}"
+ # if we aren't activated, is a backstore still left over?
+ case "${OCF_RESKEY_liot_bstype}" in
+ block) liot_configfs_core="iblock_*" ;;
+ fileio) liot_configfs_core="fileio_*" ;;
+ pscsi) liot_configfs_core="pscsi_*" ;;
+ esac
+ block_configfs_path="/sys/kernel/config/target/core/${liot_configfs_core}/${OCF_RESOURCE_INSTANCE}/udev_path"
+ [ -e ${block_configfs_path} ] && ocf_log warn "existing backstore without an active lun: ${block_configfs_path}"
[ -e ${block_configfs_path} ] && return $OCF_ERR_GENERIC
;;
esac
@@ -624,6 +669,22 @@
fi
done
+ # Validate block_size if set
+ if [ -n "${OCF_RESKEY_block_size}" ]; then
+ case "${OCF_RESKEY_block_size}" in
+ 512|1024|2048|4096)
+ ;;
+ *)
+ ocf_exit_reason "Invalid block_size ${OCF_RESKEY_block_size} (must be 512, 1024, 2048, or 4096)"
+ exit $OCF_ERR_CONFIGURED
+ ;;
+ esac
+ if [ "${OCF_RESKEY_implementation}" != "lio-t" ]; then
+ ocf_exit_reason "block_size is only supported with the lio-t implementation"
+ exit $OCF_ERR_CONFIGURED
+ fi
+ fi
+
# Is the configured implementation supported?
case "$OCF_RESKEY_implementation" in
"iet"|"tgt"|"lio"|"lio-t")
@@ -689,13 +750,13 @@
iet)
# IET does not support setting the vendor and product ID
# (it always uses "IET" and "VIRTUAL-DISK")
- unsupported_params="vendor_id product_id allowed_initiators lio_iblock tgt_bstype tgt_bsoflags tgt_bsopts tgt_device_type emulate_tpu emulate_3pc emulate_caw liot_bstype"
+ unsupported_params="vendor_id product_id allowed_initiators lio_iblock tgt_bstype tgt_bsoflags tgt_bsopts tgt_device_type emulate_tpu emulate_3pc emulate_caw emulate_write_cache liot_bstype"
;;
tgt)
- unsupported_params="allowed_initiators lio_iblock emulate_tpu emulate_3pc emulate_caw liot_bstype"
+ unsupported_params="allowed_initiators lio_iblock emulate_tpu emulate_3pc emulate_caw emulate_write_cache liot_bstype"
;;
lio)
- unsupported_params="scsi_id vendor_id product_id tgt_bstype tgt_bsoflags tgt_bsopts tgt_device_type emulate_tpu emulate_3pc emulate_caw liot_bstype"
+ unsupported_params="scsi_id vendor_id product_id tgt_bstype tgt_bsoflags tgt_bsopts tgt_device_type emulate_tpu emulate_3pc emulate_caw emulate_write_cache liot_bstype"
;;
lio-t)
unsupported_params="scsi_id vendor_id tgt_bstype tgt_bsoflags tgt_bsopts tgt_device_type lio_iblock"

View File

@ -1,283 +0,0 @@
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

@ -1,130 +0,0 @@
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

@ -1,96 +0,0 @@
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,43 @@
--- a/aliyun/aliyunsdkcore/vendored/requests/packages/urllib3/connectionpool.py
+++ b/aliyun/aliyunsdkcore/vendored/requests/packages/urllib3/connectionpool.py
@@ -850,6 +850,18 @@
body = None
headers = HTTPHeaderDict(headers)._prepare_for_method_change()
+ # Strip headers marked as unsafe to forward to the redirected location.
+ # Check remove_headers_on_redirect to avoid a potential network call within
+ # self.is_same_host() which may use socket.gethostbyname() in the future.
+ if retries.remove_headers_on_redirect and not self.is_same_host(
+ redirect_location
+ ):
+ new_headers = headers.copy()
+ for header in headers:
+ if header.lower() in retries.remove_headers_on_redirect:
+ new_headers.pop(header, None)
+ headers = new_headers
+
try:
retries = retries.increment(method, url, response=response, _pool=self)
except MaxRetryError:
--- a/gcp/google-cloud-sdk/lib/third_party/urllib3/connectionpool.py
+++ b/gcp/google-cloud-sdk/lib/third_party/urllib3/connectionpool.py
@@ -850,6 +850,18 @@
body = None
headers = HTTPHeaderDict(headers)._prepare_for_method_change()
+ # Strip headers marked as unsafe to forward to the redirected location.
+ # Check remove_headers_on_redirect to avoid a potential network call within
+ # self.is_same_host() which may use socket.gethostbyname() in the future.
+ if retries.remove_headers_on_redirect and not self.is_same_host(
+ redirect_location
+ ):
+ new_headers = headers.copy()
+ for header in headers:
+ if header.lower() in retries.remove_headers_on_redirect:
+ new_headers.pop(header, None)
+ headers = new_headers
+
try:
retries = retries.increment(method, url, response=response, _pool=self)
except MaxRetryError:

View File

@ -1,24 +0,0 @@
From b6527e11a9afaa2876f5c0ea9d4ecda0e4eea100 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Tue, 19 May 2026 12:28:29 +0200
Subject: [PATCH] IPaddr2: fix interfaces named with keywords like primary,
secondary, etc
This can cause issues when another interface is in DOWN state.
---
heartbeat/IPaddr2 | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/heartbeat/IPaddr2 b/heartbeat/IPaddr2
index e9c1cdb74..462d8db5a 100755
--- a/heartbeat/IPaddr2
+++ b/heartbeat/IPaddr2
@@ -1028,7 +1028,7 @@ ip_served() {
return 0
fi
- if ocf_is_true "$OCF_RESKEY_check_link_status" && $IP2UTIL -f $FAMILY addr show $cur_nic | \
+ if ocf_is_true "$OCF_RESKEY_check_link_status" && $IP2UTIL -f $FAMILY addr show dev $cur_nic | \
grep -q "[[:space:]]\(DOWN\|LOWERLAYERDOWN\)[[:space:]]"; then
echo "down"
return 0

View File

@ -1,28 +0,0 @@
From ab77b1df5f2c98ff8461b8819052000997cb4b52 Mon Sep 17 00:00:00 2001
From: Hideo Yamauchi <renayama19661014@ybb.ne.jp>
Date: Mon, 1 Jun 2026 20:00:27 +0900
Subject: [PATCH] Mid: IPaddr2: To allow Pacemaker to handle error handling, it
returns NOT_RUNNING even when down.
---
heartbeat/IPaddr2 | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/heartbeat/IPaddr2 b/heartbeat/IPaddr2
index 462d8db5a..7953a5596 100755
--- a/heartbeat/IPaddr2
+++ b/heartbeat/IPaddr2
@@ -1256,12 +1256,9 @@ ip_monitor() {
run_arp_sender refresh
return $OCF_SUCCESS
;;
- no)
+ no|down)
exit $OCF_NOT_RUNNING
;;
- down)
- exit $OCF_ERR_INSTALLED
- ;;
*)
# Errors on this interface?
return $OCF_ERR_GENERIC

View File

@ -1,53 +0,0 @@
From 466c1a59108c266739884284dd91b226b20e7082 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Thu, 26 Mar 2026 10:04:16 +0100
Subject: [PATCH] sybaseASE: dont report error about missing sybaseASE env
during probe-action
---
heartbeat/sybaseASE.in | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/heartbeat/sybaseASE.in b/heartbeat/sybaseASE.in
index 8b315029b..3e1305f4e 100755
--- a/heartbeat/sybaseASE.in
+++ b/heartbeat/sybaseASE.in
@@ -84,7 +84,7 @@ if [ "$__OCF_ACTION" != "meta-data" ]; then
OCF_RESKEY_sybase_home="/opt/sap"
elif [ -d "/opt/sybase" ]; then
OCF_RESKEY_sybase_home="/opt/sybase"
- else
+ elif ! ocf_is_probe && [ "$__OCF_ACTION" != "stop" ]; then
ocf_log err "sybaseASE: Unable to detect 'sybase_home'."
exit $OCF_ERR_ARGS
fi
@@ -95,7 +95,7 @@ if [ "$__OCF_ACTION" != "meta-data" ]; then
if [ "$OCF_RESKEY_sybase_ase" = "detect" ]; then
if [ -f "$sybase_env" ]; then
OCF_RESKEY_sybase_ase=$(grep "SYBASE_ASE" "$sybase_env" | cut -d= -f2)
- else
+ elif ! ocf_is_probe && [ "$__OCF_ACTION" != "stop" ]; then
ocf_log err "sybaseASE: Unable to detect 'sybase_ase'."
exit $OCF_ERR_ARGS
fi
@@ -104,7 +104,7 @@ if [ "$__OCF_ACTION" != "meta-data" ]; then
if [ "$OCF_RESKEY_sybase_ocs" = "detect" ]; then
if [ -f "$sybase_env" ]; then
OCF_RESKEY_sybase_ocs=$(grep "SYBASE_OCS" "$sybase_env" | cut -d= -f2)
- else
+ elif ! ocf_is_probe && [ "$__OCF_ACTION" != "stop" ]; then
ocf_log err "sybaseASE: Unable to detect 'sybase_ocs'."
exit $OCF_ERR_ARGS
fi
@@ -517,6 +517,11 @@ ase_stop()
{
ocf_log debug "sybaseASE: Start 'ase_stop'"
+ if [ ! -d "$OCF_RESKEY_sybase_home" ]; then
+ ocf_log warn "Unable to detect 'sybase_home', or it does not exist"
+ return $OCF_SUCCESS
+ fi
+
# Check if the ASE server is still running.
ase_is_running
if [[ $? != 0 ]]

View File

@ -1,26 +0,0 @@
--- a/heartbeat/portblock 2026-06-19 10:15:51.367092196 +0200
+++ b/heartbeat/portblock 2026-06-19 10:15:41.619851027 +0200
@@ -313,7 +313,10 @@
active_grep_pat()
{
w="[ ][ ]*"
- any="0\\.0\\.0\\.0/0"
+ case $ip_family in
+ ip6) any="::/0" ;;
+ *) any="0\\.0\\.0\\.0/0" ;;
+ esac
src=$any dst=$3
if [ "$4" = "s" ]; then
local src=$3
@@ -327,9 +330,9 @@
local prot="\(udp\|17\)"
fi
if [ "$method" = "DROP" ]; then
- echo "^DROP${w}${prot}${w}--${w}${src}${w}${dst}${w}multiport${w}${4}ports${w}${2}$"
+ 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$"
+ 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
}

View File

@ -1,73 +0,0 @@
--- a/heartbeat/nfsserver
+++ b/heartbeat/nfsserver
@@ -366,14 +366,18 @@
ocf_exit_reason "rpcbind is not running"
return $OCF_NOT_RUNNING
fi
+ fi
- ocf_log debug "Status: nfs-mountd"
- ps axww | grep -q "[r]pc.mountd"
- rc=$?
- if [ "$rc" -ne "0" ]; then
+ ocf_log debug "Status: nfs-mountd"
+ ps axww | grep -q "[r]pc.mountd"
+ rc=$?
+ if [ "$rc" -ne "0" ]; then
+ if ocf_is_probe || [ "$__OCF_ACTION" = "start" ]; then
+ ocf_log info "nfs-mountd is not running"
+ else
ocf_exit_reason "nfs-mountd is not running"
- return $OCF_NOT_RUNNING
fi
+ return $OCF_NOT_RUNNING
fi
ocf_log debug "Status: nfs-idmapd"
@@ -397,6 +401,46 @@
fi
fi
+ local nfsdcld_unit_status
+ nfsdcld_unit_status=$(systemctl --no-legend list-unit-files "nfsdcld.service" 2>/dev/null)
+
+ if echo "$nfsdcld_unit_status" | grep -qE "nfsdcld.*masked"; then
+ ocf_log debug "Status: nfsdcld (masked, skipping monitor)"
+ elif echo "$nfsdcld_unit_status" | grep -q nfsdcld; then
+ local nfsdcld_state
+ nfsdcld_state=$(systemctl show -p ActiveState --value nfsdcld.service 2>/dev/null)
+
+ ocf_log debug "Status: nfsdcld (state: $nfsdcld_state)"
+
+ case "$nfsdcld_state" in
+ active)
+ ocf_log debug "Status: nfsdcld (monitoring as it is active)"
+ fn=`mktemp`
+ nfs_exec status nfsdcld > $fn 2>&1
+ rc=$?
+ ocf_log debug "$(cat $fn)"
+ rm -f $fn
+ if [ "$rc" -ne "0" ]; then
+ if ocf_is_probe || [ "$__OCF_ACTION" = "start" ]; then
+ ocf_log info "nfsdcld service is not running"
+ else
+ ocf_exit_reason "nfsdcld service is not running"
+ fi
+ return $OCF_NOT_RUNNING
+ fi
+ ;;
+
+ failed|inactive|deactivating)
+ if ocf_is_probe || [ "$__OCF_ACTION" = "start" ]; then
+ ocf_log info "nfsdcld service is not running"
+ else
+ ocf_exit_reason "nfsdcld service is in '$nfsdcld_state' state"
+ fi
+ return $OCF_NOT_RUNNING
+ ;;
+ esac
+ fi
+
nfs_exec is-active nfs-server
rc=$?

View File

@ -1,52 +0,0 @@
From 18f01d598fb654ed629d27d69c2bfccd74cef4a8 Mon Sep 17 00:00:00 2001
From: Arslan Ahmad <arslan.ahmad97@googlemail.com>
Date: Thu, 9 Apr 2026 01:30:58 +0530
Subject: [PATCH] db2: Avoid promotion in STANDBY/REMOTE_CATCHUP_PENDING state
So to prevent data loss, promoting the standby node which is in
STANDBY/REMOTE_CATCHUP_PENDING/DISCONNECTED state will be avoided,
reporting same in cluster status.
Fixes #2043
Signed-off-by: Arslan Ahmad <arslan.ahmad97@googlemail.com>
---
heartbeat/db2 | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/heartbeat/db2 b/heartbeat/db2
index 4420b9989e..744917574b 100755
--- a/heartbeat/db2
+++ b/heartbeat/db2
@@ -944,7 +944,7 @@ db2_promote() {
for i in 1 2
do
hadr=$(db2_hadr_status $db) || return $OCF_ERR_GENERIC
- ocf_log info "DB2 database $instance($db2node)/$db has HADR status $hadr and will be promoted"
+ ocf_log info "DB2 database $instance($db2node)/$db has HADR status $hadr"
case "$hadr" in
Standard/Standalone)
@@ -965,6 +965,7 @@ db2_promote() {
done
fi
+ ocf_log info "DB2 database $instance($db2node)/$db is already promoted"
return $OCF_SUCCESS
;;
@@ -975,11 +976,12 @@ db2_promote() {
STANDBY/*PEER/DISCONNECTED|Standby/DisconnectedPeer)
# must take over by force peer window only
force="by force peer window only"
+ ocf_log info "DB2 database will be promoted using 'by force peer window only' option"
;;
- # must take over by force
STANDBY/REMOTE_CATCHUP_PENDING/DISCONNECTED)
- force="by force"
+ ocf_exit_reason "Promoting database in $hadr state can result in data loss. Manual intervention required."
+ return $OCF_ERR_GENERIC
;;
*)

View File

@ -0,0 +1,52 @@
From e60c691cb91addb8fcefa2f537e85ede6fb1e886 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <simon.pichugin@gmail.com>
Date: Wed, 8 Jul 2026 17:32:09 -0700
Subject: [PATCH] Merge commit from fork
---
pyasn1/type/univ.py | 21 +++++++++----
tests/codec/ber/test_decoder.py | 53 +++++++++++++++++++++++++++------
tests/codec/cer/test_decoder.py | 10 +++++++
tests/codec/der/test_decoder.py | 19 ++++++++++++
tests/type/test_univ.py | 40 +++++++++++++++++++++++++
5 files changed, 129 insertions(+), 14 deletions(-)
diff --git a/gcp/google-cloud-sdk/lib/third_party/pyasn1/type/univ.py b/gcp/google-cloud-sdk/lib/third_party/pyasn1/type/univ.py
index 9aff5e69..8b786dd9 100644
--- a/gcp/google-cloud-sdk/lib/third_party/pyasn1/type/univ.py
+++ b/gcp/google-cloud-sdk/lib/third_party/pyasn1/type/univ.py
@@ -1362,7 +1362,7 @@ class Pi(Real):
def __normalizeBase10(value):
m, b, e = value
while m and m % 10 == 0:
- m /= 10
+ m //= 10
e += 1
return m, b, e
@@ -1490,10 +1490,21 @@ def __int__(self):
def __float__(self):
if self._value in self._inf:
return self._value
- else:
- return float(
- self._value[0] * pow(self._value[1], self._value[2])
- )
+
+ mantissa, base, exponent = self._value
+
+ if not mantissa:
+ return 0.0
+
+ if base == 2:
+ return math.ldexp(float(mantissa), exponent)
+
+ # base is 10 (prettyIn() rejects everything else); refuse to
+ # materialize astronomically large integers via pow()
+ if exponent > sys.float_info.max_10_exp:
+ raise OverflowError('Real value too large to convert to float')
+
+ return float(mantissa * pow(base, exponent))
def __abs__(self):
return self.clone(abs(float(self)))

View File

@ -1,46 +0,0 @@
From 65a066cf9066390db65c4875e21c4c391793b9ae Mon Sep 17 00:00:00 2001
From: Arslan Ahmad <arslan.ahmad97@googlemail.com>
Date: Tue, 16 Jan 2024 09:11:17 +0530
Subject: [PATCH] Avoid false positive for VG activation
When lvm.conf file has `volume_list` parameter configured and the
cluster is managing the shared storage using `system_id_source`,
then the activation of the LV fails to happen. However it is
reported as a success.
The fixes will avoid starting of `LVM-activate` resource when
the cluster is configured with both `system_id_source` and
`volume_list`.
Signed-off-by: Arslan Ahmad <arslan.ahmad97@googlemail.com>
---
heartbeat/LVM-activate | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/heartbeat/LVM-activate b/heartbeat/LVM-activate
index f6f24a3b5..3858ed8dc 100755
--- a/heartbeat/LVM-activate
+++ b/heartbeat/LVM-activate
@@ -448,6 +448,10 @@ systemid_check()
{
# system_id_source is set in lvm.conf
source=$(lvmconfig 'global/system_id_source' 2>/dev/null | cut -d"=" -f2)
+
+ # Is volume_list set in lvm.conf
+ vol_list=$(lvmconfig 'activation/volume_list' 2>/dev/null | cut -d"=" -f2)
+
if [ "$source" = "" ] || [ "$source" = "none" ]; then
ocf_exit_reason "system_id_source in lvm.conf is not set correctly!"
exit $OCF_ERR_ARGS
@@ -458,6 +462,11 @@ systemid_check()
exit $OCF_ERR_ARGS
fi
+ if [ -n "$source" ] && [ -n "$vol_list" ]; then
+ ocf_exit_reason "Both system_id_source & volume_list cannot be defined!"
+ exit $OCF_ERR_ARGS
+ fi
+
return $OCF_SUCCESS
}

View File

@ -1,40 +0,0 @@
From 264e38e02cb4c04877e412bac254e42c7f6b2e1c Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Tue, 20 Feb 2024 12:34:42 +0100
Subject: [PATCH] Filesystem: fail when leading or trailing whitespace is
present in device or directory parameters
---
heartbeat/Filesystem | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/heartbeat/Filesystem b/heartbeat/Filesystem
index e1378f781..f88e3b552 100755
--- a/heartbeat/Filesystem
+++ b/heartbeat/Filesystem
@@ -995,6 +995,12 @@ if [ -n "${OCF_RESKEY_force_unmount}" ]; then
fi
DEVICE="$OCF_RESKEY_device"
+case "$DEVICE" in
+ [[:space:]]*|*[[:space:]])
+ ocf_exit_reason "device parameter does not accept leading or trailing whitespace characters"
+ exit $OCF_ERR_CONFIGURED
+ ;;
+esac
FSTYPE=$OCF_RESKEY_fstype
if [ ! -z "$OCF_RESKEY_options" ]; then
options="-o $OCF_RESKEY_options"
@@ -1032,6 +1038,12 @@ if [ -z "$OCF_RESKEY_directory" ]; then
else
MOUNTPOINT="$(echo "$OCF_RESKEY_directory" | sed 's/\/*$//')"
: ${MOUNTPOINT:=/}
+ case "$MOUNTPOINT" in
+ [[:space:]]*|*[[:space:]])
+ ocf_exit_reason "directory parameter does not accept leading or trailing whitespace characters"
+ exit $OCF_ERR_CONFIGURED
+ ;;
+ esac
if [ -e "$MOUNTPOINT" ] ; then
CANONICALIZED_MOUNTPOINT="$(readlink -f "$MOUNTPOINT")"
if [ $? -ne 0 ]; then

View File

@ -1,30 +0,0 @@
From 1317efc72af6b72d9fb37aea18dc16129c146148 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Tue, 25 Jun 2024 13:33:19 +0200
Subject: [PATCH] Filesystem: return success during stop-action when leading or
trailing whitespace is present in device or directory parameters
---
heartbeat/Filesystem | 2 ++
1 file changed, 2 insertions(+)
diff --git a/heartbeat/Filesystem b/heartbeat/Filesystem
index 8e0127531..3eb520e0c 100755
--- a/heartbeat/Filesystem
+++ b/heartbeat/Filesystem
@@ -1037,6 +1037,7 @@ fi
DEVICE="$OCF_RESKEY_device"
case "$DEVICE" in
[[:space:]]*|*[[:space:]])
+ [ "$__OCF_ACTION" = "stop" ] && exit $OCF_SUCCESS
ocf_exit_reason "device parameter does not accept leading or trailing whitespace characters"
exit $OCF_ERR_CONFIGURED
;;
@@ -1080,6 +1081,7 @@ else
: ${MOUNTPOINT:=/}
case "$MOUNTPOINT" in
[[:space:]]*|*[[:space:]])
+ [ "$__OCF_ACTION" = "stop" ] && exit $OCF_SUCCESS
ocf_exit_reason "directory parameter does not accept leading or trailing whitespace characters"
exit $OCF_ERR_CONFIGURED
;;

View File

@ -1,75 +0,0 @@
From 4357f0dbb8668ac4090cd7070c2ea195e5683326 Mon Sep 17 00:00:00 2001
From: Damien Ciabrini <dciabrin@redhat.com>
Date: Wed, 24 Jan 2024 13:27:26 +0100
Subject: [PATCH] galera: allow joiner to report non-Primary during initial IST
It seems that with recent galera versions, when a galera node
joins a cluster, there is a small time window where the node is
connected to the primary component of the galera cluster, but it
might still be preparing its IST. During this time, it can report
itself as being 'not ready' and in 'non-primary' state.
Update the galera resource agent to allow the node to be in
non-primary state, but only if running a "promote" operation. Any
network partition during the promotion will be caught by the
promote timeout.
In reworking the promotion code, we move the check for primary
partition into the "galera_monitor" function. The check works
as before for regular "monitor" or "probe" operations.
Related-Bug: rhbz#2255414
---
heartbeat/galera.in | 25 +++++++++++++++++--------
1 file changed, 17 insertions(+), 8 deletions(-)
diff --git a/heartbeat/galera.in b/heartbeat/galera.in
index 6aed3e4b6d..b518595cb0 100755
--- a/heartbeat/galera.in
+++ b/heartbeat/galera.in
@@ -822,6 +822,11 @@ galera_promote()
return $rc
fi
+ # At this point, the mysql pidfile is created on disk and the
+ # mysql server is reacheable via its UNIX socket. If we are a
+ # joiner, SST transfers (rsync) have finished, but an IST may
+ # still be requested or ongoing
+
galera_monitor
rc=$?
if [ $rc != $OCF_SUCCESS -a $rc != $OCF_RUNNING_MASTER ]; then
@@ -835,12 +840,6 @@ galera_promote()
return $OCF_ERR_GENERIC
fi
- is_primary
- if [ $? -ne 0 ]; then
- ocf_exit_reason "Failure. Master instance started, but is not in Primary mode."
- return $OCF_ERR_GENERIC
- fi
-
if ocf_is_true $bootstrap; then
promote_everyone
clear_bootstrap_node
@@ -991,8 +990,18 @@ galera_monitor()
fi
rc=$OCF_RUNNING_MASTER
else
- ocf_exit_reason "local node <${NODENAME}> is started, but not in primary mode. Unknown state."
- rc=$OCF_ERR_GENERIC
+ # It seems that with recent galera (26.4+), a joiner that is
+ # connected to a Primary component and is preparing its IST
+ # request might still temporarily report its state as
+ # Non-Primary. Do not fail in this case as the promote
+ # operation will loop until the IST finishes or the promote
+ # times out.
+ if [ "$__OCF_ACTION" = "promote" ] && ! ocf_is_true $(is_bootstrap); then
+ ocf_log info "local node <${NODENAME}> is receiving a State Transfer."
+ else
+ ocf_exit_reason "local node <${NODENAME}> is started, but not in primary mode. Unknown state."
+ rc=$OCF_ERR_GENERIC
+ fi
fi
return $rc

View File

@ -1,25 +0,0 @@
From f717b4a3aa83c9124e62716f421b99e314d00233 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Fri, 12 Apr 2024 12:23:21 +0200
Subject: [PATCH] findif.sh: fix corner cases
---
heartbeat/findif.sh | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/heartbeat/findif.sh b/heartbeat/findif.sh
index 7c766e6e0..13484f827 100644
--- a/heartbeat/findif.sh
+++ b/heartbeat/findif.sh
@@ -215,9 +215,9 @@ findif()
fi
if [ -n "$nic" ] ; then
# NIC supports more than two.
- set -- $(ip -o -f $family route list match $match $scope | grep "dev $nic " | awk 'BEGIN{best=0} /\// { mask=$1; sub(".*/", "", mask); if( int(mask)>=best ) { best=int(mask); best_ln=$0; } } END{print best_ln}')
+ set -- $(ip -o -f $family route list match $match $scope | grep "dev $nic " | sed -e 's,^\([0-9.]\+\) ,\1/32 ,;s,^\([0-9a-f:]\+\) ,\1/128 ,' | sort -t/ -k2,2nr)
else
- set -- $(ip -o -f $family route list match $match $scope | awk 'BEGIN{best=0} /\// { mask=$1; sub(".*/", "", mask); if( int(mask)>=best ) { best=int(mask); best_ln=$0; } } END{print best_ln}')
+ set -- $(ip -o -f $family route list match $match $scope | sed -e 's,^\([0-9.]\+\) ,\1/32 ,;s,^\([0-9a-f:]\+\) ,\1/128 ,' | sort -t/ -k2,2nr)
fi
if [ $# = 0 ] ; then
case $OCF_RESKEY_ip in

View File

@ -1,365 +0,0 @@
From 12d73d53026d219be67c0d5353010ba08ab49e98 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Tue, 28 May 2024 09:45:55 +0200
Subject: [PATCH 1/3] findif.sh: add metric for IPv6 support and fail when
matching more than 1 route
---
heartbeat/findif.sh | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/heartbeat/findif.sh b/heartbeat/findif.sh
index 13484f827..ca5d1a5c1 100644
--- a/heartbeat/findif.sh
+++ b/heartbeat/findif.sh
@@ -196,10 +196,13 @@ findif()
{
local match="$OCF_RESKEY_ip"
local family
+ local proto
local scope
local nic="$OCF_RESKEY_nic"
local netmask="$OCF_RESKEY_cidr_netmask"
local brdcast="$OCF_RESKEY_broadcast"
+ local metric
+ local routematch
echo $match | grep -qs ":"
if [ $? = 0 ] ; then
@@ -215,10 +218,19 @@ findif()
fi
if [ -n "$nic" ] ; then
# NIC supports more than two.
- set -- $(ip -o -f $family route list match $match $scope | grep "dev $nic " | sed -e 's,^\([0-9.]\+\) ,\1/32 ,;s,^\([0-9a-f:]\+\) ,\1/128 ,' | sort -t/ -k2,2nr)
+ routematch=$(ip -o -f $family route list match $match $proto $scope | grep "dev $nic " | sed -e 's,^\([0-9.]\+\) ,\1/32 ,;s,^\([0-9a-f:]\+\) ,\1/128 ,' | sort -t/ -k2,2nr)
else
- set -- $(ip -o -f $family route list match $match $scope | sed -e 's,^\([0-9.]\+\) ,\1/32 ,;s,^\([0-9a-f:]\+\) ,\1/128 ,' | sort -t/ -k2,2nr)
+ routematch=$(ip -o -f $family route list match $match $proto $scope | sed -e 's,^\([0-9.]\+\) ,\1/32 ,;s,^\([0-9a-f:]\+\) ,\1/128 ,' | sort -t/ -k2,2nr)
fi
+ if [ "$family" = "inet6" ]; then
+ routematch=$(echo "$routematch" | grep -v "^default")
+ fi
+
+ if [ $(echo "$routematch" | wc -l) -gt 1 ]; then
+ ocf_exit_reason "More than 1 routes match $match. Unable to decide which route to use."
+ return $OCF_ERR_GENERIC
+ fi
+ set -- $routematch
if [ $# = 0 ] ; then
case $OCF_RESKEY_ip in
127.*)
@@ -255,6 +267,7 @@ findif()
return $OCF_ERR_GENERIC
fi
fi
- echo "$nic netmask $netmask broadcast $brdcast"
+ metric=$(echo "$@" | sed "s/.*metric[[:blank:]]\([^ ]\+\).*/\1/")
+ echo "$nic netmask $netmask broadcast $brdcast metric $metric"
return $OCF_SUCCESS
}
From 488c096d63fe0f7e15938e65483ba20628080198 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Tue, 28 May 2024 09:47:11 +0200
Subject: [PATCH 2/3] IPaddr2: use metric for IPv6
---
heartbeat/IPaddr2 | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/heartbeat/IPaddr2 b/heartbeat/IPaddr2
index 5f30b8f98..091bea418 100755
--- a/heartbeat/IPaddr2
+++ b/heartbeat/IPaddr2
@@ -561,10 +561,11 @@ ip_init() {
if
[ $rc -eq 0 ]
then
- NICINFO=`echo "$NICINFO" | sed -e 's/netmask\ //;s/broadcast\ //'`
+ NICINFO=`echo "$NICINFO" | sed -e 's/netmask\ //;s/broadcast\ //;s/metric\ //'`
NIC=`echo "$NICINFO" | cut -d" " -f1`
NETMASK=`echo "$NICINFO" | cut -d" " -f2`
BRDCAST=`echo "$NICINFO" | cut -d" " -f3`
+ METRIC=`echo "$NICINFO" | cut -d" " -f4`
else
# findif couldn't find the interface
if ocf_is_probe; then
@@ -659,13 +660,14 @@ delete_interface () {
# Add an interface
#
add_interface () {
- local cmd msg extra_opts ipaddr netmask broadcast iface label
+ local cmd msg extra_opts ipaddr netmask broadcast iface label metric
ipaddr="$1"
netmask="$2"
broadcast="$3"
iface="$4"
label="$5"
+ metric="$6"
if [ "$FAMILY" = "inet" ] && ocf_is_true $OCF_RESKEY_run_arping &&
check_binary arping; then
@@ -688,6 +690,9 @@ add_interface () {
fi
extra_opts=""
+ if [ "$FAMILY" = "inet6" ]; then
+ extra_opts="$extra_opts metric $metric"
+ fi
if [ "$FAMILY" = "inet6" ] && ocf_is_true "${OCF_RESKEY_nodad}"; then
extra_opts="$extra_opts nodad"
fi
@@ -1083,7 +1088,7 @@ ip_start() {
done
fi
- add_interface $OCF_RESKEY_ip $NETMASK ${BRDCAST:-none} $NIC $IFLABEL
+ add_interface "$OCF_RESKEY_ip" "$NETMASK" "${BRDCAST:-none}" "$NIC" "$IFLABEL" "$METRIC"
rc=$?
if [ $rc -ne $OCF_SUCCESS ]; then
From d1c4d1969381d3e35cfaaaaae522e5687a9ed88a Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Tue, 28 May 2024 09:47:56 +0200
Subject: [PATCH 3/3] IPsrcaddr: add IPv6 support
---
heartbeat/IPsrcaddr | 116 ++++++++++++++++++++++++++++++++------------
1 file changed, 85 insertions(+), 31 deletions(-)
diff --git a/heartbeat/IPsrcaddr b/heartbeat/IPsrcaddr
index c732ce8df..1c87d5b7f 100755
--- a/heartbeat/IPsrcaddr
+++ b/heartbeat/IPsrcaddr
@@ -60,6 +60,7 @@ OCF_RESKEY_cidr_netmask_default=""
OCF_RESKEY_destination_default="0.0.0.0/0"
OCF_RESKEY_proto_default=""
OCF_RESKEY_metric_default=""
+OCF_RESKEY_pref_default=""
OCF_RESKEY_table_default=""
: ${OCF_RESKEY_ipaddress=${OCF_RESKEY_ipaddress_default}}
@@ -67,6 +68,7 @@ OCF_RESKEY_table_default=""
: ${OCF_RESKEY_destination=${OCF_RESKEY_destination_default}}
: ${OCF_RESKEY_proto=${OCF_RESKEY_proto_default}}
: ${OCF_RESKEY_metric=${OCF_RESKEY_metric_default}}
+: ${OCF_RESKEY_pref=${OCF_RESKEY_pref_default}}
: ${OCF_RESKEY_table=${OCF_RESKEY_table_default}}
#######################################################################
@@ -75,10 +77,13 @@ OCF_RESKEY_table_default=""
USAGE="usage: $0 {start|stop|status|monitor|validate-all|meta-data}";
- CMDSHOW="$IP2UTIL route show $TABLE to exact $OCF_RESKEY_destination"
-CMDCHANGE="$IP2UTIL route change to "
+echo "$OCF_RESKEY_ipaddress" | grep -q ":" && FAMILY="inet6" || FAMILY="inet"
+[ "$FAMILY" = "inet6" ] && [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] && OCF_RESKEY_destination="::/0"
-if [ "$OCF_RESKEY_destination" != "0.0.0.0/0" ]; then
+ CMDSHOW="$IP2UTIL -f $FAMILY route show $TABLE to exact $OCF_RESKEY_destination"
+CMDCHANGE="$IP2UTIL -f $FAMILY route change to "
+
+if [ "$OCF_RESKEY_destination" != "0.0.0.0/0" ] && [ "$OCF_RESKEY_destination" != "::/0" ]; then
CMDSHOW="$CMDSHOW src $OCF_RESKEY_ipaddress"
fi
@@ -153,6 +158,14 @@ Metric. Only needed if incorrect metric value is used.
<content type="string" default="${OCF_RESKEY_metric_default}" />
</parameter>
+<parameter name="pref">
+<longdesc lang="en">
+IPv6 route preference (low, medium or high). Only needed if incorrect pref value is used.
+</longdesc>
+<shortdesc lang="en">IPv6 route preference.</shortdesc>
+<content type="string" default="${OCF_RESKEY_pref_default}" />
+</parameter>
+
<parameter name="table">
<longdesc lang="en">
Table to modify and use for interface lookup. E.g. "local".
@@ -196,12 +209,21 @@ errorexit() {
# where the src clause "src Y.Y.Y.Y" may or may not be present
WS="[[:blank:]]"
-OCTET="[0-9]\{1,3\}"
-IPADDR="\($OCTET\.\)\{3\}$OCTET"
+case "$FAMILY" in
+ inet)
+ GROUP="[0-9]\{1,3\}"
+ IPADDR="\($GROUP\.\)\{3\}$GROUP"
+ ;;
+ inet6)
+ GROUP="[0-9a-f]\{0,4\}"
+ IPADDR="\($GROUP\:\)\{0,\}$GROUP"
+ ;;
+esac
SRCCLAUSE="src$WS$WS*\($IPADDR\)"
-MATCHROUTE="\(.*${WS}\)\($SRCCLAUSE\)\($WS.*\|$\)"
-METRICCLAUSE=".*\(metric$WS[^ ]\+\)"
+MATCHROUTE="\(.*${WS}\)proto [^ ]\+\(.*${WS}\)\($SRCCLAUSE\)\($WS.*\|$\)"
+METRICCLAUSE=".*\(metric$WS[^ ]\+\).*"
PROTOCLAUSE=".*\(proto$WS[^ ]\+\).*"
+PREFCLAUSE=".*\(pref$WS[^ ]\+\).*"
FINDIF=findif
# findif needs that to be set
@@ -216,17 +238,17 @@ srca_read() {
errorexit "more than 1 matching route exists"
# But there might still be no matching route
- [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] && [ -z "$ROUTE" ] && \
+ ([ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] || [ "$OCF_RESKEY_destination" = "::/0" ]) && [ -z "$ROUTE" ] && \
! ocf_is_probe && [ "$__OCF_ACTION" != stop ] && errorexit "no matching route exists"
# Sed out the source ip address if it exists
- SRCIP=`echo $ROUTE | sed -n "s/$MATCHROUTE/\3/p"`
+ SRCIP=`echo $ROUTE | sed -n "s/$MATCHROUTE/\4/p"`
# and what remains after stripping out the source ip address clause
- ROUTE_WO_SRC=`echo $ROUTE | sed "s/$MATCHROUTE/\1\5/"`
+ ROUTE_WO_SRC=`echo $ROUTE | sed "s/$MATCHROUTE/\1\2\6/"`
# using "src <ip>" only returns output if there's a match
- if [ "$OCF_RESKEY_destination" != "0.0.0.0/0" ]; then
+ if [ "$OCF_RESKEY_destination" != "0.0.0.0/0" ] && [ "$OCF_RESKEY_destination" != "::/0" ]; then
[ -z "$ROUTE" ] && return 1 || return 0
fi
@@ -249,12 +271,15 @@ srca_start() {
rc=$OCF_SUCCESS
ocf_log info "The ip route has been already set.($NETWORK, $INTERFACE, $ROUTE_WO_SRC)"
else
- $IP2UTIL route replace $TABLE $NETWORK dev $INTERFACE $PROTO src $1 $METRIC || \
- errorexit "command 'ip route replace $TABLE $NETWORK dev $INTERFACE $PROTO src $1 $METRIC' failed"
+ # NetworkManager manages routes with proto static/kernel
+ [ -z "$OCF_RESKEY_proto" ] && echo "$PROTO" | grep -q "proto \(kernel\|static\)" && PROTO="proto keepalived"
- if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] ;then
- $CMDCHANGE $ROUTE_WO_SRC src $1 || \
- errorexit "command '$CMDCHANGE $ROUTE_WO_SRC src $1' failed"
+ $IP2UTIL route replace $TABLE $NETWORK dev $INTERFACE $PROTO src $1 $METRIC $PREF || \
+ errorexit "command 'ip route replace $TABLE $NETWORK dev $INTERFACE $PROTO src $1 $METRIC $PREF' failed"
+
+ if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] || [ "$OCF_RESKEY_destination" = "::/0" ]; then
+ $CMDCHANGE $ROUTE_WO_SRC $PROTO src $1 || \
+ errorexit "command '$CMDCHANGE $ROUTE_WO_SRC $PROTO src $1' failed"
fi
rc=$?
fi
@@ -290,14 +315,15 @@ srca_stop() {
fi
PRIMARY_IP="$($IP2UTIL -4 -o addr show dev $INTERFACE primary | awk '{split($4,a,"/");print a[1]}')"
- OPTS="proto kernel scope $SCOPE src $PRIMARY_IP"
+ OPTS="proto kernel scope $SCOPE"
+ [ "$FAMILY" = "inet" ] && OPTS="$OPTS src $PRIMARY_IP"
- $IP2UTIL route replace $TABLE $NETWORK dev $INTERFACE $OPTS $METRIC || \
- errorexit "command 'ip route replace $TABLE $NETWORK dev $INTERFACE $OPTS $METRIC' failed"
+ $IP2UTIL route replace $TABLE $NETWORK dev $INTERFACE $OPTS $METRIC $PREF || \
+ errorexit "command 'ip route replace $TABLE $NETWORK dev $INTERFACE $OPTS $METRIC $PREF' failed"
- if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] ;then
- $CMDCHANGE $ROUTE_WO_SRC src $PRIMARY_IP || \
- errorexit "command '$CMDCHANGE $ROUTE_WO_SRC src $PRIMARY_IP' failed"
+ if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] || [ "$OCF_RESKEY_destination" = "::/0" ]; then
+ $CMDCHANGE $ROUTE_WO_SRC proto static || \
+ errorexit "command '$CMDCHANGE $ROUTE_WO_SRC proto static' failed"
fi
return $?
@@ -330,7 +356,7 @@ CheckIP() {
case $ip in
*[!0-9.]*) #got invalid char
false;;
- .*|*.) #begin or end by ".", which is invalid
+ .*|*.) #begin or end with ".", which is invalid
false;;
*..*) #consecutive ".", which is invalid
false;;
@@ -356,6 +382,18 @@ CheckIP() {
return $? # This return is unnecessary, this comment too :)
}
+CheckIP6() {
+ ip="$1"
+ case $ip in
+ *[!0-9a-f:]*) #got invalid char
+ false;;
+ *:::*) # more than 2 consecutive ":", which is invalid
+ false;;
+ *::*::*) # more than 1 "::", which is invalid
+ false;;
+ esac
+}
+
#
# Find out which interface or alias serves the given IP address
# The argument is an IP address, and its output
@@ -396,8 +434,7 @@ find_interface_solaris() {
# is an (aliased) interface name (e.g., "eth0" and "eth0:0").
#
find_interface_generic() {
-
- local iface=`$IP2UTIL -o -f inet 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
@@ -502,7 +539,9 @@ srca_validate_all() {
# The IP address should be in good shape
if CheckIP "$ipaddress"; then
- :
+ :
+ elif CheckIP6 "$ipaddress"; then
+ :
else
ocf_exit_reason "Invalid IP address [$ipaddress]"
return $OCF_ERR_CONFIGURED
@@ -570,21 +609,36 @@ rc=$?
}
INTERFACE=`echo $findif_out | awk '{print $1}'`
-LISTROUTE=`$IP2UTIL route list dev $INTERFACE scope link $PROTO match $ipaddress`
+case "$FAMILY" in
+ inet)
+ LISTCMD="$IP2UTIL -f $FAMILY route list dev $INTERFACE scope link $PROTO match $ipaddress"
+ ;;
+ inet6)
+ LISTCMD="$IP2UTIL -f $FAMILY route list dev $INTERFACE $PROTO match $ipaddress"
+ ;;
+esac
+LISTROUTE=`$LISTCMD`
+
[ -z "$PROTO" ] && PROTO=`echo $LISTROUTE | sed -n "s/$PROTOCLAUSE/\1/p"`
if [ -n "$OCF_RESKEY_metric" ]; then
METRIC="metric $OCF_RESKEY_metric"
-elif [ -z "$TABLE" ] || [ "${TABLE#table }" = "main" ]; then
+elif [ -z "$TABLE" ] || [ "${TABLE#table }" = "main" ] || [ "$FAMILY" = "inet6" ]; then
METRIC=`echo $LISTROUTE | sed -n "s/$METRICCLAUSE/\1/p"`
else
METRIC=""
fi
-if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] ;then
+if [ "$FAMILY" = "inet6" ]; then
+ if [ -z "$OCF_RESKEY_pref" ]; then
+ PREF=`echo $LISTROUTE | sed -n "s/$PREFCLAUSE/\1/p"`
+ else
+ PREF="pref $OCF_RESKEY_pref"
+ fi
+fi
+if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] || [ "$OCF_RESKEY_destination" = "::/0" ] ;then
NETWORK=`echo $LISTROUTE | grep -m 1 -o '^[^ ]*'`
if [ -z "$NETWORK" ]; then
- err_str="command '$IP2UTIL route list dev $INTERFACE scope link $PROTO"
- err_str="$err_str match $ipaddress' failed to find a matching route"
+ err_str="command '$LISTCMD' failed to find a matching route"
if [ "$__OCF_ACTION" = "start" ]; then
ocf_exit_reason "$err_str"

View File

@ -1,22 +0,0 @@
From 4075aff88776e2811ebc83b735b2a70bcf46247f Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Mon, 24 Jun 2024 09:45:29 +0200
Subject: [PATCH] IPaddr2: only set metric value for IPv6 when detected
---
heartbeat/IPaddr2 | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/heartbeat/IPaddr2 b/heartbeat/IPaddr2
index 091bea418..3bc5abec1 100755
--- a/heartbeat/IPaddr2
+++ b/heartbeat/IPaddr2
@@ -690,7 +690,7 @@ add_interface () {
fi
extra_opts=""
- if [ "$FAMILY" = "inet6" ]; then
+ if [ "$FAMILY" = "inet6" ] && [ -n "$metric" ]; then
extra_opts="$extra_opts metric $metric"
fi
if [ "$FAMILY" = "inet6" ] && ocf_is_true "${OCF_RESKEY_nodad}"; then

View File

@ -1,25 +0,0 @@
From f561e272e9b7fe94ba598b70c6d2f44d034446ed Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Wed, 14 Aug 2024 12:05:54 +0200
Subject: [PATCH] findif.sh: ignore unreachable, blackhole, and prohibit routes
---
heartbeat/findif.sh | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/heartbeat/findif.sh b/heartbeat/findif.sh
index ca5d1a5c1..7b817f75c 100644
--- a/heartbeat/findif.sh
+++ b/heartbeat/findif.sh
@@ -218,9 +218,9 @@ findif()
fi
if [ -n "$nic" ] ; then
# NIC supports more than two.
- routematch=$(ip -o -f $family route list match $match $proto $scope | grep "dev $nic " | sed -e 's,^\([0-9.]\+\) ,\1/32 ,;s,^\([0-9a-f:]\+\) ,\1/128 ,' | sort -t/ -k2,2nr)
+ routematch=$(ip -o -f $family route list match $match $proto $scope | grep -v "^\(unreachable\|prohibit\|blackhole\)" | grep "dev $nic " | sed -e 's,^\([0-9.]\+\) ,\1/32 ,;s,^\([0-9a-f:]\+\) ,\1/128 ,' | sort -t/ -k2,2nr)
else
- routematch=$(ip -o -f $family route list match $match $proto $scope | sed -e 's,^\([0-9.]\+\) ,\1/32 ,;s,^\([0-9a-f:]\+\) ,\1/128 ,' | sort -t/ -k2,2nr)
+ routematch=$(ip -o -f $family route list match $match $proto $scope | grep -v "^\(unreachable\|prohibit\|blackhole\)" | sed -e 's,^\([0-9.]\+\) ,\1/32 ,;s,^\([0-9a-f:]\+\) ,\1/128 ,' | sort -t/ -k2,2nr)
fi
if [ "$family" = "inet6" ]; then
routematch=$(echo "$routematch" | grep -v "^default")

View File

@ -1,36 +0,0 @@
From f23ae9c1e9ff9a44a053c7c2378975ac5b807478 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Thu, 29 Aug 2024 16:24:02 +0200
Subject: [PATCH] IPsrcaddr: specify dev for default route, as e.g. fe80::
routes can be present on multiple interfaces
---
heartbeat/IPsrcaddr | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/heartbeat/IPsrcaddr b/heartbeat/IPsrcaddr
index 1c87d5b7f..58d89a280 100755
--- a/heartbeat/IPsrcaddr
+++ b/heartbeat/IPsrcaddr
@@ -278,8 +278,8 @@ srca_start() {
errorexit "command 'ip route replace $TABLE $NETWORK dev $INTERFACE $PROTO src $1 $METRIC $PREF' failed"
if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] || [ "$OCF_RESKEY_destination" = "::/0" ]; then
- $CMDCHANGE $ROUTE_WO_SRC $PROTO src $1 || \
- errorexit "command '$CMDCHANGE $ROUTE_WO_SRC $PROTO src $1' failed"
+ $CMDCHANGE $ROUTE_WO_SRC dev $INTERFACE $PROTO src $1 || \
+ errorexit "command '$CMDCHANGE $ROUTE_WO_SRC dev $INTERFACE $PROTO src $1' failed"
fi
rc=$?
fi
@@ -322,8 +322,8 @@ srca_stop() {
errorexit "command 'ip route replace $TABLE $NETWORK dev $INTERFACE $OPTS $METRIC $PREF' failed"
if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] || [ "$OCF_RESKEY_destination" = "::/0" ]; then
- $CMDCHANGE $ROUTE_WO_SRC proto static || \
- errorexit "command '$CMDCHANGE $ROUTE_WO_SRC proto static' failed"
+ $CMDCHANGE $ROUTE_WO_SRC dev $INTERFACE proto static || \
+ errorexit "command '$CMDCHANGE $ROUTE_WO_SRC dev $INTERFACE proto static' failed"
fi
return $?

View File

@ -1,110 +0,0 @@
From 66a5308d2e8f61093716a076f4386416dc18045c Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Mon, 22 Apr 2024 11:26:09 +0200
Subject: [PATCH] Filesystem: fail when incorrect device mounted on mountpoint,
and dont unmount the mountpoint in this case, or if mountpoint set to "/"
---
heartbeat/Filesystem | 71 ++++++++++++++++++++++++++++++++++++--------
1 file changed, 58 insertions(+), 13 deletions(-)
diff --git a/heartbeat/Filesystem b/heartbeat/Filesystem
index e1378f781..cec71f1a6 100755
--- a/heartbeat/Filesystem
+++ b/heartbeat/Filesystem
@@ -582,10 +582,16 @@ Filesystem_start()
fi
# See if the device is already mounted.
- if Filesystem_status >/dev/null 2>&1 ; then
- ocf_log info "Filesystem $MOUNTPOINT is already mounted."
- return $OCF_SUCCESS
- fi
+ Filesystem_status
+ case "$?" in
+ $OCF_SUCCESS)
+ ocf_log info "Filesystem $MOUNTPOINT is already mounted."
+ return $OCF_SUCCESS
+ ;;
+ $OCF_ERR_CONFIGURED)
+ return $OCF_ERR_CONFIGURED
+ ;;
+ esac
fstype_supported || exit $OCF_ERR_INSTALLED
@@ -801,10 +807,42 @@ Filesystem_stop()
#
Filesystem_status()
{
- match_string="${TAB}${CANONICALIZED_MOUNTPOINT}${TAB}"
- if list_mounts | grep "$match_string" >/dev/null 2>&1; then
- rc=$OCF_SUCCESS
- msg="$MOUNTPOINT is mounted (running)"
+ local match_string="${TAB}${CANONICALIZED_MOUNTPOINT}${TAB}"
+ local mounted_device=$(list_mounts | grep "$match_string" | awk '{print $1}')
+
+ if [ -n "$mounted_device" ]; then
+ if [ "X$blockdevice" = "Xyes" ]; then
+ if [ -e "$DEVICE" ] ; then
+ local canonicalized_device="$(readlink -f "$DEVICE")"
+ if [ $? -ne 0 ]; then
+ ocf_exit_reason "Could not canonicalize $DEVICE because readlink failed"
+ exit $OCF_ERR_GENERIC
+ fi
+ else
+ local canonicalized_device="$DEVICE"
+ fi
+ if [ -e "$mounted_device" ] ; then
+ local canonicalized_mounted_device="$(readlink -f "$mounted_device")"
+ if [ $? -ne 0 ]; then
+ ocf_exit_reason "Could not canonicalize $mounted_device because readlink failed"
+ exit $OCF_ERR_GENERIC
+ fi
+ else
+ local canonicalized_mounted_device="$mounted_device"
+ fi
+ if [ "$canonicalized_device" != "$canonicalized_mounted_device" ]; then
+ if ocf_is_probe || [ "$__OCF_ACTION" = "stop" ]; then
+ ocf_log debug "Another device ($mounted_device) is already mounted on $MOUNTPOINT"
+ rc=$OCF_NOT_RUNNING
+ else
+ ocf_exit_reason "Another device ($mounted_device) is already mounted on $MOUNTPOINT"
+ rc=$OCF_ERR_CONFIGURED
+ fi
+ fi
+ else
+ rc=$OCF_SUCCESS
+ msg="$MOUNTPOINT is mounted (running)"
+ fi
else
rc=$OCF_NOT_RUNNING
msg="$MOUNTPOINT is unmounted (stopped)"
@@ -1041,9 +1079,18 @@ else
else
CANONICALIZED_MOUNTPOINT="$MOUNTPOINT"
fi
- # At this stage, $MOUNTPOINT does not contain trailing "/" unless it is "/"
- # TODO: / mounted via Filesystem sounds dangerous. On stop, we'll
- # kill the whole system. Is that a good idea?
+
+ if echo "$CANONICALIZED_MOUNTPOINT" | grep -q "^\s*/\s*$"; then
+ if ocf_is_probe; then
+ ocf_log debug "/ cannot be managed in a cluster"
+ exit $OCF_NOT_RUNNING
+ elif [ "$__OCF_ACTION" = "start" ] || [ "$__OCF_ACTION" = "monitor" ] || [ "$__OCF_ACTION" = "status" ]; then
+ ocf_exit_reason "/ cannot be managed in a cluster"
+ exit $OCF_ERR_CONFIGURED
+ elif [ "$__OCF_ACTION" = "stop" ]; then
+ exit $OCF_SUCCESS
+ fi
+ fi
fi
# Check to make sure the utilites are found
@@ -1124,5 +1171,3 @@ case $OP in
;;
esac
exit $?
-
-

View File

@ -1,333 +0,0 @@
From 7739c2a802c1dddb6757ff75cf7f6582a89bd518 Mon Sep 17 00:00:00 2001
From: id <happytobi@tscoding.de>
Date: Fri, 31 May 2024 09:00:18 +0200
Subject: [PATCH] azure-events-az: update to API versions, add retry
functionality for metadata requests, update tests
---
heartbeat/azure-events-az.in | 117 ++++++++++++++++++++++++-----------
heartbeat/ocf.py | 50 +++++++++++++--
2 files changed, 126 insertions(+), 41 deletions(-)
diff --git a/heartbeat/azure-events-az.in b/heartbeat/azure-events-az.in
index 46d4d1f3d9..6d31e5abae 100644
--- a/heartbeat/azure-events-az.in
+++ b/heartbeat/azure-events-az.in
@@ -27,7 +27,7 @@ import ocf
##############################################################################
-VERSION = "0.10"
+VERSION = "0.20"
USER_AGENT = "Pacemaker-ResourceAgent/%s %s" % (VERSION, ocf.distro())
attr_globalPullState = "azure-events-az_globalPullState"
@@ -39,9 +39,6 @@ attr_healthstate = "#health-azure"
default_loglevel = ocf.logging.INFO
default_relevantEventTypes = set(["Reboot", "Redeploy"])
-global_pullMaxAttempts = 3
-global_pullDelaySecs = 1
-
##############################################################################
class attrDict(defaultdict):
@@ -71,16 +68,22 @@ class azHelper:
metadata_host = "http://169.254.169.254/metadata"
instance_api = "instance"
events_api = "scheduledevents"
- api_version = "2019-08-01"
+ events_api_version = "2020-07-01"
+ instance_api_version = "2021-12-13"
@staticmethod
- def _sendMetadataRequest(endpoint, postData=None):
+ def _sendMetadataRequest(endpoint, postData=None, api_version="2019-08-01"):
"""
Send a request to Azure's Azure Metadata Service API
"""
- url = "%s/%s?api-version=%s" % (azHelper.metadata_host, endpoint, azHelper.api_version)
+
+ retryCount = int(ocf.get_parameter("retry_count",3))
+ retryWaitTime = int(ocf.get_parameter("retry_wait",20))
+ requestTimeout = int(ocf.get_parameter("request_timeout",15))
+
+ url = "%s/%s?api-version=%s" % (azHelper.metadata_host, endpoint, api_version)
data = ""
- ocf.logger.debug("_sendMetadataRequest: begin; endpoint = %s, postData = %s" % (endpoint, postData))
+ ocf.logger.debug("_sendMetadataRequest: begin; endpoint = %s, postData = %s, retry_count = %s, retry_wait time = %s, request_timeout = %s" % (endpoint, postData, retryCount, retryWaitTime, requestTimeout))
ocf.logger.debug("_sendMetadataRequest: url = %s" % url)
if postData and type(postData) != bytes:
@@ -89,18 +92,37 @@ class azHelper:
req = urllib2.Request(url, postData)
req.add_header("Metadata", "true")
req.add_header("User-Agent", USER_AGENT)
- try:
- resp = urllib2.urlopen(req)
- except URLError as e:
- if hasattr(e, 'reason'):
- ocf.logger.warning("Failed to reach the server: %s" % e.reason)
- clusterHelper.setAttr(attr_globalPullState, "IDLE")
- elif hasattr(e, 'code'):
- ocf.logger.warning("The server couldn\'t fulfill the request. Error code: %s" % e.code)
- clusterHelper.setAttr(attr_globalPullState, "IDLE")
- else:
- data = resp.read()
- ocf.logger.debug("_sendMetadataRequest: response = %s" % data)
+
+ if retryCount > 0:
+ ocf.logger.debug("_sendMetadataRequest: retry enabled")
+
+ successful = None
+ for retry in range(retryCount+1):
+ try:
+ resp = urllib2.urlopen(req, timeout=requestTimeout)
+ except Exception as e:
+ excType = e.__class__.__name__
+ if excType == TimeoutError.__name__:
+ ocf.logger.warning("Request timed out after %s seconds Error: %s" % (requestTimeout, e))
+ if excType == URLError.__name__:
+ if hasattr(e, 'reason'):
+ ocf.logger.warning("Failed to reach the server: %s" % e.reason)
+ elif hasattr(e, 'code'):
+ ocf.logger.warning("The server couldn\'t fulfill the request. Error code: %s" % e.code)
+
+ if retryCount > 1 and retry != retryCount:
+ ocf.logger.warning("Request failed, retry (%s/%s) wait %s seconds before retry (wait time)" % (retry + 1,retryCount,retryWaitTime))
+ time.sleep(retryWaitTime)
+
+ else:
+ data = resp.read()
+ ocf.logger.debug("_sendMetadataRequest: response = %s" % data)
+ successful = 1
+ break
+
+ # When no request was successful also with retry enabled, set the cluster to idle
+ if successful is None:
+ clusterHelper.setAttr(attr_globalPullState, "IDLE")
if data:
data = json.loads(data)
@@ -115,14 +137,15 @@ class azHelper:
"""
ocf.logger.debug("getInstanceInfo: begin")
- jsondata = azHelper._sendMetadataRequest(azHelper.instance_api)
+ jsondata = azHelper._sendMetadataRequest(azHelper.instance_api, None, azHelper.instance_api_version)
ocf.logger.debug("getInstanceInfo: json = %s" % jsondata)
if jsondata:
ocf.logger.debug("getInstanceInfo: finished, returning {}".format(jsondata["compute"]))
return attrDict(jsondata["compute"])
else:
- ocf.ocf_exit_reason("getInstanceInfo: Unable to get instance info")
+ apiCall = "%s/%s?api-version=%s" % (azHelper.metadata_host, azHelper.instance_api, azHelper.instance_api_version)
+ ocf.ocf_exit_reason("getInstanceInfo: Unable to get instance info - call: %s" % apiCall)
sys.exit(ocf.OCF_ERR_GENERIC)
@staticmethod
@@ -132,11 +155,17 @@ class azHelper:
"""
ocf.logger.debug("pullScheduledEvents: begin")
- jsondata = azHelper._sendMetadataRequest(azHelper.events_api)
+ jsondata = azHelper._sendMetadataRequest(azHelper.events_api, None, azHelper.events_api_version)
ocf.logger.debug("pullScheduledEvents: json = %s" % jsondata)
- ocf.logger.debug("pullScheduledEvents: finished")
- return attrDict(jsondata)
+ if jsondata:
+ ocf.logger.debug("pullScheduledEvents: finished")
+ return attrDict(jsondata)
+ else:
+ apiCall = "%s/%s?api-version=%s" % (azHelper.metadata_host, azHelper.events_api, azHelper.events_api_version)
+ ocf.ocf_exit_reason("pullScheduledEvents: Unable to get scheduledevents info - call: %s" % apiCall)
+ sys.exit(ocf.OCF_ERR_GENERIC)
+
@staticmethod
def forceEvents(eventIDs):
@@ -534,7 +563,7 @@ class Node:
except ValueError:
# Handle the exception
ocf.logger.warn("Health attribute %s on node %s cannot be converted to an integer value" % (healthAttributeStr, node))
-
+
ocf.logger.debug("isNodeInStandby: finished - result %s" % isInStandy)
return isInStandy
@@ -584,7 +613,7 @@ class raAzEvents:
def monitor(self):
ocf.logger.debug("monitor: begin")
-
+
events = azHelper.pullScheduledEvents()
# get current document version
@@ -600,21 +629,21 @@ class raAzEvents:
ocf.logger.info("monitor: already handled curDocVersion, skip")
return ocf.OCF_SUCCESS
- localAzEventIDs = set()
+ localAzEventIds = dict()
for e in localEvents:
- localAzEventIDs.add(e.EventId)
+ localAzEventIds[e.EventId] = json.dumps(e)
curState = self.node.getState()
clusterEventIDs = self.node.getEventIDs()
ocf.logger.debug("monitor: curDocVersion has not been handled yet")
-
+
if clusterEventIDs:
# there are pending events set, so our state must be STOPPING or IN_EVENT
i = 0; touchedEventIDs = False
while i < len(clusterEventIDs):
# clean up pending events that are already finished according to AZ
- if clusterEventIDs[i] not in localAzEventIDs:
+ if clusterEventIDs[i] not in localAzEventIds.keys():
ocf.logger.info("monitor: remove finished local clusterEvent %s" % (clusterEventIDs[i]))
clusterEventIDs.pop(i)
touchedEventIDs = True
@@ -644,12 +673,12 @@ class raAzEvents:
ocf.logger.info("monitor: all local events finished, but some resources have not completed startup yet -> wait")
else:
if curState == AVAILABLE:
- if len(localAzEventIDs) > 0:
+ if len(localAzEventIds) > 0:
if clusterHelper.otherNodesAvailable(self.node):
- ocf.logger.info("monitor: can handle local events %s -> set state STOPPING" % (str(localAzEventIDs)))
- curState = self.node.updateNodeStateAndEvents(STOPPING, localAzEventIDs)
+ ocf.logger.info("monitor: can handle local events %s -> set state STOPPING - %s" % (str(list(localAzEventIds.keys())), str(list(localAzEventIds.values()))))
+ curState = self.node.updateNodeStateAndEvents(STOPPING, localAzEventIds.keys())
else:
- ocf.logger.info("monitor: cannot handle azEvents %s (only node available) -> set state ON_HOLD" % str(localAzEventIDs))
+ ocf.logger.info("monitor: cannot handle azEvents %s (only node available) -> set state ON_HOLD - %s" % (str(list(localAzEventIds.keys())), str(list(localAzEventIds.values()))))
self.node.setState(ON_HOLD)
else:
ocf.logger.debug("monitor: no local azEvents to handle")
@@ -761,6 +790,24 @@ def main():
longdesc="Set to true to enable verbose logging",
content_type="boolean",
default="false")
+ agent.add_parameter(
+ "retry_count",
+ shortdesc="Azure IMDS webservice retry count",
+ longdesc="Set to any number bigger than zero to enable retry count",
+ content_type="integer",
+ default="3")
+ agent.add_parameter(
+ "retry_wait",
+ shortdesc="Configure a retry wait time",
+ longdesc="Set retry wait time in seconds",
+ content_type="integer",
+ default="20")
+ agent.add_parameter(
+ "request_timeout",
+ shortdesc="Configure a request timeout",
+ longdesc="Set request timeout in seconds",
+ content_type="integer",
+ default="15")
agent.add_action("start", timeout=10, handler=lambda: ocf.OCF_SUCCESS)
agent.add_action("stop", timeout=10, handler=lambda: ocf.OCF_SUCCESS)
agent.add_action("validate-all", timeout=20, handler=validate_action)
diff --git a/heartbeat/ocf.py b/heartbeat/ocf.py
index dda2fed4bb..571cd19664 100644
--- a/heartbeat/ocf.py
+++ b/heartbeat/ocf.py
@@ -16,7 +16,7 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-#
+#
import sys, os, logging, syslog
@@ -42,19 +42,19 @@
# OCF does not include the concept of master/slave resources so we
# need to extend it so we can discover a resource's complete state.
#
-# OCF_RUNNING_MASTER:
+# OCF_RUNNING_MASTER:
# The resource is in "master" mode and fully operational
# OCF_FAILED_MASTER:
# The resource is in "master" mode but in a failed state
-#
+#
# The extra two values should only be used during a probe.
#
# Probes are used to discover resources that were started outside of
# the CRM and/or left behind if the LRM fails.
-#
+#
# They can be identified in RA scripts by checking for:
# [ "${__OCF_ACTION}" = "monitor" -a "${OCF_RESKEY_CRM_meta_interval}" = "0" ]
-#
+#
# Failed "slaves" should continue to use: OCF_ERR_GENERIC
# Fully operational "slaves" should continue to use: OCF_SUCCESS
#
@@ -451,15 +451,17 @@ def value_for_parameter(param):
sys.exit(OCF_ERR_UNIMPLEMENTED)
+
if __name__ == "__main__":
import unittest
+ import logging
class TestMetadata(unittest.TestCase):
def test_noparams_noactions(self):
m = Agent("foo", shortdesc="shortdesc", longdesc="longdesc")
self.assertEqual("""<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
-<resource-agent name="foo">
+<resource-agent name="foo" version="1.0">
<version>1.0</version>
<longdesc lang="en">
longdesc
@@ -483,4 +485,40 @@ def test_params_actions(self):
m.add_action("start")
self.assertEqual(str(m.actions[0]), '<action name="start" />\n')
+ def test_retry_params_actions(self):
+ log= logging.getLogger( "test_retry_params_actions" )
+
+ m = Agent("foo", shortdesc="shortdesc", longdesc="longdesc")
+ m.add_parameter(
+ "retry_count",
+ shortdesc="Azure ims webservice retry count",
+ longdesc="Set to any number bigger than zero to enable retry count",
+ content_type="integer",
+ default="0")
+ m.add_parameter(
+ "retry_wait",
+ shortdesc="Configure a retry wait time",
+ longdesc="Set retry wait time in seconds",
+ content_type="integer",
+ default="20")
+ m.add_parameter(
+ "request_timeout",
+ shortdesc="Configure a request timeout",
+ longdesc="Set request timeout in seconds",
+ content_type="integer",
+ default="15")
+
+ m.add_action("start")
+
+ log.debug( "actions= %s", str(m.actions[0] ))
+ self.assertEqual(str(m.actions[0]), '<action name="start" />\n')
+
+ log.debug( "parameters= %s", str(m.parameters[0] ))
+ log.debug( "parameters= %s", str(m.parameters[1] ))
+ log.debug( "parameters= %s", str(m.parameters[2] ))
+ self.assertEqual(str(m.parameters[0]), '<parameter name="retry_count">\n<longdesc lang="en">Set to any number bigger than zero to enable retry count</longdesc>\n<shortdesc lang="en">Azure ims webservice retry count</shortdesc>\n<content type="integer" default="0" />\n</parameter>\n')
+ self.assertEqual(str(m.parameters[1]), '<parameter name="retry_wait">\n<longdesc lang="en">Set retry wait time in seconds</longdesc>\n<shortdesc lang="en">Configure a retry wait time</shortdesc>\n<content type="integer" default="20" />\n</parameter>\n')
+ self.assertEqual(str(m.parameters[2]), '<parameter name="request_timeout">\n<longdesc lang="en">Set request timeout in seconds</longdesc>\n<shortdesc lang="en">Configure a request timeout</shortdesc>\n<content type="integer" default="15" />\n</parameter>\n')
+
+ logging.basicConfig( stream=sys.stderr )
unittest.main()

File diff suppressed because it is too large Load Diff

View File

@ -1,84 +0,0 @@
From 277370f569b34e1cfb49637f9a00afc20bcd4c54 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Wed, 17 Jul 2024 10:43:29 +0200
Subject: [PATCH] build: dont build powervs-subnet if dependencies are missing
---
configure.ac | 9 +++++++++
doc/man/Makefile.am | 5 ++++-
heartbeat/Makefile.am | 5 ++++-
3 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/configure.ac b/configure.ac
index b785e2c2c..21ce27423 100644
--- a/configure.ac
+++ b/configure.ac
@@ -519,6 +519,8 @@ fi
AC_PYTHON_MODULE(json)
AC_PYTHON_MODULE(pyroute2)
+AC_PYTHON_MODULE(requests)
+AC_PYTHON_MODULE(urllib3)
AS_VERSION_COMPARE([$PYTHON_VERSION], [3.6], [BUILD_OCF_PY=0], [BUILD_OCF_PY=1], [BUILD_OCF_PY=1])
@@ -557,6 +559,13 @@ if test -z "$PYTHON" || test $BUILD_OCF_PY -eq 0; then
fi
AM_CONDITIONAL(BUILD_GCP_VPC_MOVE_VIP, test $BUILD_GCP_VPC_MOVE_VIP -eq 1)
+BUILD_POWERVS_SUBNET=1
+if test -z "$PYTHON" || test $BUILD_OCF_PY -eq 0 || test "x${HAVE_PYMOD_REQUESTS}" != xyes || test "x${HAVE_PYMOD_URLLIB3}" != xyes; then
+ BUILD_POWERVS_SUBNET=0
+ AC_MSG_WARN("Not building powervs-subnet")
+fi
+AM_CONDITIONAL(BUILD_POWERVS_SUBNET, test $BUILD_POWERVS_SUBNET -eq 1)
+
AC_PATH_PROGS(ROUTE, route)
AC_DEFINE_UNQUOTED(ROUTE, "$ROUTE", path to route command)
diff --git a/doc/man/Makefile.am b/doc/man/Makefile.am
index e577e6357..ef7639bff 100644
--- a/doc/man/Makefile.am
+++ b/doc/man/Makefile.am
@@ -190,7 +190,6 @@ man_MANS = ocf_heartbeat_AoEtarget.7 \
ocf_heartbeat_portblock.7 \
ocf_heartbeat_postfix.7 \
ocf_heartbeat_pound.7 \
- ocf_heartbeat_powervs-subnet.7 \
ocf_heartbeat_proftpd.7 \
ocf_heartbeat_rabbitmq-cluster.7 \
ocf_heartbeat_rabbitmq-server-ha.7 \
@@ -238,6 +237,10 @@ if BUILD_GCP_VPC_MOVE_VIP
man_MANS += ocf_heartbeat_gcp-vpc-move-vip.7
endif
+if BUILD_POWERVS_SUBNET
+man_MANS += ocf_heartbeat_powervs-subnet.7
+endif
+
xmlfiles = $(man_MANS:.7=.xml)
%.1 %.5 %.7 %.8: %.xml
diff --git a/heartbeat/Makefile.am b/heartbeat/Makefile.am
index ff73a15aa..409847970 100644
--- a/heartbeat/Makefile.am
+++ b/heartbeat/Makefile.am
@@ -162,7 +162,6 @@ ocf_SCRIPTS = AoEtarget \
portblock \
postfix \
pound \
- powervs-subnet \
proftpd \
rabbitmq-cluster \
rabbitmq-server-ha \
@@ -207,6 +206,10 @@ if BUILD_GCP_VPC_MOVE_VIP
ocf_SCRIPTS += gcp-vpc-move-vip
endif
+if BUILD_POWERVS_SUBNET
+ocf_SCRIPTS += powervs-subnet
+endif
+
ocfcommondir = $(OCF_LIB_DIR_PREFIX)/heartbeat
ocfcommon_DATA = ocf-shellfuncs \
ocf-binaries \

View File

@ -1,43 +0,0 @@
From 0b4bf9c23eb60455da6c6a16c1df19282ab2a8b5 Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Fri, 9 Jan 2026 12:56:14 +0100
Subject: [PATCH] powervs-subnet: wait until IP is activated before running
monitor-check
---
heartbeat/powervs-subnet.in | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/heartbeat/powervs-subnet.in b/heartbeat/powervs-subnet.in
index 84e86c0c4..062b1235e 100755
--- a/heartbeat/powervs-subnet.in
+++ b/heartbeat/powervs-subnet.in
@@ -243,7 +243,16 @@ class nmcli:
@staticmethod
def up(name, **kwargs):
- return nmcli._nmcli_cmd("connection", "up", name, **kwargs)
+ nmcli._nmcli_cmd("connection", "up", name, **kwargs)
+
+ for i in range(1, 10):
+ time.sleep(1)
+ status = nmcli._nmcli_cmd("connection", "show", name, **kwargs)
+ if len(status.get("IP4.ADDRESS[1]", "")) > 0:
+ return ocf.OCF_SUCCESS
+ ocf.logger.warning(f"nmcli.connection.up: check {i} of 10: IP not yet available.")
+
+ return ocf.OCF_ERR_GENERIC
@staticmethod
def find(match_key, match_value):
@@ -824,7 +833,9 @@ def start_action(
conn_options.update({"802-3-ethernet.mtu": "9000", "ethtool.feature-tso": "on"})
nmcli.connection.add(conn_name, options=conn_options)
- nmcli.connection.up(conn_name)
+ rc = nmcli.connection.up(conn_name)
+ if rc != ocf.OCF_SUCCESS:
+ return rc
if monitor_action(**res_options) != ocf.OCF_SUCCESS:
raise PowerCloudAPIError(f"start_action: start subnet: {ws.subnet_name} failed")

View File

@ -1,61 +0,0 @@
From 481672f73d05666ab20a883cf8fc746cb1f3050f Mon Sep 17 00:00:00 2001
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
Date: Thu, 20 Jun 2024 09:29:21 +0200
Subject: [PATCH] galera/mariadb/mysql/redis: remove Unpromoted monitor-action,
as it's covered by the regular monitor-action
---
heartbeat/galera.in | 1 -
heartbeat/mariadb.in | 1 -
heartbeat/mysql | 1 -
heartbeat/redis.in | 1 -
4 files changed, 4 deletions(-)
diff --git a/heartbeat/galera.in b/heartbeat/galera.in
index b518595cb0..b29d68bf73 100755
--- a/heartbeat/galera.in
+++ b/heartbeat/galera.in
@@ -299,7 +299,6 @@ Use it with caution! (and fencing)
<action name="status" timeout="60s" />
<action name="monitor" depth="0" timeout="30s" interval="20s" />
<action name="monitor" role="Promoted" depth="0" timeout="30s" interval="10s" />
-<action name="monitor" role="Unpromoted" depth="0" timeout="30s" interval="30s" />
<action name="promote" timeout="300s" />
<action name="demote" timeout="120s" />
<action name="validate-all" timeout="5s" />
diff --git a/heartbeat/mariadb.in b/heartbeat/mariadb.in
index e0f1f3c9f1..1dca98ba68 100644
--- a/heartbeat/mariadb.in
+++ b/heartbeat/mariadb.in
@@ -255,7 +255,6 @@ The port on which the Promoted MariaDB instance is listening.
<action name="status" timeout="60s" />
<action name="monitor" depth="0" timeout="30s" interval="20s" />
<action name="monitor" role="Promoted" depth="0" timeout="30s" interval="10s" />
-<action name="monitor" role="Unpromoted" depth="0" timeout="30s" interval="30s" />
<action name="promote" timeout="120s" />
<action name="demote" timeout="120s" />
<action name="notify" timeout="90s" />
diff --git a/heartbeat/mysql b/heartbeat/mysql
index 1df2fc0f28..6b00889ff4 100755
--- a/heartbeat/mysql
+++ b/heartbeat/mysql
@@ -322,7 +322,6 @@ whether a node is usable for clients to read from.</shortdesc>
<action name="status" timeout="60s" />
<action name="monitor" depth="0" timeout="30s" interval="20s" />
<action name="monitor" role="Promoted" depth="0" timeout="30s" interval="10s" />
-<action name="monitor" role="Unpromoted" depth="0" timeout="30s" interval="30s" />
<action name="promote" timeout="120s" />
<action name="demote" timeout="120s" />
<action name="notify" timeout="90s" />
diff --git a/heartbeat/redis.in b/heartbeat/redis.in
index 6429477e11..1e541f13d5 100755
--- a/heartbeat/redis.in
+++ b/heartbeat/redis.in
@@ -221,7 +221,6 @@ is in use.
<action name="status" timeout="60s" />
<action name="monitor" depth="0" timeout="60s" interval="45s" />
<action name="monitor" role="Promoted" depth="0" timeout="60s" interval="20s" />
-<action name="monitor" role="Unpromoted" depth="0" timeout="60s" interval="60s" />
<action name="promote" timeout="120s" />
<action name="demote" timeout="120s" />
<action name="notify" timeout="90s" />

View File

@ -0,0 +1,48 @@
From accff72ecc2f6cf5a76d9570198a93ac7c90270e Mon Sep 17 00:00:00 2001
From: Quentin Pradet <quentin.pradet@gmail.com>
Date: Mon, 17 Jun 2024 11:09:06 +0400
Subject: [PATCH] Merge pull request from GHSA-34jh-p97f-mpxf
* Strip Proxy-Authorization header on redirects
* Fix test_retry_default_remove_headers_on_redirect
* Set release date
---
CHANGES.rst | 5 +++++
src/urllib3/util/retry.py | 4 +++-
test/test_retry.py | 6 ++++-
test/with_dummyserver/test_poolmanager.py | 27 ++++++++++++++++++++---
4 files changed, 37 insertions(+), 5 deletions(-)
diff --git a/aliyun/aliyunsdkcore/vendored/requests/packages/urllib3/util/retry.py b/aliyun/aliyunsdkcore/vendored/requests/packages/urllib3/util/retry.py
index 7a76a4a6ad..0456cceba4 100644
--- a/aliyun/aliyunsdkcore/vendored/requests/packages/urllib3/util/retry.py
+++ b/aliyun/aliyunsdkcore/vendored/requests/packages/urllib3/util/retry.py
@@ -189,7 +189,9 @@ class Retry:
RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503])
#: Default headers to be used for ``remove_headers_on_redirect``
- DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset(["Cookie", "Authorization"])
+ DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset(
+ ["Cookie", "Authorization", "Proxy-Authorization"]
+ )
#: Default maximum backoff time.
DEFAULT_BACKOFF_MAX = 120
diff --git a/gcp/google-cloud-sdk/lib/third_party/urllib3/util/retry.py b/gcp/google-cloud-sdk/lib/third_party/urllib3/util/retry.py
index 7a76a4a6ad..0456cceba4 100644
--- a/gcp/google-cloud-sdk/lib/third_party/urllib3/util/retry.py
+++ b/gcp/google-cloud-sdk/lib/third_party/urllib3/util/retry.py
@@ -189,7 +189,9 @@ class Retry:
RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503])
#: Default headers to be used for ``remove_headers_on_redirect``
- DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset(["Cookie", "Authorization"])
+ DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset(
+ ["Cookie", "Authorization", "Proxy-Authorization"]
+ )
#: Default maximum backoff time.
DEFAULT_BACKOFF_MAX = 120

Some files were not shown because too many files have changed in this diff Show More