scripts/disk-image.sh ahora arma una imagen GPT de 3 particiones ext4 en vez
de una sola: vda1=/ , vda2=/store (CAS inmutable), vda3=/var/lib/hammer (estado
mutable). Construcción sin root ni loopback: cp -al stagea el rootfs por
hardlinks (vacía store/ y var/lib/hammer/, instala el wrapper /sbin/init),
mke2fs -d puebla cada ext4 bajo unshare -r (root-owned), sfdisk escribe la GPT
y dd conv=sparse,notrunc empalma cada fs en su offset (imagen sparse, ~2G
reales). El wrapper /sbin/init monta vda2/vda3 y hace exec de arje-zero (el
kernel sólo monta vda1). drive-rebuild.py: root=/dev/vda1 en modo DISK.
Consecuencia resuelta: con /store en su propia partición el sellado cruza
filesystems y rename(2) da EXDEV. Store::seal cae a copia recursiva a un
staging dentro del store (preserva symlinks+modos) + rename store-interno
(atómico, mismo FS) + borrado del origen. Test copy_tree añadido.
Verificado in-VM (kernel hammer, KVM): vda{1,2,3} montados dedicados, 0
errores Cross-device, stage1' == stage1 ✓ REPRODUCIBLE. Cierra el ☐ de
SDD 11 §6 (particionado/montaje en la imagen destino).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
121 lines
6.9 KiB
Bash
Executable File
121 lines
6.9 KiB
Bash
Executable File
#!/bin/sh
|
|
# disk-image.sh — Etapa B3 (mounts dedicados): empaqueta el rootfs como una imagen de disco virtio
|
|
# PARTICIONADA (GPT) con tres filesystems ext4 dedicados, en vez de una única partición que mete todo
|
|
# junto (B2). Es el paso hacia la imagen instalable real: el store y el estado mutable dejan de ser meros
|
|
# directorios del rootfs y pasan a ser particiones propias, como en un sistema instalado de verdad.
|
|
#
|
|
# /dev/vda1 → / rootfs (runtime + toolchain + /work del rebuild) [hammer-root]
|
|
# /dev/vda2 → /store artefactos CAS BLAKE3 (inmutables) [hammer-store]
|
|
# /dev/vda3 → /var/lib/hammer estado mutable: journal + overlays (upper/work) [hammer-state]
|
|
#
|
|
# El kernel (recipes/linux.toml) trae VIRTIO_BLK+EXT4+EFI_PARTITION(=y por defconfig) ⇒ lee la GPT y
|
|
# monta /dev/vda1 como root sin initramfs. Las otras dos particiones las monta un wrapper `/sbin/init`
|
|
# (montar /store + /var/lib/hammer, luego `exec /usr/bin/arje-zero`) — el init real (arje-zero) sigue
|
|
# en su sitio; el wrapper es el puente mientras arje no lea fstab/cards de montaje por sí mismo.
|
|
#
|
|
# Construcción sin root ni loopback:
|
|
# - `cp -al` arma un árbol del rootfs por HARDLINKS (sin copiar datos), le vacía store/ y
|
|
# var/lib/hammer/ (deja mountpoints vacíos) y le instala el wrapper /sbin/init. NUNCA muta $ROOTFS.
|
|
# - `mke2fs -d <dir>` puebla cada ext4 desde un directorio sin montar nada (e2fsprogs >= 1.43).
|
|
# - `unshare -r` mapea nuestro uid→0 ⇒ los ficheros uid-host se sellan como uid 0 en la imagen (igual
|
|
# que el `--owner=root:root` del cpio; el copy-up de overlay del rebuild falla EACCES si van uid 1000).
|
|
# - `sfdisk` escribe la GPT sobre el fichero-imagen (no necesita root) y `dd conv=notrunc` empalma cada
|
|
# ext4 en el offset de su partición. Tamaños exactos: la partición mide lo mismo que el ext4 (M==MiB).
|
|
#
|
|
# Uso:
|
|
# ./scripts/disk-image.sh # crea work/hammer-disk.img
|
|
# BOOT=1 KERNEL=<bzImage> KVM=1 ./scripts/disk-image.sh # además bootea y corre rebuild-stage1
|
|
#
|
|
# Variables:
|
|
# ROOTFS dir rootfs a empaquetar (default work/builder-rootfs, lo arma selfhost-verify)
|
|
# IMG imagen de salida (default work/hammer-disk.img)
|
|
# ROOT_SIZE MiB de la partición / (default 6144; incluye /work del rebuild)
|
|
# STORE_SIZE MiB de la partición /store (default 2048; artefactos CAS ~0.4G + reseal del rebuild)
|
|
# STATE_SIZE MiB de /var/lib/hammer (default 2048; journal + overlays)
|
|
# BOOT 1 ⇒ bootea con drive-rebuild.py tras crear la imagen
|
|
# KERNEL bzImage con VIRTIO_BLK+EXT4 (requerido si BOOT=1)
|
|
# KVM/MEM/NET passthrough a drive-rebuild.py
|
|
set -eu
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
ROOTFS="${ROOTFS:-work/builder-rootfs}"
|
|
IMG="${IMG:-work/hammer-disk.img}"
|
|
ROOT_SIZE="${ROOT_SIZE:-6144}"
|
|
STORE_SIZE="${STORE_SIZE:-2048}"
|
|
STATE_SIZE="${STATE_SIZE:-2048}"
|
|
|
|
[ -d "$ROOTFS" ] || { echo "no existe ROOTFS: $ROOTFS (corré selfhost-verify para armar el builder)" >&2; exit 1; }
|
|
[ -e "$ROOTFS/sbin/init" ] || { echo "ROOTFS sin /sbin/init (arje-zero)" >&2; exit 1; }
|
|
command -v sfdisk >/dev/null 2>&1 || { echo "falta sfdisk (util-linux) para la tabla GPT" >&2; exit 1; }
|
|
command -v mke2fs >/dev/null 2>&1 || { echo "falta mke2fs (e2fsprogs)" >&2; exit 1; }
|
|
unshare -r true 2>/dev/null || { echo "se requiere 'unshare -r' (ficheros root-owned sin sudo)" >&2; exit 1; }
|
|
|
|
STAGE="work/.disk-stage"
|
|
rm -rf "$STAGE"
|
|
mkdir -p "$STAGE"
|
|
ROOT_TREE="$STAGE/root"
|
|
ROOT_IMG="$STAGE/root.img"
|
|
STORE_IMG="$STAGE/store.img"
|
|
STATE_IMG="$STAGE/state.img"
|
|
|
|
# --- árbol del rootfs SIN store/estado, con el wrapper /sbin/init (hardlinks, no copia datos) ---
|
|
echo "==> staging root (hardlinks) sin /store ni /var/lib/hammer + wrapper /sbin/init"
|
|
cp -al "$ROOTFS" "$ROOT_TREE"
|
|
rm -rf "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer"
|
|
mkdir -p "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer" # mountpoints vacíos
|
|
rm -f "$ROOT_TREE/sbin/init" # era symlink → /usr/bin/arje-zero
|
|
cat > "$ROOT_TREE/sbin/init" <<'INIT'
|
|
#!/bin/sh
|
|
# Wrapper PID1 (Etapa B3): monta los filesystems dedicados antes del init real. El kernel arranca con
|
|
# root=/dev/vda1; /store (CAS inmutable) y /var/lib/hammer (estado mutable) son particiones propias que
|
|
# el kernel NO monta solo. DEVTMPFS_MOUNT=y deja /dev poblado, así que /dev/vda{2,3} ya existen aquí.
|
|
/bin/busybox mount -t ext4 /dev/vda2 /store || echo "init: no pude montar /store (/dev/vda2)"
|
|
/bin/busybox mount -t ext4 /dev/vda3 /var/lib/hammer || echo "init: no pude montar /var/lib/hammer (/dev/vda3)"
|
|
exec /usr/bin/arje-zero
|
|
INIT
|
|
chmod +x "$ROOT_TREE/sbin/init"
|
|
|
|
# --- tabla de particiones GPT (sectores de 512 B; 1 MiB = 2048 sectores, alineación por defecto) ---
|
|
SECT_MIB=2048
|
|
ROOT_SECT=$(( ROOT_SIZE * SECT_MIB ))
|
|
STORE_SECT=$(( STORE_SIZE * SECT_MIB ))
|
|
STATE_SECT=$(( STATE_SIZE * SECT_MIB ))
|
|
START1=2048
|
|
START2=$(( START1 + ROOT_SECT ))
|
|
START3=$(( START2 + STORE_SECT ))
|
|
TOTAL_SECT=$(( START3 + STATE_SECT + 2048 )) # + 1 MiB de slack para la GPT de respaldo
|
|
|
|
echo "==> imagen : $IMG (GPT, root ${ROOT_SIZE}M + store ${STORE_SIZE}M + estado ${STATE_SIZE}M)"
|
|
rm -f "$IMG"
|
|
truncate -s $(( TOTAL_SECT * 512 )) "$IMG"
|
|
sfdisk --quiet "$IMG" >/dev/null <<EOF
|
|
label: gpt
|
|
start=$START1, size=$ROOT_SECT, type=linux, name="hammer-root"
|
|
start=$START2, size=$STORE_SECT, type=linux, name="hammer-store"
|
|
start=$START3, size=$STATE_SECT, type=linux, name="hammer-state"
|
|
EOF
|
|
|
|
# --- poblar cada ext4 (root-owned vía unshare -r) y empalmar en su offset ---
|
|
echo "==> mke2fs + empalme de las 3 particiones (unshare -r, sin sudo)"
|
|
unshare -r sh -eu <<EOF
|
|
mke2fs -q -t ext4 -L hammer-root -E root_owner=0:0 -d "$ROOT_TREE" "$ROOT_IMG" ${ROOT_SIZE}M
|
|
mke2fs -q -t ext4 -L hammer-store -E root_owner=0:0 -d "$ROOTFS/store" "$STORE_IMG" ${STORE_SIZE}M
|
|
mke2fs -q -t ext4 -L hammer-state -E root_owner=0:0 -d "$ROOTFS/var/lib/hammer" "$STATE_IMG" ${STATE_SIZE}M
|
|
# conv=sparse,notrunc: empalma sin destruir la GPT y deja los bloques cero del ext4 como huecos (si no,
|
|
# dd rellena la imagen entera y la sparse del truncate se pierde → 11G reales en vez de ~2G).
|
|
dd if="$ROOT_IMG" of="$IMG" bs=512 seek=$START1 conv=sparse,notrunc status=none
|
|
dd if="$STORE_IMG" of="$IMG" bs=512 seek=$START2 conv=sparse,notrunc status=none
|
|
dd if="$STATE_IMG" of="$IMG" bs=512 seek=$START3 conv=sparse,notrunc status=none
|
|
EOF
|
|
|
|
rm -rf "$STAGE"
|
|
echo "==> imagen creada: $(du -h "$IMG" | cut -f1) en disco (sparse), $(( TOTAL_SECT * 512 / 1024 / 1024 ))M virtuales"
|
|
|
|
if [ "${BOOT:-0}" = 1 ]; then
|
|
[ -n "${KERNEL:-}" ] || { echo "BOOT=1 requiere KERNEL=<bzImage con VIRTIO_BLK+EXT4>" >&2; exit 1; }
|
|
echo "==> booteando de disco particionado (root=/dev/vda1, /store+/var/lib/hammer montados por /sbin/init)"
|
|
DISK="$IMG" KERNEL="$KERNEL" KVM="${KVM:-0}" MEM="${MEM:-6144}" NET="${NET:-1}" \
|
|
python3 scripts/drive-rebuild.py
|
|
fi
|