# 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 ` (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//`: - **Base de datos**: `pg_dump -Fc` → `_.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**: `_.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/-frontend/brand`, `/var/www/-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 ` | 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 ` | 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@.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 `_verify_`, 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:/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 ` | 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 ` | Valida que la instancia esté completa: DB, unit activa, conf, vhost, puerto, HTTP, frontend desplegado y marca configurada. Lista lo que falta. | | `logs [-n N]` | Tail del log de Odoo de esa instancia y del log de backups. | | `credenciales [--mostrar]` | Ruta del archivo de credenciales del provisioning. **Nunca** imprime passwords sin `--mostrar`. | | `backup [--verificar] --yes` | Lanza el script de backup de esa instancia. | | `reiniciar --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//`, 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 `; `journalctl -u odoo-.service -n 100` | Reiniciar con `./admin_instancias.py reiniciar --yes` (o `systemctl restart odoo-`). Si la DB quedó corrupta, restaurar (sección 3). | | **Instancia incompleta** | `./admin_instancias.py instalacion ` | 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-.service` y que `-c ` 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//` (`_.dump`, `_.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`).