Drop the vm- prefix, add dash separator between VM name and interface index, and remove the 10-character VM name limit. Long names that would exceed IFNAMSIZ (15 chars) are truncated with VM ID appended for uniqueness (e.g., bankingsupe3-22 for id=3). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
145 lines
5.4 KiB
Nix
145 lines
5.4 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
|
|
isVmSwitchIface
|
|
;
|
|
|
|
# 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 cfg.nixosVms;
|
|
in
|
|
{
|
|
config = lib.mkIf cfg.enable {
|
|
assertions =
|
|
let
|
|
vmIds = map (vm: vm.id) cfg.nixosVms;
|
|
vmNames = map (vm: vm.name) cfg.nixosVms;
|
|
in
|
|
[
|
|
{
|
|
assertion = cfg.hostNetworking.nat.enable -> cfg.hostNetworking.nat.interface != "";
|
|
message = "programs.vmsilo.hostNetworking.nat.interface must be set when hostNetworking.nat.enable is true";
|
|
}
|
|
{
|
|
assertion = lib.length vmIds == lib.length (lib.unique vmIds);
|
|
message = "VM IDs must be unique";
|
|
}
|
|
{
|
|
assertion = lib.length vmNames == lib.length (lib.unique vmNames);
|
|
message = "VM names must be unique";
|
|
}
|
|
]
|
|
++ map (vm: {
|
|
assertion = vm.id >= 3 && vm.id <= 255 && lib.mod vm.id 2 == 1;
|
|
message = "VM '${vm.name}' has invalid id ${toString vm.id}. Must be odd number 3-255.";
|
|
}) cfg.nixosVms
|
|
# 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
|
|
) 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
|
|
ifaceNames = lib.attrNames vm.network.interfaces;
|
|
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";
|
|
}
|
|
]
|
|
# vmNetwork required when type = "vm-switch"
|
|
++ lib.mapAttrsToList (name: iface: {
|
|
assertion = iface.type == "vm-switch" -> iface.vmNetwork != null;
|
|
message = "VM '${vm.name}' interface '${name}': vmNetwork required when type = 'vm-switch'";
|
|
}) vm.network.interfaces
|
|
# vmNetwork only valid when type = "vm-switch"
|
|
++ lib.mapAttrsToList (name: iface: {
|
|
assertion = iface.type != "vm-switch" -> iface.vmNetwork == null;
|
|
message = "VM '${vm.name}' interface '${name}': vmNetwork only valid when type = 'vm-switch'";
|
|
}) vm.network.interfaces
|
|
# 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'";
|
|
}) vm.network.interfaces
|
|
# 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'";
|
|
}) vm.network.interfaces
|
|
# 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";
|
|
}) vm.network.interfaces
|
|
# 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
|
|
) vm.network.interfaces
|
|
)
|
|
# 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)
|
|
) vm.network.interfaces
|
|
)
|
|
) 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
|
|
) cfg.nixosVms;
|
|
};
|
|
}
|