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:
2026-10-11 09:42:08 +00:00
parent dabd8c6bd7
commit 91f909a6d0
12 changed files with 1602 additions and 0 deletions

View File

@@ -0,0 +1,257 @@
# Operación de instancias SKEEN (backups + panel)
Guía de operación comercial de la VPS: cómo funcionan los **backups automáticos
verificados** de cada instancia Odoo y cómo usar el **panel/CLI multi-instancia**.
Todo vive en `infra/ops/`:
| Archivo | Para qué |
|---|---|
| `backup_instancias.sh` | Backup por instancia: dump + config, verificación de integridad, rotación y (opcional) prueba de restauración. |
| `backup.env` | Configuración central: destinos, credenciales de DB, retención, log. |
| `admin_instancias.py` | Panel/CLI multi-instancia (read-only por defecto). |
| `systemd/` | Unidades y timers (`skeen-backup*`, `backup-instancia@`). Se copian a `/etc/systemd/system/`. |
| `logrotate/skeen-backup` | Rotación del log; se copia a `/etc/logrotate.d/skeen-backup`. |
Las instancias **no se listan a mano**: se descubren desde las unidades
`odoo-*.service`. Por cada unidad se lee su `-c <conf>` (de `systemctl cat`) y de
ahí `db_name`, `http_port`, `logfile` y `data_dir`. Añadir una instancia nueva
con `infra/provision/provision_clinica.py` la incluye automáticamente en backups
y panel, sin editar nada de ops.
---
## 1. Backups automáticos
### Qué se respalda
Por cada instancia, en `/root/backups/<slug>/`:
- **Base de datos**: `pg_dump -Fc` → `<db>_<YYYYmmdd_HHMMSS>.dump`.
- Se vuelca primero a `/tmp` (el usuario `postgres` no puede escribir en `/root`)
y se mueve al destino final con `chmod 600 root:root`.
- Se **verifica la integridad** con `pg_restore --list`; si falla, el dump se
marca como corrupto, se borra y se reporta como error.
- **Configuración del host**: `<db>_<YYYYmmdd_HHMMSS>.config.tar.gz` con
`odoo.conf`, la unidad systemd, el vhost nginx (buscado por slug o por puerto)
y los directorios de marca del frontend (`/var/www/<slug>-frontend/brand`,
`/var/www/<slug>-frontend/skeen-brand`, `/var/www/skeen-frontend/skeen-brand`).
Incluye un `MANIFEST.txt`. Pesa unos pocos MB por las tipografías de marca.
### Comando
```bash
/root/skeen-proyecto/infra/ops/backup_instancias.sh # todas las instancias
/root/skeen-proyecto/infra/ops/backup_instancias.sh --instancia skeen
/root/skeen-proyecto/infra/ops/backup_instancias.sh --verificar # + prueba de restauración
/root/skeen-proyecto/infra/ops/backup_instancias.sh --solo-db # sin config
/root/skeen-proyecto/infra/ops/backup_instancias.sh --dry-run # no escribe nada
/root/skeen-proyecto/infra/ops/backup_instancias.sh --help
```
| Flag | Efecto |
|---|---|
| `--instancia <slug>` | Solo esa instancia. |
| `--solo-db` / `--sin-config` | No respalda la configuración del host. |
| `--verificar` | Además restaura el dump a una DB temporal y compara conteos (ver abajo). |
| `--dry-run` | Muestra qué haría (incluida la rotación) sin tocar nada. |
| `--backup-root <dir>` | Destino alternativo (pruebas / recuperación). |
- **Lockfile**: `/var/lock/skeen-backup.lock` (flock). Si ya hay un backup en
curso, la segunda ejecución sale con **exit 3** sin hacer nada.
- **Log**: `/var/log/skeen-backup.log` (una línea por evento y un resumen final
con `ESTADO GLOBAL: OK/FALLO`). Exit code `0` si todo va bien, `1` si alguna
instancia falla. Se rota por logrotate (semanal, 8 copias) y, además, el
script lo trunca si supera `LOG_MAX_BYTES` (5 MiB).
### Programación (systemd)
| Unidad | Cuándo | Qué corre |
|---|---|---|
| `skeen-backup.timer` | diario ~03:20 (`RandomizedDelaySec=1800`) | `skeen-backup.service` → todas las instancias |
| `skeen-backup-verify.timer` | domingos ~04:40 | `skeen-backup-verify.service` → todas con `--verificar` |
| `backup-instancia@.timer` | diario ~03:40 (plantilla, **no habilitada**) | `backup-instancia@<slug>.service` → una instancia concreta |
Los timers tienen `Persistent=true`: si la VPS estaba apagada a la hora prevista,
el backup corre al arrancar.
### Política de retención
Configurable en `backup.env` (`RETAIN_DAILY`, `RETAIN_WEEKLY`, `RETAIN_MONTHLY`).
Por defecto: **14 diarios / 8 semanales / 6 mensuales**.
Es un esquema abuelo-padre-hijo con **buckets exclusivos**: un archivo que ya
quedó retenido como mensual no vuelve a contar para semanal, y así sucesivamente.
1. **Mensual** (hasta 6): el backup más reciente de cada mes calendario.
2. **Semanal** (hasta 8): el más reciente de cada semana ISO no retenido aún.
3. **Diario** (hasta 14): el más reciente de cada día no retenido aún.
4. Todo lo demás se borra.
El backup **más reciente nunca se borra** (el "último bueno"), y por
construcción tampoco se borra el representante mensual. Cuando se borra un dump,
se borra también su `.config.tar.gz` gemelo (misma marca de tiempo).
> Consecuencia útil: varios backups del mismo día no se acumulan sin límite; se
> conservan a lo sumo uno por nivel (mensual/semanal/diario).
---
## 2. Verificación
### Integridad (siempre)
Cada dump se valida con `pg_restore --list` antes de considerarse bueno. Para
verificar a mano:
```bash
for f in /root/backups/*/*.dump; do pg_restore --list "$f" >/dev/null && echo "OK $f" || echo "FALLA $f"; done
```
### Prueba de restauración real (`--verificar`)
Restaura el dump recién creado a una DB temporal `<db>_verify_<fecha>`, cuenta
filas de tablas clave (`res_partner`, `skeen_cita`, `skeen_venta`), compara con
la original y **borra la temporal**:
```bash
/root/skeen-proyecto/infra/ops/backup_instancias.sh --instancia clinica2 --verificar
```
- Requiere **espacio libre** en el filesystem de PostgreSQL de al menos
`VERIFY_MIN_FREE_FACTOR` (2) × tamaño de la base; si no lo hay, la omite con
un aviso en el log (no falla el backup).
- La restauración corre por peer como `postgres` (superusuario), de modo que
extensiones y objetos se recrean sin problemas; la prueba real es la
comparación de conteos.
---
## 3. Restaurar un backup (procedimiento)
Las DB y sus tablas son propiedad del rol `skeen`. Restaura **siempre a una DB
nueva primero** y solo luego promueve, para no dejar la instancia sin datos si el
dump tuviera un problema.
```bash
DUMP=/root/backups/clinica2/clinica2_odoo_20261011_093633.dump
SLUG=clinica2
DB=clinica2_odoo
```
**Paso 1 — Restaurar a una DB nueva y comparar**
```bash
sudo -u postgres createdb -T template0 -O skeen "${DB}_restore"
sudo -u postgres pg_restore --role=skeen --no-privileges -d "${DB}_restore" "$DUMP"
# comparar conteos (deben coincidir)
sudo -u postgres psql -d "$DB" -Atc "select count(*) from res_partner"
sudo -u postgres psql -d "${DB}_restore" -Atc "select count(*) from res_partner"
```
**Paso 2 — Promover la DB restaurada (con parada del servicio)**
```bash
systemctl stop odoo-${SLUG}.service
# cortar conexiones y renombrar
sudo -u postgres psql -Atc "select pg_terminate_backend(pid) from pg_stat_activity \
where datname='${DB}' and pid <> pg_backend_pid()"
sudo -u postgres psql -c "ALTER DATABASE ${DB} RENAME TO ${DB}_pre_restore"
sudo -u postgres psql -c "ALTER DATABASE ${DB}_restore RENAME TO ${DB}"
systemctl start odoo-${SLUG}.service
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:<puerto>/web/login
```
**Paso 3 — Limpieza** (cuando ya validaste)
```bash
sudo -u postgres dropdb "${DB}_pre_restore"
```
**Restauración de emergencia sobre la DB viva** (solo si no puedes parar y la DB
está corrupta; pierde los datos posteriores al dump):
```bash
systemctl stop odoo-${SLUG}.service
sudo -u postgres dropdb "${DB}" # o ALTER DATABASE ... RENAME
sudo -u postgres createdb -T template0 -O skeen "${DB}"
sudo -u postgres pg_restore --role=skeen --no-privileges -d "${DB}" "$DUMP"
systemctl start odoo-${SLUG}.service
```
> El `pg_restore --role=skeen` es importante: hace que los objetos queden del rol
> `skeen` (el que usa Odoo). Sin `--role`, quedarían de `postgres` y el backend
> podría no tener permisos.
> La configuración (`*.config.tar.gz`) se restaura descomprimiendo el tar y
> copiando cada archivo a su ruta (están listados en el `MANIFEST.txt`).
---
## 4. Panel / CLI multi-instancia
`admin_instancias.py` **no requiere dependencias externas** y descubre las
instancias igual que el backup. Por defecto es de **solo lectura**.
```bash
cd /root/skeen-proyecto/infra/ops
./admin_instancias.py listar # tabla (subcomando por defecto)
./admin_instancias.py --json listar # JSON para automatización
```
| Subcomando | Qué muestra |
|---|---|
| `listar` | slug, unit, estado del servicio, puerto, DB, HTTP en `/web/login`, nº de módulos `skeen_*`, último backup (fecha/tamaño), nº de backups, uso de disco de backups y datadir, y si el puerto escucha. |
| `estado <slug>` | Detalle: conf, DB, puerto, HTTP, log, data_dir, vhost, frontend, versión de cada módulo `skeen_*`, backups. |
| `version` | Versión instalada de cada módulo `skeen_*`, en tabla por instancia. |
| `uso` | Disco, RAM, carga del host y tabla de procesos Odoo (CPU/RAM/RSS/uptime/conf). |
| `instalacion <slug>` | Valida que la instancia esté completa: DB, unit activa, conf, vhost, puerto, HTTP, frontend desplegado y marca configurada. Lista lo que falta. |
| `logs <slug> [-n N]` | Tail del log de Odoo de esa instancia y del log de backups. |
| `credenciales <slug> [--mostrar]` | Ruta del archivo de credenciales del provisioning. **Nunca** imprime passwords sin `--mostrar`. |
| `backup <slug> [--verificar] --yes` | Lanza el script de backup de esa instancia. |
| `reiniciar <slug> --yes` | `systemctl restart`. Comprueba el lock y advierte que interrumpe el servicio. |
- `--json` (global o por subcomando) da salida JSON.
- Las acciones que cambian estado (`backup`, `reiniciar`) **exigen `--yes`** y
avisan qué harán antes de hacerlo.
---
## 5. Qué hacer si algo falla
| Síntoma | Diagnóstico | Acción |
|---|---|---|
| **Disco lleno** | `./admin_instancias.py uso`; `df -h /` | Bajar retención en `backup.env` (p. ej. `RETAIN_DAILY=7`), borrar backups viejos de `/root/backups/<slug>/`, revisar `/var/log/skeen-backup.log.1.gz`. La verificación se auto-omite si no hay espacio. |
| **Dump corrupto** | En el log: `dump CORRUPTO (...)`. Comprobar con `pg_restore --list`. | El script ya borra el dump corrupto. Revisar espacio en `/tmp` y salud de PostgreSQL; volver a lanzar el backup. |
| **Backup no se ejecuta** | `systemctl list-timers \| grep skeen`; `journalctl -u skeen-backup.service -n 50`; `tail /var/log/skeen-backup.log` | Verificar el timer (`enabled`) y que el lock no esté tomado por un proceso colgado: `fuser /var/lock/skeen-backup.lock`. |
| **Servicio caído** | `./admin_instancias.py estado <slug>`; `journalctl -u odoo-<slug>.service -n 100` | Reiniciar con `./admin_instancias.py reiniciar <slug> --yes` (o `systemctl restart odoo-<slug>`). Si la DB quedó corrupta, restaurar (sección 3). |
| **Instancia incompleta** | `./admin_instancias.py instalacion <slug>` | Completar lo que falte (vhost, frontend, marca) o re-provisionar con `infra/provision/provision_clinica.py`. |
| **Restauración de emergencia** | La instancia no arranca y el dump más reciente es válido | Sección 3, "Restauración de emergencia". Antes de tocar nada, copia el directorio de datos actual (`/var/lib/odoo*`) por si acaso. |
| **No llegan backups de una instancia nueva** | `./admin_instancias.py listar` no la ve | Comprobar que existe su `odoo-<slug>.service` y que `-c <conf>` apunta a un conf con `db_name`. |
---
## 6. Rutas y estado instalado
| Recurso | Ruta |
|---|---|
| Scripts | `/root/skeen-proyecto/infra/ops/` |
| Config | `/root/skeen-proyecto/infra/ops/backup.env` (chmod 600) |
| Backups | `/root/backups/<slug>/` (`<db>_<fecha>.dump`, `<db>_<fecha>.config.tar.gz`) |
| Log | `/var/log/skeen-backup.log` (+ rotación logrotate) |
| Lock | `/var/lock/skeen-backup.lock` |
| Units | `/etc/systemd/system/skeen-backup*.{service,timer}`, `backup-instancia@.{service,timer}` |
| Logrotate | `/etc/logrotate.d/skeen-backup` |
---
## 7. Componentes opcionales (Docker)
La VPS también corre **Supabase self-hosted** y **WACRM** en Docker. **No** forman
parte del backup por instancia; se manejan aparte. En `backup.env`,
`SUPABASE_BACKUP` y `WACRM_BACKUP` están documentados y en `0`. Los backups
manuales previos de esos componentes viven en `/root/backups/*.sql`
(`supabase_skeen_backup_*.sql`, `wacrm_conversations_*.sql`).