diff --git a/.gitignore b/.gitignore index 4bea9fc..2f9099a 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.nmstate.metadata b/.nmstate.metadata index 6cde4be..fe4fd9b 100644 --- a/.nmstate.metadata +++ b/.nmstate.metadata @@ -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 diff --git a/SOURCES/0001-nispor-fix-ipoib-iface-type.patch b/SOURCES/0001-nispor-fix-ipoib-iface-type.patch new file mode 100644 index 0000000..6a4aabb --- /dev/null +++ b/SOURCES/0001-nispor-fix-ipoib-iface-type.patch @@ -0,0 +1,63 @@ +From 18fce5de48c2687131eab604c31c4738ffc2554c Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= +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 +--- + 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 + diff --git a/SOURCES/0002-iface-include-existing-interfaces-in-name-search.patch b/SOURCES/0002-iface-include-existing-interfaces-in-name-search.patch new file mode 100644 index 0000000..0f995ae --- /dev/null +++ b/SOURCES/0002-iface-include-existing-interfaces-in-name-search.patch @@ -0,0 +1,134 @@ +From 69df418c0a918163d84cddaa0cf45e6c302c8103 Mon Sep 17 00:00:00 2001 +From: Jan Vaclav +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 +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 = 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")); ++} diff --git a/SOURCES/0003-nispor-derive-alt-name-link-match-from-interface-ide.patch b/SOURCES/0003-nispor-derive-alt-name-link-match-from-interface-ide.patch new file mode 100644 index 0000000..c761047 --- /dev/null +++ b/SOURCES/0003-nispor-derive-alt-name-link-match-from-interface-ide.patch @@ -0,0 +1,275 @@ +From 45e5ed38d1f157e5963628041a461f4073c3295d Mon Sep 17 00:00:00 2001 +From: Josephine Pfeiffer +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 +--- + 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 { ++ 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 { ++ 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 + diff --git a/SOURCES/0004-nispor-keep-OriginalName-link-match-for-stacked-inte.patch b/SOURCES/0004-nispor-keep-OriginalName-link-match-for-stacked-inte.patch new file mode 100644 index 0000000..a1b8b6f --- /dev/null +++ b/SOURCES/0004-nispor-keep-OriginalName-link-match-for-stacked-inte.patch @@ -0,0 +1,405 @@ +From 43a8ad166ac0abffd1c0df047819b538bd68c3d5 Mon Sep 17 00:00:00 2001 +From: Josephine Pfeiffer +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 +--- + 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 + diff --git a/SOURCES/0005-nispor-pin-physical-non-ethernet-alt-name-match-to-o.patch b/SOURCES/0005-nispor-pin-physical-non-ethernet-alt-name-match-to-o.patch new file mode 100644 index 0000000..23bdd6e --- /dev/null +++ b/SOURCES/0005-nispor-pin-physical-non-ethernet-alt-name-match-to-o.patch @@ -0,0 +1,192 @@ +From a1f5311a0bdc3f582dd54004ee8b67e90733bd0e Mon Sep 17 00:00:00 2001 +From: Josephine Pfeiffer +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 +--- + 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 { ++ 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 + diff --git a/SOURCES/0006-nispor-skip-alt-name-removal-for-deleted-virtual-int.patch b/SOURCES/0006-nispor-skip-alt-name-removal-for-deleted-virtual-int.patch new file mode 100644 index 0000000..a828c6f --- /dev/null +++ b/SOURCES/0006-nispor-skip-alt-name-removal-for-deleted-virtual-int.patch @@ -0,0 +1,102 @@ +From 1527741c551a7ac54d5111ee264b3557fd3ad110 Mon Sep 17 00:00:00 2001 +From: Josephine Pfeiffer +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 +--- + 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 + diff --git a/SOURCES/nmstate-2.2.58.tar.gz.asc b/SOURCES/nmstate-2.2.58.tar.gz.asc deleted file mode 100644 index 5feead5..0000000 --- a/SOURCES/nmstate-2.2.58.tar.gz.asc +++ /dev/null @@ -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----- diff --git a/SOURCES/nmstate-2.2.61.tar.gz.asc b/SOURCES/nmstate-2.2.61.tar.gz.asc new file mode 100644 index 0000000..2761da0 --- /dev/null +++ b/SOURCES/nmstate-2.2.61.tar.gz.asc @@ -0,0 +1,7 @@ +-----BEGIN PGP SIGNATURE----- + +iHUEABYKAB0WIQTGmN6Via2HbWGLA7QVfQLvTdXXUgUCalS78AAKCRAVfQLvTdXX +UvPxAP9b5ZS5+KfZdtW1Opd7GUKOjKR2oBM7JQrhFKYMokEekwD/a/660BsaS7Qg +SMpCCHKa8Y0erlIBVc8yBisFb2dCoAs= +=c6+E +-----END PGP SIGNATURE----- diff --git a/SOURCES/nmstate.gpg b/SOURCES/nmstate.gpg index de81f0f..cd6db42 100644 --- a/SOURCES/nmstate.gpg +++ b/SOURCES/nmstate.gpg @@ -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----- diff --git a/SPECS/nmstate.spec b/SPECS/nmstate.spec index 2fb43c8..4dda6be 100644 --- a/SPECS/nmstate.spec +++ b/SPECS/nmstate.spec @@ -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 - 2.2.61-2 +- Fix copy-mac-from failing when source interface is down. RHEL-214096 + +* Thu Jul 23 2026 Rahul Rajesh - 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 - 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 - 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 - 2.2.59-1 +- Upgrade to 2.2.59 +- route: add support for lock-mtu option. RHEL-151934 + * Wed Feb 11 2026 Mingyu Shi - 2.2.58-1 - Upgrade to 2.2.58 - vrf: Handle ignore interface when verifying desired state. RHEL-141606