nixos: Add initial QEMU-based VM module
This commit is contained in:
		@@ -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;
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,5 +10,6 @@
 | 
			
		||||
    deploy-rs = ./deploy-rs.nix;
 | 
			
		||||
    secrets = ./secrets.nix;
 | 
			
		||||
    containers = ./containers.nix;
 | 
			
		||||
    vms = ./vms.nix;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -78,6 +78,8 @@ in
 | 
			
		||||
      diskImage = dummyOption;
 | 
			
		||||
      forwardPorts = dummyOption;
 | 
			
		||||
      sharedDirectories = dummyOption;
 | 
			
		||||
      cores = dummyOption;
 | 
			
		||||
      memorySize = dummyOption;
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										119
									
								
								nixos/modules/vms.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								nixos/modules/vms.nix
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user