vmdb2 builds disk images with Debian installed. The images can be used for virtual machines, or can be written to USB flash memory devices, and hardware computers can be booted off them.
This manual is published as HTML and as PDF.
You can get vmdb2 by getting the source code from git, either author’s server or gitlab.com.
You can then run it from the source tree:
$ sudo /path/to/vmdb2/vmdb2 ...
In Debian 10 (“buster”) and its derivatives, you can also install the vmdb2 package:
$ apt install vmdb2
For any other systems, we have no instructions. If you figure it out, please tell us how.
vmdb2 is a successor of the vmdebootstrap program, written by the same author, to fix a number of architectural problems and limitations with the old program. The new program is not compatible with the old one; that would’ve required keeping the problems, as well.
vmdebootstrap
was the first attempt by it author to
write a tool to build system images. It turned out to not be well
designed. Specifically, it was not easily extensible to be as flexible
as a tool of this sort should be.
The author likes to write tools for himself and had some free time. He sometimes prefers to write his own tools rather than spend time and energy evaluating and improving existing tools. He admits this is a character flaw.
Also, he felt ashamed of how messy vmdebootstrap
turned
out to be.
If nobody else likes vmdb2
, that just means the author
had some fun on his own.
You need to make a specification file (in YAML) that tells vmdb2 what kind of image to build, and how. An example:
steps:
- mkimg: "{{ output }}"
size: 4G
- mklabel: msdos
device: "{{ output }}"
- mkpart: primary
device: "{{ output }}"
start: 0%
end: 100%
tag: /
- kpartx: "{{ output }}"
- mkfs: ext4
partition: /
- mount: /
- debootstrap: buster
mirror: http://deb.debian.org/debian
target: /
- apt: install
packages:
- linux-image-amd64
tag: /
- fstab: /
- grub: bios
tag: /
The source repository of vmdb2 has more examples, which are also automatically tested, unlike the above one.
The list of steps builds the kind of image that the user wants. The specification file can easily be shared, and put under version control.
Every action in a step is provided by a plugin to vmdb2. Each action
is a well-defined task, which may be parameterised by some of the
key/value pairs in the step. For example, mkimg
would
create a raw disk image file. The image is 4 gigabytes in size.
mkpart
creates a partition, and mkfs
an ext4
filesystem in the partition. And so on.
Steps may need to clean up after themselves. For example, a step that mounts a filesystem will need to unmount it at the end of the image creation. Also, if a later step fails, then the unmount needs to happen as well. This is called a “teardown”.
By providing well-defined steps that the user may combine as they wish, vmdb2 gives great flexibility without much complexity, but at the cost of forcing the user to write a longer specification file than a simple command line invocation.
To use this, save the specification into test.vmdb
, and
run the following command:
$ sudo vmdb2 test.vmdb --output test.img --verbose
Alternatively the specification can be passed in via stdin by setting
the file name to -
, like so:
$ cat test.vmdb | sudo vmdb2 - --output test.img --verbose
This will take a long time, mostly at the debootstrap
step. See below for speeding that up by caching the result.
Due to the kinds of things vmdb2 does (such as mounting, creating device nodes, etc), it needs to be run using root privileges. For the same reason, it probably can’t be run in an unprivileged container.
Running vmdb2 in a container, privileged or not, is not supported. If it works, that is a happy accident. There are no tests to make sure that works, however, and no patches to make vmdb2 work in a container will be considered for merging. This is because vmdb2 is maintained by Lars, who has no use for containers, and does not want the maintenance burden of having code that supports that.
vmdb2 uses debootstrap, which copies the host’s /etc/hostname file into the image. You probably want to set the hostname for the image you’re creating. You can do this by overwriting the /etc/hostname file in the image, for example with the following step:
- chroot: rootfs
shell: |
echo myhostname > /etc/hostname
At this time, vmdb2 does not support building partitioned images without partition, or images without a partition table. Such support may be added later. If this would be useful, do tell the authors.
Instead of device filenames, which vary from run to run, vmdb2 steps refer to block devices inside the image, and their mount points, by symbolic names called tags. Tags are any names that the user likes, and vmdb2 does not assign meaning to them. They’re just strings.
To refer to the filename specified with the --output
or
--image
command line options, you can use Jinja2 templating. The variables
output
and image
can be used.
- mkimg: "{{ output }}"
- mklabel: "{{ image }}"
The difference is that --output
creates a new file, or
truncates an existing file, whereas --images
requires the
file to already exist. The former is better for image file, the latter
for real block devices.
Building an image can take several minutes, and that’s with fast access to a Debian mirror and an SSD. The slowest part is typically running debootstrap, and that always results in the same output, for a given Debian release. This means its easy to cache.
vmdb2 has the two actions cache-roots
and
unpack-rootfs
and the command line option
--rootfs-tarball
to allow user to cache. The user uses the
option to name a file. cache-rootfs
takes the root
filesystem and stores it into the file as a compress tar archive
(“tarball”). unpack-rootfs
unpacks the tarball. This allows
vmdb2 to skip running debootstrap needlessly.
The specify which steps should be skipped, the unless
field can be used: unpack-rootfs
sets the
rootfs_unpacked
flag if it actually unpacks a tarball, and
unless
allows checking for that flag. If the tarball
doesn’t exist, the flag is not set.
- unpack-rootfs: root
- debootstrap: buster
target: root
unless: rootfs_unpacked
- cache-rootfs: root
unless: rootfs_unpacked
If the tarball exists, it is unpacked, and the
debootstrap
and cache-rootfs
steps are
skipped. If the tarball doesn’t exist, the unpack step is silently
skipped, and the debootstrap and caching steps are performed
instead.
It’s possible to have any number of steps between the unpack and the cache steps. However, note that if you change anything within those steps, or time passes and you want to include the new packages that have made it into Debian, you need to delete the tarball so it is run again.
This chapter documents the user-level acceptance criteria for vmdb2, and how they are to be verified. It’s meant to be processed with the Subplot tool, but understood by all users of and contributors to the vmdb2 software. The criteria and their verification are expressed as scenarios.
For reasons of speed, security, and reliability, these scenarios test only the core functionality of vmdb2. All the useful steps for actually building images are left out.. Those are tested by actually building images. However, those useful steps are not useful, if the core that invokes them is rotten.
The first case we look at is one for the happy path: a specification with two echo steps, and nothing else. It’s very simple, and nothing goes wrong when executing it. In addition to the actual thing to do, each step also defines a “teardown” thing to do. We check that all the steps and teardown steps are performed, in the right order.
Note that the “echo” step is provided by vmdb2 explicitly for this kind of testing, and that the teardown field in the step is implemented by the echo step. It’s not a generic feature.
given an installed vmdb2
given file happy.vmdb
when I run vmdb2 -v happy.vmdb --output=happy.img
then exit code is 0
then stdout contains "foo\nbar\nbar_teardown\n"
Requirement: We can ask vmdb2 for its version.
given an installed vmdb2
when I run vmdb2 --version
then exit code is 0
then stdout matches regex ^\d+\.\d+$
vmdb2 allows values in specification files to be processed by the Jinja2 templating engine. This allows users to do thing such as write specifications that use configuration values to determine what happens. For our simple echo/error steps, we will write a rule that outputs the image file name given by the user. A more realistic specification file would instead do thing like create the file.
given an installed vmdb2
given file j2.vmdb
when I run vmdb2 -v j2.vmdb --output=foo.img
then exit code is 0
then stdout contains "image is foo.img\nbar"
Sometimes things do not quite go as they should. Does vmdb2 do things in the right order then? This scenario uses the “error” step provided for testing this kind of thing.
given an installed vmdb2
given file unhappy.vmdb
when I try to run vmdb2 -v unhappy.vmdb --output=unhappy.img
then exit code is 1
then stdout contains "foo\nyikes\n"
then stdout contains "WAT?!\n"
then stdout contains "foo_teardown\n"
then stdout doesn't contain "bar_step"
then stdout contains "bar_teardown"
steps:
- echo: foo
teardown: foo_teardown
- error: yikes
teardown: "WAT?!"
- echo: bar
teardown: bar_teardown
Run Ansible using a provided playbook, to configure the image. vmdb2
sets up Ansible so that it treats the image as the host being configured
(via the chroot
connection). The image MUST have Python
installed (version 2 or 3 depending on Ansible version).
Step keys:
ansible
— REQUIRED; value is the tag of the root
filesystem.
config_file
— OPTIONAL; value is the filename of an
Ansible configuration file, relative to the .vmdb file.
group
— OPTIONAL; the name of the Ansible inventory
group. Defaults to “image”
playbook
— REQUIRED; value is the filename of the
Ansible playbook, relative to the .vmdb file.
tags
— OPTIONAL; a comma-separated list of Ansible
tags to execute
extra_vars
— OPTIONAL; a dictionary defining
variables to pass to the Ansible playbook.
Example (in the .vmdb file):
- apt: install
tag: root
packages: [python]
- ansible: root
playbook: foo.yml
tags: bar
config_file: ansible.cfg
group: AwesomeGroup
extra_vars:
iface:
name: eth0
Example (foo.yml
):
- hosts: image
tasks:
- name: "set /etc/hostname"
shell: |
echo "{{ hostname }}" > /etc/hostname
- name: "unset root password"
shell: |
sed -i '/^root:[^:]*:/s//root::/' /etc/passwd
- name: "configure networking"
copy:
content: |
auto {{ iface.name }}
iface {{ iface.name }} inet dhcp
iface {{ iface.name }} inet6 auto
dest: /etc/network/interfaces.d/wired
vars:
hostname: discworld
Install packages using apt, which needs to already have been installed.
Step keys:
apt
— REQUIRED; value MUST be
install
.
tag
— REQUIRED; value is the tag for the root
filesystem.
packages
— REQUIRED; value is a list of packages to
install.
recommends
— OPTIONAL; defaults to true. Setting
value to a false (i.e. 0
, null
,
false
) asks apt-get to run with the
--no-install-recommends
option set.
Example (in the .vmdb file):
- apt: install
tag: root
packages:
- python
- linux-image-amd64
Create a tarball of the root filesystem in the image.
Step keys:
cache-rootfs
— REQUIRED; tag of root filesystem on
image.force
— OPTIONAL; boolean that enables overwriting of
an existing rootfs tarball by vmdb2
, allowing the tarball
to be used as both a build input and a build output. This can be useful
in multi-stage build chains where a “common base OS” rootfs filesystem
is populated and packaged as a rootfs tarball by vmdb2
, and
this tarball is consumed by multiple downstream vmdb2
builds that extract the tarball into disk images that have different
partition layouts and/or filesystems.Example (in the .vmdb file):
# typical use
- cache-rootfs: root
unless: rootfs_unpacked
# create a rootfs tarball output at the end of a build process
- cache-rootfs: root
force: true
Run a shell snippet in a chroot inside the image.
Step keys:
chroot
— REQUIRED; value is the tag for the root
filesystem.
shell
— REQUIRED; the shell snippet to run
Example (in the .vmdb file):
- chroot: root
shell: |
echo I am in chroot
Recursively copy a directory from outside into the target filesystem.
Step keys:
copy-dir
— REQUIRED; the full (starting from the new
filesystem root) path of directory to copy to. Any missing directories
will be created with the configured user and group ownership and
permissions. See the perm
, uid
,
gid
, user
and group
keys.src
— REQUIRED; the path of the directory to copy from
on the host filesystem, outside the chroot, relative to the current
working directory of the vmdb2 process.perm
— OPTIONAL; the permissions to apply to any
missing parent directories that are created on the target. The value of
umask
is applied to this value.umask
— OPTIONAL; the numeric (octal) representation of
umask to apply to the permissions of copied files and directories.
Defaults to 0022.uid
— OPTIONAL; the numeric user ID to assign to the
copied files and directories. Defaults to 0 (root).gid
— OPTIONAL; the numeric group ID to assign to the
copied files and directories. Defaults to 0 (root).Copy a file from outside into the target filesystem.
Step keys:
copy-file
— REQUIRED; the full (starting from the new
filesystem root) path name of the file to create. Any missing
directories will be created (owner root, group root, mode 0511).src
— REQUIRED; filename on the host filesystem,
outside the chroot, relative to the current working directory of the
vmdb2 process.perm
— OPTIONAL; the numeric (octal) representation of
the file’s permissions. Defaults to 0644.uid
— OPTIONAL; the numeric user ID of the file’s
owner. Defaults to 0 (root).gid
— OPTIONAL; the numeric user ID of the file’s
group. Defaults to 0 (root).Create a directory in the target filesystem
Step keys:
create-dir
— REQUIRED; the full (starting from the new
filesystem root) path name of the directory to create. It will work as a
mkdir -p
— Any intermediate directories that do not yet
exist will be created.perm
— OPTIONAL; the numeric (octal) representation of
the directory’s permissions. Defaults to 0755.uid
— OPTIONAL; the numeric user ID of the directory’s
owner. Defaults to 0 (root).gid
— OPTIONAL; the numeric user ID of the directory’s
group. Defaults to 0 (root).Create an empty file in the target filesystem.
Step keys:
create-file
— REQUIRED; the full (starting from the new
filesystem root) path name of the file to create. It will not
create any directories; if they need to be created, please use
create-dir
first.contents
— REQUIRED; the contents to be written to the
generated file.perm
— OPTIONAL; the numeric (octal) representation of
the file’s permissions. Defaults to 0644.uid
— OPTIONAL; the numeric user ID of the file’s
owner. Defaults to 0 (root).gid
— OPTIONAL; the numeric user ID of the file’s
group. Defaults to 0 (root).Set up disk encryption using LUKS with the cryptsetup
utility. The encryption passphrase is read from a file or from the
output of a command. The encrypted disk gets opened and can be mounted
using a separate tag for the cleartext view.
Step keys:
cryptsetup
— REQUIRED; the tag for the encrypted
block device. This is not directly useable by users, or
mountable.
name
— REQUIRED; the tag for the de-crypted block
device. This is what gets mounted and visible to users.
password
— OPTIONAL; the encryption
password
key-file
— OPTIONAL; file from where passphrase is
read.
key-cmd
— OPTIONAL; command to run, passphrase is
the first line of its standard output.
One of password
, key-file
, or
key-cmd
is REQUIRED.
Example (in the .vmdb file):
- cryptsetup: cleartext_pv0
password: hunter2
name: pv0
Create a directory tree with a basic Debian installation. This does not include a boot loader.
Step keys:
debootstrap
— REQUIRED; value is the codename of the
Debian release to install: stretch
, buster
,
etc.
target
— REQUIRED; value is the tag for the root
filesystem.
mirror
— REQUIRED; which Debian mirror to
use
keyring
— OPTIONAL; which gpg keyring to use to
verify the packages. This is useful when using a non-official Debian
repository (e.g. Raspbian) as by default debootstrap will use the keys
provided by the “debian-archive-keyring” package.
install_keyring
— OPTIONAL; if set to
yes
, the gpg keyring specified by the keyring
key will be installed in the image for use when installing packages from
non-official Debian repositories.
arch
— OPTIONAL; the foreign architecture to
use.
variant
— OPTIONAL; the variant for
debootstrap.
include
— OPTIONAL; a list of additional packages
for debootstrap to install.
tls_ca_certs
— OPTIONAL; a list of paths to TLS
Certificate Authority (CA) cert files to install in the image after the
debootstrap process has completed. This allows the use of package
repositories with HTTPS transports that use TLS certificates issued by
private CAs. Note that the CA cert files being installed must have a
.crt
suffix in order to be used.
Example (in the .vmdb file):
- debootstrap: buster
target: root
mirror: http://mirror.example.com/debian
keyring: /etc/apt/trusted.gpg
Create /etc/fstab
inside the the image.
Step keys:
fstab
— REQUIRED; value is the tag for the root
filesystem.Example (in the .vmdb file):
- fstab: root
Install the GRUB bootloader to the image. Works on a PC for traditional BIOS booting, PC and ARM machines for modern UEFI booting, and PowerPC machines for IEEE1275 booting. Supports Secure Boot for amd64 UEFI.
Warning: This is the least robust part of vmdb2.
Step keys:
grub
— REQUIRED; value MUST be one of
uefi
and bios
, for a UEFI or a BIOS boot,
respectively. Only PC systems support the bios
option.
tag
— REQUIRED; value is the tag for the root
filesystem.
efi
— REQUIRED for UEFI; value is the tag for the
EFI partition.
prep
— REQUIRED for IEEE1275; value is the tag for
the PReP partition.
console
— OPTIONAL; set to serial
to
configure the image to use a serial console.
image-dev
— OPTIONAL; which device to install GRUB
onto; this is needed when installing to a real hard drive, instead of an
image.
quiet
— OPTIONAL; should the kernel be configured to
boot quietly? Default is no.
timeout
— OPTIONAL; set the grub menu timeout, in
seconds. Defaults to 0 seconds.
kernel-params
— OPTIONAL; list of parameters which
grub will pass to the Linux kernel. Default is:
["biosdevname=0", "net.ifnames=0", "consoleblank=0", "rw"]
Example (in the .vmdb file):
- grub: bios
tag: root
Same, but for UEFI, assuming that a FAT32 filesystem exists on the
partition with tag efi
:
- grub: uefi
tag: root
efi: efi
console: serial
Or for IEEE1275, assuming that a partition with tag prep
exists:
- grub: ieee1275
tag: root
prep: prep
console: serial
Install to a real hard disk (named with the --image
option):
- grub: uefi
tag: root
efi: efi
image-dev: "{{ image }}"
Create loop devices for partitions in an image file. Not needed when installing to a real block device, instead of an image file.
Step keys:
kpartx
— REQUIRED; filename of block device with
partitions.
tags
— OPTIONAL; list of tags to apply to partitions
when re-using an existing image that has already been populated with
formatted partitions. This can be useful in scenarios where an appliance
disk image is being built: There needs to be a “debug” version of the
appliance that provides direct SSH access, the use of sudo
,
etc., and also a “production” version that does not. Otherwise the disk
images need to be identical to aid in diagnosing issues found in
production environments. The production and debug images can be produced
by first creating the “release” version using vmdb2
, and
then passing the production disk image to a second vmdb
where developer access is configured.
Example (in the .vmdb file):
# typical use
- kpartx: "{{ output }}"
# using an image that already contains partitions containing filesystems
# that should be mounted as `/boot` and `/`
- kpartx: "{{ output }}"
tags:
- boot
- root
Create an LVM2 logical volume (LV) in an existing volume group.
Step keys:
lvcreate
— REQUIRED; value is the tag for the volume
group.
name
— REQUIRED; tag for the new LV block
device.
size
— REQUIRED; size of the new LV. The special
value fill
will make the volume use all of the available
space.
Example (in the .vmdb file):
- lvcreate: rootvg
name: rootfs
size: 1G
Scans for existing LVM2 logical volumes (LVs) within a named volume
group. This is useful when using an existing image as input which has
been pre-populated with LVM logical volumes, allowing the volumes to be
mounted. Please see the kpartx plugin documentation for further details
on passing an existing image to vmdb2
. This plugin adds LVM
support to that use-case.
Step keys:
lvscan
— REQUIRED; value is the name of the volume
group containing the logical volumes.
tags
— REQUIRED; list of tags to apply to the
discovered logical volumes. The tags must match names of volumes in the
volume group. Volumes in the image which do not need to be mounted can
be omitted from the tags list
Example (in the .vmdb file):
- lvscan: the_volume_group
tags:
- lv_one
- lv_two
Create a filesystem.
Step keys:
mkfs
— REQUIRED; filesystem type, such as
ext4
or vfat
.
partition
— REQUIRED; tag for the block device to
use.
options
— OPTIONAL; aditional options for
mkfs.
Example (in the .vmdb file):
- mkfs: ext4
partition: root
options: -O ^64bit,^metadata_csum
Create a new image file of a desired size.
Step keys:
mkimage
— REQUIRED; name of file to create.
size
— REQUIRED; size of the image.
Example (in the .vmdb file):
- mkimg: "{{ output }}"
size: 4G
Create a partition table on a block device.
Step keys:
mklabel
— REQUIRED; type of partition table, MUST be
one of msdos
and gpt
.
device
— REQUIRED; tag for the block
device.
Example (in the .vmdb file):
- mklabel: msdos
device: "{{ output }}"
Create a partition.
Step keys:
mkpart
— REQUIRED; type of partition to create: use
primary
(but any value acceped by parted
is
OK).
device
— REQUIRED; filename of block device where to
create partition.
start
— REQUIRED; where does the partition
start?
end
— REQUIRED; where does the partition
end?
tag
— REQUIRED; tag for the new partition.
Example (in the .vmdb file):
- mkpart: primary
device: "{{ output }}"
start: 0%
end: 100%
tag: root
Mount a filesystem.
Step keys:
mount
— REQUIRED; tag of filesystem to
mount.
dirname
— OPTIONAL; the mount point.
mount-on
— OPTIONAL; tag of already mounted
filesystem in image. (FIXME: this may be wrong?)
zerofree
— OPTIONAL; Boolean flag controlling
whether or not to run the zerofree
utility on the
filesystem after unmounting it at the end of the build process. Defaults
to true
Example (in the .vmdb file):
- mount: root
Configure the system on the image so that it automatically resizes itself to fill the actual disk, upon first boot. For this to work, the root file system MUST be the last partition on the image.
Also, the image MUST have the parted
package installed
for the partprobe
command.
Step keys:
resize-rootfs
— REQUIRED; value MUST be the tag for the
root filesystem.This is based on reading the changes by Peter Lawler to the image-specs repository to do the same thing.
Example:
- resize-rootfs: root
Set or clear a flag in a partition.
Step keys:
set_part_flag
— REQUIRED; filename of block device
containing partition that will have the flag set or cleared.
tag
— REQUIRED; tag of the partition being
modified.
flag
— REQUIRED; the name of the flag to be set or
cleared
state
— OPTIONAL; the flag state: “enabled” or
“disabled”. Defaults to “enabled”.
Example (in the .vmdb file):
- set_part_flag: "{{ output }}"
tag: rootfs
flag: bios_grub
state: enabled
Run a shell snippet on the host. This is not run in a chroot, and can access the host system.
Step keys:
root-fs
— REQUIRED; value is the tag for the root
filesystem.
shell
— REQUIRED; the shell snippet to run
Example (in the .vmdb file):
- root-fs: root
shell: |
echo I am in NOT in chroot.
Unpack a tarball of the root filesystem to the image, and set the
rootfs_unpacked
condition to true. If the tarball doesn’t
exist, do nothing and leave the rootfs_unpacked
condition
to false.
Step keys:
unpack-rootfs
— REQUIRED; tag for the root
filesystem.Example (in the .vmdb file):
- unpack-rootfs: root
Create an LVM2 volume group (VG), and also initialise the physical volumes for it.
Step keys:
vgcreate
— REQUIRED; value is the tag for the volume
group. This gets initialised with vgcreate
.
physical
— REQUIRED; list of tags for block devices
(partitions) to use as physical volumes. These get initialised with
pvcreate
.
Example (in the .vmdb file):
- vgcreate: rootvg
physical:
- my_partition
- other_partition
Mount the usual Linux virtual filesystems in the chroot:
/proc
/dev
/dev/pts
/dev/shm
/run
/run/lock
/sys
They will be automatically unmounted at the end.
Often, the virtual filesystems are unnecessary, but some Debian packages won’t install without them. The grub boot loader needs them as well, but mounts what it needs itself, if necessary.
Step keys:
virtual-filesystems
— REQUIRED; value is the tag of the
root filesystem.Example (in the .vmdb file):
- virtual-filesystems: rootfs