gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1471Shell 174Rust 105Markdown 953567 Definitions

⎇ 0e5e3f8c837a10294e0b0e5dfa0f6d4afd0f1603 scripts/mudanza/declarar.py Python · 217 lines · 11.5 KB History Who touched what raw

traducir: familia «servicio» — systemd → tarjeta de arje, y el pivote pasa a ser uno POR FAMILIA fefa92ec5 · Sergio ·

1#!/usr/bin/env python3
2"""declarar.py — convierte los servicios de un censo en una SEMILLA de arje (SDD 29 etapa 3).
3
4El paso que cierra la mudanza. Copiar un binario no lo levanta al arrancar: hay que DECLARARLO. Esto
5toma lo que el censo leyó de `/proc` —la invocación real— y emite `seed.card.json`.
6
7── POR QUÉ NO ALCANZA CON `arje-absorb` ────────────────────────────────────────────────────────
8`arje-absorb` ya traduce sysvinit/runit/dinit/openrc a una Semilla, y está bien hecho. Pero lee **la
9declaración del init ajeno**, que es exactamente la que miente: en gioser `rc-status` daba `stopped`
10para cinco servicios que estaban vivos, y los que NADIE declara —los `no-declarado`, que son el
11trabajo real— no aparecen en ninguna. Absorber la declaración reproduce el agujero.
12
13Esto lee lo VIVO. Los dos se complementan: absorb para lo declarado, esto para lo que corre.
14
15── LAS TRES REGLAS ─────────────────────────────────────────────────────────────────────────────
161. **Un servicio sin `cmdline` legible NO se emite**, y se dice. Una tarjeta que no puede arrancar
17   es peor que una tarjeta ausente: la ausente falla ruidosamente al primer arranque, la rota
18   arranca el sistema y deja el servicio caído sin que nada avise.
192. **El `cwd` se preserva envolviendo**, porque el payload `Native` de arje acepta `exec`/`argv`/
20   `envp` y **NO `cwd`** (comprobado sobre la semilla real). Un servicio que depende de su
21   directorio —como `puerta-f6e393ff`, que corre con `cwd=/mnt/vvv/tawasuyu/shared/tejido`— arrancaría
22   en `/` y fallaría de un modo difícil de atribuir. Se envuelve en `sh -c 'cd … && exec …'`, que es
23   el idioma que la propia tarjeta de `sshd` ya usa.
243. **Los IDs son DETERMINISTAS**, derivados del nombre. El mismo censo produce la misma semilla byte
25   a byte, así que dos corridas se pueden comparar con `diff` — que es lo que vuelve revisable un
26   fichero generado.
27
28⚠ NO se copia el entorno. El censo no lo lee (trae tokens), así que un servicio que dependa de
29variables de entorno va a necesitar que alguien las escriba. Se avisa por nombre.
30
31Uso:
32    scripts/mudanza/declarar.py --censo censo.toml [--label mi-servidor] --out seed.card.json
33"""
34import argparse, hashlib, json, os, shlex, sys, tomllib
35
36sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
37from planear import origen_binario, alternativas, _REPO   # noqa: E402
38from ids import ulid_determinista                          # noqa: E402
39
40def tarjeta(nombre, cmdline, cwd, puertos):
41    """Una entrada `genesis`. El molde sale de la semilla real del producto."""
42    argv_all = shlex.split(cmdline)
43    if not argv_all:
44        return None
45    if cwd and cwd not in ("/", ""):
46        # Regla 2: `Native` no tiene `cwd`. Se envuelve, igual que la tarjeta de sshd del producto.
47        exec_, argv = "/bin/sh", ["-c", f"cd {shlex.quote(cwd)} && exec {cmdline}"]
48    else:
49        exec_, argv = argv_all[0], argv_all[1:]
50    return {
51        "flow": {"input": [], "output": []},
52        "genesis": [],
53        "id": ulid_determinista("takana-mudanza", nombre),
54        "label": nombre,
55        "lifecycle": "daemon",
56        "lineage": None,
57        "payload": {"Native": {"argv": argv, "envp": [], "exec": exec_}},
58        "permissions": {"filesystem": "read-write", "ipc": {"allow": []},
59                        "networking": "full", "processes": True},
60        "priority": "normal",
61        "provides": [],
62        "requires": [],
63        "schema_version": 1,
64        "soma": {
65            "cgroup": {"cpu_weight": None, "io_weight": None, "path": f"arje.slice/{nombre}"},
66            "cpu_affinity": None,
67            "namespaces": {k: False for k in
68                           ("cgroup", "ipc", "mount", "net", "pid", "user", "uts")},
69            "rlimits": {"mem_bytes": None, "nofile": None, "nproc": None},
70        },
71        "supervision": {"Restart": {"initial": 500, "max": 20000}},
72        "_mudanza": {"puertos": puertos, "cwd_original": cwd},
73    }
74
75
76def perfil(censo, label, repo):
77    """Emite un `[perfil.<label>]` para `docs/state/targets.toml`.
78
79    ── POR QUÉ UN PERFIL Y NO UNA LISTA DE `install` ────────────────────────────────────────────
80    Mudar servicio por servicio a una caja viva es lo que uno haría en cualquier distro, y en takana
81    es ir contra el diseño: **el software de una máquina se DECLARA en `targets.toml` y viene en la
82    imagen**, reproducible y firmado. Un servidor armado a fuerza de instalaciones sueltas no se
83    puede volver a construir — que es exactamente el problema que esta mudanza existe para no repetir
84    (el caddy puesto a mano en gioser no tiene dueño ni receta: se pierde con la máquina).
85
86    Además hay un impedimento medido: `takana install` REPRODUCE desde fuente, y eso exige el LAB
87    entero en el cliente (SDD 28 §5.4). Una caja de destino no lo tiene. El perfil esquiva el
88    problema por el lado correcto: el software entra al armar la imagen, en el hub, que sí tiene lab.
89
90    Lo que el perfil NO puede cubrir se lista aparte, con su motivo. Un perfil que se calla lo que le
91    falta sale N/N y describe un servidor incompleto — la lección de `foot` en `escritorio-sway`."""
92    raices, sin_receta = {}, []
93    for s_ in censo.get("servicio", []):
94        if s_.get("decision") != "muda":
95            continue
96        elegido = s_.get("alternativa") or s_["name"]
97        clase, detalle = origen_binario(dict(s_, name=elegido), repo)
98        if clase == "receta-takana":
99            import re as _re
100            m = _re.search(r'`recipes/([^`]+)\.toml`', detalle)
101            # Se agrupa POR RECETA, no por servicio: dos servicios pueden necesitar la misma
102            # (uvicorn y python3 son los dos `python3`), y repetir una raíz en un perfil no la
103            # duplica — sólo ensucia y hace creer que son dos cosas distintas.
104            receta = m.group(1) if m else elegido
105            raices.setdefault(receta, []).append((s_["name"], tuple(s_.get("ports", []))))
106        else:
107            sin_receta.append((s_["name"], clase, detalle))
108
109    L = [f"[perfil.{label}]",
110         f'descripcion = "servicios mudados desde {censo.get("maquina",{}).get("hostname","?")} '
111         f'— generado por scripts/mudanza/declarar.py"',
112         'hereda = ["base"]',
113         "# Generado desde un CENSO, no escrito a mano: cada raíz está acá porque un servicio que",
114         "# CORRÍA en la máquina vieja la necesita, y takana tiene receta para construirla.",
115         "paquetes = ["]
116    for receta in sorted(raices):
117        quienes = raices[receta]
118        nombres = ", ".join(sorted({q[0] for q in quienes}))
119        puertos = sorted({p for q in quienes for p in q[1]})
120        det = nombres + (f" · puertos {','.join(str(x) for x in puertos)}" if puertos else "")
121        nota = f"  # {det}" if det and det != receta else ""
122        L.append(f'  "{receta}",{nota}')
123    L.append("]")
124    return "\n".join(L), sin_receta
125
126
127def main():
128    ap = argparse.ArgumentParser(description="Genera una Semilla de arje desde un censo decidido.")
129    ap.add_argument("--censo", required=True)
130    ap.add_argument("--label", default="takana-servidor", help="etiqueta de la Semilla raíz")
131    ap.add_argument("--out", help="fichero de salida (default: stdout)")
132    ap.add_argument("--perfil-out", help="además, escribir un [perfil.<label>] para targets.toml")
133    a = ap.parse_args()
134
135    with open(a.censo, "rb") as f:
136        censo = tomllib.load(f)
137
138    hijas, sin_cmdline, envueltos = [], [], []
139    for s in censo.get("servicio", []):
140        if s.get("decision") != "muda":
141            continue
142        cmd = (s.get("cmdline") or "").strip()
143        if not cmd:
144            # Regla 1: no se inventa una invocación.
145            sin_cmdline.append(s["name"])
146            continue
147        t = tarjeta(s["name"], cmd, (s.get("cwd") or "").strip(), s.get("ports", []))
148        if t is None:
149            sin_cmdline.append(s["name"])
150            continue
151        if t["payload"]["Native"]["exec"] == "/bin/sh":
152            envueltos.append(s["name"])
153        hijas.append(t)
154
155    hijas.sort(key=lambda t: t["label"])    # orden estable ⇒ `diff` útil
156    semilla = {
157        "flow": {"input": [], "output": []},
158        "genesis": hijas,
159        "id": ulid_determinista("takana-mudanza-raiz", a.label),
160        "label": a.label,
161        "lifecycle": "daemon",
162        "lineage": None,
163        "payload": "Virtual",
164        "permissions": {"filesystem": "read-write", "ipc": {"allow": []},
165                        "networking": "full", "processes": True},
166        "priority": "normal",
167        "provides": ["Spawn", "Journal"],
168        "requires": [],
169        "schema_version": 1,
170        "soma": {
171            "cgroup": {"cpu_weight": None, "io_weight": None, "path": "arje.slice"},
172            "cpu_affinity": None,
173            "namespaces": {k: False for k in
174                           ("cgroup", "ipc", "mount", "net", "pid", "user", "uts")},
175            "rlimits": {"mem_bytes": None, "nofile": None, "nproc": None},
176        },
177        "supervision": {"Restart": {"initial": 500, "max": 20000}},
178    }
179
180    txt = json.dumps(semilla, indent=1, sort_keys=True) + "\n"
181    print(f"\n══ SEMILLA '{a.label}' ══")
182    print(f"   {len(hijas)} servicio(s) declarados")
183    if envueltos:
184        print(f"   {len(envueltos)} envuelto(s) en `sh -c 'cd … && exec …'` porque dependen de su "
185              f"directorio\n     (el payload Native de arje no tiene `cwd`): {' '.join(envueltos)}")
186    if sin_cmdline:
187        print(f"\n   ⚠ {len(sin_cmdline)} servicio(s) NO se declararon: no se pudo leer su cmdline.")
188        print(f"     {' '.join(sin_cmdline)}")
189        print("     Una tarjeta que no puede arrancar es PEOR que una ausente: la ausente falla")
190        print("     al primer arranque, la rota deja el servicio caído sin que nada avise.")
191    print("   ⚠ El entorno (`envp`) va VACÍO: el censo no lee /proc/<pid>/environ porque trae")
192    print("     tokens. Un servicio que dependa de variables va a necesitar que alguien las escriba.")
193    if a.perfil_out:
194        bloque, sin_receta = perfil(censo, a.label, _REPO)
195        open(a.perfil_out, "w").write(bloque + "\n")
196        n_r = len([l for l in bloque.splitlines() if l.strip().startswith('"')])
197        print(f"\n══ PERFIL '{a.label}' ══")
198        print(f"   {n_r} receta(s) que takana YA construye → {a.perfil_out}")
199        print("   Pegalo en docs/state/targets.toml y armá la imagen: el software del servidor nuevo")
200        print("   se DECLARA, no se instala a mano. Además `takana install` reproduce desde fuente y")
201        print("   eso exige el lab entero en el cliente — el perfil lo resuelve en el hub, que sí lo tiene.")
202        if sin_receta:
203            print(f"\n   ⚠ {len(sin_receta)} servicio(s) que el perfil NO puede cubrir:")
204            for nom, cl, det in sin_receta:
205                print(f"     {nom:22} [{cl}] {det[:84]}")
206            print("     Un perfil que se calla lo que le falta sale N/N describiendo un servidor")
207            print("     incompleto. Para cada uno: escribir la receta, elegir un equivalente que sí")
208            print("     esté, o llevar el binario a mano sabiendo que no se reproduce.")
209    if a.out:
210        open(a.out, "w").write(txt)
211        print(f"\n   semilla → {a.out}")
212    else:
213        print(txt)
214
215
216if __name__ == "__main__":
217    main()