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