From 4a2206b8199cf9ad98b90523d008b20b530797e9 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Mon, 13 Oct 2025 15:18:46 +0100 Subject: [PATCH 01/17] Adapt network HTTP API to v2 --- rust/Cargo.lock | 2 + rust/agama-manager/Cargo.toml | 1 + rust/agama-manager/src/lib.rs | 1 + rust/agama-manager/src/service.rs | 37 ++++++- rust/agama-manager/src/start.rs | 18 +++- rust/agama-network/src/action.rs | 4 + rust/agama-network/src/lib.rs | 2 + rust/agama-network/src/model.rs | 16 ++- rust/agama-network/src/settings.rs | 27 +++++ rust/agama-network/src/system.rs | 25 ++++- rust/agama-network/src/system_info.rs | 74 ++++++++++++++ rust/agama-server/Cargo.toml | 1 + rust/agama-server/src/lib.rs | 1 - rust/agama-server/src/web.rs | 9 -- rust/agama-server/src/web/docs.rs | 2 - rust/agama-server/src/web/docs/network.rs | 119 ---------------------- rust/agama-utils/src/api/proposal.rs | 1 + rust/agama-utils/src/api/system_info.rs | 2 + rust/xtask/src/main.rs | 5 +- 19 files changed, 207 insertions(+), 140 deletions(-) create mode 100644 rust/agama-network/src/system_info.rs delete mode 100644 rust/agama-server/src/web/docs/network.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9d07e9219b..6964d5e2ff 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -131,6 +131,7 @@ name = "agama-manager" version = "0.1.0" dependencies = [ "agama-l10n", + "agama-network", "agama-storage", "agama-utils", "async-trait", @@ -175,6 +176,7 @@ dependencies = [ "agama-lib", "agama-locale-data", "agama-manager", + "agama-network", "agama-utils", "anyhow", "async-trait", diff --git a/rust/agama-manager/Cargo.toml b/rust/agama-manager/Cargo.toml index 9738008b51..5004fffb6c 100644 --- a/rust/agama-manager/Cargo.toml +++ b/rust/agama-manager/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true [dependencies] agama-utils = { path = "../agama-utils" } agama-l10n = { path = "../agama-l10n" } +agama-network = { path = "../agama-network" } agama-storage = { path = "../agama-storage" } thiserror = "2.0.12" tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread", "sync"] } diff --git a/rust/agama-manager/src/lib.rs b/rust/agama-manager/src/lib.rs index 49a1a5b366..39260e92ad 100644 --- a/rust/agama-manager/src/lib.rs +++ b/rust/agama-manager/src/lib.rs @@ -27,4 +27,5 @@ pub use service::Service; pub mod message; pub use agama_l10n as l10n; +pub use agama_network as network; pub use agama_storage as storage; diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index 29e03e6dfc..5ec09b1b77 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -19,6 +19,8 @@ // find current contact information at www.suse.com. use crate::{l10n, message, storage}; + +use agama_network::{error::NetworkStateError, NetworkSystemClient, NetworkSystemError}; use agama_utils::{ actor::{self, Actor, Handler, MessageHandler}, api::{ @@ -50,10 +52,15 @@ pub enum Error { Questions(#[from] question::service::Error), #[error(transparent)] Progress(#[from] progress::service::Error), + #[error(transparent)] + NetworkSystemError(#[from] NetworkSystemError), + #[error(transparent)] + NetworkStateError(#[from] NetworkStateError), } pub struct Service { l10n: Handler, + network: NetworkSystemClient, storage: Handler, issues: Handler, progress: Handler, @@ -66,6 +73,7 @@ pub struct Service { impl Service { pub fn new( l10n: Handler, + network: NetworkSystemClient, storage: Handler, issues: Handler, progress: Handler, @@ -74,6 +82,7 @@ impl Service { ) -> Self { Self { l10n, + network, storage, issues, progress, @@ -147,7 +156,12 @@ impl MessageHandler for Service { async fn handle(&mut self, _message: message::GetSystem) -> Result { let l10n = self.l10n.call(l10n::message::GetSystem).await?; let storage = self.storage.call(storage::message::GetSystem).await?; - Ok(SystemInfo { l10n, storage }) + let network = self.network.get_system_config().await?; + Ok(SystemInfo { + l10n, + network, + storage, + }) } } @@ -159,10 +173,17 @@ impl MessageHandler for Service { async fn handle(&mut self, _message: message::GetExtendedConfig) -> Result { let l10n = self.l10n.call(l10n::message::GetConfig).await?; let questions = self.questions.call(question::message::GetConfig).await?; + let network_config: agama_network::SystemInfo = + self.network.get_extended_config().await?.try_into()?; + let network = Some(NetworkSettings { + connections: network_config.connections, + }); let storage = self.storage.call(storage::message::GetConfig).await?; + Ok(Config { l10n: Some(l10n), - questions, + questions: Some(questions), + network, storage, }) } @@ -239,7 +260,17 @@ impl MessageHandler for Service { async fn handle(&mut self, _message: message::GetProposal) -> Result, Error> { let l10n = self.l10n.call(l10n::message::GetProposal).await?; let storage = self.storage.call(storage::message::GetProposal).await?; - Ok(Some(Proposal { l10n, storage })) + let network_config: agama_network::SystemInfo = + self.network.get_extended_config().await?.try_into()?; + let network = Some(NetworkSettings { + connections: network_config.connections, + }); + + Ok(Some(Proposal { + l10n, + network, + storage, + })) } } diff --git a/rust/agama-manager/src/start.rs b/rust/agama-manager/src/start.rs index 98acd79976..c0b03715d2 100644 --- a/rust/agama-manager/src/start.rs +++ b/rust/agama-manager/src/start.rs @@ -19,12 +19,18 @@ // find current contact information at www.suse.com. use crate::{l10n, service::Service, storage}; +use agama_lib::network::{ + NetworkAdapterError, NetworkClientError, NetworkManagerAdapter, NetworkSystem, + NetworkSystemError, +}; use agama_utils::{ actor::{self, Handler}, api::event, issue, progress, question, }; +use tokio::sync::mpsc; + #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] @@ -34,6 +40,12 @@ pub enum Error { #[error(transparent)] L10n(#[from] l10n::start::Error), #[error(transparent)] + #[error(transparent)] + NetworkClient(#[from] NetworkClientError), + #[error(transparent)] + NetworkAdapter(#[from] NetworkAdapterError), + #[error(transparent)] + NetworkSystem(#[from] NetworkSystemError), Storage(#[from] storage::start::Error), } @@ -51,8 +63,12 @@ pub async fn start( let progress = progress::start(events.clone()).await?; let l10n = l10n::start(issues.clone(), events.clone()).await?; let storage = storage::start(progress.clone(), issues.clone(), events.clone(), dbus).await?; + let network_adapter = NetworkManagerAdapter::from_system() + .await + .expect("Could not connect to NetworkManager"); + let network = NetworkSystem::new(network_adapter).start().await?; - let service = Service::new(l10n, storage, issues, progress, questions, events); + let service = Service::new(l10n, network, storage, issues, progress, questions, events); let handler = actor::spawn(service); Ok(handler) } diff --git a/rust/agama-network/src/action.rs b/rust/agama-network/src/action.rs index d1d18a83ba..aed5e25183 100644 --- a/rust/agama-network/src/action.rs +++ b/rust/agama-network/src/action.rs @@ -19,7 +19,9 @@ // find current contact information at www.suse.com. use crate::model::{AccessPoint, Connection, Device}; +use crate::system_info::SystemInfo; use crate::types::{ConnectionState, DeviceType}; +use crate::NetworkState; use tokio::sync::oneshot; use uuid::Uuid; @@ -42,6 +44,8 @@ pub enum Action { GetConnection(String, Responder>), /// Gets a connection by its Uuid GetConnectionByUuid(Uuid, Responder>), + GetExtendedConfig(Responder), + GetSystemConfig(Responder), /// Gets a connection GetConnections(Responder>), /// Gets a controller connection diff --git a/rust/agama-network/src/lib.rs b/rust/agama-network/src/lib.rs index 01b992bc03..1d8a527cb8 100644 --- a/rust/agama-network/src/lib.rs +++ b/rust/agama-network/src/lib.rs @@ -29,6 +29,7 @@ pub mod model; mod nm; pub mod settings; mod system; +pub mod system_info; pub mod types; pub use action::Action; @@ -36,3 +37,4 @@ pub use adapter::{Adapter, NetworkAdapterError}; pub use model::NetworkState; pub use nm::NetworkManagerAdapter; pub use system::{NetworkSystem, NetworkSystemClient, NetworkSystemError}; +pub use system_info::SystemInfo; diff --git a/rust/agama-network/src/model.rs b/rust/agama-network/src/model.rs index e5a2eb28ed..7cdce7c904 100644 --- a/rust/agama-network/src/model.rs +++ b/rust/agama-network/src/model.rs @@ -252,6 +252,20 @@ impl NetworkState { )), } } + + pub fn ports_for(&self, uuid: Uuid) -> Vec { + self.connections + .iter() + .filter(|c| c.controller == Some(uuid)) + .map(|c| { + if let Some(interface) = c.interface.to_owned() { + interface + } else { + c.clone().id + } + }) + .collect() + } } #[cfg(test)] @@ -474,7 +488,7 @@ pub struct GeneralState { /// Access Point #[serde_as] -#[derive(Default, Debug, Clone, Serialize, utoipa::ToSchema)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct AccessPoint { #[serde_as(as = "DisplayFromStr")] diff --git a/rust/agama-network/src/settings.rs b/rust/agama-network/src/settings.rs index db9a4f6120..6393eccf77 100644 --- a/rust/agama-network/src/settings.rs +++ b/rust/agama-network/src/settings.rs @@ -21,6 +21,7 @@ //! Representation of the network settings use super::types::{DeviceState, DeviceType, Status}; +use crate::{error::NetworkStateError, NetworkState}; use agama_utils::openapi::schemas; use cidr::IpInet; use serde::{Deserialize, Serialize}; @@ -35,6 +36,32 @@ pub struct NetworkSettings { pub connections: Vec, } +impl TryFrom for NetworkSettings { + type Error = NetworkStateError; + + fn try_from(state: NetworkState) -> Result { + let connections = &state.connections; + + let network_connections = connections + .iter() + .filter(|c| c.controller.is_none()) + .map(|c| { + let mut conn = NetworkConnection::try_from(c.clone()).unwrap(); + if let Some(ref mut bond) = conn.bond { + bond.ports = state.ports_for(c.uuid); + } + if let Some(ref mut bridge) = conn.bridge { + bridge.ports = state.ports_for(c.uuid); + }; + conn + }) + .collect(); + + Ok(NetworkSettings { + connections: network_connections, + }) + } +} #[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] pub struct MatchSettings { #[serde(skip_serializing_if = "Vec::is_empty", default)] diff --git a/rust/agama-network/src/system.rs b/rust/agama-network/src/system.rs index 64a80cc623..9a696bce68 100644 --- a/rust/agama-network/src/system.rs +++ b/rust/agama-network/src/system.rs @@ -25,7 +25,7 @@ use crate::{ AccessPoint, Connection, Device, GeneralState, NetworkChange, NetworkState, StateConfig, }, types::DeviceType, - Adapter, NetworkAdapterError, + Adapter, NetworkAdapterError, SystemInfo, }; use std::error::Error; use tokio::sync::{ @@ -163,6 +163,17 @@ impl NetworkSystemClient { self.actions.send(Action::GetConnections(tx))?; Ok(rx.await?) } + pub async fn get_extended_config(&self) -> Result { + let (tx, rx) = oneshot::channel(); + self.actions.send(Action::GetExtendedConfig(tx))?; + Ok(rx.await?) + } + + pub async fn get_system_config(&self) -> Result { + let (tx, rx) = oneshot::channel(); + self.actions.send(Action::GetSystemConfig(tx))?; + Ok(rx.await?) + } /// Adds a new connection. pub async fn add_connection(&self, connection: Connection) -> Result<(), NetworkSystemError> { @@ -310,6 +321,13 @@ impl NetworkSystemServer { let conn = self.state.get_connection_by_uuid(uuid); tx.send(conn.cloned()).unwrap(); } + Action::GetSystemConfig(tx) => { + let result = self.read().await?.try_into()?; + tx.send(result).unwrap(); + } + Action::GetExtendedConfig(tx) => { + tx.send(self.state.clone()).unwrap(); + } Action::GetConnections(tx) => { let connections = self .state @@ -424,6 +442,11 @@ impl NetworkSystemServer { Ok((conn, controlled)) } + /// Reads the system network configuration. + pub async fn read(&mut self) -> Result { + self.adapter.read(StateConfig::default()).await + } + /// Writes the network configuration. pub async fn write(&mut self) -> Result<(), NetworkAdapterError> { self.adapter.write(&self.state).await?; diff --git a/rust/agama-network/src/system_info.rs b/rust/agama-network/src/system_info.rs new file mode 100644 index 0000000000..7686a628b8 --- /dev/null +++ b/rust/agama-network/src/system_info.rs @@ -0,0 +1,74 @@ +// Copyright (c) [2025] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + +//! Representation of the network settings + +use serde::{Deserialize, Serialize}; +use std::default::Default; + +use crate::{ + error::NetworkStateError, + model::{AccessPoint, Device, GeneralState}, + settings::NetworkConnection, + NetworkState, +}; + +/// Network settings for installation +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct SystemInfo { + pub access_points: Vec, + /// Connections to use in the installation + pub connections: Vec, + pub devices: Vec, + pub general_state: GeneralState, +} + +impl TryFrom for SystemInfo { + type Error = NetworkStateError; + + fn try_from(state: NetworkState) -> Result { + let connections = &state.connections; + let network_connections = connections + .iter() + .filter(|c| c.controller.is_none()) + .map(|c| { + let mut conn = NetworkConnection::try_from(c.clone()).unwrap(); + if let Some(ref mut bond) = conn.bond { + bond.ports = state.ports_for(c.uuid); + } + if let Some(ref mut bridge) = conn.bridge { + bridge.ports = state.ports_for(c.uuid); + }; + conn + }) + .collect(); + let access_points = state.access_points; + let devices = state.devices; + let general_state = state.general_state; + + Ok(SystemInfo { + access_points, + devices, + connections: network_connections, + general_state, + }) + } +} diff --git a/rust/agama-server/Cargo.toml b/rust/agama-server/Cargo.toml index 338df05d2e..42b3976db3 100644 --- a/rust/agama-server/Cargo.toml +++ b/rust/agama-server/Cargo.toml @@ -13,6 +13,7 @@ agama-utils = { path = "../agama-utils" } agama-l10n = { path = "../agama-l10n" } agama-locale-data = { path = "../agama-locale-data" } agama-manager = { path = "../agama-manager" } +agama-network = { path = "../agama-network" } zbus = { version = "5", default-features = false, features = ["tokio"] } uuid = { version = "1.10.0", features = ["v4"] } thiserror = "2.0.12" diff --git a/rust/agama-server/src/lib.rs b/rust/agama-server/src/lib.rs index 3000c9fd87..0339ee0d70 100644 --- a/rust/agama-server/src/lib.rs +++ b/rust/agama-server/src/lib.rs @@ -26,7 +26,6 @@ pub mod files; pub mod hostname; pub mod logs; pub mod manager; -pub mod network; pub mod profile; pub mod scripts; pub mod security; diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index 5bd9bd4b72..6641c41aba 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -30,7 +30,6 @@ use crate::{ files::web::files_service, hostname::web::hostname_service, manager::web::{manager_service, manager_stream}, - network::{web::network_service, NetworkManagerAdapter}, profile::web::profile_service, scripts::web::scripts_service, security::security_service, @@ -77,10 +76,6 @@ pub async fn service

( where P: AsRef, { - let network_adapter = NetworkManagerAdapter::from_system() - .await - .expect("Could not connect to NetworkManager to read the configuration"); - let progress = ProgressService::start(dbus.clone(), old_events.clone()).await; let router = MainServiceBuilder::new(events.clone(), old_events.clone(), web_ui_dir) @@ -97,10 +92,6 @@ where .add_service("/storage", storage_service(dbus.clone(), progress).await?) .add_service("/iscsi", iscsi_service(dbus.clone()).await?) .add_service("/bootloader", bootloader_service(dbus.clone()).await?) - .add_service( - "/network", - network_service(network_adapter, old_events).await?, - ) .add_service("/users", users_service(dbus.clone()).await?) .add_service("/scripts", scripts_service().await?) .add_service("/files", files_service().await?) diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index 219a476ed3..87fcffbc08 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -24,8 +24,6 @@ mod config; pub use config::ConfigApiDocBuilder; mod hostname; pub use hostname::HostnameApiDocBuilder; -mod network; -pub use network::NetworkApiDocBuilder; mod storage; pub use storage::StorageApiDocBuilder; mod bootloader; diff --git a/rust/agama-server/src/web/docs/network.rs b/rust/agama-server/src/web/docs/network.rs deleted file mode 100644 index 26661f41fe..0000000000 --- a/rust/agama-server/src/web/docs/network.rs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) [2024] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -use agama_utils::openapi::schemas; -use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; - -use super::ApiDocBuilder; - -pub struct NetworkApiDocBuilder; - -impl ApiDocBuilder for NetworkApiDocBuilder { - fn title(&self) -> String { - "Network HTTP API".to_string() - } - - fn paths(&self) -> Paths { - PathsBuilder::new() - .path_from::() - .path_from::() - .path_from::() - .path_from::() - .path_from::() - .path_from::() - .path_from::() - .path_from::() - .path_from::() - .path_from::() - .path_from::() - .path_from::() - .path_from::() - .build() - } - - fn components(&self) -> Components { - ComponentsBuilder::new() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema("IpAddr", schemas::ip_addr()) - .schema("IpInet", schemas::ip_inet()) - .schema("macaddr.MacAddr6", schemas::mac_addr6()) - .build() - } -} diff --git a/rust/agama-utils/src/api/proposal.rs b/rust/agama-utils/src/api/proposal.rs index 4b184c0913..185b606073 100644 --- a/rust/agama-utils/src/api/proposal.rs +++ b/rust/agama-utils/src/api/proposal.rs @@ -27,6 +27,7 @@ use serde_json::Value; pub struct Proposal { #[serde(skip_serializing_if = "Option::is_none")] pub l10n: Option, + pub network: Option, #[serde(skip_serializing_if = "Option::is_none")] pub storage: Option, } diff --git a/rust/agama-utils/src/api/system_info.rs b/rust/agama-utils/src/api/system_info.rs index 0ae7e5b8b2..82c9f5e5fd 100644 --- a/rust/agama-utils/src/api/system_info.rs +++ b/rust/agama-utils/src/api/system_info.rs @@ -19,6 +19,7 @@ // find current contact information at www.suse.com. use crate::api::l10n; +use crate::network; use serde::Serialize; use serde_json::Value; @@ -29,4 +30,5 @@ pub struct SystemInfo { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub storage: Option, + pub network: network::SystemInfo, } diff --git a/rust/xtask/src/main.rs b/rust/xtask/src/main.rs index 92cf1d7a1f..0e31d8df2b 100644 --- a/rust/xtask/src/main.rs +++ b/rust/xtask/src/main.rs @@ -6,8 +6,8 @@ mod tasks { use agama_cli::Cli; use agama_server::web::docs::{ ApiDocBuilder, ConfigApiDocBuilder, HostnameApiDocBuilder, ManagerApiDocBuilder, - MiscApiDocBuilder, NetworkApiDocBuilder, ProfileApiDocBuilder, ScriptsApiDocBuilder, - SoftwareApiDocBuilder, StorageApiDocBuilder, UsersApiDocBuilder, + MiscApiDocBuilder, ProfileApiDocBuilder, ScriptsApiDocBuilder, SoftwareApiDocBuilder, + StorageApiDocBuilder, UsersApiDocBuilder, }; use clap::CommandFactory; use clap_complete::aot; @@ -68,7 +68,6 @@ mod tasks { write_openapi(HostnameApiDocBuilder {}, out_dir.join("hostname.json"))?; write_openapi(ManagerApiDocBuilder {}, out_dir.join("manager.json"))?; write_openapi(MiscApiDocBuilder {}, out_dir.join("misc.json"))?; - write_openapi(NetworkApiDocBuilder {}, out_dir.join("network.json"))?; write_openapi(ProfileApiDocBuilder {}, out_dir.join("profile.json"))?; write_openapi(ScriptsApiDocBuilder {}, out_dir.join("scripts.json"))?; write_openapi(SoftwareApiDocBuilder {}, out_dir.join("software.json"))?; From 427ab7008979c1992b810e02fe702d565591403e Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Wed, 29 Oct 2025 14:31:27 +0000 Subject: [PATCH 02/17] Do not depend on agama-lib network changes --- rust/Cargo.lock | 1 + rust/agama-lib/src/network.rs | 4 +- rust/agama-lib/src/network/client.rs | 2 +- rust/agama-lib/src/network/store.rs | 14 +- rust/agama-manager/src/service.rs | 21 +- rust/agama-manager/src/start.rs | 20 +- rust/agama-network/src/action.rs | 6 +- rust/agama-network/src/lib.rs | 3 - rust/agama-network/src/model.rs | 73 +++- rust/agama-network/src/nm/builder.rs | 2 +- rust/agama-network/src/system.rs | 9 +- rust/agama-network/src/system_info.rs | 74 ---- rust/agama-network/src/types.rs | 314 +---------------- rust/agama-server/src/web/docs/config.rs | 20 +- rust/agama-utils/Cargo.toml | 1 + rust/agama-utils/src/api.rs | 1 + rust/agama-utils/src/api/config.rs | 4 +- rust/agama-utils/src/api/network.rs | 34 ++ rust/agama-utils/src/api/network/config.rs | 33 ++ rust/agama-utils/src/api/network/proposal.rs | 33 ++ .../src/api/network}/settings.rs | 47 +-- .../src/api/network/system_info.rs | 33 ++ rust/agama-utils/src/api/network/types.rs | 326 ++++++++++++++++++ rust/agama-utils/src/api/proposal.rs | 4 +- rust/agama-utils/src/api/system_info.rs | 2 +- 25 files changed, 602 insertions(+), 479 deletions(-) delete mode 100644 rust/agama-network/src/system_info.rs create mode 100644 rust/agama-utils/src/api/network.rs create mode 100644 rust/agama-utils/src/api/network/config.rs create mode 100644 rust/agama-utils/src/api/network/proposal.rs rename rust/{agama-network/src => agama-utils/src/api/network}/settings.rs (91%) create mode 100644 rust/agama-utils/src/api/network/system_info.rs create mode 100644 rust/agama-utils/src/api/network/types.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 6964d5e2ff..606aaa6390 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -240,6 +240,7 @@ version = "0.1.0" dependencies = [ "agama-locale-data", "async-trait", + "cidr", "gettext-rs", "serde", "serde_json", diff --git a/rust/agama-lib/src/network.rs b/rust/agama-lib/src/network.rs index 41fa7fb7d9..5fe3da04f5 100644 --- a/rust/agama-lib/src/network.rs +++ b/rust/agama-lib/src/network.rs @@ -24,9 +24,9 @@ mod client; mod store; pub use agama_network::{ - error, model, settings, types, Action, Adapter, NetworkAdapterError, NetworkManagerAdapter, + error, model, types, Action, Adapter, NetworkAdapterError, NetworkManagerAdapter, NetworkSystem, NetworkSystemClient, NetworkSystemError, }; +pub use agama_utils::api::network::*; pub use client::{NetworkClient, NetworkClientError}; -pub use settings::NetworkSettings; pub use store::{NetworkStore, NetworkStoreError}; diff --git a/rust/agama-lib/src/network/client.rs b/rust/agama-lib/src/network/client.rs index 0fb0b6bb30..dbb3854beb 100644 --- a/rust/agama-lib/src/network/client.rs +++ b/rust/agama-lib/src/network/client.rs @@ -18,8 +18,8 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use super::{settings::NetworkConnection, types::Device}; use crate::http::{BaseHTTPClient, BaseHTTPClientError}; +use crate::network::{Device, NetworkConnection}; use crate::utils::url::encode; #[derive(Debug, thiserror::Error)] diff --git a/rust/agama-lib/src/network/store.rs b/rust/agama-lib/src/network/store.rs index a591b529dd..dcc2fecfe8 100644 --- a/rust/agama-lib/src/network/store.rs +++ b/rust/agama-lib/src/network/store.rs @@ -18,11 +18,13 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use super::{settings::NetworkConnection, NetworkClientError}; +use super::NetworkClientError; use crate::{ http::BaseHTTPClient, network::{NetworkClient, NetworkSettings}, }; +use agama_network::types::NetworkConnectionsCollection; +use agama_utils::api::network::NetworkConnection; #[derive(Debug, thiserror::Error)] #[error("Error processing network settings: {0}")] @@ -44,15 +46,17 @@ impl NetworkStore { // TODO: read the settings from the service pub async fn load(&self) -> NetworkStoreResult { - let connections = self.network_client.connections().await?; + let connections = NetworkConnectionsCollection(self.network_client.connections().await?); + Ok(NetworkSettings { connections }) } pub async fn store(&self, settings: &NetworkSettings) -> NetworkStoreResult<()> { - for id in ordered_connections(&settings.connections) { + let connections = &settings.connections.0; + for id in ordered_connections(connections) { let id = id.as_str(); let fallback = default_connection(id); - let conn = find_connection(id, &settings.connections).unwrap_or(&fallback); + let conn = find_connection(id, connections).unwrap_or(&fallback); self.network_client .add_or_update_connection(conn.clone()) .await?; @@ -129,7 +133,7 @@ fn default_connection(id: &str) -> NetworkConnection { #[cfg(test)] mod tests { use super::ordered_connections; - use crate::network::settings::{BondSettings, BridgeSettings, NetworkConnection}; + use crate::network::{BondSettings, BridgeSettings, NetworkConnection}; #[test] fn test_ordered_connections() { diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index 5ec09b1b77..dee73fd20c 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -18,9 +18,7 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::{l10n, message, storage}; - -use agama_network::{error::NetworkStateError, NetworkSystemClient, NetworkSystemError}; +use crate::{l10n, message, network, storage}; use agama_utils::{ actor::{self, Actor, Handler, MessageHandler}, api::{ @@ -31,6 +29,7 @@ use agama_utils::{ }; use async_trait::async_trait; use merge_struct::merge; +use network::{error::NetworkStateError, types, NetworkSystemClient, NetworkSystemError}; use serde_json::Value; use tokio::sync::broadcast; @@ -173,17 +172,16 @@ impl MessageHandler for Service { async fn handle(&mut self, _message: message::GetExtendedConfig) -> Result { let l10n = self.l10n.call(l10n::message::GetConfig).await?; let questions = self.questions.call(question::message::GetConfig).await?; - let network_config: agama_network::SystemInfo = - self.network.get_extended_config().await?.try_into()?; - let network = Some(NetworkSettings { + let network_config: network::types::Proposal = self.network.get_extended_config().await?; + let network = agama_network::types::Config { connections: network_config.connections, - }); + }; let storage = self.storage.call(storage::message::GetConfig).await?; Ok(Config { l10n: Some(l10n), questions: Some(questions), - network, + network: Some(network), storage, }) } @@ -260,11 +258,10 @@ impl MessageHandler for Service { async fn handle(&mut self, _message: message::GetProposal) -> Result, Error> { let l10n = self.l10n.call(l10n::message::GetProposal).await?; let storage = self.storage.call(storage::message::GetProposal).await?; - let network_config: agama_network::SystemInfo = - self.network.get_extended_config().await?.try_into()?; - let network = Some(NetworkSettings { + let network_config: types::Proposal = self.network.get_extended_config().await?; + let network = types::Proposal { connections: network_config.connections, - }); + }; Ok(Some(Proposal { l10n, diff --git a/rust/agama-manager/src/start.rs b/rust/agama-manager/src/start.rs index c0b03715d2..a848bc7a75 100644 --- a/rust/agama-manager/src/start.rs +++ b/rust/agama-manager/src/start.rs @@ -18,19 +18,13 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::{l10n, service::Service, storage}; -use agama_lib::network::{ - NetworkAdapterError, NetworkClientError, NetworkManagerAdapter, NetworkSystem, - NetworkSystemError, -}; +use crate::{l10n, network, service::Service, storage}; use agama_utils::{ actor::{self, Handler}, api::event, issue, progress, question, }; -use tokio::sync::mpsc; - #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] @@ -40,13 +34,11 @@ pub enum Error { #[error(transparent)] L10n(#[from] l10n::start::Error), #[error(transparent)] + Storage(#[from] storage::start::Error), #[error(transparent)] - NetworkClient(#[from] NetworkClientError), - #[error(transparent)] - NetworkAdapter(#[from] NetworkAdapterError), + NetworkAdapter(#[from] network::NetworkAdapterError), #[error(transparent)] - NetworkSystem(#[from] NetworkSystemError), - Storage(#[from] storage::start::Error), + NetworkSystem(#[from] network::NetworkSystemError), } /// Starts the manager service. @@ -63,10 +55,10 @@ pub async fn start( let progress = progress::start(events.clone()).await?; let l10n = l10n::start(issues.clone(), events.clone()).await?; let storage = storage::start(progress.clone(), issues.clone(), events.clone(), dbus).await?; - let network_adapter = NetworkManagerAdapter::from_system() + let network_adapter = network::NetworkManagerAdapter::from_system() .await .expect("Could not connect to NetworkManager"); - let network = NetworkSystem::new(network_adapter).start().await?; + let network = network::NetworkSystem::new(network_adapter).start().await?; let service = Service::new(l10n, network, storage, issues, progress, questions, events); let handler = actor::spawn(service); diff --git a/rust/agama-network/src/action.rs b/rust/agama-network/src/action.rs index aed5e25183..abdcbc5b2a 100644 --- a/rust/agama-network/src/action.rs +++ b/rust/agama-network/src/action.rs @@ -19,9 +19,7 @@ // find current contact information at www.suse.com. use crate::model::{AccessPoint, Connection, Device}; -use crate::system_info::SystemInfo; -use crate::types::{ConnectionState, DeviceType}; -use crate::NetworkState; +use crate::types::{ConnectionState, DeviceType, Proposal, SystemInfo}; use tokio::sync::oneshot; use uuid::Uuid; @@ -44,7 +42,7 @@ pub enum Action { GetConnection(String, Responder>), /// Gets a connection by its Uuid GetConnectionByUuid(Uuid, Responder>), - GetExtendedConfig(Responder), + GetExtendedConfig(Responder), GetSystemConfig(Responder), /// Gets a connection GetConnections(Responder>), diff --git a/rust/agama-network/src/lib.rs b/rust/agama-network/src/lib.rs index 1d8a527cb8..0cf9b47e59 100644 --- a/rust/agama-network/src/lib.rs +++ b/rust/agama-network/src/lib.rs @@ -27,9 +27,7 @@ pub mod adapter; pub mod error; pub mod model; mod nm; -pub mod settings; mod system; -pub mod system_info; pub mod types; pub use action::Action; @@ -37,4 +35,3 @@ pub use adapter::{Adapter, NetworkAdapterError}; pub use model::NetworkState; pub use nm::NetworkManagerAdapter; pub use system::{NetworkSystem, NetworkSystemClient, NetworkSystemError}; -pub use system_info::SystemInfo; diff --git a/rust/agama-network/src/model.rs b/rust/agama-network/src/model.rs index 7cdce7c904..5f4a13d44f 100644 --- a/rust/agama-network/src/model.rs +++ b/rust/agama-network/src/model.rs @@ -23,11 +23,12 @@ //! * This module contains the types that represent the network concepts. They are supposed to be //! agnostic from the real network service (e.g., NetworkManager). use crate::error::NetworkStateError; -use crate::settings::{ - BondSettings, BridgeSettings, IEEE8021XSettings, NetworkConnection, VlanSettings, - WirelessSettings, +use crate::types::{ + BondMode, BondSettings, BridgeSettings, Config, ConnectionState, DeviceState, DeviceType, + IEEE8021XSettings, NetworkConnection, NetworkConnectionsCollection, NetworkSettings, Proposal, + Status, SystemInfo, VlanSettings, WirelessSettings, SSID, }; -use crate::types::{BondMode, ConnectionState, DeviceState, DeviceType, Status, SSID}; + use agama_utils::openapi::schemas; use cidr::IpInet; use macaddr::MacAddr6; @@ -1756,6 +1757,70 @@ pub struct BondConfig { pub options: BondOptions, } +impl TryFrom for NetworkConnectionsCollection { + type Error = NetworkStateError; + + fn try_from(state: NetworkState) -> Result { + let network_connections = state + .connections + .iter() + .filter(|c| c.controller.is_none()) + .map(|c| { + let mut conn = NetworkConnection::try_from(c.clone()).unwrap(); + if let Some(ref mut bond) = conn.bond { + bond.ports = state.ports_for(c.uuid); + } + if let Some(ref mut bridge) = conn.bridge { + bridge.ports = state.ports_for(c.uuid); + }; + conn + }) + .collect(); + + Ok(NetworkConnectionsCollection(network_connections)) + } +} + +impl TryFrom for NetworkSettings { + type Error = NetworkStateError; + + fn try_from(state: NetworkState) -> Result { + let connections: NetworkConnectionsCollection = state.try_into()?; + + Ok(NetworkSettings { connections }) + } +} + +impl TryFrom for Config { + type Error = NetworkStateError; + + fn try_from(state: NetworkState) -> Result { + Ok(Config { + connections: state.clone().try_into()?, + }) + } +} + +impl TryFrom for SystemInfo { + type Error = NetworkStateError; + + fn try_from(state: NetworkState) -> Result { + Ok(SystemInfo { + connections: state.try_into()?, + }) + } +} + +impl TryFrom for Proposal { + type Error = NetworkStateError; + + fn try_from(state: NetworkState) -> Result { + Ok(Proposal { + connections: state.try_into()?, + }) + } +} + impl TryFrom for BondConfig { type Error = NetworkStateError; diff --git a/rust/agama-network/src/nm/builder.rs b/rust/agama-network/src/nm/builder.rs index 3f79fa659e..41ed1bc037 100644 --- a/rust/agama-network/src/nm/builder.rs +++ b/rust/agama-network/src/nm/builder.rs @@ -20,13 +20,13 @@ //! Conversion mechanism between proxies and model structs. -use crate::types::{DeviceState, DeviceType}; use crate::{ model::{Device, IpConfig, IpRoute, MacAddress}, nm::{ model::NmDeviceType, proxies::{DeviceProxy, IP4ConfigProxy, IP6ConfigProxy}, }, + types::{DeviceState, DeviceType}, }; use cidr::IpInet; use std::{collections::HashMap, net::IpAddr, str::FromStr}; diff --git a/rust/agama-network/src/system.rs b/rust/agama-network/src/system.rs index 9a696bce68..0722ac0300 100644 --- a/rust/agama-network/src/system.rs +++ b/rust/agama-network/src/system.rs @@ -24,8 +24,8 @@ use crate::{ model::{ AccessPoint, Connection, Device, GeneralState, NetworkChange, NetworkState, StateConfig, }, - types::DeviceType, - Adapter, NetworkAdapterError, SystemInfo, + types::{DeviceType, Proposal, SystemInfo}, + Adapter, NetworkAdapterError, }; use std::error::Error; use tokio::sync::{ @@ -163,7 +163,7 @@ impl NetworkSystemClient { self.actions.send(Action::GetConnections(tx))?; Ok(rx.await?) } - pub async fn get_extended_config(&self) -> Result { + pub async fn get_extended_config(&self) -> Result { let (tx, rx) = oneshot::channel(); self.actions.send(Action::GetExtendedConfig(tx))?; Ok(rx.await?) @@ -326,7 +326,8 @@ impl NetworkSystemServer { tx.send(result).unwrap(); } Action::GetExtendedConfig(tx) => { - tx.send(self.state.clone()).unwrap(); + let config: Proposal = self.state.clone().try_into()?; + tx.send(config).unwrap(); } Action::GetConnections(tx) => { let connections = self diff --git a/rust/agama-network/src/system_info.rs b/rust/agama-network/src/system_info.rs deleted file mode 100644 index 7686a628b8..0000000000 --- a/rust/agama-network/src/system_info.rs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) [2025] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -//! Representation of the network settings - -use serde::{Deserialize, Serialize}; -use std::default::Default; - -use crate::{ - error::NetworkStateError, - model::{AccessPoint, Device, GeneralState}, - settings::NetworkConnection, - NetworkState, -}; - -/// Network settings for installation -#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct SystemInfo { - pub access_points: Vec, - /// Connections to use in the installation - pub connections: Vec, - pub devices: Vec, - pub general_state: GeneralState, -} - -impl TryFrom for SystemInfo { - type Error = NetworkStateError; - - fn try_from(state: NetworkState) -> Result { - let connections = &state.connections; - let network_connections = connections - .iter() - .filter(|c| c.controller.is_none()) - .map(|c| { - let mut conn = NetworkConnection::try_from(c.clone()).unwrap(); - if let Some(ref mut bond) = conn.bond { - bond.ports = state.ports_for(c.uuid); - } - if let Some(ref mut bridge) = conn.bridge { - bridge.ports = state.ports_for(c.uuid); - }; - conn - }) - .collect(); - let access_points = state.access_points; - let devices = state.devices; - let general_state = state.general_state; - - Ok(SystemInfo { - access_points, - devices, - connections: network_connections, - general_state, - }) - } -} diff --git a/rust/agama-network/src/types.rs b/rust/agama-network/src/types.rs index f063d63949..e53bbbfbb0 100644 --- a/rust/agama-network/src/types.rs +++ b/rust/agama-network/src/types.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2024] SUSE LLC +// Copyright (c) [2024-2025] SUSE LLC // // All Rights Reserved. // @@ -18,16 +18,10 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use cidr::errors::NetworkParseError; +pub use agama_utils::api::network::*; use serde::{Deserialize, Serialize}; -use std::{ - fmt, - str::{self, FromStr}, -}; +use std::str::{self}; use thiserror::Error; -use zbus; - -use super::settings::NetworkConnection; /// Network device #[derive(Debug, Clone, Serialize, Deserialize)] @@ -38,152 +32,6 @@ pub struct Device { pub state: DeviceState, } -#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct SSID(pub Vec); - -impl SSID { - pub fn to_vec(&self) -> &Vec { - &self.0 - } -} - -impl fmt::Display for SSID { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", str::from_utf8(&self.0).unwrap()) - } -} - -impl FromStr for SSID { - type Err = NetworkParseError; - - fn from_str(s: &str) -> Result { - Ok(SSID(s.as_bytes().into())) - } -} - -impl From for Vec { - fn from(value: SSID) -> Self { - value.0 - } -} - -#[derive(Default, Debug, PartialEq, Copy, Clone, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub enum DeviceType { - Loopback = 0, - #[default] - Ethernet = 1, - Wireless = 2, - Dummy = 3, - Bond = 4, - Vlan = 5, - Bridge = 6, -} - -/// Network device state. -#[derive( - Default, - Serialize, - Deserialize, - Debug, - PartialEq, - Eq, - Clone, - Copy, - strum::Display, - strum::EnumString, - utoipa::ToSchema, -)] -#[strum(serialize_all = "camelCase")] -#[serde(rename_all = "camelCase")] -pub enum DeviceState { - #[default] - /// The device's state is unknown. - Unknown, - /// The device is recognized but not managed by Agama. - Unmanaged, - /// The device is detected but it cannot be used (wireless switched off, missing firmware, etc.). - Unavailable, - /// The device is connecting to the network. - Connecting, - /// The device is successfully connected to the network. - Connected, - /// The device is disconnecting from the network. - Disconnecting, - /// The device is disconnected from the network. - Disconnected, - /// The device failed to connect to a network. - Failed, -} - -#[derive( - Default, - Serialize, - Deserialize, - Debug, - PartialEq, - Eq, - Clone, - Copy, - strum::Display, - strum::EnumString, - utoipa::ToSchema, -)] -#[strum(serialize_all = "camelCase")] -#[serde(rename_all = "camelCase")] -pub enum ConnectionState { - /// The connection is getting activated. - Activating, - /// The connection is activated. - Activated, - /// The connection is getting deactivated. - Deactivating, - #[default] - /// The connection is deactivated. - Deactivated, -} - -#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub enum Status { - #[default] - Up, - Down, - Removed, - // Workaound for not modify the connection status - Keep, -} - -impl fmt::Display for Status { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let name = match &self { - Status::Up => "up", - Status::Down => "down", - Status::Keep => "keep", - Status::Removed => "removed", - }; - write!(f, "{}", name) - } -} - -#[derive(Debug, Error, PartialEq)] -#[error("Invalid status: {0}")] -pub struct InvalidStatus(String); - -impl TryFrom<&str> for Status { - type Error = InvalidStatus; - - fn try_from(value: &str) -> Result { - match value { - "up" => Ok(Status::Up), - "down" => Ok(Status::Down), - "keep" => Ok(Status::Keep), - "removed" => Ok(Status::Removed), - _ => Err(InvalidStatus(value.to_string())), - } - } -} - // https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMSettingsConnectionFlags #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, utoipa::ToSchema)] pub enum ConnectionFlags { @@ -232,159 +80,3 @@ pub enum UpdateFlags { BlockAutoconnect = 0x20, NoReapply = 0x40, } - -/// Bond mode -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, utoipa::ToSchema)] -pub enum BondMode { - #[serde(rename = "balance-rr")] - RoundRobin = 0, - #[serde(rename = "active-backup")] - ActiveBackup = 1, - #[serde(rename = "balance-xor")] - BalanceXOR = 2, - #[serde(rename = "broadcast")] - Broadcast = 3, - #[serde(rename = "802.3ad")] - LACP = 4, - #[serde(rename = "balance-tlb")] - BalanceTLB = 5, - #[serde(rename = "balance-alb")] - BalanceALB = 6, -} -impl Default for BondMode { - fn default() -> Self { - Self::RoundRobin - } -} - -impl std::fmt::Display for BondMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - BondMode::RoundRobin => "balance-rr", - BondMode::ActiveBackup => "active-backup", - BondMode::BalanceXOR => "balance-xor", - BondMode::Broadcast => "broadcast", - BondMode::LACP => "802.3ad", - BondMode::BalanceTLB => "balance-tlb", - BondMode::BalanceALB => "balance-alb", - } - ) - } -} - -#[derive(Debug, Error, PartialEq)] -#[error("Invalid bond mode: {0}")] -pub struct InvalidBondMode(String); - -impl TryFrom<&str> for BondMode { - type Error = InvalidBondMode; - - fn try_from(value: &str) -> Result { - match value { - "balance-rr" => Ok(BondMode::RoundRobin), - "active-backup" => Ok(BondMode::ActiveBackup), - "balance-xor" => Ok(BondMode::BalanceXOR), - "broadcast" => Ok(BondMode::Broadcast), - "802.3ad" => Ok(BondMode::LACP), - "balance-tlb" => Ok(BondMode::BalanceTLB), - "balance-alb" => Ok(BondMode::BalanceALB), - _ => Err(InvalidBondMode(value.to_string())), - } - } -} -impl TryFrom for BondMode { - type Error = InvalidBondMode; - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(BondMode::RoundRobin), - 1 => Ok(BondMode::ActiveBackup), - 2 => Ok(BondMode::BalanceXOR), - 3 => Ok(BondMode::Broadcast), - 4 => Ok(BondMode::LACP), - 5 => Ok(BondMode::BalanceTLB), - 6 => Ok(BondMode::BalanceALB), - _ => Err(InvalidBondMode(value.to_string())), - } - } -} - -impl From for zbus::fdo::Error { - fn from(value: InvalidBondMode) -> zbus::fdo::Error { - zbus::fdo::Error::Failed(format!("Network error: {value}")) - } -} - -#[derive(Debug, Error, PartialEq)] -#[error("Invalid device type: {0}")] -pub struct InvalidDeviceType(u8); - -impl TryFrom for DeviceType { - type Error = InvalidDeviceType; - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(DeviceType::Loopback), - 1 => Ok(DeviceType::Ethernet), - 2 => Ok(DeviceType::Wireless), - 3 => Ok(DeviceType::Dummy), - 4 => Ok(DeviceType::Bond), - 5 => Ok(DeviceType::Vlan), - 6 => Ok(DeviceType::Bridge), - _ => Err(InvalidDeviceType(value)), - } - } -} - -impl From for zbus::fdo::Error { - fn from(value: InvalidDeviceType) -> zbus::fdo::Error { - zbus::fdo::Error::Failed(format!("Network error: {value}")) - } -} - -// FIXME: found a better place for the HTTP types. -// -// TODO: If the client ignores the additional "state" field, this struct -// does not need to be here. -#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct NetworkConnectionWithState { - #[serde(flatten)] - pub connection: NetworkConnection, - pub state: ConnectionState, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_display_ssid() { - let ssid = SSID(vec![97, 103, 97, 109, 97]); - assert_eq!(format!("{}", ssid), "agama"); - } - - #[test] - fn test_ssid_to_vec() { - let vec = vec![97, 103, 97, 109, 97]; - let ssid = SSID(vec.clone()); - assert_eq!(ssid.to_vec(), &vec); - } - - #[test] - fn test_device_type_from_u8() { - let dtype = DeviceType::try_from(0); - assert_eq!(dtype, Ok(DeviceType::Loopback)); - - let dtype = DeviceType::try_from(128); - assert_eq!(dtype, Err(InvalidDeviceType(128))); - } - - #[test] - fn test_display_bond_mode() { - let mode = BondMode::try_from(1).unwrap(); - assert_eq!(format!("{}", mode), "active-backup"); - } -} diff --git a/rust/agama-server/src/web/docs/config.rs b/rust/agama-server/src/web/docs/config.rs index ef0c136a18..bcc7737611 100644 --- a/rust/agama-server/src/web/docs/config.rs +++ b/rust/agama-server/src/web/docs/config.rs @@ -99,16 +99,16 @@ impl ApiDocBuilder for ConfigApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() diff --git a/rust/agama-utils/Cargo.toml b/rust/agama-utils/Cargo.toml index 15f4b79fb0..5a39c87ebb 100644 --- a/rust/agama-utils/Cargo.toml +++ b/rust/agama-utils/Cargo.toml @@ -19,6 +19,7 @@ zbus = "5.7.1" zvariant = "5.5.2" gettext-rs = { version = "0.7.2", features = ["gettext-system"] } uuid = { version = "1.10.0", features = ["v4"] } +cidr = { version = "0.3.1", features = ["serde"] } [dev-dependencies] tokio-test = "0.4.4" diff --git a/rust/agama-utils/src/api.rs b/rust/agama-utils/src/api.rs index 89ccd79dcc..9348189faf 100644 --- a/rust/agama-utils/src/api.rs +++ b/rust/agama-utils/src/api.rs @@ -52,5 +52,6 @@ mod action; pub use action::Action; pub mod l10n; +pub mod network; pub mod question; pub mod storage; diff --git a/rust/agama-utils/src/api/config.rs b/rust/agama-utils/src/api/config.rs index c648114f46..31608dc14b 100644 --- a/rust/agama-utils/src/api/config.rs +++ b/rust/agama-utils/src/api/config.rs @@ -18,7 +18,7 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::api::{l10n, question, storage}; +use crate::api::{l10n, network, question, storage}; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Default, Deserialize, Serialize, utoipa::ToSchema)] @@ -28,6 +28,8 @@ pub struct Config { #[serde(alias = "localization")] pub l10n: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub network: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub questions: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(flatten)] diff --git a/rust/agama-utils/src/api/network.rs b/rust/agama-utils/src/api/network.rs new file mode 100644 index 0000000000..75eb23f9a4 --- /dev/null +++ b/rust/agama-utils/src/api/network.rs @@ -0,0 +1,34 @@ +// Copyright (c) [2025] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + +//! This module contains all Agama public types that might be available over +//! the HTTP and WebSocket API. + +mod config; +pub use config::Config; +mod proposal; +pub use proposal::Proposal; +mod settings; +mod system_info; +pub use system_info::SystemInfo; + +mod types; +pub use settings::*; +pub use types::*; diff --git a/rust/agama-utils/src/api/network/config.rs b/rust/agama-utils/src/api/network/config.rs new file mode 100644 index 0000000000..80726790f9 --- /dev/null +++ b/rust/agama-utils/src/api/network/config.rs @@ -0,0 +1,33 @@ +// Copyright (c) [2024] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + +//! Representation of the network settings + +use crate::api::network::NetworkConnectionsCollection; +use serde::{Deserialize, Serialize}; +use std::default::Default; + +/// Network config settings for installation +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct Config { + /// Connections to use in the installation + pub connections: NetworkConnectionsCollection, +} diff --git a/rust/agama-utils/src/api/network/proposal.rs b/rust/agama-utils/src/api/network/proposal.rs new file mode 100644 index 0000000000..ec94a78bf7 --- /dev/null +++ b/rust/agama-utils/src/api/network/proposal.rs @@ -0,0 +1,33 @@ +// Copyright (c) [2025] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + +//! Representation of the network settings + +use crate::api::network::NetworkConnectionsCollection; +use serde::{Deserialize, Serialize}; +use std::default::Default; + +/// Network proposal settings for installation +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct Proposal { + /// Connections to use in the installation + pub connections: NetworkConnectionsCollection, +} diff --git a/rust/agama-network/src/settings.rs b/rust/agama-utils/src/api/network/settings.rs similarity index 91% rename from rust/agama-network/src/settings.rs rename to rust/agama-utils/src/api/network/settings.rs index 6393eccf77..4ee8625944 100644 --- a/rust/agama-network/src/settings.rs +++ b/rust/agama-utils/src/api/network/settings.rs @@ -20,48 +20,24 @@ //! Representation of the network settings -use super::types::{DeviceState, DeviceType, Status}; -use crate::{error::NetworkStateError, NetworkState}; -use agama_utils::openapi::schemas; +use super::types::{ConnectionState, DeviceState, DeviceType, Status}; +use crate::openapi::schemas; use cidr::IpInet; use serde::{Deserialize, Serialize}; use std::default::Default; use std::net::IpAddr; +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +pub struct NetworkConnectionsCollection(pub Vec); + /// Network settings for installation #[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct NetworkSettings { /// Connections to use in the installation - pub connections: Vec, + pub connections: NetworkConnectionsCollection, } -impl TryFrom for NetworkSettings { - type Error = NetworkStateError; - - fn try_from(state: NetworkState) -> Result { - let connections = &state.connections; - - let network_connections = connections - .iter() - .filter(|c| c.controller.is_none()) - .map(|c| { - let mut conn = NetworkConnection::try_from(c.clone()).unwrap(); - if let Some(ref mut bond) = conn.bond { - bond.ports = state.ports_for(c.uuid); - } - if let Some(ref mut bridge) = conn.bridge { - bridge.ports = state.ports_for(c.uuid); - }; - conn - }) - .collect(); - - Ok(NetworkSettings { - connections: network_connections, - }) - } -} #[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] pub struct MatchSettings { #[serde(skip_serializing_if = "Vec::is_empty", default)] @@ -329,3 +305,14 @@ impl NetworkConnection { } } } + +// FIXME: found a better place for the HTTP types. +// +// TODO: If the client ignores the additional "state" field, this struct +// does not need to be here. +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct NetworkConnectionWithState { + #[serde(flatten)] + pub connection: NetworkConnection, + pub state: ConnectionState, +} diff --git a/rust/agama-utils/src/api/network/system_info.rs b/rust/agama-utils/src/api/network/system_info.rs new file mode 100644 index 0000000000..89cb5d051b --- /dev/null +++ b/rust/agama-utils/src/api/network/system_info.rs @@ -0,0 +1,33 @@ +// Copyright (c) [2025] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + +//! Representation of the network settings + +use crate::api::network::NetworkConnectionsCollection; +use serde::{Deserialize, Serialize}; +use std::default::Default; + +/// Network settings for installation +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct SystemInfo { + /// Connections to use in the installation + pub connections: NetworkConnectionsCollection, +} diff --git a/rust/agama-utils/src/api/network/types.rs b/rust/agama-utils/src/api/network/types.rs new file mode 100644 index 0000000000..5b6e24a6df --- /dev/null +++ b/rust/agama-utils/src/api/network/types.rs @@ -0,0 +1,326 @@ +// Copyright (c) [2024] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + +use cidr::errors::NetworkParseError; +use serde::{Deserialize, Serialize}; +use std::{ + fmt, + str::{self, FromStr}, +}; +use thiserror::Error; + +/// Network device +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub struct Device { + pub name: String, + pub type_: DeviceType, + pub state: DeviceState, +} + +#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct SSID(pub Vec); + +impl SSID { + pub fn to_vec(&self) -> &Vec { + &self.0 + } +} + +impl fmt::Display for SSID { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", str::from_utf8(&self.0).unwrap()) + } +} + +impl FromStr for SSID { + type Err = NetworkParseError; + + fn from_str(s: &str) -> Result { + Ok(SSID(s.as_bytes().into())) + } +} + +impl From for Vec { + fn from(value: SSID) -> Self { + value.0 + } +} + +#[derive(Default, Debug, PartialEq, Copy, Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub enum DeviceType { + Loopback = 0, + #[default] + Ethernet = 1, + Wireless = 2, + Dummy = 3, + Bond = 4, + Vlan = 5, + Bridge = 6, +} + +/// Network device state. +#[derive( + Default, + Serialize, + Deserialize, + Debug, + PartialEq, + Eq, + Clone, + Copy, + strum::Display, + strum::EnumString, + utoipa::ToSchema, +)] +#[strum(serialize_all = "camelCase")] +#[serde(rename_all = "camelCase")] +pub enum DeviceState { + #[default] + /// The device's state is unknown. + Unknown, + /// The device is recognized but not managed by Agama. + Unmanaged, + /// The device is detected but it cannot be used (wireless switched off, missing firmware, etc.). + Unavailable, + /// The device is connecting to the network. + Connecting, + /// The device is successfully connected to the network. + Connected, + /// The device is disconnecting from the network. + Disconnecting, + /// The device is disconnected from the network. + Disconnected, + /// The device failed to connect to a network. + Failed, +} + +#[derive( + Default, + Serialize, + Deserialize, + Debug, + PartialEq, + Eq, + Clone, + Copy, + strum::Display, + strum::EnumString, + utoipa::ToSchema, +)] +#[strum(serialize_all = "camelCase")] +#[serde(rename_all = "camelCase")] +pub enum ConnectionState { + /// The connection is getting activated. + Activating, + /// The connection is activated. + Activated, + /// The connection is getting deactivated. + Deactivating, + #[default] + /// The connection is deactivated. + Deactivated, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub enum Status { + #[default] + Up, + Down, + Removed, + // Workaound for not modify the connection status + Keep, +} + +impl fmt::Display for Status { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match &self { + Status::Up => "up", + Status::Down => "down", + Status::Keep => "keep", + Status::Removed => "removed", + }; + write!(f, "{}", name) + } +} + +#[derive(Debug, Error, PartialEq)] +#[error("Invalid status: {0}")] +pub struct InvalidStatus(String); + +impl TryFrom<&str> for Status { + type Error = InvalidStatus; + + fn try_from(value: &str) -> Result { + match value { + "up" => Ok(Status::Up), + "down" => Ok(Status::Down), + "keep" => Ok(Status::Keep), + "removed" => Ok(Status::Removed), + _ => Err(InvalidStatus(value.to_string())), + } + } +} + +/// Bond mode +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, utoipa::ToSchema)] +pub enum BondMode { + #[serde(rename = "balance-rr")] + RoundRobin = 0, + #[serde(rename = "active-backup")] + ActiveBackup = 1, + #[serde(rename = "balance-xor")] + BalanceXOR = 2, + #[serde(rename = "broadcast")] + Broadcast = 3, + #[serde(rename = "802.3ad")] + LACP = 4, + #[serde(rename = "balance-tlb")] + BalanceTLB = 5, + #[serde(rename = "balance-alb")] + BalanceALB = 6, +} +impl Default for BondMode { + fn default() -> Self { + Self::RoundRobin + } +} + +impl std::fmt::Display for BondMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + BondMode::RoundRobin => "balance-rr", + BondMode::ActiveBackup => "active-backup", + BondMode::BalanceXOR => "balance-xor", + BondMode::Broadcast => "broadcast", + BondMode::LACP => "802.3ad", + BondMode::BalanceTLB => "balance-tlb", + BondMode::BalanceALB => "balance-alb", + } + ) + } +} + +#[derive(Debug, Error, PartialEq)] +#[error("Invalid bond mode: {0}")] +pub struct InvalidBondMode(String); + +impl TryFrom<&str> for BondMode { + type Error = InvalidBondMode; + + fn try_from(value: &str) -> Result { + match value { + "balance-rr" => Ok(BondMode::RoundRobin), + "active-backup" => Ok(BondMode::ActiveBackup), + "balance-xor" => Ok(BondMode::BalanceXOR), + "broadcast" => Ok(BondMode::Broadcast), + "802.3ad" => Ok(BondMode::LACP), + "balance-tlb" => Ok(BondMode::BalanceTLB), + "balance-alb" => Ok(BondMode::BalanceALB), + _ => Err(InvalidBondMode(value.to_string())), + } + } +} +impl TryFrom for BondMode { + type Error = InvalidBondMode; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(BondMode::RoundRobin), + 1 => Ok(BondMode::ActiveBackup), + 2 => Ok(BondMode::BalanceXOR), + 3 => Ok(BondMode::Broadcast), + 4 => Ok(BondMode::LACP), + 5 => Ok(BondMode::BalanceTLB), + 6 => Ok(BondMode::BalanceALB), + _ => Err(InvalidBondMode(value.to_string())), + } + } +} + +#[derive(Debug, Error, PartialEq)] +#[error("Invalid device type: {0}")] +pub struct InvalidDeviceType(u8); + +impl TryFrom for DeviceType { + type Error = InvalidDeviceType; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(DeviceType::Loopback), + 1 => Ok(DeviceType::Ethernet), + 2 => Ok(DeviceType::Wireless), + 3 => Ok(DeviceType::Dummy), + 4 => Ok(DeviceType::Bond), + 5 => Ok(DeviceType::Vlan), + 6 => Ok(DeviceType::Bridge), + _ => Err(InvalidDeviceType(value)), + } + } +} + +impl From for zbus::fdo::Error { + fn from(value: InvalidBondMode) -> zbus::fdo::Error { + zbus::fdo::Error::Failed(format!("Network error: {value}")) + } +} + +impl From for zbus::fdo::Error { + fn from(value: InvalidDeviceType) -> zbus::fdo::Error { + zbus::fdo::Error::Failed(format!("Network error: {value}")) + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display_ssid() { + let ssid = SSID(vec![97, 103, 97, 109, 97]); + assert_eq!(format!("{}", ssid), "agama"); + } + + #[test] + fn test_ssid_to_vec() { + let vec = vec![97, 103, 97, 109, 97]; + let ssid = SSID(vec.clone()); + assert_eq!(ssid.to_vec(), &vec); + } + + #[test] + fn test_device_type_from_u8() { + let dtype = DeviceType::try_from(0); + assert_eq!(dtype, Ok(DeviceType::Loopback)); + + let dtype = DeviceType::try_from(128); + assert_eq!(dtype, Err(InvalidDeviceType(128))); + } + + #[test] + fn test_display_bond_mode() { + let mode = BondMode::try_from(1).unwrap(); + assert_eq!(format!("{}", mode), "active-backup"); + } +} diff --git a/rust/agama-utils/src/api/proposal.rs b/rust/agama-utils/src/api/proposal.rs index 185b606073..7a66f5d050 100644 --- a/rust/agama-utils/src/api/proposal.rs +++ b/rust/agama-utils/src/api/proposal.rs @@ -18,7 +18,7 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::api::l10n; +use crate::api::{l10n, network}; use serde::Serialize; use serde_json::Value; @@ -27,7 +27,7 @@ use serde_json::Value; pub struct Proposal { #[serde(skip_serializing_if = "Option::is_none")] pub l10n: Option, - pub network: Option, + pub network: network::Proposal, #[serde(skip_serializing_if = "Option::is_none")] pub storage: Option, } diff --git a/rust/agama-utils/src/api/system_info.rs b/rust/agama-utils/src/api/system_info.rs index 82c9f5e5fd..7bc787077e 100644 --- a/rust/agama-utils/src/api/system_info.rs +++ b/rust/agama-utils/src/api/system_info.rs @@ -19,7 +19,7 @@ // find current contact information at www.suse.com. use crate::api::l10n; -use crate::network; +use crate::api::network; use serde::Serialize; use serde_json::Value; From c3f453299b71d8c753ff7f0bfbedc9484a0379f9 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Wed, 29 Oct 2025 15:35:14 +0000 Subject: [PATCH 03/17] Remove NetworkStateError dependencies --- rust/agama-manager/src/service.rs | 6 ++---- rust/agama-manager/src/start.rs | 2 -- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index dee73fd20c..df8ca8a288 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -29,7 +29,7 @@ use agama_utils::{ }; use async_trait::async_trait; use merge_struct::merge; -use network::{error::NetworkStateError, types, NetworkSystemClient, NetworkSystemError}; +use network::{types, NetworkSystemClient, NetworkSystemError}; use serde_json::Value; use tokio::sync::broadcast; @@ -53,8 +53,6 @@ pub enum Error { Progress(#[from] progress::service::Error), #[error(transparent)] NetworkSystemError(#[from] NetworkSystemError), - #[error(transparent)] - NetworkStateError(#[from] NetworkStateError), } pub struct Service { @@ -180,7 +178,7 @@ impl MessageHandler for Service { Ok(Config { l10n: Some(l10n), - questions: Some(questions), + questions: questions, network: Some(network), storage, }) diff --git a/rust/agama-manager/src/start.rs b/rust/agama-manager/src/start.rs index a848bc7a75..8a3f034e7f 100644 --- a/rust/agama-manager/src/start.rs +++ b/rust/agama-manager/src/start.rs @@ -36,8 +36,6 @@ pub enum Error { #[error(transparent)] Storage(#[from] storage::start::Error), #[error(transparent)] - NetworkAdapter(#[from] network::NetworkAdapterError), - #[error(transparent)] NetworkSystem(#[from] network::NetworkSystemError), } From 7cb2a9de9901787635c4efac3cdbd1de031201d9 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Fri, 31 Oct 2025 08:51:23 +0000 Subject: [PATCH 04/17] Moved network types to agama utils and adapt for new API changes --- rust/Cargo.lock | 1 + rust/agama-lib/src/network/store.rs | 5 +- rust/agama-manager/src/service.rs | 14 +- rust/agama-network/src/action.rs | 12 +- rust/agama-network/src/error.rs | 10 + rust/agama-network/src/model.rs | 601 +++++------------- rust/agama-network/src/nm/builder.rs | 3 +- rust/agama-network/src/nm/client.rs | 7 +- rust/agama-network/src/nm/dbus.rs | 2 +- rust/agama-network/src/nm/error.rs | 2 +- rust/agama-network/src/nm/model.rs | 4 +- rust/agama-network/src/nm/watcher.rs | 5 +- rust/agama-network/src/system.rs | 19 +- rust/agama-network/src/types.rs | 9 - rust/agama-server/src/web/docs/config.rs | 24 +- rust/agama-utils/Cargo.toml | 1 + rust/agama-utils/src/api/network/config.rs | 6 +- rust/agama-utils/src/api/network/proposal.rs | 3 +- rust/agama-utils/src/api/network/settings.rs | 3 +- .../src/api/network/system_info.rs | 5 +- rust/agama-utils/src/api/network/types.rs | 415 +++++++++++- service/lib/agama/http/clients/base.rb | 2 +- service/lib/agama/http/clients/network.rb | 10 +- service/lib/agama/network.rb | 1 + web/src/api/network.ts | 33 +- web/src/components/network/IpSettingsForm.tsx | 13 +- web/src/components/network/NetworkPage.tsx | 7 +- .../network/NoPersistentConnectionsAlert.tsx | 5 +- .../network/WiredConnectionDetails.tsx | 2 +- .../network/WiredConnectionPage.tsx | 5 +- .../network/WiredConnectionsList.tsx | 7 +- web/src/queries/network.ts | 21 +- web/src/queries/proposal.ts | 12 +- web/src/queries/system.ts | 16 +- web/src/types/network.ts | 72 +++ web/src/types/proposal.ts | 2 + web/src/types/system.ts | 2 + 37 files changed, 841 insertions(+), 520 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 606aaa6390..e1aece7139 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -242,6 +242,7 @@ dependencies = [ "async-trait", "cidr", "gettext-rs", + "macaddr", "serde", "serde_json", "serde_with", diff --git a/rust/agama-lib/src/network/store.rs b/rust/agama-lib/src/network/store.rs index dcc2fecfe8..9527d212fd 100644 --- a/rust/agama-lib/src/network/store.rs +++ b/rust/agama-lib/src/network/store.rs @@ -48,7 +48,10 @@ impl NetworkStore { pub async fn load(&self) -> NetworkStoreResult { let connections = NetworkConnectionsCollection(self.network_client.connections().await?); - Ok(NetworkSettings { connections }) + Ok(NetworkSettings { + connections, + ..Default::default() + }) } pub async fn store(&self, settings: &NetworkSettings) -> NetworkStoreResult<()> { diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index df8ca8a288..cf23ea4670 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -29,8 +29,8 @@ use agama_utils::{ }; use async_trait::async_trait; use merge_struct::merge; -use network::{types, NetworkSystemClient, NetworkSystemError}; use serde_json::Value; +use network::{NetworkSystemClient, NetworkSystemError}; use tokio::sync::broadcast; #[derive(Debug, thiserror::Error)] @@ -172,7 +172,8 @@ impl MessageHandler for Service { let questions = self.questions.call(question::message::GetConfig).await?; let network_config: network::types::Proposal = self.network.get_extended_config().await?; let network = agama_network::types::Config { - connections: network_config.connections, + connections: Some(network_config.connections), + general_state: Some(network_config.general_state), }; let storage = self.storage.call(storage::message::GetConfig).await?; @@ -213,6 +214,11 @@ impl MessageHandler for Service { .call(storage::message::SetConfig::new(config.storage.clone())) .await?; + if let Some(network) = config.network.clone() { + self.network.update_config(network).await?; + self.network.apply().await?; + } + self.config = config; Ok(()) } @@ -255,11 +261,15 @@ impl MessageHandler for Service { /// It returns the current proposal, if any. async fn handle(&mut self, _message: message::GetProposal) -> Result, Error> { let l10n = self.l10n.call(l10n::message::GetProposal).await?; +<<<<<<< HEAD let storage = self.storage.call(storage::message::GetProposal).await?; let network_config: types::Proposal = self.network.get_extended_config().await?; let network = types::Proposal { connections: network_config.connections, }; +======= + let network = self.network.get_extended_config().await?; +>>>>>>> c91270e0d (Moved network types to agama utils and adapt for new API changes) Ok(Some(Proposal { l10n, diff --git a/rust/agama-network/src/action.rs b/rust/agama-network/src/action.rs index abdcbc5b2a..ffc2fd3cc0 100644 --- a/rust/agama-network/src/action.rs +++ b/rust/agama-network/src/action.rs @@ -18,12 +18,13 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::model::{AccessPoint, Connection, Device}; -use crate::types::{ConnectionState, DeviceType, Proposal, SystemInfo}; +use crate::model::{AccessPoint, Connection}; +use crate::types::{ConnectionState, Device, DeviceType, GeneralState, Proposal, SystemInfo}; +use agama_utils::api::network::Config; use tokio::sync::oneshot; use uuid::Uuid; -use super::{error::NetworkStateError, model::GeneralState, NetworkAdapterError}; +use super::{error::NetworkStateError, NetworkAdapterError}; pub type Responder = oneshot::Sender; pub type ControllerConnection = (Connection, Vec); @@ -42,7 +43,12 @@ pub enum Action { GetConnection(String, Responder>), /// Gets a connection by its Uuid GetConnectionByUuid(Uuid, Responder>), + /// Gets the internal state of the network configuration GetExtendedConfig(Responder), + /// Updates th internal state of the network configuration + UpdateConfig(Box, Responder>), + /// Gets the current network configuration containing connections, devices, access_points and + /// also the general state GetSystemConfig(Responder), /// Gets a connection GetConnections(Responder>), diff --git a/rust/agama-network/src/error.rs b/rust/agama-network/src/error.rs index 87a3498554..291f40317f 100644 --- a/rust/agama-network/src/error.rs +++ b/rust/agama-network/src/error.rs @@ -21,6 +21,16 @@ //! Error types. use thiserror::Error; +use crate::NetworkSystemError; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + NetworkStateError(#[from] NetworkStateError), + #[error(transparent)] + NetworkSystemError(#[from] NetworkSystemError), +} + /// Errors that are related to the network configuration. #[derive(Error, Debug)] pub enum NetworkStateError { diff --git a/rust/agama-network/src/model.rs b/rust/agama-network/src/model.rs index 5f4a13d44f..4f98ae8af2 100644 --- a/rust/agama-network/src/model.rs +++ b/rust/agama-network/src/model.rs @@ -23,14 +23,9 @@ //! * This module contains the types that represent the network concepts. They are supposed to be //! agnostic from the real network service (e.g., NetworkManager). use crate::error::NetworkStateError; -use crate::types::{ - BondMode, BondSettings, BridgeSettings, Config, ConnectionState, DeviceState, DeviceType, - IEEE8021XSettings, NetworkConnection, NetworkConnectionsCollection, NetworkSettings, Proposal, - Status, SystemInfo, VlanSettings, WirelessSettings, SSID, -}; +use crate::types::*; use agama_utils::openapi::schemas; -use cidr::IpInet; use macaddr::MacAddr6; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, skip_serializing_none, DisplayFromStr}; @@ -38,12 +33,10 @@ use std::{ collections::HashMap, default::Default, fmt, - net::IpAddr, str::{self, FromStr}, }; use thiserror::Error; use uuid::Uuid; -use zbus::zvariant::Value; #[derive(PartialEq)] pub struct StateConfig { @@ -72,6 +65,95 @@ pub struct NetworkState { pub connections: Vec, } +impl TryFrom for NetworkState { + type Error = NetworkStateError; + + fn try_from(settings: NetworkSettings) -> Result { + let mut connections: Vec = Vec::with_capacity(settings.connections.0.len()); + + for conn in settings.connections.0 { + let connection = Connection::try_from(conn.clone())?; + connections.push(connection); + + if let Some(bond) = &conn.bond { + dbg!("Bond with: ", &bond.ports); + } + if let Some(bridge) = &conn.bridge { + dbg!("Bridge with: ", &bridge.ports); + } + } + + Ok(NetworkState { + connections: connections, + ..Default::default() + }) + } +} + +/// Returns the list of connections in the order they should be written to the D-Bus service. +/// +/// * `conns`: connections to write. +fn ordered_connections(conns: &Vec) -> Vec { + let mut ordered: Vec = Vec::with_capacity(conns.len()); + for conn in conns { + add_ordered_connection(conn, conns, &mut ordered); + } + + ordered +} + +/// Adds a connections and its dependencies to the list. +/// +/// * `conn`: connection to add. +/// * `conns`: existing connections. +/// * `ordered`: ordered list of connections. +fn add_ordered_connection( + conn: &NetworkConnection, + conns: &Vec, + ordered: &mut Vec, +) { + if let Some(bond) = &conn.bond { + for port in &bond.ports { + if let Some(conn) = find_connection(port, conns) { + add_ordered_connection(conn, conns, ordered); + } else if !ordered.contains(&conn.id) { + ordered.push(port.clone()); + } + } + } + + if let Some(bridge) = &conn.bridge { + for port in &bridge.ports { + if let Some(conn) = find_connection(port, conns) { + add_ordered_connection(conn, conns, ordered); + } else if !ordered.contains(&conn.id) { + ordered.push(port.clone()); + } + } + } + + if !ordered.contains(&conn.id) { + ordered.push(conn.id.to_owned()) + } +} + +/// Finds a connection by id in the list. +/// +/// * `id`: connection ID. +fn find_connection<'a>(id: &str, conns: &'a [NetworkConnection]) -> Option<&'a NetworkConnection> { + conns + .iter() + .find(|c| c.id == id || c.interface == Some(id.to_string())) +} + +fn default_connection(id: &str) -> NetworkConnection { + NetworkConnection { + id: id.to_string(), + interface: Some(id.to_string()), + ..Default::default() + } +} + impl NetworkState { /// Returns a NetworkState struct with the given devices and connections. /// @@ -165,6 +247,17 @@ impl NetworkState { Ok(()) } + pub fn update_state(&mut self, config: Config) -> Result<(), NetworkStateError> { + if let Some(connections) = config.connections { + let collection: ConnectionCollection = connections.try_into()?; + self.connections = collection.0; + } + if let Some(general_state) = config.general_state { + self.general_state = general_state; + } + Ok(()) + } + /// Updates a connection with a new one. /// /// It uses the `id` to decide which connection to update. @@ -475,18 +568,6 @@ mod tests { pub const NOT_COPY_NETWORK_PATH: &str = "/run/agama/not_copy_network"; -/// Network state -#[serde_as] -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct GeneralState { - pub hostname: String, - pub connectivity: bool, - pub copy_network: bool, - pub wireless_enabled: bool, - pub networking_enabled: bool, // pub network_state: NMSTATE -} - /// Access Point #[serde_as] #[derive(Default, Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] @@ -501,23 +582,6 @@ pub struct AccessPoint { pub wpa_flags: u32, } -/// Network device -#[serde_as] -#[skip_serializing_none] -#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct Device { - pub name: String, - #[serde(rename = "type")] - pub type_: DeviceType, - #[serde_as(as = "DisplayFromStr")] - pub mac_address: MacAddress, - pub ip_config: Option, - // Connection.id - pub connection: Option, - pub state: DeviceState, -} - /// Represents a known network connection. #[serde_as] #[skip_serializing_none] @@ -821,274 +885,6 @@ impl From for ConnectionConfig { } } -#[derive(Debug, Error)] -#[error("Invalid MAC address: {0}")] -pub struct InvalidMacAddress(String); - -#[derive(Debug, Default, Clone, PartialEq, Serialize, utoipa::ToSchema)] -pub enum MacAddress { - #[schema(value_type = String, format = "MAC address in EUI-48 format")] - MacAddress(macaddr::MacAddr6), - Preserve, - Permanent, - Random, - Stable, - #[default] - Unset, -} - -impl FromStr for MacAddress { - type Err = InvalidMacAddress; - - fn from_str(s: &str) -> Result { - match s { - "preserve" => Ok(Self::Preserve), - "permanent" => Ok(Self::Permanent), - "random" => Ok(Self::Random), - "stable" => Ok(Self::Stable), - "" => Ok(Self::Unset), - _ => Ok(Self::MacAddress(match macaddr::MacAddr6::from_str(s) { - Ok(mac) => mac, - Err(e) => return Err(InvalidMacAddress(e.to_string())), - })), - } - } -} - -impl TryFrom<&Option> for MacAddress { - type Error = InvalidMacAddress; - - fn try_from(value: &Option) -> Result { - match &value { - Some(str) => MacAddress::from_str(str), - None => Ok(Self::Unset), - } - } -} - -impl fmt::Display for MacAddress { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let output = match &self { - Self::MacAddress(mac) => mac.to_string(), - Self::Preserve => "preserve".to_string(), - Self::Permanent => "permanent".to_string(), - Self::Random => "random".to_string(), - Self::Stable => "stable".to_string(), - Self::Unset => "".to_string(), - }; - write!(f, "{}", output) - } -} - -impl From for zbus::fdo::Error { - fn from(value: InvalidMacAddress) -> Self { - zbus::fdo::Error::Failed(value.to_string()) - } -} - -#[skip_serializing_none] -#[derive(Default, Debug, PartialEq, Clone, Deserialize, Serialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct IpConfig { - pub method4: Ipv4Method, - pub method6: Ipv6Method, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[schema(schema_with = schemas::ip_inet_array)] - pub addresses: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[schema(schema_with = schemas::ip_addr_array)] - pub nameservers: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub dns_searchlist: Vec, - pub ignore_auto_dns: bool, - #[schema(schema_with = schemas::ip_addr)] - pub gateway4: Option, - #[schema(schema_with = schemas::ip_addr)] - pub gateway6: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub routes4: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub routes6: Vec, - pub dhcp4_settings: Option, - pub dhcp6_settings: Option, - pub ip6_privacy: Option, - pub dns_priority4: Option, - pub dns_priority6: Option, -} - -#[skip_serializing_none] -#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, utoipa::ToSchema)] -pub struct Dhcp4Settings { - pub send_hostname: Option, - pub hostname: Option, - pub send_release: Option, - pub client_id: DhcpClientId, - pub iaid: DhcpIaid, -} - -#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)] -pub enum DhcpClientId { - Id(String), - Mac, - PermMac, - Ipv6Duid, - Duid, - Stable, - None, - #[default] - Unset, -} - -impl From<&str> for DhcpClientId { - fn from(s: &str) -> Self { - match s { - "mac" => Self::Mac, - "perm-mac" => Self::PermMac, - "ipv6-duid" => Self::Ipv6Duid, - "duid" => Self::Duid, - "stable" => Self::Stable, - "none" => Self::None, - "" => Self::Unset, - _ => Self::Id(s.to_string()), - } - } -} - -impl From> for DhcpClientId { - fn from(value: Option) -> Self { - match &value { - Some(str) => Self::from(str.as_str()), - None => Self::Unset, - } - } -} - -impl fmt::Display for DhcpClientId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let output = match &self { - Self::Id(id) => id.to_string(), - Self::Mac => "mac".to_string(), - Self::PermMac => "perm-mac".to_string(), - Self::Ipv6Duid => "ipv6-duid".to_string(), - Self::Duid => "duid".to_string(), - Self::Stable => "stable".to_string(), - Self::None => "none".to_string(), - Self::Unset => "".to_string(), - }; - write!(f, "{}", output) - } -} - -#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)] -pub enum DhcpIaid { - Id(String), - Mac, - PermMac, - Ifname, - Stable, - #[default] - Unset, -} - -impl From<&str> for DhcpIaid { - fn from(s: &str) -> Self { - match s { - "mac" => Self::Mac, - "perm-mac" => Self::PermMac, - "ifname" => Self::Ifname, - "stable" => Self::Stable, - "" => Self::Unset, - _ => Self::Id(s.to_string()), - } - } -} - -impl From> for DhcpIaid { - fn from(value: Option) -> Self { - match value { - Some(str) => Self::from(str.as_str()), - None => Self::Unset, - } - } -} - -impl fmt::Display for DhcpIaid { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let output = match &self { - Self::Id(id) => id.to_string(), - Self::Mac => "mac".to_string(), - Self::PermMac => "perm-mac".to_string(), - Self::Ifname => "ifname".to_string(), - Self::Stable => "stable".to_string(), - Self::Unset => "".to_string(), - }; - write!(f, "{}", output) - } -} - -#[skip_serializing_none] -#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, utoipa::ToSchema)] -pub struct Dhcp6Settings { - pub send_hostname: Option, - pub hostname: Option, - pub send_release: Option, - pub duid: DhcpDuid, - pub iaid: DhcpIaid, -} - -#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)] -pub enum DhcpDuid { - Id(String), - Lease, - Llt, - Ll, - StableLlt, - StableLl, - StableUuid, - #[default] - Unset, -} - -impl From<&str> for DhcpDuid { - fn from(s: &str) -> Self { - match s { - "lease" => Self::Lease, - "llt" => Self::Llt, - "ll" => Self::Ll, - "stable-llt" => Self::StableLlt, - "stable-ll" => Self::StableLl, - "stable-uuid" => Self::StableUuid, - "" => Self::Unset, - _ => Self::Id(s.to_string()), - } - } -} - -impl From> for DhcpDuid { - fn from(value: Option) -> Self { - match &value { - Some(str) => Self::from(str.as_str()), - None => Self::Unset, - } - } -} - -impl fmt::Display for DhcpDuid { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let output = match &self { - Self::Id(id) => id.to_string(), - Self::Lease => "lease".to_string(), - Self::Llt => "llt".to_string(), - Self::Ll => "ll".to_string(), - Self::StableLlt => "stable-llt".to_string(), - Self::StableLl => "stable-ll".to_string(), - Self::StableUuid => "stable-uuid".to_string(), - Self::Unset => "".to_string(), - }; - write!(f, "{}", output) - } -} - #[skip_serializing_none] #[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub struct MatchConfig { @@ -1102,125 +898,6 @@ pub struct MatchConfig { pub kernel: Vec, } -#[derive(Debug, Error)] -#[error("Unknown IP configuration method name: {0}")] -pub struct UnknownIpMethod(String); - -#[derive(Debug, Default, Copy, Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub enum Ipv4Method { - Disabled = 0, - #[default] - Auto = 1, - Manual = 2, - LinkLocal = 3, -} - -impl fmt::Display for Ipv4Method { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let name = match &self { - Ipv4Method::Disabled => "disabled", - Ipv4Method::Auto => "auto", - Ipv4Method::Manual => "manual", - Ipv4Method::LinkLocal => "link-local", - }; - write!(f, "{}", name) - } -} - -impl FromStr for Ipv4Method { - type Err = UnknownIpMethod; - - fn from_str(s: &str) -> Result { - match s { - "disabled" => Ok(Ipv4Method::Disabled), - "auto" => Ok(Ipv4Method::Auto), - "manual" => Ok(Ipv4Method::Manual), - "link-local" => Ok(Ipv4Method::LinkLocal), - _ => Err(UnknownIpMethod(s.to_string())), - } - } -} - -#[derive(Debug, Default, Copy, Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub enum Ipv6Method { - Disabled = 0, - #[default] - Auto = 1, - Manual = 2, - LinkLocal = 3, - Ignore = 4, - Dhcp = 5, -} - -impl fmt::Display for Ipv6Method { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let name = match &self { - Ipv6Method::Disabled => "disabled", - Ipv6Method::Auto => "auto", - Ipv6Method::Manual => "manual", - Ipv6Method::LinkLocal => "link-local", - Ipv6Method::Ignore => "ignore", - Ipv6Method::Dhcp => "dhcp", - }; - write!(f, "{}", name) - } -} - -impl FromStr for Ipv6Method { - type Err = UnknownIpMethod; - - fn from_str(s: &str) -> Result { - match s { - "disabled" => Ok(Ipv6Method::Disabled), - "auto" => Ok(Ipv6Method::Auto), - "manual" => Ok(Ipv6Method::Manual), - "link-local" => Ok(Ipv6Method::LinkLocal), - "ignore" => Ok(Ipv6Method::Ignore), - "dhcp" => Ok(Ipv6Method::Dhcp), - _ => Err(UnknownIpMethod(s.to_string())), - } - } -} - -impl From for zbus::fdo::Error { - fn from(value: UnknownIpMethod) -> zbus::fdo::Error { - zbus::fdo::Error::Failed(value.to_string()) - } -} - -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct IpRoute { - #[schema(schema_with = schemas::ip_inet_ref)] - pub destination: IpInet, - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(schema_with = schemas::ip_addr)] - pub next_hop: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub metric: Option, -} - -impl From<&IpRoute> for HashMap<&str, Value<'_>> { - fn from(route: &IpRoute) -> Self { - let mut map: HashMap<&str, Value> = HashMap::from([ - ("dest", Value::new(route.destination.address().to_string())), - ( - "prefix", - Value::new(route.destination.network_length() as u32), - ), - ]); - if let Some(next_hop) = route.next_hop { - map.insert("next-hop", Value::new(next_hop.to_string())); - } - if let Some(metric) = route.metric { - map.insert("metric", Value::new(metric)); - } - map - } -} - #[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub enum VlanProtocol { #[default] @@ -1757,6 +1434,63 @@ pub struct BondConfig { pub options: BondOptions, } +#[derive(Clone, Debug, Default)] +pub struct ConnectionCollection(pub Vec); + +impl ConnectionCollection { + pub fn ports_for(&self, uuid: Uuid) -> Vec { + self.0 + .iter() + .filter(|c| c.controller == Some(uuid)) + .map(|c| { + if let Some(interface) = c.interface.to_owned() { + interface + } else { + c.clone().id + } + }) + .collect() + } +} + +impl TryFrom for NetworkConnectionsCollection { + type Error = NetworkStateError; + + fn try_from(collection: ConnectionCollection) -> Result { + let network_connections = collection + .0 + .iter() + .filter(|c| c.controller.is_none()) + .map(|c| { + let mut conn = NetworkConnection::try_from(c.clone()).unwrap(); + if let Some(ref mut bond) = conn.bond { + bond.ports = collection.ports_for(c.uuid); + } + if let Some(ref mut bridge) = conn.bridge { + bridge.ports = collection.ports_for(c.uuid); + }; + conn + }) + .collect(); + + Ok(NetworkConnectionsCollection(network_connections)) + } +} + +impl TryFrom for ConnectionCollection { + type Error = NetworkStateError; + + fn try_from(collection: NetworkConnectionsCollection) -> Result { + let network_connections = collection + .0 + .iter() + .map(|c| Connection::try_from(c.clone()).unwrap()) + .collect(); + + Ok(ConnectionCollection(network_connections)) + } +} + impl TryFrom for NetworkConnectionsCollection { type Error = NetworkStateError; @@ -1795,8 +1529,12 @@ impl TryFrom for Config { type Error = NetworkStateError; fn try_from(state: NetworkState) -> Result { + let connections: NetworkConnectionsCollection = + ConnectionCollection(state.connections).try_into()?; + Ok(Config { - connections: state.clone().try_into()?, + connections: Some(connections), + general_state: Some(state.general_state), }) } } @@ -1805,8 +1543,13 @@ impl TryFrom for SystemInfo { type Error = NetworkStateError; fn try_from(state: NetworkState) -> Result { + let connections: NetworkConnectionsCollection = + ConnectionCollection(state.connections).try_into()?; + Ok(SystemInfo { - connections: state.try_into()?, + connections, + devices: state.devices, + general_state: state.general_state, }) } } @@ -1815,8 +1558,12 @@ impl TryFrom for Proposal { type Error = NetworkStateError; fn try_from(state: NetworkState) -> Result { + let connections: NetworkConnectionsCollection = + ConnectionCollection(state.connections).try_into()?; + Ok(Proposal { - connections: state.try_into()?, + connections, + general_state: state.general_state, }) } } diff --git a/rust/agama-network/src/nm/builder.rs b/rust/agama-network/src/nm/builder.rs index 41ed1bc037..fa216c1dad 100644 --- a/rust/agama-network/src/nm/builder.rs +++ b/rust/agama-network/src/nm/builder.rs @@ -21,12 +21,11 @@ //! Conversion mechanism between proxies and model structs. use crate::{ - model::{Device, IpConfig, IpRoute, MacAddress}, nm::{ model::NmDeviceType, proxies::{DeviceProxy, IP4ConfigProxy, IP6ConfigProxy}, }, - types::{DeviceState, DeviceType}, + types::{Device, DeviceState, DeviceType, IpConfig, IpRoute, MacAddress}, }; use cidr::IpInet; use std::{collections::HashMap, net::IpAddr, str::FromStr}; diff --git a/rust/agama-network/src/nm/client.rs b/rust/agama-network/src/nm/client.rs index 3b79dd527c..e697b05504 100644 --- a/rust/agama-network/src/nm/client.rs +++ b/rust/agama-network/src/nm/client.rs @@ -35,10 +35,11 @@ use super::proxies::{ SettingsProxy, WirelessProxy, }; use crate::model::{ - AccessPoint, Connection, ConnectionConfig, Device, GeneralState, SecurityProtocol, - NOT_COPY_NETWORK_PATH, + AccessPoint, Connection, ConnectionConfig, SecurityProtocol, NOT_COPY_NETWORK_PATH, +}; +use crate::types::{ + AddFlags, ConnectionFlags, Device, DeviceType, GeneralState, UpdateFlags, SSID, }; -use crate::types::{AddFlags, ConnectionFlags, DeviceType, UpdateFlags, SSID}; use agama_utils::dbus::get_optional_property; use semver::Version; use uuid::Uuid; diff --git a/rust/agama-network/src/nm/dbus.rs b/rust/agama-network/src/nm/dbus.rs index c983180840..83741ce50c 100644 --- a/rust/agama-network/src/nm/dbus.rs +++ b/rust/agama-network/src/nm/dbus.rs @@ -24,7 +24,7 @@ //! with nested hash maps (see [NestedHash] and [OwnedNestedHash]). use super::{error::NmError, model::*}; use crate::model::*; -use crate::types::{BondMode, SSID}; +use crate::types::*; use agama_utils::dbus::{ get_optional_property, get_property, to_owned_hash, NestedHash, OwnedNestedHash, }; diff --git a/rust/agama-network/src/nm/error.rs b/rust/agama-network/src/nm/error.rs index 6e90c7bd44..be85ef8a8a 100644 --- a/rust/agama-network/src/nm/error.rs +++ b/rust/agama-network/src/nm/error.rs @@ -69,7 +69,7 @@ pub enum NmError { #[error("Invalid infiniband transport mode: '{0}'")] InvalidInfinibandTranportMode(#[from] crate::model::InvalidInfinibandTransportMode), #[error("Invalid MAC address: '{0}'")] - InvalidMACAddress(#[from] crate::model::InvalidMacAddress), + InvalidMACAddress(#[from] crate::types::InvalidMacAddress), #[error("Invalid network prefix: '{0}'")] InvalidNetworkPrefix(#[from] NetworkLengthTooLongError), #[error("Invalid network address: '{0}'")] diff --git a/rust/agama-network/src/nm/model.rs b/rust/agama-network/src/nm/model.rs index 10a6719b2f..75b499e03a 100644 --- a/rust/agama-network/src/nm/model.rs +++ b/rust/agama-network/src/nm/model.rs @@ -27,9 +27,9 @@ /// Using the newtype pattern around an String is enough. For proper support, we might replace this /// struct with an enum. use crate::{ - model::{Ipv4Method, Ipv6Method, SecurityProtocol, WirelessMode}, + model::{SecurityProtocol, WirelessMode}, nm::error::NmError, - types::{ConnectionState, DeviceType}, + types::{ConnectionState, DeviceType, Ipv4Method, Ipv6Method}, }; use std::fmt; use std::str::FromStr; diff --git a/rust/agama-network/src/nm/watcher.rs b/rust/agama-network/src/nm/watcher.rs index 2f446848fc..c8af7e1dc7 100644 --- a/rust/agama-network/src/nm/watcher.rs +++ b/rust/agama-network/src/nm/watcher.rs @@ -25,9 +25,8 @@ use std::collections::{hash_map::Entry, HashMap}; -use crate::{ - adapter::Watcher, model::Device, nm::proxies::DeviceProxy, Action, NetworkAdapterError, -}; +use crate::types::Device; +use crate::{adapter::Watcher, nm::proxies::DeviceProxy, Action, NetworkAdapterError}; use anyhow::anyhow; use async_trait::async_trait; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; diff --git a/rust/agama-network/src/system.rs b/rust/agama-network/src/system.rs index 0722ac0300..6fe68f04ca 100644 --- a/rust/agama-network/src/system.rs +++ b/rust/agama-network/src/system.rs @@ -21,10 +21,8 @@ use crate::{ action::Action, error::NetworkStateError, - model::{ - AccessPoint, Connection, Device, GeneralState, NetworkChange, NetworkState, StateConfig, - }, - types::{DeviceType, Proposal, SystemInfo}, + model::{AccessPoint, Connection, NetworkChange, NetworkState, StateConfig}, + types::{Config, Device, DeviceType, GeneralState, Proposal, SystemInfo}, Adapter, NetworkAdapterError, }; use std::error::Error; @@ -169,6 +167,14 @@ impl NetworkSystemClient { Ok(rx.await?) } + pub async fn update_config(&self, config: Config) -> Result<(), NetworkSystemError> { + let (tx, rx) = oneshot::channel(); + self.actions + .send(Action::UpdateConfig(Box::new(config.clone()), tx))?; + let result = rx.await?; + Ok(result?) + } + pub async fn get_system_config(&self) -> Result { let (tx, rx) = oneshot::channel(); self.actions.send(Action::GetSystemConfig(tx))?; @@ -329,6 +335,11 @@ impl NetworkSystemServer { let config: Proposal = self.state.clone().try_into()?; tx.send(config).unwrap(); } + Action::UpdateConfig(config, tx) => { + let result = self.state.update_state(*config); + + tx.send(result).unwrap(); + } Action::GetConnections(tx) => { let connections = self .state diff --git a/rust/agama-network/src/types.rs b/rust/agama-network/src/types.rs index e53bbbfbb0..a1b78ad55a 100644 --- a/rust/agama-network/src/types.rs +++ b/rust/agama-network/src/types.rs @@ -23,15 +23,6 @@ use serde::{Deserialize, Serialize}; use std::str::{self}; use thiserror::Error; -/// Network device -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type")] -pub struct Device { - pub name: String, - pub type_: DeviceType, - pub state: DeviceState, -} - // https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMSettingsConnectionFlags #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, utoipa::ToSchema)] pub enum ConnectionFlags { diff --git a/rust/agama-server/src/web/docs/config.rs b/rust/agama-server/src/web/docs/config.rs index bcc7737611..661083a1a2 100644 --- a/rust/agama-server/src/web/docs/config.rs +++ b/rust/agama-server/src/web/docs/config.rs @@ -61,23 +61,23 @@ impl ApiDocBuilder for ConfigApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() .schema_from::() - .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() diff --git a/rust/agama-utils/Cargo.toml b/rust/agama-utils/Cargo.toml index 5a39c87ebb..961d044bde 100644 --- a/rust/agama-utils/Cargo.toml +++ b/rust/agama-utils/Cargo.toml @@ -20,6 +20,7 @@ zvariant = "5.5.2" gettext-rs = { version = "0.7.2", features = ["gettext-system"] } uuid = { version = "1.10.0", features = ["v4"] } cidr = { version = "0.3.1", features = ["serde"] } +macaddr = { version = "1.0.1", features = ["serde_std"] } [dev-dependencies] tokio-test = "0.4.4" diff --git a/rust/agama-utils/src/api/network/config.rs b/rust/agama-utils/src/api/network/config.rs index 80726790f9..490de3d860 100644 --- a/rust/agama-utils/src/api/network/config.rs +++ b/rust/agama-utils/src/api/network/config.rs @@ -20,7 +20,8 @@ //! Representation of the network settings -use crate::api::network::NetworkConnectionsCollection; +use crate::api::network; +use network::{GeneralState, NetworkConnectionsCollection}; use serde::{Deserialize, Serialize}; use std::default::Default; @@ -29,5 +30,6 @@ use std::default::Default; #[serde(rename_all = "camelCase")] pub struct Config { /// Connections to use in the installation - pub connections: NetworkConnectionsCollection, + pub connections: Option, + pub general_state: Option, } diff --git a/rust/agama-utils/src/api/network/proposal.rs b/rust/agama-utils/src/api/network/proposal.rs index ec94a78bf7..16f05d21dd 100644 --- a/rust/agama-utils/src/api/network/proposal.rs +++ b/rust/agama-utils/src/api/network/proposal.rs @@ -20,7 +20,7 @@ //! Representation of the network settings -use crate::api::network::NetworkConnectionsCollection; +use crate::api::network::{GeneralState, NetworkConnectionsCollection}; use serde::{Deserialize, Serialize}; use std::default::Default; @@ -30,4 +30,5 @@ use std::default::Default; pub struct Proposal { /// Connections to use in the installation pub connections: NetworkConnectionsCollection, + pub general_state: GeneralState, } diff --git a/rust/agama-utils/src/api/network/settings.rs b/rust/agama-utils/src/api/network/settings.rs index 4ee8625944..8f4e68ee08 100644 --- a/rust/agama-utils/src/api/network/settings.rs +++ b/rust/agama-utils/src/api/network/settings.rs @@ -34,7 +34,6 @@ pub struct NetworkConnectionsCollection(pub Vec); #[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct NetworkSettings { - /// Connections to use in the installation pub connections: NetworkConnectionsCollection, } @@ -199,7 +198,7 @@ pub struct IEEE8021XSettings { pub peap_label: bool, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] pub struct NetworkDevice { pub id: String, pub type_: DeviceType, diff --git a/rust/agama-utils/src/api/network/system_info.rs b/rust/agama-utils/src/api/network/system_info.rs index 89cb5d051b..8e88dcc958 100644 --- a/rust/agama-utils/src/api/network/system_info.rs +++ b/rust/agama-utils/src/api/network/system_info.rs @@ -20,7 +20,7 @@ //! Representation of the network settings -use crate::api::network::NetworkConnectionsCollection; +use crate::api::network::{Device, GeneralState, NetworkConnectionsCollection}; use serde::{Deserialize, Serialize}; use std::default::Default; @@ -30,4 +30,7 @@ use std::default::Default; pub struct SystemInfo { /// Connections to use in the installation pub connections: NetworkConnectionsCollection, + pub devices: Vec, + pub general_state: GeneralState, + // networks or access_points shold be returned } diff --git a/rust/agama-utils/src/api/network/types.rs b/rust/agama-utils/src/api/network/types.rs index 5b6e24a6df..7ad6a7b812 100644 --- a/rust/agama-utils/src/api/network/types.rs +++ b/rust/agama-utils/src/api/network/types.rs @@ -18,23 +18,432 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use cidr::errors::NetworkParseError; +use crate::openapi::schemas; +use cidr::{errors::NetworkParseError, IpInet}; use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, skip_serializing_none, DisplayFromStr}; use std::{ + collections::HashMap, fmt, + net::IpAddr, str::{self, FromStr}, }; use thiserror::Error; +use zbus::zvariant::Value; + +/// Network state +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct GeneralState { + pub hostname: String, + pub connectivity: bool, + pub copy_network: bool, + pub wireless_enabled: bool, + pub networking_enabled: bool, // pub network_state: NMSTATE +} /// Network device -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type")] +#[serde_as] +#[skip_serializing_none] +#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct Device { pub name: String, + #[serde(rename = "type")] pub type_: DeviceType, + #[serde_as(as = "DisplayFromStr")] + pub mac_address: MacAddress, + pub ip_config: Option, + // Connection.id + pub connection: Option, pub state: DeviceState, } +#[derive(Debug, Default, Clone, PartialEq, Serialize, utoipa::ToSchema)] +pub enum MacAddress { + #[schema(value_type = String, format = "MAC address in EUI-48 format")] + MacAddress(macaddr::MacAddr6), + Preserve, + Permanent, + Random, + Stable, + #[default] + Unset, +} + +impl fmt::Display for MacAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let output = match &self { + Self::MacAddress(mac) => mac.to_string(), + Self::Preserve => "preserve".to_string(), + Self::Permanent => "permanent".to_string(), + Self::Random => "random".to_string(), + Self::Stable => "stable".to_string(), + Self::Unset => "".to_string(), + }; + write!(f, "{}", output) + } +} + +#[derive(Debug, Error)] +#[error("Invalid MAC address: {0}")] +pub struct InvalidMacAddress(String); + +impl FromStr for MacAddress { + type Err = InvalidMacAddress; + + fn from_str(s: &str) -> Result { + match s { + "preserve" => Ok(Self::Preserve), + "permanent" => Ok(Self::Permanent), + "random" => Ok(Self::Random), + "stable" => Ok(Self::Stable), + "" => Ok(Self::Unset), + _ => Ok(Self::MacAddress(match macaddr::MacAddr6::from_str(s) { + Ok(mac) => mac, + Err(e) => return Err(InvalidMacAddress(e.to_string())), + })), + } + } +} + +impl TryFrom<&Option> for MacAddress { + type Error = InvalidMacAddress; + + fn try_from(value: &Option) -> Result { + match &value { + Some(str) => MacAddress::from_str(str), + None => Ok(Self::Unset), + } + } +} + +impl From for zbus::fdo::Error { + fn from(value: InvalidMacAddress) -> Self { + zbus::fdo::Error::Failed(value.to_string()) + } +} + +#[skip_serializing_none] +#[derive(Default, Debug, PartialEq, Clone, Deserialize, Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct IpConfig { + pub method4: Ipv4Method, + pub method6: Ipv6Method, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[schema(schema_with = schemas::ip_inet_array)] + pub addresses: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[schema(schema_with = schemas::ip_addr_array)] + pub nameservers: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub dns_searchlist: Vec, + pub ignore_auto_dns: bool, + #[schema(schema_with = schemas::ip_addr)] + pub gateway4: Option, + #[schema(schema_with = schemas::ip_addr)] + pub gateway6: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub routes4: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub routes6: Vec, + pub dhcp4_settings: Option, + pub dhcp6_settings: Option, + pub ip6_privacy: Option, + pub dns_priority4: Option, + pub dns_priority6: Option, +} + +#[skip_serializing_none] +#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, utoipa::ToSchema)] +pub struct Dhcp4Settings { + pub send_hostname: Option, + pub hostname: Option, + pub send_release: Option, + pub client_id: DhcpClientId, + pub iaid: DhcpIaid, +} + +#[skip_serializing_none] +#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, utoipa::ToSchema)] +pub struct Dhcp6Settings { + pub send_hostname: Option, + pub hostname: Option, + pub send_release: Option, + pub duid: DhcpDuid, + pub iaid: DhcpIaid, +} +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)] +pub enum DhcpClientId { + Id(String), + Mac, + PermMac, + Ipv6Duid, + Duid, + Stable, + None, + #[default] + Unset, +} + +impl From<&str> for DhcpClientId { + fn from(s: &str) -> Self { + match s { + "mac" => Self::Mac, + "perm-mac" => Self::PermMac, + "ipv6-duid" => Self::Ipv6Duid, + "duid" => Self::Duid, + "stable" => Self::Stable, + "none" => Self::None, + "" => Self::Unset, + _ => Self::Id(s.to_string()), + } + } +} + +impl From> for DhcpClientId { + fn from(value: Option) -> Self { + match &value { + Some(str) => Self::from(str.as_str()), + None => Self::Unset, + } + } +} + +impl fmt::Display for DhcpClientId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let output = match &self { + Self::Id(id) => id.to_string(), + Self::Mac => "mac".to_string(), + Self::PermMac => "perm-mac".to_string(), + Self::Ipv6Duid => "ipv6-duid".to_string(), + Self::Duid => "duid".to_string(), + Self::Stable => "stable".to_string(), + Self::None => "none".to_string(), + Self::Unset => "".to_string(), + }; + write!(f, "{}", output) + } +} + +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)] +pub enum DhcpDuid { + Id(String), + Lease, + Llt, + Ll, + StableLlt, + StableLl, + StableUuid, + #[default] + Unset, +} + +impl From<&str> for DhcpDuid { + fn from(s: &str) -> Self { + match s { + "lease" => Self::Lease, + "llt" => Self::Llt, + "ll" => Self::Ll, + "stable-llt" => Self::StableLlt, + "stable-ll" => Self::StableLl, + "stable-uuid" => Self::StableUuid, + "" => Self::Unset, + _ => Self::Id(s.to_string()), + } + } +} + +impl From> for DhcpDuid { + fn from(value: Option) -> Self { + match &value { + Some(str) => Self::from(str.as_str()), + None => Self::Unset, + } + } +} + +impl fmt::Display for DhcpDuid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let output = match &self { + Self::Id(id) => id.to_string(), + Self::Lease => "lease".to_string(), + Self::Llt => "llt".to_string(), + Self::Ll => "ll".to_string(), + Self::StableLlt => "stable-llt".to_string(), + Self::StableLl => "stable-ll".to_string(), + Self::StableUuid => "stable-uuid".to_string(), + Self::Unset => "".to_string(), + }; + write!(f, "{}", output) + } +} + +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)] +pub enum DhcpIaid { + Id(String), + Mac, + PermMac, + Ifname, + Stable, + #[default] + Unset, +} + +impl From<&str> for DhcpIaid { + fn from(s: &str) -> Self { + match s { + "mac" => Self::Mac, + "perm-mac" => Self::PermMac, + "ifname" => Self::Ifname, + "stable" => Self::Stable, + "" => Self::Unset, + _ => Self::Id(s.to_string()), + } + } +} + +impl From> for DhcpIaid { + fn from(value: Option) -> Self { + match value { + Some(str) => Self::from(str.as_str()), + None => Self::Unset, + } + } +} + +impl fmt::Display for DhcpIaid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let output = match &self { + Self::Id(id) => id.to_string(), + Self::Mac => "mac".to_string(), + Self::PermMac => "perm-mac".to_string(), + Self::Ifname => "ifname".to_string(), + Self::Stable => "stable".to_string(), + Self::Unset => "".to_string(), + }; + write!(f, "{}", output) + } +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct IpRoute { + #[schema(schema_with = schemas::ip_inet_ref)] + pub destination: IpInet, + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(schema_with = schemas::ip_addr)] + pub next_hop: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub metric: Option, +} + +impl From<&IpRoute> for HashMap<&str, Value<'_>> { + fn from(route: &IpRoute) -> Self { + let mut map: HashMap<&str, Value> = HashMap::from([ + ("dest", Value::new(route.destination.address().to_string())), + ( + "prefix", + Value::new(route.destination.network_length() as u32), + ), + ]); + if let Some(next_hop) = route.next_hop { + map.insert("next-hop", Value::new(next_hop.to_string())); + } + if let Some(metric) = route.metric { + map.insert("metric", Value::new(metric)); + } + map + } +} + +#[derive(Debug, Error)] +#[error("Unknown IP configuration method name: {0}")] +pub struct UnknownIpMethod(String); + +#[derive(Debug, Default, Copy, Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub enum Ipv4Method { + Disabled = 0, + #[default] + Auto = 1, + Manual = 2, + LinkLocal = 3, +} + +impl fmt::Display for Ipv4Method { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match &self { + Ipv4Method::Disabled => "disabled", + Ipv4Method::Auto => "auto", + Ipv4Method::Manual => "manual", + Ipv4Method::LinkLocal => "link-local", + }; + write!(f, "{}", name) + } +} + +impl FromStr for Ipv4Method { + type Err = UnknownIpMethod; + + fn from_str(s: &str) -> Result { + match s { + "disabled" => Ok(Ipv4Method::Disabled), + "auto" => Ok(Ipv4Method::Auto), + "manual" => Ok(Ipv4Method::Manual), + "link-local" => Ok(Ipv4Method::LinkLocal), + _ => Err(UnknownIpMethod(s.to_string())), + } + } +} + +#[derive(Debug, Default, Copy, Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub enum Ipv6Method { + Disabled = 0, + #[default] + Auto = 1, + Manual = 2, + LinkLocal = 3, + Ignore = 4, + Dhcp = 5, +} + +impl fmt::Display for Ipv6Method { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match &self { + Ipv6Method::Disabled => "disabled", + Ipv6Method::Auto => "auto", + Ipv6Method::Manual => "manual", + Ipv6Method::LinkLocal => "link-local", + Ipv6Method::Ignore => "ignore", + Ipv6Method::Dhcp => "dhcp", + }; + write!(f, "{}", name) + } +} + +impl FromStr for Ipv6Method { + type Err = UnknownIpMethod; + + fn from_str(s: &str) -> Result { + match s { + "disabled" => Ok(Ipv6Method::Disabled), + "auto" => Ok(Ipv6Method::Auto), + "manual" => Ok(Ipv6Method::Manual), + "link-local" => Ok(Ipv6Method::LinkLocal), + "ignore" => Ok(Ipv6Method::Ignore), + "dhcp" => Ok(Ipv6Method::Dhcp), + _ => Err(UnknownIpMethod(s.to_string())), + } + } +} + +impl From for zbus::fdo::Error { + fn from(value: UnknownIpMethod) -> zbus::fdo::Error { + zbus::fdo::Error::Failed(value.to_string()) + } +} #[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct SSID(pub Vec); diff --git a/service/lib/agama/http/clients/base.rb b/service/lib/agama/http/clients/base.rb index 84804671fa..4a4940231a 100644 --- a/service/lib/agama/http/clients/base.rb +++ b/service/lib/agama/http/clients/base.rb @@ -29,7 +29,7 @@ module Clients # Base for HTTP clients. class Base def initialize(logger) - @base_url = "http://localhost/api/" + @base_url = "http://localhost/api/v2/" @logger = logger end diff --git a/service/lib/agama/http/clients/network.rb b/service/lib/agama/http/clients/network.rb index ad98ab1d0d..c67c7a9d61 100644 --- a/service/lib/agama/http/clients/network.rb +++ b/service/lib/agama/http/clients/network.rb @@ -26,12 +26,16 @@ module HTTP module Clients # HTTP client to interact with the network API. class Network < Base + def proposal + JSON.parse(get("proposal")) + end + def connections - JSON.parse(get("network/connections")) + proposal.fetch("network", {}).fetch("connections", []) end def devices - JSON.parse(get("network/devices")) + proposal.fetch("network", {}).fetch("devices", []) end def persist_connections @@ -39,7 +43,7 @@ def persist_connections end def state - JSON.parse(get("network/state")) + proposal.fetch("network", {}).fetch("state", {}) end end end diff --git a/service/lib/agama/network.rb b/service/lib/agama/network.rb index bd91caff30..efb4b2fb16 100644 --- a/service/lib/agama/network.rb +++ b/service/lib/agama/network.rb @@ -153,6 +153,7 @@ def persist_connections end def copy_connections? + false http_client.state["copyNetwork"] end diff --git a/web/src/api/network.ts b/web/src/api/network.ts index ec919ca3d9..6a2cea1f06 100644 --- a/web/src/api/network.ts +++ b/web/src/api/network.ts @@ -20,8 +20,18 @@ * find current contact information at www.suse.com. */ -import { del, get, post, put } from "~/api/http"; -import { APIAccessPoint, APIConnection, APIDevice, NetworkGeneralState } from "~/types/network"; +import { get, patch, post } from "~/api/http"; +import { + APIAccessPoint, + APIConnection, + APIDevice, + APINetworkProposal, + Connection, + ConnectionStatus, + NetworkGeneralState, + NetworkProposal, +} from "~/types/network"; +import { Proposal } from "~/types/proposal"; /** * Returns the network configuration @@ -61,14 +71,25 @@ const addConnection = (connection: APIConnection) => post("/api/network/connecti * * @param connection - connection to be updated */ -const updateConnection = (connection: APIConnection) => - put(`/api/network/connections/${encodeURIComponent(connection.id)}`, connection); +const updateConnection = (connection: Connection) => { + const network: APINetworkProposal = { connections: [connection.toApi()] }; + const config: Proposal = { network }; + console.log("Updating"); + console.log(config); + + patch(`/api/v2/config`, { config }); +}; /** * Deletes the connection matching given name */ -const deleteConnection = (name: string) => - del(`/api/network/connections/${encodeURIComponent(name)}`); +const deleteConnection = (name: string) => { + const connection = new Connection(name); + connection.status = ConnectionStatus.DELETE; + const network = new NetworkProposal([connection]); + + patch(`/api/v2/config`, { network }); +}; /** * Apply network changes diff --git a/web/src/components/network/IpSettingsForm.tsx b/web/src/components/network/IpSettingsForm.tsx index 0b0d87d36e..782af122d6 100644 --- a/web/src/components/network/IpSettingsForm.tsx +++ b/web/src/components/network/IpSettingsForm.tsx @@ -41,8 +41,9 @@ import AddressesDataList from "~/components/network/AddressesDataList"; import DnsDataList from "~/components/network/DnsDataList"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; -import { useConnection, useConnectionMutation } from "~/queries/network"; +import { useConnection } from "~/queries/network"; import { IPAddress, Connection, ConnectionMethod } from "~/types/network"; +import { updateConnection } from "~/api/network"; const usingDHCP = (method: ConnectionMethod) => method === ConnectionMethod.AUTO; @@ -51,7 +52,6 @@ const usingDHCP = (method: ConnectionMethod) => method === ConnectionMethod.AUTO export default function IpSettingsForm() { const { id } = useParams(); const navigate = useNavigate(); - const { mutateAsync: updateConnection } = useConnectionMutation(); const connection = useConnection(id); const [addresses, setAddresses] = useState(connection.addresses); const [nameservers, setNameservers] = useState( @@ -62,7 +62,7 @@ export default function IpSettingsForm() { const [method, setMethod] = useState(connection.method4); const [gateway, setGateway] = useState(connection.gateway4); const [fieldErrors, setFieldErrors] = useState({}); - const [requestError, setRequestError] = useState(); + const [requestError] = useState(); const isSetAsInvalid = (field: string) => Object.keys(fieldErrors).includes(field); const isGatewayDisabled = addresses.length === 0; @@ -127,11 +127,8 @@ export default function IpSettingsForm() { nameservers: sanitizedNameservers.map((s) => s.address), }); - updateConnection(updatedConnection) - .then(() => navigate(-1)) - .catch((error) => { - setRequestError(error.message); - }); + updateConnection(updatedConnection); + navigate(-1); }; const renderError = (field: string) => { diff --git a/web/src/components/network/NetworkPage.tsx b/web/src/components/network/NetworkPage.tsx index 348083bc6a..abd1296c41 100644 --- a/web/src/components/network/NetworkPage.tsx +++ b/web/src/components/network/NetworkPage.tsx @@ -23,11 +23,12 @@ import React from "react"; import { Content, Grid, GridItem } from "@patternfly/react-core"; import { EmptyState, Page } from "~/components/core"; -import { useNetworkChanges, useNetworkState } from "~/queries/network"; +import { useNetworkChanges } from "~/queries/network"; import WifiNetworksList from "./WifiNetworksList"; import WiredConnectionsList from "./WiredConnectionsList"; import NoPersistentConnectionsAlert from "./NoPersistentConnectionsAlert"; import { _ } from "~/i18n"; +import { useSystem } from "~/queries/system"; const NoWifiAvailable = () => ( @@ -44,7 +45,7 @@ const NoWifiAvailable = () => ( */ export default function NetworkPage() { useNetworkChanges(); - const networkState = useNetworkState(); + const { network: networkSystem } = useSystem(); return ( @@ -62,7 +63,7 @@ export default function NetworkPage() { - {networkState.wirelessEnabled ? ( + {networkSystem.wirelessEnabled ? ( diff --git a/web/src/components/network/NoPersistentConnectionsAlert.tsx b/web/src/components/network/NoPersistentConnectionsAlert.tsx index a2d0a66d90..f6ff2e8368 100644 --- a/web/src/components/network/NoPersistentConnectionsAlert.tsx +++ b/web/src/components/network/NoPersistentConnectionsAlert.tsx @@ -22,16 +22,17 @@ import React from "react"; import { Alert } from "@patternfly/react-core"; -import { useConnections } from "~/queries/network"; import { Connection } from "~/types/network"; import { _ } from "~/i18n"; +import { useNetworkProposal } from "~/queries/proposal"; /** * Displays a warning alert when no network connections are set to persist in * the installed system. */ export default function NoPersistentConnectionsAlert() { - const connections: Connection[] = useConnections(); + const proposal = useNetworkProposal(); + const connections: Connection[] = proposal.connections; const persistentConnections: number = connections.filter((c) => c.persistent).length; if (persistentConnections !== 0) return; diff --git a/web/src/components/network/WiredConnectionDetails.tsx b/web/src/components/network/WiredConnectionDetails.tsx index e54b3616f8..cf4ec5598f 100644 --- a/web/src/components/network/WiredConnectionDetails.tsx +++ b/web/src/components/network/WiredConnectionDetails.tsx @@ -42,7 +42,7 @@ import InstallationOnlySwitch from "./InstallationOnlySwitch"; import { Connection, Device } from "~/types/network"; import { connectionBindingMode, formatIp } from "~/utils/network"; import { NETWORK } from "~/routes/paths"; -import { useNetworkDevices } from "~/queries/network"; +import { useNetworkDevices } from "~/queries/system"; import { generateEncodedPath } from "~/utils"; import { isEmpty } from "radashi"; import { sprintf } from "sprintf-js"; diff --git a/web/src/components/network/WiredConnectionPage.tsx b/web/src/components/network/WiredConnectionPage.tsx index 2cb251b30f..072e2d9079 100644 --- a/web/src/components/network/WiredConnectionPage.tsx +++ b/web/src/components/network/WiredConnectionPage.tsx @@ -30,13 +30,14 @@ import { EmptyStateFooter, } from "@patternfly/react-core"; import { Link, Page } from "~/components/core"; -import { useConnections, useNetworkChanges } from "~/queries/network"; +import { useNetworkChanges } from "~/queries/network"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; import WiredConnectionDetails from "./WiredConnectionDetails"; import { Icon } from "../layout"; import { NETWORK } from "~/routes/paths"; import NoPersistentConnectionsAlert from "./NoPersistentConnectionsAlert"; +import { useNetworkProposal } from "~/queries/proposal"; const ConnectionNotFound = ({ id }) => { // TRANSLATORS: %s will be replaced with connection id @@ -62,8 +63,8 @@ const ConnectionNotFound = ({ id }) => { export default function WiredConnectionPage() { useNetworkChanges(); + const { connections } = useNetworkProposal(); const { id } = useParams(); - const connections = useConnections(); const connection = connections.find((c) => c.id === id); const title = _("Connection details"); diff --git a/web/src/components/network/WiredConnectionsList.tsx b/web/src/components/network/WiredConnectionsList.tsx index 873c765bb0..a1c58c565c 100644 --- a/web/src/components/network/WiredConnectionsList.tsx +++ b/web/src/components/network/WiredConnectionsList.tsx @@ -35,18 +35,19 @@ import { import a11yStyles from "@patternfly/react-styles/css/utilities/Accessibility/accessibility"; import { Annotation, EmptyState } from "~/components/core"; import { Connection } from "~/types/network"; -import { useConnections, useNetworkDevices } from "~/queries/network"; import { NETWORK as PATHS } from "~/routes/paths"; import { formatIp } from "~/utils/network"; import { _ } from "~/i18n"; import { generateEncodedPath } from "~/utils"; +import { useNetworkSystem } from "~/queries/system"; +import { useNetworkProposal } from "~/queries/proposal"; type ConnectionListItemProps = { connection: Connection }; const ConnectionListItem = ({ connection }: ConnectionListItemProps) => { const nameId = useId(); const ipId = useId(); - const devices = useNetworkDevices(); + const { devices } = useNetworkSystem(); const device = devices.find( ({ connection: deviceConnectionId }) => deviceConnectionId === connection.id, @@ -84,7 +85,7 @@ const ConnectionListItem = ({ connection }: ConnectionListItemProps) => { */ function WiredConnectionsList(props: DataListProps) { const navigate = useNavigate(); - const connections = useConnections(); + const { connections } = useNetworkProposal(); const wiredConnections = connections.filter((c) => !c.wireless); if (wiredConnections.length === 0) { diff --git a/web/src/queries/network.ts b/web/src/queries/network.ts index 09a37ee7e0..4f581c6a3f 100644 --- a/web/src/queries/network.ts +++ b/web/src/queries/network.ts @@ -45,6 +45,8 @@ import { persist, updateConnection, } from "~/api/network"; +import { useNetworkProposal } from "./proposal"; +import { useNetworkSystem } from "./system"; /** * Returns a query for retrieving the general network configuration @@ -133,8 +135,7 @@ const useAddConnectionMutation = () => { const useConnectionMutation = () => { const queryClient = useQueryClient(); const query = { - mutationFn: (newConnection: Connection) => - updateConnection(newConnection.toApi()).then(() => applyChanges()), + mutationFn: updateConnection, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["network", "connections"] }); queryClient.invalidateQueries({ queryKey: ["network", "devices"] }); @@ -287,8 +288,10 @@ const useNetworkChanges = () => { }; const useConnection = (name: string) => { - const { data } = useSuspenseQuery(connectionQuery(name)); - return data; + const { connections } = useNetworkProposal(); + const connection = connections.find((c) => c.id === name); + + return connection; }; /** @@ -303,16 +306,18 @@ const useNetworkState = (): NetworkGeneralState => { * Returns the network devices. */ const useNetworkDevices = (): Device[] => { - const { data } = useSuspenseQuery(devicesQuery()); - return data; + const { devices } = useNetworkSystem(); + + return devices; }; /** * Returns the network connections. */ const useConnections = (): Connection[] => { - const { data } = useSuspenseQuery(connectionsQuery()); - return data; + const { connections } = useNetworkProposal(); + + return connections; }; /** diff --git a/web/src/queries/proposal.ts b/web/src/queries/proposal.ts index c83df902f2..1b8b2cc49e 100644 --- a/web/src/queries/proposal.ts +++ b/web/src/queries/proposal.ts @@ -24,6 +24,7 @@ import React from "react"; import { useSuspenseQuery, useQueryClient } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; import { fetchProposal } from "~/api/api"; +import { NetworkProposal } from "~/types/network"; /** * Returns a query for retrieving the proposal @@ -35,6 +36,12 @@ const proposalQuery = () => { }; }; +const useNetworkProposal = () => { + const { data: config } = useSuspenseQuery(proposalQuery()); + + return NetworkProposal.fromApi(config.network); +}; + const useProposal = () => { const { data: config } = useSuspenseQuery(proposalQuery()); return config; @@ -48,10 +55,11 @@ const useProposalChanges = () => { if (!client) return; return client.onEvent((event) => { - if (event.type === "ProposalChanged" && event.scope === "localization") { + const invalidateEvents = ["l10n", "network"]; + if (invalidateEvents.includes(event.type) && event.name === "ProposalChanged") { queryClient.invalidateQueries({ queryKey: ["proposal"] }); } }); }, [client, queryClient]); }; -export { useProposal, useProposalChanges }; +export { useProposal, useProposalChanges, useNetworkProposal }; diff --git a/web/src/queries/system.ts b/web/src/queries/system.ts index 8d792c5c33..de65cea66f 100644 --- a/web/src/queries/system.ts +++ b/web/src/queries/system.ts @@ -25,7 +25,7 @@ import { tzOffset } from "@date-fns/tz/tzOffset"; import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; import { fetchSystem } from "~/api/api"; -import { System } from "~/types/system"; +import { NetworkSystem } from "~/types/network"; const transformLocales = (locales) => locales.map(({ id, language: name, territory }) => ({ id, name, territory })); @@ -77,6 +77,18 @@ const useSystem = () => { return system; }; +const useNetworkSystem = () => { + const { data: config } = useSuspenseQuery(systemQuery()); + + return NetworkSystem.fromApi(config.network); +}; + +const useNetworkDevices = () => { + const { devices } = useNetworkSystem(); + + return devices; +}; + const useSystemChanges = () => { const queryClient = useQueryClient(); const client = useInstallerClient(); @@ -92,4 +104,4 @@ const useSystemChanges = () => { }, [client, queryClient]); }; -export { useSystem, useSystemChanges }; +export { useSystem, useSystemChanges, useNetworkSystem, useNetworkDevices }; diff --git a/web/src/types/network.ts b/web/src/types/network.ts index d330736f95..7dbed178c8 100644 --- a/web/src/types/network.ts +++ b/web/src/types/network.ts @@ -92,6 +92,7 @@ enum DeviceState { enum ConnectionStatus { UP = "up", DOWN = "down", + DELETE = "delete", } // Current state of the connection. @@ -360,6 +361,74 @@ type NetworkGeneralState = { wirelessEnabled: boolean; }; +class NetworkSystem { + connections: Connection[]; + accessPoints: AccessPoint[]; + devices: Device[]; + state: NetworkGeneralState; + + constructor( + connections?: Connection[], + accessPoints?: AccessPoint[], + devices?: Device[], + state?: NetworkGeneralState, + ) { + if (connections !== undefined) this.connections = connections; + if (accessPoints !== undefined) this.accessPoints = accessPoints; + if (devices !== undefined) this.devices = devices; + if (state !== undefined) this.state = state; + } + + static fromApi(options: APINetworkSystem) { + const { connections: conns, accessPoints: aps, devices: devs, state } = options; + const connections = conns.map(Connection.fromApi); + const accessPoints = aps.map(AccessPoint.fromApi).sort((a, b) => b.strength - a.strength); + const devices = devs.map(Device.fromApi); + + return new NetworkSystem(connections, accessPoints, devices, state); + } +} + +class NetworkProposal { + connections: Connection[]; + //accessPoints: AccessPoint[]; + //devices: Device[]; + state: NetworkGeneralState; + + constructor( + connections?: Connection[], + //accessPoints?: AccessPoint[], + //devices?: Device[], + state?: NetworkGeneralState, + ) { + if (connections !== undefined) this.connections = connections; + //if (accessPoints !== undefined) this.accessPoints = accessPoints; + //if (devices !== undefined) this.devices = devices; + if (state !== undefined) this.state = state; + } + + static fromApi(options: APINetworkProposal) { + const { connections, state } = options; + const conns = connections.map((c) => Connection.fromApi(c)); + + return new NetworkProposal(conns, state); + } +} + +type APINetworkSystem = { + connections: APIConnection[]; + accessPoints: APIAccessPoint[]; + devices: APIDevice[]; + state: NetworkGeneralState; +}; + +type APINetworkProposal = { + connections?: APIConnection[]; + //accessPoints?: APIAccessPoint[]; + //devices?: APIDevice[]; + state?: NetworkGeneralState; +}; + export { AccessPoint, ApFlags, @@ -373,6 +442,8 @@ export { DeviceState, DeviceType, NetworkState, + NetworkProposal, + NetworkSystem, SecurityProtocols, WifiNetworkStatus, Wireless, @@ -385,6 +456,7 @@ export type { ConnectionOptions, APIDevice, IPAddress, + APINetworkProposal, NetworkGeneralState, Route, APIRoute, diff --git a/web/src/types/proposal.ts b/web/src/types/proposal.ts index 1eacf176f9..0954ec85cf 100644 --- a/web/src/types/proposal.ts +++ b/web/src/types/proposal.ts @@ -21,9 +21,11 @@ */ import { Localization } from "./l10n"; +import { APINetworkProposal } from "./network"; type Proposal = { l10n?: Localization; + network?: APINetworkProposal; }; export type { Proposal }; diff --git a/web/src/types/system.ts b/web/src/types/system.ts index 60fb1f35c1..c8f5c78df4 100644 --- a/web/src/types/system.ts +++ b/web/src/types/system.ts @@ -21,9 +21,11 @@ */ import { Localization } from "./l10n"; +import { NetworkSystem } from "./network"; type System = { l10n?: Localization; + network?: NetworkSystem; }; export type { System }; From 10512cd1c4b3e61dcdd9c96f252e6041bb3435f6 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Mon, 3 Nov 2025 10:10:42 +0000 Subject: [PATCH 05/17] Small test fixes and removed network web service --- rust/agama-network/src/model.rs | 140 ------ rust/agama-network/src/nm/dbus.rs | 2 +- rust/agama-server/src/network.rs | 25 -- rust/agama-server/src/network/web.rs | 490 --------------------- rust/agama-server/tests/network_service.rs | 285 ------------ rust/agama-utils/src/api/network/types.rs | 51 +++ 6 files changed, 52 insertions(+), 941 deletions(-) delete mode 100644 rust/agama-server/src/network.rs delete mode 100644 rust/agama-server/src/network/web.rs delete mode 100644 rust/agama-server/tests/network_service.rs diff --git a/rust/agama-network/src/model.rs b/rust/agama-network/src/model.rs index 4f98ae8af2..37c9537726 100644 --- a/rust/agama-network/src/model.rs +++ b/rust/agama-network/src/model.rs @@ -65,95 +65,6 @@ pub struct NetworkState { pub connections: Vec, } -impl TryFrom for NetworkState { - type Error = NetworkStateError; - - fn try_from(settings: NetworkSettings) -> Result { - let mut connections: Vec = Vec::with_capacity(settings.connections.0.len()); - - for conn in settings.connections.0 { - let connection = Connection::try_from(conn.clone())?; - connections.push(connection); - - if let Some(bond) = &conn.bond { - dbg!("Bond with: ", &bond.ports); - } - if let Some(bridge) = &conn.bridge { - dbg!("Bridge with: ", &bridge.ports); - } - } - - Ok(NetworkState { - connections: connections, - ..Default::default() - }) - } -} - -/// Returns the list of connections in the order they should be written to the D-Bus service. -/// -/// * `conns`: connections to write. -fn ordered_connections(conns: &Vec) -> Vec { - let mut ordered: Vec = Vec::with_capacity(conns.len()); - for conn in conns { - add_ordered_connection(conn, conns, &mut ordered); - } - - ordered -} - -/// Adds a connections and its dependencies to the list. -/// -/// * `conn`: connection to add. -/// * `conns`: existing connections. -/// * `ordered`: ordered list of connections. -fn add_ordered_connection( - conn: &NetworkConnection, - conns: &Vec, - ordered: &mut Vec, -) { - if let Some(bond) = &conn.bond { - for port in &bond.ports { - if let Some(conn) = find_connection(port, conns) { - add_ordered_connection(conn, conns, ordered); - } else if !ordered.contains(&conn.id) { - ordered.push(port.clone()); - } - } - } - - if let Some(bridge) = &conn.bridge { - for port in &bridge.ports { - if let Some(conn) = find_connection(port, conns) { - add_ordered_connection(conn, conns, ordered); - } else if !ordered.contains(&conn.id) { - ordered.push(port.clone()); - } - } - } - - if !ordered.contains(&conn.id) { - ordered.push(conn.id.to_owned()) - } -} - -/// Finds a connection by id in the list. -/// -/// * `id`: connection ID. -fn find_connection<'a>(id: &str, conns: &'a [NetworkConnection]) -> Option<&'a NetworkConnection> { - conns - .iter() - .find(|c| c.id == id || c.interface == Some(id.to_string())) -} - -fn default_connection(id: &str) -> NetworkConnection { - NetworkConnection { - id: id.to_string(), - interface: Some(id.to_string()), - ..Default::default() - } -} - impl NetworkState { /// Returns a NetworkState struct with the given devices and connections. /// @@ -368,57 +279,6 @@ mod tests { use crate::error::NetworkStateError; use uuid::Uuid; - #[test] - fn test_macaddress() { - let mut val: Option = None; - assert!(matches!( - MacAddress::try_from(&val).unwrap(), - MacAddress::Unset - )); - - val = Some(String::from("")); - assert!(matches!( - MacAddress::try_from(&val).unwrap(), - MacAddress::Unset - )); - - val = Some(String::from("preserve")); - assert!(matches!( - MacAddress::try_from(&val).unwrap(), - MacAddress::Preserve - )); - - val = Some(String::from("permanent")); - assert!(matches!( - MacAddress::try_from(&val).unwrap(), - MacAddress::Permanent - )); - - val = Some(String::from("random")); - assert!(matches!( - MacAddress::try_from(&val).unwrap(), - MacAddress::Random - )); - - val = Some(String::from("stable")); - assert!(matches!( - MacAddress::try_from(&val).unwrap(), - MacAddress::Stable - )); - - val = Some(String::from("This is not a MACAddr")); - assert!(matches!( - MacAddress::try_from(&val), - Err(InvalidMacAddress(_)) - )); - - val = Some(String::from("de:ad:be:ef:2b:ad")); - assert_eq!( - MacAddress::try_from(&val).unwrap().to_string(), - String::from("de:ad:be:ef:2b:ad").to_uppercase() - ); - } - #[test] fn test_add_connection() { let mut state = NetworkState::default(); diff --git a/rust/agama-network/src/nm/dbus.rs b/rust/agama-network/src/nm/dbus.rs index 83741ce50c..be602d7125 100644 --- a/rust/agama-network/src/nm/dbus.rs +++ b/rust/agama-network/src/nm/dbus.rs @@ -1573,7 +1573,6 @@ mod test { connection_from_dbus, connection_to_dbus, merge_dbus_connections, NestedHash, OwnedNestedHash, }; - use crate::types::{BondMode, SSID}; use crate::{ model::*, nm::{ @@ -1583,6 +1582,7 @@ mod test { }, error::NmError, }, + types::*, }; use cidr::IpInet; use macaddr::MacAddr6; diff --git a/rust/agama-server/src/network.rs b/rust/agama-server/src/network.rs deleted file mode 100644 index 95e80f2639..0000000000 --- a/rust/agama-server/src/network.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) [2024] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -pub mod web; - -pub use agama_lib::network::{ - model::NetworkState, Action, Adapter, NetworkAdapterError, NetworkManagerAdapter, NetworkSystem, -}; diff --git a/rust/agama-server/src/network/web.rs b/rust/agama-server/src/network/web.rs deleted file mode 100644 index 004f75d231..0000000000 --- a/rust/agama-server/src/network/web.rs +++ /dev/null @@ -1,490 +0,0 @@ -// Copyright (c) [2024] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -//! This module implements the web API for the network module. - -use crate::error::Error; -use anyhow::Context; -use axum::{ - extract::{Path, State}, - http::StatusCode, - response::{IntoResponse, Response}, - routing::{delete, get, post}, - Json, Router, -}; -use uuid::Uuid; - -use agama_lib::{ - error::ServiceError, - event, http, - network::{ - error::NetworkStateError, - model::{AccessPoint, Connection, Device, GeneralState}, - settings::NetworkConnection, - types::NetworkConnectionWithState, - Adapter, NetworkSystem, NetworkSystemClient, NetworkSystemError, - }, -}; - -use serde::Deserialize; -use serde_json::json; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum NetworkError { - #[error("Unknown connection id: {0}")] - UnknownConnection(String), - #[error("Cannot translate: {0}")] - CannotTranslate(#[from] Error), - #[error("Cannot add new connection: {0}")] - CannotAddConnection(String), - #[error("Cannot update configuration: {0}")] - CannotUpdate(String), - #[error("Cannot apply configuration")] - CannotApplyConfig, - // TODO: to be removed after adapting to the NetworkSystemServer API - #[error("Network state error: {0}")] - Error(#[from] NetworkStateError), - #[error("Network system error: {0}")] - SystemError(#[from] NetworkSystemError), -} - -impl IntoResponse for NetworkError { - fn into_response(self) -> Response { - let body = json!({ - "error": self.to_string() - }); - (StatusCode::BAD_REQUEST, Json(body)).into_response() - } -} - -#[derive(Clone)] -struct NetworkServiceState { - network: NetworkSystemClient, -} - -/// Sets up and returns the axum service for the network module. -/// * `adapter`: networking configuration adapter. -/// * `events`: sending-half of the broadcast channel. -pub async fn network_service( - adapter: T, - events: http::event::OldSender, -) -> Result { - let network = NetworkSystem::new(adapter); - // FIXME: we are somehow abusing ServiceError. The HTTP/JSON API should have its own - // error type. - let client = network - .start() - .await - .context("Could not start the network configuration service.")?; - - let mut changes = client.subscribe(); - tokio::spawn(async move { - loop { - match changes.recv().await { - Ok(message) => { - let change = event!(NetworkChange { change: message }); - if let Err(e) = events.send(change) { - eprintln!("Could not send the event: {}", e); - } - } - Err(e) => { - eprintln!("Could not send the event: {}", e); - } - } - } - }); - - let state = NetworkServiceState { network: client }; - - Ok(Router::new() - .route("/state", get(general_state).put(update_general_state)) - .route("/connections", get(connections).post(add_connection)) - .route( - "/connections/:id", - delete(delete_connection) - .put(update_connection) - .get(connection), - ) - .route("/connections/:id/connect", post(connect)) - .route("/connections/:id/disconnect", post(disconnect)) - .route("/connections/persist", post(persist)) - .route("/devices", get(devices)) - .route("/system/apply", post(apply)) - .route("/wifi", get(wifi_networks)) - .with_state(state)) -} - -#[utoipa::path( - get, - path = "/state", - context_path = "/api/network", - responses( - (status = 200, description = "Get general network config", body = GeneralState) - ) -)] -async fn general_state( - State(state): State, -) -> Result, NetworkError> { - let general_state = state.network.get_state().await?; - Ok(Json(general_state)) -} - -#[utoipa::path( - put, - path = "/state", - context_path = "/api/network", - responses( - (status = 200, description = "Update general network config", body = GeneralState) - ) -)] -async fn update_general_state( - State(state): State, - Json(value): Json, -) -> Result, NetworkError> { - state.network.update_state(value)?; - let state = state.network.get_state().await?; - Ok(Json(state)) -} - -#[utoipa::path( - get, - path = "/wifi", - context_path = "/api/network", - responses( - (status = 200, description = "List of wireless networks", body = Vec) - ) -)] -async fn wifi_networks( - State(state): State, -) -> Result>, NetworkError> { - state.network.wifi_scan().await?; - let access_points = state.network.get_access_points().await?; - - let mut networks = vec![]; - for ap in access_points { - if !ap.ssid.to_string().is_empty() { - networks.push(ap); - } - } - - Ok(Json(networks)) -} - -#[utoipa::path( - get, - path = "/devices", - context_path = "/api/network", - responses( - (status = 200, description = "List of devices", body = Vec) - ) -)] -async fn devices( - State(state): State, -) -> Result>, NetworkError> { - Ok(Json(state.network.get_devices().await?)) -} - -#[utoipa::path( - get, - path = "/connections", - context_path = "/api/network", - responses( - (status = 200, description = "List of known connections", body = Vec) - ) -)] -async fn connections( - State(state): State, -) -> Result>, NetworkError> { - let connections = state.network.get_connections().await?; - - let network_connections = connections - .iter() - .filter(|c| c.controller.is_none()) - .map(|c| { - let state = c.state; - let mut conn = NetworkConnection::try_from(c.clone()).unwrap(); - if let Some(ref mut bond) = conn.bond { - bond.ports = ports_for(connections.to_owned(), c.uuid); - } - if let Some(ref mut bridge) = conn.bridge { - bridge.ports = ports_for(connections.to_owned(), c.uuid); - }; - NetworkConnectionWithState { - connection: conn, - state, - } - }) - .collect(); - - Ok(Json(network_connections)) -} - -fn ports_for(connections: Vec, uuid: Uuid) -> Vec { - return connections - .iter() - .filter(|c| c.controller == Some(uuid)) - .map(|c| { - if let Some(interface) = c.interface.to_owned() { - interface - } else { - c.clone().id - } - }) - .collect(); -} - -#[utoipa::path( - post, - path = "/connections", - context_path = "/api/network", - responses( - (status = 200, description = "Add a new connection", body = Connection) - ) -)] -async fn add_connection( - State(state): State, - Json(net_conn): Json, -) -> Result, NetworkError> { - let bond = net_conn.bond.clone(); - let bridge = net_conn.bridge.clone(); - let conn = Connection::try_from(net_conn)?; - let id = conn.id.clone(); - - state.network.add_connection(conn.clone()).await?; - - match state.network.get_connection(&id).await? { - None => Err(NetworkError::CannotAddConnection(id.clone())), - Some(conn) => { - if let Some(bond) = bond { - state.network.set_ports(conn.uuid, bond.ports).await?; - } - if let Some(bridge) = bridge { - state.network.set_ports(conn.uuid, bridge.ports).await?; - } - Ok(Json(conn)) - } - } -} - -#[utoipa::path( - get, - path = "/connections/:id", - context_path = "/api/network", - responses( - (status = 200, description = "Get connection given by its ID", body = NetworkConnection) - ) -)] -async fn connection( - State(state): State, - Path(id): Path, -) -> Result, NetworkError> { - let conn = state - .network - .get_connection(&id) - .await? - .ok_or_else(|| NetworkError::UnknownConnection(id.clone()))?; - - let conn = NetworkConnection::try_from(conn)?; - - Ok(Json(conn)) -} - -#[utoipa::path( - delete, - path = "/connections/:id", - context_path = "/api/network", - responses( - (status = 200, description = "Delete connection", body = Connection) - ) -)] -async fn delete_connection( - State(state): State, - Path(id): Path, -) -> impl IntoResponse { - if state.network.remove_connection(&id).await.is_ok() { - StatusCode::NO_CONTENT - } else { - StatusCode::NOT_FOUND - } -} - -#[utoipa::path( - put, - path = "/connections/:id", - context_path = "/api/network", - responses( - (status = 204, description = "Update connection", body = Connection) - ) -)] -async fn update_connection( - State(state): State, - Path(id): Path, - Json(conn): Json, -) -> Result { - let orig_conn = state - .network - .get_connection(&id) - .await? - .ok_or_else(|| NetworkError::UnknownConnection(id.clone()))?; - let bond = conn.bond.clone(); - let bridge = conn.bridge.clone(); - - let mut conn = Connection::try_from(conn)?; - conn.uuid = orig_conn.uuid; - - state.network.update_connection(conn.clone()).await?; - - if let Some(bond) = bond { - state.network.set_ports(conn.uuid, bond.ports).await?; - } - if let Some(bridge) = bridge { - state.network.set_ports(conn.uuid, bridge.ports).await?; - } - - Ok(StatusCode::NO_CONTENT) -} - -#[utoipa::path( - post, - path = "/connections/:id/connect", - context_path = "/api/network", - responses( - (status = 204, description = "Connect to the given connection", body = String) - ) -)] -async fn connect( - State(state): State, - Path(id): Path, -) -> Result { - let Some(mut conn) = state.network.get_connection(&id).await? else { - return Err(NetworkError::UnknownConnection(id)); - }; - conn.set_up(); - - state - .network - .update_connection(conn) - .await - .map_err(|_| NetworkError::CannotApplyConfig)?; - - state - .network - .apply() - .await - .map_err(|_| NetworkError::CannotApplyConfig)?; - - Ok(StatusCode::NO_CONTENT) -} - -#[utoipa::path( - post, - path = "/connections/:id/disconnect", - context_path = "/api/network", - responses( - (status = 204, description = "Connect to the given connection", body = String) - ) -)] -async fn disconnect( - State(state): State, - Path(id): Path, -) -> Result { - let Some(mut conn) = state.network.get_connection(&id).await? else { - return Err(NetworkError::UnknownConnection(id)); - }; - conn.set_down(); - - state - .network - .update_connection(conn) - .await - .map_err(|_| NetworkError::CannotApplyConfig)?; - - state - .network - .apply() - .await - .map_err(|_| NetworkError::CannotApplyConfig)?; - - Ok(StatusCode::NO_CONTENT) -} - -#[derive(Deserialize, utoipa::ToSchema)] -pub struct PersistParams { - pub only: Option>, - pub value: bool, -} - -#[utoipa::path( - post, - path = "/connections/persist", - context_path = "/api/network", - responses( - (status = 204, description = "Persist the given connection to disk", body = PersistParams) - ) -)] -async fn persist( - State(state): State, - Json(persist): Json, -) -> Result { - let mut connections = state.network.get_connections().await?; - let ids = persist.only.unwrap_or(vec![]); - - for conn in connections.iter_mut() { - if ids.is_empty() || ids.contains(&conn.id) { - conn.persistent = persist.value; - conn.keep_status(); - - state - .network - .update_connection(conn.to_owned()) - .await - .map_err(|_| NetworkError::CannotApplyConfig)?; - } - } - - state - .network - .apply() - .await - .map_err(|_| NetworkError::CannotApplyConfig)?; - - Ok(StatusCode::NO_CONTENT) -} - -#[utoipa::path( - post, - path = "/system/apply", - context_path = "/api/network", - responses( - (status = 204, description = "Apply configuration") - ) -)] -async fn apply( - State(state): State, -) -> Result { - state - .network - .apply() - .await - .map_err(|_| NetworkError::CannotApplyConfig)?; - - Ok(StatusCode::NO_CONTENT) -} diff --git a/rust/agama-server/tests/network_service.rs b/rust/agama-server/tests/network_service.rs deleted file mode 100644 index f1714d4e8e..0000000000 --- a/rust/agama-server/tests/network_service.rs +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright (c) [2024] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -pub mod common; - -use agama_lib::error::ServiceError; -use agama_lib::network::settings::{BondSettings, BridgeSettings, NetworkConnection}; -use agama_lib::network::types::{DeviceType, SSID}; -use agama_lib::network::{ - model::{self, AccessPoint, GeneralState, NetworkState, StateConfig}, - Adapter, NetworkAdapterError, -}; -use agama_server::network::web::network_service; - -use async_trait::async_trait; -use axum::http::header; -use axum::{ - body::Body, - http::{Method, Request, StatusCode}, - Router, -}; -use common::body_to_string; -use serde_json::to_string; -use std::error::Error; -use tokio::{sync::broadcast, test}; -use tower::ServiceExt; - -async fn build_state() -> NetworkState { - let general_state = GeneralState::default(); - let device = model::Device { - name: String::from("eth0"), - type_: DeviceType::Ethernet, - ..Default::default() - }; - let eth0 = model::Connection::new("eth0".to_string(), DeviceType::Ethernet); - - NetworkState::new(general_state, vec![], vec![device], vec![eth0]) -} - -async fn build_service(state: NetworkState) -> Result { - let adapter = NetworkTestAdapter(state); - let (tx, _rx) = broadcast::channel(16); - network_service(adapter, tx).await -} - -#[derive(Default)] -pub struct NetworkTestAdapter(NetworkState); - -#[async_trait] -impl Adapter for NetworkTestAdapter { - async fn read(&self, _: StateConfig) -> Result { - Ok(self.0.clone()) - } - - async fn write(&self, _network: &NetworkState) -> Result<(), NetworkAdapterError> { - unimplemented!("Not used in tests"); - } -} - -#[test] -async fn test_network_state() -> Result<(), Box> { - let state = build_state().await; - let network_service = build_service(state).await?; - - let request = Request::builder() - .uri("/state") - .method(Method::GET) - .body(Body::empty()) - .unwrap(); - - let response = network_service.oneshot(request).await?; - assert_eq!(response.status(), StatusCode::OK); - let body = body_to_string(response.into_body()).await; - assert!(body.contains(r#""wirelessEnabled":false"#)); - Ok(()) -} - -#[test] -async fn test_change_network_state() -> Result<(), Box> { - let mut state = build_state().await; - let network_service = build_service(state.clone()).await?; - state.general_state.wireless_enabled = true; - - let request = Request::builder() - .uri("/state") - .method(Method::PUT) - .header(header::CONTENT_TYPE, "application/json") - .body(to_string(&state.general_state)?) - .unwrap(); - - let response = network_service.oneshot(request).await?; - assert_eq!(response.status(), StatusCode::OK); - let body = response.into_body(); - let body = body_to_string(body).await; - assert_eq!(body, to_string(&state.general_state)?); - Ok(()) -} - -#[test] -async fn test_network_connections() -> Result<(), Box> { - let state = build_state().await; - let network_service = build_service(state.clone()).await?; - - let request = Request::builder() - .uri("/connections") - .method(Method::GET) - .body(Body::empty()) - .unwrap(); - - let response = network_service.oneshot(request).await?; - assert_eq!(response.status(), StatusCode::OK); - let body = body_to_string(response.into_body()).await; - assert!(body.contains(r#""id":"eth0""#)); - Ok(()) -} - -#[test] -async fn test_network_devices() -> Result<(), Box> { - let state = build_state().await; - let network_service = build_service(state.clone()).await?; - - let request = Request::builder() - .uri("/devices") - .method(Method::GET) - .body(Body::empty()) - .unwrap(); - - let response = network_service.oneshot(request).await?; - assert_eq!(response.status(), StatusCode::OK); - let body = body_to_string(response.into_body()).await; - assert!(body.contains(r#""name":"eth0""#)); - Ok(()) -} - -#[test] -async fn test_network_wifis() -> Result<(), Box> { - let mut state = build_state().await; - state.access_points = vec![ - AccessPoint { - ssid: SSID("AgamaNetwork".as_bytes().into()), - hw_address: "00:11:22:33:44:00".into(), - ..Default::default() - }, - AccessPoint { - ssid: SSID("AgamaNetwork2".as_bytes().into()), - hw_address: "00:11:22:33:44:01".into(), - ..Default::default() - }, - ]; - let network_service = build_service(state.clone()).await?; - - let request = Request::builder() - .uri("/wifi") - .method(Method::GET) - .body(Body::empty()) - .unwrap(); - - let response = network_service.oneshot(request).await?; - assert_eq!(response.status(), StatusCode::OK); - let body = body_to_string(response.into_body()).await; - assert!(body.contains(r#""ssid":"AgamaNetwork""#)); - assert!(body.contains(r#""ssid":"AgamaNetwork2""#)); - Ok(()) -} - -#[test] -async fn test_add_bond_connection() -> Result<(), Box> { - let state = build_state().await; - let network_service = build_service(state.clone()).await?; - - let eth2 = NetworkConnection { - id: "eth2".to_string(), - ..Default::default() - }; - - let bond0 = NetworkConnection { - id: "bond0".to_string(), - method4: Some("auto".to_string()), - method6: Some("disabled".to_string()), - interface: Some("bond0".to_string()), - bond: Some(BondSettings { - mode: "active-backup".to_string(), - ports: vec!["eth0".to_string()], - options: Some("primary=eth0".to_string()), - }), - ..Default::default() - }; - - let request = Request::builder() - .uri("/connections") - .header("Content-Type", "application/json") - .method(Method::POST) - .body(serde_json::to_string(ð2)?) - .unwrap(); - - let response = network_service.clone().oneshot(request).await?; - assert_eq!(response.status(), StatusCode::OK); - - let request = Request::builder() - .uri("/connections") - .header("Content-Type", "application/json") - .method(Method::POST) - .body(serde_json::to_string(&bond0)?) - .unwrap(); - - let response = network_service.clone().oneshot(request).await?; - assert_eq!(response.status(), StatusCode::OK); - - let request = Request::builder() - .uri("/connections") - .method(Method::GET) - .body(Body::empty()) - .unwrap(); - - let response = network_service.clone().oneshot(request).await?; - assert_eq!(response.status(), StatusCode::OK); - let body = body_to_string(response.into_body()).await; - assert!(body.contains(r#""id":"bond0""#)); - assert!(body.contains(r#""mode":"active-backup""#)); - assert!(body.contains(r#""primary=eth0""#)); - assert!(body.contains(r#""ports":["eth0"]"#)); - - Ok(()) -} - -#[test] -async fn test_add_bridge_connection() -> Result<(), Box> { - let state = build_state().await; - let network_service = build_service(state.clone()).await?; - - let br0 = NetworkConnection { - id: "br0".to_string(), - method4: Some("manual".to_string()), - method6: Some("disabled".to_string()), - interface: Some("br0".to_string()), - bridge: Some(BridgeSettings { - ports: vec!["eth0".to_string()], - stp: Some(false), - ..Default::default() - }), - ..Default::default() - }; - - let request = Request::builder() - .uri("/connections") - .header("Content-Type", "application/json") - .method(Method::POST) - .body(serde_json::to_string(&br0)?) - .unwrap(); - - let response = network_service.clone().oneshot(request).await?; - assert_eq!(response.status(), StatusCode::OK); - - let request = Request::builder() - .uri("/connections") - .method(Method::GET) - .body(Body::empty()) - .unwrap(); - - let response = network_service.clone().oneshot(request).await?; - assert_eq!(response.status(), StatusCode::OK); - let body = body_to_string(response.into_body()).await; - assert!(body.contains(r#""id":"br0""#)); - assert!(body.contains(r#""ports":["eth0"]"#)); - assert!(body.contains(r#""stp":false"#)); - - Ok(()) -} diff --git a/rust/agama-utils/src/api/network/types.rs b/rust/agama-utils/src/api/network/types.rs index 7ad6a7b812..adb15ebd02 100644 --- a/rust/agama-utils/src/api/network/types.rs +++ b/rust/agama-utils/src/api/network/types.rs @@ -732,4 +732,55 @@ mod tests { let mode = BondMode::try_from(1).unwrap(); assert_eq!(format!("{}", mode), "active-backup"); } + + #[test] + fn test_macaddress() { + let mut val: Option = None; + assert!(matches!( + MacAddress::try_from(&val).unwrap(), + MacAddress::Unset + )); + + val = Some(String::from("")); + assert!(matches!( + MacAddress::try_from(&val).unwrap(), + MacAddress::Unset + )); + + val = Some(String::from("preserve")); + assert!(matches!( + MacAddress::try_from(&val).unwrap(), + MacAddress::Preserve + )); + + val = Some(String::from("permanent")); + assert!(matches!( + MacAddress::try_from(&val).unwrap(), + MacAddress::Permanent + )); + + val = Some(String::from("random")); + assert!(matches!( + MacAddress::try_from(&val).unwrap(), + MacAddress::Random + )); + + val = Some(String::from("stable")); + assert!(matches!( + MacAddress::try_from(&val).unwrap(), + MacAddress::Stable + )); + + val = Some(String::from("This is not a MACAddr")); + assert!(matches!( + MacAddress::try_from(&val), + Err(InvalidMacAddress(_)) + )); + + val = Some(String::from("de:ad:be:ef:2b:ad")); + assert_eq!( + MacAddress::try_from(&val).unwrap().to_string(), + String::from("de:ad:be:ef:2b:ad").to_uppercase() + ); + } } From 8ddd7367466be2c08e9b71f055033b77aee1cc37 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Tue, 4 Nov 2025 09:00:51 +0000 Subject: [PATCH 06/17] Adapted manager network setup to the new HTTP API --- service/lib/agama/http/clients/network.rb | 9 +++++++-- service/lib/agama/network.rb | 1 - 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/service/lib/agama/http/clients/network.rb b/service/lib/agama/http/clients/network.rb index c67c7a9d61..584d755655 100644 --- a/service/lib/agama/http/clients/network.rb +++ b/service/lib/agama/http/clients/network.rb @@ -39,11 +39,16 @@ def devices end def persist_connections - post("network/connections/persist", { value: true }) + conns = connections.map do |c| + c["persistent"] = true + c + end + + put("config", { "network" => { "connections" => conns, "generalState" => state } }) end def state - proposal.fetch("network", {}).fetch("state", {}) + proposal.fetch("network", {}).fetch("generalState", {}) end end end diff --git a/service/lib/agama/network.rb b/service/lib/agama/network.rb index efb4b2fb16..bd91caff30 100644 --- a/service/lib/agama/network.rb +++ b/service/lib/agama/network.rb @@ -153,7 +153,6 @@ def persist_connections end def copy_connections? - false http_client.state["copyNetwork"] end From 87f91ea5891f3760db419b4244220f52a0a71eb8 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Tue, 4 Nov 2025 16:04:31 +0000 Subject: [PATCH 07/17] Use select with the useQuery for specific network queries --- web/src/queries/proposal.ts | 12 ++++++++---- web/src/queries/system.ts | 8 ++++++-- web/src/types/network.ts | 1 + web/src/types/system.ts | 4 ++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/web/src/queries/proposal.ts b/web/src/queries/proposal.ts index 1b8b2cc49e..80b5251dec 100644 --- a/web/src/queries/proposal.ts +++ b/web/src/queries/proposal.ts @@ -37,14 +37,18 @@ const proposalQuery = () => { }; const useNetworkProposal = () => { - const { data: config } = useSuspenseQuery(proposalQuery()); + const { data } = useSuspenseQuery({ + ...proposalQuery(), + select: (d) => NetworkProposal.fromApi(d.network), + }); - return NetworkProposal.fromApi(config.network); + return data; }; const useProposal = () => { - const { data: config } = useSuspenseQuery(proposalQuery()); - return config; + const { data } = useSuspenseQuery(proposalQuery()); + + return data; }; const useProposalChanges = () => { diff --git a/web/src/queries/system.ts b/web/src/queries/system.ts index de65cea66f..670ead4033 100644 --- a/web/src/queries/system.ts +++ b/web/src/queries/system.ts @@ -26,6 +26,7 @@ import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; import { fetchSystem } from "~/api/api"; import { NetworkSystem } from "~/types/network"; +import { System } from "~/types/system"; const transformLocales = (locales) => locales.map(({ id, language: name, territory }) => ({ id, name, territory })); @@ -78,9 +79,12 @@ const useSystem = () => { }; const useNetworkSystem = () => { - const { data: config } = useSuspenseQuery(systemQuery()); + const { data } = useSuspenseQuery({ + ...systemQuery(), + select: (d) => NetworkSystem.fromApi(d.network), + }); - return NetworkSystem.fromApi(config.network); + return data; }; const useNetworkDevices = () => { diff --git a/web/src/types/network.ts b/web/src/types/network.ts index 7dbed178c8..8c45259007 100644 --- a/web/src/types/network.ts +++ b/web/src/types/network.ts @@ -457,6 +457,7 @@ export type { APIDevice, IPAddress, APINetworkProposal, + APINetworkSystem, NetworkGeneralState, Route, APIRoute, diff --git a/web/src/types/system.ts b/web/src/types/system.ts index c8f5c78df4..2061cea3ef 100644 --- a/web/src/types/system.ts +++ b/web/src/types/system.ts @@ -21,11 +21,11 @@ */ import { Localization } from "./l10n"; -import { NetworkSystem } from "./network"; +import { APINetworkSystem } from "./network"; type System = { l10n?: Localization; - network?: NetworkSystem; + network?: APINetworkSystem; }; export type { System }; From 7a4ec261c21d714139449c34a9157e529d0f913b Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Wed, 5 Nov 2025 09:48:43 +0000 Subject: [PATCH 08/17] Expose access_points through the new HTTP network API --- rust/agama-network/src/action.rs | 6 ++- rust/agama-network/src/model.rs | 15 +----- rust/agama-network/src/nm/client.rs | 12 ++--- rust/agama-network/src/nm/dbus.rs | 24 +++++----- rust/agama-network/src/nm/watcher.rs | 4 +- rust/agama-network/src/system.rs | 4 +- rust/agama-server/src/web/docs/config.rs | 46 +++++++++---------- .../src/api/network/system_info.rs | 4 +- rust/agama-utils/src/api/network/types.rs | 15 ++++++ web/src/types/network.ts | 23 ++++++++-- 10 files changed, 87 insertions(+), 66 deletions(-) diff --git a/rust/agama-network/src/action.rs b/rust/agama-network/src/action.rs index ffc2fd3cc0..b13c9f888f 100644 --- a/rust/agama-network/src/action.rs +++ b/rust/agama-network/src/action.rs @@ -18,8 +18,10 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::model::{AccessPoint, Connection}; -use crate::types::{ConnectionState, Device, DeviceType, GeneralState, Proposal, SystemInfo}; +use crate::model::Connection; +use crate::types::{ + AccessPoint, ConnectionState, Device, DeviceType, GeneralState, Proposal, SystemInfo, +}; use agama_utils::api::network::Config; use tokio::sync::oneshot; use uuid::Uuid; diff --git a/rust/agama-network/src/model.rs b/rust/agama-network/src/model.rs index 37c9537726..1e097adf43 100644 --- a/rust/agama-network/src/model.rs +++ b/rust/agama-network/src/model.rs @@ -428,20 +428,6 @@ mod tests { pub const NOT_COPY_NETWORK_PATH: &str = "/run/agama/not_copy_network"; -/// Access Point -#[serde_as] -#[derive(Default, Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct AccessPoint { - #[serde_as(as = "DisplayFromStr")] - pub ssid: SSID, - pub hw_address: String, - pub strength: u8, - pub flags: u32, - pub rsn_flags: u32, - pub wpa_flags: u32, -} - /// Represents a known network connection. #[serde_as] #[skip_serializing_none] @@ -1407,6 +1393,7 @@ impl TryFrom for SystemInfo { ConnectionCollection(state.connections).try_into()?; Ok(SystemInfo { + access_points: state.access_points, connections, devices: state.devices, general_state: state.general_state, diff --git a/rust/agama-network/src/nm/client.rs b/rust/agama-network/src/nm/client.rs index e697b05504..b90a81ae70 100644 --- a/rust/agama-network/src/nm/client.rs +++ b/rust/agama-network/src/nm/client.rs @@ -34,11 +34,9 @@ use super::proxies::{ AccessPointProxy, ActiveConnectionProxy, ConnectionProxy, DeviceProxy, NetworkManagerProxy, SettingsProxy, WirelessProxy, }; -use crate::model::{ - AccessPoint, Connection, ConnectionConfig, SecurityProtocol, NOT_COPY_NETWORK_PATH, -}; +use crate::model::{Connection, ConnectionConfig, SecurityProtocol, NOT_COPY_NETWORK_PATH}; use crate::types::{ - AddFlags, ConnectionFlags, Device, DeviceType, GeneralState, UpdateFlags, SSID, + AccessPoint, AddFlags, ConnectionFlags, Device, DeviceType, GeneralState, UpdateFlags, SSID, }; use agama_utils::dbus::get_optional_property; use semver::Version; @@ -160,6 +158,7 @@ impl<'a> NetworkManagerClient<'a> { .build() .await?; + let device = proxy.interface().await?; let ssid = SSID(wproxy.ssid().await?); let hw_address = wproxy.hw_address().await?; let strength = wproxy.strength().await?; @@ -168,6 +167,7 @@ impl<'a> NetworkManagerClient<'a> { let wpa_flags = wproxy.wpa_flags().await?; points.push(AccessPoint { + device, ssid, hw_address, strength, @@ -440,7 +440,7 @@ impl<'a> NetworkManagerClient<'a> { Ok(()) } - async fn get_connection_proxy(&self, uuid: Uuid) -> Result { + async fn get_connection_proxy(&self, uuid: Uuid) -> Result, NmError> { let proxy = SettingsProxy::new(&self.connection).await?; let uuid_s = uuid.to_string(); let path = proxy.get_connection_by_uuid(uuid_s.as_str()).await?; @@ -454,7 +454,7 @@ impl<'a> NetworkManagerClient<'a> { // Returns the DeviceProxy for the given device name // /// * `name`: Device name. - async fn get_device_proxy(&self, name: String) -> Result { + async fn get_device_proxy(&self, name: String) -> Result, NmError> { let mut device_path: Option = None; for path in &self.nm_proxy.get_all_devices().await? { let proxy = DeviceProxy::builder(&self.connection) diff --git a/rust/agama-network/src/nm/dbus.rs b/rust/agama-network/src/nm/dbus.rs index be602d7125..13dd8c66c3 100644 --- a/rust/agama-network/src/nm/dbus.rs +++ b/rust/agama-network/src/nm/dbus.rs @@ -693,13 +693,13 @@ fn wireless_config_to_dbus(config: &'_ WirelessConfig) -> NestedHash<'_> { NestedHash::from([(WIRELESS_KEY, wireless), (WIRELESS_SECURITY_KEY, security)]) } -fn bond_config_to_dbus(config: &BondConfig) -> HashMap<&str, zvariant::Value> { +fn bond_config_to_dbus(config: &BondConfig) -> HashMap<&str, zvariant::Value<'_>> { let mut options = config.options.0.clone(); options.insert("mode".to_string(), config.mode.to_string()); HashMap::from([("options", Value::new(options))]) } -fn bridge_config_to_dbus(bridge: &BridgeConfig) -> HashMap<&str, zvariant::Value> { +fn bridge_config_to_dbus(bridge: &BridgeConfig) -> HashMap<&str, zvariant::Value<'_>> { let mut hash = HashMap::new(); if let Some(stp) = bridge.stp { @@ -739,7 +739,9 @@ fn bridge_config_from_dbus(conn: &OwnedNestedHash) -> Result HashMap<&str, zvariant::Value> { +fn bridge_port_config_to_dbus( + bridge_port: &BridgePortConfig, +) -> HashMap<&str, zvariant::Value<'_>> { let mut hash = HashMap::new(); if let Some(prio) = bridge_port.priority { @@ -765,7 +767,7 @@ fn bridge_port_config_from_dbus( })) } -fn infiniband_config_to_dbus(config: &InfinibandConfig) -> HashMap<&str, zvariant::Value> { +fn infiniband_config_to_dbus(config: &InfinibandConfig) -> HashMap<&str, zvariant::Value<'_>> { let mut infiniband_config: HashMap<&str, zvariant::Value> = HashMap::from([ ( "transport-mode", @@ -801,7 +803,7 @@ fn infiniband_config_from_dbus( Ok(Some(config)) } -fn tun_config_to_dbus(config: &TunConfig) -> HashMap<&str, zvariant::Value> { +fn tun_config_to_dbus(config: &TunConfig) -> HashMap<&str, zvariant::Value<'_>> { let mut tun_config: HashMap<&str, zvariant::Value> = HashMap::from([("mode", Value::new(config.mode.clone() as u32))]); @@ -833,7 +835,7 @@ fn tun_config_from_dbus(conn: &OwnedNestedHash) -> Result, NmE })) } -fn ovs_bridge_config_to_dbus(br: &OvsBridgeConfig) -> HashMap<&str, zvariant::Value> { +fn ovs_bridge_config_to_dbus(br: &OvsBridgeConfig) -> HashMap<&str, zvariant::Value<'_>> { let mut br_config: HashMap<&str, zvariant::Value> = HashMap::new(); if let Some(mcast_snooping) = br.mcast_snooping_enable { @@ -863,7 +865,7 @@ fn ovs_bridge_from_dbus(conn: &OwnedNestedHash) -> Result HashMap<&str, zvariant::Value> { +fn ovs_port_config_to_dbus(config: &OvsPortConfig) -> HashMap<&str, zvariant::Value<'_>> { let mut port_config: HashMap<&str, zvariant::Value> = HashMap::new(); if let Some(tag) = &config.tag { @@ -883,7 +885,7 @@ fn ovs_port_from_dbus(conn: &OwnedNestedHash) -> Result, N })) } -fn ovs_interface_config_to_dbus(config: &OvsInterfaceConfig) -> HashMap<&str, zvariant::Value> { +fn ovs_interface_config_to_dbus(config: &OvsInterfaceConfig) -> HashMap<&str, zvariant::Value<'_>> { let mut ifc_config: HashMap<&str, zvariant::Value> = HashMap::new(); ifc_config.insert("type", config.interface_type.to_string().clone().into()); @@ -905,7 +907,7 @@ fn ovs_interface_from_dbus(conn: &OwnedNestedHash) -> Result HashMap<&str, zvariant::Value> { +fn match_config_to_dbus(match_config: &MatchConfig) -> HashMap<&str, zvariant::Value<'_>> { let drivers: Value = match_config.driver.to_vec().into(); let kernels: Value = match_config.kernel.to_vec().into(); @@ -1374,7 +1376,7 @@ fn bond_config_from_dbus(conn: &OwnedNestedHash) -> Result, N Ok(Some(bond)) } -fn vlan_config_to_dbus(cfg: &VlanConfig) -> NestedHash { +fn vlan_config_to_dbus(cfg: &VlanConfig) -> NestedHash<'_> { let vlan: HashMap<&str, zvariant::Value> = HashMap::from([ ("id", cfg.id.into()), ("parent", cfg.parent.clone().into()), @@ -1401,7 +1403,7 @@ fn vlan_config_from_dbus(conn: &OwnedNestedHash) -> Result, N })) } -fn ieee_8021x_config_to_dbus(config: &IEEE8021XConfig) -> HashMap<&str, zvariant::Value> { +fn ieee_8021x_config_to_dbus(config: &IEEE8021XConfig) -> HashMap<&str, zvariant::Value<'_>> { let mut ieee_8021x_config: HashMap<&str, zvariant::Value> = HashMap::from([( "eap", config diff --git a/rust/agama-network/src/nm/watcher.rs b/rust/agama-network/src/nm/watcher.rs index c8af7e1dc7..141ca193e2 100644 --- a/rust/agama-network/src/nm/watcher.rs +++ b/rust/agama-network/src/nm/watcher.rs @@ -358,14 +358,14 @@ impl<'a> ProxiesRegistry<'a> { pub fn remove_active_connection( &mut self, path: &OwnedObjectPath, - ) -> Option { + ) -> Option> { self.active_connections.remove(path) } /// Removes a device from the registry. /// /// * `path`: D-Bus object path. - pub fn remove_device(&mut self, path: &OwnedObjectPath) -> Option<(String, DeviceProxy)> { + pub fn remove_device(&mut self, path: &OwnedObjectPath) -> Option<(String, DeviceProxy<'_>)> { self.devices.remove(path) } diff --git a/rust/agama-network/src/system.rs b/rust/agama-network/src/system.rs index 6fe68f04ca..38cfecbd27 100644 --- a/rust/agama-network/src/system.rs +++ b/rust/agama-network/src/system.rs @@ -21,8 +21,8 @@ use crate::{ action::Action, error::NetworkStateError, - model::{AccessPoint, Connection, NetworkChange, NetworkState, StateConfig}, - types::{Config, Device, DeviceType, GeneralState, Proposal, SystemInfo}, + model::{Connection, NetworkChange, NetworkState, StateConfig}, + types::{AccessPoint, Config, Device, DeviceType, GeneralState, Proposal, SystemInfo}, Adapter, NetworkAdapterError, }; use std::error::Error; diff --git a/rust/agama-server/src/web/docs/config.rs b/rust/agama-server/src/web/docs/config.rs index 661083a1a2..d753092a0c 100644 --- a/rust/agama-server/src/web/docs/config.rs +++ b/rust/agama-server/src/web/docs/config.rs @@ -54,30 +54,17 @@ impl ApiDocBuilder for ConfigApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() - .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() .schema_from::() - .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() .schema_from::() .schema_from::() .schema_from::() @@ -99,16 +86,6 @@ impl ApiDocBuilder for ConfigApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() .schema_from::() .schema_from::() .schema_from::() @@ -172,6 +149,29 @@ impl ApiDocBuilder for ConfigApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() diff --git a/rust/agama-utils/src/api/network/system_info.rs b/rust/agama-utils/src/api/network/system_info.rs index 8e88dcc958..659b93e914 100644 --- a/rust/agama-utils/src/api/network/system_info.rs +++ b/rust/agama-utils/src/api/network/system_info.rs @@ -20,7 +20,7 @@ //! Representation of the network settings -use crate::api::network::{Device, GeneralState, NetworkConnectionsCollection}; +use crate::api::network::{AccessPoint, Device, GeneralState, NetworkConnectionsCollection}; use serde::{Deserialize, Serialize}; use std::default::Default; @@ -28,9 +28,9 @@ use std::default::Default; #[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct SystemInfo { + pub access_points: Vec, // networks or access_points shold be returned /// Connections to use in the installation pub connections: NetworkConnectionsCollection, pub devices: Vec, pub general_state: GeneralState, - // networks or access_points shold be returned } diff --git a/rust/agama-utils/src/api/network/types.rs b/rust/agama-utils/src/api/network/types.rs index adb15ebd02..12ee362eee 100644 --- a/rust/agama-utils/src/api/network/types.rs +++ b/rust/agama-utils/src/api/network/types.rs @@ -42,6 +42,21 @@ pub struct GeneralState { pub networking_enabled: bool, // pub network_state: NMSTATE } +/// Access Point +#[serde_as] +#[derive(Default, Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct AccessPoint { + pub device: String, + #[serde_as(as = "DisplayFromStr")] + pub ssid: SSID, + pub hw_address: String, + pub strength: u8, + pub flags: u32, + pub rsn_flags: u32, + pub wpa_flags: u32, +} + /// Network device #[serde_as] #[skip_serializing_none] diff --git a/web/src/types/network.ts b/web/src/types/network.ts index 8c45259007..1152b6a0b6 100644 --- a/web/src/types/network.ts +++ b/web/src/types/network.ts @@ -141,6 +141,7 @@ type Route = { }; type APIAccessPoint = { + device: string; ssid: string; strength: number; hwAddress: string; @@ -150,12 +151,20 @@ type APIAccessPoint = { }; class AccessPoint { + device: string; ssid: string; strength: number; hwAddress: string; security: SecurityProtocols[]; - constructor(ssid: string, strength: number, hwAddress: string, security: SecurityProtocols[]) { + constructor( + device: string, + ssid: string, + strength: number, + hwAddress: string, + security: SecurityProtocols[], + ) { + this.device = device; this.ssid = ssid; this.strength = strength; this.hwAddress = hwAddress; @@ -163,9 +172,15 @@ class AccessPoint { } static fromApi(options: APIAccessPoint) { - const { ssid, strength, hwAddress, flags, wpaFlags, rsnFlags } = options; - - return new AccessPoint(ssid, strength, hwAddress, securityFromFlags(flags, wpaFlags, rsnFlags)); + const { device, ssid, strength, hwAddress, flags, wpaFlags, rsnFlags } = options; + + return new AccessPoint( + device, + ssid, + strength, + hwAddress, + securityFromFlags(flags, wpaFlags, rsnFlags), + ); } } From 3c23b08904c5c7ab638665e0fc3f21e4ca7b5f5f Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Thu, 6 Nov 2025 10:33:03 +0000 Subject: [PATCH 09/17] Some fixes for networking config update --- rust/agama-manager/src/service.rs | 26 +++++++++++++++++++ rust/agama-network/src/model.rs | 7 ++++- web/src/api/api.ts | 6 +++-- web/src/api/network.ts | 17 ------------ .../network/BindingSettingsForm.tsx | 12 ++++++--- web/src/components/network/IpSettingsForm.tsx | 15 ++++++++--- .../components/network/NetworkPage.test.tsx | 12 ++++++++- .../components/network/WifiConnectionForm.tsx | 15 ++++++----- web/src/queries/network.ts | 11 ++++---- web/src/types/config.ts | 2 ++ web/src/types/network.ts | 17 +++++++----- 11 files changed, 93 insertions(+), 47 deletions(-) diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index cf23ea4670..4274e4b656 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -18,7 +18,15 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. +<<<<<<< HEAD use crate::{l10n, message, network, storage}; +======= +use crate::l10n; +use crate::message; +use crate::message::UpdateConfig; +use crate::network; + +>>>>>>> 89e85ab2d (Some fixes for networking config update) use agama_utils::{ actor::{self, Actor, Handler, MessageHandler}, api::{ @@ -32,6 +40,7 @@ use merge_struct::merge; use serde_json::Value; use network::{NetworkSystemClient, NetworkSystemError}; use tokio::sync::broadcast; +use zbus::conn; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -224,6 +233,18 @@ impl MessageHandler for Service { } } +fn merge_network(mut config: Config, update_config: Config) -> Config { + if let Some(network) = &update_config.network { + if let Some(connections) = &network.connections { + if let Some(ref mut config_network) = config.network { + config_network.connections = Some(connections.clone()); + } + } + } + + config +} + #[async_trait] impl MessageHandler for Service { /// Patches the config. @@ -232,6 +253,7 @@ impl MessageHandler for Service { /// config, then it keeps the values from the current config. async fn handle(&mut self, message: message::UpdateConfig) -> Result<(), Error> { let config = merge(&self.config, &message.config).map_err(|_| Error::MergeConfig)?; +<<<<<<< HEAD if let Some(l10n) = &config.l10n { self.l10n @@ -253,6 +275,10 @@ impl MessageHandler for Service { self.config = config; Ok(()) +======= + let config = merge_network(config, message.config); + self.handle(message::SetConfig::new(config)).await +>>>>>>> 89e85ab2d (Some fixes for networking config update) } } diff --git a/rust/agama-network/src/model.rs b/rust/agama-network/src/model.rs index 1e097adf43..d5b926256f 100644 --- a/rust/agama-network/src/model.rs +++ b/rust/agama-network/src/model.rs @@ -160,7 +160,12 @@ impl NetworkState { pub fn update_state(&mut self, config: Config) -> Result<(), NetworkStateError> { if let Some(connections) = config.connections { - let collection: ConnectionCollection = connections.try_into()?; + let mut collection: ConnectionCollection = connections.try_into()?; + collection.0.iter_mut().for_each(|conn| { + if let Some(current_conn) = self.get_connection(conn.id.as_str()) { + conn.uuid = current_conn.uuid; + } + }); self.connections = collection.0; } if let Some(general_state) = config.general_state { diff --git a/web/src/api/api.ts b/web/src/api/api.ts index 87911ea4ec..5a7bbda8ef 100644 --- a/web/src/api/api.ts +++ b/web/src/api/api.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import { get, patch, post } from "~/api/http"; +import { get, patch, post, put } from "~/api/http"; import { Config } from "~/types/config"; import { Proposal } from "~/types/proposal"; import { System } from "~/types/system"; @@ -39,9 +39,11 @@ const fetchProposal = (): Promise => get("/api/v2/proposal"); * Updates configuration */ const updateConfig = (config: Config) => patch("/api/v2/config", { update: config }); + +const setConfig = (config: Config) => put("/api/v2/config", config); /** * Triggers an action */ const trigger = (action) => post("/api/v2/action", action); -export { fetchSystem, fetchProposal, updateConfig, trigger }; +export { fetchSystem, fetchProposal, updateConfig, setConfig, trigger }; diff --git a/web/src/api/network.ts b/web/src/api/network.ts index 6a2cea1f06..08f5a666f4 100644 --- a/web/src/api/network.ts +++ b/web/src/api/network.ts @@ -25,13 +25,11 @@ import { APIAccessPoint, APIConnection, APIDevice, - APINetworkProposal, Connection, ConnectionStatus, NetworkGeneralState, NetworkProposal, } from "~/types/network"; -import { Proposal } from "~/types/proposal"; /** * Returns the network configuration @@ -66,20 +64,6 @@ const fetchAccessPoints = (): Promise => get("/api/network/wif */ const addConnection = (connection: APIConnection) => post("/api/network/connections", connection); -/** - * Updates given connection - * - * @param connection - connection to be updated - */ -const updateConnection = (connection: Connection) => { - const network: APINetworkProposal = { connections: [connection.toApi()] }; - const config: Proposal = { network }; - console.log("Updating"); - console.log(config); - - patch(`/api/v2/config`, { config }); -}; - /** * Deletes the connection matching given name */ @@ -122,7 +106,6 @@ export { fetchAccessPoints, applyChanges, addConnection, - updateConnection, deleteConnection, connect, disconnect, diff --git a/web/src/components/network/BindingSettingsForm.tsx b/web/src/components/network/BindingSettingsForm.tsx index 1fdb8d98d5..f0817895ec 100644 --- a/web/src/components/network/BindingSettingsForm.tsx +++ b/web/src/components/network/BindingSettingsForm.tsx @@ -33,12 +33,14 @@ import { Stack, } from "@patternfly/react-core"; import { Page, SubtleContent } from "~/components/core"; -import { useConnection, useConnectionMutation, useNetworkDevices } from "~/queries/network"; +import { useConnection, useConfigMutation, useNetworkDevices } from "~/queries/network"; import { Connection, ConnectionBindingMode, Device } from "~/types/network"; +import { Config } from "~/types/config"; import Radio from "~/components/core/RadioEnhanced"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; import { connectionBindingMode } from "~/utils/network"; +import { useNetworkProposal } from "~/queries/proposal"; type DevicesSelectProps = Omit & { /** @@ -126,8 +128,9 @@ const formReducer = (state: FormState, action: FormAction): FormState => { * connection on any interface. */ export default function BindingSettingsForm() { + const proposal = useNetworkProposal(); const { id } = useParams(); - const { mutateAsync: updateConnection } = useConnectionMutation(); + const { mutateAsync: updateConfig } = useConfigMutation(); const connection = useConnection(id); const devices = useNetworkDevices(); const navigate = useNavigate(); @@ -148,7 +151,10 @@ export default function BindingSettingsForm() { macAddress: state.mode === "mac" ? state.mac : undefined, }); - updateConnection(updatedConnection) + proposal.addOrUpdateConnection(updatedConnection); + const config: Config = { network: proposal.toApi() }; + + updateConfig(config) .then(() => navigate(-1)) .catch(console.error); }; diff --git a/web/src/components/network/IpSettingsForm.tsx b/web/src/components/network/IpSettingsForm.tsx index 782af122d6..cabde28c49 100644 --- a/web/src/components/network/IpSettingsForm.tsx +++ b/web/src/components/network/IpSettingsForm.tsx @@ -41,15 +41,18 @@ import AddressesDataList from "~/components/network/AddressesDataList"; import DnsDataList from "~/components/network/DnsDataList"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; -import { useConnection } from "~/queries/network"; +import { useConnection, useConfigMutation } from "~/queries/network"; +import { useNetworkProposal } from "~/queries/proposal"; import { IPAddress, Connection, ConnectionMethod } from "~/types/network"; -import { updateConnection } from "~/api/network"; +import { Config } from "~/types/config"; const usingDHCP = (method: ConnectionMethod) => method === ConnectionMethod.AUTO; // FIXME: rename to connedtioneditpage or so? // FIXME: improve the layout a bit. export default function IpSettingsForm() { + const proposal = useNetworkProposal(); + const { mutateAsync: updateConfig } = useConfigMutation(); const { id } = useParams(); const navigate = useNavigate(); const connection = useConnection(id); @@ -127,8 +130,12 @@ export default function IpSettingsForm() { nameservers: sanitizedNameservers.map((s) => s.address), }); - updateConnection(updatedConnection); - navigate(-1); + proposal.addOrUpdateConnection(updatedConnection); + const config: Config = { network: proposal.toApi() }; + + updateConfig(config) + .then(() => navigate(-1)) + .catch(console.error); }; const renderError = (field: string) => { diff --git a/web/src/components/network/NetworkPage.test.tsx b/web/src/components/network/NetworkPage.test.tsx index 41e61ab0e3..912a94895a 100644 --- a/web/src/components/network/NetworkPage.test.tsx +++ b/web/src/components/network/NetworkPage.test.tsx @@ -24,6 +24,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import NetworkPage from "~/components/network/NetworkPage"; +import { NetworkProposal } from "~/types/network"; jest.mock( "~/components/product/ProductRegistrationAlert", @@ -41,12 +42,21 @@ jest.mock("~/components/network/NoPersistentConnectionsAlert", () => () => ( )); const mockNetworkState = { + connectivity: true, + hostname: "Agama", + networkingEnabled: true, wirelessEnabled: true, }; +const mockNetworkProposal = { + connections: [], + state: mockNetworkState, +}; + jest.mock("~/queries/network", () => ({ useNetworkChanges: jest.fn(), - useNetworkState: () => mockNetworkState, + useNetworkProposal: () => + new NetworkProposal(mockNetworkProposal.connections, mockNetworkProposal.state), })); describe("NetworkPage", () => { diff --git a/web/src/components/network/WifiConnectionForm.tsx b/web/src/components/network/WifiConnectionForm.tsx index 1da7875d18..f003c2b41f 100644 --- a/web/src/components/network/WifiConnectionForm.tsx +++ b/web/src/components/network/WifiConnectionForm.tsx @@ -32,11 +32,12 @@ import { Spinner, } from "@patternfly/react-core"; import { Page, PasswordInput } from "~/components/core"; -import { useAddConnectionMutation, useConnectionMutation, useConnections } from "~/queries/network"; +import { useConfigMutation } from "~/queries/network"; import { Connection, ConnectionState, WifiNetwork, Wireless } from "~/types/network"; import { isEmpty } from "radashi"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; +import { useNetworkProposal } from "~/queries/proposal"; const securityOptions = [ // TRANSLATORS: WiFi authentication mode @@ -91,8 +92,8 @@ const ConnectionError = ({ ssid, isPublicNetwork }) => { // FIXME: improve error handling. The errors props should have a key/value error // and the component should show all of them, if any export default function WifiConnectionForm({ network }: { network: WifiNetwork }) { - const connections = useConnections(); - const connection = connections.find((c) => c.id === network.ssid); + const proposal = useNetworkProposal(); + const connection = proposal.connections.find((c) => c.id === network.ssid); const settings = network.settings?.wireless || new Wireless(); const [error, setError] = useState(false); const [security, setSecurity] = useState( @@ -103,8 +104,7 @@ export default function WifiConnectionForm({ network }: { network: WifiNetwork } const [isConnecting, setIsConnecting] = useState( connection?.state === ConnectionState.activating, ); - const { mutateAsync: addConnection } = useAddConnectionMutation(); - const { mutateAsync: updateConnection } = useConnectionMutation(); + const { mutateAsync: updateConfig } = useConfigMutation(); useEffect(() => { if (!isActivating) return; @@ -132,8 +132,9 @@ export default function WifiConnectionForm({ network }: { network: WifiNetwork } password, hidden: false, }); - const action = network.settings ? updateConnection : addConnection; - action(nextConnection).catch(() => setError(true)); + proposal.addOrUpdateConnection(nextConnection); + const config: Config = { network: proposal.toApi() }; + updateConfig(config).catch(() => setError(true)); setError(false); setIsConnecting(true); }; diff --git a/web/src/queries/network.ts b/web/src/queries/network.ts index 4f581c6a3f..2b1d53c353 100644 --- a/web/src/queries/network.ts +++ b/web/src/queries/network.ts @@ -43,10 +43,10 @@ import { fetchDevices, fetchState, persist, - updateConnection, } from "~/api/network"; import { useNetworkProposal } from "./proposal"; import { useNetworkSystem } from "./system"; +import { updateConfig } from "~/api/api"; /** * Returns a query for retrieving the general network configuration @@ -132,13 +132,12 @@ const useAddConnectionMutation = () => { * * It does not require to call `useMutation`. */ -const useConnectionMutation = () => { +const useConfigMutation = () => { const queryClient = useQueryClient(); const query = { - mutationFn: updateConnection, + mutationFn: updateConfig, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["network", "connections"] }); - queryClient.invalidateQueries({ queryKey: ["network", "devices"] }); + queryClient.invalidateQueries({ queryKey: ["proposal"] }); }, }; return useMutation(query); @@ -369,7 +368,7 @@ export { accessPointsQuery, useAddConnectionMutation, useConnections, - useConnectionMutation, + useConfigMutation, useConnectionPersistMutation, useRemoveConnectionMutation, useConnection, diff --git a/web/src/types/config.ts b/web/src/types/config.ts index f7248c72cf..5b441bcc48 100644 --- a/web/src/types/config.ts +++ b/web/src/types/config.ts @@ -21,9 +21,11 @@ */ import { Localization } from "./l10n"; +import { APINetworkProposal } from "./network"; type Config = { l10n?: Localization; + network?: APINetworkProposal; }; export type { Config }; diff --git a/web/src/types/network.ts b/web/src/types/network.ts index 1152b6a0b6..6b3d87d512 100644 --- a/web/src/types/network.ts +++ b/web/src/types/network.ts @@ -406,8 +406,6 @@ class NetworkSystem { class NetworkProposal { connections: Connection[]; - //accessPoints: AccessPoint[]; - //devices: Device[]; state: NetworkGeneralState; constructor( @@ -417,8 +415,6 @@ class NetworkProposal { state?: NetworkGeneralState, ) { if (connections !== undefined) this.connections = connections; - //if (accessPoints !== undefined) this.accessPoints = accessPoints; - //if (devices !== undefined) this.devices = devices; if (state !== undefined) this.state = state; } @@ -428,6 +424,17 @@ class NetworkProposal { return new NetworkProposal(conns, state); } + + addOrUpdateConnection(connection: Connection) { + const connections = this.connections.map((c) => (c.id === connection.id ? connection : c)); + this.connections = connections; + } + + toApi(): APINetworkProposal { + const connections = this.connections.map((c) => c.toApi()); + + return { connections, state: this.state }; + } } type APINetworkSystem = { @@ -439,8 +446,6 @@ type APINetworkSystem = { type APINetworkProposal = { connections?: APIConnection[]; - //accessPoints?: APIAccessPoint[]; - //devices?: APIDevice[]; state?: NetworkGeneralState; }; From 53ae79673bade4bb191a1461c007899e364c6b86 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Fri, 7 Nov 2025 00:44:55 +0000 Subject: [PATCH 10/17] Handled ports correctly --- rust/agama-manager/src/service.rs | 28 +++----------- rust/agama-network/src/model.rs | 63 ++++++++++++++++++++++++++----- 2 files changed, 59 insertions(+), 32 deletions(-) diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index 4274e4b656..db1fd45fd1 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -18,15 +18,7 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -<<<<<<< HEAD use crate::{l10n, message, network, storage}; -======= -use crate::l10n; -use crate::message; -use crate::message::UpdateConfig; -use crate::network; - ->>>>>>> 89e85ab2d (Some fixes for networking config update) use agama_utils::{ actor::{self, Actor, Handler, MessageHandler}, api::{ @@ -37,10 +29,9 @@ use agama_utils::{ }; use async_trait::async_trait; use merge_struct::merge; -use serde_json::Value; use network::{NetworkSystemClient, NetworkSystemError}; +use serde_json::Value; use tokio::sync::broadcast; -use zbus::conn; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -253,7 +244,7 @@ impl MessageHandler for Service { /// config, then it keeps the values from the current config. async fn handle(&mut self, message: message::UpdateConfig) -> Result<(), Error> { let config = merge(&self.config, &message.config).map_err(|_| Error::MergeConfig)?; -<<<<<<< HEAD + let config = merge_network(config, message.config); if let Some(l10n) = &config.l10n { self.l10n @@ -273,12 +264,12 @@ impl MessageHandler for Service { .await?; } + if let Some(network) = &config.network { + self.network.update_config(network.clone()).await?; + } + self.config = config; Ok(()) -======= - let config = merge_network(config, message.config); - self.handle(message::SetConfig::new(config)).await ->>>>>>> 89e85ab2d (Some fixes for networking config update) } } @@ -287,15 +278,8 @@ impl MessageHandler for Service { /// It returns the current proposal, if any. async fn handle(&mut self, _message: message::GetProposal) -> Result, Error> { let l10n = self.l10n.call(l10n::message::GetProposal).await?; -<<<<<<< HEAD let storage = self.storage.call(storage::message::GetProposal).await?; - let network_config: types::Proposal = self.network.get_extended_config().await?; - let network = types::Proposal { - connections: network_config.connections, - }; -======= let network = self.network.get_extended_config().await?; ->>>>>>> c91270e0d (Moved network types to agama utils and adapt for new API changes) Ok(Some(Proposal { l10n, diff --git a/rust/agama-network/src/model.rs b/rust/agama-network/src/model.rs index d5b926256f..ca73b851ea 100644 --- a/rust/agama-network/src/model.rs +++ b/rust/agama-network/src/model.rs @@ -160,14 +160,32 @@ impl NetworkState { pub fn update_state(&mut self, config: Config) -> Result<(), NetworkStateError> { if let Some(connections) = config.connections { - let mut collection: ConnectionCollection = connections.try_into()?; - collection.0.iter_mut().for_each(|conn| { + let mut collection: ConnectionCollection = connections.clone().try_into()?; + for conn in collection.0.iter_mut() { if let Some(current_conn) = self.get_connection(conn.id.as_str()) { + // Replaced the UUID with a real one conn.uuid = current_conn.uuid; + self.update_connection(conn.to_owned())?; + } else { + self.add_connection(conn.to_owned())?; + } + } + + for conn in connections.0 { + let mut ports = vec![]; + if let Some(model) = conn.bridge { + ports = model.ports; + } + if let Some(model) = conn.bond { + ports = model.ports; } - }); - self.connections = collection.0; + + if let Some(controller) = self.get_connection(conn.id.as_str()) { + self.set_ports(&controller.clone(), ports)?; + } + } } + if let Some(general_state) = config.general_state { self.general_state = general_state; } @@ -1332,13 +1350,38 @@ impl TryFrom for ConnectionCollection { type Error = NetworkStateError; fn try_from(collection: NetworkConnectionsCollection) -> Result { - let network_connections = collection - .0 - .iter() - .map(|c| Connection::try_from(c.clone()).unwrap()) - .collect(); + let mut conns: Vec = vec![]; + let mut controller_ports: HashMap = HashMap::new(); + + for net_conn in &collection.0 { + let mut conn = Connection::try_from(net_conn.clone())?; + conn.uuid = Uuid::new_v4(); + let mut ports = vec![]; + if let Some(bridge) = &net_conn.bridge { + ports = bridge.ports.clone(); + } + if let Some(bond) = &net_conn.bond { + ports = bond.ports.clone(); + } + for port in &ports { + controller_ports.insert(port.to_string(), conn.uuid); + } + + conns.push(conn); + } + + for (port, uuid) in controller_ports { + let default = Connection::new(port.clone(), DeviceType::Ethernet); + let mut conn = conns + .iter() + .find(|&c| c.id == port || c.interface == Some(port.to_string())) + .unwrap_or(&default) + .to_owned(); + conn.controller = Some(uuid); + conns.push(conn); + } - Ok(ConnectionCollection(network_connections)) + Ok(ConnectionCollection(conns)) } } From 4d0650bdc6f3e4c1720054d43d508f3d78677026 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Fri, 7 Nov 2025 15:26:10 +0000 Subject: [PATCH 11/17] Small fixes --- rust/agama-lib/share/profile.schema.json | 14 ++++++++++++++ rust/agama-network/src/model.rs | 20 +++++++++++--------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 33e06eab27..2b1946ee28 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -336,6 +336,20 @@ "type": "object", "additionalProperties": false, "properties": { + "generalState": { + "title": "Network general state settings", + "type": "object", + "properties": { + "wirelessEnabled": { + "title": "Whether the wireless should be enabled", + "type": "boolean" + }, + "networkingEnabled": { + "title": "Whether the network should be enabled", + "type": "boolean" + } + } + }, "connections": { "title": "Network connections to be defined", "type": "array", diff --git a/rust/agama-network/src/model.rs b/rust/agama-network/src/model.rs index ca73b851ea..fceb847c75 100644 --- a/rust/agama-network/src/model.rs +++ b/rust/agama-network/src/model.rs @@ -172,16 +172,18 @@ impl NetworkState { } for conn in connections.0 { - let mut ports = vec![]; - if let Some(model) = conn.bridge { - ports = model.ports; - } - if let Some(model) = conn.bond { - ports = model.ports; - } + if conn.bridge.is_some() | conn.bond.is_some() { + let mut ports = vec![]; + if let Some(model) = conn.bridge { + ports = model.ports; + } + if let Some(model) = conn.bond { + ports = model.ports; + } - if let Some(controller) = self.get_connection(conn.id.as_str()) { - self.set_ports(&controller.clone(), ports)?; + if let Some(controller) = self.get_connection(conn.id.as_str()) { + self.set_ports(&controller.clone(), ports)?; + } } } } From a8e943c81c6d04689c22f929fba6231b3858e520 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Mon, 10 Nov 2025 11:06:34 +0000 Subject: [PATCH 12/17] Added network state settings to decouple from general state --- rust/agama-lib/share/profile.schema.json | 13 +++++- rust/agama-manager/src/service.rs | 8 +--- rust/agama-network/src/action.rs | 10 ++--- rust/agama-network/src/model.rs | 43 ++++++++++++++++--- rust/agama-network/src/nm/client.rs | 6 +-- rust/agama-network/src/system.rs | 20 ++++++--- rust/agama-server/src/web/docs/config.rs | 2 +- rust/agama-utils/src/api/network/config.rs | 5 +-- rust/agama-utils/src/api/network/proposal.rs | 4 +- rust/agama-utils/src/api/network/settings.rs | 13 ++++++ .../src/api/network/system_info.rs | 4 +- rust/agama-utils/src/api/network/types.rs | 11 ----- 12 files changed, 94 insertions(+), 45 deletions(-) diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 2b1946ee28..6e0fcf731d 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -340,13 +340,22 @@ "title": "Network general state settings", "type": "object", "properties": { - "wirelessEnabled": { - "title": "Whether the wireless should be enabled", + "connectivity": { + "title": "Determines whether the user is able to access the Internet", + "type": "boolean", + "readOnly": true + }, + "copyNetwork": { + "title": "Whether the network configuration should be copied to the target system", "type": "boolean" }, "networkingEnabled": { "title": "Whether the network should be enabled", "type": "boolean" + }, + "wirelessEnabled": { + "title": "Whether the wireless should be enabled", + "type": "boolean" } } }, diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index db1fd45fd1..62873e87eb 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -170,11 +170,7 @@ impl MessageHandler for Service { async fn handle(&mut self, _message: message::GetExtendedConfig) -> Result { let l10n = self.l10n.call(l10n::message::GetConfig).await?; let questions = self.questions.call(question::message::GetConfig).await?; - let network_config: network::types::Proposal = self.network.get_extended_config().await?; - let network = agama_network::types::Config { - connections: Some(network_config.connections), - general_state: Some(network_config.general_state), - }; + let network = self.network.get_config().await?; let storage = self.storage.call(storage::message::GetConfig).await?; Ok(Config { @@ -279,7 +275,7 @@ impl MessageHandler for Service { async fn handle(&mut self, _message: message::GetProposal) -> Result, Error> { let l10n = self.l10n.call(l10n::message::GetProposal).await?; let storage = self.storage.call(storage::message::GetProposal).await?; - let network = self.network.get_extended_config().await?; + let network = self.network.get_proposal().await?; Ok(Some(Proposal { l10n, diff --git a/rust/agama-network/src/action.rs b/rust/agama-network/src/action.rs index b13c9f888f..791bcb3622 100644 --- a/rust/agama-network/src/action.rs +++ b/rust/agama-network/src/action.rs @@ -18,10 +18,8 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::model::Connection; -use crate::types::{ - AccessPoint, ConnectionState, Device, DeviceType, GeneralState, Proposal, SystemInfo, -}; +use crate::model::{Connection, GeneralState}; +use crate::types::{AccessPoint, ConnectionState, Device, DeviceType, Proposal, SystemInfo}; use agama_utils::api::network::Config; use tokio::sync::oneshot; use uuid::Uuid; @@ -46,7 +44,9 @@ pub enum Action { /// Gets a connection by its Uuid GetConnectionByUuid(Uuid, Responder>), /// Gets the internal state of the network configuration - GetExtendedConfig(Responder), + GetConfig(Responder), + /// Gets the internal state of the network configuration proposal + GetProposal(Responder), /// Updates th internal state of the network configuration UpdateConfig(Box, Responder>), /// Gets the current network configuration containing connections, devices, access_points and diff --git a/rust/agama-network/src/model.rs b/rust/agama-network/src/model.rs index fceb847c75..0e9372a04f 100644 --- a/rust/agama-network/src/model.rs +++ b/rust/agama-network/src/model.rs @@ -188,8 +188,18 @@ impl NetworkState { } } - if let Some(general_state) = config.general_state { - self.general_state = general_state; + if let Some(state) = config.state { + if let Some(wireless_enabled) = state.wireless_enabled { + self.general_state.wireless_enabled = wireless_enabled; + } + + if let Some(networking_enabled) = state.networking_enabled { + self.general_state.networking_enabled = networking_enabled; + } + + if let Some(copy_network) = state.copy_network { + self.general_state.copy_network = copy_network; + } } Ok(()) } @@ -453,6 +463,16 @@ mod tests { pub const NOT_COPY_NETWORK_PATH: &str = "/run/agama/not_copy_network"; +/// Network state +#[derive(Clone, Debug, Default)] +pub struct GeneralState { + pub hostname: String, + pub connectivity: bool, + pub copy_network: bool, + pub wireless_enabled: bool, + pub networking_enabled: bool, // pub network_state: NMSTATE +} + /// Represents a known network connection. #[serde_as] #[skip_serializing_none] @@ -1411,6 +1431,19 @@ impl TryFrom for NetworkConnectionsCollection { } } +impl TryFrom for StateSettings { + type Error = NetworkStateError; + + fn try_from(state: GeneralState) -> Result { + Ok(StateSettings { + connectivity: Some(state.connectivity), + copy_network: Some(state.copy_network), + wireless_enabled: Some(state.wireless_enabled), + networking_enabled: Some(state.networking_enabled), + }) + } +} + impl TryFrom for NetworkSettings { type Error = NetworkStateError; @@ -1430,7 +1463,7 @@ impl TryFrom for Config { Ok(Config { connections: Some(connections), - general_state: Some(state.general_state), + state: Some(state.general_state.try_into()?), }) } } @@ -1446,7 +1479,7 @@ impl TryFrom for SystemInfo { access_points: state.access_points, connections, devices: state.devices, - general_state: state.general_state, + state: state.general_state.try_into()?, }) } } @@ -1460,7 +1493,7 @@ impl TryFrom for Proposal { Ok(Proposal { connections, - general_state: state.general_state, + state: state.general_state.try_into()?, }) } } diff --git a/rust/agama-network/src/nm/client.rs b/rust/agama-network/src/nm/client.rs index b90a81ae70..2268dd014d 100644 --- a/rust/agama-network/src/nm/client.rs +++ b/rust/agama-network/src/nm/client.rs @@ -34,10 +34,10 @@ use super::proxies::{ AccessPointProxy, ActiveConnectionProxy, ConnectionProxy, DeviceProxy, NetworkManagerProxy, SettingsProxy, WirelessProxy, }; -use crate::model::{Connection, ConnectionConfig, SecurityProtocol, NOT_COPY_NETWORK_PATH}; -use crate::types::{ - AccessPoint, AddFlags, ConnectionFlags, Device, DeviceType, GeneralState, UpdateFlags, SSID, +use crate::model::{ + Connection, ConnectionConfig, GeneralState, SecurityProtocol, NOT_COPY_NETWORK_PATH, }; +use crate::types::{AccessPoint, AddFlags, ConnectionFlags, Device, DeviceType, UpdateFlags, SSID}; use agama_utils::dbus::get_optional_property; use semver::Version; use uuid::Uuid; diff --git a/rust/agama-network/src/system.rs b/rust/agama-network/src/system.rs index 38cfecbd27..69bf54c5b4 100644 --- a/rust/agama-network/src/system.rs +++ b/rust/agama-network/src/system.rs @@ -21,8 +21,8 @@ use crate::{ action::Action, error::NetworkStateError, - model::{Connection, NetworkChange, NetworkState, StateConfig}, - types::{AccessPoint, Config, Device, DeviceType, GeneralState, Proposal, SystemInfo}, + model::{Connection, GeneralState, NetworkChange, NetworkState, StateConfig}, + types::{AccessPoint, Config, Device, DeviceType, Proposal, SystemInfo}, Adapter, NetworkAdapterError, }; use std::error::Error; @@ -161,9 +161,15 @@ impl NetworkSystemClient { self.actions.send(Action::GetConnections(tx))?; Ok(rx.await?) } - pub async fn get_extended_config(&self) -> Result { + pub async fn get_config(&self) -> Result { let (tx, rx) = oneshot::channel(); - self.actions.send(Action::GetExtendedConfig(tx))?; + self.actions.send(Action::GetConfig(tx))?; + Ok(rx.await?) + } + + pub async fn get_proposal(&self) -> Result { + let (tx, rx) = oneshot::channel(); + self.actions.send(Action::GetProposal(tx))?; Ok(rx.await?) } @@ -331,7 +337,11 @@ impl NetworkSystemServer { let result = self.read().await?.try_into()?; tx.send(result).unwrap(); } - Action::GetExtendedConfig(tx) => { + Action::GetConfig(tx) => { + let config: Config = self.state.clone().try_into()?; + tx.send(config).unwrap(); + } + Action::GetProposal(tx) => { let config: Proposal = self.state.clone().try_into()?; tx.send(config).unwrap(); } diff --git a/rust/agama-server/src/web/docs/config.rs b/rust/agama-server/src/web/docs/config.rs index d753092a0c..371c3618e7 100644 --- a/rust/agama-server/src/web/docs/config.rs +++ b/rust/agama-server/src/web/docs/config.rs @@ -156,7 +156,6 @@ impl ApiDocBuilder for ConfigApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() - .schema_from::() .schema_from::() .schema_from::() .schema_from::() @@ -170,6 +169,7 @@ impl ApiDocBuilder for ConfigApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() diff --git a/rust/agama-utils/src/api/network/config.rs b/rust/agama-utils/src/api/network/config.rs index 490de3d860..9f7c60ef54 100644 --- a/rust/agama-utils/src/api/network/config.rs +++ b/rust/agama-utils/src/api/network/config.rs @@ -20,8 +20,7 @@ //! Representation of the network settings -use crate::api::network; -use network::{GeneralState, NetworkConnectionsCollection}; +use crate::api::network::{NetworkConnectionsCollection, StateSettings}; use serde::{Deserialize, Serialize}; use std::default::Default; @@ -31,5 +30,5 @@ use std::default::Default; pub struct Config { /// Connections to use in the installation pub connections: Option, - pub general_state: Option, + pub state: Option, } diff --git a/rust/agama-utils/src/api/network/proposal.rs b/rust/agama-utils/src/api/network/proposal.rs index 16f05d21dd..39bbe548a6 100644 --- a/rust/agama-utils/src/api/network/proposal.rs +++ b/rust/agama-utils/src/api/network/proposal.rs @@ -20,7 +20,7 @@ //! Representation of the network settings -use crate::api::network::{GeneralState, NetworkConnectionsCollection}; +use crate::api::network::{NetworkConnectionsCollection, StateSettings}; use serde::{Deserialize, Serialize}; use std::default::Default; @@ -30,5 +30,5 @@ use std::default::Default; pub struct Proposal { /// Connections to use in the installation pub connections: NetworkConnectionsCollection, - pub general_state: GeneralState, + pub state: StateSettings, } diff --git a/rust/agama-utils/src/api/network/settings.rs b/rust/agama-utils/src/api/network/settings.rs index 8f4e68ee08..f4ac92f023 100644 --- a/rust/agama-utils/src/api/network/settings.rs +++ b/rust/agama-utils/src/api/network/settings.rs @@ -37,6 +37,19 @@ pub struct NetworkSettings { pub connections: NetworkConnectionsCollection, } +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct StateSettings { + #[serde(skip_serializing_if = "Option::is_none")] + pub connectivity: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub wireless_enabled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub networking_enabled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub copy_network: Option, +} + #[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] pub struct MatchSettings { #[serde(skip_serializing_if = "Vec::is_empty", default)] diff --git a/rust/agama-utils/src/api/network/system_info.rs b/rust/agama-utils/src/api/network/system_info.rs index 659b93e914..f7d9d43b97 100644 --- a/rust/agama-utils/src/api/network/system_info.rs +++ b/rust/agama-utils/src/api/network/system_info.rs @@ -20,7 +20,7 @@ //! Representation of the network settings -use crate::api::network::{AccessPoint, Device, GeneralState, NetworkConnectionsCollection}; +use crate::api::network::{AccessPoint, Device, NetworkConnectionsCollection, StateSettings}; use serde::{Deserialize, Serialize}; use std::default::Default; @@ -32,5 +32,5 @@ pub struct SystemInfo { /// Connections to use in the installation pub connections: NetworkConnectionsCollection, pub devices: Vec, - pub general_state: GeneralState, + pub state: StateSettings, } diff --git a/rust/agama-utils/src/api/network/types.rs b/rust/agama-utils/src/api/network/types.rs index 12ee362eee..29115dcc1a 100644 --- a/rust/agama-utils/src/api/network/types.rs +++ b/rust/agama-utils/src/api/network/types.rs @@ -31,17 +31,6 @@ use std::{ use thiserror::Error; use zbus::zvariant::Value; -/// Network state -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct GeneralState { - pub hostname: String, - pub connectivity: bool, - pub copy_network: bool, - pub wireless_enabled: bool, - pub networking_enabled: bool, // pub network_state: NMSTATE -} - /// Access Point #[serde_as] #[derive(Default, Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] From 6f2d19ae3e95146fcf6a6ba61f2d072265274599 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Mon, 10 Nov 2025 11:24:25 +0000 Subject: [PATCH 13/17] Fixed web-server doc --- rust/agama-server/src/web/docs/config.rs | 3 ++- rust/agama-utils/src/api/network/config.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/rust/agama-server/src/web/docs/config.rs b/rust/agama-server/src/web/docs/config.rs index 371c3618e7..1ae7c030db 100644 --- a/rust/agama-server/src/web/docs/config.rs +++ b/rust/agama-server/src/web/docs/config.rs @@ -166,7 +166,8 @@ impl ApiDocBuilder for ConfigApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() - .schema_from::() + .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() diff --git a/rust/agama-utils/src/api/network/config.rs b/rust/agama-utils/src/api/network/config.rs index 9f7c60ef54..51b7848513 100644 --- a/rust/agama-utils/src/api/network/config.rs +++ b/rust/agama-utils/src/api/network/config.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2024] SUSE LLC +// Copyright (c) [2025] SUSE LLC // // All Rights Reserved. // From 2df7aadd3049de657bb911d01003a6e1c08717c1 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Mon, 10 Nov 2025 12:28:32 +0000 Subject: [PATCH 14/17] Fixed web types check --- web/src/components/network/NetworkPage.tsx | 2 +- web/src/components/network/WifiConnectionDetails.test.tsx | 1 + web/src/components/network/WifiConnectionForm.test.tsx | 1 + web/src/components/network/WifiConnectionForm.tsx | 2 ++ web/src/components/network/WifiNetworksList.test.tsx | 5 +++++ web/src/types/network.ts | 4 ++-- 6 files changed, 12 insertions(+), 3 deletions(-) diff --git a/web/src/components/network/NetworkPage.tsx b/web/src/components/network/NetworkPage.tsx index abd1296c41..dceaf99c03 100644 --- a/web/src/components/network/NetworkPage.tsx +++ b/web/src/components/network/NetworkPage.tsx @@ -63,7 +63,7 @@ export default function NetworkPage() { - {networkSystem.wirelessEnabled ? ( + {networkSystem.state.wirelessEnabled ? ( diff --git a/web/src/components/network/WifiConnectionDetails.test.tsx b/web/src/components/network/WifiConnectionDetails.test.tsx index e792927c82..00334bd9d8 100644 --- a/web/src/components/network/WifiConnectionDetails.test.tsx +++ b/web/src/components/network/WifiConnectionDetails.test.tsx @@ -59,6 +59,7 @@ const mockNetwork = { strength: 25, hwAddress: "??", security: [SecurityProtocols.RSN], + device_name: "wlan0", device: wlan0, settings: new Connection("Network 1", { iface: "wlan0", diff --git a/web/src/components/network/WifiConnectionForm.test.tsx b/web/src/components/network/WifiConnectionForm.test.tsx index 9c0afd1844..d651b50ca3 100644 --- a/web/src/components/network/WifiConnectionForm.test.tsx +++ b/web/src/components/network/WifiConnectionForm.test.tsx @@ -44,6 +44,7 @@ jest.mock("~/queries/network", () => ({ const networkMock = { ssid: "Visible Network", hidden: false, + device_name: "wlan0", status: WifiNetworkStatus.NOT_CONFIGURED, hwAddress: "00:EB:D8:17:6B:56", security: [SecurityProtocols.WPA], diff --git a/web/src/components/network/WifiConnectionForm.tsx b/web/src/components/network/WifiConnectionForm.tsx index f003c2b41f..51fd15d3bc 100644 --- a/web/src/components/network/WifiConnectionForm.tsx +++ b/web/src/components/network/WifiConnectionForm.tsx @@ -34,6 +34,7 @@ import { import { Page, PasswordInput } from "~/components/core"; import { useConfigMutation } from "~/queries/network"; import { Connection, ConnectionState, WifiNetwork, Wireless } from "~/types/network"; +import { Config } from "~/types/config"; import { isEmpty } from "radashi"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; @@ -132,6 +133,7 @@ export default function WifiConnectionForm({ network }: { network: WifiNetwork } password, hidden: false, }); + proposal.addOrUpdateConnection(nextConnection); const config: Config = { network: proposal.toApi() }; updateConfig(config).catch(() => setError(true)); diff --git a/web/src/components/network/WifiNetworksList.test.tsx b/web/src/components/network/WifiNetworksList.test.tsx index 437ef8456c..7d8c132155 100644 --- a/web/src/components/network/WifiNetworksList.test.tsx +++ b/web/src/components/network/WifiNetworksList.test.tsx @@ -94,6 +94,7 @@ describe("WifiNetworksList", () => { iface: "wlan0", addresses: [{ address: "192.168.69.201", prefix: 24 }], }), + device_name: "wlan0", status: WifiNetworkStatus.CONNECTED, }, { @@ -105,6 +106,7 @@ describe("WifiNetworksList", () => { iface: "wlan1", addresses: [{ address: "192.168.69.202", prefix: 24 }], }), + device_name: "wlan1", status: WifiNetworkStatus.CONFIGURED, }, { @@ -112,6 +114,7 @@ describe("WifiNetworksList", () => { strength: 66, hwAddress: "??", security: [], + device_name: "wlan0", status: WifiNetworkStatus.NOT_CONFIGURED, }, ]; @@ -168,6 +171,7 @@ describe("WifiNetworksList", () => { iface: "wlan1", addresses: [{ address: "192.168.69.202", prefix: 24 }], }), + device_name: "wlan1", status: WifiNetworkStatus.CONFIGURED, }, ]; @@ -206,6 +210,7 @@ describe("WifiNetworksList", () => { iface: "wlan1", addresses: [{ address: "192.168.69.202", prefix: 24 }], }), + device_name: "wlan1", status: WifiNetworkStatus.CONFIGURED, }, ]; diff --git a/web/src/types/network.ts b/web/src/types/network.ts index 6b3d87d512..f900bcd8d0 100644 --- a/web/src/types/network.ts +++ b/web/src/types/network.ts @@ -151,7 +151,7 @@ type APIAccessPoint = { }; class AccessPoint { - device: string; + device_name: string; ssid: string; strength: number; hwAddress: string; @@ -164,7 +164,7 @@ class AccessPoint { hwAddress: string, security: SecurityProtocols[], ) { - this.device = device; + this.device_name = device; this.ssid = ssid; this.strength = strength; this.hwAddress = hwAddress; From df72d6a5dfc697b0a0fd27fb705647a8a870ba26 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Mon, 10 Nov 2025 12:43:28 +0000 Subject: [PATCH 15/17] Removed not needed anymore network code --- .../network/WifiConnectionForm.test.tsx | 3 -- .../network/WifiNetworksList.test.tsx | 8 ---- web/src/queries/network.ts | 45 +------------------ 3 files changed, 1 insertion(+), 55 deletions(-) diff --git a/web/src/components/network/WifiConnectionForm.test.tsx b/web/src/components/network/WifiConnectionForm.test.tsx index d651b50ca3..4a49f90b47 100644 --- a/web/src/components/network/WifiConnectionForm.test.tsx +++ b/web/src/components/network/WifiConnectionForm.test.tsx @@ -32,9 +32,6 @@ const mockUpdateConnection = jest.fn(); jest.mock("~/queries/network", () => ({ ...jest.requireActual("~/queries/network"), useNetworkChanges: jest.fn(), - useAddConnectionMutation: () => ({ - mutateAsync: mockAddConnection, - }), useConnectionMutation: () => ({ mutateAsync: mockUpdateConnection, }), diff --git a/web/src/components/network/WifiNetworksList.test.tsx b/web/src/components/network/WifiNetworksList.test.tsx index 7d8c132155..5009e7f63e 100644 --- a/web/src/components/network/WifiNetworksList.test.tsx +++ b/web/src/components/network/WifiNetworksList.test.tsx @@ -49,20 +49,12 @@ const wlan0: Device = { macAddress: "AA:11:22:33:44::FF", }; -const mockConnectionRemoval = jest.fn(); -const mockAddConnection = jest.fn(); let mockWifiNetworks: WifiNetwork[]; let mockWifiConnections: Connection[]; jest.mock("~/queries/network", () => ({ ...jest.requireActual("~/queries/network"), useNetworkChanges: jest.fn(), - useRemoveConnectionMutation: () => ({ - mutate: mockConnectionRemoval, - }), - useAddConnectionMutation: () => ({ - mutate: mockAddConnection, - }), useWifiNetworks: () => mockWifiNetworks, useConnections: () => mockWifiConnections, })); diff --git a/web/src/queries/network.ts b/web/src/queries/network.ts index 2b1d53c353..9d62d0ef6c 100644 --- a/web/src/queries/network.ts +++ b/web/src/queries/network.ts @@ -34,9 +34,6 @@ import { WifiNetworkStatus, } from "~/types/network"; import { - addConnection, - applyChanges, - deleteConnection, fetchAccessPoints, fetchConnection, fetchConnections, @@ -108,25 +105,6 @@ const accessPointsQuery = () => ({ staleTime: 1000, }); -/** - * Hook that builds a mutation to add a new network connection - * - * It does not require to call `useMutation`. - */ -const useAddConnectionMutation = () => { - const queryClient = useQueryClient(); - const query = { - mutationFn: (newConnection: Connection) => - addConnection(newConnection.toApi()).then(() => applyChanges()), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["network", "connections"] }); - queryClient.invalidateQueries({ queryKey: ["network", "devices"] }); - queryClient.invalidateQueries({ queryKey: ["network", "accessPoints"] }); - }, - }; - return useMutation(query); -}; - /** * Hook that builds a mutation to update a network connection * @@ -183,32 +161,13 @@ const useConnectionPersistMutation = () => { * Called if the mutation fails for whatever reason. Rolls back the cache to * the previous state. */ - onError: (_, connection: Connection, context: { previousConnections: Connection[] }) => { + onError: (context: { previousConnections: Connection[] }) => { queryClient.setQueryData(["network", "connections"], context.previousConnections); }, }; return useMutation(query); }; -/** - * Hook that builds a mutation to remove a network connection - * - * It does not require to call `useMutation`. - */ -const useRemoveConnectionMutation = () => { - const queryClient = useQueryClient(); - const query = { - mutationFn: (name: string) => - deleteConnection(name) - .then(() => applyChanges()) - .catch((e) => console.log(e)), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["network", "connections"] }); - queryClient.invalidateQueries({ queryKey: ["network", "devices"] }); - }, - }; - return useMutation(query); -}; /** * Hook that returns a useEffect to listen for NetworkChanged events @@ -366,11 +325,9 @@ export { connectionQuery, connectionsQuery, accessPointsQuery, - useAddConnectionMutation, useConnections, useConfigMutation, useConnectionPersistMutation, - useRemoveConnectionMutation, useConnection, useNetworkDevices, useNetworkState, From d49dc094a39e2b7eb53760eea31627102fbe4f19 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Tue, 11 Nov 2025 06:53:26 +0000 Subject: [PATCH 16/17] Starting to fix tests --- rust/agama-lib/share/profile.schema.json | 2 +- web/src/api/network.ts | 113 --------------- .../network/InstallationOnlySwitch.test.tsx | 24 +++- .../network/InstallationOnlySwitch.tsx | 18 ++- web/src/queries/network.ts | 132 +----------------- 5 files changed, 40 insertions(+), 249 deletions(-) delete mode 100644 web/src/api/network.ts diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 6e0fcf731d..a40f9da479 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -336,7 +336,7 @@ "type": "object", "additionalProperties": false, "properties": { - "generalState": { + "state": { "title": "Network general state settings", "type": "object", "properties": { diff --git a/web/src/api/network.ts b/web/src/api/network.ts deleted file mode 100644 index 08f5a666f4..0000000000 --- a/web/src/api/network.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { get, patch, post } from "~/api/http"; -import { - APIAccessPoint, - APIConnection, - APIDevice, - Connection, - ConnectionStatus, - NetworkGeneralState, - NetworkProposal, -} from "~/types/network"; - -/** - * Returns the network configuration - */ -const fetchState = (): Promise => get("/api/network/state"); - -/** - * Returns a list of known devices - */ -const fetchDevices = (): Promise => get("/api/network/devices"); - -/** - * Returns data for given connection name - */ -const fetchConnection = (name: string): Promise => - get(`/api/network/connections/${encodeURIComponent(name)}`); - -/** - * Returns the list of known connections - */ -const fetchConnections = (): Promise => get("/api/network/connections"); - -/** - * Returns the list of known access points - */ -const fetchAccessPoints = (): Promise => get("/api/network/wifi"); - -/** - * Adds a new connection - * - * @param connection - connection to be added - */ -const addConnection = (connection: APIConnection) => post("/api/network/connections", connection); - -/** - * Deletes the connection matching given name - */ -const deleteConnection = (name: string) => { - const connection = new Connection(name); - connection.status = ConnectionStatus.DELETE; - const network = new NetworkProposal([connection]); - - patch(`/api/v2/config`, { network }); -}; - -/** - * Apply network changes - */ -const applyChanges = () => post("/api/network/system/apply"); - -/** - * Performs the connect action for connection matching given name - */ -const connect = (name: string) => - post(`/api/network/connections/${encodeURIComponent(name)}/connect`); - -/** - * Performs the disconnect action for connection matching given name - */ -const disconnect = (name: string) => - post(`/api/network/connections/${encodeURIComponent(name)}/disconnect`); - -/** - * Make the connection persistent after the installation - */ -const persist = (name: string, value: boolean) => - post(`/api/network/connections/persist`, { only: [name], value }); - -export { - fetchState, - fetchDevices, - fetchConnection, - fetchConnections, - fetchAccessPoints, - applyChanges, - addConnection, - deleteConnection, - connect, - disconnect, - persist, -}; diff --git a/web/src/components/network/InstallationOnlySwitch.test.tsx b/web/src/components/network/InstallationOnlySwitch.test.tsx index 975d76ad21..a21ebc5efc 100644 --- a/web/src/components/network/InstallationOnlySwitch.test.tsx +++ b/web/src/components/network/InstallationOnlySwitch.test.tsx @@ -24,9 +24,15 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import InstallationOnlySwitch from "./InstallationOnlySwitch"; -import { Connection, ConnectionMethod, ConnectionOptions, ConnectionState } from "~/types/network"; +import { + Connection, + ConnectionMethod, + ConnectionOptions, + ConnectionState, + NetworkProposal, +} from "~/types/network"; -const mockPersistMutation = jest.fn(); +const mockUpdateConfig = jest.fn(); const mockConnection = (options: Partial = {}) => new Connection("Newtwork 2", { method4: ConnectionMethod.AUTO, @@ -39,11 +45,19 @@ const mockConnection = (options: Partial = {}) => state: ConnectionState.activating, ...options, }); +const mockProposal = () => { + new NetworkProposal([mockConnection({ persistent: true })]); +}; + +jest.mock("~/queries/proposal", () => ({ + ...jest.requireActual("~/queries/proposal"), + useNetworkProposal: () => mockProposal(), +})); jest.mock("~/queries/network", () => ({ ...jest.requireActual("~/queries/network"), - useConnectionPersistMutation: () => ({ - mutateAsync: mockPersistMutation, + useConfigMutation: () => ({ + mutateAsync: mockUpdateConfig, }), })); @@ -74,6 +88,6 @@ describe("InstallationOnlySwitch", () => { const { user } = plainRender(); const switchInput = screen.getByRole("switch", { name: "Use for installation only" }); await user.click(switchInput); - expect(mockPersistMutation).toHaveBeenCalledWith(connection); + expect(mockUpdateConfig).toHaveBeenCalledWith(connection); }); }); diff --git a/web/src/components/network/InstallationOnlySwitch.tsx b/web/src/components/network/InstallationOnlySwitch.tsx index cce915dbf3..d2654a775f 100644 --- a/web/src/components/network/InstallationOnlySwitch.tsx +++ b/web/src/components/network/InstallationOnlySwitch.tsx @@ -23,8 +23,10 @@ import React from "react"; import { Connection } from "~/types/network"; import { SwitchEnhanced } from "~/components/core"; -import { useConnectionPersistMutation } from "~/queries/network"; import { _ } from "~/i18n"; +import { useConfigMutation } from "~/queries/network"; +import { useNetworkProposal } from "~/queries/proposal"; +import { Config } from "~/types/config"; type InstallationOnlySwitchProps = { /** The connection to configure as installation-only or not */ @@ -39,8 +41,18 @@ type InstallationOnlySwitchProps = { * */ export default function InstallationOnlySwitch({ connection }: InstallationOnlySwitchProps) { - const { mutateAsync: togglePersist } = useConnectionPersistMutation(); - const onChange = () => togglePersist(connection); + const proposal = useNetworkProposal(); + const updatedConnection = new Connection(connection.id, { + ...connection, + persistent: !connection.persistent, + }); + const { mutateAsync: updateConfig } = useConfigMutation(); + const onChange = () => { + proposal.addOrUpdateConnection(updatedConnection); + const config: Config = { network: proposal.toApi() }; + + updateConfig(config); + }; return ( { - return { - queryKey: ["network", "state"], - queryFn: fetchState, - }; -}; - -/** - * Returns a query for retrieving the list of known devices - */ -const devicesQuery = () => ({ - queryKey: ["network", "devices"], - queryFn: async () => { - const devices = await fetchDevices(); - return devices.map(Device.fromApi); - }, - staleTime: Infinity, -}); - -/** - * Returns a query for retrieving data for the given connection name - */ -const connectionQuery = (name: string) => ({ - queryKey: ["network", "connections", name], - queryFn: async () => { - const connection = await fetchConnection(name); - return Connection.fromApi(connection); - }, - staleTime: Infinity, -}); - -/** - * Returns a query for retrieving the list of known connections - */ -const connectionsQuery = () => ({ - queryKey: ["network", "connections"], - queryFn: async () => { - const connections = await fetchConnections(); - return connections.map(Connection.fromApi); - }, - staleTime: Infinity, -}); - -/** - * Returns a query for retrieving the list of known access points sortered by - * the signal strength. - */ -const accessPointsQuery = () => ({ - queryKey: ["network", "accessPoints"], - queryFn: async (): Promise => { - const accessPoints = await fetchAccessPoints(); - return accessPoints.map(AccessPoint.fromApi).sort((a, b) => b.strength - a.strength); - }, - // FIXME: Infinity vs 1second - staleTime: 1000, -}); - /** * Hook that builds a mutation to update a network connection * @@ -121,54 +53,6 @@ const useConfigMutation = () => { return useMutation(query); }; -/** - * Hook that provides a mutation for toggling the "persistent" state of a network - * connection. - * - * This hook uses optimistic updates to immediately reflect the change in the UI - * before the mutation completes. If the mutation fails, it will rollback to the - * previous state. - */ -const useConnectionPersistMutation = () => { - const queryClient = useQueryClient(); - const query = { - mutationFn: (connection: Connection) => { - return persist(connection.id, !connection.persistent); - }, - onMutate: async (connection: Connection) => { - // Get the current list of cached connections - const previousConnections: Connection[] = queryClient.getQueryData([ - "network", - "connections", - ]); - - // Optimistically toggle the 'persistent' status of the matching connection - const updatedConnections = previousConnections.map((cachedConnection) => { - if (connection.id !== cachedConnection.id) return cachedConnection; - - const { id, ...nextConnection } = cachedConnection; - return new Connection(id, { ...nextConnection, persistent: !cachedConnection.persistent }); - }); - - // Update the cached data with the optimistically updated connections - queryClient.setQueryData(["network", "connections"], updatedConnections); - - // Return the previous state for potential rollback - return { previousConnections }; - }, - - /** - * Called if the mutation fails for whatever reason. Rolls back the cache to - * the previous state. - */ - onError: (context: { previousConnections: Connection[] }) => { - queryClient.setQueryData(["network", "connections"], context.previousConnections); - }, - }; - - return useMutation(query); -}; - /** * Hook that returns a useEffect to listen for NetworkChanged events * @@ -256,8 +140,9 @@ const useConnection = (name: string) => { * Returns the general state of the network. */ const useNetworkState = (): NetworkGeneralState => { - const { data } = useSuspenseQuery(stateQuery()); - return data; + const { state } = useNetworkProposal(); + + return state; }; /** @@ -284,9 +169,8 @@ const useConnections = (): Connection[] => { const useWifiNetworks = () => { const knownSsids: string[] = []; - const devices = useNetworkDevices(); + const { devices, accessPoints } = useNetworkSystem(); const connections = useConnections(); - const { data: accessPoints } = useSuspenseQuery(accessPointsQuery()); return accessPoints .filter((ap: AccessPoint) => { @@ -320,14 +204,8 @@ const useWifiNetworks = () => { }; export { - stateQuery, - devicesQuery, - connectionQuery, - connectionsQuery, - accessPointsQuery, useConnections, useConfigMutation, - useConnectionPersistMutation, useConnection, useNetworkDevices, useNetworkState, From bb17a26e8c130630bee34d6a14a8f085470b0220 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Wed, 12 Nov 2025 09:11:32 +0000 Subject: [PATCH 17/17] Remove leftover --- rust/agama-network/src/model.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/rust/agama-network/src/model.rs b/rust/agama-network/src/model.rs index 6a8c5d7458..061a814c8a 100644 --- a/rust/agama-network/src/model.rs +++ b/rust/agama-network/src/model.rs @@ -300,20 +300,6 @@ impl NetworkState { )), } } - - pub fn ports_for(&self, uuid: Uuid) -> Vec { - self.connections - .iter() - .filter(|c| c.controller == Some(uuid)) - .map(|c| { - if let Some(interface) = c.interface.to_owned() { - interface - } else { - c.clone().id - } - }) - .collect() - } } #[cfg(test)]