vmix.nix/nixos/vms/submoduleOptions.nix
Git Sagar 702f723e6d windows: seal mode -- generic OOBE-deferred base, per-VM data on a config CD
A sealed image bakes no per-VM data. generalize.nix gains a gated
configMedium flag (false path byte-identical, so the shared macOS
generalize path is untouched): the baked answer file stays generic and
the target's first boot reads hostname/static-IP/timezone/tint off a
small removable CD via a finder (vmix-load-config.cmd) + applier
(vmix-apply-config.ps1), with the static-IP script dot-sourcing the same
config. templates.seal is a thin preset (delayOobeRun + configMedium +
D:\Users profiles + a data disk); images gain a .seal leaf next to
.generalize; makeConfigMedium renders the per-VM ISO.

So one sealed store path is shared by every VM, each mints its own SID
on first boot and builds the whole profile on D:, and only a cheap ISO
is per-VM. Also folds in the delayOobeRun reconcile: extraDisk (and the
audit-mode data-disk init) are gated off under deferral, so a sealed
image ships with no throwaway `data` output -- the target's specialize
formats the host zvol instead.

vms: disks.config.file attaches the config medium as a second CD-ROM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117qMyjpuXsjpVAcpJbFD8g
2026-09-16 13:01:12 -03:00

366 lines
12 KiB
Nix

{ config, pkgs, lib, vmixLib, ... }:
with lib;
{
options = {
autostart = mkOption {
type = types.bool;
default = false;
description = "Start VM on host boot.";
};
enable = mkOption {
type = types.bool;
default = true;
description = "Enable/disable VM service creation.";
};
vnc = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable VNC.";
};
addr = mkOption {
type = types.str;
default = "0.0.0.0";
description = "VNC bind address inside the VM namespace.";
};
port = mkOption {
type = types.ints.between 5900 5999;
default = 5900;
description = "VNC TCP port inside the VM namespace.";
};
forwardHostPort = mkOption {
type = types.nullOr types.port;
default = null;
description = "Optional host TCP port to auto-forward to this VM VNC port.";
};
websocketPort = mkOption {
type = types.nullOr types.port;
default = null;
description = "Optional websocket port for VNC (for noVNC-style clients).";
};
passwordFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Path to a runtime file containing the VNC password. When set, VNC auth is enabled via password-secret.";
};
sharePolicy = mkOption {
type = types.enum [ "allow-exclusive" "force-shared" "ignore" ];
default = "allow-exclusive";
description = "VNC client sharing policy.";
};
};
spice = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable SPICE display server.";
};
port = mkOption {
type = types.int;
default = 5930;
description = "SPICE TCP port inside the VM namespace.";
};
forwardHostPort = mkOption {
type = types.nullOr types.port;
default = null;
description = "Optional host TCP port to auto-forward to this VM SPICE port.";
};
addr = mkOption {
type = types.str;
default = "0.0.0.0";
description = "SPICE bind address inside the VM namespace.";
};
passwordFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Path to a runtime file containing SPICE password. When set, ticketing is enabled automatically, otherwise ticketing is disabled.";
};
agent.enable = mkOption {
type = types.bool;
default = true;
description = "Enable SPICE guest agent channel (clipboard/resolution helpers).";
};
usbRedir = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable SPICE USB redirection channels.";
};
channels = mkOption {
type = types.ints.between 1 16;
default = 4;
description = "Number of SPICE USB redirection channels to expose.";
};
};
displayDevice = mkOption {
type = types.enum [ "virtio" "qxl" "std" "none" ];
default = "qxl";
description = "QEMU -vga type to use with SPICE (qxl, virtio, std, none).";
};
vgamem = mkOption {
type = types.nullOr types.int;
default = null;
description = "Video memory in MB for QXL device. When set, uses -device qxl-vga instead of -vga qxl.";
};
};
nographic = mkOption {
type = types.bool;
default = true;
description = "Run QEMU without a graphical window (-nographic).";
};
cpu.cores = mkOption {
type = types.int;
default = 2;
description = "Number of CPU cores.";
};
cpu.model = mkOption {
type = types.str;
default = "host";
description = "CPU model.";
};
cpu.hideVirtualized = mkOption {
type = types.bool;
default = true;
description = "Hide hypervisor from guest. Prevents GPU driver Code 43 errors by stripping hypervisor CPUID leaf.";
};
kvm = mkOption {
type = types.bool;
default = true;
description = "Enable KVM.";
};
arch = mkOption {
type = types.str;
default = "x86_64";
description = "Architecture of the VM.";
};
pc.type = mkOption {
type = types.str;
default = "q35";
description = "PC type.";
};
bios.efi = mkOption {
type = types.bool;
default = true;
description = "Enable EFI BIOS.";
};
bios.tpm = mkOption {
type = types.bool;
default = false;
description = "Enable TPM BIOS.";
};
mem.size = mkOption {
type = types.int;
default = 1024;
description = "Memory size in MB.";
};
mem.balloon = mkOption {
type = types.bool;
default = false;
description = "Enable memory ballooning.";
};
disks.os.file = mkOption {
type = types.nullOr types.path;
default = null;
description = "Path to the OS disk image. Null for ISO-only VMs. For Windows, use a vmixLib.windows.* image config auto-detects via _vmixOsType metadata.";
};
disks.os.persist = mkOption {
type = types.bool;
default = false;
description = "Persist OS disk changes. The store image is copied to persistPath on first boot and QEMU writes to that mutable copy.";
};
disks.os.persistPath = mkOption {
type = types.str;
default = "";
description = "Mutable path for the persistent OS disk (e.g. /storage/vms/myvm/os.qcow2). Required when persist = true.";
};
disks.os.persistMode = mkOption {
type = types.enum [ "copy" "backing" ];
default = "copy";
description = ''
How the persistent OS disk is seeded from the store image (persist = true).
copy: a full cp of the store image; the mutable disk holds everything.
backing: a thin qcow2 overlay backing onto the shared store image, so many
VMs share one base and each holds only its own deltas. The store image (and
its backing chain) must then survive GC -- vmix pins it via a per-VM gcroot.
'';
};
disks.iso.file = mkOption {
type = types.nullOr (types.either types.path types.str);
description = "Path to the ISO file. Can be a Nix store path or a string path to a local file.";
default = null;
};
disks.config.file = mkOption {
type = types.nullOr (types.either types.path types.str);
default = null;
description = ''
A small read-only config medium attached as a second CD-ROM. For Windows
sealed images (templates.seal) this is the per-VM ISO from
vmixLib.windows.makeConfigMedium, carrying hostname/static-IP/etc. that
the image's baked first-boot scripts consume. Null to attach nothing.
'';
};
disks.add = mkOption {
default = {};
type = types.attrsOf (types.submodule {
options = {
file = mkOption {
type = types.str;
description = "String literal path to the additional disk.";
};
format = mkOption {
type = types.str;
description = "raw/qcow2 etc";
};
mounts = mkOption {
type = types.attrsOf types.str;
description = "Mount points for the additional disk.";
};
opts = mkOption {
type = types.str;
description = "additional options in QEMU args for this disk";
};
};
});
description = "Additional disks.";
};
shares = mkOption {
default = {};
type = types.attrsOf (types.submodule {
options = {
source = mkOption {
type = types.path;
description = "Source path for the shared directory.";
};
target = mkOption {
type = types.str;
description = "Target path inside the VM for the shared directory.";
};
};
});
description = "Shared directories.";
};
disks.bus = mkOption {
type = types.str;
default = "virtio";
description = "Bus type for the disks.";
};
boot.order = mkOption {
type = types.listOf (types.enum [ "os" "iso" "net" "floppy" ]);
description = "Boot order.";
default = [ "os" "iso" ];
};
boot.menu = mkOption {
type = types.bool;
default = false;
description = "Enable boot menu.";
};
nicModel = mkOption {
type = types.str;
default = "virtio-net-pci";
description = "QEMU NIC device model (e.g. virtio-net-pci, e1000).";
};
windows = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable Windows-optimized QEMU flags. Auto-enabled when disks.os.file carries _vmixOsType = \"windows\" metadata.";
};
};
tpm = {
stateDir = mkOption {
type = types.str;
default = "/tmp";
description = "Directory for TPM state persistence. Set to a /storage path for persistence across reboots.";
};
};
pci.passthrough = mkOption {
type = types.listOf types.str;
default = [];
description = "PCI device addresses to passthrough via VFIO (e.g. [\"0000:03:00.0\" \"0000:03:00.1\"]).";
};
pci.vgaPassthrough = mkOption {
type = types.bool;
default = true;
description = ''
Route legacy VGA to the first passthrough device (x-vga=on), which a
guest needs in order to drive that card as its own display.
Turn it off when the guest only forwards the device onward to a nested
guest: x-vga=on claims the VGA path the emulated adapter wants, and the
nested guest does its own routing anyway.
'';
};
pci.romFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "GPU VBIOS ROM file for the first passthrough device. Required when GPU PCI ROM BAR doesn't expose the full VBIOS (common with AMD Navi+).";
};
pci.viommu.enable = mkOption {
type = types.bool;
default = false;
description = ''
Give the guest a virtual Intel IOMMU, so a guest that is itself a
hypervisor can bind a passed-through device to vfio-pci and hand it on
to a nested guest. Without one the guest sees no IOMMU and cannot
re-assign anything it was given.
Implies kernel-irqchip=split, which interrupt remapping requires and
which replaces the full in-kernel irqchip cpu.hideVirtualized asks for.
'';
};
usb.hostDevices = mkOption {
default = [];
type = types.listOf (types.submodule {
options = {
vendorId = mkOption { type = types.str; description = "USB vendor ID (e.g. \"1d6b\")."; };
productId = mkOption { type = types.str; description = "USB product ID (e.g. \"0104\")."; };
};
});
description = "USB host devices to passthrough to the VM.";
};
networks.user.enable = mkOption {
type = types.bool;
default = false;
description = "enable qemu user networking";
};
networks.lans = mkOption {
default = {};
type = types.attrsOf (types.submodule {
options = {
mac = mkOption {
type = types.str;
description = "MAC address for the LAN interface.";
};
ip = mkOption {
type = types.nullOr (types.strMatching vmixLib.network.regex.ipv4);
default = null;
description = "assign static IP from the lan pool.";
};
};
});
description = "LAN interfaces.";
};
networks.macvtaps = mkOption {
default = {};
type = types.attrsOf (types.submodule {
options = {
mac = mkOption {
type = types.nullOr types.str;
default = null;
description = "MAC address for the MACVTap interface.";
};
};
});
description = "MACVTap interfaces.";
};
};
}