public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Christoph Heiss <c.heiss@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH installer v4 20/30] auto: pass through management network bond options to low-level installer
Date: Mon,  2 Nov 2026 13:45:26 +0100	[thread overview]
Message-ID: <20261102124539.1008075-21-c.heiss@proxmox.com> (raw)
In-Reply-To: <20261102124539.1008075-1-c.heiss@proxmox.com>

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>
---
Changes v3 -> v4:
  * skip serializing disabled bond options to low-level config
  * adapt to updated proxmox-installer-types, add support for
    `members-filter`

Changes v2 -> v3:
  * fixed typo in subject
  * fixed patch section in Cargo.toml

Changes v1 -> v2:
  * no changes

 Cargo.toml                                    |   2 +
 debian/control                                |   1 +
 proxmox-auto-installer/Cargo.toml             |   1 +
 proxmox-auto-installer/src/utils.rs           | 101 +++++++++++++++---
 proxmox-auto-installer/tests/parse-answer.rs  |   7 +-
 .../resources/parse_answer/network_bond.json  |  25 +++++
 .../resources/parse_answer/network_bond.toml  |  20 ++++
 .../network_bond_with_filter.json             |  24 +++++
 .../network_bond_with_filter.toml             |  22 ++++
 .../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         |  45 +++++++-
 20 files changed, 346 insertions(+), 28 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/network_bond_with_filter.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/network_bond_with_filter.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 747608e..75b5628 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,11 +27,13 @@ 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.3", features = ["legacy"] }
 
 # Local path overrides
 # NOTE: You must run `cargo update` after changing this for it to take effect!
 [patch.crates-io]
+# proxmox-network-api.path = "../proxmox/proxmox-network-api"
 # proxmox-network-types.path = "../proxmox/proxmox-network-types"
 # proxmox-installer-types.path = "../proxmox/proxmox-installer-types"
diff --git a/debian/control b/debian/control
index e860819..0f09c02 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.3+legacy-dev,
+               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 9d27086..b9b3a8d 100644
--- a/proxmox-auto-installer/src/utils.rs
+++ b/proxmox-auto-installer/src/utils.rs
@@ -1,31 +1,30 @@
 use anyhow::{Context, Result, bail};
 use glob::Pattern;
 use log::{info, warn};
+use serde::{Deserialize, Serialize};
 use std::{
     collections::{BTreeMap, HashSet},
     process::Command,
 };
 
-use proxmox_installer_types::{
-    UdevInfo,
-    answer::{
-        AutoInstallerConfig, DiskSelection, Filesystem, FilesystemOptions, FilesystemType,
-        FilterMatch, FirstBootHookSourceMode, FqdnConfig, FqdnFromDhcpConfig, FqdnSourceMode,
-        NetworkConfig,
-    },
-};
-
 use proxmox_installer_common::{
     ROOT_PASSWORD_MIN_LENGTH,
     disk_checks::check_swapsize,
     options::{FilesystemDiskInfo, NetworkInterfacePinningOptions, NetworkOptions, email_validate},
     setup::{
-        InstallBtrfsOption, InstallConfig, InstallFirstBootSetup, InstallRootPassword,
-        InstallZfsOption, LocaleInfo, RuntimeInfo, SetupInfo,
+        InstallBtrfsOption, InstallConfig, InstallFirstBootSetup, InstallNetworkBondOptions,
+        InstallRootPassword, InstallZfsOption, LocaleInfo, RuntimeInfo, SetupInfo,
     },
 };
-
-use serde::{Deserialize, Serialize};
+use proxmox_installer_types::{
+    UdevInfo,
+    answer::{
+        AutoInstallerConfig, DiskSelection, Filesystem, FilesystemOptions, FilesystemType,
+        FilterMatch, FirstBootHookSourceMode, FqdnConfig, FqdnFromDhcpConfig, FqdnSourceMode,
+        NetworkBondMemberSelection, NetworkBondOptions, NetworkConfig,
+    },
+};
+use proxmox_network_types::MacAddress;
 
 fn get_network_settings(
     answer: &AutoInstallerConfig,
@@ -48,6 +47,7 @@ fn get_network_settings(
                 &runtime_info.network,
                 None,
                 pinning_opts.as_ref(),
+                answer.network.bond(),
             );
             opts.fqdn = name.to_owned();
             opts
@@ -76,6 +76,7 @@ fn get_network_settings(
                 &runtime_info.network,
                 domain.as_deref(),
                 pinning_opts.as_ref(),
+                answer.network.bond(),
             )
         }
     };
@@ -92,6 +93,37 @@ fn get_network_settings(
         opts.verify()?;
     }
 
+    if let Some(opts) = &network_options.bond_opts {
+        let members = match_selected_network_bond_members(opts, udev_info, runtime_info)?
+            .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)
 }
@@ -192,7 +224,7 @@ pub fn get_matched_udev_indexes(
         let mut did_match_all = true;
         for (filter_key, filter_value) in filter {
             let filter_pattern =
-                Pattern::new(filter_value).context("invalid glob in disk selection")?;
+                Pattern::new(filter_value).context("invalid glob in filter selection")?;
             for (udev_key, udev_value) in dev_values {
                 if udev_key == filter_key && filter_pattern.matches(udev_value) {
                     did_match_once = true;
@@ -328,6 +360,32 @@ fn set_selected_disks(
     Ok(())
 }
 
+fn match_selected_network_bond_members(
+    bond: &NetworkBondOptions,
+    udev_info: &UdevInfo,
+    runtime_info: &RuntimeInfo,
+) -> Result<Vec<MacAddress>> {
+    match bond.members()? {
+        None => Ok(Vec::new()),
+        Some(NetworkBondMemberSelection::Fixed(list)) => Ok(list),
+        Some(NetworkBondMemberSelection::Filter(filter)) => {
+            let filter_match = bond.members_filter_match.unwrap_or_default();
+            // list of kernel-assigned names
+            let selected = get_matched_udev_indexes(
+                &filter,
+                &udev_info.nics,
+                filter_match == FilterMatch::All,
+            )?
+            .into_iter()
+            .filter_map(|ifname| runtime_info.network.interfaces.get(&ifname))
+            .map(|iface| iface.mac)
+            .collect();
+
+            Ok(selected)
+        }
+    }
+}
+
 fn get_first_selected_disk(config: &InstallConfig) -> usize {
     config
         .disk_selection
@@ -493,6 +551,10 @@ pub fn verify_network_settings(
         }
     }
 
+    if let Some(opts) = network.bond() {
+        opts.verify()?;
+    }
+
     Ok(())
 }
 
@@ -552,6 +614,17 @@ pub fn parse_answer(
         subscription_key: answer.global.subscription_key.clone(),
 
         mngmt_nic: network_settings.ifname,
+        mngmt_bond: network_settings
+            .bond_opts
+            .map(|bond| -> Result<InstallNetworkBondOptions> {
+                Ok(InstallNetworkBondOptions {
+                    members: match_selected_network_bond_members(&bond, udev_info, runtime_info)?,
+                    mode: bond.mode,
+                    hash_policy: bond.hash_policy,
+                    primary_interface: bond.primary_interface,
+                })
+            })
+            .unwrap_or_else(|| Ok(InstallNetworkBondOptions::default()))?,
         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/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..31d748c
--- /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-list = ["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_bond_with_filter.json b/proxmox-auto-installer/tests/resources/parse_answer/network_bond_with_filter.json
new file mode 100644
index 0000000..91a4723
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer/network_bond_with_filter.json
@@ -0,0 +1,24 @@
+{
+  "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": "layer2"
+  },
+  "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_with_filter.toml b/proxmox-auto-installer/tests/resources/parse_answer/network_bond_with_filter.toml
new file mode 100644
index 0000000..a1af3a1
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer/network_bond_with_filter.toml
@@ -0,0 +1,22 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root-password = "12345678"
+
+[network]
+source = "from-dhcp"
+
+[network.bond]
+# matches `eno1` and `eno2`
+members-filter.ID_NET_DRIVER = "igb"
+members-filter.ID_PATH = "pci-0000:c2:00.*"
+members-filter-match = "all"
+mode = "active-backup"
+hash-policy = "layer2"
+
+[disk-setup]
+filesystem = "ext4"
+disk-list = ["sda"]
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..f285828
--- /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-list = ["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..7ec6204
--- /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-list = ["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..31a1195 100644
--- a/proxmox-installer-common/src/setup.rs
+++ b/proxmox-installer-common/src/setup.rs
@@ -16,8 +16,12 @@ use crate::options::{
 };
 use proxmox_installer_types::{
     BootType, IsoInfo, ProductConfig,
-    answer::{BtrfsCompressOption, FilesystemType, ZfsChecksumOption, ZfsCompressOption},
+    answer::{
+        BtrfsCompressOption, FilesystemType, ZfsChecksumOption,
+        ZfsCompressOption,
+    },
 };
+use proxmox_network_api::{BondXmitHashPolicy, LinuxBondMode};
 use proxmox_network_types::{Cidr, MacAddress};
 
 /// Paths in the ISO environment containing installer data.
@@ -326,7 +330,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>,
@@ -488,6 +493,39 @@ pub fn spawn_low_level_installer(test_mode: bool) -> io::Result<process::Child>
         .spawn()
 }
 
+#[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 InstallNetworkBondOptions {
+    /// 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.
+    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 Default for InstallNetworkBondOptions {
+    fn default() -> Self {
+        Self {
+            members: Vec::new(),
+            mode: LinuxBondMode::ActiveBackup,
+            hash_policy: BondXmitHashPolicy::Layer2,
+            primary_interface: None,
+        }
+    }
+}
+
+impl InstallNetworkBondOptions {
+    fn is_disabled(&self) -> bool {
+        self.members.is_empty()
+    }
+}
+
 /// See Proxmox::Install::Config
 #[derive(Debug, Deserialize, Serialize)]
 pub struct InstallConfig {
@@ -530,6 +568,9 @@ pub struct InstallConfig {
     pub subscription_key: Option<String>,
 
     pub mngmt_nic: String,
+    #[serde(skip_serializing_if = "InstallNetworkBondOptions::is_disabled")]
+    pub mngmt_bond: InstallNetworkBondOptions,
+
     // Maps MAC addresses -> custom name. If set, enables pinning for all
     // interfaces present.
     #[serde(
-- 
2.55.0





  parent reply	other threads:[~2026-11-02 12:49 UTC|newest]

Thread overview: 32+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-02 12:45 [PATCH installer/proxmox v4 00/30] fix #2164: add bond setup across all installers Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 01/30] installer-types: clippy: use .is_multiple_of() instead of manual modulo Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 02/30] installer-types: use `MacAddress` type where applicable Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 03/30] installer-types: derive Display for RebootMode and FilterMatch Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 04/30] installer-types: answer: add options for configuring management bond Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 05/30] installer-types: answer: add options for configuring management vlan Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 06/30] installer-types: bump fetch schema for bond and VLAN answer options Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 07/30] installer-types: bump version to 0.3.0-1 Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 08/30] network-api: derive Display and FromStr for network bond types Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 09/30] install: run make tidy Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 10/30] tree-wide: use `MacAddress` type instead of string for MAC addresses Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 11/30] install: factor out ifupdown2 config generation into own subroutine Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 12/30] install: do not rely on interface name map to be fully populated Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 13/30] install: config: add option for setting up bond on management interface Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 14/30] install: config: add option for setting up vlan " Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 15/30] install: network: set up management bond if requested Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 16/30] install: network: set up vlan on management interface " Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 17/30] gui: network: factor out name pinning into advanced options dialog Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 18/30] gui: network: add bond setup options to " Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 19/30] gui: network: add vlan setup options to advanced network dialog Christoph Heiss
2026-11-02 12:45 ` Christoph Heiss [this message]
2026-11-02 12:45 ` [PATCH installer v4 21/30] auto: pass through management network vlan options to low-level installer Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 22/30] tui: drop long obsolete comment about logo formatting Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 23/30] tui: views: add widget for displaying a group of checkboxes Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 24/30] tui: network: factor out name pinning into advanced options dialog Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 25/30] tui: network: add bond setup options to advanced network dialog Christoph Heiss
2026-11-02 14:55   ` Lukas Sichert
2026-11-02 12:45 ` [PATCH installer v4 26/30] tui: network: add vlan " Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 27/30] gui: network: split advanced options dialog into tabs Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 28/30] tui: " Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 29/30] gui: network: list bond member candidates sharing a MAC address once Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 30/30] tui: " Christoph Heiss

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261102124539.1008075-21-c.heiss@proxmox.com \
    --to=c.heiss@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal