From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 971881FF0B4 for ; Tue, 08 Sep 2026 12:18:56 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id CCD132163C; Tue, 08 Sep 2026 12:18:19 +0200 (CEST) From: Christoph Heiss 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 Message-ID: <20260908101647.1057780-12-c.heiss@proxmox.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260908101647.1057780-1-c.heiss@proxmox.com> References: <20260908101647.1057780-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: 1788862667595 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.354 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: RXLPCBAONOQ2XDYPXMUJ6ZMXYCSKSBQK X-Message-ID-Hash: RXLPCBAONOQ2XDYPXMUJ6ZMXYCSKSBQK 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 --- 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("".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, + 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..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, @@ -530,6 +534,8 @@ pub struct InstallConfig { pub subscription_key: Option, 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