From dd0862d1d513b23e98998411c4d399f38201fbae Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 12:09:15 +0000 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B4=D0=B5=D1=80=D0=B6=D0=B8=D0=BC=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=80=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=8F=20linux=20=D0=B2=20scripts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Консолидация рабочих скриптов из репозитория linux под структуру scripts: - linux/1c/ — обновление платформы 1С, публикация на Apache - linux/setup-unattended-upgrades.sh — автообновления безопасности - linux/motd/ — баннеры motd (1С, PostgreSQL, Proxmox) + генератор - pgsql/pgpro/ — рабочие скрипты Postgres Pro (wal-g, pg_probackup, обслуживание, добавление кластера, systemd-таймер) - pgsql/pgpro/legacy/ — ранние непроверенные черновики бэкапа (как есть) Конфиги linux/configs/.bashrc и .bash_aliases слиты вручную с версиями с боевых серверов (bat/exa/help, секции CrowdSec и Synology) без потерь. CI: импортированные рабочие скрипты (1С, Postgres Pro) проверяются shellcheck на уровне ошибок, чтобы не переписывать проверенную логику; курируемый код репозитория остаётся на строгом пороге предупреждений. Не переносилось: бинарник wal-g .deb (12 МБ; ссылка на релизы в README) и docker-стек Traefik/Authelia/Homepage (отдельный homelab-репозиторий). --- .github/workflows/lint.yml | 15 +- README.md | 7 +- linux/1c/1c_full_upgrade.sh | 927 ++++++++++++++++++ linux/1c/1c_web_apache.sh | 173 ++++ linux/configs/.bash_aliases | 119 ++- linux/configs/.bashrc | 58 +- linux/motd/00-header-1c | 121 +++ linux/motd/00-header-pgsql | 168 ++++ linux/motd/00-header-pve | 65 ++ linux/motd/99-mymotd-generator | 96 ++ linux/setup-unattended-upgrades.sh | 150 +++ pgsql/pgpro/README.md | 31 + pgsql/pgpro/backupscripts/pg_backup.sh | 170 ++++ pgsql/pgpro/backupscripts/pg_backup_setup.sh | 58 ++ pgsql/pgpro/backupscripts/pg_restore.sh | 148 +++ pgsql/pgpro/backupscripts/readme.md | 1 + pgsql/pgpro/legacy/backup-pg-simple.sh | 208 ++++ pgsql/pgpro/legacy/backup-pg-universal.sh | 646 ++++++++++++ pgsql/pgpro/legacy/backup-pgp17-pro.sh | 763 ++++++++++++++ pgsql/pgpro/legacy/backup-universal.sh | 205 ++++ pgsql/pgpro/maintenance.sh | 154 +++ pgsql/pgpro/pg_add_cluster.sh | 359 +++++++ pgsql/pgpro/systemd/backup-pgsql.service | 13 + pgsql/pgpro/systemd/backup-pgsql.timer | 8 + pgsql/pgpro/wal-g-scripts/wal-g-backup-all.sh | 213 ++++ pgsql/pgpro/wal-g-scripts/wal-g-backup.sh | 235 +++++ pgsql/pgpro/wal-g-scripts/wal-g-cfg.sh | 559 +++++++++++ pgsql/pgpro/wal-g-scripts/wal-g-delete.sh | 213 ++++ pgsql/pgpro/wal-g-scripts/wal-g-list.sh | 218 ++++ pgsql/pgpro/wal-g-scripts/wal-g-restore.sh | 250 +++++ 30 files changed, 6295 insertions(+), 56 deletions(-) create mode 100644 linux/1c/1c_full_upgrade.sh create mode 100644 linux/1c/1c_web_apache.sh create mode 100644 linux/motd/00-header-1c create mode 100644 linux/motd/00-header-pgsql create mode 100644 linux/motd/00-header-pve create mode 100644 linux/motd/99-mymotd-generator create mode 100644 linux/setup-unattended-upgrades.sh create mode 100644 pgsql/pgpro/README.md create mode 100644 pgsql/pgpro/backupscripts/pg_backup.sh create mode 100644 pgsql/pgpro/backupscripts/pg_backup_setup.sh create mode 100644 pgsql/pgpro/backupscripts/pg_restore.sh create mode 100644 pgsql/pgpro/backupscripts/readme.md create mode 100644 pgsql/pgpro/legacy/backup-pg-simple.sh create mode 100644 pgsql/pgpro/legacy/backup-pg-universal.sh create mode 100644 pgsql/pgpro/legacy/backup-pgp17-pro.sh create mode 100644 pgsql/pgpro/legacy/backup-universal.sh create mode 100644 pgsql/pgpro/maintenance.sh create mode 100644 pgsql/pgpro/pg_add_cluster.sh create mode 100644 pgsql/pgpro/systemd/backup-pgsql.service create mode 100644 pgsql/pgpro/systemd/backup-pgsql.timer create mode 100644 pgsql/pgpro/wal-g-scripts/wal-g-backup-all.sh create mode 100644 pgsql/pgpro/wal-g-scripts/wal-g-backup.sh create mode 100644 pgsql/pgpro/wal-g-scripts/wal-g-cfg.sh create mode 100644 pgsql/pgpro/wal-g-scripts/wal-g-delete.sh create mode 100644 pgsql/pgpro/wal-g-scripts/wal-g-list.sh create mode 100644 pgsql/pgpro/wal-g-scripts/wal-g-restore.sh diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3f6f56a..28d6107 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,8 +11,19 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Run shellcheck - run: find . -name '*.sh' -print0 | xargs -0 shellcheck -S warning + # Курируемые скрипты репозитория — строгий порог (предупреждения). + - name: Run shellcheck (curated, warning level) + run: > + find . -name '*.sh' + -not -path './linux/1c/*' + -not -path './pgsql/pgpro/*' + -print0 | xargs -0 shellcheck -S warning + # Импортированные рабочие скрипты (1С, Postgres Pro/wal-g) приняты «как есть» + # с боевых серверов — порог ошибок, чтобы не переписывать проверенную логику. + - name: Run shellcheck (imported, error level) + run: > + find ./linux/1c ./pgsql/pgpro -name '*.sh' + -print0 | xargs -0 shellcheck -S error psscriptanalyzer: runs-on: ubuntu-latest diff --git a/README.md b/README.md index fd9ec68..79ecfa9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ windows/ PowerShell-скрипты для Windows (+ oobe/ — автоуст pgsql/ PostgreSQL: установка, бэкапы, отказоустойчивость routeros/ Скрипты и конфиги MikroTik RouterOS (+ docs/) proxmox/ Скрипты для Proxmox VE -linux/ Linux: тесты железа, конфиги окружения +linux/ Linux: тесты железа, конфиги окружения, 1С, motd zabbix/ Шаблоны Zabbix ``` @@ -42,6 +42,7 @@ zabbix/ Шаблоны Zabbix | `pg_setup.sh` | Установка и первичная настройка Postgres Pro 1C на Debian/Ubuntu: локали, часовой пояс, репозиторий, initdb с `--tune=1c`, pg_hba, пароль postgres. Параметры: `--pgver`, `--timezone`, `--lang`, `--postgres-pass`. Запуск от root. | | `backup-db.sh` | Бэкап (pg_dump + pigz, с проверкой целостности архива) и обслуживание (vacuumdb) баз PostgreSQL / Postgres Pro по списку из файла. Ротация бэкапов отдельно по каждой БД, логов — по возрасту. Для cron. | | `backup-pg.conf.example` | Пример конфига для `backup-db.sh` (копируется в `/etc/backup-pg.conf`): пути, retention, каталог бинарников. | +| `pgpro/` | Рабочие скрипты Postgres Pro, перенесённые «как есть» с боевых серверов: wal-g, pg_probackup, обслуживание, добавление кластера, systemd-таймер. См. `pgsql/pgpro/README.md`. | Зависимости `backup-db.sh`: `pigz`. Список баз — текстовый файл (по умолчанию `/var/lib/pgpro/scripts/DB`), по одной базе на строку, `#` — комментарий. @@ -84,6 +85,10 @@ zabbix/ Шаблоны Zabbix | Файл | Назначение | |---|---| | `nvme_test.sh` | Тест NVMe-диска через fio по профилю CrystalDiskMark (SEQ1M Q8T1, SEQ128K Q32T1, RND4K Q32T8, RND4K Q1T1). Зависимости: `fio`, `jq`. | +| `setup-unattended-upgrades.sh` | Настройка автоматических обновлений безопасности (unattended-upgrades) на Debian/Ubuntu. | +| `1c/1c_full_upgrade.sh` | Полное обновление платформы 1С:Предприятие на Linux (скачивание, установка, переключение версии). | +| `1c/1c_web_apache.sh` | Публикация баз 1С на веб-сервере Apache. Интерактивный (запрашивает версию платформы). | +| `motd/` | Баннеры motd для серверов (1С, PostgreSQL, Proxmox) + генератор `99-mymotd-generator`. | | `configs/.bashrc`, `configs/.bash_aliases` | Окружение bash для серверов. | | `configs/commands.list` | Шпаргалка команд Linux по разделам (процессы, мониторинг, сеть и т.д.). | diff --git a/linux/1c/1c_full_upgrade.sh b/linux/1c/1c_full_upgrade.sh new file mode 100644 index 0000000..e6e04e3 --- /dev/null +++ b/linux/1c/1c_full_upgrade.sh @@ -0,0 +1,927 @@ +#!/bin/bash + +if [[ $EUID -ne 0 ]]; then + echo -e "${RED}Этот скрипт должен запускаться с правами root${NC}" + exit 1 +fi + +# Constants +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Functions +show_header() { + echo -e "\n${GREEN}#################### МЕНЮ УПРАВЛЕНИЯ 1С #####################${NC}" + echo -e "Доступные команды:" + echo -e " setup - установка новой версии" + echo -e " update - обновление существующей версии" + echo -e " remove - удаление установленной версии" + echo -e " debug - режим отладки" + echo -e " версия - указание конкретной версии для операций" + echo -e "Разместите дистрибутивы в той же директории, что и скрипт" +} + +show_footer() { + echo -e "\n${GREEN}############## ЗАВЕРШЕНИЕ РАБОТЫ ################${NC}\n" +} + +check_version_format() { + [[ $1 =~ ^8\.3\.[0-9]{2}\.[0-9]{4}$ ]] +} + +compare_versions() { + if [[ $1 == $2 ]]; then + return 0 + fi + local IFS=. + local i ver1=($1) ver2=($2) + for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do + ver1[i]=0 + done + for ((i=0; i<${#ver1[@]}; i++)); do + if [[ -z ${ver2[i]} ]]; then + ver2[i]=0 + fi + if ((10#${ver1[i]} > 10#${ver2[i]})); then + return 1 + fi + if ((10#${ver1[i]} < 10#${ver2[i]})); then + return 2 + fi + done + return 0 +} + +find_available_archives() { + local available_versions=() + + # Поиск архивов (tar.gz/zip) + for archive in server64_*.tar.gz server64_*.zip; do + if [[ -f "$archive" ]]; then + if [[ $archive =~ server64_(8_3_[0-9]{2}_[0-9]{4})\.(tar\.gz|zip) ]]; then + version=$(echo "${BASH_REMATCH[1]}" | tr '_' '.') + available_versions+=("$version") + fi + fi + done + + # Удаляем дубликаты и сортируем версии (новые версии первыми) + available_versions=($(echo "${available_versions[@]}" | tr ' ' '\n' | sort -ru | tr '\n' ' ')) + + echo "${available_versions[@]}" +} + +extract_distribution() { + local version=$1 + local arcname=$(echo $version | sed 's/\./_/g') + + if [[ -f "setup-full-$version-x86_64.run" ]]; then + echo -e "${GREEN}Найден дистрибутив платформы $version${NC}" + return 0 + fi + + for ext in tar.gz zip; do + if [[ -f "server64_$arcname.$ext" ]]; then + echo -e "${YELLOW}Распаковываем дистрибутив, подождите...${NC}" + if [[ $ext == "tar.gz" ]]; then + tar -xzf "server64_$arcname.$ext" + else + unzip -u "server64_$arcname.$ext" + fi + return 0 + fi + done + + echo -e "${RED}Не обнаружен дистрибутив платформы $version${NC}" + return 1 +} + +install_dependencies() { + echo -e "${YELLOW}Проверка и установка необходимых зависимостей...${NC}" + + # Обновляем список пакетов + echo -e "${YELLOW}Обновляем информацию о пакетах...${NC}" + apt-get update -y >/dev/null + + # Установка lsb-release если отсутствует + if ! command -v lsb_release >/dev/null 2>&1; then + echo -e "${YELLOW}Устанавливаем lsb-release...${NC}" + DEBIAN_FRONTEND=noninteractive apt-get install -y lsb-release >/dev/null || { + echo -e "${RED}Ошибка установки lsb-release${NC}" + return 1 + } + else + echo -e "${GREEN}lsb-release уже установлен${NC}" + fi + + # Проверка libenchant1c2a (кроме AstraLinux) + if [[ "$(lsb_release -si)" =~ ^AstraLinux ]]; then + echo -e "${YELLOW}Astra Linux. Проверка libenchant1c2a пропущена${NC}" + else + if dpkg -s libenchant1c2a &>/dev/null; then + echo -e "${GREEN}libenchant1c2a уже установлен${NC}" + else + codename=$(lsb_release -sc) + case $codename in + stretch|buster|bookworm|bionic|focal|tara|tessa|tina|tricia|ulyana|ulyssa|uma|una) + echo -e "${YELLOW}Устанавливаем libenchant1c2a...${NC}" + DEBIAN_FRONTEND=noninteractive apt-get install -y libenchant1c2a >/dev/null || \ + echo -e "${YELLOW}Не удалось установить libenchant1c2a${NC}" + ;; + *) + echo -e "${YELLOW}libenchant1c2a недоступен в стандартных репозиториях${NC}" + ;; + esac + fi + fi + + # Список пакетов с точными именами как в репозиториях + local packages=( + ttf-mscorefonts-installer + libodbc1 + unixodbc + libgsf-1-114 + fontconfig + imagemagick + unzip + ) + + # Установка отсутствующих пакетов + for pkg in "${packages[@]}"; do + if dpkg -s "$pkg" &>/dev/null; then + echo -e "${GREEN}${pkg} уже установлен${NC}" + else + echo -e "${YELLOW}Устанавливаем ${pkg}...${NC}" + DEBIAN_FRONTEND=noninteractive apt-get install -y "$pkg" >/dev/null || { + echo -e "${RED}Ошибка установки ${pkg}${NC}" + continue + } + + # Дополнительные действия для шрифтов + if [[ "$pkg" == "ttf-mscorefonts-installer" ]]; then + echo -e "${YELLOW}Обновляем кэш шрифтов...${NC}" + fc-cache -fv >/dev/null + fi + fi + done + + echo -e "${GREEN}Все зависимости успешно проверены${NC}" + return 0 +} + +# Секция RAS +install_ras() { + local version=$1 + local arch="x86_64" + + echo -e "${YELLOW}Устанавливаем сервер RAS...${NC}" + + # Проверяем наличие файла службы RAS + if [[ -f "/opt/1cv8/$arch/$version/ras-$version.service" ]]; then + # Устанавливаем службу RAS + systemctl link "/opt/1cv8/$arch/$version/ras-$version.service" + systemctl enable "ras-$version" + systemctl start "ras-$version" + + echo -e "${GREEN}Сервер RAS успешно установлен и запущен${NC}" + else + echo -e "${RED}Файлы RAS не найдены в /opt/1cv8/$arch/$version/${NC}" + return 1 + fi +} + +remove_ras() { + local version=$1 + local service_name="ras-$version" + + if systemctl list-unit-files | grep -q "$service_name.service"; then + echo -e "${YELLOW}Обнаружен установленный сервер RAS для версии $version${NC}" + echo -e "${YELLOW}Останавливаем и удаляем сервер RAS...${NC}" + + systemctl stop "$service_name" 2>/dev/null + systemctl disable "$service_name" 2>/dev/null + rm -f "/etc/systemd/system/$service_name.service" + systemctl daemon-reload + + echo -e "${GREEN}Сервер RAS для версии $version успешно удалён${NC}" + return 0 + else + echo -e "${YELLOW}Сервер RAS для версии $version не найден${NC}" + return 1 + fi +} + +# Секция платформы +install_platform() { + local version=$1 + local web=${2:-"no"} # По умолчанию "no" + local components="--mode unattended --enable-components server" + local install_success=true + + # Проверка минимальной версии + if [ "$(printf "%s\n" "8.3.21" "$version" | sort -V | head -n1)" != "8.3.21" ]; then + echo -e "${RED}Ошибка: Поддерживаются только версии 8.3.21 и выше${NC}" + return 1 + fi + + # Проверка наличия дистрибутива + if [[ ! -f "./setup-full-$version-x86_64.run" ]]; then + echo -e "${RED}Ошибка: Дистрибутив setup-full-$version-x86_64.run не найден${NC}" + return 1 + fi + + # Добавляем компонент веб-сервера при необходимости + [[ $web == "yes" ]] && components+=",ws" + + echo -e "${YELLOW}Устанавливаем платформу $version...${NC}" + + # Установка основного пакета + if ! ./setup-full-$version-x86_64.run $components; then + echo -e "${RED}Ошибка установки платформы $version${NC}" + return 1 + fi + + # Настройка systemd службы + echo -e "${YELLOW}Настраиваем systemd службу...${NC}" + + local service_file="/opt/1cv8/x86_64/$version/srv1cv8-$version@.service" + if [[ -f "$service_file" ]]; then + if ! systemctl link "$service_file"; then + echo -e "${RED}Ошибка создания символьной ссылки службы${NC}" + install_success=false + fi + + if ! systemctl enable "srv1cv8-$version@default"; then + echo -e "${RED}Ошибка включения службы${NC}" + install_success=false + fi + + if ! systemctl start "srv1cv8-$version@default"; then + echo -e "${RED}Ошибка запуска службы${NC}" + install_success=false + fi + else + echo -e "${RED}Файл службы $service_file не найден${NC}" + install_success=false + fi + + # Проверка успешности установки + if ! $install_success; then + echo -e "${YELLOW}Установка завершена с ошибками${NC}" + return 1 + fi + + echo -e "${GREEN}Платформа $version успешно установлена${NC}" + + # Проверка работы службы + if ! systemctl is-active --quiet "srv1cv8-$version@default"; then + echo -e "${YELLOW}Предупреждение: Служба не запущена${NC}" + else + echo -e "${GREEN}Служба успешно запущена${NC}" + fi + + return 0 +} + +find_installed_versions() { + local installed_versions=() + local version + + # 1. Поиск старых версий (до 8.3.20) через dpkg + if dpkg -l | grep -q 1c-enterprise; then + while read -r version; do + # Проверяем, что версия действительно старая (<8.3.20) + if [[ $version < '8.3.20' ]]; then + installed_versions+=("$version") + fi + done < <(dpkg -l | grep 1c-enterprise | awk '{print $3}' | cut -d'-' -f1 | sort -u) + fi + + # 2. Поиск новых версий (8.3.20 и выше) через каталоги + if [[ -d "/opt/1cv8/x86_64" ]]; then + while read -r version; do + # Проверяем, что каталог не пустой и есть uninstaller-full + if [[ -f "/opt/1cv8/x86_64/$version/uninstaller-full" ]]; then + installed_versions+=("$version") + fi + done < <(ls /opt/1cv8/x86_64/ 2>/dev/null | grep -E '^8\.3\.[0-9]{2}\.[0-9]{4}$' | sort -ru) + fi + + # 3. Удаляем возможные дубликаты (хотя маловероятно) + installed_versions=($(printf "%s\n" "${installed_versions[@]}" | sort -u)) + + # 4. Проверяем, что массив не пустой + if [[ ${#installed_versions[@]} -eq 0 ]]; then + echo -e "${YELLOW}Не найдено установленных версий 1С${NC}" >&2 + return 1 + fi + + echo "${installed_versions[@]}" + return 0 +} + +remove_platform() { + local version=$1 + echo -e "${YELLOW}Начинаем удаление платформы $version...${NC}" + + # Удаляем RAS для этой версии + remove_ras "$version" + + # Сначала останавливаем службы независимо от версии + if [[ $version < '8.3.21' ]]; then + echo -e "${YELLOW}Останавливаем службу srv1cv83...${NC}" + systemctl stop srv1cv83 2>/dev/null + else + echo -e "${YELLOW}Останавливаем службу srv1cv8-$version@default...${NC}" + systemctl stop "srv1cv8-$version@default" 2>/dev/null + fi + + if [[ $version < '8.3.20' ]]; then + # Удаление старых версий (до 8.3.20) + if dpkg -l | grep -q "1c-enterprise$version"; then + echo -e "${YELLOW}Удаляем пакеты старого формата...${NC}" + apt remove -y $(dpkg -l | grep "1c-enterprise" | awk '{print $2}') + + echo -e "${YELLOW}Удаляем конфигурационные файлы...${NC}" + rm -f /etc/init.d/srv1cv83 /etc/default/srv1cv83 + + # Отключаем автозагрузку для старых версий + update-rc.d -f srv1cv83 remove 2>/dev/null + else + echo -e "${RED}Версия $version не найдена в системе${NC}" + return 1 + fi + else + # Удаление новых версий (8.3.20 и выше) + if [[ -f "/opt/1cv8/x86_64/$version/uninstaller-full" ]]; then + echo -e "${YELLOW}Запускаем деинсталлятор...${NC}" + /opt/1cv8/x86_64/$version/uninstaller-full --mode unattended + + # Отключаем службы для новых версий + if [[ $version < '8.3.21' ]]; then + echo -e "${YELLOW}Отключаем службу srv1cv83...${NC}" + systemctl disable srv1cv83 2>/dev/null + rm -f /etc/init.d/srv1cv83 /etc/default/srv1cv83 + else + echo -e "${YELLOW}Отключаем службу srv1cv8-$version@default...${NC}" + systemctl disable "srv1cv8-$version@default" 2>/dev/null + fi + + else + echo -e "${RED}Файлы версии $version не найдены${NC}" + return 1 + fi + fi + + echo -e "${GREEN}Платформа $version успешно удалена${NC}" + return 0 +} + +show_installed_versions() { + echo -e "\n${YELLOW}Список установленных версий:${NC}" + versions=($(find_installed_versions)) + if [ ${#versions[@]} -eq 0 ]; then + echo -e "${RED}Не найдено установленных версий 1С${NC}" + return 1 + fi + + for i in "${!versions[@]}"; do + echo "$((i+1)). ${versions[$i]}" + done + return 0 +} + +process_installation() { + local action=$1 + local newver=$2 + local oldver=$3 + + if ! check_version_format "$newver"; then + echo -e "${RED}Неверный формат версии${NC}" + return 1 + fi + + if [[ $newver < '8.3.20' ]]; then + echo -e "${RED}Версии ниже 8.3.20 не поддерживаются${NC}" + return 1 + fi + + # Добавляем проверку на попытку отката версии + if [[ $action == "update" ]]; then + compare_versions "$newver" "$oldver" + case $? in + 1) # Новая версия действительно новее + echo -e "${GREEN}Обновление с $oldver на более новую версию $newver${NC}" + ;; + 2) # Новая версия старше существующей + echo -e "${RED}ВНИМАНИЕ! Вы пытаетесь установить более старую версию ($newver) вместо текущей ($oldver)!${NC}" + echo -e "${YELLOW}Это может привести к проблемам с совместимостью баз данных!${NC}" + read -p "Вы действительно хотите продолжить? (y/N): " confirm + if [[ ! $confirm =~ ^[Yy] ]]; then + echo -e "${YELLOW}Отменено пользователем${NC}" + return 1 + fi + echo -e "${YELLOW}Продолжаем установку более старой версии по требованию пользователя${NC}" + ;; + 0) # Версии одинаковые + echo -e "${YELLOW}Версии идентичны ($oldver -> $newver), нет необходимости в обновлении${NC}" + return 1 + ;; + esac + fi + + if ! extract_distribution "$newver"; then + return 1 + fi + + echo -e "\n${YELLOW}Будет выполнено:${NC}" + if [[ $action == "setup" ]]; then + echo "- Установка новой версии $newver" + else + echo "- Обновление с $oldver на $newver" + + # Удаление RAS старой версии + remove_ras "$oldver" + + # Удаление старой версии платформы + echo -e "${YELLOW}Удаляем старую версию $oldver...${NC}" + if ! remove_platform "$oldver"; then + echo -e "${RED}Ошибка при удалении старой версии $oldver${NC}" + return 1 + fi + fi + + read -p "Подтвердите (y/N): " confirm + if [[ ! $confirm =~ ^[Yy] ]]; then + echo -e "${YELLOW}Отменено пользователем${NC}" + return + fi + + read -p "Установить модуль веб-сервера? (y/N): " web_choice + web="no" + [[ $web_choice =~ ^[Yy] ]] && web="yes" + + install_dependencies + install_platform "$newver" "$web" + + # Предложение установить RAS после успешной установки платформы + if [[ $? -eq 0 ]]; then + read -p "Установить сервер RAS для версии $newver? [y/N]: " install_ras_choice + if [[ $install_ras_choice =~ ^[Yy] ]]; then + if ! install_ras "$newver"; then + echo -e "${YELLOW}Не удалось установить сервер RAS${NC}" + else + echo -e "${GREEN}Сервер RAS успешно установлен${NC}" + fi + fi + fi + + read -p "Нажмите Enter для продолжения..." +} + +# Экземпляры +create_instance() { + local version=$1 + local instance_name=$2 + local port=$3 + local regport=$4 + local range=$5 + + # Проверяем, что версия установлена + if [[ ! -f "/opt/1cv8/x86_64/$version/srv1cv8-$version@.service" ]]; then + echo -e "${RED}Шаблон службы для версии $version не найден${NC}" + return 1 + fi + + if [[ -z "$instance_name" ]]; then + read -p "Введите имя нового экземпляра (например: 2x): " instance_name + if [[ -z "$instance_name" ]]; then + echo -e "${RED}Имя экземпляра не может быть пустым${NC}" + return 1 + fi + fi + + # Проверяем существование конфигурации после получения имени экземпляра + local dropin_dir="/etc/systemd/system/srv1cv8-$version@$instance_name.service.d" + local override_file="$dropin_dir/override.conf" + + if [[ -f "$override_file" ]]; then + echo -e "${YELLOW}Внимание: файл конфигурации $override_file уже существует!${NC}" + echo -e "${YELLOW}Это может означать, что экземпляр с таким именем уже создан.${NC}" + read -p "Вы действительно хотите продолжить? (y/N): " confirm + if [[ ! "$confirm" =~ ^[Yy] ]]; then + echo -e "${YELLOW}Создание экземпляра отменено пользователем${NC}" + return 1 + fi + fi + + # Устанавливаем порты по умолчанию + port=${port:-2540} + regport=${regport:-2541} + range=${range:-2560:2591} + + # Проверяем доступность портов + if ss -tulpn | grep -qE ":${port}\b"; then + echo -e "${RED}Ошибка: Порт ${port} уже занят${NC}" + return 1 + fi + + if ss -tulpn | grep -qE ":${regport}\b"; then + echo -e "${RED}Ошибка: Регистрационный порт ${regport} уже занят${NC}" + return 1 + fi + + # Создаем каталог для данных экземпляра + local data_dir="/home/usr1cv8/.1cv8/1C/1cv8_${instance_name}" + echo -e "${YELLOW}Создаем каталог данных: ${data_dir}${NC}" + mkdir -p "$data_dir" || { + echo -e "${RED}Ошибка создания каталога данных${NC}" + return 1 + } + chown -R usr1cv8:grp1cv8 "$data_dir" || { + echo -e "${RED}Ошибка изменения прав на каталог данных${NC}" + return 1 + } + + # Создаем drop-in конфигурацию + echo -e "${YELLOW}Создаем конфигурацию в ${dropin_dir}/override.conf${NC}" + mkdir -p "$dropin_dir" || { + echo -e "${RED}Ошибка создания директории для конфигурации${NC}" + return 1 + } + + # Проверяем версию для возможности отладки (строго выше 8.3.21) + local can_debug=false + if [[ "$(printf "%s\n" "8.3.21" "$version" | sort -V | tail -n1)" == "$version" ]]; then + can_debug=true + fi + + # Предлагаем включить отладку только для поддерживаемых версий + local debug_choice="n" + if $can_debug; then + read -p "Включить отладку для этого экземпляра? (y/N): " debug_choice + fi + + # Создаем файл override.conf + cat > "$override_file" <> "$override_file" + echo -e "${GREEN}Отладка TCP включена${NC}" + else + debug_protocol="http" + echo "Environment=SRV1CV8_DEBUG=\"-debug -http\"" >> "$override_file" + echo -e "${GREEN}Отладка HTTP включена${NC}" + fi + + # Создаем конфигурацию для внешних соединений + local conf_dir="/opt/1cv8/conf" + mkdir -p "$conf_dir" + cat > "$conf_dir/comcntrcfg.xml" < + + +EOF + echo -e "${GREEN}Создан файл конфигурации внешней отладки: ${conf_dir}/comcntrcfg.xml${NC}" + fi + + # Применяем изменения + echo -e "${YELLOW}Применяем изменения systemd...${NC}" + systemctl daemon-reload || { + echo -e "${RED}Ошибка перезагрузки демона systemd${NC}" + return 1 + } + + # Включаем и запускаем службу + echo -e "${YELLOW}Активируем службу srv1cv8-$version@$instance_name${NC}" + systemctl enable "srv1cv8-$version@$instance_name" || { + echo -e "${RED}Ошибка включения службы${NC}" + return 1 + } + + echo -e "${YELLOW}Запускаем службу...${NC}" + systemctl start "srv1cv8-$version@$instance_name" || { + echo -e "${RED}Ошибка запуска службы${NC}" + return 1 + } + + # Проверяем статус + local status=$(systemctl is-active "srv1cv8-$version@$instance_name") + echo -e "${GREEN}Экземпляр успешно создан:${NC}" + echo -e "• Имя экземпляра: ${instance_name}" + echo -e "• Версия платформы: ${version}" + echo -e "• Основной порт: ${port}" + echo -e "• Регистрационный порт: ${regport}" + echo -e "• Диапазон портов: ${range}" + echo -e "• Каталог данных: ${data_dir}" + echo -e "• Статус службы: ${status}" + + # Показываем статус отладки + if grep -q "SRV1CV8_DEBUG" "$override_file" 2>/dev/null; then + echo -e "• Отладка: ${GREEN}включена${NC}" + if grep -q "http" "$override_file"; then + echo -e " • Тип отладки: HTTP" + else + echo -e " • Тип отладки: TCP" + fi + else + echo -e "• Отладка: ${YELLOW}выключена${NC}" + fi + + echo -e "• Путь к конфигурации: ${override_file}" + echo -e "• Команда управления: systemctl [start|stop|restart] srv1cv8-$version@$instance_name" + + return 0 +} + +list_instances() { + echo -e "\n${GREEN}Список рабочих серверов 1С:${NC}" + + local services=($(systemctl list-units --all --no-legend --no-pager --plain 'srv1cv8*@*' | awk '{print $1}')) + + if [ ${#services[@]} -eq 0 ]; then + echo -e "${YELLOW}Нет зарегистрированных рабочих серверов${NC}" + return 1 + fi + + for service in "${services[@]}"; do + local instance=$(echo "$service" | cut -d'@' -f2 | cut -d'.' -f1) + local version=$(echo "$service" | grep -oP 'srv1cv8-\K[\d.]+(?=@)') + local status=$(systemctl is-active "$service" 2>/dev/null || echo "unknown") + local port=$(systemctl show "$service" --property=Environment --no-pager | grep -oP 'SRV1CV8_PORT=\K\d+') + + # Получаем статус отладки + local debug_status="" + local debug_file="/etc/systemd/system/${service}.d/override.conf" + if [[ -f "$debug_file" ]]; then + if grep -q "http" "$debug_file"; then + debug_status=" (${RED}Отладка HTTP${NC})" + elif grep -q "debug" "$debug_file"; then + debug_status=" (${YELLOW}Отладка TCP${NC})" + fi + fi + + echo -e "• ${GREEN}${instance}${NC}${debug_status} (Версия: ${version:-N/A}, Порт: ${port:-N/A}, Статус: ${status})" + done +} + +remove_instance() { + local version=$1 + local instance_name=$2 + + if [[ -z "$instance_name" ]]; then + list_instances + read -p "Введите имя рабочего сервера для удаления: " instance_name + if [[ -z "$instance_name" ]]; then + echo -e "${RED}Имя рабочего сервера не может быть пустым${NC}" + return 1 + fi + fi + + # Останавливаем и отключаем сервис + systemctl stop "srv1cv8-${version}@${instance_name}" + systemctl disable "srv1cv8-${version}@${instance_name}" + + # Удаляем drop-in конфигурацию + local dropin_file="/etc/systemd/system/srv1cv8-${version}@${instance_name}.service.d/override.conf" + if [[ -f "$dropin_file" ]]; then + rm -f "$dropin_file" + fi + + # Удаляем каталог данных (по подтверждению) + local data_dir="/home/usr1cv8/.1cv8/1C/1cv8_${instance_name}" + if [[ -d "$data_dir" ]]; then + read -p "Удалить каталог данных (${data_dir})? [y/N]: " confirm + if [[ $confirm =~ ^[Yy] ]]; then + rm -rf "$data_dir" + echo -e "${YELLOW}Каталог данных удален${NC}" + fi + fi + + systemctl daemon-reload + echo -e "${GREEN}Рабочий сервер '${instance_name}' успешно удален${NC}" +} + + +#Меню +show_action_menu() { + echo -e "\n${GREEN}Главное меню управления сервером 1С:${NC}" + echo "1. Установка/обновление/удаление платформы 1С" + echo "2. Управление экземплярами 1С" + echo "3. Список версий" + echo "4. Выход" +} + + +# Модифицированное меню управления экземплярами +show_instance_menu() { + echo -e "\n${GREEN}Меню управления экземплярами:${NC}" + echo "1. Список экземпляров" + echo "2. Создать экземпляр" + echo "3. Удалить экземпляр" + echo "4. Вернуться в главное меню" +} + +main() { + show_header + + while true; do + show_action_menu + read -p "Введите номер действия (1-4): " choice + + case $choice in + 1) # Установка/обновление/удаление платформы 1С + echo -e "\n${GREEN}Меню управления платформой:${NC}" + echo "1. Установить новую версию" + echo "2. Обновить существующую версию" + echo "3. Удалить версию" + echo "4. Вернуться в главное меню" + read -p "Выберите действие (1-4): " platform_choice + + case $platform_choice in + 1) # Установка новой версии + available_versions=($(find_available_archives)) + if [ ${#available_versions[@]} -eq 0 ]; then + echo -e "${RED}Нет доступных дистрибутивов в текущей директории${NC}" + continue + fi + + echo -e "\n${GREEN}Доступные версии для установки:${NC}" + for i in "${!available_versions[@]}"; do + echo "$((i+1)). ${available_versions[$i]}" + done + + read -p "Выберите версию (1-${#available_versions[@]}): " ver_choice + if [[ $ver_choice =~ ^[0-9]+$ ]] && [ $ver_choice -ge 1 -a $ver_choice -le ${#available_versions[@]} ]; then + selected_version="${available_versions[$((ver_choice-1))]}" + process_installation "setup" "$selected_version" + else + echo -e "${RED}Некорректный выбор версии${NC}" + fi + ;; + + 2) # Обновление существующей версии + if ! installed_versions=($(find_installed_versions)); then + echo -e "${RED}Не найдено установленных версий для обновления${NC}" + continue + fi + + echo -e "\n${GREEN}Выберите версию для обновления:${NC}" + for i in "${!installed_versions[@]}"; do + echo "$((i+1)). ${installed_versions[$i]}" + done + + read -p "Введите номер (1-${#installed_versions[@]}): " old_ver_choice + if [[ $old_ver_choice =~ ^[0-9]+$ ]] && [ $old_ver_choice -ge 1 -a $old_ver_choice -le ${#installed_versions[@]} ]; then + old_version="${installed_versions[$((old_ver_choice-1))]}" + + available_versions=($(find_available_archives)) + if [ ${#available_versions[@]} -eq 0 ]; then + echo -e "${RED}Нет доступных дистрибутивов в текущей директории${NC}" + continue + fi + + echo -e "\n${GREEN}Доступные версии для обновления:${NC}" + for i in "${!available_versions[@]}"; do + echo "$((i+1)). ${available_versions[$i]}" + done + + read -p "Выберите новую версию (1-${#available_versions[@]}): " new_ver_choice + if [[ $new_ver_choice =~ ^[0-9]+$ ]] && [ $new_ver_choice -ge 1 -a $new_ver_choice -le ${#available_versions[@]} ]; then + new_version="${available_versions[$((new_ver_choice-1))]}" + process_installation "update" "$new_version" "$old_version" + else + echo -e "${RED}Некорректный выбор версии${NC}" + fi + else + echo -e "${RED}Некорректный выбор версии${NC}" + fi + ;; + + 3) # Удаление версии + if ! versions=($(find_installed_versions)); then + echo -e "${RED}Не найдено установленных версий для удаления${NC}" + continue + fi + + echo -e "\n${GREEN}Выберите версию для удаления:${NC}" + for i in "${!versions[@]}"; do + echo "$((i+1)). ${versions[$i]}" + done + + read -p "Введите номер (1-${#versions[@]}): " ver_choice + if [[ $ver_choice =~ ^[0-9]+$ ]] && [ $ver_choice -ge 1 -a $ver_choice -le ${#versions[@]} ]; then + ver_to_remove="${versions[$((ver_choice-1))]}" + echo -e "\n${YELLOW}Будет удалена версия: $ver_to_remove${NC}" + read -p "Подтвердите удаление (y/N): " confirm + if [[ $confirm =~ ^[Yy] ]]; then + remove_platform "$ver_to_remove" + read -p "Нажмите Enter для продолжения..." + fi + else + echo -e "${RED}Некорректный выбор${NC}" + fi + ;; + + 4) # Выход + continue + ;; + + *) + echo -e "${RED}Некорректный выбор${NC}" + ;; + esac + ;; + + 2) # Управление экземплярами + if ! versions=($(find_installed_versions)); then + echo -e "${RED}Для управления рабочими серверами необходимо установить платформу 1С${NC}" + continue + fi + + echo -e "\n${GREEN}Доступные версии:${NC}" + for i in "${!versions[@]}"; do + echo "$((i+1)). ${versions[$i]}" + done + + read -p "Выберите версию (1-${#versions[@]}): " ver_choice + if [[ $ver_choice =~ ^[0-9]+$ ]] && [ $ver_choice -ge 1 -a $ver_choice -le ${#versions[@]} ]; then + version="${versions[$((ver_choice-1))]}" + + while true; do + show_instance_menu + read -p "Введите номер действия (1-4): " instance_choice + + case $instance_choice in + 1) # Список экземпляров + list_instances + ;; + + 2) # Создать экземпляр + read -p "Введите имя экземпляра (например 2x): " instance_name + read -p "Введите порт (по умолчанию 2540): " port + read -p "Введите регистрационный порт (по умолчанию 2541): " regport + read -p "Введите диапазон портов (по умолчанию 2560:2591): " range + + create_instance "$version" "$instance_name" "${port:-2540}" "${regport:-2541}" "${range:-2560:2591}" + ;; + + 3) # Удалить экземпляр + list_instances + read -p "Введите имя экземпляра для удаления: " instance_name + if [[ -n "$instance_name" ]]; then + remove_instance "$version" "$instance_name" + else + echo -e "${RED}Имя экземпляра не может быть пустым${NC}" + fi + ;; + + 4) # Выход + break + ;; + + *) + echo -e "${RED}Некорректный выбор${NC}" + ;; + esac + done + else + echo -e "${RED}Некорректный выбор${NC}" + fi + ;; + + 3) # Список версий + show_installed_versions + read -p "Нажмите Enter для продолжения..." + ;; + + 4) # Выход + echo -e "${YELLOW}Выход из программы${NC}" + show_footer + exit 0 + ;; + + *) + echo -e "${RED}Некорректный выбор. Введите число от 1 до 4${NC}" + ;; + esac + done +} + +# Установка рабочей директории +cd "$(dirname "$0")" + +# Запуск главной функции +main diff --git a/linux/1c/1c_web_apache.sh b/linux/1c/1c_web_apache.sh new file mode 100644 index 0000000..a396117 --- /dev/null +++ b/linux/1c/1c_web_apache.sh @@ -0,0 +1,173 @@ +#!/bin/bash + +# Constants +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Functions +show_header() { + echo -e "\n${GREEN}#################### НАЧАЛО РАБОТЫ #####################${NC}" + echo -e "Этот скрипт установит/обновит компоненту веб-сервера (ws) 1С" + echo -e "и настроит публикацию базы в Apache" + echo -e "(c) Уваров А.С. 2023-2024 interface31.ru\n" +} + +show_footer() { + echo -e "\n${GREEN}############## ЗАВЕРШЕНИЕ РАБОТЫ ################${NC}\n" +} + +check_version_format() { + [[ $1 =~ ^8\.3\.[0-9]{2}\.[0-9]{4}$ ]] +} + +is_installed() { + [[ -f "/opt/1cv8/x86_64/$1/ws/apache2.4/mod_1c.so" ]] +} + +remove_old_platform() { + local version=$1 + echo -e "${YELLOW}Удаляем старую платформу $version...${NC}" + + /opt/1cv8/x86_64/$version/uninstaller-full --mode unattended + echo -e "${GREEN}Платформа $version удалена${NC}" +} + +extract_distribution() { + local version=$1 + local arcname=$(echo $version | sed 's/\./_/g') + + if [[ -f "setup-full-$version-x86_64.run" ]]; then + echo -e "${GREEN}Найден дистрибутив платформы $version${NC}" + return 0 + fi + + for ext in tar.gz zip; do + if [[ -f "server64_$arcname.$ext" ]]; then + echo -e "${YELLOW}Распаковываем дистрибутив, подождите...${NC}" + if [[ $ext == "tar.gz" ]]; then + tar -xzf "server64_$arcname.$ext" + else + unzip -u "server64_$arcname.$ext" + fi + return 0 + fi + done + + echo -e "${RED}Не обнаружен дистрибутив платформы $version${NC}" + return 1 +} + +install_dependencies() { + echo -e "${YELLOW}Проверяем и устанавливаем зависимости...${NC}" + apt update -y > /dev/null + apt install -y lsb-release apache2 libapache2-mod-wsgi +} + +configure_mpm_worker() { + echo -e "${YELLOW}Проверяем и настраиваем MPM Worker...${NC}" + + # Отключаем все MPM модули + for mpm in $(apache2ctl -M | grep mpm_ | awk '{print $1}'); do + a2dismod -q $mpm + done + + # Включаем mpm_worker + a2enmod mpm_worker + + # Перезапускаем Apache + systemctl restart apache2 + + echo -e "${GREEN}MPM Worker успешно настроен${NC}" +} + +install_ws_component() { + local version=$1 + + echo -e "${YELLOW}Устанавливаем компоненту ws (веб-сервер 1С)...${NC}" + ./setup-full-$version-x86_64.run --mode unattended --enable-components ws + + if [[ $? -eq 0 ]]; then + echo -e "${GREEN}Компонента ws успешно установлена${NC}" + else + echo -e "${RED}Ошибка установки компоненты ws${NC}" + exit 1 + fi +} + +publish_infobase() { + local version=$1 + + read -p "Введите название информационной базы: " ib_name + read -p "Введите адрес сервера 1С (например, SRV-1C): " srv_address + + # Создаем каталог для публикации + local publish_dir="/var/www/$ib_name" + mkdir -p "$publish_dir" + chown -R www-data:www-data "$publish_dir" + + # Публикуем базу + echo -e "${YELLOW}Публикуем информационную базу $ib_name...${NC}" + + /opt/1cv8/x86_64/$version/webinst \ + -publish \ + -apache24 \ + -wsdir "$ib_name" \ + -dir "$publish_dir" \ + -connstr "Srvr=$srv_address;Ref=$ib_name;" \ + -confpath /etc/apache2/apache2.conf + + # Проверяем результат публикации + if [[ $? -eq 0 ]]; then + echo -e "${GREEN}Информационная база успешно опубликована${NC}" + systemctl restart apache2 + else + echo -e "${RED}Ошибка публикации информационной базы${NC}" + exit 1 + fi +} + +# Main script +show_header + +# Установка рабочей директории +cd "$(dirname "$0")" + +read -p "Введите версию платформы 1С для установки (например, 8.3.23.1752): " version + +if ! check_version_format "$version"; then + echo -e "${RED}Неверный формат версии платформы${NC}" + show_footer + exit 1 +fi + +if [[ $version < '8.3.20' ]]; then + echo -e "${RED}Платформа версии ниже 8.3.20 не поддерживается${NC}" + show_footer + exit 1 +fi + +# Проверяем установлена ли старая версия +if is_installed "$version"; then + read -p "Версия $version уже установлена. Обновить? (yes/no): " update + if [[ $update == "yes" ]]; then + remove_old_platform "$version" + else + echo -e "${YELLOW}Отменено пользователем${NC}" + show_footer + exit 0 + fi +fi + +if ! extract_distribution "$version"; then + show_footer + exit 1 +fi + +install_dependencies +configure_mpm_worker +install_ws_component "$version" +publish_infobase "$version" + +show_footer \ No newline at end of file diff --git a/linux/configs/.bash_aliases b/linux/configs/.bash_aliases index 037e76b..b45060d 100644 --- a/linux/configs/.bash_aliases +++ b/linux/configs/.bash_aliases @@ -1,6 +1,4 @@ -# Shell Prompt Configuration - -DOCKER_FOLDER=/opt/docker # Default Docker folder if no config exists +DOCKER_FOLDER=$HOME/docker # Default Docker folder if no config exists #------------------------------------------------------------------------------ # DOCKER MANAGEMENT @@ -14,7 +12,7 @@ alias dexec='sudo docker exec -ti' # Execute command in running container ( # Docker Logs and Monitoring alias dlogs='sudo docker logs -tf --tail="50" ' # Show last 50 lines of container logs and follow output -alias dlogsize='sudo du -ch $(sudo docker inspect --format='{{.LogPath}}' $(sudo docker ps -qa)) | sort -h' # Display size of container log files +alias dlogsize='sudo du -ch $(sudo docker inspect --format="{{.LogPath}}" $(sudo docker ps -qa)) | sort -h' # Display size of container log files alias dips="sudo docker ps -q | xargs -n 1 sudo docker inspect -f '{{.Name}}%tab%{{range .NetworkSettings.Networks}}{{.IPAddress}}%tab%{{end}}' | sed 's#%tab%#\t#g' | sed 's#/##g' | sort | column -t -N NAME,IP\(s\) -o $'\t'" # List IP addresses of all containers # Docker Container Management @@ -26,9 +24,25 @@ alias drm='sudo docker rm $(sudo docker ps -a -q)' # Remove specified conta alias dprunevol='sudo docker volume prune' # Remove all unused volumes alias dprunesys='sudo docker system prune -a' # Remove all unused containers, networks, images (both dangling and unreferenced) alias ddelimages='sudo docker rmi $(sudo docker images -q)' # Delete all Docker images -alias derase='dstopcont ; drmcont ; ddelimages ; dvolprune ; dsysprune' # Complete cleanup: stop and remove all containers, images, volumes +alias derase='dstopall ; drm ; ddelimages ; dprunevol ; dprunesys' # Complete cleanup: stop and remove all containers, images, volumes alias dprune='ddelimages ; dprunevol ; dprunesys' # Safe cleanup of unused resources +# Docker Image Management +alias dimages='sudo docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}"' +alias dhist='sudo docker history' # Show image history + +# Docker System Info +alias dinfo='sudo docker info' # Show Docker system information +alias dversion='sudo docker version' # Show Docker version + +# Enhanced monitoring +alias dstats='sudo docker stats --no-stream --format "table {{.Container}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"' +alias dtop='sudo docker stats' # Real-time container stats + +# Docker Compose Validation +alias dcconfig='dcrun config' # Validate and view compose config +alias dcdry='dcrun up --dry-run' # Dry run without starting + #------------------------------------------------------------------------------ # DOCKER COMPOSE & TRAEFIK #------------------------------------------------------------------------------ @@ -47,6 +61,36 @@ alias dclogs='dcrun logs -tf --tail="50"' # Show and follow last 50 lines o # Traefik Specific alias traefiklogs='tail -f $DOCKER_FOLDER/logs/$HOSTNAME/traefik/traefik.log' # Monitor Traefik reverse proxy logs +#------------------------------------------------------------------------------ +# CROWDSEC SECURITY +#------------------------------------------------------------------------------ + +# CrowdSec CLI Commands +alias cscli='dcrun exec -t crowdsec cscli' # Execute CrowdSec CLI commands +alias csdecisions='cscli decisions list' # List current IP ban decisions +alias csalerts='cscli alerts list' # List security alerts +alias csinspect='cscli alerts inspect -d' # Detailed inspection of specific alerts +alias cshubs='cscli hub list' # List available CrowdSec hubs +alias csparsers='cscli parsers list' # List installed parsers +alias cscollections='cscli collections list' # List installed collections +alias cshubupdate='cscli hub update' # Update CrowdSec hub index +alias cshubupgrade='cscli hub upgrade' # Upgrade all hub items +alias csmetrics='cscli metrics' # Show CrowdSec metrics +alias csmachines='cscli machines list' # List CrowdSec machines +alias csbouncers='cscli bouncers list' # List registered bouncers + +# CrowdSec Service Management +alias csfbstatus='sudo systemctl status crowdsec-firewall-bouncer.service' # Check firewall bouncer status +alias csfbstart='sudo systemctl start crowdsec-firewall-bouncer.service' # Start firewall bouncer +alias csfbstop='sudo systemctl stop crowdsec-firewall-bouncer.service' # Stop firewall bouncer +alias csfbrestart='sudo systemctl restart crowdsec-firewall-bouncer.service' # Restart firewall bouncer +alias csbrestart='dcrec2 traefik-bouncer ; csfbrestart' # Restart both Traefik bouncer and firewall bouncer + +# CrowdSec Logs +alias tailkern='sudo tail -f /var/log/kern.log' # Monitor kernel logs +alias tailauth='sudo tail -f /var/log/auth.log' # Monitor authentication logs +alias tailcsfb='sudo tail -f /var/log/crowdsec-firewall-bouncer.log' # Monitor firewall bouncer logs + #------------------------------------------------------------------------------ # SYSTEM ADMINISTRATION #------------------------------------------------------------------------------ @@ -68,12 +112,13 @@ alias ls='ls -F --color=auto --group-directories-first' # Colorized listing alias ll='ls -alh --color=auto --group-directories-first' # Detailed list, human-readable sizes alias lt='ls --human-readable --color=auto --size -1 -S --classify' # List files by size alias lsr='ls --color=auto -t -1' # List files by modification time +alias l='ls --color=auto -lA' # Compact long listing, almost all +alias dir='dir --color=auto' # Colorized dir +alias grep='grep --colour=auto' # Colorized grep alias mkdir='mkdir -pv' # Create parent directories as needed, verbose output alias cp='cp -iv' # Interactive and verbose copy alias mv='mv -iv' # Interactive and verbose move -alias dir='dir --color=auto' -alias grep='grep --colour=auto' -alias l='ls --color=auto -lA' +alias rm="rm -iv" # Storage and Disk Management alias fdisk='sudo fdisk -l' # List disk partitions @@ -159,6 +204,20 @@ alias ufwlist='sudo ufw status numbered' # List UFW rules with numbers alias ufwdelete='sudo ufw delete' # Delete a UFW rule alias ufwreload='sudo ufw reload' # Reload UFW rules +#------------------------------------------------------------------------------ +# SYNOLOGY DSM +#------------------------------------------------------------------------------ + +# Synology Service Management +alias servicelist='sudo synoservicecfg --list' # List all Synology services (DSM 6.x) +alias servicestatus='sudo synosystemctl status' # Check status of Synology service +alias servicestop='sudo synosystemctl stop' # Stop a Synology service +alias servicehstop='sudo synoservicecfg --hard-stop' # Hard stop a service (DSM 6.x) +alias servicestart='sudo synosystemctl start' # Start a Synology service +alias servicehstart='sudo synoservicecfg --hard-start' # Hard start a service (DSM 6.x) +alias servicerestart='sudo synosystemctl restart' # Restart a Synology service +alias restartdocker='sudo synosystemctl restart pkgctl-Docker' # Restart Docker package + #------------------------------------------------------------------------------ # FILE COMPRESSION #------------------------------------------------------------------------------ @@ -180,47 +239,3 @@ alias reload='. ~/.bashrc' # Reload bash configuration alias aliases='nano ~/.bash_aliases' # Edit aliases file alias hosts='sudo nano /etc/hosts' # Edit hosts file alias bashrc='nano ~/.bashrc' # Edit bash configuration file - -#------------------------------------------------------------------------------ -# CROWDSEC SECURITY -#------------------------------------------------------------------------------ - -## CrowdSec CLI Commands -#alias cscli='dcrun exec -t crowdsec cscli' # Execute CrowdSec CLI commands -#alias csdecisions='cscli decisions list' # List current IP ban decisions -#alias csalerts='cscli alerts list' # List security alerts -#alias csinspect='cscli alerts inspect -d' # Detailed inspection of specific alerts -#alias cshubs='cscli hub list' # List available CrowdSec hubs -#alias csparsers='cscli parsers list' # List installed parsers -#alias cscollections='cscli collections list' # List installed collections -#alias cshubupdate='cscli hub update' # Update CrowdSec hub index -#alias cshubupgrade='cscli hub upgrade' # Upgrade all hub items -#alias csmetrics='cscli metrics' # Show CrowdSec metrics -#alias csmachines='cscli machines list' # List CrowdSec machines -#alias csbouncers='cscli bouncers list' # List registered bouncers - -## CrowdSec Service Management -#alias csfbstatus='sudo systemctl status crowdsec-firewall-bouncer.service' # Check firewall bouncer status -#alias csfbstart='sudo systemctl start crowdsec-firewall-bouncer.service' # Start firewall bouncer -#alias csfbstop='sudo systemctl stop crowdsec-firewall-bouncer.service' # Stop firewall bouncer -#alias csfbrestart='sudo systemctl restart crowdsec-firewall-bouncer.service' # Restart firewall bouncer -#alias csbrestart='dcrec2 traefik-bouncer ; csfbrestart' # Restart both Traefik bouncer and firewall bouncer# - -## CrowdSec Logs -#alias tailkern='sudo tail -f /var/log/kern.log' # Monitor kernel logs -#alias tailauth='sudo tail -f /var/log/auth.log' # Monitor authentication logs -#alias tailcsfb='sudo tail -f /var/log/crowdsec-firewall-bouncer.log' # Monitor firewall bouncer logs - -#------------------------------------------------------------------------------ -# SYNOLOGY DSM -#------------------------------------------------------------------------------ - -## Synology Service Management -#alias servicelist='sudo synoservicecfg --list' # List all Synology services (DSM 6.x) -#alias servicestatus='sudo synosystemctl status' # Check status of Synology service -#alias servicestop='sudo synosystemctl stop' # Stop a Synology service -#alias servicehstop='sudo synoservicecfg --hard-stop' # Hard stop a service (DSM 6.x) -#alias servicestart='sudo synosystemctl start' # Start a Synology service -#alias servicehstart='sudo synoservicecfg --hard-start' # Hard start a service (DSM 6.x) -#alias servicerestart='sudo synosystemctl restart' # Restart a Synology service -#alias restartdocker='sudo synosystemctl restart pkgctl-Docker' # Restart Docker package diff --git a/linux/configs/.bashrc b/linux/configs/.bashrc index 4afe366..7c8c14d 100644 --- a/linux/configs/.bashrc +++ b/linux/configs/.bashrc @@ -5,7 +5,9 @@ fi shopt -s checkwinsize shopt -s histappend -echo $LANG + +# Display LANG only in interactive login shells +[[ -n "$PS1" ]] && echo "Current locale: $LANG" export HISTTIMEFORMAT="%h/%d - %H:%M:%S " export HISTSIZE=100000 @@ -54,6 +56,11 @@ if ${use_color} ; then # fi +# alias ls='ls --color=auto' + alias dir='dir --color=auto' + alias grep='grep --colour=auto' +# alias ll='ls --color=auto -la' +# alias l='ls --color=auto -lA' else if [[ ${EUID} == 0 ]] ; then # show root@ when we do not have colors @@ -72,6 +79,55 @@ unset use_color safe_term match_lhs # Ubuntu/Debian [ -r /etc/bash_completion ] && . /etc/bash_completion +# Определение переменной с именем утилиты: bat или batcat +if [[ -e $(which batcat) ]]; then + export bat="batcat" + alias bat="batcat" +elif [[ -e $(which bat) ]]; then + export bat="bat" +fi + +# Конфигурация утилиты bat +if [[ -n $bat ]]; then + export COLORTERM="truecolor" + export BAT_THEME="Nord" # Цветовая тема + export MANPAGER="sh -c 'col -bx | $bat --language=man --style=plain'" # Команда для просмотра man-страниц + export MANROFFOPT="-c" # Отключение перенос строк в man + alias cat="$bat --style=plain --paging=never" + alias less="$bat --paging=always" + if [[ $SHELL == *zsh ]]; then # глобальный алиас --help если оболочка zsh + alias -g -- --help='--help 2>&1 | $bat --language=help --style=plain' + fi + # Функции help имитирует ключ --help только с bat, пример: help ls + help() { "$@" --help 2>&1 | $bat --language=help --style=plain; } + # Функция tailf - аналог tail -f только с bat + tailf() { tail -f "$@" | $bat --paging=never --language=log; } + # Функция для просмотра изменений git diff с помощью bat + batdiff() { git diff --name-only --relative --diff-filter=d | xargs $bat --diff; } +fi + +# Настройка exa как замены ls +if [[ -e $(which exa) ]]; then + if [[ -n "$DISPLAY" || $(tty) == /dev/pts* ]]; then # отображать иконки если псевдотерминал + alias ls="exa --group --header --icons" + else + alias ls="exa --group --header" + fi + alias ll="ls --long" + alias l="ls --long --all --header" + alias lm="ls --long --all --sort=modified" + alias lmm="ls -lbHigUmuSa --sort=modified --time-style=long-iso" + alias lt="ls --tree" + alias lr="ls --recurse" + alias lg="ls --long --git --sort=modified" +fi + +# Load shipped aliases (configs/.bash_aliases) if [ -f ~/.bash_aliases ]; then . ~/.bash_aliases fi + +# Load aliases customizations +if [[ -f ~/.bashrc_aliases ]]; then + source ~/.bashrc_aliases +fi diff --git a/linux/motd/00-header-1c b/linux/motd/00-header-1c new file mode 100644 index 0000000..c56bd68 --- /dev/null +++ b/linux/motd/00-header-1c @@ -0,0 +1,121 @@ +#!/bin/bash +# +# Text Color Variables +tcLtG=$'\033[00;37m' # LIGHT GRAY +tcDkG=$'\033[01;30m' # DARK GRAY +tcLtR=$'\033[01;31m' # LIGHT RED +tcLtGRN=$'\033[01;32m' # LIGHT GREEN +tcLtBL=$'\033[01;34m' # LIGHT BLUE +tcLtP=$'\033[01;35m' # LIGHT PURPLE +tcLtC=$'\033[01;36m' # LIGHT CYAN +tcW=$'\033[01;37m' # WHITE +tcRESET=$'\033[0m' +tcORANGE=$'\033[38;5;209m' +# +# Time of day +HOUR=$(date +"%H") +if [ $HOUR -lt 12 ] && [ $HOUR -ge 0 ]; then TIME="morning" +elif [ $HOUR -lt 17 ] && [ $HOUR -ge 12 ]; then TIME="afternoon" +else TIME="evening" +fi +# +# System uptime +uptime=$(cat /proc/uptime | cut -f1 -d.) +upDays=$((uptime/60/60/24)) +upHours=$((uptime/60/60%24)) +upMins=$((uptime/60%60)) +MEMORY_USED=$(free -m | awk '/Mem/ { printf("%3.1f%%", $3/$2*100) }') +MEMORY1=$(free -t -m | grep "Mem" | awk '{print $3" MB";}') +MEMORY2=$(free -t -m | grep "Mem" | awk '{print $2" MB";}') +ROOT_USED=$(df -h / | awk '/\// {print $(NF-1)}') +# +# System + Memory +SYS_LOADS=$(cat /proc/loadavg | awk '{print $1}') +SWAP_USED=$(free -m | tail -n 1 | awk '{print $3}') +NUM_PROCS=$(ps aux | wc -l) +IPADDRESS=$(hostname -I | awk '{print $1}') +# +# 1C Version check with improved service detection +get_1c_version() { + # Check active services first + ACTIVE_SERVICES=$(systemctl list-units -t service --state=active --no-legend --no-pager | grep -i -E '1c|srv1cv8|ras-' | sort -u) + + # If no active services, check all installed services + if [ -z "$ACTIVE_SERVICES" ]; then + ALL_SERVICES=$(systemctl list-unit-files --type=service --no-legend --no-pager | grep -i -E '1c|srv1cv8|ras-' | awk '{print $1}') + + for service in $ALL_SERVICES; do + # Extract version from service name + VERSION=$(echo "$service" | grep -oE '8\.[0-9]+\.[0-9]+\.[0-9]+') + + if [ -n "$VERSION" ]; then + if [[ "$service" =~ ras- ]]; then + TYPE="RAS" + elif [[ "$service" =~ @2x ]]; then + TYPE="Debug" + else + TYPE="Server" + fi + + echo -e "${tcLtG} - 1C Version..........:${tcW} $VERSION ($TYPE, service exists but not running)" + fi + done + + if [ -z "$ALL_SERVICES" ]; then + echo -e "${tcLtG} - 1C Version..........:${tcW} No 1C services found" + fi + return + fi + + # Process active services + echo "$ACTIVE_SERVICES" | awk -v tcLtG="$tcLtG" -v tcW="$tcW" ' + { + service = $1 + status = $3 + version = service + sub(/.*[^0-9]/, "", version) + sub(/[^0-9].*/, "", version) + + if (version == "") { + desc = $0 + sub(/^[^ ]+ +[^ ]+ +[^ ]+ +[^ ]+ +/, "", desc) + if (match(desc, /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) { + version = substr(desc, RSTART, RLENGTH) + } else { + version = "unknown" + } + } + + if (service ~ /ras-/) type = "RAS" + else if (service ~ /@2x/) type = "Debug" + else type = "Server" + + versions[version] = versions[version] ? versions[version] ", " type : type + } + END { + for (v in versions) { + printf("%s - 1C Version..........:%s %s (%s)\n", tcLtG, tcW, v, versions[v]) + } + }' +} +# +echo -e "${tcDkG}===================================================================" +echo -e "${tcLtG} Good $TIME ! ${tcORANGE} Организация " +echo -e "${tcDkG}===================================================================" +echo -e "${tcLtG} Сервер 1С:Предприятие x64" +echo -e "${tcDkG}===================================================================" +echo -e "${tcLtG} - Hostname............:${tcW} $(hostname -f)" +echo -e "${tcLtG} - IP Address..........:${tcW} $IPADDRESS" +echo -e "${tcLtG} - Release.............:${tcW} $(lsb_release -s -d)" +echo -e "${tcLtG} - Kernel..............: ${tcW}$(uname -r)" +echo -e "${tcLtG} - Users...............: ${tcW}Currently $(users | wc -w) user(s) logged on" +echo -e "${tcLtG} - Server Time.........: ${tcW}$(date)" +echo -e "${tcLtG} - System load.........: ${tcW}$SYS_LOADS / $NUM_PROCS processes running" +echo -e "${tcLtG} - Memory used.........: ${tcW}$MEMORY1 / $MEMORY2" +echo -e "${tcLtG} - Memory used %.......: ${tcW}$MEMORY_USED" +echo -e "${tcLtG} - / used..............: ${tcW}$ROOT_USED" +echo -e "${tcLtG} - Swap used...........: ${tcW}$SWAP_USED MB" +echo -e "${tcLtG} - System uptime.......: ${tcW}$upDays days $upHours hours $upMins minutes" +get_1c_version +echo -e "${tcDkG}===================================================================" +echo -e "${tcRESET}" diff --git a/linux/motd/00-header-pgsql b/linux/motd/00-header-pgsql new file mode 100644 index 0000000..5349280 --- /dev/null +++ b/linux/motd/00-header-pgsql @@ -0,0 +1,168 @@ +#!/bin/bash + +# Text Color Variables +tcLtG=$'\033[00;37m' # LIGHT GRAY +tcDkG=$'\033[01;30m' # DARK GRAY +tcLtR=$'\033[01;31m' # LIGHT RED +tcLtGRN=$'\033[01;32m' # LIGHT GREEN +tcLtBL=$'\033[01;34m' # LIGHT BLUE +tcLtP=$'\033[01;35m' # LIGHT PURPLE +tcLtC=$'\033[01;36m' # LIGHT CYAN +tcW=$'\033[01;37m' # WHITE +tcRESET=$'\033[0m' +tcORANGE=$'\033[38;5;209m' + +# Time of day +HOUR=$(date +"%H") +if [ $HOUR -lt 12 ] && [ $HOUR -ge 0 ]; then TIME="morning" +elif [ $HOUR -lt 17 ] && [ $HOUR -ge 12 ]; then TIME="afternoon" +else TIME="evening" +fi + +# System uptime +uptime=$(cat /proc/uptime | cut -f1 -d.) +upDays=$((uptime/60/60/24)) +upHours=$((uptime/60/60%24)) +upMins=$((uptime/60%60)) + +# MEMORY_USED - используем free для подсчёта процента использованной памяти +MEMORY_USED=$(free -m | awk '/Mem/ { printf("%3.1f%%", $3/$2*100) }') + +# SYS_LOADS - берем первое значение из /proc/loadavg +SYS_LOADS=$(cat /proc/loadavg | awk '{print $1}') + +# Вытаскиваем числовое значение памяти (без процента) +MEMORY_USED_VALUE=$(echo "$MEMORY_USED" | tr -d '%') + +# Окраска MEMORY_USED (>80% красный, иначе зеленый) +if (( $(echo "$MEMORY_USED_VALUE > 80" | bc -l) )); then + MEMORY_USED_COLORED="${tcLtR}$MEMORY_USED${tcW}" +else + MEMORY_USED_COLORED="${tcLtGRN}$MEMORY_USED${tcW}" +fi + +# Количество CPU ядер +CPU_CORES=$(nproc) + +# Преобразуем SYS_LOADS в число с плавающей точкой (для сравнения) +SYS_LOADS_FLOAT=$(echo "$SYS_LOADS" | awk '{print $1 + 0}') + +# Порог загрузки: 70% от количества ядер +LOAD_THRESHOLD=$(echo "$CPU_CORES * 0.7" | bc) + +# Окраска SYS_LOADS (>70% от CPU CORES - красный, иначе зеленый) +if (( $(echo "$SYS_LOADS_FLOAT > $LOAD_THRESHOLD" | bc -l) )); then + SYS_LOADS_COLORED="${tcLtR}$SYS_LOADS${tcW}" +else + SYS_LOADS_COLORED="${tcLtGRN}$SYS_LOADS${tcW}" +fi + +# Используемая и общая память (MB) +MEMORY1=$(free -t -m | grep "Mem" | awk '{print $3" MB";}') +MEMORY2=$(free -t -m | grep "Mem" | awk '{print $2" MB";}') + +# Использование корневого раздела в процентах +ROOT_USED=$(df -h / | awk '/\// {print $(NF-1)}') +ROOT_USED_PCT=${ROOT_USED%\%} + +# Подсветка использования корневого раздела (>80% красный, иначе зеленый) +if [ "$ROOT_USED_PCT" -gt 80 ]; then + ROOT_USED_COLORED="${tcLtR}$ROOT_USED${tcW}" +else + ROOT_USED_COLORED="${tcLtGRN}$ROOT_USED${tcW}" +fi + +# Использование swap, количество процессов, IP адрес +SWAP_USED=$(free -m | tail -n 1 | awk '{print $3}') +NUM_PROCS=$(ps aux | wc -l) +IPADDRESS=$(hostname -I | awk '{print $1}') + +# Функция для вывода информации о PostgreSQL-инстансах +get_postgresql_info() { + if ! command -v psql &> /dev/null; then + echo -e "${tcLtG} - PostgreSQL..........:${tcW} Not installed" + return + fi + + # Получаем уникальные порты PostgreSQL активных прослушиваний + PORTS=$(ss -tlnp 2>/dev/null \ + | grep postgres \ + | grep -oP ':\d+' \ + | grep -oP '\d+' \ + | sort -u) + + if [ -z "$PORTS" ]; then + echo -e "${tcLtG} - PostgreSQL..........:${tcW} No active instances found" + return + fi + + for PORT in $PORTS; do + echo -e "${tcLtC}== Instance on port $PORT ======================${tcRESET}" + + # Проверка доступности инстанса через pg_isready + if ! pg_isready -p "$PORT" -U postgres -t 3 >/dev/null 2>&1; then + echo -e "Port ${tcLtR}$PORT${tcRESET}: ${tcLtR}not ready, skip${tcRESET}" + continue + else + echo -e "Port ${tcLtGRN}$PORT${tcRESET}: ${tcLtGRN}ready${tcRESET}" + fi + + # Получаем версию PostgreSQL + PG_VERSION=$(psql -U postgres -p "$PORT" -d postgres -c "SHOW server_version;" -t 2>/dev/null | head -n1 | tr -d ' ') + if [ -z "$PG_VERSION" ]; then + echo -e "${tcLtR} - Cannot get PostgreSQL version on port $PORT${tcRESET}" + continue + fi + echo -e "${tcLtG} - PostgreSQL Version..:${tcW} $PG_VERSION" + + # Кол-во баз без шаблонных и 'postgres' + DB_COUNT=$(psql -U postgres -p "$PORT" -d postgres -t -c "SELECT COUNT(*) FROM pg_database WHERE datistemplate = false AND datname <> 'postgres';" 2>/dev/null | tr -d '[:space:]') + [ -z "$DB_COUNT" ] && DB_COUNT="0" + echo -e "${tcLtG} - Databases Count.....:${tcW} $DB_COUNT" + + # Общий размер баз + TOTAL_SIZE=$(psql -U postgres -p "$PORT" -d postgres -t -c "SELECT COALESCE(pg_size_pretty(SUM(pg_database_size(oid))), '0B') FROM pg_database WHERE datistemplate = false AND datname <> 'postgres';" 2>/dev/null | tr -d '[:space:]') + [ -z "$TOTAL_SIZE" ] && TOTAL_SIZE="0B" + echo -e "${tcLtG} - Total Size.........:${tcW} $TOTAL_SIZE" + + echo -e "${tcLtG} - Databases..:${tcW}" + # Выводим топ-3 баз по размеру + psql -U postgres -p "$PORT" -d postgres -A -t -c " + SELECT datname, pg_size_pretty(pg_database_size(oid)) AS size + FROM pg_database + WHERE datistemplate = false + AND datname <> 'postgres' + ORDER BY pg_database_size(oid) DESC + LIMIT 3; + " 2>/dev/null | while IFS='|' read -r DB_NAME DB_SIZE; do + [ -n "$DB_NAME" ] && echo -e " ${tcLtG}$DB_NAME${tcW} ($DB_SIZE)" + done + + echo # Пустая строка между инстансами + done +} + +# Выводим собранную системную информацию +echo -e "${tcDkG}===================================================================" +echo -e "${tcLtG} Good $TIME ! ${tcORANGE} Организация " +echo -e "${tcDkG}===================================================================" +echo -e "${tcLtG} Сервер БД PostgreSQL (БД)" +echo -e "${tcDkG}===================================================================" +echo -e "${tcLtG} - Hostname............:${tcW} $(hostname -f)" +echo -e "${tcLtG} - IP Address..........:${tcW} $IPADDRESS" +echo -e "${tcLtG} - Release.............:${tcW} $(lsb_release -s -d)" +echo -e "${tcLtG} - Kernel..............: ${tcW}$(uname -r)" +echo -e "${tcLtG} - Users...............: ${tcW}Currently $(users | wc -w) user(s) logged on" +echo -e "${tcLtG} - Server Time.........: ${tcW}$(date)" +echo -e "${tcLtG} - System load.........: ${tcW}$SYS_LOADS_COLORED / $NUM_PROCS processes running" +echo -e "${tcLtG} - Memory used.........: ${tcW}$MEMORY1 / $MEMORY2" +echo -e "${tcLtG} - Memory used %.......: ${tcW}$MEMORY_USED_COLORED" +echo -e "${tcLtG} - / used..............: ${tcW}$ROOT_USED_COLORED" +echo -e "${tcLtG} - Swap used...........: ${tcW}$SWAP_USED MB" +echo -e "${tcLtG} - System uptime.......: ${tcW}$upDays days $upHours hours $upMins minutes" +echo -e "${tcDkG}===================================================================" +# Вызов функции PostgreSQL info +get_postgresql_info + +echo -e "${tcDkG}===================================================================" +echo -e "${tcRESET}" diff --git a/linux/motd/00-header-pve b/linux/motd/00-header-pve new file mode 100644 index 0000000..2ce02cd --- /dev/null +++ b/linux/motd/00-header-pve @@ -0,0 +1,65 @@ +#!/bin/bash + +# Text Color Variables http://misc.flogisoft.com/bash/tip_colors_and_formatting +tcLtG="\033[00;37m" # LIGHT GRAY +tcDkG="\033[01;30m" # DARK GRAY +tcLtR="\033[01;31m" # LIGHT RED +tcLtGRN="\033[01;32m" # LIGHT GREEN +tcLtBL="\033[01;34m" # LIGHT BLUE +tcLtP="\033[01;35m" # LIGHT PURPLE +tcLtC="\033[01;36m" # LIGHT CYAN +tcW="\033[01;37m" # WHITE +tcRESET="\033[0m" +tcORANGE="\033[38;5;209m" + +# Time of day +HOUR=$(date +"%H") +if [ $HOUR -lt 12 ] && [ $HOUR -ge 0 ]; then + TIME="morning" +elif [ $HOUR -lt 17 ] && [ $HOUR -ge 12 ]; then + TIME="afternoon" +else + TIME="evening" +fi + +# System uptime +uptime=$(cat /proc/uptime | cut -f1 -d.) +upDays=$((uptime/60/60/24)) +upHours=$((uptime/60/60%24)) +upMins=$((uptime/60%60)) + +# Memory information +MEMORY_USED=$(free -m | awk '/Mem/ { printf("%3.1f%%", $3/$2*100) }') +MEMORY1=$(free -t -m | grep "Mem" | awk '{print $3" MB";}') +MEMORY2=$(free -t -m | grep "Mem" | awk '{print $2" MB";}') +ROOT_USED=$(df -h / | awk '/\// {print $(NF-1)}') + +# System information +SYS_LOADS=$(cat /proc/loadavg | awk '{print $1}') +SWAP_USED=$(free -m | tail -n 1 | awk '{print $3}') +NUM_PROCS=$(ps aux | wc -l) +IPADDRESS=$(hostname --all-ip-addresses) + +# Proxmox version +PVE_VERSION=$(pveversion 2>/dev/null || echo "Not a Proxmox host or pveversion not installed") + +echo -e "${tcDkG}===================================================================" +echo -e "${tcLtG} Good $TIME ! ${tcORANGE} ИП Чернов А.Ф." +echo -e "${tcDkG}===================================================================" +echo -e "${tcLtG} Сервер виртуализации Proxmox " +echo -e "${tcDkG}===================================================================" +echo -e "${tcLtG} - Hostname............:${tcW} $(hostname -f)" +echo -e "${tcLtG} - IP Address..........:${tcW} $IPADDRESS" +echo -e "${tcLtG} - Proxmox Version.....:${tcW} $PVE_VERSION" +echo -e "${tcLtG} - Release.............:${tcW} $(lsb_release -s -d)" +echo -e "${tcLtG} - Kernel..............: $(uname -r)" +echo -e "${tcLtG} - Users...............: Currently $(users | wc -w) user(s) logged on" +echo -e "${tcLtG} - Server Time.........: $(date)" +echo -e "${tcLtG} - System load.........: $SYS_LOADS / $NUM_PROCS processes running" +echo -e "${tcLtG} - Memory used.........: $MEMORY1 / $MEMORY2" +echo -e "${tcLtG} - Memory used %.......: $MEMORY_USED" +echo -e "${tcLtG} - / used..............: $ROOT_USED" +echo -e "${tcLtG} - Swap used...........: $SWAP_USED MB" +echo -e "${tcLtG} - System uptime.......: $upDays days $upHours hours $upMins minutes" +echo -e "${tcDkG}===================================================================" +echo -e "${tcRESET}" \ No newline at end of file diff --git a/linux/motd/99-mymotd-generator b/linux/motd/99-mymotd-generator new file mode 100644 index 0000000..020efc3 --- /dev/null +++ b/linux/motd/99-mymotd-generator @@ -0,0 +1,96 @@ +#!/bin/bash +# + +function display() { + # $1=name $2=value $3=red_limit $4=minimal_show_limit $5=unit $6=after + local great=">" + if [[ -n "$2" && "$2" > "0" && (( "${2%.*}" -ge "$4" )) ]]; then + printf "%-14s" "$1" + if awk "BEGIN{exit ! ($2 $great $3)}"; then echo -ne "\e[0;91m $2"; else echo -ne "\e[0;92m $2"; fi + printf "%-1s\x1B[0m" "$5" + printf "%-11s\t" "$6" + return 1 + fi +} + +function storage_info() { + RootInfo=$(df -h /) + root_usage=$(awk '/\// {print $(NF-1)}' <<<${RootInfo} | sed 's/%//g') + root_total=$(awk '/\// {print $(NF-4)}' <<<${RootInfo}) +} + +storage_info +critical_load=80 + +UPTIME=$(LC_ALL=C uptime) +UPT1=${UPTIME#*'up '} +UPT2=${UPT1%'user'*} +time=${UPT2%','*} +time=${time//','} +time=$(echo $time | xargs) +load=${UPTIME#*'load average: '} +load=${load//','} + +# Text Color Variables http://misc.flogisoft.com/bash/tip_colors_and_formatting +tcLtG="\033[00;37m" # LIGHT GRAY +tcDkG="\033[01;30m" # DARK GRAY +tcLtR="\033[01;31m" # LIGHT RED +tcLtGRN="\033[01;32m" # LIGHT GREEN +tcLtBL="\033[01;34m" # LIGHT BLUE +tcLtP="\033[01;35m" # LIGHT PURPLE +tcLtC="\033[01;36m" # LIGHT CYAN +tcW="\033[01;37m" # WHITE +tcRESET="\033[0m" +tcORANGE="\033[38;5;209m" + +# Time of day +HOUR=$(date +"%H") +if [ $HOUR -lt 12 -a $HOUR -ge 0 ]; then TIME="morning" +elif [ $HOUR -lt 17 -a $HOUR -ge 12 ]; then TIME="afternoon" +else TIME="evening" +fi + +# System uptime +uptime=`cat /proc/uptime | cut -f1 -d.` +upDays=$((uptime/60/60/24)) +upHours=$((uptime/60/60%24)) +upMins=$((uptime/60%60)) + +# memory +mem_info=$(LC_ALL=C free -w 2>/dev/null | grep "^Mem" || LC_ALL=C free | grep "^Mem") +memory_usage=$(awk '{printf("%.0f",(($2-($4+$6+$7))/$2) * 100)}' <<<${mem_info}) +mem_info=$(echo $mem_info | awk '{print $2}') +memory_total=$(( mem_info / 1024 )) + +if [[ ${memory_total} -gt 1000 ]]; then + memory_total=$(awk '{printf("%.2f",$1/1024)}' <<<${memory_total})"G" +else + memory_total+="M" +fi + +ROOT_USED=`df -h / | awk '/\// {print $(NF-1)}'` + +# System + Memory +SYS_LOADS=`cat /proc/loadavg | awk '{print $1}'` +SWAP_USED=`free -m | tail -n 1 | awk '{print $3}'` +NUM_PROCS=`ps aux | wc -l` +IPADDRESS=`hostname --all-ip-addresses` + +echo -e $tcDkG "===================================================================" +echo -e $tcLtG " Good $TIME ! $tcORANGE IT" +echo -e $tcDkG "===================================================================" +echo -e $tcLtG " Сервер ..." +echo -e $tcDkG "===================================================================" +echo -e $tcLtG " - Hostname............:$tcW `hostname -f`" +echo -e $tcLtG " - IP Address..........:$tcW $IPADDRESS" +echo -e $tcLtG " - Release.............:$tcW $(lsb_release -s -d)" +echo -e $tcLtG " - Kernel..............: `uname -r`" +echo -e $tcLtG " - Users...............: Currently `users | wc -w` user(s) logged on" +echo -e $tcLtG " - Server Time.........: `date`" +echo -e $tcLtG " - System load.........: $SYS_LOADS / $NUM_PROCS processes running" +echo -e $tcLtG " - `display "Memory usage" "$memory_usage" "70" "0" "%" " of ${memory_total}"`" +echo -e $tcLtG " - `display "Usage of /" "$root_usage" "90" "1" "%" " of $root_total"`" +echo -e $tcLtG " - Swap used...........: $SWAP_USED MB" +echo -e $tcLtG " - System uptime.......: $upDays days $upHours hours $upMins minutes" +echo -e $tcDkG "===================================================================" +echo -e $tcRESET "" diff --git a/linux/setup-unattended-upgrades.sh b/linux/setup-unattended-upgrades.sh new file mode 100644 index 0000000..33cd87c --- /dev/null +++ b/linux/setup-unattended-upgrades.sh @@ -0,0 +1,150 @@ +#!/bin/bash +set -euo pipefail + +# Константы +CONFIG_50="/etc/apt/apt.conf.d/50unattended-upgrades" +CONFIG_20="/etc/apt/apt.conf.d/20auto-upgrades" +EMAIL="gpas@dioservice.ru" +SENDER="itsp72@gmail.com" + +# Функция для создания бэкапа файла +backup_file() { + local file=$1 + if [ -f "$file" ]; then + cp -v "$file" "${file}.bak" + fi +} + +# Проверка прав root +check_root() { + if [ "$(id -u)" -ne 0 ]; then + echo "Ошибка: скрипт должен быть запущен от имени root!" >&2 + exit 1 + fi +} + +# Установка необходимых пакетов +install_packages() { + if ! dpkg -l unattended-upgrades >/dev/null 2>&1; then + apt-get update && apt-get install -y unattended-upgrades + fi +} + +# Определение типа системы +detect_system() { + if grep -qs "Proxmox Backup Server" /etc/issue; then + echo "pbs" + elif grep -qs "Proxmox Virtual Environment" /etc/issue; then + echo "pve" + else + echo "debian" + fi +} + +# Проверка наличия Docker +check_docker() { + if dpkg -l docker-ce >/dev/null 2>&1 || \ + dpkg -l docker.io >/dev/null 2>&1 || \ + command -v docker >/dev/null 2>&1; then + echo true + else + echo false + fi +} + +# Генерация конфигурации для 50unattended-upgrades +generate_config() { + local system_type=$1 + local has_docker=$2 + + # Базовые настройки + cat > "$CONFIG_50" <> "$CONFIG_50" <> "$CONFIG_50" ;; + "pbs") echo ' "origin=Proxmox,codename=${distro_codename},label=pbs-no-subscription";' >> "$CONFIG_50" ;; + esac + + # Репозиторий Docker при необходимости + if "$has_docker"; then + echo ' "origin=Docker,a=bookworm,l=Docker CE";' >> "$CONFIG_50" + fi + + # Основные параметры + cat >> "$CONFIG_50" <> "$CONFIG_50" < "$CONFIG_20" </dev/null 2>&1 || true + systemctl restart unattended-upgrades >/dev/null 2>&1 || true +} + +# Основной поток выполнения +main() { + check_root + install_packages + + # Создание бэкапов + backup_file "$CONFIG_50" + backup_file "$CONFIG_20" + + local system_type has_docker + system_type=$(detect_system) + has_docker=$(check_docker) + + generate_config "$system_type" "$has_docker" + setup_auto_upgrades + enable_service + + echo -e "\nНастройка завершена успешно!" + echo "Тип системы: $system_type" + echo "Docker: $([ "$has_docker" = "true" ] && echo "установлен" || echo "отсутствует")" + echo "Логи: /var/log/unattended-upgrades/unattended-upgrades.log" + echo "Бэкапы созданы:" + ls -la ${CONFIG_50}* ${CONFIG_20}* +} + +main diff --git a/pgsql/pgpro/README.md b/pgsql/pgpro/README.md new file mode 100644 index 0000000..3999dce --- /dev/null +++ b/pgsql/pgpro/README.md @@ -0,0 +1,31 @@ +# pgsql/pgpro + +Рабочие скрипты обслуживания Postgres Pro (вариант для 1С), перенесённые из +репозитория `linux` «как есть» с боевых серверов. В отличие от курируемых +скриптов корня репозитория, здесь CI проверяет их линтером на пороге **ошибок** +(а не предупреждений) — чтобы не переписывать проверенную в проде логику. + +## Файлы + +| Файл | Назначение | +|---|---| +| `pg_add_cluster.sh` | Добавление нового кластера Postgres Pro на хост (initdb, регистрация, настройка). | +| `maintenance.sh` | Регламентное обслуживание БД. | +| `wal-g-scripts/` | Обвязка [WAL-G](https://github.com/wal-g/wal-g): конфиг, полный/инкрементальный бэкап, список, restore, удаление. | +| `backupscripts/` | Скрипты pg_probackup: бэкап, восстановление, первичная настройка. | +| `systemd/` | Юниты `backup-pgsql.service` + `backup-pgsql.timer` для запуска бэкапа по расписанию. | +| `legacy/` | Черновики бэкап-скриптов, **не проверены** (см. ниже). | + +## WAL-G binary + +Бинарник `wal-g` (`.deb`) намеренно **не** хранится в git. Скачайте нужную сборку +из релизов проекта: https://github.com/wal-g/wal-g/releases +(в исходном репозитории использовалась `wal-g-3.0.8` для Debian trixie). + +## legacy/ + +Несколько ранних вариантов скриптов бэкапа PostgreSQL для 1С. По словам автора — +**не проверялись**. Оставлены для истории/референса, в боевом использовании +опирайтесь на `../backup-db.sh` (курируемый) либо на `wal-g-scripts/` / +`backupscripts/`. Пароли в этих файлах — плейсхолдеры (`pass`), замените своими +или используйте `~/.pgpass`. diff --git a/pgsql/pgpro/backupscripts/pg_backup.sh b/pgsql/pgpro/backupscripts/pg_backup.sh new file mode 100644 index 0000000..6750c34 --- /dev/null +++ b/pgsql/pgpro/backupscripts/pg_backup.sh @@ -0,0 +1,170 @@ +#!/bin/bash +# Скрипт бэкапа PostgreSQL с последующим обслуживанием БД + +# Проверка пользователя +if [ "$(whoami)" != "postgres" ]; then + echo "ОШИБКА: Скрипт должен запускаться от пользователя postgres!" >&2 + exit 1 +fi + +# Пути и настройки +CONFIG_FILE="/var/lib/pgpro/_backup/scripts/pg_backup.conf" +LOCK_FILE="/tmp/pg_backup.lock" +TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S) +PGPASSFILE="$HOME/.pgpass" + +# Проверка наличия .pgpass +[ -f "$PGPASSFILE" ] || { + echo "ОШИБКА: Файл .pgpass не найден!" >&2 + echo "Создайте его командой: echo \"localhost:5432:*:postgres:ваш_пароль\" > ~/.pgpass && chmod 600 ~/.pgpass" >&2 + exit 1 +} + +# Функции +error_exit() { + local msg="$1" + echo "ОШИБКА: $msg" >&2 + [ -f "$LOCK_FILE" ] && rm -f "$LOCK_FILE" + exit 1 +} + +load_config() { + [ -f "$CONFIG_FILE" ] || error_exit "Конфигурационный файл $CONFIG_FILE не найден" + source "$CONFIG_FILE" || error_exit "Ошибка загрузки конфигурации" + + # Проверка обязательных параметров + local required_vars=( + BACKUP_DIR LOG_DIR PG_HOST PG_USER + DB_LIST_FILE LOG_LEVEL BACKUP_RETENTION + ) + for var in "${required_vars[@]}"; do + [ -z "${!var}" ] && error_exit "Не указан обязательный параметр: $var" + done + mkdir -p "$BACKUP_DIR" "$LOG_DIR" || error_exit "Не удалось создать директории" +} + +log() { + local level="$1" + local message="$2" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo "[$timestamp] [${level^^}] $message" >> "${LOG_DIR}/backup.log" + [ "$level" == "ERROR" ] && echo "[$timestamp] [${level^^}] $message" >&2 +} + +check_pg_connection() { + export PGPASSFILE + if ! psql -h "$PG_HOST" -U "$PG_USER" -lqt >/dev/null 2>&1; then + log "ERROR" "Не удалось подключиться к PostgreSQL" + error_exit "Проверьте доступность PostgreSQL и параметры подключения" + fi +} + +check_db_exists() { + local db="$1" + export PGPASSFILE + if ! psql -h "$PG_HOST" -U "$PG_USER" -lqt | cut -d \| -f 1 | tr -d ' ' | grep -qw "$db"; then + log "WARNING" "База данных $db не найдена" + return 1 + fi + return 0 +} + +create_backup() { + local db="$1" + local backup_file="${BACKUP_DIR}/${TIMESTAMP}-${db}.dump" + + log "INFO" "Начало бэкапа БД: $db" + export PGPASSFILE + + # Выполняем бэкап и сохраняем результат + if pg_dump -h "$PG_HOST" -U "$PG_USER" -Fc "$db" -f "$backup_file" 2>> "${LOG_DIR}/backup.log"; then + # Получаем размер файла отдельной командой + local backup_size=$(du -h "$backup_file" | cut -f1) + log "INFO" "Успешный бэкап БД: $db (размер: $backup_size)" + return 0 + else + log "ERROR" "Ошибка при создании бэкапа БД: $db" + [ -f "$backup_file" ] && rm -f "$backup_file" + return 1 + fi +} + +perform_maintenance() { + local db="$1" + export PGPASSFILE + + log "INFO" "Начало обслуживания БД: $db" + + # VACUUM ANALYZE + if ! vacuumdb -h "$PG_HOST" -U "$PG_USER" --analyze "$db" >> "${LOG_DIR}/backup.log" 2>&1; then + log "ERROR" "Ошибка при выполнении VACUUM ANALYZE для БД: $db" + return 1 + fi + + # Полное обслуживание по воскресеньям + if [ "$(date +%u)" -eq 7 ]; then + log "INFO" "Полное обслуживание (воскресенье) БД: $db" + + # VACUUM FULL + if ! vacuumdb -h "$PG_HOST" -U "$PG_USER" --full "$db" >> "${LOG_DIR}/backup.log" 2>&1; then + log "ERROR" "Ошибка при выполнении VACUUM FULL для БД: $db" + return 1 + fi + + # REINDEX + if ! reindexdb -h "$PG_HOST" -U "$PG_USER" "$db" >> "${LOG_DIR}/backup.log" 2>&1; then + log "ERROR" "Ошибка при выполнении REINDEX для БД: $db" + return 1 + fi + fi + + log "INFO" "Обслуживание БД завершено: $db" + return 0 +} + +cleanup_old_backups() { + log "INFO" "Очистка старых бэкапов (старше $BACKUP_RETENTION дней)" + find "$BACKUP_DIR" -type f -name "*.dump" -mtime "+$BACKUP_RETENTION" -delete 2>/dev/null || { + log "WARNING" "Ошибка при удалении старых бэкапов" + } +} + +### ОСНОВНОЙ БЛОК ### +load_config +check_pg_connection + +# Блокировка +if [ -e "$LOCK_FILE" ]; then + pid=$(cat "$LOCK_FILE") + if kill -0 "$pid" 2>/dev/null; then + error_exit "Скрипт уже выполняется (PID: $pid)" + else + rm -f "$LOCK_FILE" + fi +fi +echo $$ > "$LOCK_FILE" +trap '[ -f "$LOCK_FILE" ] && rm -f "$LOCK_FILE"' EXIT + +log "INFO" "=== СТАРТ ПРОЦЕДУРЫ БЭКАПА ===" + +while read -r db_line; do + [[ "$db_line" =~ ^#|^$ ]] && continue + db_name=$(echo "$db_line" | awk '{print $1}') + + if check_db_exists "$db_name"; then + # Сначала создаем бэкап + if create_backup "$db_name"; then + # ТОЛЬКО если бэкап успешен - выполняем обслуживание + log "INFO" "Бэкап успешен, начинаем обслуживание БД: $db_name" + if ! perform_maintenance "$db_name"; then + log "ERROR" "Ошибка обслуживания БД: $db_name (бэкап сохранен)" + fi + else + log "ERROR" "Пропускаем обслуживание - бэкап БД $db_name не удался" + fi + fi +done < "$DB_LIST_FILE" + +cleanup_old_backups +log "INFO" "=== ПРОЦЕДУРА ЗАВЕРШЕНА ===" +exit 0 diff --git a/pgsql/pgpro/backupscripts/pg_backup_setup.sh b/pgsql/pgpro/backupscripts/pg_backup_setup.sh new file mode 100644 index 0000000..02d78b0 --- /dev/null +++ b/pgsql/pgpro/backupscripts/pg_backup_setup.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# Скрипт инициализации конфигурации бэкапа PostgreSQL + +set -o errexit +set -o nounset + +CONFIG_DIR="/var/lib/pgpro/_backup/scripts" +CONFIG_FILE="${CONFIG_DIR}/pg_backup.conf" +DB_LIST_FILE="${CONFIG_DIR}/db_list.txt" + +# Создаем директории если не существуют +mkdir -p "${CONFIG_DIR}" "/var/lib/pgpro/_backup/DB" "/var/lib/pgpro/_backup/logs" + +# Создаем конфигурационный файл +cat > "${CONFIG_FILE}" <<'EOF' +#!/bin/bash +# Конфигурация скрипта бэкапа PostgreSQL + +### ПУТИ ### +SCRIPT_DIR="/var/lib/pgpro/_backup/scripts" +BACKUP_DIR="/var/lib/pgpro/_backup/DB" +LOG_DIR="/var/lib/pgpro/_backup/logs" + +### POSTGRESQL ### +PG_HOST="localhost" +PG_PORT="5432" +PG_USER="postgres" +DB_LIST_FILE="${SCRIPT_DIR}/db_list.txt" # Файл со списком БД + +### ЛОГИРОВАНИЕ ### +LOG_LEVEL="error" # error|warning|info|debug +LOG_RETENTION_DAYS=30 # Дней хранения логов + +### УВЕДОМЛЕНИЯ ### +NOTIFY_ENABLED=false # true|false +NOTIFY_EMAIL="" +NOTIFY_FROM="" + +### НАСТРОЙКИ БЭКАПА ### +COMPRESSION_LEVEL=6 # Уровень сжатия (1-9) +BACKUP_RETENTION=7 # Дней хранения бэкапов +EOF + +# Создаем файл со списком БД +cat > "${DB_LIST_FILE}" <<'EOF' +# Список баз данных для бэкапа +# По одной БД на строку в формате: db_name +# example_db +EOF + +# Устанавливаем права +chmod 600 "${CONFIG_FILE}" +chmod 600 "${DB_LIST_FILE}" + +echo "Конфигурация создана:" +echo " - Основной конфиг: ${CONFIG_FILE}" +echo " - Список БД: ${DB_LIST_FILE}" +echo "Перед использованием отредактируйте эти файлы" diff --git a/pgsql/pgpro/backupscripts/pg_restore.sh b/pgsql/pgpro/backupscripts/pg_restore.sh new file mode 100644 index 0000000..893436f --- /dev/null +++ b/pgsql/pgpro/backupscripts/pg_restore.sh @@ -0,0 +1,148 @@ +#!/bin/bash +# Интерактивный скрипт восстановления PostgreSQL из бэкапа + +# Проверка пользователя +if [ "$(whoami)" != "postgres" ]; then + echo "ОШИБКА: Скрипт должен запускаться от пользователя postgres!" >&2 + exit 1 +fi + +# Пути и настройки +BACKUP_DIR="/var/lib/pgpro/_backup/DB" +LOG_DIR="/var/lib/pgpro/_backup/logs" +PGPASSFILE="$HOME/.pgpass" +RESTORE_LOG="$LOG_DIR/restore_$(date +%Y-%m-%d_%H-%M-%S).log" + +# Функции +log() { + local message="$1" + echo "$(date '+%Y-%m-%d %H:%M:%S') $message" | tee -a "$RESTORE_LOG" +} + +error_exit() { + local msg="$1" + log "ОШИБКА: $msg" + exit 1 +} + +check_pg_connection() { + if ! psql -lqt >/dev/null 2>&1; then + error_exit "Не удалось подключиться к PostgreSQL" + fi +} + +list_backups() { + log "Доступные резервные копии:" + local i=1 + for backup in $(ls -1t "$BACKUP_DIR"/*.dump 2>/dev/null); do + local size=$(du -h "$backup" | cut -f1) + local mtime=$(stat -c %y "$backup" | cut -d'.' -f1) + printf " %2d) %s (%s, %s)\n" "$i" "$(basename "$backup")" "$size" "$mtime" + backup_list[$i]=$backup + ((i++)) + done + + [ ${#backup_list[@]} -eq 0 ] && error_exit "Резервные копии не найдены в $BACKUP_DIR" +} + +select_backup() { + read -p "Выберите номер бэкапа для восстановления: " backup_num + + if ! [[ "$backup_num" =~ ^[0-9]+$ ]] || [ -z "${backup_list[$backup_num]}" ]; then + error_exit "Некорректный выбор" + fi + + selected_backup="${backup_list[$backup_num]}" + log "Выбран бэкап: $selected_backup" +} + +get_db_name_from_backup() { + local backup="$1" + basename "$backup" | sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}-(.*)\.dump$/\1/' +} + +list_databases() { + log "Существующие базы данных:" + psql -lqt | cut -d \| -f 1 | tr -d ' ' | grep -v -E 'template[0-9]|postgres' | nl +} + +restore_database() { + local backup_file="$1" + local db_name="$2" + local target_db="$3" + + log "Начало восстановления $db_name в $target_db" + + # Удаление существующей БД (если требуется) + if psql -lqt | cut -d \| -f 1 | grep -qw "$target_db"; then + read -p "База данных $target_db уже существует. Удалить её? [y/N] " confirm + if [[ "$confirm" =~ ^[Yy]$ ]]; then + log "Удаление базы данных $target_db..." + dropdb "$target_db" || error_exit "Не удалось удалить базу данных $target_db" + else + error_exit "Восстановление отменено" + fi + fi + + # Создание новой БД + log "Создание базы данных $target_db..." + createdb "$target_db" || error_exit "Не удалось создать базу данных $target_db" + + # Восстановление + log "Восстановление данных из $backup_file в $target_db..." + if pg_restore -d "$target_db" "$backup_file"; then + log "Восстановление успешно завершено!" + + # Анализ восстановленной БД + log "Выполнение ANALYZE для восстановленной БД..." + psql -d "$target_db" -c "ANALYZE;" || log "WARNING" "Не удалось выполнить ANALYZE" + else + error_exit "Ошибка при восстановлении базы данных" + fi +} + +# Основной процесс +check_pg_connection +list_backups +select_backup + +original_db=$(get_db_name_from_backup "$selected_backup") +log "Исходное имя базы данных в бэкапе: $original_db" + +echo "Варианты восстановления:" +echo " 1) Восстановить в новую базу данных с тем же именем ($original_db)" +echo " 2) Восстановить в существующую базу данных" +echo " 3) Восстановить в базу данных с другим именем" +read -p "Выберите вариант (1-3): " restore_option + +case $restore_option in + 1) + target_db="$original_db" + ;; + 2) + list_databases + read -p "Введите имя существующей БД для восстановления: " target_db + ;; + 3) + read -p "Введите новое имя для восстановленной БД: " target_db + ;; + *) + error_exit "Некорректный выбор" + ;; +esac + +# Подтверждение +echo +echo "Параметры восстановления:" +echo " Бэкап: $selected_backup" +echo " Исходная: $original_db" +echo " Целевая: $target_db" +read -p "Подтвердить восстановление? [y/N] " confirm + +if [[ "$confirm" =~ ^[Yy]$ ]]; then + restore_database "$selected_backup" "$original_db" "$target_db" +else + log "Восстановление отменено" +fi + +exit 0 diff --git a/pgsql/pgpro/backupscripts/readme.md b/pgsql/pgpro/backupscripts/readme.md new file mode 100644 index 0000000..20cad16 --- /dev/null +++ b/pgsql/pgpro/backupscripts/readme.md @@ -0,0 +1 @@ +Создать файл .pgpass под пользователем postgres для аутентификации diff --git a/pgsql/pgpro/legacy/backup-pg-simple.sh b/pgsql/pgpro/legacy/backup-pg-simple.sh new file mode 100644 index 0000000..8f89b9b --- /dev/null +++ b/pgsql/pgpro/legacy/backup-pg-simple.sh @@ -0,0 +1,208 @@ +#!/bin/bash +# backup-pg-working.sh - РАБОЧИЙ скрипт бэкапа +# Проверено - работает! + +# Проверка root +if [ "$(id -u)" -ne 0 ]; then + echo "Запускай: sudo $0" + exit 1 +fi + +# Безопасный режим +set -e + +# ========== КОНФИГ ========== +PG_PASSWORD='pass' # Пароль postgres +PG_HOST='127.0.0.1' # Адрес сервера +PG_PORT='5432' # Порт +PG_USER='postgres' # Пользователь + +ARCHIV_DIR='/backups' +BACKUP_DIR='/backup' + +# ========== ФУНКЦИИ ========== +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +stop_1c() { + log "Останавливаю службы 1С..." + + # Сначала пытаемся через systemd + for service in $(systemctl list-units --type=service --no-legend 2>/dev/null | grep -E "srv1c|1c" | awk '{print $1}'); do + systemctl stop "$service" 2>/dev/null && log " Остановлена: $service" + done + + sleep 3 + + # Добиваем процессы если остались + pkill -f "ragent" 2>/dev/null || true + pkill -f "rmngr" 2>/dev/null || true + pkill -f "rphost" 2>/dev/null || true + sleep 2 + + log "Службы 1С остановлены" +} + +start_1c() { + log "Запускаю службы 1С..." + + # Запускаем все найденные службы 1С + for service in $(systemctl list-units --type=service --no-legend --state=inactive 2>/dev/null | grep -E "srv1c|1c" | awk '{print $1}'); do + systemctl start "$service" 2>/dev/null && log " Запущена: $service" + sleep 1 + done + + log "Службы 1С запущены" +} + +# ========== ОПРЕДЕЛЕНИЕ РЕЖИМА ========== +current_hour=$(date +%H) + +# Если запущен из терминала - спрашиваем +if [ -t 0 ]; then + echo "" + echo "=== БЭКАП POSTGRESQL ===" + echo "Выбери режим:" + echo " 1) Быстрый (без остановки 1С, только физический бэкап)" + echo " 2) Полный (с остановкой 1С + архивация + дампы)" + echo "" + read -p "Твой выбор (1/2): " choice + + if [ "$choice" = "2" ]; then + MODE="full" + else + MODE="fast" + fi +else + # Авторежим для cron + if [ $current_hour -ge 6 ] && [ $current_hour -le 23 ]; then + MODE="fast" # День - быстрый + else + MODE="full" # Ночь - полный + fi +fi + +log "Начинаю бэкап в режиме: $MODE" + +# ========== ПРОВЕРКА ПОДКЛЮЧЕНИЯ К POSTGRESQL ========== +log "Проверяю подключение к PostgreSQL..." +if ! sudo -u postgres PGPASSWORD="$PG_PASSWORD" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -c "SELECT 1;" postgres >/dev/null 2>&1; then + log "ОШИБКА: Не могу подключиться к PostgreSQL!" + exit 1 +fi +log "Подключение к PostgreSQL - OK" + +# ========== ВЫПОЛНЕНИЕ БЭКАПА ========== +if [ "$MODE" = "full" ]; then + # ========== ПОЛНЫЙ РЕЖИМ ========== + log "=== ПОЛНЫЙ РЕЖИМ ===" + + # 1. Останавливаем 1С + stop_1c + sleep 5 + + # 2. Физический бэкап + log "Делаю физический бэкап..." + sudo -u postgres PGPASSWORD="$PG_PASSWORD" pg_probackup-17 backup \ + -B "$BACKUP_DIR" \ + --instance data \ + -b FULL \ + --stream \ + --compress \ + -j 4 \ + -h "$PG_HOST" \ + -p "$PG_PORT" \ + -U "$PG_USER" \ + -d postgres + + # 3. Архивируем + log "Архивирую бэкап..." + LAST_BACKUP=$(ls -t "$BACKUP_DIR/backups/data" 2>/dev/null | head -1) + if [ -n "$LAST_BACKUP" ]; then + TIMESTAMP=$(date +"%Y-%m-%d_%H-%M") + mkdir -p "$ARCHIV_DIR/copy" + + if tar -czf "$ARCHIV_DIR/copy/pg_pro-$TIMESTAMP.tar.gz" \ + -C "$BACKUP_DIR/backups/data" \ + "$LAST_BACKUP" \ + "pg_probackup.conf" 2>/dev/null; then + + md5sum "$ARCHIV_DIR/copy/pg_pro-$TIMESTAMP.tar.gz" > "$ARCHIV_DIR/copy/pg_pro-$TIMESTAMP.tar.gz.md5" + log "Архив создан: pg_pro-$TIMESTAMP.tar.gz" + else + log "Ошибка архивации!" + fi + fi + + # 4. Логические дампы + log "Создаю логические дампы..." + mkdir -p "$ARCHIV_DIR/pg_dump" + + DBS=$(sudo -u postgres PGPASSWORD="$PG_PASSWORD" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -qAt -c \ + "SELECT datname FROM pg_database WHERE datname NOT IN ('postgres','template0','template1')" postgres) + + for DB in $DBS; do + log " Дамп базы: $DB" + sudo -u postgres PGPASSWORD="$PG_PASSWORD" pg_dump -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -Fc "$DB" \ + > "$ARCHIV_DIR/pg_dump/${DB}_$TIMESTAMP.dump" 2>/dev/null + + if [ $? -eq 0 ]; then + md5sum "$ARCHIV_DIR/pg_dump/${DB}_$TIMESTAMP.dump" > "$ARCHIV_DIR/pg_dump/${DB}_$TIMESTAMP.dump.md5" + log " ✓ Успешно" + else + log " ✗ Ошибка" + rm -f "$ARCHIV_DIR/pg_dump/${DB}_$TIMESTAMP.dump" + fi + done + + # 5. VACUUM + log "Выполняю VACUUM..." + sudo -u postgres PGPASSWORD="$PG_PASSWORD" vacuumdb -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -afz 2>/dev/null + + # 6. Запускаем 1С + start_1c + +else + # ========== БЫСТРЫЙ РЕЖИМ ========== + log "=== БЫСТРЫЙ РЕЖИМ ===" + + # Только физический бэкап + log "Делаю быстрый бэкап (без остановки 1С)..." + sudo -u postgres PGPASSWORD="$PG_PASSWORD" pg_probackup-17 backup \ + -B "$BACKUP_DIR" \ + --instance data \ + -b FULL \ + --stream \ + --compress \ + -j 4 \ + -h "$PG_HOST" \ + -p "$PG_PORT" \ + -U "$PG_USER" \ + -d postgres +fi + +# ========== ОЧИСТКА СТАРЫХ ФАЙЛОВ ========== +log "Очищаю старые файлы..." +# Храним 20 дней +find "$ARCHIV_DIR/copy" -name "*.tar.gz" -mtime +20 -delete 2>/dev/null || true +find "$ARCHIV_DIR/copy" -name "*.md5" -mtime +20 -delete 2>/dev/null || true +find "$ARCHIV_DIR/pg_dump" -name "*.dump" -mtime +20 -delete 2>/dev/null || true +find "$ARCHIV_DIR/pg_dump" -name "*.md5" -mtime +20 -delete 2>/dev/null || true + +# Очистка через pg_probackup +sudo -u postgres PGPASSWORD="$PG_PASSWORD" pg_probackup-17 delete \ + -B "$BACKUP_DIR" \ + --instance data \ + --delete-wal \ + --retention-redundancy=15 \ + --retention-window=20 \ + -h "$PG_HOST" \ + -p "$PG_PORT" \ + -U "$PG_USER" \ + -d postgres 2>/dev/null + +# ========== ИТОГ ========== +log "Бэкап успешно завершен в режиме: $MODE" +log "Лог бэкапа: sudo -u postgres pg_probackup-17 show -B $BACKUP_DIR" +echo "" \ No newline at end of file diff --git a/pgsql/pgpro/legacy/backup-pg-universal.sh b/pgsql/pgpro/legacy/backup-pg-universal.sh new file mode 100644 index 0000000..a1ac7ec --- /dev/null +++ b/pgsql/pgpro/legacy/backup-pg-universal.sh @@ -0,0 +1,646 @@ +#!/bin/bash +# ================================================================= +# backup-pg-universal.sh - Универсальный скрипт бэкапа PostgreSQL 1C +# Автоматически определяет режим по времени суток +# ================================================================= + +set -e +exec 2>&1 + +# ================================================================= +# КОНФИГУРАЦИЯ +# ================================================================= + +# Пароль PostgreSQL (если нужен) +readonly PG_PASSWORD="pass" + +# Временные диапазоны +readonly DAY_START=6 # 06:00 - начало дня +readonly DAY_END=23 # 23:59 - конец дня + +# Пути и имена +readonly BACKUP_ROOT="/backup" +readonly ARCHIVE_ROOT="/backups" +readonly PG_DATA="/var/lib/pgpro/1c-17/data" +readonly INSTANCE="data" + +# Каталоги +readonly COPY_DIR="$ARCHIVE_ROOT/copy" +readonly DUMP_DIR="$ARCHIVE_ROOT/pg_dump" +readonly LOG_DIR="$BACKUP_ROOT/log" + +# Хранение бэкапов (дней) +readonly RETENTION_DAYS=20 +readonly RETENTION_COPIES=15 + +# Уведомления +readonly MAIL_ENABLED=false +readonly MAIL_TO="root" + +# ================================================================= +# ИНИЦИАЛИЗАЦИЯ И ОПРЕДЕЛЕНИЕ РЕЖИМА +# ================================================================= + +# Автоопределение версии pg_probackup +detect_pg_probackup() { + # Ищем доступные версии + for version in 19 18 17 16 15 14 13 12; do + if command -v "pg_probackup-$version" &>/dev/null; then + echo "pg_probackup-$version" + return 0 + fi + done + + # Пробуем просто pg_probackup + if command -v "pg_probackup" &>/dev/null; then + echo "pg_probackup" + return 0 + fi + + echo "ОШИБКА: pg_probackup не найден!" >&2 + exit 1 +} + +# Автоопределение пути к бинарникам PostgreSQL +detect_pg_bin() { + # Проверяем стандартные пути + local paths=( + "/opt/pgpro/1c-17/bin" + "/opt/pgpro/1c-16/bin" + "/opt/pgpro/1c-15/bin" + "/usr/pgsql-17/bin" + "/usr/pgsql-16/bin" + "/usr/pgsql-15/bin" + "/usr/bin" + "/usr/local/bin" + ) + + for path in "${paths[@]}"; do + if [ -x "$path/psql" ]; then + echo "$path" + return 0 + fi + done + + echo "ОШИБКА: PostgreSQL binaries не найдены!" >&2 + exit 1 +} + +# Определение режима работы +determine_mode() { + local current_hour=$(date +%H) + + # Если указан параметр --hot или --full + case "$1" in + "--hot") + echo "hot" + return + ;; + "--full") + echo "full" + return + ;; + "--help") + show_help + exit 0 + ;; + esac + + # Если запущен из cron (нет TTY) или явно указан CRON_MODE + if [[ ! -t 0 ]] || [[ -n "$CRON_MODE" ]]; then + # Автоматический режим - определяем по времени + if (( current_hour >= DAY_START && current_hour <= DAY_END )); then + echo "hot" # День: 06:00-23:59 - ГОРЯЧИЙ + else + echo "full" # Ночь: 00:00-05:59 - ПОЛНЫЙ + fi + else + # Ручной запуск - спрашиваем пользователя + echo "================================================" >&2 + echo "Универсальный скрипт бэкапа PostgreSQL 1C" >&2 + echo "================================================" >&2 + echo "" >&2 + + read -p "Выберите режим работы: + 1) Горячий бэкап (без остановки 1С, только физический бэкап) + 2) Полный бэкап (с остановкой 1С, архивацией и логическими дампами) + +Ваш выбор [1/2]: " choice + + case "${choice:-2}" in + 1) echo "hot" ;; + *) echo "full" ;; + esac + fi +} + +# Показать справку +show_help() { + cat << EOF +Использование: $0 [ПАРАМЕТР] + +Параметры: + --hot Принудительно горячий режим (без остановки 1С) + --full Принудительно полный режим (с остановкой 1С) + --help Показать эту справку + +Без параметров: + - Ручной запуск: спрашивает режим + - Cron запуск: автоматически по времени суток + День (06:00-23:59): горячий режим + Ночь (00:00-05:59): полный режим + +Примеры: + $0 # Ручной запуск, спрашивает режим + $0 --hot # Принудительно горячий режим + $0 --full # Принудительно полный режим + + # В cron (каждый час) + 0 * * * * $0 +EOF +} + +# Инициализация +init() { + # Определяем компоненты + readonly PG_PROBACKUP=$(detect_pg_probackup) + readonly PG_BIN=$(detect_pg_bin) + + # Создаем каталоги + mkdir -p "$LOG_DIR" "$COPY_DIR" "$DUMP_DIR" "$BACKUP_ROOT/backups/$INSTANCE" + chown -R postgres:postgres "$BACKUP_ROOT" "$ARCHIVE_ROOT" 2>/dev/null || true + + # Определяем режим + readonly MODE=$(determine_mode "$1") + + # Главный лог + readonly TIMESTAMP=$(date +%Y%m%d_%H%M%S) + readonly MAIN_LOG="$LOG_DIR/backup_${TIMESTAMP}_${MODE}.log" + + # Настраиваем аутентификацию + setup_auth + + log_header +} + +# Настройка аутентификации +setup_auth() { + # Экспортируем пароль если указан + [ -n "$PG_PASSWORD" ] && export PGPASSWORD="$PG_PASSWORD" + + # Создаем .pgpass если нужно + local pgpass_file="/var/lib/pgpro/.pgpass" + if [ ! -f "$pgpass_file" ] && [ -n "$PG_PASSWORD" ]; then + sudo -u postgres bash -c "echo 'localhost:5432:*:postgres:$PG_PASSWORD' > '$pgpass_file'" + sudo -u postgres bash -c "echo '127.0.0.1:5432:*:postgres:$PG_PASSWORD' >> '$pgpass_file'" + sudo chmod 600 "$pgpass_file" + fi +} + +# ================================================================= +# ЛОГИРОВАНИЕ +# ================================================================= + +log() { + local level="$1" + local message="$2" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + + echo "[$timestamp] [$level] $message" | tee -a "$MAIN_LOG" +} + +log_header() { + cat << EOF | tee -a "$MAIN_LOG" +======================================================== +БЭКАП POSTGRESQL 1C - $(date) +Режим: $MODE ($([ "$MODE" = "hot" ] && echo "Горячий" || echo "Полный")) +pg_probackup: $PG_PROBACKUP +PostgreSQL: $PG_BIN +======================================================== +EOF +} + +# ================================================================= +# УПРАВЛЕНИЕ СЛУЖБАМИ 1С +# ================================================================= + +stop_1c_services() { + log "INFO" "Остановка служб 1С..." + + # Ищем все службы 1С + local services=$(systemctl list-unit-files --type=service 2>/dev/null | \ + grep -E "(srv1c|1c.*service)" | awk '{print $1}' || true) + + if [ -z "$services" ]; then + log "WARNING" "Службы 1С не найдены в systemd" + # Пробуем убить процессы напрямую + pkill -f "ragent\|rmngr\|rphost" 2>/dev/null && \ + log "INFO" "Процессы 1С остановлены" || \ + log "INFO" "Процессы 1С не найдены" + return 0 + fi + + # Останавливаем RAS сначала + for service in $services; do + if [[ "$service" == *"ras"* ]] || [[ "$service" == *"RAS"* ]]; then + log "INFO" "Останавливаем: $service" + systemctl stop "$service" 2>/dev/null && \ + log "INFO" " ✓ Остановлена" || \ + log "WARNING" " ✗ Ошибка остановки" + sleep 2 + fi + done + + # Останавливаем остальные + for service in $services; do + if [[ "$service" != *"ras"* ]] && [[ "$service" != *"RAS"* ]]; then + log "INFO" "Останавливаем: $service" + systemctl stop "$service" 2>/dev/null && \ + log "INFO" " ✓ Остановлена" || \ + log "WARNING" " ✗ Ошибка остановки" + sleep 3 + fi + done + + # Дополнительная проверка + sleep 5 + local running_count=$(ps aux | grep -E "ragent|rmngr|rphost" | grep -v grep | wc -l) + if [ "$running_count" -eq 0 ]; then + log "INFO" "Все службы 1С остановлены" + else + log "WARNING" "Осталось процессов 1С: $running_count" + fi +} + +start_1c_services() { + log "INFO" "Запуск служб 1С..." + + local services=$(systemctl list-unit-files --type=service 2>/dev/null | \ + grep -E "(srv1c|1c.*service)" | awk '{print $1}' || true) + + if [ -z "$services" ]; then + log "WARNING" "Службы 1С не найдены" + return 0 + fi + + # Запускаем основные службы (кроме RAS) + for service in $services; do + if [[ "$service" != *"ras"* ]] && [[ "$service" != *"RAS"* ]]; then + log "INFO" "Запускаем: $service" + systemctl start "$service" 2>/dev/null && \ + log "INFO" " ✓ Запущена" || \ + log "WARNING" " ✗ Ошибка запуска" + sleep 5 + fi + done + + # Запускаем RAS службы + for service in $services; do + if [[ "$service" == *"ras"* ]] || [[ "$service" == *"RAS"* ]]; then + log "INFO" "Запускаем: $service" + systemctl start "$service" 2>/dev/null && \ + log "INFO" " ✓ Запущена" || \ + log "WARNING" " ✗ Ошибка запуска" + sleep 2 + fi + done + + log "INFO" "Службы 1С запущены" +} + +# ================================================================= +# ФИЗИЧЕСКИЙ БЭКАП (PG_PROBACKUP) +# ================================================================= + +execute_backup() { + local backup_log="$LOG_DIR/pg_probackup_${TIMESTAMP}.log" + local start_time=$(date +%s) + + log "INFO" "Выполнение pg_probackup ($MODE режим)..." + + # Базовые параметры + local cmd="sudo -u postgres $PG_PROBACKUP backup \ + -B '$BACKUP_ROOT' \ + --instance '$INSTANCE' \ + -b FULL \ + --stream \ + --compress \ + -j 4" + + # Добавляем delete-wal только в полном режиме + if [ "$MODE" = "full" ]; then + cmd="$cmd --delete-wal" + fi + + log "DEBUG" "Команда: $cmd" + + # Выполняем бэкап + if eval "$cmd" > "$backup_log" 2>&1; then + # Проверяем результат + if grep -q "completed" "$backup_log"; then + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + + # Копируем лог в основной + cat "$backup_log" >> "$MAIN_LOG" + + log "SUCCESS" "Бэкап успешно завершен за ${duration}с" + + # Добавляем информацию о размере + local backup_size=$(du -sh "$BACKUP_ROOT/backups/$INSTANCE" 2>/dev/null | cut -f1) + log "INFO" "Размер бэкапов: $backup_size" + + return 0 + else + log "ERROR" "Бэкап выполнен, но нет статуса completed" + cat "$backup_log" >> "$MAIN_LOG" + return 1 + fi + else + log "ERROR" "Ошибка выполнения pg_probackup" + cat "$backup_log" >> "$MAIN_LOG" + return 1 + fi +} + +# ================================================================= +# АРХИВАЦИЯ БЭКАПА (ТОЛЬКО В ПОЛНОМ РЕЖИМЕ) +# ================================================================= + +archive_backup() { + [ "$MODE" != "full" ] && return 0 + + log "INFO" "Архивация бэкапа в .tar.gz..." + + # Ищем последний бэкап + local last_backup=$(sudo -u postgres ls "$BACKUP_ROOT/backups/$INSTANCE" -t 2>/dev/null | head -1) + + if [ -z "$last_backup" ]; then + log "ERROR" "Не найден бэкап для архивации" + return 1 + fi + + # Проверяем что бэкап завершен + if ! sudo -u postgres grep -q "$last_backup completed" "$LOG_DIR/pg_probackup.log" 2>/dev/null; then + log "ERROR" "Бэкап $last_backup не завершен" + return 1 + fi + + local archive_file="$COPY_DIR/pg_pro-${TIMESTAMP}.tar.gz" + + log "INFO" "Создание архива: $archive_file" + log "INFO" "Исходник: $last_backup" + + # Архивируем + if sudo -u postgres tar -czf "$archive_file" \ + -C "$BACKUP_ROOT/backups/$INSTANCE" \ + "$last_backup" \ + "pg_probackup.conf" > /dev/null 2>&1; then + + # MD5 сумма + sudo -u postgres md5sum "$archive_file" > "${archive_file}.md5" + + # Логируем + local size=$(du -h "$archive_file" | cut -f1) + echo "$(date '+%Y-%m-%d %H:%M:%S') создан $archive_file ($size)" >> "$COPY_DIR/copy.log" + + log "SUCCESS" "Архив создан: $archive_file ($size)" + return 0 + else + log "ERROR" "Ошибка создания архива" + return 1 + fi +} + +# ================================================================= +# ЛОГИЧЕСКИЕ ДАМПЫ (ТОЛЬКО В ПОЛНОМ РЕЖИМЕ) +# ================================================================= + +logical_dumps() { + [ "$MODE" != "full" ] && return 0 + + log "INFO" "Создание логических дампов (pg_dump)..." + + # Получаем список баз (исключая системные) + local databases=$(sudo -u postgres "$PG_BIN/psql" -qAt -c " + SELECT datname + FROM pg_database + WHERE datname NOT IN ('postgres', 'template0', 'template1') + ORDER BY datname; + " 2>/dev/null) + + if [ -z "$databases" ]; then + log "WARNING" "Пользовательские БД не найдены" + return 1 + fi + + log "INFO" "Найдено баз: $(echo "$databases" | wc -l)" + + local success=0 + local total=0 + + for db in $databases; do + total=$((total + 1)) + + local dump_file="$DUMP_DIR/${db}_${TIMESTAMP}.dump" + local start_time=$(date +%s) + + log "INFO" "Дамп базы: $db" + + if sudo -u postgres "$PG_BIN/pg_dump" -Fc "$db" > "$dump_file" 2>"$DUMP_DIR/${db}_error.log"; then + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + local size=$(du -h "$dump_file" | cut -f1) + + # MD5 сумма + sudo -u postgres md5sum "$dump_file" > "${dump_file}.md5" + + log "SUCCESS" " ✓ Дамп создан за ${duration}с ($size)" + success=$((success + 1)) + else + log "ERROR" " ✗ Ошибка дампа" + rm -f "$dump_file" 2>/dev/null + fi + done + + # Итоговый лог + echo "$(date '+%Y-%m-%d %H:%M:%S') Дампы: $success/$total успешно" >> "$DUMP_DIR/backup.log" + + if [ $success -gt 0 ]; then + log "SUCCESS" "Создано $success/$total дампов" + return 0 + else + log "ERROR" "Не создано ни одного дампа" + return 1 + fi +} + +# ================================================================= +# VACUUM (ТОЛЬКО В ПОЛНОМ РЕЖИМЕ) +# ================================================================= + +execute_vacuum() { + [ "$MODE" != "full" ] && return 0 + + log "INFO" "Выполнение VACUUM..." + local vacuum_log="$LOG_DIR/vacuum_${TIMESTAMP}.log" + local start_time=$(date +%s) + + if sudo -u postgres "$PG_BIN/vacuumdb" -afz > "$vacuum_log" 2>&1; then + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + + log "SUCCESS" "VACUUM успешно завершен за ${duration}с" + + # Добавляем детальный лог + if [ "$MAIL_ENABLED" = true ]; then + cat "$vacuum_log" >> "$MAIN_LOG" + fi + else + log "ERROR" "Ошибка VACUUM" + cat "$vacuum_log" >> "$MAIN_LOG" + fi +} + +# ================================================================= +# ОЧИСТКА СТАРЫХ ФАЙЛОВ +# ================================================================= + +cleanup_old() { + log "INFO" "Очистка старых файлов (>${RETENTION_DAYS} дней)..." + + # Архивированные бэкапы + find "$COPY_DIR" -name "*.tar.gz" -type f -mtime +$RETENTION_DAYS -delete 2>/dev/null + find "$COPY_DIR" -name "*.md5" -type f -mtime +$RETENTION_DAYS -delete 2>/dev/null + + # Логические дампы + find "$DUMP_DIR" -name "*.dump" -type f -mtime +$RETENTION_DAYS -delete 2>/dev/null + find "$DUMP_DIR" -name "*.md5" -type f -mtime +$RETENTION_DAYS -delete 2>/dev/null + + # Старые логи + find "$LOG_DIR" -name "*.log" -type f -mtime +90 -delete 2>/dev/null + + # Очистка через pg_probackup (для физических бэкапов) + sudo -u postgres $PG_PROBACKUP delete \ + -B "$BACKUP_ROOT" \ + --instance "$INSTANCE" \ + --retention-redundancy=$RETENTION_COPIES \ + --retention-window=$RETENTION_DAYS \ + > /dev/null 2>&1 + + log "INFO" "Очистка завершена (сохранено последних $RETENTION_COPIES копий)" +} + +# ================================================================= +# ПРОВЕРКА ЦЕЛОСТНОСТИ +# ================================================================= + +validate_backups() { + log "INFO" "Проверка целостности бэкапов..." + + if sudo -u postgres $PG_PROBACKUP validate \ + -B "$BACKUP_ROOT" \ + --instance "$INSTANCE" \ + > "$LOG_DIR/validate_${TIMESTAMP}.log" 2>&1; then + log "SUCCESS" "Все бэкапы целостны" + else + log "ERROR" "Найдены повреждённые бэкапы" + cat "$LOG_DIR/validate_${TIMESTAMP}.log" >> "$MAIN_LOG" + fi +} + +# ================================================================= +# ОТПРАВКА УВЕДОМЛЕНИЙ +# ================================================================= + +send_notification() { + [ "$MAIL_ENABLED" != "true" ] && return 0 + + local subject="Бэкап PostgreSQL ($MODE режим) - $(date '+%Y-%m-%d %H:%M')" + + if command -v mutt >/dev/null 2>&1; then + cat "$MAIN_LOG" | mutt -s "$subject" "$MAIL_TO" + log "INFO" "Уведомление отправлено на $MAIL_TO" + else + log "WARNING" "mutt не установлен, уведомления недоступны" + fi +} + +# ================================================================= +# ГЛАВНАЯ ФУНКЦИЯ +# ================================================================= + +main() { + # Инициализация + init "$1" + + log "INFO" "Начало выполнения в режиме: $MODE" + + # ПОЛНЫЙ РЕЖИМ: останавливаем 1С + if [ "$MODE" = "full" ]; then + stop_1c_services + fi + + # Физический бэкап (всегда) + if execute_backup; then + # Проверка целостности + validate_backups + + # ПОЛНЫЙ РЕЖИМ: дополнительные операции + if [ "$MODE" = "full" ]; then + # Архивация + archive_backup + + # Логические дампы + logical_dumps + + # VACUUM + execute_vacuum + + # Запускаем 1С обратно + start_1c_services + fi + + # Очистка старых файлов (всегда) + cleanup_old + + log "SUCCESS" "Бэкап успешно завершен в режиме: $MODE" + else + log "ERROR" "Бэкап не удался!" + + # Если в полном режиме - пытаемся запустить 1С обратно + if [ "$MODE" = "full" ]; then + log "INFO" "Пытаемся запустить 1С после ошибки..." + start_1c_services + fi + fi + + # Итоговое время выполнения + local total_duration=$(( $(date +%s) - ${START_TIME:-$(date +%s)} )) + log "INFO" "Общее время выполнения: ${total_duration}с" + + # Уведомления + send_notification + + # Итоговый разделитель + echo "========================================================" | tee -a "$MAIN_LOG" +} + +# ================================================================= +# ЗАПУСК +# ================================================================= + +# Засекаем время +readonly START_TIME=$(date +%s) + +# Проверка прав +if [ "$(id -u)" -ne 0 ]; then + echo "ОШИБКА: Запускайте от root" >&2 + exit 1 +fi + +# Ловим прерывания +trap 'echo "Прервано пользователем"; exit 1' INT TERM + +# Запускаем +main "$@" \ No newline at end of file diff --git a/pgsql/pgpro/legacy/backup-pgp17-pro.sh b/pgsql/pgpro/legacy/backup-pgp17-pro.sh new file mode 100644 index 0000000..8a18cb2 --- /dev/null +++ b/pgsql/pgpro/legacy/backup-pgp17-pro.sh @@ -0,0 +1,763 @@ +#!/bin/bash +# ================================================================= +# backup-pgp17-pro.sh - Профессиональный скрипт бэкапа PostgreSQL 1C Pro 17 +# Автор: Системный администратор +# Версия: 3.0 +# Дата: $(date +%Y-%m-%d) +# +# ОСОБЕННОСТИ: +# - Работает вручную и из cron +# - Детальное логирование с цветом +# - Автоопределение режима по времени +# - Проверка целостности всех операций +# - Отправка уведомлений (опционально) +# - Поддержка команд: validate, cleanup, report +# +# РАСПИСАНИЕ: +# - 4:00 Пн-Сб: Полный бэкап с остановкой 1С +# - 12:30 Пн-Пт: Горячий бэкап без остановки +# - 15:00 Сб: Горячий бэкап без остановки +# - Ручной запуск: Полный цикл +# ================================================================= + +set -e # Выход при ошибке +exec 2>&1 # Перенаправляем stderr в stdout + +# ================================================================= +# КОНФИГУРАЦИЯ (НАСТРОЙКИ) +# ================================================================= +readonly PG_PASSWORD="pass" # Пароль PostgreSQL +readonly PG_PROBACKUP="pg_probackup-17" # Имя утилиты +readonly PG_PATH="/opt/pgpro/1c-17/bin" # Путь к PostgreSQL +readonly PG_DATA="/var/lib/pgpro/1c-17/data" # Каталог данных +readonly INSTANCE="data" # Имя инстанса pg_probackup + +# Каталоги +readonly BACKUP_DIR="/backup" # Основной каталог бэкапов +readonly ARCHIVE_DIR="/backups" # Каталог архивов +readonly COPY_DIR="$ARCHIVE_DIR/copy" # Архивы pg_probackup +readonly DUMP_DIR="$ARCHIVE_DIR/pg_dump" # Дампы баз +readonly LOG_DIR="$BACKUP_DIR/log" # Каталог логов + +# Параметры +readonly DAYS_KEEP=20 # Хранить архивы (дней) +readonly MAIL_ENABLED=false # Отправлять email? +readonly MAIL_TO="root" # Получатель email + +# Цвета для вывода +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly BLUE='\033[0;34m' +readonly NC='\033[0m' # No Color + +# ================================================================= +# ИНИЦИАЛИЗАЦИЯ +# ================================================================= +init() { + # Создаем каталоги с правильными правами + mkdir -p "$LOG_DIR" "$COPY_DIR" "$DUMP_DIR" + chown -R postgres:postgres "$BACKUP_DIR" "$ARCHIVE_DIR" 2>/dev/null || true + + # Главный лог файл + readonly MAIN_LOG="$LOG_DIR/backup_$(date +%Y%m%d_%H%M%S).log" + + # Определяем режим запуска + if [ -t 0 ] && [ -z "$CRON_MODE" ]; then + RUN_MODE="MANUAL" + MODE="full-stop" + else + RUN_MODE="CRON" + determine_mode + fi + + # Инициализируем логи + log_header +} + +# Определение режима по времени +determine_mode() { + local hour=$(date +%H) + local minute=$(date +%M) + local day=$(date +%u) # 1=Пн, 7=Вс + + if [ "$hour" = "04" ] && [ "$minute" = "00" ]; then + MODE="full-stop" # Ночной: 4:00 + elif [ "$day" -le 5 ] && [ "$hour" = "12" ] && [ "$minute" = "30" ]; then + MODE="full-hot" # Дневной будни: 12:30 + elif [ "$day" = "6" ] && [ "$hour" = "15" ] && [ "$minute" = "00" ]; then + MODE="full-hot" # Суббота: 15:00 + else + MODE="full-hot" # По умолчанию + fi +} + +# ================================================================= +# ЛОГИРОВАНИЕ +# ================================================================= +log() { + local level="$1" + local message="$2" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + local color="$NC" + + case "$level" in + "INFO") color="$BLUE" ;; + "SUCCESS") color="$GREEN" ;; + "WARNING") color="$YELLOW" ;; + "ERROR") color="$RED" ;; + esac + + echo -e "${color}[$timestamp] [$level] [$RUN_MODE] $message${NC}" | tee -a "$MAIN_LOG" +} + +log_header() { + echo "================================================================" | tee -a "$MAIN_LOG" + echo "БЭКАП POSTGRESQL 1C PRO 17 - $(date)" | tee -a "$MAIN_LOG" + echo "================================================================" | tee -a "$MAIN_LOG" + echo "Система: $(hostname)" | tee -a "$MAIN_LOG" + echo "Режим: $RUN_MODE ($MODE)" | tee -a "$MAIN_LOG" + echo "Время: $(date '+%H:%M:%S')" | tee -a "$MAIN_LOG" + echo "Пользователь: $(whoami)" | tee -a "$MAIN_LOG" + echo "Лог файл: $MAIN_LOG" | tee -a "$MAIN_LOG" + echo "================================================================" | tee -a "$MAIN_LOG" +} + +# ================================================================= +# ПРОВЕРКИ +# ================================================================= +check_postgresql() { + log "INFO" "Проверка PostgreSQL..." + + # Проверка службы + if ! systemctl is-active postgrespro-1c-17 >/dev/null 2>&1; then + log "ERROR" "Служба PostgreSQL не запущена" + return 1 + fi + + # Проверка подключения + if ! PGPASSWORD="$PG_PASSWORD" "$PG_PATH/psql" -U postgres -h 127.0.0.1 -c "SELECT 1;" >/dev/null 2>&1; then + log "ERROR" "Не удается подключиться к PostgreSQL" + return 1 + fi + + log "SUCCESS" "PostgreSQL доступен" + return 0 +} + +check_disk_space() { + log "INFO" "Проверка свободного места..." + + local min_gb=10 + local available_gb + + # Проверяем место в основном каталоге + if available_gb=$(df -BG "$BACKUP_DIR" 2>/dev/null | awk 'NR==2 {print $4}' | sed 's/G//'); then + if [ "$available_gb" -lt "$min_gb" ]; then + log "ERROR" "В $BACKUP_DIR меньше ${min_gb}GB (${available_gb}GB)" + return 1 + fi + log "INFO" "$BACKUP_DIR: ${available_gb}GB свободно" + else + log "WARNING" "Не удалось проверить место в $BACKUP_DIR" + fi + + # Проверяем место в архивах + if [ "$BACKUP_DIR" != "$ARCHIVE_DIR" ]; then + if available_gb=$(df -BG "$ARCHIVE_DIR" 2>/dev/null | awk 'NR==2 {print $4}' | sed 's/G//'); then + if [ "$available_gb" -lt 5 ]; then + log "WARNING" "В $ARCHIVE_DIR меньше 5GB (${available_gb}GB)" + else + log "INFO" "$ARCHIVE_DIR: ${available_gb}GB свободно" + fi + fi + fi + + return 0 +} + +# ================================================================= +# УПРАВЛЕНИЕ СЛУЖБАМИ 1С +# ================================================================= +stop_1c_services() { + log "INFO" "Остановка служб 1С..." + + # Ищем все службы 1С + local services=$(systemctl list-unit-files --type=service 2>/dev/null | \ + grep -E "srv1c.*service" | awk '{print $1}') + + if [ -z "$services" ]; then + log "WARNING" "Службы 1С не найдены в systemctl" + # Проверяем процессы + local processes=$(ps aux | grep -E "ragent|rmngr|rphost" | grep -v grep | wc -l) + if [ "$processes" -gt 0 ]; then + log "INFO" "Останавливаем процессы 1С напрямую..." + pkill -f "ragent" 2>/dev/null + pkill -f "rmngr" 2>/dev/null + pkill -f "rphost" 2>/dev/null + sleep 10 + fi + return 0 + fi + + log "INFO" "Найдено служб: $(echo "$services" | wc -l)" + + # Останавливаем RAS службы (порты 1545, 1845, 1945...) + for service in $services; do + if [[ "$service" == *"-ras.service" ]] || [[ "$service" == *"ras"* ]]; then + log "INFO" " Останавливаем: $service" + if systemctl stop "$service" 2>/dev/null; then + log "SUCCESS" " Остановлена" + else + log "WARNING" " Ошибка остановки" + fi + sleep 2 + fi + done + + # Останавливаем основные службы (порты 1540, 1840, 1940...) + for service in $services; do + if [[ "$service" != *"-ras.service" ]] && [[ "$service" != *"ras"* ]]; then + log "INFO" " Останавливаем: $service" + if systemctl stop "$service" 2>/dev/null; then + log "SUCCESS" " Остановлена" + else + log "WARNING" " Ошибка остановки" + fi + sleep 3 + fi + done + + # Проверяем остановку + sleep 5 + local running_procs=$(ps aux | grep -E "ragent|rmngr|rphost" | grep -v grep | wc -l) + if [ "$running_procs" -eq 0 ]; then + log "SUCCESS" "Все службы 1С остановлены" + else + log "WARNING" "Осталось процессов: $running_procs" + fi + + # Очистка файлов блокировок + local snc_files=$(find /home/usr1cv8 -name "snccntx*" -type f 2>/dev/null | wc -l) + if [ "$snc_files" -gt 0 ]; then + find /home/usr1cv8 -name "snccntx*" -type f -delete 2>/dev/null + log "INFO" "Удалено файлов блокировок: $snc_files" + fi +} + +start_1c_services() { + log "INFO" "Запуск служб 1С..." + + local services=$(systemctl list-unit-files --type=service 2>/dev/null | \ + grep -E "srv1c.*service" | awk '{print $1}') + + if [ -z "$services" ]; then + log "WARNING" "Службы 1С не найдены" + return 0 + fi + + # Запускаем основные службы + for service in $services; do + if [[ "$service" != *"-ras.service" ]] && [[ "$service" != *"ras"* ]]; then + log "INFO" " Запускаем: $service" + if systemctl start "$service" 2>/dev/null; then + log "SUCCESS" " Запущена" + else + log "WARNING" " Ошибка запуска" + fi + sleep 5 + fi + done + + # Запускаем RAS службы + for service in $services; do + if [[ "$service" == *"-ras.service" ]] || [[ "$service" == *"ras"* ]]; then + log "INFO" " Запускаем: $service" + if systemctl start "$service" 2>/dev/null; then + log "SUCCESS" " Запущена" + else + log "WARNING" " Ошибка запуска" + fi + sleep 2 + fi + done + + # Проверка запуска + sleep 10 + local running_services=$(systemctl list-units --type=service --state=running | grep srv1c | wc -l) + local running_procs=$(ps aux | grep -E "ragent|rmngr|rphost" | grep -v grep | wc -l) + + log "SUCCESS" "Запущено служб: $running_services, процессов: $running_procs" +} + +# ================================================================= +# ВЫПОЛНЕНИЕ БЭКАПА +# ================================================================= +execute_backup() { + local backup_log="$LOG_DIR/pg_probackup_$(date +%Y%m%d_%H%M%S).log" + local start_time=$(date +%s) + + log "INFO" "Выполнение pg_probackup..." + + # Удаляем старый лог если есть + [ -f "$LOG_DIR/pg_probackup.log" ] && rm -f "$LOG_DIR/pg_probackup.log" + + # Выполняем бэкап (ВАЖНО: используем export для PGPASSWORD) + export PGPASSWORD="$PG_PASSWORD" + + if sudo -u postgres $PG_PROBACKUP backup \ + -B "$BACKUP_DIR" \ + --instance "$INSTANCE" \ + -U postgres \ + -d postgres \ + -h 127.0.0.1 \ + -b FULL \ + --stream \ + --compress \ + --expired \ + --delete-wal \ + --log-level-file=info \ + -j 4 > "$backup_log" 2>&1; then + + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + + # Проверяем что бэкап completed + if grep -q "completed" "$backup_log"; then + log "SUCCESS" "Бэкап успешно завершен за ${duration}с" + + # Получаем ID бэкапа + local backup_id=$(grep "backup ID" "$backup_log" | tail -1 | awk '{print $4}' | tr -d ',') + if [ -n "$backup_id" ]; then + log "INFO" "ID бэкапа: $backup_id" + echo "$backup_id" > /tmp/last_backup_id + fi + + return 0 + else + log "ERROR" "Бэкап выполнен, но нет статуса completed" + return 1 + fi + else + log "ERROR" "Ошибка выполнения pg_probackup" + log "INFO" "Последние строки лога:" + tail -20 "$backup_log" | while read line; do + log "ERROR" " $line" + done + return 1 + fi +} + +# ================================================================= +# АРХИВАЦИЯ БЭКАПА +# ================================================================= +archive_backup() { + log "INFO" "Архивация бэкапа..." + + # Получаем последний бэкап + local last_backup=$(sudo -u postgres ls "$BACKUP_DIR/backups/$INSTANCE" -t 2>/dev/null | head -1) + + if [ -z "$last_backup" ]; then + log "ERROR" "Не найден бэкап для архивации" + return 1 + fi + + # Проверяем что бэкап completed + if ! sudo -u postgres grep -q "$last_backup completed" "$LOG_DIR/pg_probackup.log" 2>/dev/null; then + log "ERROR" "Бэкап $last_backup не завершен" + return 1 + fi + + local timestamp=$(date +"%Y-%m-%d_%H-%M-%S") + local archive_file="$COPY_DIR/pg_pro-$timestamp.tar.gz" + + log "INFO" "Создание архива: $archive_file" + + # Архивируем + if sudo -u postgres tar -czf "$archive_file" \ + -C "$BACKUP_DIR/backups/$INSTANCE" \ + "$last_backup" \ + "pg_probackup.conf" > /dev/null 2>&1; then + + local size=$(ls -lh "$archive_file" | awk '{print $5}') + log "SUCCESS" "Архив создан: $size" + + # MD5 сумма + sudo -u postgres md5sum "$archive_file" > "${archive_file}.md5" + log "INFO" "MD5 сумма создана" + + # Логируем + echo "$(date '+%Y-%m-%d %H:%M:%S') создан $archive_file" >> "$COPY_DIR/copy.log" + + return 0 + else + log "ERROR" "Ошибка создания архива" + return 1 + fi +} + +# ================================================================= +# ЛОГИЧЕСКИЕ ДАМПЫ БАЗ +# ================================================================= +logical_dumps() { + log "INFO" "Создание логических дампов..." + + # Получаем список баз + local databases=$(sudo -u postgres "$PG_PATH/psql" -qAt -c " + SELECT datname + FROM pg_database + WHERE datname NOT IN ('postgres', 'template0', 'template1') + ORDER BY datname; + " 2>/dev/null) + + if [ -z "$databases" ]; then + log "WARNING" "Базы данных не найдены" + return 1 + fi + + local timestamp=$(date +"%Y-%m-%d_%H-%M-%S") + local success=0 + local total=0 + + log "INFO" "Найдено баз: $(echo "$databases" | wc -l)" + + # Логируем начало + echo "================================================" >> "$DUMP_DIR/backup.log" + echo "Начало дампов: $(date '+%Y-%m-%d %H:%M:%S')" >> "$DUMP_DIR/backup.log" + + for db in $databases; do + total=$((total + 1)) + log "INFO" "Дамп базы: $db" + + local dump_file="$DUMP_DIR/${db}_${timestamp}.dump" + + # Получаем размер базы + local db_size=$(sudo -u postgres "$PG_PATH/psql" -t -c " + SELECT pg_size_pretty(pg_database_size('$db')); + " 2>/dev/null | tr -d ' ') + + log "INFO" " Размер базы: $db_size" + + # Выполняем дамп + if sudo -u postgres "$PG_PATH/pg_dump" -Fc "$db" > "$dump_file" 2>/dev/null; then + local size=$(ls -lh "$dump_file" | awk '{print $5}') + log "SUCCESS" " Дамп создан: $size" + + # MD5 сумма + sudo -u postgres md5sum "$dump_file" > "${dump_file}.md5" + + success=$((success + 1)) + else + log "ERROR" " Ошибка дампа базы $db" + rm -f "$dump_file" 2>/dev/null + fi + done + + # Логируем результат + echo "Дампы завершены: $(date '+%Y-%m-%d %H:%M:%S')" >> "$DUMP_DIR/backup.log" + echo "Успешно: $success/$total" >> "$DUMP_DIR/backup.log" + echo "================================================" >> "$DUMP_DIR/backup.log" + + if [ $success -eq $total ]; then + log "SUCCESS" "Все дампы успешно созданы" + return 0 + elif [ $success -gt 0 ]; then + log "WARNING" "Создано $success/$total дампов" + return 1 + else + log "ERROR" "Не создано ни одного дампа" + return 1 + fi +} + +# ================================================================= +# VACUUM БАЗ ДАННЫХ +# ================================================================= +execute_vacuum() { + log "INFO" "Выполнение VACUUM..." + + local vacuum_log="$BACKUP_DIR/vacuumdb_$(date +%Y%m%d_%H%M%S).txt" + + echo "$(date '+%Y-%m-%d %H:%M:%S') VACUUM - начало" > "$vacuum_log" + + if sudo -u postgres "$PG_PATH/vacuumdb" -afz >> "$vacuum_log" 2>&1; then + log "SUCCESS" "VACUUM успешно завершен" + echo "$(date '+%Y-%m-%d %H:%M:%S') VACUUM - удачное завершение" >> "$vacuum_log" + else + log "ERROR" "VACUUM завершился с ошибкой" + echo "$(date '+%Y-%m-%d %H:%M:%S') VACUUM - неудачное завершение" >> "$vacuum_log" + fi + + # Сохраняем в общий лог + cat "$vacuum_log" >> "$BACKUP_DIR/vacuumdb.log" + + # Отправка email если нужно + if [ "$MAIL_ENABLED" = true ] && command -v mutt >/dev/null 2>&1; then + cat "$vacuum_log" | mutt -s "VACUUM $(hostname) $(date '+%Y-%m-%d')" "$MAIL_TO" + fi + + return 0 +} + +# ================================================================= +# ПРОВЕРКА ЦЕЛОСТНОСТИ БЭКАПОВ +# ================================================================= +validate_backups() { + log "INFO" "Проверка целостности бэкапов..." + + local validate_log="$LOG_DIR/validate_$(date +%Y%m%d_%H%M%S).log" + + if sudo -u postgres $PG_PROBACKUP validate \ + -B "$BACKUP_DIR" \ + --instance "$INSTANCE" > "$validate_log" 2>&1; then + + log "SUCCESS" "Все бэкапы целостны" + return 0 + else + log "ERROR" "Найдены повреждённые бэкапы" + log "INFO" "Последние строки лога:" + tail -20 "$validate_log" | while read line; do + log "ERROR" " $line" + done + return 1 + fi +} + +# ================================================================= +# ОЧИСТКА СТАРЫХ АРХИВОВ +# ================================================================= +cleanup_old() { + log "INFO" "Очистка старых архивов (>$DAYS_KEEP дней)..." + + local deleted=0 + + # Архивированные бэкапы + local old_backups=$(find "$COPY_DIR" -name "*.tar.gz" -type f -mtime +$DAYS_KEEP 2>/dev/null) + if [ -n "$old_backups" ]; then + log "INFO" "Удаление архивов pg_probackup..." + for file in $old_backups; do + log "INFO" " Удаляем: $(basename "$file")" + rm -f "$file" + rm -f "${file}.md5" 2>/dev/null + deleted=$((deleted + 1)) + done + fi + + # Логические дампы + local old_dumps=$(find "$DUMP_DIR" -name "*.dump" -type f -mtime +$DAYS_KEEP 2>/dev/null) + if [ -n "$old_dumps" ]; then + log "INFO" "Удаление логических дампов..." + for file in $old_dumps; do + log "INFO" " Удаляем: $(basename "$file")" + rm -f "$file" + rm -f "${file}.md5" 2>/dev/null + deleted=$((deleted + 1)) + done + fi + + # Старые логи (90 дней) + local old_logs=$(find "$LOG_DIR" -name "*.log" -type f -mtime +90 2>/dev/null | wc -l) + if [ "$old_logs" -gt 0 ]; then + log "INFO" "Удаление старых логов..." + find "$LOG_DIR" -name "*.log" -type f -mtime +90 -delete 2>/dev/null + deleted=$((deleted + old_logs)) + fi + + if [ $deleted -gt 0 ]; then + log "SUCCESS" "Удалено файлов: $deleted" + else + log "INFO" "Нет файлов для удаления" + fi +} + +# ================================================================= +# ОТЧЁТ О СОСТОЯНИИ +# ================================================================= +show_report() { + echo "" + echo "========================================" + echo "ОТЧЁТ О СОСТОЯНИИ СИСТЕМЫ БЭКАПА" + echo "========================================" + echo "Время: $(date)" + echo "Система: $(hostname)" + echo "Режим: $RUN_MODE" + echo "" + + # Свободное место + echo "1. СВОБОДНОЕ МЕСТО:" + df -h "$BACKUP_DIR" | tail -1 | awk '{print " " $1 ": " $3 "/" $2 " (" $5 " занято)"}' + if [ "$BACKUP_DIR" != "$ARCHIVE_DIR" ]; then + df -h "$ARCHIVE_DIR" | tail -1 | awk '{print " " $1 ": " $3 "/" $2 " (" $5 " занято)"}' + fi + echo "" + + # PostgreSQL + echo "2. POSTGRESQL:" + if systemctl is-active postgrespro-1c-17 >/dev/null 2>&1; then + echo " ✅ Активен" + + # Размер баз + echo " Размеры баз:" + sudo -u postgres "$PG_PATH/psql" -t -c " + SELECT datname, pg_size_pretty(pg_database_size(datname)) + FROM pg_database + WHERE datname NOT IN ('template0', 'template1') + ORDER BY pg_database_size(datname) DESC; + " 2>/dev/null | while read line; do + echo " $line" + done + else + echo " ❌ Не активен" + fi + echo "" + + # Последние бэкапы + echo "3. ПОСЛЕДНИЕ БЭКАПЫ:" + sudo -u postgres $PG_PROBACKUP show -B "$BACKUP_DIR" --instance "$INSTANCE" 2>/dev/null | \ + tail -5 | while read line; do + echo " $line" + done + echo "" + + # Архивы + echo "4. АРХИВЫ:" + local pgp_count=$(find "$COPY_DIR" -name "*.tar.gz" -type f 2>/dev/null | wc -l) + local pgp_newest=$(find "$COPY_DIR" -name "*.tar.gz" -type f -exec ls -t {} + 2>/dev/null | head -1) + echo " pg_probackup: $pgp_count файлов" + if [ -n "$pgp_newest" ]; then + echo " Последний: $(date -r "$pgp_newest" '+%Y-%m-%d %H:%M')" + fi + + local dump_count=$(find "$DUMP_DIR" -name "*.dump" -type f 2>/dev/null | wc -l) + local dump_newest=$(find "$DUMP_DIR" -name "*.dump" -type f -exec ls -t {} + 2>/dev/null | head -1) + echo " pg_dump: $dump_count файлов" + if [ -n "$dump_newest" ]; then + echo " Последний: $(date -r "$dump_newest" '+%Y-%m-%d %H:%M')" + fi + echo "" + + # Процессы 1С + echo "5. ПРОЦЕССЫ 1С:" + local proc_count=$(ps aux | grep -E "ragent|rmngr|rphost" | grep -v grep | wc -l) + echo " Активных процессов: $proc_count" + echo "" + + echo "ГЛАВНЫЙ ЛОГ: $MAIN_LOG" + echo "========================================" +} + +# ================================================================= +# ОСНОВНАЯ ЛОГИКА +# ================================================================= +full_stop_backup() { + log "INFO" "=== ПОЛНЫЙ БЭКАП С ОСТАНОВКОЙ 1С ===" + + # Проверки + check_postgresql || return 1 + check_disk_space || return 1 + + # 1. Остановка 1С + stop_1c_services + + # 2. Выполнение бэкапа + if execute_backup; then + # 3. Запуск 1С + start_1c_services + + # 4. Архивирование + archive_backup + + # 5. Логические дампы + logical_dumps + + # 6. VACUUM + execute_vacuum + + log "SUCCESS" "ПОЛНЫЙ ЦИКЛ БЭКАПА ЗАВЕРШЕН УСПЕШНО" + else + log "ERROR" "ОШИБКА БЭКАПА, ЗАПУСКАЕМ 1С" + start_1c_services + return 1 + fi +} + +full_hot_backup() { + log "INFO" "=== ГОРЯЧИЙ БЭКАП БЕЗ ОСТАНОВКИ ===" + + # Проверки + check_postgresql || return 1 + check_disk_space || return 1 + + # Только бэкап + execute_backup +} + +# ================================================================= +# ГЛАВНАЯ ФУНКЦИЯ +# ================================================================= +main() { + # Инициализация + init + + # Обработка команд + case "${1:-}" in + "validate") + validate_backups + ;; + "cleanup") + cleanup_old + ;; + "report") + show_report + ;; + *) + # Автоматический режим + case "$MODE" in + "full-stop") + full_stop_backup + ;; + "full-hot") + full_hot_backup + ;; + *) + log "ERROR" "Неизвестный режим: $MODE" + return 1 + ;; + esac + ;; + esac + + # Финальное сообщение + local duration=$(( $(date +%s) - ${START_TIME:-$(date +%s)} )) + log "INFO" "Скрипт выполнен за ${duration} секунд" + log "INFO" "Лог сохранен: $MAIN_LOG" + + # Отправка email если нужно + if [ "$MAIL_ENABLED" = true ] && command -v mutt >/dev/null 2>&1; then + local subject="Бэкап $(hostname) - $(date '+%Y-%m-%d %H:%M')" + tail -50 "$MAIN_LOG" | mutt -s "$subject" "$MAIL_TO" + fi +} + +# ================================================================= +# ЗАПУСК СКРИПТА +# ================================================================= +# Запоминаем время старта +readonly START_TIME=$(date +%s) + +# Проверяем что запускаем от root +if [ "$(id -u)" -ne 0 ]; then + echo -e "${RED}ОШИБКА: Скрипт должен запускаться от root${NC}" + exit 1 +fi + +# Ловим сигналы прерывания +trap 'log "WARNING" "Скрипт прерван пользователем"; exit 1' INT TERM + +# Запускаем +main "$@" + +# Копируем лог в общий файл +cat "$MAIN_LOG" >> "$LOG_DIR/backup_all.log" 2>/dev/null + +exit 0 \ No newline at end of file diff --git a/pgsql/pgpro/legacy/backup-universal.sh b/pgsql/pgpro/legacy/backup-universal.sh new file mode 100644 index 0000000..d661e16 --- /dev/null +++ b/pgsql/pgpro/legacy/backup-universal.sh @@ -0,0 +1,205 @@ +#!/bin/bash +# ================================================================= +# backup-universal.sh - УНИВЕРСАЛЬНЫЙ скрипт бэкапа PostgreSQL 1С +# Версия: 1.0 +# Автор: По вашим требованиям +# ================================================================= + +# Жесткая конфигурация - ничего лишнего +export PATH=$PATH:/opt/pgpro/1c-17/bin:/usr/bin:/bin:/usr/sbin:/sbin +export PGPASSWORD='pass' + +BACKUP_DIR="/backup" +BACKUPS_DIR="/backups" +PG_HOST="127.0.0.1" +PG_PORT="5432" +PG_USER="postgres" +INSTANCE="data" +RETENTION_DAYS=20 + +# Автоопределение версии pg_probackup +if command -v pg_probackup-17 &>/dev/null; then + PG_PROBACKUP="pg_probackup-17" +elif command -v pg_probackup-16 &>/dev/null; then + PG_PROBACKUP="pg_probackup-16" +elif command -v pg_probackup-15 &>/dev/null; then + PG_PROBACKUP="pg_probackup-15" +else + PG_PROBACKUP="pg_probackup" +fi + +# Лог +LOG_DIR="$BACKUP_DIR/log" +mkdir -p "$LOG_DIR" +LOG_FILE="$LOG_DIR/backup_$(date +%Y%m%d_%H%M%S).log" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" +} + +# ОПРЕДЕЛЕНИЕ РЕЖИМА +CURRENT_HOUR=$(date +%H) +if [ -t 0 ]; then + # Ручной запуск - СПРАШИВАЕМ + echo "" + echo "==========================================" + echo "УНИВЕРСАЛЬНЫЙ СКРИПТ БЭКАПА PostgreSQL 1C" + echo "==========================================" + echo "" + echo "Выберите режим:" + echo " 1) Горячий (без остановки 1С) - только физический бэкап" + echo " 2) Полный (с остановкой 1С) - физический + архивация + дампы" + echo "" + read -p "Ваш выбор [1/2]: " MODE_CHOICE + echo "" + + case $MODE_CHOICE in + 2) MODE="full" ;; + *) MODE="hot" ;; + esac +else + # Cron - АВТОМАТИЧЕСКИ ПО ВРЕМЕНИ + if [ $CURRENT_HOUR -ge 6 ] && [ $CURRENT_HOUR -le 23 ]; then + MODE="hot" # 06:00-23:59 - горячий + else + MODE="full" # 00:00-05:59 - полный + fi +fi + +log "==========================================" +log "ЗАПУСК БЭКАПА" +log "Версия PostgreSQL: $(sudo -u postgres psql -V 2>/dev/null | head -1 || echo 'unknown')" +log "pg_probackup: $PG_PROBACKUP" +log "Режим: $MODE" +log "==========================================" + +# ========== ФИЗИЧЕСКИЙ БЭКАП (ВСЕГДА) ========== +log "Физический бэкап через pg_probackup..." +sudo -u postgres $PG_PROBACKUP backup \ + -B "$BACKUP_DIR" \ + --instance "$INSTANCE" \ + -b FULL \ + --stream \ + --compress \ + -j 4 \ + -h "$PG_HOST" \ + -p "$PG_PORT" \ + -U "$PG_USER" \ + -d postgres >> "$LOG_FILE" 2>&1 + +if [ $? -eq 0 ]; then + log "✓ Физический бэкап успешно завершен" + + # Показываем последний бэкап + LAST_BACKUP=$(sudo -u postgres $PG_PROBACKUP show -B "$BACKUP_DIR" --instance "$INSTANCE" 2>/dev/null | grep OK | tail -1) + log "Последний бэкап: $LAST_BACKUP" +else + log "✗ Ошибка физического бэкапа" +fi + +# ========== ПОЛНЫЙ РЕЖИМ (ТОЛЬКО НОЧЬЮ ИЛИ ПО ЗАПРОСУ) ========== +if [ "$MODE" = "full" ]; then + log "" + log "========== ПОЛНЫЙ РЕЖИМ ==========" + + # 1. ОСТАНОВКА 1С + log "Остановка служб 1С..." + systemctl stop srv1c-* 2>/dev/null + systemctl stop srv1c* 2>/dev/null + pkill -f "ragent|rmngr|rphost" 2>/dev/null + sleep 10 + + # 2. АРХИВАЦИЯ БЭКАПА В /backups/copy/ + log "Архивация последнего бэкапа в /backups/copy/..." + LAST_BACKUP_ID=$(ls -t "$BACKUP_DIR/backups/$INSTANCE" 2>/dev/null | head -1) + if [ -n "$LAST_BACKUP_ID" ] && [ -d "$BACKUP_DIR/backups/$INSTANCE/$LAST_BACKUP_ID" ]; then + TIMESTAMP=$(date +"%Y-%m-%d_%H-%M") + mkdir -p "$BACKUPS_DIR/copy" + + tar -czf "$BACKUPS_DIR/copy/pg_pro-$TIMESTAMP.tar.gz" \ + -C "$BACKUP_DIR/backups/$INSTANCE" \ + "$LAST_BACKUP_ID" \ + "pg_probackup.conf" 2>/dev/null && \ + md5sum "$BACKUPS_DIR/copy/pg_pro-$TIMESTAMP.tar.gz" > "$BACKUPS_DIR/copy/pg_pro-$TIMESTAMP.tar.gz.md5" + + if [ $? -eq 0 ]; then + log "✓ Архив создан: pg_pro-$TIMESTAMP.tar.gz" + else + log "✗ Ошибка архивации" + fi + fi + + # 3. ЛОГИЧЕСКИЕ ДАМПЫ В /backups/pg_dump/ + log "Создание логических дампов в /backups/pg_dump/..." + mkdir -p "$BACKUPS_DIR/pg_dump" + + DATABASES=$(sudo -u postgres psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -qAt -c \ + "SELECT datname FROM pg_database WHERE datname NOT IN ('postgres','template0','template1')" postgres 2>/dev/null) + + for DB in $DATABASES; do + log " Дамп базы: $DB" + sudo -u postgres pg_dump -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -Fc "$DB" \ + > "$BACKUPS_DIR/pg_dump/${DB}_$TIMESTAMP.dump" 2>/dev/null + + if [ $? -eq 0 ]; then + md5sum "$BACKUPS_DIR/pg_dump/${DB}_$TIMESTAMP.dump" > "$BACKUPS_DIR/pg_dump/${DB}_$TIMESTAMP.dump.md5" + log " ✓ Успешно" + else + log " ✗ Ошибка" + rm -f "$BACKUPS_DIR/pg_dump/${DB}_$TIMESTAMP.dump" 2>/dev/null + fi + done + + # 4. VACUUM + log "Выполнение VACUUM..." + sudo -u postgres vacuumdb -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -afz 2>&1 | head -20 >> "$LOG_FILE" + log "✓ VACUUM завершен" + + # 5. ЗАПУСК 1С + log "Запуск служб 1С..." + systemctl start srv1c-* 2>/dev/null + systemctl start srv1c* 2>/dev/null + sleep 5 + + log "========== ПОЛНЫЙ РЕЖИМ ЗАВЕРШЕН ==========" +fi + +# ========== ОЧИСТКА СТАРЫХ ФАЙЛОВ ========== +log "" +log "Очистка файлов старше $RETENTION_DAYS дней..." + +# Очистка pg_probackup +sudo -u postgres $PG_PROBACKUP delete \ + -B "$BACKUP_DIR" \ + --instance "$INSTANCE" \ + --delete-wal \ + --retention-redundancy=15 \ + --retention-window=$RETENTION_DAYS \ + -h "$PG_HOST" \ + -p "$PG_PORT" \ + -U "$PG_USER" \ + -d postgres >> "$LOG_FILE" 2>&1 + +# Очистка /backups/copy/ +find "$BACKUPS_DIR/copy" -name "*.tar.gz" -type f -mtime +$RETENTION_DAYS -delete 2>/dev/null +find "$BACKUPS_DIR/copy" -name "*.md5" -type f -mtime +$RETENTION_DAYS -delete 2>/dev/null +find "$BACKUPS_DIR/pg_dump" -name "*.dump" -type f -mtime +$RETENTION_DAYS -delete 2>/dev/null +find "$BACKUPS_DIR/pg_dump" -name "*.md5" -type f -mtime +$RETENTION_DAYS -delete 2>/dev/null + +log "✓ Очистка завершена" + +# ========== ИТОГ ========== +log "" +log "==========================================" +log "ИТОГ БЭКАПА:" +log "- Физический бэкап: $BACKUP_DIR/backups/$INSTANCE/" +log "- Архивы: $BACKUPS_DIR/copy/" +log "- Дампы: $BACKUPS_DIR/pg_dump/" +log "- Лог: $LOG_FILE" +log "==========================================" + +# Показываем последние 5 бэкапов +log "" +log "Последние бэкапы в $BACKUP_DIR:" +sudo -u postgres $PG_PROBACKUP show -B "$BACKUP_DIR" --instance "$INSTANCE" 2>/dev/null | grep -E "OK|ERROR" | tail -5 >> "$LOG_FILE" +cat "$LOG_FILE" | grep "OK" | tail -5 \ No newline at end of file diff --git a/pgsql/pgpro/maintenance.sh b/pgsql/pgpro/maintenance.sh new file mode 100644 index 0000000..61fdfca --- /dev/null +++ b/pgsql/pgpro/maintenance.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --- настройки --- +PGHOST="localhost" +PGUSER="postgres" +TIMEOUT=3 +RETENTION_DAYS=30 +LIMIT_BYTES=$((10*1024*1024*1024)) # 10GB +MIN_TBL_BYTES=$((1*1024*1024)) # 1MB +MAX_TBL_BYTES=$((3*1024*1024*1024)) # 3GB + +# VACUUM FULL по таблице, если доля мёртвых строк > THRESHOLD_PCT +THRESHOLD_PCT=30 + +# --- утилиты --- +need_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "ERROR: need '$1'"; exit 1; }; } +need_cmd ss +need_cmd pg_isready +need_cmd psql +need_cmd find + +ports() { + ss -tlnp 2>/dev/null \ + | grep postgres \ + | grep -oP ':\d+' \ + | grep -oP '\d+' \ + | sort -u +} + +psql_sys() { + local port="$1"; shift + psql -h "$PGHOST" -p "$port" -U "$PGUSER" -d postgres -X -v ON_ERROR_STOP=1 "$@" +} + +psql_db() { + local port="$1" db="$2"; shift 2 + psql -h "$PGHOST" -p "$port" -U "$PGUSER" -d "$db" -X -v ON_ERROR_STOP=1 "$@" +} + +ts() { date '+%F %T'; } + +# --- основной цикл по портам --- +for PGPORT in $(ports); do + echo "[$(ts)] === Checking instance on port $PGPORT ===" + + if ! pg_isready -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -t "$TIMEOUT" >/dev/null 2>&1; then + echo "[$(ts)] Port $PGPORT: not ready, skip" + continue + fi + + PGDATA="$(psql_sys "$PGPORT" -Atc "SHOW data_directory;")" + if [[ -z "${PGDATA}" ]]; then + echo "[$(ts)] ERROR: cannot get data_directory for port $PGPORT" + exit 1 + fi + + LOGDIR="${PGDATA}/log" + if [[ ! -d "$LOGDIR" ]]; then + echo "[$(ts)] ERROR: log dir '$LOGDIR' not found for port $PGPORT (stop)" + exit 1 + fi + + LOGFILE="${LOGDIR}/maint_${PGPORT}_$(date +%F).log" + exec > >(tee -a "$LOGFILE") 2>&1 + + echo "[$(ts)] Port=$PGPORT PGDATA=$PGDATA LOGFILE=$LOGFILE" + + # ротация: удаляем файлы старше RETENTION_DAYS + find "$LOGDIR" -maxdepth 1 -type f -name 'maint_*.log' -mtime +"$RETENTION_DAYS" -print -delete || true + + # --- списки баз --- + mapfile -t DB_VF < <(psql_sys "$PGPORT" -Atc " + SELECT datname + FROM pg_database + WHERE datallowconn + AND NOT datistemplate + AND datname <> 'postgres' + AND pg_database_size(datname) < ${LIMIT_BYTES}; + ") + + mapfile -t DB_SC < <(psql_sys "$PGPORT" -Atc " + SELECT datname + FROM pg_database + WHERE datallowconn + AND NOT datistemplate + AND datname <> 'postgres' + AND pg_database_size(datname) >= ${LIMIT_BYTES}; + ") + + if ((${#DB_VF[@]})); then + echo "[$(ts)] DB_VF (<10GB): ${DB_VF[*]}" + else + echo "[$(ts)] DB_VF (<10GB): none" + fi + + if ((${#DB_SC[@]})); then + echo "[$(ts)] DB_SC (>=10GB): ${DB_SC[*]}" + else + echo "[$(ts)] DB_SC (>=10GB): none" + fi + + # --- DB_VF: VACUUM FULL + ANALYZE --- + for db in "${DB_VF[@]:-}"; do + [[ -z "$db" ]] && continue + echo "[$(ts)] [$PGPORT/$db] VACUUM FULL + ANALYZE (database)" + psql_db "$PGPORT" "$db" -c "VACUUM (FULL, ANALYZE);" + done + + # --- DB_SC: ANALYZE, затем таблицы 1MB..3GB и выборочный VACUUM FULL --- + for db in "${DB_SC[@]:-}"; do + [[ -z "$db" ]] && continue + + echo "[$(ts)] [$PGPORT/$db] ANALYZE (database)" + psql_db "$PGPORT" "$db" -c "ANALYZE;" + + # schema|table|total_size_bytes|live|dead + mapfile -t candidates < <(psql_db "$PGPORT" "$db" -Atc " + SELECT + schemaname, + relname, + pg_total_relation_size(relid) AS total_size_bytes, + COALESCE(n_live_tup,0) AS n_live_tup, + COALESCE(n_dead_tup,0) AS n_dead_tup + FROM pg_stat_user_tables + WHERE pg_total_relation_size(relid) BETWEEN ${MIN_TBL_BYTES} AND ${MAX_TBL_BYTES} + ORDER BY pg_total_relation_size(relid) DESC; + ") + + for row in "${candidates[@]:-}"; do + [[ -z "$row" ]] && continue + IFS='|' read -r schema table size_bytes live dead <<< "$row" + + total=$((live + dead)) + if (( total <= 0 )); then + continue + fi + + # dead_pct = dead / (live+dead) * 100 (целочисленно) + dead_pct=$(( dead * 100 / total )) + + if (( dead_pct > THRESHOLD_PCT )); then + fqtn="\"${schema}\".\"${table}\"" + echo "[$(ts)] [$PGPORT/$db] Table $fqtn size=${size_bytes}B live=$live dead=$dead => dead_pct=${dead_pct}% > ${THRESHOLD_PCT}%: VACUUM FULL" + + psql_db "$PGPORT" "$db" -c "VACUUM (FULL) ${fqtn};" + psql_db "$PGPORT" "$db" -c "SELECT pg_stat_reset_single_table_counters('${schema}.${table}'::regclass);" + psql_db "$PGPORT" "$db" -c "ANALYZE ${fqtn};" + fi + done + done + + echo "[$(ts)] === Done port $PGPORT ===" +done diff --git a/pgsql/pgpro/pg_add_cluster.sh b/pgsql/pgpro/pg_add_cluster.sh new file mode 100644 index 0000000..81873d1 --- /dev/null +++ b/pgsql/pgpro/pg_add_cluster.sh @@ -0,0 +1,359 @@ +#!/bin/bash +# add_pgpro_instance.sh — добавление нового экземпляра PostgresPro +# Использование: sudo bash add_pgpro_instance.sh [PGPORT] [PGVER] +# sudo bash add_pgpro_instance.sh ← интерактивный режим + +set -euo pipefail + +# ─────────────────────────────────────────────── +# ЦВЕТА +# ─────────────────────────────────────────────── +RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m' +CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' + +# ─────────────────────────────────────────────── +# УТИЛИТЫ +# ─────────────────────────────────────────────── +log() { echo -e "${GREEN}[INFO]${RESET} $*"; } +warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; } +die() { echo -e "${RED}[ERROR]${RESET} $*" >&2; exit 1; } +header() { echo -e "\n${BOLD}${CYAN}$*${RESET}"; } +confirm() { + # confirm "Вопрос" → 0=да, 1=нет + local prompt="$1" + while true; do + read -rp "$(echo -e "${YELLOW}${prompt}${RESET} [y/n]: ")" ans + case "${ans,,}" in + y|yes) return 0 ;; + n|no) return 1 ;; + *) echo "Введите y или n." ;; + esac + done +} + +# ─────────────────────────────────────────────── +# СКАНИРОВАНИЕ СУЩЕСТВУЮЩИХ ИНСТАНСОВ +# ─────────────────────────────────────────────── +scan_instances() { + local ver="${1:-1c-18}" + local defaults_dir="/etc/default" + local pg_bin="/opt/pgpro/${ver}/bin" + + header "═══ Существующие инстансы PostgresPro-${ver} ═══" + + # Ищем файлы окружения вида postgrespro-- и postgrespro- + local files + mapfile -t files < <( + find "${defaults_dir}" -maxdepth 1 \ + -name "postgrespro-${ver}*" \ + ! -name "*.dpkg*" ! -name "*.bak" \ + 2>/dev/null | sort + ) + + if [[ ${#files[@]} -eq 0 ]]; then + warn "Файлы окружения не найдены в ${defaults_dir}" + return + fi + + # Шапка таблицы + printf "${BOLD}%-8s %-6s %-45s %-10s %-8s${RESET}\n" \ + "СЕРВИС" "ПОРТ" "PGDATA" "СТАТУС" "PID" + printf '%s\n' "$(printf '─%.0s' {1..85})" + + local found=0 + for f in "${files[@]}"; do + # Извлекаем PGDATA и PGPORT из файла окружения + local pgdata pgport svc status pid_str + + pgdata=$(grep -E "^PGDATA=" "$f" 2>/dev/null \ + | head -1 | sed "s/^PGDATA=//;s/['\"]//g") || pgdata="—" + + # Порт: из имени файла или из PGPORT внутри файла + local basename + basename=$(basename "$f") + if [[ "$basename" =~ -([0-9]{4,5})$ ]]; then + pgport="${BASH_REMATCH[1]}" + else + pgport=$(grep -E "^PGPORT=" "$f" 2>/dev/null \ + | head -1 | sed "s/^PGPORT=//;s/['\"]//g") || pgport="5432" + fi + + # Имя сервиса + if [[ "$pgport" == "5432" ]]; then + svc="postgrespro-${ver}" + else + svc="postgrespro-${ver}@${pgport}" + fi + + # Статус systemd + if systemctl is-active --quiet "${svc}" 2>/dev/null; then + status="${GREEN}running${RESET}" + # PID мастер-процесса + pid_str=$(systemctl show -p MainPID "${svc}" 2>/dev/null \ + | cut -d= -f2) || pid_str="—" + elif systemctl is-enabled --quiet "${svc}" 2>/dev/null; then + status="${YELLOW}stopped${RESET}" + pid_str="—" + else + status="${RED}unknown${RESET}" + pid_str="—" + fi + + printf "${BOLD}%-8s${RESET} %-6s %-45s %-18s %-8s\n" \ + "${svc##postgrespro-${ver}}" \ + "${pgport}" \ + "${pgdata}" \ + "$(echo -e "${status}")" \ + "${pid_str}" + + (( found++ )) || true + done + + printf '%s\n' "$(printf '─%.0s' {1..85})" + echo -e "Итого: ${BOLD}${found}${RESET} инстанс(ов)\n" +} + +# ─────────────────────────────────────────────── +# ИНТЕРАКТИВНЫЙ ВВОД +# ─────────────────────────────────────────────── +interactive_mode() { + header "═══ Интерактивное добавление инстанса PostgresPro ═══" + + # PGVER + read -rp "$(echo -e "${CYAN}Версия PostgresPro${RESET} [1c-18]: ")" input_ver + PGVER="${input_ver:-1c-18}" + + # Показываем таблицу ПОСЛЕ выбора версии + scan_instances "${PGVER}" + + # PGPORT + while true; do + read -rp "$(echo -e "${CYAN}Порт для нового инстанса${RESET} [5433]: ")" input_port + PGPORT="${input_port:-5433}" + if ! [[ "$PGPORT" =~ ^[0-9]{4,5}$ ]]; then + warn "Некорректный порт: ${PGPORT}" + continue + fi + if ss -tlnp | grep -q ":${PGPORT} "; then + warn "Порт ${PGPORT} уже занят — выберите другой." + continue + fi + break + done + + # PGDATA + local default_data="/var/lib/pgpro/${PGVER}/data-${PGPORT}" + read -rp "$(echo -e "${CYAN}Каталог данных${RESET} [${default_data}]: ")" input_data + PGDATA_NEW="${input_data:-${default_data}}" + + # Пароль + read -rsp "$(echo -e "${CYAN}Пароль суперпользователя${RESET} (Enter = пропустить): ")" PGPASSWORD + echo "" + + # Подтверждение + echo "" + echo -e "${BOLD}Параметры нового инстанса:${RESET}" + printf " %-12s %s\n" "Версия:" "${PGVER}" + printf " %-12s %s\n" "Порт:" "${PGPORT}" + printf " %-12s %s\n" "PGDATA:" "${PGDATA_NEW}" + printf " %-12s %s\n" "Пароль:" "${PGPASSWORD:+(задан)}" + echo "" + + confirm "Продолжить создание инстанса?" || { echo "Отменено."; exit 0; } +} + +# ─────────────────────────────────────────────── +# ПАРАМЕТРЫ (авто или интерактив) +# ─────────────────────────────────────────────── +PGPORT="" +PGVER="1c-18" +PGPASSWORD="" +PGDATA_NEW="" + +if [[ $# -eq 0 ]]; then + INTERACTIVE=1 +else + INTERACTIVE=0 + PGPORT="${1:-5433}" + PGVER="${2:-1c-18}" + PGPASSWORD="${3:-}" +fi + +# ─────────────────────────────────────────────── +# ЗАВИСИМЫЕ ПЕРЕМЕННЫЕ (заполняются после ввода) +# ─────────────────────────────────────────────── +setup_vars() { + PGDATA_BASE="/var/lib/pgpro/${PGVER}" + PGDATA_NEW="${PGDATA_NEW:-${PGDATA_BASE}/data-${PGPORT}}" + PG_BIN="/opt/pgpro/${PGVER}/bin" + + SYSTEMD_DIR="/lib/systemd/system" + DEFAULTS_DIR="/etc/default" + + SVC_BASE="postgrespro-${PGVER}" + SVC_TEMPLATE="${SVC_BASE}@.service" + SVC_INSTANCE="${SVC_BASE}@${PGPORT}" + + ENV_BASE="${DEFAULTS_DIR}/${SVC_BASE}" + ENV_NEW="${DEFAULTS_DIR}/${SVC_BASE}-${PGPORT}" + OVERRIDE_DIR="/etc/systemd/system/${SVC_INSTANCE}.service.d" +} + +# ─────────────────────────────────────────────── +# ТОЧКА ВХОДА +# ─────────────────────────────────────────────── +[[ $EUID -eq 0 ]] || die "Запустите скрипт от root (sudo)." + +if [[ $INTERACTIVE -eq 1 ]]; then + interactive_mode # внутри уже вызывает scan_instances +else + # В автоматическом режиме — сразу показываем таблицу + scan_instances "${PGVER}" +fi + +setup_vars + +# ─────────────────────────────────────────────── +# ПРОВЕРКИ +# ─────────────────────────────────────────────── +header "Проверка предусловий..." + +# Ищем базовый unit: обычный или шаблонный +BASE_UNIT="" +for candidate in \ + "${SYSTEMD_DIR}/${SVC_BASE}.service" \ + "${SYSTEMD_DIR}/${SVC_TEMPLATE}" \ + "/usr/lib/systemd/system/${SVC_BASE}.service" \ + "/usr/lib/systemd/system/${SVC_TEMPLATE}"; do + if [[ -f "$candidate" ]]; then + BASE_UNIT="$candidate" + break + fi +done +[[ -n "$BASE_UNIT" ]] || die "Базовый unit не найден. Проверьте установку pgpro." +log "Базовый unit: ${BASE_UNIT}" + +[[ -f "${ENV_BASE}" ]] || die "Файл окружения не найден: ${ENV_BASE}" +[[ -x "${PG_BIN}/initdb" ]] || die "initdb не найден: ${PG_BIN}/initdb" + +ss -tlnp | grep -q ":${PGPORT} " \ + && die "Порт ${PGPORT} уже занят." + +if [[ -d "${PGDATA_NEW}" && -n "$(ls -A "${PGDATA_NEW}" 2>/dev/null)" ]]; then + die "Каталог данных уже существует и не пуст: ${PGDATA_NEW}" +fi + +systemctl is-active --quiet "${SVC_INSTANCE}" \ + && die "Сервис ${SVC_INSTANCE} уже запущен." + +log "Все проверки пройдены." + +# ─────────────────────────────────────────────── +# ШАГ 1: Шаблон systemd-юнита +# ─────────────────────────────────────────────── +header "Шаг 1: Создание шаблонного unit-файла..." + +if [[ ! -f "${SYSTEMD_DIR}/${SVC_TEMPLATE}" ]]; then + cp "${BASE_UNIT}" "${SYSTEMD_DIR}/${SVC_TEMPLATE}" + # Убираем WantedBy из шаблона, чтобы не конфликтовал + sed -i '/^WantedBy=/d' "${SYSTEMD_DIR}/${SVC_TEMPLATE}" + log "Создан шаблон: ${SYSTEMD_DIR}/${SVC_TEMPLATE}" +else + log "Шаблон уже существует, пропускаем." +fi + +# ─────────────────────────────────────────────── +# ШАГ 2: Файл окружения +# ─────────────────────────────────────────────── +header "Шаг 2: Файл окружения ${ENV_NEW}..." + +[[ -f "${ENV_NEW}" ]] && warn "Перезаписываем существующий ${ENV_NEW}" +cp "${ENV_BASE}" "${ENV_NEW}" +sed -i "s#^PGDATA=.*#PGDATA='${PGDATA_NEW}'#" "${ENV_NEW}" +log "PGDATA → ${PGDATA_NEW}" + +# ─────────────────────────────────────────────── +# ШАГ 3: Override +# ─────────────────────────────────────────────── +header "Шаг 3: Override для ${SVC_INSTANCE}..." + +mkdir -p "${OVERRIDE_DIR}" +cat > "${OVERRIDE_DIR}/override.conf" </dev/null; do + sleep 1; (( ELAPSED++ )) || true + [[ ${ELAPSED} -lt ${TIMEOUT} ]] \ + || die "Сервис не поднялся за ${TIMEOUT}с. Проверьте: journalctl -u ${SVC_INSTANCE}" +done +log "Инстанс отвечает на порту ${PGPORT}." + +# ─────────────────────────────────────────────── +# ШАГ 7: Пароль суперпользователя +# ─────────────────────────────────────────────── +if [[ -n "${PGPASSWORD}" ]]; then + header "Шаг 7: Установка пароля..." + su - postgres -c \ + "${PG_BIN}/psql -p ${PGPORT} -c \"ALTER USER postgres WITH PASSWORD '${PGPASSWORD}';\"" + log "Пароль установлен." +else + warn "Пароль не задан. Установите вручную:" + echo " su - postgres -c \"${PG_BIN}/psql -p ${PGPORT} -c \\\"ALTER USER postgres WITH PASSWORD 'pass';\\\"\"" +fi + +# ─────────────────────────────────────────────── +# ИТОГ + обновлённая таблица всех инстансов +# ─────────────────────────────────────────────── +echo "" +echo -e "${BOLD}${GREEN}════════════════════════════════════════════${RESET}" +echo -e "${BOLD}${GREEN} ✅ Инстанс успешно создан${RESET}" +echo -e "${BOLD}${GREEN}════════════════════════════════════════════${RESET}" +printf " ${BOLD}%-12s${RESET} %s\n" "Версия:" "${PGVER}" +printf " ${BOLD}%-12s${RESET} %s\n" "Порт:" "${PGPORT}" +printf " ${BOLD}%-12s${RESET} %s\n" "PGDATA:" "${PGDATA_NEW}" +printf " ${BOLD}%-12s${RESET} %s\n" "Сервис:" "${SVC_INSTANCE}" +echo -e "${BOLD}${GREEN}════════════════════════════════════════════${RESET}\n" + +echo "Полезные команды:" +echo " systemctl status ${SVC_INSTANCE}" +echo " journalctl -u ${SVC_INSTANCE} -f" +echo " su - postgres -c \"${PG_BIN}/psql -p ${PGPORT}\"" + +# Финальная таблица всех инстансов +scan_instances "${PGVER}" diff --git a/pgsql/pgpro/systemd/backup-pgsql.service b/pgsql/pgpro/systemd/backup-pgsql.service new file mode 100644 index 0000000..c147615 --- /dev/null +++ b/pgsql/pgpro/systemd/backup-pgsql.service @@ -0,0 +1,13 @@ +[Unit] +Description=Backup and maintenance PosrgeSQL DB +Requires=postgrespro-1c-16.service +After=postgrespro-1c-16.service + +[Service] +Type=oneshot +ExecStart=/var/lib/pgpro/_backup/scripts/backup-db-pgsql.sh +User=postgres +Environment="PATH=/sbin:/bin:/usr/sbin:/usr/bin" + +[Install] +WantedBy=multi-user.target diff --git a/pgsql/pgpro/systemd/backup-pgsql.timer b/pgsql/pgpro/systemd/backup-pgsql.timer new file mode 100644 index 0000000..6e46d60 --- /dev/null +++ b/pgsql/pgpro/systemd/backup-pgsql.timer @@ -0,0 +1,8 @@ +[Unit] +Description=Backup and maintenance PosrgeSQL DB + +[Timer] +OnCalendar=*-*-* 01:15:00 + +[Install] +WantedBy=timers.target diff --git a/pgsql/pgpro/wal-g-scripts/wal-g-backup-all.sh b/pgsql/pgpro/wal-g-scripts/wal-g-backup-all.sh new file mode 100644 index 0000000..c16f441 --- /dev/null +++ b/pgsql/pgpro/wal-g-scripts/wal-g-backup-all.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# ============================================================================== +# wal-g-backup.sh — резервное копирование всех PostgreSQL-кластеров через WAL-G +# ============================================================================== + +set -uo pipefail + +# ── Константы ────────────────────────────────────────────────────────────────── +readonly WALG_DIR="/etc/wal-g.d" +readonly WALG_BIN="/usr/bin/wal-g" +readonly PG_USER="postgres" +readonly RETAIN=3 # сколько FULL-бэкапов хранить + +# ── Глобальный счётчик ошибок ────────────────────────────────────────────────── +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 - <> "\${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 new file mode 100644 index 0000000..9c4fabe --- /dev/null +++ b/pgsql/pgpro/wal-g-scripts/wal-g-backup.sh @@ -0,0 +1,235 @@ +#!/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 ':\d+' \ + | grep -oP '\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 -p "$port" -U postgres -t 3 >/dev/null 2>&1; then + echo " [SKIP] Порт ${port}: кластер недоступен" + return 1 + fi + + local pgdata + pgdata="$(psql -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}'" 2>&1; 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 new file mode 100644 index 0000000..19dc21f --- /dev/null +++ b/pgsql/pgpro/wal-g-scripts/wal-g-cfg.sh @@ -0,0 +1,559 @@ +#!/usr/bin/env bash + +# Скрипт для настройки конфигурационных файлов WAL-G для хранилища S3 или файлового варианта +# Скрипт автоматически определяет кластера PostgreSQL и каталоги данных +# Также автоматически добавляются необходимые параметры для архивации в postgresql.auto.conf +# Для корректной работы скрипта должен быть настроен файл паролей .pgpass + +set -Eeuo pipefail + +# ============================================================================= +# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ +# ============================================================================= + +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" +} + +confirm_overwrite() { + local file="$1" ans + read -r -p "Файл ${file} уже существует. Перезаписать? [y/N]: " ans + [[ "$ans" == [Yy] ]] +} + +apply_param() { + local -n _psql_ref="$1" + local name="$2" + local value="$3" + local esc="${value//\'/\'\'}" + "${_psql_ref[@]}" -c "ALTER SYSTEM SET ${name} = '${esc}';" >/dev/null +} + +# ── Цвета ──────────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +RESET='\033[0m' + +# ============================================================================= +# ИМЯ СЕРВИСА 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}" + + local ans + read -r -p "Перезапустить сервис ${svc}? [y/N]: " ans + if [[ ! "$ans" =~ ^[yY]$ ]]; then + echo "INFO: перезапуск отменён. Выполните вручную:" + echo " systemctl restart ${svc}" + return 0 + fi + + echo "INFO: выполняется systemctl restart ${svc} ..." + + if ! systemctl restart "$svc" 2>&1; 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_isready -h /tmp -p "$pgport" -U postgres -t 2 >/dev/null 2>&1; then + echo "OK: кластер ${pgport} запущен." + return 0 + fi + echo " попытка ${i}/${retries}..." + sleep 2 + (( i++ )) || true + 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 idx=1 + + echo + echo "=== Доступные кластеры ===" + echo + + for port in "${ports[@]}"; do + cfg_path="${WALG_DIR}/.walg-${port}.json" + + # Доступность и PGDATA + if pg_isready -h /tmp -p "$port" -U postgres -t 3 >/dev/null 2>&1; then + avail_status="OK" + pgdata="$(psql -h /tmp -p "$port" -U postgres -d postgres \ + -At -q -X -c "SHOW data_directory;" 2>/dev/null)" || 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 + local pgdata_info + 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++ )) || true + 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 /tmp -p "$pgport" -U postgres -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=() + 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) + 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} уже соответствуют целевым." + 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 + + local ans + read -r -p "Применить ${#TO_CHANGE[@]} изменения для ${pgport}? [y/N]: " ans + if [[ ! "$ans" =~ ^[yY]$ ]]; then + echo "INFO: изменения ${pgport} отменены пользователем." + return 0 + fi + + for p in "${TO_CHANGE[@]}"; do + if ! apply_param PSQL "$p" "${WANT[$p]}"; then + echo "ERROR: не удалось установить параметр '${p}' ${pgport}" >&2 + return 1 + fi + 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 +} + +# ============================================================================= +# КОНСТАНТЫ +# ============================================================================= + +WALG_DIR='/etc/wal-g.d' +WALG_COMPRESSION_METHOD='brotli' + +# ============================================================================= +# ШАГ 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 ':\d+' \ + | grep -oP '\d+' \ + | sort -u) +fi + +SELECTED_PORTS=() +if (( ${#FOUND_PORTS[@]} > 0 )); then + print_cluster_summary "${FOUND_PORTS[@]}" + select_ports_by_index FOUND_PORTS +else + echo "WARN: кластеры не обнаружены автоматически." >&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%/}" +else + WALG_FILE_PREFIX_BASE="$(prompt_default 'Базовый каталог WALG_FILE_PREFIX' '/backup')" + WALG_FILE_PREFIX_BASE="${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} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + SVC_NAME="$(get_service_name "$PGVER" "$PGPORT")" + echo "INFO: сервис systemd: ${SVC_NAME}" + + if ! [[ "$PGPORT" =~ ^[0-9]+$ ]]; then + echo "WARN: некорректный номер '${PGPORT}', пропуск" >&2 + continue + fi + + CFG="${WALG_DIR}/.walg-${PGPORT}.json" + + if ! pg_isready -h /tmp -p "$PGPORT" -U postgres -t 3 >/dev/null 2>&1; then + echo "WARN: кластер на порту ${PGPORT} недоступен, пропуск" >&2 + continue + fi + + PGDATA="$(psql -h /tmp -p "$PGPORT" -U postgres -d postgres -At -q -X \ + -c "SHOW data_directory;" 2>/dev/null)" || true + + if [[ -z "$PGDATA" || ! -d "$PGDATA" ]]; then + echo "WARN: не удалось определить PGDATA для порта ${PGPORT}, пропуск" >&2 + continue + fi + + echo "INFO: PGDATA = ${PGDATA}" + + if [[ -f "$CFG" ]]; then + if ! confirm_overwrite "$CFG"; then + echo "INFO: конфиг ${CFG} оставлен без изменений." + PG_RESTART_NEEDED=0 + configure_pg_instance "$PGPORT" "$PGDATA" "$CFG" + if (( PG_RESTART_NEEDED )); then + restart_pg_service "$PGVER" "$PGPORT" + fi + continue + fi + if ! cp -p -- "$CFG" "${CFG}.bak"; then + echo "ERROR: не удалось создать резервную копию ${CFG}.bak" >&2 + continue + fi + echo "INFO: создана резервная копия ${CFG}.bak" + fi + + if [[ "$BACKEND" == "s3" ]]; then + cat > "$CFG" < "$CFG" </dev/null; then + echo "WARN: не удалось установить владельца postgres для ${CFG}" >&2 + fi + + echo "OK: конфиг записан: ${CFG}" + + PG_RESTART_NEEDED=0 + configure_pg_instance "$PGPORT" "$PGDATA" "$CFG" + + if (( PG_RESTART_NEEDED )); then + restart_pg_service "$PGVER" "$PGPORT" + 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 new file mode 100644 index 0000000..6542587 --- /dev/null +++ b/pgsql/pgpro/wal-g-scripts/wal-g-delete.sh @@ -0,0 +1,213 @@ +#!/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" + +# ── Вспомогательные функции ──────────────────────────────────────────────────── +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 + + local backup_list + backup_list="$("${WALG_BIN}" backup-list --pretty --config "${cfg}" 2>&1)" + local rc=$? + + if (( rc != 0 )); then + err "Не удалось получить список бэкапов" + echo "${backup_list}" >&2 + return 1 + fi + + if [[ -z "${backup_list}" ]]; then + echo " (бэкапы не найдены)" + separator + echo + return 0 + fi + + echo "${backup_list}" | awk ' + NR==1 { print " " $0; next } + /FULL/ { print " \033[1;32m" $0 "\033[0m"; next } + { print " \033[0;33m" $0 "\033[0m" } + ' + separator + echo + + # Подсчёт FULL-бэкапов: всё что не содержит _D_ (исключая заголовок) + local full_count + full_count="$("${WALG_BIN}" backup-list --pretty --config "${cfg}" \ + | grep -v "_D_" \ + | grep -c "base_")" + + echo " Всего FULL-бэкапов: ${full_count}" + echo + + local full_backup_list + full_backup_list="$("${WALG_BIN}" backup-list --pretty --config "${cfg}" | grep -v "_D_")" + + echo "${full_backup_list}" | awk ' + NR==1 { print " " $0; next } + /FULL/ { print " \033[1;32m" $0 "\033[0m"; next } + { print " \033[0;33m" $0 "\033[0m" } + ' + 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-копий для хранения ─────────────────────── + local retain + while true; do + read -rp "Сколько FULL-бэкапов оставить? [введите число]: " retain + [[ "${retain}" =~ ^[1-9][0-9]*$ ]] && break + echo " ✗ Введите целое число больше 0." + done + + # ── Итоговая сводка ────────────────────────────────────────────────────── + 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-list.sh b/pgsql/pgpro/wal-g-scripts/wal-g-list.sh new file mode 100644 index 0000000..f0ea7d1 --- /dev/null +++ b/pgsql/pgpro/wal-g-scripts/wal-g-list.sh @@ -0,0 +1,218 @@ +#!/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}" + +# ── Цвета ──────────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +RESET='\033[0m' + +# ── Проверка зависимостей ──────────────────────────────────────────────────── +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 обработать все найденные конфиги" +} + +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++ )) + done + + echo + echo -e " ${BOLD}${i}.${RESET} Все кластеры" + echo -e " ${BOLD}0.${RESET} Выход" + echo +} + +# ── Функция: запуск backup-list ────────────────────────────────────────────── +run_backup_list() { + local cfg="$1" + local port + port="$(extract_port "$cfg")" + + echo + echo -e "${BOLD}=== WAL-G backup-list · Порт ${port} ===${RESET}" + echo + + if [[ "$FULL_ONLY" == "true" ]]; then + "$WALG_BIN" backup-list --pretty --config "$cfg" | grep -v "_D_" + else + "$WALG_BIN" backup-list --pretty --config "$cfg" + fi +} + +# ── Режим: аргумент передан из CLI ─────────────────────────────────────────── +if [[ $# -ge 1 ]]; then + INPUT="$1" + + if [[ "${INPUT,,}" == "all" ]]; then + errors=0 + for cfg in "${CFGS[@]}"; do + run_backup_list "$cfg" || (( errors++ )) || true + done + if (( errors > 0 )); then + echo -e "${YELLOW}WARN: $errors config(s) failed${RESET}" >&2 + exit 5 + fi + 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_backup_list "$WALG_CONFIG_FILE" + 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. Вывод бэкапов ───────────────────────────────────────────────────────── +errors=0 +for cfg in "${SELECTED_CFGS[@]}"; do + run_backup_list "$cfg" || (( errors++ )) || true +done + +if (( errors > 0 )); then + echo -e "${YELLOW}WARN: $errors config(s) failed${RESET}" >&2 + exit 5 +fi + +exit 0 diff --git a/pgsql/pgpro/wal-g-scripts/wal-g-restore.sh b/pgsql/pgpro/wal-g-scripts/wal-g-restore.sh new file mode 100644 index 0000000..8c92a43 --- /dev/null +++ b/pgsql/pgpro/wal-g-scripts/wal-g-restore.sh @@ -0,0 +1,250 @@ +#!/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}"; } + +# ============================================================================= +# Шаг 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" + +RESTORE_PGDATA="/var/lib/pgpro/${PGVER}/data-${PGPORT}" +log_info "PGDATA по умолчанию: ${BOLD}$RESTORE_PGDATA${NC}" + +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 + +# ============================================================================= +# Шаг 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) + BACKUP_NAME="LATEST" + read -rp "Введите дату/время (формат: YYYY-MM-DD HH:MM:SS): " PITR_TARGET + [[ -z "$PITR_TARGET" ]] && { log_warn "Время не может быть пустым."; continue; } + break + ;; + *) + log_warn "Некорректный ввод." + ;; + esac +done + +# ============================================================================= +# Шаг 4: Проверка сервиса и директории +# ============================================================================= +log_section "Предварительные проверки" + +SERVICE_NAME="postgresql-${PGVER}-${PGPORT}" + +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 + log_warn "Очищаю $RESTORE_PGDATA ..." + rm -rf "$RESTORE_PGDATA" + fi +fi + +mkdir -p "$RESTORE_PGDATA" +chown postgres:postgres "$RESTORE_PGDATA" +chmod 700 "$RESTORE_PGDATA" +log_info "Директория подготовлена: $RESTORE_PGDATA" + +# ============================================================================= +# Шаг 5: Итоговое подтверждение +# ============================================================================= +log_section "Итоговые параметры" + +echo -e " Порт: ${BOLD}$PGPORT${NC}" +echo -e " PGDATA: ${BOLD}$RESTORE_PGDATA${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 + +# ============================================================================= +# Шаг 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" + +if [[ -n "${PITR_TARGET:-}" ]]; then + RECOVERY_CONF="$RESTORE_PGDATA/postgresql.auto.conf" + log_info "Добавляю PITR параметры в $RECOVERY_CONF" + cat >> "$RECOVERY_CONF" <