fase 3 (operacion): backups automaticos por instancia con descubrimiento dinamico, verificacion de integridad, retencion GFS 14/8/6, prueba de restauracion semanal, timers systemd y logrotate; panel CLI multi-instancia (admin_instancias.py) + doc de operacion y restauracion
This commit is contained in:
523
infra/ops/backup_instancias.sh
Executable file
523
infra/ops/backup_instancias.sh
Executable file
@@ -0,0 +1,523 @@
|
||||
#!/usr/bin/env bash
|
||||
# -*- coding: utf-8 -*-
|
||||
# backup_instancias.sh — Backups automáticos y verificados de instancias SKEEN.
|
||||
#
|
||||
# Descubre dinámicamente las instancias a partir de las unidades systemd
|
||||
# `odoo-*.service`, lee de cada una su `-c <conf>` (ExecStart) y de ahí la
|
||||
# base de datos, el puerto HTTP, el logfile y el data_dir. Para cada instancia:
|
||||
#
|
||||
# 1. pg_dump -Fc a /tmp y lo mueve a <BACKUP_ROOT>/<slug>/
|
||||
# (<db>_<YYYYmmdd_HHMMSS>.dump, chmod 600 root:root).
|
||||
# 2. Verifica integridad con `pg_restore --list`; si falla, borra y reporta.
|
||||
# 3. Respalda la configuración del host en un tar.gz pequeño (odoo.conf, unit
|
||||
# systemd, vhost nginx y directorios de marca del frontend).
|
||||
# 4. (--verificar) Restaura a una DB temporal <db>_verify_<fecha>, compara
|
||||
# conteos de tablas clave y la borra.
|
||||
# 5. Rota según la política de retención (14 diarios / 8 semanales / 6
|
||||
# mensuales), sin borrar el mensual vigente ni el último backup bueno.
|
||||
#
|
||||
# Uso:
|
||||
# backup_instancias.sh [--instancia <slug>] [--solo-db] [--sin-config]
|
||||
# [--verificar] [--dry-run] [--backup-root <dir>]
|
||||
#
|
||||
# Exit code: 0 si todo OK; 1 si alguna instancia falló; 3 si ya había otro
|
||||
# backup en curso (lockfile).
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="${SKEEN_BACKUP_ENV:-$SCRIPT_DIR/backup.env}"
|
||||
|
||||
# ------------------------------------------------------------- valores por defecto
|
||||
BACKUP_ROOT=/root/backups
|
||||
LOG_FILE=/var/log/skeen-backup.log
|
||||
LOCK_FILE=/var/lock/skeen-backup.lock
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=skeen
|
||||
DB_PASSWORD=
|
||||
PG_OS_USER=postgres
|
||||
VERIFY_MIN_FREE_FACTOR=2
|
||||
RETAIN_DAILY=14
|
||||
RETAIN_WEEKLY=8
|
||||
RETAIN_MONTHLY=6
|
||||
LOG_MAX_BYTES=5242880
|
||||
SUPABASE_BACKUP=0
|
||||
WACRM_BACKUP=0
|
||||
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- argumentos
|
||||
INSTANCIA=""
|
||||
SOLO_DB=0
|
||||
SIN_CONFIG=0
|
||||
VERIFICAR=0
|
||||
DRY_RUN=0
|
||||
|
||||
usage() {
|
||||
sed -n '3,33p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
||||
}
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--instancia) INSTANCIA="${2:-}"; shift 2 ;;
|
||||
--solo-db) SOLO_DB=1; SIN_CONFIG=1; shift ;;
|
||||
--sin-config) SIN_CONFIG=1; shift ;;
|
||||
--verificar) VERIFICAR=1; shift ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--backup-root) BACKUP_ROOT="${2:-}"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Opción desconocida: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
STAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
# ------------------------------------------------------------------- logging
|
||||
_log_raw() { printf '%s\n' "$*" >>"$LOG_FILE"; }
|
||||
|
||||
log() { local m="[$(date '+%F %T')] $*"; _log_raw "$m"; printf '%s\n' "$m"; }
|
||||
log_warn(){ local m="[$(date '+%F %T')] WARN $*"; _log_raw "$m"; printf '%s\n' "$m" >&2; }
|
||||
log_err() { local m="[$(date '+%F %T')] ERROR $*"; _log_raw "$m"; printf '%s\n' "$m" >&2; }
|
||||
log_ok() { local m="[$(date '+%F %T')] OK $*"; _log_raw "$m"; printf '%s\n' "$m"; }
|
||||
|
||||
log_truncate_if_needed() {
|
||||
[[ -f "$LOG_FILE" ]] || return 0
|
||||
local size
|
||||
size=$(stat -c %s "$LOG_FILE" 2>/dev/null || echo 0)
|
||||
if (( size > LOG_MAX_BYTES )); then
|
||||
mv -f "$LOG_FILE" "${LOG_FILE}.1"
|
||||
: >"$LOG_FILE"
|
||||
log_warn "log truncado por tamaño (>${LOG_MAX_BYTES} bytes); anterior en ${LOG_FILE}.1"
|
||||
fi
|
||||
}
|
||||
|
||||
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
|
||||
log_truncate_if_needed
|
||||
MODE=""; (( DRY_RUN )) && MODE=", dry-run"
|
||||
log "=== Inicio de backup (stamp=$STAMP${INSTANCIA:+, instancia=$INSTANCIA}$MODE) ==="
|
||||
|
||||
# --------------------------------------------------------------------- lock
|
||||
if ! exec 9>"$LOCK_FILE"; then
|
||||
log_err "no se pudo abrir el lock $LOCK_FILE"
|
||||
exit 1
|
||||
fi
|
||||
if ! flock -n 9; then
|
||||
log_err "ya hay otro backup en curso (lock $LOCK_FILE). Saliendo sin hacer nada."
|
||||
exit 3
|
||||
fi
|
||||
log "lock adquirido ($LOCK_FILE)"
|
||||
|
||||
# ------------------------------------------------------------- utilidades
|
||||
# Lee una clave de un odoo.conf (sección [options], formato `clave = valor`).
|
||||
conf_get() {
|
||||
local file="$1" key="$2"
|
||||
[[ -f "$file" ]] || return 1
|
||||
awk -v k="$key" '
|
||||
/^[[:space:]]*\[/ { next }
|
||||
/^[[:space:]]*[#;]/ { next }
|
||||
{
|
||||
line=$0
|
||||
sub(/^[[:space:]]+/, "", line)
|
||||
eq=index(line, "=")
|
||||
if (eq>0) {
|
||||
name=substr(line,1,eq-1); sub(/[[:space:]]+$/, "", name)
|
||||
if (name==k) { v=substr(line, eq+1); sub(/^[[:space:]]+/, "", v); sub(/[[:space:]]+$/, "", v); print v; exit }
|
||||
}
|
||||
}' "$file"
|
||||
}
|
||||
|
||||
human_bytes() {
|
||||
numfmt --to=iec --suffix=B "${1:-0}" 2>/dev/null || echo "${1:-0}B"
|
||||
}
|
||||
|
||||
# Lista las unidades odoo-*.service conocidas por systemd.
|
||||
list_units() {
|
||||
systemctl list-unit-files --type=service --no-legend --no-pager 2>/dev/null \
|
||||
| awk '$1 ~ /^odoo-.*\.service$/ { print $1 }'
|
||||
}
|
||||
|
||||
# Devuelve el vhost nginx de una instancia: por slug o por puerto.
|
||||
find_vhost() {
|
||||
local slug="$1" port="$2" f
|
||||
for f in "/etc/nginx/sites-enabled/$slug" "/etc/nginx/sites-available/$slug"; do
|
||||
[[ -f "$f" ]] && { echo "$f"; return 0; }
|
||||
done
|
||||
f=$(grep -rlE "127\.0\.0\.1:${port}\b" /etc/nginx/sites-enabled/ /etc/nginx/sites-available/ 2>/dev/null | head -1 || true)
|
||||
[[ -n "$f" ]] && { echo "$f"; return 0; }
|
||||
return 1
|
||||
}
|
||||
|
||||
# ------------------------------------------------ estructura de una instancia
|
||||
# Rellena INST_* para la unidad dada. Devuelve 1 si la unidad no es válida.
|
||||
load_instance() {
|
||||
local unit="$1"
|
||||
local fragment exec_line
|
||||
fragment=$(systemctl show "$unit" -p FragmentPath --value 2>/dev/null || true)
|
||||
[[ -z "$fragment" || ! -f "$fragment" ]] && return 1
|
||||
|
||||
exec_line=$(systemctl cat "$unit" 2>/dev/null | grep -E '^[[:space:]]*ExecStart=' | tail -1 || true)
|
||||
[[ -z "$exec_line" ]] && return 1
|
||||
|
||||
INST_SLUG="${unit#odoo-}"
|
||||
INST_SLUG="${INST_SLUG%.service}"
|
||||
INST_UNIT="$unit"
|
||||
INST_UNIT_PATH="$fragment"
|
||||
|
||||
INST_CONF="$(printf '%s' "$exec_line" | sed -nE 's/.*[[:space:]]-c[[:space:]]+([^[:space:]]+).*/\1/p')"
|
||||
[[ -z "$INST_CONF" ]] && return 1
|
||||
[[ "$INST_CONF" != /* ]] && INST_CONF="/root/$INST_CONF"
|
||||
|
||||
INST_DB="$(conf_get "$INST_CONF" db_name || true)"
|
||||
[[ -z "$INST_DB" ]] && return 1
|
||||
INST_PORT="$(conf_get "$INST_CONF" http_port || true)"
|
||||
INST_DATA_DIR="$(conf_get "$INST_CONF" data_dir || true)"
|
||||
INST_DB_HOST="$(conf_get "$INST_CONF" db_host || true)"
|
||||
INST_DB_PORT="$(conf_get "$INST_CONF" db_port || true)"
|
||||
INST_DB_USER="$(conf_get "$INST_CONF" db_user || true)"
|
||||
|
||||
INST_LOG="$(printf '%s' "$exec_line" | sed -nE 's/.*--logfile=([^[:space:]]+).*/\1/p')"
|
||||
if [[ -z "$INST_LOG" ]]; then
|
||||
INST_LOG="$(conf_get "$INST_CONF" logfile || true)"
|
||||
fi
|
||||
[[ -z "$INST_LOG" ]] && INST_LOG="/tmp/odoo-${INST_SLUG}.log"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------- pg_dump
|
||||
dump_db() {
|
||||
local db="$1" out="$2"
|
||||
if [[ -n "$DB_PASSWORD" ]]; then
|
||||
PGPASSWORD="$DB_PASSWORD" pg_dump -Fc -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$db" -f "$out"
|
||||
else
|
||||
su "$PG_OS_USER" -c "pg_dump -Fc -d $(printf %q "$db") -f $(printf %q "$out")"
|
||||
fi
|
||||
}
|
||||
|
||||
pg_restore_file() {
|
||||
# Restaura por peer como superusuario local (el dump debe ser legible por
|
||||
# el usuario PG_OS_USER). Usado por la prueba de restauración.
|
||||
local db="$1" dump="$2"
|
||||
su "$PG_OS_USER" -c "pg_restore --no-owner --no-privileges -d $(printf %q "$db") $(printf %q "$dump")"
|
||||
}
|
||||
|
||||
# Ejecuta SQL (tuples only) contra postgres por peer (superusuario local).
|
||||
psql_su() {
|
||||
local db="$1" sql="$2"
|
||||
su "$PG_OS_USER" -c "psql -d $(printf %q "$db") -Atc $(printf %q "$sql")" 2>/dev/null
|
||||
}
|
||||
|
||||
# ------------------------------------------------- respaldo de configuración
|
||||
backup_config() {
|
||||
local slug="$1" unit="$2" conf="$3" vhost="$4" data_dir="$5" db="$6" port="$7" out="$8"
|
||||
local stage
|
||||
stage="$(mktemp -d /tmp/skeen-cfg.XXXXXX)"
|
||||
local -a seen=()
|
||||
saw_dir() { local d; for d in "${seen[@]:-}"; do [[ "$d" == "$1" ]] && return 0; done; return 1; }
|
||||
|
||||
mkdir -p "$stage/systemd" "$stage/nginx" "$stage/frontend"
|
||||
[[ -f "$conf" ]] && cp -a "$conf" "$stage/odoo.conf"
|
||||
[[ -f "$unit" ]] && cp -a "$unit" "$stage/systemd/"
|
||||
[[ -n "$vhost" && -f "$vhost" ]] && cp -a "$vhost" "$stage/nginx/"
|
||||
|
||||
local d base
|
||||
for d in "/var/www/${slug}-frontend/brand" \
|
||||
"/var/www/${slug}-frontend/skeen-brand" \
|
||||
"/var/www/skeen-frontend/brand" \
|
||||
"/var/www/skeen-frontend/skeen-brand"; do
|
||||
[[ -d "$d" ]] || continue
|
||||
saw_dir "$d" && continue
|
||||
seen+=("$d")
|
||||
base="$(basename "$(dirname "$d")")-$(basename "$d")"
|
||||
cp -a "$d" "$stage/frontend/$base"
|
||||
done
|
||||
|
||||
{
|
||||
echo "Instancia: $slug"
|
||||
echo "Unidad: $unit"
|
||||
echo "Conf: $conf"
|
||||
echo "Base: $db"
|
||||
echo "Puerto: $port"
|
||||
echo "data_dir: $data_dir"
|
||||
echo "Vhost: ${vhost:-<ninguno>}"
|
||||
echo "Generado: $(date '+%F %T')"
|
||||
echo "Host: $(hostname)"
|
||||
echo "Contenido: odoo.conf, systemd/<unit>, nginx/<vhost>, frontend/<marca>"
|
||||
} >"$stage/MANIFEST.txt"
|
||||
|
||||
if (( DRY_RUN )); then
|
||||
log " [dry-run] escribiría config: $out"
|
||||
rm -rf "$stage"
|
||||
return 0
|
||||
fi
|
||||
tar czf "$out" -C "$stage" .
|
||||
rm -rf "$stage"
|
||||
chmod 600 "$out"
|
||||
chown root:root "$out" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------- rotación
|
||||
# Plan de rotación GFS. Imprime las rutas a borrar (una por línea), sin borrar.
|
||||
rotation_plan() {
|
||||
local dir="$1"
|
||||
local -a stamps=()
|
||||
local f st
|
||||
shopt -s nullglob
|
||||
for f in "$dir"/*.dump; do
|
||||
if [[ "$f" =~ _([0-9]{8}_[0-9]{6})\.dump$ ]]; then
|
||||
stamps+=("${BASH_REMATCH[1]}")
|
||||
fi
|
||||
done
|
||||
shopt -u nullglob
|
||||
((${#stamps[@]})) || return 0
|
||||
|
||||
local sorted
|
||||
sorted="$(printf '%s\n' "${stamps[@]}" | sort -r)"
|
||||
|
||||
declare -A keep=() month_rep=() week_rep=() day_rep=()
|
||||
local mcount=0 wcount=0 dcount=0
|
||||
local first=1 newest=""
|
||||
for st in $sorted; do
|
||||
local d="${st:0:8}" month="${st:0:6}" iso day
|
||||
iso="$(date -d "${d:0:4}-${d:4:2}-${d:6:2}" +%G-W%V 2>/dev/null || echo "x")"
|
||||
day="$d"
|
||||
if (( first )); then newest="$st"; keep["$st"]="ultimo"; first=0; fi
|
||||
if [[ -z "${month_rep[$month]:-}" && $mcount -lt $RETAIN_MONTHLY ]]; then
|
||||
month_rep[$month]="$st"; keep["$st"]="mensual"; mcount=$((mcount+1)); continue
|
||||
fi
|
||||
if [[ -z "${week_rep[$iso]:-}" && $wcount -lt $RETAIN_WEEKLY ]]; then
|
||||
week_rep[$iso]="$st"; keep["$st"]="semanal"; wcount=$((wcount+1)); continue
|
||||
fi
|
||||
if [[ -z "${day_rep[$day]:-}" && $dcount -lt $RETAIN_DAILY ]]; then
|
||||
day_rep[$day]="$st"; keep["$st"]="diario"; dcount=$((dcount+1)); continue
|
||||
fi
|
||||
done
|
||||
|
||||
for st in $sorted; do
|
||||
[[ -n "${keep[$st]:-}" ]] && continue
|
||||
# borra el dump y su config tar gemelo (si existe)
|
||||
local dfile
|
||||
dfile=$(ls "$dir"/*_"$st".dump 2>/dev/null | head -1 || true)
|
||||
[[ -n "$dfile" ]] && echo "$dfile"
|
||||
local cfile
|
||||
cfile=$(ls "$dir"/*_"$st".config.tar.gz 2>/dev/null | head -1 || true)
|
||||
[[ -n "$cfile" ]] && echo "$cfile"
|
||||
done
|
||||
}
|
||||
|
||||
rotate() {
|
||||
local dir="$1" slug="$2"
|
||||
local -a to_delete=()
|
||||
local line
|
||||
while IFS= read -r line; do
|
||||
[[ -n "$line" ]] && to_delete+=("$line")
|
||||
done < <(rotation_plan "$dir")
|
||||
|
||||
if ((${#to_delete[@]} == 0)); then
|
||||
log " rotación: nada que borrar en $dir"
|
||||
return 0
|
||||
fi
|
||||
local f
|
||||
for f in "${to_delete[@]}"; do
|
||||
if (( DRY_RUN )); then
|
||||
log " rotación [dry-run] borraría $(basename "$f")"
|
||||
else
|
||||
rm -f "$f"
|
||||
log " rotación: borrado $(basename "$f")"
|
||||
fi
|
||||
done
|
||||
local ndumps ncfgs
|
||||
ndumps=$(ls "$dir"/*.dump 2>/dev/null | wc -l)
|
||||
ncfgs=$(ls "$dir"/*.config.tar.gz 2>/dev/null | wc -l)
|
||||
log " rotación: quedan $ndumps dumps y $ncfgs configs en $dir"
|
||||
}
|
||||
|
||||
# --------------------------------------------------------- restaurar y verificar
|
||||
verify_restore() {
|
||||
local slug="$1" db="$2" dump="$3"
|
||||
local tmpdb="${db}_verify_${STAMP}"
|
||||
tmpdb="${tmpdb:0:63}"
|
||||
|
||||
# espacio libre en el filesystem de PostgreSQL
|
||||
local pgdata free_kb dbsize_kb
|
||||
pgdata="$(psql_su postgres 'show data_directory' | head -1)"
|
||||
dbsize_kb="$(psql_su postgres "select pg_database_size('$db')/1024" | head -1)"
|
||||
free_kb="$(df -Pk "${pgdata:-/var/lib/postgresql}" | awk 'NR==2{print $4}')"
|
||||
if [[ -n "$dbsize_kb" && -n "$free_kb" ]] && (( free_kb < dbsize_kb * VERIFY_MIN_FREE_FACTOR )); then
|
||||
log_warn " verificación OMITIDA: espacio libre insuficiente ($(human_bytes $((free_kb*1024))) < ${VERIFY_MIN_FREE_FACTOR}x base ($(human_bytes $((dbsize_kb*1024)))))"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log " verificación: restaurando $dump a la DB temporal $tmpdb"
|
||||
if (( DRY_RUN )); then
|
||||
log " [dry-run] createdb $tmpdb; pg_restore; conteos; dropdb $tmpdb"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! su "$PG_OS_USER" -c "createdb -T template0 $(printf %q "$tmpdb")" 2>>"$LOG_FILE"; then
|
||||
log_err " verificación: no se pudo crear $tmpdb"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# El dump es root:600. Se copia a un temporal legible por el superusuario
|
||||
# postgres y se restaura por peer (evita passwords y problemas de permisos
|
||||
# con extensiones que requieren superusuario).
|
||||
local tmpd
|
||||
tmpd="$(mktemp /tmp/skeen-vrf.XXXXXX)"
|
||||
cp -a "$dump" "$tmpd"
|
||||
chmod 640 "$tmpd"
|
||||
chown "${PG_OS_USER}:${PG_OS_USER}" "$tmpd"
|
||||
local restore_rc=0
|
||||
pg_restore_file "$tmpdb" "$tmpd" >>"$LOG_FILE" 2>&1 || restore_rc=$?
|
||||
rm -f "$tmpd"
|
||||
# pg_restore puede devolver !=0 por avisos benignos (comentarios/acl); la
|
||||
# prueba real es la comparación de conteos. Se registra pero no aborta.
|
||||
(( restore_rc == 0 )) || log_warn " verificación: pg_restore devolvió $restore_rc (ver $LOG_FILE); se valida por conteos"
|
||||
|
||||
local tables=(res_partner skeen_cita skeen_venta)
|
||||
local t orig rest status rc=0
|
||||
log " verificación: comparación de conteos"
|
||||
for t in "${tables[@]}"; do
|
||||
if [[ -z "$(psql_su "$db" "select to_regclass('public.$t')")" ]]; then
|
||||
log " $t: (no existe en $db) — omitida"
|
||||
continue
|
||||
fi
|
||||
orig="$(psql_su "$db" "select count(*) from $t" | head -1)"
|
||||
rest="$(psql_su "$tmpdb" "select count(*) from $t" | head -1)"
|
||||
if [[ "$orig" == "$rest" ]]; then status="OK"; else status="DIF"; rc=1; fi
|
||||
log " $t: original=$orig restaurado=$rest [$status]"
|
||||
done
|
||||
|
||||
log " verificación: borrando DB temporal $tmpdb"
|
||||
su "$PG_OS_USER" -c "dropdb --if-exists $(printf %q "$tmpdb")" >/dev/null 2>&1 || log_warn " no se pudo borrar $tmpdb"
|
||||
return $rc
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------- instancia
|
||||
process_instance() {
|
||||
local unit="$1"
|
||||
load_instance "$unit" || { log_warn "no se pudo interpretar la unidad $unit; omitida"; return 1; }
|
||||
|
||||
local slug="$INST_SLUG" db="$INST_DB" port="$INST_PORT"
|
||||
local dir="$BACKUP_ROOT/$slug"
|
||||
local dump_final="$dir/${db}_${STAMP}.dump"
|
||||
local cfg_final="$dir/${db}_${STAMP}.config.tar.gz"
|
||||
local dump_tmp="/tmp/${db}_${STAMP}.dump"
|
||||
|
||||
log "--- instancia $slug (unit=$unit, db=$db, puerto=$port, log=$INST_LOG) ---"
|
||||
|
||||
if (( DRY_RUN )); then
|
||||
log " [dry-run] destino: $dir/"
|
||||
log " [dry-run] pg_dump -Fc -> $dump_final"
|
||||
else
|
||||
mkdir -p "$dir"
|
||||
chmod 700 "$dir" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 1-2. dump + verificación de integridad
|
||||
local t0 t1 dur size
|
||||
t0=$(date +%s)
|
||||
local dump_ok=0
|
||||
if (( DRY_RUN )); then
|
||||
log " [dry-run] dump de $db a $dump_tmp (sin ejecutar)"
|
||||
dump_ok=1
|
||||
else
|
||||
if dump_db "$db" "$dump_tmp" 2>>"$LOG_FILE"; then
|
||||
if pg_restore --list "$dump_tmp" >/dev/null 2>>"$LOG_FILE"; then
|
||||
mv -f "$dump_tmp" "$dump_final"
|
||||
chmod 600 "$dump_final"; chown root:root "$dump_final" 2>/dev/null || true
|
||||
dump_ok=1
|
||||
else
|
||||
log_err " dump CORRUPTO ($db): falló pg_restore --list; se borra"
|
||||
rm -f "$dump_tmp"
|
||||
fi
|
||||
else
|
||||
log_err " falló pg_dump de $db (ver $LOG_FILE)"
|
||||
rm -f "$dump_tmp"
|
||||
fi
|
||||
fi
|
||||
t1=$(date +%s); dur=$((t1 - t0))
|
||||
if (( dump_ok )); then
|
||||
if (( DRY_RUN )); then
|
||||
log " [dry-run] dump simulado (no se ejecutó pg_dump)"
|
||||
else
|
||||
size=$(stat -c %s "$dump_final" 2>/dev/null || echo 0)
|
||||
log_ok " dump OK: $dump_final ($(human_bytes "$size"), ${dur}s)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. configuración
|
||||
local cfg_ok=1
|
||||
if (( SIN_CONFIG )); then
|
||||
log " config: omitida (--solo-db/--sin-config)"
|
||||
else
|
||||
local vhost
|
||||
vhost="$(find_vhost "$slug" "$port" || true)"
|
||||
if backup_config "$slug" "$INST_UNIT_PATH" "$INST_CONF" "$vhost" "$INST_DATA_DIR" "$db" "$port" "$cfg_final"; then
|
||||
(( DRY_RUN )) || log_ok " config OK: $cfg_final ($(human_bytes "$(stat -c %s "$cfg_final" 2>/dev/null || echo 0)"))"
|
||||
else
|
||||
cfg_ok=0; log_err " falló el respaldo de configuración"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. verificación con restauración
|
||||
local verify_ok=1
|
||||
if (( VERIFICAR )); then
|
||||
if (( dump_ok )); then
|
||||
verify_restore "$slug" "$db" "$dump_final" || verify_ok=0
|
||||
else
|
||||
verify_ok=0; log_err " verificación omitida: no hay dump válido"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 5. rotación
|
||||
if (( DRY_RUN )); then
|
||||
rotate "$dir" "$slug"
|
||||
else
|
||||
rotate "$dir" "$slug"
|
||||
fi
|
||||
|
||||
(( dump_ok && cfg_ok && verify_ok ))
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------- main
|
||||
log "retención efectiva: ${RETAIN_DAILY} diarios / ${RETAIN_WEEKLY} semanales / ${RETAIN_MONTHLY} mensuales"
|
||||
|
||||
declare -a RESULTS=()
|
||||
TOTAL_FAIL=0
|
||||
log "unidades detectadas: $(list_units | tr '\n' ' ')"
|
||||
|
||||
FOUND=0
|
||||
while IFS= read -r unit; do
|
||||
[[ -n "$unit" ]] || continue
|
||||
if [[ -n "$INSTANCIA" ]]; then
|
||||
[[ "$unit" == "odoo-${INSTANCIA}.service" ]] || continue
|
||||
fi
|
||||
FOUND=1
|
||||
if process_instance "$unit"; then
|
||||
RESULTS+=("OK $unit")
|
||||
else
|
||||
RESULTS+=("FALLO $unit"); TOTAL_FAIL=$((TOTAL_FAIL + 1))
|
||||
fi
|
||||
done < <(list_units)
|
||||
|
||||
if (( ! FOUND )); then
|
||||
if [[ -n "$INSTANCIA" ]]; then
|
||||
log_err "no se encontró la unidad odoo-${INSTANCIA}.service"
|
||||
else
|
||||
log_err "no se detectó ninguna instancia (unidades odoo-*.service)"
|
||||
fi
|
||||
TOTAL_FAIL=$((TOTAL_FAIL + 1))
|
||||
fi
|
||||
|
||||
log "---------------- RESUMEN ----------------"
|
||||
for r in "${RESULTS[@]:-}"; do [[ -n "$r" ]] && log " $r"; done
|
||||
if (( TOTAL_FAIL == 0 )); then
|
||||
log_ok "ESTADO GLOBAL: OK (${#RESULTS[@]} instancia(s)); backup $STAMP completado"
|
||||
exit 0
|
||||
else
|
||||
log_err "ESTADO GLOBAL: FALLO ($TOTAL_FAIL instancia(s) con error); backup $STAMP"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user