El servicio que falla y agota su backoff sólo se podía recuperar reiniciando la máquina entera.
Ya no. Y no hubo que escribir nada: **el cliente existía en tawasuyu y el crate se llama `arje-ctl`**
(el binario, `arjectl`). Buscarlo por `arjectl` no lo encontraba, y de ahí salió mi conclusión falsa
de que había que implementarlo — el protocolo ya traía ListEntes, SpawnCardFromDisk,
StopCardFromDisk, KillEnte y EnteStatus.
`recipes/arjectl.toml` lo construye del MISMO commit que `arje-zero` (98db584f), y no por comodidad:
el bus es un protocolo entre dos binarios y un cliente de otro árbol puede conectar sin entenderse
con el init. Publica sólo `arjectl`; el crate también produce un `systemctl` de camuflaje que acá no
se instala — en una distro sin systemd, ese nombre en el PATH invita a escribir runbooks con el
verbo ajeno.
⚠ EL HUECO QUE SÓLO SE VE USÁNDOLO: el genesis de la seed dice qué arranca AL BOOT, pero
`start`/`restart` usan `SpawnCardFromDisk`, que lee `/etc/arje/cards.d/<label>.json` — y el armado
no lo escribía:
$ arjectl start gitea
Error: arje-zero rechazó: card gitea: No such file or directory
(buscada en /etc/arje/cards.d/gitea.json)
Son dos preguntas distintas —qué arranca solo, y qué se puede encarnar a pedido— y arje las responde
desde sitios distintos. `inyectar-cards.py` escribe ahora los dos árboles, y en `cards.d` escribe
TODAS las cards, no sólo las nuevas: `sshd` viene del product-rootfs y tampoco era relanzable.
Medido con la VM arrancada UNA sola vez: la imagen trae `cards.d/{gitea,sshd}.json` · `list-units`
da PID/CPU/MEM/HILOS/reinicios · poner el `app.ini` + `arjectl start gitea` ⇒ **GET / 200 sin
reiniciar** (uptime 6 min) · `arjectl restart gitea` cambia el PID (118 → 192) y sigue en 200.
⚠ Y un aviso que costó un HTTP intermitente: **`arjectl start` sobre un Ente YA VIVO lo DUPLICA** —
`SpawnCardFromDisk` no deduplica por label y arje le da un ULID nuevo. En gitea el síntoma fue
`unable to lock level db … resource temporarily unavailable` y un `[F]`: dos servidores peleando por
el mismo estado. Para relanzar se usa `restart`, o se mira `list-units` antes. Un `start` idempotente
es trabajo de arje, no de esta imagen.
⚠ Deuda anotada, no barrida: `arjectl` va en `perfil.servidor` porque este frente es el que lo pagó.
TODA imagen de takana corre arje-zero como PID 1 y ninguna se puede operar sin él ⇒ el argumento
para subirlo a `base` es fuerte, y es una línea. Se deja como decisión.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
58 lines
3.1 KiB
Python
Executable File
58 lines
3.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# inyectar-cards.py — mete las Cards de servicio de un perfil en el `genesis` de la seed (SDD 30 §4c).
|
|
#
|
|
# Por qué es un fichero y no un heredoc dentro del script de imagen: el script ya tiene un heredoc de
|
|
# python (el del console-getty) y anidar dos es la receta del error mudo — el terminador del interno
|
|
# cierra el externo y la mitad del programa se ejecuta como shell. Pasó al escribir esto.
|
|
#
|
|
# NO genera las Cards: las recibe ya emitidas por `takana service-cards`, que es la única
|
|
# implementación de la traducción `[[service]]` → Card. Acá sólo se COMPONE la seed.
|
|
#
|
|
# ── LAS CARDS VAN A DOS SITIOS, Y HACEN FALTA LOS DOS (2026-09-14) ──────────────────────────────
|
|
# El `genesis` de la seed dice qué arranca AL BOOT. `arjectl start|restart <label>` no lee de ahí:
|
|
# usa `SpawnCardFromDisk`, que carga `/etc/arje/cards.d/<label>.json`. Medido en la imagen del
|
|
# perfil `servidor`:
|
|
#
|
|
# $ arjectl start gitea
|
|
# Error: arje-zero rechazó: card gitea: No such file or directory
|
|
# (buscada en /etc/arje/cards.d/gitea.json)
|
|
#
|
|
# O sea: con sólo el genesis, un servicio que muere tras agotar su backoff **no se puede relanzar
|
|
# sin reiniciar la máquina**. Escribir las dos no es duplicar: son dos preguntas distintas —qué
|
|
# arranca solo, y qué se puede encarnar a pedido— y arje las responde desde sitios distintos.
|
|
import json, os, sys
|
|
|
|
seed_p, cards_p = sys.argv[1], sys.argv[2]
|
|
cards_dir = sys.argv[3] if len(sys.argv) > 3 else None
|
|
seed = json.load(open(seed_p))
|
|
cards = json.load(open(cards_p))
|
|
|
|
# Idempotente por `label`: re-armar la imagen no puede duplicar Entes. Dos Cards con el mismo label
|
|
# no son "dos servicios": para el grafo de arje son una colisión de identidad, y el segundo se
|
|
# perdería en silencio o pelearía con el primero por el mismo nombre del bus.
|
|
ya = {g.get("label") for g in seed.get("genesis", [])}
|
|
puestos = [c["label"] for c in cards if c["label"] not in ya]
|
|
seed.setdefault("genesis", []).extend(c for c in cards if c["label"] not in ya)
|
|
|
|
tmp = seed_p + ".new"
|
|
with open(tmp, "w") as f:
|
|
json.dump(seed, f, indent=2)
|
|
os.chmod(tmp, 0o644)
|
|
# Reemplazar la ENTRADA de directorio, no escribir en sitio: la seed del árbol fundido es un hardlink
|
|
# read-only al artefacto del store, y escribirle encima mutaría el store.
|
|
os.replace(tmp, seed_p)
|
|
print(" cards en el genesis:", ", ".join(puestos) or "(ninguna nueva)")
|
|
|
|
# El store en disco que `arjectl` consulta. Se escriben TODAS, no sólo las nuevas: una card que ya
|
|
# estaba en el genesis (p. ej. `sshd`, que viene del product-rootfs) tampoco es relanzable sin esto.
|
|
if cards_dir:
|
|
os.makedirs(cards_dir, exist_ok=True)
|
|
for c in cards:
|
|
dst = os.path.join(cards_dir, f"{c['label']}.json")
|
|
tmp = dst + ".new"
|
|
with open(tmp, "w") as f:
|
|
json.dump(c, f, indent=2)
|
|
os.chmod(tmp, 0o644)
|
|
os.replace(tmp, dst) # `replace`, no `write`: el árbol fundido hardlinkea al store
|
|
print(" cards en", cards_dir + ":", ", ".join(c["label"] for c in cards))
|