* [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers
@ 2026-09-08 10:16 Christoph Heiss
2026-09-08 10:16 ` [PATCH proxmox 01/15] installer-types: use `MacAddress` type where applicable Christoph Heiss
` (14 more replies)
0 siblings, 15 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
Partially fixes #2164 [0] by adding the possibility to set up a bond as
management interface during the installation, for all of GUI, TUI and
auto-installer.
For the GUI and TUI, the current name pinning dialog is converted into a
full-fledged advanced network options dialog, in the same vein as with
the bootdisk options.
The auto-installer gains a new section under [network]:
[network.bond]
members = ["ab:cd:ef:12:34:56", "ab:cd:ef:12:34:57"]
mode = "active-backup"
hash-policy = "layer2+3"
primary-interface = "ab:cd:ef:12:34:57"
The respective PDM integration will be sent separately.
[0] https://bugzilla.proxmox.com/show_bug.cgi?id=2164
Questions
=========
1. Since we now discourage the use of the 'balance-alb' and 'balance-tlb'
modes in combination with a Linux bridge (as of [1]), should these not
even be available for selection here?
2. For the Rust part, it reuses the already existing bond types from the
'proxmox-network-api' crate. Given that they are a lot universal
rather than just API-specific, would it make sense to move them into
'proxmox-network-types' instead?
@Stefan might have some comment on this as well, as we discussed that
off-list recently as well.
[1] https://lore.proxmox.com/pve-devel/20260828095721.263382-1-m.sandoval@proxmox.com/
Depends on
==========
- https://lore.proxmox.com/pve-devel/20260817125907.878237-1-c.heiss@proxmox.com/
Diffstat
========
proxmox:
Christoph Heiss (2):
installer-types: use `MacAddress` type where applicable
installer-types: answer: add options for configuring management bond
Cargo.toml | 1 +
proxmox-installer-types/Cargo.toml | 1 +
proxmox-installer-types/debian/control | 2 +
proxmox-installer-types/src/answer.rs | 190 ++++++++++++++++++++++-
proxmox-installer-types/src/post_hook.rs | 4 +-
5 files changed, 194 insertions(+), 4 deletions(-)
pve-installer:
Christoph Heiss (13):
install: run make tidy
tree-wide: use `MacAddress` type instead of string for MAC addresses
install: factor out /etc/network/interfaces setup into own subroutine
install: do not rely on interface name map to be fully populated
install: config: add option for setting up bond on management
interface
install: network: set up management bond if requested
gui: network: factor out name pinning into advanced options dialog
gui: network: add bond setup options to advanced options dialog
auto: pass trough management network bond options to low-level
installer
tui: drop long obsolete comment about logo formatting
tui: views: add widget for displaying a group of checkboxes
tui: network: factor out name pinning into advanced options dialog
tui: network: add bond setup options to advanced network dialog
Cargo.toml | 2 +
Proxmox/Install.pm | 168 +++--
Proxmox/Install/Config.pm | 44 ++
Proxmox/Sys/Net.pm | 30 +
debian/control | 1 +
proxinstall | 419 +++++++++---
proxmox-auto-installer/Cargo.toml | 1 +
proxmox-auto-installer/src/utils.rs | 38 +-
proxmox-auto-installer/tests/parse-answer.rs | 9 +-
.../tests/resources/parse_answer/btrfs.json | 5 +
.../btrfs_raid_level_uppercase.json | 5 +
.../resources/parse_answer/disk_match.json | 5 +
.../parse_answer/disk_match_all.json | 5 +
.../parse_answer/disk_match_any.json | 5 +
.../resources/parse_answer/first_boot.json | 5 +
.../parse_answer/fqdn_from_dhcp.json | 5 +
...n_from_dhcp_empty_dhcp_domain_setting.json | 5 +
...cp_no_dhcp_domain_with_default_domain.json | 5 +
...ll_fqdn_from_dhcp_with_default_domain.json | 5 +
.../parse_answer/hashed_root_password.json | 5 +
.../tests/resources/parse_answer/minimal.json | 5 +
.../resources/parse_answer/network_bond.json | 25 +
.../resources/parse_answer/network_bond.toml | 20 +
.../network_interface_pinning.json | 5 +
...ace_pinning_mixed_case_mac_addresses.json} | 5 +
...ace_pinning_mixed_case_mac_addresses.toml} | 4 +-
.../resources/parse_answer/nic_matching.json | 5 +
.../resources/parse_answer/no_network.json | 5 +
.../resources/parse_answer/specific_nic.json | 5 +
.../tests/resources/parse_answer/zfs.json | 5 +
.../zfs_raid_level_uppercase.json | 5 +
.../network_bond_invalid_hash_policy.json | 3 +
.../network_bond_invalid_hash_policy.toml | 20 +
.../network_bond_invalid_mode.json | 3 +
.../network_bond_invalid_mode.toml | 18 +
.../network_bond_primary_not_member.json | 3 +
.../network_bond_primary_not_member.toml | 19 +
.../network_bond_single_interface.json | 3 +
.../network_bond_single_interface.toml | 18 +
...rface_pinning_overlong_interface_name.json | 2 +-
proxmox-installer-common/Cargo.toml | 1 +
proxmox-installer-common/src/options.rs | 104 +--
proxmox-installer-common/src/setup.rs | 38 +-
proxmox-post-hook/src/main.rs | 2 +-
proxmox-tui-installer/Cargo.toml | 1 +
proxmox-tui-installer/src/main.rs | 8 +-
proxmox-tui-installer/src/options.rs | 45 +-
proxmox-tui-installer/src/setup.rs | 5 +
proxmox-tui-installer/src/views/mod.rs | 106 ++-
proxmox-tui-installer/src/views/network.rs | 623 +++++++++++++-----
50 files changed, 1526 insertions(+), 352 deletions(-)
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/network_bond.json
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/network_bond.toml
rename proxmox-auto-installer/tests/resources/parse_answer/{network_interface_pinning_uppercase_mac_address.json => network_interface_pinning_mixed_case_mac_addresses.json} (88%)
rename proxmox-auto-installer/tests/resources/parse_answer/{network_interface_pinning_uppercase_mac_address.toml => network_interface_pinning_mixed_case_mac_addresses.toml} (85%)
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_hash_policy.json
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_hash_policy.toml
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_mode.json
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_mode.toml
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_primary_not_member.json
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_primary_not_member.toml
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_single_interface.json
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_single_interface.toml
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH proxmox 01/15] installer-types: use `MacAddress` type where applicable
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
2026-09-08 10:16 ` [PATCH proxmox 02/15] installer-types: answer: add options for configuring management bond Christoph Heiss
` (13 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
.. instead of plain strings, making the definitions more strongly typed.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-installer-types/src/answer.rs | 2 +-
proxmox-installer-types/src/post_hook.rs | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/proxmox-installer-types/src/answer.rs b/proxmox-installer-types/src/answer.rs
index e2e9b8e4..e608b4cb 100644
--- a/proxmox-installer-types/src/answer.rs
+++ b/proxmox-installer-types/src/answer.rs
@@ -435,7 +435,7 @@ pub struct NetworkInterfacePinningOptionsAnswer {
pub enabled: bool,
/// Maps MAC address to custom name
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
- pub mapping: HashMap<String, String>,
+ pub mapping: HashMap<MacAddress, String>,
}
#[cfg_attr(feature = "api-types", api(
diff --git a/proxmox-installer-types/src/post_hook.rs b/proxmox-installer-types/src/post_hook.rs
index 77cbef48..2cbc0f52 100644
--- a/proxmox-installer-types/src/post_hook.rs
+++ b/proxmox-installer-types/src/post_hook.rs
@@ -2,7 +2,7 @@
use serde::{Deserialize, Serialize};
-use proxmox_network_types::ip_address::Cidr;
+use proxmox_network_types::{MacAddress, ip_address::Cidr};
#[cfg(feature = "api-types")]
use proxmox_schema::api;
@@ -90,7 +90,7 @@ pub struct NetworkInterfaceInfo {
/// Name of the interface
pub name: String,
/// MAC address of the interface
- pub mac: String,
+ pub mac: MacAddress,
/// (Designated) IP address of the interface
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<Cidr>,
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH proxmox 02/15] installer-types: answer: add options for configuring management bond
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
2026-09-08 10:16 ` [PATCH proxmox 01/15] installer-types: use `MacAddress` type where applicable Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 03/15] install: run make tidy Christoph Heiss
` (12 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
Adds a new subsection `bond` in [network]. This allows for configuring a
bond for the management network interface during the installation.
E.g.
[network.bond]
members = ["ab:cd:ef:12:34:56", "ab:cd:ef:12:34:57"]
mode = "active-backup"
hash-policy = "layer2+3"
primary-interface = "ab:cd:ef:12:34:57"
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Cargo.toml | 1 +
proxmox-installer-types/Cargo.toml | 1 +
proxmox-installer-types/debian/control | 2 +
proxmox-installer-types/src/answer.rs | 188 ++++++++++++++++++++++++-
4 files changed, 191 insertions(+), 1 deletion(-)
diff --git a/Cargo.toml b/Cargo.toml
index 16e91c94..13f62856 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -178,6 +178,7 @@ proxmox-lang = { version = "1.5", path = "proxmox-lang" }
proxmox-log = { version = "1.0.0", path = "proxmox-log" }
proxmox-login = { version = "1.0.0", path = "proxmox-login" }
proxmox-network-types = { version = "1.0.2", path = "proxmox-network-types" }
+proxmox-network-api = { version = "1.0.5", path = "proxmox-network-api" }
proxmox-parallel-handler = { version = "1.0.0", path = "proxmox-parallel-handler" }
proxmox-pgp = { version = "1.0.0", path = "proxmox-pgp" }
proxmox-procfs = { version = "0.1.0", path = "proxmox-procfs" }
diff --git a/proxmox-installer-types/Cargo.toml b/proxmox-installer-types/Cargo.toml
index 45f436e9..b208b0b7 100644
--- a/proxmox-installer-types/Cargo.toml
+++ b/proxmox-installer-types/Cargo.toml
@@ -17,6 +17,7 @@ serde = { workspace = true, features = ["derive"] }
serde_plain.workspace = true
regex = { workspace = true }
proxmox-network-types.workspace = true
+proxmox-network-api.workspace = true
proxmox-schema = { workspace = true, features = ["api-macro"] }
proxmox-section-config = { workspace = true, optional = true }
proxmox-node-status.workspace = true
diff --git a/proxmox-installer-types/debian/control b/proxmox-installer-types/debian/control
index e6f2ad6a..9dfe0017 100644
--- a/proxmox-installer-types/debian/control
+++ b/proxmox-installer-types/debian/control
@@ -7,6 +7,7 @@ Build-Depends-Arch: cargo:native <!nocheck>,
rustc:native (>= 1.85) <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
+ librust-proxmox-network-api-1+default-dev (>= 1.0.5-~~) <!nocheck>,
librust-proxmox-network-types-1+default-dev (>= 1.0.2-~~) <!nocheck>,
librust-proxmox-node-status-1+default-dev <!nocheck>,
librust-proxmox-schema-5+api-macro-dev (>= 5.3.0-~~) <!nocheck>,
@@ -28,6 +29,7 @@ Multi-Arch: same
Depends:
${misc:Depends},
librust-anyhow-1+default-dev,
+ librust-proxmox-network-api-1+default-dev (>= 1.0.5-~~),
librust-proxmox-network-types-1+default-dev (>= 1.0.2-~~),
librust-proxmox-node-status-1+default-dev,
librust-proxmox-schema-5+api-macro-dev (>= 5.3.0-~~),
diff --git a/proxmox-installer-types/src/answer.rs b/proxmox-installer-types/src/answer.rs
index e608b4cb..93dc32a4 100644
--- a/proxmox-installer-types/src/answer.rs
+++ b/proxmox-installer-types/src/answer.rs
@@ -14,7 +14,8 @@ use std::{
str::FromStr,
};
-use proxmox_network_types::{fqdn::Fqdn, ip_address::Cidr};
+use proxmox_network_api::{BondXmitHashPolicy, LinuxBondMode};
+use proxmox_network_types::{MacAddress, fqdn::Fqdn, ip_address::Cidr};
#[cfg(feature = "api-types")]
use proxmox_schema::{
@@ -438,6 +439,106 @@ pub struct NetworkInterfacePinningOptionsAnswer {
pub mapping: HashMap<MacAddress, String>,
}
+#[cfg_attr(feature = "api-types", api(
+ properties: {
+ members: {
+ description: "List of interface MAC addresses to add as bond members.",
+ type: Array,
+ items: {
+ type: MacAddress,
+ },
+ },
+ },
+))]
+#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+/// Options for creating a bond for the management interface with the given
+/// physical interfaces and mode.
+pub struct NetworkBondOptions {
+ /// List of interface MAC addresses to add as bond members.
+ pub members: Vec<MacAddress>,
+ /// Mode for the bond to set.
+ pub mode: LinuxBondMode,
+ /// Bond hash policy. Only used with '802.3ad' and 'balance-xor' mode.
+ #[serde(default = "NetworkBondOptions::default_hash_policy")]
+ pub hash_policy: BondXmitHashPolicy,
+ /// Primary bond interface. Only used with 'active-backup' mode.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub primary_interface: Option<MacAddress>,
+}
+
+impl NetworkBondOptions {
+ /// List of all available bond modes.
+ pub const MODES: &[LinuxBondMode] = {
+ use LinuxBondMode::*;
+ &[
+ ActiveBackup,
+ BalanceAlb,
+ BalanceRr,
+ BalanceTlb,
+ BalanceXor,
+ Broadcast,
+ Ieee802_3ad,
+ ]
+ };
+
+ /// List of all available hash policies.
+ pub const HASH_POLICIES: &[BondXmitHashPolicy] = {
+ use BondXmitHashPolicy::*;
+ &[Layer2, Layer2_3, Layer3_4]
+ };
+
+ /// Returns the default hash policy for new bonds.
+ const fn default_hash_policy() -> BondXmitHashPolicy {
+ BondXmitHashPolicy::Layer2
+ }
+
+ /// Returns a bond configuration instance that disables any bonding setup in the low-level
+ /// installer.
+ pub const fn disabled() -> Self {
+ Self {
+ // an empty interface list is interpreted as disabled by the low-level installer
+ members: Vec::new(),
+ mode: LinuxBondMode::ActiveBackup,
+ hash_policy: BondXmitHashPolicy::Layer2,
+ primary_interface: None,
+ }
+ }
+
+ /// Does some basic checks on the options.
+ ///
+ /// This includes checks for:
+ /// - There are at least to members
+ /// - Whether the primary interface is applicable to the mode
+ /// - Whether the primary interface (if given) is a member
+ pub fn verify(&self) -> Result<()> {
+ if self.members.len() < 2 {
+ bail!(
+ "bond setup requires at least 2 interfaces, got {}",
+ self.members.len()
+ );
+ }
+
+ if let Some(primary_if) = &self.primary_interface {
+ if self.mode != LinuxBondMode::ActiveBackup {
+ bail!("primary bond interface can only be set in active-backup mode");
+ }
+
+ if !self.members.contains(primary_if) {
+ bail!(
+ "primary bond interface {primary_if} must be a member of the bond (members: {})",
+ self.members
+ .iter()
+ .fold(String::new(), |acc, mac| format!("{acc}, {mac}"))
+ .trim_start_matches(", ")
+ );
+ }
+ }
+
+ Ok(())
+ }
+}
+
#[cfg_attr(feature = "api-types", api(
properties: {
filter: {
@@ -464,6 +565,9 @@ pub struct NetworkConfigFromAnswer {
/// Off by default. Allowed for both `from-dhcp` and `from-answer` modes.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interface_name_pinning: Option<NetworkInterfacePinningOptionsAnswer>,
+ /// If set, sets up a Linux bond for the management interface.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub bond: Option<NetworkBondOptions>,
}
#[cfg_attr(feature = "api-types", api)]
@@ -475,6 +579,9 @@ pub struct NetworkConfigFromDhcp {
/// Off by default. Allowed for both `from-dhcp` and `from-answer` modes.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interface_name_pinning: Option<NetworkInterfacePinningOptionsAnswer>,
+ /// If set, sets up a Linux bond for the management interface.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub bond: Option<NetworkBondOptions>,
}
#[cfg_attr(feature = "api-types", api(
@@ -503,6 +610,14 @@ impl NetworkConfig {
Self::FromAnswer(answer) => answer.interface_name_pinning.as_ref(),
}
}
+
+ /// Returns the management network Linux bond options, if any.
+ pub const fn bond(&self) -> Option<&NetworkBondOptions> {
+ match self {
+ Self::FromDhcp(dhcp) => dhcp.bond.as_ref(),
+ Self::FromAnswer(answer) => answer.bond.as_ref(),
+ }
+ }
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
@@ -1475,4 +1590,75 @@ mod tests {
};
assert!(setup.filesystem_details().is_ok());
}
+
+ #[test]
+ fn network_bond_requires_at_least_two_members() {
+ let mut bond = NetworkBondOptions::disabled();
+ let res = bond.verify();
+ assert!(res.is_err());
+ assert_eq!(
+ res.unwrap_err().to_string(),
+ "bond setup requires at least 2 interfaces, got 0"
+ );
+
+ bond.members = vec!["12:34:56:ab:cd:df".parse().unwrap()];
+
+ let res = bond.verify();
+ assert!(res.is_err());
+ assert_eq!(
+ res.unwrap_err().to_string(),
+ "bond setup requires at least 2 interfaces, got 1"
+ );
+
+ bond.members = vec![
+ "12:34:56:ab:cd:df".parse().unwrap(),
+ "ab:cd:df:12:34:56".parse().unwrap(),
+ ];
+ assert!(bond.verify().is_ok());
+ }
+
+ #[test]
+ fn network_bond_primary_if_only_with_active_backup() {
+ let mut bond = NetworkBondOptions::disabled();
+ bond.members = vec![
+ "12:34:56:ab:cd:df".parse().unwrap(),
+ "ab:cd:df:12:34:56".parse().unwrap(),
+ ];
+ bond.primary_interface = Some("12:34:56:ab:cd:df".parse().unwrap());
+
+ for m in NetworkBondOptions::MODES {
+ bond.mode = *m;
+ match m {
+ LinuxBondMode::ActiveBackup => assert!(bond.verify().is_ok()),
+ _ => {
+ let res = bond.verify();
+ assert!(res.is_err());
+ assert_eq!(
+ res.unwrap_err().to_string(),
+ "primary bond interface can only be set in active-backup mode"
+ );
+ }
+ }
+ }
+ }
+
+ #[test]
+ fn network_bond_primary_if_must_be_member() {
+ let mut bond = NetworkBondOptions::disabled();
+ bond.members = vec![
+ "12:34:56:ab:cd:df".parse().unwrap(),
+ "ab:cd:df:12:34:56".parse().unwrap(),
+ ];
+ bond.primary_interface = Some("12:34:56:ab:cd:df".parse().unwrap());
+ assert!(bond.verify().is_ok());
+
+ bond.primary_interface = Some("ab:12:cd:34:ef:56".parse().unwrap());
+
+ let res = bond.verify();
+ assert!(res.is_err());
+ assert_eq!(
+ res.unwrap_err().to_string(),
+ "primary bond interface AB:12:CD:34:EF:56 must be a member of the bond (members: 12:34:56:AB:CD:DF, AB:CD:DF:12:34:56)"
+ );
+ }
}
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH installer 03/15] install: run make tidy
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
2026-09-08 10:16 ` [PATCH proxmox 01/15] installer-types: use `MacAddress` type where applicable Christoph Heiss
2026-09-08 10:16 ` [PATCH proxmox 02/15] installer-types: answer: add options for configuring management bond Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 04/15] tree-wide: use `MacAddress` type instead of string for MAC addresses Christoph Heiss
` (11 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Proxmox/Install.pm | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Proxmox/Install.pm b/Proxmox/Install.pm
index 6298cff..7bb8e29 100644
--- a/Proxmox/Install.pm
+++ b/Proxmox/Install.pm
@@ -673,7 +673,8 @@ sub prepare_grub_efi_boot_esp {
# without a shim package installed (no signed shim exists for arm64 yet) grub-install
# deploys only the plain grub image, so use that as the removable-media default loader
$shim_src = $run_env->{arch} eq 'arm64' ? 'grubaa64.efi' : 'grubx64.efi';
- warn "no shim binary found, using '$shim_src' as removable-media default boot loader\n";
+ warn
+ "no shim binary found, using '$shim_src' as removable-media default boot loader\n";
}
syscmd(
"mv $targetdir/boot/efi/EFI/BOOT/$shim_src $targetdir/boot/efi/EFI/BOOT/$boot_dst")
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH installer 04/15] tree-wide: use `MacAddress` type instead of string for MAC addresses
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
` (2 preceding siblings ...)
2026-09-08 10:16 ` [PATCH installer 03/15] install: run make tidy Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 05/15] install: factor out /etc/network/interfaces setup into own subroutine Christoph Heiss
` (10 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
Makes all Rust code dealing with MAC addresses a lot more typesafe. The
`MacAddress` type is also cheaper to construct than `String`, since the
former is just 6 bytes and avoids heap allocations.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-auto-installer/tests/parse-answer.rs | 2 +-
...ace_pinning_mixed_case_mac_addresses.json} | 0
...ace_pinning_mixed_case_mac_addresses.toml} | 4 +-
...rface_pinning_overlong_interface_name.json | 2 +-
proxmox-installer-common/src/options.rs | 66 +++++++++----------
proxmox-installer-common/src/setup.rs | 28 ++++++--
proxmox-post-hook/src/main.rs | 2 +-
proxmox-tui-installer/src/views/network.rs | 26 +++-----
8 files changed, 68 insertions(+), 62 deletions(-)
rename proxmox-auto-installer/tests/resources/parse_answer/{network_interface_pinning_uppercase_mac_address.json => network_interface_pinning_mixed_case_mac_addresses.json} (100%)
rename proxmox-auto-installer/tests/resources/parse_answer/{network_interface_pinning_uppercase_mac_address.toml => network_interface_pinning_mixed_case_mac_addresses.toml} (85%)
diff --git a/proxmox-auto-installer/tests/parse-answer.rs b/proxmox-auto-installer/tests/parse-answer.rs
index 8b0bae7..ae5b87d 100644
--- a/proxmox-auto-installer/tests/parse-answer.rs
+++ b/proxmox-auto-installer/tests/parse-answer.rs
@@ -128,7 +128,7 @@ mod tests {
hashed_root_password,
minimal,
network_interface_pinning,
- network_interface_pinning_uppercase_mac_address,
+ network_interface_pinning_mixed_case_mac_addresses,
nic_matching,
no_network,
specific_nic,
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_uppercase_mac_address.json b/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_mixed_case_mac_addresses.json
similarity index 100%
rename from proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_uppercase_mac_address.json
rename to proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_mixed_case_mac_addresses.json
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_uppercase_mac_address.toml b/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_mixed_case_mac_addresses.toml
similarity index 85%
rename from proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_uppercase_mac_address.toml
rename to proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_mixed_case_mac_addresses.toml
index 6681fe9..deb1fd3 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_uppercase_mac_address.toml
+++ b/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_mixed_case_mac_addresses.toml
@@ -14,8 +14,8 @@ 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"
+"24:8a:07:1e:05:bd" = "lan1"
+"B4:2e:99:ac:AD:b4" = "mgmt"
[disk-setup]
filesystem = "ext4"
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
index f3c9169..952b34a 100644
--- 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
@@ -1,3 +1,3 @@
{
- "error": "interface name 'waytoolonginterfacename' for 'ab:cd:ef:12:34:56' cannot be longer than 15 characters"
+ "error": "interface name 'waytoolonginterfacename' for 'AB:CD:EF:12:34:56' cannot be longer than 15 characters"
}
diff --git a/proxmox-installer-common/src/options.rs b/proxmox-installer-common/src/options.rs
index c7f0baf..fbbfe77 100644
--- a/proxmox-installer-common/src/options.rs
+++ b/proxmox-installer-common/src/options.rs
@@ -19,7 +19,7 @@ use proxmox_installer_types::{
ZfsChecksumOption, ZfsCompressOption, ZfsRaidLevel,
},
};
-use proxmox_network_types::{fqdn::Fqdn, ip_address::Cidr};
+use proxmox_network_types::{MacAddress, fqdn::Fqdn, ip_address::Cidr};
pub trait RaidLevel {
/// Returns the minimum number of disks needed for this RAID level.
@@ -315,7 +315,7 @@ impl TimezoneOptions {
pub struct NetworkInterfacePinningOptions {
/// Maps MAC address to custom name
#[serde(default)]
- pub mapping: HashMap<String, String>,
+ pub mapping: HashMap<MacAddress, String>,
}
impl NetworkInterfacePinningOptions {
@@ -341,7 +341,7 @@ impl NetworkInterfacePinningOptions {
.unwrap()
});
- let mut reverse_mapping = HashMap::<String, String>::new();
+ let mut reverse_mapping = HashMap::<String, MacAddress>::new();
for (mac, name) in self.mapping.iter() {
if name.len() < MIN_IFNAME_LEN {
bail!(
@@ -368,7 +368,7 @@ impl NetworkInterfacePinningOptions {
bail!("duplicate interface name mapping '{name}' for: {mac}, {duplicate_mac}");
}
- reverse_mapping.insert(name.clone(), mac.clone());
+ reverse_mapping.insert(name.clone(), *mac);
}
Ok(())
@@ -379,13 +379,7 @@ impl From<&NetworkInterfacePinningOptionsAnswer> for NetworkInterfacePinningOpti
fn from(answer: &NetworkInterfacePinningOptionsAnswer) -> Self {
if answer.enabled {
Self {
- // convert all MAC addresses to lowercase before further usage,
- // to enable easy comparison
- mapping: answer
- .mapping
- .iter()
- .map(|(k, v)| (k.to_lowercase(), v.clone()))
- .collect(),
+ mapping: answer.mapping.clone(),
}
} else {
Self::default()
@@ -480,7 +474,7 @@ impl NetworkOptions {
// required by the low-level installer
for iface in network.interfaces.values() {
if let Some(pinned) = iface.to_pinned(opts) {
- opts.mapping.entry(iface.mac.clone()).or_insert(pinned.name);
+ opts.mapping.entry(iface.mac).or_insert(pinned.name);
}
}
}
@@ -609,7 +603,7 @@ mod tests {
pinned_id: Some("0".to_owned()),
state: InterfaceState::Up,
driver: "dummy".to_owned(),
- mac: "01:23:45:67:89:ab".to_owned(),
+ mac: "01:23:45:67:89:ab".parse().unwrap(),
addresses: vec![Cidr::new(Ipv4Addr::new(192, 168, 0, 2), 24).unwrap()],
},
);
@@ -699,7 +693,7 @@ mod tests {
pinned_id: Some("0".to_owned()),
state: InterfaceState::Up,
driver: "dummy".to_owned(),
- mac: "01:23:45:67:89:ab".to_owned(),
+ mac: "01:23:45:67:89:ab".parse().unwrap(),
addresses: vec![
Cidr::new(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2), 64).unwrap(),
],
@@ -798,7 +792,7 @@ mod tests {
pinned_id: Some("0".to_owned()),
state: InterfaceState::Up,
driver: "dummy".to_owned(),
- mac: "01:23:45:67:89:ab".to_owned(),
+ mac: "01:23:45:67:89:ab".parse().unwrap(),
addresses: vec![],
},
);
@@ -836,13 +830,13 @@ mod tests {
let mut options = NetworkInterfacePinningOptions::default();
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), String::new());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), String::new());
let res = options.verify();
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
- "interface name for 'ab:cd:ef:12:34:56' must be at least 2 characters long"
+ "interface name for 'AB:CD:EF:12:34:56' must be at least 2 characters long"
)
}
@@ -851,13 +845,13 @@ mod tests {
let mut options = NetworkInterfacePinningOptions::default();
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "a".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "a".to_owned());
let res = options.verify();
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
- "interface name for 'ab:cd:ef:12:34:56' must be at least 2 characters long"
+ "interface name for 'AB:CD:EF:12:34:56' must be at least 2 characters long"
)
}
@@ -865,7 +859,7 @@ mod tests {
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(),
+ "ab:cd:ef:12:34:56".parse().unwrap(),
"waytoolonginterfacename".to_owned(),
);
@@ -873,7 +867,7 @@ mod tests {
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
- "interface name 'waytoolonginterfacename' for 'ab:cd:ef:12:34:56' cannot be longer than 15 characters"
+ "interface name 'waytoolonginterfacename' for 'AB:CD:EF:12:34:56' cannot be longer than 15 characters"
)
}
@@ -882,10 +876,10 @@ mod tests {
let mut options = NetworkInterfacePinningOptions::default();
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "nic0".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "nic0".to_owned());
options
.mapping
- .insert("12:34:56:ab:cd:ef".to_owned(), "nic0".to_owned());
+ .insert("12:34:56:ab:cd:ef".parse().unwrap(), "nic0".to_owned());
let res = options.verify();
assert!(res.is_err());
@@ -894,8 +888,8 @@ mod tests {
// [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"));
+ assert!(err.contains("12:34:56:AB:CD:EF"));
+ assert!(err.contains("AB:CD:EF:12:34:56"));
}
#[test]
@@ -903,13 +897,13 @@ mod tests {
let mut options = NetworkInterfacePinningOptions::default();
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "nic-".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "nic-".to_owned());
let res = options.verify();
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
- "interface name 'nic-' for 'ab:cd:ef:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
+ "interface name 'nic-' for 'AB:CD:EF:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
)
}
@@ -918,24 +912,24 @@ mod tests {
let mut options = NetworkInterfacePinningOptions::default();
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "0nic".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "0nic".to_owned());
let res = options.verify();
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
- "interface name '0nic' for 'ab:cd:ef:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
+ "interface name '0nic' for 'AB:CD:EF:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
);
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "_a".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "_a".to_owned());
let res = options.verify();
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
- "interface name '_a' for 'ab:cd:ef:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
+ "interface name '_a' for 'AB:CD:EF:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
);
}
@@ -944,21 +938,21 @@ mod tests {
let mut options = NetworkInterfacePinningOptions::default();
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "Nic0".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "Nic0".to_owned());
let res = options.verify();
assert!(res.is_ok());
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "nIc0".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "nIc0".to_owned());
let res = options.verify();
assert!(res.is_ok());
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "nic0".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "nic0".to_owned());
let res = options.verify();
assert!(res.is_ok());
@@ -969,13 +963,13 @@ mod tests {
let mut options = NetworkInterfacePinningOptions::default();
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "12345".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "12345".to_owned());
let res = options.verify();
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
- "interface name '12345' for 'ab:cd:ef:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
+ "interface name '12345' for 'AB:CD:EF:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
)
}
}
diff --git a/proxmox-installer-common/src/setup.rs b/proxmox-installer-common/src/setup.rs
index b8af4e3..14882bc 100644
--- a/proxmox-installer-common/src/setup.rs
+++ b/proxmox-installer-common/src/setup.rs
@@ -1,4 +1,4 @@
-use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
+use serde::{Deserialize, Deserializer, Serialize, Serializer, de, ser::SerializeMap};
use std::{
cmp,
collections::{BTreeMap, HashMap},
@@ -18,7 +18,7 @@ use proxmox_installer_types::{
BootType, IsoInfo, ProductConfig,
answer::{BtrfsCompressOption, FilesystemType, ZfsChecksumOption, ZfsCompressOption},
};
-use proxmox_network_types::Cidr;
+use proxmox_network_types::{Cidr, MacAddress};
/// Paths in the ISO environment containing installer data.
#[derive(Clone, Deserialize)]
@@ -395,7 +395,7 @@ pub struct Interface {
/// interface name cannot/should not be pinned due to being e.g. a non-physical link.
pub pinned_id: Option<String>,
- pub mac: String,
+ pub mac: MacAddress,
pub state: InterfaceState,
@@ -535,9 +535,10 @@ pub struct InstallConfig {
#[serde(
default,
skip_serializing_if = "HashMap::is_empty",
- deserialize_with = "deserialize_optional_map"
+ deserialize_with = "deserialize_optional_map",
+ serialize_with = "serialize_low_level_mac_address_map"
)]
- pub network_interface_pin_map: HashMap<String, String>,
+ pub network_interface_pin_map: HashMap<MacAddress, String>,
pub hostname: String,
pub domain: String,
@@ -583,3 +584,20 @@ where
let map: Option<HashMap<K, V>> = Deserialize::deserialize(deserializer)?;
Ok(map.unwrap_or_default())
}
+
+/// Serializes a [HashMap] using [MacAddress] as keys, normalizing MAC addresses into their
+/// lower-case format as the low-level installer expects it.
+fn serialize_low_level_mac_address_map<S, V>(
+ map: &HashMap<MacAddress, V>,
+ serializer: S,
+) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+ V: Serialize,
+{
+ let mut serializer = serializer.serialize_map(Some(map.len()))?;
+ for (mac, v) in map {
+ serializer.serialize_entry(&mac.to_string().to_lowercase(), &v)?;
+ }
+ serializer.end()
+}
diff --git a/proxmox-post-hook/src/main.rs b/proxmox-post-hook/src/main.rs
index 749fd0e..e50a773 100644
--- a/proxmox-post-hook/src/main.rs
+++ b/proxmox-post-hook/src/main.rs
@@ -286,7 +286,7 @@ mod detail {
anyhow::Ok(NetworkInterfaceInfo {
name: ifname.clone(),
- mac: nic.mac.clone(),
+ mac: nic.mac,
// Use the actual IP address from the low-level install config, as the runtime info
// contains the original IP address from DHCP.
address: is_management.then_some(config.cidr),
diff --git a/proxmox-tui-installer/src/views/network.rs b/proxmox-tui-installer/src/views/network.rs
index 12cef19..6401174 100644
--- a/proxmox-tui-installer/src/views/network.rs
+++ b/proxmox-tui-installer/src/views/network.rs
@@ -17,12 +17,12 @@ use proxmox_installer_common::{
options::{NetworkInterfacePinningOptions, NetworkOptions},
setup::{Interface, NetworkInfo},
};
-use proxmox_network_types::{fqdn::Fqdn, ip_address::Cidr};
+use proxmox_network_types::{MacAddress, fqdn::Fqdn, ip_address::Cidr};
use super::{CidrAddressEditView, FormView};
struct NetworkViewOptions {
- selected_mac: String,
+ selected_mac: MacAddress,
pinning_enabled: bool,
// For UI purposes, we want to always save the mapping, to save the state
// between toggling the checkbox
@@ -55,14 +55,8 @@ impl NetworkOptionsView {
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()
- });
+ .map(|iface| iface.mac)
+ .unwrap_or_else(|| ifaces.first().expect("at least one network interface").mac);
let options_ref = Arc::new(Mutex::new(NetworkViewOptions {
selected_mac,
@@ -309,7 +303,7 @@ impl NetworkOptionsView {
.on_submit({
let options_ref = options_ref.clone();
move |_, iface| {
- options_ref.lock().expect("unpoisoned lock").selected_mac = iface.mac.clone();
+ options_ref.lock().expect("unpoisoned lock").selected_mac = iface.mac;
}
});
@@ -330,7 +324,7 @@ impl ViewWrapper for NetworkOptionsView {
}
struct InterfacePinningOptionsView {
- view: ScrollView<NamedView<FormView<String>>>,
+ view: ScrollView<NamedView<FormView<MacAddress>>>,
}
impl InterfacePinningOptionsView {
@@ -344,7 +338,7 @@ impl InterfacePinningOptionsView {
// The low-level installer will skip them anyway.
let interfaces = interfaces.iter().filter(|iface| iface.pinned_id.is_some());
- let mut form = FormView::<String>::new();
+ let mut form = FormView::<MacAddress>::new();
for iface in interfaces {
let label = format!(
@@ -366,7 +360,7 @@ impl InterfacePinningOptionsView {
.fixed_width(MAX_IFNAME_LEN),
);
- form.add_child_with_data(&label, view, iface.mac.clone());
+ form.add_child_with_data(&label, view, iface.mac);
if !iface.addresses.is_empty() {
for chunk in iface.addresses.chunks(2) {
@@ -403,7 +397,7 @@ impl InterfacePinningOptionsView {
.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());
+ mapping.insert(*mac, (*name).clone());
}
let opts = NetworkInterfacePinningOptions { mapping };
@@ -414,5 +408,5 @@ impl InterfacePinningOptionsView {
}
impl ViewWrapper for InterfacePinningOptionsView {
- cursive::wrap_impl!(self.view: ScrollView<NamedView<FormView<String>>>);
+ cursive::wrap_impl!(self.view: ScrollView<NamedView<FormView<MacAddress>>>);
}
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH installer 05/15] install: factor out /etc/network/interfaces setup into own subroutine
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
` (3 preceding siblings ...)
2026-09-08 10:16 ` [PATCH installer 04/15] tree-wide: use `MacAddress` type instead of string for MAC addresses Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 06/15] install: do not rely on interface name map to be fully populated Christoph Heiss
` (9 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
Makes it more readable and a bit easier to add more logic in the future.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Proxmox/Install.pm | 124 +++++++++++++++++++++++++--------------------
1 file changed, 68 insertions(+), 56 deletions(-)
diff --git a/Proxmox/Install.pm b/Proxmox/Install.pm
index 7bb8e29..45aa53b 100644
--- a/Proxmox/Install.pm
+++ b/Proxmox/Install.pm
@@ -798,6 +798,71 @@ my sub setup_proxmox_first_boot_service {
}
}
+my sub setup_ifupdown2_config {
+ my ($targetdir) = @_;
+
+ my $run_env = Proxmox::Install::RunEnv::get();
+ my $iso_env = Proxmox::Install::ISOEnv::get();
+
+ my $ip_version = Proxmox::Install::Config::get_ip_version();
+ my $ntype = $ip_version == 4 ? 'inet' : 'inet6';
+
+ my $interfaces = $run_env->{network}->{interfaces};
+ my $ethdev = Proxmox::Install::Config::get_mngmt_nic();
+ my $cidr = Proxmox::Install::Config::get_cidr();
+ my $gateway = Proxmox::Install::Config::get_gateway();
+
+ my $netif_pin_map = Proxmox::Install::Config::get_network_interface_pin_map();
+
+ my $ifaces = "auto lo\niface lo inet loopback\n\n";
+
+ # iproute2 omits the gateway and protocol keys for some routes; route devices are original
+ # kernel interface names, while $ethdev can be the pinned one
+ my $gateways6 = $run_env->{network}->{routes}->{gateways6} // [];
+ my $is_gateway6_from_ra = grep {
+ my $dev = $_->{dev} // '';
+ if (defined($netif_pin_map) && defined($interfaces->{$dev})) {
+ $dev = $netif_pin_map->{ $interfaces->{$dev}->{mac} } // $dev;
+ }
+ $dev eq $ethdev
+ && ($_->{gateway} // '') eq $gateway
+ && ($_->{protocol} // '') eq 'ra';
+ } $gateways6->@*;
+
+ if ($iso_env->{cfg}->{bridged_network}) {
+ $ifaces .= "iface $ethdev $ntype manual\n";
+
+ $ifaces .=
+ "\nauto vmbr0\niface vmbr0 $ntype static\n"
+ . "\taddress $cidr\n"
+ . "\tgateway $gateway\n"
+ . "\tbridge-ports $ethdev\n"
+ . "\tbridge-stp off\n"
+ . "\tbridge-fd 0\n";
+ } else {
+ $ifaces .= "auto $ethdev\n" . #
+ "iface $ethdev $ntype static\n" . #
+ "\taddress $cidr\n";
+
+ if (!$is_gateway6_from_ra) {
+ $ifaces .= "\tgateway $gateway\n";
+ }
+ }
+
+ foreach my $iface (sort keys $interfaces->%*) {
+ my $if = $interfaces->{$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";
+ }
+
+ $ifaces .= "\n\nsource /etc/network/interfaces.d/*\n";
+
+ file_write_all("$targetdir/etc/network/interfaces", $ifaces);
+}
+
sub extract_data {
my $iso_env = Proxmox::Install::ISOEnv::get();
my $run_env = Proxmox::Install::RunEnv::get();
@@ -1140,19 +1205,14 @@ sub extract_data {
# configure interfaces
- my $ifaces = "auto lo\niface lo inet loopback\n\n";
-
- my $ip_version = Proxmox::Install::Config::get_ip_version();
- my $ntype = $ip_version == 4 ? 'inet' : 'inet6';
-
- my $ethdev = Proxmox::Install::Config::get_mngmt_nic();
- my $cidr = Proxmox::Install::Config::get_cidr();
- my $gateway = Proxmox::Install::Config::get_gateway();
+ setup_ifupdown2_config($targetdir);
# 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)) {
+ my $ethdev = Proxmox::Install::Config::get_mngmt_nic();
+
mkdir "$targetdir/usr/local/lib/systemd", 0755;
mkdir "$targetdir/usr/local/lib/systemd/network", 0755;
@@ -1189,54 +1249,6 @@ sub extract_data {
}
}
- my $interfaces = $run_env->{network}->{interfaces};
-
- # iproute2 omits the gateway and protocol keys for some routes; route devices are original
- # kernel interface names, while $ethdev can be the pinned one
- my $gateways6 = $run_env->{network}->{routes}->{gateways6} // [];
- my $is_gateway6_from_ra = grep {
- my $dev = $_->{dev} // '';
- if (defined($netif_pin_map) && defined($interfaces->{$dev})) {
- $dev = $netif_pin_map->{ $interfaces->{$dev}->{mac} } // $dev;
- }
- $dev eq $ethdev
- && ($_->{gateway} // '') eq $gateway
- && ($_->{protocol} // '') eq 'ra';
- } $gateways6->@*;
-
- if ($iso_env->{cfg}->{bridged_network}) {
- $ifaces .= "iface $ethdev $ntype manual\n";
-
- $ifaces .=
- "\nauto vmbr0\niface vmbr0 $ntype static\n"
- . "\taddress $cidr\n"
- . "\tgateway $gateway\n"
- . "\tbridge-ports $ethdev\n"
- . "\tbridge-stp off\n"
- . "\tbridge-fd 0\n";
- } else {
- $ifaces .= "auto $ethdev\n" . #
- "iface $ethdev $ntype static\n" . #
- "\taddress $cidr\n";
-
- if (!$is_gateway6_from_ra) {
- $ifaces .= "\tgateway $gateway\n";
- }
- }
-
- foreach my $iface (sort keys $interfaces->%*) {
- my $if = $interfaces->{$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";
- }
-
- $ifaces .= "\n\nsource /etc/network/interfaces.d/*\n";
-
- file_write_all("$targetdir/etc/network/interfaces", $ifaces);
-
# configure dns
my $dnsserver = Proxmox::Install::Config::get_dns();
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH installer 06/15] install: do not rely on interface name map to be fully populated
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
` (4 preceding siblings ...)
2026-09-08 10:16 ` [PATCH installer 05/15] install: factor out /etc/network/interfaces setup into own subroutine Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 07/15] install: config: add option for setting up bond on management interface Christoph Heiss
` (8 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
While this hasn't been a problem yet AFAIK, better be safe than sorry.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Proxmox/Install.pm | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/Proxmox/Install.pm b/Proxmox/Install.pm
index 45aa53b..04264a9 100644
--- a/Proxmox/Install.pm
+++ b/Proxmox/Install.pm
@@ -851,7 +851,11 @@ my sub setup_ifupdown2_config {
foreach my $iface (sort keys $interfaces->%*) {
my $if = $interfaces->{$iface};
- my $name = defined($netif_pin_map) ? $netif_pin_map->{ $if->{mac} } : $if->{name};
+ my $name =
+ defined($netif_pin_map)
+ && defined($netif_pin_map->{ $if->{mac} })
+ ? $netif_pin_map->{ $if->{mac} }
+ : $if->{name};
next if $name eq $ethdev;
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH installer 07/15] install: config: add option for setting up bond on management interface
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
` (5 preceding siblings ...)
2026-09-08 10:16 ` [PATCH installer 06/15] install: do not rely on interface name map to be fully populated Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 08/15] install: network: set up management bond if requested Christoph Heiss
` (7 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
Prerequisite for the rest of the installer to support setting up a bond
as management interface during installations.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Proxmox/Install/Config.pm | 44 +++++++++++++++++++++++++++++++++++++++
Proxmox/Sys/Net.pm | 30 ++++++++++++++++++++++++++
2 files changed, 74 insertions(+)
diff --git a/Proxmox/Install/Config.pm b/Proxmox/Install/Config.pm
index dba3692..946e642 100644
--- a/Proxmox/Install/Config.pm
+++ b/Proxmox/Install/Config.pm
@@ -103,6 +103,21 @@ my sub init_cfg {
# network related
mngmt_nic => undef,
+ # Options for creating a bond for the management interface with the given physical
+ # interfaces and mode.
+ # Enabled if `interfaces` is not empty.
+ mngmt_bond => {
+ # MAC addresses of member interfaces. If empty, bonding is disabled.
+ members => [],
+ # must be one of: 'balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 'balance-tlb', 'balance-alb'
+ mode => 'active-backup', # recommended by our docs
+ # hash policy, only used with '802.3ad' and 'balance-xor' mode
+ # must be one of 'layer2', 'layer2+3', 'layer3+4'
+ hash_policy => 'layer2',
+ # MAC address of primary bond interface, only used with 'active-backup' mode, optional
+ primary_interface => undef,
+ },
+
# maps mac address -> custom name
# if set to a hash, enables interface name pinning for all interfaces
network_interface_pin_map => undef,
@@ -257,6 +272,35 @@ sub get_subscription_key { return get('subscription_key'); }
sub set_mngmt_nic { set_key('mngmt_nic', $_[0]); }
sub get_mngmt_nic { return get('mngmt_nic'); }
+sub set_mngmt_bond_opt {
+ my ($k, $v) = @_;
+ my $opts = get('mngmt_bond');
+
+ croak "unknown management bond option key '$k'\n" if !exists($opts->{$k});
+ croak "unknown bond mode '$v'\n"
+ if $k eq 'mode' && !Proxmox::Sys::Net::is_valid_bond_mode($v);
+ croak "unknown bond hash policy '$v'\n"
+ if $k eq 'hash_policy' && !Proxmox::Sys::Net::is_valid_bond_hash_policy($v);
+
+ # allow setting no interfaces (disabling bonding), or 2+ for enabling it
+ croak "at least 2 member interfaces are required for bond\n"
+ if $k eq 'members' && scalar($v->@*) == 1;
+ croak "primary interface '$v' is not a member of the bond\n"
+ if $k eq 'primary_interface' && !grep { $_ eq $v } $opts->{members}->@*;
+
+ $opts->{$k} = $v;
+}
+
+sub get_mngmt_bond_opt {
+ my ($k) = @_;
+ my $opts = get('mngmt_bond');
+ return defined($k) ? $opts->{$k} : $opts;
+}
+
+sub get_mngmt_bond_enabled { # virtual config
+ return scalar(get_mngmt_bond_opt('members')->@*) >= 2;
+}
+
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'); }
diff --git a/Proxmox/Sys/Net.pm b/Proxmox/Sys/Net.pm
index c43311d..04bb782 100644
--- a/Proxmox/Sys/Net.pm
+++ b/Proxmox/Sys/Net.pm
@@ -15,6 +15,8 @@ our @EXPORT_OK = qw(
parse_ip_mask
parse_fqdn
validate_link_pin_map
+ is_valid_bond_mode
+ is_valid_bond_hash_policy
MIN_IFNAME_LEN
MAX_IFNAME_LEN
DEFAULT_PIN_PREFIX
@@ -450,4 +452,32 @@ sub validate_link_pin_map : prototype($) {
}
}
+sub is_valid_bond_mode {
+ my ($mode) = @_;
+
+ my $valid = {
+ 'active-backup' => 1,
+ 'balance-alb' => 1,
+ 'balance-rr' => 1,
+ 'balance-tlb' => 1,
+ 'balance-xor' => 1,
+ 'broadcast' => 1,
+ '802.3ad' => 1,
+ };
+
+ return exists($valid->{$mode});
+}
+
+sub is_valid_bond_hash_policy {
+ my ($policy) = @_;
+
+ my $valid = {
+ layer2 => 1,
+ 'layer2+3' => 1,
+ 'layer3+4' => 1,
+ };
+
+ return exists($valid->{$policy});
+}
+
1;
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH installer 08/15] install: network: set up management bond if requested
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
` (6 preceding siblings ...)
2026-09-08 10:16 ` [PATCH installer 07/15] install: config: add option for setting up bond on management interface Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 09/15] gui: network: factor out name pinning into advanced options dialog Christoph Heiss
` (6 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
Adds the necessary configuration to /etc/network/interfaces when bonding
is enabled in the installation config.
A bond named 'bond0' will be created, with the default bridge (as
before) 'vmbr0' on top of that.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Proxmox/Install.pm | 43 ++++++++++++++++++++++++++++++++++++++++---
1 file changed, 40 insertions(+), 3 deletions(-)
diff --git a/Proxmox/Install.pm b/Proxmox/Install.pm
index 04264a9..9c2b244 100644
--- a/Proxmox/Install.pm
+++ b/Proxmox/Install.pm
@@ -829,8 +829,39 @@ my sub setup_ifupdown2_config {
&& ($_->{protocol} // '') eq 'ra';
} $gateways6->@*;
+ my $bond_opts = Proxmox::Install::Config::get_mngmt_bond_opt();
+ my $bond_enabled = Proxmox::Install::Config::get_mngmt_bond_enabled();
+ if ($bond_enabled) {
+ # When setting up bridged networking, the bond is attached to the main bridge as port.
+ # Otherwise, the bond represents the main management interface, i.e. with the address
+ # configured directly on it.
+
+ $ethdev = 'bond0';
+ my $addr_method = $iso_env->{cfg}->{bridged_network} ? 'manual' : 'static';
+
+ # translate all bond members MACs to their final interface name, either pinned or not
+ my @members;
+ for my $mac ($bond_opts->{members}->@*) {
+ $mac = lc($mac); # normalize into lowercase
+
+ if (defined($netif_pin_map) && defined($netif_pin_map->{$mac})) {
+ push @members, $netif_pin_map->{$mac};
+ } else {
+ my ($if) = grep { $_->{mac} eq $mac } values %$interfaces;
+ push @members, $if->{name};
+ }
+ }
+
+ $ifaces .=
+ "auto $ethdev\niface $ethdev inet $addr_method\n"
+ . "\tbond-ports "
+ . join(' ', @members) . "\n"
+ . "\tbond-mode $bond_opts->{mode}\n"
+ . "\tbond-miimon 100\n";
+ }
+
if ($iso_env->{cfg}->{bridged_network}) {
- $ifaces .= "iface $ethdev $ntype manual\n";
+ $ifaces .= "\niface $ethdev $ntype manual\n" if !$bond_enabled;
$ifaces .=
"\nauto vmbr0\niface vmbr0 $ntype static\n"
@@ -839,11 +870,17 @@ my sub setup_ifupdown2_config {
. "\tbridge-ports $ethdev\n"
. "\tbridge-stp off\n"
. "\tbridge-fd 0\n";
- } else {
- $ifaces .= "auto $ethdev\n" . #
+ } elsif (!$bond_enabled) {
+ $ifaces .= "\nauto $ethdev\n" . #
"iface $ethdev $ntype static\n" . #
"\taddress $cidr\n";
+ if (!$is_gateway6_from_ra) {
+ $ifaces .= "\tgateway $gateway\n";
+ }
+ } else {
+ $ifaces .= "\taddress $cidr\n";
+
if (!$is_gateway6_from_ra) {
$ifaces .= "\tgateway $gateway\n";
}
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH installer 09/15] gui: network: factor out name pinning into advanced options dialog
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
` (7 preceding siblings ...)
2026-09-08 10:16 ` [PATCH installer 08/15] install: network: set up management bond if requested Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 10/15] gui: network: add bond setup options to " Christoph Heiss
` (5 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
This dialog can now be easily extended with additional frames for other,
advanced network-related options.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxinstall | 183 +++++++++++++++++++++++++++++++---------------------
1 file changed, 110 insertions(+), 73 deletions(-)
diff --git a/proxinstall b/proxinstall
index 51efe79..e3a4b20 100755
--- a/proxinstall
+++ b/proxinstall
@@ -349,34 +349,55 @@ my $create_basic_grid = sub {
return $grid;
};
-my sub create_network_interface_pin_view {
- my ($done_cb) = @_;
+# takes an array ref of rows with [$label_text, $widget, $suffix_label] array refs as columns
+# $suffix_label is optional
+my $create_label_widget_grid = sub {
+ my ($labeled_widgets) = @_;
- my $dialog = Gtk3::Dialog->new();
- $dialog->set_title('Interface Name Pinning Options');
- $dialog->add_button('_OK', 'ok');
+ my $grid = &$create_basic_grid();
- my $content = $dialog->get_content_area();
+ for (my $row = 0; $row < scalar($labeled_widgets->@*); $row++) {
+ my ($label_text, $widget, $suffix_label) = $labeled_widgets->[$row]->@*;
- my $hbox = Gtk3::Box->new('horizontal', 0);
- $content->pack_start($hbox, 1, 1, 5);
+ my $label = Gtk3::Label->new($label_text);
+ $label->set_visible(1);
+ $label->set_xalign(1.0);
+ $grid->attach($label, 0, $row, 1, 1);
+
+ $widget->set_visible(1);
+ $grid->attach($widget, 1, $row, 1, 1);
+
+ if ($suffix_label) {
+ my $suffix_label = Gtk3::Label->new($suffix_label);
+ $suffix_label->set_visible(1);
+ $suffix_label->set_xalign(1.0);
+ $grid->attach($suffix_label, 2, $row, 1, 1);
+ }
+ }
+
+ return $grid;
+};
+
+my sub create_network_interface_pin_frame {
+ my ($on_enable_changed) = @_;
+
+ my $frame = Gtk3::Frame->new('Interface Name Pinning');
+
+ my $vbox = Gtk3::Box->new('vertical', 5);
+ $frame->add($vbox);
+ $vbox->set_margin_start(10);
+ $vbox->set_margin_end(10);
+ $vbox->set_margin_top(5);
+ $vbox->set_margin_bottom(5);
+
+ my $enable_checkbox = Gtk3::CheckButton->new('Pin network interface names');
+ $enable_checkbox->set_active($gtk_state->{network_pinning_enabled});
+ $vbox->pack_start($enable_checkbox, 1, 0, 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_min_content_height(200);
- $scrolled_window->set_margin_start(10);
- $scrolled_window->set_margin_end(10);
-
- $hbox->pack_start($scrolled_window, 1, 0, 5);
+ $vbox->pack_start($grid, 1, 1, 5);
my $interfaces = Proxmox::Install::RunEnv::get()->{network}->{interfaces};
my $mapping = Proxmox::Install::Config::get_network_interface_pin_map();
@@ -412,24 +433,83 @@ my sub create_network_interface_pin_view {
$inputs->{ $iface->{mac} } = $input;
}
- $hbox->show_all();
+ my $set_sensitive = sub {
+ my ($active) = @_;
+ $grid->foreach(sub {
+ $_[0]->set_sensitive($active);
+ });
+ };
+
+ $set_sensitive->($gtk_state->{network_pinning_enabled});
+ $enable_checkbox->signal_connect(
+ toggled => sub {
+ my $active = !!$enable_checkbox->get_active();
+
+ $gtk_state->{network_pinning_enabled} = $active;
+ $set_sensitive->($active);
+
+ $on_enable_changed->($active);
+ },
+ );
+
+ my $on_done = sub {
+ if (!$enable_checkbox->get_active()) {
+ Proxmox::Install::Config::set_network_interface_pin_map();
+ return;
+ }
+
+ my %new_mapping = map { $_ => $inputs->{$_}->get_text() } keys %$inputs;
+
+ validate_link_pin_map(\%new_mapping);
+ Proxmox::Install::Config::set_network_interface_pin_map(\%new_mapping);
+ };
+
+ return ($frame, $on_done);
+}
+
+my sub create_network_advanced_options_view {
+ my ($done_cb) = @_;
+
+ my $dialog = Gtk3::Dialog->new();
+ $dialog->set_title('Management Network Options');
+ $dialog->add_button('_OK', 'ok');
+
+ my $content = $dialog->get_content_area();
+
+ my $scrolled_window = Gtk3::ScrolledWindow->new();
+ $scrolled_window->set_hexpand(1);
+ $scrolled_window->set_vexpand(1);
+ $scrolled_window->set_propagate_natural_height(1);
+ $scrolled_window->set_policy('never', 'automatic');
+ $scrolled_window->set_min_content_height(200);
+
+ my $hbox = Gtk3::Box->new('vertical', 0);
+
+ my ($pin_frame, $pin_on_done) = create_network_interface_pin_frame(sub {});
+
+ $hbox->pack_start($pin_frame, 0, 0, 10);
+
+ $scrolled_window->add($hbox);
+ $content->pack_start($scrolled_window, 1, 1, 10);
+
+ my $inputs = {};
$dialog->signal_connect(
response => sub {
- my %new_mapping = map { $_ => $inputs->{$_}->get_text() } keys %$inputs;
-
- eval { validate_link_pin_map(\%new_mapping); };
+ eval {
+ $pin_on_done->();
+ };
if ($@) {
Proxmox::UI::message($@, $dialog);
return;
}
- Proxmox::Install::Config::set_network_interface_pin_map(\%new_mapping);
$dialog->destroy();
$done_cb->();
},
);
+ $dialog->show_all();
$dialog->run();
}
@@ -522,21 +602,10 @@ sub create_ipconf_view {
$device_cb->set_active($active);
};
- 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(
+ my $advanced_opts_button = Gtk3::Button->new('Advanced Options');
+ $advanced_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->();
+ create_network_advanced_options_view($refresh_device_cb);
},
);
@@ -661,8 +730,7 @@ sub create_ipconf_view {
$grid->attach($dns_label, 0, 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);
+ $grid->attach($advanced_opts_button, 1, 5, 2, 1);
$gtk_state->{inbox}->show_all;
set_next(
@@ -730,8 +798,6 @@ 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++;
@@ -1188,35 +1254,6 @@ my $target_hd_label;
my $hdoption_first_setup = 1;
-# takes an array ref of rows with [$label_text, $widget, $suffix_label] array refs as columns
-# $suffix_label is optional
-my $create_label_widget_grid = sub {
- my ($labeled_widgets) = @_;
-
- my $grid = &$create_basic_grid();
-
- for (my $row = 0; $row < scalar($labeled_widgets->@*); $row++) {
- my ($label_text, $widget, $suffix_label) = $labeled_widgets->[$row]->@*;
-
- my $label = Gtk3::Label->new($label_text);
- $label->set_visible(1);
- $label->set_xalign(1.0);
- $grid->attach($label, 0, $row, 1, 1);
-
- $widget->set_visible(1);
- $grid->attach($widget, 1, $row, 1, 1);
-
- if ($suffix_label) {
- my $suffix_label = Gtk3::Label->new($suffix_label);
- $suffix_label->set_visible(1);
- $suffix_label->set_xalign(1.0);
- $grid->attach($suffix_label, 2, $row, 1, 1);
- }
- }
-
- return $grid;
-};
-
# only relevant for raid with its multipl diskX to diskY mappings.
my $get_selected_hdsize = sub {
my $hdsize = shift;
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH installer 10/15] gui: network: add bond setup options to advanced options dialog
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
` (8 preceding siblings ...)
2026-09-08 10:16 ` [PATCH installer 09/15] gui: network: factor out name pinning into advanced options dialog Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 11/15] auto: pass trough management network bond options to low-level installer Christoph Heiss
` (4 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
Partially fixes #2164 [0].
Adds a second frame to the advanced network options dialog, allowing to
enable bonding and setting all the required options for it; such as
members, mode, hash policy and primary bond member, as required.
[0] https://bugzilla.proxmox.com/show_bug.cgi?id=2164
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxinstall | 238 ++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 232 insertions(+), 6 deletions(-)
diff --git a/proxinstall b/proxinstall
index e3a4b20..d90d0ba 100755
--- a/proxinstall
+++ b/proxinstall
@@ -85,7 +85,9 @@ my @steps = (
);
# GUI global variables
-my $gtk_state = {};
+my $gtk_state = {
+ network_bonding_enabled => 0,
+};
my $target_hds; # only for the summary view
@@ -468,6 +470,179 @@ my sub create_network_interface_pin_frame {
return ($frame, $on_done);
}
+my sub create_network_bond_frame {
+ my $frame = Gtk3::Frame->new('Management Bond');
+
+ my $vbox = Gtk3::Box->new('vertical', 5);
+ $frame->add($vbox);
+ $vbox->set_margin_start(10);
+ $vbox->set_margin_end(10);
+ $vbox->set_margin_top(5);
+ $vbox->set_margin_bottom(5);
+
+ my $enable_checkbox = Gtk3::CheckButton->new('Enable bonding for management interface');
+ $enable_checkbox->set_active($gtk_state->{network_bonding_enabled});
+ $vbox->pack_start($enable_checkbox, 1, 0, 5);
+
+ my $bond_opts = Proxmox::Install::Config::get_mngmt_bond_opt();
+ my $interfaces = Proxmox::Install::RunEnv::get()->{network}->{interfaces};
+ my $name_mapping = Proxmox::Install::Config::get_network_interface_pin_map();
+
+ my $mode_combo = Gtk3::ComboBoxText->new();
+ my @modes = qw(active-backup balance-alb balance-rr balance-tlb balance-xor broadcast 802.3ad);
+ for my $m (@modes) {
+ $mode_combo->append_text($m);
+ }
+ for my $i (0 .. $#modes) {
+ if ($modes[$i] eq $bond_opts->{mode}) {
+ $mode_combo->set_active($i);
+ last;
+ }
+ }
+
+ my $hash_policy_combo = Gtk3::ComboBoxText->new();
+ my @hash_policies = qw(layer2 layer2+3 layer3+4);
+ for my $p (@hash_policies) {
+ $hash_policy_combo->append_text($p);
+ }
+ for my $i (0 .. $#hash_policies) {
+ if ($hash_policies[$i] eq $bond_opts->{hash_policy}) {
+ $hash_policy_combo->set_active($i);
+ last;
+ }
+ }
+
+ my $primary_if_input = Gtk3::Entry->new();
+ $primary_if_input->set_tooltip_text('Name of the primary interface to set on the bond.');
+
+ my $primary_if = $bond_opts->{primary_interface};
+ if (defined($primary_if)) {
+ $primary_if_input->set_text($name_mapping->{$primary_if} // $interfaces->{$primary_if});
+ }
+
+ my $on_mode_changed = sub {
+ my $mode = $mode_combo->get_active_text();
+ $hash_policy_combo->set_sensitive($mode eq '802.3ad' || $mode eq 'balance-xor');
+ $primary_if_input->set_sensitive($mode eq 'active-backup');
+ };
+ $mode_combo->signal_connect(changed => $on_mode_changed);
+
+ my $opts_grid = $create_label_widget_grid->([
+ ['Bond Mode', $mode_combo],
+ ['Hash Policy', $hash_policy_combo],
+ ['Primary Interface', $primary_if_input],
+ ]);
+
+ $vbox->pack_start($opts_grid, 0, 0, 5);
+
+ my $if_label = Gtk3::Label->new('Bond Interfaces:');
+ $if_label->set_xalign(0.0);
+ $if_label->set_margin_top(5);
+ $vbox->pack_start($if_label, 0, 0, 0);
+
+ my $checkboxes = {};
+ for my $iface (values %$interfaces) {
+ next if !defined($iface->{pinned_id});
+
+ my $checkbox = Gtk3::CheckButton->new('');
+
+ my $is_member = grep { $_ eq $iface->{mac} } $bond_opts->{members}->@*;
+ $checkbox->set_active($is_member);
+
+ $vbox->pack_start($checkbox, 0, 0, 5);
+ $checkboxes->{ $iface->{mac} } = $checkbox;
+ }
+
+ my $set_sensitive = sub {
+ my ($active) = @_;
+
+ $opts_grid->foreach(sub {
+ $_[0]->set_sensitive($active);
+ });
+
+ if ($active) {
+ # if activating all inputs, observe the rules applied by the mode change handler
+ $on_mode_changed->(Proxmox::Install::Config::get_mngmt_bond_opt('mode'));
+ }
+
+ $if_label->set_sensitive($active);
+ for my $cb (values %$checkboxes) {
+ $cb->set_sensitive($active);
+ }
+ };
+ $set_sensitive->($gtk_state->{network_bonding_enabled});
+
+ $enable_checkbox->signal_connect(
+ toggled => sub {
+ my $active = !!$enable_checkbox->get_active();
+
+ $gtk_state->{network_bonding_enabled} = $active;
+ $set_sensitive->($active);
+ },
+ );
+
+ my $refresh_if_names = sub {
+ my ($pinning_active) = @_;
+ my $mapping = Proxmox::Install::Config::get_network_interface_pin_map();
+
+ for my $iface (values %$interfaces) {
+ next if !defined($checkboxes->{ $iface->{mac} });
+
+ my $name =
+ $pinning_active
+ ? $name_mapping->{ $iface->{mac} }
+ : $iface->{name};
+
+ my $label = "$name ($iface->{mac}, $iface->{driver}, $iface->{state})";
+ $checkboxes->{ $iface->{mac} }->set_label($label);
+ }
+ };
+ $refresh_if_names->($gtk_state->{network_pinning_enabled});
+
+ my $on_done = sub {
+ if (!$enable_checkbox->get_active()) {
+ Proxmox::Install::Config::set_mngmt_bond_opt('members', []);
+ return;
+ }
+
+ my @members =
+ grep { $checkboxes->{$_}->get_active() } keys %$checkboxes;
+
+ if (scalar(@members) < 2) {
+ die "at least 2 member interfaces are required for setting up bond\n";
+ }
+
+ Proxmox::Install::Config::set_mngmt_nic('bond0');
+ Proxmox::Install::Config::set_mngmt_bond_opt('members', \@members);
+ Proxmox::Install::Config::set_mngmt_bond_opt('mode', $mode_combo->get_active_text());
+ Proxmox::Install::Config::set_mngmt_bond_opt(
+ 'hash_policy',
+ $hash_policy_combo->get_active_text(),
+ );
+
+ my $primary_if = $primary_if_input->get_text();
+ if (length($primary_if) > 0) {
+ # resolve the name to its corresponding MAC address
+ if ($gtk_state->{network_pinning_enabled}) {
+ my $mapping = Proxmox::Install::Config::get_network_interface_pin_map();
+ while (my ($mac, $name) = each %$mapping) {
+ if ($primary_if eq $name) {
+ $primary_if = $mac;
+ last;
+ }
+ }
+ } else {
+ my $interfaces = Proxmox::Install::RunEnv::get()->{network}->{interfaces};
+ $primary_if = $interfaces->{$primary_if};
+ }
+
+ Proxmox::Install::Config::set_mngmt_bond_opt('primary_interface', $primary_if);
+ }
+ };
+
+ return ($frame, $refresh_if_names, $on_done);
+}
+
my sub create_network_advanced_options_view {
my ($done_cb) = @_;
@@ -486,9 +661,11 @@ my sub create_network_advanced_options_view {
my $hbox = Gtk3::Box->new('vertical', 0);
- my ($pin_frame, $pin_on_done) = create_network_interface_pin_frame(sub {});
+ my ($bond_frame, $bond_refresh, $bond_on_done) = create_network_bond_frame();
+ my ($pin_frame, $pin_on_done) = create_network_interface_pin_frame($bond_refresh);
$hbox->pack_start($pin_frame, 0, 0, 10);
+ $hbox->pack_start($bond_frame, 0, 0, 10);
$scrolled_window->add($hbox);
$content->pack_start($scrolled_window, 1, 1, 10);
@@ -498,6 +675,7 @@ my sub create_network_advanced_options_view {
response => sub {
eval {
$pin_on_done->();
+ $bond_on_done->();
};
if ($@) {
Proxmox::UI::message($@, $dialog);
@@ -573,7 +751,31 @@ sub create_ipconf_view {
$device_cb->pack_start($cell, 0);
$device_cb->add_attribute($cell, 'text', 1);
+ my $device_bond_label = Gtk3::Label->new('bond0');
+ $device_bond_label->set_justify('center');
+ $device_bond_label->set_margin_top(5);
+ $device_bond_label->set_margin_bottom(5);
+
my $refresh_device_cb = sub {
+ my $bond_active = Proxmox::Install::Config::get_mngmt_bond_enabled();
+ if ($bond_active) {
+ my $bond_opts = Proxmox::Install::Config::get_mngmt_bond_opt();
+
+ $device_bond_label->set_text(
+ "bond0 (mode: $bond_opts->{mode}, "
+ . scalar($bond_opts->{members}->@*)
+ . " members)",
+ );
+
+ $device_cb->set_visible(0);
+ $device_cb->set_no_show_all(1);
+
+ return;
+ }
+
+ $device_cb->set_visible(1);
+ $device_cb->set_no_show_all(0);
+
# clear all entries and re-add them with their new names
my $active = $device_cb->get_active();
$device_model->clear();
@@ -711,6 +913,7 @@ sub create_ipconf_view {
$grid->attach($label, 0, 0, 1, 1);
$grid->attach($device_cb, 1, 0, 2, 1);
+ $grid->attach($device_bond_label, 1, 0, 2, 1);
my $fqdn = Proxmox::Install::Config::get_fqdn();
my $hostname = $run_env->{network}->{hostname} || $iso_env->{product};
@@ -834,11 +1037,34 @@ 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 $all_ifaces = Proxmox::Install::RunEnv::get('network')->{interfaces};
+ my $iface = $all_ifaces->{$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 $bond_opts = Proxmox::Install::Config::get_mngmt_bond_opt();
+
+ my $interface;
+ if (Proxmox::Install::Config::get_mngmt_bond_enabled()) {
+ my @member_names;
+
+ # translate all bond members MACs to their final interface name, either pinned or not
+ for my $mac ($bond_opts->{members}->@*) {
+ if ($gtk_state->{network_pinning_enabled}) {
+ push @member_names, $nic_mapping->{$mac};
+ } else {
+ my ($if) = grep { $_->{mac} eq $mac } values %$all_ifaces;
+ push @member_names, $if->{name};
+ }
+ }
+
+ my $members = join(' | ', @member_names);
+ $interface = "bond0 (mode: $bond_opts->{mode}, interfaces: $members)";
+ } else {
+ $interface =
+ $gtk_state->{network_pinning_enabled}
+ ? $nic_mapping->{ $iface->{mac} }
+ : $iface->{name};
+ }
my %config_values = (
__target_hd__ => join(' | ', $target_hds->@*),
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH installer 11/15] auto: pass trough management network bond options to low-level installer
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
` (9 preceding siblings ...)
2026-09-08 10:16 ` [PATCH installer 10/15] gui: network: add bond setup options to " Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 12/15] tui: drop long obsolete comment about logo formatting Christoph Heiss
` (3 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
Partially fixes #2164 [0].
The auto-installer gains a new section under [network] in its answer
file for setting up a management bond:
[network.bond]
members = ["ab:cd:ef:12:34:56", "ab:cd:ef:12:34:57"]
mode = "active-backup"
hash-policy = "layer2+3"
primary-interface = "ab:cd:ef:12:34:57"
This then just passes through the given bond options in the answer file
to the low-level installation configuration, verifying them beforehand
and printing a status message that bonding is enabled.
[0] https://bugzilla.proxmox.com/show_bug.cgi?id=2164
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Cargo.toml | 2 +
debian/control | 1 +
proxmox-auto-installer/Cargo.toml | 1 +
proxmox-auto-installer/src/utils.rs | 38 ++++++++++++++++++-
proxmox-auto-installer/tests/parse-answer.rs | 7 +++-
.../tests/resources/parse_answer/btrfs.json | 5 +++
.../btrfs_raid_level_uppercase.json | 5 +++
.../resources/parse_answer/disk_match.json | 5 +++
.../parse_answer/disk_match_all.json | 5 +++
.../parse_answer/disk_match_any.json | 5 +++
.../resources/parse_answer/first_boot.json | 5 +++
.../parse_answer/fqdn_from_dhcp.json | 5 +++
...n_from_dhcp_empty_dhcp_domain_setting.json | 5 +++
...cp_no_dhcp_domain_with_default_domain.json | 5 +++
...ll_fqdn_from_dhcp_with_default_domain.json | 5 +++
.../parse_answer/hashed_root_password.json | 5 +++
.../tests/resources/parse_answer/minimal.json | 5 +++
.../resources/parse_answer/network_bond.json | 25 ++++++++++++
.../resources/parse_answer/network_bond.toml | 20 ++++++++++
.../network_interface_pinning.json | 5 +++
...face_pinning_mixed_case_mac_addresses.json | 5 +++
.../resources/parse_answer/nic_matching.json | 5 +++
.../resources/parse_answer/no_network.json | 5 +++
.../resources/parse_answer/specific_nic.json | 5 +++
.../tests/resources/parse_answer/zfs.json | 5 +++
.../zfs_raid_level_uppercase.json | 5 +++
.../network_bond_invalid_hash_policy.json | 3 ++
.../network_bond_invalid_hash_policy.toml | 20 ++++++++++
.../network_bond_invalid_mode.json | 3 ++
.../network_bond_invalid_mode.toml | 18 +++++++++
.../network_bond_primary_not_member.json | 3 ++
.../network_bond_primary_not_member.toml | 19 ++++++++++
.../network_bond_single_interface.json | 3 ++
.../network_bond_single_interface.toml | 18 +++++++++
proxmox-installer-common/Cargo.toml | 1 +
proxmox-installer-common/src/options.rs | 38 +++++++++++++------
proxmox-installer-common/src/setup.rs | 10 ++++-
37 files changed, 310 insertions(+), 15 deletions(-)
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/network_bond.json
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/network_bond.toml
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_hash_policy.json
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_hash_policy.toml
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_mode.json
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_mode.toml
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_primary_not_member.json
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_primary_not_member.toml
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_single_interface.json
create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_single_interface.toml
diff --git a/Cargo.toml b/Cargo.toml
index 335fb58..3849ed3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,6 +27,7 @@ serde_plain = "1.0"
toml = "0.8"
proxmox-auto-installer.path = "./proxmox-auto-installer"
proxmox-installer-common.path = "./proxmox-installer-common"
+proxmox-network-api = "1.0"
proxmox-network-types = "1.1"
proxmox-installer-types = { version = "0.2", features = ["legacy"] }
@@ -34,4 +35,5 @@ proxmox-installer-types = { version = "0.2", features = ["legacy"] }
# NOTE: You must run `cargo update` after changing this for it to take effect!
[patch.crates-io]
# proxmox-network-types.path = "../proxmox/proxmox-network-types"
+# proxmox-installer-api.path = "../proxmox/proxmox-installer-api"
# proxmox-installer-types.path = "../proxmox/proxmox-installer-types"
diff --git a/debian/control b/debian/control
index a1d648f..75bbefd 100644
--- a/debian/control
+++ b/debian/control
@@ -20,6 +20,7 @@ Build-Depends: cargo:native,
librust-pico-args-0.5-dev,
librust-pretty-assertions-1.4-dev,
librust-proxmox-installer-types-0.2+legacy-dev (>= 0.1.1-~~),
+ librust-proxmox-network-api-1-dev,
librust-proxmox-network-types-1-dev (>= 1.1-~~),
librust-proxmox-sys+crypt-dev,
librust-regex-1+default-dev (>= 1.7~~),
diff --git a/proxmox-auto-installer/Cargo.toml b/proxmox-auto-installer/Cargo.toml
index 5ef2f4f..b7ca85d 100644
--- a/proxmox-auto-installer/Cargo.toml
+++ b/proxmox-auto-installer/Cargo.toml
@@ -15,6 +15,7 @@ anyhow.workspace = true
log.workspace = true
proxmox-installer-common = { workspace = true, features = ["http"] }
proxmox-network-types.workspace = true
+proxmox-network-api.workspace = true
proxmox-installer-types.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
diff --git a/proxmox-auto-installer/src/utils.rs b/proxmox-auto-installer/src/utils.rs
index 7205ae3..aa8a0fd 100644
--- a/proxmox-auto-installer/src/utils.rs
+++ b/proxmox-auto-installer/src/utils.rs
@@ -11,7 +11,7 @@ use proxmox_installer_types::{
answer::{
AutoInstallerConfig, DiskSelection, Filesystem, FilesystemOptions, FilesystemType,
FilterMatch, FirstBootHookSourceMode, FqdnConfig, FqdnFromDhcpConfig, FqdnSourceMode,
- NetworkConfig,
+ NetworkBondOptions, NetworkConfig,
},
};
@@ -48,6 +48,7 @@ fn get_network_settings(
&runtime_info.network,
None,
pinning_opts.as_ref(),
+ answer.network.bond(),
);
opts.fqdn = name.to_owned();
opts
@@ -76,6 +77,7 @@ fn get_network_settings(
&runtime_info.network,
domain.as_deref(),
pinning_opts.as_ref(),
+ answer.network.bond(),
)
}
};
@@ -92,6 +94,33 @@ fn get_network_settings(
opts.verify()?;
}
+ if let Some(opts) = &network_options.bond_opts {
+ let members = opts.members.iter().fold(String::new(), |acc, mac| {
+ // Resolve either to the pinned name, if enabled, otherwise to the kernel-assigned one.
+ let name = network_options
+ .pinning_opts
+ .as_ref()
+ .and_then(|o| o.mapping.get(mac).cloned())
+ .or_else(|| {
+ runtime_info
+ .network
+ .interfaces
+ .iter()
+ .find_map(|(_, iface)| (iface.mac == *mac).then_some(iface.name.to_owned()))
+ })
+ .unwrap_or("<unknown>".to_owned());
+
+ if acc.is_empty() {
+ name
+ } else {
+ format!("{acc}, {name}")
+ }
+ });
+
+ info!("Network interface bonding enabled on: {members}");
+ opts.verify()?;
+ }
+
info!("Network interface used is '{}'", &network_options.ifname);
Ok(network_options)
}
@@ -493,6 +522,10 @@ pub fn verify_network_settings(
}
}
+ if let Some(opts) = network.bond() {
+ opts.verify()?;
+ }
+
Ok(())
}
@@ -552,6 +585,9 @@ pub fn parse_answer(
subscription_key: answer.global.subscription_key.clone(),
mngmt_nic: network_settings.ifname,
+ mngmt_bond: network_settings
+ .bond_opts
+ .unwrap_or_else(NetworkBondOptions::disabled),
network_interface_pin_map: network_settings
.pinning_opts
.map(|o| o.mapping)
diff --git a/proxmox-auto-installer/tests/parse-answer.rs b/proxmox-auto-installer/tests/parse-answer.rs
index ae5b87d..0320bba 100644
--- a/proxmox-auto-installer/tests/parse-answer.rs
+++ b/proxmox-auto-installer/tests/parse-answer.rs
@@ -127,6 +127,7 @@ mod tests {
full_fqdn_from_dhcp_with_default_domain,
hashed_root_password,
minimal,
+ network_bond,
network_interface_pinning,
network_interface_pinning_mixed_case_mac_addresses,
nic_matching,
@@ -150,11 +151,15 @@ mod tests {
fqdn_hostname_only,
ipv4_and_subnet_mask_33,
lvm_swapsize_greater_than_hdsize,
+ network_bond_invalid_hash_policy,
+ network_bond_invalid_mode,
+ network_bond_primary_not_member,
+ network_bond_single_interface,
network_interface_pinning_overlong_interface_name,
no_fqdn_from_dhcp,
no_root_password_set,
short_password,
- zfs_raid_single_disk
+ zfs_raid_single_disk,
);
}
}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/btrfs.json b/proxmox-auto-installer/tests/resources/parse_answer/btrfs.json
index 0c1f032..ce27d41 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/btrfs.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/btrfs.json
@@ -16,6 +16,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"timezone": "Europe/Vienna",
"btrfs_opts": {
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/btrfs_raid_level_uppercase.json b/proxmox-auto-installer/tests/resources/parse_answer/btrfs_raid_level_uppercase.json
index cb6711c..cc0bbe9 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/btrfs_raid_level_uppercase.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/btrfs_raid_level_uppercase.json
@@ -16,6 +16,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"timezone": "Europe/Vienna",
"btrfs_opts": { "compress": "off" },
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/disk_match.json b/proxmox-auto-installer/tests/resources/parse_answer/disk_match.json
index d5ffddd..0c13325 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/disk_match.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/disk_match.json
@@ -18,6 +18,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"timezone": "Europe/Vienna",
"zfs_opts": {
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/disk_match_all.json b/proxmox-auto-installer/tests/resources/parse_answer/disk_match_all.json
index 78a5e0c..4f2150d 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/disk_match_all.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/disk_match_all.json
@@ -15,6 +15,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"timezone": "Europe/Vienna",
"zfs_opts": {
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/disk_match_any.json b/proxmox-auto-installer/tests/resources/parse_answer/disk_match_any.json
index 2e65fce..5adb9c6 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/disk_match_any.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/disk_match_any.json
@@ -22,6 +22,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"timezone": "Europe/Vienna",
"zfs_opts": {
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/first_boot.json b/proxmox-auto-installer/tests/resources/parse_answer/first_boot.json
index fafde51..e3012a8 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/first_boot.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/first_boot.json
@@ -12,6 +12,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"target_hd": "/dev/sda",
"timezone": "Europe/Vienna",
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/fqdn_from_dhcp.json b/proxmox-auto-installer/tests/resources/parse_answer/fqdn_from_dhcp.json
index 5ec6656..3424487 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/fqdn_from_dhcp.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/fqdn_from_dhcp.json
@@ -12,6 +12,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"target_hd": "/dev/sda",
"timezone": "Europe/Vienna",
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/fqdn_from_dhcp_empty_dhcp_domain_setting.json b/proxmox-auto-installer/tests/resources/parse_answer/fqdn_from_dhcp_empty_dhcp_domain_setting.json
index 5ec6656..3424487 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/fqdn_from_dhcp_empty_dhcp_domain_setting.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/fqdn_from_dhcp_empty_dhcp_domain_setting.json
@@ -12,6 +12,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"target_hd": "/dev/sda",
"timezone": "Europe/Vienna",
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/fqdn_from_dhcp_no_dhcp_domain_with_default_domain.json b/proxmox-auto-installer/tests/resources/parse_answer/fqdn_from_dhcp_no_dhcp_domain_with_default_domain.json
index 7ed46a2..78e32a7 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/fqdn_from_dhcp_no_dhcp_domain_with_default_domain.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/fqdn_from_dhcp_no_dhcp_domain_with_default_domain.json
@@ -12,6 +12,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"target_hd": "/dev/sda",
"timezone": "Europe/Vienna",
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/full_fqdn_from_dhcp_with_default_domain.json b/proxmox-auto-installer/tests/resources/parse_answer/full_fqdn_from_dhcp_with_default_domain.json
index 7ed46a2..78e32a7 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/full_fqdn_from_dhcp_with_default_domain.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/full_fqdn_from_dhcp_with_default_domain.json
@@ -12,6 +12,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"target_hd": "/dev/sda",
"timezone": "Europe/Vienna",
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/hashed_root_password.json b/proxmox-auto-installer/tests/resources/parse_answer/hashed_root_password.json
index 4e049bd..e3bf44f 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/hashed_root_password.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/hashed_root_password.json
@@ -12,6 +12,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": {
"hashed": "$y$j9T$VgMv8lsz/TEvzesCZU3xD.$SK.h4QW51Jr/EmjuaTz5Bt4kYiX2Iezz6omzoqVEwj9"
},
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/minimal.json b/proxmox-auto-installer/tests/resources/parse_answer/minimal.json
index 0339dbc..1262832 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/minimal.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/minimal.json
@@ -12,6 +12,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"target_hd": "/dev/sda",
"timezone": "Europe/Vienna",
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/network_bond.json b/proxmox-auto-installer/tests/resources/parse_answer/network_bond.json
new file mode 100644
index 0000000..cf0aa98
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer/network_bond.json
@@ -0,0 +1,25 @@
+{
+ "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": "bond0",
+ "mngmt_bond": {
+ "members": ["B4:2E:99:AC:AD:B4", "B4:2E:99:AC:AD:B5"],
+ "mode": "active-backup",
+ "hash-policy": "layer3+4",
+ "primary-interface": "B4:2E:99:AC:AD:B5"
+ },
+ "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_bond.toml b/proxmox-auto-installer/tests/resources/parse_answer/network_bond.toml
new file mode 100644
index 0000000..dc6b7b0
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer/network_bond.toml
@@ -0,0 +1,20 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root-password = "12345678"
+
+[network]
+source = "from-dhcp"
+
+[network.bond]
+members = ["b4:2e:99:ac:ad:b4", "b4:2e:99:ac:ad:b5"]
+mode = "active-backup"
+hash-policy = "layer3+4"
+primary-interface = "b4:2e:99:ac:ad:b5"
+
+[disk-setup]
+filesystem = "ext4"
+disk-list = ["sda"]
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
index 76723c8..42bc5a3 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning.json
@@ -12,6 +12,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "mgmt",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"network_interface_pin_map": {
"1c:34:da:5c:5e:24": "nic2",
"1c:34:da:5c:5e:25": "nic3",
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_mixed_case_mac_addresses.json b/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_mixed_case_mac_addresses.json
index 76723c8..42bc5a3 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_mixed_case_mac_addresses.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_mixed_case_mac_addresses.json
@@ -12,6 +12,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "mgmt",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"network_interface_pin_map": {
"1c:34:da:5c:5e:24": "nic2",
"1c:34:da:5c:5e:25": "nic3",
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/nic_matching.json b/proxmox-auto-installer/tests/resources/parse_answer/nic_matching.json
index 5d707c4..defcfa5 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/nic_matching.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/nic_matching.json
@@ -12,6 +12,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "enp65s0f0",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"target_hd": "/dev/sda",
"timezone": "Europe/Vienna",
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/no_network.json b/proxmox-auto-installer/tests/resources/parse_answer/no_network.json
index de5f263..1bc3567 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/no_network.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/no_network.json
@@ -12,6 +12,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"target_hd": "/dev/sda",
"timezone": "Europe/Vienna",
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/specific_nic.json b/proxmox-auto-installer/tests/resources/parse_answer/specific_nic.json
index 49240b4..bd4a459 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/specific_nic.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/specific_nic.json
@@ -12,6 +12,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "enp129s0f1np1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"target_hd": "/dev/sda",
"timezone": "Europe/Vienna",
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/zfs.json b/proxmox-auto-installer/tests/resources/parse_answer/zfs.json
index 622f6d6..149588f 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/zfs.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/zfs.json
@@ -16,6 +16,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"timezone": "Europe/Vienna",
"zfs_opts": {
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/zfs_raid_level_uppercase.json b/proxmox-auto-installer/tests/resources/parse_answer/zfs_raid_level_uppercase.json
index 46b8344..99ac240 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/zfs_raid_level_uppercase.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer/zfs_raid_level_uppercase.json
@@ -17,6 +17,11 @@
"keymap": "de",
"mailto": "mail@no.invalid",
"mngmt_nic": "eno1",
+ "mngmt_bond": {
+ "members": [],
+ "mode": "active-backup",
+ "hash-policy": "layer2"
+ },
"root_password": { "plain": "12345678" },
"timezone": "Europe/Vienna",
"zfs_opts": {
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_hash_policy.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_hash_policy.json
new file mode 100644
index 0000000..b76c9d6
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_hash_policy.json
@@ -0,0 +1,3 @@
+{
+ "parse-error": "error parsing answer.toml: unknown variant `vlan+srcmac`, expected one of `layer2`, `layer2+3`, `layer3+4`"
+}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_hash_policy.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_hash_policy.toml
new file mode 100644
index 0000000..af2789b
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_hash_policy.toml
@@ -0,0 +1,20 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root-password = "12345678"
+
+[network]
+source = "from-dhcp"
+
+[network.bond]
+members = ["b4:2e:99:ac:ad:b4", "b4:2e:99:ac:ad:b5"]
+mode = "active-backup"
+hash-policy = "vlan+srcmac"
+primary-interface = "b4:2e:99:ac:ad:b5"
+
+[disk-setup]
+filesystem = "ext4"
+disk-list = ["sda"]
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_mode.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_mode.json
new file mode 100644
index 0000000..95461dc
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_mode.json
@@ -0,0 +1,3 @@
+{
+ "parse-error": "error parsing answer.toml: unknown variant `non-existent-mode`, expected one of `balance-rr`, `active-backup`, `balance-xor`, `broadcast`, `802.3ad`, `balance-tlb`, `balance-alb`"
+}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_mode.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_mode.toml
new file mode 100644
index 0000000..79695da
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_invalid_mode.toml
@@ -0,0 +1,18 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root-password = "12345678"
+
+[network]
+source = "from-dhcp"
+
+[network.bond]
+members = ["b4:2e:99:ac:ad:b4", "b4:2e:99:ac:ad:b5"]
+mode = "non-existent-mode"
+
+[disk-setup]
+filesystem = "ext4"
+disk-list = ["sda"]
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_primary_not_member.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_primary_not_member.json
new file mode 100644
index 0000000..7e64e92
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_primary_not_member.json
@@ -0,0 +1,3 @@
+{
+ "error": "primary bond interface B4:2E:99:AC:AD:B6 must be a member of the bond (members: B4:2E:99:AC:AD:B4, B4:2E:99:AC:AD:B5)"
+}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_primary_not_member.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_primary_not_member.toml
new file mode 100644
index 0000000..fab400a
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_primary_not_member.toml
@@ -0,0 +1,19 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root-password = "12345678"
+
+[network]
+source = "from-dhcp"
+
+[network.bond]
+members = ["b4:2e:99:ac:ad:b4", "b4:2e:99:ac:ad:b5"]
+mode = "active-backup"
+primary-interface = "b4:2e:99:ac:ad:b6"
+
+[disk-setup]
+filesystem = "ext4"
+disk-list = ["sda"]
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_single_interface.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_single_interface.json
new file mode 100644
index 0000000..730fad6
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_single_interface.json
@@ -0,0 +1,3 @@
+{
+ "error": "bond setup requires at least 2 interfaces, got 1"
+}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_single_interface.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_single_interface.toml
new file mode 100644
index 0000000..b035cef
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_bond_single_interface.toml
@@ -0,0 +1,18 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root-password = "12345678"
+
+[network]
+source = "from-dhcp"
+
+[network.bond]
+members = ["b4:2e:99:ac:ad:b4"]
+mode = "balance-rr"
+
+[disk-setup]
+filesystem = "ext4"
+disk-list = ["sda"]
diff --git a/proxmox-installer-common/Cargo.toml b/proxmox-installer-common/Cargo.toml
index 7682680..a283094 100644
--- a/proxmox-installer-common/Cargo.toml
+++ b/proxmox-installer-common/Cargo.toml
@@ -13,6 +13,7 @@ regex.workspace = true
serde = { workspace = true, features = [ "derive" ] }
serde_json.workspace = true
serde_plain.workspace = true
+proxmox-network-api.workspace = true
proxmox-network-types.workspace = true
proxmox-installer-types.workspace = true
diff --git a/proxmox-installer-common/src/options.rs b/proxmox-installer-common/src/options.rs
index fbbfe77..1add63b 100644
--- a/proxmox-installer-common/src/options.rs
+++ b/proxmox-installer-common/src/options.rs
@@ -15,8 +15,8 @@ use crate::setup::{LocaleInfo, NetworkInfo, RuntimeInfo, SetupInfo};
use proxmox_installer_types::{
EMAIL_DEFAULT_PLACEHOLDER,
answer::{
- BtrfsCompressOption, BtrfsRaidLevel, FilesystemType, NetworkInterfacePinningOptionsAnswer,
- ZfsChecksumOption, ZfsCompressOption, ZfsRaidLevel,
+ BtrfsCompressOption, BtrfsRaidLevel, FilesystemType, NetworkBondOptions,
+ NetworkInterfacePinningOptionsAnswer, ZfsChecksumOption, ZfsCompressOption, ZfsRaidLevel,
},
};
use proxmox_network_types::{MacAddress, fqdn::Fqdn, ip_address::Cidr};
@@ -395,6 +395,7 @@ pub struct NetworkOptions {
pub gateway: IpAddr,
pub dns_server: IpAddr,
pub pinning_opts: Option<NetworkInterfacePinningOptions>,
+ pub bond_opts: Option<NetworkBondOptions>,
}
impl NetworkOptions {
@@ -405,6 +406,7 @@ impl NetworkOptions {
network: &NetworkInfo,
default_domain: Option<&str>,
pinning_opts: Option<&NetworkInterfacePinningOptions>,
+ bond_opts: Option<&NetworkBondOptions>,
) -> Self {
// Sets up sensible defaults as much as possible, such that even in the
// worse case nothing breaks down *completely*.
@@ -417,6 +419,7 @@ impl NetworkOptions {
gateway: Ipv4Addr::new(192, 168, 100, 1).into(),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
pinning_opts: pinning_opts.cloned(),
+ bond_opts: bond_opts.cloned(),
};
let iface = if let Some(gw) = &network.routes.gateway4
@@ -479,6 +482,10 @@ impl NetworkOptions {
}
}
+ if this.bond_opts.is_some() {
+ this.ifname = "bond0".to_owned();
+ }
+
this
}
@@ -632,7 +639,7 @@ mod tests {
let (setup, mut info) = mock_setup_network();
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, None, None),
+ NetworkOptions::defaults_from(&setup, &info, None, None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("foo.bar.com").unwrap(),
@@ -640,12 +647,13 @@ mod tests {
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
pinning_opts: None,
+ bond_opts: None,
}
);
info.hostname = None;
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, None, None),
+ NetworkOptions::defaults_from(&setup, &info, None, None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("pve.bar.com").unwrap(),
@@ -653,12 +661,13 @@ mod tests {
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
pinning_opts: None,
+ bond_opts: None,
}
);
info.dns.domain = None;
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, None, None),
+ NetworkOptions::defaults_from(&setup, &info, None, None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("pve.example.invalid").unwrap(),
@@ -666,12 +675,13 @@ mod tests {
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
pinning_opts: None,
+ bond_opts: None,
}
);
info.hostname = Some("foo".to_owned());
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, None, None),
+ NetworkOptions::defaults_from(&setup, &info, None, None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("foo.example.invalid").unwrap(),
@@ -679,6 +689,7 @@ mod tests {
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
pinning_opts: None,
+ bond_opts: None,
}
);
}
@@ -726,7 +737,7 @@ mod tests {
let (setup, info) = mock_setup_network_v6_only();
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, None, None),
+ NetworkOptions::defaults_from(&setup, &info, None, None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("foo.bar.com").unwrap(),
@@ -734,6 +745,7 @@ mod tests {
gateway: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
dns_server: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x53)),
pinning_opts: None,
+ bond_opts: None,
}
);
}
@@ -743,7 +755,7 @@ mod tests {
let (setup, mut info) = mock_setup_network();
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, None, None),
+ NetworkOptions::defaults_from(&setup, &info, None, None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("foo.bar.com").unwrap(),
@@ -751,12 +763,13 @@ mod tests {
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
pinning_opts: None,
+ bond_opts: None,
}
);
info.dns.domain = None;
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, Some("custom.local"), None),
+ NetworkOptions::defaults_from(&setup, &info, Some("custom.local"), None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("foo.custom.local").unwrap(),
@@ -764,12 +777,13 @@ mod tests {
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
pinning_opts: None,
+ bond_opts: None,
}
);
info.dns.domain = Some("some.domain.local".to_owned());
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, Some("custom.local"), None),
+ NetworkOptions::defaults_from(&setup, &info, Some("custom.local"), None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("foo.custom.local").unwrap(),
@@ -777,6 +791,7 @@ mod tests {
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
pinning_opts: None,
+ bond_opts: None,
}
);
}
@@ -813,7 +828,7 @@ mod tests {
let setup = SetupInfo::mocked();
pretty_assertions::assert_eq!(
- NetworkOptions::defaults_from(&setup, &info, None, None),
+ NetworkOptions::defaults_from(&setup, &info, None, None, None),
NetworkOptions {
ifname: "eth0".to_owned(),
fqdn: Fqdn::from("pve.example.invalid").unwrap(),
@@ -821,6 +836,7 @@ mod tests {
gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 100, 1)),
dns_server: Ipv4Addr::new(192, 168, 100, 1).into(),
pinning_opts: None,
+ bond_opts: None,
}
);
}
diff --git a/proxmox-installer-common/src/setup.rs b/proxmox-installer-common/src/setup.rs
index 14882bc..139d276 100644
--- a/proxmox-installer-common/src/setup.rs
+++ b/proxmox-installer-common/src/setup.rs
@@ -16,7 +16,10 @@ use crate::options::{
};
use proxmox_installer_types::{
BootType, IsoInfo, ProductConfig,
- answer::{BtrfsCompressOption, FilesystemType, ZfsChecksumOption, ZfsCompressOption},
+ answer::{
+ BtrfsCompressOption, FilesystemType, NetworkBondOptions, ZfsChecksumOption,
+ ZfsCompressOption,
+ },
};
use proxmox_network_types::{Cidr, MacAddress};
@@ -326,7 +329,8 @@ pub struct NetworkInfo {
pub dns: Dns,
pub routes: Routes,
- /// Maps devices to their configuration, if it has a usable configuration.
+ /// Maps devices to their configuration, if it has a usable configuration, keyed by their
+ /// kernel-assigned interface name.
/// (Contains no entries for devices with only link-local addresses.)
#[serde(default)]
pub interfaces: BTreeMap<String, Interface>,
@@ -530,6 +534,8 @@ pub struct InstallConfig {
pub subscription_key: Option<String>,
pub mngmt_nic: String,
+ pub mngmt_bond: NetworkBondOptions,
+
// Maps MAC addresses -> custom name. If set, enables pinning for all
// interfaces present.
#[serde(
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH installer 12/15] tui: drop long obsolete comment about logo formatting
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
` (10 preceding siblings ...)
2026-09-08 10:16 ` [PATCH installer 11/15] auto: pass trough management network bond options to low-level installer Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 13/15] tui: views: add widget for displaying a group of checkboxes Christoph Heiss
` (2 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
This was made obsolete by the changes in
7b0596e ("tui: drop long obsolete comment about logo formatting")
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-tui-installer/src/main.rs | 1 -
1 file changed, 1 deletion(-)
diff --git a/proxmox-tui-installer/src/main.rs b/proxmox-tui-installer/src/main.rs
index e9f47a9..61a06f7 100644
--- a/proxmox-tui-installer/src/main.rs
+++ b/proxmox-tui-installer/src/main.rs
@@ -36,7 +36,6 @@ use views::{
TableViewItem, TimezoneOptionsView,
};
-// TextView::center() seems to garble the first two lines, so fix it manually here.
const PROXMOX_LOGO: &str = r"
____
| _ \ _ __ _____ ___ __ ___ _____ __
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH installer 13/15] tui: views: add widget for displaying a group of checkboxes
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
` (11 preceding siblings ...)
2026-09-08 10:16 ` [PATCH installer 12/15] tui: drop long obsolete comment about logo formatting Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 14/15] tui: network: factor out name pinning into advanced options dialog Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 15/15] tui: network: add bond setup options to advanced network dialog Christoph Heiss
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
Adds a simple view which displays multiple checkboxes, each optionally
associated with some user-defined data.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-tui-installer/src/views/mod.rs | 106 ++++++++++++++++++++++++-
1 file changed, 105 insertions(+), 1 deletion(-)
diff --git a/proxmox-tui-installer/src/views/mod.rs b/proxmox-tui-installer/src/views/mod.rs
index a343e60..b4896e2 100644
--- a/proxmox-tui-installer/src/views/mod.rs
+++ b/proxmox-tui-installer/src/views/mod.rs
@@ -5,7 +5,9 @@ use cursive::{
event::{Event, EventResult},
theme::BaseColor,
view::{Resizable, ViewWrapper},
- views::{EditView, LinearLayout, NamedView, ResizedView, SelectView, TextView},
+ views::{
+ Checkbox, DummyView, EditView, LinearLayout, NamedView, ResizedView, SelectView, TextView,
+ },
};
mod bootdisk;
@@ -626,3 +628,105 @@ impl CidrAddressEditView {
impl ViewWrapper for CidrAddressEditView {
cursive::wrap_impl!(self.view: LinearLayout);
}
+
+/// List of checkboxes, each with an associated label and optional user-defined
+/// data for identifying its meaning.
+pub struct CheckboxGroup<UDT = ()> {
+ view: LinearLayout,
+ user_data: HashMap<usize, UDT>,
+}
+
+impl<UDT> CheckboxGroup<UDT> {
+ pub fn new() -> Self {
+ Self {
+ view: LinearLayout::vertical(),
+ user_data: HashMap::new(),
+ }
+ }
+
+ pub fn add(&mut self, label: &str, checked: bool, data: UDT) {
+ self.user_data.insert(self.view.len(), data);
+ self.view.add_child(
+ LinearLayout::horizontal()
+ .child(Checkbox::new().with_checked(checked))
+ .child(DummyView)
+ .child(TextView::new(label)),
+ );
+ }
+
+ pub fn entries(&self) -> impl Iterator<Item = (bool, &UDT)> {
+ CheckboxGroupIterator {
+ group: self,
+ index: 0,
+ }
+ }
+
+ pub fn set_enabled(&mut self, enabled: bool) {
+ for i in 0..self.view.len() {
+ if let Some(checkbox) = self
+ .view
+ .get_child_mut(i)
+ .and_then(|v| v.downcast_mut::<LinearLayout>())
+ .and_then(|v| v.get_child_mut(0))
+ .and_then(|v| v.downcast_mut::<Checkbox>())
+ {
+ checkbox.set_enabled(enabled);
+ }
+ }
+ }
+
+ pub fn update_labels(&mut self, callback: &dyn Fn(bool, &mut TextView, &mut UDT)) {
+ for i in 0..self.view.len() {
+ if let Some(inner) = self
+ .view
+ .get_child_mut(i)
+ .and_then(|v| v.downcast_mut::<LinearLayout>())
+ {
+ let checked = inner
+ .get_child(0)
+ .and_then(|v| v.downcast_ref::<Checkbox>())
+ .map(|v| v.is_checked())
+ .unwrap_or(false);
+
+ let label = inner
+ .get_child_mut(2)
+ .and_then(|v| v.downcast_mut::<TextView>());
+
+ let data = self.user_data.get_mut(&i);
+
+ if let (Some(label), Some(data)) = (label, data) {
+ callback(checked, label, data);
+ }
+ }
+ }
+ }
+}
+
+impl<UDT: Send + Sync + 'static> ViewWrapper for CheckboxGroup<UDT> {
+ cursive::wrap_impl!(self.view: LinearLayout);
+}
+
+pub struct CheckboxGroupIterator<'a, UDT> {
+ group: &'a CheckboxGroup<UDT>,
+ index: usize,
+}
+
+impl<'a, UDT> Iterator for CheckboxGroupIterator<'a, UDT> {
+ type Item = (bool, &'a UDT);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let checked = self
+ .group
+ .view
+ .get_child(self.index)?
+ .downcast_ref::<LinearLayout>()?
+ .get_child(0)?
+ .downcast_ref::<Checkbox>()?
+ .is_checked();
+
+ let data = self.group.user_data.get(&self.index)?;
+
+ self.index += 1;
+ Some((checked, data))
+ }
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH installer 14/15] tui: network: factor out name pinning into advanced options dialog
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
` (12 preceding siblings ...)
2026-09-08 10:16 ` [PATCH installer 13/15] tui: views: add widget for displaying a group of checkboxes Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 15/15] tui: network: add bond setup options to advanced network dialog Christoph Heiss
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
This dialog can now be easily extended with additional panels for other,
advanced network-related options.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-tui-installer/src/views/network.rs | 272 +++++++++++----------
1 file changed, 138 insertions(+), 134 deletions(-)
diff --git a/proxmox-tui-installer/src/views/network.rs b/proxmox-tui-installer/src/views/network.rs
index 6401174..f040881 100644
--- a/proxmox-tui-installer/src/views/network.rs
+++ b/proxmox-tui-installer/src/views/network.rs
@@ -1,8 +1,9 @@
+use anyhow::{Result, anyhow, bail};
use cursive::{
Cursive, View,
view::{Nameable, Resizable, ViewWrapper},
views::{
- Button, Checkbox, Dialog, DummyView, EditView, LinearLayout, NamedView, ResizedView,
+ Button, Checkbox, Dialog, DummyView, EditView, LinearLayout, NamedView, Panel, ResizedView,
ScrollView, SelectView, TextView,
},
};
@@ -22,10 +23,10 @@ use proxmox_network_types::{MacAddress, fqdn::Fqdn, ip_address::Cidr};
use super::{CidrAddressEditView, FormView};
struct NetworkViewOptions {
- selected_mac: MacAddress,
pinning_enabled: bool,
- // For UI purposes, we want to always save the mapping, to save the state
- // between toggling the checkbox
+ interfaces: Vec<Interface>,
+ // For UI purposes, we want to always save the state for the below options, even if not enabled
+ // by the user - to preserve the state between toggling the checkbox
pinning_options: NetworkInterfacePinningOptions,
}
@@ -40,32 +41,29 @@ pub struct NetworkOptionsView {
}
impl NetworkOptionsView {
- const PINNING_OPTIONS_BUTTON_NAME: &str = "network-pinning-options-button";
+ const FORM_VIEW_NAME: &str = "network-management-form";
const MGMT_IFNAME_SELECTVIEW_NAME: &str = "network-management-ifname-selectview";
+ const ADVANCED_OPTIONS_BUTTON_NAME: &str = "network-pinning-options-button";
+
+ const INTERFACE_PINNING_VIEW_NAME: &str = "network-name-pinning-view";
pub fn new(options: &NetworkOptions, network_info: &NetworkInfo) -> Self {
- let mut ifaces = network_info
+ let mut interfaces = network_info
.interfaces
.values()
- .collect::<Vec<&Interface>>();
+ .cloned()
+ .collect::<Vec<Interface>>();
// First, sort interfaces by their link state and then name
- ifaces.sort_unstable_by_key(|x| (&x.state, &x.name));
-
- let selected_mac = network_info
- .interfaces
- .get(&options.ifname)
- .map(|iface| iface.mac)
- .unwrap_or_else(|| ifaces.first().expect("at least one network interface").mac);
+ interfaces.sort_unstable_by(|a, b| (&a.state, &a.name).cmp(&(&b.state, &b.name)));
let options_ref = Arc::new(Mutex::new(NetworkViewOptions {
- selected_mac,
pinning_enabled: options.pinning_opts.is_some(),
+ interfaces,
pinning_options: options.pinning_opts.clone().unwrap_or_default(),
}));
- let iface_selection =
- Self::build_mgmt_ifname_selectview(ifaces.clone(), options_ref.clone());
+ let iface_selection = Self::build_mgmt_ifname_selectview(options_ref.clone(), None);
let form = FormView::<()>::new()
.child(
@@ -89,36 +87,13 @@ impl NetworkOptionsView {
EditView::new().content(options.dns_server.to_string()),
);
- 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())
+ let advanced_options = LinearLayout::horizontal()
.child(DummyView.full_width())
.child(
- Button::new("Pinning options", {
+ Button::new("Advanced 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());
+ let mut view = Self::advanced_options_view(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.
@@ -128,13 +103,13 @@ impl NetworkOptionsView {
siv.add_layer(view);
}
})
- .with_name(Self::PINNING_OPTIONS_BUTTON_NAME),
+ .with_name(Self::ADVANCED_OPTIONS_BUTTON_NAME),
);
let view = LinearLayout::vertical()
- .child(form)
+ .child(form.with_name(Self::FORM_VIEW_NAME))
.child(DummyView.full_width())
- .child(pinning_checkbox);
+ .child(advanced_options);
Self {
view,
@@ -145,13 +120,10 @@ impl NetworkOptionsView {
pub fn get_values(&mut self) -> Result<NetworkOptions, String> {
let form = self
.view
- .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")?;
+ .get_child_mut(0)
+ .and_then(|v| v.downcast_mut::<NamedView<FormView>>())
+ .ok_or("failed to retrieve network options form")?
+ .get_mut();
let fqdn = form
.get_value::<EditView, _>(1)
@@ -208,83 +180,69 @@ impl NetworkOptionsView {
}
}
- fn custom_name_mapping_view(
- network_info: &NetworkInfo,
- options_ref: NetworkViewOptionsRef,
- ) -> impl View {
- const DIALOG_NAME: &str = "network-interface-name-pinning-dialog";
+ fn advanced_options_view(options_ref: NetworkViewOptionsRef) -> impl View {
+ let inner = ScrollView::new(
+ LinearLayout::vertical().child(Panel::new(
+ InterfacePinningOptionsView::new(options_ref.clone())
+ .with_name(Self::INTERFACE_PINNING_VIEW_NAME),
+ )),
+ );
- 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;
+ Dialog::around(inner)
+ .title("Advanced Network Options")
+ .button("Ok", {
+ move |siv| {
+ match Self::get_view_values(siv) {
+ Ok(pin_opts) => {
+ siv.pop_layer();
+ let options = &mut options_ref.lock().expect("unpoisoned lock");
+ options.pinning_options = pin_opts;
+ }
+ Err(err) => {
+ siv.pop_layer();
+ siv.add_layer(Dialog::info(err.to_string()));
+ 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))
+ Self::refresh_ifname_selectview(siv, options_ref.clone());
+ }
+ })
+ .max_size((80, 40))
}
- fn refresh_ifname_selectview(
- siv: &mut Cursive,
- ifaces: &[Interface],
- options_ref: NetworkViewOptionsRef,
- ) {
+ fn get_view_values(siv: &mut Cursive) -> Result<NetworkInterfacePinningOptions> {
+ let pin_opts = siv
+ .call_on_name(
+ Self::INTERFACE_PINNING_VIEW_NAME,
+ InterfacePinningOptionsView::get_values,
+ )
+ .unwrap_or_else(|| {
+ bail!("failed to retrieve network interface name pinning options")
+ })?;
+
+ Ok(pin_opts)
+ }
+
+ fn refresh_ifname_selectview(siv: &mut Cursive, 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);
+ let selected_mac = view.selection().map(|iface| iface.mac);
+ *view = Self::build_mgmt_ifname_selectview(options_ref, selected_mac);
},
);
}
fn build_mgmt_ifname_selectview(
- ifaces: Vec<&Interface>,
options_ref: NetworkViewOptionsRef,
+ selected_mac: Option<MacAddress>,
) -> 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
+ let ifnames = options
+ .interfaces
.iter()
.map(|iface| {
if options.pinning_enabled
@@ -297,20 +255,20 @@ impl NetworkOptionsView {
})
.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;
- }
- });
+ let mut view = SelectView::new().popup().with_all(ifnames.clone());
+
+ let selected_mac = selected_mac.unwrap_or_else(|| {
+ options
+ .interfaces
+ .first()
+ .expect("at least one network interface")
+ .mac
+ });
// Finally, (try to) select the current one
let selected = view
.iter()
- .position(|(_label, iface)| iface.mac == options.selected_mac)
+ .position(|(_label, iface)| iface.mac == selected_mac)
.unwrap_or(0); // we sort UP interfaces first, so select the first UP interface
//
view.set_selection(selected);
@@ -324,19 +282,44 @@ impl ViewWrapper for NetworkOptionsView {
}
struct InterfacePinningOptionsView {
- view: ScrollView<NamedView<FormView<MacAddress>>>,
+ view: LinearLayout,
}
impl InterfacePinningOptionsView {
+ const ENABLED_CHECKBOX_NAME: &str = "network-interface-name-pinning-enabled";
const FORM_NAME: &str = "network-interface-name-pinning-form";
- fn new(interfaces: &[&Interface], options_ref: NetworkViewOptionsRef) -> Self {
+ fn new(options_ref: NetworkViewOptionsRef) -> Self {
let options = options_ref.lock().expect("unpoisoned lock");
// Filter out all non-physical links, as it does not make sense to pin their names
// in this way.
// The low-level installer will skip them anyway.
- let interfaces = interfaces.iter().filter(|iface| iface.pinned_id.is_some());
+ let interfaces = options
+ .interfaces
+ .iter()
+ .filter(|iface| iface.pinned_id.is_some());
+
+ let mut view = LinearLayout::vertical()
+ .child(
+ LinearLayout::horizontal()
+ .child(
+ Checkbox::new()
+ .with_checked(options.pinning_enabled)
+ .on_change({
+ let options_ref = options_ref.clone();
+ move |siv, enabled| {
+ options_ref.lock().expect("unpoisoned lock").pinning_enabled =
+ enabled;
+
+ Self::toggle_all(siv, enabled);
+ }
+ })
+ .with_name(Self::ENABLED_CHECKBOX_NAME),
+ )
+ .child(TextView::new(" Pin network interface names").no_wrap()),
+ )
+ .child(DummyView::new());
let mut form = FormView::<MacAddress>::new();
@@ -356,7 +339,7 @@ impl InterfacePinningOptionsView {
.expect("always pinnable interface")
.name,
)
- .max_content_width(MAX_IFNAME_LEN)
+ .with_enabled(options.pinning_enabled)
.fixed_width(MAX_IFNAME_LEN),
);
@@ -375,13 +358,31 @@ impl InterfacePinningOptionsView {
}
}
- Self {
- view: ScrollView::new(form.with_name(Self::FORM_NAME)),
- }
+ view.add_child(form.with_name(Self::FORM_NAME));
+
+ Self { view }
}
- fn get_values(&mut self) -> Result<NetworkInterfacePinningOptions, String> {
- let form = self.view.get_inner_mut().get_mut();
+ fn toggle_all(siv: &mut Cursive, enabled: bool) {
+ siv.call_on_name(Self::FORM_NAME, |v: &mut FormView<MacAddress>| {
+ v.call_on_childs(&|v: &mut LinearLayout| {
+ if let Some(v) = v
+ .get_child_mut(1)
+ .and_then(|v| v.downcast_mut::<ResizedView<EditView>>())
+ {
+ v.get_inner_mut().set_enabled(enabled);
+ }
+ });
+ });
+ }
+
+ fn get_values(&mut self) -> Result<NetworkInterfacePinningOptions> {
+ let form = self
+ .view
+ .get_child_mut(2)
+ .and_then(|v| v.downcast_mut::<NamedView<FormView<MacAddress>>>())
+ .ok_or(anyhow!("failed to retrieve interface pinning form"))?
+ .get_mut();
let mut mapping = HashMap::new();
@@ -395,18 +396,21 @@ impl InterfacePinningOptionsView {
.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))?;
+ .ok_or(anyhow!(
+ "failed to retrieve pinning ID for interface {}",
+ mac
+ ))?;
mapping.insert(*mac, (*name).clone());
}
let opts = NetworkInterfacePinningOptions { mapping };
- opts.verify().map_err(|err| err.to_string())?;
+ opts.verify()?;
Ok(opts)
}
}
impl ViewWrapper for InterfacePinningOptionsView {
- cursive::wrap_impl!(self.view: ScrollView<NamedView<FormView<MacAddress>>>);
+ cursive::wrap_impl!(self.view: LinearLayout);
}
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH installer 15/15] tui: network: add bond setup options to advanced network dialog
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
` (13 preceding siblings ...)
2026-09-08 10:16 ` [PATCH installer 14/15] tui: network: factor out name pinning into advanced options dialog Christoph Heiss
@ 2026-09-08 10:16 ` Christoph Heiss
14 siblings, 0 replies; 16+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:16 UTC (permalink / raw)
To: pve-devel
Partially fixes #2164 [0].
Adds a second panel to the advanced network options dialog, allowing to
enable bonding and setting all the required options for it; such as
members, mode, hash policy and primary bond member, as required.
[0] https://bugzilla.proxmox.com/show_bug.cgi?id=2164
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-tui-installer/Cargo.toml | 1 +
proxmox-tui-installer/src/main.rs | 7 +-
proxmox-tui-installer/src/options.rs | 45 ++-
proxmox-tui-installer/src/setup.rs | 5 +
proxmox-tui-installer/src/views/network.rs | 415 ++++++++++++++++++---
5 files changed, 424 insertions(+), 49 deletions(-)
diff --git a/proxmox-tui-installer/Cargo.toml b/proxmox-tui-installer/Cargo.toml
index 56395a4..cb950b3 100644
--- a/proxmox-tui-installer/Cargo.toml
+++ b/proxmox-tui-installer/Cargo.toml
@@ -9,6 +9,7 @@ homepage = "https://www.proxmox.com"
[dependencies]
proxmox-installer-common.workspace = true
+proxmox-network-api.workspace = true
proxmox-network-types.workspace = true
proxmox-installer-types.workspace = true
anyhow.workspace = true
diff --git a/proxmox-tui-installer/src/main.rs b/proxmox-tui-installer/src/main.rs
index 61a06f7..6e7c691 100644
--- a/proxmox-tui-installer/src/main.rs
+++ b/proxmox-tui-installer/src/main.rs
@@ -177,6 +177,7 @@ fn main() {
None,
// We enable network interface pinning by default in the TUI
Some(&NetworkInterfacePinningOptions::default()),
+ None,
),
autoreboot: true,
},
@@ -578,7 +579,11 @@ fn summary_dialog(siv: &mut Cursive) -> InstallerView {
("name".to_owned(), "Option".to_owned()),
("value".to_owned(), "Selected value".to_owned()),
])
- .items(state.options.to_summary(&state.locales)),
+ .items(
+ state
+ .options
+ .to_summary(&state.runtime_info, &state.locales),
+ ),
))
.child(
LinearLayout::horizontal()
diff --git a/proxmox-tui-installer/src/options.rs b/proxmox-tui-installer/src/options.rs
index 2c156e8..8b650f1 100644
--- a/proxmox-tui-installer/src/options.rs
+++ b/proxmox-tui-installer/src/options.rs
@@ -2,9 +2,9 @@ use crate::SummaryOption;
use proxmox_installer_common::{
options::{BootdiskOptions, NetworkOptions, TimezoneOptions},
- setup::LocaleInfo,
+ setup::{LocaleInfo, NetworkInfo, RuntimeInfo},
};
-use proxmox_installer_types::EMAIL_DEFAULT_PLACEHOLDER;
+use proxmox_installer_types::{EMAIL_DEFAULT_PLACEHOLDER, answer::NetworkBondOptions};
#[derive(Clone)]
pub struct PasswordOptions {
@@ -31,13 +31,27 @@ pub struct InstallerOptions {
}
impl InstallerOptions {
- pub fn to_summary(&self, locales: &LocaleInfo) -> Vec<SummaryOption> {
+ pub fn to_summary(
+ &self,
+ runtime_info: &RuntimeInfo,
+ locales: &LocaleInfo,
+ ) -> Vec<SummaryOption> {
let kb_layout = locales
.kmap
.get(&self.timezone.kb_layout)
.map(|l| &l.name)
.unwrap_or(&self.timezone.kb_layout);
+ let mngmt_interface = if let Some(opts) = &self.network.bond_opts {
+ let members = self
+ .resolve_bond_member_names(opts, &runtime_info.network)
+ .join(" | ");
+
+ format!("bond0 (mode: {}, members: {members})", opts.mode)
+ } else {
+ self.network.ifname.to_owned()
+ };
+
vec![
SummaryOption::new("Bootdisk filesystem", self.bootdisk.fstype.to_string()),
SummaryOption::new(
@@ -52,11 +66,34 @@ impl InstallerOptions {
SummaryOption::new("Timezone", &self.timezone.timezone),
SummaryOption::new("Keyboard layout", kb_layout),
SummaryOption::new("Administrator email", &self.password.email),
- SummaryOption::new("Management interface", &self.network.ifname),
+ SummaryOption::new("Management interface", &mngmt_interface),
SummaryOption::new("Hostname", self.network.fqdn.to_string()),
SummaryOption::new("Host IP (CIDR)", self.network.address.to_string()),
SummaryOption::new("Gateway", self.network.gateway.to_string()),
SummaryOption::new("DNS", self.network.dns_server.to_string()),
]
}
+
+ fn resolve_bond_member_names(
+ &self,
+ bond: &NetworkBondOptions,
+ network: &NetworkInfo,
+ ) -> Vec<String> {
+ let pin_opts = self.network.pinning_opts.as_ref();
+
+ bond.members
+ .iter()
+ .map(|mac| {
+ // Resolve either to the pinned name, if enabled, otherwise to the kernel-assigned one.
+ pin_opts
+ .and_then(|o| o.mapping.get(mac).cloned())
+ .or_else(|| {
+ network.interfaces.iter().find_map(|(_, iface)| {
+ (iface.mac == *mac).then_some(iface.name.to_owned())
+ })
+ })
+ .unwrap_or("<unknown>".to_owned())
+ })
+ .collect()
+ }
}
diff --git a/proxmox-tui-installer/src/setup.rs b/proxmox-tui-installer/src/setup.rs
index ae4b717..fb9a305 100644
--- a/proxmox-tui-installer/src/setup.rs
+++ b/proxmox-tui-installer/src/setup.rs
@@ -5,6 +5,7 @@ use proxmox_installer_common::{
options::AdvancedBootdiskOptions,
setup::{InstallConfig, InstallFirstBootSetup, InstallRootPassword},
};
+use proxmox_installer_types::answer::NetworkBondOptions;
impl From<InstallerOptions> for InstallConfig {
fn from(options: InstallerOptions) -> Self {
@@ -35,6 +36,10 @@ impl From<InstallerOptions> for InstallConfig {
subscription_key: None,
mngmt_nic: options.network.ifname,
+ mngmt_bond: options
+ .network
+ .bond_opts
+ .unwrap_or_else(NetworkBondOptions::disabled),
network_interface_pin_map: pinning_opts.map(|o| o.mapping.clone()).unwrap_or_default(),
hostname: options.network.fqdn.host().to_owned(),
diff --git a/proxmox-tui-installer/src/views/network.rs b/proxmox-tui-installer/src/views/network.rs
index f040881..f1b677d 100644
--- a/proxmox-tui-installer/src/views/network.rs
+++ b/proxmox-tui-installer/src/views/network.rs
@@ -18,16 +18,24 @@ use proxmox_installer_common::{
options::{NetworkInterfacePinningOptions, NetworkOptions},
setup::{Interface, NetworkInfo},
};
+use proxmox_installer_types::answer::NetworkBondOptions;
+use proxmox_network_api::{BondXmitHashPolicy, LinuxBondMode};
use proxmox_network_types::{MacAddress, fqdn::Fqdn, ip_address::Cidr};
-use super::{CidrAddressEditView, FormView};
+use super::{CheckboxGroup, CidrAddressEditView, FormView};
struct NetworkViewOptions {
pinning_enabled: bool,
+ bond_enabled: bool,
interfaces: Vec<Interface>,
+ /// Last selected interface (by its MAC address) in the [SelectView].
+ /// Keeps track of the selected interface across enabling/disabling bonding, which replaces the
+ /// view.
+ last_selected_iface: MacAddress,
// For UI purposes, we want to always save the state for the below options, even if not enabled
// by the user - to preserve the state between toggling the checkbox
pinning_options: NetworkInterfacePinningOptions,
+ bond_options: NetworkBondOptions,
}
/// Convenience wrapper when needing to take a (interior-mutable) reference to
@@ -46,6 +54,7 @@ impl NetworkOptionsView {
const ADVANCED_OPTIONS_BUTTON_NAME: &str = "network-pinning-options-button";
const INTERFACE_PINNING_VIEW_NAME: &str = "network-name-pinning-view";
+ const BOND_OPTIONS_VIEW_NAME: &str = "network-bond-options-view";
pub fn new(options: &NetworkOptions, network_info: &NetworkInfo) -> Self {
let mut interfaces = network_info
@@ -57,13 +66,30 @@ impl NetworkOptionsView {
// First, sort interfaces by their link state and then name
interfaces.sort_unstable_by(|a, b| (&a.state, &a.name).cmp(&(&b.state, &b.name)));
+ let selected_mac = network_info
+ .interfaces
+ .get(&options.ifname)
+ .map(|iface| iface.mac)
+ .unwrap_or_else(|| {
+ interfaces
+ .first()
+ .expect("at least one network interface")
+ .mac
+ });
+
let options_ref = Arc::new(Mutex::new(NetworkViewOptions {
pinning_enabled: options.pinning_opts.is_some(),
+ bond_enabled: options.bond_opts.is_some(),
interfaces,
+ last_selected_iface: selected_mac,
pinning_options: options.pinning_opts.clone().unwrap_or_default(),
+ bond_options: options
+ .bond_opts
+ .clone()
+ .unwrap_or_else(NetworkBondOptions::disabled),
}));
- let iface_selection = Self::build_mgmt_ifname_selectview(options_ref.clone(), None);
+ let iface_selection = Self::build_mgmt_ifname_selectview(options_ref.clone());
let form = FormView::<()>::new()
.child(
@@ -148,18 +174,26 @@ impl NetworkOptionsView {
.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 options = self.options.lock().map_err(|err| err.to_string())?;
- let ifname = if let Some(opts) = &pinning_opts
- && let Some(pinned) = iface.to_pinned(opts)
- {
- pinned.name
+ let pinning_opts = options
+ .pinning_enabled
+ .then_some(options.pinning_options.clone());
+
+ let bond_opts = options.bond_enabled.then_some(options.bond_options.clone());
+
+ let ifname = if options.bond_enabled {
+ "bond0".to_owned()
} else {
- iface.name
+ let iface = form
+ .get_value::<NamedView<SelectView<Interface>>, _>(0)
+ .ok_or("failed to retrieve management interface name")?;
+
+ pinning_opts
+ .as_ref()
+ .and_then(|opts| iface.to_pinned(opts))
+ .unwrap_or(iface)
+ .name
};
if address.address().is_ipv4() != gateway.is_ipv4() {
@@ -176,42 +210,51 @@ impl NetworkOptionsView {
gateway,
dns_server,
pinning_opts,
+ bond_opts,
})
}
}
fn advanced_options_view(options_ref: NetworkViewOptionsRef) -> impl View {
let inner = ScrollView::new(
- LinearLayout::vertical().child(Panel::new(
- InterfacePinningOptionsView::new(options_ref.clone())
- .with_name(Self::INTERFACE_PINNING_VIEW_NAME),
- )),
+ LinearLayout::vertical()
+ .child(Panel::new(
+ InterfacePinningOptionsView::new(options_ref.clone())
+ .with_name(Self::INTERFACE_PINNING_VIEW_NAME),
+ ))
+ .child(Panel::new(
+ BondOptionsView::new(options_ref.clone())
+ .with_name(Self::BOND_OPTIONS_VIEW_NAME),
+ )),
);
Dialog::around(inner)
.title("Advanced Network Options")
.button("Ok", {
move |siv| {
- match Self::get_view_values(siv) {
- Ok(pin_opts) => {
+ match Self::get_view_values(siv, options_ref.clone()) {
+ Ok((pin_opts, bond_opts)) => {
siv.pop_layer();
let options = &mut options_ref.lock().expect("unpoisoned lock");
options.pinning_options = pin_opts;
+ options.bond_options = bond_opts;
}
Err(err) => {
- siv.pop_layer();
siv.add_layer(Dialog::info(err.to_string()));
return;
}
}
- Self::refresh_ifname_selectview(siv, options_ref.clone());
+ Self::refresh_ifname_view(siv, options_ref.clone());
}
})
.max_size((80, 40))
}
- fn get_view_values(siv: &mut Cursive) -> Result<NetworkInterfacePinningOptions> {
+ fn get_view_values(
+ siv: &mut Cursive,
+ options_ref: NetworkViewOptionsRef,
+ ) -> Result<(NetworkInterfacePinningOptions, NetworkBondOptions)> {
let pin_opts = siv
.call_on_name(
Self::INTERFACE_PINNING_VIEW_NAME,
@@ -221,23 +264,42 @@ impl NetworkOptionsView {
bail!("failed to retrieve network interface name pinning options")
})?;
- Ok(pin_opts)
+ let bond_opts = siv
+ .call_on_name(Self::BOND_OPTIONS_VIEW_NAME, BondOptionsView::get_values)
+ .unwrap_or_else(|| bail!("failed to retrieve management network bond options"))?;
+
+ if options_ref
+ .lock()
+ .map(|opts| opts.bond_enabled)
+ .unwrap_or(false)
+ {
+ bond_opts.verify()?;
+ }
+
+ Ok((pin_opts, bond_opts))
}
- fn refresh_ifname_selectview(siv: &mut Cursive, options_ref: NetworkViewOptionsRef) {
- siv.call_on_name(
- Self::MGMT_IFNAME_SELECTVIEW_NAME,
- |view: &mut SelectView<Interface>| {
- let selected_mac = view.selection().map(|iface| iface.mac);
- *view = Self::build_mgmt_ifname_selectview(options_ref, selected_mac);
- },
- );
+ fn refresh_ifname_view(siv: &mut Cursive, options_ref: NetworkViewOptionsRef) {
+ siv.call_on_name(Self::FORM_VIEW_NAME, |form: &mut FormView<()>| {
+ let options = options_ref.lock().expect("unpoisoned lock");
+
+ if options.bond_enabled {
+ form.replace_child(
+ 0,
+ TextView::new(format!(
+ "bond0 (mode: {}, {} members)",
+ options.bond_options.mode,
+ options.bond_options.members.len()
+ )),
+ );
+ } else {
+ drop(options);
+ form.replace_child(0, Self::build_mgmt_ifname_selectview(options_ref.clone()));
+ }
+ });
}
- fn build_mgmt_ifname_selectview(
- options_ref: NetworkViewOptionsRef,
- selected_mac: Option<MacAddress>,
- ) -> SelectView<Interface> {
+ fn build_mgmt_ifname_selectview(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
@@ -255,20 +317,21 @@ impl NetworkOptionsView {
})
.collect::<Vec<(String, Interface)>>();
- let mut view = SelectView::new().popup().with_all(ifnames.clone());
-
- let selected_mac = selected_mac.unwrap_or_else(|| {
- options
- .interfaces
- .first()
- .expect("at least one network interface")
- .mac
- });
+ let mut view = SelectView::new()
+ .popup()
+ .with_all(ifnames.clone())
+ .on_submit({
+ let options_ref = options_ref.clone();
+ move |_, iface| {
+ let mut options = options_ref.lock().expect("unpoisoned lock");
+ options.last_selected_iface = iface.mac;
+ }
+ });
// Finally, (try to) select the current one
let selected = view
.iter()
- .position(|(_label, iface)| iface.mac == selected_mac)
+ .position(|(_label, iface)| iface.mac == options.last_selected_iface)
.unwrap_or(0); // we sort UP interfaces first, so select the first UP interface
//
view.set_selection(selected);
@@ -313,6 +376,7 @@ impl InterfacePinningOptionsView {
enabled;
Self::toggle_all(siv, enabled);
+ BondOptionsView::refresh_ifnames(siv, options_ref.clone());
}
})
.with_name(Self::ENABLED_CHECKBOX_NAME),
@@ -414,3 +478,266 @@ impl InterfacePinningOptionsView {
impl ViewWrapper for InterfacePinningOptionsView {
cursive::wrap_impl!(self.view: LinearLayout);
}
+
+struct BondOptionsView {
+ view: LinearLayout,
+ options_ref: NetworkViewOptionsRef,
+}
+
+impl BondOptionsView {
+ const FORM_NAME: &str = "network-bond-form-view";
+ const MEMBERS_VIEW_NAME: &str = "network-bond-members-checkboxes-view";
+
+ fn new(options_ref: NetworkViewOptionsRef) -> Self {
+ let options = &options_ref.lock().expect("unpoisoned lock");
+
+ let mut view = LinearLayout::vertical()
+ .child(
+ LinearLayout::horizontal()
+ .child(
+ Checkbox::new()
+ .with_checked(options.bond_enabled)
+ .on_change({
+ let options_ref = options_ref.clone();
+ move |siv, enabled| {
+ options_ref.lock().expect("unpoisoned lock").bond_enabled =
+ enabled;
+
+ Self::toggle_all(siv, enabled);
+ }
+ }),
+ )
+ .child(TextView::new(" Enable bonding for management interface").no_wrap()),
+ )
+ .child(DummyView::new())
+ .child(
+ FormView::<()>::new()
+ .child(
+ "Bond Mode",
+ SelectView::new()
+ .popup()
+ .with_all(
+ NetworkBondOptions::MODES
+ .iter()
+ .map(|m| (m.to_string(), *m)),
+ )
+ .selected(
+ NetworkBondOptions::MODES
+ .iter()
+ .position(|m| *m == options.bond_options.mode)
+ .unwrap_or_default(),
+ )
+ .on_submit(Self::toggle_mode_dependent_options)
+ .with_enabled(options.bond_enabled),
+ )
+ .child(
+ "Hash Policy",
+ SelectView::new()
+ .popup()
+ .with_all(
+ NetworkBondOptions::HASH_POLICIES
+ .iter()
+ .map(|m| (m.to_string(), *m)),
+ )
+ .selected(
+ NetworkBondOptions::HASH_POLICIES
+ .iter()
+ .position(|m| *m == options.bond_options.hash_policy)
+ .unwrap_or_default(),
+ )
+ .with_enabled(
+ options.bond_enabled
+ && Self::hash_policy_view_enabled(options.bond_options.mode),
+ ),
+ )
+ .child(
+ "Primary interface",
+ EditView::new()
+ .content({
+ options
+ .bond_options
+ .primary_interface
+ .and_then(|mac| {
+ options.interfaces.iter().find(|iface| iface.mac == mac)
+ })
+ .and_then(|iface| iface.to_pinned(&options.pinning_options))
+ .map(|iface| iface.name)
+ .unwrap_or_default()
+ })
+ .with_enabled(
+ options.bond_enabled
+ && Self::primary_interface_view_enable(
+ options.bond_options.mode,
+ ),
+ ),
+ )
+ .with_name(Self::FORM_NAME),
+ )
+ .child(DummyView::new());
+
+ let mut checkboxes = CheckboxGroup::<MacAddress>::new();
+ for iface in options
+ .interfaces
+ .iter()
+ .filter(|iface| iface.pinned_id.is_some())
+ {
+ let label = Self::format_interface_label(options, iface);
+ let is_member = options.bond_options.members.contains(&iface.mac);
+
+ checkboxes.add(&label, is_member, iface.mac);
+ }
+ checkboxes.set_enabled(options.bond_enabled);
+ view.add_child(checkboxes.with_name(Self::MEMBERS_VIEW_NAME));
+
+ Self {
+ view,
+ options_ref: options_ref.clone(),
+ }
+ }
+
+ fn hash_policy_view_enabled(mode: LinuxBondMode) -> bool {
+ matches!(mode, LinuxBondMode::Ieee802_3ad | LinuxBondMode::BalanceXor)
+ }
+
+ fn primary_interface_view_enable(mode: LinuxBondMode) -> bool {
+ mode == LinuxBondMode::ActiveBackup
+ }
+
+ fn toggle_all(siv: &mut Cursive, enabled: bool) {
+ siv.call_on_name(Self::FORM_NAME, |form: &mut FormView| {
+ if let Some(v) = form.get_child_mut::<SelectView<LinuxBondMode>>(0) {
+ v.set_enabled(enabled);
+ }
+ if let Some(v) = form.get_child_mut::<SelectView<BondXmitHashPolicy>>(1) {
+ v.set_enabled(enabled);
+ }
+ if let Some(v) = form.get_child_mut::<EditView>(2) {
+ v.set_enabled(enabled);
+ }
+ });
+
+ siv.call_on_name(
+ Self::MEMBERS_VIEW_NAME,
+ |group: &mut CheckboxGroup<MacAddress>| {
+ group.set_enabled(enabled);
+ },
+ );
+ }
+
+ fn toggle_mode_dependent_options(siv: &mut Cursive, selected: &LinuxBondMode) {
+ siv.call_on_name(Self::FORM_NAME, |form: &mut FormView| {
+ if let Some(v) = form.get_child_mut::<SelectView<BondXmitHashPolicy>>(1) {
+ v.set_enabled(Self::hash_policy_view_enabled(*selected));
+ }
+ if let Some(v) = form.get_child_mut::<EditView>(2) {
+ v.set_enabled(Self::primary_interface_view_enable(*selected));
+ }
+ });
+ }
+
+ fn refresh_ifnames(siv: &mut Cursive, options_ref: NetworkViewOptionsRef) {
+ siv.call_on_name(
+ Self::MEMBERS_VIEW_NAME,
+ |group: &mut CheckboxGroup<MacAddress>| {
+ let options = options_ref.lock().expect("unpoisoned lock");
+ group.update_labels(&|_, label, mac| {
+ if let Some(iface) = options.interfaces.iter().find(|iface| iface.mac == *mac) {
+ label.set_content(Self::format_interface_label(&options, iface));
+ }
+ });
+ },
+ );
+ }
+
+ fn get_values(&mut self) -> Result<NetworkBondOptions> {
+ let form = self
+ .view
+ .get_child_mut(2)
+ .and_then(|v| v.downcast_mut::<NamedView<FormView<()>>>())
+ .ok_or(anyhow!("failed to retrieve bond options form"))?
+ .get_mut();
+
+ let mode = form
+ .get_value::<SelectView<LinuxBondMode>, _>(0)
+ .ok_or(anyhow!("failed to retrieve bond mode"))?;
+
+ let hash_policy = form
+ .get_value::<SelectView<BondXmitHashPolicy>, _>(1)
+ .ok_or(anyhow!("failed to retrieve bond hash policy"))?;
+
+ let primary_interface = form
+ .get_value::<EditView, String>(2)
+ // translate the interface name back to the mac address, which is used as identifier
+ .ok_or(anyhow!("failed to retrieve bond primary interface"))
+ .and_then(|name| {
+ if name.is_empty() {
+ Ok(None)
+ } else {
+ Ok(Some(self.get_mac_address_for_interface_name(&name)?))
+ }
+ })?;
+
+ let members = self
+ .view
+ .get_child_mut(4)
+ .and_then(|v| v.downcast_mut::<NamedView<CheckboxGroup<MacAddress>>>())
+ .ok_or(anyhow!("failed to retrieve bond member interface group"))?
+ .get_mut()
+ .entries()
+ .filter_map(|(checked, mac)| checked.then_some(*mac))
+ .collect();
+
+ Ok(NetworkBondOptions {
+ members,
+ mode,
+ hash_policy,
+ primary_interface,
+ })
+ }
+
+ fn format_interface_label(options: &NetworkViewOptions, iface: &Interface) -> String {
+ let name = if options.pinning_enabled {
+ iface
+ .to_pinned(&options.pinning_options)
+ .map(|p| p.name)
+ .unwrap_or_else(|| iface.name.clone())
+ } else {
+ iface.name.clone()
+ };
+
+ let mut label = format!("{name} ({}, {}, {})", iface.mac, iface.driver, iface.state);
+
+ if !iface.addresses.is_empty() {
+ label += "\n ";
+ label += iface
+ .addresses
+ .iter()
+ .fold(String::new(), |acc, addr| format!("{acc}, {addr}"))
+ .trim_start_matches(", ");
+ }
+
+ label
+ }
+
+ fn get_mac_address_for_interface_name(&self, ifname: &str) -> Result<MacAddress> {
+ let options = &self.options_ref.lock().expect("unpoisoned lock");
+
+ if options.pinning_enabled {
+ options
+ .pinning_options
+ .mapping
+ .iter()
+ .find_map(|(mac, name)| (*name == ifname).then_some(*mac))
+ } else {
+ options
+ .interfaces
+ .iter()
+ .find_map(|iface| (iface.name == ifname).then_some(iface.mac))
+ }
+ .ok_or_else(|| anyhow!("interface '{ifname}' not found"))
+ }
+}
+
+impl ViewWrapper for BondOptionsView {
+ cursive::wrap_impl!(self.view: LinearLayout);
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
end of thread, other threads:[~2026-09-08 10:19 UTC | newest]
Thread overview: 16+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
2026-09-08 10:16 ` [PATCH proxmox 01/15] installer-types: use `MacAddress` type where applicable Christoph Heiss
2026-09-08 10:16 ` [PATCH proxmox 02/15] installer-types: answer: add options for configuring management bond Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 03/15] install: run make tidy Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 04/15] tree-wide: use `MacAddress` type instead of string for MAC addresses Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 05/15] install: factor out /etc/network/interfaces setup into own subroutine Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 06/15] install: do not rely on interface name map to be fully populated Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 07/15] install: config: add option for setting up bond on management interface Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 08/15] install: network: set up management bond if requested Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 09/15] gui: network: factor out name pinning into advanced options dialog Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 10/15] gui: network: add bond setup options to " Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 11/15] auto: pass trough management network bond options to low-level installer Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 12/15] tui: drop long obsolete comment about logo formatting Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 13/15] tui: views: add widget for displaying a group of checkboxes Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 14/15] tui: network: factor out name pinning into advanced options dialog Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 15/15] tui: network: add bond setup options to advanced network dialog Christoph Heiss
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox