mudanza: de dónde sale cada binario — y la herramienta corrigió un error mío
«Instalá el paquete» es lo que uno ya sabía. La respuesta útil sale de cruzar dos hechos: quién posee el fichero en el ORIGEN (el censo se lo pregunta al gestor de paquetes de esa máquina, en un lote) y si el corpus de takana tiene una receta con ese nombre. receta-takana → instalarlo del repo firmado, NO copiar el binario paquete-ajeno → escribir receta, o qorpa (ADR 0015) suelto → nadie lo provee: llevarlo con su entorno o escribirle receta Medido sobre gioser (37 servicios vivos): 4 receta-takana · 4 paquete-ajeno · **11 SUELTOS** · 7 sin binario legible. Los sueltos viven en `/usr/local/bin` o en un home (`qdrant`, `matilda`, `pacha-secretos`, `act_runner`, `shuma-gateway`) y son los que se pierden al apagar el origen. Sorpresa medida: `/usr/bin/caddy` tampoco tiene dueño — está puesto a mano. **Y corrigió un error mío**: en el SDD 28 escribí que `gitea` no tenía receta y que «es la que más peso tiene». Las dos cosas falsas — `recipes/gitea.toml` existe y está sellada. Lo afirmé de memoria; el programa fue a mirar. Corregido allá con la nota. **Y un fallo del clasificador, que vale como regla**: calculaba la raíz del repo con un `dirname` de menos, no encontraba ninguna receta y contestaba `suelto` A TODO, incluido `caddy`. Un clasificador que contesta siempre lo mismo no clasifica. Ahora falla ruidosamente si no encuentra el catálogo, en vez de dar una respuesta falsa con forma de respuesta. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
@@ -975,10 +975,20 @@ una receta candidata con un caso de uso comprobado detrás:
|
||||
|---|---|
|
||||
| `caddy` | ✅ existe (`recipes/caddy.toml`) |
|
||||
| `openssh` | ✅ existe |
|
||||
| `gitea` | ✗ — y es la que más peso tiene: aloja el `origin` de takana |
|
||||
| `gitea` | ✅ **SÍ existe** (`recipes/gitea.toml`) **y está sellada** (`35bb4f04…`) — ver la corrección abajo |
|
||||
| `qdrant`, `squid`, `fail2ban`, `php-fpm`, `metalog`, `glances` | ✗ |
|
||||
| `chrony`, `nftables`, `cronie`, `logrotate` | ✗ — ya declaradas `wanted` en `perfil.servidor` |
|
||||
|
||||
> **CORRECCIÓN (2026-09-11).** Esta tabla decía que `gitea` NO tenía receta y que «es la que más
|
||||
> peso tiene». **Las dos cosas eran falsas**: `recipes/gitea.toml` existe desde el 2026-09-09 y su
|
||||
> artefacto está sellado. Lo escribí de memoria, sin mirar el catálogo.
|
||||
>
|
||||
> Lo destapó `planear.py` al cruzar cada binario contra `recipes/`: de los servicios de gioser, **4
|
||||
> ya tienen receta takana y están sellados** (`caddy`, `gitea`, `python3`…), 4 vienen de un paquete
|
||||
> ajeno y **11 son binarios sueltos sin dueño**. Ésos últimos son el trabajo real, no gitea.
|
||||
>
|
||||
> Es exactamente para esto que la herramienta existe: yo afirmé de memoria y el programa fue a mirar.
|
||||
|
||||
**Cada una que se cierre se prueba sola**: el servicio o levanta en la caja nueva o no, y eso se ve
|
||||
el mismo día. Es la diferencia entre una receta «sellada» y una receta *usada*.
|
||||
|
||||
|
||||
@@ -187,6 +187,32 @@ comando entero salía ≠0 y se descartaba **también el `cmdline`**, que sí se
|
||||
servicios quedaban sin invocación —el dato que da sentido al censo— y el fallo se leía como «no se
|
||||
pudo». **Una sonda que falla es un dato; no puede arrastrar a las que funcionaron.**
|
||||
|
||||
### 4.0 bis · De dónde sale cada binario — «instalá el paquete» no es una instrucción
|
||||
|
||||
El paso de servicio decía «instalá el binario/paquete en el destino», que es lo que uno ya sabía. La
|
||||
respuesta útil sale de cruzar **dos hechos**: quién posee el fichero en el ORIGEN (se lo pregunta el
|
||||
censo al gestor de paquetes de esa máquina, en un solo lote) y si el corpus de takana tiene una
|
||||
receta con ese nombre. Tres casos, tres trabajos distintos:
|
||||
|
||||
| clase | qué significa | qué hay que hacer |
|
||||
|---|---|---|
|
||||
| `receta-takana` | takana ya sabe construirlo | instalarlo del repo firmado — **no** copiar el binario |
|
||||
| `paquete-ajeno` | lo trae el gestor del origen | escribir una receta, o `qorpa` (ADR 0015) |
|
||||
| `suelto` | **nadie lo provee**: puesto a mano | llevarlo con su entorno, o escribirle receta |
|
||||
|
||||
**Medido sobre gioser (37 servicios vivos):** 4 `receta-takana` · 4 `paquete-ajeno` · **11 `suelto`**
|
||||
· 7 sin binario legible. Los sueltos viven en `/usr/local/bin` o en un home — `qdrant`, `matilda`,
|
||||
`pacha-secretos`, `act_runner`, `shuma-gateway`— y **son los que se pierden al apagar el origen**.
|
||||
|
||||
⚠ **Y corrigió un error mío.** En el SDD 28 escribí que `gitea` no tenía receta y que «es la que más
|
||||
peso tiene». Las dos cosas eran falsas: `recipes/gitea.toml` existe y está sellada. Lo afirmé de
|
||||
memoria; el programa fue a mirar. Es exactamente para esto que existe.
|
||||
|
||||
⚠ **Y un fallo del propio clasificador, que vale como regla**: calculaba la raíz del repo con un
|
||||
`dirname` de menos, así que no encontraba ninguna receta y **contestaba `suelto` a todo** — incluido
|
||||
`caddy`. Un clasificador que contesta siempre lo mismo no clasifica. Ahora falla ruidosamente si no
|
||||
encuentra el catálogo, en vez de dar una respuesta falsa con forma de respuesta.
|
||||
|
||||
### 4.1 El aplicador *(implementado: `scripts/mudanza/aplicar.py`)*
|
||||
|
||||
`aplicar.py --plan plan.toml [--dry-run] [--only <clase>] [--paso N] [--hecho N]`
|
||||
|
||||
@@ -104,6 +104,38 @@ def invocaciones(t, pids):
|
||||
return out
|
||||
|
||||
|
||||
def duenos(t, ejecutables):
|
||||
"""¿Quién PROVEE el binario de cada servicio? Determinista, preguntándole al gestor de paquetes
|
||||
del origen.
|
||||
|
||||
Es lo que convierte «instalá el paquete» —que es lo que uno ya sabía— en una instrucción: o el
|
||||
binario lo trae un paquete con nombre, o NO lo trae nadie y entonces es un compilado suelto que
|
||||
hay que llevar o reconstruir. La diferencia decide el trabajo, y no se adivina.
|
||||
|
||||
Una consulta por lote, no una por servicio: son decenas de servicios y cada `ssh` cuesta."""
|
||||
if not ejecutables:
|
||||
return {}
|
||||
lista = " ".join(shlex.quote(e) for e in sorted(set(ejecutables)))
|
||||
# Se prueba el gestor que exista. `2>/dev/null || echo -` ⇒ un fichero sin dueño no aborta el lote.
|
||||
guion = (
|
||||
'for f in %s; do '
|
||||
' if command -v pacman >/dev/null 2>&1; then o=$(pacman -Qoq "$f" 2>/dev/null); '
|
||||
' elif command -v dpkg >/dev/null 2>&1; then o=$(dpkg -S "$f" 2>/dev/null | cut -d: -f1); '
|
||||
' elif command -v apk >/dev/null 2>&1; then o=$(apk info -W "$f" 2>/dev/null | tail -1 '
|
||||
' | sed "s/.* is owned by //;s/-[0-9].*//"); '
|
||||
' elif command -v rpm >/dev/null 2>&1; then o=$(rpm -qf "$f" 2>/dev/null); '
|
||||
' else o=""; fi; '
|
||||
' echo "$f|${o:-}"; done'
|
||||
) % lista
|
||||
out, _ = t.run(guion)
|
||||
res = {}
|
||||
for l in out.splitlines():
|
||||
if "|" in l:
|
||||
f, o = l.split("|", 1)
|
||||
res[f.strip()] = o.strip()
|
||||
return res
|
||||
|
||||
|
||||
def procesos_vivos(t):
|
||||
"""Lo que REALMENTE corre, colgado de PID 1. La verdad, no la declaración."""
|
||||
out, _ = t.run("ps -eo pid,ppid,comm 2>/dev/null | awk '$2==1 && $1!=1 {print $1\" \"$3}'")
|
||||
@@ -389,6 +421,8 @@ def emitir_toml(c):
|
||||
L.append(f"ports = {json.dumps([p['port'] for p in c['puertos'] if p['proc'].startswith(s['name'][:15])])}")
|
||||
L.append(f"cmdline = {toml_escape(s.get('cmdline',''))}")
|
||||
L.append(f"cwd = {toml_escape(s.get('cwd',''))}")
|
||||
L.append(f"exec = {toml_escape(s.get('exec',''))}")
|
||||
L.append(f"paquete = {toml_escape(s.get('paquete',''))}")
|
||||
L.append('decision = "" # muda | muere | (vacío = sin decidir)')
|
||||
L.append("")
|
||||
for d in c["dominios"]:
|
||||
@@ -474,6 +508,12 @@ def main():
|
||||
i = inv.get(e['name'], {})
|
||||
e['cmdline'] = i.get('cmdline', '')
|
||||
e['cwd'] = i.get('cwd', '')
|
||||
ejes = [ (e['cmdline'] or '').split(' ')[0] for e in svc if (e.get('cmdline') or '').startswith('/') ]
|
||||
dn = duenos(t, ejes)
|
||||
for e in svc:
|
||||
exe = (e.get('cmdline') or '').split(' ')[0]
|
||||
e['exec'] = exe if exe.startswith('/') else ''
|
||||
e['paquete'] = dn.get(exe, '')
|
||||
pts = puertos(t)
|
||||
nombres = dominios(t)
|
||||
sondear = (a.probe or bool(a.host)) and not a.no_probe
|
||||
|
||||
@@ -103,6 +103,51 @@ def _no_aplica(nombre):
|
||||
return None
|
||||
|
||||
|
||||
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.
|
||||
|
||||
«Instalá el paquete» es lo que uno ya sabía. Esto responde otra cosa, cruzando dos hechos: quién
|
||||
posee el fichero en el ORIGEN (se lo preguntó el censo a su gestor de paquetes) y si el corpus de
|
||||
takana tiene una receta con ese nombre. Los tres casos piden trabajo distinto:
|
||||
|
||||
· `receta-takana` — lo mejor: takana ya sabe construirlo. Se instala del repo firmado.
|
||||
· `paquete-ajeno` — lo trae el gestor del origen. En el destino hace falta una receta, o
|
||||
`qorpa` (ADR 0015) si se acepta una jaula con la distro ajena.
|
||||
· `suelto` — NADIE lo provee: un binario puesto a mano. Hay que LLEVARLO con su árbol
|
||||
o escribirle una receta. Es el que se pierde al apagar el origen.
|
||||
|
||||
Medido en gioser: 13 servicios con paquete y **13 binarios sueltos** — `caddy`, `qdrant`,
|
||||
`matilda`, `act_runner`, `pacha-secretos`… en `/usr/local/bin` o en un home. La mitad."""
|
||||
nombre, exe, pkg = e.get("name", ""), e.get("exec", ""), e.get("paquete", "")
|
||||
# scripts/mudanza/planear.py → scripts/mudanza → scripts → RAÍZ. Con un `dirname` de menos
|
||||
# apuntaba a `scripts/`, no encontraba ninguna receta, y TODO salía `suelto` — incluido `caddy`,
|
||||
# que sí tiene `recipes/caddy.toml`. Un clasificador que contesta siempre lo mismo no clasifica.
|
||||
repo = repo or os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
if not os.path.isdir(os.path.join(repo, "recipes")):
|
||||
# Falla ruidosamente: sin el catálogo no se puede distinguir `receta-takana` de `suelto`, y
|
||||
# contestar `suelto` a todo sería una respuesta falsa con forma de respuesta.
|
||||
raise SystemExit(f"!! no encuentro el catálogo de recetas en {repo}/recipes — "
|
||||
f"pasá --repo o corré desde el repo")
|
||||
for cand in (nombre, nombre.replace("_", "-"), os.path.basename(exe or "")):
|
||||
if cand and os.path.exists(os.path.join(repo, "recipes", f"{cand}.toml")):
|
||||
import glob
|
||||
sellado = bool(glob.glob(os.path.join(repo, "store", f"*-{cand}")))
|
||||
return ("receta-takana",
|
||||
f"takana YA lo construye: `recipes/{cand}.toml`"
|
||||
+ (" (sellado en el store)" if sellado else " (sin sellar: hay que construirlo)")
|
||||
+ f" ⇒ instalarlo del repo firmado, no copiar el binario de {exe or '?'}")
|
||||
if pkg:
|
||||
return ("paquete-ajeno",
|
||||
f"lo provee el gestor del origen: paquete `{pkg}`. En el destino hace falta una "
|
||||
f"receta takana, o `qorpa` si se acepta una jaula con la distro ajena (ADR 0015)")
|
||||
if exe:
|
||||
return ("suelto",
|
||||
f"NADIE lo provee: binario puesto a mano en {exe}. Hay que LLEVARLO (con lo que "
|
||||
f"necesite alrededor) o escribirle una receta — es de los que se pierden al apagar "
|
||||
f"el origen")
|
||||
return ("desconocido", "no se pudo leer su binario: hace falta averiguarlo a mano")
|
||||
|
||||
|
||||
def recomendar(tipo, e, datos_que_mueren=()):
|
||||
"""Devuelve `(decision, motivo)`. **Recomendación, nunca decisión**: se imprime con su razón para
|
||||
que aceptarla sea barato y equivocarse sea caro de no notar.
|
||||
@@ -212,6 +257,9 @@ def revisar(censo, interactivo=True):
|
||||
print(f" {t:8} {n:30} {_etiqueta(t,n,e):>18}")
|
||||
if e.get("_motivo"):
|
||||
print(f" └─ {e['_motivo']}")
|
||||
if t == "servicio" and e.get("decision") != "muere" and g != "muere":
|
||||
oc, od = origen_binario(e)
|
||||
print(f" └─ binario: [{oc}] {od[:96]}")
|
||||
|
||||
if not interactivo:
|
||||
return censo
|
||||
@@ -302,11 +350,14 @@ 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"# CÓMO CORRE HOY EN EL ORIGEN (leído de /proc, es lo que hay que reproducir):\n"
|
||||
(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"
|
||||
f"# cmdline: {s.get('cmdline') or '(no se pudo leer)'}\n"
|
||||
+ (f"# cwd : {s['cwd']}\n" if s.get("cwd") else "")
|
||||
+ (f"# puertos: {', '.join(str(p) for p in s['ports'])}\n" if s.get("ports") else "")
|
||||
+ f"#\n# 1) instalar el binario/paquete de {s['name']} en el destino\n"
|
||||
+ f"#\n# 1) conseguir el binario según lo de arriba\n"
|
||||
f"# 2) DECLARARLO en el init del destino con ESA línea de comando\n"
|
||||
f"# (arje: una tarjeta en /ente/seed.card.json · systemd: una unit)\n"
|
||||
f"# 3) arrancarlo"),
|
||||
|
||||
Reference in New Issue
Block a user