servidor-image.sh: la imagen del perfil servidor, armada por script — y netup pasa a ser raíz
El ensamblado dejaba de ser reproducible en cuanto se cerraba la terminal: cuatro pasos, tres de ellos con una trampa que no se ve. Ahora es un script, con las trampas escritas en su cabecera: 1. **El kernel.** `linux.toml` NO sirve para hcloud (apaga SCSI y el disco de Hetzner es virtio-SCSI). Va `linux-generic`, que sí trae `CONFIG_SCSI_VIRTIO=y` — dato que no está en la receta, lo pone `make defconfig`, y sólo se ve en el `.config` que el artefacto publica. 2. **El init se pisa.** La clausura del perfil trae busybox y su `/sbin/init` gana por hidratarse después. El script lo restaura e IMPRIME la reparación (`../bin/busybox → /usr/bin/arje-zero`), que es la diferencia entre arreglarlo y creer que estaba bien. 3. **EXDEV.** Comprueba con `findmnt` que STORE y WORKDIR estén en el MISMO montaje y aborta con un mensaje que dice qué hacer, en vez de fallar fichero por fichero a mitad de un `cp -al`. 4. **La clave es obligatoria.** Sin `AUTHKEYS` no arma nada: una instalación remota sin `authorized_keys` deja una máquina viva e INALCANZABLE, que es peor que una que no arrancó. Y el cmdline va SIN `init=`, a propósito: así corre el wrapper que escribe `install-image.sh`, que es quien monta `/store` y `/var/lib/hammer`. **`netup` entra como raíz de `perfil.servidor`** aunque su binario ya venga dentro del `product-rootfs`. La razón es concreta y se pagó hoy: el del producto está congelado en el artefacto sellado del bootstrap, así que el arreglo de la ruta on-link NO llegaba a la imagen. Declarado como raíz, la hidratación lo proyecta encima y la imagen lleva el vigente — verificado por sha256: la imagen trae `a23df20e…` y el product-rootfs `a84b0a0d…`. `recipes/netup.toml` re-pineado a `be383ea4` (el commit del arreglo). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
@@ -917,6 +917,12 @@ paquetes = [
|
||||
# `tmux`: un build largo por SSH que muere con la sesión es un build perdido. En una caja sin
|
||||
# pantalla es infraestructura, no comodidad.
|
||||
"tmux",
|
||||
# `netup` (2026-09-10): el cliente DHCP propio que levanta la red al arrancar. Entra como RAÍZ
|
||||
# aunque su binario YA venga dentro del `product-rootfs`, y por una razón concreta: el del producto
|
||||
# está congelado en el artefacto sellado del bootstrap, así que un arreglo de netup no llega nunca
|
||||
# a la imagen — pasó el mismo día con la ruta on-link que Hetzner necesita. Declarado acá, la
|
||||
# hidratación del perfil lo proyecta ENCIMA y la imagen lleva el vigente.
|
||||
"netup",
|
||||
|
||||
# ── declarado y NO existe todavía: la deuda del perfil, contada ───────────────────────────────
|
||||
# `takana`: el propio takana NO tiene receta. El corpus construye 869 recetas y no la suya, así que
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ license = "MIT"
|
||||
[source]
|
||||
# Repo del propio takana (origin). El commit es el identificador inmutable; subirlo cuando netup avance.
|
||||
repo = "ssh://gitea@git.tawasuyu.net:2345/sergio/takana.git"
|
||||
commit = "6886f810bddd29481e1d84b982d98e783b578bdc"
|
||||
commit = "be383ea485def684cb5886530efcd5333548743d"
|
||||
|
||||
[build]
|
||||
compiler = "zig-cc"
|
||||
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/bin/sh
|
||||
# servidor-image.sh — arma la imagen de disco del `perfil.servidor` lista para escribir en una caja
|
||||
# Hetzner Cloud por rescue (SDD 28 §3). No inventa nada: encadena `hydrate-profile.py` +
|
||||
# `install-image.sh`, que ya existían, y agrega lo que ninguno de los dos puede saber solo.
|
||||
#
|
||||
# ── LO QUE ESTE SCRIPT EXISTE PARA NO OLVIDAR (todo medido el 2026-09-10) ────────────────────────
|
||||
#
|
||||
# 1. EL KERNEL. `recipes/linux.toml` NO sirve para hcloud: apaga SCSI (`-d SCSI`) y el disco de una
|
||||
# caja Hetzner lo maneja virtio-SCSI ⇒ arranca y no ve el disco. Va `linux-generic`, que sí trae
|
||||
# `CONFIG_SCSI_VIRTIO=y` — dato que NO está en la receta (lo enciende `make defconfig`) y que sólo
|
||||
# se ve en el `.config` que el artefacto publica.
|
||||
#
|
||||
# 2. EL INIT SE PISA. La clausura del perfil trae `busybox`, y su `/sbin/init -> ../bin/busybox` gana
|
||||
# por hidratarse DESPUÉS del product-rootfs, borrando el `-> /usr/bin/arje-zero`. La imagen
|
||||
# arrancaría perfecta con el init equivocado. Se restaura acá y además `install-image.sh` escribe
|
||||
# su propio wrapper encima; el cmdline no lleva `init=` para que ese wrapper —que monta /store y
|
||||
# /var/lib/hammer— sea el que corre.
|
||||
#
|
||||
# 3. EXDEV. El staging y la hidratación HARDLINKEAN, y un hardlink no cruza un montaje aunque sea el
|
||||
# mismo disco. Con el store en un volumen (el layout normal de este repo) hay que trabajar del
|
||||
# lado del store. Se comprueba y se aborta con un mensaje claro en vez de fallar fichero a fichero.
|
||||
#
|
||||
# 4. LA CLAVE. Sin `authorized_keys` la instalación remota deja una máquina viva e INALCANZABLE, que
|
||||
# es peor que una que no arrancó. Es obligatoria: sin `AUTHKEYS` no se arma la imagen.
|
||||
#
|
||||
# Uso:
|
||||
# AUTHKEYS=~/.ssh/github5.pub ./scripts/servidor-image.sh
|
||||
#
|
||||
# Env:
|
||||
# AUTHKEYS pubkey a instalar para root (OBLIGATORIO)
|
||||
# PERFIL perfil de targets.toml a hidratar (def servidor)
|
||||
# STORE store desde el que hidratar (def /mnt/cosecha/store)
|
||||
# WORKDIR dir de trabajo, MISMO montaje que STORE (def /mnt/cosecha/servidor-build)
|
||||
# IMG imagen de salida (def $WORKDIR/servidor.img)
|
||||
# KERNELPKG paquete del kernel (def linux-generic)
|
||||
# ROOT_SIZE/STORE_SIZE/STATE_SIZE MiB de cada partición (def 6144/512/512)
|
||||
set -eu
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
|
||||
|
||||
PERFIL="${PERFIL:-servidor}"
|
||||
STORE="${STORE:-/mnt/cosecha/store}"
|
||||
WORKDIR="${WORKDIR:-/mnt/cosecha/servidor-build}"
|
||||
IMG="${IMG:-$WORKDIR/servidor.img}"
|
||||
KERNELPKG="${KERNELPKG:-linux-generic}"
|
||||
ROOT_SIZE="${ROOT_SIZE:-6144}"; STORE_SIZE="${STORE_SIZE:-512}"; STATE_SIZE="${STATE_SIZE:-512}"
|
||||
|
||||
[ -n "${AUTHKEYS:-}" ] || { echo "!! falta AUTHKEYS: una imagen sin clave deja la caja INALCANZABLE" >&2; exit 2; }
|
||||
[ -r "$AUTHKEYS" ] || { echo "!! no puedo leer AUTHKEYS=$AUTHKEYS" >&2; exit 2; }
|
||||
|
||||
# ── guarda de EXDEV: el rootfs se arma con hardlinks contra el store ─────────────────────────────
|
||||
mkdir -p "$WORKDIR"
|
||||
m_store=$(findmnt -no TARGET -T "$STORE")
|
||||
m_work=$(findmnt -no TARGET -T "$WORKDIR")
|
||||
[ "$m_store" = "$m_work" ] || {
|
||||
echo "!! STORE ($STORE → montaje $m_store) y WORKDIR ($WORKDIR → montaje $m_work) están en" >&2
|
||||
echo " montajes DISTINTOS. El armado hardlinkea y un hardlink no cruza un montaje aunque sea" >&2
|
||||
echo " el mismo disco. Poné WORKDIR bajo $m_store." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── 1. base: el product-rootfs sellado, que es quien trae /sbin/init -> arje-zero ────────────────
|
||||
PDIR=$(ls -dt "$STORE"/*-product-rootfs 2>/dev/null | head -1)
|
||||
[ -n "$PDIR" ] || { echo "!! no hay product-rootfs sellado en $STORE (corré 'takana bootstrap product')" >&2; exit 1; }
|
||||
KDIR=$(ls -dt "$STORE"/*-"$KERNELPKG" 2>/dev/null | head -1)
|
||||
[ -n "$KDIR" ] && [ -r "$KDIR/boot/bzImage" ] || { echo "!! no hay $KERNELPKG sellado con /boot/bzImage en $STORE" >&2; exit 1; }
|
||||
|
||||
RFS="$WORKDIR/rootfs"
|
||||
echo "==> base : $(basename "$PDIR")"
|
||||
echo "==> kernel : $(basename "$KDIR")"
|
||||
rm -rf "$RFS"; mkdir -p "$RFS"
|
||||
cp -al "$PDIR/." "$RFS/"
|
||||
|
||||
# ── 2. userland del perfil encima ────────────────────────────────────────────────────────────────
|
||||
echo "==> hidratando perfil '$PERFIL'"
|
||||
python3 scripts/hydrate-profile.py "$PERFIL" --into "$RFS" --store "$STORE" --keep
|
||||
|
||||
# ── 3. reparar el init que la hidratación pisó (ver cabecera, punto 2) ───────────────────────────
|
||||
antes=$(readlink "$RFS/sbin/init" 2>/dev/null || echo "(fichero)")
|
||||
ln -sf /usr/bin/arje-zero "$RFS/sbin/init"
|
||||
echo "==> init : $antes → $(readlink "$RFS/sbin/init")"
|
||||
[ -x "$RFS/usr/bin/arje-zero" ] || { echo "!! el rootfs no tiene /usr/bin/arje-zero" >&2; exit 1; }
|
||||
|
||||
# ── 4. la clave ──────────────────────────────────────────────────────────────────────────────────
|
||||
install -d -m 700 "$RFS/root/.ssh"
|
||||
install -m 600 "$AUTHKEYS" "$RFS/root/.ssh/authorized_keys"
|
||||
echo "==> clave : $(wc -l < "$RFS/root/.ssh/authorized_keys") línea(s) en /root/.ssh/authorized_keys"
|
||||
|
||||
# ── 5. la imagen. SIN `init=`: así corre el wrapper de install-image.sh, que monta /store y el diario
|
||||
# El serial primero y tty0 al final ⇒ /dev/console es tty0, que es lo que muestra la consola web
|
||||
# de Hetzner; los mensajes del kernel salen por los dos.
|
||||
CMDLINE="console=ttyS0,115200 console=tty0 root=PARTLABEL=hammer-root rw"
|
||||
echo "==> cmdline : $CMDLINE"
|
||||
ROOTFS="$RFS" STAGE="$WORKDIR/.stage" KERNEL="$KDIR/boot/bzImage" IMG="$IMG" \
|
||||
ROOT_SIZE="$ROOT_SIZE" STORE_SIZE="$STORE_SIZE" STATE_SIZE="$STATE_SIZE" CMDLINE="$CMDLINE" \
|
||||
./scripts/install-image.sh
|
||||
|
||||
echo
|
||||
echo "✓ imagen del perfil '$PERFIL': $IMG"
|
||||
echo " escribir en una caja Hetzner (con la caja en RESCUE):"
|
||||
echo " zstd -T2 -3 -c $IMG | ssh root@<ip> 'zstd -d -c | dd of=/dev/sda bs=4M conv=fsync && sync'"
|
||||
Reference in New Issue
Block a user