# 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 < 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. 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 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\ \ 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\ \ ' }