From 9bd7aada93be2238850e166e72c41ce6de458ac2 Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Sun, 14 Jun 2026 22:24:22 -0400 Subject: [PATCH 1/2] docs(#442): document saved profile updates and add get_saved_connection_uuid Clarify that update_saved_connection requires a profile UUID, not a scanned Network SSID. Add NetworkManager::get_saved_connection_uuid for resolving UUIDs by connection.id (usually the Wi-Fi SSID). --- docs/src/api/network-manager.md | 1 + docs/src/guide/profiles.md | 59 ++++++++++++++++++++++++++++ nmrs/CHANGELOG.md | 3 +- nmrs/src/api/network_manager.rs | 53 ++++++++++++++++++++++++- nmrs/src/core/connection_settings.rs | 59 ++++++++++++++++++++++------ nmrs/tests/integration_test.rs | 29 +++++++++++++- 6 files changed, 188 insertions(+), 16 deletions(-) diff --git a/docs/src/api/network-manager.md b/docs/src/api/network-manager.md index cd5d018f..d57a9172 100644 --- a/docs/src/api/network-manager.md +++ b/docs/src/api/network-manager.md @@ -122,6 +122,7 @@ let config = nm.timeout_config(); | `reload_saved_connections()` | `Result<()>` | Re-read profiles from disk | | `has_saved_connection(ssid)` | `Result` | Check if a Wi-Fi profile exists | | `get_saved_connection_path(ssid)` | `Result>` | Get profile D-Bus path | +| `get_saved_connection_uuid(name)` | `Result>` | Get profile UUID by `connection.id` (usually SSID) | | `forget(ssid)` | `Result<()>` | Delete a Wi-Fi profile | ## Monitoring Methods diff --git a/docs/src/guide/profiles.md b/docs/src/guide/profiles.md index 5d80889f..b9ecf6a7 100644 --- a/docs/src/guide/profiles.md +++ b/docs/src/guide/profiles.md @@ -114,6 +114,65 @@ into an existing profile via NM's `Update` / `UpdateUnsaved` methods. This is the right call to flip `autoconnect`, change a priority, or update DNS without rebuilding the entire profile. +**Important:** the first argument is the profile **UUID** (`connection.uuid`), +not the Wi-Fi SSID. [`Network`](../api/types.md#network) values from a scan +do not include the profile UUID — resolve it first. + +### Look up UUID by profile name (SSID) + +For Wi-Fi profiles, `connection.id` is usually the SSID. The same string +works with `has_saved_connection`, `forget`, and +[`get_saved_connection_uuid`](../api/network-manager.md#connection-profile-methods): + +```rust +use nmrs::{NetworkManager, SettingsPatch}; + +let nm = NetworkManager::new().await?; + +if let Some(uuid) = nm.get_saved_connection_uuid("HomeWiFi").await? { + nm.update_saved_connection( + &uuid, + SettingsPatch { + autoconnect: Some(false), + ..Default::default() + }, + ) + .await?; +} +``` + +### Update while listing saved profiles + +When iterating [`list_saved_connections`](../api/network-manager.md#connection-profile-methods), +each [`SavedConnection`](../api/models.md#savedconnection) already carries `uuid` +and `id`. Match on `id` (or compare against your target SSID) and pass +**`saved.uuid`** to `update_saved_connection`: + +```rust +use nmrs::{NetworkManager, SettingsPatch}; + +let nm = NetworkManager::new().await?; +let target = "HomeWiFi"; + +for saved in nm.list_saved_connections().await? { + if saved.id == target { + nm.update_saved_connection( + &saved.uuid, + SettingsPatch { + autoconnect: Some(!saved.autoconnect), + ..Default::default() + }, + ) + .await?; + } +} +``` + +Common mistake: using a scanned [`Network`](../api/types.md#network)'s `ssid` +where a UUID is required, or calling `update_saved_connection(&network.ssid, …)`. +There is no `uuid` field on `Network` — use `get_saved_connection_uuid` or +`SavedConnection::uuid` instead. + ## Deleting by UUID When the profile UUID is known, you can delete it directly: diff --git a/nmrs/CHANGELOG.md b/nmrs/CHANGELOG.md index c88ff91f..e1c10a6d 100644 --- a/nmrs/CHANGELOG.md +++ b/nmrs/CHANGELOG.md @@ -3,7 +3,8 @@ All notable changes to the `nmrs` crate will be documented in this file. ## [Unreleased] - +### Added +- `NetworkManager::get_saved_connection_uuid()` — resolve a profile UUID from `connection.id` (usually the Wi-Fi SSID) for use with `update_saved_connection` ([#442](https://github.com/networkmanager-rs/nmrs/issues/442)) ## [3.2.0] - 2026-05-31 ### Added - Add EAP-TLS support for WPA-Enterprise Wi-Fi, including TLS certificate/key path or blob configuration on `EapOptions` and `EapMethod::Tls` ([#434](https://github.com/networkmanager-rs/nmrs/pull/434)) diff --git a/nmrs/src/api/network_manager.rs b/nmrs/src/api/network_manager.rs index 599ad1cd..9630da49 100644 --- a/nmrs/src/api/network_manager.rs +++ b/nmrs/src/api/network_manager.rs @@ -18,7 +18,9 @@ use crate::core::connection::{ connect, connect_to_bssid, connect_wired, disconnect, forget_by_name_and_type, get_device_by_interface, is_connected, }; -use crate::core::connection_settings::{get_saved_connection_path, has_saved_connection}; +use crate::core::connection_settings::{ + get_saved_connection_path, get_saved_connection_uuid, has_saved_connection, +}; use crate::core::device::{ is_connecting, list_bluetooth_devices, list_devices, wait_for_wifi_ready, }; @@ -1170,6 +1172,32 @@ impl NetworkManager { } /// Merges a [`SettingsPatch`] into an existing profile (`Update` / `UpdateUnsaved`). + /// + /// `uuid` is the profile's `connection.uuid` (see [`SavedConnection::uuid`]), **not** + /// the Wi-Fi SSID from a scan [`Network`]. Use [`Self::get_saved_connection_uuid`] or + /// [`Self::list_saved_connections`] to resolve the UUID from a profile name / SSID. + /// + /// # Example + /// + /// ```no_run + /// use nmrs::{NetworkManager, SettingsPatch}; + /// + /// # async fn example() -> nmrs::Result<()> { + /// let nm = NetworkManager::new().await?; + /// + /// if let Some(uuid) = nm.get_saved_connection_uuid("HomeWiFi").await? { + /// nm.update_saved_connection( + /// &uuid, + /// SettingsPatch { + /// autoconnect: Some(false), + /// ..Default::default() + /// }, + /// ) + /// .await?; + /// } + /// # Ok(()) + /// # } + /// ``` pub async fn update_saved_connection(&self, uuid: &str, patch: SettingsPatch) -> Result<()> { saved_profiles::update_saved_connection(&self.conn, uuid, &patch).await } @@ -1229,6 +1257,29 @@ impl NetworkManager { get_saved_connection_path(&self.conn, ssid).await } + /// Returns the profile UUID for a saved connection whose `connection.id` matches `name`. + /// + /// For Wi-Fi profiles, `name` is usually the SSID. Use the returned UUID with + /// [`Self::update_saved_connection`] or [`Self::delete_saved_connection`]. + /// + /// # Example + /// + /// ```no_run + /// use nmrs::NetworkManager; + /// + /// # async fn example() -> nmrs::Result<()> { + /// let nm = NetworkManager::new().await?; + /// + /// if let Some(uuid) = nm.get_saved_connection_uuid("HomeWiFi").await? { + /// println!("Profile UUID: {uuid}"); + /// } + /// # Ok(()) + /// # } + /// ``` + pub async fn get_saved_connection_uuid(&self, name: &str) -> Result> { + get_saved_connection_uuid(&self.conn, name).await + } + /// Forgets (deletes) a saved WiFi connection for the given SSID. /// /// If currently connected to this network, disconnects first, then deletes diff --git a/nmrs/src/core/connection_settings.rs b/nmrs/src/core/connection_settings.rs index 03d084ec..76e04997 100644 --- a/nmrs/src/core/connection_settings.rs +++ b/nmrs/src/core/connection_settings.rs @@ -14,21 +14,13 @@ use crate::api::models::ConnectionError; use crate::util::utils::{connection_settings_proxy, settings_proxy}; use crate::util::validation::validate_connection_name; -/// Finds the D-Bus path of a saved connection by SSID or connection name. -/// -/// Iterates through all saved connections in NetworkManager's settings -/// and returns the path of the first one whose connection ID matches -/// the given SSID or name. +/// Finds a saved profile whose `connection.id` matches `name` (SSID for typical Wi-Fi). /// -/// Returns `None` if no saved connection exists for this SSID/name. -pub(crate) async fn get_saved_connection_path( +/// Returns the D-Bus path and `connection.uuid` of the first match. +async fn find_saved_connection_by_name( conn: &Connection, name: &str, -) -> Result> { - if should_skip_lookup(name)? { - return Ok(None); - } - +) -> Result> { let settings = settings_proxy(conn).await?; let reply = settings @@ -57,14 +49,55 @@ pub(crate) async fn get_saved_connection_path( if let Some(conn_section) = all.get("connection") && let Some(Value::Str(id)) = conn_section.get("id") && id == name + && let Some(Value::Str(uuid)) = conn_section.get("uuid") { - return Ok(Some(cpath)); + return Ok(Some((cpath, uuid.to_string()))); } } Ok(None) } +/// Finds the D-Bus path of a saved connection by SSID or connection name. +/// +/// Iterates through all saved connections in NetworkManager's settings +/// and returns the path of the first one whose connection ID matches +/// the given SSID or name. +/// +/// Returns `None` if no saved connection exists for this SSID/name. +pub(crate) async fn get_saved_connection_path( + conn: &Connection, + name: &str, +) -> Result> { + if should_skip_lookup(name)? { + return Ok(None); + } + + Ok(find_saved_connection_by_name(conn, name) + .await? + .map(|(path, _)| path)) +} + +/// Returns the profile UUID for a saved connection whose `connection.id` matches `name`. +/// +/// For Wi-Fi profiles created by nmrs, `connection.id` is usually the SSID — the same +/// string accepted by [`has_saved_connection`](crate::NetworkManager::has_saved_connection) +/// and [`forget`](crate::NetworkManager::forget). +/// +/// Returns `None` when no profile matches. +pub(crate) async fn get_saved_connection_uuid( + conn: &Connection, + name: &str, +) -> Result> { + if should_skip_lookup(name)? { + return Ok(None); + } + + Ok(find_saved_connection_by_name(conn, name) + .await? + .map(|(_, uuid)| uuid)) +} + fn should_skip_lookup(name: &str) -> Result { if name.trim().is_empty() { return Ok(true); diff --git a/nmrs/tests/integration_test.rs b/nmrs/tests/integration_test.rs index 151e6bae..044ae1c0 100644 --- a/nmrs/tests/integration_test.rs +++ b/nmrs/tests/integration_test.rs @@ -435,7 +435,34 @@ async fn test_get_saved_connection_path() { .await .expect("Failed to get saved connection path for empty SSID"); // Result can be Some or None depending on system state - assert!(result.is_some() || result.is_none()); + let _ = result; +} + +/// Test getting the UUID of a saved connection +#[tokio::test] +#[serial] +async fn test_get_saved_connection_uuid() { + require_networkmanager!(); + + let nm = NetworkManager::new() + .await + .expect("Failed to create NetworkManager"); + require_wifi!(&nm); + + let result = nm + .get_saved_connection_uuid("__NONEXISTENT_TEST_SSID__") + .await + .expect("Failed to get saved connection UUID"); + assert!( + result.is_none(), + "Non-existent SSID should not have saved connection UUID" + ); + + let result = nm + .get_saved_connection_uuid("") + .await + .expect("Failed to get saved connection UUID for empty SSID"); + let _ = result; } /// Test connecting to an open network From 4f36c8ae9d37e8eda17ffe3ae5a64282e73e7a50 Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Sun, 14 Jun 2026 22:28:30 -0400 Subject: [PATCH 2/2] fix(#442): restrict struct literals to work inside the crate --- docs/src/guide/profiles.md | 22 ++++++---------------- nmrs/src/api/network_manager.rs | 11 +++-------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/docs/src/guide/profiles.md b/docs/src/guide/profiles.md index b9ecf6a7..1ae9f9a2 100644 --- a/docs/src/guide/profiles.md +++ b/docs/src/guide/profiles.md @@ -130,14 +130,9 @@ use nmrs::{NetworkManager, SettingsPatch}; let nm = NetworkManager::new().await?; if let Some(uuid) = nm.get_saved_connection_uuid("HomeWiFi").await? { - nm.update_saved_connection( - &uuid, - SettingsPatch { - autoconnect: Some(false), - ..Default::default() - }, - ) - .await?; + let mut patch = SettingsPatch::default(); + patch.autoconnect = Some(false); + nm.update_saved_connection(&uuid, patch).await?; } ``` @@ -156,14 +151,9 @@ let target = "HomeWiFi"; for saved in nm.list_saved_connections().await? { if saved.id == target { - nm.update_saved_connection( - &saved.uuid, - SettingsPatch { - autoconnect: Some(!saved.autoconnect), - ..Default::default() - }, - ) - .await?; + let mut patch = SettingsPatch::default(); + patch.autoconnect = Some(!saved.autoconnect); + nm.update_saved_connection(&saved.uuid, patch).await?; } } ``` diff --git a/nmrs/src/api/network_manager.rs b/nmrs/src/api/network_manager.rs index 9630da49..01542488 100644 --- a/nmrs/src/api/network_manager.rs +++ b/nmrs/src/api/network_manager.rs @@ -1186,14 +1186,9 @@ impl NetworkManager { /// let nm = NetworkManager::new().await?; /// /// if let Some(uuid) = nm.get_saved_connection_uuid("HomeWiFi").await? { - /// nm.update_saved_connection( - /// &uuid, - /// SettingsPatch { - /// autoconnect: Some(false), - /// ..Default::default() - /// }, - /// ) - /// .await?; + /// let mut patch = SettingsPatch::default(); + /// patch.autoconnect = Some(false); + /// nm.update_saved_connection(&uuid, patch).await?; /// } /// # Ok(()) /// # }