diff --git a/pgsql/pgpro/README.md b/pgsql/pgpro/README.md index 096fb8c..77cb97b 100644 --- a/pgsql/pgpro/README.md +++ b/pgsql/pgpro/README.md @@ -12,7 +12,7 @@ | `pgpro-setup.sh` | Установка Postgres Pro **с выбором версии**, добавление и удаление экземпляров — по статье [wiki.dioservice.ru](https://wiki.dioservice.ru/ru/linux/postgreSQL/postgresql-linux). Циклическое меню (сам не выходит) и подкоманды `install` / `add-instance` / `delete-instance` / `list`. Установка: подготовка ОС (локали, TZ, консоль), репозиторий `repo.postgrespro.ru`, `apt-mark hold`, `pg-setup initdb --tune=1c`, контрольные суммы для PG<17, `pg_hba` scram-sha-256, пароль postgres, отключение IPv6. `list` — единая таблица экземпляров по всем версиям с реальным портом каждого кластера. При установке второй версии (порт 5432 занят) запрашивает свободный порт (`PG_PORT=NNNN`) и прописывает его в `postgresql.conf`. Запуск от root. | | `pg_add_cluster.sh` | Добавление нового кластера Postgres Pro на хост (initdb, регистрация, настройка). | | `maintenance.sh` | Регламентное обслуживание БД. | -| `wal-g-scripts/` | Обвязка [WAL-G](https://github.com/wal-g/wal-g): конфиг, полный/инкрементальный бэкап, список, restore, удаление. | +| `wal-g-manager.sh` | Единый менеджер [WAL-G](https://github.com/wal-g/wal-g): интерактивное меню (по образцу `pgpro-setup.sh`) + подкоманды `cfg`/`list`/`backup`/`backup-all`/`delete`/`restore`/`install`/`units`/`uninstall`. Конфиг архивации, полный/инкрементальный бэкап, список, restore (в т.ч. PITR), удаление по retain, установка себя + systemd-таймера. | | `backupscripts/` | Скрипты pg_probackup: бэкап, восстановление, первичная настройка. | | `systemd/` | Юниты `backup-pgsql.service` + `backup-pgsql.timer` для запуска бэкапа по расписанию. | | `legacy/` | Черновики бэкап-скриптов, **не проверены** (см. ниже). | diff --git a/pgsql/pgpro/systemd/backup-pgsql.service b/pgsql/pgpro/systemd/backup-pgsql.service index 33bcb6f..6cbd8ac 100644 --- a/pgsql/pgpro/systemd/backup-pgsql.service +++ b/pgsql/pgpro/systemd/backup-pgsql.service @@ -6,7 +6,7 @@ After=postgrespro-1c-18.service [Service] Type=oneshot # Запуск от root: скрипт сам переключается на postgres через su - postgres. -ExecStart=/var/lib/pgpro/_backup/scripts/wal-g-backup-all.sh +ExecStart=/var/lib/pgpro/_backup/scripts/wal-g-manager.sh backup-all Environment="PATH=/sbin:/bin:/usr/sbin:/usr/bin" [Install] diff --git a/pgsql/pgpro/wal-g-manager.sh b/pgsql/pgpro/wal-g-manager.sh new file mode 100755 index 0000000..2110aa4 --- /dev/null +++ b/pgsql/pgpro/wal-g-manager.sh @@ -0,0 +1,1768 @@ +#!/usr/bin/env bash +# ============================================================================== +# wal-g-manager.sh — единый менеджер резервного копирования PostgreSQL (WAL-G) +# ============================================================================== +# Объединяет весь бывший набор pgpro/wal-g-scripts/ (cfg/list/backup/backup-all/ +# delete/restore/install/menu) в один скрипт с интерактивным меню — по образцу +# pgpro-setup.sh / pg-server-manager.sh. +# +# Режимы: +# wal-g-manager.sh интерактивное меню (root) +# wal-g-manager.sh cfg настройка конфигов WAL-G + архивации PostgreSQL +# wal-g-manager.sh list [--full|-f] [PORT|all] список бэкапов +# wal-g-manager.sh backup интерактивный delta/FULL-бэкап выбранных кластеров +# wal-g-manager.sh backup-all неинтерактивный прогон (для systemd-таймера) + retain +# wal-g-manager.sh delete удаление старых бэкапов (retain FULL N) +# wal-g-manager.sh restore восстановление кластера из бэкапа (в т.ч. PITR) +# wal-g-manager.sh install установка скрипта и systemd-задания (таймер) +# wal-g-manager.sh units только (пере)установить systemd-юниты +# wal-g-manager.sh uninstall-units удалить systemd-юниты +# wal-g-manager.sh uninstall удалить юниты и установленную копию скрипта +# +# Запускать от root: большинство операций требует прав root и переключения на +# пользователя postgres (su -). Сокет PostgreSQL — PGHOST_SOCKET (по умолчанию +# /tmp, конвенция PostgresPro 1C). Конфиги WAL-G — $WALG_DIR/.walg-.json. +# ============================================================================== + +set -uo pipefail + +# ── Константы ──────────────────────────────────────────────────────────────── +WALG_DIR="/etc/wal-g.d" +WALG_BIN="${WALG_BIN:-/usr/bin/wal-g}" +PG_SUPERUSER="postgres" +PGHOST_SOCKET="/tmp" # каталог unix-сокета PostgreSQL +WALG_COMPRESSION_METHOD="brotli" + +DEFAULT_INSTALL_DIR="/var/lib/pgpro/_backup/scripts" +DEFAULT_ONCALENDAR="*-*-* 01:15:00" +SYSTEMD_DIR="/etc/systemd/system" +UNIT_SERVICE="backup-pgsql.service" +UNIT_TIMER="backup-pgsql.timer" +BIN_SYMLINK="/usr/local/bin/wal-g-manager" +SELF_NAME="wal-g-manager.sh" + +# ── Цвета (только в интерактивный TTY и при отсутствии NO_COLOR) ───────────── +if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then + RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' + CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' +else + RED='' GREEN='' YELLOW='' CYAN='' BOLD='' NC='' +fi + +log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } +log_section() { echo -e "\n${CYAN}${BOLD}>>> $* ${NC}"; } +separator() { echo "════════════════════════════════════════════════════════════"; } + +require_root() { + if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then + log_error "Требуются права root." + exit 1 + fi +} + +# ============================================================================== +# ОБЩИЕ ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ +# ============================================================================== + +prompt_default() { + local prompt="$1" default="${2-}" var + if [[ -n "${default}" ]]; then + read -r -p "${prompt} [${default}]: " var + echo "${var:-$default}" + else + read -r -p "${prompt}: " var + echo "${var}" + fi +} + +prompt_required() { + local prompt="$1" var="" + while [[ -z "$var" ]]; do + read -r -p "${prompt}: " var + [[ -z "$var" ]] && echo " Значение не может быть пустым." >&2 + done + echo "$var" +} + +# Единый y/N-вопрос: принимает y, Y, yes, YES (Enter/что угодно ещё → нет). +ask_yes_no() { + local prompt="$1" ans + read -r -p "${prompt} [y/N]: " ans || return 1 + [[ "$ans" =~ ^([yY]|[yY][eE][sS])$ ]] +} + +# Строгое подтверждение необратимого действия: нужно набрать ровно "YES". +confirm_YES() { + local prompt="$1" ans + read -r -p "${prompt} " ans || return 1 + [[ "$ans" == "YES" ]] +} + +# Экранирование строки для безопасной вставки в JSON-значение. +json_escape() { + local s="$1" + s="${s//\\/\\\\}" # обратный слэш — первым + s="${s//\"/\\\"}" # затем двойная кавычка + printf '%s' "$s" +} + +# Чтение значения ключа из JSON-конфига WAL-G (через python3). +get_cfg_value() { + local cfg="$1" key="$2" + command -v python3 >/dev/null 2>&1 || return 1 + python3 - "$cfg" "$key" <<'PY' +import json, sys +try: + with open(sys.argv[1], encoding="utf-8") as f: + val = json.load(f).get(sys.argv[2], "") + print(str(val).strip()) +except Exception: + sys.exit(1) +PY +} + +# Жив ли кластер на порту (по unix-сокету). +pg_alive() { + local port="$1" timeout="${2:-3}" + pg_isready -h "$PGHOST_SOCKET" -p "$port" -U "$PG_SUPERUSER" -t "$timeout" >/dev/null 2>&1 +} + +# SHOW с указанного кластера (пустая строка при ошибке). +pg_show() { + local port="$1" guc="$2" + psql -h "$PGHOST_SOCKET" -p "$port" -U "$PG_SUPERUSER" -d postgres -At -q -X \ + -c "SHOW ${guc};" 2>/dev/null +} + +# Имя systemd-юнита кластера: для 5432 — штатный сервис, иначе шаблонный @. +get_service_name() { + local pgver="$1" pgport="$2" + if [[ "$pgport" == "5432" ]]; then + echo "postgrespro-${pgver}.service" + else + echo "postgrespro-${pgver}@${pgport}.service" + fi +} + +# Обнаруженные версии PostgreSQL (по systemd-юнитам и каталогам /var/lib/pgpro). +detect_pgver() { + local -A seen=() + local v b d unit + + if command -v systemctl >/dev/null 2>&1; then + while read -r unit _; do + [[ "$unit" =~ ^postgrespro-(.+)\.service$ ]] || continue + v="${BASH_REMATCH[1]}" + v="${v%@}" # postgrespro-1c-18@.service → 1c-18 + [[ -n "$v" ]] && seen["$v"]=1 + done < <(systemctl list-unit-files 'postgrespro-*.service' --no-legend 2>/dev/null) + fi + + shopt -s nullglob + for d in /var/lib/pgpro/*/; do + b="$(basename "${d%/}")" + [[ "$b" == "_backup" ]] && continue # служебный каталог + seen["$b"]=1 + done + shopt -u nullglob + + (( ${#seen[@]} > 0 )) && printf '%s\n' "${!seen[@]}" | sort +} + +# Версия PostgreSQL по умолчанию для подсказок в prompt_default (первая найденная). +default_pgver() { + local first + first="$(detect_pgver | head -n1)" + echo "${first:-1c-18}" +} + +# Конфиги WAL-G: $WALG_DIR/.walg-.json, отсортированы по имени файла. +discover_walg_configs() { + local -a unsorted + shopt -s nullglob + unsorted=( "${WALG_DIR}"/.walg-*.json ) + shopt -u nullglob + mapfile -t DISCOVERED_CFGS < <(printf '%s\n' "${unsorted[@]}" | sort) +} + +cfg_port() { + local base; base="$(basename "$1")" + base="${base#.walg-}"; echo "${base%.json}" +} + +# Перезапуск сервиса кластера после ALTER SYSTEM с ожиданием готовности. +restart_pg_service() { + local pgver="$1" pgport="$2" svc + svc="$(get_service_name "$pgver" "$pgport")" + + echo + log_info "Сервис: ${svc}" + + if ! ask_yes_no "Перезапустить сервис ${svc}?"; then + log_info "Перезапуск отменён. Выполните вручную: systemctl restart ${svc}" + return 0 + fi + + log_info "Выполняется systemctl restart ${svc} ..." + if ! systemctl restart "$svc"; then + log_error "Не удалось перезапустить сервис ${svc}. Проверьте: systemctl status ${svc}" + return 1 + fi + + local retries=10 i=1 + log_info "Ожидание готовности кластера на порту ${pgport}..." + while (( i <= retries )); do + if pg_alive "$pgport" 2; then + log_info "Кластер ${pgport} запущен." + return 0 + fi + echo " попытка ${i}/${retries}..." + sleep 2 + i=$(( i + 1 )) + done + + log_warn "Кластер ${pgport} не ответил после ${retries} попыток. Проверьте: systemctl status ${svc}" + return 1 +} + +# ============================================================================== +# 1) НАСТРОЙКА (было wal-g-cfg.sh) +# ============================================================================== + +declare -A CLUSTER_PGDATA=() + +print_cluster_summary() { + local -a ports=("$@") + local port pgdata cfg_path cfg_info avail_status backend_info pgdata_info idx=1 + + echo + echo "=== Доступные кластеры ===" + echo + + for port in "${ports[@]}"; do + cfg_path="${WALG_DIR}/.walg-${port}.json" + + if pg_alive "$port"; then + avail_status="OK" + pgdata="$(pg_show "$port" data_directory)" || pgdata="" + [[ -n "$pgdata" ]] && CLUSTER_PGDATA["$port"]="$pgdata" + else + avail_status="!!" + pgdata="" + fi + + if [[ -f "$cfg_path" ]]; then + if grep -q '"WALG_S3_PREFIX"' "$cfg_path" 2>/dev/null; then + backend_info="s3" + elif grep -q '"WALG_FILE_PREFIX"' "$cfg_path" 2>/dev/null; then + backend_info="file" + else + backend_info="unknown" + fi + cfg_info="конфиг=есть (${backend_info})" + else + cfg_info="конфиг=отсутствует" + fi + + [[ -n "$pgdata" ]] && pgdata_info="PGDATA=${pgdata}" || pgdata_info="PGDATA недоступен" + + printf " %2d. [ %s ] Порт %-5s | %s, %s\n" \ + "$idx" "$avail_status" "$port" "$pgdata_info" "$cfg_info" + idx=$(( idx + 1 )) + done + + echo + printf " 0. Отмена\n" + echo +} + +select_ports_by_index() { + local -n _ports_ref="$1" + local total="${#_ports_ref[@]}" + local input sel port result=() + + while true; do + read -r -p "Введите номера через пробел или 'all' для всех [all]: " input + input="${input:-all}" + + if [[ "$input" == "0" ]]; then + SELECTED_PORTS=() + return 1 + fi + + if [[ "$input" == "all" ]]; then + SELECTED_PORTS=("${_ports_ref[@]}") + return 0 + fi + + result=() + local valid=1 + for sel in $input; do + if ! [[ "$sel" =~ ^[0-9]+$ ]]; then + echo "WARN: '${sel}' не является числом, повторите ввод." >&2 + valid=0; break + fi + if (( sel < 1 || sel > total )); then + echo "WARN: номер ${sel} вне диапазона 1–${total}, повторите ввод." >&2 + valid=0; break + fi + port="${_ports_ref[$(( sel - 1 ))]}" + result+=("$port") + done + + if (( valid )) && (( ${#result[@]} > 0 )); then + SELECTED_PORTS=("${result[@]}") + return 0 + fi + done +} + +configure_pg_instance() { + local pgport="$1" pgdata="$2" walgcfg="$3" + + local -a PSQL=( + psql -h "$PGHOST_SOCKET" -p "$pgport" -U "$PG_SUPERUSER" -d postgres + -At -q -X -v ON_ERROR_STOP=1 + ) + + local logdir + logdir="$("${PSQL[@]}" -c "SHOW log_directory;" 2>/dev/null)" || { + log_error "Не удалось получить log_directory для ${pgport}" + return 1 + } + + local pglogdir + if [[ "$logdir" = /* ]]; then + pglogdir="$logdir" + else + pglogdir="${pgdata%/}/${logdir}" + fi + + declare -A WANT=( + [wal_level]="replica" + [archive_mode]="on" + [archive_timeout]="60s" + [archive_command]="${WALG_BIN} wal-push \"%p\" --config ${walgcfg} >> ${pglogdir}/archive_command.log 2>&1" + [restore_command]="${WALG_BIN} wal-fetch \"%f\" \"%p\" --config ${walgcfg} >> ${pglogdir}/restore_command.log 2>&1" + ) + declare -A RESTART=( + [wal_level]=1 + [archive_mode]=1 + [archive_command]=0 + [archive_timeout]=0 + [restore_command]=0 + ) + + local pg_out + pg_out="$("${PSQL[@]}" -F '|' -c " + SELECT name, + CASE WHEN COALESCE(unit,'') = '' THEN setting + ELSE setting || unit + END AS setting + FROM pg_settings + WHERE name IN ( + 'wal_level','archive_mode','archive_command', + 'archive_timeout','restore_command' + ) + ORDER BY name; + " 2>&1)" || { + log_error "Не удалось получить параметры pg_settings для ${pgport}" + return 1 + } + + declare -A CUR=() + local k v + while IFS='|' read -r k v; do + [[ -n "$k" ]] && CUR["$k"]="$v" + done <<< "$pg_out" + + local -a PARAMS=(wal_level archive_mode archive_command archive_timeout restore_command) + local p + for p in "${PARAMS[@]}"; do + if [[ -z "${CUR[$p]+x}" ]]; then + log_error "Параметр '${p}' не получен из pg_settings" + return 1 + fi + done + + local -a TO_CHANGE=() + local restart_needed=0 + for p in "${PARAMS[@]}"; do + if [[ "${CUR[$p]}" != "${WANT[$p]}" ]]; then + TO_CHANGE+=("$p") + (( restart_needed |= RESTART[$p] )) || true + fi + done + + if (( ${#TO_CHANGE[@]} == 0 )); then + log_info "Параметры PostgreSQL ${pgport} уже соответствуют целевым." + PG_RESTART_NEEDED=0 + return 0 + fi + + echo + echo "=== Параметры PostgreSQL (${pgport}) ===" + for p in "${PARAMS[@]}"; do + local cur="${CUR[$p]}" want="${WANT[$p]}" + if [[ "$cur" != "$want" ]]; then + local extra="" + (( RESTART[$p] )) && extra=" (требуется перезапуск)" + printf " [CHG] %-20s\n текущий : %s\n целевой : %s%s\n" \ + "$p" "${cur:-<пусто>}" "$want" "$extra" + else + printf " [OK ] %-20s = %s\n" "$p" "$cur" + fi + done + echo + + if ! ask_yes_no "Применить ${#TO_CHANGE[@]} изменения для ${pgport}?"; then + log_info "Изменения ${pgport} отменены пользователем." + PG_RESTART_NEEDED=0 + return 0 + fi + + local sql="" + for p in "${TO_CHANGE[@]}"; do + local esc="${WANT[$p]//\'/\'\'}" + sql+="ALTER SYSTEM SET ${p} = '${esc}';"$'\n' + done + + if ! "${PSQL[@]}" -c "$sql" >/dev/null; then + log_error "Не удалось применить параметры PostgreSQL ${pgport}" + return 1 + fi + for p in "${TO_CHANGE[@]}"; do echo " SET ${p}"; done + + if ! "${PSQL[@]}" -c "SELECT pg_reload_conf();" >/dev/null 2>&1; then + log_warn "Не удалось выполнить pg_reload_conf() ${pgport}" + fi + + if (( restart_needed )); then + log_info "Параметры wal_level/archive_mode требуют перезапуска сервиса." + PG_RESTART_NEEDED=1 + else + log_info "Параметры обновлены без необходимости перезапуска." + PG_RESTART_NEEDED=0 + fi +} + +action_configure() { + require_root + # umask 077 закрывает окно world-readable для секретов между созданием файла + # и chmod 600 (снимается в конце функции, чтобы не влиять на остальное меню). + local old_umask; old_umask="$(umask)" + umask 077 + + CLUSTER_PGDATA=() + local -a FOUND_PORTS=() SELECTED_PORTS=() + + if command -v ss >/dev/null 2>&1; then + mapfile -t FOUND_PORTS < <(ss -tlnp 2>/dev/null | grep postgres | grep -oP ':\K\d+' | sort -un) + fi + + if (( ${#FOUND_PORTS[@]} > 0 )); then + print_cluster_summary "${FOUND_PORTS[@]}" + select_ports_by_index FOUND_PORTS || { log_info "Отменено."; umask "$old_umask"; return 0; } + else + log_warn "Кластеры не обнаружены автоматически (нужны права root для ss -p)." + local manual; manual="$(prompt_required 'Укажите порты через пробел (например: 5432 5433)')" + read -r -a SELECTED_PORTS <<< "$manual" + fi + + if (( ${#SELECTED_PORTS[@]} == 0 )); then + log_error "Список пуст — нечего настраивать" + umask "$old_umask"; return 1 + fi + + echo; echo "Выбраны порты: ${SELECTED_PORTS[*]}"; echo + + echo "Тип хранилища:" + echo " 1. S3" + echo " 2. Файловое" + echo " 0. Отмена" + echo + local BACKEND BACKEND_SEL + while true; do + read -r -p "Выбор [1]: " BACKEND_SEL + BACKEND_SEL="${BACKEND_SEL:-1}" + case "${BACKEND_SEL}" in + 1) BACKEND="s3"; break ;; + 2) BACKEND="file"; break ;; + 0) log_info "Отменено."; umask "$old_umask"; return 0 ;; + *) echo "WARN: введите 0, 1 или 2." >&2 ;; + esac + done + log_info "Тип хранилища = ${BACKEND}" + echo + + local PGVER WALG_DELTA_MAX_STEPS + PGVER="$(prompt_default 'Версия PostgreSQL (PGVER)' "$(default_pgver)")" + + WALG_DELTA_MAX_STEPS="$(prompt_default 'WALG_DELTA_MAX_STEPS' '6')" + if ! [[ "$WALG_DELTA_MAX_STEPS" =~ ^[0-9]+$ ]]; then + log_error "WALG_DELTA_MAX_STEPS должен быть целым числом" + umask "$old_umask"; return 1 + fi + + local J_AWS_ENDPOINT="" J_AWS_REGION="" J_AWS_ACCESS_KEY_ID="" J_AWS_SECRET_ACCESS_KEY="" J_S3_BUCKET="" + local J_WALG_FILE_PREFIX_BASE="" + if [[ "$BACKEND" == "s3" ]]; then + local AWS_ENDPOINT AWS_REGION AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY S3_BUCKET + AWS_ENDPOINT="$(prompt_default 'AWS_ENDPOINT' 's3.ru-3.storage.selcloud.ru:443')" + AWS_REGION="$(prompt_default 'AWS_REGION' 'ru-3')" + AWS_ACCESS_KEY_ID="$(prompt_required 'AWS_ACCESS_KEY_ID')" + AWS_SECRET_ACCESS_KEY="$(prompt_required 'AWS_SECRET_ACCESS_KEY')" + S3_BUCKET="$(prompt_required 'S3_BUCKET')" + + [[ "$S3_BUCKET" != s3://* ]] && S3_BUCKET="s3://${S3_BUCKET}" + S3_BUCKET="${S3_BUCKET%/}" + + J_AWS_ENDPOINT="$(json_escape "$AWS_ENDPOINT")" + J_AWS_REGION="$(json_escape "$AWS_REGION")" + J_AWS_ACCESS_KEY_ID="$(json_escape "$AWS_ACCESS_KEY_ID")" + J_AWS_SECRET_ACCESS_KEY="$(json_escape "$AWS_SECRET_ACCESS_KEY")" + J_S3_BUCKET="$(json_escape "$S3_BUCKET")" + else + local WALG_FILE_PREFIX_BASE + WALG_FILE_PREFIX_BASE="$(prompt_default 'Базовый каталог WALG_FILE_PREFIX' '/backup')" + WALG_FILE_PREFIX_BASE="${WALG_FILE_PREFIX_BASE%/}" + J_WALG_FILE_PREFIX_BASE="$(json_escape "$WALG_FILE_PREFIX_BASE")" + fi + + if ! install -d -m 755 "$WALG_DIR" 2>/dev/null; then + log_error "Нет прав на создание каталога ${WALG_DIR}" + umask "$old_umask"; return 1 + fi + + local PGPORT + for PGPORT in "${SELECTED_PORTS[@]}"; do + echo + echo "━━━ Порт ${PGPORT} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + if ! [[ "$PGPORT" =~ ^[0-9]+$ ]]; then + log_warn "Некорректный номер '${PGPORT}', пропуск" + continue + fi + + local SVC_NAME; SVC_NAME="$(get_service_name "$PGVER" "$PGPORT")" + log_info "Сервис systemd: ${SVC_NAME}" + + local CFG="${WALG_DIR}/.walg-${PGPORT}.json" + + if ! pg_alive "$PGPORT"; then + log_warn "Кластер на порту ${PGPORT} недоступен, пропуск" + continue + fi + + local PGDATA + if [[ -n "${CLUSTER_PGDATA[$PGPORT]+x}" ]]; then + PGDATA="${CLUSTER_PGDATA[$PGPORT]}" + else + PGDATA="$(pg_show "$PGPORT" data_directory)" || PGDATA="" + fi + + if [[ -z "$PGDATA" || ! -d "$PGDATA" ]]; then + log_warn "Не удалось определить PGDATA для порта ${PGPORT}, пропуск" + continue + fi + log_info "PGDATA = ${PGDATA}" + + local WRITE_CFG=1 + if [[ -f "$CFG" ]]; then + if ask_yes_no "Файл ${CFG} уже существует. Перезаписать?"; then + if ! cp -p -- "$CFG" "${CFG}.bak"; then + log_error "Не удалось создать резервную копию ${CFG}.bak, пропуск" + continue + fi + log_info "Создана резервная копия ${CFG}.bak" + else + log_info "Конфиг ${CFG} оставлен без изменений." + WRITE_CFG=0 + fi + fi + + if (( WRITE_CFG )); then + if [[ "$BACKEND" == "s3" ]]; then + cat > "$CFG" < "$CFG" </dev/null || log_warn "Не удалось установить владельца postgres для ${CFG}" + log_info "Конфиг записан: ${CFG}" + fi + + PG_RESTART_NEEDED=0 + if configure_pg_instance "$PGPORT" "$PGDATA" "$CFG"; then + if (( PG_RESTART_NEEDED )); then + restart_pg_service "$PGVER" "$PGPORT" \ + || log_warn "Перезапуск ${PGPORT} не выполнен, перехожу к следующему кластеру." + fi + else + log_warn "Настройка параметров PostgreSQL ${PGPORT} не завершена, пропуск." + fi + done + + umask "$old_umask" + echo; log_info "Настройка завершена." +} + +# ============================================================================== +# 2) СПИСОК БЭКАПОВ (было wal-g-list.sh / show_backups из wal-g-delete.sh) +# ============================================================================== + +# Постраничный вывод: пейджер только когда вывод не влезает на экран. +page_output() { + if [[ -t 1 ]]; then + if [[ -n "${PAGER:-}" ]]; then + eval "$PAGER" + elif command -v less >/dev/null 2>&1; then + less -RFX + else + cat + fi + else + cat + fi +} + +# Кол-во FULL-бэкапов из последнего run_backup_list (переиспользуется action_delete). +LAST_FULL_COUNT=0 + +# Вывод backup-list одного конфига. $2 (опционально) = "full" — только FULL. +run_backup_list() { + local cfg="$1" full_only="${2:-}" port + port="$(cfg_port "$cfg")" + + echo + echo -e "${BOLD}=== WAL-G backup-list · Порт ${port} ===${NC}" + echo + + if [[ ! -r "$cfg" ]]; then + echo -e "${RED}ERROR: конфиг недоступен для чтения: ${cfg}${NC}" >&2 + return 1 + fi + + local out rc + out="$("$WALG_BIN" backup-list --pretty --config "$cfg" 2>&1)" + rc=$? + if (( rc != 0 )); then + echo -e "${RED}ERROR: backup-list завершился с кодом ${rc}${NC}" >&2 + [[ -n "$out" ]] && echo "$out" >&2 + return 1 + fi + + local total delta full + total="$(printf '%s\n' "$out" | grep -c 'base_' || true)" + delta="$(printf '%s\n' "$out" | grep -c '_D_' || true)" + full=$(( total - delta )) + LAST_FULL_COUNT=$full + + if (( total == 0 )); then + echo " (бэкапы не найдены)" + echo + return 0 + fi + + if [[ "$full_only" == "full" ]]; then + if (( full == 0 )); then + echo " (FULL-бэкапов нет; всего delta: ${delta})" + else + printf '%s\n' "$out" | grep -v '_D_' || true + fi + else + # Таблица: FULL — зелёным, delta (_D_) — жёлтым, заголовок без цвета. + printf '%s\n' "$out" | awk -v g="${GREEN}" -v y="${YELLOW}" -v r="${NC}" ' + NR==1 { print " " $0; next } + /_D_/ { print " " y $0 r; next } + { print " " g $0 r } + ' + fi + + local latest_line latest_name latest_date summary + latest_line="$(printf '%s\n' "$out" | grep 'base_' | tail -n1 || true)" + latest_name="$(printf '%s\n' "$latest_line" | grep -oE 'base_[0-9A-Za-z_]+' | head -n1 || true)" + latest_date="$(printf '%s\n' "$latest_line" \ + | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}(:[0-9]{2})?' | head -n1 || true)" + + summary="FULL: ${full} · delta: ${delta} · всего: ${total}" + [[ -n "$latest_name" ]] && summary+=" · последний: ${latest_name}" + [[ -n "$latest_date" ]] && summary+=" (${latest_date})" + + echo + echo -e "${CYAN} Сводка: ${summary}${NC}" + return 0 +} + +run_selected_lists() { + local full_only="$1"; shift + local errors=0 cfg tmpfile + tmpfile="$(mktemp "${TMPDIR:-/tmp}/walg-list.XXXXXX")" || { + log_error "Не удалось создать временный файл" + return 1 + } + + for cfg in "$@"; do + run_backup_list "$cfg" "$full_only" >>"$tmpfile" || (( errors++ )) || true + done + + page_output <"$tmpfile" + rm -f "$tmpfile" + + if (( errors > 0 )); then + log_warn "${errors} конфиг(ов) завершились с ошибкой" + return 5 + fi + return 0 +} + +action_list() { + if [[ ! -x "$WALG_BIN" ]]; then + log_error "wal-g не найден или не исполняемый: $WALG_BIN" + return 1 + fi + if [[ ! -d "$WALG_DIR" ]]; then + log_error "Каталог не найден: $WALG_DIR" + return 1 + fi + + local -a CFGS + discover_walg_configs; CFGS=("${DISCOVERED_CFGS[@]}") + if (( ${#CFGS[@]} == 0 )); then + log_error "Конфиги не найдены в ${WALG_DIR} (ожидались .walg-*.json)" + return 2 + fi + + local full_only="" input="" + while [[ $# -gt 0 ]]; do + case "$1" in + --full|-f) full_only="full"; shift ;; + --help|-h) + echo "Использование: $SELF_NAME list [--full|-f] [PORT|all]" + return 0 + ;; + -*) log_error "Неизвестная опция: $1"; return 1 ;; + *) input="$1"; shift ;; + esac + done + + if [[ -n "$input" ]]; then + if [[ "${input,,}" == "all" ]]; then + run_selected_lists "$full_only" "${CFGS[@]}"; return $? + fi + if [[ ! "$input" =~ ^[1-9][0-9]{0,4}$ ]] || (( input > 65535 )); then + log_error "Некорректный порт: '$input' (ожидается 1–65535)" + return 3 + fi + local wcf="${WALG_DIR}/.walg-${input}.json" + if [[ ! -f "$wcf" ]]; then + log_error "Конфиг для порта $input не найден: $wcf" + return 4 + fi + run_selected_lists "$full_only" "$wcf"; return $? + fi + + if [[ ! -t 0 ]]; then + log_error "Нужен TTY либо аргумент. Использование: $SELF_NAME list [--full|-f] [PORT|all]" + return 1 + fi + + echo + echo -e "${BOLD}=== Доступные кластеры ===${NC}" + echo + local i=1 cfg port + for cfg in "${CFGS[@]}"; do + port="$(cfg_port "$cfg")" + if [[ -r "$cfg" ]]; then + echo -e " ${BOLD}${i}.${NC} [ ${GREEN}OK${NC} ] Порт ${port}" + else + echo -e " ${BOLD}${i}.${NC} [ ${RED}FAIL${NC} ] Порт ${port} — конфиг недоступен" + fi + i=$(( i + 1 )) + done + local total=${#CFGS[@]} + local all_idx=$(( total + 1 )) + echo + echo -e " ${BOLD}${all_idx}.${NC} Все кластеры" + echo -e " ${BOLD}0.${NC} Отмена" + echo + + local -a selected=() choice + while true; do + read -r -p "Выберите номер [0–${all_idx}]: " choice + [[ "$choice" == "0" ]] && { log_info "Отменено."; return 0; } + if [[ ! "$choice" =~ ^[0-9]+$ ]]; then + echo -e "${RED}Некорректный ввод.${NC}" >&2; continue + fi + if (( choice == all_idx )); then selected=("${CFGS[@]}"); break; fi + if (( choice >= 1 && choice <= total )); then selected=("${CFGS[choice-1]}"); break; fi + echo -e "${RED}Номер вне диапазона.${NC}" >&2 + done + + if [[ -z "$full_only" ]]; then + echo + if ask_yes_no "Показывать только FULL-бэкапы?"; then full_only="full"; fi + fi + + run_selected_lists "$full_only" "${selected[@]}" +} + +# ============================================================================== +# 3) ИНТЕРАКТИВНЫЙ БЭКАП (было wal-g-backup.sh) +# ============================================================================== + +action_backup() { + require_root + + local PGVER; PGVER="$(prompt_default 'Версия PostgreSQL (PGVER)' "$(default_pgver)")" + + local -a FOUND_PORTS=() VALID_PORTS=() + declare -A PORT_DATA=() + + if command -v ss >/dev/null 2>&1; then + mapfile -t FOUND_PORTS < <(ss -tlnp 2>/dev/null | grep -i postgres | grep -oP ':\K\d+' | sort -u) + fi + + check_backup_port() { + local port="$1" pgdata + local cfg="${WALG_DIR}/.walg-${port}.json" + if [[ ! -f "$cfg" ]]; then + echo " [SKIP] Порт ${port}: конфиг ${cfg} не найден"; return 1 + fi + if ! pg_alive "$port"; then + echo " [SKIP] Порт ${port}: кластер недоступен"; return 1 + fi + pgdata="$(pg_show "$port" data_directory)" + if [[ -z "$pgdata" || ! -d "$pgdata" ]]; then + echo " [SKIP] Порт ${port}: не удалось определить PGDATA"; return 1 + fi + PORT_DATA["$port"]="$pgdata" + return 0 + } + + scan_backup_ports() { + local p + for p in "$@"; do check_backup_port "$p" && VALID_PORTS+=("$p"); done + } + + echo; echo "=== Доступные кластеры ===" + if (( ${#FOUND_PORTS[@]} > 0 )); then + scan_backup_ports "${FOUND_PORTS[@]}" + else + echo " Автообнаружение не дало результатов." + fi + + if (( ${#VALID_PORTS[@]} == 0 )); then + echo + log_warn "Не найдено ни одного кластера с конфигом WAL-G." + local manual; manual="$(prompt_required 'Укажите порты вручную через пробел (например: 5432 5433)')" + local -a manual_ports=(); read -r -a manual_ports <<< "$manual" + scan_backup_ports "${manual_ports[@]}" + if (( ${#VALID_PORTS[@]} == 0 )); then + log_error "Нет доступных кластеров с валидной конфигурацией — выход." + return 1 + fi + fi + + echo + local i p cfg + for i in "${!VALID_PORTS[@]}"; do + p="${VALID_PORTS[$i]}"; cfg="${WALG_DIR}/.walg-${p}.json" + echo " $((i+1)). [ OK ] Порт ${p} | PGDATA=${PORT_DATA[$p]}, конфиг=${cfg}" + done + echo " 0. Отмена" + echo + + local sel; sel="$(prompt_default "Укажите номера через пробел, 'all' для всех или '0' для отмены" 'all')" + if [[ "$sel" == "0" || "${sel,,}" == "q" || "${sel,,}" == "exit" ]]; then + log_info "Отменено."; return 0 + fi + + local -a SELECTED_PORTS=() + if [[ "${sel,,}" == "all" ]]; then + SELECTED_PORTS=("${VALID_PORTS[@]}") + else + local -a nums=(); read -r -a nums <<< "$sel" + local num idx + for num in "${nums[@]}"; do + if ! [[ "$num" =~ ^[0-9]+$ ]]; then echo " [WARN] '${num}' — не число, пропускаем." >&2; continue; fi + idx=$(( num - 1 )) + if (( idx < 0 || idx >= ${#VALID_PORTS[@]} )); then echo " [WARN] Номер ${num} вне диапазона — пропускаем." >&2; continue; fi + SELECTED_PORTS+=("${VALID_PORTS[$idx]}") + done + fi + + if (( ${#SELECTED_PORTS[@]} == 0 )); then + log_error "Нет кластера для бэкапа — выход." + return 1 + fi + + echo + local force_full=false + ask_yes_no 'Запустить FULL-бэкап для всех выбранных кластеров?' && force_full=true + if [[ "$force_full" == true ]]; then + echo " Режим: FULL-бэкап." + else + echo " Режим: Delta (при отсутствии базового — автоматически FULL)." + fi + + echo; echo "=== Запуск бэкапов ===" + local errors=0 PGPORT PGDATA wcf + for PGPORT in "${SELECTED_PORTS[@]}"; do + PGDATA="${PORT_DATA[$PGPORT]}" + wcf="${WALG_DIR}/.walg-${PGPORT}.json" + + echo + echo ">>> Бэкап: порт=${PGPORT} PGDATA=${PGDATA}" + echo " Конфиг: ${wcf}" + + if [[ "$force_full" == true ]]; then + echo " Запуск FULL-бэкапа..." + if su - "$PG_SUPERUSER" -c "'${WALG_BIN}' backup-push --full --config '${wcf}' '${PGDATA}'"; then + echo " [OK] FULL бэкап ${PGPORT} завершён успешно." + else + echo " [FAIL] FULL бэкап ${PGPORT} завершился с ошибкой!" >&2 + (( errors++ )) || true + fi + else + echo " Попытка delta-бэкапа..." + if ! su - "$PG_SUPERUSER" -c "'${WALG_BIN}' backup-push --config '${wcf}' '${PGDATA}'"; then + echo " Delta не удалась — пробуем FULL бэкап..." >&2 + if su - "$PG_SUPERUSER" -c "'${WALG_BIN}' backup-push --full --config '${wcf}' '${PGDATA}'"; then + echo " [OK] FULL бэкап ${PGPORT} завершён успешно." + else + echo " [FAIL] FULL бэкап ${PGPORT} завершился с ошибкой!" >&2 + (( errors++ )) || true + fi + else + echo " [OK] Delta бэкап ${PGPORT} завершён успешно." + fi + fi + done + + echo; echo "=== Итог ===" + echo " Всего бэкапов: ${#SELECTED_PORTS[@]}" + echo " Ошибок: ${errors}" + (( errors == 0 )) +} + +# ============================================================================== +# 4) НЕИНТЕРАКТИВНЫЙ ПРОГОН ДЛЯ SYSTEMD (было wal-g-backup-all.sh) +# ============================================================================== + +backup_cluster_noninteractive() { + local cfg="$1" port="$2" pgdata="$3" retain="${RETAIN:-3}" + local logdir="${pgdata}/log" + local logfile="${logdir}/wal-g-backup-${port}-$(date +%F).log" + + mkdir -p "${logdir}" && chown "${PG_SUPERUSER}:${PG_SUPERUSER}" "${logdir}" || { + log_error "Не удалось создать каталог логов: ${logdir}" + return 1 + } + + log_info "Запуск резервного копирования: port=${port}, cfg=${cfg}" + log_info "Лог: ${logfile}" + + # Все значения передаём через явные переменные в heredoc, + # чтобы не зависеть от окружения login shell. + su - "${PG_SUPERUSER}" -s /bin/bash <> "\${LOGFILE}" 2>&1 + +if (( backup_rc != 0 || delete_rc != 0 )); then + exit 1 +fi +exit 0 +SHELL +} + +action_backup_all() { + require_root + + local fail=0 + [[ -d "${WALG_DIR}" ]] || { log_error "Каталог конфигов не найден: ${WALG_DIR}"; fail=1; } + [[ -x "${WALG_BIN}" ]] || { log_error "WAL-G бинарник не найден / не исполняемый: ${WALG_BIN}"; fail=1; } + command -v python3 &>/dev/null || { log_error "python3 не найден в PATH"; fail=1; } + (( fail == 0 )) || return 1 + + local -a confs + discover_walg_configs; confs=("${DISCOVERED_CFGS[@]}") + if (( ${#confs[@]} == 0 )); then + log_error "Конфиги не найдены: ${WALG_DIR}/.walg-*.json" + return 1 + fi + log_info "Найдено конфигов: ${#confs[@]}" + + local errors=0 cfg port pgdata + for cfg in "${confs[@]}"; do + port="$(get_cfg_value "${cfg}" PGPORT)" || { + log_error "Ошибка парсинга порта: ${cfg} — пропуск"; (( errors++ )) || true; continue + } + if [[ ! "${port}" =~ ^[0-9]+$ ]]; then + log_error "PGPORT не задан или некорректен: ${cfg} — пропуск"; (( errors++ )) || true; continue + fi + + pgdata="$(get_cfg_value "${cfg}" PGDATA)" || { + log_error "Ошибка парсинга PGDATA: ${cfg} — пропуск"; (( errors++ )) || true; continue + } + if [[ -z "${pgdata}" ]]; then + log_error "PGDATA не задан в конфиге: ${cfg} — пропуск"; (( errors++ )) || true; continue + fi + if [[ ! -d "${pgdata}" ]]; then + log_error "PGDATA не существует: '${pgdata}' (cfg=${cfg}) — пропуск"; (( errors++ )) || true; continue + fi + + backup_cluster_noninteractive "${cfg}" "${port}" "${pgdata}" || { + log_error "Ошибка резервного копирования: port=${port}, cfg=${cfg}"; (( errors++ )) || true + } + done + + if (( errors > 0 )); then + log_error "Завершено с ошибками: ${errors} кластер(ов)" + return 1 + fi + log_info "Все резервные копии успешно созданы" + return 0 +} + +# ============================================================================== +# 5) УДАЛЕНИЕ СТАРЫХ БЭКАПОВ (было wal-g-delete.sh) +# ============================================================================== + +action_delete() { + require_root + + local fail=0 + [[ -d "${WALG_DIR}" ]] || { log_error "Каталог конфигов не найден: ${WALG_DIR}"; fail=1; } + [[ -x "${WALG_BIN}" ]] || { log_error "WAL-G не найден / не исполняемый: ${WALG_BIN}"; fail=1; } + command -v python3 &>/dev/null || { log_error "python3 не найден в PATH"; fail=1; } + (( fail == 0 )) || return 1 + + local -a cfg_files + discover_walg_configs; cfg_files=("${DISCOVERED_CFGS[@]}") + if (( ${#cfg_files[@]} == 0 )); then + log_error "Конфиги не найдены: ${WALG_DIR}/.walg-*.json" + return 1 + fi + + declare -A port_to_cfg + local cfg port + for cfg in "${cfg_files[@]}"; do + port="$(get_cfg_value "${cfg}" PGPORT 2>/dev/null)" || continue + [[ "${port}" =~ ^[0-9]+$ ]] || continue + port_to_cfg["${port}"]="${cfg}" + done + if (( ${#port_to_cfg[@]} == 0 )); then + log_error "Не найдено ни одного корректного конфига с PGPORT" + return 1 + fi + + echo; separator + echo " Доступные кластеры PostgreSQL (WAL-G конфиги)" + separator + + local -a sorted_ports=() + mapfile -t sorted_ports < <(printf '%s\n' "${!port_to_cfg[@]}" | sort -n) + + local i=1 + declare -A idx_to_port + for port in "${sorted_ports[@]}"; do + printf " [%d] порт %-6s → %s\n" "$i" "${port}" "${port_to_cfg[${port}]}" + idx_to_port[$i]="${port}" + (( i++ )) + done + separator; echo + + local choice + while true; do + read -rp "Введите номер кластера (1-$(( i-1 ))), 0 — отмена: " choice + [[ "$choice" == "0" ]] && { log_info "Отменено."; return 0; } + [[ "${choice}" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= i-1 )) && break + echo " ✗ Некорректный выбор, попробуйте снова." + done + + local selected_port="${idx_to_port[${choice}]}" + local selected_cfg="${port_to_cfg[${selected_port}]}" + log_info "Выбран кластер: порт=${selected_port}, конфиг=${selected_cfg}" + + run_backup_list "${selected_cfg}" || return 1 + + if (( LAST_FULL_COUNT == 0 )); then + log_info "FULL-бэкапов нет — удалять нечего." + return 0 + fi + + local retain + while true; do + read -rp "Сколько FULL-бэкапов оставить? [введите число]: " retain + [[ "${retain}" =~ ^[1-9][0-9]*$ ]] && break + echo " ✗ Введите целое число больше 0." + done + + if (( retain >= LAST_FULL_COUNT )); then + echo + echo " ⚠ FULL-бэкапов всего ${LAST_FULL_COUNT}; при retain=${retain} удалять нечего." + fi + + echo; separator + echo " Параметры удаления:" + echo " Кластер : порт ${selected_port}" + echo " Конфиг : ${selected_cfg}" + echo " Оставить FULL : ${retain}" + echo " Команда : wal-g delete retain FULL ${retain} --confirm" + separator; echo + + confirm_YES "Для подтверждения введите YES (заглавными буквами):" || { + echo; log_info "Операция отменена пользователем."; return 0 + } + + echo; log_info "Запуск удаления: port=${selected_port}, retain FULL ${retain}"; echo + + "${WALG_BIN}" delete retain FULL "${retain}" --confirm --config "${selected_cfg}" + local rc=$? + echo + if (( rc == 0 )); then + log_info "Удаление завершено успешно (port=${selected_port})" + else + log_error "Удаление завершилось с ошибкой rc=${rc} (port=${selected_port})" + return "${rc}" + fi +} + +# ============================================================================== +# 6) ВОССТАНОВЛЕНИЕ (было wal-g-restore.sh) +# ============================================================================== + +action_restore() { + require_root + + local -a config_files + discover_walg_configs; config_files=("${DISCOVERED_CFGS[@]}") + if (( ${#config_files[@]} == 0 )); then + log_error "Конфиги не найдены в $WALG_DIR" + return 1 + fi + + local -a ports=() + local cfg + for cfg in "${config_files[@]}"; do ports+=("$(cfg_port "$cfg")"); done + + echo + echo -e "${BOLD}Доступные порты PostgreSQL:${NC}" + local i + for i in "${!ports[@]}"; do + echo -e " ${CYAN}[$((i+1))]${NC} Порт: ${BOLD}${ports[$i]}${NC} → ${config_files[$i]}" + done + echo -e " ${CYAN}[0]${NC} Отмена" + echo + + local choice pgport walg_cfg + while true; do + read -rp "Выберите номер (0-${#ports[@]}): " choice + [[ "$choice" == "0" ]] && { log_info "Отменено."; return 0; } + if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#ports[@]} )); then + pgport="${ports[$((choice-1))]}" + walg_cfg="${config_files[$((choice-1))]}" + break + fi + log_warn "Некорректный ввод, попробуйте снова." + done + + log_info "Выбран порт: ${BOLD}$pgport${NC}" + log_info "Конфиг: ${BOLD}$walg_cfg${NC}" + + log_section "Определение PGDATA" + local restore_pgdata pgver pgver_default + restore_pgdata="$(get_cfg_value "$walg_cfg" PGDATA 2>/dev/null || true)" + pgver="$(default_pgver)" + if [[ -n "$restore_pgdata" ]]; then + log_info "PGDATA из конфига: ${BOLD}$restore_pgdata${NC}" + else + restore_pgdata="/var/lib/pgpro/${pgver}/data-${pgport}" + log_warn "PGDATA из конфига не получен; путь по умолчанию: ${BOLD}$restore_pgdata${NC}" + fi + + if ! ask_yes_no "Использовать путь ${restore_pgdata}?"; then + local custom_data; custom_data="$(prompt_required 'Введите путь PGDATA')" + restore_pgdata="$custom_data" + log_info "Используется путь: ${BOLD}$restore_pgdata${NC}" + fi + + # Версия PostgreSQL нужна для имени systemd-юнита. Неверная версия → проверка + # is-active бьёт по чужому юниту и может не заметить ЗАПУЩЕННЫЙ кластер, что + # приведёт к перезаписи живого PGDATA. Пытаемся вывести версию из пути, + # иначе берём умолчание, и подтверждаем у оператора. + local pgver_default="$pgver" + [[ "$restore_pgdata" =~ /pgpro/([^/]+)/ ]] && pgver_default="${BASH_REMATCH[1]}" + pgver="$(prompt_default 'Версия PostgreSQL (PGVER)' "$pgver_default")" + log_info "PGVER: ${BOLD}$pgver${NC} → юнит: ${BOLD}$(get_service_name "$pgver" "$pgport")${NC}" + + log_section "Точка восстановления" + echo -e "${BOLD}Доступные бэкапы:${NC}" + su - "$PG_SUPERUSER" -c "'${WALG_BIN}' backup-list --config '$walg_cfg'" 2>/dev/null \ + || log_warn "Не удалось получить список бэкапов." + echo + + echo -e "${BOLD}Варианты восстановления:${NC}" + echo -e " ${CYAN}[1]${NC} LATEST (последний бэкап)" + echo -e " ${CYAN}[2]${NC} Указать имя бэкапа вручную" + echo -e " ${CYAN}[3]${NC} Восстановление до определённого времени (PITR)" + echo + + local restore_choice backup_name="" pitr_target="" + while true; do + read -rp "Выберите вариант [1-3]: " restore_choice + case "$restore_choice" in + 1) backup_name="LATEST"; break ;; + 2) + backup_name="$(prompt_required 'Введите имя бэкапа')" + break + ;; + 3) + pitr_target="$(prompt_required 'Введите дату/время (формат: YYYY-MM-DD HH:MM:SS [TZ])')" + # Базовый бэкап должен быть СТАРШЕ целевого времени, иначе PostgreSQL + # не сможет докатиться до точки восстановления. + log_warn "Базовый бэкап должен быть сделан РАНЬШЕ целевого времени восстановления." + backup_name="$(prompt_default 'Имя базового бэкапа' 'LATEST')" + break + ;; + *) log_warn "Некорректный ввод." ;; + esac + done + + local service_name; service_name="$(get_service_name "$pgver" "$pgport")" + + log_section "Итоговые параметры" + echo -e " Порт: ${BOLD}$pgport${NC}" + echo -e " PGDATA: ${BOLD}$restore_pgdata${NC}" + echo -e " Сервис: ${BOLD}$service_name${NC}" + echo -e " Конфиг WAL-G: ${BOLD}$walg_cfg${NC}" + echo -e " Бэкап: ${BOLD}$backup_name${NC}" + [[ -n "$pitr_target" ]] && echo -e " PITR до: ${BOLD}$pitr_target${NC}" + echo + log_warn "Будет остановлен сервис (если запущен) и ЗАМЕНЕНЫ все данные в $restore_pgdata!" + echo + + confirm_YES "Введите YES для подтверждения:" || { log_warn "Отменено пользователем."; return 0; } + + log_section "Подготовка" + if systemctl is-active --quiet "$service_name" 2>/dev/null; then + log_warn "Сервис ${BOLD}$service_name${NC} запущен!" + if ask_yes_no "Остановить сервис перед восстановлением?"; then + log_info "Останавливаю $service_name..." + systemctl stop "$service_name" + log_info "Сервис остановлен." + else + log_error "Восстановление невозможно при запущенном сервисе." + return 1 + fi + else + log_info "Сервис $service_name не запущен. ОК" + fi + + if [[ -d "$restore_pgdata" ]]; then + log_warn "Директория ${BOLD}$restore_pgdata${NC} существует и будет ПЕРЕЗАПИСАНА!" + local backup_old="${restore_pgdata}.bak.$(date +%Y%m%d_%H%M%S)" + if ask_yes_no "Создать резервную копию текущего PGDATA?"; then + log_info "Переименовываю в $backup_old ..." + mv "$restore_pgdata" "$backup_old" + log_info "Готово: $backup_old" + else + # Удаление без резервной копии — единственный безвозвратный шаг: + # требуем отдельное явное подтверждение, иначе откатываемся к mv. + log_warn "БЕЗВОЗВРАТНОЕ удаление ${BOLD}$restore_pgdata${NC} без резервной копии!" + if confirm_YES "Введите YES для удаления без резервной копии:"; then + log_warn "Удаляю $restore_pgdata ..." + rm -rf "$restore_pgdata" + else + log_info "Не подтверждено — сохраняю текущий PGDATA в $backup_old." + mv "$restore_pgdata" "$backup_old" + log_info "Готово: $backup_old" + fi + fi + fi + + mkdir -p "$restore_pgdata" + chown postgres:postgres "$restore_pgdata" + chmod 700 "$restore_pgdata" + log_info "Директория подготовлена: $restore_pgdata" + + local log_dir="${restore_pgdata}/log" + mkdir -p "$log_dir" + chown postgres:postgres "$log_dir" + local log_file="${log_dir}/wal-g-restore-${pgport}-$(date +%Y-%m-%d_%H-%M-%S).log" + + log_section "Запуск WAL-G backup-fetch" + log_info "Лог: $log_file" + + su - "$PG_SUPERUSER" -c "'${WALG_BIN}' backup-fetch --config '$walg_cfg' '$restore_pgdata' '$backup_name'" \ + 2>&1 | tee "$log_file" + + log_info "backup-fetch завершён." + + log_section "Настройка recovery" + su - "$PG_SUPERUSER" -c "touch '$restore_pgdata/recovery.signal'" + log_info "Создан recovery.signal" + + # restore_command нужен ВСЕГДА: без него PostgreSQL не сможет дотянуть WAL из + # архива и не дойдёт до согласованного состояния (а PITR не сработает вовсе). + local recovery_conf="$restore_pgdata/postgresql.auto.conf" + local restore_cmd="${WALG_BIN} wal-fetch \"%f\" \"%p\" --config ${walg_cfg} >> ${log_dir}/restore_command.log 2>&1" + + log_info "Прописываю restore_command в $recovery_conf" + cat >> "$recovery_conf" <> "$recovery_conf" <= 1 )) && def="${cands[0]}" + + if (( ${#cands[@]} > 1 )); then + { + echo "Обнаружено несколько версий PostgreSQL:" + local i=1 c + for c in "${cands[@]}"; do echo " $i) $c"; ((i++)); done + } >&2 + if [[ -t 0 ]]; then + local sel + read -rp "Выберите номер версии [1]: " sel || sel="" + sel="${sel:-1}" + if [[ "$sel" =~ ^[0-9]+$ ]] && (( sel >= 1 && sel <= ${#cands[@]} )); then + def="${cands[$((sel-1))]}" + fi + fi + elif (( ${#cands[@]} == 1 )); then + echo "INFO: обнаружена версия PostgreSQL: ${def}" >&2 + else + echo "WARN: версия PostgreSQL не обнаружена автоматически." >&2 + fi + + def="${PGVER:-${def:-1c-18}}" + + if [[ -t 0 ]]; then + local ans + read -rp "Версия PostgreSQL (PGVER) [${def}]: " ans || ans="" + def="${ans:-$def}" + fi + + printf '%s\n' "$def" +} + +current_install_dir() { + local f="${SYSTEMD_DIR}/${UNIT_SERVICE}" line + [[ -f "$f" ]] || return 1 + line="$(grep -m1 '^ExecStart=' "$f" 2>/dev/null)" || return 1 + line="${line#ExecStart=}" + [[ -n "$line" ]] || return 1 + dirname "${line%% backup-all}" +} + +current_oncalendar() { + local f="${SYSTEMD_DIR}/${UNIT_TIMER}" line + [[ -f "$f" ]] || return 1 + line="$(grep -m1 '^OnCalendar=' "$f" 2>/dev/null)" || return 1 + echo "${line#OnCalendar=}" +} + +install_self() { + install -d -m 755 "$INSTALL_DIR" || { log_error "Не удалось создать ${INSTALL_DIR}"; return 1; } + + local self_path; self_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/$(basename "${BASH_SOURCE[0]}")" + local dst="${INSTALL_DIR}/${SELF_NAME}" + + if [[ "$(cd "$(dirname "$self_path")" && pwd -P)" == "$(cd "$INSTALL_DIR" && pwd -P)" ]]; then + log_warn "Каталог установки совпадает с каталогом исходников — копирование пропущено." + return 0 + fi + + install -m 755 "$self_path" "$dst" || { log_error "Не удалось скопировать скрипт в ${dst}"; return 1; } + log_info "Скрипт установлен: ${dst}" +} + +install_units() { + local pgver="$1" + : "${INSTALL_DIR:=$DEFAULT_INSTALL_DIR}" + : "${ONCALENDAR:=$DEFAULT_ONCALENDAR}" + + cat > "${SYSTEMD_DIR}/${UNIT_SERVICE}" < "${SYSTEMD_DIR}/${UNIT_TIMER}" </dev/null 2>&1; then + systemctl daemon-reload || log_warn "systemctl daemon-reload завершился с ошибкой." + else + log_warn "systemctl недоступен — пропускаю daemon-reload." + fi +} + +uninstall_units() { + if command -v systemctl >/dev/null 2>&1; then + systemctl disable --now "${UNIT_TIMER}" 2>/dev/null || true + systemctl stop "${UNIT_SERVICE}" 2>/dev/null || true + fi + rm -f "${SYSTEMD_DIR}/${UNIT_TIMER}" "${SYSTEMD_DIR}/${UNIT_SERVICE}" + log_info "Юниты удалены." + command -v systemctl >/dev/null 2>&1 && { systemctl daemon-reload || true; } +} + +action_install() { + require_root + log_section "Установка WAL-G-менеджера" + + local pgver; pgver="$(resolve_pgver)" + + INSTALL_DIR="$(prompt_default 'Каталог установки скрипта' "$DEFAULT_INSTALL_DIR")" + ONCALENDAR="$(prompt_default 'Расписание бэкапа (OnCalendar)' "$DEFAULT_ONCALENDAR")" + + install_self || { log_error "Установка скрипта не удалась."; return 1; } + install_units "$pgver" + + if ask_yes_no "Создать симлинк ${BIN_SYMLINK} → ${SELF_NAME}?"; then + if ln -sfn "${INSTALL_DIR}/${SELF_NAME}" "$BIN_SYMLINK"; then + log_info "Симлинк создан: ${BIN_SYMLINK}" + else + log_warn "Не удалось создать симлинк ${BIN_SYMLINK}" + fi + fi + + if command -v systemctl >/dev/null 2>&1; then + if ask_yes_no "Включить и запустить таймер ${UNIT_TIMER} сейчас?"; then + if systemctl enable --now "${UNIT_TIMER}"; then + log_info "Таймер включён." + else + log_warn "Не удалось включить таймер." + fi + fi + fi + + log_section "Установка завершена" + log_info "Запуск: ${INSTALL_DIR}/${SELF_NAME}" + [[ -L "$BIN_SYMLINK" ]] && log_info " или: wal-g-manager" +} + +action_units_only() { + require_root + INSTALL_DIR="$(current_install_dir 2>/dev/null || echo "$DEFAULT_INSTALL_DIR")" + ONCALENDAR="$(current_oncalendar 2>/dev/null || echo "$DEFAULT_ONCALENDAR")" + local pgver; pgver="$(resolve_pgver)" + install_units "$pgver" +} + +action_uninstall() { + require_root + log_section "Удаление WAL-G-менеджера" + uninstall_units + + local dir; dir="$(current_install_dir 2>/dev/null || true)" + dir="${dir:-$DEFAULT_INSTALL_DIR}" + + if ask_yes_no "Удалить установленную копию скрипта из ${dir}?"; then + rm -f "${dir}/${SELF_NAME}" + rmdir "${dir}" 2>/dev/null || true + log_info "Скрипт удалён из ${dir}" + fi + + [[ -L "$BIN_SYMLINK" ]] && { rm -f "$BIN_SYMLINK"; log_info "Симлинк ${BIN_SYMLINK} удалён."; } + log_section "Удаление завершено" +} + +systemd_status() { + log_section "Статус задания резервного копирования" + + if ! command -v systemctl >/dev/null 2>&1; then + log_warn "systemctl недоступен в этой системе." + return 0 + fi + + local svc_file="${SYSTEMD_DIR}/${UNIT_SERVICE}" tmr_file="${SYSTEMD_DIR}/${UNIT_TIMER}" + + if [[ -f "$tmr_file" || -f "$svc_file" ]]; then + log_info "Файлы юнитов: $( [[ -f "$svc_file" ]] && echo "${UNIT_SERVICE} ✓" || echo "${UNIT_SERVICE} ✗" ), $( [[ -f "$tmr_file" ]] && echo "${UNIT_TIMER} ✓" || echo "${UNIT_TIMER} ✗" )" + else + log_warn "Юниты не установлены (нет файлов в ${SYSTEMD_DIR})." + return 0 + fi + + echo -e " enabled : ${BOLD}$(systemctl is-enabled "${UNIT_TIMER}" 2>/dev/null || echo '—')${NC}" + echo -e " active : ${BOLD}$(systemctl is-active "${UNIT_TIMER}" 2>/dev/null || echo '—')${NC}" + echo + systemctl list-timers --all "${UNIT_TIMER}" --no-pager 2>/dev/null || true +} + +menu_systemd() { + while true; do + echo + echo -e "${BOLD}--- Задание systemd (резервное копирование) ---${NC}" + echo -e " ${BOLD}1.${NC} Показать статус" + echo -e " ${BOLD}2.${NC} Установить/обновить юниты и включить" + echo -e " ${BOLD}3.${NC} Отключить (disable --now)" + echo -e " ${BOLD}4.${NC} Удалить юниты" + echo -e " ${BOLD}0.${NC} Назад" + echo + local c; read -rp "Выберите пункт [0-4]: " c || { echo; return 0; } + case "$c" in + 1) systemd_status; pause ;; + 2) + action_units_only + if command -v systemctl >/dev/null 2>&1; then + systemctl enable --now "${UNIT_TIMER}" \ + && log_info "Таймер ${UNIT_TIMER} включён." \ + || log_warn "Не удалось включить таймер ${UNIT_TIMER}." + fi + pause + ;; + 3) + if command -v systemctl >/dev/null 2>&1; then + systemctl disable --now "${UNIT_TIMER}" && log_info "Таймер отключён." \ + || log_warn "Не удалось отключить таймер." + else + log_warn "systemctl недоступен." + fi + pause + ;; + 4) + if ask_yes_no "Удалить юниты ${UNIT_TIMER}/${UNIT_SERVICE}?"; then + uninstall_units + else + log_info "Отменено." + fi + pause + ;; + 0|q|Q) return 0 ;; + *) log_warn "Некорректный ввод: '${c}'." ;; + esac + done +} + +# ============================================================================== +# ГЛАВНОЕ МЕНЮ +# ============================================================================== + +pause() { + echo + read -rsp "Нажмите Enter для возврата в меню..." _ || true + echo +} + +print_main_menu() { + echo + echo -e "${BOLD}============================================================${NC}" + echo -e "${BOLD} WAL-G — панель управления${NC}" + echo -e "${BOLD}============================================================${NC}" + echo + echo -e " ${BOLD}1.${NC} Настройка WAL-G — создать/обновить конфиги и параметры архивации" + echo -e " ${BOLD}2.${NC} Список бэкапов — показать доступные бэкапы по кластерам" + echo -e " ${BOLD}3.${NC} Создать бэкап — интерактивный delta/FULL-бэкап выбранных кластеров" + echo -e " ${BOLD}4.${NC} Бэкап всех кластеров — неинтерактивный прогон (как в systemd-таймере) + retain" + echo -e " ${BOLD}5.${NC} Удалить бэкапы — удаление старых бэкапов (retain FULL N)" + echo -e " ${BOLD}6.${NC} Восстановление — восстановление кластера из бэкапа (в т.ч. PITR)" + echo -e " ${BOLD}7.${NC} Задание systemd — просмотр/включение/удаление таймера бэкапа" + echo -e " ${BOLD}8.${NC} Установка/обновление — развернуть скрипт и systemd-юниты" + echo + echo -e " ${BOLD}0.${NC} Выход" + echo +} + +main_menu() { + if [[ ! -t 0 ]]; then + log_error "Нужен интерактивный терминал (stdin не является TTY)." + exit 1 + fi + trap 'echo; log_warn "Прервано пользователем."' INT + + while true; do + print_main_menu + local choice; read -rp "Выберите пункт [0-8]: " choice || { echo; exit 0; } + case "$choice" in + 0|q|Q|exit) echo "Выход."; exit 0 ;; + 1) action_configure; pause ;; + 2) action_list; pause ;; + 3) action_backup; pause ;; + 4) action_backup_all; pause ;; + 5) action_delete; pause ;; + 6) action_restore; pause ;; + 7) menu_systemd ;; + 8) action_install; pause ;; + *) log_warn "Некорректный ввод: '${choice}'. Введите число из меню." ;; + esac + done +} + +usage() { + sed -n '2,25p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' +} + +# ============================================================================== +# ТОЧКА ВХОДА +# ============================================================================== + +CMD="${1:-menu}" +[[ $# -gt 0 ]] && shift + +case "$CMD" in + menu) require_root; main_menu ;; + cfg) action_configure ;; + list) action_list "$@" ;; + backup) action_backup ;; + backup-all) action_backup_all ;; + delete) action_delete ;; + restore) action_restore ;; + install) action_install ;; + units) action_units_only ;; + uninstall-units) require_root; uninstall_units ;; + uninstall) action_uninstall ;; + -h|--help|help) usage ;; + *) + log_error "Неизвестная команда: ${CMD}" + usage + exit 1 + ;; +esac diff --git a/pgsql/pgpro/wal-g-scripts/wal-g-backup-all.sh b/pgsql/pgpro/wal-g-scripts/wal-g-backup-all.sh deleted file mode 100644 index ab0dcb9..0000000 --- a/pgsql/pgpro/wal-g-scripts/wal-g-backup-all.sh +++ /dev/null @@ -1,213 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================== -# wal-g-backup-all.sh — резервное копирование всех PostgreSQL-кластеров через WAL-G -# (неинтерактивный режим для systemd-таймера; запускать от root) -# ============================================================================== - -set -uo pipefail - -# ── Константы ────────────────────────────────────────────────────────────────── -readonly WALG_DIR="/etc/wal-g.d" -readonly WALG_BIN="/usr/bin/wal-g" -readonly PG_USER="postgres" -readonly RETAIN="${RETAIN:-3}" # сколько FULL-бэкапов хранить (переопределяемо через env) - -# ── Глобальный счётчик ошибок ────────────────────────────────────────────────── -errors=0 - -# ── Вспомогательные функции ──────────────────────────────────────────────────── -log() { echo "[$(date '+%F %T')] INFO $*"; } -warn() { echo "[$(date '+%F %T')] WARN $*" >&2; } -err() { echo "[$(date '+%F %T')] ERROR $*" >&2; } - -# ── Проверка окружения ───────────────────────────────────────────────────────── -check_env() { - local fail=0 - - [[ -d "${WALG_DIR}" ]] || { err "Каталог конфигов не найден: ${WALG_DIR}"; fail=1; } - [[ -x "${WALG_BIN}" ]] || { err "WAL-G бинарник не найден / не исполняемый: ${WALG_BIN}"; fail=1; } - command -v python3 &>/dev/null || { err "python3 не найден в PATH"; fail=1; } - - (( fail == 0 )) -} - -# ── Извлечь PGDATA из JSON-конфига ──────────────────────────────────────────── -get_pgdata() { - local cfg="$1" - python3 - "$cfg" <<'PY' -import json, sys -cfg = sys.argv[1] -try: - with open(cfg, encoding="utf-8") as f: - val = json.load(f).get("PGDATA", "") - print(val) -except Exception as e: - print(f"PARSE_ERROR: {e}", file=sys.stderr) - sys.exit(1) -PY -} - -# ── Извлечь PORT из JSON-конфига ────────────────────────────────────────────── -get_port() { - local cfg="$1" - python3 - "$cfg" <<'PY' -import json, sys -cfg = sys.argv[1] -try: - with open(cfg, encoding="utf-8") as f: - val = json.load(f).get("PGPORT", "") - print(str(val).strip()) -except Exception as e: - print(f"PARSE_ERROR: {e}", file=sys.stderr) - sys.exit(1) -PY -} - -# ── Резервное копирование одного кластера ────────────────────────────────────── -backup_cluster() { - local cfg="$1" - local port="$2" - local pgdata="$3" - # RETAIN передаём явно — su - сбрасывает окружение - local retain="${RETAIN}" - - local logdir="${pgdata}/log" - local logfile="${logdir}/wal-g-backup-${port}-$(date +%F).log" - - mkdir -p "${logdir}" && chown "${PG_USER}:${PG_USER}" "${logdir}" || { - err "Не удалось создать каталог логов: ${logdir}" - return 1 - } - - log "Запуск резервного копирования: port=${port}, cfg=${cfg}" - log "Лог: ${logfile}" - - # Все значения передаём через явные переменные в heredoc, - # чтобы не зависеть от окружения login shell. - su - "${PG_USER}" -s /bin/bash <> "\${LOGFILE}" 2>&1 - -# Возвращаем итоговый код — без (( )), чтобы не триггерить set -e при нуле -if (( backup_rc != 0 || delete_rc != 0 )); then - exit 1 -fi -exit 0 -SHELL -} - - -# ══════════════════════════════════════════════════════════════════════════════ -# MAIN -# ══════════════════════════════════════════════════════════════════════════════ -main() { - check_env || exit 1 - - shopt -s nullglob - local confs=( "${WALG_DIR}"/.walg-*.json ) - shopt -u nullglob - - if (( ${#confs[@]} == 0 )); then - err "Конфиги не найдены: ${WALG_DIR}/.walg-*.json" - exit 1 - fi - - log "Найдено конфигов: ${#confs[@]}" - - local cfg port pgdata - for cfg in "${confs[@]}"; do - - # ── Порт ──────────────────────────────────────────────────────────────── - port="$(get_port "${cfg}")" || { - err "Ошибка парсинга порта: ${cfg} — пропуск" - (( errors++ )) || true - continue - } - - if [[ ! "${port}" =~ ^[0-9]+$ ]]; then - err "PGPORT не задан или некорректен: ${cfg} — пропуск" - (( errors++ )) || true - continue - fi - - # ── PGDATA ────────────────────────────────────────────────────────────── - pgdata="$(get_pgdata "${cfg}")" || { - err "Ошибка парсинга PGDATA: ${cfg} — пропуск" - (( errors++ )) || true - continue - } - - if [[ -z "${pgdata}" ]]; then - err "PGDATA не задан в конфиге: ${cfg} — пропуск" - (( errors++ )) || true - continue - fi - - if [[ ! -d "${pgdata}" ]]; then - err "PGDATA не существует: '${pgdata}' (cfg=${cfg}) — пропуск" - (( errors++ )) || true - continue - fi - - # ── Запуск ────────────────────────────────────────────────────────────── - backup_cluster "${cfg}" "${port}" "${pgdata}" || { - err "Ошибка резервного копирования: port=${port}, cfg=${cfg}" - (( errors++ )) || true - } - done - - if (( errors > 0 )); then - err "Завершено с ошибками: ${errors} кластер(ов)" - exit 1 - fi - - log "Все резервные копии успешно созданы" - exit 0 -} - -main "$@" diff --git a/pgsql/pgpro/wal-g-scripts/wal-g-backup.sh b/pgsql/pgpro/wal-g-scripts/wal-g-backup.sh deleted file mode 100644 index 18c377f..0000000 --- a/pgsql/pgpro/wal-g-scripts/wal-g-backup.sh +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/bash -# ============================================================================== -# wal-g-backup.sh — интерактивное создание WAL-G бэкапов -# ============================================================================== -set -euo pipefail - -# ───────────────────────────────────────────── -# Вспомогательные функции -# ───────────────────────────────────────────── - -prompt_default() { - local msg="${1:-}" default="${2:-}" val - read -r -p "${msg} [${default}]: " val - echo "${val:-$default}" -} - -prompt_required() { - local msg="$1" val - while [[ -z "${val:-}" ]]; do - read -r -p "${msg}: " val - [[ -z "$val" ]] && echo " Значение не может быть пустым." >&2 - done - echo "$val" -} - -# ───────────────────────────────────────────── -# Параметры -# ───────────────────────────────────────────── - -PGVER="$(prompt_default 'Введите версию PostgreSQL (PGVER)' '1c-18')" -WALG_CONFIG_DIR="/etc/wal-g.d" - -# ───────────────────────────────────────────── -# Обнаружение кластеров (по ss) -# ───────────────────────────────────────────── - -FOUND_PORTS=() -if command -v ss >/dev/null 2>&1; then - mapfile -t FOUND_PORTS < <( - ss -tlnp 2>/dev/null \ - | grep -i postgres \ - | grep -oP ':\K\d+' \ - | sort -u - ) -fi - -# ───────────────────────────────────────────── -# Фильтрация: кластер + конфиг должны совпасть -# ───────────────────────────────────────────── - -VALID_PORTS=() -declare -A PORT_DATA # PORT_DATA[port]=pgdata - -check_port() { - local port="$1" - local cfg="${WALG_CONFIG_DIR}/.walg-${port}.json" - - if [[ ! -f "$cfg" ]]; then - echo " [SKIP] Порт ${port}: конфиг ${cfg} не найден" - return 1 - fi - - if ! pg_isready -h /tmp -p "$port" -U postgres -t 3 >/dev/null 2>&1; then - echo " [SKIP] Порт ${port}: кластер недоступен" - return 1 - fi - - local pgdata - pgdata="$(psql -h /tmp -p "$port" -U postgres -d postgres -At \ - -c "show data_directory;" 2>/dev/null || true)" - - if [[ -z "$pgdata" || ! -d "$pgdata" ]]; then - echo " [SKIP] Порт ${port}: не удалось определить PGDATA" - return 1 - fi - - PORT_DATA[$port]="$pgdata" - return 0 -} - -# ───────────────────────────────────────────── -# Сканирование + единый вывод списка -# ───────────────────────────────────────────── - -scan_ports() { - local ports=("$@") - for p in "${ports[@]}"; do - if check_port "$p"; then - VALID_PORTS+=("$p") - fi - done -} - -echo "" -echo "=== Доступные кластеры ===" - -if [[ ${#FOUND_PORTS[@]} -gt 0 ]]; then - scan_ports "${FOUND_PORTS[@]}" -else - echo " Автообнаружение не дало результатов." -fi - -# ───────────────────────────────────────────── -# Ручной ввод если ничего не найдено -# ───────────────────────────────────────────── - -if [[ ${#VALID_PORTS[@]} -eq 0 ]]; then - echo "" - echo "WARN: не найдено ни одного кластера с конфигом WAL-G." >&2 - MANUAL="$(prompt_required 'Укажите порты вручную через пробел (например: 5432 5433)')" - read -r -a MANUAL_PORTS <<< "$MANUAL" - scan_ports "${MANUAL_PORTS[@]}" - - if [[ ${#VALID_PORTS[@]} -eq 0 ]]; then - echo "ERROR: нет доступных кластеров с валидной конфигурацией — выход." >&2 - exit 1 - fi -fi - -# ── Единый нумерованный список ── -echo "" -for i in "${!VALID_PORTS[@]}"; do - p="${VALID_PORTS[$i]}" - cfg="${WALG_CONFIG_DIR}/.walg-${p}.json" - echo " $((i+1)). [ OK ] Порт ${p} | PGDATA=${PORT_DATA[$p]}, конфиг=${cfg}" -done -echo " 0. Выход" -echo "" - -# ───────────────────────────────────────────── -# Выбор портов для бэкапа -# ───────────────────────────────────────────── - -SEL="$(prompt_default "Укажите номера через пробел, 'all' для всех или '0' для выхода" 'all')" - -if [[ "$SEL" == "0" || "${SEL,,}" == "q" || "${SEL,,}" == "exit" ]]; then - echo "Выход из скрипта." - exit 0 -fi - -SELECTED_PORTS=() -if [[ "${SEL,,}" == "all" ]]; then - SELECTED_PORTS=("${VALID_PORTS[@]}") -else - read -r -a INPUT_NUMS <<< "$SEL" - for num in "${INPUT_NUMS[@]}"; do - if ! [[ "$num" =~ ^[0-9]+$ ]]; then - echo " [WARN] '${num}' — не число, пропускаем." >&2 - continue - fi - idx=$(( num - 1 )) - if [[ $idx -lt 0 || $idx -ge ${#VALID_PORTS[@]} ]]; then - echo " [WARN] Номер ${num} вне диапазона — пропускаем." >&2 - continue - fi - SELECTED_PORTS+=("${VALID_PORTS[$idx]}") - done -fi - -if [[ ${#SELECTED_PORTS[@]} -eq 0 ]]; then - echo "ERROR: нет кластера для бэкапа — выход." >&2 - exit 1 -fi - -# ───────────────────────────────────────────── -# Запрос типа бэкапа -# ───────────────────────────────────────────── - -echo "" -FORCE_FULL_INPUT="$(prompt_default 'Запустить FULL-бэкап для всех выбранных кластеров? (y/N)' 'N')" - -FORCE_FULL=false -if [[ "${FORCE_FULL_INPUT,,}" == "y" ]]; then - FORCE_FULL=true - echo " Режим: FULL-бэкап." -else - echo " Режим: Delta (при отсутствии базового — автоматически FULL)." -fi - -# ───────────────────────────────────────────── -# Запуск бэкапов -# ───────────────────────────────────────────── - -echo "" -echo "=== Запуск бэкапов ===" - -ERRORS=0 -for PGPORT in "${SELECTED_PORTS[@]}"; do - PGDATA="${PORT_DATA[$PGPORT]}" - WALG_CONFIG_FILE="${WALG_CONFIG_DIR}/.walg-${PGPORT}.json" - - echo "" - echo ">>> Бэкап: порт=${PGPORT} PGDATA=${PGDATA}" - echo " Конфиг: ${WALG_CONFIG_FILE}" - - if [[ "$FORCE_FULL" == true ]]; then - # ── Принудительный FULL ── - echo " Запуск FULL-бэкапа..." - if su - postgres -c \ - "/usr/bin/wal-g backup-push --full --config '${WALG_CONFIG_FILE}' '${PGDATA}'"; then - echo " [OK] FULL бэкап ${PGPORT} завершён успешно." - else - echo " [FAIL] FULL бэкап ${PGPORT} завершился с ошибкой!" >&2 - (( ERRORS++ )) || true - fi - else - # ── Delta → при ошибке FULL ── - echo " Попытка delta-бэкапа..." - if ! su - postgres -c \ - "/usr/bin/wal-g backup-push --config '${WALG_CONFIG_FILE}' '${PGDATA}'"; then - echo " Delta не удалась — пробуем FULL бэкап..." >&2 - if su - postgres -c \ - "/usr/bin/wal-g backup-push --full --config '${WALG_CONFIG_FILE}' '${PGDATA}'"; then - echo " [OK] FULL бэкап ${PGPORT} завершён успешно." - else - echo " [FAIL] FULL бэкап ${PGPORT} завершился с ошибкой!" >&2 - (( ERRORS++ )) || true - fi - else - echo " [OK] Delta бэкап ${PGPORT} завершён успешно." - fi - fi -done - -# ───────────────────────────────────────────── -# Итог -# ───────────────────────────────────────────── - -echo "" -echo "=== Итог ===" -echo " Всего бэкапов: ${#SELECTED_PORTS[@]}" -echo " Ошибок: ${ERRORS}" - -[[ $ERRORS -eq 0 ]] && exit 0 || exit 1 diff --git a/pgsql/pgpro/wal-g-scripts/wal-g-cfg.sh b/pgsql/pgpro/wal-g-scripts/wal-g-cfg.sh deleted file mode 100644 index c7938e1..0000000 --- a/pgsql/pgpro/wal-g-scripts/wal-g-cfg.sh +++ /dev/null @@ -1,601 +0,0 @@ -#!/usr/bin/env bash - -# Скрипт для настройки конфигурационных файлов WAL-G для хранилища S3 или файлового варианта -# Скрипт автоматически определяет кластера PostgreSQL и каталоги данных -# Также автоматически добавляются необходимые параметры для архивации в postgresql.auto.conf -# Для корректной работы скрипта должен быть настроен файл паролей .pgpass -# -# Запускать от root: требуется создание /etc/wal-g.d, chown postgres и systemctl restart. -# Сокет PostgreSQL берётся из PGHOST_SOCKET (по умолчанию /tmp — конвенция PostgresPro 1C). - -set -Eeuo pipefail - -# Диагностика непредвиденных ошибок (флаг -E делает трап ERR наследуемым функциями). -trap 'rc=$?; echo "ERROR: команда «${BASH_COMMAND}» завершилась с кодом ${rc} (строка ${LINENO})" >&2' ERR - -# ============================================================================= -# КОНСТАНТЫ -# ============================================================================= - -WALG_DIR='/etc/wal-g.d' -WALG_COMPRESSION_METHOD='brotli' -PGHOST_SOCKET='/tmp' # каталог unix-сокета PostgreSQL -PG_SUPERUSER='postgres' - -# Конфиги содержат секреты: umask 077 закрывает окно world-readable между -# созданием файла и chmod 600 (install -d ниже задаёт режим каталога явно). -umask 077 - -# Кэш PGDATA: заполняется при обзоре кластеров, переиспользуется в шаге 7. -declare -A CLUSTER_PGDATA=() - -# ============================================================================= -# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ -# ============================================================================= - -prompt_default() { - local prompt="$1" default="${2-}" var - if [[ -n "${default}" ]]; then - read -r -p "${prompt} [${default}]: " var - echo "${var:-$default}" - else - read -r -p "${prompt}: " var - echo "${var}" - fi -} - -prompt_required() { - local prompt="$1" var="" - while [[ -z "$var" ]]; do - read -r -p "${prompt}: " var - [[ -z "$var" ]] && echo "Значение обязательно" >&2 - done - echo "$var" -} - -# Единый y/N-вопрос: принимает y, Y, yes, YES. -ask_yes_no() { - local prompt="$1" ans - read -r -p "${prompt} [y/N]: " ans || return 1 - [[ "$ans" =~ ^([yY]|[yY][eE][sS])$ ]] -} - -# Экранирование строки для безопасной вставки в JSON-значение. -json_escape() { - local s="$1" - s="${s//\\/\\\\}" # обратный слэш — первым - s="${s//\"/\\\"}" # затем двойная кавычка - printf '%s' "$s" -} - -# Жив ли кластер на порту (по unix-сокету). -pg_alive() { - local port="$1" timeout="${2:-3}" - pg_isready -h "$PGHOST_SOCKET" -p "$port" -U "$PG_SUPERUSER" -t "$timeout" >/dev/null 2>&1 -} - -# SHOW с указанного кластера (пустая строка при ошибке). -pg_show() { - local port="$1" guc="$2" - psql -h "$PGHOST_SOCKET" -p "$port" -U "$PG_SUPERUSER" -d postgres -At -q -X \ - -c "SHOW ${guc};" 2>/dev/null -} - -# ============================================================================= -# ИМЯ СЕРВИСА SYSTEMD -# ============================================================================= - -get_service_name() { - local pgver="$1" - local pgport="$2" - if [[ "$pgport" == "5432" ]]; then - echo "postgrespro-${pgver}.service" - else - echo "postgrespro-${pgver}@${pgport}.service" - fi -} - -# ============================================================================= -# ПЕРЕЗАПУСК СЕРВИСА -# ============================================================================= - -restart_pg_service() { - local pgver="$1" - local pgport="$2" - local svc - svc="$(get_service_name "$pgver" "$pgport")" - - echo - echo "INFO: имя сервиса ${svc}" - - if ! ask_yes_no "Перезапустить сервис ${svc}?"; then - echo "INFO: перезапуск отменён. Выполните вручную:" - echo " systemctl restart ${svc}" - return 0 - fi - - echo "INFO: выполняется systemctl restart ${svc} ..." - - if ! systemctl restart "$svc"; then - echo "ERROR: не удалось перезапустить сервис ${svc}" >&2 - echo "INFO: проверьте статус: systemctl status ${svc}" >&2 - return 1 - fi - - # Ждём готовности кластера после перезапуска - local retries=10 i=1 - echo "INFO: ожидание готовности кластера на порту ${pgport}..." - while (( i <= retries )); do - if pg_alive "$pgport" 2; then - echo "OK: кластер ${pgport} запущен." - return 0 - fi - echo " попытка ${i}/${retries}..." - sleep 2 - i=$(( i + 1 )) - done - - echo "WARN: кластер ${pgport} не ответил после ${retries} попыток." >&2 - echo "INFO: проверьте статус: systemctl status ${svc}" >&2 - return 1 -} - -# ============================================================================= -# ВЫВОД СПИСКА КЛАСТЕРОВ -# ============================================================================= - -print_cluster_summary() { - local -a ports=("$@") - local port pgdata cfg_path cfg_info avail_status backend_info pgdata_info idx=1 - - echo - echo "=== Доступные кластеры ===" - echo - - for port in "${ports[@]}"; do - cfg_path="${WALG_DIR}/.walg-${port}.json" - - # Доступность и PGDATA - if pg_alive "$port"; then - avail_status="OK" - pgdata="$(pg_show "$port" data_directory)" || pgdata="" - [[ -n "$pgdata" ]] && CLUSTER_PGDATA["$port"]="$pgdata" - else - avail_status="!!" - pgdata="" - fi - - # Статус конфига и тип хранилища - if [[ -f "$cfg_path" ]]; then - if grep -q '"WALG_S3_PREFIX"' "$cfg_path" 2>/dev/null; then - backend_info="s3" - elif grep -q '"WALG_FILE_PREFIX"' "$cfg_path" 2>/dev/null; then - backend_info="file" - else - backend_info="unknown" - fi - cfg_info="конфиг=есть (${backend_info})" - else - cfg_info="конфиг=отсутствует" - fi - - # PGDATA - if [[ -n "$pgdata" ]]; then - pgdata_info="PGDATA=${pgdata}" - else - pgdata_info="PGDATA недоступен" - fi - - printf " %2d. [ %s ] Порт %-5s | %s, %s\n" \ - "$idx" "$avail_status" "$port" "$pgdata_info" "$cfg_info" - - idx=$(( idx + 1 )) - done - - echo - printf " 0. Выход\n" - echo -} - -# ============================================================================= -# ВЫБОР ПОРТОВ ПО НОМЕРАМ ИЗ СПИСКА -# ============================================================================= - -select_ports_by_index() { - local -n _ports_ref="$1" - local total="${#_ports_ref[@]}" - local input sel port result=() - - while true; do - read -r -p "Введите номера через пробел или 'all' для всех [all]: " input - input="${input:-all}" - - if [[ "$input" == "0" ]]; then - echo "Выход." >&2 - exit 0 - fi - - if [[ "$input" == "all" ]]; then - SELECTED_PORTS=("${_ports_ref[@]}") - return - fi - - result=() - local valid=1 - - for sel in $input; do - if ! [[ "$sel" =~ ^[0-9]+$ ]]; then - echo "WARN: '${sel}' не является числом, повторите ввод." >&2 - valid=0 - break - fi - if (( sel < 1 || sel > total )); then - echo "WARN: номер ${sel} вне диапазона 1–${total}, повторите ввод." >&2 - valid=0 - break - fi - port="${_ports_ref[$(( sel - 1 ))]}" - result+=("$port") - done - - if (( valid )) && (( ${#result[@]} > 0 )); then - SELECTED_PORTS=("${result[@]}") - return - fi - done -} - -# ============================================================================= -# НАСТРОЙКА ПАРАМЕТРОВ POSTGRESQL ДЛЯ АРХИВАЦИИ -# ============================================================================= - -configure_pg_instance() { - local pgport="$1" - local pgdata="$2" - local walgcfg="$3" - - local -a PSQL=( - psql -h "$PGHOST_SOCKET" -p "$pgport" -U "$PG_SUPERUSER" -d postgres - -At -q -X -v ON_ERROR_STOP=1 - ) - - local logdir - logdir="$("${PSQL[@]}" -c "SHOW log_directory;" 2>/dev/null)" || { - echo "ERROR: не удалось получить log_directory для ${pgport}" >&2 - return 1 - } - - local pglogdir - if [[ "$logdir" = /* ]]; then - pglogdir="$logdir" - else - pglogdir="${pgdata%/}/${logdir}" - fi - - declare -A WANT=( - [wal_level]="replica" - [archive_mode]="on" - [archive_timeout]="60s" - [archive_command]="/usr/bin/wal-g wal-push \"%p\" --config ${walgcfg} >> ${pglogdir}/archive_command.log 2>&1" - [restore_command]="/usr/bin/wal-g wal-fetch \"%f\" \"%p\" --config ${walgcfg} >> ${pglogdir}/restore_command.log 2>&1" - ) - - declare -A RESTART=( - [wal_level]=1 - [archive_mode]=1 - [archive_command]=0 - [archive_timeout]=0 - [restore_command]=0 - ) - - local pg_out - pg_out="$("${PSQL[@]}" -F '|' -c " - SELECT name, - CASE WHEN COALESCE(unit,'') = '' THEN setting - ELSE setting || unit - END AS setting - FROM pg_settings - WHERE name IN ( - 'wal_level','archive_mode','archive_command', - 'archive_timeout','restore_command' - ) - ORDER BY name; - " 2>&1)" || { - echo "ERROR: не удалось получить параметры pg_settings для ${pgport}" >&2 - return 1 - } - - declare -A CUR=() - local k v - while IFS='|' read -r k v; do - [[ -n "$k" ]] && CUR["$k"]="$v" - done <<< "$pg_out" - - local -a PARAMS=(wal_level archive_mode archive_command archive_timeout restore_command) - local p - for p in "${PARAMS[@]}"; do - if [[ -z "${CUR[$p]+x}" ]]; then - echo "ERROR: параметр '${p}' не получен из pg_settings" >&2 - return 1 - fi - done - - local -a TO_CHANGE=() - local restart_needed=0 - - for p in "${PARAMS[@]}"; do - if [[ "${CUR[$p]}" != "${WANT[$p]}" ]]; then - TO_CHANGE+=("$p") - (( restart_needed |= RESTART[$p] )) || true - fi - done - - if (( ${#TO_CHANGE[@]} == 0 )); then - echo "INFO: параметры PostgreSQL ${pgport} уже соответствуют целевым." - PG_RESTART_NEEDED=0 - return 0 - fi - - echo - echo "=== Параметры PostgreSQL (${pgport}) ===" - for p in "${PARAMS[@]}"; do - local cur="${CUR[$p]}" - local want="${WANT[$p]}" - if [[ "$cur" != "$want" ]]; then - local extra="" - (( RESTART[$p] )) && extra=" (требуется перезапуск)" - printf " [CHG] %-20s\n текущий : %s\n целевой : %s%s\n" \ - "$p" "${cur:-<пусто>}" "$want" "$extra" - else - printf " [OK ] %-20s = %s\n" "$p" "$cur" - fi - done - echo - - if ! ask_yes_no "Применить ${#TO_CHANGE[@]} изменения для ${pgport}?"; then - echo "INFO: изменения ${pgport} отменены пользователем." - PG_RESTART_NEEDED=0 - return 0 - fi - - # Все ALTER SYSTEM применяем одним вызовом psql (ON_ERROR_STOP=1 — атомарно). - local sql="" - for p in "${TO_CHANGE[@]}"; do - local esc="${WANT[$p]//\'/\'\'}" - sql+="ALTER SYSTEM SET ${p} = '${esc}';"$'\n' - done - - if ! "${PSQL[@]}" -c "$sql" >/dev/null; then - echo "ERROR: не удалось применить параметры PostgreSQL ${pgport}" >&2 - return 1 - fi - for p in "${TO_CHANGE[@]}"; do - echo " SET ${p}" - done - - if ! "${PSQL[@]}" -c "SELECT pg_reload_conf();" >/dev/null 2>&1; then - echo "WARN: не удалось выполнить pg_reload_conf() ${pgport}" >&2 - fi - - # Возвращаем признак необходимости перезапуска через глобальную переменную - if (( restart_needed )); then - echo "INFO: параметры wal_level/archive_mode требуют перезапуска сервиса." - PG_RESTART_NEEDED=1 - else - echo "OK: параметры обновлены без необходимости перезапуска." - PG_RESTART_NEEDED=0 - fi -} - -# ============================================================================= -# ШАГ 1: ОБНАРУЖЕНИЕ И ВЫВОД СПИСКА КЛАСТЕРОВ -# ============================================================================= - -FOUND_PORTS=() -if command -v ss >/dev/null 2>&1; then - mapfile -t FOUND_PORTS < <(ss -tlnp 2>/dev/null \ - | grep postgres \ - | grep -oP ':\K\d+' \ - | sort -un) -fi - -SELECTED_PORTS=() -if (( ${#FOUND_PORTS[@]} > 0 )); then - print_cluster_summary "${FOUND_PORTS[@]}" - select_ports_by_index FOUND_PORTS -else - echo "WARN: кластеры не обнаружены автоматически (нужны права root для ss -p)." >&2 - MANUAL="$(prompt_required 'Укажите порты через пробел (например: 5432 5433)')" - read -r -a SELECTED_PORTS <<< "$MANUAL" -fi - -if (( ${#SELECTED_PORTS[@]} == 0 )); then - echo "ERROR: список пуст — нечего настраивать" >&2 - exit 1 -fi - -echo -echo "Выбраны порты: ${SELECTED_PORTS[*]}" -echo - -# ============================================================================= -# ШАГ 2: ТИП ХРАНИЛИЩА -# ============================================================================= - -echo "Тип хранилища:" -echo " 1. S3" -echo " 2. Файловое" -echo " 0. Выход" -echo -while true; do - read -r -p "Выбор [1]: " BACKEND_SEL - BACKEND_SEL="${BACKEND_SEL:-1}" - case "${BACKEND_SEL}" in - 1) BACKEND="s3"; break ;; - 2) BACKEND="file"; break ;; - 0) echo "Выход."; exit 0 ;; - *) echo "WARN: введите 0, 1 или 2." >&2 ;; - esac -done - -echo "INFO: тип хранилища = ${BACKEND}" -echo - -# ============================================================================= -# ШАГ 3: ВЕРСИЯ POSTGRESQL -# ============================================================================= - -PGVER="$(prompt_default 'Версия PostgreSQL (PGVER)' '1c-18')" - -# ============================================================================= -# ШАГ 4: WALG_DELTA_MAX_STEPS -# ============================================================================= - -WALG_DELTA_MAX_STEPS="$(prompt_default 'WALG_DELTA_MAX_STEPS' '6')" -if ! [[ "$WALG_DELTA_MAX_STEPS" =~ ^[0-9]+$ ]]; then - echo "ERROR: WALG_DELTA_MAX_STEPS должен быть целым числом" >&2 - exit 1 -fi - -# ============================================================================= -# ШАГ 5: ПАРАМЕТРЫ БЭКЕНДА -# ============================================================================= - -if [[ "$BACKEND" == "s3" ]]; then - AWS_ENDPOINT="$(prompt_default 'AWS_ENDPOINT' 's3.ru-3.storage.selcloud.ru:443')" - AWS_REGION="$(prompt_default 'AWS_REGION' 'ru-3')" - AWS_ACCESS_KEY_ID="$(prompt_required 'AWS_ACCESS_KEY_ID')" - AWS_SECRET_ACCESS_KEY="$(prompt_required 'AWS_SECRET_ACCESS_KEY')" - S3_BUCKET="$(prompt_required 'S3_BUCKET')" - - [[ "$S3_BUCKET" != s3://* ]] && S3_BUCKET="s3://${S3_BUCKET}" - S3_BUCKET="${S3_BUCKET%/}" - - # Экранируем значения один раз для безопасной вставки в JSON - J_AWS_ENDPOINT="$(json_escape "$AWS_ENDPOINT")" - J_AWS_REGION="$(json_escape "$AWS_REGION")" - J_AWS_ACCESS_KEY_ID="$(json_escape "$AWS_ACCESS_KEY_ID")" - J_AWS_SECRET_ACCESS_KEY="$(json_escape "$AWS_SECRET_ACCESS_KEY")" - J_S3_BUCKET="$(json_escape "$S3_BUCKET")" -else - WALG_FILE_PREFIX_BASE="$(prompt_default 'Базовый каталог WALG_FILE_PREFIX' '/backup')" - WALG_FILE_PREFIX_BASE="${WALG_FILE_PREFIX_BASE%/}" - J_WALG_FILE_PREFIX_BASE="$(json_escape "$WALG_FILE_PREFIX_BASE")" -fi - -# ============================================================================= -# ШАГ 6: ПОДГОТОВКА КАТАЛОГА -# ============================================================================= - -if ! install -d -m 755 "$WALG_DIR" 2>/dev/null; then - echo "ERROR: нет прав на создание каталога ${WALG_DIR}" >&2 - exit 1 -fi - -# ============================================================================= -# ШАГ 7: ОБРАБОТКА КАЖДОГО КЛАСТЕРА -# ============================================================================= - -for PGPORT in "${SELECTED_PORTS[@]}"; do - - echo - echo "━━━ Порт ${PGPORT} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - - if ! [[ "$PGPORT" =~ ^[0-9]+$ ]]; then - echo "WARN: некорректный номер '${PGPORT}', пропуск" >&2 - continue - fi - - SVC_NAME="$(get_service_name "$PGVER" "$PGPORT")" - echo "INFO: сервис systemd: ${SVC_NAME}" - - CFG="${WALG_DIR}/.walg-${PGPORT}.json" - - if ! pg_alive "$PGPORT"; then - echo "WARN: кластер на порту ${PGPORT} недоступен, пропуск" >&2 - continue - fi - - # PGDATA берём из кэша обзора, иначе запрашиваем у кластера. - if [[ -n "${CLUSTER_PGDATA[$PGPORT]+x}" ]]; then - PGDATA="${CLUSTER_PGDATA[$PGPORT]}" - else - PGDATA="$(pg_show "$PGPORT" data_directory)" || PGDATA="" - fi - - if [[ -z "$PGDATA" || ! -d "$PGDATA" ]]; then - echo "WARN: не удалось определить PGDATA для порта ${PGPORT}, пропуск" >&2 - continue - fi - - echo "INFO: PGDATA = ${PGDATA}" - - # --- Решаем, нужно ли (пере)записывать конфиг --- - WRITE_CFG=1 - if [[ -f "$CFG" ]]; then - if ask_yes_no "Файл ${CFG} уже существует. Перезаписать?"; then - if ! cp -p -- "$CFG" "${CFG}.bak"; then - echo "ERROR: не удалось создать резервную копию ${CFG}.bak, пропуск" >&2 - continue - fi - echo "INFO: создана резервная копия ${CFG}.bak" - else - echo "INFO: конфиг ${CFG} оставлен без изменений." - WRITE_CFG=0 - fi - fi - - # --- Запись конфига (umask 077 уже гарантирует режим 600 при создании) --- - if (( WRITE_CFG )); then - if [[ "$BACKEND" == "s3" ]]; then - cat > "$CFG" < "$CFG" </dev/null; then - echo "WARN: не удалось установить владельца postgres для ${CFG}" >&2 - fi - echo "OK: конфиг записан: ${CFG}" - fi - - # --- Настройка параметров PostgreSQL (вызов защищён: сбой не прерывает цикл) --- - PG_RESTART_NEEDED=0 - if configure_pg_instance "$PGPORT" "$PGDATA" "$CFG"; then - if (( PG_RESTART_NEEDED )); then - restart_pg_service "$PGVER" "$PGPORT" \ - || echo "WARN: перезапуск ${PGPORT} не выполнен, перехожу к следующему кластеру." >&2 - fi - else - echo "WARN: настройка параметров PostgreSQL ${PGPORT} не завершена, пропуск." >&2 - fi - -done - -echo -echo "Готово." diff --git a/pgsql/pgpro/wal-g-scripts/wal-g-delete.sh b/pgsql/pgpro/wal-g-scripts/wal-g-delete.sh deleted file mode 100644 index a2f8d20..0000000 --- a/pgsql/pgpro/wal-g-scripts/wal-g-delete.sh +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/bash -# ============================================================================== -# wal-g-delete.sh — интерактивное удаление WAL-G бэкапов -# ============================================================================== - -set -uo pipefail - -# ── Константы ────────────────────────────────────────────────────────────────── -readonly WALG_DIR="/etc/wal-g.d" -readonly WALG_BIN="/usr/bin/wal-g" - -# ── Цвета (только в интерактивный TTY и при отсутствии NO_COLOR) ──────────────── -if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then - C_GREEN='\033[1;32m' - C_YELLOW='\033[0;33m' - C_RESET='\033[0m' -else - C_GREEN='' C_YELLOW='' C_RESET='' -fi - -# Кол-во FULL-бэкапов из последнего show_backups (для проверки retain в main). -LAST_FULL_COUNT=0 - -# ── Вспомогательные функции ──────────────────────────────────────────────────── -log() { echo "[$(date '+%F %T')] INFO $*"; } -err() { echo "[$(date '+%F %T')] ERROR $*" >&2; } - -separator() { echo "════════════════════════════════════════════════════════════"; } - -# ── Извлечь PGPORT из JSON-конфига ──────────────────────────────────────────── -get_port() { - local cfg="$1" - python3 - "$cfg" <<'PY' -import json, sys -cfg = sys.argv[1] -try: - with open(cfg, encoding="utf-8") as f: - val = json.load(f).get("PGPORT", "") - print(str(val).strip()) -except Exception as e: - print(f"PARSE_ERROR: {e}", file=sys.stderr) - sys.exit(1) -PY -} - -# ── Проверка окружения ───────────────────────────────────────────────────────── -check_env() { - local fail=0 - [[ -d "${WALG_DIR}" ]] || { err "Каталог конфигов не найден: ${WALG_DIR}"; fail=1; } - [[ -x "${WALG_BIN}" ]] || { err "WAL-G не найден / не исполняемый: ${WALG_BIN}"; fail=1; } - command -v python3 &>/dev/null || { err "python3 не найден в PATH"; fail=1; } - (( fail == 0 )) -} - -# ── Вывод списка бэкапов ─────────────────────────────────────────────────────── -show_backups() { - local cfg="$1" - echo - separator - echo " Список доступных бэкапов:" - separator - - LAST_FULL_COUNT=0 - - # Запрашиваем список один раз; stderr отделяем, чтобы INFO-логи wal-g - # не попадали в таблицу, а при сбое — показать причину. - local out rc errfile - errfile="$(mktemp)" - out="$("${WALG_BIN}" backup-list --pretty --config "${cfg}" 2>"${errfile}")" - rc=$? - - if (( rc != 0 )); then - err "Не удалось получить список бэкапов (rc=${rc})" - [[ -s "${errfile}" ]] && cat "${errfile}" >&2 - rm -f "${errfile}" - return 1 - fi - rm -f "${errfile}" - - # Строки данных содержат 'base_'; delta-копии дополнительно — '_D_'. - local total delta full - total="$(printf '%s\n' "${out}" | grep -c 'base_')" || true - delta="$(printf '%s\n' "${out}" | grep -c '_D_')" || true - full=$(( total - delta )) - LAST_FULL_COUNT=${full} - - if (( total == 0 )); then - echo " (бэкапы не найдены)" - separator - echo - return 0 - fi - - # Таблица: FULL — зелёным, delta (_D_) — жёлтым, заголовок без цвета. - printf '%s\n' "${out}" | awk -v g="${C_GREEN}" -v y="${C_YELLOW}" -v r="${C_RESET}" ' - NR==1 { print " " $0; next } - /_D_/ { print " " y $0 r; next } - { print " " g $0 r } - ' - separator - echo - echo " Итого: FULL: ${full} · delta: ${delta} · всего: ${total}" - echo - return 0 -} - - - -# ══════════════════════════════════════════════════════════════════════════════ -# MAIN -# ══════════════════════════════════════════════════════════════════════════════ -main() { - check_env || exit 1 - - # ── Сканируем конфиги ──────────────────────────────────────────────────── - shopt -s nullglob - local cfg_files=( "${WALG_DIR}"/.walg-*.json ) - shopt -u nullglob - - if (( ${#cfg_files[@]} == 0 )); then - err "Конфиги не найдены: ${WALG_DIR}/.walg-*.json" - exit 1 - fi - - # ── Собираем список портов ─────────────────────────────────────────────── - declare -A port_to_cfg - local port - - for cfg in "${cfg_files[@]}"; do - port="$(get_port "${cfg}" 2>/dev/null)" || continue - [[ "${port}" =~ ^[0-9]+$ ]] || continue - port_to_cfg["${port}"]="${cfg}" - done - - if (( ${#port_to_cfg[@]} == 0 )); then - err "Не найдено ни одного корректного конфига с PGPORT" - exit 1 - fi - - # ── Выводим список портов ──────────────────────────────────────────────── - echo - separator - echo " Доступные кластеры PostgreSQL (WAL-G конфиги)" - separator - - local sorted_ports=() - mapfile -t sorted_ports < <(printf '%s\n' "${!port_to_cfg[@]}" | sort -n) - - local i=1 - declare -A idx_to_port - for port in "${sorted_ports[@]}"; do - printf " [%d] порт %-6s → %s\n" "$i" "${port}" "${port_to_cfg[${port}]}" - idx_to_port[$i]="${port}" - (( i++ )) - done - separator - echo - - # ── Запрашиваем выбор порта ────────────────────────────────────────────── - local choice - while true; do - read -rp "Введите номер кластера (1-$(( i-1 ))): " choice - [[ "${choice}" =~ ^[0-9]+$ ]] \ - && (( choice >= 1 && choice <= i-1 )) \ - && break - echo " ✗ Некорректный выбор, попробуйте снова." - done - - local selected_port="${idx_to_port[${choice}]}" - local selected_cfg="${port_to_cfg[${selected_port}]}" - - log "Выбран кластер: порт=${selected_port}, конфиг=${selected_cfg}" - - # ── Показываем список бэкапов ──────────────────────────────────────────── - show_backups "${selected_cfg}" || exit 1 - - # ── Если FULL-бэкапов нет — удалять нечего ──────────────────────────────── - if (( LAST_FULL_COUNT == 0 )); then - log "FULL-бэкапов нет — удалять нечего." - exit 0 - fi - - # ── Запрашиваем количество FULL-копий для хранения ─────────────────────── - local retain - while true; do - read -rp "Сколько FULL-бэкапов оставить? [введите число]: " retain - [[ "${retain}" =~ ^[1-9][0-9]*$ ]] && break - echo " ✗ Введите целое число больше 0." - done - - if (( retain >= LAST_FULL_COUNT )); then - echo - echo " ⚠ FULL-бэкапов всего ${LAST_FULL_COUNT}; при retain=${retain} удалять нечего." - fi - - # ── Итоговая сводка ────────────────────────────────────────────────────── - echo - separator - echo " Параметры удаления:" - echo " Кластер : порт ${selected_port}" - echo " Конфиг : ${selected_cfg}" - echo " Оставить FULL : ${retain}" - echo " Команда : wal-g delete retain FULL ${retain} --confirm" - separator - echo - - # ── Запрашиваем подтверждение ──────────────────────────────────────────── - local confirm - read -rp "Для подтверждения введите YES (заглавными буквами): " confirm - - if [[ "${confirm}" != "YES" ]]; then - echo - log "Операция отменена пользователем." - exit 0 - fi - - # ── Выполняем удаление ─────────────────────────────────────────────────── - echo - log "Запуск удаления: port=${selected_port}, retain FULL ${retain}" - echo - - "${WALG_BIN}" delete retain FULL "${retain}" --confirm --config "${selected_cfg}" - local rc=$? - - echo - if (( rc == 0 )); then - log "Удаление завершено успешно (port=${selected_port})" - else - err "Удаление завершилось с ошибкой rc=${rc} (port=${selected_port})" - exit "${rc}" - fi -} - -main "$@" diff --git a/pgsql/pgpro/wal-g-scripts/wal-g-install.sh b/pgsql/pgpro/wal-g-scripts/wal-g-install.sh deleted file mode 100755 index 685ecab..0000000 --- a/pgsql/pgpro/wal-g-scripts/wal-g-install.sh +++ /dev/null @@ -1,316 +0,0 @@ -#!/bin/bash -# ============================================================================== -# wal-g-install.sh — установка набора WAL-G-скриптов и systemd-задания -# ============================================================================== -# Команды: -# wal-g-install.sh [install] полная установка: скрипты + systemd-юниты -# wal-g-install.sh units только установить/обновить systemd-юниты -# wal-g-install.sh uninstall-units удалить systemd-юниты (disable + rm) -# wal-g-install.sh uninstall удалить юниты и установленные скрипты -# -# Версия PostgreSQL (PGVER) определяется автоматически (systemd-юниты -# postgrespro-* и каталоги /var/lib/pgpro//), иначе запрашивается. -# Запускать от root. -# ============================================================================== - -set -uo pipefail - -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" - -# ── Значения по умолчанию ──────────────────────────────────────────────────── -DEFAULT_INSTALL_DIR="/var/lib/pgpro/_backup/scripts" -DEFAULT_ONCALENDAR="*-*-* 01:15:00" -SYSTEMD_DIR="/etc/systemd/system" -UNIT_SERVICE="backup-pgsql.service" -UNIT_TIMER="backup-pgsql.timer" -BIN_SYMLINK="/usr/local/bin/wal-g-menu" - -# Заполняются в do_install / units -INSTALL_DIR="" -ONCALENDAR="" - -# ── Цвета ──────────────────────────────────────────────────────────────────── -if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then - RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' - CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' -else - RED='' GREEN='' YELLOW='' CYAN='' BOLD='' NC='' -fi -log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } -log_sect() { echo -e "\n${CYAN}${BOLD}>>> $* ${NC}"; } - -require_root() { - if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then - log_error "Требуются права root." - exit 1 - fi -} - -# ── Определение версий PostgreSQL ──────────────────────────────────────────── -# Печатает уникальные версии (по одной в строке), отсортированные. -detect_pgver() { - local -A seen=() - local v b d unit - - if command -v systemctl >/dev/null 2>&1; then - while read -r unit _; do - [[ "$unit" =~ ^postgrespro-(.+)\.service$ ]] || continue - v="${BASH_REMATCH[1]}" - v="${v%@}" # postgrespro-1c-18@.service → 1c-18 - [[ -n "$v" ]] && seen["$v"]=1 - done < <(systemctl list-unit-files 'postgrespro-*.service' --no-legend 2>/dev/null) - fi - - shopt -s nullglob - for d in /var/lib/pgpro/*/; do - b="$(basename "${d%/}")" - [[ "$b" == "_backup" ]] && continue # служебный каталог - seen["$b"]=1 - done - shopt -u nullglob - - (( ${#seen[@]} > 0 )) && printf '%s\n' "${!seen[@]}" | sort -} - -# Возвращает выбранную версию на stdout; диалоги/сообщения — на stderr, -# поэтому безопасно вызывать как PGVER="$(resolve_pgver)". -resolve_pgver() { - local -a cands=() - mapfile -t cands < <(detect_pgver) - - local def="" - (( ${#cands[@]} >= 1 )) && def="${cands[0]}" - - if (( ${#cands[@]} > 1 )); then - { - echo "Обнаружено несколько версий PostgreSQL:" - local i=1 c - for c in "${cands[@]}"; do echo " $i) $c"; ((i++)); done - } >&2 - if [[ -t 0 ]]; then - local sel - read -rp "Выберите номер версии [1]: " sel || sel="" - sel="${sel:-1}" - if [[ "$sel" =~ ^[0-9]+$ ]] && (( sel >= 1 && sel <= ${#cands[@]} )); then - def="${cands[$((sel-1))]}" - fi - fi - elif (( ${#cands[@]} == 1 )); then - echo "INFO: обнаружена версия PostgreSQL: ${def}" >&2 - else - echo "WARN: версия PostgreSQL не обнаружена автоматически." >&2 - fi - - # Переменная окружения PGVER имеет приоритет как умолчание. - def="${PGVER:-${def:-1c-18}}" - - if [[ -t 0 ]]; then - local ans - read -rp "Версия PostgreSQL (PGVER) [${def}]: " ans || ans="" - def="${ans:-$def}" - fi - - printf '%s\n' "$def" -} - -# ── Текущие значения из уже установленных юнитов (для refresh) ─────────────── -current_install_dir() { - local f="${SYSTEMD_DIR}/${UNIT_SERVICE}" line - [[ -f "$f" ]] || return 1 - line="$(grep -m1 '^ExecStart=' "$f" 2>/dev/null)" || return 1 - line="${line#ExecStart=}" - [[ -n "$line" ]] || return 1 - dirname "$line" -} -current_oncalendar() { - local f="${SYSTEMD_DIR}/${UNIT_TIMER}" line - [[ -f "$f" ]] || return 1 - line="$(grep -m1 '^OnCalendar=' "$f" 2>/dev/null)" || return 1 - echo "${line#OnCalendar=}" -} - -# ── Установка скриптов ─────────────────────────────────────────────────────── -install_scripts() { - install -d -m 755 "$INSTALL_DIR" || { log_error "Не удалось создать ${INSTALL_DIR}"; return 1; } - - local src_real dst_real - src_real="$(cd "$SCRIPT_DIR" && pwd -P)" - dst_real="$(cd "$INSTALL_DIR" && pwd -P)" - if [[ "$src_real" == "$dst_real" ]]; then - log_warn "Каталог установки совпадает с каталогом исходников — копирование скриптов пропущено." - return 0 - fi - - local -a files=() - shopt -s nullglob - files=( "$SCRIPT_DIR"/wal-g-*.sh ) - shopt -u nullglob - if (( ${#files[@]} == 0 )); then - log_error "В каталоге ${SCRIPT_DIR} не найдено ни одного wal-g-*.sh" - return 1 - fi - - local f - for f in "${files[@]}"; do - install -m 755 "$f" "$INSTALL_DIR/" - done - log_info "Установлено скриптов: ${#files[@]} → ${INSTALL_DIR}" -} - -# ── Генерация и установка systemd-юнитов ───────────────────────────────────── -install_units() { - local pgver="$1" - : "${INSTALL_DIR:=$DEFAULT_INSTALL_DIR}" - : "${ONCALENDAR:=$DEFAULT_ONCALENDAR}" - - cat > "${SYSTEMD_DIR}/${UNIT_SERVICE}" < "${SYSTEMD_DIR}/${UNIT_TIMER}" </dev/null 2>&1; then - systemctl daemon-reload || log_warn "systemctl daemon-reload завершился с ошибкой." - else - log_warn "systemctl недоступен — пропускаю daemon-reload." - fi -} - -uninstall_units() { - if command -v systemctl >/dev/null 2>&1; then - systemctl disable --now "${UNIT_TIMER}" 2>/dev/null || true - systemctl stop "${UNIT_SERVICE}" 2>/dev/null || true - fi - rm -f "${SYSTEMD_DIR}/${UNIT_TIMER}" "${SYSTEMD_DIR}/${UNIT_SERVICE}" - log_info "Юниты удалены." - command -v systemctl >/dev/null 2>&1 && { systemctl daemon-reload || true; } -} - -# ── Полная установка ───────────────────────────────────────────────────────── -do_install() { - log_sect "Установка набора WAL-G" - - local pgver - pgver="$(resolve_pgver)" - - read -rp "Каталог установки скриптов [${DEFAULT_INSTALL_DIR}]: " INSTALL_DIR - INSTALL_DIR="${INSTALL_DIR:-$DEFAULT_INSTALL_DIR}" - - read -rp "Расписание бэкапа (OnCalendar) [${DEFAULT_ONCALENDAR}]: " ONCALENDAR - ONCALENDAR="${ONCALENDAR:-$DEFAULT_ONCALENDAR}" - - install_scripts || { log_error "Установка скриптов не удалась."; exit 1; } - install_units "$pgver" - - # Симлинк на оболочку для удобного запуска - read -rp "Создать симлинк ${BIN_SYMLINK} → wal-g-menu.sh? [Y/n]: " mk - if [[ "${mk,,}" != "n" ]]; then - if ln -sfn "${INSTALL_DIR}/wal-g-menu.sh" "$BIN_SYMLINK"; then - log_info "Симлинк создан: ${BIN_SYMLINK}" - else - log_warn "Не удалось создать симлинк ${BIN_SYMLINK}" - fi - fi - - # Включение таймера - if command -v systemctl >/dev/null 2>&1; then - read -rp "Включить и запустить таймер ${UNIT_TIMER} сейчас? [Y/n]: " en - if [[ "${en,,}" != "n" ]]; then - if systemctl enable --now "${UNIT_TIMER}"; then - log_info "Таймер включён." - else - log_warn "Не удалось включить таймер." - fi - fi - fi - - log_sect "Установка завершена" - log_info "Запуск оболочки: ${INSTALL_DIR}/wal-g-menu.sh" - [[ -L "$BIN_SYMLINK" ]] && log_info " или: wal-g-menu" -} - -do_uninstall() { - log_sect "Удаление набора WAL-G" - uninstall_units - - local dir - dir="$(current_install_dir 2>/dev/null || true)" - dir="${dir:-$DEFAULT_INSTALL_DIR}" - - read -rp "Удалить установленные скрипты из ${dir}? [y/N]: " rm_scripts - if [[ "${rm_scripts,,}" == "y" ]]; then - rm -f "${dir}"/wal-g-*.sh - rmdir "${dir}" 2>/dev/null || true - log_info "Скрипты удалены из ${dir}" - fi - - [[ -L "$BIN_SYMLINK" ]] && { rm -f "$BIN_SYMLINK"; log_info "Симлинк ${BIN_SYMLINK} удалён."; } - log_sect "Удаление завершено" -} - -usage() { - sed -n '2,16p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' -} - -# ── Точка входа ────────────────────────────────────────────────────────────── -CMD="${1:-install}" -case "$CMD" in - install) - require_root - do_install - ;; - units) - require_root - INSTALL_DIR="$(current_install_dir 2>/dev/null || echo "$DEFAULT_INSTALL_DIR")" - ONCALENDAR="$(current_oncalendar 2>/dev/null || echo "$DEFAULT_ONCALENDAR")" - PGVER_RESOLVED="$(resolve_pgver)" - install_units "$PGVER_RESOLVED" - ;; - uninstall-units) - require_root - uninstall_units - ;; - uninstall) - require_root - do_uninstall - ;; - -h|--help|help) - usage - ;; - *) - log_error "Неизвестная команда: ${CMD}" - usage - exit 1 - ;; -esac diff --git a/pgsql/pgpro/wal-g-scripts/wal-g-list.sh b/pgsql/pgpro/wal-g-scripts/wal-g-list.sh deleted file mode 100644 index 92b19f6..0000000 --- a/pgsql/pgpro/wal-g-scripts/wal-g-list.sh +++ /dev/null @@ -1,310 +0,0 @@ -#!/bin/bash -# ============================================================================== -# wal-g-list.sh — интерактивный вывод списка WAL-G бэкапов -# ============================================================================== -set -euo pipefail - -# ── Константы ──────────────────────────────────────────────────────────────── -WALG_DIR="/etc/wal-g.d" -WALG_BIN="${WALG_BIN:-/usr/bin/wal-g}" - -# ── Цвета (только в интерактивный TTY и при отсутствии NO_COLOR) ────────────── -if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then - RED='\033[0;31m' - GREEN='\033[0;32m' - YELLOW='\033[1;33m' - CYAN='\033[0;36m' - BOLD='\033[1m' - RESET='\033[0m' -else - RED='' GREEN='' YELLOW='' CYAN='' BOLD='' RESET='' -fi - -# ── Постраничный вывод ─────────────────────────────────────────────────────── -# Прогоняем вывод через пейджер, только когда он не влезает на экран. -# less -R — сохранить ANSI-цвета -# -F — не запускать пейджер, если вывод помещается в один экран -# -X — не очищать экран при выходе (вывод остаётся в истории терминала) -# Пейджер задействуется лишь в интерактивном TTY; иначе (пайп/файл) — обычный cat, -# чтобы не ломать скриптовое использование. Переопределяется через $PAGER. -page_output() { - if [[ -t 1 ]]; then - if [[ -n "${PAGER:-}" ]]; then - eval "$PAGER" - elif command -v less >/dev/null 2>&1; then - less -RFX - else - cat - fi - else - cat - fi -} - -# Временный файл для накопления вывода перед пейджером (чистим на выходе). -TMPFILE="" -cleanup() { [[ -n "$TMPFILE" ]] && rm -f "$TMPFILE"; } -trap cleanup EXIT - -# ── Проверка зависимостей ──────────────────────────────────────────────────── -if [[ ! -x "$WALG_BIN" ]]; then - echo "ERROR: wal-g not found or not executable: $WALG_BIN" >&2 - exit 1 -fi - -# ── Проверка директории ────────────────────────────────────────────────────── -if [[ ! -d "$WALG_DIR" ]]; then - echo "ERROR: directory not found: $WALG_DIR" >&2 - exit 1 -fi - -# ── Сбор конфигов ──────────────────────────────────────────────────────────── -mapfile -t CFGS < <(find "$WALG_DIR" -maxdepth 1 -type f -name ".walg-*.json" | sort) - -if [[ ${#CFGS[@]} -eq 0 ]]; then - echo "ERROR: no wal-g configs found in $WALG_DIR (expected .walg-*.json)" >&2 - exit 2 -fi - -# ── Парсинг флагов ─────────────────────────────────────────────────────────── -FULL_ONLY=false - -print_usage() { - echo "Usage: $0 [--full|-f] [PORT|all]" - echo - echo " --full, -f показывать только FULL-бэкапы (без дифференциальных)" - echo " PORT номер порта PostgreSQL (1–65535)" - echo " all обработать все найденные конфиги" - echo - echo " NO_COLOR=1 отключить ANSI-цвета принудительно" -} - -POSITIONAL=() -while [[ $# -gt 0 ]]; do - case "$1" in - --full|-f) - FULL_ONLY=true - shift - ;; - --help|-h) - print_usage - exit 0 - ;; - -*) - echo "ERROR: unknown option: $1" >&2 - print_usage >&2 - exit 1 - ;; - *) - POSITIONAL+=("$1") - shift - ;; - esac -done -set -- "${POSITIONAL[@]+"${POSITIONAL[@]}"}" - -# ── Извлечь порт из имени конфига ──────────────────────────────────────────── -extract_port() { - local cfg="$1" - local base - base="$(basename "$cfg")" - base="${base#.walg-}" - echo "${base%.json}" -} - -# ── Вывод меню доступных кластеров ─────────────────────────────────────────── -print_menu() { - echo - echo -e "${BOLD}=== Доступные кластеры ===${RESET}" - echo - - local i=1 - for cfg in "${CFGS[@]}"; do - local port - port="$(extract_port "$cfg")" - - if [[ -f "$cfg" && -r "$cfg" ]]; then - echo -e " ${BOLD}${i}.${RESET} [ ${GREEN}OK${RESET} ] Порт ${port}" - else - echo -e " ${BOLD}${i}.${RESET} [ ${RED}FAIL${RESET} ] Порт ${port} — конфиг недоступен" - fi - - i=$(( i + 1 )) - done - - echo - echo -e " ${BOLD}${i}.${RESET} Все кластеры" - echo -e " ${BOLD}0.${RESET} Выход" - echo -} - -# ── Функция: запуск backup-list для одного конфига ─────────────────────────── -# Возвращает 0 при успехе (включая «нет бэкапов»), 1 при сбое wal-g/конфига. -run_backup_list() { - local cfg="$1" - local port - port="$(extract_port "$cfg")" - - echo - echo -e "${BOLD}=== WAL-G backup-list · Порт ${port} ===${RESET}" - echo - - if [[ ! -r "$cfg" ]]; then - echo -e "${RED}ERROR: конфиг недоступен для чтения: ${cfg}${RESET}" >&2 - return 1 - fi - - # Захватываем вывод целиком, чтобы отделить сбой wal-g от пустого результата - # (иначе grep-фильтр без совпадений ложно трактуется как ошибка под pipefail). - local out rc - out="$("$WALG_BIN" backup-list --pretty --config "$cfg" 2>&1)" - rc=$? - - if (( rc != 0 )); then - echo -e "${RED}ERROR: backup-list завершился с кодом ${rc}${RESET}" >&2 - [[ -n "$out" ]] && echo "$out" >&2 - return 1 - fi - - # Подсчёт: строки бэкапов содержат 'base_', delta — дополнительно '_D_'. - local total delta full - total="$(printf '%s\n' "$out" | grep -c 'base_' || true)" - delta="$(printf '%s\n' "$out" | grep -c '_D_' || true)" - full=$(( total - delta )) - - if (( total == 0 )); then - echo " (бэкапы не найдены)" - echo - return 0 - fi - - # Вывод таблицы — с фильтром FULL-only или полностью. - if [[ "$FULL_ONLY" == "true" ]]; then - if (( full == 0 )); then - echo " (FULL-бэкапов нет; всего delta: ${delta})" - else - printf '%s\n' "$out" | grep -v '_D_' || true - fi - else - printf '%s\n' "$out" - fi - - # Сводка: счётчики + самый свежий бэкап (последняя строка таблицы). - local latest_line latest_name latest_date summary - latest_line="$(printf '%s\n' "$out" | grep 'base_' | tail -n1 || true)" - latest_name="$(printf '%s\n' "$latest_line" | grep -oE 'base_[0-9A-Za-z_]+' | head -n1 || true)" - latest_date="$(printf '%s\n' "$latest_line" \ - | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}(:[0-9]{2})?' | head -n1 || true)" - - summary="FULL: ${full} · delta: ${delta} · всего: ${total}" - [[ -n "$latest_name" ]] && summary+=" · последний: ${latest_name}" - [[ -n "$latest_date" ]] && summary+=" (${latest_date})" - - echo - echo -e "${CYAN} Сводка: ${summary}${RESET}" - return 0 -} - -# ── Обработка набора конфигов с постраничным выводом ───────────────────────── -# Накапливаем stdout всех run_backup_list во временный файл и отдаём пейджеру, -# который сам решает, нужна ли постраничность. Ошибки (stderr) идут напрямую в -# терминал, не попадая в пейджер, чтобы сохранить потоковую семантику. -# Возвращает 0 при успехе, 5 если хотя бы один конфиг завершился сбоем. -run_selected() { - local errors=0 cfg - TMPFILE="$(mktemp "${TMPDIR:-/tmp}/walg-list.XXXXXX")" || { - echo "ERROR: не удалось создать временный файл" >&2 - return 1 - } - - for cfg in "$@"; do - run_backup_list "$cfg" >>"$TMPFILE" || (( errors++ )) || true - done - - page_output <"$TMPFILE" - - if (( errors > 0 )); then - echo -e "${YELLOW}WARN: $errors config(s) failed${RESET}" >&2 - return 5 - fi - return 0 -} - -# ── Режим: аргумент передан из CLI ─────────────────────────────────────────── -if [[ $# -ge 1 ]]; then - INPUT="$1" - - if [[ "${INPUT,,}" == "all" ]]; then - run_selected "${CFGS[@]}" || exit $? - exit 0 - fi - - if [[ ! "$INPUT" =~ ^[1-9][0-9]{0,4}$ ]] || (( INPUT > 65535 )); then - echo "ERROR: invalid port: '$INPUT' (expected 1–65535, no leading zeros)" >&2 - exit 3 - fi - - WALG_CONFIG_FILE="$WALG_DIR/.walg-${INPUT}.json" - if [[ ! -f "$WALG_CONFIG_FILE" ]]; then - echo "ERROR: config not found for port $INPUT: $WALG_CONFIG_FILE" >&2 - exit 4 - fi - - run_selected "$WALG_CONFIG_FILE" || exit 5 - exit 0 -fi - -# ── Интерактивный режим ─────────────────────────────────────────────────────── -if [[ ! -t 0 ]]; then - echo "ERROR: no TTY and no argument provided. Usage: $0 [--full|-f] [PORT|all]" >&2 - exit 1 -fi - -TOTAL="${#CFGS[@]}" -ALL_IDX=$(( TOTAL + 1 )) - -# ── 1. Показываем меню ─────────────────────────────────────────────────────── -print_menu - -# ── 2. Выбор кластера ──────────────────────────────────────────────────────── -SELECTED_CFGS=() - -while true; do - read -r -p "Выберите номер [0–${ALL_IDX}]: " CHOICE - - if [[ "$CHOICE" == "0" ]]; then - echo "Выход." - exit 0 - fi - - if [[ ! "$CHOICE" =~ ^[0-9]+$ ]]; then - echo -e "${RED}Некорректный ввод. Введите число от 0 до ${ALL_IDX}.${RESET}" >&2 - continue - fi - - if (( CHOICE == ALL_IDX )); then - SELECTED_CFGS=( "${CFGS[@]}" ) - break - fi - - if (( CHOICE >= 1 && CHOICE <= TOTAL )); then - SELECTED_CFGS=( "${CFGS[CHOICE-1]}" ) - break - fi - - echo -e "${RED}Номер вне диапазона. Введите от 0 до ${ALL_IDX}.${RESET}" >&2 -done - -# ── 3. Вопрос про FULL-only (только если флаг не задан через CLI) ──────────── -if [[ "$FULL_ONLY" == "false" ]]; then - echo - read -r -p "Показывать только FULL-бэкапы? [y/N]: " FULL_ANSWER - if [[ "${FULL_ANSWER,,}" =~ ^y(es)?$ ]]; then - FULL_ONLY=true - fi -fi - -# ── 4. Вывод бэкапов ───────────────────────────────────────────────────────── -run_selected "${SELECTED_CFGS[@]}" || exit 5 - -exit 0 diff --git a/pgsql/pgpro/wal-g-scripts/wal-g-menu.sh b/pgsql/pgpro/wal-g-scripts/wal-g-menu.sh deleted file mode 100755 index 5b48c1f..0000000 --- a/pgsql/pgpro/wal-g-scripts/wal-g-menu.sh +++ /dev/null @@ -1,255 +0,0 @@ -#!/bin/bash -# ============================================================================== -# wal-g-menu.sh — единая интерактивная оболочка для набора WAL-G-скриптов -# ============================================================================== -# Запускает соседние скрипты (cfg/list/backup/backup-all/delete/restore) из -# одного меню. Скрипты ищутся рядом с этим файлом, поэтому оболочку можно -# вызывать из любого каталога. Запускать от root (большинство операций требует -# прав root и переключения на пользователя postgres). -# ============================================================================== - -# Без -e: сбой дочернего скрипта не должен ронять меню — возвращаемся в него. -set -uo pipefail - -# ── Каталог набора (рядом с этим скриптом) ─────────────────────────────────── -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" - -# ── Цвета (только в интерактивный TTY и при отсутствии NO_COLOR) ────────────── -if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then - RED='\033[0;31m' - GREEN='\033[0;32m' - YELLOW='\033[1;33m' - CYAN='\033[0;36m' - BOLD='\033[1m' - NC='\033[0m' -else - RED='' GREEN='' YELLOW='' CYAN='' BOLD='' NC='' -fi - -log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } -log_section() { echo -e "\n${CYAN}${BOLD}>>> $* ${NC}"; } - -# Прерывание (Ctrl-C) внутри дочернего скрипта не должно завершать меню. -trap 'echo; log_warn "Прервано пользователем."' INT - -# ── Описание пунктов меню (данные, а не код) ───────────────────────────────── -MENU_KEYS=(1 2 3 4 5 6) -declare -A MENU_LABEL MENU_SCRIPT MENU_DESC - -MENU_LABEL[1]="Настройка WAL-G" -MENU_SCRIPT[1]="wal-g-cfg.sh" -MENU_DESC[1]="создать/обновить конфиги и параметры архивации PostgreSQL" - -MENU_LABEL[2]="Список бэкапов" -MENU_SCRIPT[2]="wal-g-list.sh" -MENU_DESC[2]="показать доступные бэкапы по кластерам" - -MENU_LABEL[3]="Создать бэкап" -MENU_SCRIPT[3]="wal-g-backup.sh" -MENU_DESC[3]="интерактивный delta/FULL-бэкап выбранных кластеров" - -MENU_LABEL[4]="Бэкап всех кластеров" -MENU_SCRIPT[4]="wal-g-backup-all.sh" -MENU_DESC[4]="неинтерактивный прогон (как в systemd-таймере) + retain" - -MENU_LABEL[5]="Удалить бэкапы" -MENU_SCRIPT[5]="wal-g-delete.sh" -MENU_DESC[5]="удаление старых бэкапов (retain FULL N)" - -MENU_LABEL[6]="Восстановление" -MENU_SCRIPT[6]="wal-g-restore.sh" -MENU_DESC[6]="восстановление кластера из бэкапа (в т.ч. PITR)" - -# ── Проверки окружения ─────────────────────────────────────────────────────── -if [[ ! -t 0 ]]; then - log_error "Нужен интерактивный терминал (stdin не является TTY)." - exit 1 -fi - -if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then - log_warn "Оболочка запущена не от root — большинство операций потребует прав root." -fi - -# ── Вывод меню ─────────────────────────────────────────────────────────────── -print_menu() { - echo - echo -e "${BOLD}============================================================${NC}" - echo -e "${BOLD} WAL-G — панель управления${NC}" - echo -e "${BOLD}============================================================${NC}" - echo -e " Каталог скриптов: ${CYAN}${SCRIPT_DIR}${NC}" - echo - local k script_path mark - for k in "${MENU_KEYS[@]}"; do - script_path="${SCRIPT_DIR}/${MENU_SCRIPT[$k]}" - if [[ -f "$script_path" ]]; then - mark="${GREEN}●${NC}" - else - mark="${RED}✗${NC}" - fi - printf " ${BOLD}%s.${NC} %b %-24s — %s\n" \ - "$k" "$mark" "${MENU_LABEL[$k]}" "${MENU_DESC[$k]}" - done - echo - local imark - [[ -f "$INSTALLER" ]] && imark="${GREEN}●${NC}" || imark="${RED}✗${NC}" - printf " ${BOLD}7.${NC} %b %-24s — %s\n" "$imark" "Задание systemd" "просмотр/включение/удаление таймера бэкапа" - printf " ${BOLD}8.${NC} %b %-24s — %s\n" "$imark" "Установка набора" "развернуть скрипты и юниты (wal-g-install.sh)" - echo - echo -e " ${BOLD}0.${NC} Выход" - echo -} - -# ── Запуск выбранного скрипта ──────────────────────────────────────────────── -run_script() { - local script="$1" - local path="${SCRIPT_DIR}/${script}" - - if [[ ! -f "$path" ]]; then - log_error "Скрипт не найден: ${path}" - return 1 - fi - - log_section "Запуск: ${script}" - # Запускаем через интерпретатор, чтобы не зависеть от бита исполнения. - bash "$path" - local rc=$? - - echo - if (( rc == 0 )); then - log_info "«${script}» завершился успешно (rc=0)." - else - log_warn "«${script}» завершился с кодом ${rc}." - fi - return "$rc" -} - -# ── Пауза перед возвратом в меню ───────────────────────────────────────────── -pause() { - echo - read -rsp "Нажмите Enter для возврата в меню..." _ || true - echo -} - -# ── Вызов инсталлятора (общий источник логики systemd-юнитов) ──────────────── -INSTALLER="${SCRIPT_DIR}/wal-g-install.sh" - -run_installer() { - if [[ ! -f "$INSTALLER" ]]; then - log_error "Инсталлятор не найден: ${INSTALLER}" - return 1 - fi - bash "$INSTALLER" "$@" -} - -# ── Статус systemd-задания ─────────────────────────────────────────────────── -UNIT_TIMER="backup-pgsql.timer" -UNIT_SERVICE="backup-pgsql.service" -SYSTEMD_DIR="/etc/systemd/system" - -systemd_status() { - log_section "Статус задания резервного копирования" - - if ! command -v systemctl >/dev/null 2>&1; then - log_warn "systemctl недоступен в этой системе." - return 0 - fi - - local svc_file="${SYSTEMD_DIR}/${UNIT_SERVICE}" - local tmr_file="${SYSTEMD_DIR}/${UNIT_TIMER}" - - if [[ -f "$tmr_file" || -f "$svc_file" ]]; then - log_info "Файлы юнитов: $( [[ -f "$svc_file" ]] && echo "${UNIT_SERVICE} ✓" || echo "${UNIT_SERVICE} ✗" ), $( [[ -f "$tmr_file" ]] && echo "${UNIT_TIMER} ✓" || echo "${UNIT_TIMER} ✗" )" - else - log_warn "Юниты не установлены (нет файлов в ${SYSTEMD_DIR})." - return 0 - fi - - echo -e " enabled : ${BOLD}$(systemctl is-enabled "${UNIT_TIMER}" 2>/dev/null || echo '—')${NC}" - echo -e " active : ${BOLD}$(systemctl is-active "${UNIT_TIMER}" 2>/dev/null || echo '—')${NC}" - echo - systemctl list-timers --all "${UNIT_TIMER}" --no-pager 2>/dev/null || true -} - -# ── Подменю: управление systemd-заданием ───────────────────────────────────── -systemd_submenu() { - while true; do - echo - echo -e "${BOLD}--- Задание systemd (резервное копирование) ---${NC}" - echo -e " ${BOLD}1.${NC} Показать статус" - echo -e " ${BOLD}2.${NC} Установить/обновить юниты и включить" - echo -e " ${BOLD}3.${NC} Отключить (disable --now)" - echo -e " ${BOLD}4.${NC} Удалить юниты" - echo -e " ${BOLD}0.${NC} Назад" - echo - local c - read -rp "Выберите пункт [0-4]: " c || { echo; return 0; } - - case "$c" in - 1) systemd_status; pause ;; - 2) - run_installer units || true - if command -v systemctl >/dev/null 2>&1; then - if systemctl enable --now "${UNIT_TIMER}"; then - log_info "Таймер ${UNIT_TIMER} включён." - else - log_warn "Не удалось включить таймер ${UNIT_TIMER}." - fi - fi - pause - ;; - 3) - if command -v systemctl >/dev/null 2>&1; then - systemctl disable --now "${UNIT_TIMER}" && log_info "Таймер отключён." \ - || log_warn "Не удалось отключить таймер." - else - log_warn "systemctl недоступен." - fi - pause - ;; - 4) - read -rp "Удалить юниты ${UNIT_TIMER}/${UNIT_SERVICE}? [y/N]: " yn - if [[ "${yn,,}" == "y" ]]; then - run_installer uninstall-units || true - else - log_info "Отменено." - fi - pause - ;; - 0|q|Q) return 0 ;; - *) log_warn "Некорректный ввод: '${c}'." ;; - esac - done -} - -# ── Главный цикл ───────────────────────────────────────────────────────────── -while true; do - print_menu - read -rp "Выберите пункт [0-8]: " CHOICE || { echo; exit 0; } - - case "$CHOICE" in - 0|q|Q|exit) - echo "Выход." - exit 0 - ;; - 7) - systemd_submenu - ;; - 8) - run_installer install || true - pause - ;; - [1-9]*) - if [[ -n "${MENU_SCRIPT[$CHOICE]:-}" ]]; then - run_script "${MENU_SCRIPT[$CHOICE]}" || true - pause - else - log_warn "Нет такого пункта: ${CHOICE}" - fi - ;; - *) - log_warn "Некорректный ввод: '${CHOICE}'. Введите число из меню." - ;; - esac -done diff --git a/pgsql/pgpro/wal-g-scripts/wal-g-restore.sh b/pgsql/pgpro/wal-g-scripts/wal-g-restore.sh deleted file mode 100644 index 87a6d87..0000000 --- a/pgsql/pgpro/wal-g-scripts/wal-g-restore.sh +++ /dev/null @@ -1,324 +0,0 @@ -#!/bin/bash - -# ============================================================================= -# WAL-G Interactive Restore Script -# ============================================================================= - -set -euo pipefail - -PGVER="${PGVER:-1c-18}" -CONFIG_DIR="/etc/wal-g.d" -CONFIG_PREFIX=".walg-" -CONFIG_SUFFIX=".json" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } -log_section() { echo -e "\n${CYAN}${BOLD}>>> $* ${NC}"; } - -# Имя systemd-юнита (как в wal-g-cfg.sh): для 5432 — без инстанса, -# для прочих портов — шаблонный юнит postgrespro-@. -get_service_name() { - local pgver="$1" pgport="$2" - if [[ "$pgport" == "5432" ]]; then - echo "postgrespro-${pgver}.service" - else - echo "postgrespro-${pgver}@${pgport}.service" - fi -} - -# Чтение значения ключа из JSON-конфига wal-g (через python3, если доступен). -get_cfg_value() { - local cfg="$1" key="$2" - command -v python3 >/dev/null 2>&1 || return 1 - python3 - "$cfg" "$key" <<'PY' -import json, sys -try: - with open(sys.argv[1], encoding="utf-8") as f: - val = json.load(f).get(sys.argv[2], "") - print(str(val).strip()) -except Exception: - sys.exit(1) -PY -} - -# ============================================================================= -# Шаг 1: Выбор порта / конфига -# ============================================================================= -log_section "Поиск конфигурационных файлов WAL-G" - -mapfile -t CONFIG_FILES < <(find "$CONFIG_DIR" -maxdepth 1 -name "${CONFIG_PREFIX}*${CONFIG_SUFFIX}" 2>/dev/null | sort) - -if [[ ${#CONFIG_FILES[@]} -eq 0 ]]; then - log_error "Конфиги не найдены в $CONFIG_DIR" - exit 1 -fi - -declare -a PORTS=() -for cfg in "${CONFIG_FILES[@]}"; do - filename=$(basename "$cfg") - port="${filename#${CONFIG_PREFIX}}" - port="${port%${CONFIG_SUFFIX}}" - PORTS+=("$port") -done - -echo "" -echo -e "${BOLD}Доступные порты PostgreSQL:${NC}" -for i in "${!PORTS[@]}"; do - echo -e " ${CYAN}[$((i+1))]${NC} Порт: ${BOLD}${PORTS[$i]}${NC} → ${CONFIG_FILES[$i]}" -done -echo "" - -while true; do - read -rp "Выберите номер (1-${#PORTS[@]}): " CHOICE - if [[ "$CHOICE" =~ ^[0-9]+$ ]] && (( CHOICE >= 1 && CHOICE <= ${#PORTS[@]} )); then - IDX=$((CHOICE - 1)) - PGPORT="${PORTS[$IDX]}" - WALG_CONFIG_FILE="${CONFIG_FILES[$IDX]}" - break - fi - log_warn "Некорректный ввод, попробуйте снова." -done - -log_info "Выбран порт: ${BOLD}$PGPORT${NC}" -log_info "Конфиг: ${BOLD}$WALG_CONFIG_FILE${NC}" - -# ============================================================================= -# Шаг 2: Определение PGDATA -# ============================================================================= -log_section "Определение PGDATA" - -# Приоритет — PGDATA из конфига (там записан фактический путь кластера); -# если получить не удалось, используем путь по умолчанию. -RESTORE_PGDATA="$(get_cfg_value "$WALG_CONFIG_FILE" PGDATA 2>/dev/null || true)" -if [[ -n "$RESTORE_PGDATA" ]]; then - log_info "PGDATA из конфига: ${BOLD}$RESTORE_PGDATA${NC}" -else - RESTORE_PGDATA="/var/lib/pgpro/${PGVER}/data-${PGPORT}" - log_warn "PGDATA из конфига не получен; путь по умолчанию: ${BOLD}$RESTORE_PGDATA${NC}" -fi - -read -rp "Использовать этот путь? [Y/n]: " CONFIRM_DATA -if [[ "${CONFIRM_DATA,,}" == "n" ]]; then - read -rp "Введите путь PGDATA: " CUSTOM_DATA - [[ -z "$CUSTOM_DATA" ]] && { log_error "Путь не может быть пустым."; exit 1; } - RESTORE_PGDATA="$CUSTOM_DATA" - log_info "Используется путь: ${BOLD}$RESTORE_PGDATA${NC}" -fi - -# Версия PostgreSQL нужна для имени systemd-юнита. Неверная версия → проверка -# is-active бьёт по чужому юниту и может не заметить ЗАПУЩЕННЫЙ кластер, что -# приведёт к перезаписи живого PGDATA. Пытаемся вывести версию из пути -# (.../pgpro//...), иначе берём env/умолчание, и подтверждаем у оператора. -PGVER_DEFAULT="$PGVER" -if [[ "$RESTORE_PGDATA" =~ /pgpro/([^/]+)/ ]]; then - PGVER_DEFAULT="${BASH_REMATCH[1]}" -fi -read -rp "Версия PostgreSQL (PGVER) [${PGVER_DEFAULT}]: " PGVER_INPUT -PGVER="${PGVER_INPUT:-$PGVER_DEFAULT}" -log_info "PGVER: ${BOLD}$PGVER${NC} → юнит: ${BOLD}$(get_service_name "$PGVER" "$PGPORT")${NC}" - -# ============================================================================= -# Шаг 3: Выбор точки восстановления -# ============================================================================= -log_section "Точка восстановления" - -echo -e "${BOLD}Доступные бэкапы:${NC}" -su - postgres -c "/usr/bin/wal-g backup-list --config $WALG_CONFIG_FILE" 2>/dev/null || { - log_warn "Не удалось получить список бэкапов." -} -echo "" - -echo -e "${BOLD}Варианты восстановления:${NC}" -echo -e " ${CYAN}[1]${NC} LATEST (последний бэкап)" -echo -e " ${CYAN}[2]${NC} Указать имя бэкапа вручную" -echo -e " ${CYAN}[3]${NC} Восстановление до определённого времени (PITR)" -echo "" - -while true; do - read -rp "Выберите вариант [1-3]: " RESTORE_CHOICE - case "$RESTORE_CHOICE" in - 1) - BACKUP_NAME="LATEST" - PITR_TARGET="" - break - ;; - 2) - read -rp "Введите имя бэкапа: " BACKUP_NAME - [[ -z "$BACKUP_NAME" ]] && { log_warn "Имя не может быть пустым."; continue; } - PITR_TARGET="" - break - ;; - 3) - read -rp "Введите дату/время (формат: YYYY-MM-DD HH:MM:SS [TZ]): " PITR_TARGET - [[ -z "$PITR_TARGET" ]] && { log_warn "Время не может быть пустым."; continue; } - # Базовый бэкап должен быть СТАРШЕ целевого времени, иначе PostgreSQL - # не сможет докатиться до точки восстановления. LATEST подходит, только - # если последний бэкап сделан раньше PITR_TARGET. - log_warn "Базовый бэкап должен быть сделан РАНЬШЕ целевого времени восстановления." - read -rp "Имя базового бэкапа [LATEST]: " BACKUP_NAME - BACKUP_NAME="${BACKUP_NAME:-LATEST}" - break - ;; - *) - log_warn "Некорректный ввод." - ;; - esac -done - -# ============================================================================= -# Шаг 4: Итоговое подтверждение (ДО любых необратимых действий) -# ============================================================================= -SERVICE_NAME="$(get_service_name "$PGVER" "$PGPORT")" - -log_section "Итоговые параметры" - -echo -e " Порт: ${BOLD}$PGPORT${NC}" -echo -e " PGDATA: ${BOLD}$RESTORE_PGDATA${NC}" -echo -e " Сервис: ${BOLD}$SERVICE_NAME${NC}" -echo -e " Конфиг WAL-G: ${BOLD}$WALG_CONFIG_FILE${NC}" -echo -e " Бэкап: ${BOLD}$BACKUP_NAME${NC}" -[[ -n "${PITR_TARGET:-}" ]] && \ -echo -e " PITR до: ${BOLD}$PITR_TARGET${NC}" -echo "" -log_warn "Будет остановлен сервис (если запущен) и ЗАМЕНЕНЫ все данные в $RESTORE_PGDATA!" -echo "" - -read -rp "Введите YES для подтверждения: " FINAL_CONFIRM -if [[ "$FINAL_CONFIRM" != "YES" ]]; then - log_warn "Отменено пользователем." - exit 0 -fi - -# ============================================================================= -# Шаг 5: Остановка сервиса и подготовка каталога -# ============================================================================= -log_section "Подготовка" - -if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then - log_warn "Сервис ${BOLD}$SERVICE_NAME${NC} запущен!" - read -rp "Остановить сервис перед восстановлением? [Y/n]: " STOP_SVC - if [[ "${STOP_SVC,,}" != "n" ]]; then - log_info "Останавливаю $SERVICE_NAME..." - systemctl stop "$SERVICE_NAME" - log_info "Сервис остановлен." - else - log_error "Восстановление невозможно при запущенном сервисе." - exit 1 - fi -else - log_info "Сервис $SERVICE_NAME не запущен. ОК" -fi - -if [[ -d "$RESTORE_PGDATA" ]]; then - log_warn "Директория ${BOLD}$RESTORE_PGDATA${NC} существует и будет ПЕРЕЗАПИСАНА!" - BACKUP_OLD="${RESTORE_PGDATA}.bak.$(date +%Y%m%d_%H%M%S)" - read -rp "Создать резервную копию текущего PGDATA? [Y/n]: " BACKUP_EXISTING - if [[ "${BACKUP_EXISTING,,}" != "n" ]]; then - log_info "Переименовываю в $BACKUP_OLD ..." - mv "$RESTORE_PGDATA" "$BACKUP_OLD" - log_info "Готово: $BACKUP_OLD" - else - # Удаление без резервной копии — единственный безвозвратный шаг: - # требуем отдельное явное подтверждение, иначе откатываемся к mv. - log_warn "БЕЗВОЗВРАТНОЕ удаление ${BOLD}$RESTORE_PGDATA${NC} без резервной копии!" - read -rp "Введите YES для удаления без резервной копии: " WIPE_CONFIRM - if [[ "$WIPE_CONFIRM" == "YES" ]]; then - log_warn "Удаляю $RESTORE_PGDATA ..." - rm -rf "$RESTORE_PGDATA" - else - log_info "Не подтверждено — сохраняю текущий PGDATA в $BACKUP_OLD." - mv "$RESTORE_PGDATA" "$BACKUP_OLD" - log_info "Готово: $BACKUP_OLD" - fi - fi -fi - -mkdir -p "$RESTORE_PGDATA" -chown postgres:postgres "$RESTORE_PGDATA" -chmod 700 "$RESTORE_PGDATA" -log_info "Директория подготовлена: $RESTORE_PGDATA" - -# ============================================================================= -# Шаг 6: Восстановление -# ============================================================================= -LOG_DIR="${RESTORE_PGDATA}/log" -mkdir -p "$LOG_DIR" -chown postgres:postgres "$LOG_DIR" -LOG_FILE="${LOG_DIR}/wal-g-restore-${PGPORT}-$(date +%Y-%m-%d_%H-%M-%S).log" - -log_section "Запуск WAL-G backup-fetch" -log_info "Лог: $LOG_FILE" - -su - postgres -c "/usr/bin/wal-g backup-fetch \ - --config $WALG_CONFIG_FILE \ - $RESTORE_PGDATA \ - $BACKUP_NAME" 2>&1 | tee "$LOG_FILE" - -log_info "backup-fetch завершён." - -# ============================================================================= -# Шаг 7: recovery.signal + recovery.conf (PITR) -# ============================================================================= -log_section "Настройка recovery" - -su - postgres -c "touch $RESTORE_PGDATA/recovery.signal" -log_info "Создан recovery.signal" - -# restore_command нужен ВСЕГДА: без него PostgreSQL не сможет дотянуть WAL из -# архива и не дойдёт до согласованного состояния (а PITR не сработает вовсе). -# Формат совпадает с тем, что прописывает wal-g-cfg.sh. -RECOVERY_CONF="$RESTORE_PGDATA/postgresql.auto.conf" -RESTORE_CMD="/usr/bin/wal-g wal-fetch \"%f\" \"%p\" --config ${WALG_CONFIG_FILE} >> ${LOG_DIR}/restore_command.log 2>&1" - -log_info "Прописываю restore_command в $RECOVERY_CONF" -cat >> "$RECOVERY_CONF" <> "$RECOVERY_CONF" <