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 ovmfFilePrefix = if pkgs.stdenv.isAarch64 then "AAVMF" else "OVMF";
15 qemuConfigFile = pkgs.writeText "qemu.conf" ''
16 ${optionalString cfg.qemu.ovmf.enable ''
17 nvram = [ "/run/libvirt/nix-ovmf/${ovmfFilePrefix}_CODE.fd:/run/libvirt/nix-ovmf/${ovmfFilePrefix}_VARS.fd" ]
18 ''}
19 ${optionalString (!cfg.qemu.runAsRoot) ''
20 user = "qemu-libvirtd"
21 group = "qemu-libvirtd"
22 ''}
23 ${cfg.qemu.verbatimConfig}
24 '';
25 dirName = "libvirt";
26 subDirs = list: [ dirName ] ++ map (e: "${dirName}/${e}") list;
27
28 ovmfModule = types.submodule {
29 options = {
30 enable = mkOption {
31 type = types.bool;
32 default = true;
33 description = ''
34 Allows libvirtd to take advantage of OVMF when creating new
35 QEMU VMs with UEFI boot.
36 '';
37 };
38
39 package = mkOption {
40 type = types.package;
41 default = pkgs.OVMF;
42 defaultText = literalExpression "pkgs.OVMF";
43 example = literalExpression "pkgs.OVMFFull";
44 description = ''
45 OVMF package to use.
46 '';
47 };
48 };
49 };
50
51 swtpmModule = types.submodule {
52 options = {
53 enable = mkOption {
54 type = types.bool;
55 default = false;
56 description = ''
57 Allows libvirtd to use swtpm to create an emulated TPM.
58 '';
59 };
60
61 package = mkOption {
62 type = types.package;
63 default = pkgs.swtpm;
64 defaultText = literalExpression "pkgs.swtpm";
65 description = ''
66 swtpm package to use.
67 '';
68 };
69 };
70 };
71
72 qemuModule = types.submodule {
73 options = {
74 package = mkOption {
75 type = types.package;
76 default = pkgs.qemu;
77 defaultText = literalExpression "pkgs.qemu";
78 description = ''
79 Qemu package to use with libvirt.
80 `pkgs.qemu` can emulate alien architectures (e.g. aarch64 on x86)
81 `pkgs.qemu_kvm` saves disk space allowing to emulate only host architectures.
82 '';
83 };
84
85 runAsRoot = mkOption {
86 type = types.bool;
87 default = true;
88 description = ''
89 If true, libvirtd runs qemu as root.
90 If false, libvirtd runs qemu as unprivileged user qemu-libvirtd.
91 Changing this option to false may cause file permission issues
92 for existing guests. To fix these, manually change ownership
93 of affected files in /var/lib/libvirt/qemu to qemu-libvirtd.
94 '';
95 };
96
97 verbatimConfig = mkOption {
98 type = types.lines;
99 default = ''
100 namespaces = []
101 '';
102 description = ''
103 Contents written to the qemu configuration file, qemu.conf.
104 Make sure to include a proper namespace configuration when
105 supplying custom configuration.
106 '';
107 };
108
109 ovmf = mkOption {
110 type = ovmfModule;
111 default = { };
112 description = ''
113 QEMU's OVMF options.
114 '';
115 };
116
117 swtpm = mkOption {
118 type = swtpmModule;
119 default = { };
120 description = ''
121 QEMU's swtpm options.
122 '';
123 };
124 };
125 };
126in
127{
128
129 imports = [
130 (mkRemovedOptionModule [ "virtualisation" "libvirtd" "enableKVM" ]
131 "Set the option `virtualisation.libvirtd.qemu.package' instead.")
132 (mkRenamedOptionModule
133 [ "virtualisation" "libvirtd" "qemuPackage" ]
134 [ "virtualisation" "libvirtd" "qemu" "package" ])
135 (mkRenamedOptionModule
136 [ "virtualisation" "libvirtd" "qemuRunAsRoot" ]
137 [ "virtualisation" "libvirtd" "qemu" "runAsRoot" ])
138 (mkRenamedOptionModule
139 [ "virtualisation" "libvirtd" "qemuVerbatimConfig" ]
140 [ "virtualisation" "libvirtd" "qemu" "verbatimConfig" ])
141 (mkRenamedOptionModule
142 [ "virtualisation" "libvirtd" "qemuOvmf" ]
143 [ "virtualisation" "libvirtd" "qemu" "ovmf" "enable" ])
144 (mkRenamedOptionModule
145 [ "virtualisation" "libvirtd" "qemuOvmfPackage" ]
146 [ "virtualisation" "libvirtd" "qemu" "ovmf" "package" ])
147 (mkRenamedOptionModule
148 [ "virtualisation" "libvirtd" "qemuSwtpm" ]
149 [ "virtualisation" "libvirtd" "qemu" "swtpm" "enable" ])
150 ];
151
152 ###### interface
153
154 options.virtualisation.libvirtd = {
155
156 enable = mkOption {
157 type = types.bool;
158 default = false;
159 description = ''
160 This option enables libvirtd, a daemon that manages
161 virtual machines. Users in the "libvirtd" group can interact with
162 the daemon (e.g. to start or stop VMs) using the
163 <command>virsh</command> command line tool, among others.
164 '';
165 };
166
167 package = mkOption {
168 type = types.package;
169 default = pkgs.libvirt;
170 defaultText = literalExpression "pkgs.libvirt";
171 description = ''
172 libvirt package to use.
173 '';
174 };
175
176 extraConfig = mkOption {
177 type = types.lines;
178 default = "";
179 description = ''
180 Extra contents appended to the libvirtd configuration file,
181 libvirtd.conf.
182 '';
183 };
184
185 extraOptions = mkOption {
186 type = types.listOf types.str;
187 default = [ ];
188 example = [ "--verbose" ];
189 description = ''
190 Extra command line arguments passed to libvirtd on startup.
191 '';
192 };
193
194 onBoot = mkOption {
195 type = types.enum [ "start" "ignore" ];
196 default = "start";
197 description = ''
198 Specifies the action to be done to / on the guests when the host boots.
199 The "start" option starts all guests that were running prior to shutdown
200 regardless of their autostart settings. The "ignore" option will not
201 start the formerly running guest on boot. However, any guest marked as
202 autostart will still be automatically started by libvirtd.
203 '';
204 };
205
206 onShutdown = mkOption {
207 type = types.enum [ "shutdown" "suspend" ];
208 default = "suspend";
209 description = ''
210 When shutting down / restarting the host what method should
211 be used to gracefully halt the guests. Setting to "shutdown"
212 will cause an ACPI shutdown of each guest. "suspend" will
213 attempt to save the state of the guests ready to restore on boot.
214 '';
215 };
216
217 allowedBridges = mkOption {
218 type = types.listOf types.str;
219 default = [ "virbr0" ];
220 description = ''
221 List of bridge devices that can be used by qemu:///session
222 '';
223 };
224
225 qemu = mkOption {
226 type = qemuModule;
227 default = { };
228 description = ''
229 QEMU related options.
230 '';
231 };
232 };
233
234
235 ###### implementation
236
237 config = mkIf cfg.enable {
238
239 assertions = [
240 {
241 assertion = config.security.polkit.enable;
242 message = "The libvirtd module currently requires Polkit to be enabled ('security.polkit.enable = true').";
243 }
244 {
245 assertion = builtins.elem "fd" cfg.qemu.ovmf.package.outputs;
246 message = "The option 'virtualisation.libvirtd.qemuOvmfPackage' needs a package that has an 'fd' output.";
247 }
248 ];
249
250 environment = {
251 # this file is expected in /etc/qemu and not sysconfdir (/var/lib)
252 etc."qemu/bridge.conf".text = lib.concatMapStringsSep "\n"
253 (e:
254 "allow ${e}")
255 cfg.allowedBridges;
256 systemPackages = with pkgs; [ libressl.nc iptables cfg.package cfg.qemu.package ];
257 etc.ethertypes.source = "${pkgs.iptables}/etc/ethertypes";
258 };
259
260 boot.kernelModules = [ "tun" ];
261
262 users.groups.libvirtd.gid = config.ids.gids.libvirtd;
263
264 # libvirtd runs qemu as this user and group by default
265 users.extraGroups.qemu-libvirtd.gid = config.ids.gids.qemu-libvirtd;
266 users.extraUsers.qemu-libvirtd = {
267 uid = config.ids.uids.qemu-libvirtd;
268 isNormalUser = false;
269 group = "qemu-libvirtd";
270 };
271
272 security.wrappers.qemu-bridge-helper = {
273 setuid = true;
274 owner = "root";
275 group = "root";
276 source = "/run/${dirName}/nix-helpers/qemu-bridge-helper";
277 };
278
279 systemd.packages = [ cfg.package ];
280
281 systemd.services.libvirtd-config = {
282 description = "Libvirt Virtual Machine Management Daemon - configuration";
283 script = ''
284 # Copy default libvirt network config .xml files to /var/lib
285 # Files modified by the user will not be overwritten
286 for i in $(cd ${cfg.package}/var/lib && echo \
287 libvirt/qemu/networks/*.xml libvirt/qemu/networks/autostart/*.xml \
288 libvirt/nwfilter/*.xml );
289 do
290 mkdir -p /var/lib/$(dirname $i) -m 755
291 cp -npd ${cfg.package}/var/lib/$i /var/lib/$i
292 done
293
294 # Copy generated qemu config to libvirt directory
295 cp -f ${qemuConfigFile} /var/lib/${dirName}/qemu.conf
296
297 # stable (not GC'able as in /nix/store) paths for using in <emulator> section of xml configs
298 for emulator in ${cfg.package}/libexec/libvirt_lxc ${cfg.qemu.package}/bin/qemu-kvm ${cfg.qemu.package}/bin/qemu-system-*; do
299 ln -s --force "$emulator" /run/${dirName}/nix-emulators/
300 done
301
302 for helper in libexec/qemu-bridge-helper bin/qemu-pr-helper; do
303 ln -s --force ${cfg.qemu.package}/$helper /run/${dirName}/nix-helpers/
304 done
305
306 ${optionalString cfg.qemu.ovmf.enable ''
307 ln -s --force ${cfg.qemu.ovmf.package.fd}/FV/${ovmfFilePrefix}_CODE.fd /run/${dirName}/nix-ovmf/
308 ln -s --force ${cfg.qemu.ovmf.package.fd}/FV/${ovmfFilePrefix}_VARS.fd /run/${dirName}/nix-ovmf/
309 ''}
310 '';
311
312 serviceConfig = {
313 Type = "oneshot";
314 RuntimeDirectoryPreserve = "yes";
315 LogsDirectory = subDirs [ "qemu" ];
316 RuntimeDirectory = subDirs [ "nix-emulators" "nix-helpers" "nix-ovmf" ];
317 StateDirectory = subDirs [ "dnsmasq" ];
318 };
319 };
320
321 systemd.services.libvirtd = {
322 requires = [ "libvirtd-config.service" ];
323 after = [ "libvirtd-config.service" ]
324 ++ optional vswitch.enable "ovs-vswitchd.service";
325
326 environment.LIBVIRTD_ARGS = escapeShellArgs (
327 [
328 "--config"
329 configFile
330 "--timeout"
331 "120" # from ${libvirt}/var/lib/sysconfig/libvirtd
332 ] ++ cfg.extraOptions
333 );
334
335 path = [ cfg.qemu.package ] # libvirtd requires qemu-img to manage disk images
336 ++ optional vswitch.enable vswitch.package
337 ++ optional cfg.qemu.swtpm.enable cfg.qemu.swtpm.package;
338
339 serviceConfig = {
340 Type = "notify";
341 KillMode = "process"; # when stopping, leave the VMs alone
342 Restart = "no";
343 };
344 restartIfChanged = false;
345 };
346
347 systemd.services.libvirt-guests = {
348 wantedBy = [ "multi-user.target" ];
349 path = with pkgs; [ coreutils gawk cfg.package ];
350 restartIfChanged = false;
351
352 environment.ON_BOOT = "${cfg.onBoot}";
353 environment.ON_SHUTDOWN = "${cfg.onShutdown}";
354 };
355
356 systemd.sockets.virtlogd = {
357 description = "Virtual machine log manager socket";
358 wantedBy = [ "sockets.target" ];
359 listenStreams = [ "/run/${dirName}/virtlogd-sock" ];
360 };
361
362 systemd.services.virtlogd = {
363 description = "Virtual machine log manager";
364 serviceConfig.ExecStart = "@${cfg.package}/sbin/virtlogd virtlogd";
365 restartIfChanged = false;
366 };
367
368 systemd.sockets.virtlockd = {
369 description = "Virtual machine lock manager socket";
370 wantedBy = [ "sockets.target" ];
371 listenStreams = [ "/run/${dirName}/virtlockd-sock" ];
372 };
373
374 systemd.services.virtlockd = {
375 description = "Virtual machine lock manager";
376 serviceConfig.ExecStart = "@${cfg.package}/sbin/virtlockd virtlockd";
377 restartIfChanged = false;
378 };
379
380 # https://libvirt.org/daemons.html#monolithic-systemd-integration
381 systemd.sockets.libvirtd.wantedBy = [ "sockets.target" ];
382
383 security.polkit.extraConfig = ''
384 polkit.addRule(function(action, subject) {
385 if (action.id == "org.libvirt.unix.manage" &&
386 subject.isInGroup("libvirtd")) {
387 return polkit.Result.YES;
388 }
389 });
390 '';
391 };
392}