diff --git a/nixos/boxes/colony.nix b/nixos/boxes/colony.nix index 1b4c391..89e85a0 100644 --- a/nixos/boxes/colony.nix +++ b/nixos/boxes/colony.nix @@ -37,6 +37,12 @@ networking.bridge = "virtual"; }; }; + vms = { + instances.test = { + networks.virtual = {}; + vga = "none"; + }; + }; }; fileSystems = { @@ -82,6 +88,10 @@ }; #systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug"; + virtualisation = { + cores = 8; + memorySize = 8192; + }; }; }; } diff --git a/nixos/modules/_list.nix b/nixos/modules/_list.nix index 418aceb..84c5033 100644 --- a/nixos/modules/_list.nix +++ b/nixos/modules/_list.nix @@ -10,5 +10,6 @@ deploy-rs = ./deploy-rs.nix; secrets = ./secrets.nix; containers = ./containers.nix; + vms = ./vms.nix; }; } diff --git a/nixos/modules/build.nix b/nixos/modules/build.nix index a618ca0..b795c88 100644 --- a/nixos/modules/build.nix +++ b/nixos/modules/build.nix @@ -78,6 +78,8 @@ in diskImage = dummyOption; forwardPorts = dummyOption; sharedDirectories = dummyOption; + cores = dummyOption; + memorySize = dummyOption; }; }; diff --git a/nixos/modules/user.nix b/nixos/modules/user.nix index 400cfa9..59c5bfe 100644 --- a/nixos/modules/user.nix +++ b/nixos/modules/user.nix @@ -31,7 +31,7 @@ in name = mkDefault' "dev"; isNormalUser = true; uid = mkDefault 1000; - extraGroups = mkDefault [ "wheel" ]; + extraGroups = mkDefault [ "wheel" "kvm" ]; password = mkDefault "hunter2"; # TODO: secrets... shell = let shell = cfg.homeConfig.my.shell; diff --git a/nixos/modules/vms.nix b/nixos/modules/vms.nix new file mode 100644 index 0000000..d1b79f2 --- /dev/null +++ b/nixos/modules/vms.nix @@ -0,0 +1,119 @@ +{ lib, pkgs, config, ... }: +let + inherit (lib) optional optionals optionalString flatten concatStringsSep mapAttrsToList mapAttrs' mkIf mkDefault; + inherit (lib.my) mkOpt' mkBoolOpt'; + + flattenQEMUOpts = attrs: + concatStringsSep + "," + (mapAttrsToList + (k: v: if (v != null) then "${k}=${toString v}" else k) + attrs); + qemuOpts = with lib.types; coercedTo (attrsOf unspecified) flattenQEMUOpts str; + extraQEMUOpts = o: optionalString (o != "") ",${o}"; + + cfg = config.my.vms; + + netOpts = with lib.types; { name, ... }: { + options = { + bridge = mkOpt' str name "Network bridge to connect to."; + model = mkOpt' str "virtio-net" "Device type for network interface."; + extraOptions = mkOpt' qemuOpts { } "Extra QEMU options to set for the NIC."; + }; + }; + + vmOpts = with lib.types; { name, ... }: { + options = { + qemuBin = mkOpt' path "${pkgs.qemu_kvm}/bin/qemu-kvm" "Path to QEMU executable."; + qemuFlags = mkOpt' (listOf str) [ ] "Additional flags to pass to QEMU."; + autoStart = mkBoolOpt' true "Whether to start the VM automatically at boot."; + + machine = mkOpt' str "q35" "QEMU machine type."; + enableKVM = mkBoolOpt' true "Whether to enable KVM."; + enableUEFI = mkBoolOpt' true "Whether to enable UEFI."; + cpu = mkOpt' str "host" "QEMU CPU model."; + smp = { + cpus = mkOpt' ints.unsigned 1 "Number of CPU cores."; + threads = mkOpt' ints.unsigned 1 "Number of threads per core."; + }; + memory = mkOpt' ints.unsigned 1024 "Amount of RAM (mebibytes)."; + vga = mkOpt' str "qxl" "VGA card type."; + spice.enable = mkBoolOpt' true "Whether to enable SPICE."; + networks = mkOpt' (attrsOf (submodule netOpts)) { } "Networks to attach VM to."; + }; + }; + + mkQemuCommand = n: i: + let + flags = + i.qemuFlags ++ + [ + "name ${n}" + "machine ${i.machine}" + "cpu ${i.cpu}" + "smp cpus=${toString i.smp.cpus},threads=${toString i.smp.threads}" + "m ${toString i.memory}" + "nographic" + "vga ${i.vga}" + "chardev socket,id=monitor,path=/run/vms/${n}/monitor.sock,server=on,wait=off" + "mon chardev=monitor" + "chardev socket,id=tty,path=/run/vms/${n}/tty.sock,server=on,wait=off" + "device isa-serial,chardev=tty" + ] ++ + (optional i.enableKVM "enable-kvm") ++ + (optional i.spice.enable "spice unix=on,addr=/run/vms/${n}/spice.sock,disable-ticketing=on") ++ + (flatten (mapAttrsToList (nn: c: [ + "netdev bridge,id=${nn},br=${c.bridge}" + ("device ${c.model},netdev=${nn}" + (extraQEMUOpts c.extraOptions)) + ]) i.networks)) ++ + (optionals i.enableUEFI [ + "drive if=pflash,format=raw,unit=0,readonly=on,file=${cfg.ovmfPackage.fd}/FV/OVMF_CODE.fd" + "drive if=pflash,format=raw,unit=1,file=/var/lib/vms/${n}/ovmf_vars.bin" + ]); + args = map (v: "-${v}") flags; + in + concatStringsSep " " ([ i.qemuBin ] ++ args); +in +{ + options.my.vms = with lib.types; { + instances = mkOpt' (attrsOf (submodule vmOpts)) { } "VM instances."; + ovmfPackage = mkOpt' package pkgs.OVMF "OVMF package."; + }; + + config = mkIf (cfg.instances != { }) { + # qemu-bridge-helper will fail otherwise + environment.etc."qemu/bridge.conf".text = "allow all"; + systemd = { + services = mapAttrs' (n: i: { + name = "vm@${n}"; + value = { + description = "Virtual machine '${n}'"; + serviceConfig = { + ExecStart = mkQemuCommand n i; + RuntimeDirectory = "vms/${n}"; + StateDirectory = "vms/${n}"; + }; + + preStart = + '' + if [ ! -e "$STATE_DIRECTORY"/ovmf_vars.bin ]; then + cp "${cfg.ovmfPackage.fd}"/FV/OVMF_VARS.fd "$STATE_DIRECTORY"/ovmf_vars.bin + fi + ''; + postStart = + '' + socks=(monitor tty spice) + for s in ''${socks[@]}; do + path="$RUNTIME_DIRECTORY"/''${s}.sock + until [ -e "$path" ]; do sleep 0.1; done + chgrp kvm "$path" + chmod 770 "$path" + done + ''; + restartIfChanged = mkDefault false; + wantedBy = optional i.autoStart "machines.target"; + }; + }) cfg.instances; + }; + }; +}