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.
358 lines
9.9 KiB
Bash
358 lines
9.9 KiB
Bash
# Sourced by managed-container; uses the shared OCF and parameter environment.
|
|
|
|
pg_command()
|
|
{
|
|
local command="$1"
|
|
|
|
mc_runtime_exec \
|
|
--user "${PG_OS_USER}" \
|
|
"${CONTAINER_NAME}" \
|
|
sh -c "${command}"
|
|
}
|
|
|
|
pgdata_path()
|
|
{
|
|
pg_command '
|
|
if [ -z "$PGDATA" ]; then
|
|
exit 1
|
|
fi
|
|
|
|
printf "%s\n" "$PGDATA"
|
|
'
|
|
}
|
|
|
|
pg_running()
|
|
{
|
|
pg_command '
|
|
BINDIR="$(pg_config --bindir)" || exit 1
|
|
"$BINDIR/pg_ctl" status -D "$PGDATA" >/dev/null 2>&1
|
|
'
|
|
}
|
|
|
|
pg_ready()
|
|
{
|
|
mc_runtime_exec \
|
|
--user "${PG_OS_USER}" \
|
|
"${CONTAINER_NAME}" \
|
|
pg_isready -q \
|
|
-U "${DB_USER}" \
|
|
-d "${DB_NAME}"
|
|
}
|
|
|
|
pg_role()
|
|
{
|
|
mc_runtime_exec \
|
|
--user "${PG_OS_USER}" \
|
|
"${CONTAINER_NAME}" \
|
|
psql \
|
|
-U "${DB_USER}" \
|
|
-d "${DB_NAME}" \
|
|
-Atqc 'SELECT pg_is_in_recovery();'
|
|
}
|
|
|
|
pg_wal_receiver_status()
|
|
{
|
|
mc_runtime_exec \
|
|
--user "${PG_OS_USER}" \
|
|
"${CONTAINER_NAME}" \
|
|
psql \
|
|
-U "${DB_USER}" \
|
|
-d "${DB_NAME}" \
|
|
-Atqc "SELECT status FROM pg_stat_wal_receiver LIMIT 1;"
|
|
}
|
|
|
|
managed_monitor()
|
|
{
|
|
local role
|
|
|
|
if ! service_running; then
|
|
ocf_log debug "User service ${SERVICE_NAME} is not active"
|
|
return "${OCF_NOT_RUNNING}"
|
|
fi
|
|
|
|
if ! container_running; then
|
|
ocf_log warn \
|
|
"Service ${SERVICE_NAME} is active but container ${CONTAINER_NAME} is not running"
|
|
return "${OCF_NOT_RUNNING}"
|
|
fi
|
|
|
|
if ! pg_running; then
|
|
ocf_log debug \
|
|
"Container ${CONTAINER_NAME} is running but PostgreSQL is stopped"
|
|
return "${OCF_NOT_RUNNING}"
|
|
fi
|
|
|
|
if ! pg_ready; then
|
|
ocf_exit_reason \
|
|
"PostgreSQL process exists but pg_isready failed"
|
|
return "${OCF_ERR_GENERIC}"
|
|
fi
|
|
|
|
role="$(pg_role 2>/dev/null)" || {
|
|
ocf_exit_reason \
|
|
"PostgreSQL is accepting connections but role query failed"
|
|
return "${OCF_ERR_GENERIC}"
|
|
}
|
|
|
|
case "${role}" in
|
|
f)
|
|
ocf_log debug \
|
|
"PostgreSQL healthy: role=primary (promoted)"
|
|
return "${OCF_RUNNING_PROMOTED}"
|
|
;;
|
|
t)
|
|
ocf_log debug \
|
|
"PostgreSQL healthy: role=standby (unpromoted)"
|
|
return "${OCF_SUCCESS}"
|
|
;;
|
|
*)
|
|
ocf_exit_reason \
|
|
"Unexpected pg_is_in_recovery() result: '${role}'"
|
|
return "${OCF_ERR_GENERIC}"
|
|
;;
|
|
esac
|
|
|
|
}
|
|
|
|
managed_start()
|
|
{
|
|
local i monitor_rc
|
|
|
|
managed_monitor
|
|
monitor_rc=$?
|
|
|
|
case "${monitor_rc}" in
|
|
"${OCF_SUCCESS}")
|
|
ocf_log info \
|
|
"PostgreSQL resource already running unpromoted"
|
|
return "${OCF_SUCCESS}"
|
|
;;
|
|
|
|
"${OCF_RUNNING_PROMOTED}")
|
|
ocf_exit_reason \
|
|
"Refusing start: PostgreSQL is already primary; promotion must occur through promote()"
|
|
return "${OCF_ERR_GENERIC}"
|
|
;;
|
|
|
|
"${OCF_NOT_RUNNING}")
|
|
# Expected for a stopped resource. Continue.
|
|
;;
|
|
|
|
*)
|
|
ocf_exit_reason \
|
|
"Cannot start unhealthy PostgreSQL instance (monitor rc=${monitor_rc})"
|
|
return "${monitor_rc}"
|
|
;;
|
|
esac
|
|
|
|
ocf_log info \
|
|
"Ensuring container service ${SERVICE_NAME} is running"
|
|
|
|
if ! service_running; then
|
|
if ! mc_lifecycle_start "${SERVICE_NAME}"
|
|
then
|
|
ocf_exit_reason \
|
|
"Failed to start ${SERVICE_NAME}"
|
|
return "${OCF_ERR_GENERIC}"
|
|
fi
|
|
fi
|
|
|
|
for i in $(seq 1 20); do
|
|
container_running && break
|
|
sleep 0.5
|
|
done
|
|
|
|
if ! container_running; then
|
|
ocf_exit_reason \
|
|
"Container ${CONTAINER_NAME} did not become running"
|
|
return "${OCF_ERR_GENERIC}"
|
|
fi
|
|
|
|
if ! pg_running; then
|
|
#
|
|
# OCF promotable-resource rule:
|
|
# start() must produce an Unpromoted instance.
|
|
#
|
|
# A PostgreSQL data directory without standby.signal would start
|
|
# writable/primary, so never start it from this action.
|
|
#
|
|
if ! pg_command '
|
|
test -f "$PGDATA/standby.signal"
|
|
'
|
|
then
|
|
ocf_exit_reason \
|
|
"Refusing start: PGDATA is not configured as a standby (standby.signal missing)"
|
|
return "${OCF_ERR_GENERIC}"
|
|
fi
|
|
|
|
ocf_log info \
|
|
"Starting PostgreSQL as an unpromoted standby inside ${CONTAINER_NAME}"
|
|
|
|
if ! pg_command '
|
|
BINDIR="$(pg_config --bindir)" || exit 1
|
|
exec "$BINDIR/pg_ctl" \
|
|
-D "$PGDATA" \
|
|
-l "$PGDATA/postgresql.log" \
|
|
start -w
|
|
'
|
|
then
|
|
ocf_exit_reason "pg_ctl start failed"
|
|
return "${OCF_ERR_GENERIC}"
|
|
fi
|
|
fi
|
|
|
|
managed_monitor
|
|
monitor_rc=$?
|
|
|
|
case "${monitor_rc}" in
|
|
"${OCF_SUCCESS}")
|
|
ocf_log info \
|
|
"PostgreSQL start completed in unpromoted state"
|
|
return "${OCF_SUCCESS}"
|
|
;;
|
|
|
|
"${OCF_RUNNING_PROMOTED}")
|
|
ocf_exit_reason \
|
|
"PostgreSQL became primary during start; stopping it to preserve OCF role semantics"
|
|
|
|
pg_command '
|
|
BINDIR="$(pg_config --bindir)" || exit 1
|
|
exec "$BINDIR/pg_ctl" \
|
|
-D "$PGDATA" \
|
|
stop -m fast -w
|
|
' >/dev/null 2>&1 || true
|
|
|
|
return "${OCF_ERR_GENERIC}"
|
|
;;
|
|
|
|
*)
|
|
return "${monitor_rc}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
managed_stop()
|
|
{
|
|
local i
|
|
|
|
if container_running && pg_running; then
|
|
ocf_log info \
|
|
"Stopping PostgreSQL cleanly inside ${CONTAINER_NAME}"
|
|
|
|
if ! pg_command '
|
|
BINDIR="$(pg_config --bindir)" || exit 1
|
|
exec "$BINDIR/pg_ctl" \
|
|
-D "$PGDATA" \
|
|
stop -m fast -w
|
|
'
|
|
then
|
|
ocf_exit_reason "pg_ctl stop failed"
|
|
return "${OCF_ERR_GENERIC}"
|
|
fi
|
|
fi
|
|
|
|
if service_running; then
|
|
ocf_log info \
|
|
"Stopping container service ${SERVICE_NAME}"
|
|
|
|
if ! mc_lifecycle_stop "${SERVICE_NAME}"
|
|
then
|
|
ocf_exit_reason \
|
|
"Failed to stop ${SERVICE_NAME}"
|
|
return "${OCF_ERR_GENERIC}"
|
|
fi
|
|
fi
|
|
|
|
for i in $(seq 1 20); do
|
|
if ! service_running && ! container_running; then
|
|
return "${OCF_SUCCESS}"
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
|
|
if service_running; then
|
|
ocf_exit_reason \
|
|
"${SERVICE_NAME} remained active after stop"
|
|
return "${OCF_ERR_GENERIC}"
|
|
fi
|
|
|
|
if container_running; then
|
|
ocf_exit_reason \
|
|
"${CONTAINER_NAME} remained running after stop"
|
|
return "${OCF_ERR_GENERIC}"
|
|
fi
|
|
|
|
return "${OCF_SUCCESS}"
|
|
}
|
|
|
|
# Conservative scoring eligibility only; this does not replace role detection,
|
|
# fencing, the promotion action, or the existing controlled-demote WAL checks.
|
|
# This strict predicate remains unchanged; disconnected policy is separate below.
|
|
mc_pg_promotion_eligible()
|
|
{
|
|
local eligible
|
|
|
|
eligible="$(
|
|
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 "
|
|
SELECT CASE WHEN pg_is_in_recovery() THEN
|
|
NOT pg_is_wal_replay_paused()
|
|
AND EXISTS (
|
|
SELECT 1 FROM pg_stat_wal_receiver
|
|
WHERE status = 'streaming'
|
|
AND flushed_lsn IS NOT NULL
|
|
AND pg_last_wal_replay_lsn() >= flushed_lsn
|
|
)
|
|
ELSE false END;
|
|
" 2>/dev/null
|
|
)" || return 1
|
|
[ "${eligible}" = t ]
|
|
}
|
|
|
|
# One observation supplies both strict connected eligibility and disconnected
|
|
# evidence. PostgreSQL restart time deliberately invalidates prior observations,
|
|
# including a new basebackup sharing the same PostgreSQL system identifier.
|
|
mc_pg_standby_observation()
|
|
{
|
|
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 "
|
|
WITH s AS MATERIALIZED (
|
|
SELECT pg_is_in_recovery() AS recovery,
|
|
pg_last_wal_replay_lsn() AS replay,
|
|
r.status AS receiver, r.flushed_lsn AS flush
|
|
FROM (SELECT 1) AS singleton
|
|
LEFT JOIN pg_stat_wal_receiver r ON true
|
|
)
|
|
SELECT json_build_object(
|
|
'identity', json_build_array(
|
|
(SELECT system_identifier::text FROM pg_control_system()),
|
|
current_setting('data_directory'),
|
|
extract(epoch FROM pg_postmaster_start_time())::text),
|
|
'recovery', recovery,
|
|
'paused', CASE WHEN recovery THEN pg_is_wal_replay_paused() ELSE true END,
|
|
'receiver', receiver, 'replay', replay::text, 'flush', flush::text,
|
|
'strict', CASE WHEN recovery THEN
|
|
NOT pg_is_wal_replay_paused() AND receiver = 'streaming'
|
|
AND flush IS NOT NULL AND replay >= flush
|
|
ELSE false END)
|
|
FROM s;
|
|
" 2>/dev/null
|
|
}
|
|
|
|
mc_pg_standby_score()
|
|
{
|
|
local observation
|
|
observation="$(mc_pg_standby_observation)" || return 1
|
|
# 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_APPLICATION_DIR}/promotion_state.py" \
|
|
"${MC_PROMOTION_RESOURCE_INSTANCE:-}" \
|
|
"${SERVICE_USER}" "${SERVICE_NAME}" "${CONTAINER_NAME}" \
|
|
"${OCF_RESKEY_disconnected_max_age:-300}"
|
|
}
|