# Spec 026 — Purge VM112: validação Traefik pós-remoção (CT114) **Criado:** 2026-06-19 **Solicitado por:** Roger **Prioridade:** **P0** (incidente produção) **Status:** ✅ Implementado (VM112 + CT114, 2026-06-19) **Sistema:** Wizard VM112 · Traefik CT114 · Desk VM122 **Relacionado:** Spec **017** (purge domínio) · Spec **025** (continuidade wizard) · Spec **018** (Serviços / drawer purge) --- ## Incidente que motivou a spec **Data:** 2026-06-19 ~02:18 UTC **Sintoma:** `https://onboard.ligbox.com.br/onboard` → **404 page not found** (Traefik Go default), afectando **todos** os domínios onboard — não só o domínio purgado. **Domínios purgados na sessão:** `iofficebooks.com`, `exuberanti.com.br`. **Causa raiz:** 1. `_purge_traefik_routers()` em `/opt/ligbox-wizard/backend/app/services/domain_orchestration.py` remove routers por **corte de texto** (`Host(...)` → próximo `\n `). 2. Isso deixou bloco **`mail-mail-exuberanti-com-br-Router` sem `rule:`** e **chave duplicada** no `dynamic.yml`. 3. Traefik v3.6 **rejeitou o ficheiro inteiro**: ``` yaml: unmarshal errors: mapping key "mail-mail-exuberanti-com-br-Router" already defined ``` 4. Após restart, só **3 routers internos** activos (`acme`, `api`, `dashboard`) — zero rotas de produção. 5. O purge reportou **`traefik_ok`** porque validou apenas **SSH write + restart**, não carga efectiva da config. **Correcção manual aplicada (19/06):** remoção de routers inválidos/duplicados + restart Traefik → 62 routers activos. --- ## Objetivo Tornar o purge de domínio **seguro para a plataforma inteira**: após remover um tenant, o Traefik **tem de continuar operacional** e o onboard **tem de responder 200**. **Regra de ouro (nova):** > Purge só está **concluído** quando o domínio sumiu da base **e** o Traefik tem ≥ N routers **e** `GET https://onboard.ligbox.com.br/onboard` → **200** com HTML do wizard. --- ## Fora de escopo - Reescrever Spec 017 (histórico Desk, RBAC, drawer) - Purge parcial (só DNS, só contas) - Validação de certificados LE por domínio purgado (opcional futuro) - Automatizar purge agendado --- ## Problema na implementação actual (VM112) | Função | Ficheiro | Problema | |--------|----------|----------| | `_purge_traefik_routers` | `domain_orchestration.py` | Corte textual frágil; não remove middleware `webmail-pending-{slug}`; não valida YAML | | `_purge_traefik_sni` | idem | OK funcional; falta verificação pós-restart HAProxy | | `_execute_purge` | idem | Marca `traefik_ok` sem smoke test | | Portal users | `_purge_portal_users` | Só `/var/lib/ibytera-mail-portal/portal_users/` — **falta** `/var/lib/ligbox-wizard/portal_users/` | | Nginx Carbonio | — | **Não** limpa vhosts `mail.{domain}` em `/opt/zextras/conf/nginx/includes/` | | Branding / scripts deploy | — | **Não** remove entrada `tenant_branding.py` nem refs em `apply-admin-nginx-overrides.py` | --- ## Solução proposta ### Fase A — Remoção Traefik robusta (P0) Substituir corte textual por script Python remoto no CT114 (mesmo padrão de `infrastructure.do_traefik()`): 1. **Backup** antes de editar: ``` /root/traefik/dynamic.yml.bak-purge-{domain_slug}-{timestamp} ``` 2. **Parse YAML** (`yaml.safe_load` para validação; **edição linha-a-linha** — nunca `safe_dump` no ficheiro inteiro). 3. Remover, por domínio: - Router `mail-mail-{slug}-Router` (e variantes) - Middleware `webmail-pending-{slug}` (redirect regex para wizard) - Qualquer router cujo `rule` contenha `Host(\`mail.{domain}\`)` ou alias mail 4. **Validação pré-restart:** - YAML parse OK - Zero chaves duplicadas em `http.routers` - Todo router tem campo `rule` não vazio - Zero ocorrências de `mail.{domain}` no texto (sanity grep) 5. **Restart** Traefik só se validação OK. 6. Se validação falhar → **rollback** do backup **sem** restart. **Slug:** `{domain}` com `.` → `-` (ex.: `exuberanti.com.br` → `exuberanti-com-br`). --- ### Fase B — Verificação pós-purge (P0) Novo step `_execute_purge`: **`traefik_validate`** (após `traefik_routers`). | # | Check | Comando / origem | Critério | |---|-------|------------------|----------| | B1 | Routers carregados | `curl -s http://127.0.0.1:8080/api/http/routers` (CT114) | `count ≥ 10` (alerta se `< 10`; falha se `< 5`) | | B2 | Onboard router activo | JSON routers | existe `onboard-ligbox-Router@file` enabled | | B3 | Smoke HTTPS onboard | `curl -sf -o /dev/null -w '%{http_code}' https://onboard.ligbox.com.br/onboard` | `200` | | B4 | Smoke API VM112 | `curl -sf -o /dev/null -w '%{http_code}' http://10.10.10.112:8090/onboard` | `200` | | B5 | Sem refs domínio no dynamic | `grep -i {domain}` em `dynamic.yml` | 0 matches (excepto backup) | | B6 | Log Traefik limpo | `docker logs traefik 2>&1 \| tail -20` | sem `unmarshal errors` / `invalid rule` nos últimos 30s | **Falha em B1–B4:** rollback `dynamic.yml` + restart Traefik + step `traefik_validate` = **error** + job purge = **error** (não `done`). **Timeline Desk:** novo passo visível «Validar Traefik / onboard» com detalhe de cada check. --- ### Fase C — Purge VM112 completo (P1) Expandir `_execute_purge` com steps adicionais (ou sub-steps documentados): | Step | Acção | |------|--------| | `portal_users_wizard_store` | Apagar JSON em `/var/lib/ligbox-wizard/portal_users/` cujo email ∈ domínio | | `nginx_vhosts` | Remover `server_name mail.{domain}` de includes nginx Carbonio + `nginx -t` + reload | | `tenant_branding` | Remover linha em `tenant_branding.py` | | `deploy_scripts` | Remover `mail.{domain}` de `apply-admin-nginx-overrides.py` e `sync-traefik-admin-certs.sh` | | `traefik_export_certs` | Apagar `mail-{slug}*.pem` em `/opt/zextras/ssl/letsencrypt/traefik-export/` | Cada step reporta `ok` / `error` na timeline; falha nginx `nginx -t` → **error** (não deixa mail quebrado). --- ### Fase D — Desk / histórico (P2) - Persistir em `vm112_json` do job: resultado de `traefik_validate` (checks B1–B6). - Badge **error** no histórico se rollback Traefik ocorreu. - Alerta ops (ntfy / webhook) quando purge falha em `traefik_validate`. --- ## Alterações de API / timeline ### VM112 — novos steps em `POST /api/admin/domains/{domain}/purge` Ordem actualizada (trecho Traefik): ``` … traefik_sni → running → done|error traefik_routers → running → done|error (Fase A — lógica nova) traefik_validate → running → done|error (Fase B — NOVO) … ``` **Resposta `result` (campos novos):** ```json { "traefik_validate": { "ok": true, "router_count": 62, "onboard_http": 200, "rollback": false }, "traefik_rollback": null } ``` Em falha: ```json { "traefik_validate": { "ok": false, "router_count": 3, "onboard_http": 404, "rollback": true }, "traefik_rollback": "dynamic.yml.bak-purge-exuberanti-com-br-20260619T021800Z" } ``` --- ## Ficheiros a alterar | VM | Ficheiro | Fase | |----|----------|------| | 112 | `backend/app/services/domain_orchestration.py` | A, B, C | | 112 | `backend/app/services/infrastructure.py` | A (reutilizar `_router_key_for_host`, SSH helpers) | | 114 | `/root/traefik/dynamic.yml` | _(runtime — só via purge script)_ | | 122 | `api/app/vm112_domains_routes.py` | D (opcional — repassar novos campos) | | 122 | `frontend/assets/app.js` | D (render checks no modal histórico) | **Deploy:** VM112 `systemctl restart ligbox-wizard` após merge. --- ## Critérios de aceitação 1. Purge de domínio teste remove router/middleware **sem** duplicar chaves YAML. 2. Após purge, Traefik API reporta **≥ 10** routers HTTP. 3. `curl -sf https://onboard.ligbox.com.br/onboard` → **200** imediatamente após purge. 4. Purge com YAML inválido simulado → **rollback** automático + job status **error** (não `done`). 5. Portal users removidos de **ambas** as pastas (`ibytera-mail-portal` + `ligbox-wizard`). 6. Histórico Desk (Spec 017 v2) mostra step `traefik_validate` com detalhe. 7. Regressão: purge de domínio inexistente (`no_zone`, `domínio já ausente`) continua idempotente. --- ## Test plan (E2E) ```bash # Pré: criar domínio teste via wizard (zona CF pending OK) DOMAIN=teste-purge-$(date +%s).example.com # ou domínio real de lab # Executar purge curl -s -X POST "http://10.10.10.112:8090/api/admin/domains/${DOMAIN}/purge?sync=true" \ -H "X-Api-Key: $ADMIN_API_KEY" | jq '.result.traefik_validate' # Validar plataforma curl -sf -o /dev/null -w "onboard:%{http_code}\n" https://onboard.ligbox.com.br/onboard ssh root@10.10.10.114 'curl -s http://127.0.0.1:8080/api/http/routers | python3 -c "import sys,json; print(len(json.load(sys.stdin)),\"routers\")"' ``` **Teste de regressão (incidente 19/06):** purge `exuberanti.com.br` duas vezes seguidas → segunda execução idempotente, Traefik estável. --- ## Riscos e mitigação | Risco | Mitigação | |-------|-----------| | Rollback falha | Manter últimos 5 backups `dynamic.yml.bak-purge-*` | | Traefik API :8080 fechado externamente | Checks só via SSH CT114 localhost | | Purge longo (>60s) | Jobs async Spec 017 já existem; validate no final | | Race: dois purges simultâneos | Lock file CT114 `/tmp/traefik-dynamic.lock` | --- ## Prioridade no backlog | Fase | Prioridade | Motivo | |------|------------|--------| | **A + B** | **P0** | Evita outage total do onboard | | **C** | P1 | Limpeza completa tenant (nginx, branding) | | **D** | P2 | Observabilidade Desk | --- ## Referências - Incidente: purge `exuberanti.com.br` 2026-06-19 — Traefik 3 routers only - Spec 017 — ordem purge VM112 + histórico Desk - Spec 025 — item backlog «Traefik YAML validation» (consolidar implementação aqui) - Log Traefik: `mapping key "mail-mail-exuberanti-com-br-Router" already defined at line 475` - Fix manual: `dynamic.yml.bak-fix-dup-exuberanti-20260619` --- ## Conclusão (estado actual) | Fase | Entrega | Estado | |------|---------|--------| | A | Remoção YAML estruturada + backup/rollback | ✅ | | B | `traefik_validate` + smoke onboard | ✅ | | C | Purge nginx / branding / wizard store | ✅ (parcial — VM112) | | D | Histórico Desk + alerta ops | 📋 | **Implementação pendente em VM112** — esta spec documenta o backlog acordado com Roger (2026-06-19).