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_pubkeyandroot_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_pubkeyand noroot_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>