diff --git a/modules/nixos/virtualisation/libvirt-guest.nix b/modules/nixos/virtualisation/libvirt-guest.nix new file mode 100644 index 0000000..e5a71db --- /dev/null +++ b/modules/nixos/virtualisation/libvirt-guest.nix @@ -0,0 +1,527 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit (lib) + concatStrings + mapAttrsToList + mkIf + mkMerge + mkOption + optionalString + ; + inherit (lib.types) + attrsOf + bool + enum + int + listOf + nullOr + path + str + submodule + ; + cfg = config.ataraxia.virtualisation.guests; + + diskOptions.options = { + diskFile = mkOption { + type = str; + }; + # TODO + bus = mkOption { + type = enum [ + "virtio" + "ide" + "scsi" + "sata" + ]; + default = "virtio"; + }; + type = mkOption { + type = enum [ + "raw" + "qcow2" + ]; + default = "qcow2"; + }; + targetName = mkOption { + type = str; + default = "vda"; + }; + discard = mkOption { + type = enum [ + "ignore" + "unmap" + ]; + default = "unmap"; + }; + cache = mkOption { + type = enum [ + "none" + "writethrough" + "writeback" + "directsync" + "unsafe" + ]; + default = "writeback"; + }; + }; + mountOptions.options = { + sourceDir = mkOption { + type = str; + default = ""; + }; + targetDir = mkOption { + type = str; + default = ""; + }; + # TODO + type = mkOption { + type = enum [ + "virtiofs" + "9p" + ]; + default = "virtiofs"; + }; + }; + guestsOptions = + { ... }: + { + options = rec { + xmlFile = mkOption { + type = nullOr path; + default = null; + }; + connectUri = mkOption { + type = str; + default = "qemu:///system"; + }; + user = mkOption { + type = str; + default = "qemu-libvirtd"; + }; + group = mkOption { + type = str; + default = "qemu-libvirtd"; + }; + autoStart = mkOption { + type = bool; + default = false; + }; + autoDefine = mkOption { + type = bool; + default = true; + }; + guestOsType = mkOption { + type = enum [ + "linux" + "windows" + ]; + default = "linux"; + }; + uefi = mkOption { + type = bool; + default = false; + }; + memory = mkOption { + type = int; + default = 1024; + }; + sharedMemory = mkOption { + type = bool; + # TODO: not needed if using 9p mount + default = devices.mounts != [ ]; + }; + cpu = { + sockets = mkOption { + type = int; + default = 1; + }; + cores = mkOption { + type = int; + default = 1; + }; + threads = mkOption { + type = int; + default = 1; + }; + }; + devices = { + disks = mkOption { + type = listOf (submodule diskOptions); + default = [ ]; + }; + mounts = mkOption { + type = listOf (submodule mountOptions); + default = [ ]; + }; + tablet = mkOption { + type = bool; + default = true; + }; + serial = mkOption { + type = bool; + default = true; + }; + qemuGuestAgent = mkOption { + type = bool; + default = guestOsType != "windows"; + }; + audio = { + enable = mkOption { + type = bool; + default = true; + }; + type = mkOption { + # TODO + type = enum [ + "none" + "alsa" + "coreaudio" + "dbus" + "jack" + "oss" + "pulseaudio" + "sdl" + "spice" + "file" + ]; + default = "spice"; + }; + }; + graphics = { + enable = mkOption { + type = bool; + # TODO: must be true if video == true? + default = true; + }; + type = mkOption { + # TODO + type = enum [ + "sdl" + "vnc" + "spice" + "rdp" + "desktop" + "egl-headless" + ]; + default = "spice"; + }; + }; + video = { + enable = mkOption { + type = bool; + default = true; + }; + type = mkOption { + # TODO + type = enum [ + "vga" + "cirrus" + "vmvga" + "xen" + "vbox" + "qxl" + "virtio" + "gop" + "bochs" + "ramfb" + "none" + ]; + default = "virtio"; + }; + }; + network = { + enable = mkOption { + type = bool; + default = true; + }; + interfaceType = mkOption { + # TODO + type = enum [ + "network" + "macvlan" + "bridge" + ]; + default = "network"; + }; + modelType = mkOption { + type = enum [ + "virtio" + "e1000" + ]; + default = "virtio"; + }; + macAddress = mkOption { + type = nullOr str; + default = null; + }; + active = mkOption { + type = bool; + default = true; + }; + sourceDev = mkOption { + type = str; + default = "default"; + }; + }; + }; + timeout = mkOption { + type = int; + default = 10; + }; + }; + }; + + genXML = + name: guest: + pkgs.writeText "libvirt-guest-${name}.xml" '' + + ${name} + UUID + ${toString guest.memory} + ${optionalString guest.sharedMemory '' + + + + + ''} + ${with guest.cpu; toString (sockets * cores * threads)} + + hvm + ${optionalString guest.uefi '' + /run/libvirt/nix-ovmf/OVMF_CODE.fd + /var/lib/libvirt/qemu/nvram/${name}_VARS.fd + ''} + + + + + ${optionalString (guest.guestOsType == "windows") '' + + + + + + + + + ''} + + + + ${with guest.cpu; '' + + ''} + + + + + + ${optionalString (guest.guestOsType == "windows") '' + + ''} + + + + + + + /run/libvirt/nix-emulators/qemu-system-x86_64 + ${concatStrings ( + map (disk: '' + + + + + + '') guest.devices.disks + )} + ${concatStrings ( + map (mount: '' + + + + + + + + + + '') guest.devices.mounts + )} + ${ + with guest.devices.network; + if enable then + if interfaceType == "network" then + '' + + ${optionalString (macAddress != null) '' + + ''} + + + + '' + else if interfaceType == "bridge" then + '' + + ${optionalString (macAddress != null) '' + + ''} + + + + '' + else if interfaceType == "macvlan" then + '' + + ${optionalString (macAddress != null) '' + + ''} + + + + '' + else + "" + else + "" + } + ${optionalString guest.devices.tablet '' + + ''} + ${optionalString guest.devices.serial '' + + ''} + ${optionalString guest.devices.qemuGuestAgent '' + + + + ''} + ${optionalString guest.devices.audio.enable '' + + + ''; +in +{ + options.ataraxia.virtualisation.guests = mkOption { + default = { }; + type = attrsOf (submodule guestsOptions); + }; + + config.systemd.services = mkMerge ( + mapAttrsToList (name: guest: { + "libvirt-guest-define-${name}" = { + after = [ "libvirtd.service" ]; + requires = [ "libvirtd.service" ]; + wantedBy = mkIf guest.autoDefine [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = "no"; + User = guest.user; + Group = guest.group; + }; + environment = { + LIBVIRT_DEFAULT_URI = guest.connectUri; + }; + script = + if guest.xmlFile != null then + '' + ${pkgs.libvirt}/bin/virsh define --file ${guest.xmlFile} + ${pkgs.libvirt}/bin/virsh net-start ${guest.devices.network.sourceDev} || true + '' + else + '' + uuid="$(${pkgs.libvirt}/bin/virsh domuuid '${name}' || true)" + ${pkgs.libvirt}/bin/virsh define <(sed "s/UUID/$uuid/" '${genXML name guest}') + ${optionalString ( + guest.devices.network.interfaceType == "network" + ) "${pkgs.libvirt}/bin/virsh net-start ${guest.devices.network.sourceDev} || true"} + ''; + }; + "libvirt-guest-${name}" = { + after = [ "libvirt-guest-define-${name}.service" ]; + requires = [ "libvirt-guest-define-${name}.service" ]; + wantedBy = mkIf guest.autoStart [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = "yes"; + User = guest.user; + Group = guest.group; + }; + environment = { + LIBVIRT_DEFAULT_URI = guest.connectUri; + }; + script = "${pkgs.libvirt}/bin/virsh start '${name}'"; + preStop = '' + ${pkgs.libvirt}/bin/virsh shutdown '${name}' + let "timeout = $(date +%s) + ${toString guest.timeout}" + while [ "$(${pkgs.libvirt}/bin/virsh list --name | grep --count '^${name}$')" -gt 0 ]; do + if [ "$(date +%s)" -ge "$timeout" ]; then + ${pkgs.libvirt}/bin/virsh destroy '${name}' + else + sleep 0.5 + fi + done + ''; + }; + }) cfg + ); +}