| App::FuguVM::Guest(3p) | Perl Library Manual | App::FuguVM::Guest(3p) |
NAME
App::FuguVM::Guest - the lifecycle of one OpenBSD virtual machine
SYNOPSIS
use App::FuguVM::Guest;
my $vm = App::FuguVM::Guest->new(
config => $config->load_vm('default'),
state => $state,
log => $log,
);
exit $vm->up;
DESCRIPTION
The module runs QEMU for an OpenBSD guest, installs the system on the first run, and stops it again without corrupting its disk.
Everything that is not QEMU comes from Fugu. Processes and liveness come from Fugu::Process, which reaps: a QEMU that became a zombie reads as stopped, and a caller that is about to start a second one needs that answer. Bounded waits come from Fugu::Timeout. The random root password comes from Fugu::Random. The guest connection comes from Fugu::SSH.
METHODS
new
App::FuguVM::Guest->new(config => \%vm, state => $state, log => $log, ...)
Build a controller. "emulate" forces TCG instead of hardware acceleration; "no_cache" ignores the installed-image cache.
up
Make sure the VM runs, and do whatever that needs: verify the backing chain, check the disk after an unclean shutdown, restore from the installed-image cache, start the proxy, download the miniroot, create the disk, start QEMU, install OpenBSD, and install the SSH key.
The method is idempotent. A VM that already runs, and whose SSH key is current, returns success and changes nothing.
INSTALL MODES
"install_mode" of the configuration selects the origin of the disk. App::FuguVM::Config derives the value from the directives.
In the "expect" mode, "up" installs with install.exp over the serial console. A failed install script gives exit code 9.
In the "autoinstall" mode, "up" starts the mirror proxy, starts the App::FuguVM::Autoinstall responder, and starts QEMU with the miniroot attached and with "-no-reboot". autoinstall.exp then answers the install prompt with "a" and types the response-file URL. The guest fetches the file through the QEMU gateway, applies every answer, and reboots itself; "-no-reboot" makes QEMU exit instead, so the guest cannot install a second time, and the exit leaves a consistent disk. "up" publishes the disk as a cache entry, exactly as after an expect install, and it stops the responder on every path out of the install. "down" and "destroy" also stop it. A failed autoinstall script gives exit code 9.
In the "import" mode, "up" installs nothing. On the first run it publishes the "base_disk" file as the cache entry of the derived key, with "qemu-img convert", and then overlays the working disk on the published entry. A later "up" reads the file no more. The tool never writes to the source file. Every guest of the project derives the same key, and the population lock of the cache serializes the publication, so a parallel fleet publishes one time. Every cache verb and snapshot verb works on the imported entry. A cache miss with an absent source gives exit code 1, and the message names the path and the key.
THE VERIFICATION OF AN INSTALL
Before the first boot, "up" builds an App::FuguVM::Mirror over the version, the architecture, the "verify" switch and the "signify_dir" directive of the configuration, and App::FuguVM::Miniroot downloads the install media through it. The mirror proves the "SHA256" manifest of the release under the release key, and it verifies the miniroot before the image enters the cache. The host must verify the miniroot, because the miniroot is the boot medium and no code in the guest inspects it before it runs. A verification failure stops the run with exit code 1, leaves no file in the cache, and the diagnostic holds the mirror reason. When signify(1) is absent on the host, the reason names the command and the packages.
The guest verifies its own sets: the installer of a numbered release carries the release key in bsd.rd, and the proxy serves SHA256.sig beside each set. "up" passes the "verify" switch to install.exp, and with "verify yes" the script stops on the "Continue without verification?" prompt instead of answering it, with exit code 9. With "verify no" the tool downloads without a proof, logs one warning, and the script waives the installer check with a warning of its own.
"up" also exports the "distfile_cache" byte count as "FUGUVM_DISTFILE_LIMIT" before it starts the proxy, so the proxy child holds the distfile tree under the cap.
CREDENTIALS
The tool generates a root password only in the "expect" mode. In the "autoinstall" mode the response file owns every credential, and in the "import" mode the publisher of the image owns them. Outside the "expect" mode, "up" therefore behaves by configuration:
- With "ssh_pubkey" and "root_password_file": read the password from the first line of the file, store it in the state, and install the key over a password login, exactly as after an expect install. The tool warns when the file mode lets the group or another user read the file.
- With no "ssh_pubkey": wait for SSH with the key of the operator. The image must trust that key already.
- With "ssh_pubkey" and no "root_password_file": the configuration loader refuses with exit code 3.
A published image and a consuming host share one credential: the response file of the build sets the root password, the stack keeps the password in a file, and each consuming host names that file with "root_password_file". The metadata of an imported cache entry holds no root password, because the tool must not invent a credential for an image that it did not install.
down, destroy, start, stop, status
"down" stops the proxy and then the VM, gracefully, and records a clean shutdown. "destroy" stops it and deletes the disk. "start" and "stop" are the narrow forms that do not touch the proxy. "status" reports the state, with "proxy_url" from "App::FuguVM::Proxy->guest_url": the proxy URL as the guest reaches it, or "undef" while the proxy does not run. The port changes between runs, so a consumer reads the value from "status" and must not write it in a file.
wait_ssh, is_running
"wait_ssh" polls until the guest takes an authenticated connection; with a password argument it authenticates with that password instead of the SSH agent.
accel, bind_address, connect_address, ssh_port, console_port
The facts of the guest that "status" reports and that the guest connections use.
"accel" returns "kvm", "hvf" or "tcg". While the guest runs, the answer is the recorded value: a guest that started under "--emulate" runs TCG whatever the host can do now. In every other case the answer is the accelerator that the tool selects now.
"bind_address" returns the configured host address of the forwarded ports. "connect_address" returns the address that the tool connects to: the bind address, or 127.0.0.1 when the bind address is 0.0.0.0, because 0.0.0.0 is not a destination.
"ssh_port" and "console_port" return the recorded port while the guest runs, and fall back to the configured number. They return "undef" for "auto" in every other case: the record describes one run, so a stopped guest has no port. Thus a record that a crash left behind cannot read as a live port.
ADDRESSES AND PORTS
QEMU binds the forwarded SSH port and the serial console to "bind_address". The default is 127.0.0.1, so no other machine reaches a guest. "bind_address 0.0.0.0" restores the wider reach. Every connection that the tool makes to its own guest uses "connect_address".
"up" and "start" resolve both ports one time for each invocation, before the first spawn, and record them with the accelerator in App::FuguVM::State. A directive of "auto" takes the first free port of a fixed range: 100 ports from the default port of the directive. The probe binds a listening socket on the bind address and the candidate port. It skips every port that the record of a sibling guest of the project holds. It also skips every fixed port that a VM declaration of the project holds, stopped or not. An exhausted range stops the run with exit code 1.
An exclusive flock(2) on ports.lock in the cache directory covers the probe and the record of both ports, with a deadline of 30 seconds. Every project that shares the cache directory shares the lock, so the probes serialize while every holder is inside its deadline. The record exclusion covers the guests of one project only. One window stays open: a foreign process, a guest of an other project included, can take a probed port before QEMU binds it. QEMU then fails to start, and the tool reports that failure with the QEMU log. On the deadline the tool logs a warning and probes without the lock. The stop verbs clear the record, so a later "start" resolves again. "stop" on a guest that does not run also clears the record that a crash left behind.
VERSION GATE
The optional "qemu_version" directive pins the version of the QEMU system binary. "up" and "start" enforce it one time for each invocation, before the first spawn. The tool runs the binary that the architecture selects with "--version", under a deadline of 10 seconds, and parses the first dotted-decimal token of the first output line.
The match runs component by component, over the components that the directive names: 9.0 accepts 9.0.4 and refuses 9.1.0, and 9.0.4 accepts 9.0.4 only. A mismatch gives exit code 3, with a message that names both versions. A pinned version that the tool cannot verify fails closed with the same code: an absent binary, and output with no version in it, both refuse the start. With no directive the tool checks nothing, and it does not run the binary.
ARCHITECTURE
The "arch" value of the configuration selects the QEMU binary, the machine type, the firmware, and the TCG CPU model, through App::FuguVM::Arch. "up" and "start" exit with code 3 when the QEMU binary of the architecture is not on "PATH".
An arm64 guest boots its firmware code file with "-bios". An amd64 guest boots through two pflash devices: the code file read-only, and a fresh copy of its variable-store template in the state directory. The copy is throwaway state, and every start makes it again. A guest does not start without a firmware file, and the message names the architecture.
The accelerator follows the host. KVM and HVF need a host machine that runs the instruction set of the guest. KVM serves Linux with a writable /dev/kvm, and HVF serves Darwin. Every other case, an OpenBSD host included, uses TCG software emulation. "host" CPU passthrough pairs with hardware acceleration, and the TCG model of the architecture pairs with TCG.
A disk belongs to one architecture for its whole life. "up" and "start" stop with an error when the state records an other architecture for the existing disk, and the message names "fuguvm destroy".
SHUTDOWN
A shutdown syncs the guest filesystems first, then asks the guest to power off through ACPI, and only then forces the process to stop. Every step is bounded: a guest that stops answering must not hold the caller.
The order matters. A force stop of a guest with unwritten buffers leaves a filesystem that the next boot has to repair.
SEE ALSO
Fugu::Process, Fugu::SSH, Fugu::Timeout, App::FuguVM::Arch, App::FuguVM::Disk, App::FuguVM::DiskCache, App::FuguVM::QMP, App::FuguVM::State, fuguvm(1)
AUTHOR
Dick Olsson <hi@senzilla.io>
| 2026-10-16 | OpenBSD |