# 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_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}}" 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}" # 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 < 1.0 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. Manage PostgreSQL independently inside a persistent container Linux user owning the rootless systemd/Podman service. Rootless service owner systemd user service generated by Quadlet. Quadlet service name Podman container containing PostgreSQL. Container name Operating-system user running PostgreSQL inside the container. PostgreSQL OS user PostgreSQL database user used for health queries. Database user PostgreSQL database used for health queries. Database name PostgreSQL primary host used as the source for rejoin operations. Rejoin source host PostgreSQL port on the rejoin source. Rejoin source port PostgreSQL role used for replication and pg_rewind. Replication user 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. Physical replication slot PostgreSQL replication application_name. Defaults to the local hostname. Replication application name 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. Allow pg_basebackup rejoin fallback 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. pg_basebackup free-space headroom percent 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 "${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\ \ 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.\ Disconnected standby observation lifetime\ \ \ \ 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.\ Native replication peer map\ \ ' } 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 }