428 lines
16 KiB
Bash
Executable File
428 lines
16 KiB
Bash
Executable File
# 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_rejoin_basebackup_headroom_percent_default="50"
|
|
OCF_RESKEY_demote_database_gate_default="false"
|
|
OCF_RESKEY_promote_database_gate_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}}"
|
|
: "${OCF_RESKEY_rejoin_basebackup_headroom_percent:=${OCF_RESKEY_rejoin_basebackup_headroom_percent_default}}"
|
|
: "${OCF_RESKEY_demote_database_gate:=${OCF_RESKEY_demote_database_gate_default}}"
|
|
: "${OCF_RESKEY_promote_database_gate:=${OCF_RESKEY_promote_database_gate_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}"
|
|
REJOIN_BASEBACKUP_HEADROOM_PERCENT="${OCF_RESKEY_rejoin_basebackup_headroom_percent}"
|
|
DEMOTE_DATABASE_GATE="${OCF_RESKEY_demote_database_gate}"
|
|
PROMOTE_DATABASE_GATE="${OCF_RESKEY_promote_database_gate}"
|
|
export MC_DEMOTE_DATABASE_GATE="${DEMOTE_DATABASE_GATE}"
|
|
export MC_PROMOTE_DATABASE_GATE="${PROMOTE_DATABASE_GATE}"
|
|
|
|
# Resolve PostgreSQL configuration from the managed service when Pacemaker has
|
|
# not explicitly supplied it. The application configuration remains the source
|
|
# of truth; no slot naming convention is inferred.
|
|
mc_pg_resolve_replication_slot()
|
|
{
|
|
local resolved
|
|
|
|
if [ -n "${REPLICATION_SLOT}" ]; then
|
|
return 0
|
|
fi
|
|
|
|
# Prefer PostgreSQL's effective setting when the server is already up.
|
|
# This asks PostgreSQL itself rather than reconstructing configuration.
|
|
if declare -F pg_running >/dev/null 2>&1 && pg_running; then
|
|
resolved="$(
|
|
mc_runtime_exec \
|
|
--user "${PG_OS_USER}" \
|
|
"${CONTAINER_NAME}" \
|
|
psql -X -U "${DB_USER}" -d "${DB_NAME}" -Atqc \
|
|
'SHOW primary_slot_name;' 2>/dev/null
|
|
)" || resolved=
|
|
if [ -n "${resolved}" ]; then
|
|
REPLICATION_SLOT=${resolved}
|
|
OCF_RESKEY_replication_slot=${resolved}
|
|
ocf_log debug "Resolved PostgreSQL replication slot from PostgreSQL effective configuration"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# When PostgreSQL is stopped but its maintenance container is running,
|
|
# obtain the exact value injected into the managed container. Runtime
|
|
# adapters own this lookup; the application plugin does not parse Quadlets.
|
|
if declare -F mc_runtime_environment >/dev/null 2>&1; then
|
|
resolved="$(
|
|
mc_runtime_environment \
|
|
"${CONTAINER_NAME}" \
|
|
POSTGRES_REPLICATION_SLOT
|
|
)" || resolved=
|
|
if [ -n "${resolved}" ]; then
|
|
REPLICATION_SLOT=${resolved}
|
|
OCF_RESKEY_replication_slot=${resolved}
|
|
ocf_log debug "Resolved PostgreSQL replication slot from managed container environment"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
mc_pg_require_replication_slot()
|
|
{
|
|
mc_pg_resolve_replication_slot || {
|
|
ocf_exit_reason "PostgreSQL replication slot is required but could not be resolved from replication_slot, PostgreSQL, or the managed container environment"
|
|
return "${OCF_ERR_CONFIGURED}"
|
|
}
|
|
|
|
case "${REPLICATION_SLOT}" in
|
|
''|*[!a-z0-9_]*)
|
|
ocf_exit_reason "Resolved replication slot is invalid; expected lowercase letters, digits, and underscore"
|
|
return "${OCF_ERR_CONFIGURED}"
|
|
;;
|
|
esac
|
|
|
|
if [ ${#REPLICATION_SLOT} -gt 63 ]; then
|
|
ocf_exit_reason "Resolved replication slot exceeds PostgreSQL's 63-character identifier limit"
|
|
return "${OCF_ERR_CONFIGURED}"
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
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. If unset, the agent
|
|
resolves the effective PostgreSQL primary_slot_name when available,
|
|
otherwise POSTGRES_REPLICATION_SLOT from the running managed container.
|
|
No slot name is derived or guessed.
|
|
</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>
|
|
|
|
<parameter name="rejoin_basebackup_headroom_percent" unique="0" required="0">
|
|
<longdesc lang="en">
|
|
Additional free-space headroom, as a percentage of the estimated
|
|
source database payload, required before pg_basebackup fallback.
|
|
The old PGDATA is renamed in place and therefore is not counted as
|
|
another free-space copy. A minimum 1 GiB headroom is always required.
|
|
</longdesc>
|
|
<shortdesc lang="en">pg_basebackup free-space headroom percent</shortdesc>
|
|
<content type="integer" default="${OCF_RESKEY_rejoin_basebackup_headroom_percent_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
|
|
|
|
case "${DEMOTE_DATABASE_GATE}" in
|
|
true|false)
|
|
;;
|
|
*)
|
|
ocf_exit_reason "demote_database_gate must be true or false"
|
|
return "${OCF_ERR_CONFIGURED}"
|
|
;;
|
|
esac
|
|
|
|
case "${PROMOTE_DATABASE_GATE}" in
|
|
true|false)
|
|
;;
|
|
*)
|
|
ocf_exit_reason "promote_database_gate must be true or false"
|
|
return "${OCF_ERR_CONFIGURED}"
|
|
;;
|
|
esac
|
|
|
|
case "${REJOIN_BASEBACKUP_HEADROOM_PERCENT}" in
|
|
""|*[!0-9]*)
|
|
ocf_exit_reason "rejoin_basebackup_headroom_percent must be a non-negative integer"
|
|
return "${OCF_ERR_CONFIGURED}"
|
|
;;
|
|
esac
|
|
if [ "${REJOIN_BASEBACKUP_HEADROOM_PERCENT}" -gt 500 ]; then
|
|
ocf_exit_reason "rejoin_basebackup_headroom_percent must be between 0 and 500"
|
|
return "${OCF_ERR_CONFIGURED}"
|
|
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>\
|
|
<parameter name="demote_database_gate" unique="0" required="0">\
|
|
<longdesc lang="en">When true, reject native PostgreSQL demotion unless the managed-containerd demote.database checks confirm a reachable, streaming, caught-up successor. When false, the daemon check remains shadow-only and native demotion fails open.</longdesc>\
|
|
<shortdesc lang="en">Gate demotion on database checks</shortdesc>\
|
|
<content type="boolean" default="false"/>\
|
|
</parameter>\
|
|
<parameter name="promote_database_gate" unique="0" required="0">\
|
|
<longdesc lang="en">When true, reject native PostgreSQL promotion unless the managed-containerd promote.database preflight agrees that this node is the notified promotion target, is an eligible standby, and no reachable writable peer exists. When false, the daemon check remains shadow-only and native promotion fails open.</longdesc>\
|
|
<shortdesc lang="en">Gate promotion on database checks</shortdesc>\
|
|
<content type="boolean" default="false"/>\
|
|
</parameter>\
|
|
<parameter name="replication_peers" unique="0" required="0">\
|
|
<longdesc lang="en">Opt-in stable peer map: node=host:port,node=host:port. Include the local node; exclude the client VIP. IPv4 or DNS hosts only. Requires a configured per-node replication slot, supplied explicitly or resolved from PostgreSQL/the managed container environment, plus peer authentication and slots on every upstream. Uses native primary-only selection; demotion verifies final WAL then ready recovery before successor promotion.</longdesc>\
|
|
<shortdesc lang="en">Native replication peer map</shortdesc>\
|
|
<content type="string" default=""/>\
|
|
</parameter>'
|
|
}
|
|
|
|
MC_APPLICATION_API=1
|
|
MC_APPLICATION_DIR="${MC_MODULE_DIR}/applications/postgresql"
|
|
for MC_PG_MODULE in peer_config native_peers 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()
|
|
{
|
|
mc_pg_peer_validate || return "$?"
|
|
validate_all
|
|
}
|
|
|
|
mc_application_metadata()
|
|
{
|
|
mc_promotion_metadata
|
|
}
|
|
|
|
mc_application_notify()
|
|
{
|
|
managed_notify
|
|
}
|
|
|
|
mc_application_action()
|
|
{
|
|
mc_pg_peer_validate || return "$?"
|
|
if [ "$1" = demote ] && [ -n "${OCF_RESKEY_replication_peers:-}" ]; then
|
|
mc_pg_native_demote
|
|
return "$?"
|
|
fi
|
|
case "$1" in
|
|
start|stop|promote|demote|rejoin) "managed_$1" ;;
|
|
*) return "${OCF_ERR_UNIMPLEMENTED}" ;;
|
|
esac
|
|
}
|