diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9d07e9219b..e1aece7139 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", @@ -238,7 +240,9 @@ version = "0.1.0" dependencies = [ "agama-locale-data", "async-trait", + "cidr", "gettext-rs", + "macaddr", "serde", "serde_json", "serde_with", diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 33e06eab27..8c5df5597e 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -336,6 +336,29 @@ "type": "object", "additionalProperties": false, "properties": { + "state": { + "title": "Network general settings", + "type": "object", + "properties": { + "connectivity": { + "title": "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" + } + } + }, "connections": { "title": "Network connections to be defined", "type": "array", 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..9527d212fd 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,20 @@ impl NetworkStore { // TODO: read the settings from the service pub async fn load(&self) -> NetworkStoreResult { - let connections = self.network_client.connections().await?; - Ok(NetworkSettings { connections }) + let connections = NetworkConnectionsCollection(self.network_client.connections().await?); + + Ok(NetworkSettings { + connections, + ..Default::default() + }) } 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 +136,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/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..eaacf002f3 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.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::{l10n, message, storage}; +use crate::{l10n, message, network, storage}; use agama_utils::{ actor::{self, Actor, Handler, MessageHandler}, api::{ @@ -29,6 +29,7 @@ use agama_utils::{ }; use async_trait::async_trait; use merge_struct::merge; +use network::NetworkSystemClient; use serde_json::Value; use tokio::sync::broadcast; @@ -50,10 +51,13 @@ pub enum Error { Questions(#[from] question::service::Error), #[error(transparent)] Progress(#[from] progress::service::Error), + #[error(transparent)] + Network(#[from] network::NetworkSystemError), } pub struct Service { l10n: Handler, + network: NetworkSystemClient, storage: Handler, issues: Handler, progress: Handler, @@ -66,6 +70,7 @@ pub struct Service { impl Service { pub fn new( l10n: Handler, + network: NetworkSystemClient, storage: Handler, issues: Handler, progress: Handler, @@ -74,6 +79,7 @@ impl Service { ) -> Self { Self { l10n, + network, storage, issues, progress, @@ -147,7 +153,13 @@ 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().await?; + + Ok(SystemInfo { + l10n, + network, + storage, + }) } } @@ -159,10 +171,13 @@ 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 = self.network.get_config().await?; let storage = self.storage.call(storage::message::GetConfig).await?; + Ok(Config { l10n: Some(l10n), - questions, + questions: questions, + network: Some(network), storage, }) } @@ -196,11 +211,28 @@ 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(()) } } +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. @@ -209,6 +241,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)?; + let config = merge_network(config, message.config); if let Some(l10n) = &config.l10n { self.l10n @@ -228,6 +261,10 @@ impl MessageHandler for Service { .await?; } + if let Some(network) = &config.network { + self.network.update_config(network.clone()).await?; + } + self.config = config; Ok(()) } @@ -239,7 +276,13 @@ 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 = self.network.get_proposal().await?; + + Ok(Some(Proposal { + l10n, + network, + storage, + })) } } diff --git a/rust/agama-manager/src/start.rs b/rust/agama-manager/src/start.rs index 98acd79976..60537f10da 100644 --- a/rust/agama-manager/src/start.rs +++ b/rust/agama-manager/src/start.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::{l10n, service::Service, storage}; +use crate::{l10n, network, service::Service, storage}; use agama_utils::{ actor::{self, Handler}, api::event, @@ -35,6 +35,8 @@ pub enum Error { L10n(#[from] l10n::start::Error), #[error(transparent)] Storage(#[from] storage::start::Error), + #[error(transparent)] + Network(#[from] network::start::Error), } /// Starts the manager service. @@ -51,8 +53,8 @@ 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 service = Service::new(l10n, storage, issues, progress, questions, events); + let network = network::start().await?; + 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..4d4462f460 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}; +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; -use super::{error::NetworkStateError, model::GeneralState, NetworkAdapterError}; +use super::{error::NetworkStateError, NetworkAdapterError}; pub type Responder = oneshot::Sender; pub type ControllerConnection = (Connection, Vec); @@ -42,6 +43,15 @@ pub enum Action { GetConnection(String, Responder>), /// Gets a connection by its Uuid GetConnectionByUuid(Uuid, Responder>), + /// Gets the internal state of the network configuration + GetConfig(Responder), + /// Gets the internal state of the network configuration proposal + GetProposal(Responder), + /// Updates the internal state of the network configuration applying the changes to the system + UpdateConfig(Box, Responder>), + /// Gets the current network system configuration containing connections, devices, access_points and + /// also the general state + GetSystem(Responder), /// Gets a connection GetConnections(Responder>), /// Gets a controller connection 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/lib.rs b/rust/agama-network/src/lib.rs index 01b992bc03..735ca6ca96 100644 --- a/rust/agama-network/src/lib.rs +++ b/rust/agama-network/src/lib.rs @@ -27,7 +27,8 @@ pub mod adapter; pub mod error; pub mod model; mod nm; -pub mod settings; +pub mod start; +pub use start::start; mod system; pub mod types; diff --git a/rust/agama-network/src/model.rs b/rust/agama-network/src/model.rs index e5a2eb28ed..061a814c8a 100644 --- a/rust/agama-network/src/model.rs +++ b/rust/agama-network/src/model.rs @@ -23,13 +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::settings::{ - BondSettings, BridgeSettings, IEEE8021XSettings, NetworkConnection, VlanSettings, - WirelessSettings, -}; -use crate::types::{BondMode, ConnectionState, DeviceState, DeviceType, Status, 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}; @@ -37,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,7 +66,8 @@ pub struct NetworkState { } impl NetworkState { - /// Returns a NetworkState struct with the given devices and connections. + /// Returns a NetworkState struct with the given general_state, access_points, devices + /// and connections. /// /// * `general_state`: General network configuration /// * `access_points`: Access points to include in the state. @@ -144,6 +139,7 @@ impl NetworkState { self.devices.iter_mut().find(|c| c.name == name) } + /// Returns the controller's connection for the givne connection Uuid. pub fn get_controlled_by(&mut self, uuid: Uuid) -> Vec<&Connection> { let uuid = Some(uuid); self.connections @@ -164,6 +160,58 @@ impl NetworkState { Ok(()) } + /// Updates the current [NetworkState] with the configuration provided. + /// + /// The config could contain a [NetworkConnectionsCollection] to be updated, in case of + /// provided it will iterate over the connections adding or updating them. + /// + /// If the general state is provided it will sets the options given. + pub fn update_state(&mut self, config: Config) -> Result<(), NetworkStateError> { + if let Some(connections) = config.connections { + let mut collection: ConnectionCollection = connections.clone().try_into()?; + for conn in collection.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 { + 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(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(()) + } + /// Updates a connection with a new one. /// /// It uses the `id` to decide which connection to update. @@ -260,57 +308,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(); @@ -461,9 +458,7 @@ 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")] +#[derive(Clone, Debug, Default)] pub struct GeneralState { pub hostname: String, pub connectivity: bool, @@ -472,37 +467,6 @@ pub struct GeneralState { pub networking_enabled: bool, // pub network_state: NMSTATE } -/// Access Point -#[serde_as] -#[derive(Default, Debug, Clone, Serialize, 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, -} - -/// 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] @@ -806,274 +770,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 { @@ -1087,125 +783,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] @@ -1742,6 +1319,144 @@ 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.iter() + .filter(|c| c.controller == Some(uuid)) + .map(|c| c.interface.as_ref().unwrap_or(&c.id).clone()) + .collect() + } + + fn iter(&self) -> impl Iterator { + self.0.iter() + } + + fn iter_mut(&mut self) -> impl Iterator { + self.0.iter_mut() + } +} + +impl TryFrom for NetworkConnectionsCollection { + type Error = NetworkStateError; + + fn try_from(collection: ConnectionCollection) -> Result { + let network_connections = collection + .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 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 mut conn = conns + .iter() + .find(|c| c.id == port || c.interface.as_ref() == Some(&port)) + .cloned() + .unwrap_or_else(|| Connection::new(port, DeviceType::Ethernet)); + conn.controller = Some(uuid); + conns.push(conn); + } + + Ok(ConnectionCollection(conns)) + } +} + +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 Config { + type Error = NetworkStateError; + + fn try_from(state: NetworkState) -> Result { + let connections: NetworkConnectionsCollection = + ConnectionCollection(state.connections).try_into()?; + + Ok(Config { + connections: Some(connections), + state: Some(state.general_state.try_into()?), + }) + } +} + +impl TryFrom for SystemInfo { + type Error = NetworkStateError; + + fn try_from(state: NetworkState) -> Result { + let connections: NetworkConnectionsCollection = + ConnectionCollection(state.connections).try_into()?; + + Ok(SystemInfo { + access_points: state.access_points, + connections, + devices: state.devices, + state: state.general_state.try_into()?, + }) + } +} + +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: state.general_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..fa216c1dad 100644 --- a/rust/agama-network/src/nm/builder.rs +++ b/rust/agama-network/src/nm/builder.rs @@ -20,13 +20,12 @@ //! 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::{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..2268dd014d 100644 --- a/rust/agama-network/src/nm/client.rs +++ b/rust/agama-network/src/nm/client.rs @@ -35,10 +35,9 @@ use super::proxies::{ SettingsProxy, WirelessProxy, }; use crate::model::{ - AccessPoint, Connection, ConnectionConfig, Device, GeneralState, SecurityProtocol, - NOT_COPY_NETWORK_PATH, + Connection, ConnectionConfig, GeneralState, SecurityProtocol, NOT_COPY_NETWORK_PATH, }; -use crate::types::{AddFlags, ConnectionFlags, DeviceType, UpdateFlags, SSID}; +use crate::types::{AccessPoint, AddFlags, ConnectionFlags, Device, DeviceType, UpdateFlags, SSID}; use agama_utils::dbus::get_optional_property; use semver::Version; use uuid::Uuid; @@ -159,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?; @@ -167,6 +167,7 @@ impl<'a> NetworkManagerClient<'a> { let wpa_flags = wproxy.wpa_flags().await?; points.push(AccessPoint { + device, ssid, hw_address, strength, @@ -439,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?; @@ -453,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 c983180840..13dd8c66c3 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, }; @@ -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 @@ -1573,7 +1575,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 +1584,7 @@ mod test { }, error::NmError, }, + types::*, }; use cidr::IpInet; use macaddr::MacAddr6; 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..141ca193e2 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}; @@ -359,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/start.rs b/rust/agama-network/src/start.rs new file mode 100644 index 0000000000..5f27c7f8b2 --- /dev/null +++ b/rust/agama-network/src/start.rs @@ -0,0 +1,8 @@ +pub use crate::error::Error; +use crate::{NetworkManagerAdapter, NetworkSystem, NetworkSystemClient}; + +pub async fn start() -> Result { + let system = NetworkSystem::::for_network_manager().await; + + Ok(system.start().await?) +} diff --git a/rust/agama-network/src/system.rs b/rust/agama-network/src/system.rs index 64a80cc623..a987457be6 100644 --- a/rust/agama-network/src/system.rs +++ b/rust/agama-network/src/system.rs @@ -21,11 +21,9 @@ use crate::{ action::Action, error::NetworkStateError, - model::{ - AccessPoint, Connection, Device, GeneralState, NetworkChange, NetworkState, StateConfig, - }, - types::DeviceType, - Adapter, NetworkAdapterError, + model::{Connection, GeneralState, NetworkChange, NetworkState, StateConfig}, + types::{AccessPoint, Config, Device, DeviceType, Proposal, SystemInfo}, + Adapter, NetworkAdapterError, NetworkManagerAdapter, }; use std::error::Error; use tokio::sync::{ @@ -87,6 +85,15 @@ impl NetworkSystem { Self { adapter } } + /// Returns a new instance of the network configuration system using the [NetworkManagerAdapter] for the system. + pub async fn for_network_manager() -> NetworkSystem> { + let adapter = NetworkManagerAdapter::from_system() + .await + .expect("Could not connect to NetworkManager"); + + NetworkSystem::new(adapter) + } + /// Starts the network configuration service and returns a client for communication purposes. /// /// This function starts the server (using [NetworkSystemServer]) on a separate @@ -164,6 +171,36 @@ impl NetworkSystemClient { Ok(rx.await?) } + /// Returns the cofiguration from the current network state as a [Config]. + pub async fn get_config(&self) -> Result { + let (tx, rx) = oneshot::channel(); + self.actions.send(Action::GetConfig(tx))?; + Ok(rx.await?) + } + + /// Returns the cofiguration from the current network state as a [Proposal]. + pub async fn get_proposal(&self) -> Result { + let (tx, rx) = oneshot::channel(); + self.actions.send(Action::GetProposal(tx))?; + Ok(rx.await?) + } + + /// Updates the current network state based on the configuration given. + 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?) + } + + /// Reads the current system network configuration returning it directly + pub async fn get_system(&self) -> Result { + let (tx, rx) = oneshot::channel(); + self.actions.send(Action::GetSystem(tx))?; + Ok(rx.await?) + } + /// Adds a new connection. pub async fn add_connection(&self, connection: Connection) -> Result<(), NetworkSystemError> { let (tx, rx) = oneshot::channel(); @@ -310,6 +347,23 @@ impl NetworkSystemServer { let conn = self.state.get_connection_by_uuid(uuid); tx.send(conn.cloned()).unwrap(); } + Action::GetSystem(tx) => { + let result = self.read().await?.try_into()?; + tx.send(result).unwrap(); + } + 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(); + } + Action::UpdateConfig(config, tx) => { + let result = self.state.update_state(*config); + + tx.send(result).unwrap(); + } Action::GetConnections(tx) => { let connections = self .state @@ -424,6 +478,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/types.rs b/rust/agama-network/src/types.rs index f063d63949..a1b78ad55a 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,171 +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)] -#[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())), - } - } -} // https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMSettingsConnectionFlags #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, utoipa::ToSchema)] @@ -232,159 +71,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/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/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/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/config.rs b/rust/agama-server/src/web/docs/config.rs index ef0c136a18..1ae7c030db 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,30 @@ 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::() 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-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/Cargo.toml b/rust/agama-utils/Cargo.toml index 15f4b79fb0..961d044bde 100644 --- a/rust/agama-utils/Cargo.toml +++ b/rust/agama-utils/Cargo.toml @@ -19,6 +19,8 @@ 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"] } +macaddr = { version = "1.0.1", features = ["serde_std"] } [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-server/src/network.rs b/rust/agama-utils/src/api/network.rs similarity index 70% rename from rust/agama-server/src/network.rs rename to rust/agama-utils/src/api/network.rs index 95e80f2639..75eb23f9a4 100644 --- a/rust/agama-server/src/network.rs +++ b/rust/agama-utils/src/api/network.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2024] SUSE LLC +// Copyright (c) [2025] SUSE LLC // // All Rights Reserved. // @@ -18,8 +18,17 @@ // 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; +//! This module contains all Agama public types that might be available over +//! the HTTP and WebSocket API. -pub use agama_lib::network::{ - model::NetworkState, Action, Adapter, NetworkAdapterError, NetworkManagerAdapter, NetworkSystem, -}; +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..f7f9d1d791 --- /dev/null +++ b/rust/agama-utils/src/api/network/config.rs @@ -0,0 +1,35 @@ +// 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, StateSettings}; +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: Option, + /// Network general settings + pub state: Option, +} 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..75d93c25f3 --- /dev/null +++ b/rust/agama-utils/src/api/network/proposal.rs @@ -0,0 +1,35 @@ +// 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, StateSettings}; +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, + /// General network settings + pub state: StateSettings, +} diff --git a/rust/agama-network/src/settings.rs b/rust/agama-utils/src/api/network/settings.rs similarity index 89% rename from rust/agama-network/src/settings.rs rename to rust/agama-utils/src/api/network/settings.rs index db9a4f6120..be354de3d9 100644 --- a/rust/agama-network/src/settings.rs +++ b/rust/agama-utils/src/api/network/settings.rs @@ -20,19 +20,37 @@ //! Representation of the network settings -use super::types::{DeviceState, DeviceType, Status}; -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, +} + +/// Network general settings for the installation like enabling wireless, networking and +/// allowing to enable or disable the copy of the network settings to the +/// target system +#[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)] @@ -196,7 +214,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, @@ -302,3 +320,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..f7d9d43b97 --- /dev/null +++ b/rust/agama-utils/src/api/network/system_info.rs @@ -0,0 +1,36 @@ +// 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::{AccessPoint, Device, NetworkConnectionsCollection, StateSettings}; +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 { + pub access_points: Vec, // networks or access_points shold be returned + /// Connections to use in the installation + pub connections: NetworkConnectionsCollection, + pub devices: Vec, + pub state: StateSettings, +} 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..29115dcc1a --- /dev/null +++ b/rust/agama-utils/src/api/network/types.rs @@ -0,0 +1,790 @@ +// 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 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; + +/// 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] +#[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); + +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"); + } + + #[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() + ); + } +} diff --git a/rust/agama-utils/src/api/proposal.rs b/rust/agama-utils/src/api/proposal.rs index 4b184c0913..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,6 +27,7 @@ use serde_json::Value; pub struct Proposal { #[serde(skip_serializing_if = "Option::is_none")] pub l10n: 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 0ae7e5b8b2..7bc787077e 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::api::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"))?;