all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Christoph Heiss <c.heiss@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH installer 11/15] auto: pass trough management network bond options to low-level installer
Date: Tue,  8 Sep 2026 12:16:25 +0200	[thread overview]
Message-ID: <20260908101647.1057780-12-c.heiss@proxmox.com> (raw)
In-Reply-To: <20260908101647.1057780-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>
---
 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





  parent reply	other threads:[~2026-09-08 10:18 UTC|newest]

Thread overview: 16+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 ` Christoph Heiss [this message]
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

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=20260908101647.1057780-12-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 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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal