From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 370651FF09C for ; Mon, 02 Nov 2026 13:49:02 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 2468621823; Mon, 02 Nov 2026 13:48:36 +0100 (CET) From: Christoph Heiss To: pve-devel@lists.proxmox.com Subject: [PATCH installer v4 21/30] auto: pass through management network vlan options to low-level installer Date: Mon, 2 Nov 2026 13:45:27 +0100 Message-ID: <20261102124539.1008075-22-c.heiss@proxmox.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20261102124539.1008075-1-c.heiss@proxmox.com> References: <20261102124539.1008075-1-c.heiss@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793623650818 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.195 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 55JE2GYOCKC3APXBF6GXWYFLGIUPYZBN X-Message-ID-Hash: 55JE2GYOCKC3APXBF6GXWYFLGIUPYZBN X-MailFrom: c.heiss@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Fixes #2164 [0]. The auto-installer gains a new section under [network] in its answer file for setting up a VLAN on the management interface during the installation: [network.vlan] vid = 5 Where `vid` is the desired VLAN ID. This then just passes through the given VLAN options in the answer file to the low-level installation configuration, verifying them beforehand and printing a status message if the management interface will be put on a VLAN. [0] https://bugzilla.proxmox.com/show_bug.cgi?id=2164 Signed-off-by: Christoph Heiss --- Changes v3 -> v4: * skip serializing disabled vlan options to low-level config Changes v2 -> v3: * new patch proxmox-auto-installer/src/utils.rs | 14 ++++++++ proxmox-auto-installer/tests/parse-answer.rs | 3 ++ .../parse_answer/network_bond_and_vlan.json | 29 ++++++++++++++++ .../parse_answer/network_bond_and_vlan.toml | 23 +++++++++++++ .../resources/parse_answer/network_vlan.json | 23 +++++++++++++ .../resources/parse_answer/network_vlan.toml | 17 ++++++++++ .../network_vlan_invalid_vid.json | 3 ++ .../network_vlan_invalid_vid.toml | 17 ++++++++++ proxmox-installer-common/src/options.rs | 33 +++++++++++++------ proxmox-installer-common/src/setup.rs | 30 +++++++++++++++-- 10 files changed, 180 insertions(+), 12 deletions(-) create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/network_bond_and_vlan.json create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/network_bond_and_vlan.toml create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/network_vlan.json create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/network_vlan.toml create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_vlan_invalid_vid.json create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/network_vlan_invalid_vid.toml diff --git a/proxmox-auto-installer/src/utils.rs b/proxmox-auto-installer/src/utils.rs index b9b3a8d..46598f9 100644 --- a/proxmox-auto-installer/src/utils.rs +++ b/proxmox-auto-installer/src/utils.rs @@ -48,6 +48,7 @@ fn get_network_settings( None, pinning_opts.as_ref(), answer.network.bond(), + answer.network.vlan(), ); opts.fqdn = name.to_owned(); opts @@ -77,6 +78,7 @@ fn get_network_settings( domain.as_deref(), pinning_opts.as_ref(), answer.network.bond(), + answer.network.vlan(), ) } }; @@ -124,6 +126,10 @@ fn get_network_settings( opts.verify()?; } + if let Some(opts) = &network_options.vlan_opts { + info!("Management interface reachable on VLAN {}", opts.vid); + } + info!("Network interface used is '{}'", &network_options.ifname); Ok(network_options) } @@ -555,6 +561,10 @@ pub fn verify_network_settings( opts.verify()?; } + if let Some(opts) = network.vlan() { + opts.verify()?; + } + Ok(()) } @@ -625,6 +635,10 @@ pub fn parse_answer( }) }) .unwrap_or_else(|| Ok(InstallNetworkBondOptions::default()))?, + mngmt_vlan: network_settings + .vlan_opts + .map(|opts| opts.into()) + .unwrap_or_default(), network_interface_pin_map: network_settings .pinning_opts .map(|o| o.mapping) diff --git a/proxmox-auto-installer/tests/parse-answer.rs b/proxmox-auto-installer/tests/parse-answer.rs index 0320bba..4776c4d 100644 --- a/proxmox-auto-installer/tests/parse-answer.rs +++ b/proxmox-auto-installer/tests/parse-answer.rs @@ -128,8 +128,10 @@ mod tests { hashed_root_password, minimal, network_bond, + network_bond_and_vlan, network_interface_pinning, network_interface_pinning_mixed_case_mac_addresses, + network_vlan, nic_matching, no_network, specific_nic, @@ -156,6 +158,7 @@ mod tests { network_bond_primary_not_member, network_bond_single_interface, network_interface_pinning_overlong_interface_name, + network_vlan_invalid_vid, no_fqdn_from_dhcp, no_root_password_set, short_password, diff --git a/proxmox-auto-installer/tests/resources/parse_answer/network_bond_and_vlan.json b/proxmox-auto-installer/tests/resources/parse_answer/network_bond_and_vlan.json new file mode 100644 index 0000000..7a27868 --- /dev/null +++ b/proxmox-auto-installer/tests/resources/parse_answer/network_bond_and_vlan.json @@ -0,0 +1,29 @@ +{ + "autoreboot": 1, + "cidr": "192.168.1.114/24", + "country": "at", + "dns": "192.168.1.254", + "domain": "testinstall", + "filesys": "ext4", + "gateway": "192.168.1.1", + "hdsize": 223.57088470458984, + "existing_storage_auto_rename": 1, + "hostname": "pveauto", + "keymap": "de", + "mailto": "mail@no.invalid", + "mngmt_nic": "bond0", + "mngmt_bond": { + "members": ["B4:2E:99:AC:AD:B4", "B4:2E:99:AC:AD:B5"], + "mode": "active-backup", + "hash-policy": "layer3+4", + "primary-interface": "B4:2E:99:AC:AD:B5" + }, + "mngmt_vlan": { + "enabled": true, + "vid": 5 + }, + "root_password": { "plain": "12345678" }, + "target_hd": "/dev/sda", + "timezone": "Europe/Vienna", + "first_boot": { "enabled": 0 } +} diff --git a/proxmox-auto-installer/tests/resources/parse_answer/network_bond_and_vlan.toml b/proxmox-auto-installer/tests/resources/parse_answer/network_bond_and_vlan.toml new file mode 100644 index 0000000..1352391 --- /dev/null +++ b/proxmox-auto-installer/tests/resources/parse_answer/network_bond_and_vlan.toml @@ -0,0 +1,23 @@ +[global] +keyboard = "de" +country = "at" +fqdn = "pveauto.testinstall" +mailto = "mail@no.invalid" +timezone = "Europe/Vienna" +root-password = "12345678" + +[network] +source = "from-dhcp" + +[network.bond] +members-list = ["b4:2e:99:ac:ad:b4", "b4:2e:99:ac:ad:b5"] +mode = "active-backup" +hash-policy = "layer3+4" +primary-interface = "b4:2e:99:ac:ad:b5" + +[network.vlan] +vid = 5 + +[disk-setup] +filesystem = "ext4" +disk-list = ["sda"] diff --git a/proxmox-auto-installer/tests/resources/parse_answer/network_vlan.json b/proxmox-auto-installer/tests/resources/parse_answer/network_vlan.json new file mode 100644 index 0000000..2761922 --- /dev/null +++ b/proxmox-auto-installer/tests/resources/parse_answer/network_vlan.json @@ -0,0 +1,23 @@ +{ + "autoreboot": 1, + "cidr": "192.168.1.114/24", + "country": "at", + "dns": "192.168.1.254", + "domain": "testinstall", + "filesys": "ext4", + "gateway": "192.168.1.1", + "hdsize": 223.57088470458984, + "existing_storage_auto_rename": 1, + "hostname": "pveauto", + "keymap": "de", + "mailto": "mail@no.invalid", + "mngmt_nic": "eno1", + "mngmt_vlan": { + "enabled": true, + "vid": 5 + }, + "root_password": { "plain": "12345678" }, + "target_hd": "/dev/sda", + "timezone": "Europe/Vienna", + "first_boot": { "enabled": 0 } +} diff --git a/proxmox-auto-installer/tests/resources/parse_answer/network_vlan.toml b/proxmox-auto-installer/tests/resources/parse_answer/network_vlan.toml new file mode 100644 index 0000000..c977711 --- /dev/null +++ b/proxmox-auto-installer/tests/resources/parse_answer/network_vlan.toml @@ -0,0 +1,17 @@ +[global] +keyboard = "de" +country = "at" +fqdn = "pveauto.testinstall" +mailto = "mail@no.invalid" +timezone = "Europe/Vienna" +root-password = "12345678" + +[network] +source = "from-dhcp" + +[network.vlan] +vid = 5 + +[disk-setup] +filesystem = "ext4" +disk-list = ["sda"] diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/network_vlan_invalid_vid.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_vlan_invalid_vid.json new file mode 100644 index 0000000..61ce63c --- /dev/null +++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_vlan_invalid_vid.json @@ -0,0 +1,3 @@ +{ + "error": "VLAN id must be between 2 and 4094 inclusive, got 5000" +} diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/network_vlan_invalid_vid.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_vlan_invalid_vid.toml new file mode 100644 index 0000000..c0e51c2 --- /dev/null +++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_vlan_invalid_vid.toml @@ -0,0 +1,17 @@ +[global] +keyboard = "de" +country = "at" +fqdn = "pveauto.testinstall" +mailto = "mail@no.invalid" +timezone = "Europe/Vienna" +root-password = "12345678" + +[network] +source = "from-dhcp" + +[network.vlan] +vid = 5000 + +[disk-setup] +filesystem = "ext4" +disk-list = ["sda"] diff --git a/proxmox-installer-common/src/options.rs b/proxmox-installer-common/src/options.rs index 1add63b..e32b77d 100644 --- a/proxmox-installer-common/src/options.rs +++ b/proxmox-installer-common/src/options.rs @@ -16,7 +16,8 @@ use proxmox_installer_types::{ EMAIL_DEFAULT_PLACEHOLDER, answer::{ BtrfsCompressOption, BtrfsRaidLevel, FilesystemType, NetworkBondOptions, - NetworkInterfacePinningOptionsAnswer, ZfsChecksumOption, ZfsCompressOption, ZfsRaidLevel, + NetworkInterfacePinningOptionsAnswer, NetworkVlanOptions, ZfsChecksumOption, + ZfsCompressOption, ZfsRaidLevel, }, }; use proxmox_network_types::{MacAddress, fqdn::Fqdn, ip_address::Cidr}; @@ -396,6 +397,7 @@ pub struct NetworkOptions { pub dns_server: IpAddr, pub pinning_opts: Option, pub bond_opts: Option, + pub vlan_opts: Option, } impl NetworkOptions { @@ -407,6 +409,7 @@ impl NetworkOptions { default_domain: Option<&str>, pinning_opts: Option<&NetworkInterfacePinningOptions>, bond_opts: Option<&NetworkBondOptions>, + vlan_opts: Option<&NetworkVlanOptions>, ) -> Self { // Sets up sensible defaults as much as possible, such that even in the // worse case nothing breaks down *completely*. @@ -420,6 +423,7 @@ impl NetworkOptions { dns_server: Ipv4Addr::new(192, 168, 100, 1).into(), pinning_opts: pinning_opts.cloned(), bond_opts: bond_opts.cloned(), + vlan_opts: vlan_opts.cloned(), }; let iface = if let Some(gw) = &network.routes.gateway4 @@ -639,7 +643,7 @@ mod tests { let (setup, mut info) = mock_setup_network(); pretty_assertions::assert_eq!( - NetworkOptions::defaults_from(&setup, &info, None, None, None), + NetworkOptions::defaults_from(&setup, &info, None, None, None, None), NetworkOptions { ifname: "eth0".to_owned(), fqdn: Fqdn::from("foo.bar.com").unwrap(), @@ -648,12 +652,13 @@ mod tests { dns_server: Ipv4Addr::new(192, 168, 100, 1).into(), pinning_opts: None, bond_opts: None, + vlan_opts: None, } ); info.hostname = None; pretty_assertions::assert_eq!( - NetworkOptions::defaults_from(&setup, &info, None, None, None), + NetworkOptions::defaults_from(&setup, &info, None, None, None, None), NetworkOptions { ifname: "eth0".to_owned(), fqdn: Fqdn::from("pve.bar.com").unwrap(), @@ -662,12 +667,13 @@ mod tests { dns_server: Ipv4Addr::new(192, 168, 100, 1).into(), pinning_opts: None, bond_opts: None, + vlan_opts: None, } ); info.dns.domain = None; pretty_assertions::assert_eq!( - NetworkOptions::defaults_from(&setup, &info, None, None, None), + NetworkOptions::defaults_from(&setup, &info, None, None, None, None), NetworkOptions { ifname: "eth0".to_owned(), fqdn: Fqdn::from("pve.example.invalid").unwrap(), @@ -676,12 +682,13 @@ mod tests { dns_server: Ipv4Addr::new(192, 168, 100, 1).into(), pinning_opts: None, bond_opts: None, + vlan_opts: None, } ); info.hostname = Some("foo".to_owned()); pretty_assertions::assert_eq!( - NetworkOptions::defaults_from(&setup, &info, None, None, None), + NetworkOptions::defaults_from(&setup, &info, None, None, None, None), NetworkOptions { ifname: "eth0".to_owned(), fqdn: Fqdn::from("foo.example.invalid").unwrap(), @@ -690,6 +697,7 @@ mod tests { dns_server: Ipv4Addr::new(192, 168, 100, 1).into(), pinning_opts: None, bond_opts: None, + vlan_opts: None, } ); } @@ -737,7 +745,7 @@ mod tests { let (setup, info) = mock_setup_network_v6_only(); pretty_assertions::assert_eq!( - NetworkOptions::defaults_from(&setup, &info, None, None, None), + NetworkOptions::defaults_from(&setup, &info, None, None, None, None), NetworkOptions { ifname: "eth0".to_owned(), fqdn: Fqdn::from("foo.bar.com").unwrap(), @@ -746,6 +754,7 @@ mod tests { dns_server: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x53)), pinning_opts: None, bond_opts: None, + vlan_opts: None, } ); } @@ -755,7 +764,7 @@ mod tests { let (setup, mut info) = mock_setup_network(); pretty_assertions::assert_eq!( - NetworkOptions::defaults_from(&setup, &info, None, None, None), + NetworkOptions::defaults_from(&setup, &info, None, None, None, None), NetworkOptions { ifname: "eth0".to_owned(), fqdn: Fqdn::from("foo.bar.com").unwrap(), @@ -764,12 +773,13 @@ mod tests { dns_server: Ipv4Addr::new(192, 168, 100, 1).into(), pinning_opts: None, bond_opts: None, + vlan_opts: None, } ); info.dns.domain = None; pretty_assertions::assert_eq!( - NetworkOptions::defaults_from(&setup, &info, Some("custom.local"), None, None), + NetworkOptions::defaults_from(&setup, &info, Some("custom.local"), None, None, None), NetworkOptions { ifname: "eth0".to_owned(), fqdn: Fqdn::from("foo.custom.local").unwrap(), @@ -778,12 +788,13 @@ mod tests { dns_server: Ipv4Addr::new(192, 168, 100, 1).into(), pinning_opts: None, bond_opts: None, + vlan_opts: None, } ); info.dns.domain = Some("some.domain.local".to_owned()); pretty_assertions::assert_eq!( - NetworkOptions::defaults_from(&setup, &info, Some("custom.local"), None, None), + NetworkOptions::defaults_from(&setup, &info, Some("custom.local"), None, None, None), NetworkOptions { ifname: "eth0".to_owned(), fqdn: Fqdn::from("foo.custom.local").unwrap(), @@ -792,6 +803,7 @@ mod tests { dns_server: Ipv4Addr::new(192, 168, 100, 1).into(), pinning_opts: None, bond_opts: None, + vlan_opts: None, } ); } @@ -828,7 +840,7 @@ mod tests { let setup = SetupInfo::mocked(); pretty_assertions::assert_eq!( - NetworkOptions::defaults_from(&setup, &info, None, None, None), + NetworkOptions::defaults_from(&setup, &info, None, None, None, None), NetworkOptions { ifname: "eth0".to_owned(), fqdn: Fqdn::from("pve.example.invalid").unwrap(), @@ -837,6 +849,7 @@ mod tests { dns_server: Ipv4Addr::new(192, 168, 100, 1).into(), pinning_opts: None, bond_opts: None, + vlan_opts: None, } ); } diff --git a/proxmox-installer-common/src/setup.rs b/proxmox-installer-common/src/setup.rs index 31a1195..e5dfef4 100644 --- a/proxmox-installer-common/src/setup.rs +++ b/proxmox-installer-common/src/setup.rs @@ -17,8 +17,8 @@ use crate::options::{ use proxmox_installer_types::{ BootType, IsoInfo, ProductConfig, answer::{ - BtrfsCompressOption, FilesystemType, ZfsChecksumOption, - ZfsCompressOption, + BtrfsCompressOption, FilesystemType, NetworkVlanOptions, + ZfsChecksumOption, ZfsCompressOption, }, }; use proxmox_network_api::{BondXmitHashPolicy, LinuxBondMode}; @@ -526,6 +526,30 @@ impl InstallNetworkBondOptions { } } +#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq)] +/// Low-level installer options for setting up a VLAN configuration on the management interface. +pub struct InstallNetworkVlanOptions { + /// Whether to enable VLAN tagging on the management interface + enabled: bool, + /// VLAN tag to configure. + pub vid: u16, +} + +impl From for InstallNetworkVlanOptions { + fn from(opts: NetworkVlanOptions) -> Self { + Self { + enabled: true, + vid: opts.vid, + } + } +} + +impl InstallNetworkVlanOptions { + fn is_disabled(&self) -> bool { + !self.enabled + } +} + /// See Proxmox::Install::Config #[derive(Debug, Deserialize, Serialize)] pub struct InstallConfig { @@ -570,6 +594,8 @@ pub struct InstallConfig { pub mngmt_nic: String, #[serde(skip_serializing_if = "InstallNetworkBondOptions::is_disabled")] pub mngmt_bond: InstallNetworkBondOptions, + #[serde(skip_serializing_if = "InstallNetworkVlanOptions::is_disabled")] + pub mngmt_vlan: InstallNetworkVlanOptions, // Maps MAC addresses -> custom name. If set, enables pinning for all // interfaces present. -- 2.55.0