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