Inclui console handoff Desk↔Console (Spec 019), melhorias DNS Viewer (037), OpenPanel/Nextcloud/VM116 deploy notes, contracts stack e sidebar actualizado. Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
6.4 KiB
Python
190 lines
6.4 KiB
Python
"""
|
|
Spec 037 — Resolve qual conta Cloudflare Ligbox gere um domínio.
|
|
|
|
Deploy: /opt/ligbox-wizard/backend/app/services/dns_account_registry.py
|
|
Config: /opt/ligbox-wizard/dns-accounts.yaml
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml
|
|
|
|
from app.config import _PROJECT_ROOT, _parse_token_line
|
|
from app.services.cloudflare import CloudflareDNS, CloudflareError
|
|
|
|
_DEFAULT_CONFIG = _PROJECT_ROOT / "dns-accounts.yaml"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LigboxCfAccount:
|
|
id: str
|
|
label: str
|
|
admin_email: str
|
|
cloudflare_account_id: str
|
|
token_file: str
|
|
|
|
def load_token(self) -> str:
|
|
path = _PROJECT_ROOT / self.token_file
|
|
if not path.is_file():
|
|
return ""
|
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|
token = _parse_token_line(line)
|
|
if token:
|
|
return token
|
|
return ""
|
|
|
|
def client(self) -> CloudflareDNS:
|
|
return CloudflareDNS(token=self.load_token())
|
|
|
|
|
|
@dataclass
|
|
class ZoneMatch:
|
|
domain: str
|
|
account: LigboxCfAccount
|
|
zone: dict[str, Any]
|
|
zone_id: str
|
|
|
|
@property
|
|
def dns_mode(self) -> str:
|
|
return f"ligbox_cf_{self.account.id}"
|
|
|
|
|
|
def load_accounts(config_path: Path | None = None) -> tuple[list[LigboxCfAccount], list[str]]:
|
|
path = config_path or _DEFAULT_CONFIG
|
|
if not path.is_file():
|
|
return [], []
|
|
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
order = list(data.get("probe_order") or [])
|
|
accounts: list[LigboxCfAccount] = []
|
|
for raw in data.get("accounts") or []:
|
|
accounts.append(
|
|
LigboxCfAccount(
|
|
id=str(raw["id"]),
|
|
label=str(raw.get("label") or raw["id"]),
|
|
admin_email=str(raw.get("admin_email") or ""),
|
|
cloudflare_account_id=str(raw.get("cloudflare_account_id") or ""),
|
|
token_file=str(raw.get("token_file") or ""),
|
|
)
|
|
)
|
|
by_id = {a.id: a for a in accounts}
|
|
ordered = [by_id[i] for i in order if i in by_id]
|
|
# append any not listed in probe_order
|
|
seen = {a.id for a in ordered}
|
|
for a in accounts:
|
|
if a.id not in seen:
|
|
ordered.append(a)
|
|
return ordered, order
|
|
|
|
|
|
def resolve_ligbox_zone(
|
|
domain: str,
|
|
config_path: Path | None = None,
|
|
) -> ZoneMatch | None:
|
|
"""Procura zona nas contas Ligbox por ordem do catálogo."""
|
|
domain = domain.lower().strip().rstrip(".")
|
|
accounts, _ = load_accounts(config_path)
|
|
errors: list[str] = []
|
|
for account in accounts:
|
|
token = account.load_token()
|
|
if not token:
|
|
errors.append(f"{account.id}: token ausente")
|
|
continue
|
|
try:
|
|
cf = account.client()
|
|
zone = cf.get_zone_by_name(domain)
|
|
if zone and zone.get("id"):
|
|
return ZoneMatch(
|
|
domain=domain,
|
|
account=account,
|
|
zone=zone,
|
|
zone_id=str(zone["id"]),
|
|
)
|
|
except CloudflareError as e:
|
|
errors.append(f"{account.id}: {e}")
|
|
return None
|
|
|
|
|
|
def _load_config_raw(config_path: Path | None = None) -> dict[str, Any]:
|
|
path = config_path or _DEFAULT_CONFIG
|
|
if not path.is_file():
|
|
return {}
|
|
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
|
|
|
|
def pick_provision_account(config_path: Path | None = None) -> LigboxCfAccount | None:
|
|
"""Conta Ligbox onde o wizard cria zona nova do cliente."""
|
|
data = _load_config_raw(config_path)
|
|
default_id = str(data.get("default_provision_account") or "ibytera")
|
|
accounts, _ = load_accounts(config_path)
|
|
by_id = {a.id: a for a in accounts}
|
|
preferred = by_id.get(default_id)
|
|
if preferred and preferred.load_token():
|
|
return preferred
|
|
for account in accounts:
|
|
if account.load_token():
|
|
return account
|
|
return None
|
|
|
|
|
|
def provision_ligbox_zone(
|
|
domain: str,
|
|
account: LigboxCfAccount | None = None,
|
|
config_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Garante zona do cliente na conta CF Ligbox indicada (cria se necessário)."""
|
|
domain = domain.lower().strip().rstrip(".")
|
|
account = account or pick_provision_account(config_path)
|
|
if not account:
|
|
raise CloudflareError("Nenhuma conta Ligbox com token configurado para provisionar zona.")
|
|
cf = account.client()
|
|
existing = cf.get_zone_by_name(domain)
|
|
if existing:
|
|
return {"zone": existing, "created": False, "account": account}
|
|
acc_id = account.cloudflare_account_id or None
|
|
if acc_id and acc_id.startswith("PREENCHER"):
|
|
acc_id = None
|
|
zone = cf.create_zone(domain, account_id=acc_id)
|
|
return {"zone": zone, "created": True, "account": account}
|
|
|
|
|
|
def resolve_payload(domain: str, config_path: Path | None = None) -> dict[str, Any]:
|
|
match = resolve_ligbox_zone(domain, config_path)
|
|
provision_acct = pick_provision_account(config_path)
|
|
base = {
|
|
"domain": domain.lower().strip().rstrip("."),
|
|
"paths_available": ["provision_ligbox", "byo", "external"],
|
|
}
|
|
if match:
|
|
return {
|
|
**base,
|
|
"matched": True,
|
|
"account_id": match.account.id,
|
|
"account_label": f"{match.account.label} ({match.account.admin_email})",
|
|
"zone_id": match.zone_id,
|
|
"zone_status": match.zone.get("status"),
|
|
"dns_mode": match.dns_mode,
|
|
"can_provision_ligbox": True,
|
|
"provision_account_id": match.account.id,
|
|
"paths_available": ["apply_ligbox", "provision_ligbox", "byo", "external"],
|
|
"message": (
|
|
f"Domínio encontrado na conta Cloudflare {match.account.label}. "
|
|
"Pode aplicar apontamentos Ligbox automaticamente."
|
|
),
|
|
}
|
|
prov_id = provision_acct.id if provision_acct else None
|
|
return {
|
|
**base,
|
|
"matched": False,
|
|
"dns_mode": f"ligbox_cf_{prov_id}" if prov_id else None,
|
|
"can_provision_ligbox": provision_acct is not None,
|
|
"provision_account_id": prov_id,
|
|
"message": (
|
|
"Domínio novo — o wizard pode criar a zona na Cloudflare gerenciada pela Ligbox "
|
|
f"({provision_acct.label}) ou usar BYO Cloudflare / registrador."
|
|
if provision_acct
|
|
else "Configure tokens Ligbox para provisionar zona, ou use BYO/registrador."
|
|
),
|
|
}
|