vmsilo/modules/options.nix
Davíð Steinn Geirsson ba0f77acf7 feat(modules): add per-VM sharedHome option for virtiofs home directory
Shares a host directory as /home/user in guest VMs via virtiofs, enabled
by default. Accepts true (/shared/<vmname>), a custom path string, or
false to disable. Host directory is created with correct uid:gid ownership
at VM start.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 12:01:12 +00:00

732 lines
23 KiB
Nix

# Option definitions for vmsilo NixOS module
{
config,
pkgs,
lib,
...
}:
let
cfg = config.programs.vmsilo;
# Shared route submodule type for IPv4/IPv6 routes
routeSubmodule = lib.types.attrsOf (
lib.types.submodule {
options.via = lib.mkOption {
type = lib.types.str;
description = "Gateway address for this route.";
};
}
);
# Network interface configuration submodule (for vm.network.interfaces)
networkInterfaceSubmodule = lib.types.submodule {
options = {
type = lib.mkOption {
type = lib.types.enum [
"vm-switch"
"tap"
];
default = "vm-switch";
description = "Interface type: 'vm-switch' for VM-to-VM networking, 'tap' for host tap interface.";
};
macAddress = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "MAC address. Auto-generated from SHA1('<vmName>-<ifName>') if null.";
example = "52:ab:cd:ef:12:34";
};
vmNetwork = lib.mkOption {
type = lib.types.nullOr (
lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
description = "Network name for vm-switch.";
example = "internal";
};
receiveBroadcast = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether this VM receives broadcast traffic on this network.";
};
routes = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "CIDR prefixes this VM advertises to the network (e.g. \"192.168.1.0/24\"). Used for subnet routing via longest-prefix-match.";
example = [
"192.168.1.0/24"
"10.0.0.0/8"
];
};
};
}
);
default = null;
description = "VM-switch network configuration. Required when type = 'vm-switch'.";
};
tap = lib.mkOption {
type = lib.types.nullOr (
lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "TAP interface name on host. Defaults to '<vmname>-<ifIndex>'.";
};
hostAddress = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Host-side IP address with prefix (e.g., '10.0.0.254/24'). If null, the TAP is created without a host IP. Mutually exclusive with bridge.";
example = "10.0.0.254/24";
};
bridge = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Bridge name to add this TAP interface to (via networking.bridges). Mutually exclusive with hostAddress.";
example = "br0";
};
};
}
);
default = null;
description = "TAP interface configuration. Required when type = 'tap'.";
};
dhcp = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable DHCP for this interface.";
};
addresses = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Static IPv4 addresses with prefix (e.g., '10.0.0.1/24').";
example = [ "10.0.0.1/24" ];
};
routes = lib.mkOption {
type = routeSubmodule;
default = { };
description = "IPv4 routes. Keys are destination CIDRs, values have 'via' gateway.";
example = lib.literalExpression ''{ "0.0.0.0/0" = { via = "10.0.0.254"; }; }'';
};
v6Addresses = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Static IPv6 addresses with prefix.";
example = [ "fc00::2/64" ];
};
v6Routes = lib.mkOption {
type = routeSubmodule;
default = { };
description = "IPv6 routes. Keys are destination CIDRs, values have 'via' gateway.";
example = lib.literalExpression ''{ "::/0" = { via = "fc00::1"; }; }'';
};
};
};
# Network configuration submodule (for vm.network)
networkSubmodule = lib.types.submodule {
options = {
nameservers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "DNS nameservers for this VM.";
example = [
"8.8.8.8"
"1.1.1.1"
];
};
interfaces = lib.mkOption {
type = lib.types.attrsOf networkInterfaceSubmodule;
default = { };
description = "Network interfaces for this VM. Keys are guest-visible interface names.";
};
};
};
# Type for attribute values in crosvm argument attrsets
attrValueType = lib.types.oneOf [
lib.types.str
lib.types.int
lib.types.bool
];
# Type for crosvm argument attrsets (used for gpu, sound, etc.)
crosvmAttrsetType = lib.types.attrsOf attrValueType;
# Type for free-form attrsets (disks, pciDevices, sharedDirectories, vhostUser)
freeformAttrsetType = lib.types.attrsOf (lib.types.nullOr attrValueType);
vmSubmodule = lib.types.submodule {
options = {
id = lib.mkOption {
type = lib.types.int;
description = "VM ID (odd number 3-255). Used for IP address and vsock CID.";
example = 3;
};
name = lib.mkOption {
type = lib.types.str;
description = "VM name. Used for TAP interface naming and scripts.";
example = "banking";
};
color = lib.mkOption {
type = lib.types.str;
default = "red";
description = ''
Window decoration color for this VM. Used in wayland security context.
Supported formats: named colors ("red", "green"), hex ("#FF0000"), RGB ("rgb(255,0,0)").
'';
example = "#3498db";
};
waylandProxy = lib.mkOption {
type = lib.types.enum [
"wayland-proxy-virtwl"
"sommelier"
];
default = "wayland-proxy-virtwl";
description = "Wayland proxy to use in the guest VM.";
example = "sommelier";
};
memory = lib.mkOption {
type = lib.types.int;
default = 1024;
description = "Memory allocation in MB.";
example = 4096;
};
cpus = lib.mkOption {
type = lib.types.int;
default = 2;
description = "Number of virtual CPUs.";
example = 4;
};
network = lib.mkOption {
type = networkSubmodule;
default = { };
description = "Network configuration for this VM.";
example = lib.literalExpression ''
{
nameservers = [ "8.8.8.8" ];
interfaces = {
wan = {
type = "tap";
tap.hostAddress = "10.0.0.254/24";
addresses = [ "10.0.0.1/24" ];
routes."0.0.0.0/0" = { via = "10.0.0.254"; };
};
};
}
'';
};
autoShutdown = lib.mkOption {
type = lib.types.submodule {
options = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable auto-shutdown when idle. VM shuts down after autoShutdown.after seconds with no active commands.";
};
after = lib.mkOption {
type = lib.types.int;
default = 60;
description = "Seconds to wait after last command exits before shutting down.";
};
};
};
default = { };
description = "Auto-shutdown configuration for idle VMs.";
};
autoStart = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Start this VM automatically at boot instead of on-demand via socket activation.";
};
rootOverlay = lib.mkOption {
type = lib.types.submodule {
options = {
type = lib.mkOption {
type = lib.types.enum [
"qcow2"
"tmpfs"
];
default = "qcow2";
description = "Overlay upper layer type: 'qcow2' (disk-backed, default) or 'tmpfs' (RAM-backed).";
};
size = lib.mkOption {
type = lib.types.str;
default = "10G";
description = "Maximum size of ephemeral disk (qcow2 only). Parsed by qemu-img create.";
example = "20G";
};
};
};
default = { };
description = "Root overlay configuration. Controls where VM writes go.";
};
dependsOn = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "List of VM names to start when this VM starts.";
};
additionalDisks = lib.mkOption {
type = lib.types.listOf freeformAttrsetType;
default = [ ];
description = ''
Additional disks to attach to the VM. Each attrset requires `path` (positional).
Other keys are passed directly to crosvm --block (e.g., ro, sparse, block-size, id).
'';
example = lib.literalExpression ''
[{
path = "/tmp/data.qcow2";
ro = false;
sparse = true;
block-size = 4096;
id = "datadisk";
}]
'';
};
rootDisk = lib.mkOption {
type = lib.types.nullOr freeformAttrsetType;
default = null;
description = ''
Custom root disk. If not set, uses the built rootfs image with rootDiskReadonly.
Requires `path` (positional). Other keys are passed directly to crosvm --block.
'';
example = lib.literalExpression ''
{
path = "/path/to/custom-root.qcow2";
ro = true;
}
'';
};
kernel = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Custom kernel image. If not set, uses the built rootfs kernel.";
example = "/path/to/bzImage";
};
initramfs = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Custom initramfs. If not set, uses the built rootfs initrd.";
example = "/path/to/initrd";
};
rootDiskReadonly = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether the built rootfs should be read-only. Ignored when rootDisk is set.";
};
sharedHome = lib.mkOption {
type = lib.types.either lib.types.bool lib.types.str;
default = true;
description = ''
Share a host directory as /home/user in the guest via virtiofs.
Set to true for default path (/shared/<vmname>), a string for a custom host path, or false to disable.
'';
example = "/data/vms/banking-home";
};
kernelParams = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra kernel command line parameters.";
example = [
"acpi=off"
"debug"
];
};
gpu = lib.mkOption {
type = lib.types.either lib.types.bool crosvmAttrsetType;
default = true;
description = ''
GPU configuration. Set to false to disable, true for default config,
or an attrset for custom --gpu options.
'';
example = lib.literalExpression ''
{
context-types = "cross-domain:virgl2";
width = 1920;
height = 1080;
}
'';
};
sound = lib.mkOption {
type = lib.types.either lib.types.bool crosvmAttrsetType;
default = false;
description = ''
Sound configuration. Set to false to disable, true for default PulseAudio config,
or an attrset for custom --virtio-snd options.
'';
example = lib.literalExpression ''
{
backend = "pulse";
capture = true;
}
'';
};
sharedDirectories = lib.mkOption {
type = lib.types.listOf freeformAttrsetType;
default = [ ];
description = ''
Shared directories for --shared-dir. Each attrset requires `path` and `tag`.
Other keys are passed as key=value pairs with colon separator.
'';
example = lib.literalExpression ''
[{
path = "/tmp/shared";
tag = "shared";
type = "fs";
uid = 1000;
}]
'';
};
vhostUser = lib.mkOption {
type = lib.types.listOf freeformAttrsetType;
default = [ ];
description = ''
vhost-user devices for --vhost-user. Each attrset is formatted as key=value pairs.
Typically auto-populated from vmNetwork with type=net.
'';
example = lib.literalExpression ''
[{
type = "net";
socket = "/run/vm-switch/internet/banking/client.sock";
}]
'';
};
pciDevices = lib.mkOption {
type = lib.types.listOf freeformAttrsetType;
default = [ ];
description = ''
PCI devices to pass through via --vfio. Each attrset requires `path` (BDF or sysfs path).
BDF format ("01:00.0" or "0000:01:00.0") is auto-converted to sysfs path.
Other keys are passed as key=value pairs (e.g., iommu=on).
Devices must also be listed in isolatedPciDevices.
'';
example = lib.literalExpression ''
[{
path = "01:00.0";
iommu = "on";
}]
'';
};
guestPrograms = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
description = "VM-specific packages to include in the rootfs.";
example = lib.literalExpression "[ pkgs.firefox ]";
};
guestConfig = lib.mkOption {
type = lib.types.either lib.types.attrs (lib.types.functionTo lib.types.attrs);
default = { };
description = ''
VM-specific NixOS configuration.
Can be an attrset or a function. If a function, it receives vmConfig
(the VM configuration with enriched interfaces including computed name fields).
'';
example = lib.literalExpression ''
# As attrs
{ fileSystems."/home/user" = { device = "/dev/vdb"; fsType = "ext4"; }; }
# As function (access interface names via attrset keys)
vmConfig: {
networking.firewall.interfaces.wan.allowedTCPPorts = [ 22 ];
}
'';
};
crosvm = lib.mkOption {
type = lib.types.submodule {
options = {
logLevel = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Per-VM log level override. If null, uses global crosvm.logLevel.";
example = "debug";
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Per-VM extra arguments passed to crosvm before 'run'. Appended to global crosvm.extraArgs.";
example = [
"--syslog-tag"
"banking"
];
};
extraRunArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Per-VM extra arguments passed to crosvm after 'run'. Appended to global crosvm.extraRunArgs.";
example = [ "--disable-sandbox" ];
};
};
};
default = { };
description = "Per-VM crosvm configuration.";
};
};
};
in
{
options.programs.vmsilo = {
enable = lib.mkEnableOption "vmsilo VM management";
enableBashIntegration = lib.mkEnableOption "Bash completion for vmsilo commands" // {
default = true;
};
crosvm = lib.mkOption {
type = lib.types.submodule {
options = {
logLevel = lib.mkOption {
type = lib.types.str;
default = "info";
description = "Log level for crosvm (error, warn, info, debug, trace).";
example = "debug";
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra arguments passed to crosvm before 'run' subcommand.";
example = [
"--syslog-tag"
"vmsilo"
];
};
extraRunArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra arguments passed to crosvm after 'run' subcommand.";
example = [ "--disable-sandbox" ];
};
};
};
default = { };
description = "Global crosvm configuration.";
};
user = lib.mkOption {
type = lib.types.str;
description = "User who owns TAP interfaces and runs VMs. Required.";
example = "david";
};
hostNetworking = lib.mkOption {
type = lib.types.submodule {
options = {
nat = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable NAT for VM internet access.";
};
interface = lib.mkOption {
type = lib.types.str;
default = "";
description = "External interface for NAT (required if nat.enable is true).";
example = "eth0";
};
};
};
};
default = { };
description = "Host networking configuration for NAT.";
};
isolatedPciDevices = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
PCI devices to isolate from the host using vfio-pci.
Accepts BDF format: "01:00.0" or "0000:01:00.0".
These devices will be claimed by vfio-pci at early boot.
Note: For maximum reliability, also blacklist native drivers
using boot.blacklistedKernelModules.
'';
example = [
"01:00.0"
"02:00.0"
];
};
nixosVms = lib.mkOption {
type = lib.types.listOf vmSubmodule;
default = [ ];
description = "List of NixOS-based VMs to create.";
example = lib.literalExpression ''
[{
id = 3;
name = "banking";
memory = 4096;
cpus = 4;
guestPrograms = [ pkgs.firefox ];
}]
'';
};
vm-switch = lib.mkOption {
type = lib.types.submodule {
options = {
logLevel = lib.mkOption {
type = lib.types.str;
default = "info";
description = "Log level for vm-switch (error, warn, info, debug, trace).";
example = "debug";
};
bufferSize = lib.mkOption {
type = lib.types.str;
default = "256k";
description = "Ring buffer data region size per peer (e.g. \"256k\", \"131072\").";
example = "512k";
};
fqCodelTarget = lib.mkOption {
type = lib.types.str;
default = "5ms";
description = "FQ-CoDel target sojourn time (e.g. \"5ms\", \"10ms\").";
example = "10ms";
};
fqCodelInterval = lib.mkOption {
type = lib.types.str;
default = "100ms";
description = "FQ-CoDel measurement interval (e.g. \"100ms\", \"1s\").";
example = "100ms";
};
fqCodelLimit = lib.mkOption {
type = lib.types.str;
default = "2m";
description = "FQ-CoDel hard byte limit per peer (e.g. \"2m\", \"1048576\").";
example = "4m";
};
fqCodelQuantum = lib.mkOption {
type = lib.types.int;
default = 1514;
description = "FQ-CoDel DRR quantum in bytes (bytes per round per flow).";
example = 3028;
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra command line arguments to pass to vm-switch.";
example = [ "--some-flag" ];
};
};
};
default = { };
description = "VM-switch daemon configuration.";
};
# Internal options for dependency injection from flake.nix and inter-module communication
_internal = {
crosvm = lib.mkOption {
type = lib.types.package;
description = "crosvm package (injected by flake).";
internal = true;
};
wayland-proxy-virtwl = lib.mkOption {
type = lib.types.package;
description = "wayland-proxy-virtwl package (injected by flake).";
internal = true;
};
sommelier = lib.mkOption {
type = lib.types.package;
description = "sommelier package (injected by flake).";
internal = true;
};
vm-switch = lib.mkOption {
type = lib.types.package;
description = "vm-switch package (injected by flake).";
internal = true;
};
# Generated scripts (set by scripts.nix, consumed by services.nix and package.nix)
vmScripts = lib.mkOption {
type = lib.types.attrsOf lib.types.path;
description = "VM launcher scripts (name -> script path).";
internal = true;
default = { };
};
proxyScripts = lib.mkOption {
type = lib.types.attrsOf lib.types.path;
description = "Proxy scripts (name -> script path).";
internal = true;
default = { };
};
userScripts = lib.mkOption {
type = lib.types.attrsOf lib.types.path;
description = "User-facing scripts (vm-run, vm-start, etc).";
internal = true;
default = { };
};
desktopFilesPackage = lib.mkOption {
type = lib.types.package;
description = "Package containing .desktop files for VM apps.";
internal = true;
};
bashCompletionScript = lib.mkOption {
type = lib.types.path;
description = "Bash completion script for vm-* commands.";
internal = true;
};
};
# Output: generated package with all scripts
package = lib.mkOption {
type = lib.types.package;
readOnly = true;
description = "Generated package containing VM launcher scripts.";
};
};
}