Fix copy-mac-from for down interfaces
- Fix copy-mac-from failing when source interface is down. RHEL-214096 - Fix OriginalName link match for stacked interfaces. RHEL-223630 - Skip alt-name removal for deleted virtual interfaces. - Pin physical non ethernet alt-name match. - Derive alt-name link match ffrom interface identifier. Resolves: RHEL-214096 RHEL-223630 Signed-off-by: Rahul Rajesh <rrajesh@redhat.com>
This commit is contained in:
parent
ec9991b370
commit
afb6081cc1
134
0002-iface-include-existing-interfaces-in-name-search.patch
Normal file
134
0002-iface-include-existing-interfaces-in-name-search.patch
Normal file
@ -0,0 +1,134 @@
|
||||
From 69df418c0a918163d84cddaa0cf45e6c302c8103 Mon Sep 17 00:00:00 2001
|
||||
From: Jan Vaclav <jvaclav@redhat.com>
|
||||
Date: Wed, 29 Jul 2026 11:07:22 +0200
|
||||
Subject: [PATCH] iface: include existing interfaces in name search
|
||||
|
||||
InterfaceNameSearch filtered kernel_nics to only up interfaces, which
|
||||
excluded existing interfaces that were not UP in the merged state,
|
||||
which broke copy-mac-from when referencing an interface that exists in
|
||||
current state but is being added as a bond/bridge port in the same
|
||||
transaction. The port's merged state isn't Up, so it gets filtered out
|
||||
of kernel_nics.
|
||||
|
||||
Include all existing interfaces in kernel_nics, and only filter desired
|
||||
interfaces to those that will be up.
|
||||
|
||||
Fixes: aaf11db38e97 ("iface: support matching against alt-name in copy-mac-from")
|
||||
Signed-off-by: Jan Vaclav <jvaclav@redhat.com>
|
||||
Assisted-by: Claude Sonnet 4.5
|
||||
---
|
||||
rust/src/lib/ifaces/inter_ifaces.rs | 3 +-
|
||||
rust/src/lib/unit_tests/alt_name.rs | 82 +++++++++++++++++++++++++++++
|
||||
2 files changed, 84 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/rust/src/lib/ifaces/inter_ifaces.rs b/rust/src/lib/ifaces/inter_ifaces.rs
|
||||
index 09c5a10f82..76b83a273b 100644
|
||||
--- a/rust/src/lib/ifaces/inter_ifaces.rs
|
||||
+++ b/rust/src/lib/ifaces/inter_ifaces.rs
|
||||
@@ -1248,7 +1248,7 @@ impl InterfaceNameSearch {
|
||||
{
|
||||
let mut kernel_nics: HashSet<String> = HashSet::new();
|
||||
|
||||
- for iface in merged_ifaces.clone().filter(|i| i.merged.is_up()) {
|
||||
+ for iface in merged_ifaces.clone() {
|
||||
if let Some(cur_iface) = iface.current.as_ref() {
|
||||
// Existing kernel interface
|
||||
kernel_nics.insert(cur_iface.name().to_string());
|
||||
@@ -1256,6 +1256,7 @@ impl InterfaceNameSearch {
|
||||
// Creating new kernel interface
|
||||
if des_iface.base_iface().identifier.as_ref()
|
||||
!= Some(&InterfaceIdentifier::MacAddress)
|
||||
+ && des_iface.is_up()
|
||||
{
|
||||
kernel_nics.insert(des_iface.name().to_string());
|
||||
}
|
||||
diff --git a/rust/src/lib/unit_tests/alt_name.rs b/rust/src/lib/unit_tests/alt_name.rs
|
||||
index 4fa417df7c..8ff35f7d96 100644
|
||||
--- a/rust/src/lib/unit_tests/alt_name.rs
|
||||
+++ b/rust/src/lib/unit_tests/alt_name.rs
|
||||
@@ -420,3 +420,85 @@ fn test_do_not_resolve_mac_identifer_iface_to_alt_name() {
|
||||
assert!(merged_iface.for_verify.is_none());
|
||||
assert!(merged_iface.desired.is_none());
|
||||
}
|
||||
+
|
||||
+#[test]
|
||||
+fn test_copy_mac_from_existing_down_port() {
|
||||
+ let desired: NetworkState = serde_yaml::from_str(
|
||||
+ r"---
|
||||
+ interfaces:
|
||||
+ - name: bond99
|
||||
+ type: bond
|
||||
+ state: up
|
||||
+ copy-mac-from: eth1
|
||||
+ link-aggregation:
|
||||
+ mode: balance-rr
|
||||
+ port:
|
||||
+ - eth1
|
||||
+ - eth2",
|
||||
+ )
|
||||
+ .unwrap();
|
||||
+ let current: NetworkState = serde_yaml::from_str(
|
||||
+ r"---
|
||||
+ interfaces:
|
||||
+ - name: eth1
|
||||
+ type: ethernet
|
||||
+ state: down
|
||||
+ mac-address: AA:BB:CC:DD:EE:FF
|
||||
+ - name: eth2
|
||||
+ type: ethernet
|
||||
+ state: up
|
||||
+ mac-address: 11:22:33:44:55:66",
|
||||
+ )
|
||||
+ .unwrap();
|
||||
+
|
||||
+ let merged_state =
|
||||
+ MergedNetworkState::new(desired, current, Default::default(), false)
|
||||
+ .unwrap();
|
||||
+
|
||||
+ let bond_iface =
|
||||
+ merged_state.interfaces.kernel_ifaces.get("bond99").unwrap();
|
||||
+ let bond_mac = bond_iface
|
||||
+ .for_apply
|
||||
+ .as_ref()
|
||||
+ .and_then(|i| i.base_iface().mac_address.as_ref());
|
||||
+
|
||||
+ assert_eq!(bond_mac, Some(&"AA:BB:CC:DD:EE:FF".to_string()));
|
||||
+}
|
||||
+
|
||||
+#[test]
|
||||
+fn test_copy_mac_from_new_down_interface_fails() {
|
||||
+ let desired: NetworkState = serde_yaml::from_str(
|
||||
+ r"---
|
||||
+ interfaces:
|
||||
+ - name: eth1
|
||||
+ type: ethernet
|
||||
+ state: down
|
||||
+ mac-address: AA:BB:CC:DD:EE:FF
|
||||
+ - name: bond99
|
||||
+ type: bond
|
||||
+ state: up
|
||||
+ copy-mac-from: eth1
|
||||
+ link-aggregation:
|
||||
+ mode: balance-rr
|
||||
+ port:
|
||||
+ - eth2",
|
||||
+ )
|
||||
+ .unwrap();
|
||||
+ let current: NetworkState = serde_yaml::from_str(
|
||||
+ r"---
|
||||
+ interfaces:
|
||||
+ - name: eth2
|
||||
+ type: ethernet
|
||||
+ state: up
|
||||
+ mac-address: 11:22:33:44:55:66",
|
||||
+ )
|
||||
+ .unwrap();
|
||||
+
|
||||
+ let result =
|
||||
+ MergedNetworkState::new(desired, current, Default::default(), false);
|
||||
+
|
||||
+ let e = result.unwrap_err();
|
||||
+ assert_eq!(e.kind(), ErrorKind::InvalidArgument);
|
||||
+ assert!(e.msg().contains("eth1"));
|
||||
+ assert!(e.msg().contains("copy-mac-from"));
|
||||
+}
|
||||
275
0003-nispor-derive-alt-name-link-match-from-interface-ide.patch
Normal file
275
0003-nispor-derive-alt-name-link-match-from-interface-ide.patch
Normal file
@ -0,0 +1,275 @@
|
||||
From 45e5ed38d1f157e5963628041a461f4073c3295d Mon Sep 17 00:00:00 2001
|
||||
From: Josephine Pfeiffer <josie@redhat.com>
|
||||
Date: Mon, 6 Jul 2026 20:02:47 +0200
|
||||
Subject: [PATCH 3/6] nispor: derive alt-name link match from interface
|
||||
identifier
|
||||
|
||||
Route systemd `.link` `[Match]` generation through a single
|
||||
`gen_systemd_match_rules` that dispatches on the interface identifier,
|
||||
resolve the identifier in `effective_identifier`, and move the shared
|
||||
`PermanentMACAddress`/`MACAddress`/`Driver` formatting into
|
||||
`mac_match_rules`.
|
||||
|
||||
When the desired state omits the identifier, `effective_identifier`
|
||||
falls back to the current interface's identifier. The guard that makes
|
||||
this safe for stacked interfaces comes next.
|
||||
|
||||
Signed-off-by: Josephine Pfeiffer <josie@redhat.com>
|
||||
---
|
||||
rust/src/lib/nispor/alt_name.rs | 213 +++++++++++++++++---------------
|
||||
1 file changed, 116 insertions(+), 97 deletions(-)
|
||||
|
||||
diff --git a/rust/src/lib/nispor/alt_name.rs b/rust/src/lib/nispor/alt_name.rs
|
||||
index a8188934..3251f31c 100644
|
||||
--- a/rust/src/lib/nispor/alt_name.rs
|
||||
+++ b/rust/src/lib/nispor/alt_name.rs
|
||||
@@ -6,7 +6,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::{
|
||||
BaseInterface, ErrorKind, InterfaceIdentifier, InterfaceState,
|
||||
- InterfaceType, MergedInterfaces, NmstateError,
|
||||
+ MergedInterfaces, NmstateError,
|
||||
};
|
||||
|
||||
/// Comment added into our generated link files
|
||||
@@ -199,70 +199,133 @@ async fn remove_systemd_network_link_file(
|
||||
}
|
||||
}
|
||||
|
||||
-// The `OriginalName` match rule is unstable because kernel interface
|
||||
-// name can not be predicted.
|
||||
-// For ethernet interface, using `OriginalName` match rule is unstable because
|
||||
-// kernel interface name cannot be predicted. Hence use try in these order:
|
||||
-// * permanent MAC address + driver name[1]
|
||||
-// * permanent MAC address
|
||||
-// * PCI address
|
||||
-// * MAC address + driver defined in current
|
||||
-// * MAC address defined in desired
|
||||
-// * Interface name defined in desired
|
||||
-// For non-ethernet interface, we use `OriginalName` match rule.
|
||||
-// [1]: Azure VM with `Microsoft Azure Network Adapter` has two
|
||||
-// NICs holding the same MAC address. We need to make sure we matched
|
||||
-// the correct by including driver information also.
|
||||
-fn gen_systemd_network_link_match_rules(
|
||||
+fn gen_systemd_match_rules(
|
||||
+ des_iface: &BaseInterface,
|
||||
+ cur_iface: Option<&BaseInterface>,
|
||||
+) -> Result<String, NmstateError> {
|
||||
+ let (identifier, src_iface) = effective_identifier(des_iface, cur_iface);
|
||||
+
|
||||
+ match identifier {
|
||||
+ InterfaceIdentifier::MacAddress => {
|
||||
+ // Include driver in case two NICs hold the same MAC.
|
||||
+ // (e.g. Microsoft Azure Network Adapter)
|
||||
+ let driver = cur_iface.and_then(|i| i.driver.as_ref());
|
||||
+ mac_match_rules(
|
||||
+ src_iface.permanent_mac_address.as_ref(),
|
||||
+ src_iface.mac_address.as_ref(),
|
||||
+ driver,
|
||||
+ )
|
||||
+ .ok_or_else(|| {
|
||||
+ NmstateError::new(
|
||||
+ ErrorKind::Bug,
|
||||
+ format!(
|
||||
+ "Got no MAC address for `identifier:mac-address` on \
|
||||
+ interface {}, should be failed by previous checker",
|
||||
+ des_iface.name
|
||||
+ ),
|
||||
+ )
|
||||
+ })
|
||||
+ }
|
||||
+ InterfaceIdentifier::PciAddress => {
|
||||
+ if let Some(pci_addr) = src_iface.pci_address.as_ref() {
|
||||
+ Ok(format!("Path=pci-{pci_addr}"))
|
||||
+ } else {
|
||||
+ Err(NmstateError::new(
|
||||
+ ErrorKind::Bug,
|
||||
+ format!(
|
||||
+ "Got no PCI address for `identifier:pci-address` on \
|
||||
+ interface {}, should be failed by previous checker",
|
||||
+ des_iface.name
|
||||
+ ),
|
||||
+ ))
|
||||
+ }
|
||||
+ }
|
||||
+ InterfaceIdentifier::Name => {
|
||||
+ Ok(gen_systemd_name_match_rules(des_iface, cur_iface))
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// Resolve the identifier to match on and the interface that supplies its
|
||||
+// MAC/PCI value. Desired state's explicit identifier is honoured as-is; when it
|
||||
+// is absent the current interface's identifier is inherited.
|
||||
+fn effective_identifier<'a>(
|
||||
+ des_iface: &'a BaseInterface,
|
||||
+ cur_iface: Option<&'a BaseInterface>,
|
||||
+) -> (InterfaceIdentifier, &'a BaseInterface) {
|
||||
+ if let Some(id) = des_iface.identifier {
|
||||
+ return (id, des_iface);
|
||||
+ }
|
||||
+ if let Some(cur) = cur_iface {
|
||||
+ match cur.identifier {
|
||||
+ Some(InterfaceIdentifier::MacAddress) => {
|
||||
+ return (InterfaceIdentifier::MacAddress, cur);
|
||||
+ }
|
||||
+ Some(InterfaceIdentifier::PciAddress) => {
|
||||
+ return (InterfaceIdentifier::PciAddress, cur);
|
||||
+ }
|
||||
+ _ => {}
|
||||
+ }
|
||||
+ }
|
||||
+ (InterfaceIdentifier::Name, des_iface)
|
||||
+}
|
||||
+
|
||||
+// Fallback used when the resolved identifier is `name` (the default).
|
||||
+//
|
||||
+// The kernel name is unpredictable, so match on the most stable attribute
|
||||
+// available: permanent MAC, then PCI path, then current MAC, each with driver
|
||||
+// when known to disambiguate NICs sharing a MAC (e.g. Azure's Microsoft Azure
|
||||
+// Network Adapter).
|
||||
+fn gen_systemd_name_match_rules(
|
||||
des_iface: &BaseInterface,
|
||||
cur_iface: Option<&BaseInterface>,
|
||||
) -> String {
|
||||
if let Some(cur_iface) = cur_iface {
|
||||
- match (
|
||||
+ if let Some(rules) = mac_match_rules(
|
||||
cur_iface.permanent_mac_address.as_ref(),
|
||||
+ None,
|
||||
cur_iface.driver.as_ref(),
|
||||
- cur_iface.pci_address.as_ref(),
|
||||
+ ) {
|
||||
+ rules
|
||||
+ } else if let Some(pci_addr) = cur_iface.pci_address.as_ref() {
|
||||
+ format!("Path=pci-{pci_addr}")
|
||||
+ } else if let Some(rules) = mac_match_rules(
|
||||
+ None,
|
||||
cur_iface.mac_address.as_ref(),
|
||||
+ cur_iface.driver.as_ref(),
|
||||
) {
|
||||
- (Some(perm_mac), Some(driver), _pci_addr, _mac) => {
|
||||
- format!("PermanentMACAddress={perm_mac}\nDriver={driver}")
|
||||
- }
|
||||
- (Some(perm_mac), None, _pci_addr, _mac) => {
|
||||
- format!("PermanentMACAddress={perm_mac}")
|
||||
- }
|
||||
- (None, _driver, Some(pci_addr), _mac) => {
|
||||
- format!("Path=pci-{pci_addr}")
|
||||
- }
|
||||
- (None, Some(driver), None, Some(mac)) => {
|
||||
- format!("MACAddress={mac}\nDriver={driver}")
|
||||
- }
|
||||
- (None, None, None, Some(mac)) => {
|
||||
- format!("MACAddress={mac}")
|
||||
- }
|
||||
- _ => {
|
||||
- format!("OriginalName={}", cur_iface.name)
|
||||
- }
|
||||
- }
|
||||
- } else {
|
||||
- // User is creating software interface or gen_conf mode
|
||||
- if des_iface.iface_type == InterfaceType::Ethernet {
|
||||
- match (des_iface.mac_address.as_ref(), des_iface.driver.as_ref()) {
|
||||
- (Some(mac), Some(driver)) => {
|
||||
- format!("MACAddress={mac}\nDriver={driver}")
|
||||
- }
|
||||
- (Some(mac), None) => {
|
||||
- format!("MACAddress={mac}")
|
||||
- }
|
||||
- (None, _) => {
|
||||
- format!("OriginalName={}", des_iface.name)
|
||||
- }
|
||||
- }
|
||||
+ rules
|
||||
} else {
|
||||
- format!("OriginalName={}", des_iface.name)
|
||||
+ format!("OriginalName={}", cur_iface.name)
|
||||
}
|
||||
+ } else {
|
||||
+ // User is creating the ethernet interface or gen_conf mode
|
||||
+ mac_match_rules(
|
||||
+ None,
|
||||
+ des_iface.mac_address.as_ref(),
|
||||
+ des_iface.driver.as_ref(),
|
||||
+ )
|
||||
+ .unwrap_or_else(|| format!("OriginalName={}", des_iface.name))
|
||||
}
|
||||
}
|
||||
|
||||
+fn mac_match_rules(
|
||||
+ permanent_mac: Option<&String>,
|
||||
+ mac: Option<&String>,
|
||||
+ driver: Option<&String>,
|
||||
+) -> Option<String> {
|
||||
+ let mut rules = if let Some(mac) = permanent_mac {
|
||||
+ format!("PermanentMACAddress={mac}")
|
||||
+ } else {
|
||||
+ let mac = mac?;
|
||||
+ format!("MACAddress={mac}")
|
||||
+ };
|
||||
+ if let Some(driver) = driver {
|
||||
+ write!(rules, "\nDriver={driver}").ok();
|
||||
+ }
|
||||
+ Some(rules)
|
||||
+}
|
||||
+
|
||||
async fn save_systemd_network_link_file(
|
||||
des_base_iface: &BaseInterface,
|
||||
cur_base_iface: Option<&BaseInterface>,
|
||||
@@ -283,51 +346,7 @@ async fn save_systemd_network_link_file(
|
||||
}
|
||||
let iface_name = des_base_iface.name.as_str();
|
||||
|
||||
- let match_rules = match des_base_iface.identifier.as_ref() {
|
||||
- Some(InterfaceIdentifier::MacAddress) => {
|
||||
- let mut match_rules = if let Some(mac) =
|
||||
- des_base_iface.permanent_mac_address.as_ref()
|
||||
- {
|
||||
- format!("PermanentMACAddress={mac}")
|
||||
- } else if let Some(mac) = des_base_iface.mac_address.as_ref() {
|
||||
- format!("MACAddress={mac}")
|
||||
- } else {
|
||||
- return Err(NmstateError::new(
|
||||
- ErrorKind::Bug,
|
||||
- format!(
|
||||
- "Got no MAC address for `identifier:mac-address` on \
|
||||
- interface {}, should be failed by previous checker",
|
||||
- des_base_iface.name
|
||||
- ),
|
||||
- ));
|
||||
- };
|
||||
- // Include driver in match rule in case we have two NIC holding the
|
||||
- // same MAC. (e.g. M$ Azure - Microsoft Azure Network Adapter)
|
||||
- if let Some(cur_base_iface) = cur_base_iface.as_ref()
|
||||
- && let Some(driver) = cur_base_iface.driver.as_ref()
|
||||
- {
|
||||
- write!(match_rules, "\nDriver={driver}").ok();
|
||||
- }
|
||||
- match_rules
|
||||
- }
|
||||
- Some(InterfaceIdentifier::PciAddress) => {
|
||||
- if let Some(pci_addr) = des_base_iface.pci_address.as_ref() {
|
||||
- format!("Path=pci-{pci_addr}")
|
||||
- } else {
|
||||
- return Err(NmstateError::new(
|
||||
- ErrorKind::Bug,
|
||||
- format!(
|
||||
- "Got no PCI address for `identifier:pci-address` on \
|
||||
- interface {}, should be failed by previous checker",
|
||||
- des_base_iface.name
|
||||
- ),
|
||||
- ));
|
||||
- }
|
||||
- }
|
||||
- _ => {
|
||||
- gen_systemd_network_link_match_rules(des_base_iface, cur_base_iface)
|
||||
- }
|
||||
- };
|
||||
+ let match_rules = gen_systemd_match_rules(des_base_iface, cur_base_iface)?;
|
||||
|
||||
let file_path = gen_systemd_link_file_path(iface_name);
|
||||
let mut content = {
|
||||
--
|
||||
2.54.0
|
||||
|
||||
405
0004-nispor-keep-OriginalName-link-match-for-stacked-inte.patch
Normal file
405
0004-nispor-keep-OriginalName-link-match-for-stacked-inte.patch
Normal file
@ -0,0 +1,405 @@
|
||||
From 43a8ad166ac0abffd1c0df047819b538bd68c3d5 Mon Sep 17 00:00:00 2001
|
||||
From: Josephine Pfeiffer <josie@redhat.com>
|
||||
Date: Mon, 6 Jul 2026 20:03:33 +0200
|
||||
Subject: [PATCH 4/6] nispor: keep OriginalName link match for stacked
|
||||
interfaces
|
||||
|
||||
A VLAN takes the MAC of its lower device, and on a bond that MAC is
|
||||
borrowed from the first port and shifts with the port set. On an
|
||||
alt-names-only reapply the interface already exists, so falling back to
|
||||
its identifier would pin the persisted `.link` `[Match]` to that
|
||||
borrowed MAC and lose the match once the ports change.
|
||||
|
||||
Fall back to a current mac-address/pci-address identifier only when the
|
||||
live interface owns it (a permanent MAC or a PCI address); otherwise use
|
||||
`OriginalName` on non-ethernet interfaces. An explicit
|
||||
`identifier: mac-address` is still honoured.
|
||||
|
||||
Resolves: https://redhat.atlassian.net/browse/RHEL-167955
|
||||
Resolves: https://redhat.atlassian.net/browse/NMT-2193
|
||||
Signed-off-by: Josephine Pfeiffer <josie@redhat.com>
|
||||
---
|
||||
rust/src/lib/nispor/alt_name.rs | 269 ++++++++++++++++++++++++++++-
|
||||
tests/integration/alt_name_test.py | 41 +++++
|
||||
2 files changed, 301 insertions(+), 9 deletions(-)
|
||||
|
||||
diff --git a/rust/src/lib/nispor/alt_name.rs b/rust/src/lib/nispor/alt_name.rs
|
||||
index 3251f31c..4471e9e4 100644
|
||||
--- a/rust/src/lib/nispor/alt_name.rs
|
||||
+++ b/rust/src/lib/nispor/alt_name.rs
|
||||
@@ -6,7 +6,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::{
|
||||
BaseInterface, ErrorKind, InterfaceIdentifier, InterfaceState,
|
||||
- MergedInterfaces, NmstateError,
|
||||
+ InterfaceType, MergedInterfaces, NmstateError,
|
||||
};
|
||||
|
||||
/// Comment added into our generated link files
|
||||
@@ -247,8 +247,13 @@ fn gen_systemd_match_rules(
|
||||
}
|
||||
|
||||
// Resolve the identifier to match on and the interface that supplies its
|
||||
-// MAC/PCI value. Desired state's explicit identifier is honoured as-is; when it
|
||||
-// is absent the current interface's identifier is inherited.
|
||||
+// MAC/PCI value. Desired state's explicit identifier is honoured as-is. When it
|
||||
+// is absent, a current `mac-address`/`pci-address` identifier is inherited only
|
||||
+// if the live interface owns that attribute (a permanent MAC or a PCI address),
|
||||
+// so a stacked interface never inherits a borrowed, unstable MAC; it falls
|
||||
+// through to the name match instead. A persisted `.link` file outlives the
|
||||
+// current port set, so it must never pin to a borrowed MAC even though NM
|
||||
+// profile matching may.
|
||||
fn effective_identifier<'a>(
|
||||
des_iface: &'a BaseInterface,
|
||||
cur_iface: Option<&'a BaseInterface>,
|
||||
@@ -258,10 +263,14 @@ fn effective_identifier<'a>(
|
||||
}
|
||||
if let Some(cur) = cur_iface {
|
||||
match cur.identifier {
|
||||
- Some(InterfaceIdentifier::MacAddress) => {
|
||||
+ Some(InterfaceIdentifier::MacAddress)
|
||||
+ if cur.permanent_mac_address.is_some() =>
|
||||
+ {
|
||||
return (InterfaceIdentifier::MacAddress, cur);
|
||||
}
|
||||
- Some(InterfaceIdentifier::PciAddress) => {
|
||||
+ Some(InterfaceIdentifier::PciAddress)
|
||||
+ if cur.pci_address.is_some() =>
|
||||
+ {
|
||||
return (InterfaceIdentifier::PciAddress, cur);
|
||||
}
|
||||
_ => {}
|
||||
@@ -272,14 +281,20 @@ fn effective_identifier<'a>(
|
||||
|
||||
// Fallback used when the resolved identifier is `name` (the default).
|
||||
//
|
||||
-// The kernel name is unpredictable, so match on the most stable attribute
|
||||
-// available: permanent MAC, then PCI path, then current MAC, each with driver
|
||||
-// when known to disambiguate NICs sharing a MAC (e.g. Azure's Microsoft Azure
|
||||
-// Network Adapter).
|
||||
+// For ethernet the kernel name is unpredictable, so match on the most stable
|
||||
+// attribute available: permanent MAC, then PCI path, then current MAC, each
|
||||
+// with driver when known to disambiguate NICs sharing a MAC (e.g. Azure's
|
||||
+// Microsoft Azure Network Adapter). For non-ethernet the kernel name is
|
||||
+// user-assigned and the MAC is borrowed from a lower device (a VLAN takes its
|
||||
+// bond's MAC, itself from the first enslaved port), so use `OriginalName`.
|
||||
fn gen_systemd_name_match_rules(
|
||||
des_iface: &BaseInterface,
|
||||
cur_iface: Option<&BaseInterface>,
|
||||
) -> String {
|
||||
+ if des_iface.iface_type != InterfaceType::Ethernet {
|
||||
+ return format!("OriginalName={}", des_iface.name);
|
||||
+ }
|
||||
+
|
||||
if let Some(cur_iface) = cur_iface {
|
||||
if let Some(rules) = mac_match_rules(
|
||||
cur_iface.permanent_mac_address.as_ref(),
|
||||
@@ -391,3 +406,239 @@ async fn save_systemd_network_link_file(
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
+
|
||||
+#[cfg(test)]
|
||||
+mod tests {
|
||||
+ use super::*;
|
||||
+
|
||||
+ // Desired VLAN has no MAC; the live one carries the inherited bond MAC.
|
||||
+ #[test]
|
||||
+ fn vlan_reapply_uses_original_name() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "external0".to_string();
|
||||
+ des.iface_type = InterfaceType::Vlan;
|
||||
+
|
||||
+ let mut cur = des.clone();
|
||||
+ cur.mac_address = Some("52:54:00:80:77:C9".to_string());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, Some(&cur)).unwrap();
|
||||
+ assert_eq!(rules, "OriginalName=external0");
|
||||
+ }
|
||||
+
|
||||
+ // Create path (no current iface), non-VLAN software type, MAC ignored.
|
||||
+ #[test]
|
||||
+ fn software_iface_create_uses_original_name() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "bond0".to_string();
|
||||
+ des.iface_type = InterfaceType::Bond;
|
||||
+ des.mac_address = Some("52:54:00:80:77:C9".to_string());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, None).unwrap();
|
||||
+ assert_eq!(rules, "OriginalName=bond0");
|
||||
+ }
|
||||
+
|
||||
+ #[test]
|
||||
+ fn ethernet_reapply_uses_permanent_mac() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "eth0".to_string();
|
||||
+ des.iface_type = InterfaceType::Ethernet;
|
||||
+
|
||||
+ let mut cur = des.clone();
|
||||
+ cur.permanent_mac_address = Some("52:54:00:11:22:33".to_string());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, Some(&cur)).unwrap();
|
||||
+ assert_eq!(rules, "PermanentMACAddress=52:54:00:11:22:33");
|
||||
+ }
|
||||
+
|
||||
+ // A live mac-address identifier without a permanent MAC (borrowed from a
|
||||
+ // lower device) is not inherited.
|
||||
+ #[test]
|
||||
+ fn inherited_mac_ignored_for_virtual_iface() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "vlan0".to_string();
|
||||
+ des.iface_type = InterfaceType::Vlan;
|
||||
+
|
||||
+ let mut cur = des.clone();
|
||||
+ cur.identifier = Some(InterfaceIdentifier::MacAddress);
|
||||
+ cur.mac_address = Some("52:54:00:80:77:C9".to_string());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, Some(&cur)).unwrap();
|
||||
+ assert_eq!(rules, "OriginalName=vlan0");
|
||||
+ }
|
||||
+
|
||||
+ // A physical NIC with a live mac-address identifier owns a permanent MAC,
|
||||
+ // so an alt-names-only reapply inherits it and matches by permanent MAC.
|
||||
+ #[test]
|
||||
+ fn inherited_mac_uses_permanent_for_physical() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "eth0".to_string();
|
||||
+ des.iface_type = InterfaceType::Ethernet;
|
||||
+
|
||||
+ let mut cur = des.clone();
|
||||
+ cur.identifier = Some(InterfaceIdentifier::MacAddress);
|
||||
+ cur.permanent_mac_address = Some("52:54:00:11:22:33".to_string());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, Some(&cur)).unwrap();
|
||||
+ assert_eq!(rules, "PermanentMACAddress=52:54:00:11:22:33");
|
||||
+ }
|
||||
+
|
||||
+ // A live pci-address identifier is inherited over the name fallback's
|
||||
+ // default order, pinning the PCI path the user originally chose.
|
||||
+ #[test]
|
||||
+ fn inherited_pci_identifier_preserved() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "eth0".to_string();
|
||||
+ des.iface_type = InterfaceType::Ethernet;
|
||||
+
|
||||
+ let mut cur = des.clone();
|
||||
+ cur.identifier = Some(InterfaceIdentifier::PciAddress);
|
||||
+ cur.permanent_mac_address = Some("52:54:00:11:22:33".to_string());
|
||||
+ cur.pci_address =
|
||||
+ Some(crate::PciAddress::try_from("0000:00:1f.6").unwrap());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, Some(&cur)).unwrap();
|
||||
+ assert_eq!(rules, "Path=pci-0000:00:1f.6");
|
||||
+ }
|
||||
+
|
||||
+ // Explicit `identifier: name` wins over the live mac-address identifier.
|
||||
+ #[test]
|
||||
+ fn explicit_name_identifier_keeps_original_name() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "vlan0".to_string();
|
||||
+ des.iface_type = InterfaceType::Vlan;
|
||||
+ des.identifier = Some(InterfaceIdentifier::Name);
|
||||
+
|
||||
+ let mut cur = des.clone();
|
||||
+ cur.identifier = Some(InterfaceIdentifier::MacAddress);
|
||||
+ cur.mac_address = Some("52:54:00:80:77:C9".to_string());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, Some(&cur)).unwrap();
|
||||
+ assert_eq!(rules, "OriginalName=vlan0");
|
||||
+ }
|
||||
+
|
||||
+ // mac-address identifier appends the live driver to disambiguate two NICs
|
||||
+ // sharing one MAC.
|
||||
+ #[test]
|
||||
+ fn mac_identifier_appends_current_driver() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "eth0".to_string();
|
||||
+ des.iface_type = InterfaceType::Ethernet;
|
||||
+ des.identifier = Some(InterfaceIdentifier::MacAddress);
|
||||
+ des.mac_address = Some("52:54:00:11:22:33".to_string());
|
||||
+
|
||||
+ let mut cur = des.clone();
|
||||
+ cur.driver = Some("e1000e".to_string());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, Some(&cur)).unwrap();
|
||||
+ assert_eq!(rules, "MACAddress=52:54:00:11:22:33\nDriver=e1000e");
|
||||
+ }
|
||||
+
|
||||
+ #[test]
|
||||
+ fn pci_identifier_uses_path() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "eth0".to_string();
|
||||
+ des.iface_type = InterfaceType::Ethernet;
|
||||
+ des.identifier = Some(InterfaceIdentifier::PciAddress);
|
||||
+ des.pci_address =
|
||||
+ Some(crate::PciAddress::try_from("0000:00:1f.6").unwrap());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, None).unwrap();
|
||||
+ assert_eq!(rules, "Path=pci-0000:00:1f.6");
|
||||
+ }
|
||||
+
|
||||
+ #[test]
|
||||
+ fn mac_identifier_without_mac_is_bug() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "eth0".to_string();
|
||||
+ des.iface_type = InterfaceType::Ethernet;
|
||||
+ des.identifier = Some(InterfaceIdentifier::MacAddress);
|
||||
+
|
||||
+ let e = gen_systemd_match_rules(&des, None).unwrap_err();
|
||||
+ assert_eq!(e.kind(), ErrorKind::Bug);
|
||||
+ }
|
||||
+
|
||||
+ #[test]
|
||||
+ fn pci_identifier_without_pci_is_bug() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "eth0".to_string();
|
||||
+ des.iface_type = InterfaceType::Ethernet;
|
||||
+ des.identifier = Some(InterfaceIdentifier::PciAddress);
|
||||
+
|
||||
+ let e = gen_systemd_match_rules(&des, None).unwrap_err();
|
||||
+ assert_eq!(e.kind(), ErrorKind::Bug);
|
||||
+ }
|
||||
+
|
||||
+ #[test]
|
||||
+ fn name_fallback_uses_permanent_mac_and_driver() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "eth0".to_string();
|
||||
+ des.iface_type = InterfaceType::Ethernet;
|
||||
+
|
||||
+ let mut cur = des.clone();
|
||||
+ cur.permanent_mac_address = Some("52:54:00:11:22:33".to_string());
|
||||
+ cur.driver = Some("e1000e".to_string());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, Some(&cur)).unwrap();
|
||||
+ assert_eq!(
|
||||
+ rules,
|
||||
+ "PermanentMACAddress=52:54:00:11:22:33\nDriver=e1000e"
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
+ #[test]
|
||||
+ fn name_fallback_uses_current_mac_and_driver() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "eth0".to_string();
|
||||
+ des.iface_type = InterfaceType::Ethernet;
|
||||
+
|
||||
+ let mut cur = des.clone();
|
||||
+ cur.mac_address = Some("52:54:00:11:22:33".to_string());
|
||||
+ cur.driver = Some("e1000e".to_string());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, Some(&cur)).unwrap();
|
||||
+ assert_eq!(rules, "MACAddress=52:54:00:11:22:33\nDriver=e1000e");
|
||||
+ }
|
||||
+
|
||||
+ // Explicit desired mac-address identifier is honoured on a non-ethernet
|
||||
+ // iface too, matching by MAC rather than falling back to OriginalName.
|
||||
+ #[test]
|
||||
+ fn non_eth_explicit_mac_identifier_uses_mac() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "vlan0".to_string();
|
||||
+ des.iface_type = InterfaceType::Vlan;
|
||||
+ des.identifier = Some(InterfaceIdentifier::MacAddress);
|
||||
+ des.mac_address = Some("52:54:00:11:22:33".to_string());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, None).unwrap();
|
||||
+ assert_eq!(rules, "MACAddress=52:54:00:11:22:33");
|
||||
+ }
|
||||
+
|
||||
+ // Permanent MAC wins when both a permanent and a current MAC are present.
|
||||
+ #[test]
|
||||
+ fn permanent_mac_preferred_over_current() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "eth0".to_string();
|
||||
+ des.iface_type = InterfaceType::Ethernet;
|
||||
+
|
||||
+ let mut cur = des.clone();
|
||||
+ cur.permanent_mac_address = Some("52:54:00:11:22:33".to_string());
|
||||
+ cur.mac_address = Some("52:54:00:AA:BB:CC".to_string());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, Some(&cur)).unwrap();
|
||||
+ assert_eq!(rules, "PermanentMACAddress=52:54:00:11:22:33");
|
||||
+ }
|
||||
+
|
||||
+ // Ethernet reapply with no MAC, PCI, or permanent MAC on the live iface
|
||||
+ // falls back to its current kernel name.
|
||||
+ #[test]
|
||||
+ fn eth_name_fallback_with_no_attributes() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "eth0".to_string();
|
||||
+ des.iface_type = InterfaceType::Ethernet;
|
||||
+
|
||||
+ let cur = des.clone();
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, Some(&cur)).unwrap();
|
||||
+ assert_eq!(rules, "OriginalName=eth0");
|
||||
+ }
|
||||
+}
|
||||
diff --git a/tests/integration/alt_name_test.py b/tests/integration/alt_name_test.py
|
||||
index a72d4ad0..f53bfe62 100644
|
||||
--- a/tests/integration/alt_name_test.py
|
||||
+++ b/tests/integration/alt_name_test.py
|
||||
@@ -1,6 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import copy
|
||||
+import os
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -73,6 +74,13 @@ def eth1_with_alt_names(eth1_up):
|
||||
assert get_ip_link_alt_names("eth1") == []
|
||||
|
||||
|
||||
+def read_systemd_link_file(iface_name):
|
||||
+ path = f"/etc/systemd/network/98-nmstate-{iface_name}.link"
|
||||
+ assert os.path.exists(path), f"link file {path} not found"
|
||||
+ with open(path) as f:
|
||||
+ return f.read()
|
||||
+
|
||||
+
|
||||
def udev_trigger_check_alt_names(iface_name, expected_alt_names):
|
||||
exec_cmd(
|
||||
"udevadm trigger --settle --action add "
|
||||
@@ -235,6 +243,39 @@ class TestAltNames:
|
||||
assert iface[VLAN.CONFIG_SUBTREE][VLAN.BASE_IFACE] == "eth1"
|
||||
assert iface[VLAN.CONFIG_SUBTREE][VLAN.ID] == 101
|
||||
|
||||
+ # https://issues.redhat.com/browse/RHEL-167955
|
||||
+ @pytest.mark.tier1
|
||||
+ def test_vlan_on_bond_alt_name_uses_original_name_on_reapply(
|
||||
+ self, eth1_up, eth2_up
|
||||
+ ):
|
||||
+ vlan_name = f"{TEST_BOND_NIC}.101"
|
||||
+ alt_name = "altone"
|
||||
+ with bond_interface(TEST_BOND_NIC, ["eth1", "eth2"]), vlan_interface(
|
||||
+ vlan_name, 101, TEST_BOND_NIC
|
||||
+ ) as vlan_state:
|
||||
+ desired_state = copy.deepcopy(vlan_state)
|
||||
+ iface = desired_state[Interface.KEY][0]
|
||||
+ iface[InterfaceAltName.KEY] = [{InterfaceAltName.NAME: alt_name}]
|
||||
+ # The VLAN already exists, so persisting alt-names takes the
|
||||
+ # current-state path. Apply twice: the reapply, not the first
|
||||
+ # write, is what must keep OriginalName.
|
||||
+ for _ in range(2):
|
||||
+ libnmstate.apply(desired_state)
|
||||
+ content = read_systemd_link_file(vlan_name)
|
||||
+ lines = content.splitlines()
|
||||
+ assert f"OriginalName={vlan_name}" in lines
|
||||
+ assert f"Name={vlan_name}" in lines
|
||||
+ # Any hardware key shifts the match off the kernel name;
|
||||
+ # "MACAddress=" also catches "PermanentMACAddress=".
|
||||
+ for key in ("MACAddress=", "Path=", "Driver="):
|
||||
+ assert key not in content
|
||||
+ assert retry_till_true_or_timeout(
|
||||
+ RETRY_TIMEOUT,
|
||||
+ udev_trigger_check_alt_names,
|
||||
+ vlan_name,
|
||||
+ [alt_name],
|
||||
+ )
|
||||
+
|
||||
# https://issues.redhat.com/browse/RHEL-126508
|
||||
@pytest.mark.tier1
|
||||
def test_ref_alt_name_in_route(self, eth1_with_alt_names):
|
||||
--
|
||||
2.54.0
|
||||
|
||||
192
0005-nispor-pin-physical-non-ethernet-alt-name-match-to-o.patch
Normal file
192
0005-nispor-pin-physical-non-ethernet-alt-name-match-to-o.patch
Normal file
@ -0,0 +1,192 @@
|
||||
From a1f5311a0bdc3f582dd54004ee8b67e90733bd0e Mon Sep 17 00:00:00 2001
|
||||
From: Josephine Pfeiffer <josie@redhat.com>
|
||||
Date: Tue, 7 Jul 2026 08:20:26 +0200
|
||||
Subject: [PATCH 5/6] nispor: pin physical non-ethernet alt-name match to owned
|
||||
identifier
|
||||
|
||||
The name fallback matched every non-ethernet interface on OriginalName,
|
||||
on the assumption that a non-ethernet kernel name is user-assigned and
|
||||
its MAC borrowed. That holds for VLAN, bond, and bridge, but not for
|
||||
physical InfiniBand: its kernel name comes from probe order and udev can
|
||||
rename it, so an OriginalName match can silently break after a reboot.
|
||||
|
||||
Prefer a permanent MAC, then a PCI address the live interface owns,
|
||||
before falling back to OriginalName, and never match on a borrowed
|
||||
current MAC. A VLAN over a bond owns neither, so it still matches on
|
||||
OriginalName; a physical InfiniBand pins to its permanent MAC or PCI
|
||||
path.
|
||||
|
||||
Signed-off-by: Josephine Pfeiffer <josie@redhat.com>
|
||||
---
|
||||
rust/src/lib/nispor/alt_name.rs | 119 +++++++++++++++++++++++---------
|
||||
1 file changed, 85 insertions(+), 34 deletions(-)
|
||||
|
||||
diff --git a/rust/src/lib/nispor/alt_name.rs b/rust/src/lib/nispor/alt_name.rs
|
||||
index 4471e9e4..220e6919 100644
|
||||
--- a/rust/src/lib/nispor/alt_name.rs
|
||||
+++ b/rust/src/lib/nispor/alt_name.rs
|
||||
@@ -246,14 +246,10 @@ fn gen_systemd_match_rules(
|
||||
}
|
||||
}
|
||||
|
||||
-// Resolve the identifier to match on and the interface that supplies its
|
||||
-// MAC/PCI value. Desired state's explicit identifier is honoured as-is. When it
|
||||
-// is absent, a current `mac-address`/`pci-address` identifier is inherited only
|
||||
-// if the live interface owns that attribute (a permanent MAC or a PCI address),
|
||||
-// so a stacked interface never inherits a borrowed, unstable MAC; it falls
|
||||
-// through to the name match instead. A persisted `.link` file outlives the
|
||||
-// current port set, so it must never pin to a borrowed MAC even though NM
|
||||
-// profile matching may.
|
||||
+// Pick the identifier and the interface holding its value. An explicit desired
|
||||
+// identifier wins; a live mac/pci identifier is inherited only when the iface
|
||||
+// owns it (permanent MAC or PCI), never a stacked iface's borrowed MAC, since
|
||||
+// the persisted `.link` file outlives the port set that MAC came from.
|
||||
fn effective_identifier<'a>(
|
||||
des_iface: &'a BaseInterface,
|
||||
cur_iface: Option<&'a BaseInterface>,
|
||||
@@ -279,40 +275,33 @@ fn effective_identifier<'a>(
|
||||
(InterfaceIdentifier::Name, des_iface)
|
||||
}
|
||||
|
||||
-// Fallback used when the resolved identifier is `name` (the default).
|
||||
-//
|
||||
-// For ethernet the kernel name is unpredictable, so match on the most stable
|
||||
-// attribute available: permanent MAC, then PCI path, then current MAC, each
|
||||
-// with driver when known to disambiguate NICs sharing a MAC (e.g. Azure's
|
||||
-// Microsoft Azure Network Adapter). For non-ethernet the kernel name is
|
||||
-// user-assigned and the MAC is borrowed from a lower device (a VLAN takes its
|
||||
-// bond's MAC, itself from the first enslaved port), so use `OriginalName`.
|
||||
+// Fallback for the `name` identifier: match the most stable attribute the live
|
||||
+// interface owns (permanent MAC, then PCI, with driver to disambiguate NICs
|
||||
+// sharing a MAC). A borrowed MAC is never used, so software interfaces fall
|
||||
+// through to `OriginalName`; ethernet also accepts its own current MAC first.
|
||||
fn gen_systemd_name_match_rules(
|
||||
des_iface: &BaseInterface,
|
||||
cur_iface: Option<&BaseInterface>,
|
||||
) -> String {
|
||||
if des_iface.iface_type != InterfaceType::Ethernet {
|
||||
- return format!("OriginalName={}", des_iface.name);
|
||||
+ return cur_iface.and_then(owned_hw_match_rules).unwrap_or_else(|| {
|
||||
+ let name = cur_iface
|
||||
+ .map(|i| i.name.as_str())
|
||||
+ .unwrap_or(des_iface.name.as_str());
|
||||
+ format!("OriginalName={name}")
|
||||
+ });
|
||||
}
|
||||
|
||||
if let Some(cur_iface) = cur_iface {
|
||||
- if let Some(rules) = mac_match_rules(
|
||||
- cur_iface.permanent_mac_address.as_ref(),
|
||||
- None,
|
||||
- cur_iface.driver.as_ref(),
|
||||
- ) {
|
||||
- rules
|
||||
- } else if let Some(pci_addr) = cur_iface.pci_address.as_ref() {
|
||||
- format!("Path=pci-{pci_addr}")
|
||||
- } else if let Some(rules) = mac_match_rules(
|
||||
- None,
|
||||
- cur_iface.mac_address.as_ref(),
|
||||
- cur_iface.driver.as_ref(),
|
||||
- ) {
|
||||
- rules
|
||||
- } else {
|
||||
- format!("OriginalName={}", cur_iface.name)
|
||||
- }
|
||||
+ owned_hw_match_rules(cur_iface)
|
||||
+ .or_else(|| {
|
||||
+ mac_match_rules(
|
||||
+ None,
|
||||
+ cur_iface.mac_address.as_ref(),
|
||||
+ cur_iface.driver.as_ref(),
|
||||
+ )
|
||||
+ })
|
||||
+ .unwrap_or_else(|| format!("OriginalName={}", cur_iface.name))
|
||||
} else {
|
||||
// User is creating the ethernet interface or gen_conf mode
|
||||
mac_match_rules(
|
||||
@@ -324,6 +313,22 @@ fn gen_systemd_name_match_rules(
|
||||
}
|
||||
}
|
||||
|
||||
+// A stable hardware identifier the interface owns: permanent MAC (with driver)
|
||||
+// or PCI path. None for a borrowed MAC (e.g. a VLAN), which matches by name.
|
||||
+fn owned_hw_match_rules(cur_iface: &BaseInterface) -> Option<String> {
|
||||
+ mac_match_rules(
|
||||
+ cur_iface.permanent_mac_address.as_ref(),
|
||||
+ None,
|
||||
+ cur_iface.driver.as_ref(),
|
||||
+ )
|
||||
+ .or_else(|| {
|
||||
+ cur_iface
|
||||
+ .pci_address
|
||||
+ .as_ref()
|
||||
+ .map(|pci_addr| format!("Path=pci-{pci_addr}"))
|
||||
+ })
|
||||
+}
|
||||
+
|
||||
fn mac_match_rules(
|
||||
permanent_mac: Option<&String>,
|
||||
mac: Option<&String>,
|
||||
@@ -425,6 +430,21 @@ mod tests {
|
||||
assert_eq!(rules, "OriginalName=external0");
|
||||
}
|
||||
|
||||
+ // The name fallback keys on the live kernel name, not the desired name, so
|
||||
+ // udev matches the interface it names.
|
||||
+ #[test]
|
||||
+ fn non_eth_original_name_uses_current_name() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "bond-vlan".to_string();
|
||||
+ des.iface_type = InterfaceType::Vlan;
|
||||
+
|
||||
+ let mut cur = des.clone();
|
||||
+ cur.name = "external0".to_string();
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, Some(&cur)).unwrap();
|
||||
+ assert_eq!(rules, "OriginalName=external0");
|
||||
+ }
|
||||
+
|
||||
// Create path (no current iface), non-VLAN software type, MAC ignored.
|
||||
#[test]
|
||||
fn software_iface_create_uses_original_name() {
|
||||
@@ -450,6 +470,37 @@ mod tests {
|
||||
assert_eq!(rules, "PermanentMACAddress=52:54:00:11:22:33");
|
||||
}
|
||||
|
||||
+ // A physical non-ethernet iface (InfiniBand) owns a permanent MAC, so an
|
||||
+ // alt-names-only reapply pins to it instead of its renameable kernel name.
|
||||
+ #[test]
|
||||
+ fn infiniband_reapply_uses_permanent_mac() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "mlx0".to_string();
|
||||
+ des.iface_type = InterfaceType::InfiniBand;
|
||||
+
|
||||
+ let mut cur = des.clone();
|
||||
+ cur.permanent_mac_address = Some("52:54:00:11:22:33".to_string());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, Some(&cur)).unwrap();
|
||||
+ assert_eq!(rules, "PermanentMACAddress=52:54:00:11:22:33");
|
||||
+ }
|
||||
+
|
||||
+ // A physical non-ethernet iface owning a PCI address but no permanent MAC
|
||||
+ // pins to the PCI path rather than the kernel name.
|
||||
+ #[test]
|
||||
+ fn non_eth_reapply_uses_pci_path() {
|
||||
+ let mut des = BaseInterface::new();
|
||||
+ des.name = "mlx0".to_string();
|
||||
+ des.iface_type = InterfaceType::InfiniBand;
|
||||
+
|
||||
+ let mut cur = des.clone();
|
||||
+ cur.pci_address =
|
||||
+ Some(crate::PciAddress::try_from("0000:00:1f.6").unwrap());
|
||||
+
|
||||
+ let rules = gen_systemd_match_rules(&des, Some(&cur)).unwrap();
|
||||
+ assert_eq!(rules, "Path=pci-0000:00:1f.6");
|
||||
+ }
|
||||
+
|
||||
// A live mac-address identifier without a permanent MAC (borrowed from a
|
||||
// lower device) is not inherited.
|
||||
#[test]
|
||||
--
|
||||
2.54.0
|
||||
|
||||
102
0006-nispor-skip-alt-name-removal-for-deleted-virtual-int.patch
Normal file
102
0006-nispor-skip-alt-name-removal-for-deleted-virtual-int.patch
Normal file
@ -0,0 +1,102 @@
|
||||
From 1527741c551a7ac54d5111ee264b3557fd3ad110 Mon Sep 17 00:00:00 2001
|
||||
From: Josephine Pfeiffer <josie@redhat.com>
|
||||
Date: Fri, 10 Jul 2026 14:56:09 +0200
|
||||
Subject: [PATCH 6/6] nispor: skip alt-name removal for deleted virtual
|
||||
interfaces
|
||||
|
||||
Deleting a virtual interface (e.g. a VLAN) that carries an nmstate-managed
|
||||
alt-name failed because NetworkManager removes the interface first, then the
|
||||
nispor alt-name removal runs against the now-absent interface and the kernel
|
||||
rejects it with EOPNOTSUPP.
|
||||
|
||||
A deleted virtual interface takes its alt-names with it, so skip the removal.
|
||||
Physical interfaces survive state:absent, so they still get their alt-names
|
||||
cleared.
|
||||
|
||||
Signed-off-by: Josephine Pfeiffer <josie@redhat.com>
|
||||
---
|
||||
rust/src/lib/nispor/alt_name.rs | 16 +++++++++----
|
||||
tests/integration/alt_name_test.py | 36 ++++++++++++++++++++++++++++++
|
||||
2 files changed, 48 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/rust/src/lib/nispor/alt_name.rs b/rust/src/lib/nispor/alt_name.rs
|
||||
index 220e6919..3fc575f4 100644
|
||||
--- a/rust/src/lib/nispor/alt_name.rs
|
||||
+++ b/rust/src/lib/nispor/alt_name.rs
|
||||
@@ -80,14 +80,22 @@ pub(crate) async fn apply_ifaces_alt_names(
|
||||
|
||||
for merged_iface in merged_ifaces.kernel_ifaces.values() {
|
||||
let apply_iface = if let Some(i) = merged_iface.for_apply.as_ref() {
|
||||
- i.base_iface()
|
||||
+ i
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
- let cur_iface = merged_iface.current.as_ref().map(|i| i.base_iface());
|
||||
- if let Some(np_iface) =
|
||||
- gen_nispor_iface_conf_for_alt_name(apply_iface, cur_iface)?
|
||||
+ // A virtual interface being deleted takes its alt-names with it, so
|
||||
+ // skip the nispor removal, which would otherwise race the deletion.
|
||||
+ if apply_iface.base_iface().state == InterfaceState::Absent
|
||||
+ && apply_iface.is_virtual()
|
||||
{
|
||||
+ continue;
|
||||
+ }
|
||||
+ let cur_iface = merged_iface.current.as_ref().map(|i| i.base_iface());
|
||||
+ if let Some(np_iface) = gen_nispor_iface_conf_for_alt_name(
|
||||
+ apply_iface.base_iface(),
|
||||
+ cur_iface,
|
||||
+ )? {
|
||||
np_ifaces.push(np_iface);
|
||||
}
|
||||
}
|
||||
diff --git a/tests/integration/alt_name_test.py b/tests/integration/alt_name_test.py
|
||||
index f53bfe62..df41f18e 100644
|
||||
--- a/tests/integration/alt_name_test.py
|
||||
+++ b/tests/integration/alt_name_test.py
|
||||
@@ -276,6 +276,42 @@ class TestAltNames:
|
||||
[alt_name],
|
||||
)
|
||||
|
||||
+ # https://issues.redhat.com/browse/RHEL-167955
|
||||
+ @pytest.mark.tier1
|
||||
+ def test_delete_vlan_on_bond_with_alt_name(self, eth1_up, eth2_up):
|
||||
+ vlan_name = f"{TEST_BOND_NIC}.102"
|
||||
+ link_file = f"/etc/systemd/network/98-nmstate-{vlan_name}.link"
|
||||
+ with bond_interface(TEST_BOND_NIC, ["eth1", "eth2"]):
|
||||
+ libnmstate.apply(
|
||||
+ load_yaml(
|
||||
+ f"""---
|
||||
+ interfaces:
|
||||
+ - name: {vlan_name}
|
||||
+ type: vlan
|
||||
+ state: up
|
||||
+ vlan:
|
||||
+ base-iface: {TEST_BOND_NIC}
|
||||
+ id: 102
|
||||
+ alt-names:
|
||||
+ - name: altdel
|
||||
+ """
|
||||
+ )
|
||||
+ )
|
||||
+ assert os.path.exists(link_file)
|
||||
+ # Deleting the VLAN must not fail trying to remove an alt-name
|
||||
+ # from an interface it just deleted; the link file goes with it.
|
||||
+ libnmstate.apply(
|
||||
+ load_yaml(
|
||||
+ f"""---
|
||||
+ interfaces:
|
||||
+ - name: {vlan_name}
|
||||
+ type: vlan
|
||||
+ state: absent
|
||||
+ """
|
||||
+ )
|
||||
+ )
|
||||
+ assert not os.path.exists(link_file)
|
||||
+
|
||||
# https://issues.redhat.com/browse/RHEL-126508
|
||||
@pytest.mark.tier1
|
||||
def test_ref_alt_name_in_route(self, eth1_with_alt_names):
|
||||
--
|
||||
2.54.0
|
||||
|
||||
17
nmstate.spec
17
nmstate.spec
@ -4,15 +4,27 @@
|
||||
|
||||
Name: nmstate
|
||||
Version: 2.2.61
|
||||
Release: 1%{?dist}
|
||||
Release: 2%{?dist}
|
||||
Summary: Declarative network manager API
|
||||
License: LGPLv2+
|
||||
URL: https://github.com/%{srcname}/%{srcname}
|
||||
|
||||
Source0: https://github.com/nmstate/nmstate/releases/download/v%{version}/nmstate-%{version}.tar.gz
|
||||
# Keep these patches until next rebase
|
||||
Patch2: 0002-iface-include-existing-interfaces-in-name-search.patch
|
||||
Patch0003: 0003-nispor-derive-alt-name-link-match-from-interface-ide.patch
|
||||
Patch0004: 0004-nispor-keep-OriginalName-link-match-for-stacked-inte.patch
|
||||
Patch0005: 0005-nispor-pin-physical-non-ethernet-alt-name-match-to-o.patch
|
||||
Patch0006: 0006-nispor-skip-alt-name-removal-for-deleted-virtual-int.patch
|
||||
|
||||
Source1: https://github.com/nmstate/nmstate/releases/download/v%{version}/nmstate-%{version}.tar.gz.asc
|
||||
Source2: https://nmstate.io/nmstate.gpg
|
||||
|
||||
Source3: https://github.com/nmstate/nmstate/releases/download/v%{version}/nmstate-vendor-%{version}.tar.xz
|
||||
# Vendored dependency patch, see %prep section for more details
|
||||
# on how it's applied.
|
||||
Patch1: 0001-nispor-fix-ipoib-iface-type.patch
|
||||
|
||||
BuildRequires: python3-devel
|
||||
BuildRequires: python3-setuptools
|
||||
BuildRequires: gnupg2
|
||||
@ -164,6 +176,9 @@ popd
|
||||
/sbin/ldconfig
|
||||
|
||||
%changelog
|
||||
* Wed Jul 29 2026 Rahul Rajesh <rrajesh@redhat.com> - 2.2.61-2
|
||||
- Fix copy-mac-from failing when source interface is down. RHEL-214096
|
||||
|
||||
* Thu Jul 23 2026 Rahul Rajesh <rrajesh@redhat.com> - 2.2.61-1
|
||||
- Upgrade to 2.2.61
|
||||
- Support referring alt-name in `copy-mac-from` property. RHEL-214096
|
||||
|
||||
Loading…
Reference in New Issue
Block a user