`openrc → arje` sin escribir ningún traductor de ese par: cuarto plugin, cuarto par. OpenRC es el init de gioser, la máquina que esta mudanza termina borrando. **El hecho que manda acá: un servicio de OpenRC es un PROGRAMA, no una declaración.** Un unit de systemd se lee; un script de OpenRC puede hacer cualquier cosa antes de arrancar nada. Medido sobre el `/etc/init.d` real: **60 de 121 definen su propia `start()`/`stop()`**. En ésos no hay `command=` que valga — leer las variables y emitir tarjeta daría un servicio que arranca OTRA COSA. Regla al revés que en systemd: `start()` propia ⇒ SIN-TRADUCIR y NO se emite tarjeta. Los números del corpus real cierran solos: 121 − 1 (no es openrc-run) − 60 (start propia) − 1 (sin `command=`) = 59, y el lector emitió exactamente 59, con 59 ids únicos. Dos cosas que OpenRC obliga a ir a buscar fuera del script: `/etc/conf.d/<x>`, donde viven los argumentos de verdad (a diferencia del `EnvironmentFile=` de systemd, éste SÍ está en la máquina: se lee y se aplica), y `/etc/runlevels/`, que es lo único que dice si el servicio arranca solo. **Tres defectos que sólo aparecieron corriendo contra las 121 de verdad**, no sobre un ejemplo mío: - `name=` no es un identificador sino un rótulo humano: en gioser vale «Aura Backend», con espacio, y se iba al id de la tarjeta y al path del cgroup. El identificador es el nombre del fichero. - Ids repetidos: `/etc/init.d` guarda copias `*.bak-FECHA` junto a los servicios vivos y son scripts válidos; dos con el mismo nombre dan el MISMO id determinista ⇒ semilla con dos cards homónimas. El escritor lo detecta y no emite la segunda. - La tarjeta decía `"desde": "systemd"` viniendo de OpenRC: el campo que existe para saber de dónde salió algo era justo el que mentía. Estaba cableado. Y el centro deja de filtrar la entrada por extensión: los servicios de OpenRC no tienen ninguna, y filtrar en el centro es que lo que no entra se pierda EN SILENCIO. Filtra el lector, que sabe, y lo anota — así un `.bak` en `/etc/init.d` se REPORTA en vez de desaparecer. Controles: dos corridas dan el fichero byte a byte idéntico; los tres pares previos sin regresión (`nginx → caddy` sigue dando `Valid configuration`, `systemd → arje` sigue emitiendo sus 3 tarjetas). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
162 lines
8.8 KiB
Python
162 lines
8.8 KiB
Python
"""Escritor de tarjetas de arje ← modelo pivote «servicio» (SDD 29).
|
|
|
|
Emite una **semilla** (`seed.card.json`): una card `Virtual` raíz cuyos `genesis` son los servicios.
|
|
Es el mismo molde que `declarar.py` saca del censo — la diferencia es de dónde viene el dato: allá de
|
|
`/proc` (lo que CORRE), acá de los units (lo que está DECLARADO). Se complementan, y ninguno de los
|
|
dos solo alcanza: en gioser el init daba `stopped` para cinco servicios vivos.
|
|
|
|
⚠ **LO QUE ESTE ESCRITOR NO PUEDE Y NO FINGE**
|
|
|
|
El payload `Native` de arje acepta `exec`, `argv` y `envp` — **y nada más** (comprobado en la semilla
|
|
real del producto, `crates/takana-bootstrap/src/lib.rs`). No hay campo de usuario. Por lo tanto un
|
|
`User=www-data` no tiene traducción: la tarjeta correría el servicio **como root**. Eso no es perder
|
|
una opción, es **escalar privilegios en un fichero generado que nadie vuelve a leer**, así que sale
|
|
como SIN-TRADUCIR y el traductor termina con código ≠ 0.
|
|
|
|
`Type=forking` es el otro: arje supervisa al proceso que lanza, y un demonio que se va a segundo
|
|
plano y sale deja a su padre muerto a los milisegundos ⇒ **bucle de reinicio**. Casi siempre se
|
|
arregla con la bandera de primer plano del propio demonio (`-g 'daemon off;'`, `-D`, `--foreground`),
|
|
pero cuál es depende del demonio: decirlo es del humano, adivinarlo no.
|
|
"""
|
|
import json, shlex, sys, os
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
from ids import ulid_determinista # noqa: E402
|
|
from formatos import escritor # noqa: E402
|
|
|
|
SEM = "takana-mudanza"
|
|
|
|
|
|
def _tarjeta(srv, acta, desde):
|
|
argv_all = shlex.split(srv.cmd) if srv.cmd else []
|
|
if not argv_all:
|
|
return None
|
|
if srv.cwd and srv.cwd not in ("/", ""):
|
|
# `Native` no tiene `cwd`: se envuelve, que es el idioma que la tarjeta de sshd ya usa.
|
|
exec_, argv = "/bin/sh", ["-c", f"cd {shlex.quote(srv.cwd)} && exec {srv.cmd}"]
|
|
acta.anota(srv.lineas.get("cwd", srv.origen), "decision", f"WorkingDirectory={srv.cwd}",
|
|
"el payload `Native` no tiene `cwd`: se envuelve en `sh -c 'cd … && exec …'`")
|
|
else:
|
|
exec_, argv = argv_all[0], argv_all[1:]
|
|
|
|
if srv.usuario and srv.usuario != "root":
|
|
acta.anota(srv.lineas.get("usuario", srv.origen), "SIN-TRADUCIR", f"User={srv.usuario}",
|
|
"el payload `Native` de arje NO tiene campo de usuario ⇒ esta tarjeta corre el "
|
|
"servicio COMO ROOT. Hay que bajar el privilegio dentro del comando "
|
|
"(p. ej. `setpriv --reuid`) antes de desplegarla")
|
|
if srv.grupo and srv.grupo != "root":
|
|
acta.anota(srv.lineas.get("grupo", srv.origen), "SIN-TRADUCIR", f"Group={srv.grupo}",
|
|
"mismo motivo que `User=`: no hay dónde ponerlo en la tarjeta")
|
|
|
|
if srv.tipo == "forking":
|
|
acta.anota(srv.lineas.get("tipo", srv.origen), "SIN-TRADUCIR", f"Type=forking ({srv.nombre})",
|
|
"arje supervisa al proceso que lanza: un demonio que se va a segundo plano sale "
|
|
"enseguida y queda en BUCLE DE REINICIO. Hay que agregarle su bandera de primer "
|
|
"plano, que depende del demonio")
|
|
elif srv.tipo in ("notify", "dbus"):
|
|
acta.anota(srv.lineas.get("tipo", srv.origen), "decision", f"Type={srv.tipo} ({srv.nombre})",
|
|
f"arje no implementa el protocolo de `{srv.tipo}`: se trata como `simple`, o sea "
|
|
"que se da por arrancado al ejecutarse y no cuando el servicio avisa que está listo")
|
|
|
|
# `oneshot` SÍ tiene destino: la semilla real del producto usa `\"supervision\": \"OneShot\"`.
|
|
if srv.tipo == "oneshot":
|
|
sup = "OneShot"
|
|
elif srv.reinicio == "no":
|
|
sup = "OneShot"
|
|
acta.anota(srv.lineas.get("reinicio", srv.origen), "decision", f"Restart=no ({srv.nombre})",
|
|
"sin reinicio, la tarjeta se emite como `OneShot`: arje no lo revive si muere")
|
|
else:
|
|
ini = srv.reinicio_ms or 500
|
|
sup = {"Restart": {"initial": ini, "max": max(ini * 40, 20000)}}
|
|
if srv.reinicio == "si-falla":
|
|
acta.anota(srv.lineas.get("reinicio", srv.origen), "decision", f"Restart=si-falla ({srv.nombre})",
|
|
"arje reinicia SIEMPRE, no sólo tras fallo: una salida limpia también lo revive")
|
|
|
|
if srv.entorno_ficheros:
|
|
acta.anota(srv.lineas.get("entorno_ficheros", srv.origen), "SIN-TRADUCIR", f"EnvironmentFile ({srv.nombre})",
|
|
"no se copió ninguna variable de esos ficheros: el `envp` de la tarjeta sale "
|
|
"INCOMPLETO y el servicio va a fallar tarde")
|
|
|
|
return {
|
|
"flow": {"input": [], "output": []},
|
|
"genesis": [],
|
|
"id": ulid_determinista(SEM, srv.nombre),
|
|
"label": srv.nombre,
|
|
"lifecycle": "daemon",
|
|
"lineage": None,
|
|
"payload": {"Native": {"argv": argv, "envp": [f"{k}={v}" for k, v in sorted(srv.entorno.items())],
|
|
"exec": exec_}},
|
|
"permissions": {"filesystem": "read-write", "ipc": {"allow": []},
|
|
"networking": "full", "processes": True},
|
|
"priority": "normal",
|
|
"provides": [],
|
|
# `After=`/`Requires=` se conservan por NOMBRE: arje resuelve el orden por su grafo, y los
|
|
# objetivos de systemd (`network.target`…) no existen acá — por eso se filtran los `.target`.
|
|
"requires": sorted({d.replace(".service", "") for d in (srv.tras + srv.necesita)
|
|
if not d.endswith(".target")}),
|
|
"schema_version": 1,
|
|
"soma": {
|
|
"cgroup": {"cpu_weight": None, "io_weight": None, "path": f"arje.slice/{srv.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": sup,
|
|
"_mudanza": {"desde": desde, "descripcion": srv.descripcion,
|
|
"tipo_origen": srv.tipo, "cwd_original": srv.cwd,
|
|
"usuario_original": srv.usuario, "arranque_auto": srv.arranque_auto},
|
|
}
|
|
|
|
|
|
@escritor("arje", familia="servicio")
|
|
def escribir(sis):
|
|
acta = sis.acta
|
|
hijas, vistos = [], {}
|
|
for srv in sis.servicios:
|
|
# Dos servicios con el mismo nombre ⇒ el mismo id determinista ⇒ una semilla con dos cards
|
|
# que se llaman igual. Pasa de verdad: `/etc/init.d` de gioser guarda copias `*.bak-FECHA`
|
|
# junto a los servicios vivos, y son scripts de OpenRC perfectamente válidos.
|
|
if srv.nombre in vistos:
|
|
acta.anota(srv.origen, "SIN-TRADUCIR", srv.nombre,
|
|
f"nombre repetido (ya salió en la línea {vistos[srv.nombre]}): produciría DOS "
|
|
"tarjetas con el mismo id. No se emite la segunda — decidí cuál vale y borrá "
|
|
"la otra (suelen ser copias `*.bak` en el mismo directorio)")
|
|
continue
|
|
vistos[srv.nombre] = srv.origen
|
|
if not srv.arranque_auto:
|
|
# Un unit instalado pero sin `WantedBy=` no lo levanta el arranque. Declararlo igual
|
|
# cambiaría el sistema: se emite, pero se dice.
|
|
acta.anota(srv.origen, "decision", f"{srv.nombre} (sin WantedBy=)",
|
|
"el unit no se levanta al arrancar en el origen; la semilla SÍ lo va a "
|
|
"levantar. Sacalo si no lo querés al boot")
|
|
t = _tarjeta(srv, acta, sis.formato_origen or "?")
|
|
if t is None:
|
|
acta.anota(srv.origen, "SIN-TRADUCIR", srv.nombre,
|
|
"sin comando de arranque: NO se emite tarjeta. Una tarjeta que no arranca es "
|
|
"peor que ninguna — la ausente falla ruidosamente al primer arranque")
|
|
continue
|
|
hijas.append(t)
|
|
|
|
raiz = {
|
|
"flow": {"input": [], "output": []},
|
|
"genesis": hijas,
|
|
"id": ulid_determinista(SEM + "-raiz", "mudanza"),
|
|
"label": "mudanza",
|
|
"lifecycle": "daemon",
|
|
"lineage": None,
|
|
"payload": "Virtual",
|
|
"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": "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": "OneShot",
|
|
}
|
|
return json.dumps(raiz, indent=1, sort_keys=True, ensure_ascii=False) + "\n"
|