#!/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, shlex, sys, tomllib CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" # sin I, L, O, U def ulid_determinista(sem, nombre): """26 caracteres Crockford base32, derivados del nombre. No es un ULID con marca de tiempo real —no hace falta— pero respeta el formato, y ser determinista es lo que permite `diff`.""" h = hashlib.blake2b(f"{sem}/{nombre}".encode(), digest_size=16).digest() n = int.from_bytes(h, "big") out = [] for _ in range(26): out.append(CROCKFORD[n & 31]) n >>= 5 s = "".join(reversed(out)) return "0" + s[1:] # el primer carácter de un ULID válido es <= '7' def tarjeta(nombre, cmdline, cwd, puertos): """Una entrada `genesis`. El molde sale de la semilla real del producto.""" argv_all = shlex.split(cmdline) if not argv_all: return None if cwd and cwd not in ("/", ""): # Regla 2: `Native` no tiene `cwd`. Se envuelve, igual que la tarjeta de sshd del producto. exec_, argv = "/bin/sh", ["-c", f"cd {shlex.quote(cwd)} && exec {cmdline}"] else: exec_, argv = argv_all[0], argv_all[1:] return { "flow": {"input": [], "output": []}, "genesis": [], "id": ulid_determinista("takana-mudanza", nombre), "label": nombre, "lifecycle": "daemon", "lineage": None, "payload": {"Native": {"argv": argv, "envp": [], "exec": exec_}}, "permissions": {"filesystem": "read-write", "ipc": {"allow": []}, "networking": "full", "processes": True}, "priority": "normal", "provides": [], "requires": [], "schema_version": 1, "soma": { "cgroup": {"cpu_weight": None, "io_weight": None, "path": f"arje.slice/{nombre}"}, "cpu_affinity": None, "namespaces": {k: False for k in ("cgroup", "ipc", "mount", "net", "pid", "user", "uts")}, "rlimits": {"mem_bytes": None, "nofile": None, "nproc": None}, }, "supervision": {"Restart": {"initial": 500, "max": 20000}}, "_mudanza": {"puertos": puertos, "cwd_original": cwd}, } 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)") a = ap.parse_args() with open(a.censo, "rb") as f: censo = tomllib.load(f) hijas, sin_cmdline, envueltos = [], [], [] for s in censo.get("servicio", []): if s.get("decision") != "muda": continue cmd = (s.get("cmdline") or "").strip() if not cmd: # Regla 1: no se inventa una invocación. sin_cmdline.append(s["name"]) continue t = tarjeta(s["name"], cmd, (s.get("cwd") or "").strip(), s.get("ports", [])) if t is None: sin_cmdline.append(s["name"]) continue if t["payload"]["Native"]["exec"] == "/bin/sh": envueltos.append(s["name"]) hijas.append(t) hijas.sort(key=lambda t: t["label"]) # orden estable ⇒ `diff` útil semilla = { "flow": {"input": [], "output": []}, "genesis": hijas, "id": ulid_determinista("takana-mudanza-raiz", a.label), "label": a.label, "lifecycle": "daemon", "lineage": None, "payload": "Virtual", "permissions": {"filesystem": "read-write", "ipc": {"allow": []}, "networking": "full", "processes": True}, "priority": "normal", "provides": ["Spawn", "Journal"], "requires": [], "schema_version": 1, "soma": { "cgroup": {"cpu_weight": None, "io_weight": None, "path": "arje.slice"}, "cpu_affinity": None, "namespaces": {k: False for k in ("cgroup", "ipc", "mount", "net", "pid", "user", "uts")}, "rlimits": {"mem_bytes": None, "nofile": None, "nproc": None}, }, "supervision": {"Restart": {"initial": 500, "max": 20000}}, } txt = json.dumps(semilla, indent=1, sort_keys=True) + "\n" print(f"\n══ SEMILLA '{a.label}' ══") print(f" {len(hijas)} servicio(s) declarados") if envueltos: print(f" {len(envueltos)} envuelto(s) en `sh -c 'cd … && exec …'` porque dependen de su " f"directorio\n (el payload Native de arje no tiene `cwd`): {' '.join(envueltos)}") if sin_cmdline: print(f"\n ⚠ {len(sin_cmdline)} servicio(s) NO se declararon: no se pudo leer su cmdline.") print(f" {' '.join(sin_cmdline)}") print(" Una tarjeta que no puede arrancar es PEOR que una ausente: la ausente falla") print(" al primer arranque, la rota deja el servicio caído sin que nada avise.") print(" ⚠ El entorno (`envp`) va VACÍO: el censo no lee /proc//environ porque trae") print(" tokens. Un servicio que dependa de variables va a necesitar que alguien las escriba.") if a.out: open(a.out, "w").write(txt) print(f"\n semilla → {a.out}") else: print(txt) if __name__ == "__main__": main()