Compare commits

...

No commits in common. "c8" and "c9-beta" have entirely different histories.
c8 ... c9-beta

16 changed files with 1749 additions and 700 deletions

4
.gitignore vendored
View File

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

View File

@ -1,2 +1,2 @@
2c5d46ae03fe2d836e165aa3562f03386d215fdf SOURCES/nmstate-1.4.6.tar.gz
4735ef08c31684624a7844832cc2ba20e67983f6 SOURCES/nmstate-vendor-1.4.6.tar.xz
e7c3a7490392927f2bc80f899b7a62be61016e9e SOURCES/nmstate-2.2.61.tar.gz
6243b20ec53da6985c8cf696b5b47dbf103be4ef SOURCES/nmstate-vendor-2.2.61.tar.xz

View File

@ -1,60 +0,0 @@
From 0b530d4c8e75f60015d13c225b1c634389fbe798 Mon Sep 17 00:00:00 2001
From: Gris Ge <fge@redhat.com>
Date: Fri, 17 May 2024 13:12:14 +0800
Subject: [PATCH] clib: Use build.rs to fix SONAME
Use [`cargo:rustc-cdylib-link-arg`][1] to `build.rs` to fix the SONAME issue
of cargo.
Removed workarounds in rpm spec and `.cargo/config.toml`.
Changed Makefile to place `-lnmstate` after the source file to fix
compile issue on ubuntu 20.04 old gcc.
[1]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-cdylib-link-arg
Signed-off-by: Gris Ge <fge@redhat.com>
---
rust/.cargo/config.toml | 3 ---
rust/src/clib/Cargo.toml | 1 +
rust/src/clib/build.rs | 6 ++++++
3 files changed, 7 insertions(+), 3 deletions(-)
create mode 100644 rust/src/clib/build.rs
diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml
index 018e59d4..ca6c72f7 100644
--- a/rust/.cargo/config.toml
+++ b/rust/.cargo/config.toml
@@ -1,5 +1,2 @@
-[build]
-rustflags = "-Clink-arg=-Wl,-soname=libnmstate.so.1"
-
[target.x86_64-unknown-linux-gnu]
runner = 'sudo -E'
diff --git a/rust/src/clib/Cargo.toml b/rust/src/clib/Cargo.toml
index 462757ca..0eb00922 100644
--- a/rust/src/clib/Cargo.toml
+++ b/rust/src/clib/Cargo.toml
@@ -6,6 +6,7 @@ authors = ["Gris Ge <fge@redhat.com>"]
license = "Apache-2.0"
edition = "2018"
rust-version = "1.58"
+build = "build.rs"
[lib]
name = "nmstate"
diff --git a/rust/src/clib/build.rs b/rust/src/clib/build.rs
new file mode 100644
index 00000000..74ad7e48
--- /dev/null
+++ b/rust/src/clib/build.rs
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: Apache-2.0
+
+fn main() {
+ #[cfg(target_os = "linux")]
+ println!("cargo:rustc-cdylib-link-arg=-Wl,-soname=libnmstate.so.1");
+}
--
2.45.1

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,31 +0,0 @@
From 248cd0bff6e3d030ee72b62a8a8b0e37e9f2ef80 Mon Sep 17 00:00:00 2001
From: Fernando Fernandez Mancera <ffmancera@riseup.net>
Date: Tue, 29 Nov 2022 23:56:13 +0100
Subject: [PATCH] nm: reverse IPv6 order before adding them to setting
This is a downstream patch that needs to be applied before any other
patch. Please check:
https://github.com/nmstate/nmstate/commit/2d0cfd5ad8e049f30cad10d977a5fae8bc4e6b64
Signed-off-by: Fernando Fernandez Mancera <ffmancera@riseup.net>
---
libnmstate/nm/ipv6.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libnmstate/nm/ipv6.py b/libnmstate/nm/ipv6.py
index 8e01fd70..7eb3196c 100644
--- a/libnmstate/nm/ipv6.py
+++ b/libnmstate/nm/ipv6.py
@@ -157,7 +157,7 @@ def _set_dynamic(setting_ip, is_dhcp, is_autoconf):
def _set_static(setting_ip, ip_addresses):
- for address in ip_addresses:
+ for address in reversed(ip_addresses):
if iplib.is_ipv6_link_local_addr(
address[InterfaceIPv6.ADDRESS_IP],
address[InterfaceIPv6.ADDRESS_PREFIX_LENGTH],
--
2.38.1

View File

@ -1,184 +0,0 @@
From daf5e4e2282312a80ade85ac5728babf8b9af8b5 Mon Sep 17 00:00:00 2001
From: Lubomir Rintel <lkundrak@v3.sk>
Date: Mon, 17 Jun 2024 19:25:07 +0200
Subject: [PATCH] nm: don't clear connection DNS if global DNS is not specified
If the global DNS state is not specified, let's not overwrite it in
Networkmanager profiles while doing unrelated changes.
This is consistent with mainline (Rust) version of nmstate.
Resolves: https://issues.redhat.com/browse/RHEL-31095
Signed-off-by: Lubomir Rintel <lkundrak@v3.sk>
Signed-off-by: Gris Ge <fge@redhat.com>
---
libnmstate/dns.py | 2 +-
libnmstate/nm/connection.py | 10 +++++++---
libnmstate/nm/ipv4.py | 11 ++++++-----
libnmstate/nm/ipv6.py | 11 ++++++-----
libnmstate/nm/profile.py | 8 +++++---
libnmstate/nm/profiles.py | 7 ++++++-
6 files changed, 31 insertions(+), 18 deletions(-)
diff --git a/libnmstate/dns.py b/libnmstate/dns.py
index 5bb512e8..f50b9bfd 100644
--- a/libnmstate/dns.py
+++ b/libnmstate/dns.py
@@ -51,7 +51,7 @@ class DnsState:
self._config_changed = False
self._cur_dns_state = deepcopy(cur_dns_state) if cur_dns_state else {}
self._dns_state = merge_dns(des_dns_state, cur_dns_state or {})
- if self._dns_state == REMOVE_DNS_CONFIG:
+ if des_dns_state is not None and self._dns_state == REMOVE_DNS_CONFIG:
self._config_changed = True
elif des_dns_state and des_dns_state.get(DNS.CONFIG):
if cur_dns_state:
diff --git a/libnmstate/nm/connection.py b/libnmstate/nm/connection.py
index 6448e372..a6aac82c 100644
--- a/libnmstate/nm/connection.py
+++ b/libnmstate/nm/connection.py
@@ -104,11 +104,15 @@ class _ConnectionSetting:
return self._setting
-def create_new_nm_simple_conn(iface, nm_profile):
+def create_new_nm_simple_conn(iface, nm_profile, clear_dns=False):
nm_iface_type = Api2Nm.get_iface_type(iface.type)
iface_info = iface.to_dict()
- ipv4_set = create_ipv4_setting(iface_info.get(Interface.IPV4), nm_profile)
- ipv6_set = create_ipv6_setting(iface_info.get(Interface.IPV6), nm_profile)
+ ipv4_set = create_ipv4_setting(
+ iface_info.get(Interface.IPV4), nm_profile, clear_dns
+ )
+ ipv6_set = create_ipv6_setting(
+ iface_info.get(Interface.IPV6), nm_profile, clear_dns
+ )
set_wait_ip(ipv4_set, ipv6_set, iface_info.get(Interface.WAIT_IP))
settings = [ipv4_set, ipv6_set]
con_setting = _ConnectionSetting()
diff --git a/libnmstate/nm/ipv4.py b/libnmstate/nm/ipv4.py
index 32b3428e..3f4de059 100644
--- a/libnmstate/nm/ipv4.py
+++ b/libnmstate/nm/ipv4.py
@@ -13,7 +13,7 @@ from .common import NM
INT32_MAX = 2**31 - 1
-def create_setting(config, base_con_profile):
+def create_setting(config, base_con_profile, clear_dns=True):
setting_ipv4 = None
if base_con_profile and config and config.get(InterfaceIPv4.ENABLED):
setting_ipv4 = base_con_profile.get_setting_ip4_config()
@@ -28,10 +28,11 @@ def create_setting(config, base_con_profile):
setting_ipv4.props.route_metric = Route.USE_DEFAULT_METRIC
setting_ipv4.clear_routes()
setting_ipv4.clear_routing_rules()
- setting_ipv4.clear_dns()
- setting_ipv4.clear_dns_searches()
- setting_ipv4.clear_dns_options(False)
- setting_ipv4.props.dns_priority = nm_dns.DEFAULT_DNS_PRIORITY
+ if clear_dns:
+ setting_ipv4.clear_dns()
+ setting_ipv4.clear_dns_searches()
+ setting_ipv4.clear_dns_options(False)
+ setting_ipv4.props.dns_priority = nm_dns.DEFAULT_DNS_PRIORITY
if not setting_ipv4:
setting_ipv4 = NM.SettingIP4Config.new()
diff --git a/libnmstate/nm/ipv6.py b/libnmstate/nm/ipv6.py
index f84d895c..fb3cdcf0 100644
--- a/libnmstate/nm/ipv6.py
+++ b/libnmstate/nm/ipv6.py
@@ -67,7 +67,7 @@ def get_info(active_connection, applied_config):
return info
-def create_setting(config, base_con_profile):
+def create_setting(config, base_con_profile, clear_dns=True):
setting_ip = None
if base_con_profile and config and config.get(InterfaceIPv6.ENABLED):
setting_ip = base_con_profile.get_setting_ip6_config()
@@ -82,10 +82,11 @@ def create_setting(config, base_con_profile):
setting_ip.props.gateway = None
setting_ip.props.route_table = Route.USE_DEFAULT_ROUTE_TABLE
setting_ip.props.route_metric = Route.USE_DEFAULT_METRIC
- setting_ip.clear_dns()
- setting_ip.clear_dns_searches()
- setting_ip.clear_dns_options(False)
- setting_ip.props.dns_priority = nm_dns.DEFAULT_DNS_PRIORITY
+ if clear_dns:
+ setting_ip.clear_dns()
+ setting_ip.clear_dns_searches()
+ setting_ip.clear_dns_options(False)
+ setting_ip.props.dns_priority = nm_dns.DEFAULT_DNS_PRIORITY
if not setting_ip:
setting_ip = NM.SettingIP6Config.new()
diff --git a/libnmstate/nm/profile.py b/libnmstate/nm/profile.py
index a0b1c8f8..acb849b6 100644
--- a/libnmstate/nm/profile.py
+++ b/libnmstate/nm/profile.py
@@ -273,7 +273,9 @@ class NmProfile:
self._iface.type == InterfaceType.ETHERNET and self._iface.is_peer
)
- def prepare_config(self, save_to_disk, gen_conf_mode=False):
+ def prepare_config(
+ self, save_to_disk, gen_conf_mode=False, clear_dns=True
+ ):
if self._iface.is_absent or (
self._iface.is_down
and not gen_conf_mode
@@ -307,7 +309,7 @@ class NmProfile:
# of nmstate should provide full/merged configure.
if self._iface.is_changed or self._iface.is_desired:
self._nm_simple_conn = create_new_nm_simple_conn(
- self._iface, self._nm_profile
+ self._iface, self._nm_profile, clear_dns
)
elif self._nm_profile:
self._nm_simple_conn = NM.SimpleConnection.new_clone(
@@ -316,7 +318,7 @@ class NmProfile:
else:
try:
self._nm_simple_conn = create_new_nm_simple_conn(
- self._iface, self._nm_profile
+ self._iface, self._nm_profile, clear_dns
)
# No error for undesired interface
except NmstateError:
diff --git a/libnmstate/nm/profiles.py b/libnmstate/nm/profiles.py
index e68efdf3..9ce21938 100644
--- a/libnmstate/nm/profiles.py
+++ b/libnmstate/nm/profiles.py
@@ -52,6 +52,7 @@ class NmProfiles:
def apply_config(self, net_state, save_to_disk):
if net_state.dns.config_changed:
+ clear_profile_dns = True
if net_state.use_global_dns:
apply_global_dns(
net_state.dns.config_servers,
@@ -60,6 +61,8 @@ class NmProfiles:
)
else:
apply_global_dns([], [], [])
+ else:
+ clear_profile_dns = False
self._prepare_state_for_profiles(net_state)
# The activation order on bridge/bond ports determins their controler's
@@ -74,7 +77,9 @@ class NmProfiles:
for profile in all_profiles:
profile.import_current()
- profile.prepare_config(save_to_disk, gen_conf_mode=False)
+ profile.prepare_config(
+ save_to_disk, gen_conf_mode=False, clear_dns=clear_profile_dns
+ )
_use_uuid_as_controller_and_parent(all_profiles)
changed_ovs_bridges_and_ifaces = {}
--
2.45.2

View File

@ -1,70 +0,0 @@
From 364842c0c09f9799a2c48a1bc3ce4debb1a3ddc2 Mon Sep 17 00:00:00 2001
From: Gris Ge <fge@redhat.com>
Date: Tue, 18 Jun 2024 13:44:55 +0800
Subject: [PATCH] dns: Do not touch iface DNS when apply identical DNS state
When applying the same DNS only desire state again, nmstate incorrectly
purged interface DNS.
The root cause is we only set `self.use_global_dns` to True when
DNS changed. The fix is set `self.use_global_dns` to True always unless
iface DNS is required.
Signed-off-by: Gris Ge <fge@redhat.com>
---
libnmstate/net_state.py | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/libnmstate/net_state.py b/libnmstate/net_state.py
index 7b208daa..fe6fc31d 100644
--- a/libnmstate/net_state.py
+++ b/libnmstate/net_state.py
@@ -32,7 +32,7 @@ class NetState:
gen_conf_mode=False,
ignored_dns_ifaces=None,
):
- self.use_global_dns = False
+ self.use_global_dns = True
if current_state is None:
current_state = {}
self._ifaces = Ifaces(
@@ -76,17 +76,17 @@ class NetState:
"interface profile, using global DNS"
)
logging.warning(
- "Storing DNS to NetworkManager via global dns API, "
- "this will cause __all__ interface level DNS settings "
- "been ignored"
+ "Storing DNS to NetworkManager via global DNS "
+ "API, this will cause __all__ interface level "
+ "DNS settings been ignored"
)
- self.use_global_dns = True
else:
if self.dns.is_purge() or self._is_iface_dns_prefered():
try:
self._ifaces.gen_dns_metadata(
self._dns, self._route, ignored_dns_ifaces
)
+ self.use_global_dns = False
except NmstateValueError as e:
if (
gen_conf_mode
@@ -99,14 +99,12 @@ class NetState:
"API, this will cause __all__ interface level "
"DNS settings been ignored"
)
- self.use_global_dns = True
elif self.dns.config_changed:
logging.warning(
"Storing DNS to NetworkManager via global DNS "
"API, this will cause __all__ interface level "
"DNS settings been ignored"
)
- self.use_global_dns = True
self._ifaces.gen_route_metadata(self._route)
self._ifaces.gen_route_rule_metadata(self._route_rule, self._route)
--
2.45.2

View File

@ -1,16 +0,0 @@
-----BEGIN PGP SIGNATURE-----
iQIzBAABCAAdFiEESP1vrlFad7SENoIch4lWe4cVzrwFAmZFuTcACgkQh4lWe4cV
zrx0Jg/9FaBh+ine42rKdD+vSITPnGMB2pNSgU8RMdb35fpypVE6Fx4zavvlN14r
rqO+DA57n2NAQ0Cj+n1HOaCxzbfYG2hAX8NwuFN3iY/KadqUdjdPx9G8vEzZDOFu
MCRZxUVIyrHBB9eoTLbhF6hd0XqWminHK43xaLZFBXZAe7DO9QEz+VxM77qsZaE5
EhxASs2mUERvjuY61h9lJb41DOxGLZBM5950S9lDM8cE9ZOi7H/8Q/8fhBPdHh5T
ubPZxsgEBQPEBPz6lE5g4Wcc8ggiCROEuUqRmOTpkb6smgFDsLtCBTakv1Z2U5Q0
YKdnDBfw7T9EV3dhkHR/AHvrLDUZ/bTwe6vO0GQBaSLG/WhvcftzuiDKTcqpAVOm
yUdxla1tinB0cDXohGb50VV9aHd9gFisoLGsPE7BJTqSKhCoQL2zToLHNaMnxNtz
/fJTOyGq1bBtchjbXjaoQO8sa/cxrllWlBYVjaTH7vqwgActrznos/N1sASrMfTw
H1VCKRgwurnomWoIbFNKxPnXNW0Lo31paWhW9wVD78J7Kf9xHrfYiOK2siYgSP7x
nVMuMHZ/j9/EOgHZAPZN3Aod0LWK0/WNwTyFB4IQcLWLaBYVhqlFhShtPo8MeIsJ
Wy7UQmGhtnP8Mt1DQTsfWCH8lQnWpIb84xHYXSEs+2e39GRDCq8=
=lwjx
-----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
@ -95,5 +102,56 @@ YjVo4IPCmJLRb9b8kPX9JuJWDnYWd0SOB00ImaGeXd/kV8W30Lss1OeQ7iya/Ej7
t878uw4RVPKsgCQTWKOWhC0r0DNE/bskGrWZAJGC3M7yqzAErxiIOBKRwH2haegT
syMyW5sNgF43zvxzEHACZnbx+qzHYf+SeQg4pRxLlZj6/Udc3hM/j1cGkMMiwl23
i2QY7dEEs/uMRtq8C8kSWg==
=259x
=Byy/
-----END PGP PUBLIC KEY BLOCK-----
Íñigo Huguet (ihuguet):
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBGC54gsBCACtCdyHeyKmLLyrS7K60vBA/ZzBjdNWo2gVzbp3uuXMYmYaVo4E
pIFxfkgtflefM8lZU//pFgCbj4l9uCyNrcMv5SDwR6T0JFoUyQgNVP2tc3d3Zzks
ktHOBxLyI0HSusKVXcYUSORcMLgW1QfpWWE01zND+Kur+9EQcKhkSVeh/APcDKh3
3OhiqKoo1zRQw/GyiXB8NY+/+zYmJc/Ag4ZL4zwTYfCcFYk2bUD3XWgYICqoY647
wlb398PdyJfKN3SNQdRUZ0zvMBxh6L5uN/CT92W5gey/m6UKfSiwmyzUcxtKvktA
g6DbWlIhjBhNssx/yJM/WWW8FE3piYpXhOQrABEBAAG0I8ONw7FpZ28gSHVndWV0
IDxpaHVndWV0QHJlZGhhdC5jb20+iQFOBBMBCAA4FiEEB/muyGFEOG2VdiELZqRH
gbTrwtAFAmC54gsCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQZqRHgbTr
wtCGSQf+K/yt1oxk85ehjjmT/O+4PhMxPLDTTIbPWwJpSQ7paRkOa53VeA1gmI+m
miDK4CllrO90ZD6ZvzXjbXQP/MA1PDUNNOnCbXX+Tk9uZEx4GdWmkRbdEAOuBK8b
Vg7sH/2a/mtwAW/AYY/K8rcRGTDsNJAcNNfcXl61wGYWHuqyeNXmMB+Y3dXGUxKR
iJOcA0sO8d6DMCDMyeyAdREThbKT2pUfnXN9HdASBU8HHTmf2nyDAD6u/8EEMDfu
2Yb4VtCZ0k92mp/043JRbwJ56AUxdLbWFbBmYgrIeKAtyHaACv06PQXZpQ2dz+WP
S3URA/n5JvntMbIkarU7ftLuYTkn/LkBDQRgueILAQgAvm9vMG73bYZ5dp/TDKMk
y6E6mxi0TXVILfQV8bO2H1S2k19wNOIavqGtbH15QzsL6B2OuwHeAPkUkwQwV8Tz
NCbUFeQb8z5jWjdFVBi42JXwBQmQcYHKYjaOu/7kW+v/5pyfFRLPLfimYCF+YbhY
vTqSMrW4sInUXhUOL1mg0yxWEoZ6Qv3AlUsVc91Ok2j5f1UzjFg+7T4BEYc/pE+d
oP5y/mvYoUt+o6Ma6ENcBgsJ8JFRJTb1OTrXmzebCAW7Md+6rdXGz5mMeqW0QFGV
LgHS1euGza0Vm9wfjA6l3LMxW5jrmxLWsEiugbm4QPIxBR2u8HFVk/G6kBYxsNwd
3QARAQABiQE2BBgBCAAgFiEEB/muyGFEOG2VdiELZqRHgbTrwtAFAmC54gsCGwwA
CgkQZqRHgbTrwtD33wf+ORu+FgSV6m/5M8bsLNA2uh6jp3lFNT6CZy3yza/8Uxqt
RsucUc0qlWmnHbQmwCjAoHpapEtQNvisDgYGAo8ICw+Ij12psxOyFgh0inF7p2ms
86g1Km7DHzFE6B8gwakkVhN6QR+wzr3msJMfj+gfKCbmbuD7PQLjgJL4ITRbMj0u
XUuxEMD5z7mtWDDVscCna0bgaxX0KZC71ViWsPx7Wp1v6+kUzL4KLK1R/7xt6kim
2nJubv+VVuNkx8THxh6chgux1ev8z8fOKiPWcyYZ+wWhbr1Bt98Z24IV4BYY2Ee8
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-----

File diff suppressed because it is too large Load Diff