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