App-FuguVM
view release on metacpan or search on metacpan
lib/App/FuguVM/DiskCache.pod view on Meta::CPAN
=head1 NAME
App::FuguVM::DiskCache - cache of installed OpenBSD disks
=head1 SYNOPSIS
use App::FuguVM::DiskCache;
my $cache = App::FuguVM::DiskCache->new('~/.cache/fuguvm');
my $key = $cache->key($vm_config);
# Boot from a previous installation
if (my $hit = $cache->lookup($key)) {
$disk->create($name, undef, $hit->{base});
$state->set_root_password($hit->{meta}{root_password});
}
# Publish a freshly installed disk
$cache->store($key, $disk_path, { root_password => $password });
for my $entry (@{ $cache->list }) {
printf "%s %d bytes\n", $entry->{key}, $entry->{size};
}
=head1 DESCRIPTION
An installation of OpenBSD under TCG emulation takes tens of minutes.
This module keeps the result, thus later runs do not do the
installation again. The result is a clean, compacted copy of the
disk. The module makes this copy when the installation is complete.
Later VMs use this copy as the backing image of a throwaway qcow2
overlay.
The entries are in the configured C<cache_dir>, adjacent to the proxy
cache that holds the miniroot and the install sets:
<cache_dir>/installed/<key>/base.qcow2 mode 0400
<cache_dir>/installed/<key>/meta.json mode 0600
<cache_dir>/installed/<key>/snapshots/<name>.qcow2 mode 0400
<cache_dir>/installed/<key>/snapshots/<name>.json mode 0600
The entries are immutable and write-once. C<store> builds a full entry
in a sibling C<.tmp.*> directory, and then it publishes the entry with
a rename of that directory. Thus a reader never sees the base image of
one installation adjacent to the metadata of a different installation.
Such a mismatch looks live, but it then stops every later boot,
because the root password does not open the image.
=head2 Cache keys
C<key> returns C<< <version>-<arch>-<hash8> >>. C<hash8> is a
truncated SHA-256 over each input that shapes an installed disk. These
inputs are:
=over 4
=item * the OpenBSD version
=item * the architecture
=item * the disk size
=item * the contents of the F<install.exp> script that
L<App::FuguVM::Console> resolves
=item * the contents of F<share/fuguvm/cache-generation>
=back
The last file is a generation counter. Increase the number in this
file when the install driver changes in a way that the F<install.exp>
hash cannot see. One example is a change in how L<App::FuguVM::Guest> or
L<App::FuguVM::Console> drives the installer.
The counter is a data file, not a constant in this module. Thus a
continuous-integration cache key can hash the file too. Keep the file
content to the bare number: the hash covers the whole content, and an
edit to a comment changes the key.
The memory settings and the port settings do not shape the disk, and
the key intentionally excludes them. Thus a change to these settings
hits the same entry.
=head2 Snapshots
A snapshot is an immutable, named layer over the base image of an
entry. Its purpose is to cache states that fuguvm itself does not
know. One example is a provisioning script that caches its "guest
packages installed" state. The mechanism is in this module. The
policy stays in the callers: they select the states that get a cache.
The snapshots are inside C<< installed/<key>/ >>. Thus, when a base
becomes invalid - through a new key, or through C<cache clear> - the
snapshots of that base also become invalid automatically.
C<snapshot_store> B<flattens> the working disk onto C<base.qcow2>. It
does not copy the disk. Thus each snapshot is a direct child of the
base:
base.qcow2 <- <name>.qcow2 <- disk.qcow2
A copy keeps the backing-file header of the working disk verbatim.
That header is correct only while the disk hangs directly off the
base. After a restore, the disk hangs off a snapshot. Thus a copy
stacks chains without a limit, or it publishes a qcow2 that names
itself as its own backing file. The second result occurs when a caller
saves the same name again. A normal second run does this.
Because C<snapshot_store> flattens the disk, no snapshot is the parent
of a different snapshot. Thus C<snapshot_remove> cannot make a
snapshot an orphan.
The working disk must come from a stopped VM. A live overlay is not
consistent.
=head1 METHODS
=over 4
=item new($cache_dir)
The constructor creates a cache over C<$cache_dir>. It expands a
C<~> at the start of the path.
=item installed_dir
=item entry_dir($key)
=item base_path($key)
These methods return locations. The paths do not have to exist.
=item key($vm_config)
The method derives the cache key for a VM configuration hash. It
returns C<undef> when it cannot read an input. The caller then has no
key, and thus no cache.
=item lookup($key)
The method returns C<< { key, dir, base, meta } >> for a complete
entry. In other cases, it returns C<undef>. A half-written entry is a
miss, not an error.
=item store($key, $disk_path, $meta)
The method compacts C<$disk_path> into the cache as the base image
for C<$key>, and writes C<$meta> adjacent to it. Put the guest
C<root_password> in C<$meta>. The method returns the path of the base
image. It returns C<undef> on a failure, and a try to overwrite a
populated key is one such failure. The caller then degrades to a
standalone disk.
=item list
The method returns each complete entry, newest first, as
C<< { key, dir, base, meta, size, created_at, snapshots } >>.
=item remove($key)
The method deletes an entry and all data under it. It returns true
when the entry is gone.
=item sweep_temp
The method removes the C<.tmp.*> trees that an interrupted C<store>
left. This includes the trees of earlier processes. The method
returns the count of removed trees.
=item key_for_path($path)
The method returns the key of the entry that holds C<$path>. The
path points to a base image or a snapshot. The method returns
C<undef> when C<$path> is outside the cache. The method answers this
question: "Which cached image is the base of this working disk?"
=item snapshot_dir($key)
=item snapshot_path($key, $name)
These methods return locations. The paths do not have to exist.
=item valid_snapshot_name($name)
The method returns true when C<$name> is usable. A name becomes a
file name inside the cache. Thus a name must start with an
alphanumeric character, it must hold only word characters, dots, and
dashes, and it must have a bounded length.
=item snapshot_store($key, $name, $disk_path, $meta)
The method flattens the stopped working disk at C<$disk_path> onto
the base image of C<$key>, and publishes the result as the named
layer. Put the state fields that the disk holds in C<$meta>:
C<installed> and the installed SSH public key. With these fields, a
restore can reseed L<App::FuguVM::State>. The method copies the root
password from the metadata of the base itself, and it does not trust
the caller for this value. The method returns the snapshot path, or
C<undef> on a failure.
When a caller saves the same name again, the method replaces the
snapshot. This is the normal second run of a provisioning script.
=item snapshot_lookup($key, $name)
The method returns C<< { key, name, path, base, meta } >> for a
snapshot whose image, metadata, and backing chain all resolve. In
other cases, it returns C<undef>. A snapshot with a removed base is a
miss, not an error. Thus a caller can provision from scratch, and it
does not fail hard.
=item snapshot_list($key)
The method returns the sorted snapshots of an entry, as
C<< { name, path, size, created_at, meta } >>.
=item snapshot_remove($key, $name)
The method deletes a snapshot and its metadata. Deletion in any order
is safe, because each snapshot is a direct child of the base, never
of a different snapshot.
=back
=head1 SECURITY
F<meta.json> is mode 0600, and it holds the generated guest root
password, the same secret that the VM state directory keeps. The cache
makes the life of this secret longer: the password stays after
C<fuguvm destroy>, and it changes only when the base key changes.
The guest permits root login with a password. QEMU forwards the SSH
port and the serial console port of the guest on each host interface.
Thus the password is not a localhost-only secret. See L<fuguvm(1)>.
=head1 SEE ALSO
L<App::FuguVM::Disk>, L<App::FuguVM::Console>, L<App::FuguVM::Miniroot>, L<App::FuguVM::Guest>,
L<fuguvm(1)>
=cut
( run in 1.726 second using v1.01-cache-2.11-cpan-4ef0a570458 )