diff options
author | Emile <git@emile.space> | 2025-02-10 23:58:05 +0100 |
---|---|---|
committer | Emile <git@emile.space> | 2025-02-10 23:58:05 +0100 |
commit | 906026e924e3aac0f0ed32ec8ab2f7468e8e1c0a (patch) | |
tree | cadcd8aa380b713a573266701d0b63b766c93b97 | |
parent | f62271e0d0739cbc7a4fae21ebfcd76e0e7e9d58 (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.nix | 1 | ||||
-rw-r--r-- | nix/hosts/corrino/vm.nix | 43 | ||||
-rw-r--r-- | nix/modules/libvirtnix/default.nix | 102 | ||||
-rw-r--r-- | nix/modules/libvirtnix/domain.nix | 580 | ||||
-rw-r--r-- | nix/modules/vm/default.nix | 86 | ||||
-rw-r--r-- | nix/modules/x86_64-linux.nix | 1 |
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 ]; } |