#!/usr/bin/env python3 # drive-rebuild.py — Driver NO-interactivo del rebuild in-rootfs (auto-alojamiento, SDD 11 §7; # runbook docs/runbooks/stage1-vm-boot.md §8c). # # Bootea el builder en QEMU, espera la shell de arje-zero (PID 1), manda `rebuild-stage1` y captura # todo hasta el veredicto (✓ REPRODUCIBLE / ✗ DIVERGENTE), el RC o el timeout. Imprime un resumen y # sale con 0 si REPRODUCIBLE, 1 si DIVERGENTE/otro. # # Variables de entorno (todas opcionales): # BUILDER_CPIO initramfs del builder (default work/builder.cpio.gz) # DISK imagen de disco PARTICIONADA (GPT) a bootear como ROOT real (Etapa B3). Si está # seteada, se ignora el initramfs: QEMU monta la imagen como disco virtio y el kernel # arranca con `root=/dev/vda1 rw rdinit=/sbin/init` — sin initramfs ni switch_root (/ ya # es un mount ext4 real, pivotable). La partición 1 es /, y el wrapper /sbin/init monta # las particiones dedicadas /store (vda2) y /var/lib/hammer (vda3) antes del init real. # La arma scripts/disk-image.sh. Requiere kernel con VIRTIO_BLK+EXT4 (recipes/linux.toml). # KERNEL bzImage/vmlinuz a bootear (default /boot/vmlinuz-linux) # MEM RAM de la VM en MiB (default 6144) # CPU modelo de CPU TCG (default Broadwell; AVX2 sin KVM) # KVM 1 ⇒ -enable-kvm -cpu host si hay /dev/kvm (mucho más rápido; recomendado) # DEADLINE techo en segundos (default 14400 = 4h) # LOG fichero de captura (default work/rebuild-run.log) # NET 1 ⇒ NIC e1000 user-mode para el vendoring Rust (default 1) # # El rebuild Rust (hammerd/arje-zero) hace `cargo vendor` desde crates.io ⇒ necesita red: por eso la # NIC e1000 (qemu user-mode 10.0.2.0/24). Sin red, sólo sellan los componentes C. import os, pty, select, subprocess, sys, time, re ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) def envpath(name, default): v = os.environ.get(name, default) return v if os.path.isabs(v) else os.path.join(ROOT, v) CPIO = envpath("BUILDER_CPIO", "work/builder.cpio.gz") DISK = os.environ.get("DISK", "") # Etapa B: si está, booteamos de disco real en vez de initramfs. if DISK and not os.path.isabs(DISK): DISK = os.path.join(ROOT, DISK) KERNEL = os.environ.get("KERNEL", "/boot/vmlinuz-linux") LOG = envpath("LOG", "work/rebuild-run.log") MEM = os.environ.get("MEM", "6144") CPU = os.environ.get("CPU", "Broadwell") DEADLINE = int(os.environ.get("DEADLINE", "14400")) WANT_KVM = os.environ.get("KVM", "0") == "1" WANT_NET = os.environ.get("NET", "1") == "1" _needed = [(DISK, "imagen de disco")] if DISK else [(CPIO, "initramfs del builder")] for p, what in _needed + [(KERNEL, "kernel")]: if not os.path.exists(p): sys.exit(f"no existe {what}: {p}") accel = ["-cpu", CPU] if WANT_KVM and os.access("/dev/kvm", os.W_OK): accel = ["-enable-kvm", "-cpu", "host"] print("==> KVM activo (-cpu host)") else: print(f"==> TCG (sin KVM); -cpu {CPU}. Lento — el rebuild Rust puede tardar.") net = ["-netdev", "user,id=n0", "-device", "e1000,netdev=n0"] if WANT_NET else [] # APPEND override: por defecto consola serie + initramfs. Útil para experimentar con params del kernel # (p.ej. el frente kernel-from-source: el bzImage takana bootea pero bwrap falla en pivot_root porque / # es el rootfs absoluto del namespace; `rootfstype=tmpfs` NO lo arregla — la causa es que / no tiene # mount padre, no el tipo de fs). Override con APPEND="…". # Boot de disco (Etapa B3) vs initramfs. En disco / ya es un mount ext4 real ⇒ rdinit=/sbin/init directo, # sin el wrapper switch_root del path initramfs. La GPT tiene 3 particiones: vda1=/ , vda2=/store , # vda3=/var/lib/hammer; el kernel monta vda1 y el wrapper /sbin/init monta las otras dos. if DISK: APPEND = os.environ.get("APPEND", "console=ttyS0 root=/dev/vda1 rw rdinit=/sbin/init") media = ["-drive", f"file={DISK},if=virtio,format=raw", "-append", APPEND] else: APPEND = os.environ.get("APPEND", "console=ttyS0 rdinit=/sbin/init") media = ["-initrd", CPIO, "-append", APPEND] QEMU = (["qemu-system-x86_64", "-m", MEM, "-no-reboot", "-nographic"] + accel + net + ["-kernel", KERNEL] + media) print(f"==> kernel : {KERNEL}") print(f"==> {'disco ' if DISK else 'initramfs '}: {DISK or CPIO}") print(f"==> mem : {MEM} MiB deadline: {DEADLINE}s net: {'sí' if WANT_NET else 'no'}") print(f"==> log : {LOG}") ansi = re.compile(rb'\x1b\[[0-9;?]*[a-zA-Z]') strip = lambda b: ansi.sub(b'', b) master, slave = pty.openpty() p = subprocess.Popen(QEMU, stdin=slave, stdout=slave, stderr=slave, close_fds=True) os.close(slave) log = open(LOG, "wb") buf = b"" sent = False start = time.time() send = lambda s: os.write(master, s.encode()) while True: if time.time() - start > DEADLINE: log.write(b"\n[DRIVER] DEADLINE\n") break r, _, _ = select.select([master], [], [], 10) if r: try: data = os.read(master, 8192) except OSError: break if not data: break log.write(data) log.flush() buf = strip(buf + data)[-20000:] # Boot listo: arje-zero levantó hammerd (fanotify) o apareció la shell. if not sent and (b"watcher fanotify activo" in buf or b"\n~ #" in buf or b"\r~ #" in buf): time.sleep(3) send("echo DRIVER_BEGIN; rebuild-stage1; echo DRIVER_RC=$?\n") sent = True log.write(b"\n[DRIVER] enviado rebuild-stage1\n") log.flush() # OJO: el comando tecleado ECHOA "DRIVER_RC=$?"; el marcador real es la SALIDA # "DRIVER_RC=". Matchear sólo eso (o el veredicto) evita salir antes de tiempo. if sent and (re.search(rb'DRIVER_RC=[0-9]', buf) or b"REPRODUCIBLE" in buf or b"DIVERGENTE" in buf): time.sleep(2) try: log.write(os.read(master, 8192)) except Exception: pass log.write(b"\n[DRIVER] FIN (marcador)\n") break else: if p.poll() is not None: log.write(b"\n[DRIVER] qemu salio\n") break try: p.terminate() time.sleep(2) p.kill() except Exception: pass log.close() # Veredicto a partir de lo capturado. blob = strip(open(LOG, "rb").read()) ok = (b"REPRODUCIBLE" in blob) and (b"DIVERGENTE" not in blob) print(f"\nDRIVER done sent={sent} elapsed={int(time.time() - start)}s") if ok: print("RESULTADO: ✓ REPRODUCIBLE — auto-alojamiento bit a bit verificado") elif b"DIVERGENTE" in blob: print("RESULTADO: ✗ DIVERGENTE — hay no-determinismo que cazar (SDD 09 §2)") else: print("RESULTADO: ? incompleto (timeout/boot/OOM) — revisá el log") sys.exit(0 if ok else 1)