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…
⎇ f33df351cfd0c70b5909b01d1f0ff4105adf9010
scripts/mudanza/formatos/arje.py
Ñawpa willay
Pi imata llamkarqan
ch'uya
declarar: `/proc` pasa a ser un lector del centro — y había DOS emisores de tarjetas, mal los dos
1"""Escritor de tarjetas de arje ← modelo pivote «servicio» (SDD 29).
2
3Emite una **semilla** (`seed.card.json`): una card `Virtual` raíz cuyos `genesis` son los servicios.
4Es el mismo molde que `declarar.py` saca del censo — la diferencia es de dónde viene el dato: allá de
5`/proc` (lo que CORRE), acá de los units (lo que está DECLARADO). Se complementan, y ninguno de los
6dos solo alcanza: en gioser el init daba `stopped` para cinco servicios vivos.
7
8⚠ **LO QUE ESTE ESCRITOR NO PUEDE Y NO FINGE**
9
10El payload `Native` de arje acepta `exec`, `argv` y `envp` — **y nada más** (comprobado en la semilla
11real del producto, `crates/takana-bootstrap/src/lib.rs`). No hay campo de usuario. Por lo tanto un
12`User=www-data` no tiene traducción: la tarjeta correría el servicio **como root**. Eso no es perder
13una opción, es **escalar privilegios en un fichero generado que nadie vuelve a leer**, así que sale
14como SIN-TRADUCIR y el traductor termina con código ≠ 0.
15
16`Type=forking` es el otro: arje supervisa al proceso que lanza, y un demonio que se va a segundo
17plano y sale deja a su padre muerto a los milisegundos ⇒ **bucle de reinicio**. Casi siempre se
18arregla con la bandera de primer plano del propio demonio (`-g 'daemon off;'`, `-D`, `--foreground`),
19pero cuál es depende del demonio: decirlo es del humano, adivinarlo no.
20"""
21import json, shlex, sys, os
22sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
23from ids import ulid_determinista # noqa: E402
24from formatos import escritor # noqa: E402
25
26SEM = "takana-mudanza"
27
28
29def _tarjeta(srv, acta, desde):
30 argv_all = shlex.split(srv.cmd) if srv.cmd else []
31 if not argv_all:
32 return None
33 if srv.cwd and srv.cwd not in ("/", ""):
34 # `Native` no tiene `cwd`: se envuelve, que es el idioma que la tarjeta de sshd ya usa.
35 exec_, argv = "/bin/sh", ["-c", f"cd {shlex.quote(srv.cwd)} && exec {srv.cmd}"]
36 acta.anota(srv.lineas.get("cwd", srv.origen), "decision", f"{srv.nombre}: cwd={srv.cwd}",
37 "el payload `Native` no tiene `cwd`: se envuelve en `sh -c 'cd … && exec …'`")
38 else:
39 exec_, argv = argv_all[0], argv_all[1:]
40
41 if srv.usuario and srv.usuario != "root":
42 acta.anota(srv.lineas.get("usuario", srv.origen), "SIN-TRADUCIR", f"{srv.nombre}: User={srv.usuario}",
43 "el payload `Native` de arje NO tiene campo de usuario ⇒ esta tarjeta corre el "
44 "servicio COMO ROOT. Hay que bajar el privilegio dentro del comando "
45 "(p. ej. `setpriv --reuid`) antes de desplegarla")
46 if srv.grupo and srv.grupo != "root":
47 acta.anota(srv.lineas.get("grupo", srv.origen), "SIN-TRADUCIR", f"{srv.nombre}: Group={srv.grupo}",
48 "mismo motivo que `User=`: no hay dónde ponerlo en la tarjeta")
49
50 if srv.tipo == "forking":
51 acta.anota(srv.lineas.get("tipo", srv.origen), "SIN-TRADUCIR", f"Type=forking ({srv.nombre})",
52 "arje supervisa al proceso que lanza: un demonio que se va a segundo plano sale "
53 "enseguida y queda en BUCLE DE REINICIO. Hay que agregarle su bandera de primer "
54 "plano, que depende del demonio")
55 elif srv.tipo in ("notify", "dbus"):
56 acta.anota(srv.lineas.get("tipo", srv.origen), "decision", f"Type={srv.tipo} ({srv.nombre})",
57 f"arje no implementa el protocolo de `{srv.tipo}`: se trata como `simple`, o sea "
58 "que se da por arrancado al ejecutarse y no cuando el servicio avisa que está listo")
59
60 # `oneshot` SÍ tiene destino: la semilla real del producto usa `\"supervision\": \"OneShot\"`.
61 if srv.tipo == "oneshot":
62 sup = "OneShot"
63 elif srv.reinicio == "no":
64 sup = "OneShot"
65 acta.anota(srv.lineas.get("reinicio", srv.origen), "decision", f"Restart=no ({srv.nombre})",
66 "sin reinicio, la tarjeta se emite como `OneShot`: arje no lo revive si muere")
67 else:
68 ini = srv.reinicio_ms or 500
69 sup = {"Restart": {"initial": ini, "max": max(ini * 40, 20000)}}
70 if srv.reinicio == "si-falla":
71 acta.anota(srv.lineas.get("reinicio", srv.origen), "decision", f"Restart=si-falla ({srv.nombre})",
72 "arje reinicia SIEMPRE, no sólo tras fallo: una salida limpia también lo revive")
73
74 if srv.entorno_ficheros:
75 acta.anota(srv.lineas.get("entorno_ficheros", srv.origen), "SIN-TRADUCIR", f"EnvironmentFile ({srv.nombre})",
76 "no se copió ninguna variable de esos ficheros: el `envp` de la tarjeta sale "
77 "INCOMPLETO y el servicio va a fallar tarde")
78
79 return {
80 "flow": {"input": [], "output": []},
81 "genesis": [],
82 "id": ulid_determinista(SEM, srv.nombre),
83 "label": srv.nombre,
84 "lifecycle": "daemon",
85 "lineage": None,
86 "payload": {"Native": {"argv": argv, "envp": [f"{k}={v}" for k, v in sorted(srv.entorno.items())],
87 "exec": exec_}},
88 "permissions": {"filesystem": "read-write", "ipc": {"allow": []},
89 "networking": "full", "processes": True},
90 "priority": "normal",
91 "provides": [],
92 # `After=`/`Requires=` se conservan por NOMBRE: arje resuelve el orden por su grafo, y los
93 # objetivos de systemd (`network.target`…) no existen acá — por eso se filtran los `.target`.
94 "requires": sorted({d.replace(".service", "") for d in (srv.tras + srv.necesita)
95 if not d.endswith(".target")}),
96 "schema_version": 1,
97 "soma": {
98 "cgroup": {"cpu_weight": None, "io_weight": None, "path": f"arje.slice/{srv.nombre}"},
99 "cpu_affinity": None,
100 "namespaces": {k: False for k in
101 ("cgroup", "ipc", "mount", "net", "pid", "user", "uts")},
102 "rlimits": {"mem_bytes": None, "nofile": None, "nproc": None},
103 },
104 "supervision": sup,
105 "_mudanza": {"desde": desde, "descripcion": srv.descripcion,
106 "tipo_origen": srv.tipo, "cwd_original": srv.cwd,
107 "usuario_original": srv.usuario, "arranque_auto": srv.arranque_auto},
108 }
109
110
111@escritor("arje", familia="servicio")
112def escribir(sis, etiqueta="mudanza"):
113 acta = sis.acta
114 hijas, vistos = [], {}
115 for srv in sis.servicios:
116 # Dos servicios con el mismo nombre ⇒ el mismo id determinista ⇒ una semilla con dos cards
117 # que se llaman igual. Pasa de verdad: `/etc/init.d` de gioser guarda copias `*.bak-FECHA`
118 # junto a los servicios vivos, y son scripts de OpenRC perfectamente válidos.
119 if srv.nombre in vistos:
120 acta.anota(srv.origen, "SIN-TRADUCIR", srv.nombre,
121 f"nombre repetido (ya salió en la línea {vistos[srv.nombre]}): produciría DOS "
122 "tarjetas con el mismo id. No se emite la segunda — decidí cuál vale y borrá "
123 "la otra (suelen ser copias `*.bak` en el mismo directorio)")
124 continue
125 vistos[srv.nombre] = srv.origen
126 if not srv.arranque_auto:
127 # Un unit instalado pero sin `WantedBy=` no lo levanta el arranque. Declararlo igual
128 # cambiaría el sistema: se emite, pero se dice.
129 acta.anota(srv.origen, "decision", f"{srv.nombre} (sin WantedBy=)",
130 "el unit no se levanta al arrancar en el origen; la semilla SÍ lo va a "
131 "levantar. Sacalo si no lo querés al boot")
132 t = _tarjeta(srv, acta, sis.formato_origen or "?")
133 if t is None:
134 acta.anota(srv.origen, "SIN-TRADUCIR", srv.nombre,
135 "sin comando de arranque: NO se emite tarjeta. Una tarjeta que no arranca es "
136 "peor que ninguna — la ausente falla ruidosamente al primer arranque")
137 continue
138 hijas.append(t)
139
140 hijas.sort(key=lambda t: t["label"]) # orden estable ⇒ `diff` útil entre corridas
141 # ⚠ La raíz se calca de la semilla REAL del producto (`STAGE1_SEED_CARD`), y el calco importa:
142 # cuando esto vivía en dos sitios, cada copia tenía un campo mal y ninguno de los dos igual.
143 # `provides` debe anunciar Spawn y Journal —si no, las hijas no tienen quién las lance ni dónde
144 # escribir— y la supervisión de una card `Virtual` es `OneShot`: no hay proceso que reiniciar,
145 # así que un `Restart` acá le pide a arje que respawnee algo que nunca corrió.
146 raiz = {
147 "flow": {"input": [], "output": []},
148 "genesis": hijas,
149 "id": ulid_determinista(SEM + "-raiz", etiqueta),
150 "label": etiqueta,
151 "lifecycle": "daemon",
152 "lineage": None,
153 "payload": "Virtual",
154 "permissions": {"filesystem": "read-write", "ipc": {"allow": []},
155 "networking": "full", "processes": True},
156 "priority": "normal",
157 "provides": ["Spawn", "Journal"],
158 "requires": [],
159 "schema_version": 1,
160 "soma": {"cgroup": {"cpu_weight": None, "io_weight": None, "path": "arje.slice"},
161 "cpu_affinity": None,
162 "namespaces": {k: False for k in
163 ("cgroup", "ipc", "mount", "net", "pid", "user", "uts")},
164 "rlimits": {"mem_bytes": None, "nofile": None, "nproc": None}},
165 "supervision": "OneShot",
166 }
167 return json.dumps(raiz, indent=1, sort_keys=True, ensure_ascii=False) + "\n"