Copiar un binario no lo levanta al arrancar. Esto toma la invocación que el censo leyó de `/proc` y emite `seed.card.json`. Medido sobre gioser: **37 de 37 servicios vivos declarados, cero fallos**, y la tarjeta de caddy sale con su invocación real — exactamente lo que faltaba para que no muriera en el próximo reinicio. Por qué no alcanza con `arje-absorb`: absorb lee la DECLARACIÓN del init ajeno, que es justo la que miente (`rc-status` daba `stopped` para cinco servicios vivos), y los `no-declarado` no aparecen en ninguna declaración por definición. Absorber la declaración reproduce el agujero. Se complementan. Tres reglas: 1. Un servicio sin `cmdline` legible NO se emite y se dice: una tarjeta que no arranca es peor que una ausente — la ausente falla ruidosamente, la rota deja el servicio caído en silencio. 2. El `cwd` se preserva ENVOLVIENDO, porque el payload `Native` acepta `exec`/`argv`/`envp` y no `cwd` (comprobado sobre la semilla real). 9 de 37 servicios de gioser dependen de su directorio. 3. IDs deterministas ⇒ misma entrada, misma semilla BYTE A BYTE (verificado con sha256). Un fichero generado que cambia en cada corrida no se puede revisar con `diff`. **Y un bug del censo que costaba el 70 % del dato**: `cmdline` y `cwd` se leían en un solo comando, y como `readlink /proc/<pid>/cwd` exige permiso de ptrace, en cualquier proceso de root el comando entero salía ≠0 y se descartaba TAMBIÉN el `cmdline`. 26 de 37 servicios quedaban sin invocación. Sondas separadas. **Una sonda que falla es un dato; no puede arrastrar a las que funcionaron.** Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
162 lines
7.7 KiB
Python
Executable File
162 lines
7.7 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, 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/<pid>/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()
|