* [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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.