From: Christoph Heiss <c.heiss@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [PATCH datacenter-manager 3/5] api: auto-installer: wire up new vlan and bond network options
Date: Mon, 2 Nov 2026 13:49:35 +0100 [thread overview]
Message-ID: <20261102124939.1023342-4-c.heiss@proxmox.com> (raw)
In-Reply-To: <20261102124939.1023342-1-c.heiss@proxmox.com>
Overall pretty straight-forward; adds the required handling in the
update and configuration rendering path.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
server/src/api/auto_installer/mod.rs | 238 +++++++++++++++++++++++++++
1 file changed, 238 insertions(+)
diff --git a/server/src/api/auto_installer/mod.rs b/server/src/api/auto_installer/mod.rs
index 7c8ef0fb..96264d12 100644
--- a/server/src/api/auto_installer/mod.rs
+++ b/server/src/api/auto_installer/mod.rs
@@ -25,6 +25,7 @@ use proxmox_installer_types::{
},
post_hook::PostHookInfo,
};
+use proxmox_network_api::{BondXmitHashPolicy, LinuxBondMode};
use proxmox_network_types::fqdn::Fqdn;
use proxmox_router::{
ApiHandler, ApiMethod, ApiResponseFuture, Permission, Router, RpcEnvironment, SubdirMap,
@@ -427,6 +428,7 @@ async fn create_prepared_answer(
validate_udev_filter_map(&config.netdev_filter)?;
validate_udev_filter_map(&config.disk_filter)?;
+ validate_udev_filter_map(&config.bond_members_filter)?;
validate_template_map(&config.template_counters)?;
prepared.insert(config.id.clone(), config.clone().try_into()?);
@@ -546,6 +548,18 @@ async fn update_prepared_answer(
DeletablePreparedInstallationConfigProperty::NetdevFilter => {
p.netdev_filter.clear();
}
+ DeletablePreparedInstallationConfigProperty::BondMembersFilter => {
+ p.bond_members_filter.clear();
+ }
+ DeletablePreparedInstallationConfigProperty::BondMode => {
+ p.bond_mode = None;
+ }
+ DeletablePreparedInstallationConfigProperty::BondHashPolicy => {
+ p.bond_hash_policy = None;
+ }
+ DeletablePreparedInstallationConfigProperty::VlanId => {
+ p.vlan_id = None;
+ }
DeletablePreparedInstallationConfigProperty::DiskFilter => {
p.disk_filter.clear();
}
@@ -589,6 +603,12 @@ async fn update_prepared_answer(
dns,
netdev_filter,
netif_name_pinning_enabled,
+ bond_enabled,
+ bond_members_filter,
+ bond_members_filter_match,
+ bond_mode,
+ bond_hash_policy,
+ vlan_id,
filesystem,
disk_mode,
disk_list,
@@ -687,6 +707,31 @@ async fn update_prepared_answer(
p.netif_name_pinning_enabled = enabled;
}
+ if let Some(enabled) = bond_enabled {
+ p.bond_enabled = enabled;
+ }
+
+ if let Some(filter) = bond_members_filter {
+ validate_udev_filter_map(&filter)?;
+ **p.bond_members_filter = filter;
+ }
+
+ if let Some(filter_match) = bond_members_filter_match {
+ p.bond_members_filter_match = Some(filter_match);
+ }
+
+ if let Some(mode) = bond_mode {
+ p.bond_mode = Some(mode);
+ }
+
+ if let Some(hash_policy) = bond_hash_policy {
+ p.bond_hash_policy = Some(hash_policy);
+ }
+
+ if let Some(vid) = vlan_id {
+ p.vlan_id = Some(vid);
+ }
+
if let Some(fs) = filesystem {
*p.filesystem = fs;
}
@@ -1168,6 +1213,40 @@ fn find_config(
default_answer.map(|a| a.try_into()).transpose()
}
+/// Collects both bond and VLAN options of a prepared configuration into the corresponding
+/// answer types and doing some sanity checks.
+///
+/// Bonding is considered off if no members are set, mirroring the meaning of an empty member list
+/// in the low-level installer.
+fn render_prepared_advanced_network_config(
+ conf: &PreparedInstallationConfig,
+) -> Result<(
+ Option<answer::NetworkBondOptions>,
+ Option<answer::NetworkVlanOptions>,
+)> {
+ let bond = if conf.bond_enabled {
+ let bond = answer::NetworkBondOptions {
+ members_list: vec![],
+ members_filter: conf.bond_members_filter.clone(),
+ members_filter_match: conf.bond_members_filter_match,
+ mode: conf.bond_mode.unwrap_or(LinuxBondMode::ActiveBackup),
+ hash_policy: conf.bond_hash_policy.unwrap_or(BondXmitHashPolicy::Layer2),
+ primary_interface: None,
+ };
+ bond.verify()?;
+ Some(bond)
+ } else {
+ None
+ };
+
+ let vlan = conf.vlan_id.map(|vid| answer::NetworkVlanOptions { vid });
+ if let Some(vlan) = &vlan {
+ vlan.verify()?;
+ }
+
+ Ok((bond, vlan))
+}
+
/// Renders a given [`PreparedInstallationConfig`] into the target [`AutoInstallerConfig`] struct.
///
/// Converts all types as needed and renders out MiniJinja templates in applicable fields.
@@ -1237,9 +1316,13 @@ fn render_prepared_config(
},
);
+ let (bond, vlan) = render_prepared_advanced_network_config(conf)?;
+
if conf.use_dhcp_network {
answer::NetworkConfig::FromDhcp(answer::NetworkConfigFromDhcp {
interface_name_pinning,
+ bond,
+ vlan,
})
} else {
let cidr = conf
@@ -1284,6 +1367,8 @@ fn render_prepared_config(
gateway,
filter: conf.netdev_filter.clone(),
interface_name_pinning,
+ bond,
+ vlan,
})
}
};
@@ -1367,3 +1452,156 @@ fn generate_token(
rpcenv,
)
}
+
+#[cfg(test)]
+mod tests {
+ use pdm_api_types::auto_installer::DiskSelectionMode;
+ use proxmox_installer_types::{
+ IsoInfo, NetworkInterface, ProductConfig, ProxmoxProduct, SystemDMI,
+ answer::NetworkVlanOptions,
+ };
+
+ use super::*;
+
+ fn config() -> PreparedInstallationConfig {
+ PreparedInstallationConfig {
+ id: "test".to_owned(),
+ authorized_tokens: Vec::new(),
+ // target filter
+ is_default: false,
+ target_filter: BTreeMap::new(),
+ // global options
+ country: "at".to_owned(),
+ fqdn: "test.example.com".to_owned(),
+ use_dhcp_fqdn: false,
+ keyboard: answer::KeyboardLayout::default(),
+ mailto: String::new(),
+ timezone: "Europe/Vienna".to_owned(),
+ root_password_hashed: None,
+ reboot_on_error: false,
+ reboot_mode: answer::RebootMode::default(),
+ root_ssh_keys: Vec::new(),
+ // network options
+ use_dhcp_network: true,
+ cidr: None,
+ gateway: None,
+ dns: None,
+ netdev_filter: BTreeMap::new(),
+ netif_name_pinning_enabled: true,
+ bond_enabled: false,
+ bond_members_filter: BTreeMap::new(),
+ bond_members_filter_match: None,
+ bond_mode: None,
+ bond_hash_policy: None,
+ vlan_id: None,
+ // disk options
+ filesystem: answer::FilesystemOptions::Ext4(answer::LvmOptions::default()),
+ disk_mode: DiskSelectionMode::default(),
+ disk_list: Vec::new(),
+ disk_filter: BTreeMap::new(),
+ disk_filter_match: None,
+ // post hook
+ post_hook_base_url: None,
+ post_hook_cert_fp: None,
+ // templating
+ template_counters: BTreeMap::new(),
+ // subscription
+ subscription_key: None,
+ }
+ }
+
+ fn sysinfo() -> SystemInfo {
+ SystemInfo {
+ product: ProductConfig {
+ fullname: "Proxmox VE".to_owned(),
+ product: ProxmoxProduct::Pve,
+ enable_btrfs: true,
+ },
+ iso: IsoInfo {
+ release: "9.1".to_owned(),
+ isorelease: "1".to_owned(),
+ },
+ dmi: SystemDMI {
+ baseboard: HashMap::new(),
+ chassis: HashMap::new(),
+ system: HashMap::new(),
+ },
+ network_interfaces: vec![
+ NetworkInterface {
+ link: "ens18".to_owned(),
+ mac: "ab:cd:ef:12:34:56".parse().unwrap(),
+ },
+ NetworkInterface {
+ link: "ens19".to_owned(),
+ mac: "12:34:56:ab:cd:ef".parse().unwrap(),
+ },
+ ],
+ }
+ }
+
+ #[test]
+ fn netwoork_bond_and_vlan_unset_stays_disabled() {
+ let prepared = render_prepared_config(&config(), &sysinfo()).unwrap();
+ assert!(prepared.network.bond().is_none());
+ assert!(prepared.network.vlan().is_none());
+ }
+
+ #[test]
+ fn network_bond_renders_default_options() {
+ let mut config = config();
+ config.bond_enabled = true;
+ config
+ .bond_members_filter
+ .insert("ID_NET_DRIVER".to_owned(), "igc".to_owned());
+
+ let prepared = render_prepared_config(&config, &sysinfo()).unwrap();
+ let bond = prepared.network.bond().unwrap();
+ assert_eq!(bond.mode, LinuxBondMode::ActiveBackup);
+ assert_eq!(bond.hash_policy, BondXmitHashPolicy::Layer2);
+ }
+
+ #[test]
+ fn bond_options_without_members_filter_are_rejected() {
+ let mut config = config();
+ config.bond_enabled = true;
+
+ let prepared = render_prepared_config(&config, &sysinfo());
+ assert!(prepared.is_err());
+ assert_eq!(
+ prepared.unwrap_err().to_string(),
+ "bond setup requires either fixed list of members or udev filter list"
+ );
+ }
+
+ #[test]
+ fn vlan_id_is_validated() {
+ let mut config = config();
+ let sysinfo = sysinfo();
+
+ assert_eq!(
+ render_prepared_config(&config, &sysinfo)
+ .unwrap()
+ .network
+ .vlan(),
+ None
+ );
+
+ config.vlan_id = Some(7);
+ assert_eq!(
+ render_prepared_config(&config, &sysinfo)
+ .unwrap()
+ .network
+ .vlan(),
+ Some(&NetworkVlanOptions { vid: 7 }),
+ );
+
+ // 1 is the untagged default PVID, thus not accepted
+ config.vlan_id = Some(1);
+ assert_eq!(
+ render_prepared_config(&config, &sysinfo)
+ .unwrap_err()
+ .to_string(),
+ "VLAN id must be between 2 and 4094 inclusive, got 1"
+ );
+ }
+}
--
2.55.0
next prev parent reply other threads:[~2026-11-02 12:50 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-02 12:49 [PATCH datacenter-manager 0/5] auto-install: add support for VLAN and bond configurations Christoph Heiss
2026-11-02 12:49 ` [PATCH datacenter-manager 1/5] config: auto-install: add new options for configuring vlan and bond Christoph Heiss
2026-11-02 12:49 ` [PATCH datacenter-manager 2/5] api-types: auto-installer: " Christoph Heiss
2026-11-02 12:49 ` Christoph Heiss [this message]
2026-11-02 12:49 ` [PATCH datacenter-manager 4/5] ui: auto-installer: use .to_string() instead of manual serde variant Christoph Heiss
2026-11-02 12:49 ` [PATCH datacenter-manager 5/5] ui: auto-installer: add vlan and bond config as advanced options 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=20261102124939.1023342-4-c.heiss@proxmox.com \
--to=c.heiss@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox