{ config, pkgs, lib, ... }: with lib; let cfg = config.virtualisation.libvirt.guests; diskOptions.options = { diskFile = mkOption { type = types.str; default = "/var/lib/libvirt/images/guest-${name}.qcow2"; }; # TODO bus = mkOption { type = types.enum [ "virtio" "ide" "scsi" "sata" ]; default = "virtio"; }; type = mkOption { type = types.enum [ "raw" "qcow2" ]; default = "qcow2"; }; targetName = mkOption { type = types.str; default = "vda"; }; discard = mkOption { type = types.enum [ "ignore" "unmap" ]; default = "unmap"; }; cache = mkOption { type = types.enum [ "none" "writethrough" "writeback" "directsync" "unsafe" ]; default = "writeback"; }; }; mountOptions.options = { sourceDir = mkOption { type = types.str; default = ""; }; targetDir = mkOption { type = types.str; default = ""; }; # TODO type = mkOption { type = types.enum [ "virtiofs" "9p" ]; default = "virtiofs"; }; }; guestsOptions = { ... }: { options = rec { xmlFile = mkOption { type = with types; nullOr path; default = null; }; connectUri = mkOption { type = types.str; default = "qemu:///system"; }; user = mkOption { type = types.str; default = "qemu-libvirtd"; }; group = mkOption { type = types.str; default = "qemu-libvirtd"; }; autoStart = mkOption { type = types.bool; default = false; }; autoDefine = mkOption { type = types.bool; default = true; }; guestOsType = mkOption { type = types.enum [ "linux" "windows" ]; default = "linux"; }; uefi = mkOption { type = types.bool; default = false; }; memory = mkOption { type = types.int; default = 1024; }; sharedMemory = mkOption { type = types.bool; # TODO: not needed if using 9p mount default = devices.mounts != [ ]; }; cpu = { sockets = mkOption { type = types.int; default = 1; }; cores = mkOption { type = types.int; default = 1; }; threads = mkOption { type = types.int; default = 1; }; }; devices = { disks = mkOption { type = with types; listOf (submodule diskOptions); default = [ ]; }; mounts = mkOption { type = with types; listOf (submodule mountOptions); default = [ ]; }; tablet = mkOption { type = types.bool; default = true; }; serial = mkOption { type = types.bool; default = true; }; qemuGuestAgent = mkOption { type = types.bool; default = guestOsType != "windows"; }; audio = { enable = mkOption { type = types.bool; default = true; }; type = mkOption { # TODO type = types.enum [ "none" "alsa" "coreaudio" "dbus" "jack" "oss" "pulseaudio" "sdl" "spice" "file" ]; default = "spice"; }; }; graphics = { enable = mkOption { type = types.bool; # TODO: must be true if video == true? default = true; }; type = mkOption { # TODO type = types.enum [ "sdl" "vnc" "spice" "rdp" "desktop" "egl-headless" ]; default = "spice"; }; }; video = { enable = mkOption { type = types.bool; default = true; }; type = mkOption { # TODO type = types.enum [ "vga" "cirrus" "vmvga" "xen" "vbox" "qxl" "virtio" "gop" "bochs" "ramfb" "none" ]; default = "virtio"; }; }; network = { enable = mkOption { type = types.bool; default = true; }; interfaceType = mkOption { # TODO type = types.enum [ "network" "macvlan" "bridge" ]; default = "network"; }; modelType = mkOption { type = types.enum [ "virtio" "e1000" ]; default = "virtio"; }; macAddress = mkOption { type = with types; nullOr str; default = null; }; active = mkOption { type = types.bool; default = true; }; sourceDev = mkOption { type = types.str; default = "default"; }; }; }; timeout = mkOption { type = types.int; default = 10; }; }; }; in { options.virtualisation.libvirt.guests = mkOption { default = { }; type = types.attrsOf (types.submodule guestsOptions); }; config.systemd.services = lib.mkMerge (lib.mapAttrsToList ( name: guest: let xml = pkgs.writeText "libvirt-guest-${name}.xml" '' ${name} UUID ${toString guest.memory} ${ lib.optionalString guest.sharedMemory '' '' } ${ with guest.cpu; toString (sockets * cores * threads) } hvm ${ lib.optionalString guest.uefi '' /run/libvirt/nix-ovmf/OVMF_CODE.fd /var/lib/libvirt/qemu/nvram/${name}_VARS.fd '' } ${ lib.optionalString (guest.guestOsType == "windows") '' '' } ${ with guest.cpu; '' '' } ${ lib.optionalString (guest.guestOsType == "windows") '' '' } /run/libvirt/nix-emulators/qemu-system-x86_64 ${ lib.concatStrings (map (disk: '' '') guest.devices.disks) } ${ lib.concatStrings (map (mount: '' '') guest.devices.mounts) } ${ with guest.devices.network; if enable then if interfaceType == "network" then '' ${ lib.optionalString (macAddress != null) '' '' } '' else if interfaceType == "bridge" then '' ${lib.optionalString (macAddress != null) '' ''} '' else if interfaceType == "macvlan" then '' ${lib.optionalString (macAddress != null) '' ''} '' else "" else "" } ${ lib.optionalString guest.devices.tablet '' '' } ${ lib.optionalString guest.devices.serial '' '' } ${ lib.optionalString guest.devices.qemuGuestAgent '' '' } ${ lib.optionalString guest.devices.audio.enable '' ''; in { "libvirt-guest-define-${name}" = { after = [ "libvirtd.service" ]; requires = [ "libvirtd.service" ]; wantedBy = lib.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/" '${xml}') ${lib.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 = lib.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); }