Files
ThothII/harness/tht/cli/db_cmd.py
T

148 lines
5.8 KiB
Python

from pathlib import Path
import typer
from tht.adapters.factory import build_dwh
from tht.cli.config_cmd import CONFIG_OPT
from tht.config import ConfigError, load_config
from tht.db.fetch_ca import CaFetchError, describe_pem, fetch_chain_pem, parse_host_port
db_app = typer.Typer(help="Operazioni sul database target")
@db_app.command("ping")
def ping_cmd(config: Path = CONFIG_OPT) -> None:
"""Testa la connessione e verifica che l'utente sia effettivamente read-only."""
try:
cfg = load_config(config)
except ConfigError as e:
typer.secho(f"ERRORE: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(code=1)
health = build_dwh(cfg).health()
if not health.ok:
typer.secho(f"ERRORE di connessione: {health.detail}", fg=typer.colors.RED, err=True)
raise typer.Exit(code=1)
if health.endpoint:
typer.secho(f"OK: connesso via REST a {health.endpoint} (schema {health.schema})",
fg=typer.colors.GREEN)
typer.secho("OK: accesso read-only garantito dall'API (solo SELECT/WITH).",
fg=typer.colors.GREEN)
return
typer.secho(f"OK: connesso a {health.database} (schema {health.schema})",
fg=typer.colors.GREEN)
if not health.read_only:
typer.secho(
f"ERRORE: l'utente '{cfg.database.user}' NON e' read-only.", fg=typer.colors.RED, err=True
)
if health.writable_tables:
typer.echo(f" Tabelle scrivibili: {', '.join(health.writable_tables[:10])}", err=True)
if health.can_create:
typer.echo(f" L'utente puo' creare oggetti nello schema {health.schema}.", err=True)
typer.echo(" Crea un ruolo read-only con scripts/create_readonly_role.sql.", err=True)
raise typer.Exit(code=2)
typer.secho("OK: l'utente e' read-only sullo schema target.", fg=typer.colors.GREEN)
@db_app.command("fetch-ca")
def fetch_ca_cmd(
config: Path = CONFIG_OPT,
out: Path = typer.Option(
Path("config/ca-chain.pem"), "--out", "-o", help="File PEM di destinazione."
),
) -> None:
"""Scarica la catena CA presentata dal server REST e la salva in un bundle PEM.
Utile sulle postazioni *workstation* dietro una CA interna: il file va poi puntato
con `THT_SSL_CA` nel `.env` (lo consuma `requests`). NON tocca il trust store dell'OS.
"""
try:
cfg = load_config(config)
except ConfigError as e:
typer.secho(f"ERRORE: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(code=1)
if cfg.rest is None:
typer.secho(
"ERRORE: comando pensato per postazioni con accesso REST. "
"Configura la sezione `rest` (base_url) nel workspace yaml.",
fg=typer.colors.RED, err=True,
)
raise typer.Exit(code=1)
# Host del DWH REST; se il vector REST è su host diverso, aggiungi anche quello.
targets: list[tuple[str, int]] = [parse_host_port(cfg.rest.base_url)]
if cfg.vector_rest is not None:
vec = parse_host_port(cfg.vector_rest.base_url)
if vec not in targets:
targets.append(vec)
pems: list[str] = []
seen: set[str] = set()
try:
for host, port in targets:
typer.echo(f"Recupero catena TLS da {host}:{port} ...")
for pem in fetch_chain_pem(host, port, cfg.rest.timeout):
key = pem.strip()
if key not in seen:
seen.add(key)
pems.append(pem if pem.endswith("\n") else pem + "\n")
except CaFetchError as e:
typer.secho(f"ERRORE: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(code=1)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text("".join(pems))
abs_out = out.resolve()
typer.secho(
f"OK: salvati {len(pems)} certificati -> {abs_out}", fg=typer.colors.GREEN
)
typer.echo("Controlla che corrispondano alla CA interna attesa:")
for i, pem in enumerate(pems, 1):
desc = describe_pem(pem)
if desc:
typer.echo(f" [{i}] {desc}")
typer.echo(
"NOTA: il recupero non verifica la fiducia; la verifica avviene al prossimo "
"`tht db ping`, quando THT_SSL_CA punta a questo bundle."
)
# Controlla se la config *effettiva* (THT_SSL_CA nel .env + ssl_ca nel workspace yaml)
# gia' punta al bundle appena salvato: in caso contrario, il certificato non e'
# ancora collegato e il `db ping` fallirebbe ancora con certificate verify failed.
endpoints = [("rest", cfg.rest)]
if cfg.vector_rest is not None:
endpoints.append(("vector_rest", cfg.vector_rest))
not_wired = [name for name, ep in endpoints if not _points_to(ep.ssl_ca, abs_out)]
if not not_wired:
typer.secho(
"OK: THT_SSL_CA punta gia' a questo bundle (rest"
+ (", vector_rest" if cfg.vector_rest is not None else "")
+ "). Lancia `tht db ping`.",
fg=typer.colors.GREEN,
)
return
typer.secho(
"\nATTENZIONE: il certificato non e' ancora collegato "
f"(ssl_ca di {', '.join(not_wired)} non punta a questo bundle). Per attivarlo:",
fg=typer.colors.YELLOW,
)
typer.echo(f" 1. nel .env imposta: THT_SSL_CA={abs_out}")
typer.echo(
" 2. in il workspace yaml, sotto `rest:`"
+ (" e `vector_rest:`" if cfg.vector_rest is not None else "")
+ ", decommenta/aggiungi:"
)
typer.echo(" ssl_ca: ${THT_SSL_CA}")
typer.echo("Poi rilancia `tht db ping`.")
def _points_to(ssl_ca: str | None, target: Path) -> bool:
"""True se `ssl_ca` (path risolto) coincide col bundle appena salvato."""
if not ssl_ca:
return False
try:
return Path(ssl_ca).resolve() == target
except (OSError, ValueError):
return False