about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEmile <git@emile.space>2025-02-10 23:58:05 +0100
committerEmile <git@emile.space>2025-02-10 23:58:05 +0100
commit906026e924e3aac0f0ed32ec8ab2f7468e8e1c0a (patch)
treecadcd8aa380b713a573266701d0b63b766c93b97
parentf62271e0d0739cbc7a4fae21ebfcd76e0e7e9d58 (diff)
corrino: libvirtnix foo, less XML, more nix!
So I've finally started this. Let's see how far I can push this!
-rw-r--r--nix/hosts/corrino/configuration.nix1
-rw-r--r--nix/hosts/corrino/vm.nix43
-rw-r--r--nix/modules/libvirtnix/default.nix102
-rw-r--r--nix/modules/libvirtnix/domain.nix580
-rw-r--r--nix/modules/vm/default.nix86
-rw-r--r--nix/modules/x86_64-linux.nix1
6 files changed, 727 insertions, 86 deletions
diff --git a/nix/hosts/corrino/configuration.nix b/nix/hosts/corrino/configuration.nix
index f5f6444..f69d16a 100644
--- a/nix/hosts/corrino/configuration.nix
+++ b/nix/hosts/corrino/configuration.nix
@@ -22,6 +22,7 @@ in
     ./hardware-configuration.nix
 
     ./ports.nix
+    ./vm.nix
 
     ./www/git
     ./www/nix-cache
diff --git a/nix/hosts/corrino/vm.nix b/nix/hosts/corrino/vm.nix
new file mode 100644
index 0000000..7fcf4af
--- /dev/null
+++ b/nix/hosts/corrino/vm.nix
@@ -0,0 +1,43 @@
+{ pkgs, ... }:
+
+{
+	services.emile.libvirtnix = {
+		enable = true;
+		instances = {
+
+			vm1 = {
+				domain = {
+					name = "VM1";
+					title = "vm one";
+					description = "The first VM";
+					id = 1;
+
+					uuid = "E34DE478-1402-45BB-B3FD-FC960549258E";
+					genid = "CA1E2462-1E9D-404C-8DDB-19EEF9D9651B";
+
+					packages = {
+						libvirt = pkgs.libvirt;
+						qemu = pkgs.qemu;
+					};
+					memory = 1024;
+				};
+			};
+
+			vm2 = {
+				domain = {
+					name = "VM2";
+					title = "vm one";
+					description = "The second VM";
+					id = 2;
+
+					uuid = "E34DE478-1402-45BB-B3FD-FC960549258E";
+					genid = "002D0D8F-B21A-4001-92BF-2313707EED9D";
+
+					memory = 2048;
+				};
+			};
+
+		};
+	};
+}
+
diff --git a/nix/modules/libvirtnix/default.nix b/nix/modules/libvirtnix/default.nix
new file mode 100644
index 0000000..4689979
--- /dev/null
+++ b/nix/modules/libvirtnix/default.nix
@@ -0,0 +1,102 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+
+let
+  cfg = config.services.emile.libvirtnix;
+in {
+  options.services.emile.libvirtnix = with lib; {
+    enable = mkEnableOption "enable libvirtnix";
+    instances = 
+      mkOption {
+        type = types.attrsOf (types.submodule ({ name, ... }: {
+          options = {
+            enable = mkEnableOption "enable this instance";
+            domain = mkOption {
+              type = types.submodule (import ./domain.nix);
+            };
+          };
+        }));
+        default = {};
+        description = ''
+          A full libvirt config, statically defined using nix.
+        '';
+        example = ''
+          {
+            domain = {
+              name = "vm";
+            };
+          }
+        '';
+      };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services = lib.mapAttrs' (
+      name: guest:
+      lib.nameValuePair "libvirtd-guest-${name}" {
+        after = [ "libvirtd.service" ];
+        requires = [ "libvirtd.service" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = "yes";
+        };
+        script =
+          let
+            xml = pkgs.writeText "libvirt-guest-${name}.xml" ''
+              <domain ''
+              + (lib.optionalString (guest.domain.type != "") "type='${guest.domain.type}' ")
+              + (lib.optionalString (guest.domain.id != 0) "id='${guest.domain.type}'")
+              + ''>''
+              + (lib.optionalString (guest.domain.name != "") "<name>${guest.domain.name}</name>")
+              + (lib.optionalString (guest.domain.name != "") "<name>${guest.domain.name}</name>")
+              + (lib.optionalString (guest.domain.uuid != "") "<uuid>${guest.domain.uuid}</uuid>")
+              + (lib.optionalString (guest.domain.genid != "") "<genid>${guest.domain.genid}</genid>")
+              + (lib.optionalString (guest.domain.title != "") "<title>${guest.domain.title}</title>")
+              + (lib.optionalString (guest.domain.description != "") "<description>${guest.domain.description}</description>")
+              + (lib.optionalString (guest.domain.metadata != "") "${guest.domain.metadata}")
+              + ''
+                <os>
+                  <type>hvm</type>
+                </os>
+                <memory unit="GiB">${toString guest.domain.memory}</memory>
+                <devices>
+                  <disk type="volume">
+                    <source volume="guest-${name}"/>
+                    <target dev="vda" bus="virtio"/>
+                  </disk>
+                  <graphics type="spice" autoport="yes"/>
+                  <input type="keyboard" bus="usb"/>
+                </devices>
+                <features>
+                  <acpi/>
+                </features>
+              </domain>
+            '';
+          in
+          ''
+            uuid="$(${pkgs.libvirt}/bin/virsh domuuid '${name}' || true)"
+            ${pkgs.libvirt}/bin/virsh define <(sed "s/UUID/$uuid/" '${xml}')
+            ${pkgs.libvirt}/bin/virsh start '${name}'
+          '';
+        preStop = ''
+          ${pkgs.libvirt}/bin/virsh shutdown '${name}'
+          let "timeout = $(date +%s) + 10"
+          while [ "$(${pkgs.libvirt}/bin/virsh list --name | grep --count '^${name}$')" -gt 0 ]; do
+            if [ "$(date +%s)" -ge "$timeout" ]; then
+              # Meh, we warned it...
+              ${pkgs.libvirt}/bin/virsh destroy '${name}'
+            else
+              # The machine is still running, let's give it some time to shut down
+              sleep 0.5
+            fi
+          done
+        '';
+      }
+    ) config.services.emile.libvirtnix.instances;
+  };
+}
diff --git a/nix/modules/libvirtnix/domain.nix b/nix/modules/libvirtnix/domain.nix
new file mode 100644
index 0000000..027edda
--- /dev/null
+++ b/nix/modules/libvirtnix/domain.nix
@@ -0,0 +1,580 @@
+{ pkgs, lib, ... }:
+
+{
+	options = with lib; {
+
+		# meta, this allows defining the package used for each vm individually, defaults should be sane
+    packages = mkOption {
+      type = types.submodule {
+				options = {
+					libvirt = mkPackageOption pkgs "libvirt" { };
+					qemu = mkPackageOption pkgs "qemu" { };
+				};
+			};
+    };
+
+		# attributs for the root `<domain ...>` node
+		type = mkOption {
+			type = types.enum [ "xen" "hvm" "kvm" "qemu" "lxc" ];
+			default = "kvm";
+			example = "qemu";
+			description = ''
+				hypervisor used for running the domain
+
+				allowed values are driver specific, if missing, plz send PR
+
+				hvm since since 8.1.0 and QEMU 2.12
+			'';
+		};
+
+    id = mkOption {
+      type = types.int;
+      default = 0;
+      example = 42;
+    };
+
+		# General metadata
+    name = mkOption {
+      type = types.str;
+      example = "MyGuest";
+    };
+
+		uuid = mkOption {
+			type = types.str;
+			example = "3e3fce45-4f53-4fa7-bb32-11f34168b82b";
+		};
+
+		genid = mkOption {
+			type = types.str;
+			example = "43dc0cf8-809b-4adb-9bea-a9abb5f3d90e";
+		};
+
+		title = mkOption {
+			type = types.str;
+			example = "A short description - title - of the domain";
+		};
+
+		description = mkOption {
+			type = types.str;
+			example = "Some human readable description";
+		};
+
+		metadata = mkOption {
+			type = types.str;
+			default = "";
+			example = ''
+			  <metadata>
+			    <app1:foo xmlns:app1="http://app1.org/app1/">..</app1:foo>
+			    <app2:bar xmlns:app2="http://app1.org/app2/">..</app2:bar>
+			  </metadata>
+			'';
+		};
+
+    os = mkOption {
+			type = types.submodule {
+				options = {
+					firmware = mkOption {
+						type = types.enum [ "bios" "efi" ];
+						default = "efi";
+						example = "bios";
+					};
+					type = mkOption {
+						type = types.enum [ "hvm" "linux" ];
+						default = "hvm";
+						example = "linux";
+					};
+					arch = mkOption { type = types.str; };
+					machine = mkOption { type = types.str; };
+
+					# TODO(emile): features
+					# Search for the following...
+					# > When using firmware auto-selection there are different features enabled in the firmwares
+					# ... here: https://libvirt.org/formatdomain.html
+					# can't bother to implement this now
+
+					# features = mkOption {
+					# 	type = types.submodule {
+					# 		options = {
+					# 			enabled = {
+					# 				type = types.enum [ "yes" "no" ];
+					# 			};
+					# 			name = {};
+					# 		};
+					# 	};
+					# };
+
+					
+					# such as:
+					# <loader readonly='yes' secure='yes' type='pflash'>/usr/share/OVMF/OVMF_CODE.fd</loader>
+					loader = mkOption {
+						type = types.submodule {
+							options = {
+								readonly = mkOption {
+									type = types.enum [ "yes" "no" ];
+								};
+								type = mkOption {
+									type = types.enum [ "rom" "pflash" ];
+								};
+								secure = mkOption {
+									type = types.enum [ "yes" "no" ];
+								};
+								stateless = mkOption {
+									type = types.enum [ "yes" "no" ];
+								};
+								format = mkOption {
+									type = types.enum [ "raw" "qcow2" ];
+								};
+								value = mkOption {
+									type = types.str;
+									example = "/usr/share/OVMF/OVMF_CODE.fd";
+								};
+							};
+						};
+					};
+
+				  # <nvram type='network'>
+				  #   <source protocol='iscsi' name='iqn.2013-07.com.example:iscsi-nopool/0'>
+				  #     <host name='example.com' port='6000'/>
+				  #     <auth username='myname'>
+				  #       <secret type='iscsi' usage='mycluster_myname'/>
+				  #     </auth>
+				  #   </source>
+				  # </nvram>
+
+					# nvram = {
+					# 	type = "network";
+					# 	source = {
+					# 		protocol = "iscsi";
+					# 		name = "iqn.2013-07.com.example:iscsi-nopool/0";
+					# 	};
+					# };
+
+					nvram = mkOption {
+						type = types.submodule {
+							options = {
+								type = mkOption {
+									type = types.enum [ "file" "block" "dir" "network" "volume" "nvme" "vhostuser" "vhostvdpa" ];
+									default = "";
+								};
+
+								source = mkOption {
+									type = types.submodule {
+										options = {
+											# TODO(emile): figure out how to conditionally allow setting the options below
+											#              if the type defined above has been set
+
+											# if type == file
+											file = mkOption { type = types.str; };
+											fdgroup = mkOption { type = types.str; };
+
+											# if type == block
+											dev = mkOption { type = types.str; };
+
+											# if type == dir
+											dir = mkOption { type = types.str; };
+
+											# if type == network
+											protocol = mkOption { type = types.enum ["nbd" "iscsi" "rbd" "sheepdog" "gluster" "vxhs" "nfs" "http" "https" "ftp" "ftps" "tftp" "ssh"]; };
+											name = mkOption { type = types.str; };
+											tls = mkOption { type = types.enum [ "yes" "no" ]; };
+											tlsHostname = mkOption { type = types.str; };
+											query = mkOption { type = types.str; };
+
+											# if type == volume
+											pool = mkOption { type = types.str; };
+											volume = mkOption { type = types.str; };
+											mode = mkOption {
+												type = types.enum [ "direct" "host" ];
+												default = "host";
+											};
+
+											# if type == nvme
+											type = mkOption {
+												type = types.enum [ "pci" ];
+												default = "pci";
+												description = ''
+													When the type is `nvme`, only `pci` is supported
+													When the type is `vhostuser`, only `unix` is supported
+
+													(I've got not clue how to model this is nix, it's essentially an attribute that is overloaded based on another value)
+												'';
+											};
+											managed = mkOption { type = types.enum [ "yes" "no" ]; };
+											namespace = mkOption {
+												type = types.int;
+												default = 0;
+											};
+																						
+											# if type == vhostuser
+											# type = see the type in the nvme section above
+											path = mkOption { type = types.str; };
+											
+											# if type == vhostvdpa
+											# dev = (defined above in the "type == block" section)
+
+											
+											# if type == file
+											# if type == block
+											# if type == volume
+											seclabel = mkOption { type = types.str; };
+
+											index = mkOption {
+												type = types.int;
+												default = 0;
+											};
+
+											
+											# TODO(emile): implement checks here
+											# start here and scroll down a bit to the table
+											# https://libvirt.org/formatdomain.html#hard-drives-floppy-disks-cdroms
+											# if type == network
+											host = mkOption {
+												type = types.submodule {
+													options = {
+														name = mkOption { type = types.str; };
+														port = mkOption {
+															type = types.int;
+															default = 0;
+														};
+														transport = mkOption { type = types.str; };
+														socket = mkOption { type = types.str; };
+														
+													};
+												};
+											};
+
+											
+											snapshot = mkOption {
+												type = types.submodule {
+													options = {
+														name = mkOption { type = types.str; };
+													};
+												};
+											};
+											
+											config = mkOption {
+												type = types.submodule {
+													options = {
+														file = mkOption { type = types.str; };
+													};
+												};
+											};
+
+											# Since 3.9.0, the auth element is supported for a disk type "network" that is using a source element with the protocol attributes "rbd", "iscsi", or "ssh". 
+											auth = mkOption {
+												type = types.submodule {
+													options = {
+														type = mkOption {
+															# type = types.enum [ "chap" ];
+															# freeform, yet "chap" is one of the allowed options, I don't know others (yet)
+															type = types.str;
+														};	
+														username = mkOption { type = types.str; };
+
+														# https://libvirt.org/formatsecret.html
+														secret = mkOption {
+															type = types.submodule {
+																options = {
+																	ephemeral = mkOption {
+																		type = types.enum [ "yes" "no" ];
+																		default = "no";
+																	};
+																	private = mkOption {
+																		type = types.enum [ "yes" "no" ];
+																		default = "no";
+																	};
+
+																	uuid = types.submodule {
+																		options = {
+																			value = mkOption { type = types.str; default = ""; };	
+																		};
+																	};
+																	description = types.submodule {
+																		options = {
+																			value = mkOption { type = types.str; default = ""; };	
+																		};
+																	};
+
+																	usage = types.submodule {
+																		options = {
+																			type = mkOption {
+																				type = types.enum [ "volume" "ceph" "iscsi" "tls" "vtpm" ];
+																				default = "";
+																			};	
+																			value = mkOption { type = types.str; default = ""; };	
+
+																			name = mkOption {
+																				type = types.submodule {
+																					options = {
+																						value = mkOption { type = types.str; };
+																					};
+																				};
+																			};
+
+																			volume = mkOption {
+																				type = types.submodule {
+																					options = {
+																						value = mkOption { type = types.str; };
+																					};
+																				};
+																			};
+
+																			# when using the "iscsi" type
+																			target = mkOption {
+																				type = types.submodule {
+																					options = {
+																						value = mkOption { type = types.str; };
+																					};
+																				};
+																			};
+																
+																		};
+																	};
+
+																};
+															};
+														}; # end of secret
+
+													};
+												};
+											}; # end of auth
+
+											
+											# https://libvirt.org/formatstorageencryption.html
+											encryption = mkOption {
+												type = types.submodule {
+													options = {
+														type = mkOption { type = types.str; };
+
+														# mandatory
+														format = mkOption { type = types.enum [ "default" "qcow" "luks" "luks2" "luks-any" ]; };
+
+														engine = mkOption { type = types.enum [ "qemu" "librbd" ]; };
+
+														secrets = mkOption {
+															type = types.listOf (mkOption {
+																type = types.submodule {
+																	options = {
+
+																		# mandatory
+																		type = mkOption { type = types.enum [ "volume" ]; };
+
+																		# uuid or usage
+																		uuid = mkOption { type = types.str; };
+																		usage = mkOption { type = types.str; };
+																		
+																	};
+																};
+															});
+														}; # end of secrets
+
+														cipher = mkOption {
+															type = types.submodule {
+																options = {
+																	name = mkOption {
+																		type = types.str;
+																		example = "'aes', 'des', 'cast5', 'serpent', 'twofish', etc.";
+																	};
+																	size = mkOption {
+																		type = types.str;
+																		example = "'256', '192', '128', etc.";
+																	};
+																	mode = mkOption {
+																		type = types.str;
+																		example = "'cbc', 'xts', 'ecb', etc.";
+																	};
+																	hash = mkOption {
+																		type = types.str;
+																		example = "'md5', 'sha1', 'sha256', etc.";
+																	};
+																};
+															};
+														}; # end of cipher
+
+														ivgen = mkOption {
+															type = types.submodule {
+																options = {
+																	name = mkOption {
+																		type = types.str;
+																		example = "'plain', 'plain64', 'essiv', etc.";
+																	};
+																	hash = mkOption {
+																		type = types.str;
+																		example = "'md5', 'sha1', 'sha256'";
+																	};
+																};
+															};
+														}; # end of ivygen
+													};
+												};
+											}; # end of encryption
+
+											# TODO(emile): reservations
+											# Looking at the following, it seems like this can use the source element recursively
+											# Haven't looked into how to define recursive nix options yet...
+											# https://github.com/virt-manager/virt-manager/blob/5ddd3456a0ca9836a98fc6ca4f0b2eaab268bf47/tests/data/cli/compare/virt-install-many-devices.xml#L398-L400
+
+											# TODO(emile): initiator
+
+											# Based on this here:
+											# https://github.com/virt-manager/virt-manager/blob/5ddd3456a0ca9836a98fc6ca4f0b2eaab268bf47/tests/data/cli/compare/virt-install-many-devices.xml#L440
+											address = mkOption {
+												type = types.submodule {
+													options = {
+														domain   = mkOption { type = types.int; };
+														bus      = mkOption { type = types.int; };
+														slot     = mkOption { type = types.int; };
+														function = mkOption { type = types.int; };
+													};
+												};
+											}; # end of address
+
+											# TODO(emile): slices
+											# Didn't find any usage of it, why is there documentation for it then?
+
+											ssl = mkOption {
+												type = types.submodule {
+													options = {
+														verify = mkOption { type = types.enum [ "yes" "no" ]; };
+													};
+												};
+											}; # end of ssl
+
+											# TODO(emile): cookies for http and https
+
+											readahead = mkOption {
+												type = types.submodule {
+													options = {
+														size = mkOption { type = types.int; };
+													};
+												};
+											}; # end of readahead
+
+											timeout = mkOption {
+												type = types.submodule {
+													options = {
+														seconds = mkOption { type = types.int; };
+													};
+												};
+											}; # end of timeout
+
+											identity = mkOption {
+												type = types.submodule {
+													options = {
+														user = mkOption { type = types.str; };
+														group = mkOption { type = types.str; };
+
+														# if ssh
+														# required
+														username = mkOption { type = types.str; };
+
+														# one of these:
+														agentsock = mkOption { type = types.str; };
+														keyfile = mkOption { type = types.str; };
+													};
+												};
+											}; # end of identity
+
+											# disk type "vhostuser"
+											reconnect = mkOption {
+												type = types.submodule {
+													options = {
+														# mandatory
+														enabled = mkOption { type = types.enum [ "yes" "no" ]; };
+														timeout = mkOption { type = types.int; description = "seconds"; };
+
+														# optional for disk type network and protocol nbd
+														delay = mkOption { type = types.int; default = 0; description = "seconds"; };
+													};
+												};
+											}; # end of reconnect
+
+											# disk type "ssh"
+											knownHosts = mkOption {
+												type = types.submodule {
+													options = {
+														path = mkOption { type = types.str; };
+													};
+												};
+											}; # end of knownHosts
+
+											dataStore = mkOption {
+												type = types.submodule {
+													options = {
+														# TODO(emile): can accept the same types as a `source` element
+														type = mkOption { type = types.str; };
+
+														# TODO(emile): can accept format and source subelements
+														# this is (once again) recursive for the source, stil need to figure
+														# out how to handle this, just not now
+													};
+												};
+											}; # end of dataStore
+
+											startupPolicy = mkOption {
+												type = types.enum [ "mandatory" "requisite" "optional" ];
+												default = "mandatory";
+											}; # end of startupPolicy
+											
+											backingStore = mkOption {
+												type = types.submodule {
+													options = {
+														type = mkOption {
+															# TODO(emile): can accept the same types as a `source` element
+															type = types.str;
+														};
+
+														index = mkOption {
+															# TODO(emile): figure out if this can just be an int
+															type = types.str;
+														};
+
+														# TODO(emile): can use the following sub elements:
+														# - format
+														# - source
+														# - backingStore
+													};
+												};
+											}; # end of backingStore
+
+											mirror = mkOption {
+												type = types.submodule {
+													options = {
+														api = mkOption {
+															type = types.enum [ "copy" "active-commit" ];
+														};
+														ready = mkOption {
+															type = types.enum [ "yes" "abort" "pivot" ];
+														};
+														# TODO(emile): can use the following sub elements:
+														# - type (disk types)
+														# - format
+														# - source
+														# - file
+													};
+												};
+											}; # end of mirror
+
+
+										};
+									};
+								}; # end of source
+
+							};
+						};
+					}; # end of nvram
+
+				};
+			};
+		};
+
+    memory = lib.mkOption {
+      type = lib.types.int;
+      default = 1024;
+      example = 2048;
+      description = ''
+        The amount of memory to provide to the VM
+      '';
+    };
+  };
+}
diff --git a/nix/modules/vm/default.nix b/nix/modules/vm/default.nix
deleted file mode 100644
index 9428c94..0000000
--- a/nix/modules/vm/default.nix
+++ /dev/null
@@ -1,86 +0,0 @@
-{
-  config,
-  lib,
-  pkgs,
-  ...
-}:
-
-let
-  cfg = config.services.emile.vm;
-in
-with lib;
-{
-  options.services.emile.vm = {
-    enable = mkEnableOption "Enable vm";
-
-    # ip and port to listen on
-    guest = mkOption {
-      type = types.str;
-      default = "vmnameone";
-      example = "vmnameone";
-      description = "The name of the vm";
-    };
-  };
-
-  config = mkIf cfg.enable {
-    systemd.services = lib.mapAttrs' (
-      name: guest:
-      lib.nameValuePair "libvirtd-guest-${name}" {
-        after = [ "libvirtd.service" ];
-        requires = [ "libvirtd.service" ];
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig = {
-          Type = "oneshot";
-          RemainAfterExit = "yes";
-        };
-        script =
-          let
-            xml = pkgs.writeText "libvirt-guest-${name}.xml" ''
-              <domain type="kvm">
-                <name>${name}</name>
-                <uuid>UUID</uuid>
-                <os>
-                  <type>hvm</type>
-                </os>
-                <memory unit="GiB">${guest.memory}</memory>
-                <devices>
-                  <disk type="volume">
-                    <source volume="guest-${name}"/>
-                    <target dev="vda" bus="virtio"/>
-                  </disk>
-                  <graphics type="spice" autoport="yes"/>
-                  <input type="keyboard" bus="usb"/>
-                  <interface type="direct">
-                    <source dev="${hostNic}" mode="bridge"/>
-                    <mac address="${guest.mac}"/>
-                    <model type="virtio"/>
-                  </interface>
-                </devices>
-                <features>
-                  <acpi/>
-                </features>
-              </domain>
-            '';
-          in
-          ''
-            uuid="$(${pkgs.libvirt}/bin/virsh domuuid '${name}' || true)"
-            ${pkgs.libvirt}/bin/virsh define <(sed "s/UUID/$uuid/" '${xml}')
-            ${pkgs.libvirt}/bin/virsh start '${name}'
-          '';
-        preStop = ''
-          ${pkgs.libvirt}/bin/virsh shutdown '${name}'
-          let "timeout = $(date +%s) + 10"
-          while [ "$(${pkgs.libvirt}/bin/virsh list --name | grep --count '^${name}$')" -gt 0 ]; do
-            if [ "$(date +%s)" -ge "$timeout" ]; then
-              # Meh, we warned it...
-              ${pkgs.libvirt}/bin/virsh destroy '${name}'
-            else
-              # The machine is still running, let's give it some time to shut down
-              sleep 0.5
-            fi
-          done
-        '';
-      }
-    ) guests;
-  };
-}
diff --git a/nix/modules/x86_64-linux.nix b/nix/modules/x86_64-linux.nix
index b913c68..e5dbc64 100644
--- a/nix/modules/x86_64-linux.nix
+++ b/nix/modules/x86_64-linux.nix
@@ -4,5 +4,6 @@
     ./r2wars-web
     ./remarvin
     ./filebrowser
+    ./libvirtnix
   ];
 }