diff --git a/modules/libvirt-guests/default.nix b/modules/libvirt-guests/default.nix new file mode 100644 index 0000000..b5b2110 --- /dev/null +++ b/modules/libvirt-guests/default.nix @@ -0,0 +1,389 @@ +{ 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"; + }; + }; + 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 = { name, ... }: { + options = rec { + # TODO + guestOsType = mkOption { + type = 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); }; + mounts = + mkOption { type = with types; listOf (submodule mountOptions); }; + tablet = mkOption { + type = types.bool; + default = true; + }; + serial = mkOption { + type = types.bool; + default = true; + }; + qemuGuestAgent = mkOption { + type = types.bool; + default = true; + }; + 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.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" '' + + ${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 '' + 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) + ${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; + }; +}