1import ../make-test-python.nix (
2 {
3 pkgs,
4 lib,
5
6 lts ? true,
7
8 allTests ? false,
9
10 appArmor ? false,
11 featureUser ? allTests,
12 initLegacy ? true,
13 initSystemd ? true,
14 instanceContainer ? allTests,
15 instanceVm ? allTests,
16 networkOvs ? allTests,
17 storageLvm ? allTests,
18 storageZfs ? allTests,
19 ...
20 }:
21
22 let
23 releases =
24 init:
25 import ../../release.nix {
26 configuration = {
27 # Building documentation makes the test unnecessarily take a longer time:
28 documentation.enable = lib.mkForce false;
29
30 boot.initrd.systemd.enable = init == "systemd";
31
32 # Arbitrary sysctl modification to ensure containers can update sysctl
33 boot.kernel.sysctl."net.ipv4.ip_forward" = "1";
34 };
35 };
36
37 images = init: {
38 container = {
39 metadata =
40 (releases init).incusContainerMeta.${pkgs.stdenv.hostPlatform.system}
41 + "/tarball/nixos-image-lxc-*-${pkgs.stdenv.hostPlatform.system}.tar.xz";
42
43 rootfs =
44 (releases init).incusContainerImage.${pkgs.stdenv.hostPlatform.system}
45 + "/nixos-lxc-image-${pkgs.stdenv.hostPlatform.system}.squashfs";
46 };
47
48 virtual-machine = {
49 metadata =
50 (releases init).incusVirtualMachineImageMeta.${pkgs.stdenv.hostPlatform.system} + "/*/*.tar.xz";
51 disk = (releases init).incusVirtualMachineImage.${pkgs.stdenv.hostPlatform.system} + "/nixos.qcow2";
52 };
53 };
54
55 initVariants = lib.optionals initLegacy [ "legacy" ] ++ lib.optionals initSystemd [ "systemd" ];
56
57 canTestVm = instanceVm && pkgs.stdenv.isLinux && pkgs.stdenv.isx86_64;
58 in
59 {
60 name = "incus" + lib.optionalString lts "-lts";
61
62 meta = {
63 maintainers = lib.teams.lxc.members;
64 };
65
66 nodes.machine = {
67 virtualisation = {
68 cores = 2;
69 memorySize = 2048;
70 diskSize = 12 * 1024;
71 emptyDiskImages = [
72 # vdb for zfs
73 2048
74 # vdc for lvm
75 2048
76 ];
77
78 incus = {
79 enable = true;
80 package = if lts then pkgs.incus-lts else pkgs.incus;
81
82 preseed = {
83 networks = [
84 {
85 name = "incusbr0";
86 type = "bridge";
87 config = {
88 "ipv4.address" = "10.0.10.1/24";
89 "ipv4.nat" = "true";
90 };
91 }
92 ]
93 ++ lib.optionals networkOvs [
94 {
95 name = "ovsbr0";
96 type = "bridge";
97 config = {
98 "bridge.driver" = "openvswitch";
99 "ipv4.address" = "10.0.20.1/24";
100 "ipv4.nat" = "true";
101 };
102 }
103 ];
104 profiles = [
105 {
106 name = "default";
107 devices = {
108 eth0 = {
109 name = "eth0";
110 network = "incusbr0";
111 type = "nic";
112 };
113 root = {
114 path = "/";
115 pool = "default";
116 size = "35GiB";
117 type = "disk";
118 };
119 };
120 }
121 ];
122 storage_pools = [
123 {
124 name = "default";
125 driver = "dir";
126 }
127 ];
128 };
129 };
130
131 vswitch.enable = networkOvs;
132 };
133
134 boot.supportedFilesystems = lib.optionals storageZfs [ "zfs" ];
135 boot.zfs.forceImportRoot = false;
136
137 environment.systemPackages = [ pkgs.parted ];
138
139 networking.hostId = "01234567";
140 networking.firewall.trustedInterfaces = [ "incusbr0" ];
141
142 security.apparmor.enable = appArmor;
143 services.dbus.apparmor = (if appArmor then "enabled" else "disabled");
144
145 services.lvm = {
146 boot.thin.enable = storageLvm;
147 dmeventd.enable = storageLvm;
148 };
149
150 networking.nftables.enable = true;
151
152 users.users.testuser = {
153 isNormalUser = true;
154 shell = pkgs.bashInteractive;
155 group = "incus";
156 uid = 1000;
157 };
158 };
159
160 testScript = # python
161 ''
162 import json
163
164 def wait_for_instance(name: str, project: str = "default"):
165 machine.wait_until_succeeds(f"incus exec {name} --disable-stdin --force-interactive --project {project} -- /run/current-system/sw/bin/systemctl is-system-running")
166
167
168 def wait_incus_exec_success(name: str, command: str, timeout: int = 900, project: str = "default"):
169 def check_command(_) -> bool:
170 status, _ = machine.execute(f"incus exec {name} --disable-stdin --force-interactive --project {project} -- {command}")
171 return status == 0
172
173 with machine.nested(f"Waiting for successful exec: {command}"):
174 retry(check_command, timeout)
175
176
177 def set_config(name: str, config: str, restart: bool = False, unset: bool = False):
178 if restart:
179 machine.succeed(f"incus stop {name}")
180
181 if unset:
182 machine.succeed(f"incus config unset {name} {config}")
183 else:
184 machine.succeed(f"incus config set {name} {config}")
185
186 if restart:
187 machine.succeed(f"incus start {name}")
188 wait_for_instance(name)
189 else:
190 # give a moment to settle
191 machine.sleep(1)
192
193
194 def cleanup():
195 # avoid conflict between preseed and cleanup operations
196 machine.execute("systemctl kill incus-preseed.service")
197
198 instances = json.loads(machine.succeed("incus list --format json --all-projects"))
199 with subtest("Stopping all running instances"):
200 for instance in [a for a in instances if a['status'] == 'Running']:
201 machine.execute(f"incus stop --force {instance['name']} --project {instance['project']}")
202 machine.execute(f"incus delete --force {instance['name']} --project {instance['project']}")
203
204
205 def check_sysctl(name: str):
206 with subtest("systemd sysctl settings are applied"):
207 machine.succeed(f"incus exec {name} -- systemctl status systemd-sysctl")
208 sysctl = machine.succeed(f"incus exec {name} -- sysctl net.ipv4.ip_forward").strip().split(" ")[-1]
209 assert "1" == sysctl, f"systemd-sysctl configuration not correctly applied, {sysctl} != 1"
210
211
212 with subtest("Wait for startup"):
213 machine.wait_for_unit("incus.service")
214 machine.wait_for_unit("incus-preseed.service")
215
216
217 with subtest("Verify preseed resources created"):
218 machine.succeed("incus profile show default")
219 machine.succeed("incus network info incusbr0")
220 machine.succeed("incus storage show default")
221
222 ''
223 + lib.optionalString appArmor ''
224 with subtest("Verify AppArmor service is started without issue"):
225 # restart AppArmor service since the Incus AppArmor folders are
226 # created after AA service is started
227 machine.systemctl("restart apparmor.service")
228 machine.succeed("systemctl --no-pager -l status apparmor.service")
229 machine.wait_for_unit("apparmor.service")
230 ''
231 + lib.optionalString instanceContainer (
232 lib.foldl (
233 acc: variant:
234 acc
235 # python
236 + ''
237 metadata = "${(images variant).container.metadata}"
238 rootfs = "${(images variant).container.rootfs}"
239 alias = "nixos/container/${variant}"
240 variant = "${variant}"
241
242 with subtest("container image can be imported"):
243 machine.succeed(f"incus image import {metadata} {rootfs} --alias {alias}")
244
245
246 with subtest("container can be launched and managed"):
247 machine.succeed(f"incus launch {alias} container-{variant}1")
248 wait_for_instance(f"container-{variant}1")
249
250
251 with subtest("container mounts lxcfs overlays"):
252 machine.succeed(f"incus exec container-{variant}1 mount | grep 'lxcfs on /proc/cpuinfo type fuse.lxcfs'")
253 machine.succeed(f"incus exec container-{variant}1 mount | grep 'lxcfs on /proc/meminfo type fuse.lxcfs'")
254
255
256 with subtest("container CPU limits can be managed"):
257 set_config(f"container-{variant}1", "limits.cpu 1", restart=True)
258 wait_incus_exec_success(f"container-{variant}1", "nproc | grep '^1$'", timeout=90)
259
260
261 with subtest("container CPU limits can be hotplug changed"):
262 set_config(f"container-{variant}1", "limits.cpu 2")
263 wait_incus_exec_success(f"container-{variant}1", "nproc | grep '^2$'", timeout=90)
264
265
266 with subtest("container memory limits can be managed"):
267 set_config(f"container-{variant}1", "limits.memory 128MB", restart=True)
268 wait_incus_exec_success(f"container-{variant}1", "grep 'MemTotal:[[:space:]]*125000 kB' /proc/meminfo", timeout=90)
269
270
271 with subtest("container memory limits can be hotplug changed"):
272 set_config(f"container-{variant}1", "limits.memory 256MB")
273 wait_incus_exec_success(f"container-{variant}1", "grep 'MemTotal:[[:space:]]*250000 kB' /proc/meminfo", timeout=90)
274
275
276 with subtest("container software tpm can be configured"):
277 machine.succeed(f"incus config device add container-{variant}1 vtpm tpm path=/dev/tpm0 pathrm=/dev/tpmrm0")
278 machine.succeed(f"incus exec container-{variant}1 -- test -e /dev/tpm0")
279 machine.succeed(f"incus exec container-{variant}1 -- test -e /dev/tpmrm0")
280 machine.succeed(f"incus config device remove container-{variant}1 vtpm")
281 machine.fail(f"incus exec container-{variant}1 -- test -e /dev/tpm0")
282
283
284 with subtest("container lxc-generator compatibility"):
285 with subtest("lxc-container generator configures plain container"):
286 # default container is plain
287 machine.succeed(f"incus exec container-{variant}1 test -- -e /run/systemd/system/service.d/zzz-lxc-service.conf")
288
289 check_sysctl(f"container-{variant}1")
290
291 with subtest("lxc-container generator configures nested container"):
292 set_config(f"container-{variant}1", "security.nesting=true", restart=True)
293
294 machine.fail(f"incus exec container-{variant}1 test -- -e /run/systemd/system/service.d/zzz-lxc-service.conf")
295 target = machine.succeed(f"incus exec container-{variant}1 readlink -- -f /run/systemd/system/systemd-binfmt.service").strip()
296 assert target == "/dev/null", "lxc generator did not correctly mask /run/systemd/system/systemd-binfmt.service"
297
298 check_sysctl(f"container-{variant}1")
299
300 with subtest("lxc-container generator configures privileged container"):
301 # Create a new instance for a clean state
302 machine.succeed(f"incus launch {alias} container-{variant}2")
303 wait_for_instance(f"container-{variant}2")
304
305 machine.succeed(f"incus exec container-{variant}2 test -- -e /run/systemd/system/service.d/zzz-lxc-service.conf")
306
307 check_sysctl(f"container-{variant}2")
308
309 with subtest("container supports per-instance lxcfs"):
310 machine.succeed(f"incus stop container-{variant}1")
311 machine.fail(f"pgrep -a lxcfs | grep 'incus/devices/container-{variant}1/lxcfs'")
312
313 machine.succeed("incus config set instances.lxcfs.per_instance=true")
314
315 machine.succeed(f"incus start container-{variant}1")
316 wait_for_instance(f"container-{variant}1")
317 machine.succeed(f"pgrep -a lxcfs | grep 'incus/devices/container-{variant}1/lxcfs'")
318
319
320 with subtest("container can successfully restart"):
321 machine.succeed(f"incus restart container-{variant}1")
322 wait_for_instance(f"container-{variant}1")
323
324
325 with subtest("container remains running when softDaemonRestart is enabled and service is stopped"):
326 pid = machine.succeed(f"incus info container-{variant}1 | grep 'PID'").split(":")[1].strip()
327 machine.succeed(f"ps {pid}")
328 machine.succeed("systemctl stop incus")
329 machine.succeed(f"ps {pid}")
330 machine.succeed("systemctl start incus")
331
332 with subtest("containers stop with incus-startup.service"):
333 pid = machine.succeed(f"incus info container-{variant}1 | grep 'PID'").split(":")[1].strip()
334 machine.succeed(f"ps {pid}")
335 machine.succeed("systemctl stop incus-startup.service")
336 machine.wait_until_fails(f"ps {pid}", timeout=120)
337 machine.succeed("systemctl start incus-startup.service")
338
339
340 cleanup()
341 ''
342 ) "" initVariants
343 )
344 + lib.optionalString canTestVm (
345 (lib.foldl (
346 acc: variant:
347 acc
348 # python
349 + ''
350 metadata = "${(images variant).virtual-machine.metadata}"
351 disk = "${(images variant).virtual-machine.disk}"
352 alias = "nixos/virtual-machine/${variant}"
353 variant = "${variant}"
354
355 with subtest("virtual-machine image can be imported"):
356 machine.succeed(f"incus image import {metadata} {disk} --alias {alias}")
357
358
359 with subtest("virtual-machine can be created"):
360 machine.succeed(f"incus create {alias} vm-{variant}1 --vm --config limits.memory=512MB --config security.secureboot=false")
361
362
363 with subtest("virtual-machine software tpm can be configured"):
364 machine.succeed(f"incus config device add vm-{variant}1 vtpm tpm path=/dev/tpm0")
365
366
367 with subtest("virtual-machine can be launched and become available"):
368 machine.succeed(f"incus start vm-{variant}1")
369 wait_for_instance(f"vm-{variant}1")
370
371
372 with subtest("virtual-machine incus-agent is started"):
373 machine.succeed(f"incus exec vm-{variant}1 systemctl is-active incus-agent")
374
375
376 with subtest("virtual-machine incus-agent has a valid path"):
377 machine.succeed(f"incus exec vm-{variant}1 -- bash -c 'true'")
378
379
380 with subtest("virtual-machine CPU limits can be managed"):
381 set_config(f"vm-{variant}1", "limits.cpu 1", restart=True)
382 wait_incus_exec_success(f"vm-{variant}1", "nproc | grep '^1$'", timeout=90)
383
384
385 with subtest("virtual-machine CPU limits can be hotplug changed"):
386 set_config(f"vm-{variant}1", "limits.cpu 2")
387 wait_incus_exec_success(f"vm-{variant}1", "nproc | grep '^2$'", timeout=90)
388
389
390 with subtest("virtual-machine can successfully restart"):
391 machine.succeed(f"incus restart vm-{variant}1")
392 wait_for_instance(f"vm-{variant}1")
393
394
395 with subtest("virtual-machine remains running when softDaemonRestart is enabled and service is stopped"):
396 pid = machine.succeed(f"incus info vm-{variant}1 | grep 'PID'").split(":")[1].strip()
397 machine.succeed(f"ps {pid}")
398 machine.succeed("systemctl stop incus")
399 machine.succeed(f"ps {pid}")
400 machine.succeed("systemctl start incus")
401
402
403 with subtest("virtual-machines stop with incus-startup.service"):
404 pid = machine.succeed(f"incus info vm-{variant}1 | grep 'PID'").split(":")[1].strip()
405 machine.succeed(f"ps {pid}")
406 machine.succeed("systemctl stop incus-startup.service")
407 machine.wait_until_fails(f"ps {pid}", timeout=120)
408 machine.succeed("systemctl start incus-startup.service")
409
410
411 cleanup()
412 ''
413 ) "" initVariants)
414 +
415 # python
416 ''
417 with subtest("virtual-machine can launch CSM (BIOS)"):
418 machine.succeed("incus init csm --vm --empty -c security.csm=true -c security.secureboot=false")
419 machine.succeed("incus start csm")
420
421
422 cleanup()
423 ''
424 )
425 +
426 lib.optionalString featureUser # python
427 ''
428 with subtest("incus-user allows restricted access for users"):
429 machine.fail("incus project show user-1000")
430 machine.succeed("su - testuser bash -c 'incus list'")
431 # a project is created dynamically for the user
432 machine.succeed("incus project show user-1000")
433 # users shouldn't be able to list storage pools
434 machine.fail("su - testuser bash -c 'incus storage list'")
435
436
437 with subtest("incus-user allows users to launch instances"):
438 machine.succeed("su - testuser bash -c 'incus image import ${(images "systemd").container.metadata} ${(images "systemd").container.rootfs} --alias nixos'")
439 machine.succeed("su - testuser bash -c 'incus launch nixos instance2'")
440 wait_for_instance("instance2", "user-1000")
441
442 cleanup()
443 ''
444 +
445 lib.optionalString networkOvs # python
446 ''
447 with subtest("Verify openvswitch bridge"):
448 machine.succeed("incus network info ovsbr0")
449
450
451 with subtest("Verify openvswitch bridge"):
452 machine.succeed("ovs-vsctl br-exists ovsbr0")
453 ''
454
455 +
456 lib.optionalString storageZfs # python
457 ''
458 with subtest("Verify zfs pool created and usable"):
459 machine.succeed(
460 "zpool status",
461 "parted --script /dev/vdb mklabel gpt",
462 "zpool create zfs_pool /dev/vdb",
463 )
464
465 machine.succeed("incus storage create zfs_pool zfs source=zfs_pool/incus")
466 machine.succeed("zfs list zfs_pool/incus")
467
468 machine.succeed("incus storage volume create zfs_pool test_fs --type filesystem")
469 machine.succeed("incus storage volume create zfs_pool test_vol --type block")
470
471 machine.succeed("incus storage show zfs_pool")
472 machine.succeed("incus storage volume list zfs_pool")
473 machine.succeed("incus storage volume show zfs_pool test_fs")
474 machine.succeed("incus storage volume show zfs_pool test_vol")
475
476 machine.succeed("incus create zfs1 --empty --storage zfs_pool")
477 machine.succeed("incus list zfs1")
478 ''
479
480 +
481 lib.optionalString storageLvm # python
482 ''
483 with subtest("Verify lvm pool created and usable"):
484 machine.succeed("incus storage create lvm_pool lvm source=/dev/vdc lvm.vg_name=incus_pool")
485 machine.succeed("vgs incus_pool")
486
487 machine.succeed("incus storage volume create lvm_pool test_fs --type filesystem")
488 machine.succeed("incus storage volume create lvm_pool test_vol --type block")
489
490 machine.succeed("incus storage show lvm_pool")
491
492 machine.succeed("incus storage volume list lvm_pool")
493 machine.succeed("incus storage volume show lvm_pool test_fs")
494 machine.succeed("incus storage volume show lvm_pool test_vol")
495
496 machine.succeed("incus create lvm1 --empty --storage lvm_pool")
497 machine.succeed("incus list lvm1")
498 '';
499 }
500)