nixpkgs/nixos/modules/virtualisation/nspawn-container/default.nix
Jared Baur 68e2e2bde3
nixos/test-driver: fix eval with noUserModules
When "non-user" modules are not placed in baseModules or
extraModules (arguments of eval-config.nix), noUserModules.evalModules
breaks if the options defined in those modules are consumed. In order
to restore noUserModules functionality for NixOS VM (and now nspawn)
tests, the modules implementing test behavior are moved to baseModules.
2026-05-09 18:31:52 -07:00

154 lines
5.3 KiB
Nix

# This module creates a lightweight "container" from the NixOS configuration.
# Building the `config.system.build.nspawn` attribute gives you a command
# that starts a systemd-nspawn container running the NixOS configuration
# defined in `config`. By default, the Nix store is shared read-only with the
# host, which makes (re)building very efficient.
# This shares a lot in common with
# `nixos/modules/virtualisation/nixos-containers.nix`, but doesn't use systemd
# units.
# The networking options here match the options in
# `nixos/modules/virtualisation/nixos-containers.nix` which allows using these
# lightweight containers for nixos integration tests.
{
config,
pkgs,
lib,
...
}:
let
inherit (lib) types;
cfg = config.virtualisation;
in
{
options = {
virtualisation.cmdline = lib.mkOption {
type = types.listOf types.str;
default = [ ];
example = [
"systemd.unit=rescue.target"
"systemd.log_level=debug"
"systemd.log_target=console"
];
description = ''
Command line arguments to pass to the init process (likely systemd).
Useful for debugging.
'';
};
virtualisation.rootDir = lib.mkOption {
type = types.str;
default = "./${config.system.name}-root";
defaultText = lib.literalExpression ''"./''${config.system.name}-root"'';
description = ''
Path to a directory for the root filesystem for the container.
The directory will be created on startup if it does not
exist.
'';
};
virtualisation.systemd-nspawn = {
package = lib.mkPackageOption pkgs "systemd" { };
options = lib.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--bind=/home:/home" ];
description = ''
Options passed to systemd-nspawn.
See [systemd-nspawn docs](https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html) for a complete list.
'';
};
};
};
config = {
boot.isNspawnContainer = true;
assertions = [
{
assertion = config.specialisation == { };
message = ''
Setting 'specialisation' is disallowed for systemd-nspawn container configurations.
Activating a specialisation requires creating SUID wrappers (e.g., for 'sudo'),
which is prohibited within the Nix build sandbox where the test is run.
'';
}
{
# Check every interface defined in allInterfaces.
# Containers try to create a bridge "${config.system.name}-${interfaceName}"
assertion = lib.all (
iface:
let
hostName = "${config.system.name}-${iface.name}";
in
lib.stringLength hostName <= 15
) (lib.attrValues cfg.allInterfaces);
message =
let
offendingInterfaces = lib.filter (
iface: lib.stringLength "${config.system.name}-${iface.name}" > 15
) (lib.attrValues cfg.allInterfaces);
offenderList = map (
i:
"${config.system.name}-${i.name} (${toString (lib.stringLength "${config.system.name}-${i.name}")} chars)"
) offendingInterfaces;
in
''
The following generated host interface names exceed the Linux 15-character limit:
${lib.concatStringsSep "\n " offenderList}
Please shorten 'config.system.name' or the interface names in 'virtualisation.interfaces'.
'';
}
];
# TODO(arianvp): Remove after https://github.com/NixOS/nixpkgs/pull/480686 is merged
console.enable = true;
virtualisation.systemd-nspawn.options = [
"--private-network"
"--machine=${config.system.name}"
"--bind-ro=/nix/store:/nix/store"
# systemd-nspawn does some cleverness to mount a procfs and sysfs in an
# unprivileged container, see
# <https://github.com/systemd/systemd/blob/v258.2/src/nspawn/nspawn.c#L4341-L4349>.
# Unfortunately, this doesn't work in the Nix build sandbox as we do not
# have permission to mount filesystems of type `sysfs` nor `procfs`.
# Fortunately, the build sandbox does provide a `/proc` and `/sys` that
# we can just forward onto the container.
"--private-users=no"
"--bind=/proc:/run/host/proc"
"--bind=/sys:/run/host/sys"
# From `man systemd-nspawn`:
# > Use --keep-unit and --register=no in combination to disable any
# > kind of unit allocation or registration with systemd-machined.
"--keep-unit"
"--register=no"
# Send a READY=1 notification to a socket when the container is fully booted.
"--notify-ready=yes"
];
system.build.nspawn =
let
run-nspawn = pkgs.callPackage ./run-nspawn { };
commandLineOptions = lib.cli.toCommandLineShellGNU { } {
container-name = config.system.name;
root-dir = cfg.rootDir;
interfaces-json = builtins.toJSON (lib.attrValues cfg.allInterfaces);
init = "${config.system.build.toplevel}/init";
cmdline-json = builtins.toJSON cfg.cmdline;
};
in
pkgs.writers.writeDashBin "run-${config.system.name}-nspawn" ''
exec ${lib.getExe run-nspawn} ${commandLineOptions} ${lib.escapeShellArgs config.virtualisation.systemd-nspawn.options} "$@"
'';
};
}