1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11
12 cfg = config.virtualisation.libvirtd;
13 vswitch = config.virtualisation.vswitch;
14 configFile = pkgs.writeText "libvirtd.conf" ''
15 auth_unix_ro = "polkit"
16 auth_unix_rw = "polkit"
17 ${cfg.extraConfig}
18 '';
19 qemuConfigFile = pkgs.writeText "qemu.conf" ''
20 ${optionalString cfg.qemu.ovmf.enable ''
21 nvram = [
22 "/run/libvirt/nix-ovmf/AAVMF_CODE.fd:/run/libvirt/nix-ovmf/AAVMF_VARS.fd",
23 "/run/libvirt/nix-ovmf/AAVMF_CODE.ms.fd:/run/libvirt/nix-ovmf/AAVMF_VARS.ms.fd",
24 "/run/libvirt/nix-ovmf/OVMF_CODE.fd:/run/libvirt/nix-ovmf/OVMF_VARS.fd",
25 "/run/libvirt/nix-ovmf/OVMF_CODE.ms.fd:/run/libvirt/nix-ovmf/OVMF_VARS.ms.fd"
26 ]
27 ''}
28 ${optionalString (!cfg.qemu.runAsRoot) ''
29 user = "qemu-libvirtd"
30 group = "qemu-libvirtd"
31 ''}
32 ${cfg.qemu.verbatimConfig}
33 '';
34 dirName = "libvirt";
35 subDirs = list: [ dirName ] ++ map (e: "${dirName}/${e}") list;
36
37 ovmfModule = types.submodule {
38 options = {
39 enable = mkOption {
40 type = types.bool;
41 default = true;
42 description = ''
43 Allows libvirtd to take advantage of OVMF when creating new
44 QEMU VMs with UEFI boot.
45 '';
46 };
47
48 # mkRemovedOptionModule does not work in submodules, do it manually
49 package = mkOption {
50 type = types.nullOr types.package;
51 default = null;
52 internal = true;
53 };
54
55 packages = mkOption {
56 type = types.listOf types.package;
57 default = [ pkgs.OVMF.fd ];
58 defaultText = literalExpression "[ pkgs.OVMF.fd ]";
59 example = literalExpression "[ pkgs.OVMFFull.fd pkgs.pkgsCross.aarch64-multiplatform.OVMF.fd ]";
60 description = ''
61 List of OVMF packages to use. Each listed package must contain files names FV/OVMF_CODE.fd and FV/OVMF_VARS.fd or FV/AAVMF_CODE.fd and FV/AAVMF_VARS.fd
62 '';
63 };
64 };
65 };
66
67 swtpmModule = types.submodule {
68 options = {
69 enable = mkOption {
70 type = types.bool;
71 default = false;
72 description = ''
73 Allows libvirtd to use swtpm to create an emulated TPM.
74 '';
75 };
76
77 package = mkPackageOption pkgs "swtpm" { };
78 };
79 };
80
81 qemuModule = types.submodule {
82 options = {
83 package = mkPackageOption pkgs "qemu" {
84 extraDescription = ''
85 `pkgs.qemu` can emulate alien architectures (e.g. aarch64 on x86)
86 `pkgs.qemu_kvm` saves disk space allowing to emulate only host architectures.
87 '';
88 };
89
90 runAsRoot = mkOption {
91 type = types.bool;
92 default = true;
93 description = ''
94 If true, libvirtd runs qemu as root.
95 If false, libvirtd runs qemu as unprivileged user qemu-libvirtd.
96 Changing this option to false may cause file permission issues
97 for existing guests. To fix these, manually change ownership
98 of affected files in /var/lib/libvirt/qemu to qemu-libvirtd.
99 '';
100 };
101
102 verbatimConfig = mkOption {
103 type = types.lines;
104 default = ''
105 namespaces = []
106 '';
107 description = ''
108 Contents written to the qemu configuration file, qemu.conf.
109 Make sure to include a proper namespace configuration when
110 supplying custom configuration.
111 '';
112 };
113
114 ovmf = mkOption {
115 type = ovmfModule;
116 default = { };
117 description = ''
118 QEMU's OVMF options.
119 '';
120 };
121
122 swtpm = mkOption {
123 type = swtpmModule;
124 default = { };
125 description = ''
126 QEMU's swtpm options.
127 '';
128 };
129
130 vhostUserPackages = mkOption {
131 type = types.listOf types.package;
132 default = [ ];
133 example = lib.literalExpression "[ pkgs.virtiofsd ]";
134 description = ''
135 Packages containing out-of-tree vhost-user drivers.
136 '';
137 };
138 };
139 };
140
141 hooksModule = types.submodule {
142 options = {
143 daemon = mkOption {
144 type = types.attrsOf types.path;
145 default = { };
146 description = ''
147 Hooks that will be placed under /var/lib/libvirt/hooks/daemon.d/
148 and called for daemon start/shutdown/SIGHUP events.
149 Please see https://libvirt.org/hooks.html for documentation.
150 '';
151 };
152
153 qemu = mkOption {
154 type = types.attrsOf types.path;
155 default = { };
156 description = ''
157 Hooks that will be placed under /var/lib/libvirt/hooks/qemu.d/
158 and called for qemu domains begin/end/migrate events.
159 Please see https://libvirt.org/hooks.html for documentation.
160 '';
161 };
162
163 lxc = mkOption {
164 type = types.attrsOf types.path;
165 default = { };
166 description = ''
167 Hooks that will be placed under /var/lib/libvirt/hooks/lxc.d/
168 and called for lxc domains begin/end events.
169 Please see https://libvirt.org/hooks.html for documentation.
170 '';
171 };
172
173 libxl = mkOption {
174 type = types.attrsOf types.path;
175 default = { };
176 description = ''
177 Hooks that will be placed under /var/lib/libvirt/hooks/libxl.d/
178 and called for libxl-handled xen domains begin/end events.
179 Please see https://libvirt.org/hooks.html for documentation.
180 '';
181 };
182
183 network = mkOption {
184 type = types.attrsOf types.path;
185 default = { };
186 description = ''
187 Hooks that will be placed under /var/lib/libvirt/hooks/network.d/
188 and called for networks begin/end events.
189 Please see https://libvirt.org/hooks.html for documentation.
190 '';
191 };
192 };
193 };
194
195 nssModule = types.submodule {
196 options = {
197 enable = mkOption {
198 type = types.bool;
199 default = false;
200 description = ''
201 This option enables the older libvirt NSS module. This method uses
202 DHCP server records, therefore is dependent on the hostname provided
203 by the guest.
204 Please see https://libvirt.org/nss.html for more information.
205 '';
206 };
207
208 enableGuest = mkOption {
209 type = types.bool;
210 default = false;
211 description = ''
212 This option enables the newer libvirt_guest NSS module. This module
213 uses the libvirt guest name instead of the hostname of the guest.
214 Please see https://libvirt.org/nss.html for more information.
215 '';
216 };
217 };
218 };
219in
220{
221
222 imports = [
223 (mkRemovedOptionModule [
224 "virtualisation"
225 "libvirtd"
226 "enableKVM"
227 ] "Set the option `virtualisation.libvirtd.qemu.package' instead.")
228 (mkRenamedOptionModule
229 [ "virtualisation" "libvirtd" "qemuPackage" ]
230 [ "virtualisation" "libvirtd" "qemu" "package" ]
231 )
232 (mkRenamedOptionModule
233 [ "virtualisation" "libvirtd" "qemuRunAsRoot" ]
234 [ "virtualisation" "libvirtd" "qemu" "runAsRoot" ]
235 )
236 (mkRenamedOptionModule
237 [ "virtualisation" "libvirtd" "qemuVerbatimConfig" ]
238 [ "virtualisation" "libvirtd" "qemu" "verbatimConfig" ]
239 )
240 (mkRenamedOptionModule
241 [ "virtualisation" "libvirtd" "qemuOvmf" ]
242 [ "virtualisation" "libvirtd" "qemu" "ovmf" "enable" ]
243 )
244 (mkRemovedOptionModule [ "virtualisation" "libvirtd" "qemuOvmfPackage" ]
245 "If this option was set to `foo`, set the option `virtualisation.libvirtd.qemu.ovmf.packages' to `[foo.fd]` instead."
246 )
247 (mkRenamedOptionModule
248 [ "virtualisation" "libvirtd" "qemuSwtpm" ]
249 [ "virtualisation" "libvirtd" "qemu" "swtpm" "enable" ]
250 )
251 ];
252
253 ###### interface
254
255 options.virtualisation.libvirtd = {
256
257 enable = mkOption {
258 type = types.bool;
259 default = false;
260 description = ''
261 This option enables libvirtd, a daemon that manages
262 virtual machines. Users in the "libvirtd" group can interact with
263 the daemon (e.g. to start or stop VMs) using the
264 {command}`virsh` command line tool, among others.
265 '';
266 };
267
268 package = mkPackageOption pkgs "libvirt" { };
269
270 extraConfig = mkOption {
271 type = types.lines;
272 default = "";
273 description = ''
274 Extra contents appended to the libvirtd configuration file,
275 libvirtd.conf.
276 '';
277 };
278
279 extraOptions = mkOption {
280 type = types.listOf types.str;
281 default = [ ];
282 example = [ "--verbose" ];
283 description = ''
284 Extra command line arguments passed to libvirtd on startup.
285 '';
286 };
287
288 onBoot = mkOption {
289 type = types.enum [
290 "start"
291 "ignore"
292 ];
293 default = "start";
294 description = ''
295 Specifies the action to be done to / on the guests when the host boots.
296 The "start" option starts all guests that were running prior to shutdown
297 regardless of their autostart settings. The "ignore" option will not
298 start the formerly running guest on boot. However, any guest marked as
299 autostart will still be automatically started by libvirtd.
300 '';
301 };
302
303 onShutdown = mkOption {
304 type = types.enum [
305 "shutdown"
306 "suspend"
307 ];
308 default = "suspend";
309 description = ''
310 When shutting down / restarting the host what method should
311 be used to gracefully halt the guests. Setting to "shutdown"
312 will cause an ACPI shutdown of each guest. "suspend" will
313 attempt to save the state of the guests ready to restore on boot.
314 '';
315 };
316
317 parallelShutdown = mkOption {
318 type = types.ints.unsigned;
319 default = 0;
320 description = ''
321 Number of guests that will be shutdown concurrently, taking effect when onShutdown
322 is set to "shutdown". If set to 0, guests will be shutdown one after another.
323 Number of guests on shutdown at any time will not exceed number set in this
324 variable.
325 '';
326 };
327
328 shutdownTimeout = mkOption {
329 type = types.ints.unsigned;
330 default = 300;
331 description = ''
332 Number of seconds we're willing to wait for a guest to shut down.
333 If parallel shutdown is enabled, this timeout applies as a timeout
334 for shutting down all guests on a single URI defined in the variable URIS.
335 If this is 0, then there is no time out (use with caution, as guests might not
336 respond to a shutdown request).
337 '';
338 };
339
340 startDelay = mkOption {
341 type = types.ints.unsigned;
342 default = 0;
343 description = ''
344 Number of seconds to wait between each guest start.
345 If set to 0, all guests will start up in parallel.
346 '';
347 };
348
349 allowedBridges = mkOption {
350 type = types.listOf types.str;
351 default = [ "virbr0" ];
352 description = ''
353 List of bridge devices that can be used by qemu:///session
354 '';
355 };
356
357 qemu = mkOption {
358 type = qemuModule;
359 default = { };
360 description = ''
361 QEMU related options.
362 '';
363 };
364
365 hooks = mkOption {
366 type = hooksModule;
367 default = { };
368 description = ''
369 Hooks related options.
370 '';
371 };
372
373 nss = mkOption {
374 type = nssModule;
375 default = { };
376 description = ''
377 libvirt NSS module options.
378 '';
379 };
380
381 sshProxy = mkOption {
382 type = types.bool;
383 default = true;
384 description = ''
385 Whether to configure OpenSSH to use the [SSH Proxy](https://libvirt.org/ssh-proxy.html).
386 '';
387 };
388 };
389
390 ###### implementation
391
392 config = mkIf cfg.enable {
393
394 assertions = [
395 {
396 assertion = config.virtualisation.libvirtd.qemu.ovmf.package == null;
397 message = ''
398 The option virtualisation.libvirtd.qemu.ovmf.package is superseded by virtualisation.libvirtd.qemu.ovmf.packages.
399 If this option was set to `foo`, set the option `virtualisation.libvirtd.qemu.ovmf.packages' to `[foo.fd]` instead.
400 '';
401 }
402 {
403 assertion = config.security.polkit.enable;
404 message = "The libvirtd module currently requires Polkit to be enabled ('security.polkit.enable = true').";
405 }
406 ];
407
408 environment = {
409 # this file is expected in /etc/qemu and not sysconfdir (/var/lib)
410 etc."qemu/bridge.conf".text = lib.concatMapStringsSep "\n" (e: "allow ${e}") cfg.allowedBridges;
411 systemPackages = with pkgs; [
412 libressl.nc
413 config.networking.firewall.package
414 cfg.package
415 cfg.qemu.package
416 ];
417 etc.ethertypes.source = "${pkgs.iptables}/etc/ethertypes";
418 };
419
420 boot.kernelModules = [ "tun" ];
421
422 users.groups.libvirtd.gid = config.ids.gids.libvirtd;
423
424 # libvirtd runs qemu as this user and group by default
425 users.extraGroups.qemu-libvirtd.gid = config.ids.gids.qemu-libvirtd;
426 users.extraUsers.qemu-libvirtd = {
427 uid = config.ids.uids.qemu-libvirtd;
428 isNormalUser = false;
429 group = "qemu-libvirtd";
430 };
431
432 security.wrappers.qemu-bridge-helper = {
433 setuid = true;
434 owner = "root";
435 group = "root";
436 source = "${cfg.qemu.package}/libexec/qemu-bridge-helper";
437 };
438
439 programs.ssh.extraConfig = mkIf cfg.sshProxy ''
440 Include ${cfg.package}/etc/ssh/ssh_config.d/30-libvirt-ssh-proxy.conf
441 '';
442
443 systemd.packages = [ cfg.package ];
444
445 systemd.services.libvirtd-config = {
446 description = "Libvirt Virtual Machine Management Daemon - configuration";
447 script = ''
448 # Copy default libvirt network config .xml files to /var/lib
449 # Files modified by the user will not be overwritten
450 for i in $(cd ${cfg.package}/var/lib && echo \
451 libvirt/qemu/networks/*.xml \
452 libvirt/nwfilter/*.xml );
453 do
454 # Intended behavior
455 # shellcheck disable=SC2174
456 mkdir -p "/var/lib/$(dirname "$i")" -m 755
457 if [ ! -e "/var/lib/$i" ]; then
458 cp -pd "${cfg.package}/var/lib/$i" "/var/lib/$i"
459 fi
460 done
461
462 # Copy generated qemu config to libvirt directory
463 cp -f ${qemuConfigFile} /var/lib/${dirName}/qemu.conf
464
465 # stable (not GC'able as in /nix/store) paths for using in <emulator> section of xml configs
466 for emulator in ${cfg.package}/libexec/libvirt_lxc ${cfg.qemu.package}/bin/qemu-kvm ${cfg.qemu.package}/bin/qemu-system-*; do
467 ln -s --force "$emulator" /run/${dirName}/nix-emulators/
468 done
469
470 ln -s --force ${cfg.qemu.package}/bin/qemu-pr-helper /run/${dirName}/nix-helpers/
471
472 ${optionalString cfg.qemu.ovmf.enable (
473 let
474 ovmfpackage = pkgs.buildEnv {
475 name = "qemu-ovmf";
476 paths = cfg.qemu.ovmf.packages;
477 };
478 in
479 ''
480 ln -s --force ${ovmfpackage}/FV/AAVMF_CODE{,.ms}.fd /run/${dirName}/nix-ovmf/
481 ln -s --force ${ovmfpackage}/FV/OVMF_CODE{,.ms}.fd /run/${dirName}/nix-ovmf/
482 ln -s --force ${ovmfpackage}/FV/AAVMF_VARS{,.ms}.fd /run/${dirName}/nix-ovmf/
483 ln -s --force ${ovmfpackage}/FV/OVMF_VARS{,.ms}.fd /run/${dirName}/nix-ovmf/
484 ''
485 )}
486
487 # Symlink hooks to /var/lib/libvirt
488 ${concatStringsSep "\n" (
489 map (driver: ''
490 mkdir -p /var/lib/${dirName}/hooks/${driver}.d
491 rm -rf /var/lib/${dirName}/hooks/${driver}.d/*
492 ${concatStringsSep "\n" (
493 mapAttrsToList (
494 name: value: "ln -s --force ${value} /var/lib/${dirName}/hooks/${driver}.d/${name}"
495 ) cfg.hooks.${driver}
496 )}
497 '') (attrNames cfg.hooks)
498 )}
499 '';
500
501 serviceConfig = {
502 Type = "oneshot";
503 RuntimeDirectoryPreserve = "yes";
504 LogsDirectory = subDirs [ "qemu" ];
505 RuntimeDirectory = subDirs [
506 "nix-emulators"
507 "nix-helpers"
508 "nix-ovmf"
509 ];
510 StateDirectory = subDirs [ "dnsmasq" ];
511 };
512 };
513
514 systemd.services.libvirtd = {
515 wantedBy = [ "multi-user.target" ];
516 requires = [ "libvirtd-config.service" ];
517 after = [ "libvirtd-config.service" ] ++ optional vswitch.enable "ovs-vswitchd.service";
518
519 environment.LIBVIRTD_ARGS = escapeShellArgs (
520 [
521 "--config"
522 configFile
523 "--timeout"
524 "120" # from ${libvirt}/var/lib/sysconfig/libvirtd
525 ]
526 ++ cfg.extraOptions
527 );
528
529 path =
530 [
531 cfg.qemu.package
532 pkgs.netcat
533 ] # libvirtd requires qemu-img to manage disk images
534 ++ optional vswitch.enable vswitch.package
535 ++ optional cfg.qemu.swtpm.enable cfg.qemu.swtpm.package;
536
537 serviceConfig = {
538 Type = "notify";
539 KillMode = "process"; # when stopping, leave the VMs alone
540 Restart = "no";
541 OOMScoreAdjust = "-999";
542 };
543 restartIfChanged = false;
544 };
545
546 systemd.services.virtchd = {
547 path = [ pkgs.cloud-hypervisor ];
548 };
549
550 systemd.services.libvirt-guests = {
551 wantedBy = [ "multi-user.target" ];
552 requires = [ "libvirtd.service" ];
553 after = [ "libvirtd.service" ];
554 path = with pkgs; [
555 coreutils
556 gawk
557 cfg.package
558 ];
559 restartIfChanged = false;
560
561 environment.ON_BOOT = "${cfg.onBoot}";
562 environment.ON_SHUTDOWN = "${cfg.onShutdown}";
563 environment.PARALLEL_SHUTDOWN = "${toString cfg.parallelShutdown}";
564 environment.SHUTDOWN_TIMEOUT = "${toString cfg.shutdownTimeout}";
565 environment.START_DELAY = "${toString cfg.startDelay}";
566 };
567
568 systemd.sockets.virtlogd = {
569 description = "Virtual machine log manager socket";
570 wantedBy = [ "sockets.target" ];
571 listenStreams = [ "/run/${dirName}/virtlogd-sock" ];
572 };
573
574 systemd.services.virtlogd = {
575 description = "Virtual machine log manager";
576 serviceConfig.ExecStart = "@${cfg.package}/sbin/virtlogd virtlogd";
577 restartIfChanged = false;
578 };
579
580 systemd.sockets.virtlockd = {
581 description = "Virtual machine lock manager socket";
582 wantedBy = [ "sockets.target" ];
583 listenStreams = [ "/run/${dirName}/virtlockd-sock" ];
584 };
585
586 systemd.services.virtlockd = {
587 description = "Virtual machine lock manager";
588 serviceConfig.ExecStart = "@${cfg.package}/sbin/virtlockd virtlockd";
589 restartIfChanged = false;
590 };
591
592 # https://libvirt.org/daemons.html#monolithic-systemd-integration
593 systemd.sockets.libvirtd.wantedBy = [ "sockets.target" ];
594
595 systemd.tmpfiles.rules =
596 let
597 vhostUserCollection = pkgs.buildEnv {
598 name = "vhost-user";
599 paths = cfg.qemu.vhostUserPackages;
600 pathsToLink = [ "/share/qemu/vhost-user" ];
601 };
602 in
603 [
604 "L+ /var/lib/qemu/vhost-user - - - - ${vhostUserCollection}/share/qemu/vhost-user"
605 "L+ /var/lib/qemu/firmware - - - - ${cfg.qemu.package}/share/qemu/firmware"
606 ];
607
608 security.polkit = {
609 enable = true;
610 extraConfig = ''
611 polkit.addRule(function(action, subject) {
612 if (action.id == "org.libvirt.unix.manage" &&
613 subject.isInGroup("libvirtd")) {
614 return polkit.Result.YES;
615 }
616 });
617 '';
618 };
619
620 system.nssModules = optional (cfg.nss.enable or cfg.nss.enableGuest) cfg.package;
621 system.nssDatabases.hosts = mkMerge [
622 # ensure that the NSS modules come between mymachines (which is 400) and resolve (which is 501)
623 (mkIf cfg.nss.enable (mkOrder 430 [ "libvirt" ]))
624 (mkIf cfg.nss.enableGuest (mkOrder 432 [ "libvirt_guest" ]))
625 ];
626 };
627}