From: Christoph Heiss <c.heiss@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH installer 04/15] tree-wide: use `MacAddress` type instead of string for MAC addresses
Date: Tue, 8 Sep 2026 12:16:18 +0200 [thread overview]
Message-ID: <20260908101647.1057780-5-c.heiss@proxmox.com> (raw)
In-Reply-To: <20260908101647.1057780-1-c.heiss@proxmox.com>
Makes all Rust code dealing with MAC addresses a lot more typesafe. The
`MacAddress` type is also cheaper to construct than `String`, since the
former is just 6 bytes and avoids heap allocations.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-auto-installer/tests/parse-answer.rs | 2 +-
...ace_pinning_mixed_case_mac_addresses.json} | 0
...ace_pinning_mixed_case_mac_addresses.toml} | 4 +-
...rface_pinning_overlong_interface_name.json | 2 +-
proxmox-installer-common/src/options.rs | 66 +++++++++----------
proxmox-installer-common/src/setup.rs | 28 ++++++--
proxmox-post-hook/src/main.rs | 2 +-
proxmox-tui-installer/src/views/network.rs | 26 +++-----
8 files changed, 68 insertions(+), 62 deletions(-)
rename proxmox-auto-installer/tests/resources/parse_answer/{network_interface_pinning_uppercase_mac_address.json => network_interface_pinning_mixed_case_mac_addresses.json} (100%)
rename proxmox-auto-installer/tests/resources/parse_answer/{network_interface_pinning_uppercase_mac_address.toml => network_interface_pinning_mixed_case_mac_addresses.toml} (85%)
diff --git a/proxmox-auto-installer/tests/parse-answer.rs b/proxmox-auto-installer/tests/parse-answer.rs
index 8b0bae7..ae5b87d 100644
--- a/proxmox-auto-installer/tests/parse-answer.rs
+++ b/proxmox-auto-installer/tests/parse-answer.rs
@@ -128,7 +128,7 @@ mod tests {
hashed_root_password,
minimal,
network_interface_pinning,
- network_interface_pinning_uppercase_mac_address,
+ network_interface_pinning_mixed_case_mac_addresses,
nic_matching,
no_network,
specific_nic,
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_uppercase_mac_address.json b/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_mixed_case_mac_addresses.json
similarity index 100%
rename from proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_uppercase_mac_address.json
rename to proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_mixed_case_mac_addresses.json
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_uppercase_mac_address.toml b/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_mixed_case_mac_addresses.toml
similarity index 85%
rename from proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_uppercase_mac_address.toml
rename to proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_mixed_case_mac_addresses.toml
index 6681fe9..deb1fd3 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_uppercase_mac_address.toml
+++ b/proxmox-auto-installer/tests/resources/parse_answer/network_interface_pinning_mixed_case_mac_addresses.toml
@@ -14,8 +14,8 @@ enabled = true
[network.interface-name-pinning.mapping]
"24:8A:07:1E:05:BC" = "lan0"
-"24:8A:07:1E:05:BD" = "lan1"
-"B4:2E:99:AC:AD:B4" = "mgmt"
+"24:8a:07:1e:05:bd" = "lan1"
+"B4:2e:99:ac:AD:b4" = "mgmt"
[disk-setup]
filesystem = "ext4"
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/network_interface_pinning_overlong_interface_name.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_interface_pinning_overlong_interface_name.json
index f3c9169..952b34a 100644
--- a/proxmox-auto-installer/tests/resources/parse_answer_fail/network_interface_pinning_overlong_interface_name.json
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/network_interface_pinning_overlong_interface_name.json
@@ -1,3 +1,3 @@
{
- "error": "interface name 'waytoolonginterfacename' for 'ab:cd:ef:12:34:56' cannot be longer than 15 characters"
+ "error": "interface name 'waytoolonginterfacename' for 'AB:CD:EF:12:34:56' cannot be longer than 15 characters"
}
diff --git a/proxmox-installer-common/src/options.rs b/proxmox-installer-common/src/options.rs
index c7f0baf..fbbfe77 100644
--- a/proxmox-installer-common/src/options.rs
+++ b/proxmox-installer-common/src/options.rs
@@ -19,7 +19,7 @@ use proxmox_installer_types::{
ZfsChecksumOption, ZfsCompressOption, ZfsRaidLevel,
},
};
-use proxmox_network_types::{fqdn::Fqdn, ip_address::Cidr};
+use proxmox_network_types::{MacAddress, fqdn::Fqdn, ip_address::Cidr};
pub trait RaidLevel {
/// Returns the minimum number of disks needed for this RAID level.
@@ -315,7 +315,7 @@ impl TimezoneOptions {
pub struct NetworkInterfacePinningOptions {
/// Maps MAC address to custom name
#[serde(default)]
- pub mapping: HashMap<String, String>,
+ pub mapping: HashMap<MacAddress, String>,
}
impl NetworkInterfacePinningOptions {
@@ -341,7 +341,7 @@ impl NetworkInterfacePinningOptions {
.unwrap()
});
- let mut reverse_mapping = HashMap::<String, String>::new();
+ let mut reverse_mapping = HashMap::<String, MacAddress>::new();
for (mac, name) in self.mapping.iter() {
if name.len() < MIN_IFNAME_LEN {
bail!(
@@ -368,7 +368,7 @@ impl NetworkInterfacePinningOptions {
bail!("duplicate interface name mapping '{name}' for: {mac}, {duplicate_mac}");
}
- reverse_mapping.insert(name.clone(), mac.clone());
+ reverse_mapping.insert(name.clone(), *mac);
}
Ok(())
@@ -379,13 +379,7 @@ impl From<&NetworkInterfacePinningOptionsAnswer> for NetworkInterfacePinningOpti
fn from(answer: &NetworkInterfacePinningOptionsAnswer) -> Self {
if answer.enabled {
Self {
- // convert all MAC addresses to lowercase before further usage,
- // to enable easy comparison
- mapping: answer
- .mapping
- .iter()
- .map(|(k, v)| (k.to_lowercase(), v.clone()))
- .collect(),
+ mapping: answer.mapping.clone(),
}
} else {
Self::default()
@@ -480,7 +474,7 @@ impl NetworkOptions {
// required by the low-level installer
for iface in network.interfaces.values() {
if let Some(pinned) = iface.to_pinned(opts) {
- opts.mapping.entry(iface.mac.clone()).or_insert(pinned.name);
+ opts.mapping.entry(iface.mac).or_insert(pinned.name);
}
}
}
@@ -609,7 +603,7 @@ mod tests {
pinned_id: Some("0".to_owned()),
state: InterfaceState::Up,
driver: "dummy".to_owned(),
- mac: "01:23:45:67:89:ab".to_owned(),
+ mac: "01:23:45:67:89:ab".parse().unwrap(),
addresses: vec![Cidr::new(Ipv4Addr::new(192, 168, 0, 2), 24).unwrap()],
},
);
@@ -699,7 +693,7 @@ mod tests {
pinned_id: Some("0".to_owned()),
state: InterfaceState::Up,
driver: "dummy".to_owned(),
- mac: "01:23:45:67:89:ab".to_owned(),
+ mac: "01:23:45:67:89:ab".parse().unwrap(),
addresses: vec![
Cidr::new(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2), 64).unwrap(),
],
@@ -798,7 +792,7 @@ mod tests {
pinned_id: Some("0".to_owned()),
state: InterfaceState::Up,
driver: "dummy".to_owned(),
- mac: "01:23:45:67:89:ab".to_owned(),
+ mac: "01:23:45:67:89:ab".parse().unwrap(),
addresses: vec![],
},
);
@@ -836,13 +830,13 @@ mod tests {
let mut options = NetworkInterfacePinningOptions::default();
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), String::new());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), String::new());
let res = options.verify();
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
- "interface name for 'ab:cd:ef:12:34:56' must be at least 2 characters long"
+ "interface name for 'AB:CD:EF:12:34:56' must be at least 2 characters long"
)
}
@@ -851,13 +845,13 @@ mod tests {
let mut options = NetworkInterfacePinningOptions::default();
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "a".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "a".to_owned());
let res = options.verify();
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
- "interface name for 'ab:cd:ef:12:34:56' must be at least 2 characters long"
+ "interface name for 'AB:CD:EF:12:34:56' must be at least 2 characters long"
)
}
@@ -865,7 +859,7 @@ mod tests {
fn network_interface_pinning_options_fail_on_overlong_name() {
let mut options = NetworkInterfacePinningOptions::default();
options.mapping.insert(
- "ab:cd:ef:12:34:56".to_owned(),
+ "ab:cd:ef:12:34:56".parse().unwrap(),
"waytoolonginterfacename".to_owned(),
);
@@ -873,7 +867,7 @@ mod tests {
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
- "interface name 'waytoolonginterfacename' for 'ab:cd:ef:12:34:56' cannot be longer than 15 characters"
+ "interface name 'waytoolonginterfacename' for 'AB:CD:EF:12:34:56' cannot be longer than 15 characters"
)
}
@@ -882,10 +876,10 @@ mod tests {
let mut options = NetworkInterfacePinningOptions::default();
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "nic0".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "nic0".to_owned());
options
.mapping
- .insert("12:34:56:ab:cd:ef".to_owned(), "nic0".to_owned());
+ .insert("12:34:56:ab:cd:ef".parse().unwrap(), "nic0".to_owned());
let res = options.verify();
assert!(res.is_err());
@@ -894,8 +888,8 @@ mod tests {
// [HashMap] does not guarantee iteration order, so just check for the substrings
// we expect to find
assert!(err.contains("duplicate interface name mapping 'nic0' for: "));
- assert!(err.contains("12:34:56:ab:cd:ef"));
- assert!(err.contains("ab:cd:ef:12:34:56"));
+ assert!(err.contains("12:34:56:AB:CD:EF"));
+ assert!(err.contains("AB:CD:EF:12:34:56"));
}
#[test]
@@ -903,13 +897,13 @@ mod tests {
let mut options = NetworkInterfacePinningOptions::default();
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "nic-".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "nic-".to_owned());
let res = options.verify();
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
- "interface name 'nic-' for 'ab:cd:ef:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
+ "interface name 'nic-' for 'AB:CD:EF:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
)
}
@@ -918,24 +912,24 @@ mod tests {
let mut options = NetworkInterfacePinningOptions::default();
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "0nic".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "0nic".to_owned());
let res = options.verify();
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
- "interface name '0nic' for 'ab:cd:ef:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
+ "interface name '0nic' for 'AB:CD:EF:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
);
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "_a".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "_a".to_owned());
let res = options.verify();
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
- "interface name '_a' for 'ab:cd:ef:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
+ "interface name '_a' for 'AB:CD:EF:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
);
}
@@ -944,21 +938,21 @@ mod tests {
let mut options = NetworkInterfacePinningOptions::default();
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "Nic0".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "Nic0".to_owned());
let res = options.verify();
assert!(res.is_ok());
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "nIc0".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "nIc0".to_owned());
let res = options.verify();
assert!(res.is_ok());
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "nic0".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "nic0".to_owned());
let res = options.verify();
assert!(res.is_ok());
@@ -969,13 +963,13 @@ mod tests {
let mut options = NetworkInterfacePinningOptions::default();
options
.mapping
- .insert("ab:cd:ef:12:34:56".to_owned(), "12345".to_owned());
+ .insert("ab:cd:ef:12:34:56".parse().unwrap(), "12345".to_owned());
let res = options.verify();
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
- "interface name '12345' for 'ab:cd:ef:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
+ "interface name '12345' for 'AB:CD:EF:12:34:56' is invalid: name must start with a letter and contain only ascii characters, digits and underscores"
)
}
}
diff --git a/proxmox-installer-common/src/setup.rs b/proxmox-installer-common/src/setup.rs
index b8af4e3..14882bc 100644
--- a/proxmox-installer-common/src/setup.rs
+++ b/proxmox-installer-common/src/setup.rs
@@ -1,4 +1,4 @@
-use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
+use serde::{Deserialize, Deserializer, Serialize, Serializer, de, ser::SerializeMap};
use std::{
cmp,
collections::{BTreeMap, HashMap},
@@ -18,7 +18,7 @@ use proxmox_installer_types::{
BootType, IsoInfo, ProductConfig,
answer::{BtrfsCompressOption, FilesystemType, ZfsChecksumOption, ZfsCompressOption},
};
-use proxmox_network_types::Cidr;
+use proxmox_network_types::{Cidr, MacAddress};
/// Paths in the ISO environment containing installer data.
#[derive(Clone, Deserialize)]
@@ -395,7 +395,7 @@ pub struct Interface {
/// interface name cannot/should not be pinned due to being e.g. a non-physical link.
pub pinned_id: Option<String>,
- pub mac: String,
+ pub mac: MacAddress,
pub state: InterfaceState,
@@ -535,9 +535,10 @@ pub struct InstallConfig {
#[serde(
default,
skip_serializing_if = "HashMap::is_empty",
- deserialize_with = "deserialize_optional_map"
+ deserialize_with = "deserialize_optional_map",
+ serialize_with = "serialize_low_level_mac_address_map"
)]
- pub network_interface_pin_map: HashMap<String, String>,
+ pub network_interface_pin_map: HashMap<MacAddress, String>,
pub hostname: String,
pub domain: String,
@@ -583,3 +584,20 @@ where
let map: Option<HashMap<K, V>> = Deserialize::deserialize(deserializer)?;
Ok(map.unwrap_or_default())
}
+
+/// Serializes a [HashMap] using [MacAddress] as keys, normalizing MAC addresses into their
+/// lower-case format as the low-level installer expects it.
+fn serialize_low_level_mac_address_map<S, V>(
+ map: &HashMap<MacAddress, V>,
+ serializer: S,
+) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+ V: Serialize,
+{
+ let mut serializer = serializer.serialize_map(Some(map.len()))?;
+ for (mac, v) in map {
+ serializer.serialize_entry(&mac.to_string().to_lowercase(), &v)?;
+ }
+ serializer.end()
+}
diff --git a/proxmox-post-hook/src/main.rs b/proxmox-post-hook/src/main.rs
index 749fd0e..e50a773 100644
--- a/proxmox-post-hook/src/main.rs
+++ b/proxmox-post-hook/src/main.rs
@@ -286,7 +286,7 @@ mod detail {
anyhow::Ok(NetworkInterfaceInfo {
name: ifname.clone(),
- mac: nic.mac.clone(),
+ mac: nic.mac,
// Use the actual IP address from the low-level install config, as the runtime info
// contains the original IP address from DHCP.
address: is_management.then_some(config.cidr),
diff --git a/proxmox-tui-installer/src/views/network.rs b/proxmox-tui-installer/src/views/network.rs
index 12cef19..6401174 100644
--- a/proxmox-tui-installer/src/views/network.rs
+++ b/proxmox-tui-installer/src/views/network.rs
@@ -17,12 +17,12 @@ use proxmox_installer_common::{
options::{NetworkInterfacePinningOptions, NetworkOptions},
setup::{Interface, NetworkInfo},
};
-use proxmox_network_types::{fqdn::Fqdn, ip_address::Cidr};
+use proxmox_network_types::{MacAddress, fqdn::Fqdn, ip_address::Cidr};
use super::{CidrAddressEditView, FormView};
struct NetworkViewOptions {
- selected_mac: String,
+ selected_mac: MacAddress,
pinning_enabled: bool,
// For UI purposes, we want to always save the mapping, to save the state
// between toggling the checkbox
@@ -55,14 +55,8 @@ impl NetworkOptionsView {
let selected_mac = network_info
.interfaces
.get(&options.ifname)
- .map(|iface| iface.mac.clone())
- .unwrap_or_else(|| {
- ifaces
- .first()
- .expect("at least one network interface")
- .mac
- .clone()
- });
+ .map(|iface| iface.mac)
+ .unwrap_or_else(|| ifaces.first().expect("at least one network interface").mac);
let options_ref = Arc::new(Mutex::new(NetworkViewOptions {
selected_mac,
@@ -309,7 +303,7 @@ impl NetworkOptionsView {
.on_submit({
let options_ref = options_ref.clone();
move |_, iface| {
- options_ref.lock().expect("unpoisoned lock").selected_mac = iface.mac.clone();
+ options_ref.lock().expect("unpoisoned lock").selected_mac = iface.mac;
}
});
@@ -330,7 +324,7 @@ impl ViewWrapper for NetworkOptionsView {
}
struct InterfacePinningOptionsView {
- view: ScrollView<NamedView<FormView<String>>>,
+ view: ScrollView<NamedView<FormView<MacAddress>>>,
}
impl InterfacePinningOptionsView {
@@ -344,7 +338,7 @@ impl InterfacePinningOptionsView {
// The low-level installer will skip them anyway.
let interfaces = interfaces.iter().filter(|iface| iface.pinned_id.is_some());
- let mut form = FormView::<String>::new();
+ let mut form = FormView::<MacAddress>::new();
for iface in interfaces {
let label = format!(
@@ -366,7 +360,7 @@ impl InterfacePinningOptionsView {
.fixed_width(MAX_IFNAME_LEN),
);
- form.add_child_with_data(&label, view, iface.mac.clone());
+ form.add_child_with_data(&label, view, iface.mac);
if !iface.addresses.is_empty() {
for chunk in iface.addresses.chunks(2) {
@@ -403,7 +397,7 @@ impl InterfacePinningOptionsView {
.map(|v| v.get_inner().get_content())
.ok_or_else(|| format!("failed to retrieve pinning ID for interface {}", mac))?;
- mapping.insert(mac.clone(), (*name).clone());
+ mapping.insert(*mac, (*name).clone());
}
let opts = NetworkInterfacePinningOptions { mapping };
@@ -414,5 +408,5 @@ impl InterfacePinningOptionsView {
}
impl ViewWrapper for InterfacePinningOptionsView {
- cursive::wrap_impl!(self.view: ScrollView<NamedView<FormView<String>>>);
+ cursive::wrap_impl!(self.view: ScrollView<NamedView<FormView<MacAddress>>>);
}
--
2.55.0
next prev parent reply other threads:[~2026-09-08 10:17 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 ` Christoph Heiss [this message]
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 ` [PATCH installer 15/15] tui: network: add bond setup options to advanced network dialog 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=20260908101647.1057780-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.