445 lines
16 KiB
Markdown
445 lines
16 KiB
Markdown
|
---
|
||
|
title: "NixOS on Xen PV... debootstrap style"
|
||
|
date: 2023-08-19
|
||
|
layout: Post
|
||
|
hero: https://images.unsplash.com/photo-1481729379561-01e43a3e1ed4
|
||
|
hero credit: https://unsplash.com/photos/H6HNYGsyeKQ
|
||
|
hero credit text: "Noah Buscher"
|
||
|
classes:
|
||
|
header: header-black-gradient
|
||
|
---
|
||
|
|
||
|
One of my work colleagues was commenting that they like the Xen PV model -
|
||
|
where you have a fairly lightweight hypervisor that runs cooperating kernels
|
||
|
(or, as Xen calls them, "domains"). They've been meaning to try out NixOS
|
||
|
but couldn't figure out how to build a debootstrap-style root FS.
|
||
|
|
||
|
---
|
||
|
|
||
|
NixOS is interesting in that really the only thing that _needs_ to exist on the
|
||
|
disk is a /nix/store directory containing a built system -- everything else
|
||
|
somewhat springs out of nothing as long as the boot process can figure out what
|
||
|
exactly it was you were intending to boot.
|
||
|
|
||
|
This means that, if you have a system with Nix on (which doesn't have to be
|
||
|
NixOS -- I'm going to use Debian 12 running as a Xen dom0 to demonstrate), then
|
||
|
it's relatively easy to build a Xen PV compatible rootfs.
|
||
|
|
||
|
## Table of contents
|
||
|
|
||
|
## Getting your host system set up
|
||
|
|
||
|
### Installing Nix itself
|
||
|
|
||
|
You'll need Nix installed. I prefer to use the [Determinate Nix
|
||
|
Installer](https://github.com/DeterminateSystems/nix-installer) in most cases
|
||
|
rather than the official one - it does a better job, and provides an uninstall
|
||
|
tool that works, so if you decide you're not interested in Nix anymore then
|
||
|
it's easy to get rid of again.
|
||
|
|
||
|
So...
|
||
|
|
||
|
```text
|
||
|
$ curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
|
||
|
info: downloading installer https://install.determinate.systems/nix/tag/v0.11.0/nix-installer-x86_64-linux
|
||
|
`nix-installer` needs to run as `root`, attempting to escalate now via `sudo`...
|
||
|
Nix install plan (v0.11.0)
|
||
|
Planner: linux (with default settings)
|
||
|
|
||
|
Planned actions:
|
||
|
* Create directory `/nix`
|
||
|
* Fetch `https://releases.nixos.org/nix/nix-2.17.0/nix-2.17.0-x86_64-linux.tar.xz` to `/nix/temp-install-dir`
|
||
|
* Create a directory tree in `/nix`
|
||
|
* Move the downloaded Nix into `/nix`
|
||
|
* Create build group (GID 30000)
|
||
|
* Setup the default Nix profile
|
||
|
* Place the Nix configuration in `/etc/nix/nix.conf`
|
||
|
* Configure the shell profiles
|
||
|
* Create directory `/etc/tmpfiles.d`
|
||
|
* Configure Nix daemon related settings with systemd
|
||
|
* Remove directory `/nix/temp-install-dir`
|
||
|
|
||
|
|
||
|
Proceed? ([Y]es/[n]o/[e]xplain): y
|
||
|
INFO Step: Create directory `/nix`
|
||
|
INFO Step: Provision Nix
|
||
|
INFO Step: Create build group (GID 30000)
|
||
|
INFO Step: Configure Nix
|
||
|
INFO Step: Create directory `/etc/tmpfiles.d`
|
||
|
INFO Step: Configure Nix daemon related settings with systemd
|
||
|
INFO Step: Remove directory `/nix/temp-install-dir`
|
||
|
Nix was installed successfully!
|
||
|
To get started using Nix, open a new shell or run `. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh`
|
||
|
```
|
||
|
|
||
|
### Getting a copy of nixpkgs
|
||
|
|
||
|
The Determinate Nix installer doesn't pull a nixpkgs channel; it's expecting you
|
||
|
to use flakes. For the purposes of this demo though, I'm not really interested
|
||
|
in doing that right now, so I'm going to clone nixpkgs myself.
|
||
|
|
||
|
To speed things up, I'm just cloning the latest nixpkgs NixOS release branch (23.05
|
||
|
"Stoat" at the time of writing).
|
||
|
|
||
|
```text
|
||
|
$ git clone --depth 1 --branch nixos-23.05 https://github.com/NixOS/nixpkgs.git
|
||
|
Cloning into 'nixpkgs'...
|
||
|
remote: Enumerating objects: 58617, done.
|
||
|
remote: Counting objects: 100% (58617/58617), done.
|
||
|
remote: Compressing objects: 100% (37070/37070), done.
|
||
|
remote: Total 58617 (delta 2534), reused 55277 (delta 2393), pack-reused 0
|
||
|
Receiving objects: 100% (58617/58617), 43.47 MiB | 9.97 MiB/s, done.
|
||
|
Resolving deltas: 100% (2534/2534), done.
|
||
|
Updating files: 100% (35411/35411), done.
|
||
|
```
|
||
|
|
||
|
## Building a NixOS system
|
||
|
|
||
|
### Writing a system configuration
|
||
|
|
||
|
Next up on our grand tour: a NixOS system configuration. This is the file which describes
|
||
|
NixOS system looks like and should encapsulate most of the non-runtime configuration.
|
||
|
|
||
|
I wrote this file out to `system-configuration.nix`:
|
||
|
|
||
|
```nix
|
||
|
{ config, lib, pkgs, ... }:
|
||
|
|
||
|
{
|
||
|
# Ensure we have the Xen block device module available during boot.
|
||
|
boot.initrd.availableKernelModules = [ "xen-blkfront" "xen-kbdfront" ];
|
||
|
|
||
|
# Mount /dev/xvda1 at /.
|
||
|
fileSystems."/" = {
|
||
|
device = "/dev/xvda1";
|
||
|
fsType = "ext4";
|
||
|
};
|
||
|
|
||
|
# Disable GRUB: we're not using it here.
|
||
|
boot.loader.grub.enable = false;
|
||
|
|
||
|
networking = {
|
||
|
# Set our hostname.
|
||
|
hostName = "nixos-inside-xen";
|
||
|
|
||
|
# Disable global DHCP but enable it on the NIC we will get.
|
||
|
useDHCP = false;
|
||
|
interfaces.enX0.useDHCP = true;
|
||
|
|
||
|
# Use systemd-networkd, rather than legacy script-based networking.
|
||
|
useNetworkd = true;
|
||
|
};
|
||
|
|
||
|
# Use systemd-as-stage-1, rather than legacy script-based stage 1/stage 2.
|
||
|
boot.initrd.systemd.enable = true;
|
||
|
|
||
|
# Set our timezone.
|
||
|
time.timeZone = "Europe/London";
|
||
|
|
||
|
# Create a 'user' user, with sudo powers.
|
||
|
users.users.user = {
|
||
|
isNormalUser = true;
|
||
|
extraGroups = [ "wheel" ];
|
||
|
password = "thisisinsecure";
|
||
|
};
|
||
|
|
||
|
# Enable SSH for good measure.
|
||
|
services.openssh.enable = true;
|
||
|
|
||
|
# Make sure we have an editor.
|
||
|
environment.systemPackages = [ pkgs.vim ];
|
||
|
# This can also be written as:
|
||
|
# environment.systemPackages = with pkgs; [ vim ];
|
||
|
|
||
|
system.stateVersion = "23.11";
|
||
|
}
|
||
|
```
|
||
|
|
||
|
### Building the system
|
||
|
|
||
|
Now that we have a NixOS system configuration, we can use the NixOS machinery
|
||
|
to turn this into a system.
|
||
|
|
||
|
```text
|
||
|
# Assuming that 'nixpkgs' and 'system-configuration.nix' are in the current directory:
|
||
|
$ nix-build 'nixpkgs/nixos' -A system --arg configuration ./system-configuration.nix
|
||
|
# The ./ before system-configuration.nix is important to ensure Nix interprets
|
||
|
# it as a path rather than a string.
|
||
|
[... a lot of output later ...]
|
||
|
building '/nix/store/125c1x5n9lgcx4gf7wswdy4m8kawmf4i-etc.drv'...
|
||
|
building '/nix/store/g2mlr67i0z45n695wvc5rsnpiqcmahzk-nixos-system-nixos-inside-xen-23.05pre-git.drv'...
|
||
|
/nix/store/7cxgpci704yqcgqizv5ih5b47n9ckmg9-nixos-system-nixos-inside-xen-23.05pre-git
|
||
|
```
|
||
|
|
||
|
The final line output (which also gets written to the current directory as a
|
||
|
symlink named `result`) is the Nix store path to the system you've just built.
|
||
|
|
||
|
Now we have the system, we can start making the disk image for Xen.
|
||
|
|
||
|
## Making the disk image
|
||
|
|
||
|
### Formatting a disk
|
||
|
|
||
|
For my purposes, I'm going to make a 10GB disk image with a single ext4
|
||
|
partition. If you want to do something different, you'll need to adjust
|
||
|
the NixOS configuration above accordingly, and rebuild it.
|
||
|
|
||
|
```text
|
||
|
# Make a 10G empty file.
|
||
|
$ truncate --size 10G nixos.img
|
||
|
|
||
|
# Create a single partition.
|
||
|
$ fdisk nixos.img
|
||
|
|
||
|
Welcome to fdisk (util-linux 2.38.1).
|
||
|
Changes will remain in memory only, until you decide to write them.
|
||
|
Be careful before using the write command.
|
||
|
|
||
|
Device does not contain a recognized partition table.
|
||
|
Created a new DOS (MBR) disklabel with disk identifier 0x3980dcd5.
|
||
|
|
||
|
Command (m for help): n
|
||
|
Partition type
|
||
|
p primary (0 primary, 0 extended, 4 free)
|
||
|
e extended (container for logical partitions)
|
||
|
Select (default p): p
|
||
|
Partition number (1-4, default 1):
|
||
|
First sector (2048-20971519, default 2048):
|
||
|
Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-20971519, default 20971519):
|
||
|
|
||
|
Created a new partition 1 of type 'Linux' and of size 10 GiB.
|
||
|
|
||
|
Command (m for help): w
|
||
|
The partition table has been altered.
|
||
|
Syncing disks.
|
||
|
|
||
|
# Make a partitioned loop device.
|
||
|
$ sudo losetup --find --partscan --show nixos.img
|
||
|
/dev/loop0
|
||
|
|
||
|
# Format the new partition with ext4.
|
||
|
$ sudo mkfs.ext4 -L NIXOS /dev/loop0p1
|
||
|
mke2fs 1.47.0 (5-Feb-2023)
|
||
|
Discarding device blocks: done
|
||
|
Creating filesystem with 2621184 4k blocks and 655360 inodes
|
||
|
Filesystem UUID: 77a3e697-e6b0-4645-99f3-47528256d47b
|
||
|
Superblock backups stored on blocks:
|
||
|
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632
|
||
|
|
||
|
Allocating group tables: done
|
||
|
Writing inode tables: done
|
||
|
Creating journal (16384 blocks): done
|
||
|
Writing superblocks and filesystem accounting information: done
|
||
|
```
|
||
|
|
||
|
### Installing NixOS
|
||
|
|
||
|
We'll need the disk mounted, so I'll do that first:
|
||
|
|
||
|
```text
|
||
|
$ mkdir mnt
|
||
|
$ sudo mount /dev/loop0p1 mnt
|
||
|
```
|
||
|
|
||
|
Now actually installing the system is as simple as using `nix` to copy it:
|
||
|
|
||
|
```text
|
||
|
$ sudo $(which nix) copy --to $(readlink -f mnt) ./result --no-check-sigs
|
||
|
[... progress output ...]
|
||
|
|
||
|
# Now there's a "nix/store" inside the disk.
|
||
|
$ ls mnt/nix
|
||
|
store var
|
||
|
$ ls mnt/nix/store | wc -l
|
||
|
503
|
||
|
```
|
||
|
|
||
|
We can now set this as the current system inside the image - this mostly just
|
||
|
creates a symlink, but this sets up the generational semantics of NixOS.
|
||
|
|
||
|
```text
|
||
|
$ sudo nix-env -p mnt/nix/var/nix/profiles/system --set ./result
|
||
|
$ ls mnt/nix/var/nix/profiles -l
|
||
|
total 8
|
||
|
drwxr-xr-x 2 root root 4096 Aug 19 14:49 per-user
|
||
|
lrwxrwxrwx 1 root root 13 Aug 19 14:59 system -> system-1-link
|
||
|
lrwxrwxrwx 1 root root 86 Aug 19 14:59 system-1-link -> /nix/store/7cxgpci704yqcgqizv5ih5b47n9ckmg9-nixos-system-nixos-inside-xen-23.05pre-git
|
||
|
```
|
||
|
|
||
|
As you can see, this has created the `system` link, which points at the current
|
||
|
generation (`system-1-link`), which finally points at the actual system.
|
||
|
|
||
|
For our convenience, we'll copy the configuration into the disk image. The
|
||
|
conventional location is `/etc/nixos/configuration.nix`. This isn't strictly
|
||
|
necessary to have a working system, but it makes upkeep much easier.
|
||
|
|
||
|
```text
|
||
|
$ sudo mkdir -p mnt/etc/nixos
|
||
|
$ sudo cp system-configuration.nix /etc/nixos/configuration.nix
|
||
|
```
|
||
|
|
||
|
For better or for worse, we'll also set up the NixOS channel definition.
|
||
|
|
||
|
```text
|
||
|
$ sudo mkdir mnt/root
|
||
|
$ echo "https://nixos.org/channels/nixos-23.05 nixos" | sudo tee mnt/root/.nix-channels
|
||
|
https://nixos.org/channels/nixos-23.05 nixos
|
||
|
```
|
||
|
|
||
|
OK, we're done with the disk image now, so we can unmount it:
|
||
|
|
||
|
```text
|
||
|
$ sudo umount mnt
|
||
|
$ sudo losetup -d /dev/loop0
|
||
|
```
|
||
|
|
||
|
## Booting this in Xen
|
||
|
|
||
|
We now have a disk image with NixOS installed. We don't need to copy the kernel
|
||
|
or ramdisk out of it because we already have it on the host. For longer term use,
|
||
|
though, I suggest using Xen's built in grub emulator or similar to make sure that
|
||
|
things are kept up to date. This will boot the same system configuration every
|
||
|
time.
|
||
|
|
||
|
I wrote this into my `domain.conf` - you'll need to substitute the path to
|
||
|
your own Nix system, and your `nixos.img`.
|
||
|
|
||
|
```ini
|
||
|
kernel = "/nix/store/7cxgpci704yqcgqizv5ih5b47n9ckmg9-nixos-system-nixos-inside-xen-23.05pre-git/kernel"
|
||
|
ramdisk = "/nix/store/7cxgpci704yqcgqizv5ih5b47n9ckmg9-nixos-system-nixos-inside-xen-23.05pre-git/initrd"
|
||
|
memory = 2048
|
||
|
name = "nixos"
|
||
|
vif = [ '' ]
|
||
|
dhcp = "dhcp"
|
||
|
cmdline = "xencons=tty init=/nix/store/7cxgpci704yqcgqizv5ih5b47n9ckmg9-nixos-system-nixos-inside-xen-23.05pre-git/init"
|
||
|
disk = ['/home/lukegb/nix-blogpost/nixos.img,,hda']
|
||
|
```
|
||
|
|
||
|
And now we can boot the system:
|
||
|
|
||
|
```text
|
||
|
$ sudo xl create ./domain.conf -c
|
||
|
[... it boots ...]
|
||
|
|
||
|
|
||
|
<<< Welcome to NixOS 23.05pre-git (x86_64) - hvc0 >>>
|
||
|
|
||
|
Run 'nixos-help' for the NixOS manual.
|
||
|
|
||
|
nixos-inside-xen login:
|
||
|
```
|
||
|
|
||
|
You can then log in with the `user` / `thisisinsecure` pair. The `user` user
|
||
|
has sudo permission, and when you're done you can shut the VM down. Assuming
|
||
|
that your Xen system is set up similar to mine, with a xenbr0 that has internet
|
||
|
access with a DHCP server (and hopefully even IPv6...!), then you should get
|
||
|
an IP address inside the VM as well.
|
||
|
|
||
|
### Updating the nixpkgs channel
|
||
|
|
||
|
The first thing you'll probably want to do is actually fetch the Nix channel
|
||
|
that we configured while creating the disk image:
|
||
|
|
||
|
```text
|
||
|
[user@nixos-inside-xen:~]$ sudo nix-channel --update
|
||
|
unpacking channels...
|
||
|
```
|
||
|
|
||
|
Now you can freely experiment with Nix.
|
||
|
|
||
|
## Bonus round: Making a trivial configuration change
|
||
|
|
||
|
This is some bonus work, to explore making a NixOS configuration change. You
|
||
|
already have a working system at this point so you can stop reading.
|
||
|
|
||
|
As an example, say you want to have `mtr` available inside your system. It's
|
||
|
not installed by default:
|
||
|
|
||
|
```text
|
||
|
[user@nixos-inside-xen:~]$ mtr --report 8.8.8.8
|
||
|
The program 'mtr' is not in your PATH. It is provided by several packages.
|
||
|
You can make it available in an ephemeral shell by typing one of the following:
|
||
|
nix-shell -p mtr
|
||
|
nix-shell -p mtr-gui
|
||
|
```
|
||
|
|
||
|
You can follow the instructions and use `nix-shell` to provide it temporarily:
|
||
|
|
||
|
```text
|
||
|
[user@nixos-inside-xen:~]$ nix-shell -p mtr
|
||
|
[... nix downloads mtr from cache ...]
|
||
|
[nix-shell:~]$ mtr --report 8.8.8.8
|
||
|
Start: 2023-08-19T15:10:53+0100
|
||
|
HOST: nixos-inside-xen Loss% Snt Last Avg Best Wrst StDev
|
||
|
1.|-- _gateway 0.0% 10 0.6 0.5 0.5 0.6 0.0
|
||
|
2.|-- tuvok.gnet-tuvok.mldn-rd. 20.0% 10 2.0 2.2 2.0 2.6 0.2
|
||
|
3.|-- blade-tuvok.public.as2054 0.0% 10 2.5 2.4 1.9 2.5 0.2
|
||
|
4.|-- 195.66.224.125 0.0% 10 2.7 3.1 2.5 5.3 0.9
|
||
|
5.|-- 74.125.242.97 0.0% 10 4.7 4.7 4.2 7.2 0.9
|
||
|
6.|-- 192.178.46.87 0.0% 10 3.6 3.6 3.2 4.7 0.4
|
||
|
7.|-- dns.google 0.0% 10 3.2 3.4 3.0 3.7 0.2
|
||
|
```
|
||
|
|
||
|
except it won't quite work properly, because its `mtr-packet` process isn't
|
||
|
privileged:
|
||
|
|
||
|
```text
|
||
|
[nix-shell:~]$ mtr --udp --report 8.8.8.8
|
||
|
Start: 2023-08-19T15:13:03+0100
|
||
|
HOST: nixos-inside-xen Loss% Snt Last Avg Best Wrst StDev
|
||
|
|
||
|
[nix-shell:~]$ exit
|
||
|
[user@nixos-inside-xen:~]$
|
||
|
```
|
||
|
|
||
|
You can solve this by installing it in your NixOS configuration using the
|
||
|
`programs.mtr.enable` configuration option, defined
|
||
|
[in this module](https://github.com/NixOS/nixpkgs/blob/nixos-23.05/nixos/modules/programs/mtr.nix).
|
||
|
This both installs the package (into `environment.systemPackages`), but
|
||
|
also installs a setcap wrapper for `mtr-packet`.
|
||
|
|
||
|
Edit `/etc/nixos/configuration.nix`, and add `programs.mtr.enable = true;`
|
||
|
somewhere, probably just below the `environment.systemPackages` line.
|
||
|
|
||
|
Once you've done that, rebuild and switch to the new system:
|
||
|
|
||
|
```text
|
||
|
[user@nixos-inside-xen:~]$ sudo nixos-rebuild switch
|
||
|
building Nix...
|
||
|
building the system configuration...
|
||
|
these 11 derivations will be built:
|
||
|
/nix/store/c9nkzphpp4hfgbx4da8pfh4b6xcfwy1s-system-path.drv
|
||
|
/nix/store/0bm8y3d51c93dr27jkzzaaz0kv43vv62-unit-systemd-fsck-.service.drv
|
||
|
/nix/store/k984vii2lsh7yjz8acvbfqgph0vyssfj-dbus-1.drv
|
||
|
/nix/store/j9lmndz2by4ridi3vyzbd32mnay2pn4q-X-Restart-Triggers.drv
|
||
|
/nix/store/d0j9nmy6qylmgllaw21l6w11b0gkhxkx-unit-dbus.service.drv
|
||
|
/nix/store/1cjakrjkp764r26fddkpixjp6cff0kq7-user-units.drv
|
||
|
/nix/store/8l53jy12sbls3ppxc7pqh40ialnw53x4-unit-dbus.service.drv
|
||
|
/nix/store/vnky6khlsj4zasp4q8g6rwp2b3jhp99l-system-units.drv
|
||
|
/nix/store/d5zmdd9g1zvv7f5s9w2n3praq8d4k1p6-etc.drv
|
||
|
/nix/store/qixzqgmf54k8xypajycry4v42llhhwi1-ensure-all-wrappers-paths-exist.drv
|
||
|
/nix/store/hp0dhaw2rgcpa7y70rfhnmnr2qiw8lm2-nixos-system-nixos-inside-xen-23.05.2891.ae521bd4e460.drv
|
||
|
[...a little bit of output...]
|
||
|
building '/nix/store/hp0dhaw2rgcpa7y70rfhnmnr2qiw8lm2-nixos-system-nixos-inside-xen-23.05.2891.ae521bd4e460.drv'...
|
||
|
Warning: do not know how to make this configuration bootable; please enable a boot loader.
|
||
|
activating the configuration...
|
||
|
setting up /etc...
|
||
|
reloading user units for user...
|
||
|
setting up tmpfiles
|
||
|
reloading the following units: dbus.service
|
||
|
```
|
||
|
|
||
|
...and you now have `mtr` available system-wide, but now it works in UDP mode:
|
||
|
|
||
|
```text
|
||
|
[user@nixos-inside-xen:~]$ mtr --udp --report 8.8.8.8
|
||
|
Start: 2023-08-19T15:19:03+0100
|
||
|
HOST: nixos-inside-xen Loss% Snt Last Avg Best Wrst StDev
|
||
|
1.|-- _gateway 0.0% 10 0.5 0.5 0.4 0.6 0.0
|
||
|
2.|-- tuvok.gnet-tuvok.mldn-rd. 90.0% 10 2.3 2.3 2.3 2.3 0.0
|
||
|
3.|-- ??? 100.0 10 0.0 0.0 0.0 0.0 0.0
|
||
|
4.|-- 195.66.224.125 0.0% 10 2.8 2.8 2.5 3.3 0.3
|
||
|
5.|-- ??? 100.0 10 0.0 0.0 0.0 0.0 0.0
|
||
|
|
||
|
```
|