1{ lib, nodes, ... }:
2
3let
4 inherit (lib)
5 attrNames concatMap concatMapStrings flip forEach head
6 listToAttrs mkDefault mkOption nameValuePair optionalString
7 range toLower types zipListsWith zipLists
8 mdDoc
9 ;
10
11 nodeNumbers =
12 listToAttrs
13 (zipListsWith
14 nameValuePair
15 (attrNames nodes)
16 (range 1 254)
17 );
18
19 networkModule = { config, nodes, pkgs, ... }:
20 let
21 qemu-common = import ../qemu-common.nix { inherit lib pkgs; };
22
23 # Convert legacy VLANs to named interfaces and merge with explicit interfaces.
24 vlansNumbered = forEach (zipLists config.virtualisation.vlans (range 1 255)) (v: {
25 name = "eth${toString v.snd}";
26 vlan = v.fst;
27 assignIP = true;
28 });
29 explicitInterfaces = lib.mapAttrsToList (n: v: v // { name = n; }) config.virtualisation.interfaces;
30 interfaces = vlansNumbered ++ explicitInterfaces;
31 interfacesNumbered = zipLists interfaces (range 1 255);
32
33 # Automatically assign IP addresses to requested interfaces.
34 assignIPs = lib.filter (i: i.assignIP) interfaces;
35 ipInterfaces = forEach assignIPs (i:
36 nameValuePair i.name { ipv4.addresses =
37 [ { address = "192.168.${toString i.vlan}.${toString config.virtualisation.test.nodeNumber}";
38 prefixLength = 24;
39 }];
40 });
41
42 qemuOptions = lib.flatten (forEach interfacesNumbered ({ fst, snd }:
43 qemu-common.qemuNICFlags snd fst.vlan config.virtualisation.test.nodeNumber));
44 udevRules = forEach interfacesNumbered ({ fst, snd }:
45 # MAC Addresses for QEMU network devices are lowercase, and udev string comparison is case-sensitive.
46 ''SUBSYSTEM=="net",ACTION=="add",ATTR{address}=="${toLower(qemu-common.qemuNicMac fst.vlan config.virtualisation.test.nodeNumber)}",NAME="${fst.name}"'');
47
48 networkConfig =
49 {
50 networking.hostName = mkDefault config.virtualisation.test.nodeName;
51
52 networking.interfaces = listToAttrs ipInterfaces;
53
54 networking.primaryIPAddress =
55 optionalString (ipInterfaces != [ ]) (head (head ipInterfaces).value.ipv4.addresses).address;
56
57 # Put the IP addresses of all VMs in this machine's
58 # /etc/hosts file. If a machine has multiple
59 # interfaces, use the IP address corresponding to
60 # the first interface (i.e. the first network in its
61 # virtualisation.vlans option).
62 networking.extraHosts = flip concatMapStrings (attrNames nodes)
63 (m':
64 let config = nodes.${m'}; in
65 optionalString (config.networking.primaryIPAddress != "")
66 ("${config.networking.primaryIPAddress} " +
67 optionalString (config.networking.domain != null)
68 "${config.networking.hostName}.${config.networking.domain} " +
69 "${config.networking.hostName}\n"));
70
71 virtualisation.qemu.options = qemuOptions;
72 boot.initrd.services.udev.rules = concatMapStrings (x: x + "\n") udevRules;
73 };
74
75 in
76 {
77 key = "network-interfaces";
78 config = networkConfig // {
79 # Expose the networkConfig items for tests like nixops
80 # that need to recreate the network config.
81 system.build.networkConfig = networkConfig;
82 };
83 };
84
85 nodeNumberModule = (regular@{ config, name, ... }: {
86 options = {
87 virtualisation.test.nodeName = mkOption {
88 internal = true;
89 default = name;
90 # We need to force this in specilisations, otherwise it'd be
91 # readOnly = true;
92 description = mdDoc ''
93 The `name` in `nodes.<name>`; stable across `specialisations`.
94 '';
95 };
96 virtualisation.test.nodeNumber = mkOption {
97 internal = true;
98 type = types.int;
99 readOnly = true;
100 default = nodeNumbers.${config.virtualisation.test.nodeName};
101 description = mdDoc ''
102 A unique number assigned for each node in `nodes`.
103 '';
104 };
105
106 # specialisations override the `name` module argument,
107 # so we push the real `virtualisation.test.nodeName`.
108 specialisation = mkOption {
109 type = types.attrsOf (types.submodule {
110 options.configuration = mkOption {
111 type = types.submoduleWith {
112 modules = [
113 {
114 config.virtualisation.test.nodeName =
115 # assert regular.config.virtualisation.test.nodeName != "configuration";
116 regular.config.virtualisation.test.nodeName;
117 }
118 ];
119 };
120 };
121 });
122 };
123 };
124 });
125
126in
127{
128 config = {
129 extraBaseModules = { imports = [ networkModule nodeNumberModule ]; };
130 };
131}