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:
257
docs/producto/operacion-instancia.md
Normal file
257
docs/producto/operacion-instancia.md
Normal 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`).
|
||||
Reference in New Issue
Block a user