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