class

Beryl::Bootstrap::QemuInRescue

Inherits Reference < Object

Bootstrap complet FreeBSD via mfsBSD-in-QEMU + post-install no-chroot.

Flux validé manuellement sur loulou le 21 avril 2026 :

. SSH rescue Linux → apt install qemu-system-x86 ovmf sshpass curl . Download mfsBSD SE (cache) . NOGO si BSD déjà sur le disque cible . Upload rescue-run-vm.sh via scp (évite Process.run stdin-pipe) . Exécute le driver shell sur le rescue (UN seul ssh outer, zéro nested côté Crystal → zéro hang macOS). Le driver : .. systemd-run QEMU UEFI (survit à la fermeture ssh) .. attend SSH mfsBSD .. pré-fetch txz + bsdinstall PRÉAMBULE SEUL (pas de post-install chroot) .. remonte ZFS + post-install HORS chroot ciblant /mnt (contourne Capsicum) .. unmount + poweroff . OVH API boot_from_disk + wait SSH FreeBSD (admin user)

Voir ADR-013 pour la justification détaillée du pattern no-chroot.

Constants

DEFAULT_MFSBSD_URL_TEMPLATE = "https://github.com/mmatuska/mfsbsd/releases/latest/download/mfsbsd-se-__VERSION_MFS__-RELEASE-amd64.iso"

Template URL GitHub releases (source officielle actuelle mfsBSD). L'endpoint /releases/latest/download/<asset> redirige toujours vers la release la plus récente — pas besoin de tag à maintenir.

INSTALL_PKGBASE_PATH = "#{WORK_DIR}/install-pkgbase.sh"
INSTALLERCFG = "#{WORK_DIR}/installerconfig"
MFSBSD_ROOT_PASSWORD = "mfsroot"
OVMF_CODE_SOURCE = "/usr/share/OVMF/OVMF_CODE_4M.fd"
OVMF_VARS_PATH = "#{WORK_DIR}/vars.fd"
OVMF_VARS_SOURCE = "/usr/share/OVMF/OVMF_VARS_4M.fd"
QEMU_MAX_RUNTIME = 45.minutes
QEMU_PATTERN = "qemu-system-x86_64.*mfsbsd-se-"

Matche tous les mfsBSD-SE quelle que soit leur version / extension (.iso côté GitHub, .img côté ancien vx.sk).

QEMU_SERIAL = "#{WORK_DIR}/qemu-serial.log"
REBOOT_GRACE_PERIOD = 30.seconds
RESCUE_RUN_VM_PATH = "#{WORK_DIR}/rescue-run-vm.sh"
RESCUE_SUSPECT_AFTER = 2.minutes

Le rescue répond encore « Permission denied » au 1er essai (il n'est pas encore tombé) : au-delà de ce délai, un refus fait vérifier si le serveur n'est pas tout simplement REVENU au rescue.

SSH_POLL_INTERVAL = 15.seconds
SSH_WAIT_TIMEOUT = 30.minutes
TEMPLATE_INSTALL_PKGBASE = {{ read_file("/tmp/tmp.jKlcok/src/src/beryl/bootstrap/templates/install-pkgbase.sh") }}

Script d'install pkgbase autonome (validé in vivo, boot FreeBSD 15). Utilisé quand install_type: packages au lieu de bsdinstall+tarballs.

TEMPLATE_INSTALLERCONFIG = {{ read_file("/tmp/tmp.jKlcok/src/src/beryl/bootstrap/templates/installerconfig.sh") }}
TEMPLATE_RESCUE_RUN_VM = {{ read_file("/tmp/tmp.jKlcok/src/src/beryl/bootstrap/templates/rescue-run-vm.sh") }}
VALID_INSTALL_TYPES = ["distribution_sets", "packages"] of ::String
VALID_RAID = ["stripe", "mirror", "raidz", "raidz2", "raidz3", "mirror_stripe"] of ::String
VM_BOOT_TIMEOUT = 3.minutes
VM_BSDINSTALL_LG = "/tmp/bsdinstall.log"
VM_INSTALLERCFG = "/tmp/installerconfig"
VM_SSH_HOST = "127.0.0.1"
VM_SSH_PORT = 2223
WORK_DIR = "/root/beryl-test"

Constructors

new(rescue_conn : SSH::Connection, disks : Array(String), hostname : String, users : Array(UserSpec), freebsd_version : String, mfsbsd_version : String, abi : String, raid : String = "stripe", timezone : String = "Europe/Paris", iso_url : String | Nil = nil, iso_sha256 : String | Nil = nil, pool_name : String = "zroot", swap_gb : Int32 = 4, qemu_ram_mb : Int32 = 4096, qemu_cpus : Int32 = 4, installed_user : String = "admin", installed_port : Int32 = 22, ovh_client : OvhApi::Client | Nil = nil, ovh_service_name : String | Nil = nil, dedibox_client : DediboxApi::Client | Nil = nil, dedibox_server_id : Int32 | Nil = nil, scaleway_client : ScalewayApi::Client | Nil = nil, scaleway_server_id : String | Nil = nil, scaleway_zone : String | Nil = nil, packages : Array(String) = [] of String, sudoers : Array(String) = [] of String, install_type : String = "distribution_sets", data_pools : Array(DataPoolSpec) = [] of DataPoolSpec, system_datasets : Array(Beryl::Config::SystemDataset) = [] of Beryl::Config::SystemDataset, encryption_root : String | Nil = nil, system_datasets_key_hex : String | Nil = nil, boot_ashift : Int32 = 12, follow_hint_host_name : String | Nil = nil, fqdn : String = "")
Source

Class methods

default_mfsbsd_url(mfsbsd_version : String) : String
Source
denied_verdict(root_os : String | Nil) : Symbol

admin refusé : Linux à root = serveur revenu au rescue ; FreeBSD = FreeBSD démarré mais clé d'admin refusée ; sinon, rien de sûr.

Source
download_command(path : String, url : String, sha256 : String | Nil) : String

Commande (rescue Linux) de téléchargement de l'image. Avec une empreinte : vérifiée par sha256sum -c, y compris pour une image déjà en cache ; une image qui ne correspond pas est effacée et la commande échoue (rien n'est démarré). Pure, exposée pour les specs.

Source
timestamp
Source

Instance methods

all_qemu_disks

Ordre global des disques passés à QEMU : boot d'abord, puis pools data dans l'ordre de déclaration. Les index QEMU correspondent : vtbd0 = mfsBSD, vtbd1..vtbd(N) = boot, vtbd(N+1).. = data.

Source
data_pools
Source
data_pools_script

Script shell (non base64) qui crée les pools data sous /mnt via zpool create. Vide si aucun pool data. Pour chaque pool :

zpool create -f -R /mnt -m <mountpoint> <nom> <vdev...> zpool set cachefile=/mnt/boot/zfs/zpool.cache <nom>

-R /mnt = altroot : les cache files ZFS écrivent sous /mnt (bon emplacement au reboot bare metal). -f car les disques sont neufs, mais zpool create chipote parfois sur résidus.

Le zpool set cachefile est INDISPENSABLE : sans lui, l'export final (zpool export -a) retire le pool du cache, et au reboot bare-metal FreeBSD ne retrouve plus que zroot dans /boot/zfs/zpool.cache. Le pool data existe sur disque (visible via zpool import) mais doit être importé à la main. Constaté terrain quantas.example.net 24 avril 2026.

Pointer vers /mnt/boot/zfs/zpool.cache (le cachefile du système cible, dans zroot altroot /mnt) garantit qu'au reboot, le rc.d/zfs du FreeBSD installé importe les deux pools.

Pools chiffrés : si encryption_key_hex est posé, on génère :

  • Un bloc { ... } qui décode la clé base64 dans une variable locale KEY (jamais en argument de commande).
  • zpool create -O encryption=on -O keyformat=hex -O keylocation=prompt … avec la clé fournie sur stdin.
  • unset KEY pour libérer la mémoire shell après l'usage.
  • PAS de zpool set cachefile : on EXCLUT le pool chiffré du cache pour qu'au reboot bare-metal, FreeBSD ne tente pas d'importer un pool dont la clé n'est pas chargée. L'opérateur fera beryl unlock (qui appelle zpool import + zfs load-key). Voir zpool-encryption-architecture.adoc § « Au bootstrap ».

Le script complet est ensuite encodé en base64 et exécuté côté VM via bash -c "$(echo ... | base64 -d)" — la clé hex n'apparaît jamais dans la ligne de commande.

Source
data_pools_script_b64

Version base64 du script pour injection dans le template shell (évite les problèmes de quoting). Vide si aucun pool data.

Source
dedibox_client
Source
dedibox_server_id
Source
disks
Source
follow_hint_host_name

Nom à suggérer dans le hint beryl follow-install <name> (typiquement FQDN ou <société>/<host>). nil → fallback « <host> ».

Source
freebsd_version
Source
hostname
Source
install_type
Source
installed_port
Source
installed_user
Source
iso_sha256
Source
iso_url

Alias rétrocompat — ancien paramètre iso_url.

Source
mfsbsd_local_path

Chemin local (côté rescue) où l'image mfsBSD SE est téléchargée. Inclut la version pour qu'un changement de version upstream (ex: 14.2 → 15.0) force un nouveau téléchargement au lieu de réutiliser un cache périmé d'une version précédente. L'extension provient de l'URL (.iso GitHub, .img vx.sk).

Source
mfsbsd_url
Source
mfsbsd_version
Source
ovh_client
Source
ovh_service_name
Source
packages
Source
pool_name
Source
qemu_cpus
Source
qemu_ram_mb
Source
qemu_target_disks_args

Args -drive pour chaque disque cible passthrough dans QEMU. Inclut boot ET pools data : on veut que la VM voie tout pour créer les pools data post-install dans le même qemu-system-x86_64. Chaque disque devient un vtbd* dans la VM (vtbd0=mfsBSD, vtbd1+=cibles).

Source
raid
Source
render_install_pkgbase

Rend le script d'install pkgbase (autonome). Multi-disque (Phase 2) : le pool boot s'étend sur vtbd1..vtbd(N) (N = @disks.size ; vtbd0 = mfsBSD), assemblés selon @raid (stripe/mirror/raidz…). Les pools data (vtbd(N+1)..) sont créés via le même data_pools_script que le chemin tarball (mapping vtbd, cachefile, chiffrement réutilisés). Clés root = union des clés SSH des users déclarés.

Source
render_installerconfig

Rend l'installerconfig minimaliste (préambule seul, pas de chroot). ZFSBOOT_DISKS : dans la VM QEMU, les disques passthrough sont vtbd1, vtbd2, … (vtbd0 est l'image mfsBSD elle-même). Ordre = ordre d'apparition dans le -drive côté QEMU.

Source
render_rescue_run_vm

Rend le driver shell. Tous les placeholders de la template sont substitués ici (noms mfsbsd, OVMF, QEMU args, users, packages, sudoers).

Source
rescue_conn
Source
root_authorized_keys_b64

Clés à autoriser pour root (base64, une par ligne). UNIQUEMENT en Option I (/home chiffré) : root clé-seule devient la porte de secours fail-safe lisible avant l'unlock (/root est clair), beryl unlock s'y connecte. Vide sinon → root reste coupé (durcissement historique). Voir zpool-encryption-architecture.adoc § « Accès SSH au boot ».

Source
sudoers
Source
sudoers_base64

Contenu base64 du fichier sudoers (chaque ligne == une règle). Vide si aucune règle → le driver shell skip la création du fichier.

Source
swap_gb
Source
system_datasets
Source
system_datasets_script

Script ZFS des datasets système du profil C+ (profile: standard). Exécuté SUR la VM APRÈS le remount de zroot sur /mnt, AVANT le post-install (users/packages écrivent alors DANS les datasets chiffrés montés, pas dans zroot/ROOT/default qui serait masqué au montage). Flux :

  1. Encryptionroot zroot/encrypted (non monté), clé 64-hex via stdin.
  2. Datasets enfants chiffrés (héritent la clé) + datasets clairs, chacun avec sa compression et son mountpoint (altroot /mnt → /mnt/<mp>).

La clé n'apparaît jamais en argv (base64 → $KEY → stdin). L'zpool export -a final du template largue la clé → datasets verrouillés au reboot bare-metal (l'opérateur fait beryl unlock). Vide si pas de profil.

Source
system_datasets_script_b64
Source
target_disk
Source
timezone
Source
users
Source

Nested types