OPENHVF(1) General Commands Manual OPENHVF(1)

openhvfinstall and manage OpenBSD virtual machines

openhvf [-hq] [--vm name] [--project dir] [--emulate] command [argument ...]

openhvf installs and manages OpenBSD virtual machines under QEMU. It downloads the installation image, creates the disk image, answers the installer over the serial console, and from then on controls the machine's lifecycle, guest access, and console automation. Getting from nothing to a machine that accepts ssh(1) is one command, and running it again does nothing.

The host does not have to run OpenBSD. Anything that runs QEMU will do, and the accelerator is chosen to suit it, so a Linux or Darwin workstation can build, test, or reproduce a bug on a real OpenBSD system without one on the desk.

Within the OpenHAP source tree, openhvf is a development tool: it is not installed by ‘make install’ and is not part of any release package.

openhvf operates on a project: a directory containing an .openhvfrc configuration file and an .openhvf/ state directory, created by the init command. Unless --project is given, the project root is auto-discovered by searching upward from the current directory for an .openhvfrc file.

The options are as follows:

name
Operate on the named VM. The default is “default”. VM definitions live in .openhvf/vms/name.conf.
dir
Use dir as the project root instead of auto-discovering it.
, --quiet
Suppress informational output.
Force TCG software emulation instead of hardware acceleration. Without this flag the accelerator is chosen automatically: HVF on macOS, KVM on aarch64 Linux hosts with /dev/kvm, and TCG everywhere else.
, --help
Display usage information.

The following commands are available:

[dir]
Initialize a project in dir (default: the current directory). Creates the .openhvfrc project configuration, the .openhvf/ directory with vms/ and state/ subdirectories, and a default VM configuration. Idempotent: does nothing if the project is already initialized.
[--no-cache]
Ensure the VM is running. Downloads the installation image, creates the disk image, and starts the VM as needed. Idempotent.

Unless the installed-image cache is disabled, up takes both sides of that cache: with no disk image present it creates one as an overlay on a matching cached base and boots the installed system directly, skipping the miniroot download and the installer entirely; after an installation it publishes the freshly installed disk as a new cached base. Caching is best effort: a failure warns and the VM comes up anyway, installing from scratch or keeping a standalone disk.

--no-cache skips both the restore and the save for this invocation, forcing a fresh installation and leaving the cache untouched. It overrides the image_cache directive and is meant for debugging the installer itself.

Stop the VM gracefully.
Stop the VM and delete its disk image.
Start the VM in the background.
[-f, --force]
Stop the VM. With -f, terminate it forcefully.
Show VM status as key: value lines, including ssh_port and console_port.
[command]
Open an interactive SSH session to the VM as root, or run command and exit with its exit code. Authentication uses the SSH agent.
Show how to connect to the VM serial console (via telnet(1)).
script [argument ...]
Run an expect(1) script against the VM console.
[--timeout=seconds]
Block until the VM accepts SSH connections (default timeout: 120 seconds).
download |
Manage cached installation images. list shows cached images; download [version] shows the cache path or download URL for version (default: 7.8). Images are downloaded via the built-in proxy when the VM boots.
list | [--stale]
Manage the installed-image cache described in IMAGE CACHE. list prints one line per cached entry (key, size, creation time, snapshot count), marking the entry the invoked VM's configuration currently derives. clear removes every entry; --stale keeps only that one entry and removes the rest. Both forms also sweep temporary trees left by an interrupted save.

The scope of --stale is one VM, the one named by --vm. The key inputs version and disk_size are per-VM, so pruning for one VM removes bases another VM would have hit.

clear refuses to remove an entry while a VM in this project is running on it, and warns when removal orphans the disk of a stopped one (rebuild it with destroy and up). VMs in other projects sharing the same cache_dir cannot be surveyed and are not covered by either check.

save | | name
 
list [--names]
Manage named snapshot layers over the cached base image, for caching guest states openhvf knows nothing about (typically the result of a provisioning script). See IMAGE CACHE.

save flattens the working disk into the cache under the base image it is built on; restore replaces the working disk with a fresh overlay on name and reseeds the VM state the snapshot records. The VM must be stopped for both: a live overlay is not consistent. save additionally needs an installed VM whose disk is built on a cached image; a standalone disk (from --no-cache or image_cache no) cannot be snapshotted. restore needs neither disk nor state and can run on a fresh checkout before the first up.

A missing or unresolvable snapshot exits 11 rather than 1, so a script can distinguish it from a real failure:

openhvf snapshot restore deps-$hash || provision_from_scratch

list and rm are scoped to the cache entry the current configuration derives. list --names writes bare snapshot names to standard output, one per line, for scripts; without it the listing goes to standard error like all other informational output.

check | |
Manage the VM disk image: verify integrity, repair errors (the VM must be stopped), or show image details.
Display usage information.

Installing OpenBSD under software emulation costs tens of minutes. openhvf therefore keeps the result: a pristine, compacted copy of the disk taken the moment the installer finished, which later runs use as the backing image of a throwaway overlay. A warm cache turns destroy followed by up into a cheap factory reset.

Entries live under cache_dir, beside the proxy cache holding the miniroot and install sets:

<cache_dir>/proxy/...                            miniroot, install sets
<cache_dir>/installed/<key>/base.qcow2           mode 0400, compacted
<cache_dir>/installed/<key>/meta.json            mode 0600
<cache_dir>/installed/<key>/snapshots/<n>.qcow2  mode 0400
<cache_dir>/installed/<key>/snapshots/<n>.json   mode 0600

key is version-arch-hash, where hash is a truncated SHA-256 over everything that shapes an installed disk: the OpenBSD version, the architecture, disk_size, the contents of the install.exp installer script, and the contents of share/openhvf/cache-generation. Memory and port settings do not shape the disk and are excluded, so changing them still hits the same entry.

share/openhvf/cache-generation is a generation counter. Bump the number in it when the way the installer is driven changes outside install.exp, so existing cached images are invalidated. Keep it to the bare number: the whole file is hashed.

Entries are immutable and write-once. Each is built whole in a temporary directory and published by renaming that directory, so a reader never sees one installation's base image beside another installation's metadata.

A snapshot is an immutable named layer over an entry's base image, created by snapshot save from a stopped VM. Snapshots are flattened onto base.qcow2, so each is a direct child of the base whatever the working disk was built on:

base.qcow2  <-  <name>.qcow2  <-  disk.qcow2

No snapshot is therefore ever another's parent, and removing one cannot orphan another. Because snapshots live inside the entry, anything that invalidates a base (a new key, or cache clear) invalidates its snapshots with it.

.openhvfrc
Project configuration: cache_dir, state_dir, default_vm, image_cache.
.openhvf/vms/name.conf
Per-VM configuration: name, version, memory, disk_size, ssh_port, console_port.
.openhvf/state/
Runtime state (PID files, sockets, disk images).
~/.cache/openhvf/
Default cache directory: downloaded installation images and installed-image cache.
share/openhvf/cache-generation
Installed-image cache generation counter.

image_cache yes | no (default yes) switches the installed-image cache on or off. It is read from the project .openhvfrc or the global ~/.openhvfrc, project first, and may also appear inside a vm block. --no-cache on up overrides all of them.

openhvf exits with one of the following values:

0
Success.
1
General error.
2
Invalid arguments.
3
Configuration error, or not in an openhvf project.
4
VM not found.
5
VM is running.
6
VM is not running.
7
Timeout waiting for the VM.
8
SSH connection failed.
9
Expect script failed.
10
Image download failed.
11
Snapshot not found.

The ssh command with a command argument exits with the remote command's exit code.

Initialize a project and boot the default VM:

$ openhvf init
$ openhvf up
$ openhvf wait --timeout=300

Run a command in the VM:

$ openhvf ssh "uname -a"
OpenBSD openbsd-default.my.domain 7.8 GENERIC arm64

Operate on a VM other than the default:

$ openhvf --vm minimal up

expect(1), ssh(1), telnet(1)

The openhvf utility first appeared in 2025, written to run the openhapd(8) test suite against a real OpenBSD guest, and generalised from there.

Dick Olsson <hi@senzilla.io>

openhvf generates a random root password for each installation and stores it in the VM state directory, and, when the installed image is cached, in that entry's meta.json (mode 0600). Caching lengthens the secret's life: it survives destroy and rotates only when the base key does.

This is not a localhost-only secret. The guest permits password root login, and QEMU is given ssh_port and console_port with no bind address, so the forwarded SSH port and the telnet serial console listen on every host interface. Treat a running VM, and a populated cache_dir, as reachable by anyone who can reach the host.

July 27, 2026 OpenBSD