vmsilo/modules/assertions.nix
Davíð Steinn Geirsson 9055ffe954 refactor(modules): change TAP naming from vm-<name><idx> to <name>-<idx>
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>
2026-02-14 16:32:37 +00:00

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;
};
}