* [pve-devel] [PATCH installer 00/14] support network interface name pinning
@ 2025-10-14 13:21 Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 01/14] test: parse-kernel-cmdline: fix module import statement Christoph Heiss
` (13 more replies)
0 siblings, 14 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
This series adds support for pinning the names of network interfaces
directly during the installation, for all of GUI, TUI and auto-installer.
There are also some smaller clean-ups and quality-of-life improvements
interspersed - to keep the patches for each part of the installer
together - in the series, which can also be applied
separately/beforehand if wanted.
Tested all combinations, i.e. for each of GUI, TUI and auto-installer,
installed with pinning disabled (checking for regressions) and with
pinning enabled at some custom interface names set.
The auto-installer changes can be tested by specifying e.g.
[network.interface-name-pinning]
enabled = true
[network.interface-name-pinning.mapping]
"ab:cd:ef:12:34:56" = "mgmt"
"12:34:56:ab:cd:ef" = "lan0"
in the answer file.
Christoph Heiss (13):
test: parse-kernel-cmdline: fix module import statement
install: add support for network interface name pinning
run env: network: add kernel driver name to network interface info
common: utils: fix clippy warnings
common: setup: simplify network address list serialization
common: implement support for `network_interface_pin_map` config
auto: add support for pinning network interface names
assistant: verify network settings in `validate-answer` subcommand
post-hook: avoid redundant Option<bool> for (de-)serialization
post-hook: add network interface name and pinning status
tui: views: move network options view to own module
tui: views: form: allow attaching user-defined data to children
tui: add support for pinning network interface names
gui: add support for pinning network interface names
Proxmox/Install.pm | 47 +-
Proxmox/Install/Config.pm | 8 +
Proxmox/Install/RunEnv.pm | 11 +
Proxmox/Sys/Net.pm | 63 ++-
proxinstall | 209 ++++++++-
proxmox-auto-install-assistant/src/main.rs | 3 +-
proxmox-auto-installer/src/answer.rs | 63 ++-
proxmox-auto-installer/src/utils.rs | 36 +-
proxmox-auto-installer/tests/parse-answer.rs | 2 +
.../network_interface_pinning.json | 30 ++
.../network_interface_pinning.toml | 22 +
...n_from_dhcp_no_default_domain.run-env.json | 36 +-
...rface_pinning_overlong_interface_name.json | 3 +
...rface_pinning_overlong_interface_name.toml | 18 +
.../no_fqdn_from_dhcp.run-env.json | 36 +-
.../tests/resources/run-env-info.json | 38 +-
proxmox-installer-common/src/lib.rs | 5 +
proxmox-installer-common/src/options.rs | 174 ++++++--
proxmox-installer-common/src/setup.rs | 74 +++-
proxmox-installer-common/src/utils.rs | 6 +-
proxmox-post-hook/src/main.rs | 62 +--
proxmox-tui-installer/src/main.rs | 105 +----
proxmox-tui-installer/src/setup.rs | 3 +
proxmox-tui-installer/src/views/bootdisk.rs | 6 +-
proxmox-tui-installer/src/views/mod.rs | 41 +-
proxmox-tui-installer/src/views/network.rs | 406 ++++++++++++++++++
test/parse-kernel-cmdline.pl | 2 +-
27 files changed, 1274 insertions(+), 235 deletions(-)
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning.json
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning.toml
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_interface_pinning_overlong_interface_name.json
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_interface_pinning_overlong_interface_name.toml
create mode 100644 proxmox-tui-installer/src/views/network.rs
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pve-devel] [PATCH installer 01/14] test: parse-kernel-cmdline: fix module import statement
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
@ 2025-10-14 13:21 ` Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 02/14] install: add support for network interface name pinning Christoph Heiss
` (12 subsequent siblings)
13 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
The subroutine is called with its fully-qualified path below, so drop
the explicit import - as it otherwise fails to find it.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
test/parse-kernel-cmdline.pl | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/parse-kernel-cmdline.pl b/test/parse-kernel-cmdline.pl
index b236734..7c849c5 100755
--- a/test/parse-kernel-cmdline.pl
+++ b/test/parse-kernel-cmdline.pl
@@ -7,7 +7,7 @@ use Test::More;
use Test::MockModule qw(strict);
use Proxmox::Install::RunEnv;
-use Proxmox::Install::Config qw(parse_kernel_cmdline);
+use Proxmox::Install::Config;
my $proxmox_install_runenv = Test::MockModule->new('Proxmox::Install::RunEnv');
my $proxmox_install_isoenv = Test::MockModule->new('Proxmox::Install::ISOEnv');
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pve-devel] [PATCH installer 02/14] install: add support for network interface name pinning
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 01/14] test: parse-kernel-cmdline: fix module import statement Christoph Heiss
@ 2025-10-14 13:21 ` Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 03/14] run env: network: add kernel driver name to network interface info Christoph Heiss
` (11 subsequent siblings)
13 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
From: Thomas Lamprecht <t.lamprecht@proxmox.com>
Adds the (low-level) part for pinning network interface names during
installation based on their MAC address.
Adds a new option `pin_interface_name_map`, which maps MAC addresses to
their respective new interface name.
Co-authored-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Proxmox/Install.pm | 47 +++++++++++++++-
Proxmox/Install/Config.pm | 8 +++
Proxmox/Install/RunEnv.pm | 4 ++
Proxmox/Sys/Net.pm | 54 +++++++++++++++++++
...n_from_dhcp_no_default_domain.run-env.json | 27 ++++++----
.../no_fqdn_from_dhcp.run-env.json | 27 ++++++----
.../tests/resources/run-env-info.json | 29 ++++++----
7 files changed, 167 insertions(+), 29 deletions(-)
diff --git a/Proxmox/Install.pm b/Proxmox/Install.pm
index 2ebd376..8d02c88 100644
--- a/Proxmox/Install.pm
+++ b/Proxmox/Install.pm
@@ -1101,6 +1101,49 @@ sub extract_data {
my $cidr = Proxmox::Install::Config::get_cidr();
my $gateway = Proxmox::Install::Config::get_gateway();
+ # configure pinned names for all network interfaces, if enabled
+
+ my $netif_pin_map = Proxmox::Install::Config::get_network_interface_pin_map();
+ if (defined($netif_pin_map)) {
+ mkdir "$targetdir/usr/local/lib/systemd", 0755;
+ mkdir "$targetdir/usr/local/lib/systemd/network", 0755;
+
+ my $ip_links = Proxmox::Sys::Net::ip_link_details();
+
+ for my $ifname (sort keys $run_env->{network}->{interfaces}->%*) {
+ next
+ if !Proxmox::Sys::Net::ip_link_is_physical($ip_links->{$ifname})
+ || Proxmox::Sys::Net::iface_is_vf($ifname);
+
+ my $if = $run_env->{network}->{interfaces}->{$ifname};
+
+ if (!defined($netif_pin_map->{ $if->{mac} })) {
+ # each installer frontend must ensure that either
+ # - pinning is disabled by never setting the map or
+ # - setting a pinned name for each
+ die "no network interface name set for '$if->{mac}'!";
+ }
+
+ my $pinned_name = $netif_pin_map->{ $if->{mac} };
+ my $link_file_content = Proxmox::Sys::Net::get_pin_link_file_content(
+ $if->{mac}, $pinned_name,
+ );
+
+ file_write_all(
+ "$targetdir/usr/local/lib/systemd/network/50-pmx-${pinned_name}.link",
+ $link_file_content,
+ );
+
+ # ensure that we use the correct interface name when writing out
+ # /etc/network/interfaces below - just in case the interface name
+ # is pinned and get_mngmt_nic() returned the original interface name.
+ # better be safe than sorry
+ if ($ethdev eq $ifname) {
+ $ethdev = $pinned_name;
+ }
+ }
+ }
+
if ($iso_env->{cfg}->{bridged_network}) {
$ifaces .= "iface $ethdev $ntype manual\n";
@@ -1121,7 +1164,9 @@ sub extract_data {
my $ipconf = $run_env->{ipconf};
foreach my $iface (sort keys %{ $ipconf->{ifaces} }) {
- my $name = $ipconf->{ifaces}->{$iface}->{name};
+ my $if = $ipconf->{ifaces}->{$iface};
+ my $name = defined($netif_pin_map) ? $netif_pin_map->{ $if->{mac} } : $if->{name};
+
next if $name eq $ethdev;
$ifaces .= "\niface $name $ntype manual\n";
diff --git a/Proxmox/Install/Config.pm b/Proxmox/Install/Config.pm
index da476da..7b81fcf 100644
--- a/Proxmox/Install/Config.pm
+++ b/Proxmox/Install/Config.pm
@@ -101,6 +101,11 @@ my sub init_cfg {
# network related
mngmt_nic => undef,
+
+ # maps mac address -> custom name
+ # if set to a hash, enables interface name pinning for all interfaces
+ network_interface_pin_map => undef,
+
hostname => undef,
domain => undef,
cidr => undef,
@@ -248,6 +253,9 @@ sub get_root_ssh_keys { return get('root_ssh_keys'); }
sub set_mngmt_nic { set_key('mngmt_nic', $_[0]); }
sub get_mngmt_nic { return get('mngmt_nic'); }
+sub set_network_interface_pin_map { set_key('network_interface_pin_map', $_[0]); }
+sub get_network_interface_pin_map { return get('network_interface_pin_map'); }
+
sub set_hostname { set_key('hostname', $_[0]); }
sub get_hostname { return get('hostname'); }
diff --git a/Proxmox/Install/RunEnv.pm b/Proxmox/Install/RunEnv.pm
index 540bec7..f0fa6ec 100644
--- a/Proxmox/Install/RunEnv.pm
+++ b/Proxmox/Install/RunEnv.pm
@@ -86,6 +86,7 @@ my sub query_netdevs : prototype() {
# FIXME: not the same as the battle proven way we used in the installer for years?
my $interfaces = fromjs(qx/ip --json address show/);
+ my $pinned_counter = 0;
for my $if (@$interfaces) {
my ($index, $name, $state, $mac, $addresses) =
$if->@{qw(ifindex ifname operstate address addr_info)};
@@ -115,10 +116,13 @@ my sub query_netdevs : prototype() {
$ifs->{$name} = {
index => $index,
name => $name,
+ pinned_id => "${pinned_counter}",
mac => $mac,
state => uc($state),
};
$ifs->{$name}->{addresses} = \@valid_addrs if @valid_addrs;
+
+ $pinned_counter++;
}
return $ifs;
diff --git a/Proxmox/Sys/Net.pm b/Proxmox/Sys/Net.pm
index 6fe99ec..2183d27 100644
--- a/Proxmox/Sys/Net.pm
+++ b/Proxmox/Sys/Net.pm
@@ -3,7 +3,9 @@ package Proxmox::Sys::Net;
use strict;
use warnings;
+use Proxmox::Sys::Command;
use Proxmox::Sys::Udev;
+use JSON qw();
use base qw(Exporter);
our @EXPORT_OK = qw(parse_ip_address parse_ip_mask parse_fqdn);
@@ -129,9 +131,59 @@ sub parse_ip_mask {
return;
}
+sub iface_is_vf {
+ my ($iface_name) = @_;
+ return -l "/sys/class/net/$iface_name/device/physfn";
+}
+
+# Duplicated from pve-common/src/PVE/Network.pm for now
+sub ip_link_details {
+ my $link_json = '';
+
+ Proxmox::Sys::Command::run_command(
+ ['ip', '-details', '-json', 'link', 'show'],
+ sub {
+ $link_json .= shift;
+ return;
+ },
+ );
+
+ my $links = JSON::decode_json($link_json);
+ my %ip_links = map { $_->{ifname} => $_ } $links->@*;
+
+ return \%ip_links;
+}
+
+# Duplicated from pve-common/src/PVE/Network.pm from now
+sub ip_link_is_physical {
+ my ($ip_link) = @_;
+
+ # ether alone isn't enough, as virtual interfaces can also have link_type
+ # ether
+ return $ip_link->{link_type} eq 'ether'
+ && (!defined($ip_link->{linkinfo}) || !defined($ip_link->{linkinfo}->{info_kind}));
+}
+
+my $LINK_FILE_TEMPLATE = <<EOF;
+# setup by the Proxmox installer.
+[Match]
+MACAddress=%s
+Type=ether
+
+[Link]
+Name=%s
+EOF
+
+sub get_pin_link_file_content {
+ my ($mac, $pin_name) = @_;
+
+ return sprintf($LINK_FILE_TEMPLATE, $mac, $pin_name);
+}
+
sub get_ip_config {
my $ifaces = {};
my $default;
+ my $pinned_counter = 0;
my $links = `ip -o l`;
foreach my $l (split /\n/, $links) {
@@ -144,11 +196,13 @@ sub get_ip_config {
$ifaces->{"$index"} = {
name => $name,
+ pinned_id => "${pinned_counter}",
driver => $driver,
flags => $flags,
state => $state,
mac => $mac,
};
+ $pinned_counter++;
my $addresses = `ip -o a s $name`;
for my $addr_line (split /\n/, $addresses) {
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/fqdn_from_dhcp_no_default_domain.run-env.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/fqdn_from_dhcp_no_default_domain.run-env.json
index ef14419..e7c2205 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer_fail/fqdn_from_dhcp_no_default_domain.run-env.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/fqdn_from_dhcp_no_default_domain.run-env.json
@@ -181,55 +181,64 @@
"index": 4,
"mac": "b4:2e:99:ac:ad:b4",
"name": "eno1",
- "state": "UP"
+ "state": "UP",
+ "pinned_id": "0"
},
"eno2": {
"index": 6,
"mac": "b4:2e:99:ac:ad:b5",
"name": "eno2",
- "state": "UP"
+ "state": "UP",
+ "pinned_id": "1"
},
"enp129s0f0np0": {
"index": 7,
"mac": "1c:34:da:5c:5e:24",
"name": "enp129s0f0np0",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "2"
},
"enp129s0f1np1": {
"index": 8,
"mac": "1c:34:da:5c:5e:25",
"name": "enp129s0f1np1",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "3"
},
"enp193s0f0np0": {
"index": 9,
"mac": "24:8a:07:1e:05:bc",
"name": "enp193s0f0np0",
- "state": "UP"
+ "state": "UP",
+ "pinned_id": "4"
},
"enp193s0f1np1": {
"index": 10,
"mac": "24:8a:07:1e:05:bd",
"name": "enp193s0f1np1",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "5"
},
"enp65s0f0": {
"index": 2,
"mac": "a0:36:9f:0a:b3:82",
"name": "enp65s0f0",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "6"
},
"enp65s0f1": {
"index": 3,
"mac": "a0:36:9f:0a:b3:83",
"name": "enp65s0f1",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "7"
},
"enx5a4732ddc747": {
"index": 5,
"mac": "5a:47:32:dd:c7:47",
"name": "enx5a4732ddc747",
- "state": "UNKNOWN"
+ "state": "UNKNOWN",
+ "pinned_id": "8"
}
},
"routes": {
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/no_fqdn_from_dhcp.run-env.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/no_fqdn_from_dhcp.run-env.json
index e3cea3e..47d7bde 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer_fail/no_fqdn_from_dhcp.run-env.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/no_fqdn_from_dhcp.run-env.json
@@ -180,55 +180,64 @@
"index": 4,
"mac": "b4:2e:99:ac:ad:b4",
"name": "eno1",
- "state": "UP"
+ "state": "UP",
+ "pinned_id": "0"
},
"eno2": {
"index": 6,
"mac": "b4:2e:99:ac:ad:b5",
"name": "eno2",
- "state": "UP"
+ "state": "UP",
+ "pinned_id": "1"
},
"enp129s0f0np0": {
"index": 7,
"mac": "1c:34:da:5c:5e:24",
"name": "enp129s0f0np0",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "2"
},
"enp129s0f1np1": {
"index": 8,
"mac": "1c:34:da:5c:5e:25",
"name": "enp129s0f1np1",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "3"
},
"enp193s0f0np0": {
"index": 9,
"mac": "24:8a:07:1e:05:bc",
"name": "enp193s0f0np0",
- "state": "UP"
+ "state": "UP",
+ "pinned_id": "4"
},
"enp193s0f1np1": {
"index": 10,
"mac": "24:8a:07:1e:05:bd",
"name": "enp193s0f1np1",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "5"
},
"enp65s0f0": {
"index": 2,
"mac": "a0:36:9f:0a:b3:82",
"name": "enp65s0f0",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "6"
},
"enp65s0f1": {
"index": 3,
"mac": "a0:36:9f:0a:b3:83",
"name": "enp65s0f1",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "7"
},
"enx5a4732ddc747": {
"index": 5,
"mac": "5a:47:32:dd:c7:47",
"name": "enx5a4732ddc747",
- "state": "UNKNOWN"
+ "state": "UNKNOWN",
+ "pinned_id": "8"
}
},
"routes": {
diff --git a/proxmox-auto-installer/tests/resources/run-env-info.json b/proxmox-auto-installer/tests/resources/run-env-info.json
index 5a8d80a..5291721 100644
--- a/proxmox-auto-installer/tests/resources/run-env-info.json
+++ b/proxmox-auto-installer/tests/resources/run-env-info.json
@@ -181,55 +181,64 @@
"index": 4,
"mac": "b4:2e:99:ac:ad:b4",
"name": "eno1",
- "state": "UP"
+ "state": "UP",
+ "pinned_id": "0"
},
"eno2": {
"index": 6,
"mac": "b4:2e:99:ac:ad:b5",
"name": "eno2",
- "state": "UP"
+ "state": "UP",
+ "pinned_id": "1"
},
"enp129s0f0np0": {
"index": 7,
"mac": "1c:34:da:5c:5e:24",
"name": "enp129s0f0np0",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "2"
},
"enp129s0f1np1": {
"index": 8,
"mac": "1c:34:da:5c:5e:25",
"name": "enp129s0f1np1",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "3"
},
"enp193s0f0np0": {
"index": 9,
"mac": "24:8a:07:1e:05:bc",
"name": "enp193s0f0np0",
- "state": "UP"
+ "state": "UP",
+ "pinned_id": "4"
},
"enp193s0f1np1": {
"index": 10,
"mac": "24:8a:07:1e:05:bd",
"name": "enp193s0f1np1",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "5"
},
"enp65s0f0": {
"index": 2,
"mac": "a0:36:9f:0a:b3:82",
"name": "enp65s0f0",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "6"
},
- "en p65s0f1": {
+ "enp65s0f1": {
"index": 3,
"mac": "a0:36:9f:0a:b3:83",
"name": "enp65s0f1",
- "state": "DOWN"
+ "state": "DOWN",
+ "pinned_id": "7"
},
"enx5a4732ddc747": {
"index": 5,
"mac": "5a:47:32:dd:c7:47",
"name": "enx5a4732ddc747",
- "state": "UNKNOWN"
+ "state": "UNKNOWN",
+ "pinned_id": "8"
}
},
"routes": {
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pve-devel] [PATCH installer 03/14] run env: network: add kernel driver name to network interface info
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 01/14] test: parse-kernel-cmdline: fix module import statement Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 02/14] install: add support for network interface name pinning Christoph Heiss
@ 2025-10-14 13:21 ` Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 04/14] common: utils: fix clippy warnings Christoph Heiss
` (10 subsequent siblings)
13 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
This information is already available in the GUI, this makes it possible
to show it in the TUI too.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Proxmox/Install/RunEnv.pm | 7 +++++++
.../fqdn_from_dhcp_no_default_domain.run-env.json | 9 +++++++++
.../parse_answer_fail/no_fqdn_from_dhcp.run-env.json | 9 +++++++++
proxmox-auto-installer/tests/resources/run-env-info.json | 9 +++++++++
proxmox-installer-common/src/options.rs | 2 ++
proxmox-installer-common/src/setup.rs | 3 +++
6 files changed, 39 insertions(+)
diff --git a/Proxmox/Install/RunEnv.pm b/Proxmox/Install/RunEnv.pm
index f0fa6ec..40a9621 100644
--- a/Proxmox/Install/RunEnv.pm
+++ b/Proxmox/Install/RunEnv.pm
@@ -72,6 +72,9 @@ sub query_cpu_info : prototype() {
# mac => <mac address>,
# index => <index>,
# name => <ifname>,
+# state => <UP|DOWN|LOWERLAYERDOWN|DORMANT|TESTING|NOTPRESENT|UNKNOWN>,
+# pinned_id => <sequential numerical ID>,
+# driver => <driver name>,
# addresses => [
# family => <inet|inet6>,
# address => <mac address>,
@@ -113,12 +116,16 @@ my sub query_netdevs : prototype() {
}
}
+ my $driver = readlink "/sys/class/net/$name/device/driver" || 'unknown';
+ $driver =~ s!^.*/!!;
+
$ifs->{$name} = {
index => $index,
name => $name,
pinned_id => "${pinned_counter}",
mac => $mac,
state => uc($state),
+ driver => $driver,
};
$ifs->{$name}->{addresses} = \@valid_addrs if @valid_addrs;
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/fqdn_from_dhcp_no_default_domain.run-env.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/fqdn_from_dhcp_no_default_domain.run-env.json
index e7c2205..4c41c13 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer_fail/fqdn_from_dhcp_no_default_domain.run-env.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/fqdn_from_dhcp_no_default_domain.run-env.json
@@ -182,6 +182,7 @@
"mac": "b4:2e:99:ac:ad:b4",
"name": "eno1",
"state": "UP",
+ "driver": "igb",
"pinned_id": "0"
},
"eno2": {
@@ -189,6 +190,7 @@
"mac": "b4:2e:99:ac:ad:b5",
"name": "eno2",
"state": "UP",
+ "driver": "igb",
"pinned_id": "1"
},
"enp129s0f0np0": {
@@ -196,6 +198,7 @@
"mac": "1c:34:da:5c:5e:24",
"name": "enp129s0f0np0",
"state": "DOWN",
+ "driver": "mlx5_core",
"pinned_id": "2"
},
"enp129s0f1np1": {
@@ -203,6 +206,7 @@
"mac": "1c:34:da:5c:5e:25",
"name": "enp129s0f1np1",
"state": "DOWN",
+ "driver": "mlx5_core",
"pinned_id": "3"
},
"enp193s0f0np0": {
@@ -210,6 +214,7 @@
"mac": "24:8a:07:1e:05:bc",
"name": "enp193s0f0np0",
"state": "UP",
+ "driver": "mlx5_core",
"pinned_id": "4"
},
"enp193s0f1np1": {
@@ -217,6 +222,7 @@
"mac": "24:8a:07:1e:05:bd",
"name": "enp193s0f1np1",
"state": "DOWN",
+ "driver": "mlx5_core",
"pinned_id": "5"
},
"enp65s0f0": {
@@ -224,6 +230,7 @@
"mac": "a0:36:9f:0a:b3:82",
"name": "enp65s0f0",
"state": "DOWN",
+ "driver": "igb",
"pinned_id": "6"
},
"enp65s0f1": {
@@ -231,6 +238,7 @@
"mac": "a0:36:9f:0a:b3:83",
"name": "enp65s0f1",
"state": "DOWN",
+ "driver": "igb",
"pinned_id": "7"
},
"enx5a4732ddc747": {
@@ -238,6 +246,7 @@
"mac": "5a:47:32:dd:c7:47",
"name": "enx5a4732ddc747",
"state": "UNKNOWN",
+ "driver": "cdc_ether",
"pinned_id": "8"
}
},
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/no_fqdn_from_dhcp.run-env.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/no_fqdn_from_dhcp.run-env.json
index 47d7bde..fa4e834 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer_fail/no_fqdn_from_dhcp.run-env.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/no_fqdn_from_dhcp.run-env.json
@@ -181,6 +181,7 @@
"mac": "b4:2e:99:ac:ad:b4",
"name": "eno1",
"state": "UP",
+ "driver": "igb",
"pinned_id": "0"
},
"eno2": {
@@ -188,6 +189,7 @@
"mac": "b4:2e:99:ac:ad:b5",
"name": "eno2",
"state": "UP",
+ "driver": "igb",
"pinned_id": "1"
},
"enp129s0f0np0": {
@@ -195,6 +197,7 @@
"mac": "1c:34:da:5c:5e:24",
"name": "enp129s0f0np0",
"state": "DOWN",
+ "driver": "mlx5_core",
"pinned_id": "2"
},
"enp129s0f1np1": {
@@ -202,6 +205,7 @@
"mac": "1c:34:da:5c:5e:25",
"name": "enp129s0f1np1",
"state": "DOWN",
+ "driver": "mlx5_core",
"pinned_id": "3"
},
"enp193s0f0np0": {
@@ -209,6 +213,7 @@
"mac": "24:8a:07:1e:05:bc",
"name": "enp193s0f0np0",
"state": "UP",
+ "driver": "mlx5_core",
"pinned_id": "4"
},
"enp193s0f1np1": {
@@ -216,6 +221,7 @@
"mac": "24:8a:07:1e:05:bd",
"name": "enp193s0f1np1",
"state": "DOWN",
+ "driver": "mlx5_core",
"pinned_id": "5"
},
"enp65s0f0": {
@@ -223,6 +229,7 @@
"mac": "a0:36:9f:0a:b3:82",
"name": "enp65s0f0",
"state": "DOWN",
+ "driver": "igb",
"pinned_id": "6"
},
"enp65s0f1": {
@@ -230,6 +237,7 @@
"mac": "a0:36:9f:0a:b3:83",
"name": "enp65s0f1",
"state": "DOWN",
+ "driver": "igb",
"pinned_id": "7"
},
"enx5a4732ddc747": {
@@ -237,6 +245,7 @@
"mac": "5a:47:32:dd:c7:47",
"name": "enx5a4732ddc747",
"state": "UNKNOWN",
+ "driver": "cdc_ether",
"pinned_id": "8"
}
},
diff --git a/proxmox-auto-installer/tests/resources/run-env-info.json b/proxmox-auto-installer/tests/resources/run-env-info.json
index 5291721..add5f2a 100644
--- a/proxmox-auto-installer/tests/resources/run-env-info.json
+++ b/proxmox-auto-installer/tests/resources/run-env-info.json
@@ -182,6 +182,7 @@
"mac": "b4:2e:99:ac:ad:b4",
"name": "eno1",
"state": "UP",
+ "driver": "igb",
"pinned_id": "0"
},
"eno2": {
@@ -189,6 +190,7 @@
"mac": "b4:2e:99:ac:ad:b5",
"name": "eno2",
"state": "UP",
+ "driver": "igb",
"pinned_id": "1"
},
"enp129s0f0np0": {
@@ -196,6 +198,7 @@
"mac": "1c:34:da:5c:5e:24",
"name": "enp129s0f0np0",
"state": "DOWN",
+ "driver": "mlx5_core",
"pinned_id": "2"
},
"enp129s0f1np1": {
@@ -203,6 +206,7 @@
"mac": "1c:34:da:5c:5e:25",
"name": "enp129s0f1np1",
"state": "DOWN",
+ "driver": "mlx5_core",
"pinned_id": "3"
},
"enp193s0f0np0": {
@@ -210,6 +214,7 @@
"mac": "24:8a:07:1e:05:bc",
"name": "enp193s0f0np0",
"state": "UP",
+ "driver": "mlx5_core",
"pinned_id": "4"
},
"enp193s0f1np1": {
@@ -217,6 +222,7 @@
"mac": "24:8a:07:1e:05:bd",
"name": "enp193s0f1np1",
"state": "DOWN",
+ "driver": "mlx5_core",
"pinned_id": "5"
},
"enp65s0f0": {
@@ -224,6 +230,7 @@
"mac": "a0:36:9f:0a:b3:82",
"name": "enp65s0f0",
"state": "DOWN",
+ "driver": "igb",
"pinned_id": "6"
},
"enp65s0f1": {
@@ -231,6 +238,7 @@
"mac": "a0:36:9f:0a:b3:83",
"name": "enp65s0f1",
"state": "DOWN",
+ "driver": "igb",
"pinned_id": "7"
},
"enx5a4732ddc747": {
@@ -238,6 +246,7 @@
"mac": "5a:47:32:dd:c7:47",
"name": "enx5a4732ddc747",
"state": "UNKNOWN",
+ "driver": "cdc_ether",
"pinned_id": "8"
}
},
diff --git a/proxmox-installer-common/src/options.rs b/proxmox-installer-common/src/options.rs
index 48c77c9..50fd74e 100644
--- a/proxmox-installer-common/src/options.rs
+++ b/proxmox-installer-common/src/options.rs
@@ -677,6 +677,7 @@ mod tests {
name: "eth0".to_owned(),
index: 0,
state: InterfaceState::Up,
+ driver: "dummy".to_owned(),
mac: "01:23:45:67:89:ab".to_owned(),
addresses: Some(vec![
CidrAddress::new(Ipv4Addr::new(192, 168, 0, 2), 24).unwrap(),
@@ -804,6 +805,7 @@ mod tests {
name: "eth0".to_owned(),
index: 0,
state: InterfaceState::Up,
+ driver: "dummy".to_owned(),
mac: "01:23:45:67:89:ab".to_owned(),
addresses: None,
},
diff --git a/proxmox-installer-common/src/setup.rs b/proxmox-installer-common/src/setup.rs
index 66cea72..4fda39d 100644
--- a/proxmox-installer-common/src/setup.rs
+++ b/proxmox-installer-common/src/setup.rs
@@ -473,6 +473,9 @@ pub struct Interface {
pub state: InterfaceState,
+ /// Kernel driver name that claims this interface.
+ pub driver: String,
+
#[serde(default)]
#[serde(deserialize_with = "deserialize_cidr_list")]
pub addresses: Option<Vec<CidrAddress>>,
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pve-devel] [PATCH installer 04/14] common: utils: fix clippy warnings
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
` (2 preceding siblings ...)
2025-10-14 13:21 ` [pve-devel] [PATCH installer 03/14] run env: network: add kernel driver name to network interface info Christoph Heiss
@ 2025-10-14 13:21 ` Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 05/14] common: setup: simplify network address list serialization Christoph Heiss
` (9 subsequent siblings)
13 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
No functional changes.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-installer-common/src/utils.rs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/proxmox-installer-common/src/utils.rs b/proxmox-installer-common/src/utils.rs
index 054a0fd..ffc862e 100644
--- a/proxmox-installer-common/src/utils.rs
+++ b/proxmox-installer-common/src/utils.rs
@@ -130,14 +130,14 @@ fn mask_limit(addr: &IpAddr) -> usize {
}
fn check_mask_limit(addr: &IpAddr, mask: usize) -> Result<(), CidrAddressParseError> {
- let limit = mask_limit(&addr);
- return if mask > limit {
+ let limit = mask_limit(addr);
+ if mask > limit {
Err(CidrAddressParseError::InvalidMask(
format!("mask cannot be greater than {limit}").into(),
))
} else {
Ok(())
- };
+ }
}
/// Possible errors that might occur when parsing FQDNs.
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pve-devel] [PATCH installer 05/14] common: setup: simplify network address list serialization
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
` (3 preceding siblings ...)
2025-10-14 13:21 ` [pve-devel] [PATCH installer 04/14] common: utils: fix clippy warnings Christoph Heiss
@ 2025-10-14 13:21 ` Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 06/14] common: implement support for `network_interface_pin_map` config Christoph Heiss
` (8 subsequent siblings)
13 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
Option<Vec<..>> is redundant in combination with #[serde(default)],
as the latter already defaults to an empty Vec<..> if the key does not
exist.
No functional changes.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-installer-common/src/options.rs | 26 ++++++++++---------------
proxmox-installer-common/src/setup.rs | 6 +++---
2 files changed, 13 insertions(+), 19 deletions(-)
diff --git a/proxmox-installer-common/src/options.rs b/proxmox-installer-common/src/options.rs
index 50fd74e..59e8560 100644
--- a/proxmox-installer-common/src/options.rs
+++ b/proxmox-installer-common/src/options.rs
@@ -518,24 +518,20 @@ impl NetworkOptions {
if let Some(gw) = &routes.gateway4 {
if let Some(iface) = network.interfaces.get(&gw.dev) {
this.ifname.clone_from(&iface.name);
- if let Some(addresses) = &iface.addresses {
- if let Some(addr) = addresses.iter().find(|addr| addr.is_ipv4()) {
- this.gateway = gw.gateway;
- this.address = addr.clone();
- filled = true;
- }
+ if let Some(addr) = iface.addresses.iter().find(|addr| addr.is_ipv4()) {
+ this.gateway = gw.gateway;
+ this.address = addr.clone();
+ filled = true;
}
}
}
if !filled {
if let Some(gw) = &routes.gateway6 {
if let Some(iface) = network.interfaces.get(&gw.dev) {
- if let Some(addresses) = &iface.addresses {
- if let Some(addr) = addresses.iter().find(|addr| addr.is_ipv6()) {
- this.ifname.clone_from(&iface.name);
- this.gateway = gw.gateway;
- this.address = addr.clone();
- }
+ if let Some(addr) = iface.addresses.iter().find(|addr| addr.is_ipv6()) {
+ this.ifname.clone_from(&iface.name);
+ this.gateway = gw.gateway;
+ this.address = addr.clone();
}
}
}
@@ -679,9 +675,7 @@ mod tests {
state: InterfaceState::Up,
driver: "dummy".to_owned(),
mac: "01:23:45:67:89:ab".to_owned(),
- addresses: Some(vec![
- CidrAddress::new(Ipv4Addr::new(192, 168, 0, 2), 24).unwrap(),
- ]),
+ addresses: vec![CidrAddress::new(Ipv4Addr::new(192, 168, 0, 2), 24).unwrap()],
},
);
@@ -807,7 +801,7 @@ mod tests {
state: InterfaceState::Up,
driver: "dummy".to_owned(),
mac: "01:23:45:67:89:ab".to_owned(),
- addresses: None,
+ addresses: vec![],
},
);
diff --git a/proxmox-installer-common/src/setup.rs b/proxmox-installer-common/src/setup.rs
index 4fda39d..4873fff 100644
--- a/proxmox-installer-common/src/setup.rs
+++ b/proxmox-installer-common/src/setup.rs
@@ -321,7 +321,7 @@ where
.collect())
}
-fn deserialize_cidr_list<'de, D>(deserializer: D) -> Result<Option<Vec<CidrAddress>>, D::Error>
+fn deserialize_cidr_list<'de, D>(deserializer: D) -> Result<Vec<CidrAddress>, D::Error>
where
D: Deserializer<'de>,
{
@@ -347,7 +347,7 @@ where
);
}
- Ok(Some(result))
+ Ok(result)
}
fn serialize_as_display<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
@@ -478,7 +478,7 @@ pub struct Interface {
#[serde(default)]
#[serde(deserialize_with = "deserialize_cidr_list")]
- pub addresses: Option<Vec<CidrAddress>>,
+ pub addresses: Vec<CidrAddress>,
}
impl Interface {
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pve-devel] [PATCH installer 06/14] common: implement support for `network_interface_pin_map` config
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
` (4 preceding siblings ...)
2025-10-14 13:21 ` [pve-devel] [PATCH installer 05/14] common: setup: simplify network address list serialization Christoph Heiss
@ 2025-10-14 13:21 ` Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 07/14] auto: add support for pinning network interface names Christoph Heiss
` (7 subsequent siblings)
13 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
Adds all the pieces for installer frontends to wire up pinning support,
i.e. deserializing from the runtime environment, doing verification and
serializing it out to the low-level installer.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-auto-installer/src/utils.rs | 3 +-
proxmox-installer-common/src/lib.rs | 5 +
proxmox-installer-common/src/options.rs | 158 ++++++++++++++++++++++--
proxmox-installer-common/src/setup.rs | 51 +++++++-
proxmox-tui-installer/src/setup.rs | 3 +-
5 files changed, 203 insertions(+), 17 deletions(-)
diff --git a/proxmox-auto-installer/src/utils.rs b/proxmox-auto-installer/src/utils.rs
index 7d42f2c..eb666d1 100644
--- a/proxmox-auto-installer/src/utils.rs
+++ b/proxmox-auto-installer/src/utils.rs
@@ -2,7 +2,7 @@ use anyhow::{Context, Result, bail};
use glob::Pattern;
use log::info;
use std::{
- collections::{BTreeMap, HashSet},
+ collections::{BTreeMap, HashMap, HashSet},
process::Command,
};
@@ -485,6 +485,7 @@ pub fn parse_answer(
root_ssh_keys: answer.global.root_ssh_keys.clone(),
mngmt_nic: network_settings.ifname,
+ network_interface_pin_map: HashMap::new(),
hostname: network_settings
.fqdn
diff --git a/proxmox-installer-common/src/lib.rs b/proxmox-installer-common/src/lib.rs
index ea907a0..a85d5f8 100644
--- a/proxmox-installer-common/src/lib.rs
+++ b/proxmox-installer-common/src/lib.rs
@@ -10,6 +10,11 @@ pub mod http;
#[cfg(feature = "cli")]
pub mod cli;
+pub mod net {
+ /// Maximum length of the (primary) name of a network interface
+ pub const MAX_IFNAME_LEN: usize = 15; // IFNAMSIZ - 1 to account for NUL byte
+}
+
pub const RUNTIME_DIR: &str = "/run/proxmox-installer";
/// Default placeholder value for the administrator email address.
diff --git a/proxmox-installer-common/src/options.rs b/proxmox-installer-common/src/options.rs
index 59e8560..60ea227 100644
--- a/proxmox-installer-common/src/options.rs
+++ b/proxmox-installer-common/src/options.rs
@@ -1,12 +1,14 @@
use anyhow::{Result, bail};
use regex::Regex;
use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr};
use std::str::FromStr;
use std::sync::OnceLock;
use std::{cmp, fmt};
use crate::disk_checks::check_raid_min_disks;
+use crate::net::MAX_IFNAME_LEN;
use crate::setup::{LocaleInfo, NetworkInfo, RuntimeInfo, SetupInfo};
use crate::utils::{CidrAddress, Fqdn};
@@ -476,6 +478,54 @@ impl TimezoneOptions {
}
}
+/// Options controlling the behaviour of the network interface pinning (by
+/// creating appropriate systemd.link files) during the installation.
+#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
+pub struct NetworkInterfacePinningOptions {
+ /// Maps MAC address to custom name
+ #[serde(default)]
+ pub mapping: HashMap<String, String>,
+}
+
+impl NetworkInterfacePinningOptions {
+ /// Default prefix to prepend to the pinned interface ID as received from the low-level
+ /// installer.
+ pub const DEFAULT_PREFIX: &str = "nic";
+
+ /// Do some basic checks on the options.
+ ///
+ /// This checks for:
+ /// - empty interface names
+ /// - overlong interface names
+ /// - duplicate interface names
+ pub fn verify(&self) -> Result<()> {
+ let mut reverse_mapping = HashMap::<String, String>::new();
+ for (mac, name) in self.mapping.iter() {
+ if name.is_empty() {
+ bail!("interface name mapping for '{mac}' cannot be empty");
+ }
+
+ if name.len() > MAX_IFNAME_LEN {
+ bail!(
+ "interface name mapping '{name}' for '{mac}' cannot be longer than {} characters",
+ MAX_IFNAME_LEN
+ );
+ }
+
+ if let Some(duplicate_mac) = reverse_mapping.get(name) {
+ if mac != duplicate_mac {
+ bail!("duplicate interface name mapping '{name}' for: {mac}, {duplicate_mac}");
+ }
+ }
+
+ reverse_mapping.insert(name.clone(), mac.clone());
+ }
+
+ Ok(())
+ }
+}
+
#[derive(Clone, Debug, PartialEq)]
pub struct NetworkOptions {
pub ifname: String,
@@ -483,6 +533,7 @@ pub struct NetworkOptions {
pub address: CidrAddress,
pub gateway: IpAddr,
pub dns_server: IpAddr,
+ pub pinning_opts: Option<NetworkInterfacePinningOptions>,
}
impl NetworkOptions {
@@ -492,6 +543,7 @@ impl NetworkOptions {
setup: &SetupInfo,
network: &NetworkInfo,
default_domain: Option<&str>,
+ pinning_opts: Option<&NetworkInterfacePinningOptions>,
) -> Self {
// Sets up sensible defaults as much as possible, such that even in the
// worse case nothing breaks down *completely*.
@@ -507,6 +559,7 @@ impl NetworkOptions {
address: CidrAddress::new(Ipv4Addr::new(192, 168, 100, 2), 24).unwrap(),
gateway: Ipv4Addr::new(192, 168, 100, 1).into(),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
+ pinning_opts: pinning_opts.cloned(),
};
if let Some(ip) = network.dns.dns.first() {
@@ -517,7 +570,11 @@ impl NetworkOptions {
let mut filled = false;
if let Some(gw) = &routes.gateway4 {
if let Some(iface) = network.interfaces.get(&gw.dev) {
- this.ifname.clone_from(&iface.name);
+ if let Some(opts) = pinning_opts {
+ this.ifname.clone_from(&iface.to_pinned(opts).name);
+ } else {
+ this.ifname.clone_from(&iface.name);
+ }
if let Some(addr) = iface.addresses.iter().find(|addr| addr.is_ipv4()) {
this.gateway = gw.gateway;
this.address = addr.clone();
@@ -529,7 +586,11 @@ impl NetworkOptions {
if let Some(gw) = &routes.gateway6 {
if let Some(iface) = network.interfaces.get(&gw.dev) {
if let Some(addr) = iface.addresses.iter().find(|addr| addr.is_ipv6()) {
- this.ifname.clone_from(&iface.name);
+ if let Some(opts) = pinning_opts {
+ this.ifname.clone_from(&iface.to_pinned(opts).name);
+ } else {
+ this.ifname.clone_from(&iface.name);
+ }
this.gateway = gw.gateway;
this.address = addr.clone();
}
@@ -544,7 +605,20 @@ impl NetworkOptions {
// earlier in that case, so use the first one enumerated.
if this.ifname.is_empty() {
if let Some(iface) = network.interfaces.values().min_by_key(|v| v.index) {
- this.ifname.clone_from(&iface.name);
+ if let Some(opts) = pinning_opts {
+ this.ifname.clone_from(&iface.to_pinned(opts).name);
+ } else {
+ this.ifname.clone_from(&iface.name);
+ }
+ }
+ }
+
+ if let Some(ref mut opts) = this.pinning_opts {
+ // Ensure that all unique interfaces indeed have an entry in the map,
+ // as required by the low-level installer
+ for iface in network.interfaces.values() {
+ let pinned_name = iface.to_pinned(opts).name;
+ opts.mapping.entry(iface.mac.clone()).or_insert(pinned_name);
}
}
@@ -672,6 +746,7 @@ mod tests {
Interface {
name: "eth0".to_owned(),
index: 0,
+ pinned_id: "0".to_owned(),
state: InterfaceState::Up,
driver: "dummy".to_owned(),
mac: "01:23:45:67:89:ab".to_owned(),
@@ -703,49 +778,53 @@ mod tests {
let (setup, mut info) = mock_setup_network();
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, None),
+ NetworkOptions::defaults_from(&setup, &info, None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("foo.bar.com").unwrap(),
address: CidrAddress::new(Ipv4Addr::new(192, 168, 0, 2), 24).unwrap(),
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
+ pinning_opts: None,
}
);
info.hostname = None;
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, None),
+ NetworkOptions::defaults_from(&setup, &info, None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("pve.bar.com").unwrap(),
address: CidrAddress::new(Ipv4Addr::new(192, 168, 0, 2), 24).unwrap(),
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
+ pinning_opts: None,
}
);
info.dns.domain = None;
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, None),
+ NetworkOptions::defaults_from(&setup, &info, None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("pve.example.invalid").unwrap(),
address: CidrAddress::new(Ipv4Addr::new(192, 168, 0, 2), 24).unwrap(),
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
+ pinning_opts: None,
}
);
info.hostname = Some("foo".to_owned());
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, None),
+ NetworkOptions::defaults_from(&setup, &info, None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("foo.example.invalid").unwrap(),
address: CidrAddress::new(Ipv4Addr::new(192, 168, 0, 2), 24).unwrap(),
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
+ pinning_opts: None,
}
);
}
@@ -755,37 +834,40 @@ mod tests {
let (setup, mut info) = mock_setup_network();
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, None),
+ NetworkOptions::defaults_from(&setup, &info, None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("foo.bar.com").unwrap(),
address: CidrAddress::new(Ipv4Addr::new(192, 168, 0, 2), 24).unwrap(),
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
+ pinning_opts: None,
}
);
info.dns.domain = None;
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, Some("custom.local")),
+ NetworkOptions::defaults_from(&setup, &info, Some("custom.local"), None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("foo.custom.local").unwrap(),
address: CidrAddress::new(Ipv4Addr::new(192, 168, 0, 2), 24).unwrap(),
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
+ pinning_opts: None,
}
);
info.dns.domain = Some("some.domain.local".to_owned());
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, Some("custom.local")),
+ NetworkOptions::defaults_from(&setup, &info, Some("custom.local"), None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("foo.custom.local").unwrap(),
address: CidrAddress::new(Ipv4Addr::new(192, 168, 0, 2), 24).unwrap(),
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
+ pinning_opts: None,
}
);
}
@@ -798,6 +880,7 @@ mod tests {
Interface {
name: "eth0".to_owned(),
index: 0,
+ pinned_id: "0".to_owned(),
state: InterfaceState::Up,
driver: "dummy".to_owned(),
mac: "01:23:45:67:89:ab".to_owned(),
@@ -818,14 +901,67 @@ mod tests {
let setup = SetupInfo::mocked();
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, None),
+ NetworkOptions::defaults_from(&setup, &info, None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("pve.example.invalid").unwrap(),
address: CidrAddress::new(Ipv4Addr::new(192, 168, 100, 2), 24).unwrap(),
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 100, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
+ pinning_opts: None,
}
);
}
+
+ #[test]
+ fn network_interface_pinning_options_fail_on_empty_name() {
+ let mut options = NetworkInterfacePinningOptions::default();
+ options
+ .mapping
+ .insert("ab:cd:ef:12:34:56".to_owned(), String::new());
+
+ let res = options.verify();
+ assert!(res.is_err());
+ assert_eq!(
+ res.unwrap_err().to_string(),
+ "interface name mapping for 'ab:cd:ef:12:34:56' cannot be empty"
+ )
+ }
+
+ #[test]
+ fn network_interface_pinning_options_fail_on_overlong_name() {
+ let mut options = NetworkInterfacePinningOptions::default();
+ options.mapping.insert(
+ "ab:cd:ef:12:34:56".to_owned(),
+ "waytoolonginterfacename".to_owned(),
+ );
+
+ let res = options.verify();
+ assert!(res.is_err());
+ assert_eq!(
+ res.unwrap_err().to_string(),
+ "interface name mapping 'waytoolonginterfacename' for 'ab:cd:ef:12:34:56' cannot be longer than 15 characters"
+ )
+ }
+
+ #[test]
+ fn network_interface_pinning_options_fail_on_duplicate_name() {
+ let mut options = NetworkInterfacePinningOptions::default();
+ options
+ .mapping
+ .insert("ab:cd:ef:12:34:56".to_owned(), "nic0".to_owned());
+ options
+ .mapping
+ .insert("12:34:56:ab:cd:ef".to_owned(), "nic0".to_owned());
+
+ let res = options.verify();
+ assert!(res.is_err());
+ let err = res.unwrap_err().to_string();
+
+ // [HashMap] does not guarantee iteration order, so just check for the substrings
+ // we expect to find
+ assert!(err.contains("duplicate interface name mapping 'nic0' for: "));
+ assert!(err.contains("12:34:56:ab:cd:ef"));
+ assert!(err.contains("ab:cd:ef:12:34:56"));
+ }
}
diff --git a/proxmox-installer-common/src/setup.rs b/proxmox-installer-common/src/setup.rs
index 4873fff..3e99576 100644
--- a/proxmox-installer-common/src/setup.rs
+++ b/proxmox-installer-common/src/setup.rs
@@ -3,6 +3,7 @@ use std::{
collections::{BTreeMap, HashMap},
fmt,
fs::File,
+ hash::Hash,
io::{self, BufReader},
net::IpAddr,
path::{Path, PathBuf},
@@ -13,8 +14,8 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use crate::{
options::{
- BtrfsBootdiskOptions, BtrfsCompressOption, Disk, FsType, ZfsBootdiskOptions,
- ZfsChecksumOption, ZfsCompressOption,
+ BtrfsBootdiskOptions, BtrfsCompressOption, Disk, FsType, NetworkInterfacePinningOptions,
+ ZfsBootdiskOptions, ZfsChecksumOption, ZfsCompressOption,
},
utils::CidrAddress,
};
@@ -443,8 +444,9 @@ pub struct Gateway {
pub gateway: IpAddr,
}
-#[derive(Clone, Deserialize)]
+#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "UPPERCASE")]
+/// The current interface state as reported by the kernel.
pub enum InterfaceState {
Up,
Down,
@@ -452,6 +454,8 @@ pub enum InterfaceState {
Unknown,
}
+serde_plain::derive_display_from_serialize!(InterfaceState);
+
impl InterfaceState {
// avoid display trait as this is not the string representation for a serializer
pub fn render(&self) -> String {
@@ -469,6 +473,9 @@ pub struct Interface {
pub index: usize,
+ /// Sequential interface ID for pinning interface names.
+ pub pinned_id: String,
+
pub mac: String,
pub state: InterfaceState,
@@ -484,7 +491,22 @@ pub struct Interface {
impl Interface {
// avoid display trait as this is not the string representation for a serializer
pub fn render(&self) -> String {
- format!("{} {}", self.state.render(), self.name)
+ format!("{} {} ({})", self.state.render(), self.name, self.mac)
+ }
+
+ pub fn to_pinned(&self, options: &NetworkInterfacePinningOptions) -> Self {
+ let mut this = self.clone();
+ this.name = options
+ .mapping
+ .get(&this.mac)
+ .unwrap_or(&format!(
+ "{}{}",
+ NetworkInterfacePinningOptions::DEFAULT_PREFIX,
+ this.pinned_id
+ ))
+ .clone();
+
+ this
}
}
@@ -577,6 +599,14 @@ pub struct InstallConfig {
pub root_ssh_keys: Vec<String>,
pub mngmt_nic: String,
+ // Maps MAC addresses -> custom name. If set, enables pinning for all
+ // interfaces present.
+ #[serde(
+ default,
+ skip_serializing_if = "HashMap::is_empty",
+ deserialize_with = "deserialize_optional_map"
+ )]
+ pub network_interface_pin_map: HashMap<String, String>,
pub hostname: String,
pub domain: String,
@@ -610,3 +640,16 @@ pub enum LowLevelMessage {
text: Option<String>,
},
}
+
+/// Deserializes an optional [HashMap].
+///
+/// If missing, it returns an empty map, otherwise the deserialized map.
+fn deserialize_optional_map<'de, D, K, V>(deserializer: D) -> Result<HashMap<K, V>, D::Error>
+where
+ D: Deserializer<'de>,
+ K: Eq + Hash + Deserialize<'de>,
+ V: Deserialize<'de>,
+{
+ let map: Option<HashMap<K, V>> = Deserialize::deserialize(deserializer)?;
+ Ok(map.unwrap_or_default())
+}
diff --git a/proxmox-tui-installer/src/setup.rs b/proxmox-tui-installer/src/setup.rs
index b90c7dc..d2ffb70 100644
--- a/proxmox-tui-installer/src/setup.rs
+++ b/proxmox-tui-installer/src/setup.rs
@@ -1,4 +1,4 @@
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashMap};
use crate::options::InstallerOptions;
use proxmox_installer_common::{
@@ -32,6 +32,7 @@ impl From<InstallerOptions> for InstallConfig {
root_ssh_keys: vec![],
mngmt_nic: options.network.ifname,
+ network_interface_pin_map: HashMap::new(),
// Safety: At this point, it is know that we have a valid FQDN, as
// this is set by the TUI network panel, which only lets the user
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pve-devel] [PATCH installer 07/14] auto: add support for pinning network interface names
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
` (5 preceding siblings ...)
2025-10-14 13:21 ` [pve-devel] [PATCH installer 06/14] common: implement support for `network_interface_pin_map` config Christoph Heiss
@ 2025-10-14 13:21 ` Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 08/14] assistant: verify network settings in `validate-answer` subcommand Christoph Heiss
` (6 subsequent siblings)
13 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
Introduce a new `[network.interface-name-pinning]` section in the answer
file, which is just a (TOML) table mapping MAC addresses to interface
names.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-auto-installer/src/answer.rs | 63 ++++++++++++++-----
proxmox-auto-installer/src/utils.rs | 40 ++++++++++--
proxmox-auto-installer/tests/parse-answer.rs | 2 +
.../network_interface_pinning.json | 30 +++++++++
.../network_interface_pinning.toml | 22 +++++++
...rface_pinning_overlong_interface_name.json | 3 +
...rface_pinning_overlong_interface_name.toml | 18 ++++++
7 files changed, 159 insertions(+), 19 deletions(-)
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning.json
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning.toml
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_interface_pinning_overlong_interface_name.json
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_interface_pinning_overlong_interface_name.toml
diff --git a/proxmox-auto-installer/src/answer.rs b/proxmox-auto-installer/src/answer.rs
index 88f4c87..1e455ca 100644
--- a/proxmox-auto-installer/src/answer.rs
+++ b/proxmox-auto-installer/src/answer.rs
@@ -1,13 +1,17 @@
-use anyhow::{Result, format_err};
+use anyhow::{Result, bail, format_err};
use proxmox_installer_common::{
options::{
- BtrfsCompressOption, BtrfsRaidLevel, FsType, ZfsChecksumOption, ZfsCompressOption,
- ZfsRaidLevel,
+ BtrfsCompressOption, BtrfsRaidLevel, FsType, NetworkInterfacePinningOptions,
+ ZfsChecksumOption, ZfsCompressOption, ZfsRaidLevel,
},
utils::{CidrAddress, Fqdn},
};
use serde::{Deserialize, Serialize};
-use std::{collections::BTreeMap, io::BufRead, net::IpAddr};
+use std::{
+ collections::{BTreeMap, HashMap},
+ io::BufRead,
+ net::IpAddr,
+};
// NOTE New answer file properties must use kebab-case, but should allow snake_case for backwards
// compatibility. TODO Remove the snake_cased variants in a future major version (e.g. PVE 10).
@@ -178,6 +182,18 @@ enum NetworkConfigMode {
FromAnswer,
}
+/// Options controlling the behaviour of the network interface pinning (by
+/// creating appropriate systemd.link files) during the installation.
+#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
+pub struct NetworkInterfacePinningOptionsAnswer {
+ /// Whether interfaces should be pinned during the installation.
+ pub enabled: bool,
+ /// Maps MAC address to custom name
+ #[serde(default)]
+ pub mapping: HashMap<String, String>,
+}
+
#[derive(Clone, Deserialize, Debug)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct NetworkInAnswer {
@@ -188,30 +204,47 @@ struct NetworkInAnswer {
pub gateway: Option<IpAddr>,
#[serde(default)]
pub filter: BTreeMap<String, String>,
+ /// Controls network interface pinning behaviour during installation.
+ /// Off by default. Allowed for both `from-dhcp` and `from-answer` modes.
+ #[serde(default)]
+ pub interface_name_pinning: Option<NetworkInterfacePinningOptionsAnswer>,
}
#[derive(Clone, Deserialize, Debug)]
#[serde(try_from = "NetworkInAnswer", deny_unknown_fields)]
pub struct Network {
pub network_settings: NetworkSettings,
+ /// Controls network interface pinning behaviour during installation.
+ pub interface_name_pinning: Option<NetworkInterfacePinningOptions>,
}
impl TryFrom<NetworkInAnswer> for Network {
- type Error = &'static str;
+ type Error = anyhow::Error;
+
+ fn try_from(network: NetworkInAnswer) -> Result<Self> {
+ let interface_name_pinning = match network.interface_name_pinning {
+ Some(opts) if opts.enabled => {
+ let opts = NetworkInterfacePinningOptions {
+ mapping: opts.mapping,
+ };
+ opts.verify()?;
+ Some(opts)
+ }
+ _ => None,
+ };
- fn try_from(network: NetworkInAnswer) -> Result<Self, Self::Error> {
if network.source == NetworkConfigMode::FromAnswer {
if network.cidr.is_none() {
- return Err("Field 'cidr' must be set.");
+ bail!("Field 'cidr' must be set.");
}
if network.dns.is_none() {
- return Err("Field 'dns' must be set.");
+ bail!("Field 'dns' must be set.");
}
if network.gateway.is_none() {
- return Err("Field 'gateway' must be set.");
+ bail!("Field 'gateway' must be set.");
}
if network.filter.is_empty() {
- return Err("Field 'filter' must be set.");
+ bail!("Field 'filter' must be set.");
}
Ok(Network {
@@ -221,23 +254,25 @@ impl TryFrom<NetworkInAnswer> for Network {
gateway: network.gateway.unwrap(),
filter: network.filter,
}),
+ interface_name_pinning,
})
} else {
if network.cidr.is_some() {
- return Err("Field 'cidr' not supported for 'from-dhcp' config.");
+ bail!("Field 'cidr' not supported for 'from-dhcp' config.");
}
if network.dns.is_some() {
- return Err("Field 'dns' not supported for 'from-dhcp' config.");
+ bail!("Field 'dns' not supported for 'from-dhcp' config.");
}
if network.gateway.is_some() {
- return Err("Field 'gateway' not supported for 'from-dhcp' config.");
+ bail!("Field 'gateway' not supported for 'from-dhcp' config.");
}
if !network.filter.is_empty() {
- return Err("Field 'filter' not supported for 'from-dhcp' config.");
+ bail!("Field 'filter' not supported for 'from-dhcp' config.");
}
Ok(Network {
network_settings: NetworkSettings::FromDhcp,
+ interface_name_pinning,
})
}
}
diff --git a/proxmox-auto-installer/src/utils.rs b/proxmox-auto-installer/src/utils.rs
index eb666d1..14085a4 100644
--- a/proxmox-auto-installer/src/utils.rs
+++ b/proxmox-auto-installer/src/utils.rs
@@ -2,14 +2,14 @@ use anyhow::{Context, Result, bail};
use glob::Pattern;
use log::info;
use std::{
- collections::{BTreeMap, HashMap, HashSet},
+ collections::{BTreeMap, HashSet},
process::Command,
};
use crate::{
answer::{
self, Answer, DiskSelection, FirstBootHookSourceMode, FqdnConfig, FqdnExtendedConfig,
- FqdnSourceMode,
+ FqdnSourceMode, Network,
},
udevinfo::UdevInfo,
};
@@ -35,7 +35,12 @@ fn get_network_settings(
let mut network_options = match &answer.global.fqdn {
// If the user set a static FQDN in the answer file, override it
FqdnConfig::Simple(name) => {
- let mut opts = NetworkOptions::defaults_from(setup_info, &runtime_info.network, None);
+ let mut opts = NetworkOptions::defaults_from(
+ setup_info,
+ &runtime_info.network,
+ None,
+ answer.network.interface_name_pinning.as_ref(),
+ );
opts.fqdn = name.to_owned();
opts
}
@@ -58,7 +63,12 @@ fn get_network_settings(
bail!("no domain received from DHCP server and `global.fqdn.domain` is unset!");
}
- NetworkOptions::defaults_from(setup_info, &runtime_info.network, domain.as_deref())
+ NetworkOptions::defaults_from(
+ setup_info,
+ &runtime_info.network,
+ domain.as_deref(),
+ answer.network.interface_name_pinning.as_ref(),
+ )
}
};
@@ -68,6 +78,12 @@ fn get_network_settings(
network_options.gateway = settings.gateway;
network_options.ifname = get_single_udev_index(&settings.filter, &udev_info.nics)?;
}
+
+ if let Some(opts) = &network_options.pinning_opts {
+ info!("Network interface name pinning is enabled");
+ opts.verify()?;
+ }
+
info!("Network interface used is '{}'", &network_options.ifname);
Ok(network_options)
}
@@ -430,6 +446,16 @@ pub fn verify_first_boot_settings(answer: &Answer) -> Result<()> {
Ok(())
}
+pub fn verify_network_settings(network: &Network) -> Result<()> {
+ info!("Verifying network settings");
+
+ if let Some(pin_opts) = &network.interface_name_pinning {
+ pin_opts.verify()?;
+ }
+
+ Ok(())
+}
+
pub fn parse_answer(
answer: &Answer,
udev_info: &UdevInfo,
@@ -451,6 +477,7 @@ pub fn parse_answer(
verify_disks_settings(answer)?;
verify_email_and_root_password_settings(answer)?;
verify_first_boot_settings(answer)?;
+ verify_network_settings(&answer.network)?;
let root_password = match (
&answer.global.root_password,
@@ -485,7 +512,10 @@ pub fn parse_answer(
root_ssh_keys: answer.global.root_ssh_keys.clone(),
mngmt_nic: network_settings.ifname,
- network_interface_pin_map: HashMap::new(),
+ network_interface_pin_map: network_settings
+ .pinning_opts
+ .map(|o| o.mapping)
+ .unwrap_or_default(),
hostname: network_settings
.fqdn
diff --git a/proxmox-auto-installer/tests/parse-answer.rs b/proxmox-auto-installer/tests/parse-answer.rs
index 6754374..696fe1f 100644
--- a/proxmox-auto-installer/tests/parse-answer.rs
+++ b/proxmox-auto-installer/tests/parse-answer.rs
@@ -129,6 +129,7 @@ mod tests {
full_fqdn_from_dhcp_with_default_domain,
hashed_root_password,
minimal,
+ network_interface_pinning,
nic_matching,
specific_nic,
zfs,
@@ -149,6 +150,7 @@ mod tests {
fqdn_hostname_only,
ipv4_and_subnet_mask_33,
lvm_swapsize_greater_than_hdsize,
+ network_interface_pinning_overlong_interface_name,
no_fqdn_from_dhcp,
no_root_password_set,
short_password,
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning.json b/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning.json
new file mode 100644
index 0000000..76723c8
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning.json
@@ -0,0 +1,30 @@
+{
+ "autoreboot": 1,
+ "cidr": "192.168.1.114/24",
+ "country": "at",
+ "dns": "192.168.1.254",
+ "domain": "testinstall",
+ "filesys": "ext4",
+ "gateway": "192.168.1.1",
+ "hdsize": 223.57088470458984,
+ "existing_storage_auto_rename": 1,
+ "hostname": "pveauto",
+ "keymap": "de",
+ "mailto": "mail@no.invalid",
+ "mngmt_nic": "mgmt",
+ "network_interface_pin_map": {
+ "1c:34:da:5c:5e:24": "nic2",
+ "1c:34:da:5c:5e:25": "nic3",
+ "24:8a:07:1e:05:bc": "lan0",
+ "24:8a:07:1e:05:bd": "lan1",
+ "5a:47:32:dd:c7:47": "nic8",
+ "a0:36:9f:0a:b3:82": "nic6",
+ "a0:36:9f:0a:b3:83": "nic7",
+ "b4:2e:99:ac:ad:b4": "mgmt",
+ "b4:2e:99:ac:ad:b5": "nic1"
+ },
+ "root_password": { "plain": "12345678" },
+ "target_hd": "/dev/sda",
+ "timezone": "Europe/Vienna",
+ "first_boot": { "enabled": 0 }
+}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning.toml b/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning.toml
new file mode 100644
index 0000000..d9a2110
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning.toml
@@ -0,0 +1,22 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root-password = "12345678"
+
+[network]
+source = "from-dhcp"
+
+[network.interface-name-pinning]
+enabled = true
+
+[network.interface-name-pinning.mapping]
+"24:8a:07:1e:05:bc" = "lan0"
+"24:8a:07:1e:05:bd" = "lan1"
+"b4:2e:99:ac:ad:b4" = "mgmt"
+
+[disk-setup]
+filesystem = "ext4"
+disk-list = ["sda"]
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/network_interface_pinning_overlong_interface_name.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_interface_pinning_overlong_interface_name.json
new file mode 100644
index 0000000..70e196c
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_interface_pinning_overlong_interface_name.json
@@ -0,0 +1,3 @@
+{
+ "parse-error": "error parsing answer.toml: interface name mapping 'waytoolonginterfacename' for 'ab:cd:ef:12:34:56' cannot be longer than 15 characters"
+}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/network_interface_pinning_overlong_interface_name.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_interface_pinning_overlong_interface_name.toml
new file mode 100644
index 0000000..e82b47d
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_interface_pinning_overlong_interface_name.toml
@@ -0,0 +1,18 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.fail.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root-password = "12345678"
+
+[network]
+source = "from-dhcp"
+interface-name-pinning.enabled = true
+
+[network.interface-name-pinning.mapping]
+"ab:cd:ef:12:34:56" = "waytoolonginterfacename"
+
+[disk-setup]
+filesystem = "ext4"
+disk-list = ["sda"]
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pve-devel] [PATCH installer 08/14] assistant: verify network settings in `validate-answer` subcommand
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
` (6 preceding siblings ...)
2025-10-14 13:21 ` [pve-devel] [PATCH installer 07/14] auto: add support for pinning network interface names Christoph Heiss
@ 2025-10-14 13:21 ` Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 09/14] post-hook: avoid redundant Option<bool> for (de-)serialization Christoph Heiss
` (5 subsequent siblings)
13 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-auto-install-assistant/src/main.rs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/proxmox-auto-install-assistant/src/main.rs b/proxmox-auto-install-assistant/src/main.rs
index c0d932c..093169a 100644
--- a/proxmox-auto-install-assistant/src/main.rs
+++ b/proxmox-auto-install-assistant/src/main.rs
@@ -23,7 +23,7 @@ use proxmox_auto_installer::{
AutoInstSettings, FetchAnswerFrom, HttpOptions, default_partition_label,
get_matched_udev_indexes, get_nic_list, get_single_udev_index, verify_disks_settings,
verify_email_and_root_password_settings, verify_first_boot_settings,
- verify_locale_settings,
+ verify_locale_settings, verify_network_settings,
},
};
use proxmox_installer_common::{FIRST_BOOT_EXEC_MAX_SIZE, FIRST_BOOT_EXEC_NAME, cli};
@@ -909,6 +909,7 @@ fn parse_answer(path: impl AsRef<Path> + fmt::Debug) -> Result<Answer> {
verify_disks_settings(&answer)?;
verify_first_boot_settings(&answer)?;
verify_email_and_root_password_settings(&answer)?;
+ verify_network_settings(&answer.network)?;
Ok(answer)
}
Err(err) => bail!("Error parsing answer file: {err}"),
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pve-devel] [PATCH installer 09/14] post-hook: avoid redundant Option<bool> for (de-)serialization
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
` (7 preceding siblings ...)
2025-10-14 13:21 ` [pve-devel] [PATCH installer 08/14] assistant: verify network settings in `validate-answer` subcommand Christoph Heiss
@ 2025-10-14 13:21 ` Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 10/14] post-hook: add network interface name and pinning status Christoph Heiss
` (4 subsequent siblings)
13 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
Instead, for the serialization case just skip it if the value is falsy,
for deserialization default-initialize it with `false`.
No functional changes.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-installer-common/src/setup.rs | 12 ++----------
proxmox-post-hook/src/main.rs | 27 +++++++++++++++------------
2 files changed, 17 insertions(+), 22 deletions(-)
diff --git a/proxmox-installer-common/src/setup.rs b/proxmox-installer-common/src/setup.rs
index 3e99576..1a584ba 100644
--- a/proxmox-installer-common/src/setup.rs
+++ b/proxmox-installer-common/src/setup.rs
@@ -275,14 +275,6 @@ where
Ok(val != 0)
}
-fn deserialize_bool_from_int_maybe<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
-where
- D: Deserializer<'de>,
-{
- let val: Option<u32> = Deserialize::deserialize(deserializer)?;
- Ok(val.map(|v| v != 0))
-}
-
fn deserialize_cczones_map<'de, D>(
deserializer: D,
) -> Result<HashMap<String, Vec<String>>, D::Error>
@@ -389,8 +381,8 @@ pub struct RuntimeInfo {
pub hvm_supported: bool,
/// Whether the system was booted with SecureBoot enabled
- #[serde(default, deserialize_with = "deserialize_bool_from_int_maybe")]
- pub secure_boot: Option<bool>,
+ #[serde(default, deserialize_with = "deserialize_bool_from_int")]
+ pub secure_boot: bool,
/// Default upper limit for the ZFS ARC size, in MiB.
pub default_zfs_arc_max: usize,
diff --git a/proxmox-post-hook/src/main.rs b/proxmox-post-hook/src/main.rs
index bd27121..0a9e661 100644
--- a/proxmox-post-hook/src/main.rs
+++ b/proxmox-post-hook/src/main.rs
@@ -44,8 +44,8 @@ struct BootInfo {
/// Whether the system is booted using UEFI or legacy BIOS.
mode: BootType,
/// Whether SecureBoot is enabled for the installation.
- #[serde(skip_serializing_if = "Option::is_none")]
- secureboot: Option<bool>,
+ #[serde(skip_serializing_if = "bool_is_false")]
+ secureboot: bool,
}
/// Holds all the public keys for the different algorithms available.
@@ -66,8 +66,8 @@ struct DiskInfo {
/// Size in bytes
size: usize,
/// Set to true if the disk is used for booting.
- #[serde(skip_serializing_if = "Option::is_none")]
- is_bootdisk: Option<bool>,
+ #[serde(skip_serializing_if = "bool_is_false")]
+ is_bootdisk: bool,
/// Properties about the device as given by udev.
udev_properties: UdevProperties,
}
@@ -83,12 +83,16 @@ struct NetworkInterfaceInfo {
address: Option<CidrAddress>,
/// Set to true if the interface is the chosen management interface during
/// installation.
- #[serde(skip_serializing_if = "Option::is_none")]
- is_management: Option<bool>,
+ #[serde(skip_serializing_if = "bool_is_false")]
+ is_management: bool,
/// Properties about the device as given by udev.
udev_properties: UdevProperties,
}
+fn bool_is_false(value: &bool) -> bool {
+ !value
+}
+
/// Information about the installed product itself.
#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
@@ -323,7 +327,8 @@ impl PostHookInfo {
let is_bootdisk = config
.target_hd
.as_ref()
- .and_then(|hd| (*hd == disk.path).then_some(true));
+ .map(|hd| *hd == disk.path)
+ .unwrap_or_default();
anyhow::Ok(DiskInfo {
size: (config.hdsize * (SIZE_GIB as f64)) as usize,
@@ -341,9 +346,7 @@ impl PostHookInfo {
.disks
.iter()
.flat_map(|disk| {
- let is_bootdisk = selected_disks_indices
- .contains(&&disk.index)
- .then_some(true);
+ let is_bootdisk = selected_disks_indices.contains(&&disk.index);
anyhow::Ok(DiskInfo {
size: (config.hdsize * (SIZE_GIB as f64)) as usize,
@@ -389,14 +392,14 @@ impl PostHookInfo {
anyhow::Ok(NetworkInterfaceInfo {
mac: nic.mac.clone(),
address: Some(config.cidr.clone()),
- is_management: Some(true),
+ is_management: true,
udev_properties,
})
} else {
anyhow::Ok(NetworkInterfaceInfo {
mac: nic.mac.clone(),
address: None,
- is_management: None,
+ is_management: false,
udev_properties,
})
}
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pve-devel] [PATCH installer 10/14] post-hook: add network interface name and pinning status
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
` (8 preceding siblings ...)
2025-10-14 13:21 ` [pve-devel] [PATCH installer 09/14] post-hook: avoid redundant Option<bool> for (de-)serialization Christoph Heiss
@ 2025-10-14 13:21 ` Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 11/14] tui: views: move network options view to own module Christoph Heiss
` (3 subsequent siblings)
13 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
Adds a new `name` and `is-pinned` attribute to each reported network interface in
the post-hook data.
Also increments the minor schema version by one, as it is a
backwards-compatible change.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-post-hook/src/main.rs | 39 +++++++++++++++++++++--------------
1 file changed, 23 insertions(+), 16 deletions(-)
diff --git a/proxmox-post-hook/src/main.rs b/proxmox-post-hook/src/main.rs
index 0a9e661..ce64533 100644
--- a/proxmox-post-hook/src/main.rs
+++ b/proxmox-post-hook/src/main.rs
@@ -76,6 +76,8 @@ struct DiskInfo {
#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
struct NetworkInterfaceInfo {
+ /// Name of the interface
+ name: String,
/// MAC address of the interface
mac: String,
/// (Designated) IP address of the interface
@@ -85,6 +87,10 @@ struct NetworkInterfaceInfo {
/// installation.
#[serde(skip_serializing_if = "bool_is_false")]
is_management: bool,
+ /// Set to true if the network interface name was pinned based on the MAC
+ /// address during the installation.
+ #[serde(skip_serializing_if = "bool_is_false")]
+ is_pinned: bool,
/// Properties about the device as given by udev.
udev_properties: UdevProperties,
}
@@ -151,7 +157,7 @@ struct PostHookInfoSchema {
}
impl PostHookInfoSchema {
- const SCHEMA_VERSION: &str = "1.1";
+ const SCHEMA_VERSION: &str = "1.2";
}
impl Default for PostHookInfoSchema {
@@ -386,23 +392,24 @@ impl PostHookInfo {
})?
.clone();
- if config.mngmt_nic == nic.name {
+ let is_pinned = config.network_interface_pin_map.contains_key(&nic.mac);
+ let ifname = config
+ .network_interface_pin_map
+ .get(&nic.mac)
+ .unwrap_or(&nic.name);
+
+ let is_management = config.mngmt_nic == *ifname;
+
+ anyhow::Ok(NetworkInterfaceInfo {
+ name: ifname.clone(),
+ mac: nic.mac.clone(),
// Use the actual IP address from the low-level install config, as the runtime info
// contains the original IP address from DHCP.
- anyhow::Ok(NetworkInterfaceInfo {
- mac: nic.mac.clone(),
- address: Some(config.cidr.clone()),
- is_management: true,
- udev_properties,
- })
- } else {
- anyhow::Ok(NetworkInterfaceInfo {
- mac: nic.mac.clone(),
- address: None,
- is_management: false,
- udev_properties,
- })
- }
+ address: is_management.then_some(config.cidr.clone()),
+ is_management,
+ is_pinned,
+ udev_properties,
+ })
})
.collect())
}
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pve-devel] [PATCH installer 11/14] tui: views: move network options view to own module
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
` (9 preceding siblings ...)
2025-10-14 13:21 ` [pve-devel] [PATCH installer 10/14] post-hook: add network interface name and pinning status Christoph Heiss
@ 2025-10-14 13:21 ` Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 12/14] tui: views: form: allow attaching user-defined data to children Christoph Heiss
` (2 subsequent siblings)
13 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
In preparation for adding network interface name pinning, which will
introduce quite a bit more functionality.
No functional changes.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-tui-installer/src/main.rs | 90 +---------------
proxmox-tui-installer/src/views/mod.rs | 3 +
proxmox-tui-installer/src/views/network.rs | 113 +++++++++++++++++++++
3 files changed, 121 insertions(+), 85 deletions(-)
create mode 100644 proxmox-tui-installer/src/views/network.rs
diff --git a/proxmox-tui-installer/src/main.rs b/proxmox-tui-installer/src/main.rs
index 15ee5d3..b24f90b 100644
--- a/proxmox-tui-installer/src/main.rs
+++ b/proxmox-tui-installer/src/main.rs
@@ -1,6 +1,6 @@
#![forbid(unsafe_code)]
-use std::{collections::HashMap, env, net::IpAddr};
+use std::{collections::HashMap, env};
use cursive::{
Cursive, CursiveRunnable, ScreenId, View, XY,
@@ -9,7 +9,7 @@ use cursive::{
view::{Nameable, Offset, Resizable, ViewWrapper},
views::{
Button, Checkbox, Dialog, DummyView, EditView, Layer, LinearLayout, PaddedView, Panel,
- ResizedView, ScrollView, SelectView, StackView, TextView,
+ ResizedView, ScrollView, StackView, TextView,
},
};
@@ -20,7 +20,6 @@ use proxmox_installer_common::{
ROOT_PASSWORD_MIN_LENGTH,
options::{BootdiskOptions, NetworkOptions, TimezoneOptions, email_validate},
setup::{LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo, installer_setup},
- utils::{CidrAddress, Fqdn},
};
mod setup;
@@ -28,7 +27,7 @@ mod system;
mod views;
use views::{
- BootdiskOptionsView, CidrAddressEditView, FormView, InstallProgressView, TableView,
+ BootdiskOptionsView, FormView, InstallProgressView, NetworkOptionsView, TableView,
TableViewItem, TimezoneOptionsView,
};
@@ -483,91 +482,12 @@ fn password_dialog(siv: &mut Cursive) -> InstallerView {
fn network_dialog(siv: &mut Cursive) -> InstallerView {
let state = siv.user_data::<InstallerState>().unwrap();
let options = &state.options.network;
- let ifaces = state.runtime_info.network.interfaces.values();
- let ifnames = ifaces
- .clone()
- .map(|iface| (iface.render(), iface.name.clone()));
- let mut ifaces_selection = SelectView::new().popup().with_all(ifnames.clone());
-
- // sort first to always have stable view
- ifaces_selection.sort();
- let selected = ifaces_selection
- .iter()
- .position(|(_label, iface)| *iface == options.ifname)
- .unwrap_or(ifaces.len() - 1);
-
- ifaces_selection.set_selection(selected);
-
- let inner = FormView::new()
- .child("Management interface", ifaces_selection)
- .child(
- "Hostname (FQDN)",
- EditView::new().content(options.fqdn.to_string()),
- )
- .child(
- "IP address (CIDR)",
- CidrAddressEditView::new().content(options.address.clone()),
- )
- .child(
- "Gateway address",
- EditView::new().content(options.gateway.to_string()),
- )
- .child(
- "DNS server address",
- EditView::new().content(options.dns_server.to_string()),
- )
- .with_name("network-options");
InstallerView::new(
state,
- inner,
+ NetworkOptionsView::new(options, &state.runtime_info.network).with_name("network-options"),
Box::new(|siv| {
- let options = siv.call_on_name("network-options", |view: &mut FormView| {
- let ifname = view
- .get_value::<SelectView, _>(0)
- .ok_or("failed to retrieve management interface name")?;
-
- let fqdn = view
- .get_value::<EditView, _>(1)
- .ok_or("failed to retrieve host FQDN")?
- .parse::<Fqdn>()
- .map_err(|err| format!("hostname does not look valid:\n\n{err}"))?;
-
- let address = view
- .get_value::<CidrAddressEditView, _>(2)
- .ok_or("failed to retrieve host address".to_string())
- .and_then(|(ip_addr, mask)| {
- CidrAddress::new(ip_addr, mask).map_err(|err| err.to_string())
- })?;
-
- let gateway = view
- .get_value::<EditView, _>(3)
- .ok_or("failed to retrieve gateway address")?
- .parse::<IpAddr>()
- .map_err(|err| err.to_string())?;
-
- let dns_server = view
- .get_value::<EditView, _>(4)
- .ok_or("failed to retrieve DNS server address")?
- .parse::<IpAddr>()
- .map_err(|err| err.to_string())?;
-
- if address.addr().is_ipv4() != gateway.is_ipv4() {
- Err("host and gateway IP address version must not differ".to_owned())
- } else if address.addr().is_ipv4() != dns_server.is_ipv4() {
- Err("host and DNS IP address version must not differ".to_owned())
- } else if fqdn.to_string().ends_with(".invalid") {
- Err("hostname does not look valid".to_owned())
- } else {
- Ok(NetworkOptions {
- ifname,
- fqdn,
- address,
- gateway,
- dns_server,
- })
- }
- });
+ let options = siv.call_on_name("network-options", NetworkOptionsView::get_values);
match options {
Some(Ok(options)) => {
diff --git a/proxmox-tui-installer/src/views/mod.rs b/proxmox-tui-installer/src/views/mod.rs
index 537e3ed..43ca999 100644
--- a/proxmox-tui-installer/src/views/mod.rs
+++ b/proxmox-tui-installer/src/views/mod.rs
@@ -16,6 +16,9 @@ pub use bootdisk::*;
mod install_progress;
pub use install_progress::*;
+mod network;
+pub use network::*;
+
mod tabbed_view;
pub use tabbed_view::*;
diff --git a/proxmox-tui-installer/src/views/network.rs b/proxmox-tui-installer/src/views/network.rs
new file mode 100644
index 0000000..5e3e258
--- /dev/null
+++ b/proxmox-tui-installer/src/views/network.rs
@@ -0,0 +1,113 @@
+use std::net::IpAddr;
+
+use cursive::{
+ view::ViewWrapper,
+ views::{EditView, SelectView},
+};
+
+use super::{CidrAddressEditView, FormView};
+use proxmox_installer_common::{
+ options::NetworkOptions,
+ setup::NetworkInfo,
+ utils::{CidrAddress, Fqdn},
+};
+
+pub struct NetworkOptionsView {
+ view: FormView,
+}
+
+impl NetworkOptionsView {
+ pub fn new(options: &NetworkOptions, network_info: &NetworkInfo) -> Self {
+ let ifaces = network_info.interfaces.values();
+ let ifnames = ifaces
+ .clone()
+ .map(|iface| (iface.render(), iface.name.clone()));
+ let mut ifaces_selection = SelectView::new().popup().with_all(ifnames.clone());
+
+ // sort first to always have stable view
+ ifaces_selection.sort();
+ let selected = ifaces_selection
+ .iter()
+ .position(|(_label, iface)| *iface == options.ifname)
+ .unwrap_or(ifaces.len() - 1);
+
+ ifaces_selection.set_selection(selected);
+
+ let view = FormView::new()
+ .child("Management interface", ifaces_selection)
+ .child(
+ "Hostname (FQDN)",
+ EditView::new().content(options.fqdn.to_string()),
+ )
+ .child(
+ "IP address (CIDR)",
+ CidrAddressEditView::new().content(options.address.clone()),
+ )
+ .child(
+ "Gateway address",
+ EditView::new().content(options.gateway.to_string()),
+ )
+ .child(
+ "DNS server address",
+ EditView::new().content(options.dns_server.to_string()),
+ );
+
+ Self { view }
+ }
+
+ pub fn get_values(&mut self) -> Result<NetworkOptions, String> {
+ let ifname = self
+ .view
+ .get_value::<SelectView, _>(0)
+ .ok_or("failed to retrieve management interface name")?;
+
+ let fqdn = self
+ .view
+ .get_value::<EditView, _>(1)
+ .ok_or("failed to retrieve host FQDN")?
+ .parse::<Fqdn>()
+ .map_err(|err| format!("hostname does not look valid:\n\n{err}"))?;
+
+ let address = self
+ .view
+ .get_value::<CidrAddressEditView, _>(2)
+ .ok_or("failed to retrieve host address".to_string())
+ .and_then(|(ip_addr, mask)| {
+ CidrAddress::new(ip_addr, mask).map_err(|err| err.to_string())
+ })?;
+
+ let gateway = self
+ .view
+ .get_value::<EditView, _>(3)
+ .ok_or("failed to retrieve gateway address")?
+ .parse::<IpAddr>()
+ .map_err(|err| err.to_string())?;
+
+ let dns_server = self
+ .view
+ .get_value::<EditView, _>(4)
+ .ok_or("failed to retrieve DNS server address")?
+ .parse::<IpAddr>()
+ .map_err(|err| err.to_string())?;
+
+ if address.addr().is_ipv4() != gateway.is_ipv4() {
+ Err("host and gateway IP address version must not differ".to_owned())
+ } else if address.addr().is_ipv4() != dns_server.is_ipv4() {
+ Err("host and DNS IP address version must not differ".to_owned())
+ } else if fqdn.to_string().ends_with(".invalid") {
+ Err("hostname does not look valid".to_owned())
+ } else {
+ Ok(NetworkOptions {
+ ifname,
+ fqdn,
+ address,
+ gateway,
+ dns_server,
+ })
+ }
+ }
+}
+
+impl ViewWrapper for NetworkOptionsView {
+ cursive::wrap_impl!(self.view: FormView);
+}
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pve-devel] [PATCH installer 12/14] tui: views: form: allow attaching user-defined data to children
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
` (10 preceding siblings ...)
2025-10-14 13:21 ` [pve-devel] [PATCH installer 11/14] tui: views: move network options view to own module Christoph Heiss
@ 2025-10-14 13:21 ` Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 13/14] tui: add support for pinning network interface names Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 14/14] gui: " Christoph Heiss
13 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
Allows storing (typesafe) arbitrary data together with each child.
No functional changes for existing code.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-tui-installer/src/main.rs | 2 +-
proxmox-tui-installer/src/views/bootdisk.rs | 6 ++--
proxmox-tui-installer/src/views/mod.rs | 33 +++++++++++++++++----
proxmox-tui-installer/src/views/network.rs | 2 +-
4 files changed, 33 insertions(+), 10 deletions(-)
diff --git a/proxmox-tui-installer/src/main.rs b/proxmox-tui-installer/src/main.rs
index b24f90b..fce9fc2 100644
--- a/proxmox-tui-installer/src/main.rs
+++ b/proxmox-tui-installer/src/main.rs
@@ -420,7 +420,7 @@ fn password_dialog(siv: &mut Cursive) -> InstallerView {
let state = siv.user_data::<InstallerState>().unwrap();
let options = &state.options.password;
- let inner = FormView::new()
+ let inner = FormView::<()>::new()
.child(
"Root password [at least 8 characters]",
EditView::new().secret(),
diff --git a/proxmox-tui-installer/src/views/bootdisk.rs b/proxmox-tui-installer/src/views/bootdisk.rs
index 9f6d235..3566814 100644
--- a/proxmox-tui-installer/src/views/bootdisk.rs
+++ b/proxmox-tui-installer/src/views/bootdisk.rs
@@ -47,7 +47,7 @@ impl BootdiskOptionsView {
pub fn new(siv: &mut Cursive, runinfo: &RuntimeInfo, options: &BootdiskOptions) -> Self {
let advanced_options = Arc::new(Mutex::new(options.clone()));
- let bootdisk_form = FormView::new()
+ let bootdisk_form = FormView::<()>::new()
.child(
"Target harddisk",
target_bootdisk_selectview(
@@ -152,7 +152,7 @@ impl AdvancedBootdiskOptionsView {
let mut view = LinearLayout::vertical()
.child(DummyView.full_width())
- .child(FormView::new().child("Filesystem", fstype_select))
+ .child(FormView::<()>::new().child("Filesystem", fstype_select))
.child(DummyView.full_width());
// Create the appropriate (inner) advanced options view
@@ -493,7 +493,7 @@ impl<T: View> MultiDiskOptionsView<T> {
selectable_disks.push(("-- do not use --".to_owned(), None));
- let mut disk_form = FormView::new();
+ let mut disk_form = FormView::<()>::new();
for (i, _) in avail_disks.iter().enumerate() {
disk_form.add_child(
&format!("Harddisk {i}"),
diff --git a/proxmox-tui-installer/src/views/mod.rs b/proxmox-tui-installer/src/views/mod.rs
index 43ca999..5723fed 100644
--- a/proxmox-tui-installer/src/views/mod.rs
+++ b/proxmox-tui-installer/src/views/mod.rs
@@ -1,4 +1,4 @@
-use std::{net::IpAddr, str::FromStr, sync::Arc};
+use std::{collections::HashMap, net::IpAddr, str::FromStr, sync::Arc};
use cursive::{
Printer, Rect, Vec2, View,
@@ -414,17 +414,21 @@ impl FormViewGetValue<f64> for DiskSizeEditView {
}
}
-pub struct FormView {
+pub struct FormView<UDT = ()> {
view: LinearLayout,
+ user_data: HashMap<usize, UDT>,
}
-impl FormView {
+impl<UDT> FormView<UDT> {
pub fn new() -> Self {
let view = LinearLayout::horizontal()
.child(LinearLayout::vertical().full_width())
.child(LinearLayout::vertical().full_width());
- Self { view }
+ Self {
+ view,
+ user_data: HashMap::new(),
+ }
}
pub fn add_child(&mut self, label: &str, view: impl View) {
@@ -432,6 +436,12 @@ impl FormView {
self.add_to_column(1, view);
}
+ pub fn add_child_with_data(&mut self, label: &str, view: impl View, data: UDT) {
+ self.add_to_column(0, TextView::new(format!("{label}: ")).no_wrap());
+ self.add_to_column(1, view);
+ self.user_data.insert(self.len() - 1, data);
+ }
+
pub fn child(mut self, label: &str, view: impl View) -> Self {
self.add_child(label, view);
self
@@ -453,6 +463,19 @@ impl FormView {
.downcast_ref::<T>()
}
+ pub fn get_child_with_data<T: View>(&self, index: usize) -> Option<(&T, &UDT)> {
+ let view = self
+ .view
+ .get_child(1)?
+ .downcast_ref::<ResizedView<LinearLayout>>()?
+ .get_inner()
+ .get_child(index)?
+ .downcast_ref::<T>()?;
+
+ let data = self.user_data.get(&index)?;
+ Some((view, data))
+ }
+
pub fn get_child_mut<T: View>(&mut self, index: usize) -> Option<&mut T> {
self.view
.get_child_mut(1)?
@@ -509,7 +532,7 @@ impl FormView {
}
}
-impl ViewWrapper for FormView {
+impl<UDT: Send + Sync + 'static> ViewWrapper for FormView<UDT> {
cursive::wrap_impl!(self.view: LinearLayout);
fn wrap_important_area(&self, size: Vec2) -> Rect {
diff --git a/proxmox-tui-installer/src/views/network.rs b/proxmox-tui-installer/src/views/network.rs
index 5e3e258..960c25e 100644
--- a/proxmox-tui-installer/src/views/network.rs
+++ b/proxmox-tui-installer/src/views/network.rs
@@ -33,7 +33,7 @@ impl NetworkOptionsView {
ifaces_selection.set_selection(selected);
- let view = FormView::new()
+ let form = FormView::<()>::new()
.child("Management interface", ifaces_selection)
.child(
"Hostname (FQDN)",
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pve-devel] [PATCH installer 13/14] tui: add support for pinning network interface names
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
` (11 preceding siblings ...)
2025-10-14 13:21 ` [pve-devel] [PATCH installer 12/14] tui: views: form: allow attaching user-defined data to children Christoph Heiss
@ 2025-10-14 13:21 ` Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 14/14] gui: " Christoph Heiss
13 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
Adds an additional checkbox and option button in the network panel, the
latter triggering a dialog for setting custom names per network
interface present on the system.
Pinning is enabled by default.
Each pinned network interface name defaults to `nicN`, where N is the
pinned ID from the low-level installer.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-installer-common/src/setup.rs | 10 +-
proxmox-tui-installer/src/main.rs | 13 +-
proxmox-tui-installer/src/setup.rs | 6 +-
proxmox-tui-installer/src/views/mod.rs | 5 +
proxmox-tui-installer/src/views/network.rs | 357 +++++++++++++++++++--
5 files changed, 353 insertions(+), 38 deletions(-)
diff --git a/proxmox-installer-common/src/setup.rs b/proxmox-installer-common/src/setup.rs
index 1a584ba..c93ee30 100644
--- a/proxmox-installer-common/src/setup.rs
+++ b/proxmox-installer-common/src/setup.rs
@@ -436,7 +436,7 @@ pub struct Gateway {
pub gateway: IpAddr,
}
-#[derive(Clone, Deserialize, Serialize)]
+#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "UPPERCASE")]
/// The current interface state as reported by the kernel.
pub enum InterfaceState {
@@ -483,7 +483,13 @@ pub struct Interface {
impl Interface {
// avoid display trait as this is not the string representation for a serializer
pub fn render(&self) -> String {
- format!("{} {} ({})", self.state.render(), self.name, self.mac)
+ format!(
+ "{} {} ({}, {})",
+ self.state.render(),
+ self.name,
+ self.mac,
+ self.driver
+ )
}
pub fn to_pinned(&self, options: &NetworkInterfacePinningOptions) -> Self {
diff --git a/proxmox-tui-installer/src/main.rs b/proxmox-tui-installer/src/main.rs
index fce9fc2..cd590b8 100644
--- a/proxmox-tui-installer/src/main.rs
+++ b/proxmox-tui-installer/src/main.rs
@@ -18,7 +18,10 @@ use options::{InstallerOptions, PasswordOptions};
use proxmox_installer_common::{
ROOT_PASSWORD_MIN_LENGTH,
- options::{BootdiskOptions, NetworkOptions, TimezoneOptions, email_validate},
+ options::{
+ BootdiskOptions, NetworkInterfacePinningOptions, NetworkOptions, TimezoneOptions,
+ email_validate,
+ },
setup::{LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo, installer_setup},
};
mod setup;
@@ -167,7 +170,13 @@ fn main() {
bootdisk: BootdiskOptions::defaults_from(&runtime_info.disks[0]),
timezone: TimezoneOptions::defaults_from(&runtime_info, &locales),
password: Default::default(),
- network: NetworkOptions::defaults_from(&setup_info, &runtime_info.network, None),
+ network: NetworkOptions::defaults_from(
+ &setup_info,
+ &runtime_info.network,
+ None,
+ // We enable network interface pinning by default in the TUI
+ Some(&NetworkInterfacePinningOptions::default()),
+ ),
autoreboot: true,
},
setup_info,
diff --git a/proxmox-tui-installer/src/setup.rs b/proxmox-tui-installer/src/setup.rs
index d2ffb70..3ab1869 100644
--- a/proxmox-tui-installer/src/setup.rs
+++ b/proxmox-tui-installer/src/setup.rs
@@ -1,4 +1,4 @@
-use std::collections::{BTreeMap, HashMap};
+use std::collections::BTreeMap;
use crate::options::InstallerOptions;
use proxmox_installer_common::{
@@ -8,6 +8,8 @@ use proxmox_installer_common::{
impl From<InstallerOptions> for InstallConfig {
fn from(options: InstallerOptions) -> Self {
+ let pinning_opts = options.network.pinning_opts.as_ref();
+
let mut config = Self {
autoreboot: options.autoreboot as usize,
@@ -32,7 +34,7 @@ impl From<InstallerOptions> for InstallConfig {
root_ssh_keys: vec![],
mngmt_nic: options.network.ifname,
- network_interface_pin_map: HashMap::new(),
+ network_interface_pin_map: pinning_opts.map(|o| o.mapping.clone()).unwrap_or_default(),
// Safety: At this point, it is know that we have a valid FQDN, as
// this is set by the TUI network panel, which only lets the user
diff --git a/proxmox-tui-installer/src/views/mod.rs b/proxmox-tui-installer/src/views/mod.rs
index 5723fed..5784681 100644
--- a/proxmox-tui-installer/src/views/mod.rs
+++ b/proxmox-tui-installer/src/views/mod.rs
@@ -436,6 +436,11 @@ impl<UDT> FormView<UDT> {
self.add_to_column(1, view);
}
+ pub fn add_child_with_custom_label(&mut self, label: &str, view: impl View) {
+ self.add_to_column(0, TextView::new(label).no_wrap());
+ self.add_to_column(1, view);
+ }
+
pub fn add_child_with_data(&mut self, label: &str, view: impl View, data: UDT) {
self.add_to_column(0, TextView::new(format!("{label}: ")).no_wrap());
self.add_to_column(1, view);
diff --git a/proxmox-tui-installer/src/views/network.rs b/proxmox-tui-installer/src/views/network.rs
index 960c25e..4388c7d 100644
--- a/proxmox-tui-installer/src/views/network.rs
+++ b/proxmox-tui-installer/src/views/network.rs
@@ -1,40 +1,82 @@
-use std::net::IpAddr;
-
use cursive::{
- view::ViewWrapper,
- views::{EditView, SelectView},
+ Cursive, View,
+ view::{Nameable, Resizable, ViewWrapper},
+ views::{
+ Button, Checkbox, Dialog, DummyView, EditView, LinearLayout, NamedView, ResizedView,
+ ScrollView, SelectView, TextView,
+ },
+};
+use std::{
+ collections::HashMap,
+ net::IpAddr,
+ sync::{Arc, Mutex},
};
use super::{CidrAddressEditView, FormView};
use proxmox_installer_common::{
- options::NetworkOptions,
- setup::NetworkInfo,
+ net::MAX_IFNAME_LEN,
+ options::{NetworkInterfacePinningOptions, NetworkOptions},
+ setup::{Interface, NetworkInfo},
utils::{CidrAddress, Fqdn},
};
+struct NetworkViewOptions {
+ selected_mac: String,
+ pinning_enabled: bool,
+ // For UI purposes, we want to always save the mapping, to save the state
+ // between toggling the checkbox
+ pinning_options: NetworkInterfacePinningOptions,
+}
+
+/// Convenience wrapper when needing to take a (interior-mutable) reference to
+/// `NetworkViewOptions`.
+type NetworkViewOptionsRef = Arc<Mutex<NetworkViewOptions>>;
+
+/// View for configuring anything related to network setup.
pub struct NetworkOptionsView {
- view: FormView,
+ view: LinearLayout,
+ options: NetworkViewOptionsRef,
}
impl NetworkOptionsView {
+ const PINNING_OPTIONS_BUTTON_NAME: &str = "network-pinning-options-button";
+ const MGMT_IFNAME_SELECTVIEW_NAME: &str = "network-management-ifname-selectview";
+
pub fn new(options: &NetworkOptions, network_info: &NetworkInfo) -> Self {
- let ifaces = network_info.interfaces.values();
- let ifnames = ifaces
- .clone()
- .map(|iface| (iface.render(), iface.name.clone()));
- let mut ifaces_selection = SelectView::new().popup().with_all(ifnames.clone());
+ let mut ifaces = network_info
+ .interfaces
+ .values()
+ .collect::<Vec<&Interface>>();
- // sort first to always have stable view
- ifaces_selection.sort();
- let selected = ifaces_selection
- .iter()
- .position(|(_label, iface)| *iface == options.ifname)
- .unwrap_or(ifaces.len() - 1);
+ // First, sort interfaces by their link state and then name
+ ifaces.sort_unstable_by_key(|x| (&x.state, &x.name));
- ifaces_selection.set_selection(selected);
+ let selected_mac = network_info
+ .interfaces
+ .get(&options.ifname)
+ .map(|iface| iface.mac.clone())
+ .unwrap_or_else(|| {
+ ifaces
+ .first()
+ .expect("at least one network interface")
+ .mac
+ .clone()
+ });
+
+ let options_ref = Arc::new(Mutex::new(NetworkViewOptions {
+ selected_mac,
+ pinning_enabled: options.pinning_opts.is_some(),
+ pinning_options: options.pinning_opts.clone().unwrap_or_default(),
+ }));
+
+ let iface_selection =
+ Self::build_mgmt_ifname_selectview(ifaces.clone(), options_ref.clone());
let form = FormView::<()>::new()
- .child("Management interface", ifaces_selection)
+ .child(
+ "Management interface",
+ iface_selection.with_name(Self::MGMT_IFNAME_SELECTVIEW_NAME),
+ )
.child(
"Hostname (FQDN)",
EditView::new().content(options.fqdn.to_string()),
@@ -52,44 +94,106 @@ impl NetworkOptionsView {
EditView::new().content(options.dns_server.to_string()),
);
- Self { view }
+ let pinning_checkbox = LinearLayout::horizontal()
+ .child(Checkbox::new().checked().on_change({
+ let ifaces = ifaces
+ .iter()
+ .map(|iface| (*iface).clone())
+ .collect::<Vec<Interface>>();
+ let options_ref = options_ref.clone();
+ move |siv, enable_pinning| {
+ siv.call_on_name(Self::PINNING_OPTIONS_BUTTON_NAME, {
+ let options_ref = options_ref.clone();
+ move |view: &mut Button| {
+ view.set_enabled(enable_pinning);
+
+ options_ref.lock().expect("unpoisoned lock").pinning_enabled =
+ enable_pinning;
+ }
+ });
+
+ Self::refresh_ifname_selectview(siv, &ifaces, options_ref.clone());
+ }
+ }))
+ .child(TextView::new(" Pin network interface names").no_wrap())
+ .child(DummyView.full_width())
+ .child(
+ Button::new("Pinning options", {
+ let options_ref = options_ref.clone();
+ let network_info = network_info.clone();
+ move |siv| {
+ let mut view =
+ Self::custom_name_mapping_view(&network_info, options_ref.clone());
+
+ // Pre-compute the child's layout, since it might depend on the size. Without this,
+ // the view will be empty until focused.
+ // The screen size never changes in our case, so this is completely OK.
+ view.layout(siv.screen_size());
+
+ siv.add_layer(view);
+ }
+ })
+ .with_name(Self::PINNING_OPTIONS_BUTTON_NAME),
+ );
+
+ let view = LinearLayout::vertical()
+ .child(form)
+ .child(DummyView.full_width())
+ .child(pinning_checkbox);
+
+ Self {
+ view,
+ options: options_ref,
+ }
}
pub fn get_values(&mut self) -> Result<NetworkOptions, String> {
- let ifname = self
+ let form = self
.view
- .get_value::<SelectView, _>(0)
+ .get_child(0)
+ .and_then(|v| v.downcast_ref::<FormView>())
+ .ok_or("failed to retrieve network options form")?;
+
+ let iface = form
+ .get_value::<NamedView<SelectView<Interface>>, _>(0)
.ok_or("failed to retrieve management interface name")?;
- let fqdn = self
- .view
+ let fqdn = form
.get_value::<EditView, _>(1)
.ok_or("failed to retrieve host FQDN")?
.parse::<Fqdn>()
.map_err(|err| format!("hostname does not look valid:\n\n{err}"))?;
- let address = self
- .view
+ let address = form
.get_value::<CidrAddressEditView, _>(2)
.ok_or("failed to retrieve host address".to_string())
.and_then(|(ip_addr, mask)| {
CidrAddress::new(ip_addr, mask).map_err(|err| err.to_string())
})?;
- let gateway = self
- .view
+ let gateway = form
.get_value::<EditView, _>(3)
.ok_or("failed to retrieve gateway address")?
.parse::<IpAddr>()
.map_err(|err| err.to_string())?;
- let dns_server = self
- .view
+ let dns_server = form
.get_value::<EditView, _>(4)
.ok_or("failed to retrieve DNS server address")?
.parse::<IpAddr>()
.map_err(|err| err.to_string())?;
+ let pinning_opts = self
+ .options
+ .lock()
+ .map(|opt| opt.pinning_enabled.then_some(opt.pinning_options.clone()))
+ .map_err(|err| err.to_string())?;
+
+ let ifname = match &pinning_opts {
+ Some(opts) => iface.to_pinned(opts).name,
+ None => iface.name,
+ };
+
if address.addr().is_ipv4() != gateway.is_ipv4() {
Err("host and gateway IP address version must not differ".to_owned())
} else if address.addr().is_ipv4() != dns_server.is_ipv4() {
@@ -103,11 +207,200 @@ impl NetworkOptionsView {
address,
gateway,
dns_server,
+ pinning_opts,
})
}
}
+
+ fn custom_name_mapping_view(
+ network_info: &NetworkInfo,
+ options_ref: NetworkViewOptionsRef,
+ ) -> impl View {
+ const DIALOG_NAME: &str = "network-interface-name-pinning-dialog";
+
+ let mut interfaces = network_info
+ .interfaces
+ .values()
+ .collect::<Vec<&Interface>>();
+
+ interfaces.sort_by(|a, b| (&a.state, &a.name).cmp(&(&b.state, &b.name)));
+
+ Dialog::around(InterfacePinningOptionsView::new(
+ &interfaces,
+ options_ref.clone(),
+ ))
+ .title("Interface Name Pinning Options")
+ .button("Ok", {
+ let interfaces = interfaces
+ .iter()
+ .map(|v| (*v).clone())
+ .collect::<Vec<Interface>>();
+ move |siv| {
+ let options = siv
+ .call_on_name(DIALOG_NAME, |view: &mut Dialog| {
+ view.get_content_mut()
+ .downcast_mut::<InterfacePinningOptionsView>()
+ .map(InterfacePinningOptionsView::get_values)
+ })
+ .flatten();
+
+ let options = match options {
+ Some(Ok(options)) => options,
+ Some(Err(err)) => {
+ siv.add_layer(Dialog::info(err));
+ return;
+ }
+ None => {
+ siv.add_layer(Dialog::info(
+ "Failed to retrieve network interface name pinning options view",
+ ));
+ return;
+ }
+ };
+
+ siv.pop_layer();
+ options_ref.lock().expect("unpoisoned lock").pinning_options = options;
+
+ Self::refresh_ifname_selectview(siv, &interfaces, options_ref.clone());
+ }
+ })
+ .with_name(DIALOG_NAME)
+ .max_size((80, 40))
+ }
+
+ fn refresh_ifname_selectview(
+ siv: &mut Cursive,
+ ifaces: &[Interface],
+ options_ref: NetworkViewOptionsRef,
+ ) {
+ siv.call_on_name(
+ Self::MGMT_IFNAME_SELECTVIEW_NAME,
+ |view: &mut SelectView<Interface>| {
+ *view = Self::build_mgmt_ifname_selectview(ifaces.iter().collect(), options_ref);
+ },
+ );
+ }
+
+ fn build_mgmt_ifname_selectview(
+ ifaces: Vec<&Interface>,
+ options_ref: NetworkViewOptionsRef,
+ ) -> SelectView<Interface> {
+ let options = options_ref.lock().expect("unpoisoned lock");
+
+ // Map all interfaces to a list of (human-readable interface name, [Interface]) pairs
+ let ifnames = ifaces
+ .iter()
+ .map(|iface| {
+ let iface = if options.pinning_enabled {
+ &iface.to_pinned(&options.pinning_options)
+ } else {
+ iface
+ };
+
+ (iface.render(), iface.clone())
+ })
+ .collect::<Vec<(String, Interface)>>();
+
+ let mut view = SelectView::new()
+ .popup()
+ .with_all(ifnames.clone())
+ .on_submit({
+ let options_ref = options_ref.clone();
+ move |_, iface| {
+ options_ref.lock().expect("unpoisoned lock").selected_mac = iface.mac.clone();
+ }
+ });
+
+ // Finally, (try to) select the current one
+ let selected = view
+ .iter()
+ .position(|(_label, iface)| iface.mac == options.selected_mac)
+ .unwrap_or(0); // we sort UP interfaces first, so select the first UP interface
+ //
+ view.set_selection(selected);
+
+ view
+ }
}
impl ViewWrapper for NetworkOptionsView {
- cursive::wrap_impl!(self.view: FormView);
+ cursive::wrap_impl!(self.view: LinearLayout);
+}
+
+struct InterfacePinningOptionsView {
+ view: ScrollView<NamedView<FormView<String>>>,
+}
+
+impl InterfacePinningOptionsView {
+ const FORM_NAME: &str = "network-interface-name-pinning-form";
+
+ fn new(interfaces: &[&Interface], options_ref: NetworkViewOptionsRef) -> Self {
+ let options = options_ref.lock().expect("unpoisoned lock");
+
+ let mut form = FormView::<String>::new();
+
+ for iface in interfaces {
+ let label = format!(
+ "{} ({}, {}, {})",
+ iface.mac, iface.name, iface.driver, iface.state
+ );
+
+ let view = LinearLayout::horizontal()
+ .child(DummyView.full_width()) // right align below form elements
+ .child(
+ EditView::new()
+ .content(iface.to_pinned(&options.pinning_options).name)
+ .max_content_width(MAX_IFNAME_LEN)
+ .fixed_width(MAX_IFNAME_LEN),
+ );
+
+ form.add_child_with_data(&label, view, iface.mac.clone());
+
+ if !iface.addresses.is_empty() {
+ for chunk in iface.addresses.chunks(2) {
+ let addrs = chunk
+ .iter()
+ .map(|v| v.to_string())
+ .collect::<Vec<String>>()
+ .join(", ");
+
+ form.add_child_with_custom_label(&format!(" {addrs}\n"), DummyView);
+ }
+ }
+ }
+
+ Self {
+ view: ScrollView::new(form.with_name(Self::FORM_NAME)),
+ }
+ }
+
+ fn get_values(&mut self) -> Result<NetworkInterfacePinningOptions, String> {
+ let form = self.view.get_inner_mut().get_mut();
+
+ let mut mapping = HashMap::new();
+
+ for i in 0..form.len() {
+ let (inner, mac) = match form.get_child_with_data::<LinearLayout>(i) {
+ Some(formdata) => formdata,
+ None => continue,
+ };
+
+ let name = inner
+ .get_child(1)
+ .and_then(|v| v.downcast_ref::<ResizedView<EditView>>())
+ .map(|v| v.get_inner().get_content())
+ .ok_or_else(|| format!("failed to retrieve pinning ID for interface {}", mac))?;
+
+ mapping.insert(mac.clone(), (*name).clone());
+ }
+
+ let opts = NetworkInterfacePinningOptions { mapping };
+ opts.verify().map_err(|err| err.to_string())?;
+
+ Ok(opts)
+ }
+}
+
+impl ViewWrapper for InterfacePinningOptionsView {
+ cursive::wrap_impl!(self.view: ScrollView<NamedView<FormView<String>>>);
}
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pve-devel] [PATCH installer 14/14] gui: add support for pinning network interface names
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
` (12 preceding siblings ...)
2025-10-14 13:21 ` [pve-devel] [PATCH installer 13/14] tui: add support for pinning network interface names Christoph Heiss
@ 2025-10-14 13:21 ` Christoph Heiss
2025-10-14 15:04 ` Maximiliano Sandoval
13 siblings, 1 reply; 17+ messages in thread
From: Christoph Heiss @ 2025-10-14 13:21 UTC (permalink / raw)
To: pve-devel
Adds an additional checkbox and option button in the network panel, the
latter triggering a dialog for setting custom names per network
interface present on the system.
Pinning is enabled by default.
Each pinned network interface name defaults to `nicN`, where N is the
pinned ID from the low-level installer.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Proxmox/Sys/Net.pm | 9 +-
proxinstall | 209 ++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 197 insertions(+), 21 deletions(-)
diff --git a/Proxmox/Sys/Net.pm b/Proxmox/Sys/Net.pm
index 2183d27..7fe800c 100644
--- a/Proxmox/Sys/Net.pm
+++ b/Proxmox/Sys/Net.pm
@@ -8,7 +8,14 @@ use Proxmox::Sys::Udev;
use JSON qw();
use base qw(Exporter);
-our @EXPORT_OK = qw(parse_ip_address parse_ip_mask parse_fqdn);
+our @EXPORT_OK = qw(parse_ip_address parse_ip_mask parse_fqdn MAX_IFNAME_LEN DEFAULT_PIN_PREFIX);
+
+# Maximum length of the (primary) name of a network interface
+# IFNAMSIZ - 1 to account for NUL byte
+use constant {
+ MAX_IFNAME_LEN => 15,
+ DEFAULT_PIN_PREFIX => 'nic',
+};
our $HOSTNAME_RE = "(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{,61}?[a-zA-Z0-9])?)";
our $FQDN_RE = "(?:${HOSTNAME_RE}\\.)*${HOSTNAME_RE}";
diff --git a/proxinstall b/proxinstall
index 5ba65fa..35e948a 100755
--- a/proxinstall
+++ b/proxinstall
@@ -37,7 +37,7 @@ use Proxmox::Sys;
use Proxmox::Sys::Block qw(get_cached_disks);
use Proxmox::Sys::Command qw(syscmd);
use Proxmox::Sys::File qw(file_read_all file_write_all);
-use Proxmox::Sys::Net qw(parse_ip_address parse_ip_mask);
+use Proxmox::Sys::Net qw(parse_ip_address parse_ip_mask MAX_IFNAME_LEN DEFAULT_PIN_PREFIX);
use Proxmox::UI;
my $step_number = 0; # Init number for global function list
@@ -340,11 +340,117 @@ my $create_basic_grid = sub {
return $grid;
};
+my sub create_network_interface_pin_view {
+ my ($done_cb) = @_;
+
+ my $dialog = Gtk3::Dialog->new();
+ $dialog->set_title('Interface Name Pinning Options');
+ $dialog->add_button('_OK', 1);
+
+ my $content = $dialog->get_content_area();
+
+ my $hbox = Gtk3::Box->new('horizontal', 0);
+ $content->pack_start($hbox, 1, 1, 5);
+
+ my $grid = Gtk3::Grid->new();
+ $grid->set_column_spacing(10);
+ $grid->set_row_spacing(10);
+
+ # make the list scrollable, in case there are lots of interfaces
+ my $scrolled_window = Gtk3::ScrolledWindow->new();
+ $scrolled_window->set_hexpand(1);
+ $scrolled_window->set_propagate_natural_height(1);
+
+ $scrolled_window->add($grid);
+ $scrolled_window->set_policy('never', 'automatic');
+ $scrolled_window->set_visible(1);
+ $scrolled_window->set_min_content_height(200);
+ $scrolled_window->set_margin_end(10);
+
+ $hbox->pack_start($scrolled_window, 1, 0, 5);
+
+ my $interfaces = Proxmox::Install::RunEnv::get()->{network}->{interfaces};
+ my $mapping = Proxmox::Install::Config::get_network_interface_pin_map();
+
+ my $inputs = {};
+ my $row = 0;
+ for my $ifname (sort keys $interfaces->%*) {
+ my $iface = $interfaces->{$ifname};
+
+ my $name = $mapping->{ $iface->{mac} };
+ my $label_text = "$iface->{mac} ($ifname, $iface->{driver}, $iface->{state})";
+
+ # if the interface has addresses assigned through DHCP, show them for
+ # reference
+ if (defined($iface->{addresses})) {
+ $label_text .=
+ "\n " . join(', ', map { "$_->{address}/$_->{prefix}" } @{ $iface->{addresses} });
+ }
+
+ my ($label, $input) = create_text_input($name, $label_text);
+ $label->set_xalign(0.);
+
+ $grid->attach($label, 0, $row, 1, 1);
+ $grid->attach($input, 1, $row, 1, 1);
+ $row++;
+
+ $inputs->{ $iface->{mac} } = $input;
+ }
+
+ $hbox->show_all();
+
+ $dialog->signal_connect(
+ response => sub {
+ my $new_mapping = {};
+ my $reverse_mapping = {};
+ foreach my $mac (keys %$inputs) {
+ my $name = $inputs->{$mac}->get_text();
+
+ if (!defined($name) || $name eq '') {
+ Proxmox::UI::message("interface name mapping for $mac cannot be empty");
+ $inputs->{$mac}->grab_focus();
+ return;
+ }
+
+ if ($reverse_mapping->{$name}) {
+ Proxmox::UI::message(
+ "duplicate interface name mapping '$name' for: $mac, $reverse_mapping->{$name}"
+ );
+ $inputs->{$mac}->grab_focus();
+ return;
+ }
+
+ if (length($name) > MAX_IFNAME_LEN) {
+ Proxmox::UI::message(
+ "interface name mapping '$name' for $mac cannot be longer than "
+ . MAX_IFNAME_LEN
+ . " characters");
+ $inputs->{$mac}->grab_focus();
+ return;
+ }
+
+ $new_mapping->{$mac} = $name;
+ $reverse_mapping->{$name} = $mac;
+ }
+
+ Proxmox::Install::Config::set_network_interface_pin_map($new_mapping);
+ $dialog->destroy();
+ $done_cb->();
+ },
+ );
+
+ $dialog->show();
+ $dialog->run();
+}
+
sub create_ipconf_view {
cleanup_view();
Proxmox::UI::display_html('ipconf.htm');
+ my $run_env = Proxmox::Install::RunEnv::get();
+ my $ipconf = $run_env->{ipconf};
+
my $grid = &$create_basic_grid();
$grid->set_row_spacing(10);
$grid->set_column_spacing(10);
@@ -355,7 +461,7 @@ sub create_ipconf_view {
my ($cidr_label, $cidr_box, $ipconf_entry_addr, $ipconf_entry_mask) = create_cidr_inputs($cidr);
- my $device_model = Gtk3::ListStore->new('Glib::String', 'Glib::String');
+ my $device_model = Gtk3::ListStore->new('Glib::String', 'Glib::String', 'Glib::String');
my $device_cb = Gtk3::ComboBox->new_with_model($device_model);
$device_cb->set_active(0);
$device_cb->set_visible(1);
@@ -369,19 +475,59 @@ sub create_ipconf_view {
$device_cb->pack_start($cell, 0);
$device_cb->add_attribute($cell, 'text', 1);
- my $get_device_desc = sub {
- my $iface = shift;
- return "$iface->{name} - $iface->{mac} ($iface->{driver})";
+ my $refresh_device_cb = sub {
+ # clear all entries and re-add them with their new names
+ my $active = $device_cb->get_active();
+ $device_model->clear();
+
+ my $mapping = Proxmox::Install::Config::get_network_interface_pin_map();
+ my $i = 0;
+ for my $index (sort keys $ipconf->{ifaces}->%*) {
+ my $iface = $ipconf->{ifaces}->{$index};
+ my $iter = $device_model->append();
+
+ my $symbol = "$iface->{state}" eq "UP" ? "\x{25CF}" : ' ';
+ my $name = $gtk_state->{network_pinning_enabled} ? $mapping->{ $iface->{mac} } : $iface->{name};
+
+ $device_model->set(
+ $iter,
+ 0 => $symbol,
+ 1 => "$name - $iface->{mac} ($iface->{driver})",
+ );
+ $i++;
+ }
+
+ # re-set the currently active entry to keep the users selection
+ $device_cb->set_active($active);
};
- my $run_env = Proxmox::Install::RunEnv::get();
- my $ipconf = $run_env->{ipconf};
+ my $name_pin_opts_button = Gtk3::Button->new('Options');
+ $name_pin_opts_button->set_sensitive($gtk_state->{network_pinning_enabled});
+ $name_pin_opts_button->signal_connect(
+ clicked => sub {
+ create_network_interface_pin_view($refresh_device_cb);
+ },
+ );
+
+ my $name_pin_checkbox = Gtk3::CheckButton->new('Pin network interface names');
+ $name_pin_checkbox->set_active($gtk_state->{network_pinning_enabled});
+ $name_pin_checkbox->signal_connect(
+ toggled => sub {
+ $name_pin_opts_button->set_sensitive(!!$name_pin_checkbox->get_active());
+ $gtk_state->{network_pinning_enabled} = !!$name_pin_checkbox->get_active();
+ $refresh_device_cb->();
+ },
+ );
my ($device_active_map, $device_active_reverse_map) = ({}, {});
my $device_change_handler = sub {
my $current = shift;
+ # happens during the clear + re-insertion of all interfaces after
+ # the pinning changed, can be safely ignored
+ return if $current->get_active() == -1;
+
my $new = $device_active_map->{ $current->get_active() };
my $iface = $ipconf->{ifaces}->{$new};
@@ -389,6 +535,7 @@ sub create_ipconf_view {
return if defined($selected) && $iface->{name} eq $selected;
Proxmox::Install::Config::set_mngmt_nic($iface->{name});
+
$ipconf_entry_addr->set_text($iface->{inet}->{addr} || $iface->{inet6}->{addr})
if $iface->{inet}->{addr} || $iface->{inet6}->{addr};
$ipconf_entry_mask->set_text($iface->{inet}->{prefix} || $iface->{inet6}->{prefix})
@@ -400,13 +547,6 @@ sub create_ipconf_view {
my $i = 0;
for my $index (sort keys $ipconf->{ifaces}->%*) {
my $iface = $ipconf->{ifaces}->{$index};
- my $iter = $device_model->append();
- my $symbol = "$iface->{state}" eq "UP" ? "\x{25CF}" : ' ';
- $device_model->set(
- $iter,
- 0 => $symbol,
- 1 => $get_device_desc->($iface),
- );
$device_active_map->{$i} = $index;
$device_active_reverse_map->{ $iface->{name} } = $i;
@@ -418,6 +558,9 @@ sub create_ipconf_view {
$i++;
}
+ # fill the combobox with entries
+ $refresh_device_cb->();
+
if (my $nic = Proxmox::Install::Config::get_mngmt_nic()) {
$initial_active_device_pos = $device_active_reverse_map->{$nic};
} else {
@@ -443,7 +586,7 @@ sub create_ipconf_view {
$label->set_xalign(1.0);
$grid->attach($label, 0, 0, 1, 1);
- $grid->attach($device_cb, 1, 0, 1, 1);
+ $grid->attach($device_cb, 1, 0, 2, 1);
my $fqdn = Proxmox::Install::Config::get_fqdn();
my $hostname = $run_env->{network}->{hostname} || $iso_env->{product};
@@ -452,17 +595,17 @@ sub create_ipconf_view {
my ($host_label, $hostentry) = create_text_input($fqdn, 'Hostname (FQDN)');
$grid->attach($host_label, 0, 1, 1, 1);
- $grid->attach($hostentry, 1, 1, 1, 1);
+ $grid->attach($hostentry, 1, 1, 2, 1);
$grid->attach($cidr_label, 0, 2, 1, 1);
- $grid->attach($cidr_box, 1, 2, 1, 1);
+ $grid->attach($cidr_box, 1, 2, 2, 1);
my $cfg_gateway = Proxmox::Install::Config::get_gateway();
my $gateway = $cfg_gateway // $ipconf->{gateway} || '192.168.100.1';
my ($gw_label, $ipconf_entry_gw) = create_text_input($gateway, 'Gateway');
$grid->attach($gw_label, 0, 3, 1, 1);
- $grid->attach($ipconf_entry_gw, 1, 3, 1, 1);
+ $grid->attach($ipconf_entry_gw, 1, 3, 2, 1);
my $cfg_dns = Proxmox::Install::Config::get_dns();
my $dnsserver = $cfg_dns // $ipconf->{dnsserver} || $gateway;
@@ -470,7 +613,10 @@ sub create_ipconf_view {
my ($dns_label, $ipconf_entry_dns) = create_text_input($dnsserver, 'DNS Server');
$grid->attach($dns_label, 0, 4, 1, 1);
- $grid->attach($ipconf_entry_dns, 1, 4, 1, 1);
+ $grid->attach($ipconf_entry_dns, 1, 4, 2, 1);
+
+ $grid->attach($name_pin_checkbox, 1, 5, 1, 1);
+ $grid->attach($name_pin_opts_button, 2, 5, 1, 1);
$gtk_state->{inbox}->show_all;
set_next(
@@ -538,6 +684,8 @@ sub create_ipconf_view {
}
Proxmox::Install::Config::set_dns($dns_ip);
+ $gtk_state->{network_pinning_enabled} = !!$name_pin_checkbox->get_active();
+
#print STDERR "TEST $ipaddress/$netmask $gateway_ip $dns_ip\n";
$step_number++;
@@ -573,6 +721,12 @@ sub create_ack_view {
my $country = Proxmox::Install::Config::get_country();
+ my $mngmt_nic = Proxmox::Install::Config::get_mngmt_nic();
+ my $iface = Proxmox::Install::RunEnv::get('network')->{interfaces}->{$mngmt_nic};
+
+ my $nic_mapping = Proxmox::Install::Config::get_network_interface_pin_map();
+ my $interface = $gtk_state->{network_pinning_enabled} ? $nic_mapping->{ $iface->{mac} } : $iface->{name};
+
my %config_values = (
__target_hd__ => join(' | ', $target_hds->@*),
__target_fs__ => Proxmox::Install::Config::get_filesys(),
@@ -580,7 +734,7 @@ sub create_ack_view {
__timezone__ => Proxmox::Install::Config::get_timezone(),
__keymap__ => Proxmox::Install::Config::get_keymap(),
__mailto__ => Proxmox::Install::Config::get_mailto(),
- __interface__ => Proxmox::Install::Config::get_mngmt_nic(),
+ __interface__ => $interface,
__hostname__ => Proxmox::Install::Config::get_hostname(),
__cidr__ => Proxmox::Install::Config::get_cidr(),
__gateway__ => Proxmox::Install::Config::get_gateway(),
@@ -609,6 +763,12 @@ sub create_ack_view {
set_next(
undef,
sub {
+ # before starting the install, unset the name pinning map if it
+ # is disabled
+ if (!$gtk_state->{network_pinning_enabled}) {
+ Proxmox::Install::Config::set_network_interface_pin_map(undef);
+ }
+
$step_number++;
create_extract_view();
},
@@ -1775,6 +1935,15 @@ if (!$initial_error && (scalar keys $run_env->{ipconf}->{ifaces}->%* == 0)) {
$initial_error = 1;
Proxmox::UI::display_html("nonics.htm");
set_next("Reboot", sub { app_quit(0); });
+} else {
+ # we enable it by default for new installation
+ $gtk_state->{network_pinning_enabled} = 1;
+
+ # pre-fill the name mapping before starting
+ my %mapping = map {
+ $_->{mac} => DEFAULT_PIN_PREFIX . $_->{pinned_id}
+ } values $run_env->{network}->{interfaces}->%*;
+ Proxmox::Install::Config::set_network_interface_pin_map(\%mapping);
}
create_intro_view() if !$initial_error;
--
2.51.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [pve-devel] [PATCH installer 14/14] gui: add support for pinning network interface names
2025-10-14 13:21 ` [pve-devel] [PATCH installer 14/14] gui: " Christoph Heiss
@ 2025-10-14 15:04 ` Maximiliano Sandoval
2025-10-16 12:01 ` Christoph Heiss
0 siblings, 1 reply; 17+ messages in thread
From: Maximiliano Sandoval @ 2025-10-14 15:04 UTC (permalink / raw)
To: Christoph Heiss; +Cc: pve-devel
Christoph Heiss <c.heiss@proxmox.com> writes:
Some comments bellow:
> Adds an additional checkbox and option button in the network panel, the
> latter triggering a dialog for setting custom names per network
> interface present on the system.
>
> Pinning is enabled by default.
>
> Each pinned network interface name defaults to `nicN`, where N is the
> pinned ID from the low-level installer.
>
> Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
> ---
> Proxmox/Sys/Net.pm | 9 +-
> proxinstall | 209 ++++++++++++++++++++++++++++++++++++++++-----
> 2 files changed, 197 insertions(+), 21 deletions(-)
>
> diff --git a/Proxmox/Sys/Net.pm b/Proxmox/Sys/Net.pm
> index 2183d27..7fe800c 100644
> --- a/Proxmox/Sys/Net.pm
> +++ b/Proxmox/Sys/Net.pm
> @@ -8,7 +8,14 @@ use Proxmox::Sys::Udev;
> use JSON qw();
>
> use base qw(Exporter);
> -our @EXPORT_OK = qw(parse_ip_address parse_ip_mask parse_fqdn);
> +our @EXPORT_OK = qw(parse_ip_address parse_ip_mask parse_fqdn MAX_IFNAME_LEN DEFAULT_PIN_PREFIX);
> +
> +# Maximum length of the (primary) name of a network interface
> +# IFNAMSIZ - 1 to account for NUL byte
> +use constant {
> + MAX_IFNAME_LEN => 15,
> + DEFAULT_PIN_PREFIX => 'nic',
> +};
>
> our $HOSTNAME_RE = "(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{,61}?[a-zA-Z0-9])?)";
> our $FQDN_RE = "(?:${HOSTNAME_RE}\\.)*${HOSTNAME_RE}";
> diff --git a/proxinstall b/proxinstall
> index 5ba65fa..35e948a 100755
> --- a/proxinstall
> +++ b/proxinstall
> @@ -37,7 +37,7 @@ use Proxmox::Sys;
> use Proxmox::Sys::Block qw(get_cached_disks);
> use Proxmox::Sys::Command qw(syscmd);
> use Proxmox::Sys::File qw(file_read_all file_write_all);
> -use Proxmox::Sys::Net qw(parse_ip_address parse_ip_mask);
> +use Proxmox::Sys::Net qw(parse_ip_address parse_ip_mask MAX_IFNAME_LEN DEFAULT_PIN_PREFIX);
> use Proxmox::UI;
>
> my $step_number = 0; # Init number for global function list
> @@ -340,11 +340,117 @@ my $create_basic_grid = sub {
> return $grid;
> };
>
> +my sub create_network_interface_pin_view {
> + my ($done_cb) = @_;
> +
> + my $dialog = Gtk3::Dialog->new();
> + $dialog->set_title('Interface Name Pinning Options');
> + $dialog->add_button('_OK', 1);
The response argument is indeed a number (gint), but there is an enum
[1] for this. In perl one can use the string 'ok' instead of
GTK_RESPONSE_OK, for example.
I do not see the value of the response being used during the
`GtkDialog::response` signal handler, note that a dialog can be closed
either be pressing ESC, clicking the X button, or by clicking the `OK`
button as per the callback bellow. As it stands, all the methods I
described above would run the handler equally, is this intended?
[1] https://docs.gtk.org/gtk3/enum.ResponseType.html
> +
> + my $content = $dialog->get_content_area();
> +
> + my $hbox = Gtk3::Box->new('horizontal', 0);
> + $content->pack_start($hbox, 1, 1, 5);
> +
> + my $grid = Gtk3::Grid->new();
> + $grid->set_column_spacing(10);
> + $grid->set_row_spacing(10);
> +
> + # make the list scrollable, in case there are lots of interfaces
> + my $scrolled_window = Gtk3::ScrolledWindow->new();
> + $scrolled_window->set_hexpand(1);
> + $scrolled_window->set_propagate_natural_height(1);
> +
> + $scrolled_window->add($grid);
> + $scrolled_window->set_policy('never', 'automatic');
> + $scrolled_window->set_visible(1);
The scrolled window is the child of hbox and gtk_widget_show_all is
called on the later, it should not be necessary to call
gtk_widget_set_visible on this one.
> + $scrolled_window->set_min_content_height(200);
> + $scrolled_window->set_margin_end(10);
It is a bit asymmetrical that there is no margin on the start.
> +
> + $hbox->pack_start($scrolled_window, 1, 0, 5);
> +
> + my $interfaces = Proxmox::Install::RunEnv::get()->{network}->{interfaces};
> + my $mapping = Proxmox::Install::Config::get_network_interface_pin_map();
> +
> + my $inputs = {};
> + my $row = 0;
> + for my $ifname (sort keys $interfaces->%*) {
> + my $iface = $interfaces->{$ifname};
> +
> + my $name = $mapping->{ $iface->{mac} };
> + my $label_text = "$iface->{mac} ($ifname, $iface->{driver}, $iface->{state})";
> +
> + # if the interface has addresses assigned through DHCP, show them for
> + # reference
> + if (defined($iface->{addresses})) {
> + $label_text .=
> + "\n " . join(', ', map { "$_->{address}/$_->{prefix}" } @{ $iface->{addresses} });
> + }
> +
> + my ($label, $input) = create_text_input($name, $label_text);
> + $label->set_xalign(0.);
> +
> + $grid->attach($label, 0, $row, 1, 1);
> + $grid->attach($input, 1, $row, 1, 1);
> + $row++;
> +
> + $inputs->{ $iface->{mac} } = $input;
> + }
> +
> + $hbox->show_all();
> +
> + $dialog->signal_connect(
> + response => sub {
> + my $new_mapping = {};
> + my $reverse_mapping = {};
> + foreach my $mac (keys %$inputs) {
> + my $name = $inputs->{$mac}->get_text();
> +
> + if (!defined($name) || $name eq '') {
> + Proxmox::UI::message("interface name mapping for $mac cannot be empty");
> + $inputs->{$mac}->grab_focus();
> + return;
> + }
> +
> + if ($reverse_mapping->{$name}) {
> + Proxmox::UI::message(
> + "duplicate interface name mapping '$name' for: $mac, $reverse_mapping->{$name}"
> + );
> + $inputs->{$mac}->grab_focus();
> + return;
> + }
> +
> + if (length($name) > MAX_IFNAME_LEN) {
> + Proxmox::UI::message(
> + "interface name mapping '$name' for $mac cannot be longer than "
> + . MAX_IFNAME_LEN
> + . " characters");
> + $inputs->{$mac}->grab_focus();
> + return;
> + }
> +
> + $new_mapping->{$mac} = $name;
> + $reverse_mapping->{$name} = $mac;
> + }
> +
> + Proxmox::Install::Config::set_network_interface_pin_map($new_mapping);
> + $dialog->destroy();
> + $done_cb->();
> + },
> + );
> +
> + $dialog->show();
> + $dialog->run();
There are two ways to present dialogs, either by running
`gtk_dialog_run` which will block until the dialog is done and will
return the response (deprecated) and then close/destroy the dialog, or
connect to the response signal which will be emitted once there is a
response and the dialog can be closed (as done above) but instead of
calling `gtk_dialog_run()` one would call `gtk_window_present()` on it.
So please run `present` instead of `run` here.
In general `present()` and `gtk_widget_show()` are kinda similar but the
former is a wrapper around the later (among other things) and is
preferable (might have a different effect depending on the compositor).
Incidentally using gtk_widget_show() for the purpose of displaying a
window/dialog is deprecated in GTK 4.
> +}
> +
> sub create_ipconf_view {
>
> cleanup_view();
> Proxmox::UI::display_html('ipconf.htm');
>
> + my $run_env = Proxmox::Install::RunEnv::get();
> + my $ipconf = $run_env->{ipconf};
> +
> my $grid = &$create_basic_grid();
> $grid->set_row_spacing(10);
> $grid->set_column_spacing(10);
> @@ -355,7 +461,7 @@ sub create_ipconf_view {
>
> my ($cidr_label, $cidr_box, $ipconf_entry_addr, $ipconf_entry_mask) = create_cidr_inputs($cidr);
>
> - my $device_model = Gtk3::ListStore->new('Glib::String', 'Glib::String');
> + my $device_model = Gtk3::ListStore->new('Glib::String', 'Glib::String', 'Glib::String');
> my $device_cb = Gtk3::ComboBox->new_with_model($device_model);
> $device_cb->set_active(0);
> $device_cb->set_visible(1);
> @@ -369,19 +475,59 @@ sub create_ipconf_view {
> $device_cb->pack_start($cell, 0);
> $device_cb->add_attribute($cell, 'text', 1);
>
> - my $get_device_desc = sub {
> - my $iface = shift;
> - return "$iface->{name} - $iface->{mac} ($iface->{driver})";
> + my $refresh_device_cb = sub {
> + # clear all entries and re-add them with their new names
> + my $active = $device_cb->get_active();
> + $device_model->clear();
> +
> + my $mapping = Proxmox::Install::Config::get_network_interface_pin_map();
> + my $i = 0;
> + for my $index (sort keys $ipconf->{ifaces}->%*) {
> + my $iface = $ipconf->{ifaces}->{$index};
> + my $iter = $device_model->append();
> +
> + my $symbol = "$iface->{state}" eq "UP" ? "\x{25CF}" : ' ';
> + my $name = $gtk_state->{network_pinning_enabled} ? $mapping->{ $iface->{mac} } : $iface->{name};
> +
> + $device_model->set(
> + $iter,
> + 0 => $symbol,
> + 1 => "$name - $iface->{mac} ($iface->{driver})",
> + );
> + $i++;
> + }
> +
> + # re-set the currently active entry to keep the users selection
> + $device_cb->set_active($active);
> };
>
> - my $run_env = Proxmox::Install::RunEnv::get();
> - my $ipconf = $run_env->{ipconf};
> + my $name_pin_opts_button = Gtk3::Button->new('Options');
> + $name_pin_opts_button->set_sensitive($gtk_state->{network_pinning_enabled});
> + $name_pin_opts_button->signal_connect(
> + clicked => sub {
> + create_network_interface_pin_view($refresh_device_cb);
> + },
> + );
> +
> + my $name_pin_checkbox = Gtk3::CheckButton->new('Pin network interface names');
> + $name_pin_checkbox->set_active($gtk_state->{network_pinning_enabled});
> + $name_pin_checkbox->signal_connect(
> + toggled => sub {
> + $name_pin_opts_button->set_sensitive(!!$name_pin_checkbox->get_active());
> + $gtk_state->{network_pinning_enabled} = !!$name_pin_checkbox->get_active();
> + $refresh_device_cb->();
> + },
> + );
>
> my ($device_active_map, $device_active_reverse_map) = ({}, {});
>
> my $device_change_handler = sub {
> my $current = shift;
>
> + # happens during the clear + re-insertion of all interfaces after
> + # the pinning changed, can be safely ignored
> + return if $current->get_active() == -1;
> +
> my $new = $device_active_map->{ $current->get_active() };
> my $iface = $ipconf->{ifaces}->{$new};
>
> @@ -389,6 +535,7 @@ sub create_ipconf_view {
> return if defined($selected) && $iface->{name} eq $selected;
>
> Proxmox::Install::Config::set_mngmt_nic($iface->{name});
> +
> $ipconf_entry_addr->set_text($iface->{inet}->{addr} || $iface->{inet6}->{addr})
> if $iface->{inet}->{addr} || $iface->{inet6}->{addr};
> $ipconf_entry_mask->set_text($iface->{inet}->{prefix} || $iface->{inet6}->{prefix})
> @@ -400,13 +547,6 @@ sub create_ipconf_view {
> my $i = 0;
> for my $index (sort keys $ipconf->{ifaces}->%*) {
> my $iface = $ipconf->{ifaces}->{$index};
> - my $iter = $device_model->append();
> - my $symbol = "$iface->{state}" eq "UP" ? "\x{25CF}" : ' ';
> - $device_model->set(
> - $iter,
> - 0 => $symbol,
> - 1 => $get_device_desc->($iface),
> - );
> $device_active_map->{$i} = $index;
> $device_active_reverse_map->{ $iface->{name} } = $i;
>
> @@ -418,6 +558,9 @@ sub create_ipconf_view {
> $i++;
> }
>
> + # fill the combobox with entries
> + $refresh_device_cb->();
> +
> if (my $nic = Proxmox::Install::Config::get_mngmt_nic()) {
> $initial_active_device_pos = $device_active_reverse_map->{$nic};
> } else {
> @@ -443,7 +586,7 @@ sub create_ipconf_view {
> $label->set_xalign(1.0);
>
> $grid->attach($label, 0, 0, 1, 1);
> - $grid->attach($device_cb, 1, 0, 1, 1);
> + $grid->attach($device_cb, 1, 0, 2, 1);
>
> my $fqdn = Proxmox::Install::Config::get_fqdn();
> my $hostname = $run_env->{network}->{hostname} || $iso_env->{product};
> @@ -452,17 +595,17 @@ sub create_ipconf_view {
>
> my ($host_label, $hostentry) = create_text_input($fqdn, 'Hostname (FQDN)');
> $grid->attach($host_label, 0, 1, 1, 1);
> - $grid->attach($hostentry, 1, 1, 1, 1);
> + $grid->attach($hostentry, 1, 1, 2, 1);
>
> $grid->attach($cidr_label, 0, 2, 1, 1);
> - $grid->attach($cidr_box, 1, 2, 1, 1);
> + $grid->attach($cidr_box, 1, 2, 2, 1);
>
> my $cfg_gateway = Proxmox::Install::Config::get_gateway();
> my $gateway = $cfg_gateway // $ipconf->{gateway} || '192.168.100.1';
>
> my ($gw_label, $ipconf_entry_gw) = create_text_input($gateway, 'Gateway');
> $grid->attach($gw_label, 0, 3, 1, 1);
> - $grid->attach($ipconf_entry_gw, 1, 3, 1, 1);
> + $grid->attach($ipconf_entry_gw, 1, 3, 2, 1);
>
> my $cfg_dns = Proxmox::Install::Config::get_dns();
> my $dnsserver = $cfg_dns // $ipconf->{dnsserver} || $gateway;
> @@ -470,7 +613,10 @@ sub create_ipconf_view {
> my ($dns_label, $ipconf_entry_dns) = create_text_input($dnsserver, 'DNS Server');
>
> $grid->attach($dns_label, 0, 4, 1, 1);
> - $grid->attach($ipconf_entry_dns, 1, 4, 1, 1);
> + $grid->attach($ipconf_entry_dns, 1, 4, 2, 1);
> +
> + $grid->attach($name_pin_checkbox, 1, 5, 1, 1);
> + $grid->attach($name_pin_opts_button, 2, 5, 1, 1);
>
> $gtk_state->{inbox}->show_all;
> set_next(
> @@ -538,6 +684,8 @@ sub create_ipconf_view {
> }
> Proxmox::Install::Config::set_dns($dns_ip);
>
> + $gtk_state->{network_pinning_enabled} = !!$name_pin_checkbox->get_active();
> +
> #print STDERR "TEST $ipaddress/$netmask $gateway_ip $dns_ip\n";
>
> $step_number++;
> @@ -573,6 +721,12 @@ sub create_ack_view {
>
> my $country = Proxmox::Install::Config::get_country();
>
> + my $mngmt_nic = Proxmox::Install::Config::get_mngmt_nic();
> + my $iface = Proxmox::Install::RunEnv::get('network')->{interfaces}->{$mngmt_nic};
> +
> + my $nic_mapping = Proxmox::Install::Config::get_network_interface_pin_map();
> + my $interface = $gtk_state->{network_pinning_enabled} ? $nic_mapping->{ $iface->{mac} } : $iface->{name};
> +
> my %config_values = (
> __target_hd__ => join(' | ', $target_hds->@*),
> __target_fs__ => Proxmox::Install::Config::get_filesys(),
> @@ -580,7 +734,7 @@ sub create_ack_view {
> __timezone__ => Proxmox::Install::Config::get_timezone(),
> __keymap__ => Proxmox::Install::Config::get_keymap(),
> __mailto__ => Proxmox::Install::Config::get_mailto(),
> - __interface__ => Proxmox::Install::Config::get_mngmt_nic(),
> + __interface__ => $interface,
> __hostname__ => Proxmox::Install::Config::get_hostname(),
> __cidr__ => Proxmox::Install::Config::get_cidr(),
> __gateway__ => Proxmox::Install::Config::get_gateway(),
> @@ -609,6 +763,12 @@ sub create_ack_view {
> set_next(
> undef,
> sub {
> + # before starting the install, unset the name pinning map if it
> + # is disabled
> + if (!$gtk_state->{network_pinning_enabled}) {
> + Proxmox::Install::Config::set_network_interface_pin_map(undef);
> + }
> +
> $step_number++;
> create_extract_view();
> },
> @@ -1775,6 +1935,15 @@ if (!$initial_error && (scalar keys $run_env->{ipconf}->{ifaces}->%* == 0)) {
> $initial_error = 1;
> Proxmox::UI::display_html("nonics.htm");
> set_next("Reboot", sub { app_quit(0); });
> +} else {
> + # we enable it by default for new installation
> + $gtk_state->{network_pinning_enabled} = 1;
> +
> + # pre-fill the name mapping before starting
> + my %mapping = map {
> + $_->{mac} => DEFAULT_PIN_PREFIX . $_->{pinned_id}
> + } values $run_env->{network}->{interfaces}->%*;
> + Proxmox::Install::Config::set_network_interface_pin_map(\%mapping);
> }
>
> create_intro_view() if !$initial_error;
--
Maximiliano
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [pve-devel] [PATCH installer 14/14] gui: add support for pinning network interface names
2025-10-14 15:04 ` Maximiliano Sandoval
@ 2025-10-16 12:01 ` Christoph Heiss
0 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2025-10-16 12:01 UTC (permalink / raw)
To: Maximiliano Sandoval; +Cc: pve-devel
Thanks for the review!
On Tue Oct 14, 2025 at 5:04 PM CEST, Maximiliano Sandoval wrote:
> Christoph Heiss <c.heiss@proxmox.com> writes:
>
> Some comments bellow:
>> diff --git a/proxinstall b/proxinstall
>> index 5ba65fa..35e948a 100755
>> --- a/proxinstall
>> +++ b/proxinstall
[..]
>> +my sub create_network_interface_pin_view {
>> + my ($done_cb) = @_;
>> +
>> + my $dialog = Gtk3::Dialog->new();
>> + $dialog->set_title('Interface Name Pinning Options');
>> + $dialog->add_button('_OK', 1);
>
> The response argument is indeed a number (gint), but there is an enum
> [1] for this. In perl one can use the string 'ok' instead of
> GTK_RESPONSE_OK, for example.
I'll change it to 'ok', thanks!
>
> I do not see the value of the response being used during the
> `GtkDialog::response` signal handler, note that a dialog can be closed
> either be pressing ESC, clicking the X button, or by clicking the `OK`
> button as per the callback bellow. As it stands, all the methods I
> described above would run the handler equally, is this intended?
I tried to be consistent with the advanced disk dialog, which has the
exact same behaviour - so yes.
[..]
>> +
>> + $scrolled_window->add($grid);
>> + $scrolled_window->set_policy('never', 'automatic');
>> + $scrolled_window->set_visible(1);
>
> The scrolled window is the child of hbox and gtk_widget_show_all is
> called on the later, it should not be necessary to call
> gtk_widget_set_visible on this one.
I see, I'll remove it.
>
>> + $scrolled_window->set_min_content_height(200);
>> + $scrolled_window->set_margin_end(10);
>
> It is a bit asymmetrical that there is no margin on the start.
Right, thanks for noticing!
[..]
>> +
>> + $dialog->show();
>> + $dialog->run();
>
> There are two ways to present dialogs, either by running
> `gtk_dialog_run` which will block until the dialog is done and will
> return the response (deprecated) and then close/destroy the dialog, or
> connect to the response signal which will be emitted once there is a
> response and the dialog can be closed (as done above) but instead of
> calling `gtk_dialog_run()` one would call `gtk_window_present()` on it.
> So please run `present` instead of `run` here.
Will do in v2.
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
end of thread, other threads:[~2025-10-16 12:02 UTC | newest]
Thread overview: 17+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-10-14 13:21 [pve-devel] [PATCH installer 00/14] support network interface name pinning Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 01/14] test: parse-kernel-cmdline: fix module import statement Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 02/14] install: add support for network interface name pinning Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 03/14] run env: network: add kernel driver name to network interface info Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 04/14] common: utils: fix clippy warnings Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 05/14] common: setup: simplify network address list serialization Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 06/14] common: implement support for `network_interface_pin_map` config Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 07/14] auto: add support for pinning network interface names Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 08/14] assistant: verify network settings in `validate-answer` subcommand Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 09/14] post-hook: avoid redundant Option<bool> for (de-)serialization Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 10/14] post-hook: add network interface name and pinning status Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 11/14] tui: views: move network options view to own module Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 12/14] tui: views: form: allow attaching user-defined data to children Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 13/14] tui: add support for pinning network interface names Christoph Heiss
2025-10-14 13:21 ` [pve-devel] [PATCH installer 14/14] gui: " Christoph Heiss
2025-10-14 15:04 ` Maximiliano Sandoval
2025-10-16 12:01 ` Christoph Heiss
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox