public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Christoph Heiss <c.heiss@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH installer 15/15] tui: network: add bond setup options to advanced network dialog
Date: Tue,  8 Sep 2026 12:16:29 +0200	[thread overview]
Message-ID: <20260908101647.1057780-16-c.heiss@proxmox.com> (raw)
In-Reply-To: <20260908101647.1057780-1-c.heiss@proxmox.com>

Partially fixes #2164 [0].

Adds a second panel to the advanced network options dialog, allowing to
enable bonding and setting all the required options for it; such as
members, mode, hash policy and primary bond member, as required.

[0] https://bugzilla.proxmox.com/show_bug.cgi?id=2164

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-tui-installer/Cargo.toml           |   1 +
 proxmox-tui-installer/src/main.rs          |   7 +-
 proxmox-tui-installer/src/options.rs       |  45 ++-
 proxmox-tui-installer/src/setup.rs         |   5 +
 proxmox-tui-installer/src/views/network.rs | 415 ++++++++++++++++++---
 5 files changed, 424 insertions(+), 49 deletions(-)

diff --git a/proxmox-tui-installer/Cargo.toml b/proxmox-tui-installer/Cargo.toml
index 56395a4..cb950b3 100644
--- a/proxmox-tui-installer/Cargo.toml
+++ b/proxmox-tui-installer/Cargo.toml
@@ -9,6 +9,7 @@ homepage = "https://www.proxmox.com"
 
 [dependencies]
 proxmox-installer-common.workspace = true
+proxmox-network-api.workspace = true
 proxmox-network-types.workspace = true
 proxmox-installer-types.workspace = true
 anyhow.workspace = true
diff --git a/proxmox-tui-installer/src/main.rs b/proxmox-tui-installer/src/main.rs
index 61a06f7..6e7c691 100644
--- a/proxmox-tui-installer/src/main.rs
+++ b/proxmox-tui-installer/src/main.rs
@@ -177,6 +177,7 @@ fn main() {
                 None,
                 // We enable network interface pinning by default in the TUI
                 Some(&NetworkInterfacePinningOptions::default()),
+                None,
             ),
             autoreboot: true,
         },
@@ -578,7 +579,11 @@ fn summary_dialog(siv: &mut Cursive) -> InstallerView {
                     ("name".to_owned(), "Option".to_owned()),
                     ("value".to_owned(), "Selected value".to_owned()),
                 ])
-                .items(state.options.to_summary(&state.locales)),
+                .items(
+                    state
+                        .options
+                        .to_summary(&state.runtime_info, &state.locales),
+                ),
         ))
         .child(
             LinearLayout::horizontal()
diff --git a/proxmox-tui-installer/src/options.rs b/proxmox-tui-installer/src/options.rs
index 2c156e8..8b650f1 100644
--- a/proxmox-tui-installer/src/options.rs
+++ b/proxmox-tui-installer/src/options.rs
@@ -2,9 +2,9 @@ use crate::SummaryOption;
 
 use proxmox_installer_common::{
     options::{BootdiskOptions, NetworkOptions, TimezoneOptions},
-    setup::LocaleInfo,
+    setup::{LocaleInfo, NetworkInfo, RuntimeInfo},
 };
-use proxmox_installer_types::EMAIL_DEFAULT_PLACEHOLDER;
+use proxmox_installer_types::{EMAIL_DEFAULT_PLACEHOLDER, answer::NetworkBondOptions};
 
 #[derive(Clone)]
 pub struct PasswordOptions {
@@ -31,13 +31,27 @@ pub struct InstallerOptions {
 }
 
 impl InstallerOptions {
-    pub fn to_summary(&self, locales: &LocaleInfo) -> Vec<SummaryOption> {
+    pub fn to_summary(
+        &self,
+        runtime_info: &RuntimeInfo,
+        locales: &LocaleInfo,
+    ) -> Vec<SummaryOption> {
         let kb_layout = locales
             .kmap
             .get(&self.timezone.kb_layout)
             .map(|l| &l.name)
             .unwrap_or(&self.timezone.kb_layout);
 
+        let mngmt_interface = if let Some(opts) = &self.network.bond_opts {
+            let members = self
+                .resolve_bond_member_names(opts, &runtime_info.network)
+                .join(" | ");
+
+            format!("bond0 (mode: {}, members: {members})", opts.mode)
+        } else {
+            self.network.ifname.to_owned()
+        };
+
         vec![
             SummaryOption::new("Bootdisk filesystem", self.bootdisk.fstype.to_string()),
             SummaryOption::new(
@@ -52,11 +66,34 @@ impl InstallerOptions {
             SummaryOption::new("Timezone", &self.timezone.timezone),
             SummaryOption::new("Keyboard layout", kb_layout),
             SummaryOption::new("Administrator email", &self.password.email),
-            SummaryOption::new("Management interface", &self.network.ifname),
+            SummaryOption::new("Management interface", &mngmt_interface),
             SummaryOption::new("Hostname", self.network.fqdn.to_string()),
             SummaryOption::new("Host IP (CIDR)", self.network.address.to_string()),
             SummaryOption::new("Gateway", self.network.gateway.to_string()),
             SummaryOption::new("DNS", self.network.dns_server.to_string()),
         ]
     }
+
+    fn resolve_bond_member_names(
+        &self,
+        bond: &NetworkBondOptions,
+        network: &NetworkInfo,
+    ) -> Vec<String> {
+        let pin_opts = self.network.pinning_opts.as_ref();
+
+        bond.members
+            .iter()
+            .map(|mac| {
+                // Resolve either to the pinned name, if enabled, otherwise to the kernel-assigned one.
+                pin_opts
+                    .and_then(|o| o.mapping.get(mac).cloned())
+                    .or_else(|| {
+                        network.interfaces.iter().find_map(|(_, iface)| {
+                            (iface.mac == *mac).then_some(iface.name.to_owned())
+                        })
+                    })
+                    .unwrap_or("<unknown>".to_owned())
+            })
+            .collect()
+    }
 }
diff --git a/proxmox-tui-installer/src/setup.rs b/proxmox-tui-installer/src/setup.rs
index ae4b717..fb9a305 100644
--- a/proxmox-tui-installer/src/setup.rs
+++ b/proxmox-tui-installer/src/setup.rs
@@ -5,6 +5,7 @@ use proxmox_installer_common::{
     options::AdvancedBootdiskOptions,
     setup::{InstallConfig, InstallFirstBootSetup, InstallRootPassword},
 };
+use proxmox_installer_types::answer::NetworkBondOptions;
 
 impl From<InstallerOptions> for InstallConfig {
     fn from(options: InstallerOptions) -> Self {
@@ -35,6 +36,10 @@ impl From<InstallerOptions> for InstallConfig {
             subscription_key: None,
 
             mngmt_nic: options.network.ifname,
+            mngmt_bond: options
+                .network
+                .bond_opts
+                .unwrap_or_else(NetworkBondOptions::disabled),
             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/network.rs b/proxmox-tui-installer/src/views/network.rs
index f040881..f1b677d 100644
--- a/proxmox-tui-installer/src/views/network.rs
+++ b/proxmox-tui-installer/src/views/network.rs
@@ -18,16 +18,24 @@ use proxmox_installer_common::{
     options::{NetworkInterfacePinningOptions, NetworkOptions},
     setup::{Interface, NetworkInfo},
 };
+use proxmox_installer_types::answer::NetworkBondOptions;
+use proxmox_network_api::{BondXmitHashPolicy, LinuxBondMode};
 use proxmox_network_types::{MacAddress, fqdn::Fqdn, ip_address::Cidr};
 
-use super::{CidrAddressEditView, FormView};
+use super::{CheckboxGroup, CidrAddressEditView, FormView};
 
 struct NetworkViewOptions {
     pinning_enabled: bool,
+    bond_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
+    /// view.
+    last_selected_iface: MacAddress,
     // For UI purposes, we want to always save the state for the below options, even if not enabled
     // by the user - to preserve the state between toggling the checkbox
     pinning_options: NetworkInterfacePinningOptions,
+    bond_options: NetworkBondOptions,
 }
 
 /// Convenience wrapper when needing to take a (interior-mutable) reference to
@@ -46,6 +54,7 @@ impl NetworkOptionsView {
     const ADVANCED_OPTIONS_BUTTON_NAME: &str = "network-pinning-options-button";
 
     const INTERFACE_PINNING_VIEW_NAME: &str = "network-name-pinning-view";
+    const BOND_OPTIONS_VIEW_NAME: &str = "network-bond-options-view";
 
     pub fn new(options: &NetworkOptions, network_info: &NetworkInfo) -> Self {
         let mut interfaces = network_info
@@ -57,13 +66,30 @@ impl NetworkOptionsView {
         // First, sort interfaces by their link state and then name
         interfaces.sort_unstable_by(|a, b| (&a.state, &a.name).cmp(&(&b.state, &b.name)));
 
+        let selected_mac = network_info
+            .interfaces
+            .get(&options.ifname)
+            .map(|iface| iface.mac)
+            .unwrap_or_else(|| {
+                interfaces
+                    .first()
+                    .expect("at least one network interface")
+                    .mac
+            });
+
         let options_ref = Arc::new(Mutex::new(NetworkViewOptions {
             pinning_enabled: options.pinning_opts.is_some(),
+            bond_enabled: options.bond_opts.is_some(),
             interfaces,
+            last_selected_iface: selected_mac,
             pinning_options: options.pinning_opts.clone().unwrap_or_default(),
+            bond_options: options
+                .bond_opts
+                .clone()
+                .unwrap_or_else(NetworkBondOptions::disabled),
         }));
 
-        let iface_selection = Self::build_mgmt_ifname_selectview(options_ref.clone(), None);
+        let iface_selection = Self::build_mgmt_ifname_selectview(options_ref.clone());
 
         let form = FormView::<()>::new()
             .child(
@@ -148,18 +174,26 @@ impl NetworkOptionsView {
             .parse::<IpAddr>()
             .map_err(|err| err.to_string())?;
 
-        let pinning_opts = self
-            .options
-            .lock()
-            .map(|opt| opt.pinning_enabled.then_some(opt.pinning_options.clone()))
-            .map_err(|err| err.to_string())?;
+        let options = self.options.lock().map_err(|err| err.to_string())?;
 
-        let ifname = if let Some(opts) = &pinning_opts
-            && let Some(pinned) = iface.to_pinned(opts)
-        {
-            pinned.name
+        let pinning_opts = options
+            .pinning_enabled
+            .then_some(options.pinning_options.clone());
+
+        let bond_opts = options.bond_enabled.then_some(options.bond_options.clone());
+
+        let ifname = if options.bond_enabled {
+            "bond0".to_owned()
         } else {
-            iface.name
+            let iface = form
+                .get_value::<NamedView<SelectView<Interface>>, _>(0)
+                .ok_or("failed to retrieve management interface name")?;
+
+            pinning_opts
+                .as_ref()
+                .and_then(|opts| iface.to_pinned(opts))
+                .unwrap_or(iface)
+                .name
         };
 
         if address.address().is_ipv4() != gateway.is_ipv4() {
@@ -176,42 +210,51 @@ impl NetworkOptionsView {
                 gateway,
                 dns_server,
                 pinning_opts,
+                bond_opts,
             })
         }
     }
 
     fn advanced_options_view(options_ref: NetworkViewOptionsRef) -> impl View {
         let inner = ScrollView::new(
-            LinearLayout::vertical().child(Panel::new(
-                InterfacePinningOptionsView::new(options_ref.clone())
-                    .with_name(Self::INTERFACE_PINNING_VIEW_NAME),
-            )),
+            LinearLayout::vertical()
+                .child(Panel::new(
+                    InterfacePinningOptionsView::new(options_ref.clone())
+                        .with_name(Self::INTERFACE_PINNING_VIEW_NAME),
+                ))
+                .child(Panel::new(
+                    BondOptionsView::new(options_ref.clone())
+                        .with_name(Self::BOND_OPTIONS_VIEW_NAME),
+                )),
         );
 
         Dialog::around(inner)
             .title("Advanced Network Options")
             .button("Ok", {
                 move |siv| {
-                    match Self::get_view_values(siv) {
-                        Ok(pin_opts) => {
+                    match Self::get_view_values(siv, options_ref.clone()) {
+                        Ok((pin_opts, bond_opts)) => {
                             siv.pop_layer();
                             let options = &mut options_ref.lock().expect("unpoisoned lock");
                             options.pinning_options = pin_opts;
+                            options.bond_options = bond_opts;
                         }
                         Err(err) => {
-                            siv.pop_layer();
                             siv.add_layer(Dialog::info(err.to_string()));
                             return;
                         }
                     }
 
-                    Self::refresh_ifname_selectview(siv, options_ref.clone());
+                    Self::refresh_ifname_view(siv, options_ref.clone());
                 }
             })
             .max_size((80, 40))
     }
 
-    fn get_view_values(siv: &mut Cursive) -> Result<NetworkInterfacePinningOptions> {
+    fn get_view_values(
+        siv: &mut Cursive,
+        options_ref: NetworkViewOptionsRef,
+    ) -> Result<(NetworkInterfacePinningOptions, NetworkBondOptions)> {
         let pin_opts = siv
             .call_on_name(
                 Self::INTERFACE_PINNING_VIEW_NAME,
@@ -221,23 +264,42 @@ impl NetworkOptionsView {
                 bail!("failed to retrieve network interface name pinning options")
             })?;
 
-        Ok(pin_opts)
+        let bond_opts = siv
+            .call_on_name(Self::BOND_OPTIONS_VIEW_NAME, BondOptionsView::get_values)
+            .unwrap_or_else(|| bail!("failed to retrieve management network bond options"))?;
+
+        if options_ref
+            .lock()
+            .map(|opts| opts.bond_enabled)
+            .unwrap_or(false)
+        {
+            bond_opts.verify()?;
+        }
+
+        Ok((pin_opts, bond_opts))
     }
 
-    fn refresh_ifname_selectview(siv: &mut Cursive, options_ref: NetworkViewOptionsRef) {
-        siv.call_on_name(
-            Self::MGMT_IFNAME_SELECTVIEW_NAME,
-            |view: &mut SelectView<Interface>| {
-                let selected_mac = view.selection().map(|iface| iface.mac);
-                *view = Self::build_mgmt_ifname_selectview(options_ref, selected_mac);
-            },
-        );
+    fn refresh_ifname_view(siv: &mut Cursive, options_ref: NetworkViewOptionsRef) {
+        siv.call_on_name(Self::FORM_VIEW_NAME, |form: &mut FormView<()>| {
+            let options = options_ref.lock().expect("unpoisoned lock");
+
+            if options.bond_enabled {
+                form.replace_child(
+                    0,
+                    TextView::new(format!(
+                        "bond0 (mode: {}, {} members)",
+                        options.bond_options.mode,
+                        options.bond_options.members.len()
+                    )),
+                );
+            } else {
+                drop(options);
+                form.replace_child(0, Self::build_mgmt_ifname_selectview(options_ref.clone()));
+            }
+        });
     }
 
-    fn build_mgmt_ifname_selectview(
-        options_ref: NetworkViewOptionsRef,
-        selected_mac: Option<MacAddress>,
-    ) -> SelectView<Interface> {
+    fn build_mgmt_ifname_selectview(options_ref: NetworkViewOptionsRef) -> SelectView<Interface> {
         let options = options_ref.lock().expect("unpoisoned lock");
 
         // Map all interfaces to a list of (human-readable interface name, [Interface]) pairs
@@ -255,20 +317,21 @@ impl NetworkOptionsView {
             })
             .collect::<Vec<(String, Interface)>>();
 
-        let mut view = SelectView::new().popup().with_all(ifnames.clone());
-
-        let selected_mac = selected_mac.unwrap_or_else(|| {
-            options
-                .interfaces
-                .first()
-                .expect("at least one network interface")
-                .mac
-        });
+        let mut view = SelectView::new()
+            .popup()
+            .with_all(ifnames.clone())
+            .on_submit({
+                let options_ref = options_ref.clone();
+                move |_, iface| {
+                    let mut options = options_ref.lock().expect("unpoisoned lock");
+                    options.last_selected_iface = iface.mac;
+                }
+            });
 
         // Finally, (try to) select the current one
         let selected = view
             .iter()
-            .position(|(_label, iface)| iface.mac == selected_mac)
+            .position(|(_label, iface)| iface.mac == options.last_selected_iface)
             .unwrap_or(0); // we sort UP interfaces first, so select the first UP interface
         //
         view.set_selection(selected);
@@ -313,6 +376,7 @@ impl InterfacePinningOptionsView {
                                         enabled;
 
                                     Self::toggle_all(siv, enabled);
+                                    BondOptionsView::refresh_ifnames(siv, options_ref.clone());
                                 }
                             })
                             .with_name(Self::ENABLED_CHECKBOX_NAME),
@@ -414,3 +478,266 @@ impl InterfacePinningOptionsView {
 impl ViewWrapper for InterfacePinningOptionsView {
     cursive::wrap_impl!(self.view: LinearLayout);
 }
+
+struct BondOptionsView {
+    view: LinearLayout,
+    options_ref: NetworkViewOptionsRef,
+}
+
+impl BondOptionsView {
+    const FORM_NAME: &str = "network-bond-form-view";
+    const MEMBERS_VIEW_NAME: &str = "network-bond-members-checkboxes-view";
+
+    fn new(options_ref: NetworkViewOptionsRef) -> Self {
+        let options = &options_ref.lock().expect("unpoisoned lock");
+
+        let mut view = LinearLayout::vertical()
+            .child(
+                LinearLayout::horizontal()
+                    .child(
+                        Checkbox::new()
+                            .with_checked(options.bond_enabled)
+                            .on_change({
+                                let options_ref = options_ref.clone();
+                                move |siv, enabled| {
+                                    options_ref.lock().expect("unpoisoned lock").bond_enabled =
+                                        enabled;
+
+                                    Self::toggle_all(siv, enabled);
+                                }
+                            }),
+                    )
+                    .child(TextView::new(" Enable bonding for management interface").no_wrap()),
+            )
+            .child(DummyView::new())
+            .child(
+                FormView::<()>::new()
+                    .child(
+                        "Bond Mode",
+                        SelectView::new()
+                            .popup()
+                            .with_all(
+                                NetworkBondOptions::MODES
+                                    .iter()
+                                    .map(|m| (m.to_string(), *m)),
+                            )
+                            .selected(
+                                NetworkBondOptions::MODES
+                                    .iter()
+                                    .position(|m| *m == options.bond_options.mode)
+                                    .unwrap_or_default(),
+                            )
+                            .on_submit(Self::toggle_mode_dependent_options)
+                            .with_enabled(options.bond_enabled),
+                    )
+                    .child(
+                        "Hash Policy",
+                        SelectView::new()
+                            .popup()
+                            .with_all(
+                                NetworkBondOptions::HASH_POLICIES
+                                    .iter()
+                                    .map(|m| (m.to_string(), *m)),
+                            )
+                            .selected(
+                                NetworkBondOptions::HASH_POLICIES
+                                    .iter()
+                                    .position(|m| *m == options.bond_options.hash_policy)
+                                    .unwrap_or_default(),
+                            )
+                            .with_enabled(
+                                options.bond_enabled
+                                    && Self::hash_policy_view_enabled(options.bond_options.mode),
+                            ),
+                    )
+                    .child(
+                        "Primary interface",
+                        EditView::new()
+                            .content({
+                                options
+                                    .bond_options
+                                    .primary_interface
+                                    .and_then(|mac| {
+                                        options.interfaces.iter().find(|iface| iface.mac == mac)
+                                    })
+                                    .and_then(|iface| iface.to_pinned(&options.pinning_options))
+                                    .map(|iface| iface.name)
+                                    .unwrap_or_default()
+                            })
+                            .with_enabled(
+                                options.bond_enabled
+                                    && Self::primary_interface_view_enable(
+                                        options.bond_options.mode,
+                                    ),
+                            ),
+                    )
+                    .with_name(Self::FORM_NAME),
+            )
+            .child(DummyView::new());
+
+        let mut checkboxes = CheckboxGroup::<MacAddress>::new();
+        for iface in options
+            .interfaces
+            .iter()
+            .filter(|iface| iface.pinned_id.is_some())
+        {
+            let label = Self::format_interface_label(options, iface);
+            let is_member = options.bond_options.members.contains(&iface.mac);
+
+            checkboxes.add(&label, is_member, iface.mac);
+        }
+        checkboxes.set_enabled(options.bond_enabled);
+        view.add_child(checkboxes.with_name(Self::MEMBERS_VIEW_NAME));
+
+        Self {
+            view,
+            options_ref: options_ref.clone(),
+        }
+    }
+
+    fn hash_policy_view_enabled(mode: LinuxBondMode) -> bool {
+        matches!(mode, LinuxBondMode::Ieee802_3ad | LinuxBondMode::BalanceXor)
+    }
+
+    fn primary_interface_view_enable(mode: LinuxBondMode) -> bool {
+        mode == LinuxBondMode::ActiveBackup
+    }
+
+    fn toggle_all(siv: &mut Cursive, enabled: bool) {
+        siv.call_on_name(Self::FORM_NAME, |form: &mut FormView| {
+            if let Some(v) = form.get_child_mut::<SelectView<LinuxBondMode>>(0) {
+                v.set_enabled(enabled);
+            }
+            if let Some(v) = form.get_child_mut::<SelectView<BondXmitHashPolicy>>(1) {
+                v.set_enabled(enabled);
+            }
+            if let Some(v) = form.get_child_mut::<EditView>(2) {
+                v.set_enabled(enabled);
+            }
+        });
+
+        siv.call_on_name(
+            Self::MEMBERS_VIEW_NAME,
+            |group: &mut CheckboxGroup<MacAddress>| {
+                group.set_enabled(enabled);
+            },
+        );
+    }
+
+    fn toggle_mode_dependent_options(siv: &mut Cursive, selected: &LinuxBondMode) {
+        siv.call_on_name(Self::FORM_NAME, |form: &mut FormView| {
+            if let Some(v) = form.get_child_mut::<SelectView<BondXmitHashPolicy>>(1) {
+                v.set_enabled(Self::hash_policy_view_enabled(*selected));
+            }
+            if let Some(v) = form.get_child_mut::<EditView>(2) {
+                v.set_enabled(Self::primary_interface_view_enable(*selected));
+            }
+        });
+    }
+
+    fn refresh_ifnames(siv: &mut Cursive, options_ref: NetworkViewOptionsRef) {
+        siv.call_on_name(
+            Self::MEMBERS_VIEW_NAME,
+            |group: &mut CheckboxGroup<MacAddress>| {
+                let options = options_ref.lock().expect("unpoisoned lock");
+                group.update_labels(&|_, label, mac| {
+                    if let Some(iface) = options.interfaces.iter().find(|iface| iface.mac == *mac) {
+                        label.set_content(Self::format_interface_label(&options, iface));
+                    }
+                });
+            },
+        );
+    }
+
+    fn get_values(&mut self) -> Result<NetworkBondOptions> {
+        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 mode = form
+            .get_value::<SelectView<LinuxBondMode>, _>(0)
+            .ok_or(anyhow!("failed to retrieve bond mode"))?;
+
+        let hash_policy = form
+            .get_value::<SelectView<BondXmitHashPolicy>, _>(1)
+            .ok_or(anyhow!("failed to retrieve bond hash policy"))?;
+
+        let primary_interface = form
+            .get_value::<EditView, String>(2)
+            // translate the interface name back to the mac address, which is used as identifier
+            .ok_or(anyhow!("failed to retrieve bond primary interface"))
+            .and_then(|name| {
+                if name.is_empty() {
+                    Ok(None)
+                } else {
+                    Ok(Some(self.get_mac_address_for_interface_name(&name)?))
+                }
+            })?;
+
+        let members = self
+            .view
+            .get_child_mut(4)
+            .and_then(|v| v.downcast_mut::<NamedView<CheckboxGroup<MacAddress>>>())
+            .ok_or(anyhow!("failed to retrieve bond member interface group"))?
+            .get_mut()
+            .entries()
+            .filter_map(|(checked, mac)| checked.then_some(*mac))
+            .collect();
+
+        Ok(NetworkBondOptions {
+            members,
+            mode,
+            hash_policy,
+            primary_interface,
+        })
+    }
+
+    fn format_interface_label(options: &NetworkViewOptions, iface: &Interface) -> String {
+        let name = if options.pinning_enabled {
+            iface
+                .to_pinned(&options.pinning_options)
+                .map(|p| p.name)
+                .unwrap_or_else(|| iface.name.clone())
+        } else {
+            iface.name.clone()
+        };
+
+        let mut label = format!("{name} ({}, {}, {})", iface.mac, iface.driver, iface.state);
+
+        if !iface.addresses.is_empty() {
+            label += "\n  ";
+            label += iface
+                .addresses
+                .iter()
+                .fold(String::new(), |acc, addr| format!("{acc}, {addr}"))
+                .trim_start_matches(", ");
+        }
+
+        label
+    }
+
+    fn get_mac_address_for_interface_name(&self, ifname: &str) -> Result<MacAddress> {
+        let options = &self.options_ref.lock().expect("unpoisoned lock");
+
+        if options.pinning_enabled {
+            options
+                .pinning_options
+                .mapping
+                .iter()
+                .find_map(|(mac, name)| (*name == ifname).then_some(*mac))
+        } else {
+            options
+                .interfaces
+                .iter()
+                .find_map(|iface| (iface.name == ifname).then_some(iface.mac))
+        }
+        .ok_or_else(|| anyhow!("interface '{ifname}' not found"))
+    }
+}
+
+impl ViewWrapper for BondOptionsView {
+    cursive::wrap_impl!(self.view: LinearLayout);
+}
-- 
2.55.0





      parent reply	other threads:[~2026-09-08 10:19 UTC|newest]

Thread overview: 16+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
2026-09-08 10:16 ` [PATCH proxmox 01/15] installer-types: use `MacAddress` type where applicable Christoph Heiss
2026-09-08 10:16 ` [PATCH proxmox 02/15] installer-types: answer: add options for configuring management bond Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 03/15] install: run make tidy Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 04/15] tree-wide: use `MacAddress` type instead of string for MAC addresses Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 05/15] install: factor out /etc/network/interfaces setup into own subroutine Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 06/15] install: do not rely on interface name map to be fully populated Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 07/15] install: config: add option for setting up bond on management interface Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 08/15] install: network: set up management bond if requested Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 09/15] gui: network: factor out name pinning into advanced options dialog Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 10/15] gui: network: add bond setup options to " Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 11/15] auto: pass trough management network bond options to low-level installer Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 12/15] tui: drop long obsolete comment about logo formatting Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 13/15] tui: views: add widget for displaying a group of checkboxes Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 14/15] tui: network: factor out name pinning into advanced options dialog Christoph Heiss
2026-09-08 10:16 ` Christoph Heiss [this message]

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=20260908101647.1057780-16-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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal