Microsegment.ru
  • Главная страница
  • О проекте
  • Портфолио
  • Блог
Системы

Автоматизация создания слоя RAW гибридного хранилища данных

Автоматизация создания слоя RAW гибридного хранилища данных
Системы

Здесь представлены файлы bash для автоматизации создания слоя сырых данных RAW с архивируемыми партициями на отдельных отмонтируемых сегментах диска минимального жизнеспособного (MVP) гибридного хранилища данных (Data Lakehouse) на основе PostgreSQL.

Содержание:

  1. Скрипт файла create_raw.sh для создания слоя RAW
  2. Скрипт файла drop_raw.sh для удаления слоя RAW

Скрипт файла create_raw.sh для создания слоя RAW¶

Файл create_raw.sh — предназначен для автоматизации создания «с нуля» слоя RAW минимального жизнеспособного гибридного хранилища данных. Это полезно при первичном создании слоя и отладки его функционирования. Обязательно сделать этот файл исполняемым с помощью команды: sudo chmod +x /opt/dlh/scripts/create_raw.sh. После этого его можно будет использовать с помощью команды ./create_raw.sh.

#!/usr/bin/env bash
# ============================================================================
# create_raw.sh — идемпотентное создание слоя RAW (PostgreSQL 17, РЕД ОС)
#
# Принципы:
#   1. Идемпотентность: повторный запуск не ломает систему.
#   2. Атомарность: каждый SQL-скрипт выполняется в одной транзакции.
#   3. Наименьшие привилегии: отдельные роли, никаких PUBLIC-грантов.
#   4. Наблюдаемость: каждый шаг логируется с контекстом.
#   5. Готовность к автоматизации: конфигурация вынесена, скрипт пригоден
#      для вызова из Ansible / CI-CD пайплайна.
#
# Использование:
#   sudo /opt/dlh/scripts/create_raw.sh
#   RAW_YEAR=2027 RAW_QUARTERS="2 3 4" /opt/dlh/scripts/create_raw.sh
# ============================================================================

set -Eeuo pipefail

# ---------------------------------------------------------------------------
# 0. Трассировка и очистка при ошибках
# ---------------------------------------------------------------------------
trap 'on_error $? $LINENO "${BASH_COMMAND}"' ERR
trap 'cleanup' EXIT

on_error() {
    local exit_code=$1 line_no=$2 failed_cmd=$3
    log "КРИТИЧЕСКАЯ ОШИБКА: код=${exit_code}, строка=${line_no}, команда='${failed_cmd}'"
    exit "${exit_code}"
}

cleanup() {
    # Здесь можно добавить снятие LVM-снапшотов, остановку временных сервисов и т.п.
    log "Завершение работы скрипта."
}

# ---------------------------------------------------------------------------
# 1. Загрузка конфигурации (вынесена из тела скрипта)
# ---------------------------------------------------------------------------
CONFIG_FILE="${CONFIG_FILE:-/etc/dlh/raw.env}"
if [[ -f "$CONFIG_FILE" ]]; then
    # shellcheck source=/dev/null
    source "$CONFIG_FILE"
fi

# Значения по умолчанию — переопределяются через переменные окружения
PG_VERSION="${PG_VERSION:-17}"
PG_SERVICE="${PG_SERVICE:-postgresql-${PG_VERSION}}"
PG_DB="${PG_DB:-dlh_db}"
PG_USER="${PG_USER:-postgres}"

RAW_YEAR="${RAW_YEAR:-2026}"
RAW_QUARTERS="${RAW_QUARTERS:-2 3 4}"      # кварталы для создания в этом прогоне
TABLE_NAME="${TABLE_NAME:-table_1}"
RAW_SCHEMA="${RAW_SCHEMA:-raw}"
RAW_TABLE="${RAW_SCHEMA}.${TABLE_NAME}"

VG_NAME="${VG_NAME:-vg_data}"
LV_SIZE="${LV_SIZE:-800G}"
LV_PREFIX="${LV_PREFIX:-lv_raw}"
MOUNT_BASE="${MOUNT_BASE:-/mnt}"
TS_PREFIX="${TS_PREFIX:-ts_raw}"

LOG_DIR="${LOG_DIR:-/var/log/dlh/scripts}"
LOG_FILE="${LOG_FILE:-${LOG_DIR}/create_raw_$(date +%Y%m%d_%H%M%S).log}"

# Роли (least privilege)
APP_USER="${APP_USER:-dlh_user}"           # роль только для чтения/вставки
MAINT_USER="${MAINT_USER:-dlh_maint}"      # роль для обслуживания партиций

# pgBackRest (интеграция, не обязательна для запуска)
PGBACKREST_STANZA="${PGBACKREST_STANZA:-dlh}"

# ---------------------------------------------------------------------------
# 2. Базовые функции
# ---------------------------------------------------------------------------
mkdir -p "$LOG_DIR"

log() {
    local msg
    msg="$(date '+%Y-%m-%d %H:%M:%S') - $1"
    echo "$msg" | tee -a "$LOG_FILE" >&2
}

die() {
    log "ФАТАЛЬНАЯ ОШИБКА: $1"
    exit 1
}

require_root() {
    [[ $EUID -eq 0 ]] || die "Скрипт должен быть запущен от root (sudo)."
}

# ---------------------------------------------------------------------------
# 3. Универсальный запуск SQL (безопасно, атомарно, без sed)
# ---------------------------------------------------------------------------
run_sql() {
    local sql="$1"
    local -a psql_vars=("${@:2}")

    log "Выполняется SQL-скрипт (${#sql} байт, переменных: ${#psql_vars[@]})."

    if printf '%s\n' "$sql" |
        sudo -u "$PG_USER" -- psql \
            -X \
            -d "$PG_DB" \
            -v ON_ERROR_STOP=1 \
            -1 \
            "${psql_vars[@]}" \
            -f - >/dev/null; then
        return 0
    else
        local rc=$?
        log "Ошибка выполнения SQL (rc=${rc})."
        return "${rc}"
    fi
}

# ---------------------------------------------------------------------------
# 4. Работа с LVM / ФС (идемпотентно)
# ---------------------------------------------------------------------------
ensure_pv() {
    local dev="$1"
    if ! pvs "$dev" &>/dev/null; then
        log "Создание физического тома ${dev}."
        pvcreate "$dev" || die "pvcreate ${dev} не удался."
    else
        log "PV ${dev} уже существует."
    fi
}

ensure_vg() {
    local vg="$1" dev="$2"
    if ! vgs "$vg" &>/dev/null; then
        log "Создание VG ${vg} на ${dev}."
        vgcreate "$vg" "$dev" || die "vgcreate ${vg} не удался."
    else
        log "VG ${vg} уже существует."
    fi
}

ensure_lv() {
    local vg="$1" lv="$2" size="$3"
    local path="/dev/${vg}/${lv}"
    if ! lvs "$path" &>/dev/null; then
        log "Создание LV ${lv} размером ${size} в VG ${vg}."
        lvcreate -L "$size" -n "$lv" "$vg" || die "lvcreate ${lv} не удался."
    else
        log "LV ${lv} уже существует."
    fi
}

ensure_xfs() {
    local dev="$1"
    local fstype
    fstype="$(blkid -o value -s TYPE "$dev" 2>/dev/null || true)"
    if [[ "$fstype" == "xfs" ]]; then
        log "ФС XFS на ${dev} уже создана — форматирование пропущено."
    else
        log "Форматирование ${dev} в XFS."
        mkfs.xfs -f "$dev" || die "mkfs.xfs ${dev} не удался."
    fi
}

ensure_mount() {
    local dev="$1" mp="$2"
    mkdir -p "$mp"

    if mountpoint -q "$mp"; then
        local actual
        actual="$(findmnt -no SOURCE -T "$mp" | tr -d '[:space:]')"
        [[ "$actual" == "$dev" ]] || die "Каталог ${mp} смонтирован с ${actual}, а ожидался ${dev}."
        log "Каталог ${mp} уже смонтирован корректно (${dev})."
    else
        log "Монтирование ${dev} → ${mp}."
        mount "$dev" "$mp" || die "mount ${dev} ${mp} не удался."
    fi

    # fstab: добавляем только если записи ещё нет
    if ! grep -qsF "${dev} ${mp}" /etc/fstab; then
        printf '%s %s xfs defaults,nofail 0 0\n' "$dev" "$mp" >> /etc/fstab
        log "Запись в /etc/fstab добавлена для ${mp}."
        systemctl daemon-reload || log "Предупреждение: systemctl daemon-reload завершился с ошибкой."
    else
        log "Запись в /etc/fstab для ${mp} уже существует."
    fi

    chown postgres:postgres "$mp" || die "chown ${mp} не удался."
    chmod 700 "$mp" || die "chmod ${mp} не удался."
}

# ---------------------------------------------------------------------------
# 5. Подготовка схемы и ролей (least privilege)
# ---------------------------------------------------------------------------
ensure_roles() {
    run_sql "
DO \$\$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${APP_USER}') THEN
        CREATE ROLE \"${APP_USER}\" LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE;
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${MAINT_USER}') THEN
        CREATE ROLE \"${MAINT_USER}\" LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE;
    END IF;
END
\$\$;
"
}

ensure_schema_and_grants() {
    run_sql "
-- Схема
CREATE SCHEMA IF NOT EXISTS ${RAW_SCHEMA} AUTHORIZATION postgres;

-- Снимаем широкие права с PUBLIC (принцип наименьших привилегий)
REVOKE ALL ON SCHEMA ${RAW_SCHEMA} FROM PUBLIC;

-- Прикладная роль: только чтение и вставка в RAW
GRANT USAGE ON SCHEMA ${RAW_SCHEMA} TO ${APP_USER};
GRANT SELECT, INSERT ON ${RAW_TABLE} TO ${APP_USER};

-- Роль обслуживания: может управлять партициями
GRANT USAGE, CREATE ON SCHEMA ${RAW_SCHEMA} TO ${MAINT_USER};
GRANT SELECT, INSERT, UPDATE, DELETE ON ${RAW_TABLE} TO ${MAINT_USER};
ALTER DEFAULT PRIVILEGES IN SCHEMA ${RAW_SCHEMA}
    GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${MAINT_USER};
"
}

# ---------------------------------------------------------------------------
# 6. Партиционирование (RANGE по dt — правильный выбор для time-series)
# ---------------------------------------------------------------------------
ensure_parent_table() {
    run_sql "
CREATE TABLE IF NOT EXISTS ${RAW_TABLE} (
    id          VARCHAR(16)   NOT NULL,
    dt          TIMESTAMP     NOT NULL,
    json_data   JSONB,
    col4        TEXT,
    col5        TEXT
) PARTITION BY RANGE (dt);
"
}

ensure_quarter_partition() {
    local quarter="$1"
    local start_date end_date
    local table="${RAW_TABLE}_q${quarter}_${RAW_YEAR}"
    local tablespace="${TS_PREFIX}_q${quarter}_${RAW_YEAR}"

    case "$quarter" in
        1) start_date="${RAW_YEAR}-01-01"; end_date="${RAW_YEAR}-04-01" ;;
        2) start_date="${RAW_YEAR}-04-01"; end_date="${RAW_YEAR}-07-01" ;;
        3) start_date="${RAW_YEAR}-07-01"; end_date="${RAW_YEAR}-10-01" ;;
        4) start_date="${RAW_YEAR}-10-01"; end_date="$((RAW_YEAR + 1))-01-01" ;;
        *) die "Некорректный квартал: ${quarter}" ;;
    esac

    log "Партиция q${quarter}_${RAW_YEAR}: ${start_date} → ${end_date}, tablespace=${tablespace}."

    # Табличное пространство (идемпотентно через проверку в pg_tablespace)
    run_sql "
DO \$\$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_tablespace WHERE spcname = '${tablespace}') THEN
        EXECUTE format(
            'CREATE TABLESPACE %I OWNER postgres LOCATION %L',
            '${tablespace}',
            '${MOUNT_BASE}/raw_q${quarter}_${RAW_YEAR}'
        );
    END IF;
END
\$\$;
"

    # Партиция
    run_sql "
CREATE TABLE IF NOT EXISTS ${table}
    PARTITION OF ${RAW_TABLE}
    FOR VALUES FROM ('${start_date}') TO ('${end_date}')
    TABLESPACE ${tablespace};
"
}

# ---------------------------------------------------------------------------
# 7. Интеграция с pgBackRest (не блокирующая, но важная)
# ---------------------------------------------------------------------------
check_pgbackrest() {
    if ! command -v pgbackrest &>/dev/null; then
        log "pgBackRest не установлен — пропускаем проверку резервного копирования."
        return 0
    fi

    log "Проверка stanza pgBackRest: ${PGBACKREST_STANZA}."
    if sudo -u postgres -- pgbackrest --stanza="$PGBACKREST_STANZA" check &>>"$LOG_FILE"; then
        log "pgBackRest stanza '${PGBACKREST_STANZA}' в порядке."
    else
        log "ВНИМАНИЕ: pgBackRest stanza '${PGBACKREST_STANZA}' не прошла проверку. Продолжаем, но резервное копирование требует внимания."
    fi
}

# ---------------------------------------------------------------------------
# 8. Основной сценарий
# ---------------------------------------------------------------------------
main() {
    require_root
    log "=== Начало создания слоя RAW (год=${RAW_YEAR}, кварталы=${RAW_QUARTERS}) ==="

    # 8.1. Остановка PostgreSQL
    if systemctl is-active --quiet "$PG_SERVICE"; then
        log "Остановка службы ${PG_SERVICE}."
        systemctl stop "$PG_SERVICE" || die "Не удалось остановить ${PG_SERVICE}."
    else
        log "Служба ${PG_SERVICE} уже остановлена."
    fi

    # 8.2. LVM + ФС + монтирование (идемпотентно)
    ensure_pv /dev/sdb2
    ensure_vg "$VG_NAME" /dev/sdb2

    for q in $RAW_QUARTERS; do
        local lv="${LV_PREFIX}_q${q}_${RAW_YEAR}"
        local mp="${MOUNT_BASE}/raw_q${q}_${RAW_YEAR}"
        ensure_lv "$VG_NAME" "$lv" "$LV_SIZE"
        ensure_xfs "/dev/${VG_NAME}/${lv}"
        ensure_mount "/dev/${VG_NAME}/${lv}" "$mp"
    done

    # 8.3. Запуск PostgreSQL
    log "Запуск службы ${PG_SERVICE}."
    systemctl start "$PG_SERVICE" || die "Не удалось запустить ${PG_SERVICE}."

    # Ждём готовности
    for _ in {1..30}; do
        sudo -u "$PG_USER" -- pg_isready -q && break
        sleep 1
    done

    # 8.4. Роли и схема
    ensure_roles
    ensure_parent_table

    # 8.5. Партиции (цикл по кварталам)
    for q in $RAW_QUARTERS; do
        ensure_quarter_partition "$q"
    done

    # 8.6. Гранты (после создания родительской таблицы)
    ensure_schema_and_grants

    # 8.7. Комментарии
    run_sql "
COMMENT ON TABLE ${RAW_TABLE} IS
    'Слой RAW: исходные данные Table1 из MS SQL. Партиционирование RANGE по dt (кварталы).';
COMMENT ON COLUMN ${RAW_TABLE}.id IS 'Исходное поле RowID.';
COMMENT ON COLUMN ${RAW_TABLE}.dt IS 'Дата/время создания записи в источнике; ключ партиционирования.';
COMMENT ON COLUMN ${RAW_TABLE}.json_data IS 'Неструктурированные данные (JSONB).';
COMMENT ON COLUMN ${RAW_TABLE}.col4 IS 'Дополнительное поле 4.';
COMMENT ON COLUMN ${RAW_TABLE}.col5 IS 'Дополнительное поле 5.';
"

    # 8.8. Проверка pgBackRest
    check_pgbackrest

    log "=== Слой RAW успешно создан (год=${RAW_YEAR}). ==="
}

main "$@"

Скрипт файла drop_raw.sh для удаления слоя RAW¶

Файл drop_raw.sh — предназначен для автоматизации удаления слоя RAW. Это полезно, как и в случае использования файла create_raw.sh, при первичном создании слоя и отладки его функционирования. Обязательно сделать этот файл исполняемым с пмощью команды: sudo chmod +x /opt/dlh/scripts/drop_raw.sh. После этого его можно будет использовать с помощью команды ./drop_raw.sh --remove-lv.

#!/bin/bash
#!/usr/bin/env bash
# ============================================================================
# drop_raw.sh — полное удаление слоя RAW (PostgreSQL 17, РЕД ОС)
#
# Принципы:
#   1. Идемпотентность: повторный запуск безопасен.
#   2. Атомарность SQL: каждая операция — одна транзакция.
#   3. Разрушение — только по явному согласию (--yes-i-really-mean-it).
#   4. Наблюдаемость: предполётный отчёт + логирование каждого шага.
#   5. Безопасность ФС: никаких rm -rf; umount → rmdir → lvremove.
#   6. Готовность к CI/CD: --dry-run, вынесенная конфигурация.
#
# Использование:
#   sudo ./drop_raw.sh                          # только БД-объекты
#   sudo ./drop_raw.sh --dry-run                # показать план, ничего не менять
#   sudo ./drop_raw.sh --remove-lv \
#        --yes-i-really-mean-it                 # + снести LVM/ФС
#   sudo ./drop_raw.sh --remove-lv --remove-pv \
#        --yes-i-really-mean-it                 # + снять метку PV с /dev/sdb2
# ============================================================================

set -Eeuo pipefail

trap 'on_error $? $LINENO "${BASH_COMMAND}"' ERR
trap 'cleanup' EXIT

on_error() {
    local exit_code=$1 line_no=$2 failed_cmd=$3
    log "КРИТИЧЕСКАЯ ОШИБКА: код=${exit_code}, строка=${line_no}, команда='${failed_cmd}'"
    exit "${exit_code}"
}

cleanup() {
    log "Завершение работы drop_raw.sh."
}

# ---------------------------------------------------------------------------
# 1. Конфигурация (вынесена, переопределяется через окружение)
# ---------------------------------------------------------------------------
CONFIG_FILE="${CONFIG_FILE:-/etc/dlh/raw.env}"
if [[ -f "$CONFIG_FILE" ]]; then
    # shellcheck source=/dev/null
    source "$CONFIG_FILE"
fi

PG_VERSION="${PG_VERSION:-17}"
PG_SERVICE="${PG_SERVICE:-postgresql-${PG_VERSION}}"
PG_DB="${PG_DB:-dlh_db}"
PG_USER="${PG_USER:-postgres}"
PG_DATA="${PG_DATA:-/var/lib/pgsql/${PG_VERSION}/data}"

RAW_YEAR="${RAW_YEAR:-2026}"
RAW_SCHEMA="${RAW_SCHEMA:-raw}"
TABLE_NAME="${TABLE_NAME:-table_1}"
RAW_TABLE="${RAW_SCHEMA}.${TABLE_NAME}"

VG_NAME="${VG_NAME:-vg_data}"
LV_PREFIX="${LV_PREFIX:-lv_raw}"
MOUNT_BASE="${MOUNT_BASE:-/mnt}"
TS_PREFIX="${TS_PREFIX:-ts_raw}"

LOG_DIR="${LOG_DIR:-/var/log/dlh/scripts}"
LOG_FILE="${LOG_FILE:-${LOG_DIR}/drop_raw_$(date +%Y%m%d_%H%M%S).log}"

DROP_ROLES="${DROP_ROLES:-false}"
APP_USER="${APP_USER:-dlh_user}"
MAINT_USER="${MAINT_USER:-dlh_maint}"

# ---------------------------------------------------------------------------
# 2. Аргументы
# ---------------------------------------------------------------------------
REMOVE_LV=false
REMOVE_PV=false
DRY_RUN=false
CONFIRM=false
SCHEMA_CASCADE=true

usage() {
    cat <<EOF
Использование: $0 [опции]

Опции:
  --remove-lv               Удалить логические тома, VG и записи fstab.
                            ТРЕБУЕТ --yes-i-really-mean-it.
  --remove-pv               Дополнительно снять метку PV с /dev/sdb2.
                            ТРЕБУЕТ --remove-lv и --yes-i-really-mean-it.
  --yes-i-really-mean-it    Подтверждение деструктивных операций.
  --dry-run                 Показать план, ничего не выполнять.
  --no-schema-cascade       DROP SCHEMA raw без CASCADE (RESTRICT).
                            Скрипт упадёт, если в схеме есть посторонние объекты.
  -h, --help                Эта справка.

Конфигурация: $CONFIG_FILE (или CONFIG_FILE=... в окружении).
EOF
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        --remove-lv)             REMOVE_LV=true ;;
        --remove-pv)             REMOVE_PV=true ;;
        --yes-i-really-mean-it)  CONFIRM=true ;;
        --dry-run)               DRY_RUN=true ;;
        --no-schema-cascade)     SCHEMA_CASCADE=false ;;
        -h|--help)               usage; exit 0 ;;
        *)                       echo "Неизвестный аргумент: $1" >&2; usage; exit 2 ;;
    esac
    shift
done

# Валидация связки флагов
if $REMOVE_LV && ! $CONFIRM; then
    echo "ОШИБКА: --remove-lv уничтожает данные. Добавьте --yes-i-really-mean-it." >&2
    exit 2
fi
if $REMOVE_PV && ! $REMOVE_LV; then
    echo "ОШИБКА: --remove-pv требует --remove-lv." >&2
    exit 2
fi
if $REMOVE_PV && ! $CONFIRM; then
    echo "ОШИБКА: --remove-pv уничтожает метку PV. Добавьте --yes-i-really-mean-it." >&2
    exit 2
fi

# ---------------------------------------------------------------------------
# 3. Базовые функции
# ---------------------------------------------------------------------------
mkdir -p "$LOG_DIR"

log() {
    local msg
    msg="$(date '+%Y-%m-%d %H:%M:%S') - $1"
    echo "$msg" | tee -a "$LOG_FILE" >&2
}

die() {
    log "ФАТАЛЬНАЯ ОШИБКА: $1"
    exit 1
}

require_root() {
    [[ $EUID -eq 0 ]] || die "Скрипт должен быть запущен от root (sudo)."
}

# Унифицированный исполнитель shell-команд с поддержкой dry-run
run_cmd() {
    local desc="$1"; shift
    if $DRY_RUN; then
        log "[DRY-RUN] ${desc}"
        return 0
    fi
    log "${desc}"
    "$@"
}

# Унифицированный исполнитель SQL (атомарно, без sed)
run_sql() {
    local sql="$1"
    local -a psql_vars=("${@:2}")

    if $DRY_RUN; then
        log "[DRY-RUN] SQL: $(printf '%s' "$sql" | tr '\n' ' ' | head -c 400)"
        return 0
    fi

    log "Выполняется SQL (${#sql} байт)."
    if printf '%s\n' "$sql" |
        sudo -u "$PG_USER" -- psql \
            -X -q -d "$PG_DB" \
            -v ON_ERROR_STOP=1 -1 \
            "${psql_vars[@]}" \
            -f - >/dev/null; then
        return 0
    else
        local rc=$?
        log "Ошибка выполнения SQL (rc=${rc})."
        return "${rc}"
    fi
}

# Прямой (неатомарный, не-SELECT) запрос — только для чтения каталога
psql_at() {
    sudo -u "$PG_USER" -- psql -X -At -d "$PG_DB" -v ON_ERROR_STOP=1 -c "$1" 2>/dev/null || true
}

# ---------------------------------------------------------------------------
# 4. Управление сервисом PostgreSQL
# ---------------------------------------------------------------------------
pg_is_running() {
    systemctl is-active --quiet "$PG_SERVICE"
}

start_pg_if_needed() {
    if pg_is_running; then
        log "Служба ${PG_SERVICE} уже запущена."
        return 0
    fi
    run_cmd "Запуск службы ${PG_SERVICE}" systemctl start "$PG_SERVICE" || die "Не удалось запустить ${PG_SERVICE}."
    for _ in {1..30}; do
        sudo -u "$PG_USER" -- pg_isready -q && { log "PostgreSQL готов."; return 0; }
        sleep 1
    done
    die "PostgreSQL не стал готов за 30 секунд."
}

stop_pg_if_running() {
    if ! pg_is_running; then
        log "Служба ${PG_SERVICE} уже остановлена."
        return 0
    fi
    run_cmd "Остановка службы ${PG_SERVICE}" systemctl stop "$PG_SERVICE" || die "Не удалось остановить ${PG_SERVICE}."
    for _ in {1..30}; do
        pg_is_running || { log "PostgreSQL остановлен."; return 0; }
        sleep 1
    done
    die "PostgreSQL не остановился за 30 секунд."
}

# ---------------------------------------------------------------------------
# 5. Предполётный отчёт
# ---------------------------------------------------------------------------
preflight_report() {
    log "=== Предполётный отчёт ==="
    log "Режим: $($DRY_RUN && echo 'DRY-RUN' || echo 'РЕАЛЬНЫЕ ДЕЙСТВИЯ')"
    log "БД: ${PG_DB} | схема: ${RAW_SCHEMA} | таблица: ${RAW_TABLE}"
    log "Год: ${RAW_YEAR} | VG: ${VG_NAME} | базовый путь: ${MOUNT_BASE}"
    log "Удалять LV: ${REMOVE_LV} | Удалять PV: ${REMOVE_PV}"

    # Что фактически есть в схеме
    local objs
    objs="$(psql_at "SELECT c.relkind || ': ' || c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = '${RAW_SCHEMA}' ORDER BY 1;")"
    if [[ -n "$objs" ]]; then
        log "Объекты в схеме ${RAW_SCHEMA}:"
        while IFS= read -r line; do log "  - ${line}"; done <<< "$objs"
    else
        log "Схема ${RAW_SCHEMA} отсутствует или пуста."
    fi

    # Tablespaces, которые будут затронуты
    local tss
    tss="$(psql_at "SELECT spcname FROM pg_tablespace WHERE spcname LIKE '${TS_PREFIX}\\_q%\\_${RAW_YEAR}' ORDER BY 1;")"
    if [[ -n "$tss" ]]; then
        log "Tablespace-кандидаты на DROP:"
        while IFS= read -r ts; do
            local refs
            refs="$(psql_at "SELECT count(*) FROM pg_class c JOIN pg_tablespace t ON t.oid = c.reltablespace WHERE t.spcname = '${ts}';")"
            log "  - ${ts} (объектов внутри: ${refs})"
        done <<< "$tss"
    else
        log "Tablespace-кандидатов не найдено."
    fi

    if $REMOVE_LV; then
        log "LVM-кандидаты:"
        lvs --noheadings -o lv_name,vg_name,lv_size -S "vg_name=${VG_NAME}" 2>/dev/null |
            awk -v p="$LV_PREFIX" '$1 ~ "^"p"_" {print "  - " $2 "/" $1 " (" $3 ")"}' |
            tee -a "$LOG_FILE" >&2 || log "  (VG ${VG_NAME} не существует)"
    fi
}

# ---------------------------------------------------------------------------
# 6. БД-фаза
# ---------------------------------------------------------------------------
drop_schema_phase() {
    local exists
    exists="$(psql_at "SELECT 1 FROM pg_namespace WHERE nspname = '${RAW_SCHEMA}';")"
    if [[ "$exists" != "1" ]]; then
        log "Схема ${RAW_SCHEMA} отсутствует — пропускаем."
        return 0
    fi

    # Считаем объекты, чтобы предупредить, если CASCADE снесёт больше ожидаемого
    local n
    n="$(psql_at "SELECT count(*) FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = '${RAW_SCHEMA}';")"
    log "В схеме ${RAW_SCHEMA} объектов: ${n}."

    if $SCHEMA_CASCADE; then
        run_cmd "DROP SCHEMA ${RAW_SCHEMA} CASCADE" \
            bash -c "printf 'DROP SCHEMA IF EXISTS ${RAW_SCHEMA} CASCADE;\n' | sudo -u ${PG_USER} -- psql -X -q -d ${PG_DB} -v ON_ERROR_STOP=1 -1 -f -" \
            || die "DROP SCHEMA ${RAW_SCHEMA} CASCADE не удался."
    else
        run_cmd "DROP SCHEMA ${RAW_SCHEMA} (RESTRICT)" \
            bash -c "printf 'DROP SCHEMA IF EXISTS ${RAW_SCHEMA};\n' | sudo -u ${PG_USER} -- psql -X -q -d ${PG_DB} -v ON_ERROR_STOP=1 -1 -f -" \
            || die "DROP SCHEMA ${RAW_SCHEMA} не удался (возможно, схема не пуста; используйте --no-schema-cascade=false)."
    fi
}

drop_tablespaces_phase() {
    local tss
    tss="$(psql_at "SELECT spcname FROM pg_tablespace WHERE spcname LIKE '${TS_PREFIX}\\_q%\\_${RAW_YEAR}' ORDER BY 1;")"

    if [[ -z "$tss" ]]; then
        log "Tablespace-объектов для удаления нет."
        return 0
    fi

    while IFS= read -r ts; do
        [[ -z "$ts" ]] && continue
        local refs
        refs="$(psql_at "SELECT count(*) FROM pg_class c JOIN pg_tablespace t ON t.oid = c.reltablespace WHERE t.spcname = '${ts}';")"
        if [[ "${refs:-0}" != "0" ]]; then
            log "ВНИМАНИЕ: tablespace ${ts} содержит ${refs} объектов. DROP невозможен без их удаления. Пропуск."
            continue
        fi
        if $DRY_RUN; then
            log "[DRY-RUN] DROP TABLESPACE IF EXISTS \"${ts}\""
            continue
        fi
        if run_sql "DROP TABLESPACE IF EXISTS \"${ts}\";"; then
            log "Tablespace ${ts} удалён из каталога PostgreSQL."
        else
            log "ВНИМАНИЕ: DROP TABLESPACE ${ts} завершился ошибкой."
        fi
    done <<< "$tss"
}

drop_roles_phase() {
    if ! $DROP_ROLES; then
        log "Удаление ролей отключено (DROP_ROLES=${DROP_ROLES})."
        return 0
    fi
    for role in "$APP_USER" "$MAINT_USER"; do
        local exists
        exists="$(psql_at "SELECT 1 FROM pg_roles WHERE rolname = '${role}';")"
        if [[ "$exists" == "1" ]]; then
            if run_sql "REASSIGN OWNED BY \"${role}\" TO postgres; DROP OWNED BY \"${role}\"; DROP ROLE IF EXISTS \"${role}\";"; then
                log "Роль ${role} удалена."
            else
                log "ВНИМАНИЕ: не удалось удалить роль ${role} (возможно, от неё зависят объекты вне dlh_db)."
            fi
        else
            log "Роль ${role} уже отсутствует."
        fi
    done
}

# ---------------------------------------------------------------------------
# 7. LVM-фаза (строго по --remove-lv + --yes-i-really-mean-it)
# ---------------------------------------------------------------------------
remove_fstab_entry() {
    local mp="$1"
    if ! grep -qsF " ${mp} " /etc/fstab; then
        log "Запись для ${mp} в /etc/fstab отсутствует."
        return 0
    fi
    if $DRY_RUN; then
        log "[DRY-RUN] Удаление записи ${mp} из /etc/fstab"
        return 0
    fi
    cp -a /etc/fstab /etc/fstab.dlh.bak
    awk -v mp="$mp" '$2 != mp' /etc/fstab.dlh.bak > /etc/fstab.new
    mv /etc/fstab.new /etc/fstab
    log "Запись ${mp} удалена из /etc/fstab (бэкап: /etc/fstab.dlh.bak)."
}

cleanup_one_lv() {
    local lv="$1"
    local mp="${MOUNT_BASE}/raw_${lv#${LV_PREFIX}_}"    # lv_raw_q2_2026 → raw_q2_2026
    local lv_path="/dev/${VG_NAME}/${lv}"

    log "--- Очистка ${lv} (${mp}) ---"

    # 1. umount (если смонтирован)
    if mountpoint -q "$mp"; then
        run_cmd "Размонтирование ${mp}" umount "$mp" || die "umount ${mp} не удался."
    else
        log "${mp} не смонтирован."
    fi

    # 2. fstab
    remove_fstab_entry "$mp"

    # 3. rmdir (только пустой каталог; никаких rm -rf)
    if [[ -d "$mp" ]]; then
        local content
        content="$(ls -A "$mp" 2>/dev/null || true)"
        if [[ -n "$content" ]]; then
            log "ВНИМАНИЕ: каталог ${mp} не пуст после umount:"
            log "$(printf '%s\n' "$content" | head -10)"
            die "Отказ удалять непустой каталог ${mp}. Проверьте вручную."
        fi
        run_cmd "rmdir ${mp}" rmdir "$mp" || die "rmdir ${mp} не удался."
    else
        log "Каталог ${mp} отсутствует."
    fi

    # 4. lvremove
    if lvs "$lv_path" &>/dev/null; then
        run_cmd "lvremove -y ${lv_path}" lvremove -y "$lv_path" || die "lvremove ${lv_path} не удался."
    else
        log "LV ${lv_path} отсутствует."
    fi
}

cleanup_vg() {
    if ! vgs "$VG_NAME" &>/dev/null; then
        log "VG ${VG_NAME} отсутствует."
        return 0
    fi

    local remaining
    remaining="$(lvs --noheadings -o lv_name "$VG_NAME" 2>/dev/null | tr -d ' ' | grep -v '^$' || true)"
    if [[ -n "$remaining" ]]; then
        log "ВНИМАНИЕ: в VG ${VG_NAME} остались LV:"
        log "$remaining"
        die "VG ${VG_NAME} не пуста — vgremove отменён."
    fi

    run_cmd "vgremove -y ${VG_NAME}" vgremove -y "$VG_NAME" || die "vgremove ${VG_NAME} не удался."
}

cleanup_pv() {
    # Перечислим PV, входившие в VG, ДО vgremove (вызывается после cleanup_vg)
    local pvs_in_vg="${1:-}"
    [[ -z "$pvs_in_vg" ]] && { log "Список PV пуст — pvremove не требуется."; return 0; }

    while IFS= read -r pv; do
        [[ -z "$pv" ]] && continue

        # Безопасность: не снимать метку, если PV входит в другую VG
        local other_vg
        other_vg="$(pvs --noheadings -o vg_name "$pv" 2>/dev/null | tr -d ' ' || true)"
        if [[ -n "$other_vg" ]]; then
            log "Пропуск pvremove ${pv}: PV принадлежит VG '${other_vg}'."
            continue
        fi

        # Безопасность: pvremove только с явного разрешения и только для известных устройств
        if [[ "$pv" != "/dev/sdb2" ]]; then
            log "Пропуск pvremove ${pv}: не входит в белый список (/dev/sdb2)."
            log "Если уверены — выполните вручную: pvremove ${pv}"
            continue
        fi

        run_cmd "pvremove ${pv}" pvremove "$pv" || log "ВНИМАНИЕ: pvremove ${pv} не удался."
    done <<< "$pvs_in_vg"
}

cleanup_lvm_phase() {
    log "=== LVM-фаза: удаление LV/VG/PV ==="

    if ! vgs "$VG_NAME" &>/dev/null; then
        log "VG ${VG_NAME} отсутствует — LVM-фаза не требуется."
        return 0
    fi

    # Зафиксируем PV до vgremove (потом pvs уже не покажет их в VG)
    local pvs_in_vg
    pvs_in_vg="$(pvs --noheadings -o pv_name -S "vg_name=${VG_NAME}" 2>/dev/null | tr -d ' ' || true)"

    # Список LV, которые надо снять
    local lvs_to_remove
    lvs_to_remove="$(lvs --noheadings -o lv_name -S "vg_name=${VG_NAME}" 2>/dev/null | tr -d ' ' | grep -E "^${LV_PREFIX}_" || true)"

    if [[ -z "$lvs_to_remove" ]]; then
        log "В VG ${VG_NAME} нет LV с префиксом ${LV_PREFIX} — очистка LV пропущена."
    else
        while IFS= read -r lv; do
            [[ -z "$lv" ]] && continue
            cleanup_one_lv "$lv"
        done <<< "$lvs_to_remove"
    fi

    cleanup_vg

    if $REMOVE_PV; then
        cleanup_pv "$pvs_in_vg"
    else
        log "pvremove не запрошен (--remove-pv отсутствует)."
    fi
}

# ---------------------------------------------------------------------------
# 8. Пост-проверки
# ---------------------------------------------------------------------------
post_checks() {
    log "=== Пост-проверки ==="

    # Осиротевшие pg_tblspc (если DROP TABLESPACE не удался, но LV уже размонтированы)
    if [[ -d "${PG_DATA}/pg_tblspc" ]]; then
        local orphans
        orphans="$(find "${PG_DATA}/pg_tblspc" -maxdepth 1 -type l -xtype l 2>/dev/null || true)"
        if [[ -n "$orphans" ]]; then
            log "ВНИМАНИЕ: обнаружены битые симлинки в ${PG_DATA}/pg_tblspc:"
            log "$orphans"
            log "PostgreSQL может не стартовать. Удалите их вручную или повторите DROP TABLESPACE."
        fi
    fi

    log "Итоговое состояние LVM (если применимо):"
    { pvs 2>/dev/null; vgs 2>/dev/null; lvs 2>/dev/null; } | tee -a "$LOG_FILE" >&2 || true

    log "Свободное место:"
    df -h 2>/dev/null | grep -E "(${MOUNT_BASE}/raw_|Filesystem|Файловая)" | tee -a "$LOG_FILE" >&2 || true
}

# ---------------------------------------------------------------------------
# 9. Main
# ---------------------------------------------------------------------------
main() {
    require_root
    log "=== Начало drop_raw.sh (режим: $($DRY_RUN && echo DRY-RUN || echo REAL)) ==="

    start_pg_if_needed
    preflight_report

    # БД-фаза
    log "--- БД-фаза ---"
    drop_schema_phase
    drop_tablespaces_phase
    drop_roles_phase

    # LVM-фаза
    if $REMOVE_LV; then
        log "--- LVM-фаза ---"
        stop_pg_if_running
        cleanup_lvm_phase
    else
        log "LVM-фаза пропущена (--remove-lv не передан)."
    fi

    # Возврат сервиса
    start_pg_if_needed

    post_checks

    if $DRY_RUN; then
        log "=== DRY-RUN завершён. Реальных изменений не производилось. ==="
    else
        log "=== drop_raw.sh завершён успешно. ==="
    fi
}

main "$@"

AI-Ready платформа Bash Data Lakehouse ELT PostgreSQL RAW гибридное хранилище данных корпоративная информационная система практика

Предыдущая статьяCоздание минимального жизнеспособного гибридного хранилища данныхCоздание минимального жизнеспособного гибридного хранилища данныхСледующая статья Мультиагентная система техподдержки продукта

Рубрики

Метки

abc abcd AI-Ready платформа Bash Data Lakehouse ELT excel ms sql n8n PostgreSQL PowerShell Python RAG RAW sql STG tessa VBA xyz ИИ-агент анализ виртуальный помощник гибридное хранилище данных данные знания информационная система информация искусственный интеллект кластерный анализ компетенции корпоративная информационная система математика модель предоставления прав мудрость о проекте оптимизация практика программное обеспечение ролевая модель сеть; теория теория вероятностей тесса юмор языки программирования

Политика конфиденциальности

Продолжая использовать данный сайт вы подтверждаете свое согласие с условиями его политики конфиденциальности. Подробнее…




Администрация и владельцы данного информационного ресурса не несут ответственности за возможные последствия, связанные с использованием информации, размещенной на нем.


Все права защищены. При копировании материалов сайта обязательно указывать ссылку на © Microsegment.ru (2020-2026)