267 lines
10 KiB
Nix
267 lines
10 KiB
Nix
# Validation assertions for vmsilo NixOS module
|
|
{
|
|
config,
|
|
lib,
|
|
...
|
|
}:
|
|
|
|
let
|
|
cfg = config.programs.vmsilo;
|
|
helpers = import ./lib/helpers.nix { inherit lib; };
|
|
inherit (helpers)
|
|
isValidCIDR
|
|
extractBdf
|
|
mkEffectiveSharedDirs
|
|
parseCIDR
|
|
allocateNetvmSubnet
|
|
ipToInt
|
|
assignVmIds
|
|
;
|
|
|
|
# User UID/GID for shared directory assertions
|
|
userUid = config.users.users.${cfg.user}.uid;
|
|
userGid = config.users.groups.${config.users.users.${cfg.user}.group}.gid;
|
|
|
|
# Normalize all isolated devices
|
|
normalizedIsolatedDevices = map helpers.normalizeBdf cfg.isolatedPciDevices;
|
|
|
|
# Get normalized PCI devices for a VM (extract path from attrset)
|
|
normalizedVmPciDevices = vm: map (dev: extractBdf dev.path) vm.pciDevices;
|
|
|
|
# All PCI devices across all VMs (for duplicate check)
|
|
allVmPciDevices = lib.concatMap normalizedVmPciDevices (lib.attrValues cfg.nixosVms);
|
|
|
|
# netvm helpers
|
|
vmsForNetvm = assignVmIds cfg.nixosVms;
|
|
clientVmsForAssert = lib.filter (vm: vm.network.netvm != null) vmsForNetvm;
|
|
parsedNetvmRange = parseCIDR cfg.netvmRange;
|
|
|
|
computeBaseAddr =
|
|
clientVm:
|
|
let
|
|
auto =
|
|
allocateNetvmSubnet clientVm.network.netvm clientVm.name parsedNetvmRange.ip
|
|
parsedNetvmRange.prefix;
|
|
in
|
|
if clientVm.network.netvmSubnet != null then
|
|
ipToInt (parseCIDR clientVm.network.netvmSubnet).ip
|
|
else
|
|
auto.baseAddr;
|
|
|
|
allBaseAddrs = map computeBaseAddr clientVmsForAssert;
|
|
in
|
|
{
|
|
config = lib.mkIf cfg.enable {
|
|
assertions =
|
|
let
|
|
vmNames = lib.attrNames cfg.nixosVms;
|
|
in
|
|
[
|
|
{
|
|
assertion = !lib.hasAttr "host" cfg.nixosVms;
|
|
message = "VM name 'host' is reserved for use with network.netvm = \"host\"";
|
|
}
|
|
]
|
|
# PCI passthrough assertions
|
|
++ lib.concatMap (
|
|
vm:
|
|
map (
|
|
dev:
|
|
let
|
|
normalizedDev = extractBdf dev.path;
|
|
in
|
|
{
|
|
assertion = lib.elem normalizedDev normalizedIsolatedDevices;
|
|
message = "VM '${vm.name}' uses PCI device ${dev.path} which is not in isolatedPciDevices";
|
|
}
|
|
) vm.pciDevices
|
|
) (lib.attrValues cfg.nixosVms)
|
|
++ [
|
|
{
|
|
assertion = lib.length allVmPciDevices == lib.length (lib.unique allVmPciDevices);
|
|
message = "PCI devices cannot be assigned to multiple VMs";
|
|
}
|
|
{
|
|
assertion = config.users.users.${cfg.user}.uid != null;
|
|
message = "programs.vmsilo.user '${cfg.user}' must have an explicit uid set in users.users";
|
|
}
|
|
]
|
|
# Network interface assertions
|
|
++ lib.concatMap (
|
|
vm:
|
|
let
|
|
effectiveIfaces =
|
|
vm.network.interfaces // (cfg._internal.netvmInjections.${vm.name}.interfaces or { });
|
|
ifaceNames = lib.attrNames effectiveIfaces;
|
|
in
|
|
# Max 15 interfaces per VM (PCI slots 16-31)
|
|
[
|
|
{
|
|
assertion = lib.length ifaceNames <= 15;
|
|
message = "VM '${vm.name}' has ${toString (lib.length ifaceNames)} interfaces, max is 15";
|
|
}
|
|
]
|
|
# Validate interface name format
|
|
++ lib.mapAttrsToList (name: _iface: {
|
|
assertion = builtins.match "[a-zA-Z][a-zA-Z0-9_-]{0,14}" name != null;
|
|
message = "VM '${vm.name}' interface '${name}': invalid name (must start with a letter, contain only letters/digits/hyphens/underscores, max 15 chars)";
|
|
}) effectiveIfaces
|
|
# Validate host TAP name format when explicitly set
|
|
++ lib.concatLists (
|
|
lib.mapAttrsToList (
|
|
name: iface:
|
|
lib.optional (iface.tap != null && iface.tap.name != null) {
|
|
assertion = builtins.match "[a-zA-Z][a-zA-Z0-9_-]{0,14}" iface.tap.name != null;
|
|
message = "VM '${vm.name}' interface '${name}': tap.name '${iface.tap.name}' is invalid (must start with a letter, contain only letters/digits/hyphens/underscores, max 15 chars)";
|
|
}
|
|
) effectiveIfaces
|
|
)
|
|
# tap required when type = "tap"
|
|
++ lib.mapAttrsToList (name: iface: {
|
|
assertion = iface.type == "tap" -> iface.tap != null;
|
|
message = "VM '${vm.name}' interface '${name}': tap required when type = 'tap'";
|
|
}) effectiveIfaces
|
|
# tap only valid when type = "tap"
|
|
++ lib.mapAttrsToList (name: iface: {
|
|
assertion = iface.type != "tap" -> iface.tap == null;
|
|
message = "VM '${vm.name}' interface '${name}': tap only valid when type = 'tap'";
|
|
}) effectiveIfaces
|
|
# tap.hostAddress and tap.bridge are mutually exclusive
|
|
++ lib.mapAttrsToList (name: iface: {
|
|
assertion =
|
|
iface.type == "tap" && iface.tap != null
|
|
-> !(iface.tap.hostAddress != null && iface.tap.bridge != null);
|
|
message = "VM '${vm.name}' interface '${name}': tap.hostAddress and tap.bridge are mutually exclusive";
|
|
}) effectiveIfaces
|
|
# Validate addresses are valid CIDR
|
|
++ lib.concatLists (
|
|
lib.mapAttrsToList (
|
|
name: iface:
|
|
map (addr: {
|
|
assertion = isValidCIDR addr;
|
|
message = "VM '${vm.name}' interface '${name}': address '${addr}' is not valid CIDR notation";
|
|
}) iface.addresses
|
|
) effectiveIfaces
|
|
)
|
|
# Validate route destinations are valid CIDR
|
|
++ lib.concatLists (
|
|
lib.mapAttrsToList (
|
|
name: iface:
|
|
map (dest: {
|
|
assertion = isValidCIDR dest;
|
|
message = "VM '${vm.name}' interface '${name}': route destination '${dest}' is not valid CIDR notation";
|
|
}) (lib.attrNames iface.routes)
|
|
) effectiveIfaces
|
|
)
|
|
) (lib.attrValues cfg.nixosVms)
|
|
# dependsOn assertions: referenced VM names must exist
|
|
++ lib.concatMap (
|
|
vm:
|
|
map (depName: {
|
|
assertion = lib.elem depName vmNames;
|
|
message = "VM '${vm.name}' has dependsOn '${depName}' which is not a defined VM name";
|
|
}) vm.dependsOn
|
|
) (lib.attrValues cfg.nixosVms)
|
|
# Shared directory assertions: translateUid/translateGid incompatible with posixAcl
|
|
++ lib.concatMap (
|
|
vm:
|
|
let
|
|
effectiveSharedDirs = mkEffectiveSharedDirs {
|
|
inherit (vm) sharedDirectories sharedHome;
|
|
vmName = vm.name;
|
|
inherit userUid userGid;
|
|
};
|
|
in
|
|
lib.concatLists (
|
|
lib.mapAttrsToList (
|
|
tag: d:
|
|
lib.optional (d.posixAcl && (d.translateUid != null || d.translateGid != null)) {
|
|
assertion = false;
|
|
message = "VM '${vm.name}' shared dir '${tag}': translateUid/translateGid are incompatible with posixAcl";
|
|
}
|
|
) effectiveSharedDirs
|
|
)
|
|
) (lib.attrValues cfg.nixosVms)
|
|
# USB device assertions: no duplicate VID:PID+serial across VMs
|
|
++ (
|
|
let
|
|
allUsbDeviceKeys = lib.concatMap (
|
|
vm: map (dev: "${dev.vendorId}:${dev.productId}:${toString (dev.serial or "")}") vm.usbDevices
|
|
) (lib.attrValues cfg.nixosVms);
|
|
in
|
|
[
|
|
{
|
|
assertion = lib.length allUsbDeviceKeys == lib.length (lib.unique allUsbDeviceKeys);
|
|
message = "USB devices (VID:PID+serial) cannot be assigned to multiple VMs";
|
|
}
|
|
]
|
|
)
|
|
# netvm: network.netvm must reference a VM with isNetvm = true (skip for "host")
|
|
++ lib.concatMap (
|
|
vm:
|
|
lib.optionals (vm.network.netvm != "host") [
|
|
{
|
|
assertion = lib.hasAttr vm.network.netvm cfg.nixosVms;
|
|
message = "VM '${vm.name}': network.netvm = '${vm.network.netvm}' is not a defined VM name";
|
|
}
|
|
{
|
|
assertion =
|
|
!lib.hasAttr vm.network.netvm cfg.nixosVms || cfg.nixosVms.${vm.network.netvm}.network.isNetvm;
|
|
message = "VM '${vm.name}': network.netvm = '${vm.network.netvm}' does not have network.isNetvm = true";
|
|
}
|
|
]
|
|
++ [
|
|
{
|
|
assertion = !lib.hasAttr "upstream" vm.network.interfaces;
|
|
message = "VM '${vm.name}': cannot set both network.netvm and network.interfaces.upstream (name conflict)";
|
|
}
|
|
{
|
|
assertion =
|
|
vm.network.netvmSubnet == null
|
|
|| (
|
|
let
|
|
ip = ipToInt (parseCIDR vm.network.netvmSubnet).ip;
|
|
in
|
|
ip / 2 * 2 == ip
|
|
);
|
|
message = "VM '${vm.name}': network.netvmSubnet '${toString vm.network.netvmSubnet}' must specify the lower (even) /31 address. The netvm automatically takes clientIp + 1.";
|
|
}
|
|
]
|
|
) clientVmsForAssert
|
|
# netvm: no /31 collisions across all (netvm, client) pairs
|
|
++ lib.optional (clientVmsForAssert != [ ]) {
|
|
assertion = lib.length allBaseAddrs == lib.length (lib.unique allBaseAddrs);
|
|
message = ''
|
|
netvm subnet collision detected: two or more VM pairs hashed to the same /31.
|
|
Use network.netvmSubnet on one of the conflicting VMs to assign an explicit subnet.
|
|
'';
|
|
}
|
|
# cloud-hypervisor: USB passthrough is not supported
|
|
++ [
|
|
{
|
|
assertion = lib.all (vm: vm.hypervisor != "cloud-hypervisor" || vm.usbDevices == [ ]) (
|
|
lib.attrValues cfg.nixosVms
|
|
);
|
|
message = ''
|
|
vmsilo: USB passthrough is not supported for cloud-hypervisor VMs.
|
|
VMs using hypervisor = "cloud-hypervisor" cannot have usbDevices configured.
|
|
Affected VMs: ${
|
|
lib.concatMapStringsSep ", " (vm: vm.name) (
|
|
lib.filter (vm: vm.hypervisor == "cloud-hypervisor" && vm.usbDevices != [ ]) (
|
|
lib.attrValues cfg.nixosVms
|
|
)
|
|
)
|
|
}
|
|
'';
|
|
}
|
|
];
|
|
|
|
warnings =
|
|
lib.optional
|
|
(
|
|
cfg.schedulerIsolation == "full"
|
|
&& lib.any (vm: vm.hypervisor == "cloud-hypervisor") (lib.attrValues cfg.nixosVms)
|
|
)
|
|
"vmsilo: schedulerIsolation = \"full\" behaves as \"vm\" for cloud-hypervisor VMs. Cloud-hypervisor only supports VM-level core scheduling.";
|
|
};
|
|
}
|