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 1534Shell 199Rust 109Markdown 993713 Sutichaykuna

⎇ 93c035f15f8f8aab331a99650e173b325ebf91b0 scripts/servidor-image.sh Shell · 154 siq'ikuna · 10.1 KB Ñawpa willay ch'uya

fix(servidor): la imagen ya se puede reiniciar, y el tope de 10 Claudes deja de ser una pared 28cd6d029 · Sergio ·

1#!/bin/sh
2# servidor-image.sh — arma la imagen de disco del `perfil.servidor` lista para escribir en una caja
3# Hetzner Cloud por rescue (SDD 28 §3). No inventa nada: encadena `hydrate-profile.py` +
4# `install-image.sh`, que ya existían, y agrega lo que ninguno de los dos puede saber solo.
5#
6# ── LO QUE ESTE SCRIPT EXISTE PARA NO OLVIDAR (todo medido el 2026-09-10) ────────────────────────
7#
8# 1. EL KERNEL. `recipes/linux.toml` NO sirve para hcloud: apaga SCSI (`-d SCSI`) y el disco de una
9#    caja Hetzner lo maneja virtio-SCSI ⇒ arranca y no ve el disco. Va `linux-generic`, que sí trae
10#    `CONFIG_SCSI_VIRTIO=y` — dato que NO está en la receta (lo enciende `make defconfig`) y que sólo
11#    se ve en el `.config` que el artefacto publica.
12#
13# 2. EL INIT SE PISA. La clausura del perfil trae `busybox`, y su `/sbin/init -> ../bin/busybox` gana
14#    por hidratarse DESPUÉS del product-rootfs, borrando el `-> /usr/bin/arje-zero`. La imagen
15#    arrancaría perfecta con el init equivocado. Se restaura acá y además `install-image.sh` escribe
16#    su propio wrapper encima; el cmdline no lleva `init=` para que ese wrapper —que monta /store y
17#    /var/lib/hammer— sea el que corre.
18#
19# 3. EXDEV. El staging y la hidratación HARDLINKEAN, y un hardlink no cruza un montaje aunque sea el
20#    mismo disco. Con el store en un volumen (el layout normal de este repo) hay que trabajar del
21#    lado del store. Se comprueba y se aborta con un mensaje claro en vez de fallar fichero a fichero.
22#
23# 4. LA CLAVE. Sin `authorized_keys` la instalación remota deja una máquina viva e INALCANZABLE, que
24#    es peor que una que no arrancó. Es obligatoria: sin `AUTHKEYS` no se arma la imagen.
25#
26# Uso:
27#   AUTHKEYS=~/.ssh/github5.pub ./scripts/servidor-image.sh
28#
29# Env:
30#   AUTHKEYS   pubkey a instalar para root                      (OBLIGATORIO)
31#   PERFIL     perfil de targets.toml a hidratar                (def servidor)
32#   STORE      store desde el que hidratar                      (def /mnt/cosecha/store)
33#   WORKDIR    dir de trabajo, MISMO montaje que STORE           (def /mnt/cosecha/servidor-build)
34#   IMG        imagen de salida                                  (def $WORKDIR/servidor.img)
35#   KERNELPKG  paquete del kernel                                (def linux-generic)
36#   ROOT_SIZE/STORE_SIZE/STATE_SIZE   MiB de cada partición      (def 6144/512/512)
37set -eu
38ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
39
40PERFIL="${PERFIL:-servidor}"
41STORE="${STORE:-/mnt/cosecha/store}"
42WORKDIR="${WORKDIR:-/mnt/cosecha/servidor-build}"
43IMG="${IMG:-$WORKDIR/servidor.img}"
44KERNELPKG="${KERNELPKG:-linux-generic}"
45ROOT_SIZE="${ROOT_SIZE:-6144}"; STORE_SIZE="${STORE_SIZE:-512}"; STATE_SIZE="${STATE_SIZE:-512}"
46# El binario: en un hub de desarrollo está en `target/release`, en una caja instalada es
47# `/usr/bin/takana` y `target/` ni existe. Es el bloqueo que ya costó una vez en la caja nueva
48# («unhashable 875» con el lab bien): los scripts asumían árbol de desarrollo.
49TAKANA="${TAKANA:-$ROOT/target/release/takana}"
50[ -x "$TAKANA" ] || TAKANA="$(command -v takana || true)"
51[ -n "$TAKANA" ] && [ -x "$TAKANA" ] || { echo "!! no encuentro el binario takana (probá TAKANA=/usr/bin/takana)" >&2; exit 2; }
52
53[ -n "${AUTHKEYS:-}" ] || { echo "!! falta AUTHKEYS: una imagen sin clave deja la caja INALCANZABLE" >&2; exit 2; }
54[ -r "$AUTHKEYS" ]     || { echo "!! no puedo leer AUTHKEYS=$AUTHKEYS" >&2; exit 2; }
55
56# ── guarda de EXDEV: el rootfs se arma con hardlinks contra el store ─────────────────────────────
57mkdir -p "$WORKDIR"
58m_store=$(findmnt -no TARGET -T "$STORE")
59m_work=$(findmnt -no TARGET -T "$WORKDIR")
60[ "$m_store" = "$m_work" ] || {
61    echo "!! STORE ($STORE → montaje $m_store) y WORKDIR ($WORKDIR → montaje $m_work) están en" >&2
62    echo "   montajes DISTINTOS. El armado hardlinkea y un hardlink no cruza un montaje aunque sea" >&2
63    echo "   el mismo disco. Poné WORKDIR bajo $m_store." >&2
64    exit 1
65}
66
67# ── 1. base: el product-rootfs sellado, que es quien trae /sbin/init -> arje-zero ────────────────
68PDIR=$(ls -dt "$STORE"/*-product-rootfs 2>/dev/null | head -1)
69[ -n "$PDIR" ] || { echo "!! no hay product-rootfs sellado en $STORE (corré 'takana bootstrap product')" >&2; exit 1; }
70KDIR=$(ls -dt "$STORE"/*-"$KERNELPKG" 2>/dev/null | head -1)
71[ -n "$KDIR" ] && [ -r "$KDIR/boot/bzImage" ] || { echo "!! no hay $KERNELPKG sellado con /boot/bzImage en $STORE" >&2; exit 1; }
72
73RFS="$WORKDIR/rootfs"
74echo "==> base    : $(basename "$PDIR")"
75echo "==> kernel  : $(basename "$KDIR")"
76rm -rf "$RFS"; mkdir -p "$RFS"
77cp -al "$PDIR/." "$RFS/"
78
79# ── 2. userland del perfil encima ────────────────────────────────────────────────────────────────
80echo "==> hidratando perfil '$PERFIL'"
81python3 scripts/hydrate-profile.py "$PERFIL" --into "$RFS" --store "$STORE" --keep
82
83# ── 2 bis. las CUENTAS que los paquetes del perfil declaran (`[[user]]`, SDD 30) ─────────────────
84# El `/etc/passwd` del product-rootfs trae `root` y `sshd` y nada más, porque es una constante de
85# Rust. Un paquete cuyo demonio se niega a correr como root —`gitea` lo hace explícitamente— tiene
86# su Card haciendo `setuidgid`, así que SIN esta línea el servicio arranca, muere y reintenta para
87# siempre, y el log dice «unknown user», no «a la imagen le falta una cuenta».
88#
89# `--merge` fusiona por clave y es idempotente: componer dos veces no duplica. Si una cuenta ya está
90# con OTRA línea, sale ≠0 sin tocar ningún fichero — y acá eso ABORTA la imagen a propósito: un
91# rootfs con el uid equivocado produce ficheros de un dueño que no existe, y eso se descubre dentro
92# de la VM, varios pasos más tarde.
93USERPATHS=$(python3 scripts/targets.py --user-paths "$PERFIL")
94if [ -n "$USERPATHS" ]; then
95  echo "==> cuentas : $(echo "$USERPATHS" | wc -l) receta(s) declaran usuarios"
96  # shellcheck disable=SC2086
97  "$TAKANA" --store "$STORE" users $USERPATHS --merge "$RFS"
98else
99  echo "==> cuentas : ninguna receta del perfil declara [[user]]"
100fi
101
102# ── 2 ter. las CARDS de los servicios que el perfil arranca (SDD 30 §4c) ─────────────────────────
103# Sin esto la imagen trae el binario, trae la cuenta… y NADIE lo arranca: la seed del producto sólo
104# conoce las cards que `takana-bootstrap` hornea (`sshd`, `console-getty`, `hammerd`). Medido en la
105# primera imagen de este perfil: `gitea` instalado, su usuario en `/etc/passwd`, y en el `genesis`
106# NADA — un servidor que arranca perfecto sin el servicio por el que existe. La métrica no lo ve:
107# `--services` dice que el perfil lo habilita, y lo habilita; lo que faltaba era el paso que lleva
108# esa declaración a la imagen.
109#
110# `service-cards` es la ÚNICA traducción `[[service]]` → Card, y `inyectar-cards.py` sólo compone la
111# seed (idempotente por label, y con `os.replace` porque la seed es un hardlink al store).
112SVCPATHS=$(python3 scripts/targets.py --service-paths "$PERFIL")
113if [ -n "$SVCPATHS" ]; then
114  # shellcheck disable=SC2086
115  "$TAKANA" --store "$STORE" service-cards $SVCPATHS > "$WORKDIR/cards.json"
116  echo "==> cards   : $(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1]))))' "$WORKDIR/cards.json") card(s) de $(echo "$SVCPATHS" | wc -l) receta(s)"
117  python3 scripts/gnome/inyectar-cards.py "$RFS/ente/seed.card.json" "$WORKDIR/cards.json" "$RFS/etc/arje/cards.d"
118else
119  echo "==> cards   : el perfil no arranca ningún servicio de paquete"
120fi
121
122# ── 3. reparar el init que la hidratación pisó (ver cabecera, punto 2) ───────────────────────────
123antes=$(readlink "$RFS/sbin/init" 2>/dev/null || echo "(fichero)")
124ln -sf /usr/bin/arje-zero "$RFS/sbin/init"
125echo "==> init    : $antes → $(readlink "$RFS/sbin/init")"
126[ -x "$RFS/usr/bin/arje-zero" ] || { echo "!! el rootfs no tiene /usr/bin/arje-zero" >&2; exit 1; }
127
128# ── 3 bis. los BOTONES de apagado, que sin esto quedan mudos ─────────────────────────────────────
129# Con arje-zero de PID 1 no alcanza con que el init sea el correcto: `reboot` termina mandándole una
130# señal que arje NO atrapa (atrapa SIGINT y sólo SIGINT), y el kernel descarta las señales sin
131# handler dirigidas a PID 1. O sea que la imagen arranca perfecta y después NO SE PUEDE REINICIAR:
132# vuelve el prompt, sin error y sin mensaje. Medido en `takana` el 2026-09-25 — siete días y medio
133# de uptime sin poder reiniciar, ni por `sudo reboot` ni desde el panel del proveedor. El camino
134# ordenado de arje (`apagar_maquina()`) existía entero y no estaba conectado a ningún botón.
135sh scripts/servidor/arje-botones.sh "$RFS"
136
137# ── 4. la clave ──────────────────────────────────────────────────────────────────────────────────
138install -d -m 700 "$RFS/root/.ssh"
139install -m 600 "$AUTHKEYS" "$RFS/root/.ssh/authorized_keys"
140echo "==> clave   : $(wc -l < "$RFS/root/.ssh/authorized_keys") línea(s) en /root/.ssh/authorized_keys"
141
142# ── 5. la imagen. SIN `init=`: así corre el wrapper de install-image.sh, que monta /store y el diario
143#      El serial primero y tty0 al final ⇒ /dev/console es tty0, que es lo que muestra la consola web
144#      de Hetzner; los mensajes del kernel salen por los dos.
145CMDLINE="console=ttyS0,115200 console=tty0 root=PARTLABEL=hammer-root rw"
146echo "==> cmdline : $CMDLINE"
147ROOTFS="$RFS" STAGE="$WORKDIR/.stage" KERNEL="$KDIR/boot/bzImage" IMG="$IMG" \
148  ROOT_SIZE="$ROOT_SIZE" STORE_SIZE="$STORE_SIZE" STATE_SIZE="$STATE_SIZE" CMDLINE="$CMDLINE" \
149  ./scripts/install-image.sh
150
151echo
152echo "✓ imagen del perfil '$PERFIL': $IMG"
153echo "  escribir en una caja Hetzner (con la caja en RESCUE):"
154echo "    zstd -T2 -3 -c $IMG | ssh root@<ip> 'zstd -d -c | dd of=/dev/sda bs=4M conv=fsync && sync'"