Freeze lab-tested PostgreSQL plugin engine
Extract PostgreSQL, Podman runtime and systemd lifecycle adapters while preserving existing lifecycle and scoring behavior. Update RPM layout and add guarded operator-run lab harnesses. Record successful manual stop/start, promotion, demotion and rewind-only rejoin with pinned installed hashes. The unfenced lab does not validate automatic failover, Pacemaker-managed transitions or divergent-data rewind. Validation: 103 offline tests, original-function differential checks, checksum manifest and whitespace checks passed.
This commit is contained in:
+1
-2
@@ -2,8 +2,7 @@
|
||||
|
||||
pg_replication_exec()
|
||||
{
|
||||
as_service_user \
|
||||
podman exec \
|
||||
mc_runtime_exec \
|
||||
--user "${PG_OS_USER}" \
|
||||
"${CONTAINER_NAME}" \
|
||||
sh -c '
|
||||
+1
-2
@@ -130,8 +130,7 @@ managed_demote()
|
||||
ocf_log info \
|
||||
"Configuring ${local_node} as cascading standby of ${target_node}"
|
||||
|
||||
if ! as_service_user \
|
||||
podman exec \
|
||||
if ! mc_runtime_exec \
|
||||
--user "${PG_OS_USER}" \
|
||||
--env "RA_PRIMARY_HOST=${target_addr}" \
|
||||
--env "RA_PRIMARY_PORT=${target_port}" \
|
||||
@@ -0,0 +1,281 @@
|
||||
# Sourced by managed-container; uses the shared OCF and parameter environment.
|
||||
|
||||
OCF_RESKEY_service_user_default="david"
|
||||
OCF_RESKEY_service_name_default="postgres.service"
|
||||
OCF_RESKEY_container_name_default="postgres"
|
||||
OCF_RESKEY_pg_os_user_default="postgres"
|
||||
OCF_RESKEY_db_user_default="david"
|
||||
OCF_RESKEY_db_name_default="david"
|
||||
OCF_RESKEY_rejoin_source_host_default=""
|
||||
OCF_RESKEY_rejoin_source_port_default="5432"
|
||||
OCF_RESKEY_replication_user_default="replicator"
|
||||
OCF_RESKEY_replication_slot_default=""
|
||||
OCF_RESKEY_application_name_default=""
|
||||
OCF_RESKEY_rejoin_allow_basebackup_default="false"
|
||||
|
||||
: "${OCF_RESKEY_service_user:=${OCF_RESKEY_service_user_default}}"
|
||||
: "${OCF_RESKEY_service_name:=${OCF_RESKEY_service_name_default}}"
|
||||
: "${OCF_RESKEY_container_name:=${OCF_RESKEY_container_name_default}}"
|
||||
: "${OCF_RESKEY_pg_os_user:=${OCF_RESKEY_pg_os_user_default}}"
|
||||
: "${OCF_RESKEY_db_user:=${OCF_RESKEY_db_user_default}}"
|
||||
: "${OCF_RESKEY_db_name:=${OCF_RESKEY_db_name_default}}"
|
||||
: "${OCF_RESKEY_rejoin_source_host:=${OCF_RESKEY_rejoin_source_host_default}}"
|
||||
: "${OCF_RESKEY_rejoin_source_port:=${OCF_RESKEY_rejoin_source_port_default}}"
|
||||
: "${OCF_RESKEY_replication_user:=${OCF_RESKEY_replication_user_default}}"
|
||||
: "${OCF_RESKEY_replication_slot:=${OCF_RESKEY_replication_slot_default}}"
|
||||
: "${OCF_RESKEY_application_name:=${OCF_RESKEY_application_name_default}}"
|
||||
: "${OCF_RESKEY_rejoin_allow_basebackup:=${OCF_RESKEY_rejoin_allow_basebackup_default}}"
|
||||
|
||||
SERVICE_USER="${OCF_RESKEY_service_user}"
|
||||
SERVICE_NAME="${OCF_RESKEY_service_name}"
|
||||
CONTAINER_NAME="${OCF_RESKEY_container_name}"
|
||||
PG_OS_USER="${OCF_RESKEY_pg_os_user}"
|
||||
DB_USER="${OCF_RESKEY_db_user}"
|
||||
DB_NAME="${OCF_RESKEY_db_name}"
|
||||
REJOIN_SOURCE_HOST="${OCF_RESKEY_rejoin_source_host}"
|
||||
REJOIN_SOURCE_PORT="${OCF_RESKEY_rejoin_source_port}"
|
||||
REPLICATION_USER="${OCF_RESKEY_replication_user}"
|
||||
REPLICATION_SLOT="${OCF_RESKEY_replication_slot}"
|
||||
APPLICATION_NAME="${OCF_RESKEY_application_name}"
|
||||
REJOIN_ALLOW_BASEBACKUP="${OCF_RESKEY_rejoin_allow_basebackup}"
|
||||
|
||||
if [ -z "${APPLICATION_NAME}" ]; then
|
||||
APPLICATION_NAME="$(hostname -s)"
|
||||
fi
|
||||
|
||||
meta_data()
|
||||
{
|
||||
cat <<EOF_META
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
|
||||
<resource-agent name="managed-container" version="0.4">
|
||||
<version>1.0</version>
|
||||
|
||||
<longdesc lang="en">
|
||||
Manage an application inside a persistent rootless container.
|
||||
|
||||
Version 0.4 implements role-aware monitoring, promotion, and demotion.
|
||||
The systemd user Quadlet owns the container lifecycle while pg_ctl owns
|
||||
the PostgreSQL lifecycle inside the container.
|
||||
</longdesc>
|
||||
|
||||
<shortdesc lang="en">
|
||||
Manage PostgreSQL independently inside a persistent container
|
||||
</shortdesc>
|
||||
|
||||
<parameters>
|
||||
|
||||
<parameter name="service_user" unique="0" required="1">
|
||||
<longdesc lang="en">
|
||||
Linux user owning the rootless systemd/Podman service.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Rootless service owner</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_service_user_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="service_name" unique="0" required="1">
|
||||
<longdesc lang="en">
|
||||
systemd user service generated by Quadlet.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Quadlet service name</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_service_name_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="container_name" unique="0" required="1">
|
||||
<longdesc lang="en">
|
||||
Podman container containing PostgreSQL.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Container name</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_container_name_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="pg_os_user" unique="0" required="1">
|
||||
<longdesc lang="en">
|
||||
Operating-system user running PostgreSQL inside the container.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">PostgreSQL OS user</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_pg_os_user_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="db_user" unique="0" required="1">
|
||||
<longdesc lang="en">
|
||||
PostgreSQL database user used for health queries.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Database user</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_db_user_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="db_name" unique="0" required="1">
|
||||
<longdesc lang="en">
|
||||
PostgreSQL database used for health queries.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Database name</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_db_name_default}"/>
|
||||
</parameter>
|
||||
|
||||
|
||||
<parameter name="rejoin_source_host" unique="0" required="0">
|
||||
<longdesc lang="en">
|
||||
PostgreSQL primary host used as the source for rejoin operations.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Rejoin source host</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_rejoin_source_host_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="rejoin_source_port" unique="0" required="0">
|
||||
<longdesc lang="en">
|
||||
PostgreSQL port on the rejoin source.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Rejoin source port</shortdesc>
|
||||
<content type="integer" default="${OCF_RESKEY_rejoin_source_port_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="replication_user" unique="0" required="0">
|
||||
<longdesc lang="en">
|
||||
PostgreSQL role used for replication and pg_rewind.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Replication user</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_replication_user_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="replication_slot" unique="0" required="0">
|
||||
<longdesc lang="en">
|
||||
Physical replication slot used by this standby.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Physical replication slot</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_replication_slot_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="application_name" unique="0" required="0">
|
||||
<longdesc lang="en">
|
||||
PostgreSQL replication application_name. Defaults to the local hostname.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Replication application name</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_application_name_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="rejoin_allow_basebackup" unique="0" required="0">
|
||||
<longdesc lang="en">
|
||||
Allow rejoin to fall back to pg_basebackup when pg_rewind cannot
|
||||
repair the local PostgreSQL data directory. The old PGDATA is
|
||||
preserved before replacement. This must only be enabled when the
|
||||
operator has established that this node may safely discard its
|
||||
divergent PostgreSQL history.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Allow pg_basebackup rejoin fallback</shortdesc>
|
||||
<content type="boolean" default="${OCF_RESKEY_rejoin_allow_basebackup_default}"/>
|
||||
</parameter>
|
||||
|
||||
</parameters>
|
||||
|
||||
<actions>
|
||||
<action name="start" timeout="60s"/>
|
||||
<action name="stop" timeout="60s"/>
|
||||
<action name="promote" timeout="60s"/>
|
||||
<action name="demote" timeout="60s"/>
|
||||
<action name="notify" timeout="20s"/>
|
||||
<action name="monitor" timeout="20s" interval="10s"/>
|
||||
<action name="validate-all" timeout="20s"/>
|
||||
<action name="meta-data" timeout="5s"/>
|
||||
</actions>
|
||||
</resource-agent>
|
||||
EOF_META
|
||||
}
|
||||
|
||||
validate_all()
|
||||
{
|
||||
if ! id "${SERVICE_USER}" >/dev/null 2>&1; then
|
||||
ocf_exit_reason "Service user '${SERVICE_USER}' does not exist"
|
||||
return "${OCF_ERR_CONFIGURED}"
|
||||
fi
|
||||
|
||||
for cmd in runuser systemctl podman getent; do
|
||||
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
||||
ocf_exit_reason "Required command '${cmd}' is not installed"
|
||||
return "${OCF_ERR_INSTALLED}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -d "/run/user/$(service_uid)" ]; then
|
||||
ocf_exit_reason \
|
||||
"Runtime directory /run/user/$(service_uid) does not exist"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
|
||||
if ! mc_lifecycle_cat "${SERVICE_NAME}" >/dev/null 2>&1
|
||||
then
|
||||
ocf_exit_reason \
|
||||
"User service '${SERVICE_NAME}' cannot be found for ${SERVICE_USER}"
|
||||
return "${OCF_ERR_CONFIGURED}"
|
||||
fi
|
||||
|
||||
local podman_error
|
||||
|
||||
podman_error="$(
|
||||
mc_runtime_info 2>&1 >/dev/null
|
||||
)"
|
||||
local podman_rc=$?
|
||||
|
||||
if [ "${podman_rc}" -ne 0 ]; then
|
||||
ocf_exit_reason \
|
||||
"Rootless Podman failed for ${SERVICE_USER}: ${podman_error}"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
|
||||
return "${OCF_SUCCESS}"
|
||||
}
|
||||
|
||||
|
||||
# Keep the original metadata function intact; advertise the added policy here.
|
||||
mc_promotion_metadata()
|
||||
{
|
||||
meta_data | sed '/<\/parameters>/i\
|
||||
<parameter name="disconnected_max_age" unique="0" required="0">\
|
||||
<longdesc lang="en">Maximum age in seconds of a verified streaming observation for disconnected promotion eligibility. Default 300; 0 disables disconnected eligibility. Requires fencing managed by Pacemaker. PostgreSQL restart requires fresh streaming verification.</longdesc>\
|
||||
<shortdesc lang="en">Disconnected standby observation lifetime</shortdesc>\
|
||||
<content type="integer" default="300"/>\
|
||||
</parameter>'
|
||||
}
|
||||
|
||||
MC_APPLICATION_API=1
|
||||
MC_APPLICATION_DIR="${MC_MODULE_DIR}/applications/postgresql"
|
||||
for MC_PG_MODULE in postgres auth replication notify promote demote rejoin; do
|
||||
if ! . "${MC_APPLICATION_DIR}/${MC_PG_MODULE}.sh"; then
|
||||
ocf_exit_reason "Unable to load resource-agent module ${MC_APPLICATION_DIR}/${MC_PG_MODULE}.sh"
|
||||
return "${OCF_ERR_CONFIGURED}"
|
||||
fi
|
||||
done
|
||||
unset MC_PG_MODULE
|
||||
|
||||
mc_application_monitor()
|
||||
{
|
||||
managed_monitor
|
||||
}
|
||||
|
||||
mc_application_standby_score()
|
||||
{
|
||||
mc_pg_standby_score
|
||||
}
|
||||
|
||||
mc_application_validate()
|
||||
{
|
||||
validate_all
|
||||
}
|
||||
|
||||
mc_application_metadata()
|
||||
{
|
||||
mc_promotion_metadata
|
||||
}
|
||||
|
||||
mc_application_notify()
|
||||
{
|
||||
managed_notify
|
||||
}
|
||||
|
||||
mc_application_action()
|
||||
{
|
||||
case "$1" in
|
||||
start|stop|promote|demote|rejoin) "managed_$1" ;;
|
||||
*) return "${OCF_ERR_UNIMPLEMENTED}" ;;
|
||||
esac
|
||||
}
|
||||
+9
-17
@@ -4,8 +4,7 @@ pg_command()
|
||||
{
|
||||
local command="$1"
|
||||
|
||||
as_service_user \
|
||||
podman exec \
|
||||
mc_runtime_exec \
|
||||
--user "${PG_OS_USER}" \
|
||||
"${CONTAINER_NAME}" \
|
||||
sh -c "${command}"
|
||||
@@ -32,8 +31,7 @@ pg_running()
|
||||
|
||||
pg_ready()
|
||||
{
|
||||
as_service_user \
|
||||
podman exec \
|
||||
mc_runtime_exec \
|
||||
--user "${PG_OS_USER}" \
|
||||
"${CONTAINER_NAME}" \
|
||||
pg_isready -q \
|
||||
@@ -43,8 +41,7 @@ pg_ready()
|
||||
|
||||
pg_role()
|
||||
{
|
||||
as_service_user \
|
||||
podman exec \
|
||||
mc_runtime_exec \
|
||||
--user "${PG_OS_USER}" \
|
||||
"${CONTAINER_NAME}" \
|
||||
psql \
|
||||
@@ -55,8 +52,7 @@ pg_role()
|
||||
|
||||
pg_wal_receiver_status()
|
||||
{
|
||||
as_service_user \
|
||||
podman exec \
|
||||
mc_runtime_exec \
|
||||
--user "${PG_OS_USER}" \
|
||||
"${CONTAINER_NAME}" \
|
||||
psql \
|
||||
@@ -153,8 +149,7 @@ managed_start()
|
||||
"Ensuring container service ${SERVICE_NAME} is running"
|
||||
|
||||
if ! service_running; then
|
||||
if ! as_service_user \
|
||||
systemctl --user start "${SERVICE_NAME}"
|
||||
if ! mc_lifecycle_start "${SERVICE_NAME}"
|
||||
then
|
||||
ocf_exit_reason \
|
||||
"Failed to start ${SERVICE_NAME}"
|
||||
@@ -260,8 +255,7 @@ managed_stop()
|
||||
ocf_log info \
|
||||
"Stopping container service ${SERVICE_NAME}"
|
||||
|
||||
if ! as_service_user \
|
||||
systemctl --user stop "${SERVICE_NAME}"
|
||||
if ! mc_lifecycle_stop "${SERVICE_NAME}"
|
||||
then
|
||||
ocf_exit_reason \
|
||||
"Failed to stop ${SERVICE_NAME}"
|
||||
@@ -299,8 +293,7 @@ mc_pg_promotion_eligible()
|
||||
local eligible
|
||||
|
||||
eligible="$(
|
||||
as_service_user timeout --kill-after=1s 3s \
|
||||
podman exec --user "${PG_OS_USER}" \
|
||||
mc_runtime_exec_bounded 3s --user "${PG_OS_USER}" \
|
||||
--env 'PGOPTIONS=-c statement_timeout=2000' \
|
||||
"${CONTAINER_NAME}" \
|
||||
psql -X -v ON_ERROR_STOP=1 -U "${DB_USER}" -d "${DB_NAME}" -Atqc "
|
||||
@@ -323,8 +316,7 @@ mc_pg_promotion_eligible()
|
||||
# including a new basebackup sharing the same PostgreSQL system identifier.
|
||||
mc_pg_standby_observation()
|
||||
{
|
||||
as_service_user timeout --kill-after=1s 3s \
|
||||
podman exec --user "${PG_OS_USER}" \
|
||||
mc_runtime_exec_bounded 3s --user "${PG_OS_USER}" \
|
||||
--env 'PGOPTIONS=-c statement_timeout=2000' \
|
||||
"${CONTAINER_NAME}" \
|
||||
psql -X -v ON_ERROR_STOP=1 -U "${DB_USER}" -d "${DB_NAME}" -Atqc "
|
||||
@@ -358,7 +350,7 @@ mc_pg_standby_score()
|
||||
# The helper is root-owned alongside the modules; state never enters the
|
||||
# rootless container. A failed/slow helper always withdraws eligibility.
|
||||
printf '%s' "${observation}" | timeout --kill-after=1s 1s python3 \
|
||||
"${MC_MODULE_DIR}/promotion_state.py" \
|
||||
"${MC_APPLICATION_DIR}/promotion_state.py" \
|
||||
"${MC_PROMOTION_RESOURCE_INSTANCE:-}" \
|
||||
"${SERVICE_USER}" "${SERVICE_NAME}" "${CONTAINER_NAME}" \
|
||||
"${OCF_RESKEY_disconnected_max_age:-300}"
|
||||
+1
-2
@@ -268,8 +268,7 @@ managed_rejoin()
|
||||
ocf_log info \
|
||||
"Configuring PostgreSQL as standby of ${REJOIN_SOURCE_HOST}"
|
||||
|
||||
if ! as_service_user \
|
||||
podman exec \
|
||||
if ! mc_runtime_exec \
|
||||
--user "${PG_OS_USER}" \
|
||||
--env "RA_PRIMARY_HOST=${REJOIN_SOURCE_HOST}" \
|
||||
--env "RA_PRIMARY_PORT=${REJOIN_SOURCE_PORT}" \
|
||||
@@ -1,230 +1,4 @@
|
||||
# Sourced by managed-container; uses the shared OCF and parameter environment.
|
||||
|
||||
OCF_RESKEY_service_user_default="david"
|
||||
OCF_RESKEY_service_name_default="postgres.service"
|
||||
OCF_RESKEY_container_name_default="postgres"
|
||||
OCF_RESKEY_pg_os_user_default="postgres"
|
||||
OCF_RESKEY_db_user_default="david"
|
||||
OCF_RESKEY_db_name_default="david"
|
||||
OCF_RESKEY_rejoin_source_host_default=""
|
||||
OCF_RESKEY_rejoin_source_port_default="5432"
|
||||
OCF_RESKEY_replication_user_default="replicator"
|
||||
OCF_RESKEY_replication_slot_default=""
|
||||
OCF_RESKEY_application_name_default=""
|
||||
OCF_RESKEY_rejoin_allow_basebackup_default="false"
|
||||
|
||||
: "${OCF_RESKEY_service_user:=${OCF_RESKEY_service_user_default}}"
|
||||
: "${OCF_RESKEY_service_name:=${OCF_RESKEY_service_name_default}}"
|
||||
: "${OCF_RESKEY_container_name:=${OCF_RESKEY_container_name_default}}"
|
||||
: "${OCF_RESKEY_pg_os_user:=${OCF_RESKEY_pg_os_user_default}}"
|
||||
: "${OCF_RESKEY_db_user:=${OCF_RESKEY_db_user_default}}"
|
||||
: "${OCF_RESKEY_db_name:=${OCF_RESKEY_db_name_default}}"
|
||||
: "${OCF_RESKEY_rejoin_source_host:=${OCF_RESKEY_rejoin_source_host_default}}"
|
||||
: "${OCF_RESKEY_rejoin_source_port:=${OCF_RESKEY_rejoin_source_port_default}}"
|
||||
: "${OCF_RESKEY_replication_user:=${OCF_RESKEY_replication_user_default}}"
|
||||
: "${OCF_RESKEY_replication_slot:=${OCF_RESKEY_replication_slot_default}}"
|
||||
: "${OCF_RESKEY_application_name:=${OCF_RESKEY_application_name_default}}"
|
||||
: "${OCF_RESKEY_rejoin_allow_basebackup:=${OCF_RESKEY_rejoin_allow_basebackup_default}}"
|
||||
|
||||
SERVICE_USER="${OCF_RESKEY_service_user}"
|
||||
SERVICE_NAME="${OCF_RESKEY_service_name}"
|
||||
CONTAINER_NAME="${OCF_RESKEY_container_name}"
|
||||
PG_OS_USER="${OCF_RESKEY_pg_os_user}"
|
||||
DB_USER="${OCF_RESKEY_db_user}"
|
||||
DB_NAME="${OCF_RESKEY_db_name}"
|
||||
REJOIN_SOURCE_HOST="${OCF_RESKEY_rejoin_source_host}"
|
||||
REJOIN_SOURCE_PORT="${OCF_RESKEY_rejoin_source_port}"
|
||||
REPLICATION_USER="${OCF_RESKEY_replication_user}"
|
||||
REPLICATION_SLOT="${OCF_RESKEY_replication_slot}"
|
||||
APPLICATION_NAME="${OCF_RESKEY_application_name}"
|
||||
REJOIN_ALLOW_BASEBACKUP="${OCF_RESKEY_rejoin_allow_basebackup}"
|
||||
|
||||
if [ -z "${APPLICATION_NAME}" ]; then
|
||||
APPLICATION_NAME="$(hostname -s)"
|
||||
fi
|
||||
|
||||
meta_data()
|
||||
{
|
||||
cat <<EOF_META
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
|
||||
<resource-agent name="managed-container" version="0.4">
|
||||
<version>1.0</version>
|
||||
|
||||
<longdesc lang="en">
|
||||
Manage an application inside a persistent rootless container.
|
||||
|
||||
Version 0.4 implements role-aware monitoring, promotion, and demotion.
|
||||
The systemd user Quadlet owns the container lifecycle while pg_ctl owns
|
||||
the PostgreSQL lifecycle inside the container.
|
||||
</longdesc>
|
||||
|
||||
<shortdesc lang="en">
|
||||
Manage PostgreSQL independently inside a persistent container
|
||||
</shortdesc>
|
||||
|
||||
<parameters>
|
||||
|
||||
<parameter name="service_user" unique="0" required="1">
|
||||
<longdesc lang="en">
|
||||
Linux user owning the rootless systemd/Podman service.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Rootless service owner</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_service_user_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="service_name" unique="0" required="1">
|
||||
<longdesc lang="en">
|
||||
systemd user service generated by Quadlet.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Quadlet service name</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_service_name_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="container_name" unique="0" required="1">
|
||||
<longdesc lang="en">
|
||||
Podman container containing PostgreSQL.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Container name</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_container_name_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="pg_os_user" unique="0" required="1">
|
||||
<longdesc lang="en">
|
||||
Operating-system user running PostgreSQL inside the container.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">PostgreSQL OS user</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_pg_os_user_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="db_user" unique="0" required="1">
|
||||
<longdesc lang="en">
|
||||
PostgreSQL database user used for health queries.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Database user</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_db_user_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="db_name" unique="0" required="1">
|
||||
<longdesc lang="en">
|
||||
PostgreSQL database used for health queries.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Database name</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_db_name_default}"/>
|
||||
</parameter>
|
||||
|
||||
|
||||
<parameter name="rejoin_source_host" unique="0" required="0">
|
||||
<longdesc lang="en">
|
||||
PostgreSQL primary host used as the source for rejoin operations.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Rejoin source host</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_rejoin_source_host_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="rejoin_source_port" unique="0" required="0">
|
||||
<longdesc lang="en">
|
||||
PostgreSQL port on the rejoin source.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Rejoin source port</shortdesc>
|
||||
<content type="integer" default="${OCF_RESKEY_rejoin_source_port_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="replication_user" unique="0" required="0">
|
||||
<longdesc lang="en">
|
||||
PostgreSQL role used for replication and pg_rewind.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Replication user</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_replication_user_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="replication_slot" unique="0" required="0">
|
||||
<longdesc lang="en">
|
||||
Physical replication slot used by this standby.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Physical replication slot</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_replication_slot_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="application_name" unique="0" required="0">
|
||||
<longdesc lang="en">
|
||||
PostgreSQL replication application_name. Defaults to the local hostname.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Replication application name</shortdesc>
|
||||
<content type="string" default="${OCF_RESKEY_application_name_default}"/>
|
||||
</parameter>
|
||||
|
||||
<parameter name="rejoin_allow_basebackup" unique="0" required="0">
|
||||
<longdesc lang="en">
|
||||
Allow rejoin to fall back to pg_basebackup when pg_rewind cannot
|
||||
repair the local PostgreSQL data directory. The old PGDATA is
|
||||
preserved before replacement. This must only be enabled when the
|
||||
operator has established that this node may safely discard its
|
||||
divergent PostgreSQL history.
|
||||
</longdesc>
|
||||
<shortdesc lang="en">Allow pg_basebackup rejoin fallback</shortdesc>
|
||||
<content type="boolean" default="${OCF_RESKEY_rejoin_allow_basebackup_default}"/>
|
||||
</parameter>
|
||||
|
||||
</parameters>
|
||||
|
||||
<actions>
|
||||
<action name="start" timeout="60s"/>
|
||||
<action name="stop" timeout="60s"/>
|
||||
<action name="promote" timeout="60s"/>
|
||||
<action name="demote" timeout="60s"/>
|
||||
<action name="notify" timeout="20s"/>
|
||||
<action name="monitor" timeout="20s" interval="10s"/>
|
||||
<action name="validate-all" timeout="20s"/>
|
||||
<action name="meta-data" timeout="5s"/>
|
||||
</actions>
|
||||
</resource-agent>
|
||||
EOF_META
|
||||
}
|
||||
|
||||
validate_all()
|
||||
{
|
||||
if ! id "${SERVICE_USER}" >/dev/null 2>&1; then
|
||||
ocf_exit_reason "Service user '${SERVICE_USER}' does not exist"
|
||||
return "${OCF_ERR_CONFIGURED}"
|
||||
fi
|
||||
|
||||
for cmd in runuser systemctl podman getent; do
|
||||
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
||||
ocf_exit_reason "Required command '${cmd}' is not installed"
|
||||
return "${OCF_ERR_INSTALLED}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -d "/run/user/$(service_uid)" ]; then
|
||||
ocf_exit_reason \
|
||||
"Runtime directory /run/user/$(service_uid) does not exist"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
|
||||
if ! as_service_user \
|
||||
systemctl --user cat "${SERVICE_NAME}" >/dev/null 2>&1
|
||||
then
|
||||
ocf_exit_reason \
|
||||
"User service '${SERVICE_NAME}' cannot be found for ${SERVICE_USER}"
|
||||
return "${OCF_ERR_CONFIGURED}"
|
||||
fi
|
||||
|
||||
local podman_error
|
||||
|
||||
podman_error="$(
|
||||
as_service_user podman info 2>&1 >/dev/null
|
||||
)"
|
||||
local podman_rc=$?
|
||||
|
||||
if [ "${podman_rc}" -ne 0 ]; then
|
||||
ocf_exit_reason \
|
||||
"Rootless Podman failed for ${SERVICE_USER}: ${podman_error}"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
|
||||
return "${OCF_SUCCESS}"
|
||||
}
|
||||
# Shared OCF scoring wrappers; application policy is supplied by the plugin.
|
||||
|
||||
# Promotion scores are scoped by Pacemaker to OCF_RESOURCE_INSTANCE and the
|
||||
# executing node. Do not construct a legacy attribute name or cache locally:
|
||||
@@ -269,7 +43,7 @@ mc_promotion_score_from_monitor()
|
||||
case "${monitor_rc}" in
|
||||
"${OCF_RUNNING_PROMOTED}") score=1000 ;;
|
||||
"${OCF_SUCCESS}")
|
||||
score="$(mc_pg_standby_score)" || score=-INFINITY
|
||||
score="$(mc_application_standby_score)" || score=-INFINITY
|
||||
;;
|
||||
esac
|
||||
case "${score}" in
|
||||
@@ -284,7 +58,7 @@ mc_promotion_monitor()
|
||||
{
|
||||
local monitor_rc
|
||||
|
||||
managed_monitor
|
||||
mc_application_monitor
|
||||
monitor_rc=$?
|
||||
mc_promotion_score_from_monitor "${monitor_rc}" || true
|
||||
return "${monitor_rc}"
|
||||
@@ -310,10 +84,10 @@ mc_promotion_action()
|
||||
[ "${action_rc}" -eq "${OCF_SUCCESS}" ] || return "${action_rc}"
|
||||
fi
|
||||
|
||||
"managed_${action}"
|
||||
mc_application_action "${action}"
|
||||
action_rc=$?
|
||||
if [ "${action_rc}" -eq "${OCF_SUCCESS}" ] && [ "${action}" != stop ]; then
|
||||
managed_monitor
|
||||
mc_application_monitor
|
||||
monitor_rc=$?
|
||||
mc_promotion_score_from_monitor "${monitor_rc}" || true
|
||||
fi
|
||||
@@ -335,16 +109,5 @@ mc_promotion_validate()
|
||||
return "${OCF_ERR_INSTALLED}"
|
||||
fi
|
||||
done
|
||||
validate_all
|
||||
}
|
||||
|
||||
# Keep the original metadata function intact; advertise the added policy here.
|
||||
mc_promotion_metadata()
|
||||
{
|
||||
meta_data | sed '/<\/parameters>/i\
|
||||
<parameter name="disconnected_max_age" unique="0" required="0">\
|
||||
<longdesc lang="en">Maximum age in seconds of a verified streaming observation for disconnected promotion eligibility. Default 300; 0 disables disconnected eligibility. Requires fencing managed by Pacemaker. PostgreSQL restart requires fresh streaming verification.</longdesc>\
|
||||
<shortdesc lang="en">Disconnected standby observation lifetime</shortdesc>\
|
||||
<content type="integer" default="300"/>\
|
||||
</parameter>'
|
||||
mc_application_validate
|
||||
}
|
||||
|
||||
@@ -37,8 +37,7 @@ as_service_user()
|
||||
|
||||
service_running()
|
||||
{
|
||||
as_service_user \
|
||||
systemctl --user is-active --quiet "${SERVICE_NAME}"
|
||||
mc_lifecycle_running "${SERVICE_NAME}"
|
||||
}
|
||||
|
||||
container_running()
|
||||
@@ -46,7 +45,7 @@ container_running()
|
||||
local state
|
||||
|
||||
state="$(
|
||||
as_service_user podman inspect \
|
||||
mc_runtime_inspect \
|
||||
--format '{{.State.Running}}' \
|
||||
"${CONTAINER_NAME}" 2>/dev/null
|
||||
)" || return 1
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Engine API 1. Selection is an allowlist of packaged implementations, never a
|
||||
# filename, shell fragment or a plugin downloaded from cluster configuration.
|
||||
mc_engine_load()
|
||||
{
|
||||
local plugin hook
|
||||
if [ "${OCF_RESKEY_application:-postgresql}" != postgresql ] ||
|
||||
[ "${OCF_RESKEY_container_runtime:-podman}" != podman ] ||
|
||||
[ "${OCF_RESKEY_lifecycle_manager:-systemd}" != systemd ]; then
|
||||
ocf_exit_reason "Supported plugin combination: application=postgresql container_runtime=podman lifecycle_manager=systemd"
|
||||
return "${OCF_ERR_CONFIGURED}"
|
||||
fi
|
||||
|
||||
unset MC_RUNTIME_API MC_LIFECYCLE_API MC_APPLICATION_API
|
||||
for plugin in runtimes/podman lifecycles/systemd applications/postgresql/plugin; do
|
||||
if ! . "${MC_MODULE_DIR}/${plugin}.sh"; then
|
||||
ocf_exit_reason "Unable to load resource-agent module ${MC_MODULE_DIR}/${plugin}.sh"
|
||||
return "${OCF_ERR_CONFIGURED}"
|
||||
fi
|
||||
done
|
||||
if [ "${MC_RUNTIME_API:-}" != 1 ] || [ "${MC_LIFECYCLE_API:-}" != 1 ] ||
|
||||
[ "${MC_APPLICATION_API:-}" != 1 ]; then
|
||||
ocf_exit_reason "Incompatible plugin interface; API 1 required"
|
||||
return "${OCF_ERR_CONFIGURED}"
|
||||
fi
|
||||
for hook in mc_runtime_exec mc_runtime_exec_bounded mc_runtime_inspect mc_runtime_info \
|
||||
mc_lifecycle_start mc_lifecycle_stop mc_lifecycle_running mc_lifecycle_cat \
|
||||
mc_application_monitor mc_application_standby_score mc_application_validate \
|
||||
mc_application_metadata mc_application_notify mc_application_action; do
|
||||
if ! declare -F "${hook}" >/dev/null; then
|
||||
ocf_exit_reason "Required plugin hook is missing: ${hook}"
|
||||
return "${OCF_ERR_CONFIGURED}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
mc_engine_metadata()
|
||||
{
|
||||
# Metadata discovery works without configured resource instance attributes.
|
||||
# Capture the plugin's status before transforming XML; avoid a pipeline
|
||||
# masking a metadata failure. No runtime/service/cluster access is required.
|
||||
local metadata
|
||||
metadata="$(mc_application_metadata)" || return "$?"
|
||||
printf '%s\n' "${metadata}" | sed '/<\/parameters>/i\
|
||||
<parameter name="application" unique="0" required="0">\
|
||||
<longdesc lang="en">Packaged application plugin. Only postgresql is supported.</longdesc>\
|
||||
<shortdesc lang="en">Application plugin</shortdesc>\
|
||||
<content type="string" default="postgresql"/>\
|
||||
</parameter>\
|
||||
<parameter name="container_runtime" unique="0" required="0">\
|
||||
<longdesc lang="en">Packaged runtime adapter. Only podman is supported.</longdesc>\
|
||||
<shortdesc lang="en">Container runtime</shortdesc>\
|
||||
<content type="string" default="podman"/>\
|
||||
</parameter>\
|
||||
<parameter name="lifecycle_manager" unique="0" required="0">\
|
||||
<longdesc lang="en">Container lifecycle owner. Only systemd with the existing user service is supported.</longdesc>\
|
||||
<shortdesc lang="en">Lifecycle manager</shortdesc>\
|
||||
<content type="string" default="systemd"/>\
|
||||
</parameter>'
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Lifecycle adapter API 1. Quadlet remains the owner of container construction,
|
||||
# init/maintenance command, signal settings and service restart/exit policies.
|
||||
MC_LIFECYCLE_API=1
|
||||
|
||||
mc_lifecycle_start()
|
||||
{
|
||||
as_service_user systemctl --user start "$1"
|
||||
}
|
||||
|
||||
mc_lifecycle_stop()
|
||||
{
|
||||
as_service_user systemctl --user stop "$1"
|
||||
}
|
||||
|
||||
mc_lifecycle_running()
|
||||
{
|
||||
as_service_user systemctl --user is-active --quiet "$1"
|
||||
}
|
||||
|
||||
mc_lifecycle_cat()
|
||||
{
|
||||
as_service_user systemctl --user cat "$1"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Runtime adapter API 1. The existing rootless privilege boundary is unchanged.
|
||||
MC_RUNTIME_API=1
|
||||
|
||||
mc_runtime_exec()
|
||||
{
|
||||
as_service_user podman exec "$@"
|
||||
}
|
||||
|
||||
mc_runtime_exec_bounded()
|
||||
{
|
||||
local duration="$1"
|
||||
shift
|
||||
as_service_user timeout --kill-after=1s "${duration}" podman exec "$@"
|
||||
}
|
||||
|
||||
mc_runtime_inspect()
|
||||
{
|
||||
as_service_user podman inspect "$@"
|
||||
}
|
||||
|
||||
mc_runtime_info()
|
||||
{
|
||||
as_service_user podman info "$@"
|
||||
}
|
||||
Reference in New Issue
Block a user