at 25.11-pre 15 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 cfg = config.virtualisation.incus; 10 preseedFormat = pkgs.formats.yaml { }; 11 12 nvidiaEnabled = (lib.elem "nvidia" config.services.xserver.videoDrivers); 13 14 serverBinPath = ''/run/wrappers/bin:${pkgs.qemu_kvm}/libexec:${ 15 lib.makeBinPath ( 16 with pkgs; 17 [ 18 cfg.package 19 20 acl 21 attr 22 bash 23 btrfs-progs 24 cdrkit 25 coreutils 26 criu 27 dnsmasq 28 e2fsprogs 29 findutils 30 getent 31 gawk 32 gnugrep 33 gnused 34 gnutar 35 gptfdisk 36 gzip 37 iproute2 38 iptables 39 iw 40 kmod 41 libxfs 42 lvm2 43 lxcfs 44 minio 45 minio-client 46 nftables 47 qemu-utils 48 qemu_kvm 49 rsync 50 squashfs-tools-ng 51 squashfsTools 52 sshfs 53 swtpm 54 systemd 55 thin-provisioning-tools 56 util-linux 57 virtiofsd 58 xdelta 59 xz 60 ] 61 ++ lib.optionals (lib.versionAtLeast cfg.package.version "6.3.0") [ 62 skopeo 63 umoci 64 ] 65 ++ lib.optionals (lib.versionAtLeast cfg.package.version "6.11.0") [ 66 lego 67 ] 68 ++ lib.optionals config.security.apparmor.enable [ 69 apparmor-bin-utils 70 71 (writeShellScriptBin "apparmor_parser" '' 72 exec '${apparmor-parser}/bin/apparmor_parser' -I '${apparmor-profiles}/etc/apparmor.d' "$@" 73 '') 74 ] 75 ++ lib.optionals config.services.ceph.client.enable [ ceph-client ] 76 ++ lib.optionals config.virtualisation.vswitch.enable [ config.virtualisation.vswitch.package ] 77 ++ lib.optionals config.boot.zfs.enabled [ 78 config.boot.zfs.package 79 "${config.boot.zfs.package}/lib/udev" 80 ] 81 ++ lib.optionals nvidiaEnabled [ 82 libnvidia-container 83 ] 84 ) 85 }''; 86 87 # https://github.com/lxc/incus/blob/cff35a29ee3d7a2af1f937cbb6cf23776941854b/internal/server/instance/drivers/driver_qemu.go#L123 88 OVMF2MB = pkgs.OVMF.override { 89 secureBoot = true; 90 fdSize2MB = true; 91 }; 92 ovmf-prefix = if pkgs.stdenv.hostPlatform.isAarch64 then "AAVMF" else "OVMF"; 93 ovmf = pkgs.linkFarm "incus-ovmf" ( 94 [ 95 # 2MB must remain the default or existing VMs will fail to boot. New VMs will prefer 4MB 96 { 97 name = "OVMF_CODE.fd"; 98 path = "${OVMF2MB.fd}/FV/${ovmf-prefix}_CODE.fd"; 99 } 100 { 101 name = "OVMF_VARS.fd"; 102 path = "${OVMF2MB.fd}/FV/${ovmf-prefix}_VARS.fd"; 103 } 104 { 105 name = "OVMF_VARS.ms.fd"; 106 path = "${OVMF2MB.fd}/FV/${ovmf-prefix}_VARS.fd"; 107 } 108 109 { 110 name = "OVMF_CODE.4MB.fd"; 111 path = "${pkgs.OVMFFull.fd}/FV/${ovmf-prefix}_CODE.fd"; 112 } 113 { 114 name = "OVMF_VARS.4MB.fd"; 115 path = "${pkgs.OVMFFull.fd}/FV/${ovmf-prefix}_VARS.fd"; 116 } 117 { 118 name = "OVMF_VARS.4MB.ms.fd"; 119 path = "${pkgs.OVMFFull.fd}/FV/${ovmf-prefix}_VARS.fd"; 120 } 121 ] 122 ++ lib.optionals pkgs.stdenv.hostPlatform.isx86_64 [ 123 { 124 name = "seabios.bin"; 125 path = "${pkgs.seabios-qemu}/share/seabios/bios.bin"; 126 } 127 ] 128 ); 129 130 environment = lib.mkMerge [ 131 { 132 INCUS_DOCUMENTATION = "${cfg.package.doc}/html"; 133 INCUS_EDK2_PATH = ovmf; 134 INCUS_LXC_HOOK = "${cfg.lxcPackage}/share/lxc/hooks"; 135 INCUS_LXC_TEMPLATE_CONFIG = "${pkgs.lxcfs}/share/lxc/config"; 136 INCUS_USBIDS_PATH = "${pkgs.hwdata}/share/hwdata/usb.ids"; 137 PATH = lib.mkForce serverBinPath; 138 } 139 (lib.mkIf (cfg.ui.enable) { "INCUS_UI" = cfg.ui.package; }) 140 ]; 141 142 incus-startup = pkgs.writeShellScript "incus-startup" '' 143 case "$1" in 144 start) 145 systemctl is-active incus.service -q && exit 0 146 exec incusd activateifneeded 147 ;; 148 149 stop) 150 systemctl is-active incus.service -q || exit 0 151 exec incusd shutdown 152 ;; 153 154 *) 155 echo "unknown argument \`$1'" >&2 156 exit 1 157 ;; 158 esac 159 160 exit 0 161 ''; 162in 163{ 164 meta = { 165 maintainers = lib.teams.lxc.members; 166 }; 167 168 options = { 169 virtualisation.incus = { 170 enable = lib.mkEnableOption '' 171 incusd, a daemon that manages containers and virtual machines. 172 173 Users in the "incus-admin" group can interact with 174 the daemon (e.g. to start or stop containers) using the 175 {command}`incus` command line tool, among others. 176 Users in the "incus" group can also interact with 177 the daemon, but with lower permissions 178 (i.e. administrative operations are forbidden). 179 ''; 180 181 package = lib.mkPackageOption pkgs "incus-lts" { }; 182 183 lxcPackage = lib.mkOption { 184 type = lib.types.package; 185 default = config.virtualisation.lxc.package; 186 defaultText = lib.literalExpression "config.virtualisation.lxc.package"; 187 description = "The lxc package to use."; 188 }; 189 190 clientPackage = lib.mkOption { 191 type = lib.types.package; 192 default = cfg.package.client; 193 defaultText = lib.literalExpression "config.virtualisation.incus.package.client"; 194 description = "The incus client package to use. This package is added to PATH."; 195 }; 196 197 softDaemonRestart = lib.mkOption { 198 type = lib.types.bool; 199 default = true; 200 description = '' 201 Allow for incus.service to be stopped without affecting running instances. 202 ''; 203 }; 204 205 preseed = lib.mkOption { 206 type = lib.types.nullOr (lib.types.submodule { freeformType = preseedFormat.type; }); 207 208 default = null; 209 210 description = '' 211 Configuration for Incus preseed, see 212 <https://linuxcontainers.org/incus/docs/main/howto/initialize/#non-interactive-configuration> 213 for supported values. 214 215 Changes to this will be re-applied to Incus which will overwrite existing entities or create missing ones, 216 but entities will *not* be removed by preseed. 217 ''; 218 219 example = { 220 networks = [ 221 { 222 name = "incusbr0"; 223 type = "bridge"; 224 config = { 225 "ipv4.address" = "10.0.100.1/24"; 226 "ipv4.nat" = "true"; 227 }; 228 } 229 ]; 230 profiles = [ 231 { 232 name = "default"; 233 devices = { 234 eth0 = { 235 name = "eth0"; 236 network = "incusbr0"; 237 type = "nic"; 238 }; 239 root = { 240 path = "/"; 241 pool = "default"; 242 size = "35GiB"; 243 type = "disk"; 244 }; 245 }; 246 } 247 ]; 248 storage_pools = [ 249 { 250 name = "default"; 251 driver = "dir"; 252 config = { 253 source = "/var/lib/incus/storage-pools/default"; 254 }; 255 } 256 ]; 257 }; 258 }; 259 260 socketActivation = lib.mkEnableOption ('' 261 socket-activation for starting incus.service. Enabling this option 262 will stop incus.service from starting automatically on boot. 263 ''); 264 265 startTimeout = lib.mkOption { 266 type = lib.types.ints.unsigned; 267 default = 600; 268 apply = toString; 269 description = '' 270 Time to wait (in seconds) for incusd to become ready to process requests. 271 If incusd does not reply within the configured time, `incus.service` will be 272 considered failed and systemd will attempt to restart it. 273 ''; 274 }; 275 276 ui = { 277 enable = lib.mkEnableOption "Incus Web UI"; 278 279 package = lib.mkPackageOption pkgs [ "incus-ui-canonical" ] { }; 280 }; 281 }; 282 }; 283 284 config = lib.mkIf cfg.enable { 285 assertions = [ 286 { 287 assertion = 288 !( 289 config.networking.firewall.enable 290 && !config.networking.nftables.enable 291 && config.virtualisation.incus.enable 292 ); 293 message = "Incus on NixOS is unsupported using iptables. Set `networking.nftables.enable = true;`"; 294 } 295 ]; 296 297 # https://github.com/lxc/incus/blob/f145309929f849b9951658ad2ba3b8f10cbe69d1/doc/reference/server_settings.md 298 boot.kernel.sysctl = { 299 "fs.aio-max-nr" = lib.mkDefault 524288; 300 "fs.inotify.max_queued_events" = lib.mkDefault 1048576; 301 "fs.inotify.max_user_instances" = lib.mkOverride 1050 1048576; # override in case conflict nixos/modules/services/x11/xserver.nix 302 "fs.inotify.max_user_watches" = lib.mkOverride 1050 1048576; # override in case conflict nixos/modules/services/x11/xserver.nix 303 "kernel.dmesg_restrict" = lib.mkDefault 1; 304 "kernel.keys.maxbytes" = lib.mkDefault 2000000; 305 "kernel.keys.maxkeys" = lib.mkDefault 2000; 306 "net.core.bpf_jit_limit" = lib.mkDefault 1000000000; 307 "net.ipv4.neigh.default.gc_thresh3" = lib.mkDefault 8192; 308 "net.ipv6.neigh.default.gc_thresh3" = lib.mkDefault 8192; 309 # vm.max_map_count is set higher in nixos/modules/config/sysctl.nix 310 }; 311 312 boot.kernelModules = [ 313 "br_netfilter" 314 "veth" 315 "xt_comment" 316 "xt_CHECKSUM" 317 "xt_MASQUERADE" 318 "vhost_vsock" 319 ] ++ lib.optionals nvidiaEnabled [ "nvidia_uvm" ]; 320 321 environment.systemPackages = [ 322 cfg.clientPackage 323 324 # gui console support 325 pkgs.spice-gtk 326 ]; 327 328 # Note: the following options are also declared in virtualisation.lxc, but 329 # the latter can't be simply enabled to reuse the formers, because it 330 # does a bunch of unrelated things. 331 systemd.tmpfiles.rules = [ "d /var/lib/lxc/rootfs 0755 root root -" ]; 332 333 security.apparmor = { 334 packages = [ cfg.lxcPackage ]; 335 policies = { 336 "bin.lxc-start".profile = '' 337 include ${cfg.lxcPackage}/etc/apparmor.d/usr.bin.lxc-start 338 ''; 339 "lxc-containers".profile = '' 340 include ${cfg.lxcPackage}/etc/apparmor.d/lxc-containers 341 ''; 342 "incusd".profile = '' 343 # This profile allows everything and only exists to give the 344 # application a name instead of having the label "unconfined" 345 346 abi <abi/4.0>, 347 include <tunables/global> 348 349 profile incusd ${lib.getExe' config.virtualisation.incus.package "incusd"} flags=(unconfined) { 350 userns, 351 </var/lib/incus/security/apparmor/cache> 352 </var/lib/incus/security/apparmor/profiles> 353 354 # Site-specific additions and overrides. See local/README for details. 355 include if exists <local/incusd> 356 } 357 ''; 358 }; 359 includes."abstractions/base" = 360 '' 361 # Allow incusd's various AA profiles to load dynamic libraries from Nix store 362 # https://discuss.linuxcontainers.org/t/creating-new-containers-vms-blocked-by-apparmor-on-nixos/21908/6 363 mr /nix/store/*/lib/*.so*, 364 r ${pkgs.stdenv.cc.libc}/lib/gconv/gconv-modules, 365 r ${pkgs.stdenv.cc.libc}/lib/gconv/gconv-modules.d/, 366 r ${pkgs.stdenv.cc.libc}/lib/gconv/gconv-modules.d/gconv-modules-extra.conf, 367 368 # Support use of VM instance 369 mrix ${pkgs.qemu_kvm}/bin/*, 370 k ${OVMF2MB.fd}/FV/*.fd, 371 k ${pkgs.OVMFFull.fd}/FV/*.fd, 372 '' 373 + lib.optionalString pkgs.stdenv.hostPlatform.isx86_64 '' 374 k ${pkgs.seabios-qemu}/share/seabios/bios.bin, 375 ''; 376 }; 377 378 systemd.services.incus = { 379 description = "Incus Container and Virtual Machine Management Daemon"; 380 381 inherit environment; 382 383 wantedBy = lib.mkIf (!cfg.socketActivation) [ "multi-user.target" ]; 384 after = [ 385 "network-online.target" 386 "lxcfs.service" 387 "incus.socket" 388 ] ++ lib.optionals config.virtualisation.vswitch.enable [ "ovs-vswitchd.service" ]; 389 390 requires = [ 391 "lxcfs.service" 392 "incus.socket" 393 ] ++ lib.optionals config.virtualisation.vswitch.enable [ "ovs-vswitchd.service" ]; 394 395 wants = [ "network-online.target" ]; 396 397 serviceConfig = { 398 ExecStart = "${cfg.package}/bin/incusd --group incus-admin"; 399 ExecStartPost = "${cfg.package}/bin/incusd waitready --timeout=${cfg.startTimeout}"; 400 ExecStop = lib.optionalString (!cfg.softDaemonRestart) "${cfg.package}/bin/incus admin shutdown"; 401 402 KillMode = "process"; # when stopping, leave the containers alone 403 Delegate = "yes"; 404 LimitMEMLOCK = "infinity"; 405 LimitNOFILE = "1048576"; 406 LimitNPROC = "infinity"; 407 TasksMax = "infinity"; 408 409 Restart = "on-failure"; 410 TimeoutStartSec = "${cfg.startTimeout}s"; 411 TimeoutStopSec = "30s"; 412 }; 413 }; 414 415 systemd.services.incus-user = { 416 description = "Incus Container and Virtual Machine Management User Daemon"; 417 418 inherit environment; 419 420 after = [ 421 "incus.service" 422 "incus-user.socket" 423 ]; 424 425 requires = [ 426 "incus-user.socket" 427 ]; 428 429 serviceConfig = { 430 ExecStart = "${cfg.package}/bin/incus-user --group incus"; 431 432 Restart = "on-failure"; 433 }; 434 }; 435 436 systemd.services.incus-startup = lib.mkIf cfg.softDaemonRestart { 437 description = "Incus Instances Startup/Shutdown"; 438 439 inherit environment; 440 441 after = [ 442 "incus.service" 443 "incus.socket" 444 ]; 445 requires = [ "incus.socket" ]; 446 wantedBy = config.systemd.services.incus.wantedBy; 447 448 serviceConfig = { 449 ExecStart = "${incus-startup} start"; 450 ExecStop = "${incus-startup} stop"; 451 RemainAfterExit = true; 452 TimeoutStartSec = "600s"; 453 TimeoutStopSec = "600s"; 454 Type = "oneshot"; 455 }; 456 }; 457 458 systemd.sockets.incus = { 459 description = "Incus UNIX socket"; 460 wantedBy = [ "sockets.target" ]; 461 462 socketConfig = { 463 ListenStream = "/var/lib/incus/unix.socket"; 464 SocketMode = "0660"; 465 SocketGroup = "incus-admin"; 466 }; 467 }; 468 469 systemd.sockets.incus-user = { 470 description = "Incus user UNIX socket"; 471 wantedBy = [ "sockets.target" ]; 472 473 socketConfig = { 474 ListenStream = "/var/lib/incus/unix.socket.user"; 475 SocketMode = "0660"; 476 SocketGroup = "incus"; 477 }; 478 }; 479 480 systemd.services.incus-preseed = lib.mkIf (cfg.preseed != null) { 481 description = "Incus initialization with preseed file"; 482 483 wantedBy = [ "incus.service" ]; 484 after = [ "incus.service" ]; 485 bindsTo = [ "incus.service" ]; 486 partOf = [ "incus.service" ]; 487 488 script = '' 489 ${cfg.package}/bin/incus admin init --preseed <${preseedFormat.generate "incus-preseed.yaml" cfg.preseed} 490 ''; 491 492 serviceConfig = { 493 Type = "oneshot"; 494 RemainAfterExit = true; 495 }; 496 }; 497 498 users.groups.incus = { }; 499 users.groups.incus-admin = { }; 500 501 users.users.root = { 502 # match documented default ranges https://linuxcontainers.org/incus/docs/main/userns-idmap/#allowed-ranges 503 subUidRanges = [ 504 { 505 startUid = 1000000; 506 count = 1000000000; 507 } 508 ]; 509 subGidRanges = [ 510 { 511 startGid = 1000000; 512 count = 1000000000; 513 } 514 ]; 515 }; 516 517 virtualisation.lxc.lxcfs.enable = true; 518 }; 519}