import CS nmstate-2.2.61-2.el9

This commit is contained in:
AlmaLinux RelEng Bot 2026-08-24 09:27:09 -04:00
parent 3d7994af3f
commit 0e3f85819a
12 changed files with 1272 additions and 75 deletions

4
.gitignore vendored
View File

@ -1,2 +1,2 @@
SOURCES/nmstate-2.2.58.tar.gz
SOURCES/nmstate-vendor-2.2.58.tar.xz
SOURCES/nmstate-2.2.61.tar.gz
SOURCES/nmstate-vendor-2.2.61.tar.xz

View File

@ -1,2 +1,2 @@
314fbd26f1f5bbafbfd78bc0232aa44d7b992987 SOURCES/nmstate-2.2.58.tar.gz
4b3c5af41356c3dcab9a302d9aa81aef7e77b835 SOURCES/nmstate-vendor-2.2.58.tar.xz
e7c3a7490392927f2bc80f899b7a62be61016e9e SOURCES/nmstate-2.2.61.tar.gz
6243b20ec53da6985c8cf696b5b47dbf103be4ef SOURCES/nmstate-vendor-2.2.61.tar.xz

View File

@ -0,0 +1,63 @@
From 18fce5de48c2687131eab604c31c4738ffc2554c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= <ihuguet@riseup.net>
Date: Mon, 4 May 2026 07:24:15 +0200
Subject: [PATCH] infiniband: restore detection of Ipoib iface type
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fixes 5e13d3f "Prefer LinkInfo::Kind as interface type". This commit
made to always prefer InfoKind over anything for Ethernet, but at the
same time it made to prefer LinkLayerType over InfoKind for Infiniband.
This causes that the Ipoib iface typeis not detected anymore, causing
two problems:
- Interfaces previously detected as Ipoib are now detected as
Infiniband, breaking some clients like nmstate.
- The Ipoib data is not parsed, as it is only done for Ipoib iface type.
Fix it by prefering the "Infiniband" generic type only if no InfoKind
was detected, or InfoKind::Other was detected.
Signed-off-by: Íñigo Huguet <ihuguet@riseup.net>
---
src/lib/query/iface.rs | 23 ++++++++++++++++-------
1 file changed, 16 insertions(+), 7 deletions(-)
diff --git a/src/lib/query/iface.rs b/src/lib/query/iface.rs
index 9e21070..cce4f40 100644
--- a/vendor/nispor/query/iface.rs
+++ b/vendor/nispor/query/iface.rs
@@ -420,14 +420,23 @@ pub(crate) fn parse_nl_msg_to_iface(
},
_ => IfaceType::Other(format!("{t:?}").to_lowercase()),
};
- // Always prefer InfoKind unless link type is loopback or
- // infiniband.
- if !matches!(
- iface_state.iface_type,
- IfaceType::Loopback | IfaceType::Infiniband
- ) {
- iface_state.iface_type = iface_type;
+
+ // We prefer LinkLayerType over InfoKind for loopback, as it
+ // is more accurate in this case. We also prefer it for
+ // infiniband, but only if we didn't detect a specific
+ // InfoKind (we detected Other). For example, Ipoib is more
+ // specific than Infiniband, but Other is not.
+ if iface_state.iface_type == IfaceType::Loopback
+ || (iface_state.iface_type == IfaceType::Infiniband
+ && matches!(iface_type, IfaceType::Other(_)))
+ {
+ continue;
}
+
+ // For any other case, we prefer InfoKind over LinkLayerType
+ // as it is almost always more accurate. LinkLayerType is
+ // set as ethernet for most device types.
+ iface_state.iface_type = iface_type;
}
}
for info in infos {
--
2.53.0

View 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"));
+}

View 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

View 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

View 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

View 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

View File

@ -1,16 +0,0 @@
-----BEGIN PGP SIGNATURE-----
iQIzBAABCgAdFiEESP1vrlFad7SENoIch4lWe4cVzrwFAmmBms0ACgkQh4lWe4cV
zrzmcg/8C7w/0bKC6apqcux+HJb4J38fBy1ZLrvItBhT9gPka8VmbbUWKziBzHa6
+4bRKxKJGY76RCZ88zlMR3hvKvWbir60WxsMi+Wk8pa8x4nWsvxJUTCZSJW437K7
DWuaGui8OAMwLyiocUcf2BXTv0fwlj77PY4h9i6tI81BQoWbD2YHB7/oUxER9SgT
E4Yh/UO9HTlwxtEbM9qQ3m6mnA8vVsdJ6d4vaeqoGGHvLHtr+2jN1nGZb14MyqTU
ke0tS1JWPK5zMeyPs8H4I6cXUbiryeGhLBVr6G4OL5ipA2j6T1HXfQM4VKFgd9oR
z68AP1lNm7zmuEURyd0mCxRSF55I06RvQ48U41SYbTkGJ4sGBcf+cSmNFm6C3u92
QfJGwa2+FXQQC0vte8p7K/5nLXn13qFPgmVv7BuDoaBytg4Ph3jBuVEP5T0ciADK
Vd9MHNLVL/bUHKkoCtGy9Jl4orxC/7tKxOSq2/eG9z+lFS/1J6P/KotIwWMN6udp
PNDhTFTAJT8QKl/syETGpnQbfeZ0q8612UQB0VzwoIiQM+stTJQ8HRejUCtL0dp6
kkBAdPrKEdHzU6dmfZWwnKU9Y9a7B8h9IdRjsMkwQMY2E071ZmgQToU//Vst2rsf
++p7gYVfCH9AVXht4N6eNeaTCD4A5JLk66OTXlxyu/aIpmFCbfI=
=OoqX
-----END PGP SIGNATURE-----

View File

@ -0,0 +1,7 @@
-----BEGIN PGP SIGNATURE-----
iHUEABYKAB0WIQTGmN6Via2HbWGLA7QVfQLvTdXXUgUCalS78AAKCRAVfQLvTdXX
UvPxAP9b5ZS5+KfZdtW1Opd7GUKOjKR2oBM7JQrhFKYMokEekwD/a/660BsaS7Qg
SMpCCHKa8Y0erlIBVc8yBisFb2dCoAs=
=c6+E
-----END PGP SIGNATURE-----

View File

@ -1,3 +1,4 @@
Gris Ge (cathay4t):
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGRA/64BEADubtKFxtanzR3/oa3+/krHUWFcPhUvJNl9kuHY5YykyLG8O8L9
@ -47,6 +48,12 @@ E9NlXivSZRblfY9DEa4v0Zr7L9uyp2JU7taIexoLmPbefORThtGmNoc8DwzLlH8s
SJXEX1ckgzCUNUPQs37ZmV7q4pXh2yYtcZwufH10o02nl67Yuv+43II3vuvEg9CO
qOF1CIUdWB9SZwkAz4MeAjtw5d/YBSqYv9s0pSobvGuo7wBW7MTJ6PkGBzTvdR3H
aOfE6WjbuPjr5H0J1oWyWa0/VB7i1OQ7/55IChT137OnVHRENP8HaGmRZYhxwLsI
=PdCG
-----END PGP PUBLIC KEY BLOCK-----
Fernando Fernández Mancera (ffmancera):
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGRBGdIBEACbSCdhXJBuJnXoENxJSw4TjSiVi20p2jHxJvX5PnesFc+Vt/L4
tbM90JEK93tXSe0SykYxS++UGGsXKwXllBKRy9VG3TpxeajXu6AnnKiuUxV+o+Bq
TT49i8elT4XvoD3eTs5GU3ulzwdCJBcZYgtYWVFIRema1ZCjpOPtu2nl7VCo6/px
@ -94,56 +101,11 @@ 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
KfYnetRPLeXiMqWKTUaImA==
=a0yN
i2QY7dEEs/uMRtq8C8kSWg==
=Byy/
-----END PGP PUBLIC KEY BLOCK-----
Íñigo Huguet (ihuguet):
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBGC54gsBCACtCdyHeyKmLLyrS7K60vBA/ZzBjdNWo2gVzbp3uuXMYmYaVo4E
@ -174,3 +136,22 @@ XUuxEMD5z7mtWDDVscCna0bgaxX0KZC71ViWsPx7Wp1v6+kUzL4KLK1R/7xt6kim
fOao91QN8xXpO4C1MljjqaWb3DWqvdBsTx/nUBPOlw==
=hJlJ
-----END PGP PUBLIC KEY BLOCK-----
Ján Václav (jvaclav-rh):
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEajE0ZRYJKwYBBAHaRw8BAQdA7Fk1giKb1EBdPYNPxMevbIyl/aQeX7YgQFZI
kf+/Vsy0IUrDoW4gVsOhY2xhdiA8anZhY2xhdkByZWRoYXQuY29tPoiZBBMWCgBB
FiEExpjelYmth21hiwO0FX0C703V11IFAmoxNGUCGwMFCQlmAYAFCwkIBwICIgIG
FQoJCAsCBBYCAwECHgcCF4AACgkQFX0C703V11JAjwD9Gy5pSmovjX9GO0S2YTsn
vAZLkWs9YOBYRTXOBsvwjgEBAN6fHaz5RV9tTWby89eMVzuHT3M6xZICMmjWd2rX
510DuDMEajE0ZRYJKwYBBAHaRw8BAQdAa4fXIUTilCDWoD91ffCywxo6lA++3CRO
OASdohzzaYKIfgQYFgoAJhYhBMaY3pWJrYdtYYsDtBV9Au9N1ddSBQJqMTRlAhsg
BQkJZgGAAAoJEBV9Au9N1ddSCeIA/2Ke9zfnnnZtC5/i02JPgAB2E7E4WrZd9+F4
5jqTz8zZAQCtDpVd5tLpMyaFpReIaQd14B8wdEx20lbSWxy/UYXoA7g4BGoxNGUS
CisGAQQBl1UBBQEBB0ChZvD4wSXrHC7GGMDkYo4GZUGHg0IPY16w1JJ5ixn/GgMB
CAeIfgQYFgoAJhYhBMaY3pWJrYdtYYsDtBV9Au9N1ddSBQJqMTRlAhsMBQkJZgGA
AAoJEBV9Au9N1ddSrIIA/RqB58MMw7gEX7zZ06eTXiMc70uXPA0pDkKsLPujVWr4
AQCQ5DdXWt0T15KenMELqNMfz+C3Rq7kGXi9gu+l0ndiAQ==
=N3Ce
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -3,15 +3,28 @@
%define libname libnmstate
Name: nmstate
Version: 2.2.58
Release: 1%{?dist}
Version: 2.2.61
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
@ -78,12 +91,23 @@ This package contains the Python 3 library for Nmstate.
%prep
gpg2 --import --import-options import-export,import-minimal %{SOURCE2} > ./gpgkey-mantainers.gpg
gpgv2 --keyring ./gpgkey-mantainers.gpg %{SOURCE1} %{SOURCE0}
%autosetup -p1
%autosetup -n %{name}-%{version_no_tilde} -p1 %{?rhel:-a3}
# If we have a patch for a vendored dependency, cargo refuses to build,
# in order to prevent accidental manual changes to vendored crates.
# This is not needed in an rpm build. Clear the list of files for which
# to check the checksum.
find vendor -name .cargo-checksum.json \
-exec sed -i.uncheck -e 's/"files":{[^}]*}/"files":{ }/' '{}' '+'
pushd rust
# Source3 is vendored dependencies
%cargo_prep -V 3
%if 0%{?rhel}
mv ../vendor ./
%cargo_prep -v vendor
%else
%cargo_prep
%endif
popd
%build
@ -92,6 +116,11 @@ pushd rust/src/python
popd
pushd rust
%cargo_build
%cargo_license_summary
%{cargo_license} > ../LICENSE.dependencies
%if 0%{?rhel}
%cargo_vendor_manifest
%endif
popd
%install
@ -108,6 +137,10 @@ popd
%files
%doc README.md
%license LICENSE.dependencies
%if 0%{?rhel}
%license rust/cargo-vendor.txt
%endif
%doc examples/
%{_mandir}/man8/nmstate.service.8*
%{_mandir}/man8/nmstatectl.8*
@ -143,6 +176,27 @@ 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
- Fix validation of conflicting routes in kernel mode. RHEL-214095
* Wed May 6 2026 Íñigo Huguet <ihuguet@redhat.com> - 2.2.60-2
- Fix a bug in Nispor that causes not to recognize Infiniband devices (RHEL-173627)
* Wed Apr 22 2026 Ján Václav <jvaclav@redhat.com> - 2.2.60-1
- Upgrade to 2.2.60
- ipv4: Add new parameter prefix-route-metric RHEL-166365
- Fix error when modify port list of down interfaces RHEL-166366
- Add support for configuring ipip tunnel in nmstate RHEL-166367
* Tue Mar 3 2026 Rahul Rajesh <rrajesh@redhat.com> - 2.2.59-1
- Upgrade to 2.2.59
- route: add support for lock-mtu option. RHEL-151934
* Wed Feb 11 2026 Mingyu Shi <mshi@redhat.com> - 2.2.58-1
- Upgrade to 2.2.58
- vrf: Handle ignore interface when verifying desired state. RHEL-141606