public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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





  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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal