nixos/virtualisation: factor out common networking options between qemu-vm and nspawn-container

This commit is contained in:
Kierán Meinhardt
2026-01-19 10:36:38 +01:00
parent 1e3c78f75d
commit f4baee30bb
3 changed files with 141 additions and 251 deletions
@@ -0,0 +1,138 @@
# This module defines networking options for virtual machines and containers.
# It is intended to be used with systemd-nspawn containers and QEMU virtual machines.
{ config, lib, ... }:
let
inherit (lib) types;
interfaceType = types.submodule (
{ name, ... }:
{
options = {
name = lib.mkOption {
type = types.str;
default = name;
description = ''
Interface name
'';
};
vlan = lib.mkOption {
type = types.ints.unsigned;
description = ''
VLAN to which the network interface is connected.
'';
};
assignIP = lib.mkOption {
type = types.bool;
default = false;
description = ''
Automatically assign an IP address to the network interface using the same scheme as
virtualisation.vlans.
'';
};
};
}
);
cfg = config.virtualisation;
# Convert legacy VLANs to named interfaces.
vlansNumbered = lib.listToAttrs (
lib.forEach (lib.zipLists cfg.vlans (lib.range 1 255)) (
v:
let
name = "eth${toString v.snd}";
in
lib.nameValuePair name {
inherit name;
vlan = v.fst;
assignIP = true;
}
)
);
in
{
options = {
networking.primaryIPAddress = lib.mkOption {
type = types.str;
default = "";
internal = true;
description = "Primary IP address used in /etc/hosts.";
};
networking.primaryIPv6Address = lib.mkOption {
type = types.str;
default = "";
internal = true;
description = "Primary IPv6 address used in /etc/hosts.";
};
virtualisation.vlans = lib.mkOption {
type = types.listOf types.ints.unsigned;
default = if cfg.interfaces == { } then [ 1 ] else [ ];
defaultText = lib.literalExpression ''if cfg.interfaces == {} then [ 1 ] else [ ]'';
example = [
1
2
];
description = ''
Virtual networks to which the container or VM is connected. Each number «N» in
this list causes the container to have a virtual Ethernet interface
attached to a separate virtual network on which it will be assigned IP
address `192.168.«N».«M»`, where «M» is the index of this container in
the list of containers.
'';
};
virtualisation.interfaces = lib.mkOption {
default = { };
example = {
enp1s0.vlan = 1;
};
description = ''
Extra network interfaces to add to the container or VM in addition to the ones
created by {option}`virtualisation.vlans`.
'';
type = types.attrsOf interfaceType;
};
virtualisation.allInterfaces = lib.mkOption {
type = types.attrsOf interfaceType;
readOnly = true;
description = ''
All network interfaces for the container or VM. Combines
{option}`virtualisation.vlans` and {option}`virtualisation.interfaces`.
'';
default = vlansNumbered // cfg.interfaces;
};
};
config = {
assertions = [
(
let
conflictingKeys = lib.intersectAttrs vlansNumbered cfg.interfaces;
in
{
assertion = conflictingKeys == { };
message = ''
`virtualisation.vlans` and `virtualisation.interfaces` have conflicting keys: ${lib.concatStringsSep "," (lib.attrNames conflictingKeys)}
'';
}
)
(
let
allInterfaceNames =
(lib.mapAttrsToList (k: i: i.name) vlansNumbered)
++ (lib.mapAttrsToList (k: i: i.name) cfg.interfaces);
in
{
assertion = lib.allUnique allInterfaceNames;
message = ''
`virtualisation.vlans` and `virtualisation.interfaces` have conflicting interface names: ${lib.concatStringsSep "," allInterfaceNames}
'';
}
)
];
};
}
@@ -19,106 +19,11 @@
let
inherit (lib) types;
cfg = config.virtualisation;
interfaceType = types.submodule (
{ name, ... }:
{
options = {
name = lib.mkOption {
type = types.str;
default = name;
description = ''
Interface name
'';
};
vlan = lib.mkOption {
type = types.ints.unsigned;
description = ''
VLAN to which the network interface is connected.
'';
};
assignIP = lib.mkOption {
type = types.bool;
default = false;
description = ''
Automatically assign an IP address to the network interface using the same scheme as
virtualisation.vlans.
'';
};
};
}
);
# Convert legacy VLANs to named interfaces.
vlansNumbered = lib.listToAttrs (
lib.forEach (lib.zipLists cfg.vlans (lib.range 1 255)) (
v:
let
name = "eth${toString v.snd}";
in
lib.nameValuePair name {
inherit name;
vlan = v.fst;
assignIP = true;
}
)
);
in
{
imports = [ ../guest-networking-options.nix ];
options = {
networking.primaryIPAddress = lib.mkOption {
type = types.str;
default = "";
internal = true;
description = "Primary IP address used in /etc/hosts.";
};
networking.primaryIPv6Address = lib.mkOption {
type = types.str;
default = "";
internal = true;
description = "Primary IPv6 address used in /etc/hosts.";
};
virtualisation.vlans = lib.mkOption {
type = types.listOf types.ints.unsigned;
default = if cfg.interfaces == { } then [ 1 ] else [ ];
defaultText = lib.literalExpression ''if cfg.interfaces == {} then [ 1 ] else [ ]'';
example = [
1
2
];
description = ''
Virtual networks to which the container is connected. Each number «N» in
this list causes the container to have a virtual Ethernet interface
attached to a separate virtual network on which it will be assigned IP
address `192.168.«N».«M»`, where «M» is the index of this container in
the list of containers.
'';
};
virtualisation.interfaces = lib.mkOption {
default = { };
example = {
enp1s0.vlan = 1;
};
description = ''
Network interfaces to add to the container.
'';
type = types.attrsOf interfaceType;
};
virtualisation.allInterfaces = lib.mkOption {
type = types.attrsOf interfaceType;
readOnly = true;
description = ''
All network interfaces for the container. Combines
{option}`virtualisation.vlans` and {option}`virtualisation.interfaces`.
'';
default = vlansNumbered // cfg.interfaces;
};
virtualisation.cmdline = lib.mkOption {
type = types.listOf types.str;
@@ -163,33 +68,6 @@ in
};
config = {
assertions = [
(
let
conflictingKeys = lib.intersectAttrs vlansNumbered cfg.interfaces;
in
{
assertion = conflictingKeys == { };
message = ''
`virtualisation.vlans` and `virtualisation.interfaces` have conflicting keys: ${lib.concatStringsSep "," (lib.attrNames conflictingKeys)}
'';
}
)
(
let
allInterfaceNames =
(lib.mapAttrsToList (k: i: i.name) vlansNumbered)
++ (lib.mapAttrsToList (k: i: i.name) cfg.interfaces);
in
{
assertion = lib.allUnique allInterfaceNames;
message = ''
`virtualisation.vlans` and `virtualisation.interfaces` have conflicting interface names: ${lib.concatStringsSep "," allInterfaceNames}
'';
}
)
];
boot.isNspawnContainer = true;
# Needed since nixpkgs 7fb2f407c01b017737eafc26b065d7f56434a992 removed the getty unit by default.
+1 -127
View File
@@ -360,58 +360,12 @@ let
copyChannel = false;
OVMF = cfg.efi.OVMF;
};
interfaceType = types.submodule (
{ name, ... }:
{
options = {
name = mkOption {
type = types.str;
default = name;
description = ''
Interface name
'';
};
vlan = mkOption {
type = types.ints.unsigned;
description = ''
VLAN to which the network interface is connected.
'';
};
assignIP = mkOption {
type = types.bool;
default = false;
description = ''
Automatically assign an IP address to the network interface using the same scheme as
virtualisation.vlans.
'';
};
};
}
);
# Convert legacy VLANs to named interfaces.
vlansNumbered = lib.listToAttrs (
lib.forEach (lib.zipLists cfg.vlans (lib.range 1 255)) (
v:
let
name = "eth${toString v.snd}";
in
lib.nameValuePair name {
inherit name;
vlan = v.fst;
assignIP = true;
}
)
);
in
{
imports = [
../profiles/qemu-guest.nix
./disk-size-option.nix
./guest-networking-options.nix
(mkRenamedOptionModule
[
"virtualisation"
@@ -726,48 +680,6 @@ in
'';
};
virtualisation.vlans = mkOption {
type = types.listOf types.ints.unsigned;
default = if config.virtualisation.interfaces == { } then [ 1 ] else [ ];
defaultText = lib.literalExpression ''if config.virtualisation.interfaces == {} then [ 1 ] else [ ]'';
example = [
1
2
];
description = ''
Virtual networks to which the VM is connected. Each
number «N» in this list causes
the VM to have a virtual Ethernet interface attached to a
separate virtual network on which it will be assigned IP
address
`192.168.«N».«M»`,
where «M» is the index of this VM
in the list of VMs.
'';
};
virtualisation.interfaces = mkOption {
default = { };
example = {
enp1s0.vlan = 1;
};
description = ''
Extra network interfaces to add to the VM in addition to the ones
created by {option}`virtualisation.vlans`.
'';
type = types.attrsOf interfaceType;
};
virtualisation.allInterfaces = mkOption {
type = types.attrsOf interfaceType;
readOnly = true;
description = ''
All network interfaces for the VM. Combines
{option}`virtualisation.vlans` and {option}`virtualisation.interfaces`.
'';
default = vlansNumbered // cfg.interfaces;
};
virtualisation.writableStore = mkOption {
type = types.bool;
default = cfg.mountHostNixStore;
@@ -790,20 +702,6 @@ in
'';
};
networking.primaryIPAddress = mkOption {
type = types.str;
default = "";
internal = true;
description = "Primary IP address used in /etc/hosts.";
};
networking.primaryIPv6Address = mkOption {
type = types.str;
default = "";
internal = true;
description = "Primary IPv6 address used in /etc/hosts.";
};
virtualisation.host.pkgs = mkOption {
type = options.nixpkgs.pkgs.type;
default = pkgs;
@@ -1286,30 +1184,6 @@ in
)
)
++ [
(
let
conflictingKeys = lib.intersectAttrs vlansNumbered cfg.interfaces;
in
{
assertion = conflictingKeys == { };
message = ''
`virtualisation.vlans` and `virtualisation.interfaces` have conflicting keys: ${lib.concatStringsSep "," (lib.attrNames conflictingKeys)}
'';
}
)
(
let
allInterfaceNames =
(lib.mapAttrsToList (k: i: i.name) vlansNumbered)
++ (lib.mapAttrsToList (k: i: i.name) cfg.interfaces);
in
{
assertion = lib.allUnique allInterfaceNames;
message = ''
`virtualisation.vlans` and `virtualisation.interfaces` have conflicting interface names: ${lib.concatStringsSep "," allInterfaceNames}
'';
}
)
{
assertion = pkgs.stdenv.hostPlatform.is32bit -> cfg.memorySize < 2047;
message = ''