Files
nixpkgs/nixos/lib/testing/network.nix
Alyssa Ross beeb739e48 nixos/test-driver: fix corrupt hosts files
primaryIPAddress is defined using lib.optionalString.  If it is the
empty string (because there are no assigned IP addresses), and the
machine has a hostname, we don't want to generate an invalid hosts
file with that empty string as the first (IP address) field.
2026-07-23 22:25:21 +02:00

214 lines
6.6 KiB
Nix

testModuleArgs@{
lib,
...
}:
let
inherit (lib)
attrNames
concatMapAttrsStringSep
concatMapStrings
forEach
head
listToAttrs
mkDefault
mkOption
nameValuePair
optionalString
range
toLower
types
zipListsWith
zipLists
;
nodeNumbers = listToAttrs (
zipListsWith nameValuePair (attrNames testModuleArgs.config.allMachines) (range 1 254)
);
networkModule =
{ config, ... }:
let
interfaces = lib.attrValues config.virtualisation.allInterfaces;
# Automatically assign IP addresses to requested interfaces.
assignIPs = lib.filter (i: i.assignIP) interfaces;
ipInterfaces = forEach assignIPs (
i:
nameValuePair i.name {
ipv4.addresses = [
{
address = "192.168.${toString i.vlan}.${toString config.virtualisation.test.nodeNumber}";
prefixLength = 24;
}
];
ipv6.addresses = [
{
address = "2001:db8:${toString i.vlan}::${toString config.virtualisation.test.nodeNumber}";
prefixLength = 64;
}
];
}
);
networkConfig = {
networking.hostName = mkDefault config.virtualisation.test.nodeName;
networking.interfaces = listToAttrs ipInterfaces;
networking.primaryIPAddress =
optionalString (ipInterfaces != [ ])
(head (head ipInterfaces).value.ipv4.addresses).address;
networking.primaryIPv6Address =
optionalString (ipInterfaces != [ ])
(head (head ipInterfaces).value.ipv6.addresses).address;
# Generate /etc/hosts including every remote's primary IP addresses
# (whichever VLAN they may belong to) as well as all IP addresses from
# VLANs that both the local machine and the remote machine share.
networking.extraHosts =
let
localVlans = config.virtualisation.vlans;
in
concatMapAttrsStringSep "" (
mName: remoteConfig:
let
remoteInterfaces = remoteConfig.networking.interfaces;
sharedIps = lib.flatten (
lib.mapAttrsToList (
ifaceName: ifaceCfg:
let
remoteIfaceMeta = remoteConfig.virtualisation.allInterfaces."${ifaceName}" or { };
vlanId = remoteIfaceMeta.vlan or null;
in
if vlanId != null && builtins.elem vlanId localVlans then
builtins.map (addr: addr.address) ifaceCfg.ipv4.addresses
++ builtins.map (addr: addr.address) ifaceCfg.ipv6.addresses
else
[ ]
) remoteInterfaces
);
# We also want to test router protocols that enable connections
# between nodes even if they don't share a VLAN, so we include
# the primary IPs of all machines in the hosts file.
primaryIPs = lib.remove "" [
remoteConfig.networking.primaryIPAddress
remoteConfig.networking.primaryIPv6Address
];
allReachableIps = lib.lists.uniqueStrings (sharedIps ++ primaryIPs);
hostnames =
optionalString (
remoteConfig.networking.domain != null
) "${remoteConfig.networking.hostName}.${remoteConfig.networking.domain} "
+ "${remoteConfig.networking.hostName}\n";
in
builtins.concatStringsSep "" (map (ip: "${ip} ${hostnames}") allReachableIps)
) testModuleArgs.config.allMachines;
};
in
{
key = "network-interfaces";
config = networkConfig // {
# Expose the networkConfig items for tests like nixops
# that need to recreate the network config.
system.build.networkConfig = networkConfig;
};
};
qemuNetworkModule =
{ config, pkgs, ... }:
let
qemu-common = import ../qemu-common.nix { inherit (pkgs) lib stdenv; };
interfaces = lib.attrValues config.virtualisation.allInterfaces;
interfacesNumbered = zipLists interfaces (range 1 255);
qemuOptions = lib.flatten (
forEach interfacesNumbered (
{ fst, snd }: qemu-common.qemuNICFlags snd fst.vlan config.virtualisation.test.nodeNumber
)
);
udevRules = map (
interface:
lib.concatStringsSep ", " [
''SUBSYSTEM=="net"''
''ACTION=="add"''
# MAC Addresses for QEMU network devices are lowercase, and udev string comparison is case-sensitive.
''ATTR{address}=="${toLower (qemu-common.qemuNicMac interface.vlan config.virtualisation.test.nodeNumber)}"''
''NAME="${interface.name}"''
]
) interfaces;
in
{
virtualisation.qemu.options = qemuOptions;
boot.initrd.services.udev.rules = concatMapStrings (x: x + "\n") udevRules;
};
nodeNumberModule = (
regular@{ config, name, ... }:
{
options = {
virtualisation.test.nodeName = mkOption {
internal = true;
default = name;
# We need to force this in specialisations, otherwise it'd be
# readOnly = true;
description = ''
The `name` in `nodes.<name>` and `containers.<name>`; stable across `specialisations`.
'';
};
virtualisation.test.nodeNumber = mkOption {
internal = true;
type = types.int;
readOnly = true;
default = nodeNumbers.${config.virtualisation.test.nodeName};
description = ''
A unique number assigned for each machine in `nodes` and `containers`.
'';
};
# specialisations override the `name` module argument,
# so we push the real `virtualisation.test.nodeName`.
specialisation = mkOption {
type = types.attrsOf (
types.submodule {
options.configuration = mkOption {
type = types.submoduleWith {
modules = [
{
config.virtualisation.test.nodeName =
# assert regular.config.virtualisation.test.nodeName != "configuration";
regular.config.virtualisation.test.nodeName;
}
];
};
};
}
);
};
};
}
);
in
{
config = {
extraBaseModules = {
imports = [
networkModule
nodeNumberModule
];
};
extraBaseNodeModules = {
imports = [
qemuNetworkModule
];
};
};
}