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 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.