Здесь представлены файлы bash для автоматизации создания слоя сырых данных RAW с архивируемыми партициями на отдельных отмонтируемых сегментах диска минимального жизнеспособного (MVP) гибридного хранилища данных (Data Lakehouse) на основе PostgreSQL.
Скрипт файла 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 "$@"

