nixos/incus: init module and tests

This commit is contained in:
Adam Stephens 2023-10-20 00:10:59 -04:00
parent 247c509f1b
commit 1fff6a9031
No known key found for this signature in database
12 changed files with 481 additions and 6 deletions

View file

@ -1506,6 +1506,7 @@
./virtualisation/docker.nix
./virtualisation/ecs-agent.nix
./virtualisation/hyperv-guest.nix
./virtualisation/incus.nix
./virtualisation/kvmgt.nix
./virtualisation/libvirtd.nix
./virtualisation/lxc.nix

View file

@ -0,0 +1,232 @@
{ config, lib, pkgs, ... }:
let
cfg = config.virtualisation.incus;
preseedFormat = pkgs.formats.yaml { };
in
{
meta.maintainers = [ lib.maintainers.adamcstephens ];
options = {
virtualisation.incus = {
enable = lib.mkEnableOption (lib.mdDoc ''
incusd, a daemon that manages containers and virtual machines.
Users in the "incus-admin" group can interact with
the daemon (e.g. to start or stop containers) using the
{command}`incus` command line tool, among others.
'');
package = lib.mkPackageOptionMD pkgs "incus" { };
lxcPackage = lib.mkPackageOptionMD pkgs "lxc" { };
preseed = lib.mkOption {
type = lib.types.nullOr (
lib.types.submodule { freeformType = preseedFormat.type; }
);
default = null;
description = lib.mdDoc ''
Configuration for Incus preseed, see
<https://linuxcontainers.org/incus/docs/main/howto/initialize/#non-interactive-configuration>
for supported values.
Changes to this will be re-applied to Incus which will overwrite existing entities or create missing ones,
but entities will *not* be removed by preseed.
'';
example = {
networks = [
{
name = "incusbr0";
type = "bridge";
config = {
"ipv4.address" = "10.0.100.1/24";
"ipv4.nat" = "true";
};
}
];
profiles = [
{
name = "default";
devices = {
eth0 = {
name = "eth0";
network = "incusbr0";
type = "nic";
};
root = {
path = "/";
pool = "default";
size = "35GiB";
type = "disk";
};
};
}
];
storage_pools = [
{
name = "default";
driver = "dir";
config = {
source = "/var/lib/incus/storage-pools/default";
};
}
];
};
};
socketActivation = lib.mkEnableOption (
lib.mdDoc ''
socket-activation for starting incus.service. Enabling this option
will stop incus.service from starting automatically on boot.
''
);
startTimeout = lib.mkOption {
type = lib.types.ints.unsigned;
default = 600;
apply = toString;
description = lib.mdDoc ''
Time to wait (in seconds) for incusd to become ready to process requests.
If incusd does not reply within the configured time, `incus.service` will be
considered failed and systemd will attempt to restart it.
'';
};
};
};
config = lib.mkIf cfg.enable {
# https://github.com/lxc/incus/blob/f145309929f849b9951658ad2ba3b8f10cbe69d1/doc/reference/server_settings.md
boot.kernel.sysctl = {
"fs.aio-max-nr" = lib.mkDefault 524288;
"fs.inotify.max_queued_events" = lib.mkDefault 1048576;
"fs.inotify.max_user_instances" = lib.mkOverride 1050 1048576; # override in case conflict nixos/modules/services/x11/xserver.nix
"fs.inotify.max_user_watches" = lib.mkOverride 1050 1048576; # override in case conflict nixos/modules/services/x11/xserver.nix
"kernel.dmesg_restrict" = lib.mkDefault 1;
"kernel.keys.maxbytes" = lib.mkDefault 2000000;
"kernel.keys.maxkeys" = lib.mkDefault 2000;
"net.core.bpf_jit_limit" = lib.mkDefault 1000000000;
"net.ipv4.neigh.default.gc_thresh3" = lib.mkDefault 8192;
"net.ipv6.neigh.default.gc_thresh3" = lib.mkDefault 8192;
# vm.max_map_count is set higher in nixos/modules/config/sysctl.nix
};
boot.kernelModules = [
"veth"
"xt_comment"
"xt_CHECKSUM"
"xt_MASQUERADE"
"vhost_vsock"
] ++ lib.optionals (!config.networking.nftables.enable) [ "iptable_mangle" ];
environment.systemPackages = [ cfg.package ];
# Note: the following options are also declared in virtualisation.lxc, but
# the latter can't be simply enabled to reuse the formers, because it
# does a bunch of unrelated things.
systemd.tmpfiles.rules = [ "d /var/lib/lxc/rootfs 0755 root root -" ];
security.apparmor = {
packages = [ cfg.lxcPackage ];
policies = {
"bin.lxc-start".profile = ''
include ${cfg.lxcPackage}/etc/apparmor.d/usr.bin.lxc-start
'';
"lxc-containers".profile = ''
include ${cfg.lxcPackage}/etc/apparmor.d/lxc-containers
'';
};
};
systemd.services.incus = {
description = "Incus Container and Virtual Machine Management Daemon";
wantedBy = lib.mkIf (!cfg.socketActivation) [ "multi-user.target" ];
after = [
"network-online.target"
"lxcfs.service"
] ++ (lib.optional cfg.socketActivation "incus.socket");
requires = [
"lxcfs.service"
] ++ (lib.optional cfg.socketActivation "incus.socket");
wants = [
"network-online.target"
];
path = lib.mkIf config.boot.zfs.enabled [ config.boot.zfs.package ];
environment = {
# Override Path to the LXC template configuration directory
INCUS_LXC_TEMPLATE_CONFIG = "${pkgs.lxcfs}/share/lxc/config";
};
serviceConfig = {
ExecStart = "${cfg.package}/bin/incusd --group incus-admin";
ExecStartPost = "${cfg.package}/bin/incusd waitready --timeout=${cfg.startTimeout}";
KillMode = "process"; # when stopping, leave the containers alone
Delegate = "yes";
LimitMEMLOCK = "infinity";
LimitNOFILE = "1048576";
LimitNPROC = "infinity";
TasksMax = "infinity";
Restart = "on-failure";
TimeoutStartSec = "${cfg.startTimeout}s";
TimeoutStopSec = "30s";
};
};
systemd.sockets.incus = lib.mkIf cfg.socketActivation {
description = "Incus UNIX socket";
wantedBy = [ "sockets.target" ];
socketConfig = {
ListenStream = "/var/lib/incus/unix.socket";
SocketMode = "0660";
SocketGroup = "incus-admin";
Service = "incus.service";
};
};
systemd.services.incus-preseed = lib.mkIf (cfg.preseed != null) {
description = "Incus initialization with preseed file";
wantedBy = [ "multi-user.target" ];
requires = [ "incus.service" ];
after = [ "incus.service" ];
script = ''
${cfg.package}/bin/incus admin init --preseed <${
preseedFormat.generate "incus-preseed.yaml" cfg.preseed
}
'';
serviceConfig = {
Type = "oneshot";
};
};
users.groups.incus-admin = { };
users.users.root = {
# match documented default ranges https://linuxcontainers.org/incus/docs/main/userns-idmap/#allowed-ranges
subUidRanges = [
{
startUid = 1000000;
count = 1000000000;
}
];
subGidRanges = [
{
startGid = 1000000;
count = 1000000000;
}
];
};
virtualisation.lxc.lxcfs.enable = true;
};
}

View file

@ -392,6 +392,7 @@ in {
icingaweb2 = handleTest ./icingaweb2.nix {};
iftop = handleTest ./iftop.nix {};
incron = handleTest ./incron.nix {};
incus = pkgs.recurseIntoAttrs (handleTest ./incus { inherit handleTestOn; });
influxdb = handleTest ./influxdb.nix {};
influxdb2 = handleTest ./influxdb2.nix {};
initrd-network-openvpn = handleTest ./initrd-network-openvpn {};

View file

@ -0,0 +1,76 @@
import ../make-test-python.nix ({ pkgs, lib, ... } :
let
releases = import ../../release.nix {
configuration = {
# Building documentation makes the test unnecessarily take a longer time:
documentation.enable = lib.mkForce false;
};
};
container-image-metadata = releases.lxdContainerMeta.${pkgs.stdenv.hostPlatform.system};
container-image-rootfs = releases.lxdContainerImage.${pkgs.stdenv.hostPlatform.system};
in
{
name = "incus-container";
meta.maintainers = with lib.maintainers; [ adamcstephens ];
nodes.machine = { ... }: {
virtualisation = {
# Ensure test VM has enough resources for creating and managing guests
cores = 2;
memorySize = 1024;
diskSize = 4096;
incus.enable = true;
};
};
testScript = ''
def instance_is_up(_) -> bool:
status, _ = machine.execute("incus exec container --disable-stdin --force-interactive /run/current-system/sw/bin/true")
return status == 0
machine.wait_for_unit("incus.service")
# no preseed should mean no service
machine.fail("systemctl status incus-preseed.service")
machine.succeed("incus admin init --minimal")
with subtest("Container image can be imported"):
machine.succeed("incus image import ${container-image-metadata}/*/*.tar.xz ${container-image-rootfs}/*/*.tar.xz --alias nixos")
with subtest("Container can be launched and managed"):
machine.succeed("incus launch nixos container")
with machine.nested("Waiting for instance to start and be usable"):
retry(instance_is_up)
machine.succeed("echo true | incus exec container /run/current-system/sw/bin/bash -")
with subtest("Container CPU limits can be managed"):
machine.succeed("incus config set container limits.cpu 1")
# give container a chance to apply new resource limit
machine.sleep(1)
cpuinfo = machine.succeed("incus exec container grep -- -c ^processor /proc/cpuinfo").strip()
assert cpuinfo == "1", f"Wrong number of CPUs reported from /proc/cpuinfo, want: 1, got: {cpuinfo}"
machine.succeed("incus config set container limits.cpu 2")
machine.sleep(1)
cpuinfo = machine.succeed("incus exec container grep -- -c ^processor /proc/cpuinfo").strip()
assert cpuinfo == "2", f"Wrong number of CPUs reported from /proc/cpuinfo, want: 2, got: {cpuinfo}"
with subtest("Container memory limits can be managed"):
machine.succeed("incus config set container limits.memory 64MB")
machine.sleep(1)
meminfo = machine.succeed("incus exec container grep -- MemTotal /proc/meminfo").strip()
meminfo_bytes = " ".join(meminfo.split(' ')[-2:])
assert meminfo_bytes == "62500 kB", f"Wrong amount of memory reported from /proc/meminfo, want: '62500 kB', got: '{meminfo_bytes}'"
machine.succeed("incus config set container limits.memory 128MB")
machine.sleep(1)
meminfo = machine.succeed("incus exec container grep -- MemTotal /proc/meminfo").strip()
meminfo_bytes = " ".join(meminfo.split(' ')[-2:])
assert meminfo_bytes == "125000 kB", f"Wrong amount of memory reported from /proc/meminfo, want: '62500 kB', got: '{meminfo_bytes}'"
'';
})

View file

@ -0,0 +1,14 @@
{
system ? builtins.currentSystem,
config ? { },
pkgs ? import ../../.. { inherit system config; },
handleTestOn,
}:
{
container = import ./container.nix { inherit system pkgs; };
preseed = import ./preseed.nix { inherit system pkgs; };
socket-activated = import ./socket-activated.nix { inherit system pkgs; };
virtual-machine = handleTestOn [ "x86_64-linux" ] ./virtual-machine.nix {
inherit system pkgs;
};
}

View file

@ -0,0 +1,65 @@
import ../make-test-python.nix ({ pkgs, lib, ... } :
{
name = "incus-preseed";
meta.maintainers = with lib.maintainers; [ adamcstephens ];
nodes.machine = { lib, ... }: {
virtualisation = {
incus.enable = true;
incus.preseed = {
networks = [
{
name = "nixostestbr0";
type = "bridge";
config = {
"ipv4.address" = "10.0.100.1/24";
"ipv4.nat" = "true";
};
}
];
profiles = [
{
name = "nixostest_default";
devices = {
eth0 = {
name = "eth0";
network = "nixostestbr0";
type = "nic";
};
root = {
path = "/";
pool = "default";
size = "35GiB";
type = "disk";
};
};
}
];
storage_pools = [
{
name = "nixostest_pool";
driver = "dir";
}
];
};
};
};
testScript = ''
def wait_for_preseed(_) -> bool:
_, output = machine.systemctl("is-active incus-preseed.service")
return ("inactive" in output)
machine.wait_for_unit("incus.service")
with machine.nested("Waiting for preseed to complete"):
retry(wait_for_preseed)
with subtest("Verify preseed resources created"):
machine.succeed("incus profile show nixostest_default")
machine.succeed("incus network info nixostestbr0")
machine.succeed("incus storage show nixostest_pool")
'';
})

View file

@ -0,0 +1,25 @@
import ../make-test-python.nix ({ pkgs, lib, ... } :
{
name = "incus-socket-activated";
meta.maintainers = with lib.maintainers; [ adamcstephens ];
nodes.machine = { lib, ... }: {
virtualisation = {
incus.enable = true;
incus.socketActivation = true;
};
};
testScript = ''
machine.wait_for_unit("incus.socket")
# ensure service is not running by default
machine.fail("systemctl is-active incus.service")
# access the socket and ensure the service starts
machine.succeed("incus list")
machine.wait_for_unit("incus.service")
'';
})

View file

@ -0,0 +1,55 @@
import ../make-test-python.nix ({ pkgs, lib, ... }:
let
releases = import ../../release.nix {
configuration = {
# Building documentation makes the test unnecessarily take a longer time:
documentation.enable = lib.mkForce false;
# Our tests require `grep` & friends:
environment.systemPackages = with pkgs; [busybox];
};
};
vm-image-metadata = releases.lxdVirtualMachineImageMeta.${pkgs.stdenv.hostPlatform.system};
vm-image-disk = releases.lxdVirtualMachineImage.${pkgs.stdenv.hostPlatform.system};
instance-name = "instance1";
in
{
name = "incus-virtual-machine";
meta.maintainers = with lib.maintainers; [ adamcstephens ];
nodes.machine = {...}: {
virtualisation = {
# Ensure test VM has enough resources for creating and managing guests
cores = 2;
memorySize = 1024;
diskSize = 4096;
incus.enable = true;
};
};
testScript = ''
def instance_is_up(_) -> bool:
status, _ = machine.execute("incus exec ${instance-name} --disable-stdin --force-interactive /run/current-system/sw/bin/true")
return status == 0
machine.wait_for_unit("incus.service")
machine.succeed("incus admin init --minimal")
with subtest("virtual-machine image can be imported"):
machine.succeed("incus image import ${vm-image-metadata}/*/*.tar.xz ${vm-image-disk}/nixos.qcow2 --alias nixos")
with subtest("virtual-machine can be launched and become available"):
machine.succeed("incus launch nixos ${instance-name} --vm --config limits.memory=512MB --config security.secureboot=false")
with machine.nested("Waiting for instance to start and be usable"):
retry(instance_is_up)
with subtest("lxd-agent is started"):
machine.succeed("incus exec ${instance-name} systemctl is-active lxd-agent")
'';
})

View file

@ -39,9 +39,7 @@ stdenv.mkDerivation (finalAttrs: {
outputs = [ "dev" "out" ];
passthru = {
tests = {
inherit incus;
};
inherit (incus) tests;
updateScript = gitUpdater {
rev-prefix = "v";

View file

@ -11,6 +11,7 @@
, udev
, installShellFiles
, nix-update-script
, nixosTests
}:
buildGoModule rec {
@ -79,6 +80,8 @@ buildGoModule rec {
'';
passthru = {
tests.incus = nixosTests.incus;
updateScript = nix-update-script {
extraArgs = [
"-vr" "incus-\(.*\)"

View file

@ -24,6 +24,7 @@
, rsync
, spice-gtk
, squashfsTools
, util-linux
, virtiofsd
, xz
}:
@ -44,6 +45,7 @@ let
qemu-utils
rsync
squashfsTools
util-linux
virtiofsd
xz

View file

@ -41,9 +41,12 @@ stdenv.mkDerivation rec {
outputs = [ "dev" "out" ];
passthru.tests = {
inherit incus;
updateScript = gitUpdater { };
passthru = {
inherit (incus) tests;
updateScript = gitUpdater {
rev-prefix = "v";
};
};
meta = with lib; {