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