* [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