tawasuyu

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 1379Shell 129Rust 79Markdown 732473 Sutichaykuna

⎇ f33df351cfd0c70b5909b01d1f0ff4105adf9010 scripts/mudanza/formatos/arje.py Python · 167 siq'ikuna · 9.4 KB Ñ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 d0e3aadb1 · Sergio ·

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"