`declarar.py` tenía su propio molde de card de arje y `formatos/arje.py` el suyo: el N×M que el
pivote existe para evitar, adentro de mi propio código. Y no quedó en teoría — CADA COPIA TENÍA UN
CAMPO MAL DE LA RAÍZ, Y NINGUNO DE LOS DOS EL MISMO:
campo declarar.py arje.py semilla REAL del producto
provides ["Spawn","Journal"] ✓ [] ✗ ["Spawn","Journal"]
supervision Restart{…} ✗ "OneShot" ✓ "OneShot"
Las consecuencias son concretas: sin `Spawn`/`Journal` las hijas no tienen quién las lance ni dónde
escribir, y una card `Virtual` con `Restart` le pide a arje que respawnee algo que nunca corrió.
Ahora hay un lector `proc` (el censo es un formato de origen, igual que systemd u OpenRC) y el
escritor `arje` es el ÚNICO que emite tarjetas; `declarar.py` queda con lo suyo, el perfil. Tres
lectores en la familia: systemd y openrc leen LO DECLARADO —que es lo que miente, `rc-status` daba
`stopped` para cinco servicios vivos— y `proc` lee LO QUE CORRE, que es donde aparecen los 15
`no-declarado`.
Dos cosas más, las dos sobre no mentir:
· **Una semilla vacía parecería un éxito.** Si ningún servicio tiene `decision = "muda"`, el lector
lo DICE y sale ≠0 en vez de emitir cero tarjetas en silencio. Mismo modo de fallo que un artefacto
vacío en el store.
· **El acta se ahogaba en su propio ruido.** Anotaba el `envp` vacío una vez POR SERVICIO: 26 líneas
idénticas que tapaban los dos hallazgos reales (`shuma-daemon` corre como `sergio`; los que no
tienen cmdline). La limitación es del lector y vale para todos ⇒ una entrada nombrando a los 26.
Un acta donde casi todo es la misma línea se deja de leer, y entonces no queda ningún acta. Y al
revés: las entradas que SÍ son por servicio ahora lo nombran (`gitea: cwd=/var/lib/gitea`).
Controles: la raíz generada coincide campo por campo con la del producto; dos corridas dan el fichero
byte a byte idéntico; 26 tarjetas con 26 ids únicos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
151 lines
8.9 KiB
Python
Executable File
151 lines
8.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""declarar.py — convierte los servicios de un censo en una SEMILLA de arje (SDD 29 etapa 3).
|
||
|
||
El paso que cierra la mudanza. Copiar un binario no lo levanta al arrancar: hay que DECLARARLO. Esto
|
||
toma lo que el censo leyó de `/proc` —la invocación real— y emite `seed.card.json`.
|
||
|
||
── POR QUÉ NO ALCANZA CON `arje-absorb` ────────────────────────────────────────────────────────
|
||
`arje-absorb` ya traduce sysvinit/runit/dinit/openrc a una Semilla, y está bien hecho. Pero lee **la
|
||
declaración del init ajeno**, que es exactamente la que miente: en gioser `rc-status` daba `stopped`
|
||
para cinco servicios que estaban vivos, y los que NADIE declara —los `no-declarado`, que son el
|
||
trabajo real— no aparecen en ninguna. Absorber la declaración reproduce el agujero.
|
||
|
||
Esto lee lo VIVO. Los dos se complementan: absorb para lo declarado, esto para lo que corre.
|
||
|
||
── LAS TRES REGLAS ─────────────────────────────────────────────────────────────────────────────
|
||
1. **Un servicio sin `cmdline` legible NO se emite**, y se dice. Una tarjeta que no puede arrancar
|
||
es peor que una tarjeta ausente: la ausente falla ruidosamente al primer arranque, la rota
|
||
arranca el sistema y deja el servicio caído sin que nada avise.
|
||
2. **El `cwd` se preserva envolviendo**, porque el payload `Native` de arje acepta `exec`/`argv`/
|
||
`envp` y **NO `cwd`** (comprobado sobre la semilla real). Un servicio que depende de su
|
||
directorio —como `puerta-f6e393ff`, que corre con `cwd=/mnt/vvv/tawasuyu/shared/tejido`— arrancaría
|
||
en `/` y fallaría de un modo difícil de atribuir. Se envuelve en `sh -c 'cd … && exec …'`, que es
|
||
el idioma que la propia tarjeta de `sshd` ya usa.
|
||
3. **Los IDs son DETERMINISTAS**, derivados del nombre. El mismo censo produce la misma semilla byte
|
||
a byte, así que dos corridas se pueden comparar con `diff` — que es lo que vuelve revisable un
|
||
fichero generado.
|
||
|
||
⚠ NO se copia el entorno. El censo no lo lee (trae tokens), así que un servicio que dependa de
|
||
variables de entorno va a necesitar que alguien las escriba. Se avisa por nombre.
|
||
|
||
Uso:
|
||
scripts/mudanza/declarar.py --censo censo.toml [--label mi-servidor] --out seed.card.json
|
||
"""
|
||
import argparse, hashlib, json, os, shlex, sys, tomllib
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
from planear import origen_binario, alternativas, _REPO # noqa: E402
|
||
import formatos # noqa: E402
|
||
|
||
def perfil(censo, label, repo):
|
||
"""Emite un `[perfil.<label>]` para `docs/state/targets.toml`.
|
||
|
||
── POR QUÉ UN PERFIL Y NO UNA LISTA DE `install` ────────────────────────────────────────────
|
||
Mudar servicio por servicio a una caja viva es lo que uno haría en cualquier distro, y en takana
|
||
es ir contra el diseño: **el software de una máquina se DECLARA en `targets.toml` y viene en la
|
||
imagen**, reproducible y firmado. Un servidor armado a fuerza de instalaciones sueltas no se
|
||
puede volver a construir — que es exactamente el problema que esta mudanza existe para no repetir
|
||
(el caddy puesto a mano en gioser no tiene dueño ni receta: se pierde con la máquina).
|
||
|
||
Además hay un impedimento medido: `takana install` REPRODUCE desde fuente, y eso exige el LAB
|
||
entero en el cliente (SDD 28 §5.4). Una caja de destino no lo tiene. El perfil esquiva el
|
||
problema por el lado correcto: el software entra al armar la imagen, en el hub, que sí tiene lab.
|
||
|
||
Lo que el perfil NO puede cubrir se lista aparte, con su motivo. Un perfil que se calla lo que le
|
||
falta sale N/N y describe un servidor incompleto — la lección de `foot` en `escritorio-sway`."""
|
||
raices, sin_receta = {}, []
|
||
for s_ in censo.get("servicio", []):
|
||
if s_.get("decision") != "muda":
|
||
continue
|
||
elegido = s_.get("alternativa") or s_["name"]
|
||
clase, detalle = origen_binario(dict(s_, name=elegido), repo)
|
||
if clase == "receta-takana":
|
||
import re as _re
|
||
m = _re.search(r'`recipes/([^`]+)\.toml`', detalle)
|
||
# Se agrupa POR RECETA, no por servicio: dos servicios pueden necesitar la misma
|
||
# (uvicorn y python3 son los dos `python3`), y repetir una raíz en un perfil no la
|
||
# duplica — sólo ensucia y hace creer que son dos cosas distintas.
|
||
receta = m.group(1) if m else elegido
|
||
raices.setdefault(receta, []).append((s_["name"], tuple(s_.get("ports", []))))
|
||
else:
|
||
sin_receta.append((s_["name"], clase, detalle))
|
||
|
||
L = [f"[perfil.{label}]",
|
||
f'descripcion = "servicios mudados desde {censo.get("maquina",{}).get("hostname","?")} '
|
||
f'— generado por scripts/mudanza/declarar.py"',
|
||
'hereda = ["base"]',
|
||
"# Generado desde un CENSO, no escrito a mano: cada raíz está acá porque un servicio que",
|
||
"# CORRÍA en la máquina vieja la necesita, y takana tiene receta para construirla.",
|
||
"paquetes = ["]
|
||
for receta in sorted(raices):
|
||
quienes = raices[receta]
|
||
nombres = ", ".join(sorted({q[0] for q in quienes}))
|
||
puertos = sorted({p for q in quienes for p in q[1]})
|
||
det = nombres + (f" · puertos {','.join(str(x) for x in puertos)}" if puertos else "")
|
||
nota = f" # {det}" if det and det != receta else ""
|
||
L.append(f' "{receta}",{nota}')
|
||
L.append("]")
|
||
return "\n".join(L), sin_receta
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description="Genera una Semilla de arje desde un censo decidido.")
|
||
ap.add_argument("--censo", required=True)
|
||
ap.add_argument("--label", default="takana-servidor", help="etiqueta de la Semilla raíz")
|
||
ap.add_argument("--out", help="fichero de salida (default: stdout)")
|
||
ap.add_argument("--perfil-out", help="además, escribir un [perfil.<label>] para targets.toml")
|
||
a = ap.parse_args()
|
||
|
||
with open(a.censo, "rb") as f:
|
||
censo = tomllib.load(f)
|
||
|
||
# ── las tarjetas las emite el CENTRO, no este guión ─────────────────────────────────────────
|
||
# Antes esto tenía su propio molde de card, y `formatos/arje.py` el suyo. Dos emisores del mismo
|
||
# formato es el N×M que el pivote existe para evitar — y no fue teórico: cada copia tenía un
|
||
# campo mal de la raíz, y ninguno de los dos el mismo. `declarar.py` lee el censo con el lector
|
||
# `proc` y escribe con el escritor `arje`; lo propio que queda acá es el PERFIL.
|
||
lectores, escritores = formatos.cargar()
|
||
sistema = lectores["proc"][0](open(a.censo).read(), a.censo)
|
||
txt = escritores["arje"][0](sistema, a.label)
|
||
hijas = json.loads(txt)["genesis"]
|
||
|
||
print(f"\n══ SEMILLA '{a.label}' ══")
|
||
print(f" {len(hijas)} servicio(s) declarados")
|
||
por = sistema.acta.por_estado()
|
||
for e, titulo in (("SIN-TRADUCIR", "⚠ NO se pudo traducir — trabajo a mano"),
|
||
("decision", "◆ traducido con una DECISIÓN — revisalo")):
|
||
if e in por:
|
||
print(f"\n {titulo} ({len(por[e])}):")
|
||
for x in por[e][:12]:
|
||
print(f" {x['directiva'][:58]}")
|
||
if x["motivo"]:
|
||
print(f" └─ {x['motivo'][:92]}")
|
||
if len(por[e]) > 12:
|
||
print(f" … y {len(por[e])-12} más (usá `traducir.py --from proc --to arje` para verlas)")
|
||
|
||
if a.perfil_out:
|
||
bloque, sin_receta = perfil(censo, a.label, _REPO)
|
||
open(a.perfil_out, "w").write(bloque + "\n")
|
||
n_r = len([l for l in bloque.splitlines() if l.strip().startswith('"')])
|
||
print(f"\n══ PERFIL '{a.label}' ══")
|
||
print(f" {n_r} receta(s) que takana YA construye → {a.perfil_out}")
|
||
print(" Pegalo en docs/state/targets.toml y armá la imagen: el software del servidor nuevo")
|
||
print(" se DECLARA, no se instala a mano. Además `takana install` reproduce desde fuente y")
|
||
print(" eso exige el lab entero en el cliente — el perfil lo resuelve en el hub, que sí lo tiene.")
|
||
if sin_receta:
|
||
print(f"\n ⚠ {len(sin_receta)} servicio(s) que el perfil NO puede cubrir:")
|
||
for nom, cl, det in sin_receta:
|
||
print(f" {nom:22} [{cl}] {det[:84]}")
|
||
print(" Un perfil que se calla lo que le falta sale N/N describiendo un servidor")
|
||
print(" incompleto. Para cada uno: escribir la receta, elegir un equivalente que sí")
|
||
print(" esté, o llevar el binario a mano sabiendo que no se reproduce.")
|
||
if a.out:
|
||
open(a.out, "w").write(txt)
|
||
print(f"\n semilla → {a.out}")
|
||
else:
|
||
print(txt)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|