Skip to content

flyinghorse0510/bsd_go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bsd_go — BSD kernel-driver dev environment

A minimal, scripted environment for spinning up OpenBSD and FreeBSD guests under QEMU on macOS or Linux, with enough structure around it that you can focus on writing kernel-space code instead of babysitting the hypervisor.

  • Hosts supported: macOS (Apple Silicon / Intel), Linux (amd64, arm64).
  • Target guest archs: amd64, arm64, riscv64.
  • Guest distros: OpenBSD (releases + snapshots) and FreeBSD (releases + pre-built VM images).
  • Policy: hardware acceleration (HVF / KVM / WHPX / NVMM) is mandatory whenever it is technically available; falling back to software emulation (TCG) requires an explicit --emulation flag.

TL;DR

# 1. One-time: create and activate the bsd_go Python virtualenv.
source env.sh                                   # bootstraps on first run

# 2. Install QEMU + UEFI firmware on the host (see "Prerequisites" below).
#    macOS:          brew install qemu
#    Debian/Ubuntu:  sudo apt install qemu-system qemu-utils ovmf qemu-efi-aarch64
#    Fedora:         sudo dnf install qemu edk2-ovmf edk2-aarch64 edk2-riscv
#    Arch:           sudo pacman -S qemu-full edk2-ovmf edk2-armvirt

# 3. Confirm everything is wired up.
./bsd_go.py qemu info --all-archs

# 4. Download the latest OpenBSD ISO for the host architecture.
#    The mirror is auto-picked from your region (IP geolocation →
#    same-country → same-continent → distro CDN). Override with
#    --region CC or --mirror URL if needed.
./bsd_go.py images get

# 5. Create a 40 GiB qcow2 disk for a new VM called "openbsd".
./bsd_go.py qemu disk create openbsd --size 40G

# 6. Boot the installer in the terminal (text-console mode). The OpenBSD /
#    FreeBSD installers talk to the serial console, so --tty puts the
#    installer in your terminal where you can actually type. Without --tty
#    you get a GUI window that usually shows nothing interactive and the
#    installer prompt ends up on serial where no one is listening.
./bsd_go.py qemu install openbsd --tty \
    --cdrom images/openbsd/7.8/arm64/install78.iso

# 7. After the installer finishes and halts the VM, boot normally in text
#    mode with SSH forwarded from localhost:2222 to guest:22.
./bsd_go.py qemu run openbsd --tty --ssh-port 2222
# Escape: Ctrl-A x to exit QEMU, Ctrl-A c to drop into the QEMU monitor.

# Cleanup when you're done:
./bsd_go.py qemu disk delete openbsd --yes
./bsd_go.py images delete --distro openbsd --version 7.8 --yes
# or nuke everything:
./bsd_go.py qemu disk erase --yes
./bsd_go.py images erase --yes

Want FreeBSD instead? Swap step 4 for:

./bsd_go.py images get --distro freebsd --type vm-qcow2 --decompress
./bsd_go.py qemu disk create freebsd --distro freebsd --size 40G
# point --disk at the decompressed qcow2, or convert the pre-built image in
# place — see "Recipes" below.

Repo layout

bsd_go/
├── bsd_go.py            # top-level wrapper around scripts/images.py + scripts/qemu.py
├── env.sh               # sourceable shell hook: activates the venv (bootstraps if missing)
├── env/
│   └── install_deps.sh  # creates ~/opt/python_venv/bsd_go and pip-installs requirements
├── images/              # downloaded ISOs / VM images (gitignored contents, .gitkeep tracked)
│   └── .gitkeep
├── disks/               # per-VM qcow2 + UEFI NVRAM + sockets (gitignored contents)
│   └── .gitkeep
└── scripts/
    ├── _common.py       # host/arch detection, download helper, QEMU discovery
    ├── _distros.py      # mirror + URL + file-naming knowledge for OpenBSD / FreeBSD
    ├── images.py        # release-image manager (get / list / delete / erase / verify)
    ├── qemu.py          # VM + disk + snapshot manager
    └── requirements.txt # typer + rich

You can either drive everything through the unified wrapper:

./bsd_go.py <group> <subcommand> ...

or invoke the sub-scripts directly:

./scripts/images.py <subcommand> ...
./scripts/qemu.py   <subcommand> ...

Both paths are identical — bsd_go.py just mounts the two apps under images and qemu subcommand groups.


Prerequisites

Python virtualenv (automated)

The repo ships with an env.sh hook that creates an isolated venv at ~/opt/python_venv/bsd_go on first use and activates it in your current shell:

source env.sh         # bash
. env.sh              # any POSIX shell

On first invocation this calls env/install_deps.sh, which:

  1. Verifies Python 3.10+ is on PATH,
  2. Creates ~/opt/python_venv/bsd_go via python3 -m venv,
  3. pip install -r scripts/requirements.txt (typer + rich).

Subsequent source env.sh invocations just activate the venv — no pip reinstall. Override the venv path with BSD_GO_VENV or re-run env/install_deps.sh directly if you want to upgrade packages.

Python (manual alternative)

If you prefer your own venv / system install:

python3 -m venv .venv
. .venv/bin/activate
pip install -r scripts/requirements.txt

Python 3.10 or newer is required.

QEMU + UEFI firmware

Host Install
macOS (Homebrew) brew install qemu — ships edk2 firmware for amd64, arm64, riscv64 under share/qemu/.
Debian / Ubuntu sudo apt install qemu-system qemu-utils ovmf qemu-efi-aarch64
Fedora / RHEL sudo dnf install qemu edk2-ovmf edk2-aarch64 edk2-riscv
Arch Linux sudo pacman -S qemu-full edk2-ovmf edk2-armvirt (riscv edk2 is community/AUR)

If a target arch's UEFI firmware is missing the scripts print a distro-aware hint. riscv64 also falls back to QEMU's bundled OpenSBI firmware (fine for non-UEFI boots).

KVM (Linux)

Make sure your user can open /dev/kvm:

sudo usermod -aG kvm "$USER"   # then log back in

Without a writable /dev/kvm, native-arch runs require --emulation (TCG).


bsd_go.py

Top-level CLI. It delegates to scripts/images.py and scripts/qemu.py, plus a small paths command for sanity checks.

$ ./bsd_go.py --help
Usage: bsd_go.py [OPTIONS] COMMAND [ARGS]...

  Unified CLI for OpenBSD / FreeBSD VM workflows
  (wraps scripts/images.py and scripts/qemu.py).

Commands:
  paths   Print the repo-root directories the tools read and write.
  images  Manage OpenBSD / FreeBSD release images: get / list / delete / erase.
  qemu    Manage QEMU VMs: disks, snapshots, installer, run, console, ps/kill.
$ ./bsd_go.py paths
bsd_go :: paths
 repo root     /path/to/bsd_go
 scripts dir   /path/to/bsd_go/scripts
 images dir    /path/to/bsd_go/images    (downloaded ISOs / VM images)
 disks dir     /path/to/bsd_go/disks     (per-VM qcow2 / NVRAM / sockets)
 ...

images — fetch, list, delete release images

$ ./bsd_go.py images --help
Commands:
  info       Print detected host and default download targets.
  versions   List releases visible on the mirror.
  mirrors    List configured mirrors for the selected distro.
  archs      List architectures supported by the distro.
  files      List files available for a release / architecture.
  get        Download a BSD release for an arch / version combination.
  verify     Verify a single file's SHA256 against a manifest.
  list       List cached images under the images directory, grouped by distro.
  delete     Delete cached images, filtered by distro / version / arch / file.
  erase      Wipe every cached image; keeps the images/ directory + .gitkeep.

The info, versions, mirrors, archs, files, get and verify commands all accept --distro openbsd|freebsd (default openbsd), --mirror URL, and --region CC.

Mirror auto-selection

The first time any command needs to talk to a mirror it calls a free IP geolocation service (tries ipinfo.io, ifconfig.co, api.country.is in order) to learn the user's ISO alpha-2 country code, then picks a mirror using this priority list:

  1. Same country — e.g. a Singapore user picks https://mirror.freedif.org/pub/OpenBSD.
  2. Same continent — e.g. a Singapore FreeBSD user (no .sg mirror in the FreeBSD project) falls through to a nearby Asian mirror (mirrors.ustc.edu.cn/freebsd).
  3. Distro default CDNcdn.openbsd.org / download.freebsd.org.

Overrides (all via flags or env vars):

want this pass this
Explicit mirror (highest priority) --mirror URL or BSD_MIRROR=URL
Explicit region (skips IP lookup) --region CC or BSD_REGION=CC
Skip region matching entirely --region none or BSD_REGION=none
See which mirror would be auto-selected ./bsd_go.py images mirrors (★ marks pick)
Confirm all mirrors are reachable ./bsd_go.py images mirrors --probe

The per-process result is cached, so repeated subcommands in one invocation don't do extra HTTP lookups. If geolocation fails (no network, every endpoint down), the scripts print a warning and fall back to the distro's default CDN.

info

Host detection, detected region, and the mirror that would be auto-selected.

./bsd_go.py images info                    # OpenBSD defaults, auto region
./bsd_go.py images info --distro freebsd   # FreeBSD defaults, auto region
./bsd_go.py images info --region DE        # force Germany region
./bsd_go.py images info --region none      # skip geolocation, use CDN
./bsd_go.py images info --offline          # skip the HTTP probes entirely

Sample output for a Singapore host:

images :: openbsd defaults
 host OS              macos
 host arch (raw)      arm64
 guest arch (mapped)  arm64
 distro               openbsd
 detected region      SG
 selected mirror      https://mirror.freedif.org/pub/OpenBSD  (auto)
 default CDN mirror   https://cdn.openbsd.org/pub/OpenBSD
 default images dir   /path/to/bsd_go/images
 latest release       7.8

versions, mirrors, archs, files

./bsd_go.py images versions --distro freebsd --limit 10
./bsd_go.py images mirrors  --distro openbsd --probe    # HEAD each mirror
./bsd_go.py images archs    --distro freebsd --probe --version 14.4
./bsd_go.py images files    --distro openbsd --version 7.8 --arch arm64
./bsd_go.py images files    --distro freebsd --variant vm --version 14.4

--probe HEADs every URL so the tables mark entries reachable/not reachable; files --variant vm lists the pre-built VM-IMAGES tree for FreeBSD.

The mirrors command marks the entry that would be auto-picked for the current host with :

openbsd mirrors  (★ = auto-select for this host, country=SG)
  #  ★  URL                                     country  region
  1  ★  https://mirror.freedif.org/pub/OpenBSD  SG       Singapore
  2     https://ftp.jaist.ac.jp/pub/OpenBSD     JP       Japan (Nomi, Ishikawa)
  ...

Pass --region or --mirror here to see what would be picked under a different policy without actually changing anything.

get

The main download command. Combines presets, explicit file names, glob filters, and optional post-processing.

Presets (--type / -t):

distro preset files expanded
OpenBSD iso* install<V>.iso
OpenBSD img install<V>.img
OpenBSD full install<V>.iso + install<V>.img
OpenBSD miniroot miniroot<V>.img
OpenBSD kernel bsd, bsd.mp, bsd.rd
OpenBSD sets base<V>.tgz, comp<V>.tgz, man<V>.tgz, game<V>.tgz, x*<V>.tgz
OpenBSD sums SHA256, SHA256.sig
OpenBSD all every ISO, IMG, miniroot, kernel, set
FreeBSD iso* FreeBSD-<VER>-RELEASE-<A>-disc1.iso
FreeBSD dvd ...-dvd1.iso
FreeBSD bootonly ...-bootonly.iso
FreeBSD memstick ...-memstick.img
FreeBSD mini ...-mini-memstick.img
FreeBSD full disc1 ISO + memstick IMG
FreeBSD sums CHECKSUM.SHA256-..., CHECKSUM.SHA512-... (ISO-IMAGES suffixed names)
FreeBSD all all ISOs and mem-stick images
FreeBSD vm, vm-qcow2 FreeBSD-<VER>-RELEASE-<A>.qcow2.xz (VM-IMAGES subtree)
FreeBSD vm-raw ...raw.xz
FreeBSD vm-vmdk ...vmdk.xz
FreeBSD vm-vhd ...vhd.xz
FreeBSD vm-all every qcow2/raw/vmdk/vhd in VM-IMAGES

*Default when --type is omitted.

Examples:

# Latest OpenBSD ISO for host arch, SHA256-verified into images/openbsd/<ver>/<arch>/
./bsd_go.py images get

# Full USB image for OpenBSD amd64 7.7 (override arch + version)
./bsd_go.py images get --arch amd64 --version 7.7 --type img

# OpenBSD -CURRENT (snapshots); auto-detects the real version tag from the mirror
./bsd_go.py images get --snapshots --arch arm64

# Just the sets (base/comp/man/X), parallel download, verified
./bsd_go.py images get --type sets --jobs 4

# FreeBSD pre-built qcow2 for arm64, decompressed in place
./bsd_go.py images get --distro freebsd --type vm-qcow2 \
    --arch arm64 --decompress

# Explicit file(s) via --file (one or more), bypasses presets
./bsd_go.py images get --file install78.iso --file bsd.rd

# Glob filter against the mirror listing
./bsd_go.py images get --include 'install*.iso' --exclude '*.sig'

# Dry-run: show the plan, download nothing
./bsd_go.py images get --dry-run

# Just list what would be fetched, then exit
./bsd_go.py images get --list

Switches worth knowing:

  • --output DIR / $BSD_IMAGES_DIR — override the target directory (default images/ at the repo root).
  • --flat — skip the <distro>/<version>/<arch>/ subdirs.
  • --force / -f — re-download existing files.
  • --resume/--no-resume — toggle HTTP range resume (on by default).
  • --jobs N — parallel downloads (default 1, max 16). Note: the per-file progress bar is rendered only when --jobs 1.
  • --verify/--no-verify — SHA256 manifest verification (on by default).
  • --signify (+ --signify-key) — additionally run signify verification on OpenBSD SHA256.sig files; requires the signify binary (brew install signify-osx, apt install signify-openbsd).
  • --decompress — transparently unxz any .xz outputs (mostly FreeBSD VM-IMAGES). Use --delete-compressed to drop the .xz after.
  • --mirror URL / $BSD_MIRROR — override the default mirror.
  • --timeout SECONDS — HTTP timeout per request.

verify

Verify a single file against an adjacent SHA256 / CHECKSUM.SHA256:

./bsd_go.py images verify images/openbsd/7.8/arm64/install78.iso

list

Inventory of what's already cached on disk, grouped by distro / version / arch:

./bsd_go.py images list
cached images @ /path/to/bsd_go/images
 distro   version   arch    files   size
 openbsd  7.8       arm64       3   60.6 MiB
 freebsd  14.4      amd64       3   1.2 GiB
i total: 6 files, 1.3 GiB

--output DIR / $BSD_IMAGES_DIR points the command at a different cache root.

delete

Remove images filtered by distro / version / arch / file. At least --distro is required; the other filters let you zero in on a specific subtree.

# Delete everything for one distro
./bsd_go.py images delete --distro freebsd

# Delete one release (all archs under it)
./bsd_go.py images delete --distro openbsd --version 7.7

# Delete one arch inside a release
./bsd_go.py images delete --distro openbsd --version 7.8 --arch arm64

# Delete a single file
./bsd_go.py images delete --distro openbsd --version 7.8 --arch arm64 \
    --file install78.iso

# Preview without touching anything
./bsd_go.py images delete --distro openbsd --version 7.7 --dry-run

Flags:

  • --yes / -y — skip the "are you sure?" prompt.
  • --dry-run — print the target path (and size), then exit.
  • --output DIR — act on a non-default images root.

Empty parent directories (e.g. images/openbsd/ after its last version is gone) are pruned automatically; the top-level images/ and its .gitkeep sentinel are always preserved.

erase

The nuclear option — wipes every cached image:

./bsd_go.py images erase              # interactive prompt
./bsd_go.py images erase --yes        # no prompt
./bsd_go.py images erase --dry-run    # show what would go

The images/ directory and its .gitkeep are left behind; everything else inside (distros, versions, archs, files) is removed.


qemu — disks, snapshots, VMs

$ ./bsd_go.py qemu --help
Commands:
  info      Show host, QEMU, and acceleration details.
  check     Verify required tools for the guest architecture(s).
  run       Boot a VM (no installer media unless --cdrom passed).
  install   Run the installer: boot the VM with an ISO attached as a CD-ROM.
  ps        List VMs that appear to be running.
  kill      Stop a running VM (by sending a signal to its PID).
  console   Attach to the VM's HMP monitor Unix socket.
  eject     Eject a CD-ROM via QMP.
  disk      Create / inspect / resize / delete / erase disks.
  snapshot  Manage internal qcow2 snapshots.

VM layout

Each VM lives in a single directory under disks/:

disks/<name>/
├── meta.json         # arch, distro, memory, smp, uefi flag, disk list, last_run
├── disk-0.qcow2      # primary disk (virtio-blk)
├── efi-vars.fd       # UEFI NVRAM (arm64 / riscv64 / --uefi on amd64)
├── qemu.pid          # present only while the VM is running
├── monitor.sock      # HMP Unix socket
└── qmp.sock          # QMP Unix socket (eject, etc.)

Per-VM run config (last_run)

Every successful qemu run / qemu install writes the effective options back to meta.json under last_run. The next qemu run <name> reads that as its defaults, so you don't have to re-type --tty, --ssh-port 2222, --port-forward tcp::8080-:80, --memory 8G, etc. every time. CLI flags you pass this time always beat the stored values.

Typical flow:

# First time: pin the options you want.
./bsd_go.py qemu run obsd --tty --ssh-port 2222 --memory 4G

# Later: inherits everything from last_run automatically.
./bsd_go.py qemu run obsd

# Override one thing without touching the rest.
./bsd_go.py qemu run obsd --ssh-port 3333      # tty + memory still 4G, ssh now 3333

# Start fresh (discard saved config, keep the disk).
./bsd_go.py qemu disk reset obsd

# Run without persisting this invocation's choices.
./bsd_go.py qemu run obsd --snapshot --no-save

# Inspect what's stored:
python3 -c 'import json; print(json.dumps(json.load(open("disks/obsd/meta.json"))["last_run"], indent=2))'

What's persisted: memory/smp/cpu/machine, accel + emulation, headless/tty, display, vga, vnc, serial, monitor, network mode/model/forwards/ssh-port/mac, uefi/firmware/nvram, rng/balloon/tablet/keyboard/usb, audio, virtiofs, boot, exit-on-reboot, and raw --extra args. Not persisted: --cdrom (install media is one-shot), --snapshot, --detach, --dry-run, --qmp-port.

Saves happen just before QEMU launches, so even if a boot fails you still get "the last thing I asked for". Skip with --no-save for a one-shot run; clear with qemu disk reset <name>.

You can also edit meta.json directly — it's plain JSON. Three shortcuts keep the round-trip safer than a plain text editor:

# Print the file (syntax-highlighted).
./bsd_go.py qemu disk show obsd

# Raw JSON for piping into jq / python / whatever.
./bsd_go.py qemu disk show obsd --raw | jq '.last_run'

# Single-field tweak (JSON-parsed by default):
./bsd_go.py qemu disk set obsd memory 8G
./bsd_go.py qemu disk set obsd smp 4
./bsd_go.py qemu disk set obsd last_run.ssh_port 3333
./bsd_go.py qemu disk set obsd last_run.headless true
./bsd_go.py qemu disk set obsd last_run.port_forward '["tcp::8080-:80"]'
./bsd_go.py qemu disk set obsd last_run null           # clear wholesale

# Full edit in your $EDITOR (vim / nvim / code -w / ...).
./bsd_go.py qemu disk edit obsd           # uses $EDITOR, then $VISUAL, then vi
./bsd_go.py qemu disk edit obsd --editor nvim

Both edit and set refuse to run while the VM is live, and both roll back to a backup if the resulting file is no longer valid JSON / no longer loads as a VMMeta. Type-correctness of individual fields isn't checked — if you put smp: [1,2,3] you'll get a QEMU launch error next time, not a save error.

info / check

./bsd_go.py qemu info                  # single-arch (host arch by default)
./bsd_go.py qemu info --arch riscv64   # drill into a specific target
./bsd_go.py qemu info --all-archs      # table across amd64/arm64/riscv64

./bsd_go.py qemu check --arch amd64
./bsd_go.py qemu check --all-archs     # fail/warn per missing piece

info --all-archs prints a table like:

 arch    qemu-system  accel       native  firmware CODE  vars tmpl
 amd64   ok           tcg (TCG)   no      ok             ok
 arm64   ok           hvf (HW)    yes     ok             ok
 riscv64 ok           tcg (TCG)   no      ok             ok

disk group

Commands:
  create   Create a new VM and its primary disk.
  list     List VMs and their disks.
  info     Run `qemu-img info` on a VM's disk.
  resize   Resize a VM's disk (extending is safe, shrinking is not).
  convert  Convert a disk to another format.
  show     Pretty-print meta.json (or `--raw` for `| jq` pipes).
  edit     Open meta.json in $EDITOR; rolls back on invalid JSON.
  set      Set a single field (dot-notation into last_run), JSON-parsed.
  reset    Clear the VM's stored last_run config (see "Per-VM run config").
  delete   Delete a single VM's directory (disks + metadata + sockets).
  erase    Wipe every VM directory; keeps disks/ + .gitkeep.
./bsd_go.py qemu disk create <name>      # create qcow2 + metadata (+ NVRAM for UEFI)
./bsd_go.py qemu disk list               # all VMs: distro, arch, running, disks, memory
./bsd_go.py qemu disk info <name>        # qemu-img info on the primary disk
./bsd_go.py qemu disk resize <name> SIZE # +10G, 80G, etc. (VM must be stopped)
./bsd_go.py qemu disk convert <name> OUT --format qcow2
./bsd_go.py qemu disk delete <name>      # rm -rf disks/<name>/ (confirms; --yes to skip)
./bsd_go.py qemu disk erase              # rm -rf every disks/*/ (confirms; --yes to skip)

disk create options worth knowing:

  • --arch {amd64,arm64,riscv64} (default: host's canonical arch).
  • --force-arch — allow uncommon target arches (i386, sparc64, ...); scripts are still best-effort outside the supported three.
  • --distro openbsd|freebsd — stored in metadata so later tables can tag the VM.
  • --size 40G — disk size (K/M/G/T units).
  • --memory 2G, --smp 2, --cpu host — stored as defaults for run/install.
  • --uefi/--no-uefi — default is on for arm64/riscv64, off for amd64. Flip --uefi on amd64 if you want UEFI firmware (OVMF) instead of SeaBIOS.
  • --machine q35 — override the default QEMU machine type.
  • --preallocation metadata|falloc|full — pass through to qemu-img create.
  • --force — overwrite an existing VM directory.

disk delete options:

  • --yes / -y — skip the "are you sure?" prompt.
  • --keep-config — delete the disk files but keep meta.json (so you can disk create --force back with the same settings).

disk erase options:

  • --yes / -y — skip the prompt.
  • --dry-run — list every VM that would be wiped (and its size), then exit.
  • --stop-running — if any listed VM is still running, SIGTERM it first. Without this flag, erase refuses to proceed.
  • --force — combined with --stop-running, escalates to SIGKILL for VMs that don't exit within 10 seconds of SIGTERM.

snapshot group

Uses qcow2 internal snapshots (must be stopped):

./bsd_go.py qemu snapshot create <name> fresh-install
./bsd_go.py qemu snapshot list   <name>
./bsd_go.py qemu snapshot revert <name> fresh-install
./bsd_go.py qemu snapshot delete <name> fresh-install

Pair with qemu run --snapshot (a separate flag) if you want a run that discards all disk writes on exit without creating a snapshot at all.

install — boot the installer

./bsd_go.py qemu install <name> --cdrom <path/to.iso>

Attaches the ISO as a virtio-scsi CD-ROM with bootindex=0 so it boots ahead of the disk. All run options below are available too.

run — boot a VM

./bsd_go.py qemu run <name>                          # use meta's defaults
./bsd_go.py qemu run <name> --tty --ssh-port 2222
./bsd_go.py qemu run <name> --detach                 # background, via -daemonize + pidfile
./bsd_go.py qemu run <name> --cdrom <path>           # attach an ISO alongside the disk
./bsd_go.py qemu run <name> --snapshot               # discard disk writes on exit
./bsd_go.py qemu run <name> --dry-run                # print the QEMU command, exit
./bsd_go.py qemu run --disk /some/external.qcow2    # anonymous VM, no metadata

Option groups (complete list via --help):

  • CPU / memory: --arch, --machine, --cpu, --smp, --memory, --uuid.
  • Acceleration: --accel auto|hvf|kvm|whpx|tcg, --emulation (required to allow TCG fallback).
  • Disks / media: --disk (repeatable), --extra-disk (repeatable), --cdrom (repeatable), --boot cdn, --snapshot.
  • Display (see "Display modes" below for the full matrix): --tty / --text / --headless, --display cocoa|gtk|sdl|vnc|curses|none, --vga virtio|std|cirrus|qxl, --vnc :1.
  • Serial / monitor: --serial mon:stdio|pty|..., --monitor ..., --qmp-port 4444.
  • Network: --network user|bridge|tap|none, --net-model virtio-net-pci, --port-forward tcp::8080-:80 (repeatable), --ssh-port 2222, --mac ....
  • UEFI: --uefi/--bios, --firmware PATH, --nvram PATH.
  • Devices: --rng/--no-rng, --balloon/--no-balloon, --tablet/--no-tablet, --usb/--no-usb, --audio none|coreaudio|pa|alsa, --virtiofs PATH:TAG (repeatable).
  • Escape hatches: --extra <raw QEMU arg> (repeatable), --dry-run, --detach.

Display modes

mode flags how to interact when to use
GUI window (default) (no flag) Click the window to capture keyboard. Guest that has a real graphical console.
Text / serial (recommended) --tty (aliases: --text, --headless) Type in the current terminal; Ctrl-A x exits. OpenBSD / FreeBSD installer, day-to-day dev.
Curses framebuffer --display curses In-terminal ncurses rendering of the guest VGA. BIOS / text-mode output without giving up the terminal.
Detached background --detach (+ --tty) Reattach via qemu.py console <name> or SSH. Long-running VMs; driver dev with scp/ssh loops.
VNC --vnc :1 or --vnc 127.0.0.1:5900 Connect a VNC viewer to the advertised port. Remote graphical access over SSH tunnel.

Why --tty exists: the OpenBSD and FreeBSD installers print to com0 (serial) by default. In the default GUI window the installer output is invisible and anything you type there goes nowhere useful, so the VM appears "frozen" even though it's actually sitting at a serial prompt. --tty wires QEMU's serial port to the terminal you launched from, so the prompt lands in front of you and you can type at it directly. The three flags (--tty, --text, --headless) are all aliases for the same mode — pick whichever reads best in your command history.

Escape sequences while in --tty mode:

  • Ctrl-A x — exit QEMU (sends ACPI powerdown; hard-kill with Ctrl-A then x twice if needed).
  • Ctrl-A c — toggle between the guest serial and the QEMU monitor (info cpus, info mem, sendkey ctrl-alt-f1, quit, ...).
  • Ctrl-A h — list all escape sequences.

Managing a running VM

./bsd_go.py qemu ps                    # list running VMs (pid, qmp/monitor paths)
./bsd_go.py qemu console <name>        # attach to monitor.sock (Ctrl-D to detach)
./bsd_go.py qemu eject <name>          # eject cd0 via QMP (after install)
./bsd_go.py qemu kill <name>           # SIGTERM; --force to SIGKILL

Target architectures

guest machine UEFI CODE default path (macOS Homebrew)
amd64 q35 /opt/homebrew/share/qemu/edk2-x86_64-code.fd
arm64 virt,highmem=on /opt/homebrew/share/qemu/edk2-aarch64-code.fd
riscv64 virt (+ OpenSBI) /opt/homebrew/share/qemu/edk2-riscv-code.fd

On Linux the same file names live under /usr/share/OVMF/, /usr/share/AAVMF/, /usr/share/qemu-efi-aarch64/, /usr/share/edk2/* or /usr/share/edk2-ovmf/, /usr/share/edk2-armvirt/, /usr/share/edk2-riscv/ depending on your distro — the scripts search all of them.


Hardware acceleration policy

qemu.py will refuse to start a VM whose selected accelerator is tcg unless you pass --emulation. The check cascades like this:

  1. Same-arch host + guest (e.g. arm64 Mac running an arm64 guest) → pick the host's accelerator (hvf / kvm / whpx / nvmm). If none are usable (/dev/kvm missing, wrong user, etc.) you get a pointed error unless you confirm TCG with --emulation.
  2. Cross-arch (e.g. amd64 guest on arm64 host) → hardware accel is impossible. You must opt in with --emulation. TCG cross-arch boots routinely take 3–10× longer.
  3. Explicit --accel tcg is also guarded by --emulation so you can never hit TCG by accident.

If you supply --cpu host in metadata but end up on TCG, the scripts auto-downgrade to -cpu max and warn (because -cpu host refuses to start under TCG on amd64 and riscv64).


Recipes

OpenBSD arm64 on an Apple Silicon Mac

source env.sh                                             # activate venv
./bsd_go.py images get                                    # install78.iso + SHA256
./bsd_go.py qemu disk create obsd --size 40G              # UEFI on by default
./bsd_go.py qemu install obsd \
    --cdrom images/openbsd/7.8/arm64/install78.iso
# ...go through installer, pick sd0, halt when done...
./bsd_go.py qemu run obsd --tty --ssh-port 2222
ssh -p 2222 root@localhost

After a known-good install, snapshot it:

./bsd_go.py qemu snapshot create obsd fresh-install
./bsd_go.py qemu snapshot revert obsd fresh-install   # rollback any time

FreeBSD arm64 via the pre-built qcow2

./bsd_go.py images get --distro freebsd \
    --type vm-qcow2 --arch arm64 --decompress
./bsd_go.py qemu disk create fbsd --distro freebsd --size 40G
# Replace the empty primary disk with the downloaded qcow2:
cp images/freebsd/15.0/arm64/FreeBSD-15.0-RELEASE-arm64-aarch64.qcow2 \
   disks/fbsd/disk-0.qcow2
./bsd_go.py qemu run fbsd --tty --ssh-port 2222

Cross-arch (amd64 guest on arm64 host)

./bsd_go.py qemu disk create obsd-x86 --arch amd64 --size 40G --uefi
./bsd_go.py qemu install obsd-x86 --emulation \
    --cdrom images/openbsd/7.8/amd64/install78.iso --uefi
./bsd_go.py qemu run obsd-x86 --emulation --tty --ssh-port 2223

RISC-V target

./bsd_go.py images get --arch riscv64                     # OpenBSD riscv64 ISO
./bsd_go.py qemu disk create obsd-rv --arch riscv64 --size 40G
./bsd_go.py qemu install obsd-rv --emulation \
    --cdrom images/openbsd/7.8/riscv64/install78.iso

Driver dev loop (build on host, load on guest)

# In one terminal: VM running, serial + SSH exposed
./bsd_go.py qemu run obsd --tty --ssh-port 2222 --detach

# In another terminal:
scp -P 2222 -r /path/to/driver/src root@localhost:/root/
ssh -p 2222 root@localhost 'cd /root/src && make && make load'

--detach parks QEMU in the background; qemu.py ps shows its pid, qemu.py console obsd attaches to the monitor socket, and qemu.py kill obsd stops it cleanly.

Reclaiming disk space

# See what you've got cached
./bsd_go.py images list
./bsd_go.py qemu disk list

# Drop one release
./bsd_go.py images delete --distro openbsd --version 7.7

# Drop everything (images + VMs)
./bsd_go.py images erase --yes
./bsd_go.py qemu disk erase --yes

images/.gitkeep and disks/.gitkeep survive both operations — the top-level directories stay tracked in git.


Environment variables

var effect
BSD_MIRROR explicit mirror for every images command (highest priority)
BSD_REGION ISO alpha-2 country code for images auto-selection (none to skip)
BSD_IMAGES_DIR default output dir for images {get,list,delete,erase} (overridden by --output)
BSD_GO_VENV venv path used by env.sh and env/install_deps.sh (default ~/opt/python_venv/bsd_go)
PYTHON Python interpreter used by env/install_deps.sh (default python3)
OPENBSD_MIRROR* legacy alias, still accepted

*Prefer BSD_MIRROR going forward.


Troubleshooting

"Guest arch ... differs from host ...; hardware acceleration is impossible." You asked for a cross-arch boot. Add --emulation to confirm you want the TCG path, or drop --arch to run a native-arch guest.

"UEFI firmware for arm64 not found on standard paths." Install edk2 for your host's package manager (see the Prerequisites table) or pass --firmware /path/to/edk2-aarch64-code.fd explicitly.

"/dev/kvm not available (check kvm module / group)." (Linux) Add your user to the kvm group (sudo usermod -aG kvm $USER), reboot or log out/in, and make sure the kvm_intel / kvm_amd module is loaded.

FreeBSD VM-IMAGES checksum file not matching. FreeBSD uses two naming schemes for manifests: plain CHECKSUM.SHA256 inside the VM-IMAGES/Latest/ tree, and CHECKSUM.SHA256-FreeBSD-<VER>-RELEASE-<A> inside ISO-IMAGES/. The scripts handle both — just make sure --type matches what you actually want (vm-qcow2 for VM-IMAGES, iso/dvd/etc. for the installer tree).

Signature verification requested but signify is missing. Either install it (brew install signify-osx, apt install signify-openbsd) and point --signify-key at the matching openbsd-<VER>-base.pub, or drop --signify and rely on SHA256 alone.

"images delete --distro openbsd --file install78.iso" fails. --file is relative to a single <distro>/<version>/<arch>/ tuple, so it needs --version and --arch too. Examples:

./bsd_go.py images delete --distro openbsd --version 7.8 --arch arm64 \
    --file install78.iso

"disk erase" refuses because a VM is running. Either stop the VM first (qemu kill <name>) or pass --stop-running (and optionally --force to SIGKILL any VM that won't SIGTERM within 10 s).

Auto-mirror picked something strange / far away. Pin it with --region CC (ISO alpha-2, e.g. --region SG) or --mirror URL; or force the distro CDN with --region none. Run ./bsd_go.py images mirrors to see the full list plus which entry would be auto-picked (marked ★).

"I get a GUI window and can't type anything." The default display is a cocoa (macOS) / gtk (Linux) window; BSD installers print to com0 (serial), so the window shows nothing interactive and keystrokes don't go anywhere useful. Pass --tty (or --text / --headless, they're aliases) on qemu run or qemu install to put the serial console into your terminal where you can type at it directly. See "Display modes" above for the full comparison.

Geolocation is blocked on my network. Same fix: --region CC skips the HTTP lookup entirely, as does --region none (which uses the distro's default CDN regardless of where you are).

OpenBSD installer boots but says "no installable disk" Check that the CD attaches via virtio-scsi (it does in this project) and that your disk is big enough for OpenBSD's minimum layout (1 GiB works, 4 GiB is saner). Serial console: at the boot> prompt, type set tty com0 if you want text install via --tty (alias: --headless).

--serial stdio + --detach refused That combination can't work (stdio has no parent when daemonized). Either drop --detach or pass --serial unix:<path>.sock.


License & notes

Personal / internal project; no license file yet. OpenBSD and FreeBSD release images are redistributed under their respective licenses — consult https://www.openbsd.org/faq/faq1.html and https://www.freebsd.org/copyright/ before rehosting.

Hardware acceleration is a correctness choice, not a performance one: cross-arch TCG is slow and can mask bugs in lock-step drivers (IPI timing, MMIO ordering, cache-line races). Run on a native host whenever practical.

About

Spawn BSD VM conveniently on macOS and Linux

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors