public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH datacenter-manager 0/5] auto-install: add support for VLAN and bond configurations
@ 2026-11-02 12:49 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
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Christoph Heiss @ 2026-11-02 12:49 UTC (permalink / raw)
  To: pdm-devel

Prerequisite: https://lore.proxmox.com/pve-devel/20261102124539.1008075-1-c.heiss@proxmox.com/

This adds a new advanced section in the "Network" tab of the prepared
answer add/edit dialog.

With this, the user can the VLAN tag and/or bonding for newly installed
system.
Bond member selection uses udev filters, to provide a way to dynamically
select network interfaces for target systems.

For more details w.r.t. the implementation, see the corresponding
proxmox-installer-types/pve-installer changes in the patch series link above.

Diffstat
========

proxmox-datacenter-manager:

Christoph Heiss (5):
  config: auto-install: add new options for configuring vlan and bond
  api-types: auto-installer: add new options for configuring vlan and
    bond
  api: auto-installer: wire up new vlan and bond network options
  ui: auto-installer: use .to_string() instead of manual serde variant
  ui: auto-installer: add vlan and bond config as advanced options

 Cargo.toml                                    |   2 +-
 lib/pdm-api-types/Cargo.toml                  |   1 +
 lib/pdm-api-types/src/auto_installer.rs       |  49 ++++
 lib/pdm-config/Cargo.toml                     |   1 +
 lib/pdm-config/src/auto_install.rs            | 138 ++++++++++
 server/src/api/auto_installer/mod.rs          | 238 ++++++++++++++++++
 ui/Cargo.toml                                 |   3 +-
 .../prepared_answer_add_wizard.rs             |   6 +
 .../prepared_answer_edit_window.rs            |   5 +
 .../auto_installer/prepared_answer_form.rs    | 175 ++++++++++---
 10 files changed, 583 insertions(+), 35 deletions(-)





^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH datacenter-manager 1/5] config: auto-install: add new options for configuring vlan and bond
  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 ` Christoph Heiss
  2026-11-02 12:49 ` [PATCH datacenter-manager 2/5] api-types: auto-installer: " Christoph Heiss
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Christoph Heiss @ 2026-11-02 12:49 UTC (permalink / raw)
  To: pdm-devel

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 <c.heiss@proxmox.com>
---
 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<BTreeMapWrapper<String>>,
+        /// 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<answer::FilterMatch>,
+        /// Mode of the management interface bond.
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        pub bond_mode: Option<LinuxBondMode>,
+        /// Hash policy of the management interface bond.
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        pub bond_hash_policy: Option<BondXmitHashPolicy>,
+        /// VLAN tag to configure on the management interface.
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        pub vlan_id: Option<u16>,
+
         /// Root filesystem options.
         pub filesystem: PropertyString<answer::FilesystemOptions>,
 
@@ -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<String, String>) -> 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<PreparedInstallationSectionConfigWrapper> =
+            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





^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH datacenter-manager 2/5] api-types: auto-installer: add new options for configuring vlan and bond
  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 ` Christoph Heiss
  2026-11-02 12:49 ` [PATCH datacenter-manager 3/5] api: auto-installer: wire up new vlan and bond network options Christoph Heiss
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Christoph Heiss @ 2026-11-02 12:49 UTC (permalink / raw)
  To: pdm-devel

Effectively mirroring the available options from
`pdm_config::auto_install::types::PreparedInstallationSectionConfig`.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 lib/pdm-api-types/Cargo.toml            |  1 +
 lib/pdm-api-types/src/auto_installer.rs | 48 +++++++++++++++++++++++++
 2 files changed, 49 insertions(+)

diff --git a/lib/pdm-api-types/Cargo.toml b/lib/pdm-api-types/Cargo.toml
index f9e3d07e..0a133889 100644
--- a/lib/pdm-api-types/Cargo.toml
+++ b/lib/pdm-api-types/Cargo.toml
@@ -23,6 +23,7 @@ proxmox-lang.workspace = true
 proxmox-config-digest.workspace = true
 proxmox-installer-types = { workspace = true, features = ["api-types"] }
 proxmox-network-types = { workspace = true, features = ["api-types"] }
+proxmox-network-api.workspace = true
 proxmox-schema = { workspace = true, features = ["api-macro"] }
 proxmox-section-config.workspace = true
 proxmox-dns-api.workspace = true
diff --git a/lib/pdm-api-types/src/auto_installer.rs b/lib/pdm-api-types/src/auto_installer.rs
index 4b5027fb..4a2dce13 100644
--- a/lib/pdm-api-types/src/auto_installer.rs
+++ b/lib/pdm-api-types/src/auto_installer.rs
@@ -7,6 +7,7 @@ use proxmox_auth_api::types::Userid;
 use proxmox_installer_types::{
     SystemInfo, answer::SUBSCRIPTION_KEY_SCHEMA, post_hook::PostHookInfo,
 };
+use proxmox_network_api::{BondXmitHashPolicy, LinuxBondMode};
 use proxmox_schema::{
     ApiStringFormat, Schema, StringSchema, Updater, api,
     api_types::{CERT_FINGERPRINT_SHA256_SCHEMA, COMMENT_SCHEMA, HTTP_URL_SCHEMA, UUID_FORMAT},
@@ -144,6 +145,19 @@ pub const PREPARED_INSTALL_CONFIG_ID_SCHEMA: proxmox_schema::Schema =
             additional_properties: true,
             optional: true,
         },
+        "bond-members-filter": {
+            type: Object,
+            properties: {},
+            additional_properties: true,
+            optional: true,
+        },
+        // 0 and 4095 are reserved, 1 is the untagged default PVID of VLAN-aware bridges on Linux
+        "vlan-id": {
+            type: Integer,
+            optional: true,
+            minimum: 2,
+            maximum: 4094,
+        },
         "disk-mode": {
             type: String,
         },
@@ -298,6 +312,32 @@ pub struct PreparedInstallationConfig {
     #[updater(serde(default, skip_serializing_if = "Option::is_none"))]
     pub netif_name_pinning_enabled: bool,
 
+    /// Whether to enable interface bonding for the management network.
+    #[updater(serde(default, skip_serializing_if = "Option::is_none"))]
+    pub bond_enabled: bool,
+    /// Netdev filter for selecting management network bond members
+    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
+    #[updater(serde(default, skip_serializing_if = "Option::is_none"))]
+    pub bond_members_filter: BTreeMap<String, String>,
+    /// 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")]
+    #[updater(serde(default, skip_serializing_if = "Option::is_none"))]
+    pub bond_members_filter_match: Option<answer::FilterMatch>,
+    /// Mode of the management interface bond, defaults to `active-backup`.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[updater(serde(default, skip_serializing_if = "Option::is_none"))]
+    pub bond_mode: Option<LinuxBondMode>,
+    /// Hash policy of the management interface bond, only used with the `802.3ad` and
+    /// `balance-xor` modes.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[updater(serde(default, skip_serializing_if = "Option::is_none"))]
+    pub bond_hash_policy: Option<BondXmitHashPolicy>,
+    /// Optional VLAN tag to configure on the management interface.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[updater(serde(default, skip_serializing_if = "Option::is_none"))]
+    pub vlan_id: Option<u16>,
+
     /// Root filesystem options.
     #[updater(serde(default, skip_serializing_if = "Option::is_none"))]
     pub filesystem: answer::FilesystemOptions,
@@ -353,6 +393,14 @@ pub enum DeletablePreparedInstallationConfigProperty {
     TargetFilter,
     /// Delete all udev property filters for the management network device
     NetdevFilter,
+    /// Delete all udev property filters for selecting management network bond members
+    BondMembersFilter,
+    /// Delete the management interface bond mode.
+    BondMode,
+    /// Delete the management interface bond hash policy.
+    BondHashPolicy,
+    /// Delete the management interface VLAN tag.
+    VlanId,
     /// Delete all udev property filters for disks
     DiskFilter,
     /// Delete all `root` user public ssh keys.
-- 
2.55.0





^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH datacenter-manager 3/5] api: auto-installer: wire up new vlan and bond network options
  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
  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
  4 siblings, 0 replies; 6+ messages in thread
From: Christoph Heiss @ 2026-11-02 12:49 UTC (permalink / raw)
  To: pdm-devel

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





^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH datacenter-manager 4/5] ui: auto-installer: use .to_string() instead of manual serde variant
  2026-11-02 12:49 [PATCH datacenter-manager 0/5] auto-install: add support for VLAN and bond configurations Christoph Heiss
                   ` (2 preceding siblings ...)
  2026-11-02 12:49 ` [PATCH datacenter-manager 3/5] api: auto-installer: wire up new vlan and bond network options Christoph Heiss
@ 2026-11-02 12:49 ` 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
  4 siblings, 0 replies; 6+ messages in thread
From: Christoph Heiss @ 2026-11-02 12:49 UTC (permalink / raw)
  To: pdm-devel

No functional changes.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 lib/pdm-api-types/src/auto_installer.rs       |  1 +
 ui/Cargo.toml                                 |  2 +-
 .../auto_installer/prepared_answer_form.rs    | 41 +++++++------------
 3 files changed, 16 insertions(+), 28 deletions(-)

diff --git a/lib/pdm-api-types/src/auto_installer.rs b/lib/pdm-api-types/src/auto_installer.rs
index 4a2dce13..858bd0c2 100644
--- a/lib/pdm-api-types/src/auto_installer.rs
+++ b/lib/pdm-api-types/src/auto_installer.rs
@@ -87,6 +87,7 @@ pub enum DiskSelectionMode {
 }
 
 serde_plain::derive_fromstr_from_deserialize!(DiskSelectionMode);
+serde_plain::derive_display_from_serialize!(DiskSelectionMode);
 
 const_regex! {
     /// Regex for matching the key of udev filters.
diff --git a/ui/Cargo.toml b/ui/Cargo.toml
index 99ff5619..d3053802 100644
--- a/ui/Cargo.toml
+++ b/ui/Cargo.toml
@@ -38,7 +38,7 @@ proxmox-acme-api = "1"
 proxmox-deb-version = "0.1"
 proxmox-client = "1"
 proxmox-human-byte = "1"
-proxmox-installer-types = "0.2"
+proxmox-installer-types = "0.3"
 proxmox-login = "1"
 proxmox-network-types = "1.1"
 proxmox-schema = "5"
diff --git a/ui/src/remotes/auto_installer/prepared_answer_form.rs b/ui/src/remotes/auto_installer/prepared_answer_form.rs
index 960decae..0b01baf1 100644
--- a/ui/src/remotes/auto_installer/prepared_answer_form.rs
+++ b/ui/src/remotes/auto_installer/prepared_answer_form.rs
@@ -2,7 +2,7 @@
 //! edit window, as well as some utility to collect and prepare the form data for submission.
 
 use anyhow::{Result, anyhow, bail};
-use serde::{Deserialize, Serialize};
+use serde::Deserialize;
 use serde_json::{Value, json};
 use std::{collections::BTreeMap, ops::Deref, rc::Rc, sync::LazyLock};
 
@@ -248,7 +248,7 @@ pub fn render_global_options_form(
                 .items(Rc::new(
                     KEYBOARD_LAYOUTS
                         .iter()
-                        .map(|l| serde_variant_name(l).expect("valid variant").into())
+                        .map(|l| l.to_string().into())
                         .collect(),
                 ))
                 .render_value(|v: &AttrValue| {
@@ -257,7 +257,7 @@ pub fn render_global_options_form(
                         .unwrap_or_default()
                         .into()
                 })
-                .value(serde_variant_name(config.keyboard))
+                .value(config.keyboard.to_string())
                 .filter(|item: &AttrValue, query: &str| {
                     let query = query.to_string().to_lowercase();
                     let item = item.to_string();
@@ -317,7 +317,7 @@ pub fn render_global_options_form(
                 .items(Rc::new(
                     [RebootMode::Reboot, RebootMode::PowerOff]
                         .iter()
-                        .map(|opt| serde_variant_name(opt).expect("valid variant").into())
+                        .map(|m| m.to_string().into())
                         .collect(),
                 ))
                 .render_value(|v: &AttrValue| match v.parse::<RebootMode>() {
@@ -325,7 +325,7 @@ pub fn render_global_options_form(
                     Ok(RebootMode::PowerOff) => tr!("Power off").into(),
                     _ => v.into(),
                 })
-                .value(serde_variant_name(config.reboot_mode))
+                .value(config.reboot_mode.to_string())
                 .required(true),
         )
         .with_field(
@@ -482,7 +482,7 @@ pub fn render_disk_setup_form(
                 .items(Rc::new(
                     FILESYSTEM_TYPE_OPTIONS
                         .iter()
-                        .map(|opt| serde_variant_name(opt).expect("valid variant").into())
+                        .map(|opt| opt.to_string().into())
                         .collect(),
                 ))
                 .render_value(|v: &AttrValue| {
@@ -491,7 +491,7 @@ pub fn render_disk_setup_form(
                         .unwrap_or_default()
                         .into()
                 })
-                .value(serde_variant_name(config.filesystem.to_type()))
+                .value(config.filesystem.to_type().to_string())
                 .required(true)
                 .show_filter(false),
         )
@@ -508,7 +508,7 @@ pub fn render_disk_setup_form(
                     _ => v.into(),
                 })
                 .required(true)
-                .value(serde_variant_name(config.disk_mode)),
+                .value(config.disk_mode.to_string()),
         )
         .with_field(
             tr!("Disk Names"),
@@ -528,7 +528,7 @@ pub fn render_disk_setup_form(
                 .items(Rc::new(
                     [FilterMatch::Any, FilterMatch::All]
                         .iter()
-                        .map(|opt| serde_variant_name(opt).expect("valid variant").into())
+                        .map(|opt| opt.to_string().into())
                         .collect(),
                 ))
                 .render_value(|v: &AttrValue| match v.parse::<FilterMatch>() {
@@ -536,8 +536,8 @@ pub fn render_disk_setup_form(
                     Ok(FilterMatch::All) => tr!("Match All Filters").into(),
                     _ => v.into(),
                 })
-                .default(serde_variant_name(FilterMatch::default()))
-                .value(config.disk_filter_match.and_then(serde_variant_name))
+                .default(FilterMatch::default().to_string())
+                .value(config.disk_filter_match.map(|v| v.to_string()))
                 .disabled(disk_mode != DiskSelectionMode::Filter),
         )
         .with_large_field(
@@ -729,7 +729,7 @@ fn add_zfs_advanced_form_fields(panel: &mut InputPanel, fs_opts: &ZfsOptions) {
             .items(Rc::new(
                 ZFS_CHECKSUM_OPTIONS
                     .iter()
-                    .map(|opt| serde_variant_name(opt).expect("valid variant").into())
+                    .map(|opt| opt.to_string().into())
                     .collect(),
             ))
             .render_value(|v: &AttrValue| {
@@ -751,7 +751,7 @@ fn add_zfs_advanced_form_fields(panel: &mut InputPanel, fs_opts: &ZfsOptions) {
             .items(Rc::new(
                 ZFS_COMPRESS_OPTIONS
                     .iter()
-                    .map(|opt| serde_variant_name(opt).expect("valid variant").into())
+                    .map(|opt| opt.to_string().into())
                     .collect(),
             ))
             .render_value(|v: &AttrValue| {
@@ -789,7 +789,7 @@ fn add_btrfs_advanced_form_fields(panel: &mut InputPanel, fs_opts: &BtrfsOptions
             .items(Rc::new(
                 BTRFS_COMPRESS_OPTIONS
                     .iter()
-                    .map(|opt| serde_variant_name(opt).expect("valid variant").into())
+                    .map(|opt| opt.to_string().into())
                     .collect(),
             ))
             .render_value(|v: &AttrValue| {
@@ -1223,19 +1223,6 @@ fn kv_list_to_template_counter_map_validate(v: &Vec<(String, Value)>) -> Result<
     Ok(serde_json::to_value(map)?)
 }
 
-fn serde_variant_name<T: Serialize>(ty: T) -> Option<String> {
-    match serde_json::to_value(ty) {
-        Ok(Value::String(s)) => Some(s),
-        other => {
-            log::warn!(
-                "expected string of type {}, got {other:?}",
-                std::any::type_name::<T>()
-            );
-            None
-        }
-    }
-}
-
 const KEYBOARD_LAYOUTS: &[KeyboardLayout] = {
     use KeyboardLayout::*;
     &[
-- 
2.55.0





^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH datacenter-manager 5/5] ui: auto-installer: add vlan and bond config as advanced options
  2026-11-02 12:49 [PATCH datacenter-manager 0/5] auto-install: add support for VLAN and bond configurations Christoph Heiss
                   ` (3 preceding siblings ...)
  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 ` Christoph Heiss
  4 siblings, 0 replies; 6+ messages in thread
From: Christoph Heiss @ 2026-11-02 12:49 UTC (permalink / raw)
  To: pdm-devel

This adds a new advanced section in the "Network" tab of the prepared
answer add/edit dialog.

With this, the user can the VLAN tag and/or bonding for newly installed
system.
Bond member selection uses udev filters, to provide a way to dynamically
select network interfaces for target systems.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 ui/Cargo.toml                                 |   1 +
 .../prepared_answer_add_wizard.rs             |   6 +
 .../prepared_answer_edit_window.rs            |   5 +
 .../auto_installer/prepared_answer_form.rs    | 134 +++++++++++++++++-
 4 files changed, 140 insertions(+), 6 deletions(-)

diff --git a/ui/Cargo.toml b/ui/Cargo.toml
index d3053802..0b00914c 100644
--- a/ui/Cargo.toml
+++ b/ui/Cargo.toml
@@ -40,6 +40,7 @@ proxmox-client = "1"
 proxmox-human-byte = "1"
 proxmox-installer-types = "0.3"
 proxmox-login = "1"
+proxmox-network-api = "1"
 proxmox-network-types = "1.1"
 proxmox-schema = "5"
 proxmox-subscription = { version = "1.0.1", features = ["api-types"], default-features = false }
diff --git a/ui/src/remotes/auto_installer/prepared_answer_add_wizard.rs b/ui/src/remotes/auto_installer/prepared_answer_add_wizard.rs
index 03d78be8..63f10dcf 100644
--- a/ui/src/remotes/auto_installer/prepared_answer_add_wizard.rs
+++ b/ui/src/remotes/auto_installer/prepared_answer_add_wizard.rs
@@ -72,6 +72,12 @@ impl AddAnswerWizardProperties {
             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(),
diff --git a/ui/src/remotes/auto_installer/prepared_answer_edit_window.rs b/ui/src/remotes/auto_installer/prepared_answer_edit_window.rs
index 33a9ce00..5b4108ba 100644
--- a/ui/src/remotes/auto_installer/prepared_answer_edit_window.rs
+++ b/ui/src/remotes/auto_installer/prepared_answer_edit_window.rs
@@ -157,6 +157,11 @@ async fn submit(
             "post-hook-cert-fp",
             "disk-filter",
             "netdev-filter",
+            "bond-members-filter",
+            "bond-mode",
+            "bond-hash-policy",
+            "bond-primary-interface",
+            "vlan-id",
             "target-filter",
             "subscription-key",
             "template-counters",
diff --git a/ui/src/remotes/auto_installer/prepared_answer_form.rs b/ui/src/remotes/auto_installer/prepared_answer_form.rs
index 0b01baf1..e24d23aa 100644
--- a/ui/src/remotes/auto_installer/prepared_answer_form.rs
+++ b/ui/src/remotes/auto_installer/prepared_answer_form.rs
@@ -18,11 +18,12 @@ use proxmox_installer_types::{
     answer::{
         BTRFS_COMPRESS_OPTIONS, BtrfsCompressOption, BtrfsOptions, DiskSetup,
         FILESYSTEM_TYPE_OPTIONS, Filesystem, FilesystemOptions, FilesystemType, FilterMatch,
-        KeyboardLayout, LvmOptions, ROOT_PASSWORD_SCHEMA, RebootMode, SUBSCRIPTION_KEY_SCHEMA,
-        ZFS_CHECKSUM_OPTIONS, ZFS_COMPRESS_OPTIONS, ZfsChecksumOption, ZfsCompressOption,
-        ZfsOptions, email_validate,
+        KeyboardLayout, LvmOptions, NetworkBondOptions, ROOT_PASSWORD_SCHEMA, RebootMode,
+        SUBSCRIPTION_KEY_SCHEMA, ZFS_CHECKSUM_OPTIONS, ZFS_COMPRESS_OPTIONS, ZfsChecksumOption,
+        ZfsCompressOption, ZfsOptions, email_validate,
     },
 };
+use proxmox_network_api::{BondXmitHashPolicy, LinuxBondMode};
 use proxmox_network_types::fqdn::Fqdn;
 use proxmox_schema::api_types::{CIDR_SCHEMA, IP_SCHEMA};
 use proxmox_yew_comp::{KeyValueList, SchemaValidation, utils::copy_text_to_clipboard};
@@ -355,7 +356,20 @@ pub fn render_network_options_form(
         .and_then(|v| v.as_bool())
         .unwrap_or(true);
 
-    InputPanel::new()
+    let bond_enabled = form_ctx
+        .read()
+        .get_field_value("bond-enabled")
+        .and_then(|v| v.as_bool())
+        .unwrap_or(!config.bond_members_filter.is_empty());
+
+    let bond_mode = form_ctx
+        .read()
+        .get_field_value("bond-mode")
+        .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
+        .or(config.bond_mode)
+        .unwrap_or(LinuxBondMode::ActiveBackup);
+
+    let panel = InputPanel::new()
         .class(Flex::Fill)
         .class(Overflow::Auto)
         .padding(4)
@@ -451,7 +465,99 @@ pub fn render_network_options_form(
                 .disabled(use_dhcp_network)
                 .required(!use_dhcp_network),
         )
-        .into()
+        // Bonding and VLAN tagging are supported for both the DHCP and the static
+        // configuration, so they are independent of the 'Use DHCP' setting.
+        .with_advanced_spacer()
+        .with_advanced_field(
+            tr!("VLAN ID"),
+            Number::<u16>::new()
+                .name("vlan-id")
+                .min(2)
+                .max(4094)
+                .step(1)
+                .submit_empty(false)
+                .placeholder(tr!("Optional, 2-4094. Disabled when empty."))
+                .value(config.vlan_id.map(|v| v.to_string())),
+        )
+        .with_advanced_field(
+            tr!("Enable Bonding"),
+            Checkbox::new()
+                .name("bond-enabled")
+                .tip(tr!(
+                    "Bond the member interfaces for the management network."
+                ))
+                .default(config.bond_enabled),
+        )
+        .with_field(
+            tr!("Bond Members udev Filter Mode"),
+            filter_match_combobox()
+                .name("bond-members-filter-match")
+                .value(config.bond_members_filter_match.map(|v| v.to_string()))
+                .disabled(!bond_enabled),
+        )
+        .with_advanced_field(
+            tr!("Bond Members udev Filters"),
+            KeyValueList::new()
+                .name("bond-members-filter")
+                .class(FlexFit)
+                .value(
+                    config
+                        .bond_members_filter
+                        .iter()
+                        .map(|(k, v)| (k.clone(), Value::String(v.clone())))
+                        .collect(),
+                )
+                .key_label(tr!("Property Name"))
+                .value_label(tr!("Value To Match"))
+                .key_placeholder(tr!("udev Property Name, e.g. ID_MODEL"))
+                .value_renderer(render_udev_filter_value.into())
+                .submit_validate(kv_list_to_udev_filter_map_validate)
+                .submit_empty(false)
+                .disabled(!bond_enabled)
+                .required(bond_enabled),
+        )
+        .with_advanced_field(
+            tr!("Bond Mode"),
+            Combobox::new()
+                .name("bond-mode")
+                .items(Rc::new(
+                    NetworkBondOptions::MODES
+                        .iter()
+                        .map(|mode| mode.to_string().into())
+                        .collect(),
+                ))
+                .default(LinuxBondMode::ActiveBackup.to_string())
+                .value(config.bond_mode.map(|v| v.to_string()))
+                .disabled(!bond_enabled)
+                .required(bond_enabled)
+                .show_filter(false),
+        )
+        .with_advanced_field(
+            tr!("Bond Hash Policy"),
+            Combobox::new()
+                .name("bond-hash-policy")
+                .items(Rc::new(
+                    NetworkBondOptions::HASH_POLICIES
+                        .iter()
+                        .map(|policy| policy.to_string().into())
+                        .collect(),
+                ))
+                .default(BondXmitHashPolicy::Layer2.to_string())
+                .value(config.bond_hash_policy.map(|v| v.to_string()))
+                .disabled(
+                    !bond_enabled
+                        || !matches!(
+                            bond_mode,
+                            LinuxBondMode::Ieee802_3ad | LinuxBondMode::BalanceXor
+                        ),
+                )
+                .show_filter(false),
+        );
+
+    // the bond and VLAN options live in the advanced section, report that to the dialog so it can
+    // offer the toggle
+    form_ctx.set_has_advanced(panel.has_advanced());
+    panel.into()
 }
 
 pub fn render_disk_setup_form(
@@ -523,7 +629,7 @@ pub fn render_disk_setup_form(
         .with_spacer()
         .with_field(
             tr!("Disk udev Filter Mode"),
-            Combobox::new()
+            filter_match_combobox()
                 .name("disk-filter-match")
                 .items(Rc::new(
                     [FilterMatch::Any, FilterMatch::All]
@@ -1147,6 +1253,22 @@ fn render_template_counter_value(
         .into()
 }
 
+fn filter_match_combobox() -> Combobox {
+    Combobox::new()
+        .items(Rc::new(
+            [FilterMatch::Any, FilterMatch::All]
+                .iter()
+                .map(|opt| opt.to_string().into())
+                .collect(),
+        ))
+        .render_value(|v: &AttrValue| match v.parse::<FilterMatch>() {
+            Ok(FilterMatch::Any) => tr!("Match Any Filter").into(),
+            Ok(FilterMatch::All) => tr!("Match All Filters").into(),
+            _ => v.into(),
+        })
+        .default(FilterMatch::default().to_string())
+}
+
 /// Validate against a schema, but accept any value carrying a MiniJinja placeholder, as the
 /// backend substitutes those before parsing the field.
 fn templated_schema_validate(schema: &'static proxmox_schema::Schema) -> ValidateFn<String> {
-- 
2.55.0





^ permalink raw reply related	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-11-02 12:50 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH datacenter-manager 3/5] api: auto-installer: wire up new vlan and bond network options Christoph Heiss
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

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