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 3342E1FF09C for ; Mon, 02 Nov 2026 13:50:40 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 4ACFC2184F; Mon, 02 Nov 2026 13:50:37 +0100 (CET) From: Christoph Heiss To: pdm-devel@lists.proxmox.com Subject: [PATCH datacenter-manager 1/5] config: auto-install: add new options for configuring vlan and bond Date: Mon, 2 Nov 2026 13:49:33 +0100 Message-ID: <20261102124939.1023342-2-c.heiss@proxmox.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20261102124939.1023342-1-c.heiss@proxmox.com> References: <20261102124939.1023342-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: 1793623794916 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.179 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: N4QYNDFVTK4T3ERVUIAP3TSRMWEHXMKF X-Message-ID-Hash: N4QYNDFVTK4T3ERVUIAP3TSRMWEHXMKF 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 Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: This mostly mirror the the options available from `proxmox_installer_types::NetworkVlanOptions` and `proxmox_installer_types::NetworkBondOptions`. For bonding, an additional, explicit on/off setting is required to properly keep UI state, and only udev filters are supported, not fixed member lists. Signed-off-by: Christoph Heiss --- Cargo.toml | 2 +- lib/pdm-config/Cargo.toml | 1 + lib/pdm-config/src/auto_install.rs | 138 +++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d9e7dece..b8450a1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,7 @@ proxmox-tfa = { version = "6", features = [ "api-types" ], default-features = fa proxmox-time = "2" proxmox-upgrade-checks = "1.1.1" proxmox-uuid = "1" -proxmox-installer-types = "0.2" +proxmox-installer-types = "0.3" proxmox-network-types = "1.1" # other proxmox crates diff --git a/lib/pdm-config/Cargo.toml b/lib/pdm-config/Cargo.toml index 8c1b46da..af077a62 100644 --- a/lib/pdm-config/Cargo.toml +++ b/lib/pdm-config/Cargo.toml @@ -26,6 +26,7 @@ proxmox-sys = { workspace = true, features = [ "acl", "crypt", "timer" ] } proxmox-acme-api.workspace = true proxmox-serde.workspace = true proxmox-installer-types = { workspace = true, features = [ "api-types" ] } +proxmox-network-api.workspace = true proxmox-network-types.workspace = true pdm-api-types.workspace = true pdm-buildcfg.workspace = true diff --git a/lib/pdm-config/src/auto_install.rs b/lib/pdm-config/src/auto_install.rs index 8ac48fb6..ccb9eaa6 100644 --- a/lib/pdm-config/src/auto_install.rs +++ b/lib/pdm-config/src/auto_install.rs @@ -38,6 +38,7 @@ pub mod types { }, }; use proxmox_installer_types::answer::SUBSCRIPTION_KEY_SCHEMA; + use proxmox_network_api::{BondXmitHashPolicy, LinuxBondMode}; use proxmox_schema::{ApiStringFormat, ApiType, PropertyString, api}; #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] @@ -124,6 +125,19 @@ pub mod types { type: String, optional: true, }, + "bond-enabled": { + optional: true, + }, + "bond-members-filter": { + type: String, + optional: true, + }, + "vlan-id": { + type: Integer, + optional: true, + minimum: 2, + maximum: 4094, + }, filesystem: { type: String, }, @@ -256,6 +270,26 @@ pub mod types { /// Whether to enable network interface name pinning. pub netif_name_pinning_enabled: bool, + /// Whether to enable interface bonding for the management network. + #[serde(default)] + pub bond_enabled: bool, + /// Netdev filter for selecting management network bond members + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub bond_members_filter: PropertyString>, + /// Whether it is enough that any filter matches on a network interface or all given + /// filters must match to select an interface as bond member. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bond_members_filter_match: Option, + /// Mode of the management interface bond. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bond_mode: Option, + /// Hash policy of the management interface bond. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bond_hash_policy: Option, + /// VLAN tag to configure on the management interface. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub vlan_id: Option, + /// Root filesystem options. pub filesystem: PropertyString, @@ -319,6 +353,12 @@ pub mod types { dns: conf.dns, netdev_filter: PropertyString::new(BTreeMapWrapper(conf.netdev_filter)), netif_name_pinning_enabled: conf.netif_name_pinning_enabled, + bond_enabled: conf.bond_enabled, + bond_members_filter: PropertyString::new(BTreeMapWrapper(conf.bond_members_filter)), + bond_members_filter_match: conf.bond_members_filter_match, + bond_mode: conf.bond_mode, + bond_hash_policy: conf.bond_hash_policy, + vlan_id: conf.vlan_id, // disk options filesystem: PropertyString::new(conf.filesystem), disk_mode: conf.disk_mode, @@ -372,6 +412,12 @@ pub mod types { dns: self.dns, netdev_filter: self.netdev_filter.into_inner().0, netif_name_pinning_enabled: self.netif_name_pinning_enabled, + bond_enabled: self.bond_enabled, + bond_members_filter: self.bond_members_filter.into_inner().0, + bond_members_filter_match: self.bond_members_filter_match, + bond_mode: self.bond_mode, + bond_hash_policy: self.bond_hash_policy, + vlan_id: self.vlan_id, // disk options filesystem: self.filesystem.into_inner(), disk_mode: self.disk_mode, @@ -660,3 +706,95 @@ fn write_token_shadow(data: HashMap) -> Result<()> { let raw = serde_json::to_string(&data)?; replace_config(TOKENS_SHADOW_FILE, raw.as_bytes()) } + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::types::*; + use super::*; + use pdm_api_types::auto_installer::PreparedInstallationConfig; + + fn prepared_config(extra: serde_json::Value) -> PreparedInstallationConfig { + let mut value = json!({ + "id": "test", + "is-default": true, + "country": "at", + "fqdn": "test.example.com", + "use-dhcp-fqdn": false, + "keyboard": "en-us", + "mailto": "root@example.com", + "timezone": "UTC", + "reboot-on-error": false, + "reboot-mode": "reboot", + "use-dhcp-network": false, + "cidr": "192.168.100.2/24", + "gateway": "192.168.100.1", + "dns": "192.168.100.254", + "netif-name-pinning-enabled": false, + "filesystem": { "filesystem": "ext4" }, + "disk-mode": "fixed", + }); + + for (key, v) in extra.as_object().unwrap() { + value + .as_object_mut() + .unwrap() + .insert(key.clone(), v.clone()); + } + + serde_json::from_value(value).unwrap() + } + + fn round_trip(conf: PreparedInstallationConfig) -> PreparedInstallationConfig { + let mut data: SectionConfigData = + HashMap::new().into(); + data.insert(conf.id.clone(), conf.clone().try_into().unwrap()); + + let raw = PreparedInstallationSectionConfigWrapper::write_section_config( + PREPARED_CONF_FILE, + &data, + ) + .unwrap(); + let parsed = PreparedInstallationSectionConfigWrapper::parse_section_config( + PREPARED_CONF_FILE, + &raw, + ) + .unwrap(); + + match parsed.get(&conf.id).expect("entry survived the round trip") { + PreparedInstallationSectionConfigWrapper::PreparedConfig(section) => { + section.clone().try_into().unwrap() + } + } + } + + #[test] + fn bond_and_vlan_survive_the_config_file_round_trip() { + let conf = prepared_config(json!({ + "bond-members": ["bc:24:11:49:8d:75", "bc:24:11:49:8d:76"], + "bond-mode": "802.3ad", + "bond-hash-policy": "layer2+3", + "vlan-id": 100, + })); + + let back = round_trip(conf.clone()); + + assert_eq!(conf.bond_members_filter, back.bond_members_filter); + assert_eq!(conf.bond_mode, back.bond_mode); + assert_eq!(conf.bond_hash_policy, back.bond_hash_policy); + assert_eq!(conf.vlan_id, back.vlan_id); + assert_eq!(conf, back); + } + + #[test] + fn absent_bond_and_vlan_stay_absent() { + let conf = prepared_config(json!({})); + let back = round_trip(conf.clone()); + + assert!(back.bond_members_filter.is_empty()); + assert_eq!(back.bond_mode, None); + assert_eq!(back.vlan_id, None); + assert_eq!(conf, back); + } +} -- 2.55.0