Files
ocf-managed-container/refactored/managed-container.d/common.sh
T

351 lines
12 KiB
Bash

# 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}"
}
# Promotion scores are scoped by Pacemaker to OCF_RESOURCE_INSTANCE and the
# executing node. Do not construct a legacy attribute name or cache locally:
# querying attrd on every observation also repairs a lost transient attribute.
mc_promotion_score_set()
{
local score="$1" current
local resource="${MC_PROMOTION_RESOURCE_INSTANCE:-}"
if [ -z "${resource}" ]; then
ocf_log warn "Cannot publish promotion score without OCF_RESOURCE_INSTANCE"
return 1
fi
case "${score}" in
1000|100|50|-INFINITY) ;;
*) return 1 ;;
esac
current="$(
timeout --kill-after=1s 2s crm_attribute \
--promotion="${resource}" --lifetime=reboot \
--query --quiet 2>/dev/null
)" || current=""
[ "${current}" = "${score}" ] && return 0
if ! timeout --kill-after=1s 2s crm_attribute \
--promotion="${resource}" --lifetime=reboot \
--update="${score}" >/dev/null 2>&1
then
ocf_log err "Unable to publish promotion score ${score} for ${resource}; prior score may remain"
return 1
fi
return 0
}
mc_promotion_score_from_monitor()
{
local monitor_rc="$1" score=-INFINITY
case "${monitor_rc}" in
"${OCF_RUNNING_PROMOTED}") score=1000 ;;
"${OCF_SUCCESS}")
score="$(mc_pg_standby_score)" || score=-INFINITY
;;
esac
case "${score}" in
1000|100|50|-INFINITY) ;;
*) score=-INFINITY ;;
esac
mc_promotion_score_set "${score}"
}
# Score publication must not change OCF role detection or its return code.
mc_promotion_monitor()
{
local monitor_rc
managed_monitor
monitor_rc=$?
mc_promotion_score_from_monitor "${monitor_rc}" || true
return "${monitor_rc}"
}
# Lifecycle functions retain their original checks, commands and return codes.
# Withdraw the previous score before any transition (including validation
# failure), then reassess only after success. Their internal monitor calls do
# not publish scores, so they cannot restore a positive score mid-transition.
mc_promotion_action()
{
local action="$1" action_rc monitor_rc
case "${action}" in
start|stop|promote|demote|rejoin) ;;
*) return "${OCF_ERR_UNIMPLEMENTED}" ;;
esac
mc_promotion_score_set -INFINITY || true
if [ "${action}" != stop ]; then
mc_promotion_validate
action_rc=$?
[ "${action_rc}" -eq "${OCF_SUCCESS}" ] || return "${action_rc}"
fi
"managed_${action}"
action_rc=$?
if [ "${action_rc}" -eq "${OCF_SUCCESS}" ] && [ "${action}" != stop ]; then
managed_monitor
monitor_rc=$?
mc_promotion_score_from_monitor "${monitor_rc}" || true
fi
return "${action_rc}"
}
mc_promotion_validate()
{
local utility
if ! [[ "${OCF_RESKEY_disconnected_max_age:-300}" =~ ^(0|[1-9][0-9]{0,4})$ ]]; then
ocf_exit_reason "disconnected_max_age must be an integer from 0 to 99999 seconds"
return "${OCF_ERR_CONFIGURED}"
fi
for utility in crm_attribute timeout python3; do
if ! command -v "${utility}" >/dev/null 2>&1; then
ocf_exit_reason "Required promotion scoring command '${utility}' is not installed"
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>'
}