mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-08-26 18:24:53 +00:00
virtual-machine releases use virtualisation.incus.package to find where the agent-loader configuration is defined. In practice it's likely not a problem, but when marking lts v6 on 25.11 as vulnerable it exposed the mismatch. For correctness we should ensure the VM tests are using the agent loader from the relevant incus package.
303 lines
14 KiB
Nix
303 lines
14 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
jsonFormat = pkgs.formats.json { };
|
|
cfg = config.tests.incus;
|
|
in
|
|
{
|
|
options.tests.incus = {
|
|
name = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "name appended to test";
|
|
};
|
|
|
|
package = lib.mkPackageOption pkgs "incus" { };
|
|
|
|
preseed = lib.mkOption {
|
|
description = "configuration provided to incus preseed. https://linuxcontainers.org/incus/docs/main/howto/initialize/#non-interactive-configuration";
|
|
type = lib.types.submodule {
|
|
freeformType = jsonFormat.type;
|
|
};
|
|
};
|
|
|
|
instances = lib.mkOption {
|
|
type = lib.types.attrsOf (
|
|
lib.types.submodule (
|
|
{ name, config, ... }:
|
|
{
|
|
options = {
|
|
name = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = name;
|
|
};
|
|
|
|
type = lib.mkOption {
|
|
type = lib.types.enum [
|
|
"container"
|
|
"virtual-machine"
|
|
];
|
|
|
|
};
|
|
|
|
imageAlias = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "name of image when imported";
|
|
default = "nixos/${name}/${config.type}";
|
|
};
|
|
|
|
nixosConfig = lib.mkOption {
|
|
type = lib.types.attrsOf lib.types.anything;
|
|
default = { };
|
|
};
|
|
|
|
incusConfig = lib.mkOption {
|
|
type = lib.types.submodule {
|
|
freeformType = jsonFormat.type;
|
|
};
|
|
description = "incus configuration provided at launch";
|
|
default = { };
|
|
};
|
|
|
|
copyChannel = lib.mkEnableOption ''
|
|
copy channel in test image. disabled by default as it forces image
|
|
rebuilds excessively. enable to validate channel things.
|
|
'';
|
|
|
|
testScript = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "final script provided to test runner";
|
|
readOnly = true;
|
|
};
|
|
};
|
|
config =
|
|
let
|
|
releases = import ../../release.nix {
|
|
configuration = lib.recursiveUpdate config.nixosConfig {
|
|
virtualisation.incus = {
|
|
inherit (cfg) package;
|
|
};
|
|
};
|
|
};
|
|
|
|
images = {
|
|
container = {
|
|
metadata =
|
|
releases.incusContainerMeta.${pkgs.stdenv.hostPlatform.system}
|
|
+ "/tarball/nixos-image-lxc-*-${pkgs.stdenv.hostPlatform.system}.tar.xz";
|
|
|
|
root =
|
|
releases.incusContainerImage.${pkgs.stdenv.hostPlatform.system}
|
|
+ "/nixos-lxc-image-${pkgs.stdenv.hostPlatform.system}.squashfs";
|
|
};
|
|
|
|
virtual-machine = {
|
|
metadata = releases.incusVirtualMachineImageMeta.${pkgs.stdenv.hostPlatform.system} + "/*/*.tar.xz";
|
|
root = releases.incusVirtualMachineImage.${pkgs.stdenv.hostPlatform.system} + "/nixos.qcow2";
|
|
};
|
|
};
|
|
|
|
root = images.${config.type}.root;
|
|
metadata = images.${config.type}.metadata;
|
|
|
|
image_id = "${config.type}/${config.name}";
|
|
in
|
|
{
|
|
incusConfig = lib.optionalAttrs (config.type == "virtual-machine") {
|
|
config."security.secureboot" = false;
|
|
};
|
|
|
|
nixosConfig = {
|
|
# Building documentation makes the test unnecessarily take a longer time:
|
|
documentation.enable = lib.mkForce false;
|
|
documentation.nixos.enable = lib.mkForce false;
|
|
# including a channel forces images to be rebuilt on any changes
|
|
system.installer.channel.enable = lib.mkForce config.copyChannel;
|
|
|
|
environment.etc."nix/registry.json".text = lib.mkForce "{}";
|
|
|
|
# Arbitrary sysctl setting changed from nixos default
|
|
# used for verifying `distrobuilder.generator` properly allows
|
|
# for containers to modify sysctl
|
|
boot.kernel.sysctl."net.ipv4.ip_forward" = "1";
|
|
};
|
|
|
|
testScript = # python
|
|
''
|
|
with subtest("[${image_id}] image can be imported"):
|
|
server.succeed("incus image import ${metadata} ${root} --alias ${config.imageAlias}")
|
|
|
|
with subtest("[${image_id}] can be launched and managed"):
|
|
instance_name = server.succeed("incus launch ${config.imageAlias}${
|
|
lib.optionalString (config.type == "virtual-machine") " --vm"
|
|
} --quiet < ${jsonFormat.generate "${config.name}.json" config.incusConfig}").split(":")[1].strip()
|
|
server.wait_for_instance(instance_name)
|
|
|
|
with subtest("[${image_id}] can successfully restart"):
|
|
server.succeed(f"incus restart {instance_name}")
|
|
server.wait_for_instance(instance_name)
|
|
|
|
with subtest("[${image_id}] remains running when softDaemonRestart is enabled and service is stopped"):
|
|
pid = server.succeed(f"incus info {instance_name} | grep 'PID'").split(":")[1].strip()
|
|
server.succeed(f"ps {pid}")
|
|
server.succeed("systemctl stop incus")
|
|
server.succeed(f"ps {pid}")
|
|
server.succeed("systemctl start incus")
|
|
|
|
with subtest("[${image_id}] CPU limits can be managed"):
|
|
server.set_instance_config(instance_name, "limits.cpu=1", restart=True)
|
|
server.wait_instance_exec_success(instance_name, "nproc | grep '^1$'", timeout=90)
|
|
|
|
with subtest("[${image_id}] CPU limits can be hotplug changed"):
|
|
server.set_instance_config(instance_name, "limits.cpu=2")
|
|
server.wait_instance_exec_success(instance_name, "nproc | grep '^2$'", timeout=90)
|
|
|
|
with subtest("[${image_id}] exec has a valid path"):
|
|
server.succeed(f"incus exec {instance_name} -- bash -c 'true'")
|
|
|
|
with subtest("[${image_id}] software tpm can be configured"):
|
|
# this can be hot added to containers, but stopping for vm
|
|
server.succeed(f"incus stop {instance_name}")
|
|
server.succeed(f"incus config device add {instance_name} vtpm tpm path=/dev/tpm0 pathrm=/dev/tpmrm0")
|
|
server.succeed(f"incus start {instance_name}")
|
|
server.wait_for_instance(instance_name)
|
|
|
|
server.succeed(f"incus exec {instance_name} -- test -e /dev/tpm0")
|
|
server.succeed(f"incus exec {instance_name} -- test -e /dev/tpmrm0")
|
|
|
|
with subtest("[${image_id}] default configuration.nix is created on first boot"):
|
|
server.succeed(f"incus exec {instance_name} -- test -f /etc/nixos/configuration.nix")
|
|
server.succeed(f"incus exec {instance_name} -- grep -q 'default incus configuration' /etc/nixos/configuration.nix")
|
|
|
|
with subtest("[${image_id}] configuration.nix create service does not overwrite existing config"):
|
|
server.succeed(f"incus exec {instance_name} -- systemctl restart incus-create-nixos-config.service")
|
|
status = server.succeed(
|
|
f"incus exec {instance_name} -- systemctl show -p ActiveState incus-create-nixos-config.service"
|
|
).strip()
|
|
assert "inactive" in status, f"Expected inactive (ConditionPathExists should prevent start), got {status}"
|
|
''
|
|
#
|
|
# container specific
|
|
#
|
|
+ lib.optionalString (config.type == "container") (
|
|
# python
|
|
''
|
|
with subtest("[${image_id}] switch-to-configuration updates /sbin/init via installBootLoader"):
|
|
# Remove /sbin/init so we can verify installBootLoader recreates it
|
|
server.succeed(f"incus exec {instance_name} -- rm -f /sbin/init")
|
|
server.fail(f"incus exec {instance_name} -- test -e /sbin/init")
|
|
|
|
server.succeed(
|
|
f"incus exec {instance_name} -- /run/current-system/bin/switch-to-configuration switch"
|
|
)
|
|
|
|
# Verify installBootLoader recreated /sbin/init pointing to the system's init
|
|
server.succeed(f"incus exec {instance_name} -- test -x /sbin/init")
|
|
target = server.succeed(f"incus exec {instance_name} -- readlink -f /sbin/init").strip()
|
|
current = server.succeed(f"incus exec {instance_name} -- readlink -f /run/current-system/init").strip()
|
|
assert target == current, f"/sbin/init -> {target}, expected {current}"
|
|
|
|
# TODO troubleshoot VM hot memory resizing which was introduced in 6.12
|
|
with subtest("[${image_id}] memory limits can be hotplug changed"):
|
|
server.set_instance_config(instance_name, "limits.memory=512MB")
|
|
# can't use lsmem since it sees the host's memory size
|
|
server.wait_instance_exec_success(instance_name, "grep 'MemTotal:[[:space:]]*500000 kB' /proc/meminfo", timeout=1)
|
|
|
|
# verify the patched container systemd generator from `pkgs.distrobuilder.generator`
|
|
with subtest("[${image_id}] lxc-generator compatibility"):
|
|
with subtest("[${image_id}] lxc-container generator configures plain container"):
|
|
# default container is plain
|
|
server.succeed(f"incus exec {instance_name} test -- -e /run/systemd/system/service.d/zzz-lxc-service.conf")
|
|
|
|
server.check_instance_sysctl(instance_name)
|
|
|
|
with subtest("[${image_id}] lxc-container generator configures nested container"):
|
|
server.set_instance_config(instance_name, "security.nesting=true", restart=True)
|
|
|
|
server.fail(f"incus exec {instance_name} test -- -e /run/systemd/system/service.d/zzz-lxc-service.conf")
|
|
target = server.succeed(f"incus exec {instance_name} readlink -- -f /run/systemd/system/systemd-binfmt.service").strip()
|
|
assert target == "/dev/null", "lxc generator did not correctly mask /run/systemd/system/systemd-binfmt.service"
|
|
|
|
server.check_instance_sysctl(instance_name)
|
|
|
|
with subtest("[${image_id}] lxcfs"):
|
|
with subtest("[${image_id}] mounts lxcfs overlays"):
|
|
server.succeed(f"incus exec {instance_name} mount | grep 'lxcfs on /proc/cpuinfo type fuse.lxcfs'")
|
|
server.succeed(f"incus exec {instance_name} mount | grep 'lxcfs on /proc/meminfo type fuse.lxcfs'")
|
|
|
|
with subtest("[${image_id}] supports per-instance lxcfs"):
|
|
server.succeed(f"incus stop {instance_name}")
|
|
server.fail(f"pgrep -a lxcfs | grep 'incus/devices/{instance_name}/lxcfs'")
|
|
|
|
server.succeed("incus config set instances.lxcfs.per_instance=true")
|
|
|
|
server.succeed(f"incus start {instance_name}")
|
|
server.wait_for_instance(instance_name)
|
|
server.succeed(f"pgrep -a lxcfs | grep 'incus/devices/{instance_name}/lxcfs'")
|
|
''
|
|
+ lib.optionalString (config.copyChannel) ''
|
|
with subtest("[${image_id}] channel copied correctly"):
|
|
server.succeed(f"incus exec {instance_name} -- systemctl status nix-channel-init.service")
|
|
''
|
|
)
|
|
#
|
|
# virtual-machine specific
|
|
#
|
|
+
|
|
lib.optionalString (config.type == "virtual-machine")
|
|
# python
|
|
''
|
|
with subtest("[${image_id}] memory limits can be managed"):
|
|
server.set_instance_config(instance_name, "limits.memory=384MB", restart=True)
|
|
lsmem = json.loads(server.instance_succeed(instance_name, "lsmem --json"))
|
|
memsize = lsmem["memory"][0]["size"]
|
|
assert memsize == "384M", f"failed to manage memory limit. {memsize} != 384M"
|
|
|
|
with subtest("[${image_id}] incus-agent is started"):
|
|
server.succeed(f"incus exec {instance_name} systemctl is-active incus-agent")
|
|
''
|
|
|
|
+
|
|
#
|
|
# finalize
|
|
#
|
|
# python
|
|
''
|
|
# this will leave the instances stopped
|
|
with subtest("[${image_id}] stop with incus-startup.service"):
|
|
pid = server.succeed(f"incus info {instance_name} | grep 'PID'").split(":")[1].strip()
|
|
server.succeed(f"ps {pid}")
|
|
server.succeed("systemctl stop incus-startup.service")
|
|
server.wait_until_fails(f"ps {pid}", timeout=120)
|
|
server.succeed("systemctl start incus-startup.service")
|
|
|
|
'';
|
|
|
|
};
|
|
}
|
|
)
|
|
);
|
|
description = "";
|
|
default = { };
|
|
};
|
|
|
|
appArmor = lib.mkEnableOption "AppArmor during tests";
|
|
|
|
feature.user = lib.mkEnableOption "Validate incus user access feature";
|
|
|
|
network.ovs = lib.mkEnableOption "Validate OVS network integration";
|
|
|
|
storage = {
|
|
lvm = lib.mkEnableOption "Validate LVM storage integration";
|
|
zfs = lib.mkEnableOption "Validate ZFS storage integration";
|
|
};
|
|
};
|
|
|
|
config = {
|
|
tests.incus = { };
|
|
};
|
|
}
|