test: vaultwarden microVM

This commit is contained in:
Nick 2025-11-07 13:36:30 -06:00
parent e90d05f83d
commit 7ba592c0c5
43 changed files with 4005 additions and 267 deletions

335
example/host/default.nix Executable file
View file

@ -0,0 +1,335 @@
{
pkgs,
config,
lib,
...
}:
let
inherit (config.microvm) stateDir;
microvmCommand = import ../../pkgs/microvm-command.nix {
inherit pkgs;
};
user = "microvm";
group = "kvm";
in
{
imports = [ ./options.nix ];
config = lib.mkIf config.microvm.host.enable {
assertions = lib.concatMap (vmName: [
{
assertion =
config.microvm.vms.${vmName}.config != null -> config.microvm.vms.${vmName}.flake == null;
message = "vm ${vmName}: Fully-declarative VMs cannot also set a flake!";
}
{
assertion =
config.microvm.vms.${vmName}.config != null -> config.microvm.vms.${vmName}.updateFlake == null;
message = "vm ${vmName}: Fully-declarative VMs cannot set a updateFlake!";
}
]) (builtins.attrNames config.microvm.vms);
boot.kernelModules = [ "tun" ];
system.activationScripts.microvm-host = ''
mkdir -p ${stateDir}
chown ${user}:${group} ${stateDir}
chmod g+w ${stateDir}
'';
environment.systemPackages = [
microvmCommand
];
users.users.${user} = {
isSystemUser = true;
inherit group;
};
security.pam.loginLimits = [
{
domain = user;
item = "memlock";
type = "hard";
value = "infinity";
}
{
domain = user;
item = "memlock";
type = "soft";
value = "infinity";
}
];
systemd.services =
builtins.foldl'
(
result: name:
result
// (
let
microvmConfig = config.microvm.vms.${name};
inherit (microvmConfig) flake updateFlake;
isFlake = flake != null;
guestConfig =
if isFlake then flake.nixosConfigurations.${name}.config else microvmConfig.config.config;
runner = guestConfig.microvm.declaredRunner;
in
{
"install-microvm-${name}" = {
description = "Install MicroVM '${name}'";
before = [
"microvm@${name}.service"
"microvm-tap-interfaces@${name}.service"
"microvm-pci-devices@${name}.service"
"microvm-virtiofsd@${name}.service"
];
partOf = [ "microvm@${name}.service" ];
wantedBy = [ "microvms.target" ];
# Run on every rebuild for fully-declarative MicroVMs and flake-based MicroVMs without updateFlake.
# For MicroVMs with updateFlake set, only run on initial installation.
unitConfig.ConditionPathExists = lib.mkIf (isFlake && updateFlake != null) "!${stateDir}/${name}";
serviceConfig.Type = "oneshot";
script = ''
mkdir -p ${stateDir}/${name}
cd ${stateDir}/${name}
ln -sTf ${runner} current
chown -h ${user}:${group} . current
''
# Including the toplevel here is crucial to have the service definition
# change when the host is rebuilt and the vm definition changed.
+ lib.optionalString (!isFlake) ''
ln -sTf ${guestConfig.system.build.toplevel} toplevel
''
# Declarative deployment requires storing just the flake
+ lib.optionalString isFlake ''
echo '${if updateFlake != null then updateFlake else flake}' > flake
chown -h ${user}:${group} flake
'';
serviceConfig.SyslogIdentifier = "install-microvm-${name}";
};
"microvm@${name}" = {
# restartIfChanged is opt-out, so we have to include the definition unconditionally
serviceConfig.X-RestartIfChanged = [
""
microvmConfig.restartIfChanged
];
path = lib.mkForce [ ];
# If the given declarative microvm wants to be restarted on change,
# We have to make sure this service group is restarted. To make sure
# that this service is also changed when the microvm configuration changes,
# we also have to include a trigger here.
restartTriggers = [ guestConfig.system.build.toplevel ];
overrideStrategy = "asDropin";
serviceConfig.Type =
if guestConfig.microvm.declaredRunner.supportsNotifySocket then "notify" else "simple";
};
"microvm-tap-interfaces@${name}" = {
serviceConfig.X-RestartIfChanged = [
""
microvmConfig.restartIfChanged
];
path = lib.mkForce [ ];
overrideStrategy = "asDropin";
};
"microvm-pci-devices@${name}" = {
serviceConfig.X-RestartIfChanged = [
""
microvmConfig.restartIfChanged
];
path = lib.mkForce [ ];
overrideStrategy = "asDropin";
};
"microvm-virtiofsd@${name}" = {
serviceConfig.X-RestartIfChanged = [
""
microvmConfig.restartIfChanged
];
path = lib.mkForce [ ];
overrideStrategy = "asDropin";
};
}
)
)
{
"microvm-tap-interfaces@" = {
description = "Setup MicroVM '%i' TAP interfaces";
before = [ "microvm@%i.service" ];
partOf = [ "microvm@%i.service" ];
after = [ "network.target" ];
unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/tap-up";
restartIfChanged = false;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
SyslogIdentifier = "microvm-tap-interfaces@%i";
ExecStart = "${stateDir}/%i/current/bin/tap-up";
ExecStop = "${stateDir}/%i/booted/bin/tap-down";
};
};
"microvm-macvtap-interfaces@" = {
description = "Setup MicroVM '%i' MACVTAP interfaces";
before = [ "microvm@%i.service" ];
partOf = [ "microvm@%i.service" ];
unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/macvtap-up";
restartIfChanged = false;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
SyslogIdentifier = "microvm-macvtap-interfaces@%i";
ExecStart = "${stateDir}/%i/current/bin/macvtap-up";
ExecStop = "${stateDir}/%i/booted/bin/macvtap-down";
};
};
"microvm-pci-devices@" = {
description = "Setup MicroVM '%i' devices for passthrough";
before = [ "microvm@%i.service" ];
partOf = [ "microvm@%i.service" ];
unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/pci-setup";
restartIfChanged = false;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
SyslogIdentifier = "microvm-pci-devices@%i";
ExecStart = "${stateDir}/%i/current/bin/pci-setup";
};
};
"microvm-virtiofsd@" =
let
runFromBootedOrCurrent = pkgs.writeShellScript "microvm-runFromBootedOrCurrent" ''
NAME="$1"
VM="$2"
cd "${stateDir}/$VM"
if [ -e booted ]; then
exec booted/bin/$NAME
else
exec current/bin/$NAME
fi
'';
in
{
description = "VirtioFS daemons for MicroVM '%i'";
before = [ "microvm@%i.service" ];
after = [ "local-fs.target" ];
partOf = [ "microvm@%i.service" ];
unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/virtiofsd-run";
restartIfChanged = false;
serviceConfig = {
WorkingDirectory = "${stateDir}/%i";
ExecStart = "${stateDir}/%i/current/bin/virtiofsd-run";
ExecStop = "${runFromBootedOrCurrent} virtiofsd-shutdown %i";
LimitNOFILE = 1048576;
NotifyAccess = "all";
PrivateTmp = "yes";
Restart = "always";
RestartSec = "5s";
SyslogIdentifier = "microvm-virtiofsd@%i";
Type = "notify";
};
};
"microvm@" = {
description = "MicroVM '%i'";
requires = [
"microvm-tap-interfaces@%i.service"
"microvm-macvtap-interfaces@%i.service"
"microvm-pci-devices@%i.service"
"microvm-virtiofsd@%i.service"
];
after = [
"network.target"
"microvm-tap-interfaces@%i.service"
"microvm-macvtap-interfaces@%i.service"
"microvm-pci-devices@%i.service"
"microvm-virtiofsd@%i.service"
];
unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/microvm-run";
restartIfChanged = false;
preStart = ''
rm -f booted
ln -s $(readlink current) booted
'';
postStop = ''
rm booted
'';
serviceConfig = {
Type = if config.microvm.host.useNotifySockets then "notify" else "simple";
WorkingDirectory = "${stateDir}/%i";
ExecStart = "${stateDir}/%i/current/bin/microvm-run";
ExecStop = "${stateDir}/%i/booted/bin/microvm-shutdown";
TimeoutSec = config.microvm.host.startupTimeout;
Restart = "always";
RestartSec = "5s";
User = user;
Group = group;
SyslogIdentifier = "microvm@%i";
LimitNOFILE = 1048576;
NotifyAccess = "all";
LimitMEMLOCK = "infinity";
};
};
}
(builtins.attrNames config.microvm.vms);
microvm.autostart = builtins.filter (vmName: config.microvm.vms.${vmName}.autostart) (
builtins.attrNames config.microvm.vms
);
# Starts all the containers after boot
systemd.targets.microvms = {
wantedBy = [ "multi-user.target" ];
wants = map (name: "microvm@${name}.service") config.microvm.autostart;
};
# This helper creates tap interfaces and attaches them to a bridge
# for qemu regardless if it is run as root or not.
security.wrappers.qemu-bridge-helper = lib.mkIf (!config.virtualisation.libvirtd.enable) {
source = "${pkgs.qemu-utils}/libexec/qemu-bridge-helper";
owner = "root";
group = "root";
capabilities = "cap_net_admin+ep";
};
# You must define this file with your bridge interfaces if you
# intend to use qemu-bridge-helper through a `type = "bridge"`
# interface.
environment.etc."qemu/bridge.conf".text = lib.mkDefault ''
allow all
'';
# Enable Kernel Same-Page Merging
hardware.ksm.enable = lib.mkDefault true;
# TODO: remove in 2026
system.activationScripts.microvm-update-check = ''
if [ -d ${stateDir} ]; then
_outdated_microvms=""
for dir in ${stateDir}/*; do
if [ -e $dir/current/share/microvm/virtiofs ] &&
[ ! -e $dir/current/bin/virtiofsd-run ]; then
_outdated_microvms="$_outdated_microvms $(basename $dir)"
elif [ -e $dir/current/share/microvm/tap-interfaces ] &&
[ ! -e $dir/current/bin/tap-up ]; then
_outdated_microvms="$_outdated_microvms $(basename $dir)"
elif [ -e $dir/current/share/microvm/macvtap-interfaces ] &&
[ ! -e $dir/current/bin/macvtap-up ]; then
_outdated_microvms="$_outdated_microvms $(basename $dir)"
elif [ -e $dir/current/share/microvm/pci-devices ] &&
[ ! -e $dir/current/bin/pci-setup ]; then
_outdated_microvms="$_outdated_microvms $(basename $dir)"
fi
done
if [ "$_outdated_microvms" != "" ]; then
echo "The following MicroVMs must be updated to follow the new virtiofsd/tap/macvtap/pci setup scheme: $_outdated_microvms"
fi
fi
'';
};
}

182
example/host/options.nix Executable file
View file

@ -0,0 +1,182 @@
{ pkgs, lib, ... }:
{
options.microvm = with lib; {
host.enable = mkOption {
type = types.bool;
default = true;
description = ''
Whether to enable the microvm.nix host module.
'';
};
host.startupTimeout = mkOption {
description = "Start up timeout for the VMs in seconds";
type = types.ints.positive;
default = 150;
};
host.useNotifySockets = mkOption {
type = types.bool;
default = false;
description = ''
Enable if all your MicroVMs run with a Hypervisor that sends
readiness notification over a VSOCK.
**Danger!** If one of your MicroVMs doesn't do this, its
systemd service will not start up successfully!
'';
};
vms = mkOption {
type =
with types;
attrsOf (
submodule (
{ config, name, ... }:
{
options = {
config = mkOption {
description = ''
A specification of the desired configuration of this MicroVM,
as a NixOS module, for building **without** a flake.
'';
default = null;
type = nullOr (
lib.mkOptionType {
name = "Toplevel NixOS config";
merge =
loc: defs:
(import "${config.nixpkgs}/nixos/lib/eval-config.nix" {
modules =
let
extraConfig = (
{ lib, ... }:
{
_file = "module at ${__curPos.file}:${toString __curPos.line}";
config = {
networking.hostName = lib.mkDefault name;
};
}
);
in
[
extraConfig
../microvm
]
++ (map (x: x.value) defs);
prefix = [
"microvm"
"vms"
name
"config"
];
inherit (config) specialArgs pkgs;
system =
if config.pkgs != null then
config.pkgs.stdenv.hostPlatform.system
else
pkgs.stdenv.hostPlatform.system;
});
}
);
};
nixpkgs = mkOption {
type = types.path;
default = if config.pkgs != null then config.pkgs.path else pkgs.path;
defaultText = literalExpression "pkgs.path";
description = ''
This option is only respected when `config` is
specified.
The nixpkgs path to use for the MicroVM. Defaults to the
host's nixpkgs.
'';
};
pkgs = mkOption {
type = types.nullOr types.unspecified;
default = pkgs;
defaultText = literalExpression "pkgs";
description = ''
This option is only respected when `config` is specified.
The package set to use for the MicroVM. Must be a
nixpkgs package set with the microvm overlay. Determines
the system of the MicroVM.
If set to null, a new package set will be instantiated.
'';
};
specialArgs = mkOption {
type = types.attrsOf types.unspecified;
default = { };
description = ''
This option is only respected when `config` is specified.
A set of special arguments to be passed to NixOS modules.
This will be merged into the `specialArgs` used to evaluate
the NixOS configurations.
'';
};
flake = mkOption {
description = "Source flake for declarative build";
type = nullOr path;
default = null;
defaultText = literalExpression ''flakeInputs.my-infra'';
};
updateFlake = mkOption {
description = "Source flakeref to store for later imperative update";
type = nullOr str;
default = null;
defaultText = literalExpression ''"git+file:///home/user/my-infra"'';
};
autostart = mkOption {
description = "Add this MicroVM to config.microvm.autostart?";
type = bool;
default = true;
};
restartIfChanged = mkOption {
type = types.bool;
default = config.config != null;
description = ''
Restart this MicroVM's services if the systemd units are changed,
i.e. if it has been updated by rebuilding the host.
Defaults to true for fully-declarative MicroVMs.
'';
};
};
}
)
);
default = { };
description = ''
The MicroVMs that shall be built declaratively with the host NixOS.
'';
};
stateDir = mkOption {
type = types.path;
default = "/var/lib/microvms";
description = ''
Directory that contains the MicroVMs
'';
};
autostart = mkOption {
type = with types; listOf str;
default = [ ];
description = ''
MicroVMs to start by default.
This includes declarative `config.microvm.vms` as well as MicroVMs that are managed through the `microvm` command.
'';
};
};
}