Mastodon

Playing with Vagrant, Virtualbox and Guix

September 04, 2024
Tags:

It is specifically convenient using Guix-the-system within a foreign distribution, such as Debian, for development and tests. The package management system can be used on top of the system, but I find it quite interesting to explore the potential of the Guix distribution in the context of virtualized environments. For personal use, that is also the ideal way to avoid breaking your own daily boxes every couple of days with daredevil approaches to personal computing.

I find it quite handy to use Vagrant to synthetically describe multiple virtual hosts within a virtual network, by means of a single Vagrantfile. Nowadays, there are multiple virtualization systems usable for the same purpose, including the libvirt ecosystem. Under the hood, those systems can generally manage multiple virtual machine engines (or even cloud systems). Vagrant under Debian can use either Libvirt+KVM or Virtualbox (the default one used by the upstream Vagrant) as primary engines.

Users can develop their own Vagrant images (called boxes) or use those made available on the Hashicorp cloud, as developed by third-party teams (which is the common case).

Unfortunately, while there are a handful of Debian boxes developed by the Debian Cloud Team, like many other ones for multiple distributions and releases, this is not the case for Guix. At the time of this post, there is only a single box created some years ago by Tom Parker-Shemilt.

Of course, the Guix team also made a qcow2 image available to run a virtualized desktop machine under a QEMU/KVM system, as explained in the manual. What is described in this post can also be easily applied to create an alternative custom implementation with QEMU. What is required is to create a virtual disk image of some kind and use it to populate a custom distribution configuration via guix. The resulting image will be runnable under a suitable virtualization system in the host foreign distribution.

Let's assume to use any recent version of Vagrant and Virtualbox, both installed on the host system, along with the Guix package manager. The following are the steps required to create a Vagrant box with a minimal configuration of Guix that is compatible (with some limitations) with Vagrant and can be used to run any number of Guix VMs. Each Guix vm can be reconfigured for specific goals using multiple configurations.

In this case, we will not need to use Packer to prepare a box because the vagrant package command directly supports Virtualbox images with the same purpose.

Create a virtual disk image and partition it

This is the straightforward part. Here, I'm using the vdi format, the native Virtualbox one, but the VMware vmdk format can be used instead.

 # load the Network Block Device module
 sudo modprobe nbd
 # create a thin device
 qemu-img create -f vdi guix-hd.vdi 100G
 # ... mount it
 sudo qemu-nbd -f vdi --connect=/dev/nbd0 guix-hd.vdi
 # ... partition it 
 sudo parted /dev/nbd0 mklabel msdos
 sudo parted -a cylinder /dev/nbd0 mkpart primary ext4 1 93G
 sudo parted -a cylinder /dev/nbd0 mkpart primary linux-swap 93G 100%
 sudo parted /dev/nbd0 set 1 boot on 
 # ... create a suitable fs
 sudo mkfs.ext4 /dev/nbd0p1
 sudo mkswap /dev/nbd0p2
 # ... have a look to the partions IDs
 sudo blkid /dev/nbd0p1 
 sudo blkid /dev/nbd0p2
 # ... mount the root fs
 sudo mount /dev/nbd0p1 /mnt 

Prepare a suitable configuration written in Guile for Guix

Any valid configuration can be prepared, in this case this is a minimal one with a pair of changes for Vagrant. Specifically, a vagrant user in the wheel group needs to be added, and it should be able to run sudo without a password. Even, the unsecure Vagrant key needs to be authorized to access via ssh.

Note that the key is added at the system level, and the root user has no password, which should be appropriately changed after the first run or, even better, at provisioning time. That is a requirement if the box would be exposed on public network, because the default private and public keys of Vagrant are publicly distributed.

That is usually and automagically done by vagrant at first boot, but Guix is a read-only system and - as we will see - the Guix system is still not completely supported by Vagrant.

ROOTFS_UUID=$(sudo blkid -o value /dev/nbd0p1|head -1)
SWAP_UUID=$(sudo blkid -o value /dev/nbd0p2|head -1)
DEVICE=/dev/nbd0

# Download the unsecure ssh key used by Vagrant
wget -q -O vagrant.pub https://raw.githubusercontent.com/mitchellh/vagrant/master/keys/vagrant.pub

cat >guix-config.scm <<EOF
;; Indicate which modules to import to access the variables
;; used in this configuration.
(use-modules (gnu))
(use-modules (gnu system))
(use-modules (gnu services avahi))
(use-service-modules desktop networking ssh)

;; Prepare a salt function for a less silly password encryption
(set! *random-state* (random-state-from-platform))
(define str "0123456789abcdefghijklmnopqrstuvwxyz")
(define rnd-chr (lambda () (string-ref str (random (- (string-length str) 1)))))
(define salt (lambda () (string-append (string (rnd-chr)) (string (rnd-chr)) (string (rnd-chr)))))

(operating-system
  (locale "en_US.utf8")
  (timezone "Europe/Rome")
  (keyboard-layout (keyboard-layout "us" "intl"))
  (host-name "guix")

  ;; The list of user accounts ('root' is implicit).
  (users (cons* (user-account
                  (name "vagrant")
                  (comment "Vagrant user")
                  (group "users")
                  (home-directory "/home/vagrant")
                  (password (crypt "vagrant" (string-append "\$6\$" (salt))))
                  (supplementary-groups '("wheel" "netdev" "audio" "video")))
                %base-user-accounts))

  ;; Packages installed system-wide.  Users can also install packages
  ;; under their own account: use 'guix search KEYWORD' to search
  ;; for packages and 'guix install PACKAGE' to install a package.
  (packages (append (list (specification->package "nss-certs")
                          (specification->package "rsync"))
                    %base-packages))

  ;; Below is the list of system services.  To search for available
  ;; services, run 'guix system search KEYWORD' in a terminal.
  (services
   (append (list (service dhcp-client-service-type)
                 (service openssh-service-type
                    ;; here the official unsecure Vagrant ssh key is used...
                    (openssh-configuration
                      (authorized-keys \`(("vagrant" ,(local-file "vagrant.pub")))))))

           ;; This is the default list of services we
           ;; are appending to.
           %base-services))

  ;; Authorize vagrant to run sudo without password.
  (sudoers-file
    (plain-file "sudoers"
                 (string-append (plain-file-content %sudoers-specification)
                                "vagrant ALL=(ALL) NOPASSWD: ALL\\n")))

  (bootloader (bootloader-configuration
                (bootloader grub-bootloader)
                (targets (list "$DEVICE"))
                (keyboard-layout keyboard-layout)))
  (swap-devices (list (swap-space
                        (target (uuid
                                 "$SWAP_UUID")))))

  ;; The list of file systems that get "mounted".  The unique
  ;; file system identifiers there ("UUIDs") can be obtained
  ;; by running 'blkid' in a terminal.
  (file-systems (cons* (file-system
                         (mount-point "/")
                         (device (uuid
                                  "$ROOTFS_UUID"
                                  'ext4))
                         (type "ext4")) %base-file-systems)))
EOF

Run an initial setup of the mounted virtual disk using guix on your host

The system init can be run more than once, if required.

sudo guix pull # only if needed to update packages...
sudo guix system init guix-config.scm /mnt
sudo umount /mnt
sudo qemu-nbd --disconnect /dev/nbd0

Create a basic Virtualbox machine and attach the virtual disk to it

Now, the virtual disk should already be registered and visibile under your Virtualbox configuration.

VDI_UUID=`vboxmanage showhdinfo guix-hd.vdi|grep ^UUID|awk '{print $2}'` && echo $VDI_UUID

should show the hexadecimal code associated to the now populated disk. It is now possible to create a simple virtual machine, for instance:

vboxmanage createvm --name=Guix --default --ostype=Linux_64 --register
vboxmanage modifyvm Guix --memory=4096 --cpus=2 --ioapic=on --vram=256 --cpu-profile=host \
                --audio-enabled=off --usb-xhci=off --usb-ehci=off --usb-ohci=off --mouse=ps2

and attach the virtual disk to it, as follows:

vboxmanage storageattach Guix --storagectl=SATA --type=hdd --port=0 --device=0 --medium=$VDI_UUID
vboxmanage showvminfo Guix

Convert the machine into a Vagrant box and register it

The resulting vm can be directly used under Virtualbox, but the final touch is creating a proper Vagrant box to recycle and possibly publish on the cloud.

vagrant package --base Guix --output guix-small.box
vagrant box add guix-small.box --name=guix-small
vagrant box list

Now the new box is ready for use in multiple configurations within a Vagrantfile.

Prepare a Vagrantfile snippet and run the vm(s)

A simple Vagrantfile can be used to create an instance of the box, such as:

Vagrant.configure("2") do |config|
    config.vm.define "guix1" do |vm1|
     vm1.vm.box = "guix-small"
     vm1.vm.network "private_network", ip: "192.168.1.2"
     vm1.vm.provider "virtualbox" do |vb1|
        vb1.memory = "8192"
    	vb1.name = "guix8G"
    	vb1.cpus = 4
    end
    config.vm.provision "shell", inline: <<-SHELL
        guix pull
        guix install htop 
     SHELL
    end
end

A new machine can be created, started up and connected easily, with also an initial provisioning, by issuing:

vagant up guix1
vagrant ssh guix1

(Re)configure vm(s) on the basis of your needs

Both the box and the dependent virtual machines can be reconfigured as usual by guix system reconfigure in the guest or even remounting the original virtual disk (or any of the vmdk copies cloned by Vagrant) as previously done, then re-issuing a system init with a new Guile configuration. Note that the same reconfigure can also be run as described in the manual.

Missing features in Vagrant to support Guix

Vagrant needs to recognize the installed system in order to perform a few operations, but unfortunately that is still not the case for Guix. Specifically, that prevents the capability of halting the system, which is not exactly a nice thing. That must be done manually by running halt within an ssh session. For the same reason, Vagrant is not able to fully configure networking, so the second network interface shown in the previous Vagrantfile needs to be configured manually by adding a suitable static-networking-service-type section to the Guile configuration.

A suitable support would need to be added as a proper vagrant plugin as in the case of other operating systems and distributions. So, good but not good enough, maybe it is matter for another post, even if I'm not exactly a Ruby language fan.

In the meantime, have a good time by upping and destroying Vagrant Guix boxes.