add module for declarative libvirt vm's

This commit is contained in:
Dmitriy Kholkin 2023-04-08 03:21:39 +03:00
parent e7e03a3cd4
commit faf851a55f

View File

@ -0,0 +1,353 @@
{ 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 != [ ];
};
vcpu = 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" ''
<domain type="kvm">
<name>${name}</name>
<uuid>UUID</uuid>
<memory unit="MiB">${toString guest.memory}</memory>
${
lib.optionalString guest.sharedMemory ''
<memoryBacking>
<source type="memfd"/>
<access mode="shared"/>
</memoryBacking>
''
}
<vcpu placement="static">${toString guest.vcpu}</vcpu>
<os>
<type arch="x86_64" machine="pc-q35-7.2">hvm</type>
${
lib.optionalString guest.uefi ''
<loader readonly="yes" type="pflash">/run/libvirt/nix-ovmf/OVMF_CODE.fd</loader>
<nvram template="/run/libvirt/nix-ovmf/OVMF_CODE.fd">/var/lib/libvirt/qemu/nvram/${name}_VARS.fd</nvram>
''
}
</os>
<features>
<acpi/>
<apic/>
<vmport state="off"/>
</features>
<cpu mode="host-passthrough" check="none" migratable="on"/>
<clock offset="utc">
<timer name="rtc" tickpolicy="catchup"/>
<timer name="pit" tickpolicy="delay"/>
<timer name="hpet" present="no"/>
</clock>
<pm>
<suspend-to-mem enabled="no"/>
<suspend-to-disk enabled="no"/>
</pm>
<devices>
<emulator>/run/libvirt/nix-emulators/qemu-system-x86_64</emulator>
${
lib.concatStrings (map (disk: ''
<disk type="file" device="disk">
<driver name="qemu" type="${disk.type}"/>
<source file="${disk.diskFile}"/>
<target dev="vda" bus="${disk.bus}"/>
</disk>
'') guest.devices.disks)
}
${
lib.concatStrings (map (mount: ''
<filesystem type="mount" accessmode="passthrough">
<driver type="virtiofs" queue="1024"/>
<binary path="/run/current-system/sw/bin/virtiofsd" xattr="on">
<cache mode="always"/>
<lock posix="on" flock="on"/>
</binary>
<source dir="${mount.sourceDir}"/>
<target dir="${mount.targetDir}"/>
</filesystem>
'') guest.devices.mounts)
}
${
with guest.devices.network;
if enable then
if interfaceType == "network" then ''
<interface type="network">
${
lib.optionalString (macAddress != null) ''
<mac address="${macAddress}"/>
''
}
<source network="${sourceDev}"/>
<model type="${modelType}"/>
</interface>
'' else if interfaceType == "bridge" then ''
<interface type="bridge">
${lib.optionalString (macAddress != null) ''
<mac address="${macAddress}"/>
''}
<source bridge="${sourceDev}"/>
<model type="${modelType}"/>
</interface>
'' else if interfaceType == "macvlan" then ''
<interface type="direct">
${lib.optionalString (macAddress != null) ''
<mac address="${macAddress}"/>
''}
<source dev="${sourceDev}" mode="bridge"/>
<model type="${modelType}"/>
</interface>
'' else
""
else
""
}
${
lib.optionalString guest.devices.tablet ''
<input type="tablet" bus="usb"/>
''
}
${
lib.optionalString guest.devices.serial ''
<serial type="pty"/>
''
}
${
lib.optionalString guest.devices.qemuGuestAgent ''
<channel type="unix">
<target type="virtio" name="org.qemu.guest_agent.0"/>
</channel>
''
}
${
lib.optionalString guest.devices.audio.enable ''
<audio id="1" type="${guest.devices.audio.type}"/>
<sound model="ich9"/>
''
}
${
if guest.devices.graphics.enable then
if guest.devices.graphics.type == "spice" then ''
<graphics type="spice" autoport="yes">
<listen type="address"/>
<image compression="off"/>
</graphics>
'' else
""
else
""
}
${
lib.optionalString guest.devices.video.enable ''
<video>
<model type="${guest.devices.video.type}" heads="1"/>
</video>
''
}
<channel type="spicevmc">
<target type="virtio" name="com.redhat.spice.0"/>
</channel>
<input type="mouse" bus="ps2"/>
<input type="keyboard" bus="ps2"/>
<redirdev bus='usb' type='spicevmc'/>
<memballoon model="virtio"/>
<rng model="virtio">
<backend model="random">/dev/urandom</backend>
</rng>
</devices>
</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) + ${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;
};
}