Building vagrant images with mkosi
Posted on za 27 juli 2024 in systemd
Last FOSDEM, there where some talks around mkosi using it for kernel hacking and systemd integration tests. These talks got me interested in mkosi, a systemd project for building OS images. After chatting some more with the maintainers, I considered the idea of moving the arch-boxes project to mkosi. (note: it has not been decided yet on whether to move the project to mkosi yet) Arch-boxes is an Arch Linux project which creates official vagrant and cloud images for Arch Linux, using bash scripts to create these images and requiring root privileges. This made me wonder, can I create Vagrant images for Arch Linux with mkosi.
In short mkosi does not require root privileges for building, has configuration file support with profiles and does most of the heavy lifting.
What is Vagrant?
Vagrant was a popular way to setup a development virtual machine for projects,
all you had to do is provide a Vagrantfile
and vagrant up
would setup a
development VM and for example mount your code into the VM via sshfs allowing
for a "reproducible" development environment.
The image you use in your Vagrantfile
is called a box
in Vagrant's
terminology and are hosted by the Vagrant
project.
Vagrant also supports the concept of providers, these are the backends on which the box would run, the default being VirtualBox, alternatives are Hyper-V, libvirt and more.
Format
Usually the Vagrant boxes are made using packer
, but other tools such as kiwi, osbuild (only libvirt provider) also support creating these boxes. The alternative tools usually only support a subset of the providers packer
supports. In the Linux community VirtualBox
and libvirt
are usually the most popular ones. At least this is what Arch Linux and Fedora supports.
The base box format is described in a very short summary what needs to be in the image: * Package manager * Enough disk space to do interesting things * A vagrant user with SSH pubkey and password-less sudo * SSH server enabled and running * Provider specific tools (for example VirtualBox guest tools)
The image format is provider dependant and is described for VirtualBox and libvirt.
mkosi
We start things off by creating a base mkosi.conf
, which defines the packages we require for both the libvirt and VirtualBox provider:
[Distribution]
Distribution=arch
[Content]
Packages=
systemd
udev
linux
base
grub
openssh
sudo
btrfs-progs
reflector
Hostname=archlinux
Locale=C.UTF-8
Timezone=UTC
Keymap=us
Bootable=yes
Bootloader=grub
BiosBootloader=grub
KernelCommandLine=net.ifnames=0
The image that mkosi creates runs systemd-firstboot
by default, to avoid the
first boot to prompt interactively for details we pre-configure the hostname, locale, etc.
By default only an UEFI bootable image is created, as BIOS boot is required we inform mkosi to set this up for us.
Partitioning
As described in the vagrant box format, a user should have enough disk space to
do interesting things, by default mkosi makes a small ext4 image. We want to
provide a 20G disk and use btrfs by default, if we want to change the
partitioning in mkosi we have to define everything in a directory called mkosi.repart
:
00-esp.conf
[Partition]
Type=esp
Format=vfat
CopyFiles=/boot:/
CopyFiles=/efi:/
SizeMinBytes=512M
SizeMaxBytes=512M
05-bios.conf
[Partition]
# UUID of the grub BIOS boot partition which grubs needs on GPT to
# embed itself into.
Type=21686148-6449-6e6f-744e-656564454649
SizeMinBytes=1M
SizeMaxBytes=1M
10-root.conf
[Partition]
Type=root
Format=btrfs
CopyFiles=/
SizeMinBytes=20G
SizeMaxBytes=20G
User creation / ssh
Vagrant images require a vagrant user with sudo (password-less) and a vagrant specific pubkey to be able to log in to the box. There are no user creation helpers, but there are various steps of the build process where you can execute your own scripts. For user creation we use the post package installation hook:
mkosi.postinst.chroot
useradd --create-home --user-group vagrant --password "$(openssl passwd -6 vagrant)"
install --directory --owner=vagrant --group=vagrant --mode=0700 $DESTDIR/home/vagrant/.ssh
install --owner=vagrant --group=vagrant --mode=600 authorized_keys ${DESTDIR}/home/vagrant/.ssh/authorized_keys
Creating a sudo config file for the vagrant user is as easy as dropping a file
into mkosi.extra
:
mkosi.extra/etc/sudoers.d/vagrant
Defaults:vagrant !requiretty
vagrant ALL=(ALL) NOPASSWD: ALL
Services
By default the vagrant box needs to run sshd
, on Arch Linux services are not
started automatically but this can be easily done with a systemd-preset file:
mkosi.extra/etc/systemd/system-preset/10-base-image.preset
enable sshd.service
Profiles
We have now full-filled all the steps required for a vagrant box except the
provider specific tooling, for this we will use mkosi profile support. For
every profile we create a directory in mkosi.profiles
.
VirtualBox
For the VirtualBox image the guest utils needs to be installed and a special
service running for more advanced (for example shared clipboard, folders). We
can append the base packages in the profile specific mkosi.conf
:
mkosi.conf
[Output]
ImageId=Arch-Linux-x86_64-virtualbox
[Content]
Packages=
virtualbox-guest-utils-nox
Enabling the vboxservice
is adding another systemd-preset file:
mkosi.extra/etc/systemd/system-preset/15-virtualbox.preset
enable vboxservice.service
Libvirt
Libvirt does not require any specific packages to be installed or services
enabled, we only configure the ImageId
to differ from VirtualBox:
mkosi.conf
[Output]
Format=disk
ImageId=Arch-Linux-x86_64
Image
By default mkosi outputs a raw image, Vagrant boxes require something different
depending on the provider. To allow a user to easily customize the build image
mkosi offers a mkosi.postoutput
script to be run.
Libvirt
The box format is described in the official provider docs. We need to provide a tarball containing:
- Vagrantfile
- box.img (as qcow2)
- metadata.json
mkosi.postoutput
#!/bin/bash
IMAGE_NAME="Arch-Linux-x86_64-libvirt-latest.box"
cp "${SRCDIR}/Vagrantfile.libvirt" "${OUTPUTDIR}/Vagrantfile"
qemu-img convert -f raw "${OUTPUTDIR}/${IMAGE_ID}.raw" -O qcow2 "${OUTPUTDIR}/box.img"
# Calculate the disk size for metadata.json, this size needs to be in Gb and without a fraction so round this up.
DISK_SIZE=$(qemu-img info --output=json "${OUTPUTDIR}/box.img" | jq '."virtual-size" / 1024 / 1024 / 1024 + 0.5 | round')
echo '{"format":"qcow2","provider":"libvirt","virtual_size":'"${DISK_SIZE}"'}' > "${OUTPUTDIR}/metadata.json"
tar -C "${OUTPUTDIR}" -czf "${OUTPUTDIR}/${IMAGE_NAME}" Vagrantfile metadata.json box.img
In this script the OUTPUTDIR
is the staging directory used to store
artifacts generated during the build, from the SRCDIR
we copy the static
libivrt specific Vagrantfile, convert the created image to qcow2 and name it
box.img
. The most "tricky" part is generating the metadata.json
file which
needs to know the size of the image, specifying the wrong size can lead booting
without a root partition or a non-bootable image.
The last step creates the tar archive which can be imported using vagrant box add archvirt /path/to/file.box
.
VirtualBox
The VirtualBox format is similar, it requires a tar archive with:
- Vagrantfile
- metadata.json
- packer-virtualbox.vmdk (vm image as vmdk)
- box.ovf file (Open Virtualization Format)
mkosi.postout
#!/bin/bash
IMAGE_NAME="Arch-Linux-x86_64-virtualbox-latest.box"
cp "${SRCDIR}/Vagrantfile.virtualbox" "${OUTPUTDIR}/Vagrantfile"
echo '{"provider":"virtualbox"}' > "${OUTPUTDIR}/metadata.json"
qemu-img convert -f raw -O vmdk "${OUTPUTDIR}/${IMAGE_ID}.raw" "${OUTPUTDIR}/packer-virtualbox.vmdk"
sed -e "s/MACHINE_UUID/$(uuidgen)/" \
-e "s/DISK_UUID/$(uuidgen)/" \
-e "s/DISK_CAPACITY/$(qemu-img info --output=json "${OUTPUTDIR}/packer-virtualbox.vmdk" | jq '."virtual-size"')/" \
-e "s/UNIX/$(date +%s)/" \
-e "s/MAC_ADDRESS/080027AF9290/" \
box.ovf > "${OUTPUTDIR}/box.ovf"
tar -C "${OUTPUTDIR}" -czf "${OUTPUTDIR}/${IMAGE_NAME}" Vagrantfile metadata.json packer-virtualbox.vmdk box.ovf
Similar to the libvirt script, we copy the Vagrantfile, create a metadata.json file and convert the created image to a vmdk image. The last step is rather hackily creating the required ovf file from a known template.
The xml part can use some improvements, although other projects also simply replace a XML template, it is worth investigating if xmlstarlet for example is a better way to format and validate the created xml file.
Conclusion
It was quite easy to create a Vagrant image with mkosi, most of the discovery work was already done in arch-boxes so all that was left was moving it over to mkosi configuration and figuring out the post installation and image conversion scripts.
Next up will be converting the cloud images to mkosi. You can find all the files mentioned in this post in the following GitHub repository.