From: Christoph Heiss <c.heiss@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH installer v4 26/30] tui: network: add vlan setup options to advanced network dialog
Date: Mon, 2 Nov 2026 13:45:32 +0100 [thread overview]
Message-ID: <20261102124539.1008075-27-c.heiss@proxmox.com> (raw)
In-Reply-To: <20261102124539.1008075-1-c.heiss@proxmox.com>
Fixes #2164 [0].
Adds a third panel to the advanced network options dialog, allowing the
user to set a VLAN ID for the management interface.
[0] https://bugzilla.proxmox.com/show_bug.cgi?id=2164
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v3 -> v4:
* no changes
Changes v2 -> v3:
* no changes
Changes v1 -> v2:
* new patch
proxmox-tui-installer/src/main.rs | 1 +
proxmox-tui-installer/src/options.rs | 6 +-
proxmox-tui-installer/src/setup.rs | 5 +
proxmox-tui-installer/src/views/mod.rs | 35 +++++--
proxmox-tui-installer/src/views/network.rs | 112 ++++++++++++++++++++-
5 files changed, 147 insertions(+), 12 deletions(-)
diff --git a/proxmox-tui-installer/src/main.rs b/proxmox-tui-installer/src/main.rs
index 6e7c691..cd3b0ec 100644
--- a/proxmox-tui-installer/src/main.rs
+++ b/proxmox-tui-installer/src/main.rs
@@ -178,6 +178,7 @@ fn main() {
// We enable network interface pinning by default in the TUI
Some(&NetworkInterfacePinningOptions::default()),
None,
+ None,
),
autoreboot: true,
},
diff --git a/proxmox-tui-installer/src/options.rs b/proxmox-tui-installer/src/options.rs
index 15b14a9..b6b5211 100644
--- a/proxmox-tui-installer/src/options.rs
+++ b/proxmox-tui-installer/src/options.rs
@@ -42,7 +42,7 @@ impl InstallerOptions {
.map(|l| &l.name)
.unwrap_or(&self.timezone.kb_layout);
- let mngmt_interface = if let Some(opts) = &self.network.bond_opts {
+ let mut mngmt_interface = if let Some(opts) = &self.network.bond_opts {
let members = self
.resolve_bond_member_names(opts, &runtime_info.network)
.join(" | ");
@@ -52,6 +52,10 @@ impl InstallerOptions {
self.network.ifname.to_owned()
};
+ if let Some(opts) = &self.network.vlan_opts {
+ mngmt_interface += &format!(", VLAN {}", opts.vid);
+ }
+
vec![
SummaryOption::new("Bootdisk filesystem", self.bootdisk.fstype.to_string()),
SummaryOption::new(
diff --git a/proxmox-tui-installer/src/setup.rs b/proxmox-tui-installer/src/setup.rs
index e06d99e..db59d9e 100644
--- a/proxmox-tui-installer/src/setup.rs
+++ b/proxmox-tui-installer/src/setup.rs
@@ -48,6 +48,11 @@ impl From<InstallerOptions> for InstallConfig {
}
})
.unwrap_or_default(),
+ mngmt_vlan: options
+ .network
+ .vlan_opts
+ .map(|opts| opts.into())
+ .unwrap_or_default(),
network_interface_pin_map: pinning_opts.map(|o| o.mapping.clone()).unwrap_or_default(),
hostname: options.network.fqdn.host().to_owned(),
diff --git a/proxmox-tui-installer/src/views/mod.rs b/proxmox-tui-installer/src/views/mod.rs
index b4896e2..9349a9c 100644
--- a/proxmox-tui-installer/src/views/mod.rs
+++ b/proxmox-tui-installer/src/views/mod.rs
@@ -31,6 +31,7 @@ pub use timezone::*;
pub struct NumericEditView<T> {
view: LinearLayout,
+ min_value: Option<T>,
max_value: Option<T>,
placeholder: Option<T>,
max_content_width: Option<usize>,
@@ -44,6 +45,7 @@ impl<T: Copy + ToString + FromStr + PartialOrd> NumericEditView<T> {
Self {
view,
+ min_value: None,
max_value: None,
placeholder: None,
max_content_width: None,
@@ -63,6 +65,7 @@ impl<T: Copy + ToString + FromStr + PartialOrd> NumericEditView<T> {
Self {
view,
+ min_value: None,
max_value: None,
placeholder: None,
max_content_width: None,
@@ -70,6 +73,16 @@ impl<T: Copy + ToString + FromStr + PartialOrd> NumericEditView<T> {
}
}
+ pub fn enabled(mut self, enabled: bool) -> Self {
+ self.inner_mut().set_enabled(enabled);
+ self
+ }
+
+ pub fn min_value(mut self, min: T) -> Self {
+ self.min_value = Some(min);
+ self
+ }
+
pub fn max_value(mut self, max: T) -> Self {
self.max_value = Some(max);
self
@@ -139,13 +152,23 @@ impl<T: Copy + ToString + FromStr + PartialOrd> NumericEditView<T> {
self.max_value = Some(max);
}
+ pub fn set_enabled(&mut self, enabled: bool) {
+ self.inner_mut().set_enabled(enabled);
+ }
+
+ /// Checks if the new value is actually valid according to the min and max value, if set
fn check_bounds(&mut self, original: Arc<String>, result: EventResult) -> EventResult {
- // Check if the new value is actually valid according to the max value, if set
- if let Some(max) = self.max_value
- && let Ok(val) = self.get_content()
- && result.is_consumed()
- && val > max
- {
+ let above_min = self
+ .min_value
+ .map(|min| self.get_content().map(|val| val >= min).unwrap_or(true))
+ .unwrap_or(true);
+
+ let below_max = self
+ .max_value
+ .map(|max| self.get_content().map(|val| val <= max).unwrap_or(true))
+ .unwrap_or(true);
+
+ if result.is_consumed() && (!above_min || !below_max) {
// Restore the original value, before the insert
let cb = self.inner_mut().set_content((*original).clone());
return EventResult::with_cb_once(move |siv| {
diff --git a/proxmox-tui-installer/src/views/network.rs b/proxmox-tui-installer/src/views/network.rs
index a7a5ec9..ad5d1d9 100644
--- a/proxmox-tui-installer/src/views/network.rs
+++ b/proxmox-tui-installer/src/views/network.rs
@@ -18,15 +18,16 @@ use proxmox_installer_common::{
options::{NetworkInterfacePinningOptions, NetworkOptions},
setup::{Interface, NetworkInfo},
};
-use proxmox_installer_types::answer::NetworkBondOptions;
+use proxmox_installer_types::answer::{NetworkBondOptions, NetworkVlanOptions};
use proxmox_network_api::{BondXmitHashPolicy, LinuxBondMode};
use proxmox_network_types::{MacAddress, fqdn::Fqdn, ip_address::Cidr};
-use super::{CheckboxGroup, CidrAddressEditView, FormView};
+use super::{CheckboxGroup, CidrAddressEditView, FormView, IntegerEditView};
struct NetworkViewOptions {
pinning_enabled: bool,
bond_enabled: bool,
+ vlan_enabled: bool,
interfaces: Vec<Interface>,
/// Last selected interface (by its MAC address) in the [SelectView].
/// Keeps track of the selected interface across enabling/disabling bonding, which replaces the
@@ -36,6 +37,7 @@ struct NetworkViewOptions {
// by the user - to preserve the state between toggling the checkbox
pinning_options: NetworkInterfacePinningOptions,
bond_options: NetworkBondOptions,
+ vlan_options: NetworkVlanOptions,
}
/// Convenience wrapper when needing to take a (interior-mutable) reference to
@@ -55,6 +57,7 @@ impl NetworkOptionsView {
const INTERFACE_PINNING_VIEW_NAME: &str = "network-name-pinning-view";
const BOND_OPTIONS_VIEW_NAME: &str = "network-bond-options-view";
+ const VLAN_OPTIONS_VIEW_NAME: &str = "network-vlan-options-view";
pub fn new(options: &NetworkOptions, network_info: &NetworkInfo) -> Self {
let mut interfaces = network_info
@@ -80,6 +83,7 @@ impl NetworkOptionsView {
let options_ref = Arc::new(Mutex::new(NetworkViewOptions {
pinning_enabled: options.pinning_opts.is_some(),
bond_enabled: options.bond_opts.is_some(),
+ vlan_enabled: options.vlan_opts.is_some(),
interfaces,
last_selected_iface: selected_mac,
pinning_options: options.pinning_opts.clone().unwrap_or_default(),
@@ -87,6 +91,10 @@ impl NetworkOptionsView {
.bond_opts
.clone()
.unwrap_or_else(NetworkBondOptions::disabled),
+ vlan_options: options
+ .vlan_opts
+ .clone()
+ .unwrap_or(NetworkVlanOptions { vid: 0 }),
}));
let iface_selection = Self::build_mgmt_ifname_selectview(options_ref.clone());
@@ -181,6 +189,7 @@ impl NetworkOptionsView {
.then_some(options.pinning_options.clone());
let bond_opts = options.bond_enabled.then_some(options.bond_options.clone());
+ let vlan_opts = options.vlan_enabled.then_some(options.vlan_options.clone());
let ifname = if options.bond_enabled {
"bond0".to_owned()
@@ -211,6 +220,7 @@ impl NetworkOptionsView {
dns_server,
pinning_opts,
bond_opts,
+ vlan_opts,
})
}
}
@@ -225,6 +235,10 @@ impl NetworkOptionsView {
.child(Panel::new(
BondOptionsView::new(options_ref.clone())
.with_name(Self::BOND_OPTIONS_VIEW_NAME),
+ ))
+ .child(Panel::new(
+ VlanOptionsView::new(options_ref.clone())
+ .with_name(Self::VLAN_OPTIONS_VIEW_NAME),
)),
);
@@ -233,11 +247,12 @@ impl NetworkOptionsView {
.button("Ok", {
move |siv| {
match Self::get_view_values(siv, options_ref.clone()) {
- Ok((pin_opts, bond_opts)) => {
+ Ok((pin_opts, bond_opts, vlan_opts)) => {
siv.pop_layer();
let options = &mut options_ref.lock().expect("unpoisoned lock");
options.pinning_options = pin_opts;
options.bond_options = bond_opts;
+ options.vlan_options = vlan_opts;
}
Err(err) => {
siv.add_layer(Dialog::info(err.to_string()));
@@ -254,7 +269,11 @@ impl NetworkOptionsView {
fn get_view_values(
siv: &mut Cursive,
options_ref: NetworkViewOptionsRef,
- ) -> Result<(NetworkInterfacePinningOptions, NetworkBondOptions)> {
+ ) -> Result<(
+ NetworkInterfacePinningOptions,
+ NetworkBondOptions,
+ NetworkVlanOptions,
+ )> {
let pin_opts = siv
.call_on_name(
Self::INTERFACE_PINNING_VIEW_NAME,
@@ -276,7 +295,19 @@ impl NetworkOptionsView {
bond_opts.verify()?;
}
- Ok((pin_opts, bond_opts))
+ let vlan_opts = siv
+ .call_on_name(Self::VLAN_OPTIONS_VIEW_NAME, VlanOptionsView::get_values)
+ .unwrap_or_else(|| bail!("failed to retrieve management network VLAN options"))?;
+
+ if options_ref
+ .lock()
+ .map(|opts| opts.vlan_enabled)
+ .unwrap_or(false)
+ {
+ vlan_opts.verify()?;
+ }
+
+ Ok((pin_opts, bond_opts, vlan_opts))
}
fn refresh_ifname_view(siv: &mut Cursive, options_ref: NetworkViewOptionsRef) {
@@ -747,3 +778,74 @@ impl BondOptionsView {
impl ViewWrapper for BondOptionsView {
cursive::wrap_impl!(self.view: LinearLayout);
}
+
+struct VlanOptionsView {
+ view: LinearLayout,
+}
+
+impl VlanOptionsView {
+ const FORM_NAME: &str = "network-vlan-form-view";
+
+ fn new(options_ref: NetworkViewOptionsRef) -> Self {
+ let options = &options_ref.lock().expect("unpoisoned lock");
+
+ let view = LinearLayout::vertical()
+ .child(
+ LinearLayout::horizontal()
+ .child(
+ Checkbox::new()
+ .with_checked(options.vlan_enabled)
+ .on_change({
+ let options_ref = options_ref.clone();
+ move |siv, enabled| {
+ options_ref.lock().expect("unpoisoned lock").vlan_enabled =
+ enabled;
+
+ siv.call_on_name(Self::FORM_NAME, |form: &mut FormView| {
+ if let Some(v) = form.get_child_mut::<IntegerEditView>(0) {
+ v.set_enabled(enabled);
+ }
+ });
+ }
+ }),
+ )
+ .child(TextView::new(" Enable VLAN setup on management interface").no_wrap()),
+ )
+ .child(DummyView::new())
+ .child(
+ FormView::<()>::new()
+ .child(
+ "VLAN ID",
+ IntegerEditView::new()
+ .min_value(1)
+ .max_value(4094)
+ .content(options.vlan_options.vid as usize)
+ .enabled(options.vlan_enabled),
+ )
+ .with_name(Self::FORM_NAME),
+ );
+
+ Self { view }
+ }
+
+ fn get_values(&mut self) -> Result<NetworkVlanOptions> {
+ let form = self
+ .view
+ .get_child_mut(2)
+ .and_then(|v| v.downcast_mut::<NamedView<FormView<()>>>())
+ .ok_or(anyhow!("failed to retrieve bond options form"))?
+ .get_mut();
+
+ let vid = form
+ .get_value::<IntegerEditView, _>(0)
+ .ok_or(anyhow!("failed to retrieve VLAN ID"))?;
+
+ Ok(NetworkVlanOptions {
+ vid: vid.try_into()?,
+ })
+ }
+}
+
+impl ViewWrapper for VlanOptionsView {
+ cursive::wrap_impl!(self.view: LinearLayout);
+}
--
2.55.0
next prev parent reply other threads:[~2026-11-02 12:49 UTC|newest]
Thread overview: 32+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-02 12:45 [PATCH installer/proxmox v4 00/30] fix #2164: add bond setup across all installers Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 01/30] installer-types: clippy: use .is_multiple_of() instead of manual modulo Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 02/30] installer-types: use `MacAddress` type where applicable Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 03/30] installer-types: derive Display for RebootMode and FilterMatch Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 04/30] installer-types: answer: add options for configuring management bond Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 05/30] installer-types: answer: add options for configuring management vlan Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 06/30] installer-types: bump fetch schema for bond and VLAN answer options Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 07/30] installer-types: bump version to 0.3.0-1 Christoph Heiss
2026-11-02 12:45 ` [PATCH proxmox v4 08/30] network-api: derive Display and FromStr for network bond types Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 09/30] install: run make tidy Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 10/30] tree-wide: use `MacAddress` type instead of string for MAC addresses Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 11/30] install: factor out ifupdown2 config generation into own subroutine Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 12/30] install: do not rely on interface name map to be fully populated Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 13/30] install: config: add option for setting up bond on management interface Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 14/30] install: config: add option for setting up vlan " Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 15/30] install: network: set up management bond if requested Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 16/30] install: network: set up vlan on management interface " Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 17/30] gui: network: factor out name pinning into advanced options dialog Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 18/30] gui: network: add bond setup options to " Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 19/30] gui: network: add vlan setup options to advanced network dialog Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 20/30] auto: pass through management network bond options to low-level installer Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 21/30] auto: pass through management network vlan " Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 22/30] tui: drop long obsolete comment about logo formatting Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 23/30] tui: views: add widget for displaying a group of checkboxes Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 24/30] tui: network: factor out name pinning into advanced options dialog Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 25/30] tui: network: add bond setup options to advanced network dialog Christoph Heiss
2026-11-02 14:55 ` Lukas Sichert
2026-11-02 12:45 ` Christoph Heiss [this message]
2026-11-02 12:45 ` [PATCH installer v4 27/30] gui: network: split advanced options dialog into tabs Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 28/30] tui: " Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 29/30] gui: network: list bond member candidates sharing a MAC address once Christoph Heiss
2026-11-02 12:45 ` [PATCH installer v4 30/30] tui: " Christoph Heiss
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261102124539.1008075-27-c.heiss@proxmox.com \
--to=c.heiss@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox