{ 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 ); }