9.6 KiB
Nueva instancia white-label (provisioning automatizado)
Cómo levantar una clínica/spa nueva sobre el mismo código y la misma VPS, con
sus propios secretos, puerto, base de datos, vhost, marca y configuración. Es
la versión automatizada de docs/segunda-instancia.md (que sigue vigente como
referencia del procedimiento manual y del caso clinica2).
El código de los addons es compartido y read-only (/root/skeen-odoo/addons);
cada instancia es solo infra + base de datos + configuración. La marca no vive
en el código: sale del parámetro skeen.clinic.brand (API /brand).
Requisitos previos
- VPS con Odoo, PostgreSQL, nginx, Node y el venv
/root/odoo-venvya instalados (verinstalacion.md). - Al menos una instancia de referencia (
skeen_odoo) con los módulosskeen_*instalados: de ahí se copia la lista de módulos a inicializar. - Un slug, nombre comercial, dominio y puerto libres para la instancia nueva.
Uso en 2 pasos
cd /root/skeen-proyecto/infra/provision
# Paso 1 — SIEMPRE simular primero. No escribe ni ejecuta nada.
./provision_clinica.py \
--slug spa-bella --nombre "Spa Bella" \
--dominio spa.consultoria-as.com --puerto 8071 --db spa_bella_odoo \
--pack spa --demo --dry-run
# Paso 2 — ejecución real (coordinada, fuera de horario pico).
./provision_clinica.py \
--slug spa-bella --nombre "Spa Bella" \
--dominio spa.consultoria-as.com --puerto 8071 --db spa_bella_odoo \
--pack spa --demo
Opciones
| Opción | Default | Descripción |
|---|---|---|
--slug |
— | Identificador corto, ^[a-z0-9-]+$. Nombra conf, unit, vhost y frontend. |
--nombre |
— | Nombre comercial (marca, empresa, menú raíz). |
--dominio |
— | Dominio público, p. ej. spa.consultoria-as.com. |
--puerto |
— | Puerto HTTP de Odoo; longpolling usa puerto+3. |
--db |
<slug>_odoo |
Nombre de la base de datos. |
--db-role |
<slug> |
Rol PostgreSQL. Por defecto se crea uno propio con password nueva; --db-role skeen usa el rol compartido (ver abajo). |
--pack |
generico |
Giro para glosario y demo: generico, spa, clinica, derma. |
--demo |
off | Siembra servicios, clientes, empleados, citas, paquetes y monedero de ejemplo. |
--demo-limpia |
off | Corre demo_limpiar.py al final (deja la instancia sin datos demo). |
--dominio-lan |
— | Puerto LAN extra en el vhost (p. ej. 8881 → http://<ip>:8881). |
--admin-login |
admin |
Login del usuario admin del frontend. |
--pie |
genérico | Línea del pie de página de la marca. |
--salida |
/root/backups/<slug>-credenciales-<fecha>.txt |
Archivo de credenciales (chmod 600). |
--sin-wacrm / --con-wacrm |
bloquear | Por defecto el vhost corta /api/odoo/skeen/frontend/v1/wacrm/ con 403. |
--skip-build |
off | No recompilar el frontend; reusar /root/skeen-frontend/dist. |
--force |
off | Continuar aunque existan colisiones (puerto/DB/unit/vhost/dirs). |
--dry-run |
off | Imprime pasos, contenido de archivos y comandos sin ejecutar nada. |
Qué crea
Con --slug spa-bella --puerto 8071 --db spa_bella_odoo:
| Recurso | Valor |
|---|---|
| Rol PostgreSQL | spa_bella (LOGIN, password aleatoria) |
| Base de datos | spa_bella_odoo (owner = rol propio) |
| Conf Odoo | /root/spa-bella-odoo.conf (chmod 600) |
| data_dir | /var/lib/odoo-spa-bella |
| Log | /tmp/odoo-spa-bella.log |
| Unit systemd | odoo-spa-bella.service |
| Vhost nginx | /etc/nginx/sites-available/spa-bella + symlink en sites-enabled |
| Frontend | /var/www/spa-bella-frontend |
| Credenciales | /root/backups/spa-bella-credenciales-<fecha>.txt |
Orden de ejecución del script
- Validaciones: slug/DB/puerto libres, sin unit/vhost/dir/conf previos.
Aborta listando colisiones salvo
--force. - Secretos propios:
admin_passwd(master de Odoo),db_passwordy password del admin del frontend, todos aleatorios. - DB: crea el rol y
su postgres -c "createdb -O <rol> <db>"(el rolskeenno tiene CREATEDB; se crea comopostgres). odoo.confdesde plantilla: puerto, db, addons_path, log propio,without_demo = all, sin workers.- Unit systemd desde plantilla +
daemon-reload+enable. - Módulos: corre
odoo-bin ... --stop-after-init --no-http -i <módulos>. - Post-instalación (
postinstall_clinica.py): marca, empresa, admin, y siembra tolerante de glosario/sucursal/settings (abajo). - Menú raíz: actualiza el xmlid
skeen_pacientes.menu_skeen_roota{"en_US": "<nombre>"}(igual queclinica2_odoo). - Vhost nginx +
nginx -t+reload. - Frontend: build compartido +
rsync --delete+ logos +chown. - Arranque:
systemctl start. - Credenciales y resumen (público, LAN, logs, cómo limpiar).
Lista de módulos a inicializar
Se toma de la instancia de referencia en vez de hardcodearla:
su postgres -c "psql -d skeen_odoo -Atc \
\"SELECT name FROM ir_module_module WHERE state='installed' AND name LIKE 'skeen%'\""
El script consulta eso y ordena los módulos topológicamente con
ir_module_module_dependency (dependencias primero). Odoo resuelve igual las
dependencias, pero el orden explícito evita la trampa documentada en
segunda-instancia.md (el menú raíz debe existir antes de que las vistas que
lo referencian se carguen).
Post-instalación y el nuevo sistema de configuración
postinstall_clinica.py corre con el ORM de Odoo y hace:
- Brand neutro en
skeen.clinic.brand(nombre comercial, logos/brand/logo-blanco.svgy/brand/logo-negro.svg, pie genérico). res.company.name= nombre.- Admin del frontend (
skeen.frontend.user) con la password generada ymust_change_password = True. - Oculta WACRM en
skeen.menu_roles(salvo--con-wacrm).
Y solo si existen en runtime siembra los parámetros/modelos del nuevo sistema de configuración (otro cambio en curso):
| Qué | Cómo lo detecta | Qué siembra |
|---|---|---|
| Glosario | llave ir.config_parameter skeen.glosario o modelo skeen.glosario |
pack elegido |
skeen.clinic.settings |
el modelo existe | nombre, url_publica = https://<dominio>, horario 08:00–19:00, moneda MXN |
skeen.sucursal |
el modelo existe | 1 sucursal default con el nombre del negocio |
skeen.service.category |
el modelo existe | semilla de categorías del pack |
Lo que no exista se reporta como TODO en la salida y no aborta el
provisioning. Cuando ese cambio esté desplegado, una corrida nueva ya lo
siembra; para una instancia ya creada, se puede re-ejecutar solo el
post-install:
cat > /tmp/post.json <<'JSON'
{"conf": "/root/spa-bella-odoo.conf", "db": "spa_bella_odoo",
"nombre": "Spa Bella", "dominio": "spa.consultoria-as.com",
"pack": "spa", "admin_login": "admin", "admin_password": "<la-generada>",
"con_wacrm": false, "demo": false}
JSON
/root/odoo-venv/bin/python /root/skeen-proyecto/infra/provision/postinstall_clinica.py /tmp/post.json
Datos demo
--demo siembra ~10 servicios con precios, ~15 clientes, 2–3 empleados con
staff_type, ~20 citas en la semana actual y algunos paquetes/monedero, todo
coherente con el --pack. Los registros quedan marcados (DEMO- en códigos de
servicio, referred_by='DEMO', [DEMO] en notas) y sus ids en el parámetro
skeen.demo.ids.
Para borrarlos antes de entregar la instancia a un cliente:
/root/odoo-venv/bin/python /root/skeen-proyecto/infra/provision/demo_limpiar.py \
--conf /root/spa-bella-odoo.conf --db spa_bella_odoo
(o pasar --demo-limpia en la corrida de provisioning).
Frontend
El build es compartido: un solo bundle (npm run build en
/root/skeen-frontend) se copia con rsync -a --delete a cada
/var/www/<slug>-frontend. La marca y el giro salen del API, no del build; por
eso no hace falta compilar por instancia (usar --skip-build cuando ya haya un
dist/ fresco). Los logos genéricos se copian desde
/var/www/clinica2-frontend/brand/ (o frontend/public/brand/) a
<destino>/brand/.
Acceso público (paso manual)
Igual que con clinica2, el túnel cloudflared es remotamente administrado: hay
que agregar el ingress en el dashboard (Zero Trust → Networks → Tunnels):
- hostname: spa.consultoria-as.com
service: http://localhost:80
El vhost ya escucha en el 80 con ese server_name. El --dominio-lan deja
además un acceso LAN permanente.
Rol compartido vs rol propio (decisión de diseño)
El procedimiento manual creaba la DB con -O skeen y usaba
db_password = skeen_dev_2026 para todas las instancias. Eso obliga a
reutilizar la misma password y no permite "secretos propios por instancia". Por
eso el script, por defecto, crea un rol propio (<slug>) con password
aleatoria y hace a ese rol owner de la DB. No toca el rol skeen ni ninguna
instancia existente.
Si se necesita reproducir el esquema compartido:
./provision_clinica.py ... --db-role skeen
En ese modo el script avisa que db_password en la conf debe coincidir con la
password real del rol (no la cambia).
Fallos a medias y limpieza
Si algo falla, el script imprime qué quedó hecho y el comando para
deshacerlo (rol, DB, conf, unit, vhost, frontend, service). Opciones:
--force para saltar colisiones, o limpiar a mano con la lista de undo que
imprime.
Verificación tras una corrida real
systemctl status odoo-<slug> # active (running)
tail -50 /tmp/odoo-<slug>.log
curl -s http://127.0.0.1:<puerto>/web/health
curl -s http://127.0.0.1:<puerto>/skeen/frontend/v1/brand # nombre comercial
nginx -t
Y en el navegador: login con el admin del frontend (fuerza cambio de password), verificar el nombre en el menú, el logo y que WACRM esté bloqueado (403).