From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id A89B21FF09C for ; Mon, 02 Nov 2026 13:49:26 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 4FB33218B5; Mon, 02 Nov 2026 13:48:37 +0100 (CET) From: Christoph Heiss 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 Message-ID: <20261102124539.1008075-21-c.heiss@proxmox.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20261102124539.1008075-1-c.heiss@proxmox.com> References: <20261102124539.1008075-1-c.heiss@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793623645748 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.192 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: QZ34V6M22HOKCCGFFVNKSEMFUAUHEKW2 X-Message-ID-Hash: QZ34V6M22HOKCCGFFVNKSEMFUAUHEKW2 X-MailFrom: c.heiss@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 --- 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("".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> { + 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 { + 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, + pub bond_opts: Option, } 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, @@ -488,6 +493,39 @@ pub fn spawn_low_level_installer(test_mode: bool) -> io::Result .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, + /// 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, +} + +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, 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