planear: alternativas por familia funcional, con una recomendación que no es gusto
Un servicio casi nunca es insustituible. `planear.py` agrupa por familia (servidor web, base de datos, caché, forja git, contenedores, dns, correo, base vectorial), muestra qué equivalentes tiene takana —marcando sellado / receta sin sellar / no está— y recomienda. Pero no «el mejor» en abstracto: un gusto disfrazado de dato es peor que no recomendar. Los criterios son hechos comprobables, en orden: 1. **El que ya corre, si takana lo construye** — porque cambiar de servidor web no es cambiar un binario: es REESCRIBIR la configuración entera, y eso casi siempre pesa más que cualquier ventaja teórica del otro. 2. Si el que corre no está en el catálogo pero un equivalente sí, se recomienda ése (takana puede construirlo, firmarlo y reproducirlo) diciendo lo que cuesta. 3. Si no hay ninguno, se dice. **No se sugiere «usá otro» cuando ese otro tampoco está.** Probado contra el catálogo real: `caddy` → se queda (ya corre y está sellado); `nginx` → recomienda caddy nombrando el costo; `redis` → «recomendado: NINGUNO», porque ni redis ni valkey ni memcached están en el corpus. ⚠ Y el dato que la recomendación destapa, que vale más que la recomendación: de servidores web el corpus sólo tiene **caddy y traefik**. No hay nginx ni apache. Si se elige un equivalente queda en el censo (`alternativa = "…"`) y el plan lo dice en su paso, con la advertencia arriba de todo: la configuración del origen no sirve tal cual. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
@@ -28,6 +28,8 @@ Uso:
|
||||
"""
|
||||
import argparse, os, sys, shlex, time, tomllib
|
||||
|
||||
_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# ── tamaños ─────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
_MUL = {"K": 1 / 1024, "M": 1, "G": 1024, "T": 1024 * 1024}
|
||||
@@ -103,6 +105,74 @@ def _no_aplica(nombre):
|
||||
return None
|
||||
|
||||
|
||||
# ── familias de equivalentes funcionales ────────────────────────────────────────────────────────
|
||||
# Agrupa cosas que hacen EL MISMO trabajo, para poder ofrecer alternativas. Es conocimiento de
|
||||
# dominio, no gusto: que nginx y caddy sirvan HTTP es un hecho. Lo que NO se decide acá es cuál es
|
||||
# mejor — eso sale de datos medibles (¿está en el catálogo? ¿está sellado?) y del costo de cambiar.
|
||||
FAMILIAS = {
|
||||
"servidor web / proxy": ["caddy", "nginx", "apache", "httpd", "lighttpd", "traefik", "haproxy"],
|
||||
"base de datos": ["postgresql", "mariadb", "mysql", "sqlite"],
|
||||
"caché en memoria": ["redis", "valkey", "memcached"],
|
||||
"forja git": ["gitea", "forgejo", "gitlab"],
|
||||
"contenedores": ["docker", "podman", "containerd"],
|
||||
"dns": ["unbound", "bind", "dnsmasq", "coredns"],
|
||||
"correo": ["postfix", "exim", "opensmtpd"],
|
||||
"base vectorial": ["qdrant", "milvus", "weaviate"],
|
||||
}
|
||||
|
||||
|
||||
def _receta(nombre, repo):
|
||||
"""(existe, sellado) para un nombre de receta."""
|
||||
import glob
|
||||
f = os.path.join(repo, "recipes", f"{nombre}.toml")
|
||||
if not os.path.exists(f):
|
||||
return False, False
|
||||
return True, bool(glob.glob(os.path.join(repo, "store", f"*-{nombre}")))
|
||||
|
||||
|
||||
def alternativas(nombre, repo):
|
||||
"""Equivalentes funcionales del servicio, con una recomendación RAZONADA.
|
||||
|
||||
── LOS CRITERIOS, Y POR QUÉ ÉSOS ────────────────────────────────────────────────────────────
|
||||
No se recomienda «el mejor» en abstracto: eso es gusto, y un gusto disfrazado de dato es peor que
|
||||
no recomendar. Se recomienda con hechos comprobables y en este orden:
|
||||
|
||||
1. **El que YA CORRE, si takana lo construye.** Cambiar de servidor web no es cambiar un
|
||||
binario: es **reescribir la configuración entera**. Ése es el costo real y casi siempre
|
||||
supera cualquier ventaja teórica del otro.
|
||||
2. Si el que corre NO está en el catálogo pero un equivalente sí, se recomienda el equivalente
|
||||
— porque es el que takana puede construir, firmar y reproducir. Y se dice lo que cuesta.
|
||||
3. Si no hay ninguno, se dice que hay que escribir la receta. No se sugiere «usá otro» sin
|
||||
decir que ese otro tampoco está.
|
||||
|
||||
Devuelve `(lista, recomendado, motivo)` o `(None, None, None)` si el servicio no cae en ninguna
|
||||
familia conocida — que es lo normal y no es un error."""
|
||||
n = nombre.lower().replace("_", "-")
|
||||
fam = next((f for f, miembros in FAMILIAS.items() if n in miembros), None)
|
||||
if not fam:
|
||||
return None, None, None
|
||||
lista = []
|
||||
for cand in FAMILIAS[fam]:
|
||||
existe, sellado = _receta(cand, repo)
|
||||
lista.append({"nombre": cand, "receta": existe, "sellado": sellado, "actual": cand == n})
|
||||
yo = next((x for x in lista if x["actual"]), None)
|
||||
disponibles = [x for x in lista if x["sellado"]]
|
||||
|
||||
if yo and yo["sellado"]:
|
||||
return (lista, n,
|
||||
f"es el que YA corre y takana lo construye (sellado). Cambiar a otro de la familia "
|
||||
f"«{fam}» no es cambiar un binario: obliga a REESCRIBIR la configuración entera")
|
||||
if disponibles:
|
||||
otros = ", ".join(x["nombre"] for x in disponibles)
|
||||
return (lista, disponibles[0]["nombre"],
|
||||
f"el que corre ({n}) no está en el catálogo de takana; de la familia «{fam}» sí "
|
||||
f"están sellados: {otros}. Cambiar implica reescribir la configuración — es una "
|
||||
f"decisión, no un trámite")
|
||||
return (lista, None,
|
||||
f"de la familia «{fam}» NO hay ninguno en el catálogo de takana: ni {n} ni sus "
|
||||
f"equivalentes. Hay que escribir la receta del que se elija")
|
||||
|
||||
|
||||
def origen_binario(e, repo=None):
|
||||
"""De dónde sale el binario, y por lo tanto QUÉ HAY QUE HACER para tenerlo en el destino.
|
||||
|
||||
@@ -260,6 +330,14 @@ def revisar(censo, interactivo=True):
|
||||
if t == "servicio" and e.get("decision") != "muere" and g != "muere":
|
||||
oc, od = origen_binario(e)
|
||||
print(f" └─ binario: [{oc}] {od[:96]}")
|
||||
lista, rec, motivo = alternativas(n, _REPO)
|
||||
if lista:
|
||||
op = " ".join(
|
||||
("**" + x["nombre"] + "**" if x["nombre"] == rec else x["nombre"])
|
||||
+ ("✓" if x["sellado"] else ("·" if x["receta"] else "✗"))
|
||||
for x in lista)
|
||||
print(f" └─ alternativas: {op} (✓ sellado · receta sin sellar · ✗ no está)")
|
||||
print(f" recomendado: {rec or 'ninguno'} — {motivo[:110]}")
|
||||
|
||||
if not interactivo:
|
||||
return censo
|
||||
@@ -279,6 +357,18 @@ def revisar(censo, interactivo=True):
|
||||
resp = input(f" {t:8} {n:30} {sug} {e.get('_motivo','')[:60]}\n"
|
||||
f" [m]uda/[x]muere/Enter={rec or 'nada'} > ").strip().lower()
|
||||
e["decision"] = {"m": "muda", "x": "muere", "": rec}.get(resp, "")
|
||||
if e["decision"] == "muda" and t == "servicio":
|
||||
lista, alt_rec, motivo = alternativas(n, _REPO)
|
||||
if lista and len([x for x in lista if x["sellado"]]) > 1:
|
||||
op = " ".join(f"[{i}]{x['nombre']}" + ("✓" if x["sellado"] else "")
|
||||
for i, x in enumerate(lista))
|
||||
print(f" alternativas: {op}")
|
||||
print(f" recomendado: {alt_rec} — {motivo[:100]}")
|
||||
r2 = input(f" número, o Enter para {alt_rec}: ").strip()
|
||||
if r2.isdigit() and int(r2) < len(lista):
|
||||
e["alternativa"] = lista[int(r2)]["nombre"]
|
||||
else:
|
||||
e["alternativa"] = alt_rec
|
||||
return censo
|
||||
|
||||
|
||||
@@ -350,7 +440,11 @@ def pasos(censo, target, key):
|
||||
for s in svc_muda:
|
||||
nd = s.get("class") == "no-declarado"
|
||||
paso("servicio", f"declarar y levantar {s['name']}" + (" ⚠ NO-DECLARADO" if nd else ""),
|
||||
(f"# DE DÓNDE SALE EL BINARIO: {origen_binario(s)[0]}\n"
|
||||
((f"# ⚠ SE ELIGIÓ UN EQUIVALENTE: {s['alternativa']} en vez de {s['name']}.\n"
|
||||
f"# Eso NO es cambiar un binario: hay que REESCRIBIR la configuración del servicio.\n"
|
||||
f"# La del origen no sirve tal cual.\n#\n")
|
||||
if s.get("alternativa") and s["alternativa"] != s["name"] else "")
|
||||
+ (f"# DE DÓNDE SALE EL BINARIO: {origen_binario(s)[0]}\n"
|
||||
f"# {origen_binario(s)[1]}\n"
|
||||
f"#\n"
|
||||
f"# CÓMO CORRE HOY EN EL ORIGEN (leído de /proc, es lo que hay que reproducir):\n"
|
||||
|
||||
Reference in New Issue
Block a user