nixos/qemu-vm: add option for named network interfaces

Adds a new option to the virtualisation modules that enables specifying explicitly named network interfaces in QEMU VMs.
The existing `virtualisation.vlans` option is still supported for cases where the name of the network interface is irrelevant.
This commit is contained in:
Graham Dennis 2023-05-22 13:38:52 +10:00
parent e6e049b7a2
commit 93502aa3b1
5 changed files with 140 additions and 96 deletions

View file

@ -16,4 +16,4 @@
## Other Notable Changes {#sec-release-23.11-notable-changes}
- Create the first release note entry in this section!
- A new option was added to the virtualisation module that enables specifying explicitly named network interfaces in QEMU VMs. The existing `virtualisation.vlans` is still supported for cases where the name of the network interface is irrelevant.

View file

@ -12,7 +12,9 @@ let
};
vlans = map (m: m.virtualisation.vlans) (lib.attrValues config.nodes);
vlans = map (m: (
m.virtualisation.vlans ++
(lib.mapAttrsToList (_: v: v.vlan) m.virtualisation.interfaces))) (lib.attrValues config.nodes);
vms = map (m: m.system.build.vm) (lib.attrValues config.nodes);
nodeHostNames =

View file

@ -4,7 +4,7 @@ let
inherit (lib)
attrNames concatMap concatMapStrings flip forEach head
listToAttrs mkDefault mkOption nameValuePair optionalString
range types zipListsWith zipLists
range toLower types zipListsWith zipLists
mdDoc
;
@ -18,24 +18,41 @@ let
networkModule = { config, nodes, pkgs, ... }:
let
interfacesNumbered = zipLists config.virtualisation.vlans (range 1 255);
interfaces = forEach interfacesNumbered ({ fst, snd }:
nameValuePair "eth${toString snd}" {
ipv4.addresses =
[{
address = "192.168.${toString fst}.${toString config.virtualisation.test.nodeNumber}";
qemu-common = import ../qemu-common.nix { inherit lib pkgs; };
# Convert legacy VLANs to named interfaces and merge with explicit interfaces.
vlansNumbered = forEach (zipLists config.virtualisation.vlans (range 1 255)) (v: {
name = "eth${toString v.snd}";
vlan = v.fst;
assignIP = true;
});
explicitInterfaces = lib.mapAttrsToList (n: v: v // { name = n; }) config.virtualisation.interfaces;
interfaces = vlansNumbered ++ explicitInterfaces;
interfacesNumbered = zipLists interfaces (range 1 255);
# 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;
}];
});
qemuOptions = lib.flatten (forEach interfacesNumbered ({ fst, snd }:
qemu-common.qemuNICFlags snd fst.vlan config.virtualisation.test.nodeNumber));
udevRules = forEach interfacesNumbered ({ fst, snd }:
# MAC Addresses for QEMU network devices are lowercase, and udev string comparison is case-sensitive.
"SUBSYSTEM==\"net\",ACTION==\"add\",ATTR{address}==\"${toLower(qemu-common.qemuNicMac fst.vlan config.virtualisation.test.nodeNumber)}\",NAME=\"${fst.name}\"");
networkConfig =
{
networking.hostName = mkDefault config.virtualisation.test.nodeName;
networking.interfaces = listToAttrs interfaces;
networking.interfaces = listToAttrs ipInterfaces;
networking.primaryIPAddress =
optionalString (interfaces != [ ]) (head (head interfaces).value.ipv4.addresses).address;
optionalString (ipInterfaces != [ ]) (head (head ipInterfaces).value.ipv4.addresses).address;
# Put the IP addresses of all VMs in this machine's
# /etc/hosts file. If a machine has multiple
@ -51,16 +68,13 @@ let
"${config.networking.hostName}.${config.networking.domain} " +
"${config.networking.hostName}\n"));
virtualisation.qemu.options =
let qemu-common = import ../qemu-common.nix { inherit lib pkgs; };
in
flip concatMap interfacesNumbered
({ fst, snd }: qemu-common.qemuNICFlags snd fst config.virtualisation.test.nodeNumber);
virtualisation.qemu.options = qemuOptions;
boot.initrd.services.udev.rules = concatMapStrings (x: x + "\n") udevRules;
};
in
{
key = "ip-address";
key = "network-interfaces";
config = networkConfig // {
# Expose the networkConfig items for tests like nixops
# that need to recreate the network config.

View file

@ -564,7 +564,8 @@ in
virtualisation.vlans =
mkOption {
type = types.listOf types.ints.unsigned;
default = [ 1 ];
default = if config.virtualisation.interfaces == {} then [ 1 ] else [ ];
defaultText = lib.literalExpression ''if config.virtualisation.interfaces == {} then [ 1 ] else [ ]'';
example = [ 1 2 ];
description =
lib.mdDoc ''
@ -579,6 +580,35 @@ in
'';
};
virtualisation.interfaces = mkOption {
default = {};
example = {
enp1s0.vlan = 1;
};
description = lib.mdDoc ''
Network interfaces to add to the VM.
'';
type = with types; attrsOf (submodule {
options = {
vlan = mkOption {
type = types.ints.unsigned;
description = lib.mdDoc ''
VLAN to which the network interface is connected.
'';
};
assignIP = mkOption {
type = types.bool;
default = false;
description = lib.mdDoc ''
Automatically assign an IP address to the network interface using the same scheme as
virtualisation.vlans.
'';
};
};
});
};
virtualisation.writableStore =
mkOption {
type = types.bool;

View file

@ -93,18 +93,19 @@ let
name = "Static";
nodes.router = router;
nodes.client = { pkgs, ... }: with pkgs.lib; {
virtualisation.vlans = [ 1 2 ];
virtualisation.interfaces.enp1s0.vlan = 1;
virtualisation.interfaces.enp2s0.vlan = 2;
networking = {
useNetworkd = networkd;
useDHCP = false;
defaultGateway = "192.168.1.1";
defaultGateway6 = "fd00:1234:5678:1::1";
interfaces.eth1.ipv4.addresses = mkOverride 0 [
interfaces.enp1s0.ipv4.addresses = [
{ address = "192.168.1.2"; prefixLength = 24; }
{ address = "192.168.1.3"; prefixLength = 32; }
{ address = "192.168.1.10"; prefixLength = 32; }
];
interfaces.eth2.ipv4.addresses = mkOverride 0 [
interfaces.enp2s0.ipv4.addresses = [
{ address = "192.168.2.2"; prefixLength = 24; }
];
};
@ -170,12 +171,12 @@ let
# Disable test driver default config
networking.interfaces = lib.mkForce {};
networking.useNetworkd = networkd;
virtualisation.vlans = [ 1 ];
virtualisation.interfaces.enp1s0.vlan = 1;
};
testScript = ''
start_all()
client.wait_for_unit("multi-user.target")
client.wait_until_succeeds("ip addr show dev eth1 | grep '192.168.1'")
client.wait_until_succeeds("ip addr show dev enp1s0 | grep '192.168.1'")
client.shell_interact()
client.succeed("ping -c 1 192.168.1.1")
router.succeed("ping -c 1 192.168.1.1")
@ -187,20 +188,13 @@ let
name = "SimpleDHCP";
nodes.router = router;
nodes.client = { pkgs, ... }: with pkgs.lib; {
virtualisation.vlans = [ 1 2 ];
virtualisation.interfaces.enp1s0.vlan = 1;
virtualisation.interfaces.enp2s0.vlan = 2;
networking = {
useNetworkd = networkd;
useDHCP = false;
interfaces.eth1 = {
ipv4.addresses = mkOverride 0 [ ];
ipv6.addresses = mkOverride 0 [ ];
useDHCP = true;
};
interfaces.eth2 = {
ipv4.addresses = mkOverride 0 [ ];
ipv6.addresses = mkOverride 0 [ ];
useDHCP = true;
};
interfaces.enp1s0.useDHCP = true;
interfaces.enp2s0.useDHCP = true;
};
};
testScript = { ... }:
@ -211,10 +205,10 @@ let
router.wait_for_unit("network-online.target")
with subtest("Wait until we have an ip address on each interface"):
client.wait_until_succeeds("ip addr show dev eth1 | grep -q '192.168.1'")
client.wait_until_succeeds("ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'")
client.wait_until_succeeds("ip addr show dev eth2 | grep -q '192.168.2'")
client.wait_until_succeeds("ip addr show dev eth2 | grep -q 'fd00:1234:5678:2:'")
client.wait_until_succeeds("ip addr show dev enp1s0 | grep -q '192.168.1'")
client.wait_until_succeeds("ip addr show dev enp1s0 | grep -q 'fd00:1234:5678:1:'")
client.wait_until_succeeds("ip addr show dev enp2s0 | grep -q '192.168.2'")
client.wait_until_succeeds("ip addr show dev enp2s0 | grep -q 'fd00:1234:5678:2:'")
with subtest("Test vlan 1"):
client.wait_until_succeeds("ping -c 1 192.168.1.1")
@ -243,16 +237,15 @@ let
name = "OneInterfaceDHCP";
nodes.router = router;
nodes.client = { pkgs, ... }: with pkgs.lib; {
virtualisation.vlans = [ 1 2 ];
virtualisation.interfaces.enp1s0.vlan = 1;
virtualisation.interfaces.enp2s0.vlan = 2;
networking = {
useNetworkd = networkd;
useDHCP = false;
interfaces.eth1 = {
ipv4.addresses = mkOverride 0 [ ];
interfaces.enp1s0 = {
mtu = 1343;
useDHCP = true;
};
interfaces.eth2.ipv4.addresses = mkOverride 0 [ ];
};
};
testScript = { ... }:
@ -264,10 +257,10 @@ let
router.wait_for_unit("network.target")
with subtest("Wait until we have an ip address on each interface"):
client.wait_until_succeeds("ip addr show dev eth1 | grep -q '192.168.1'")
client.wait_until_succeeds("ip addr show dev enp1s0 | grep -q '192.168.1'")
with subtest("ensure MTU is set"):
assert "mtu 1343" in client.succeed("ip link show dev eth1")
assert "mtu 1343" in client.succeed("ip link show dev enp1s0")
with subtest("Test vlan 1"):
client.wait_until_succeeds("ping -c 1 192.168.1.1")
@ -286,16 +279,15 @@ let
};
bond = let
node = address: { pkgs, ... }: with pkgs.lib; {
virtualisation.vlans = [ 1 2 ];
virtualisation.interfaces.enp1s0.vlan = 1;
virtualisation.interfaces.enp2s0.vlan = 2;
networking = {
useNetworkd = networkd;
useDHCP = false;
bonds.bond0 = {
interfaces = [ "eth1" "eth2" ];
interfaces = [ "enp1s0" "enp2s0" ];
driverOptions.mode = "802.3ad";
};
interfaces.eth1.ipv4.addresses = mkOverride 0 [ ];
interfaces.eth2.ipv4.addresses = mkOverride 0 [ ];
interfaces.bond0.ipv4.addresses = mkOverride 0
[ { inherit address; prefixLength = 30; } ];
};
@ -326,12 +318,11 @@ let
};
bridge = let
node = { address, vlan }: { pkgs, ... }: with pkgs.lib; {
virtualisation.vlans = [ vlan ];
virtualisation.interfaces.enp1s0.vlan = vlan;
networking = {
useNetworkd = networkd;
useDHCP = false;
interfaces.eth1.ipv4.addresses = mkOverride 0
[ { inherit address; prefixLength = 24; } ];
interfaces.enp1s0.ipv4.addresses = [ { inherit address; prefixLength = 24; } ];
};
};
in {
@ -339,11 +330,12 @@ let
nodes.client1 = node { address = "192.168.1.2"; vlan = 1; };
nodes.client2 = node { address = "192.168.1.3"; vlan = 2; };
nodes.router = { pkgs, ... }: with pkgs.lib; {
virtualisation.vlans = [ 1 2 ];
virtualisation.interfaces.enp1s0.vlan = 1;
virtualisation.interfaces.enp2s0.vlan = 2;
networking = {
useNetworkd = networkd;
useDHCP = false;
bridges.bridge.interfaces = [ "eth1" "eth2" ];
bridges.bridge.interfaces = [ "enp1s0" "enp2s0" ];
interfaces.eth1.ipv4.addresses = mkOverride 0 [ ];
interfaces.eth2.ipv4.addresses = mkOverride 0 [ ];
interfaces.bridge.ipv4.addresses = mkOverride 0
@ -377,7 +369,7 @@ let
nodes.router = router;
nodes.client = { pkgs, ... }: with pkgs.lib; {
environment.systemPackages = [ pkgs.iptables ]; # to debug firewall rules
virtualisation.vlans = [ 1 ];
virtualisation.interfaces.enp1s0.vlan = 1;
networking = {
useNetworkd = networkd;
useDHCP = false;
@ -385,14 +377,9 @@ let
# reverse path filtering rules for the macvlan interface seem
# to be incorrect, causing the test to fail. Disable temporarily.
firewall.checkReversePath = false;
macvlans.macvlan.interface = "eth1";
interfaces.eth1 = {
ipv4.addresses = mkOverride 0 [ ];
useDHCP = true;
};
interfaces.macvlan = {
useDHCP = true;
};
macvlans.macvlan.interface = "enp1s0";
interfaces.enp1s0.useDHCP = true;
interfaces.macvlan.useDHCP = true;
};
};
testScript = { ... }:
@ -404,7 +391,7 @@ let
router.wait_for_unit("network.target")
with subtest("Wait until we have an ip address on each interface"):
client.wait_until_succeeds("ip addr show dev eth1 | grep -q '192.168.1'")
client.wait_until_succeeds("ip addr show dev enp1s0 | grep -q '192.168.1'")
client.wait_until_succeeds("ip addr show dev macvlan | grep -q '192.168.1'")
with subtest("Print lots of diagnostic information"):
@ -431,23 +418,22 @@ let
fou = {
name = "foo-over-udp";
nodes.machine = { ... }: {
virtualisation.vlans = [ 1 ];
virtualisation.interfaces.enp1s0.vlan = 1;
networking = {
useNetworkd = networkd;
useDHCP = false;
interfaces.eth1.ipv4.addresses = mkOverride 0
[ { address = "192.168.1.1"; prefixLength = 24; } ];
interfaces.enp1s0.ipv4.addresses = [ { address = "192.168.1.1"; prefixLength = 24; } ];
fooOverUDP = {
fou1 = { port = 9001; };
fou2 = { port = 9002; protocol = 41; };
fou3 = mkIf (!networkd)
{ port = 9003; local.address = "192.168.1.1"; };
fou4 = mkIf (!networkd)
{ port = 9004; local = { address = "192.168.1.1"; dev = "eth1"; }; };
{ port = 9004; local = { address = "192.168.1.1"; dev = "enp1s0"; }; };
};
};
systemd.services = {
fou3-fou-encap.after = optional (!networkd) "network-addresses-eth1.service";
fou3-fou-encap.after = optional (!networkd) "network-addresses-enp1s0.service";
};
};
testScript = { ... }:
@ -470,22 +456,22 @@ let
"gue": None,
"family": "inet",
"local": "192.168.1.1",
"dev": "eth1",
"dev": "enp1s0",
} in fous, "fou4 exists"
'';
};
sit = let
node = { address4, remote, address6 }: { pkgs, ... }: with pkgs.lib; {
virtualisation.vlans = [ 1 ];
virtualisation.interfaces.enp1s0.vlan = 1;
networking = {
useNetworkd = networkd;
useDHCP = false;
sits.sit = {
inherit remote;
local = address4;
dev = "eth1";
dev = "enp1s0";
};
interfaces.eth1.ipv4.addresses = mkOverride 0
interfaces.enp1s0.ipv4.addresses = mkOverride 0
[ { address = address4; prefixLength = 24; } ];
interfaces.sit.ipv6.addresses = mkOverride 0
[ { address = address6; prefixLength = 64; } ];
@ -685,10 +671,10 @@ let
vlan-ping = let
baseIP = number: "10.10.10.${number}";
vlanIP = number: "10.1.1.${number}";
baseInterface = "eth1";
baseInterface = "enp1s0";
vlanInterface = "vlan42";
node = number: {pkgs, ... }: with pkgs.lib; {
virtualisation.vlans = [ 1 ];
virtualisation.interfaces.enp1s0.vlan = 1;
networking = {
#useNetworkd = networkd;
useDHCP = false;
@ -785,12 +771,12 @@ let
privacy = {
name = "Privacy";
nodes.router = { ... }: {
virtualisation.vlans = [ 1 ];
virtualisation.interfaces.enp1s0.vlan = 1;
boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = true;
networking = {
useNetworkd = networkd;
useDHCP = false;
interfaces.eth1.ipv6.addresses = singleton {
interfaces.enp1s0.ipv6.addresses = singleton {
address = "fd00:1234:5678:1::1";
prefixLength = 64;
};
@ -798,7 +784,7 @@ let
services.radvd = {
enable = true;
config = ''
interface eth1 {
interface enp1s0 {
AdvSendAdvert on;
AdvManagedFlag on;
AdvOtherConfigFlag on;
@ -812,11 +798,11 @@ let
};
};
nodes.client_with_privacy = { pkgs, ... }: with pkgs.lib; {
virtualisation.vlans = [ 1 ];
virtualisation.interfaces.enp1s0.vlan = 1;
networking = {
useNetworkd = networkd;
useDHCP = false;
interfaces.eth1 = {
interfaces.enp1s0 = {
tempAddress = "default";
ipv4.addresses = mkOverride 0 [ ];
ipv6.addresses = mkOverride 0 [ ];
@ -825,11 +811,11 @@ let
};
};
nodes.client = { pkgs, ... }: with pkgs.lib; {
virtualisation.vlans = [ 1 ];
virtualisation.interfaces.enp1s0.vlan = 1;
networking = {
useNetworkd = networkd;
useDHCP = false;
interfaces.eth1 = {
interfaces.enp1s0 = {
tempAddress = "enabled";
ipv4.addresses = mkOverride 0 [ ];
ipv6.addresses = mkOverride 0 [ ];
@ -847,9 +833,9 @@ let
with subtest("Wait until we have an ip address"):
client_with_privacy.wait_until_succeeds(
"ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'"
"ip addr show dev enp1s0 | grep -q 'fd00:1234:5678:1:'"
)
client.wait_until_succeeds("ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'")
client.wait_until_succeeds("ip addr show dev enp1s0 | grep -q 'fd00:1234:5678:1:'")
with subtest("Test vlan 1"):
client_with_privacy.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::1")
@ -947,7 +933,7 @@ let
), "The IPv6 routing table has not been properly cleaned:\n{}".format(ipv6Residue)
'';
};
rename = {
rename = if networkd then {
name = "RenameInterface";
nodes.machine = { pkgs, ... }: {
virtualisation.vlans = [ 1 ];
@ -955,23 +941,20 @@ let
useNetworkd = networkd;
useDHCP = false;
};
} //
(if networkd
then { systemd.network.links."10-custom_name" = {
matchConfig.MACAddress = "52:54:00:12:01:01";
linkConfig.Name = "custom_name";
};
}
else { boot.initrd.services.udev.rules = ''
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="52:54:00:12:01:01", KERNEL=="eth*", NAME="custom_name"
'';
});
systemd.network.links."10-custom_name" = {
matchConfig.MACAddress = "52:54:00:12:01:01";
linkConfig.Name = "custom_name";
};
};
testScript = ''
machine.succeed("udevadm settle")
print(machine.succeed("ip link show dev custom_name"))
'';
};
} else {
name = "RenameInterface";
nodes = { };
testScript = "";
};
# even with disabled networkd, systemd.network.links should work
# (as it's handled by udev, not networkd)
link = {
@ -1015,6 +998,21 @@ let
machine.fail("ip address show wlan0 | grep -q ${testMac}")
'';
};
caseSensitiveRenaming = {
name = "CaseSensitiveRenaming";
nodes.machine = { pkgs, ... }: {
virtualisation.interfaces.enCustom.vlan = 11;
networking = {
useNetworkd = networkd;
useDHCP = false;
};
};
testScript = ''
machine.succeed("udevadm settle")
print(machine.succeed("ip link show dev enCustom"))
machine.wait_until_succeeds("ip link show dev enCustom | grep -q '52:54:00:12:0b:01")
'';
};
};
in mapAttrs (const (attrs: makeTest (attrs // {