all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Christoph Heiss <c.heiss@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox v4 04/30] installer-types: answer: add options for configuring management bond
Date: Mon,  2 Nov 2026 13:45:10 +0100	[thread overview]
Message-ID: <20261102124539.1008075-5-c.heiss@proxmox.com> (raw)
In-Reply-To: <20261102124539.1008075-1-c.heiss@proxmox.com>

Adds a new subsection `bond` in [network]. This allows for configuring a
bond for the management network interface during the installation.

E.g.

  [network.bond]
  members-list = ["ab:cd:ef:12:34:56", "ab:cd:ef:12:34:57"]
  mode = "active-backup"
  hash-policy = "layer2+3"
  primary-interface = "ab:cd:ef:12:34:57"

Or to use udev filters for selecting the members:

  [network.bond]
  members-filter.ID_NET_DRIVER = "igc"
  members-filter-match = "all"
  mode = "active-backup"
  hash-policy = "layer2+3"
  primary-interface = "ab:cd:ef:12:34:57"

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v3 -> v4:
    * separate [bond.members] into separate attributes for selecting by
      either MAC addresses or filters.

Changes v2 -> v3:
  * no changes

Changes v1 -> v2:
  * no changes

 Cargo.toml                             |   1 +
 proxmox-installer-types/Cargo.toml     |   1 +
 proxmox-installer-types/debian/control |   2 +
 proxmox-installer-types/src/answer.rs  | 238 +++++++++++++++++++++++++
 4 files changed, 242 insertions(+)

diff --git a/Cargo.toml b/Cargo.toml
index 09ef21b4..c35d7fd7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -180,6 +180,7 @@ proxmox-lang = { version = "1.5", path = "proxmox-lang" }
 proxmox-log = { version = "1.0.0", path = "proxmox-log" }
 proxmox-login = { version = "1.0.0", path = "proxmox-login" }
 proxmox-network-types = { version = "1.0.2", path = "proxmox-network-types" }
+proxmox-network-api = { version = "1.0.5", path = "proxmox-network-api" }
 proxmox-parallel-handler = { version = "1.0.0", path = "proxmox-parallel-handler" }
 proxmox-pgp = { version = "1.0.0", path = "proxmox-pgp" }
 proxmox-procfs = { version = "0.1.0", path = "proxmox-procfs" }
diff --git a/proxmox-installer-types/Cargo.toml b/proxmox-installer-types/Cargo.toml
index 8ac4708b..6b634c19 100644
--- a/proxmox-installer-types/Cargo.toml
+++ b/proxmox-installer-types/Cargo.toml
@@ -17,6 +17,7 @@ serde = { workspace = true, features = ["derive"] }
 serde_plain.workspace = true
 regex = { workspace = true }
 proxmox-network-types.workspace = true
+proxmox-network-api.workspace = true
 proxmox-schema = { workspace = true, features = ["api-macro"] }
 proxmox-section-config = { workspace = true, optional = true }
 proxmox-node-status.workspace = true
diff --git a/proxmox-installer-types/debian/control b/proxmox-installer-types/debian/control
index e75f349c..0d12dcf6 100644
--- a/proxmox-installer-types/debian/control
+++ b/proxmox-installer-types/debian/control
@@ -7,6 +7,7 @@ Build-Depends-Arch: cargo:native <!nocheck>,
  rustc:native (>= 1.88) <!nocheck>,
  libstd-rust-dev <!nocheck>,
  librust-anyhow-1+default-dev <!nocheck>,
+ librust-proxmox-network-api-1+default-dev (>= 1.0.5-~~) <!nocheck>,
  librust-proxmox-network-types-1+default-dev (>= 1.0.2-~~) <!nocheck>,
  librust-proxmox-node-status-1+default-dev <!nocheck>,
  librust-proxmox-schema-5+api-macro-dev (>= 5.5.0-~~) <!nocheck>,
@@ -28,6 +29,7 @@ Multi-Arch: same
 Depends:
  ${misc:Depends},
  librust-anyhow-1+default-dev,
+ librust-proxmox-network-api-1+default-dev (>= 1.0.5-~~),
  librust-proxmox-network-types-1+default-dev (>= 1.0.2-~~),
  librust-proxmox-node-status-1+default-dev,
  librust-proxmox-schema-5+api-macro-dev (>= 5.5.0-~~),
diff --git a/proxmox-installer-types/src/answer.rs b/proxmox-installer-types/src/answer.rs
index d4c4acce..8482356c 100644
--- a/proxmox-installer-types/src/answer.rs
+++ b/proxmox-installer-types/src/answer.rs
@@ -14,6 +14,7 @@ use std::{
     str::FromStr,
 };
 
+use proxmox_network_api::{BondXmitHashPolicy, LinuxBondMode};
 use proxmox_network_types::{MacAddress, fqdn::Fqdn, ip_address::Cidr};
 
 #[cfg(feature = "api-types")]
@@ -440,6 +441,158 @@ pub struct NetworkInterfacePinningOptionsAnswer {
     pub mapping: HashMap<MacAddress, String>,
 }
 
+/// Specifies how members for the management network bond are selected.
+#[derive(Clone, Debug, PartialEq)]
+pub enum NetworkBondMemberSelection {
+    /// List of interface MAC addresses to add as bond members.
+    Fixed(Vec<MacAddress>),
+    /// Udev filter list for dynamically select interfaces as bond members.
+    Filter(BTreeMap<String, String>),
+}
+
+#[cfg_attr(feature = "api-types", api(
+    properties: {
+        "members-list": {
+            description: "List or filter of interface MAC addresses to add as bond members.",
+            type: Array,
+            items: {
+                type: MacAddress,
+            },
+        },
+        "members-filter": {
+            type: Object,
+            properties: {},
+            additional_properties: true,
+        }
+    },
+))]
+#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+/// Options for creating a bond for the management interface with the given
+/// physical interfaces and mode.
+pub struct NetworkBondOptions {
+    /// List of interface MAC addresses to add as bond members. Mutual exclusive with the
+    /// 'members_filter' option.
+    #[serde(default)]
+    pub members_list: Vec<MacAddress>,
+    /// Udev filter list for dynamically select interfaces as bond members. Mutual exclusive with
+    /// the 'members' option.
+    #[serde(default)]
+    pub members_filter: BTreeMap<String, String>,
+    /// Set whether it is enough that any filter matches on a disk or all given
+    /// filters must match to select a disk. Only useful in combination with 'members-filter'.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub members_filter_match: Option<FilterMatch>,
+    /// Mode for the bond to set.
+    pub mode: LinuxBondMode,
+    /// Bond hash policy. Only used with '802.3ad' and 'balance-xor' mode.
+    #[serde(default = "NetworkBondOptions::default_hash_policy")]
+    pub hash_policy: BondXmitHashPolicy,
+    /// Primary bond interface. Only used with 'active-backup' mode.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub primary_interface: Option<MacAddress>,
+}
+
+impl NetworkBondOptions {
+    /// List of all available bond modes.
+    pub const MODES: &[LinuxBondMode] = {
+        use LinuxBondMode::*;
+        &[
+            ActiveBackup,
+            BalanceAlb,
+            BalanceRr,
+            BalanceTlb,
+            BalanceXor,
+            Broadcast,
+            Ieee802_3ad,
+        ]
+    };
+
+    /// List of all available hash policies.
+    pub const HASH_POLICIES: &[BondXmitHashPolicy] = {
+        use BondXmitHashPolicy::*;
+        &[Layer2, Layer2_3, Layer3_4]
+    };
+
+    /// Returns the default hash policy for new bonds.
+    const fn default_hash_policy() -> BondXmitHashPolicy {
+        BondXmitHashPolicy::Layer2
+    }
+
+    /// Returns a bond configuration instance that disables any bonding setup in the low-level
+    /// installer.
+    pub const fn disabled() -> Self {
+        Self {
+            // an empty interface list and filter is interpreted as disabled by the low-level installer
+            members_list: Vec::new(),
+            members_filter: BTreeMap::new(),
+            members_filter_match: None,
+            mode: LinuxBondMode::ActiveBackup,
+            hash_policy: BondXmitHashPolicy::Layer2,
+            primary_interface: None,
+        }
+    }
+
+    /// Returns the given selection of bond members.
+    ///
+    /// If `Ok(None)` is returned, bonding is disabled.
+    pub fn members(&self) -> Result<Option<NetworkBondMemberSelection>> {
+        if self.members_list.is_empty() && self.members_filter.is_empty() {
+            Ok(None)
+        } else if !self.members_list.is_empty() && !self.members_filter.is_empty() {
+            bail!("Bond options 'members-list' and 'members-filter' are mutual exclusive");
+        } else if !self.members_list.is_empty() {
+            Ok(Some(NetworkBondMemberSelection::Fixed(
+                self.members_list.clone(),
+            )))
+        } else {
+            Ok(Some(NetworkBondMemberSelection::Filter(
+                self.members_filter.clone(),
+            )))
+        }
+    }
+
+    /// Does some basic checks on the options.
+    ///
+    /// This includes checks for:
+    /// - There are at least to members
+    /// - Whether the primary interface is applicable to the mode
+    /// - Whether the primary interface (if given) is a member
+    pub fn verify(&self) -> Result<()> {
+        let members = self.members()?;
+        let members = match members {
+            Some(NetworkBondMemberSelection::Fixed(list)) if list.len() < 2 => {
+                bail!(
+                    "bond setup requires at least 2 interfaces, got {}",
+                    list.len()
+                );
+            }
+            Some(sel) => sel,
+            None => bail!("bond setup requires either fixed list of members or udev filter list"),
+        };
+
+        if let Some(primary_if) = &self.primary_interface {
+            if self.mode != LinuxBondMode::ActiveBackup {
+                bail!("primary bond interface can only be set in active-backup mode");
+            }
+
+            match members {
+                NetworkBondMemberSelection::Fixed(list) if !list.contains(primary_if) => {
+                    bail!(
+                        "primary bond interface {primary_if} must be a member of the bond (members: {})",
+                        list.iter()
+                            .fold(String::new(), |acc, mac| format!("{acc}, {mac}"))
+                            .trim_start_matches(", ")
+                    );
+                }
+                _ => {}
+            }
+        }
+
+        Ok(())
+    }
+}
+
 #[cfg_attr(feature = "api-types", api(
     properties: {
         filter: {
@@ -466,6 +619,9 @@ pub struct NetworkConfigFromAnswer {
     /// Off by default. Allowed for both `from-dhcp` and `from-answer` modes.
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub interface_name_pinning: Option<NetworkInterfacePinningOptionsAnswer>,
+    /// If set, sets up a Linux bond for the management interface.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub bond: Option<NetworkBondOptions>,
 }
 
 #[cfg_attr(feature = "api-types", api)]
@@ -477,6 +633,9 @@ pub struct NetworkConfigFromDhcp {
     /// Off by default. Allowed for both `from-dhcp` and `from-answer` modes.
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub interface_name_pinning: Option<NetworkInterfacePinningOptionsAnswer>,
+    /// If set, sets up a Linux bond for the management interface.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub bond: Option<NetworkBondOptions>,
 }
 
 #[cfg_attr(feature = "api-types", api(
@@ -505,6 +664,14 @@ impl NetworkConfig {
             Self::FromAnswer(answer) => answer.interface_name_pinning.as_ref(),
         }
     }
+
+    /// Returns the management network Linux bond options, if any.
+    pub const fn bond(&self) -> Option<&NetworkBondOptions> {
+        match self {
+            Self::FromDhcp(dhcp) => dhcp.bond.as_ref(),
+            Self::FromAnswer(answer) => answer.bond.as_ref(),
+        }
+    }
 }
 
 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
@@ -1477,4 +1644,75 @@ mod tests {
         };
         assert!(setup.filesystem_details().is_ok());
     }
+
+    #[test]
+    fn network_bond_requires_at_least_two_members() {
+        let mut bond = NetworkBondOptions::disabled();
+        let res = bond.verify();
+        assert!(res.is_err());
+        assert_eq!(
+            res.unwrap_err().to_string(),
+            "bond setup requires at least 2 interfaces, got 0"
+        );
+
+        bond.members = vec!["12:34:56:ab:cd:df".parse().unwrap()];
+
+        let res = bond.verify();
+        assert!(res.is_err());
+        assert_eq!(
+            res.unwrap_err().to_string(),
+            "bond setup requires at least 2 interfaces, got 1"
+        );
+
+        bond.members = vec![
+            "12:34:56:ab:cd:df".parse().unwrap(),
+            "ab:cd:df:12:34:56".parse().unwrap(),
+        ];
+        assert!(bond.verify().is_ok());
+    }
+
+    #[test]
+    fn network_bond_primary_if_only_with_active_backup() {
+        let mut bond = NetworkBondOptions::disabled();
+        bond.members = vec![
+            "12:34:56:ab:cd:df".parse().unwrap(),
+            "ab:cd:df:12:34:56".parse().unwrap(),
+        ];
+        bond.primary_interface = Some("12:34:56:ab:cd:df".parse().unwrap());
+
+        for m in NetworkBondOptions::MODES {
+            bond.mode = *m;
+            match m {
+                LinuxBondMode::ActiveBackup => assert!(bond.verify().is_ok()),
+                _ => {
+                    let res = bond.verify();
+                    assert!(res.is_err());
+                    assert_eq!(
+                        res.unwrap_err().to_string(),
+                        "primary bond interface can only be set in active-backup mode"
+                    );
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn network_bond_primary_if_must_be_member() {
+        let mut bond = NetworkBondOptions::disabled();
+        bond.members = vec![
+            "12:34:56:ab:cd:df".parse().unwrap(),
+            "ab:cd:df:12:34:56".parse().unwrap(),
+        ];
+        bond.primary_interface = Some("12:34:56:ab:cd:df".parse().unwrap());
+        assert!(bond.verify().is_ok());
+
+        bond.primary_interface = Some("ab:12:cd:34:ef:56".parse().unwrap());
+
+        let res = bond.verify();
+        assert!(res.is_err());
+        assert_eq!(
+            res.unwrap_err().to_string(),
+            "primary bond interface AB:12:CD:34:EF:56 must be a member of the bond (members: 12:34:56:AB:CD:DF, AB:CD:DF:12:34:56)"
+        );
+    }
 }
-- 
2.55.0





  parent reply	other threads:[~2026-11-02 12:46 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 ` Christoph Heiss [this message]
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 ` [PATCH installer v4 26/30] tui: network: add vlan " Christoph Heiss
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-5-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal