297 lines
10 KiB
Python
297 lines
10 KiB
Python
"""Proxy VM112 domínios orquestrados + limpeza Desk (Spec 017)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sqlite3
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from app import auth
|
|
|
|
VM112_API = os.getenv("VM112_API_URL", "http://10.10.10.112:8090")
|
|
VM112_ADMIN_API_KEY = os.getenv("VM112_ADMIN_API_KEY", "ibytera-corp-api-key-change-later")
|
|
|
|
PURGE_BLOCKLIST = frozenset({"ligbox.com.br", "itecnologys.com"})
|
|
|
|
VM112_PURGE_STEP_LABELS = (
|
|
"Contas Carbonio (zmprov da)",
|
|
"Domínio Carbonio (zmprov dd)",
|
|
"Portal users Self-Service",
|
|
"Pasta ligbox-sites",
|
|
"Zona Cloudflare Ibytera",
|
|
"Traefik / SNI CT114",
|
|
"Logs de sessão wizard",
|
|
)
|
|
|
|
|
|
def _ts() -> str:
|
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
|
def _timeline_entry(label: str, status: str, detail: str = "") -> dict[str, str]:
|
|
return {"at": _ts(), "label": label, "status": status, "detail": detail}
|
|
|
|
|
|
def _vm112_headers() -> dict[str, str]:
|
|
return {"X-Api-Key": VM112_ADMIN_API_KEY}
|
|
|
|
|
|
def verify_root_password(conn: sqlite3.Connection, password: str) -> bool:
|
|
row = conn.execute(
|
|
"SELECT password_hash FROM desk_users WHERE username = 'root' AND active = 1"
|
|
).fetchone()
|
|
if not row or not row["password_hash"]:
|
|
return False
|
|
return auth.verify_password(password, row["password_hash"])
|
|
|
|
|
|
def delete_carbonio_account(email: str) -> dict[str, Any]:
|
|
"""Remove uma conta Carbonio (zmprov da) — Spec 022."""
|
|
email = email.lower().strip()
|
|
if "@" not in email:
|
|
raise ValueError("e-mail inválido")
|
|
domain = email.split("@", 1)[1]
|
|
if domain in PURGE_BLOCKLIST:
|
|
raise ValueError(f"Domínio protegido: {domain}")
|
|
with httpx.Client(timeout=120.0) as client:
|
|
r = client.post(
|
|
f"{VM112_API}/api/admin/accounts/{email}/delete",
|
|
headers=_vm112_headers(),
|
|
)
|
|
if r.status_code == 404:
|
|
return {"ok": True, "email": email, "message": "Conta já não existia no Carbonio", "skipped": True}
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
return {
|
|
"ok": True,
|
|
"email": email,
|
|
"message": data.get("message") or f"Conta {email} removida",
|
|
"detail": data,
|
|
}
|
|
|
|
|
|
def list_domains(query: str = "") -> dict[str, Any]:
|
|
with httpx.Client(timeout=60.0) as client:
|
|
r = client.get(
|
|
f"{VM112_API}/api/admin/domains",
|
|
params={"q": query} if query else None,
|
|
headers=_vm112_headers(),
|
|
)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def get_domain(domain: str) -> dict[str, Any]:
|
|
domain = domain.lower().strip()
|
|
with httpx.Client(timeout=180.0) as client:
|
|
r = client.get(
|
|
f"{VM112_API}/api/admin/domains/{domain}",
|
|
headers=_vm112_headers(),
|
|
)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
|
|
def domain_exists_on_vm112(domain: str) -> bool:
|
|
"""True se o domínio ainda consta na lista orquestrada VM112."""
|
|
domain = domain.lower().strip()
|
|
try:
|
|
data = list_domains()
|
|
items = data.get("domains") if isinstance(data, dict) else data
|
|
if not isinstance(items, list):
|
|
return True
|
|
for item in items:
|
|
name = item.get("domain") if isinstance(item, dict) else item
|
|
if str(name or "").lower().strip() == domain:
|
|
return True
|
|
return False
|
|
except Exception:
|
|
# VM112 indisponível — não assumir removido durante poll
|
|
return True
|
|
|
|
|
|
|
|
def start_purge_vm112(domain: str) -> dict[str, Any]:
|
|
"""Inicia purge assíncrono na VM112 (Spec 017 Fase 3)."""
|
|
domain = domain.lower().strip()
|
|
with httpx.Client(timeout=60.0) as client:
|
|
r = client.post(
|
|
f"{VM112_API}/api/admin/domains/{domain}/purge",
|
|
headers=_vm112_headers(),
|
|
)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def poll_purge_vm112_job(job_id: str) -> dict[str, Any]:
|
|
with httpx.Client(timeout=60.0) as client:
|
|
r = client.get(
|
|
f"{VM112_API}/api/admin/domains/purge-jobs/{job_id}",
|
|
headers=_vm112_headers(),
|
|
)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def vm112_job_steps_timeline(job: dict[str, Any]) -> list[dict[str, str]]:
|
|
"""Passos individuais VM112 durante execução (Fase 3)."""
|
|
out: list[dict[str, str]] = []
|
|
for step in job.get("steps") or []:
|
|
if not isinstance(step, dict):
|
|
continue
|
|
st = str(step.get("status") or "pending")
|
|
if st == "pending":
|
|
continue
|
|
label = str(step.get("label") or "Passo VM112")
|
|
if st == "done":
|
|
status = "ok"
|
|
elif st == "error":
|
|
status = "fail"
|
|
else:
|
|
status = "running"
|
|
detail = str(step.get("detail") or "")
|
|
at = step.get("finished_at") or step.get("started_at") or _ts()
|
|
out.append({"at": at, "label": label, "status": status, "detail": detail})
|
|
return out
|
|
|
|
|
|
def purge_vm112_with_poll(domain: str, poll_interval: float = 1.5, timeout: float = 600.0):
|
|
"""Generator: (event_type, payload) — passos em tempo real + resultado final."""
|
|
import time
|
|
|
|
started = start_purge_vm112(domain)
|
|
job_id = started.get("job_id")
|
|
if not job_id:
|
|
yield ("final", started)
|
|
return
|
|
|
|
t0 = time.monotonic()
|
|
deadline = t0 + timeout
|
|
seen = 0
|
|
while time.monotonic() < deadline:
|
|
job = poll_purge_vm112_job(job_id)
|
|
steps = vm112_job_steps_timeline(job)
|
|
if len(steps) > seen:
|
|
for step in steps[seen:]:
|
|
yield ("step", step)
|
|
seen = len(steps)
|
|
status = job.get("status")
|
|
if status == "completed":
|
|
yield (
|
|
"final",
|
|
{
|
|
"ok": True,
|
|
"job_id": job_id,
|
|
"steps": steps,
|
|
"result": job.get("result") or {},
|
|
},
|
|
)
|
|
return
|
|
if status == "failed":
|
|
yield (
|
|
"final",
|
|
{
|
|
"ok": False,
|
|
"job_id": job_id,
|
|
"steps": steps,
|
|
"error": job.get("error") or "Purge VM112 falhou",
|
|
"result": job.get("result") or {},
|
|
},
|
|
)
|
|
return
|
|
yield ("heartbeat", {"elapsed": int(time.monotonic() - t0), "job_id": job_id})
|
|
time.sleep(poll_interval)
|
|
|
|
yield ("final", {"ok": False, "error": "Timeout purge VM112", "job_id": job_id})
|
|
|
|
|
|
def purge_vm112(domain: str) -> dict[str, Any]:
|
|
domain = domain.lower().strip()
|
|
for kind, payload in purge_vm112_with_poll(domain):
|
|
if kind == "final":
|
|
return payload
|
|
return {"ok": False, "error": "Purge VM112 sem resposta"}
|
|
|
|
|
|
def vm112_purge_timeline(vm112_result: dict[str, Any]) -> list[dict[str, str]]:
|
|
"""Converte resposta VM112 em linhas de timeline."""
|
|
raw_steps = vm112_result.get("steps")
|
|
if isinstance(raw_steps, list) and raw_steps:
|
|
out: list[dict[str, str]] = []
|
|
for step in raw_steps:
|
|
if not isinstance(step, dict):
|
|
continue
|
|
label = str(step.get("label") or step.get("name") or "Passo VM112")
|
|
ok = step.get("ok", step.get("success", True))
|
|
status = "ok" if ok else "fail"
|
|
detail = str(step.get("message") or step.get("detail") or "")
|
|
at = step.get("at") or _ts()
|
|
out.append({"at": at, "label": label, "status": status, "detail": detail})
|
|
return out
|
|
if vm112_result.get("ok") is False:
|
|
return [
|
|
_timeline_entry(
|
|
"Purge VM112",
|
|
"fail",
|
|
str(vm112_result.get("message") or vm112_result.get("error") or "falhou"),
|
|
)
|
|
]
|
|
return [_timeline_entry("Purge VM112", "ok", "Orquestração VM112 concluída")]
|
|
|
|
|
|
def purge_desk_records(conn: sqlite3.Connection, domain: str) -> dict[str, int]:
|
|
domain = domain.lower().strip()
|
|
like = f"%{domain}%"
|
|
counts = {}
|
|
counts["webhook_events"] = conn.execute(
|
|
"DELETE FROM webhook_events WHERE payload LIKE ?", (like,)
|
|
).rowcount
|
|
counts["tickets"] = conn.execute(
|
|
"DELETE FROM tickets WHERE subject LIKE ? OR payload LIKE ?", (like, like)
|
|
).rowcount
|
|
counts["audit_domains"] = conn.execute(
|
|
"DELETE FROM audit_domains WHERE domain = ?", (domain,)
|
|
).rowcount
|
|
counts["assist_sessions"] = conn.execute(
|
|
"DELETE FROM assist_sessions WHERE domain = ?", (domain,)
|
|
).rowcount
|
|
counts["audit_checks"] = conn.execute(
|
|
"DELETE FROM audit_checks WHERE domain = ?", (domain,)
|
|
).rowcount
|
|
conn.commit()
|
|
return counts
|
|
|
|
|
|
def purge_desk_timeline(conn: sqlite3.Connection, domain: str) -> tuple[dict[str, int], list[dict[str, str]]]:
|
|
"""Purge Desk com uma linha de timeline por tabela."""
|
|
domain = domain.lower().strip()
|
|
like = f"%{domain}%"
|
|
timeline: list[dict[str, str]] = []
|
|
counts: dict[str, int] = {}
|
|
|
|
desk_steps = (
|
|
("Desk — webhook_events", "webhook_events", "DELETE FROM webhook_events WHERE payload LIKE ?", (like,)),
|
|
("Desk — tickets", "tickets", "DELETE FROM tickets WHERE subject LIKE ? OR payload LIKE ?", (like, like)),
|
|
("Desk — audit_domains", "audit_domains", "DELETE FROM audit_domains WHERE domain = ?", (domain,)),
|
|
("Desk — assist_sessions", "assist_sessions", "DELETE FROM assist_sessions WHERE domain = ?", (domain,)),
|
|
("Desk — audit_checks", "audit_checks", "DELETE FROM audit_checks WHERE domain = ?", (domain,)),
|
|
)
|
|
for label, key, sql, params in desk_steps:
|
|
n = conn.execute(sql, params).rowcount
|
|
counts[key] = n
|
|
timeline.append(_timeline_entry(label, "ok", f"{n} registo(s) removido(s)"))
|
|
conn.commit()
|
|
return counts, timeline
|
|
|
|
|
|
def build_purge_timeline(vm112_result: dict[str, Any], desk_counts: dict[str, int], desk_timeline: list[dict[str, str]]) -> list[dict[str, str]]:
|
|
timeline = [_timeline_entry("Validação Root + confirmação", "ok")]
|
|
timeline.extend(vm112_purge_timeline(vm112_result))
|
|
timeline.extend(desk_timeline)
|
|
total_desk = sum(desk_counts.values())
|
|
timeline.append(_timeline_entry("Purge concluído", "ok", f"Desk: {total_desk} registo(s)"))
|
|
return timeline
|