diff --git a/rust/agama-lib/src/network/types.rs b/rust/agama-lib/src/network/types.rs index cb8f9695c8..e3ec61957a 100644 --- a/rust/agama-lib/src/network/types.rs +++ b/rust/agama-lib/src/network/types.rs @@ -1,10 +1,15 @@ +use cidr::errors::NetworkParseError; use serde::{Deserialize, Serialize}; -use std::{fmt, str}; +use std::{ + fmt, + str::{self, FromStr}, +}; use thiserror::Error; use zbus; /// Network device -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] pub struct Device { pub name: String, pub type_: DeviceType, @@ -25,13 +30,21 @@ impl fmt::Display for SSID { } } +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(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub enum DeviceType { Loopback = 0, Ethernet = 1, diff --git a/rust/agama-server/Cargo.toml b/rust/agama-server/Cargo.toml index 1e6cd6652f..739926c0a6 100644 --- a/rust/agama-server/Cargo.toml +++ b/rust/agama-server/Cargo.toml @@ -25,7 +25,7 @@ tokio-stream = "0.1.14" gettext-rs = { version = "0.7.0", features = ["gettext-system"] } regex = "1.10.2" once_cell = "1.18.0" -macaddr = "1.0" +macaddr = { version = "1.0", features = ["serde_std"] } async-trait = "0.1.75" axum = { version = "0.7.4", features = ["ws"] } serde_json = "1.0.113" diff --git a/rust/agama-server/src/network.rs b/rust/agama-server/src/network.rs index 536fdfdbf8..633397a3b6 100644 --- a/rust/agama-server/src/network.rs +++ b/rust/agama-server/src/network.rs @@ -45,6 +45,7 @@ pub mod error; pub mod model; mod nm; pub mod system; +pub mod web; pub use action::Action; pub use adapter::{Adapter, NetworkAdapterError}; diff --git a/rust/agama-server/src/network/action.rs b/rust/agama-server/src/network/action.rs index 3b98ee912f..ed29ec2a10 100644 --- a/rust/agama-server/src/network/action.rs +++ b/rust/agama-server/src/network/action.rs @@ -1,10 +1,10 @@ -use crate::network::model::Connection; +use crate::network::model::{AccessPoint, Connection, Device}; use agama_lib::network::types::DeviceType; use tokio::sync::oneshot; use uuid::Uuid; use zbus::zvariant::OwnedObjectPath; -use super::{error::NetworkStateError, NetworkAdapterError}; +use super::{error::NetworkStateError, model::GeneralState, NetworkAdapterError}; pub type Responder = oneshot::Sender; pub type ControllerConnection = (Connection, Vec); @@ -21,11 +21,18 @@ pub enum Action { DeviceType, Responder>, ), - /// Gets a connection + /// Add a new connection + NewConnection( + Connection, + Responder>, + ), + /// Gets a connection by its Uuid GetConnection(Uuid, Responder>), /// Gets a connection + GetConnections(Responder>), + /// Gets a connection path GetConnectionPath(Uuid, Responder>), - /// Gets a connection + /// Gets a connection path by id GetConnectionPathById(String, Responder>), /// Get connections paths GetConnectionsPaths(Responder>), @@ -34,8 +41,17 @@ pub enum Action { Uuid, Responder>, ), + /// Gets all scanned access points + GetAccessPoints(Responder>), + /// Gets a device by its name + GetDevice(String, Responder>), + /// Gets all the existent devices + GetDevices(Responder>), + /// Gets a device path + GetDevicePath(String, Responder>), /// Get devices paths GetDevicesPaths(Responder>), + GetGeneralState(Responder), /// Sets a controller's ports. It uses the Uuid of the controller and the IDs or interface names /// of the ports. SetPorts( @@ -43,10 +59,14 @@ pub enum Action { Box>, Responder>, ), - /// Update a connection (replacing the old one). + /// Updates a connection (replacing the old one). UpdateConnection(Box), + /// Updates the general network configuration + UpdateGeneralState(GeneralState), + /// Forces a wireless networks scan refresh + RefreshScan(Responder>), /// Remove the connection with the given Uuid. - RemoveConnection(Uuid), + RemoveConnection(Uuid, Responder>), /// Apply the current configuration. Apply(Responder>), } diff --git a/rust/agama-server/src/network/adapter.rs b/rust/agama-server/src/network/adapter.rs index 8aba3251db..c6002faee7 100644 --- a/rust/agama-server/src/network/adapter.rs +++ b/rust/agama-server/src/network/adapter.rs @@ -1,3 +1,4 @@ +use crate::network::model::StateConfig; use crate::network::NetworkState; use agama_lib::error::ServiceError; use async_trait::async_trait; @@ -16,7 +17,7 @@ pub enum NetworkAdapterError { /// A trait for the ability to read/write from/to a network service #[async_trait] pub trait Adapter { - async fn read(&self) -> Result; + async fn read(&self, config: StateConfig) -> Result; async fn write(&self, network: &NetworkState) -> Result<(), NetworkAdapterError>; } diff --git a/rust/agama-server/src/network/dbus/interfaces/connections.rs b/rust/agama-server/src/network/dbus/interfaces/connections.rs index 885aefb042..56a2e11363 100644 --- a/rust/agama-server/src/network/dbus/interfaces/connections.rs +++ b/rust/agama-server/src/network/dbus/interfaces/connections.rs @@ -21,7 +21,7 @@ pub struct Connections { impl Connections { /// Creates a Connections interface object. /// - /// * `objects`: Objects paths registry. + /// * `actions`: sending-half of a channel to send actions. pub fn new(actions: UnboundedSender) -> Self { Self { actions: Arc::new(Mutex::new(actions)), @@ -101,7 +101,10 @@ impl Connections { .parse() .map_err(|_| NetworkStateError::InvalidUuid(uuid.to_string()))?; let actions = self.actions.lock().await; - actions.send(Action::RemoveConnection(uuid)).unwrap(); + let (tx, rx) = oneshot::channel(); + actions.send(Action::RemoveConnection(uuid, tx)).unwrap(); + + rx.await.unwrap()?; Ok(()) } diff --git a/rust/agama-server/src/network/dbus/tree.rs b/rust/agama-server/src/network/dbus/tree.rs index a1872c2da9..76674d9a90 100644 --- a/rust/agama-server/src/network/dbus/tree.rs +++ b/rust/agama-server/src/network/dbus/tree.rs @@ -130,6 +130,10 @@ impl Tree { self.objects.devices_paths() } + pub fn device_path(&self, name: &str) -> Option { + self.objects.device_path(name).map(|o| o.into()) + } + /// Returns all connection paths. pub fn connections_paths(&self) -> Vec { self.objects.connections_paths() @@ -237,6 +241,12 @@ impl ObjectsRegistry { path } + /// Returns the path for a device. + /// + /// * `name`: device name. + pub fn device_path(&self, name: &str) -> Option { + self.devices.get(name).map(|p| p.as_ref()) + } /// Returns the path for a connection. /// /// * `uuid`: connection ID. diff --git a/rust/agama-server/src/network/model.rs b/rust/agama-server/src/network/model.rs index 551c353496..7428942da1 100644 --- a/rust/agama-server/src/network/model.rs +++ b/rust/agama-server/src/network/model.rs @@ -3,8 +3,11 @@ //! * 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::network::error::NetworkStateError; +use agama_lib::network::settings::{BondSettings, NetworkConnection, WirelessSettings}; use agama_lib::network::types::{BondMode, DeviceType, SSID}; use cidr::IpInet; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, skip_serializing_none, DisplayFromStr}; use std::{ collections::HashMap, default::Default, @@ -16,8 +19,29 @@ use thiserror::Error; use uuid::Uuid; use zbus::zvariant::Value; -#[derive(Default, Clone, Debug)] +#[derive(PartialEq)] +pub struct StateConfig { + pub access_points: bool, + pub devices: bool, + pub connections: bool, + pub general_state: bool, +} + +impl Default for StateConfig { + fn default() -> Self { + Self { + access_points: true, + devices: true, + connections: true, + general_state: true, + } + } +} + +#[derive(Default, Clone, Debug, utoipa::ToSchema)] pub struct NetworkState { + pub general_state: GeneralState, + pub access_points: Vec, pub devices: Vec, pub connections: Vec, } @@ -25,10 +49,19 @@ pub struct NetworkState { impl NetworkState { /// Returns a NetworkState struct with the given devices and connections. /// + /// * `general_state`: General network configuration + /// * `access_points`: Access points to include in the state. /// * `devices`: devices to include in the state. /// * `connections`: connections to include in the state. - pub fn new(devices: Vec, connections: Vec) -> Self { + pub fn new( + general_state: GeneralState, + access_points: Vec, + devices: Vec, + connections: Vec, + ) -> Self { Self { + general_state, + access_points, devices, connections, } @@ -367,18 +400,41 @@ mod tests { } } +/// Network state +#[serde_as] +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct GeneralState { + pub connectivity: bool, + pub wireless_enabled: bool, + pub networking_enabled: bool, // pub network_state: NMSTATE + // pub dns: GlobalDnsConfiguration +} + +/// Access Point +#[derive(Default, Debug, Clone, Serialize, utoipa::ToSchema)] +pub struct AccessPoint { + pub ssid: SSID, + pub hw_address: String, + pub strength: u8, + pub security_protocols: Vec, +} + /// Network device -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct Device { pub name: String, + #[serde(rename = "type")] pub type_: DeviceType, } -/// Represents an availble network connection. -#[derive(Debug, Clone, PartialEq)] +/// Represents a known network connection. +#[serde_as] +#[skip_serializing_none] +#[derive(Debug, Clone, PartialEq, Serialize, utoipa::ToSchema)] pub struct Connection { pub id: String, pub uuid: Uuid, + #[serde_as(as = "DisplayFromStr")] pub mac_address: MacAddress, pub ip_config: IpConfig, pub status: Status, @@ -459,7 +515,85 @@ impl Default for Connection { } } -#[derive(Default, Debug, PartialEq, Clone)] +impl TryFrom for Connection { + type Error = NetworkStateError; + + fn try_from(conn: NetworkConnection) -> Result { + let id = conn.clone().id; + let mut connection = Connection::new(id, conn.device_type()); + + if let Some(method) = conn.clone().method4 { + let method: Ipv4Method = method.parse().unwrap(); + connection.ip_config.method4 = method; + } + + if let Some(method) = conn.method6 { + let method: Ipv6Method = method.parse().unwrap(); + connection.ip_config.method6 = method; + } + + if let Some(wireless_config) = conn.wireless { + let config = WirelessConfig::try_from(wireless_config)?; + connection.config = config.into(); + } + + if let Some(bond_config) = conn.bond { + let config = BondConfig::try_from(bond_config)?; + connection.config = config.into(); + } + + connection.ip_config.nameservers = conn.nameservers; + connection.ip_config.gateway4 = conn.gateway4; + connection.ip_config.gateway6 = conn.gateway6; + connection.interface = conn.interface; + + Ok(connection) + } +} + +impl TryFrom for NetworkConnection { + type Error = NetworkStateError; + + fn try_from(conn: Connection) -> Result { + let id = conn.clone().id; + let mac = conn.mac_address.to_string(); + let method4 = Some(conn.ip_config.method4.to_string()); + let method6 = Some(conn.ip_config.method6.to_string()); + let mac_address = (!mac.is_empty()).then(|| mac); + let nameservers = conn.ip_config.nameservers.into(); + let addresses = conn.ip_config.addresses.into(); + let gateway4 = conn.ip_config.gateway4.into(); + let gateway6 = conn.ip_config.gateway6.into(); + let interface = conn.interface.into(); + + let mut connection = NetworkConnection { + id, + method4, + method6, + gateway4, + gateway6, + nameservers, + mac_address, + interface, + addresses, + ..Default::default() + }; + + match conn.config { + ConnectionConfig::Wireless(config) => { + connection.wireless = Some(WirelessSettings::try_from(config)?); + } + ConnectionConfig::Bond(config) => { + connection.bond = Some(BondSettings::try_from(config)?); + } + _ => {} + } + + Ok(connection) + } +} + +#[derive(Default, Debug, PartialEq, Clone, Serialize)] pub enum ConnectionConfig { #[default] Ethernet, @@ -471,7 +605,7 @@ pub enum ConnectionConfig { Bridge(BridgeConfig), } -#[derive(Default, Debug, PartialEq, Clone)] +#[derive(Default, Debug, PartialEq, Clone, Serialize)] pub enum PortConfig { #[default] None, @@ -494,7 +628,7 @@ impl From for ConnectionConfig { #[error("Invalid MAC address: {0}")] pub struct InvalidMacAddress(String); -#[derive(Debug, Default, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq, Serialize)] pub enum MacAddress { MacAddress(macaddr::MacAddr6), Preserve, @@ -554,7 +688,7 @@ impl From for zbus::fdo::Error { } } -#[derive(Debug, Default, Clone, Copy, PartialEq)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize)] pub enum Status { #[default] Up, @@ -562,11 +696,14 @@ pub enum Status { Removed, } -#[derive(Default, Debug, PartialEq, Clone)] +#[skip_serializing_none] +#[derive(Default, Debug, PartialEq, Clone, Serialize)] pub struct IpConfig { pub method4: Ipv4Method, pub method6: Ipv6Method, + #[serde(skip_serializing_if = "Vec::is_empty")] pub addresses: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] pub nameservers: Vec, pub gateway4: Option, pub gateway6: Option, @@ -574,11 +711,16 @@ pub struct IpConfig { pub routes6: Option>, } -#[derive(Debug, Default, PartialEq, Clone)] +#[skip_serializing_none] +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub struct MatchConfig { + #[serde(skip_serializing_if = "Vec::is_empty")] pub driver: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] pub interface: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] pub path: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] pub kernel: Vec, } @@ -586,7 +728,7 @@ pub struct MatchConfig { #[error("Unknown IP configuration method name: {0}")] pub struct UnknownIpMethod(String); -#[derive(Debug, Default, Copy, Clone, PartialEq)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Serialize)] pub enum Ipv4Method { #[default] Disabled = 0, @@ -621,7 +763,7 @@ impl FromStr for Ipv4Method { } } -#[derive(Debug, Default, Copy, Clone, PartialEq)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Serialize)] pub enum Ipv6Method { #[default] Disabled = 0, @@ -668,10 +810,12 @@ impl From for zbus::fdo::Error { } } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Serialize)] pub struct IpRoute { pub destination: IpInet, + #[serde(skip_serializing_if = "Option::is_none")] pub next_hop: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub metric: Option, } @@ -694,7 +838,7 @@ impl From<&IpRoute> for HashMap<&str, Value<'_>> { } } -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub enum VlanProtocol { #[default] IEEE802_1Q, @@ -727,22 +871,29 @@ impl fmt::Display for VlanProtocol { } } -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub struct VlanConfig { pub parent: String, pub id: u32, pub protocol: VlanProtocol, } -#[derive(Debug, Default, PartialEq, Clone)] +#[serde_as] +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub struct WirelessConfig { pub mode: WirelessMode, + #[serde_as(as = "DisplayFromStr")] pub ssid: SSID, + #[serde(skip_serializing_if = "Option::is_none")] pub password: Option, pub security: SecurityProtocol, + #[serde(skip_serializing_if = "Option::is_none")] pub band: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub channel: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub bssid: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub wep_security: Option, pub hidden: bool, } @@ -758,7 +909,37 @@ impl TryFrom for WirelessConfig { } } -#[derive(Debug, Default, Clone, Copy, PartialEq)] +impl TryFrom for WirelessConfig { + type Error = NetworkStateError; + + fn try_from(settings: WirelessSettings) -> Result { + let ssid = SSID(settings.ssid.as_bytes().into()); + let mode = WirelessMode::try_from(settings.mode.as_str())?; + let security = SecurityProtocol::try_from(settings.security.as_str())?; + Ok(WirelessConfig { + ssid, + mode, + security, + password: Some(settings.password), + ..Default::default() + }) + } +} + +impl TryFrom for WirelessSettings { + type Error = NetworkStateError; + + fn try_from(wireless: WirelessConfig) -> Result { + Ok(WirelessSettings { + ssid: wireless.ssid.to_string(), + mode: wireless.mode.to_string(), + security: wireless.security.to_string(), + password: wireless.password.unwrap_or_default(), + }) + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize)] pub enum WirelessMode { Unknown = 0, AdHoc = 1, @@ -796,7 +977,7 @@ impl fmt::Display for WirelessMode { } } -#[derive(Debug, Clone, Copy, Default, PartialEq)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize)] pub enum SecurityProtocol { #[default] WEP, // No encryption or WEP ("none") @@ -842,15 +1023,16 @@ impl TryFrom<&str> for SecurityProtocol { } } -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub struct WEPSecurity { pub auth_alg: WEPAuthAlg, pub wep_key_type: WEPKeyType, + #[serde(skip_serializing_if = "Vec::is_empty")] pub keys: Vec, pub wep_key_index: u32, } -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub enum WEPKeyType { #[default] Unknown = 0, @@ -871,7 +1053,7 @@ impl TryFrom for WEPKeyType { } } -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub enum WEPAuthAlg { #[default] Unset, @@ -906,7 +1088,7 @@ impl fmt::Display for WEPAuthAlg { } } -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize)] pub enum WirelessBand { A, // 5GHz BG, // 2.4GHz @@ -934,7 +1116,7 @@ impl TryFrom<&str> for WirelessBand { } } -#[derive(Debug, Default, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq, Serialize)] pub struct BondOptions(pub HashMap); impl TryFrom<&str> for BondOptions { @@ -967,7 +1149,7 @@ impl fmt::Display for BondOptions { } } -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub struct BondConfig { pub mode: BondMode, pub options: BondOptions, @@ -984,18 +1166,52 @@ impl TryFrom for BondConfig { } } -#[derive(Debug, Default, PartialEq, Clone)] +impl TryFrom for BondConfig { + type Error = NetworkStateError; + + fn try_from(settings: BondSettings) -> Result { + let mode = BondMode::try_from(settings.mode.as_str()) + .map_err(|_| NetworkStateError::InvalidBondMode(settings.mode))?; + let mut options = BondOptions::default(); + if let Some(setting_options) = settings.options { + options = BondOptions::try_from(setting_options.as_str())?; + } + + Ok(BondConfig { mode, options }) + } +} + +impl TryFrom for BondSettings { + type Error = NetworkStateError; + + fn try_from(bond: BondConfig) -> Result { + Ok(BondSettings { + mode: bond.mode.to_string(), + options: Some(bond.options.to_string()), + ..Default::default() + }) + } +} + +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub struct BridgeConfig { pub stp: bool, + #[serde(skip_serializing_if = "Option::is_none")] pub priority: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub forward_delay: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub hello_time: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub max_age: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub ageing_time: Option, } -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub struct BridgePortConfig { + #[serde(skip_serializing_if = "Option::is_none")] pub priority: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub path_cost: Option, } diff --git a/rust/agama-server/src/network/nm/adapter.rs b/rust/agama-server/src/network/nm/adapter.rs index 0df720e5ba..fcd5160a9f 100644 --- a/rust/agama-server/src/network/nm/adapter.rs +++ b/rust/agama-server/src/network/nm/adapter.rs @@ -1,11 +1,13 @@ use crate::network::{ - model::{Connection, NetworkState}, + model::{Connection, NetworkState, StateConfig}, nm::NetworkManagerClient, Adapter, NetworkAdapterError, }; use agama_lib::error::ServiceError; use async_trait::async_trait; +use core::time; use log; +use std::thread; /// An adapter for NetworkManager pub struct NetworkManagerAdapter<'a> { @@ -29,18 +31,47 @@ impl<'a> NetworkManagerAdapter<'a> { #[async_trait] impl<'a> Adapter for NetworkManagerAdapter<'a> { - async fn read(&self) -> Result { - let devices = self + async fn read(&self, config: StateConfig) -> Result { + let general_state = self .client - .devices() + .general_state() .await .map_err(NetworkAdapterError::Read)?; - let connections = self - .client - .connections() - .await - .map_err(NetworkAdapterError::Read)?; - Ok(NetworkState::new(devices, connections)) + + let mut state = NetworkState::default(); + + if config.devices { + state.devices = self + .client + .devices() + .await + .map_err(NetworkAdapterError::Read)?; + } + + if config.connections { + state.connections = self + .client + .connections() + .await + .map_err(NetworkAdapterError::Read)?; + } + + if config.access_points && general_state.wireless_enabled { + if !config.devices && !config.connections { + self.client + .request_scan() + .await + .map_err(NetworkAdapterError::Read)?; + thread::sleep(time::Duration::from_secs(1)); + }; + state.access_points = self + .client + .access_points() + .await + .map_err(NetworkAdapterError::Read)?; + } + + Ok(state) } /// Writes the connections to NetworkManager. @@ -51,13 +82,34 @@ impl<'a> Adapter for NetworkManagerAdapter<'a> { /// /// * `network`: network model. async fn write(&self, network: &NetworkState) -> Result<(), NetworkAdapterError> { - let old_state = self.read().await?; + let old_state = self.read(StateConfig::default()).await?; let checkpoint = self .client .create_checkpoint() .await .map_err(NetworkAdapterError::Checkpoint)?; + log::info!("Updating the general state {:?}", &network.general_state); + + let result = self + .client + .update_general_state(&network.general_state) + .await; + + if let Err(e) = result { + self.client + .rollback_checkpoint(&checkpoint.as_ref()) + .await + .map_err(NetworkAdapterError::Checkpoint)?; + + log::error!( + "Could not update the general state {:?}: {}", + &network.general_state, + &e + ); + return Err(NetworkAdapterError::Write(e)); + } + for conn in ordered_connections(network) { if !Self::is_writable(conn) { continue; @@ -88,6 +140,7 @@ impl<'a> Adapter for NetworkManagerAdapter<'a> { return Err(NetworkAdapterError::Write(e)); } } + self.client .destroy_checkpoint(&checkpoint.as_ref()) .await diff --git a/rust/agama-server/src/network/nm/client.rs b/rust/agama-server/src/network/nm/client.rs index 8396d9d649..81dc8a41c3 100644 --- a/rust/agama-server/src/network/nm/client.rs +++ b/rust/agama-server/src/network/nm/client.rs @@ -6,9 +6,13 @@ use super::dbus::{ merge_dbus_connections, }; use super::model::NmDeviceType; -use super::proxies::{ConnectionProxy, DeviceProxy, NetworkManagerProxy, SettingsProxy}; -use crate::network::model::{Connection, Device}; +use super::proxies::{ + AccessPointProxy, ConnectionProxy, DeviceProxy, NetworkManagerProxy, SettingsProxy, + WirelessProxy, +}; +use crate::network::model::{AccessPoint, Connection, Device, GeneralState}; use agama_lib::error::ServiceError; +use agama_lib::network::types::{DeviceType, SSID}; use log; use uuid::Uuid; use zbus; @@ -41,7 +45,94 @@ impl<'a> NetworkManagerClient<'a> { connection, }) } + /// Returns the general state + pub async fn general_state(&self) -> Result { + let wireless_enabled = self.nm_proxy.wireless_enabled().await?; + let networking_enabled = self.nm_proxy.networking_enabled().await?; + // TODO:: Allow to set global DNS configuration + // let global_dns_configuration = self.nm_proxy.global_dns_configuration().await?; + // Fixme: save as NMConnectivityState enum + let connectivity = self.nm_proxy.connectivity().await? == 4; + + Ok(GeneralState { + wireless_enabled, + networking_enabled, + connectivity, + }) + } + + /// Updates the general state + pub async fn update_general_state(&self, state: &GeneralState) -> Result<(), ServiceError> { + let wireless_enabled = self.nm_proxy.wireless_enabled().await?; + if wireless_enabled != state.wireless_enabled { + self.nm_proxy + .set_wireless_enabled(state.wireless_enabled) + .await?; + }; + + Ok(()) + } + + /// Returns the list of access points. + pub async fn request_scan(&self) -> Result<(), ServiceError> { + for path in &self.nm_proxy.get_devices().await? { + let proxy = DeviceProxy::builder(&self.connection) + .path(path.as_str())? + .build() + .await?; + + let device_type = NmDeviceType(proxy.device_type().await?).try_into(); + if let Ok(DeviceType::Wireless) = device_type { + let wproxy = WirelessProxy::builder(&self.connection) + .path(path.as_str())? + .build() + .await?; + wproxy.request_scan(HashMap::new()).await?; + } + } + + Ok(()) + } + + /// Returns the list of access points. + pub async fn access_points(&self) -> Result, ServiceError> { + let mut points = vec![]; + for path in &self.nm_proxy.get_devices().await? { + let proxy = DeviceProxy::builder(&self.connection) + .path(path.as_str())? + .build() + .await?; + + let device_type = NmDeviceType(proxy.device_type().await?).try_into(); + if let Ok(DeviceType::Wireless) = device_type { + let wproxy = WirelessProxy::builder(&self.connection) + .path(path.as_str())? + .build() + .await?; + + for ap_path in wproxy.access_points().await? { + let wproxy = AccessPointProxy::builder(&self.connection) + .path(ap_path.as_str())? + .build() + .await?; + + let ssid = SSID(wproxy.ssid().await?); + let hw_address = wproxy.hw_address().await?; + let strength = wproxy.strength().await?; + + points.push(AccessPoint { + ssid, + hw_address, + strength, + security_protocols: vec![], + }) + } + } + } + + Ok(points) + } /// Returns the list of network devices. pub async fn devices(&self) -> Result, ServiceError> { let mut devs = vec![]; diff --git a/rust/agama-server/src/network/nm/proxies.rs b/rust/agama-server/src/network/nm/proxies.rs index fd933a3d9d..56927caa4d 100644 --- a/rust/agama-server/src/network/nm/proxies.rs +++ b/rust/agama-server/src/network/nm/proxies.rs @@ -252,6 +252,115 @@ trait NetworkManager { fn wwan_hardware_enabled(&self) -> zbus::Result; } +#[dbus_proxy( + interface = "org.freedesktop.NetworkManager.AccessPoint", + default_service = "org.freedesktop.NetworkManager", + default_path = "/org/freedesktop/NetworkManager/AccessPoint/1" +)] +trait AccessPoint { + /// Flags property + #[dbus_proxy(property)] + fn flags(&self) -> zbus::Result; + + /// Frequency property + #[dbus_proxy(property)] + fn frequency(&self) -> zbus::Result; + + /// HwAddress property + #[dbus_proxy(property)] + fn hw_address(&self) -> zbus::Result; + + /// LastSeen property + #[dbus_proxy(property)] + fn last_seen(&self) -> zbus::Result; + + /// MaxBitrate property + #[dbus_proxy(property)] + fn max_bitrate(&self) -> zbus::Result; + + /// Mode property + #[dbus_proxy(property)] + fn mode(&self) -> zbus::Result; + + /// RsnFlags property + #[dbus_proxy(property)] + fn rsn_flags(&self) -> zbus::Result; + + /// Ssid property + #[dbus_proxy(property)] + fn ssid(&self) -> zbus::Result>; + + /// Strength property + #[dbus_proxy(property)] + fn strength(&self) -> zbus::Result; + + /// WpaFlags property + #[dbus_proxy(property)] + fn wpa_flags(&self) -> zbus::Result; +} + +/// # DBus interface proxies for: `org.freedesktop.NetworkManager.Device.Wireless` +/// +/// This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data. +#[dbus_proxy( + interface = "org.freedesktop.NetworkManager.Device.Wireless", + default_service = "org.freedesktop.NetworkManager", + default_path = "/org/freedesktop/NetworkManager/Devices/5" +)] +trait Wireless { + /// GetAllAccessPoints method + fn get_all_access_points(&self) -> zbus::Result>; + + /// RequestScan method + fn request_scan( + &self, + options: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, + ) -> zbus::Result<()>; + + /// AccessPointAdded signal + #[dbus_proxy(signal)] + fn access_point_added(&self, access_point: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; + + /// AccessPointRemoved signal + #[dbus_proxy(signal)] + fn access_point_removed( + &self, + access_point: zbus::zvariant::ObjectPath<'_>, + ) -> zbus::Result<()>; + + /// AccessPoints property + #[dbus_proxy(property)] + fn access_points(&self) -> zbus::Result>; + + /// ActiveAccessPoint property + #[dbus_proxy(property)] + fn active_access_point(&self) -> zbus::Result; + + /// Bitrate property + #[dbus_proxy(property)] + fn bitrate(&self) -> zbus::Result; + + /// HwAddress property + #[dbus_proxy(property)] + fn hw_address(&self) -> zbus::Result; + + /// LastScan property + #[dbus_proxy(property)] + fn last_scan(&self) -> zbus::Result; + + /// Mode property + #[dbus_proxy(property)] + fn mode(&self) -> zbus::Result; + + /// PermHwAddress property + #[dbus_proxy(property)] + fn perm_hw_address(&self) -> zbus::Result; + + /// WirelessCapabilities property + #[dbus_proxy(property)] + fn wireless_capabilities(&self) -> zbus::Result; +} + /// # DBus interface proxies for: `org.freedesktop.NetworkManager.Device` /// /// This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data. diff --git a/rust/agama-server/src/network/system.rs b/rust/agama-server/src/network/system.rs index a347caca6a..afb0cf2b69 100644 --- a/rust/agama-server/src/network/system.rs +++ b/rust/agama-server/src/network/system.rs @@ -1,4 +1,4 @@ -use super::{error::NetworkStateError, NetworkAdapterError}; +use super::{error::NetworkStateError, model::StateConfig, NetworkAdapterError}; use crate::network::{dbus::Tree, model::Connection, Action, Adapter, NetworkState}; use agama_lib::network::types::DeviceType; use std::{error::Error, sync::Arc}; @@ -37,7 +37,7 @@ impl NetworkSystem { /// Writes the network configuration. pub async fn write(&mut self) -> Result<(), NetworkAdapterError> { self.adapter.write(&self.state).await?; - self.state = self.adapter.read().await?; + self.state = self.adapter.read(StateConfig::default()).await?; Ok(()) } @@ -50,7 +50,7 @@ impl NetworkSystem { /// Populates the D-Bus tree with the known devices and connections. pub async fn setup(&mut self) -> Result<(), Box> { - self.state = self.adapter.read().await?; + self.state = self.adapter.read(StateConfig::default()).await?; let mut tree = self.tree.lock().await; tree.set_connections(&mut self.state.connections).await?; tree.set_devices(&self.state.devices).await?; @@ -75,10 +75,36 @@ impl NetworkSystem { let result = self.add_connection_action(name, ty).await; tx.send(result).unwrap(); } + Action::RefreshScan(tx) => { + let state = self + .adapter + .read(StateConfig { + access_points: true, + ..Default::default() + }) + .await?; + self.state.general_state = state.general_state; + self.state.access_points = state.access_points; + tx.send(Ok(())).unwrap(); + } + Action::GetAccessPoints(tx) => { + tx.send(self.state.access_points.clone()).unwrap(); + } + Action::NewConnection(conn, tx) => { + let result = self.new_connection_action(conn).await; + tx.send(result).unwrap(); + } + Action::GetGeneralState(tx) => { + let config = self.state.general_state.clone(); + tx.send(config.clone()).unwrap(); + } Action::GetConnection(uuid, tx) => { let conn = self.state.get_connection_by_uuid(uuid); tx.send(conn.cloned()).unwrap(); } + Action::GetConnections(tx) => { + tx.send(self.state.connections.clone()).unwrap(); + } Action::GetConnectionPath(uuid, tx) => { let tree = self.tree.lock().await; let path = tree.connection_path(uuid); @@ -92,6 +118,18 @@ impl NetworkSystem { let result = self.get_controller_action(uuid); tx.send(result).unwrap() } + Action::GetDevice(name, tx) => { + let device = self.state.get_device(name.as_str()); + tx.send(device.cloned()).unwrap(); + } + Action::GetDevicePath(name, tx) => { + let tree = self.tree.lock().await; + let path = tree.device_path(name.as_str()); + tx.send(path).unwrap(); + } + Action::GetDevices(tx) => { + tx.send(self.state.devices.clone()).unwrap(); + } Action::GetDevicesPaths(tx) => { let tree = self.tree.lock().await; tx.send(tree.devices_paths()).unwrap(); @@ -107,10 +145,15 @@ impl NetworkSystem { Action::UpdateConnection(conn) => { self.state.update_connection(*conn)?; } - Action::RemoveConnection(uuid) => { + Action::UpdateGeneralState(general_state) => { + self.state.general_state = general_state; + } + Action::RemoveConnection(uuid, tx) => { let mut tree = self.tree.lock().await; tree.remove_connection(uuid).await?; - self.state.remove_connection(uuid)?; + let result = self.state.remove_connection(uuid); + + tx.send(result).unwrap(); } Action::Apply(tx) => { let result = self.write().await; @@ -155,6 +198,20 @@ impl NetworkSystem { Ok(path) } + async fn new_connection_action( + &mut self, + conn: Connection, + ) -> Result { + // TODO: handle tree handling problems + self.state.add_connection(conn.clone())?; + let mut tree = self.tree.lock().await; + let path = tree + .add_connection(&conn) + .await + .expect("Could not update the D-Bus tree"); + Ok(path) + } + fn set_ports_action( &mut self, uuid: Uuid, diff --git a/rust/agama-server/src/network/web.rs b/rust/agama-server/src/network/web.rs new file mode 100644 index 0000000000..a29624fdd9 --- /dev/null +++ b/rust/agama-server/src/network/web.rs @@ -0,0 +1,243 @@ +//! This module implements the web API for the network module. + +use crate::error::Error; +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Response}, + routing::{delete, get, put}, + Json, Router, +}; + +use super::{error::NetworkStateError, model::GeneralState, Action, Adapter}; + +use crate::network::{model::Connection, model::Device, NetworkSystem}; +use agama_lib::error::ServiceError; +use agama_lib::network::settings::NetworkConnection; +use uuid::Uuid; + +use serde_json::json; +use thiserror::Error; +use tokio::sync::{mpsc::UnboundedSender, oneshot}; + +#[derive(Error, Debug)] +pub enum NetworkError { + #[error("Unknown connection id: {0}")] + UnknownConnection(String), + #[error("Cannot translate: {0}")] + CannotTranslate(#[from] Error), + #[error("Cannot update configuration: {0}")] + CannotUpdate(Uuid), + #[error("Cannot apply configuration")] + CannotApplyConfig, + #[error("Network state error: {0}")] + Error(#[from] NetworkStateError), +} + +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 NetworkState { + actions: UnboundedSender, +} + +/// Sets up and returns the axum service for the network module. +/// +/// * `dbus`: zbus Connection. +pub async fn network_service( + dbus: zbus::Connection, + adapter: T, +) -> Result { + let mut network = NetworkSystem::new(dbus.clone(), adapter); + + let state = NetworkState { + actions: network.actions_tx(), + }; + + tokio::spawn(async move { + network + .setup() + .await + .expect("Could not set up the D-Bus tree"); + + network.listen().await; + }); + + 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), + ) + .route("/devices", get(devices)) + .route("/system/apply", put(apply)) + .route("/wifi", get(wifi_networks)) + .with_state(state)) +} + +#[utoipa::path(get, path = "/network/state", responses( + (status = 200, description = "Get general network config", body = GenereralState) +))] +async fn general_state(State(state): State) -> Json { + let (tx, rx) = oneshot::channel(); + state.actions.send(Action::GetGeneralState(tx)).unwrap(); + + let state = rx.await.unwrap(); + + Json(state) +} + +#[utoipa::path(put, path = "/network/state", responses( + (status = 200, description = "Update general network config", body = GenereralState) +))] +async fn update_general_state( + State(state): State, + Json(value): Json, +) -> Result, NetworkError> { + state + .actions + .send(Action::UpdateGeneralState(value.clone())) + .unwrap(); + + let (tx, rx) = oneshot::channel(); + state.actions.send(Action::GetGeneralState(tx)).unwrap(); + let state = rx.await.unwrap(); + + Ok(Json(state)) +} + +#[utoipa::path(get, path = "/network/wifi", responses( + (status = 200, description = "List of wireless networks", body = Vec) +))] +async fn wifi_networks(State(state): State) -> Json> { + let (tx, rx) = oneshot::channel(); + state.actions.send(Action::RefreshScan(tx)).unwrap(); + let _ = rx.await.unwrap(); + let (tx, rx) = oneshot::channel(); + state.actions.send(Action::GetAccessPoints(tx)).unwrap(); + + let access_points = rx.await.unwrap(); + let mut networks = vec![]; + for ap in access_points { + let ssid = ap.ssid.to_string(); + if !ssid.is_empty() && !networks.contains(&ssid) { + networks.push(ssid); + } + } + + Json(networks) +} + +#[utoipa::path(get, path = "/network/devices", responses( + (status = 200, description = "List of devices", body = Vec) +))] +async fn devices(State(state): State) -> Json> { + let (tx, rx) = oneshot::channel(); + state.actions.send(Action::GetDevices(tx)).unwrap(); + + Json(rx.await.unwrap()) +} + +#[utoipa::path(get, path = "/network/connections", responses( + (status = 200, description = "List of known connections", body = Vec) +))] +async fn connections(State(state): State) -> Json> { + let (tx, rx) = oneshot::channel(); + state.actions.send(Action::GetConnections(tx)).unwrap(); + let connections = rx.await.unwrap(); + let connections = connections + .iter() + .map(|c| NetworkConnection::try_from(c.clone()).unwrap()) + .collect(); + + Json(connections) +} + +#[utoipa::path(post, path = "/network/connections", responses( + (status = 200, description = "Add a new connection", body = Connection) +))] +async fn add_connection( + State(state): State, + Json(conn): Json, +) -> Result, NetworkError> { + let (tx, rx) = oneshot::channel(); + + state + .actions + .send(Action::AddConnection( + conn.id.clone(), + conn.device_type(), + tx, + )) + .unwrap(); + let _ = rx.await.unwrap(); + + let conn = Connection::try_from(conn)?; + let uuid = conn.uuid.clone(); + + state + .actions + .send(Action::UpdateConnection(Box::new(conn))) + .unwrap(); + + Ok(Json(uuid)) +} + +#[utoipa::path(delete, path = "/network/connections/:uuid", responses( + (status = 200, description = "Delete connection", body = Connection) +))] +async fn delete_connection( + State(state): State, + Path(id): Path, +) -> Result, NetworkError> { + let (tx, rx) = oneshot::channel(); + state + .actions + .send(Action::RemoveConnection(id, tx)) + .unwrap(); + + let _ = rx.await.unwrap(); + let (tx, rx) = oneshot::channel(); + state.actions.send(Action::Apply(tx)).unwrap(); + let _ = rx.await.unwrap(); + + Ok(Json(())) +} + +#[utoipa::path(put, path = "/network/connections/:uuid", responses( + (status = 200, description = "Update connection", body = Connection) +))] +async fn update_connection( + State(state): State, + Path(id): Path, + Json(conn): Json, +) -> Result, NetworkError> { + let mut conn = Connection::try_from(conn)?; + conn.uuid = id; + + state + .actions + .send(Action::UpdateConnection(Box::new(conn))) + .unwrap(); + + Ok(Json(())) +} + +#[utoipa::path(put, path = "/network/system/apply", responses( + (status = 200, description = "Apply configuration") +))] +async fn apply(State(state): State) -> Result, NetworkError> { + let (tx, rx) = oneshot::channel(); + state.actions.send(Action::Apply(tx)).unwrap(); + let _ = rx.await.map_err(|_| NetworkError::CannotApplyConfig)?; + + Ok(Json(())) +} diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index 754142c6a7..c87d219594 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -8,6 +8,7 @@ use crate::{ error::Error, l10n::web::l10n_service, manager::web::{manager_service, manager_stream}, + network::{web::network_service, NetworkManagerAdapter}, questions::web::{questions_service, questions_stream}, software::web::{software_service, software_stream}, web::common::{issues_stream, progress_stream, service_status_stream}, @@ -48,10 +49,18 @@ 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 router = MainServiceBuilder::new(events.clone(), web_ui_dir) .add_service("/l10n", l10n_service(events.clone())) .add_service("/manager", manager_service(dbus.clone()).await?) .add_service("/software", software_service(dbus.clone()).await?) + .add_service( + "/network", + network_service(dbus.clone(), network_adapter).await?, + ) .add_service("/questions", questions_service(dbus).await?) .with_config(config) .build(); diff --git a/rust/agama-server/src/web/common.rs b/rust/agama-server/src/web/common.rs index 86b3fb1c49..891a292f31 100644 --- a/rust/agama-server/src/web/common.rs +++ b/rust/agama-server/src/web/common.rs @@ -138,7 +138,7 @@ async fn build_service_status_proxy<'a>( /// ).await.unwrap(); /// let router: Router = Router::new() /// .route("/hello", get(hello)) -/// .merge(progress) +/// .merge(progress_router) /// .with_state(HelloWorldState {}); /// }); /// ``` @@ -238,7 +238,7 @@ async fn build_progress_proxy<'a>( /// ```no_run /// # use axum::{extract::State, routing::get, Json, Router}; /// # use agama_lib::connection; -/// # use agama_server::web::common::service_status_router; +/// # use agama_server::web::common::{issues_router, service_status_router}; /// # use tokio_test; /// /// # tokio_test::block_on(async { diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index af8309be15..fdacf6cc86 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -8,6 +8,8 @@ use utoipa::OpenApi; crate::l10n::web::locales, crate::l10n::web::set_config, crate::l10n::web::timezones, + crate::network::web::devices, + crate::network::web::connections, crate::software::web::get_config, crate::software::web::patterns, crate::software::web::patterns, @@ -28,6 +30,10 @@ use utoipa::OpenApi; schemas(crate::l10n::LocaleEntry), schemas(crate::l10n::TimezoneEntry), schemas(crate::l10n::web::LocaleConfig), + schemas(crate::network::model::NetworkState), + schemas(crate::network::model::Device), + schemas(crate::network::model::Connection), + schemas(agama_lib::network::types::DeviceType), schemas(crate::software::web::PatternEntry), schemas(crate::software::web::SoftwareConfig), schemas(crate::software::web::SoftwareProposal), diff --git a/rust/agama-server/tests/network.rs b/rust/agama-server/tests/network.rs index e437331d9d..5e783a14af 100644 --- a/rust/agama-server/tests/network.rs +++ b/rust/agama-server/tests/network.rs @@ -8,7 +8,7 @@ use agama_lib::network::{ }; use agama_server::network::{ self, - model::{self, Ipv4Method, Ipv6Method}, + model::{self, GeneralState, Ipv4Method, Ipv6Method, StateConfig}, Adapter, NetworkAdapterError, NetworkService, NetworkState, }; use async_trait::async_trait; @@ -21,7 +21,7 @@ pub struct NetworkTestAdapter(network::NetworkState); #[async_trait] impl Adapter for NetworkTestAdapter { - async fn read(&self) -> Result { + async fn read(&self, _: StateConfig) -> Result { Ok(self.0.clone()) } @@ -34,12 +34,14 @@ impl Adapter for NetworkTestAdapter { async fn test_read_connections() -> Result<(), Box> { let mut server = DBusServer::new().start().await?; + let general_state = GeneralState::default(); + let device = model::Device { name: String::from("eth0"), type_: DeviceType::Ethernet, }; let eth0 = model::Connection::new("eth0".to_string(), DeviceType::Ethernet); - let state = NetworkState::new(vec![device], vec![eth0]); + let state = NetworkState::new(general_state, vec![], vec![device], vec![eth0]); let adapter = NetworkTestAdapter(state); NetworkService::start(&server.connection(), adapter).await?; @@ -143,12 +145,13 @@ async fn test_add_bond_connection() -> Result<(), Box> { async fn test_update_connection() -> Result<(), Box> { let mut server = DBusServer::new().start().await?; + let general_state = GeneralState::default(); let device = model::Device { name: String::from("eth0"), type_: DeviceType::Ethernet, }; let eth0 = model::Connection::new("eth0".to_string(), DeviceType::Ethernet); - let state = NetworkState::new(vec![device], vec![eth0]); + let state = NetworkState::new(general_state, vec![], vec![device], vec![eth0]); let adapter = NetworkTestAdapter(state); NetworkService::start(&server.connection(), adapter).await?; diff --git a/rust/agama-server/tests/network_service.rs b/rust/agama-server/tests/network_service.rs new file mode 100644 index 0000000000..be8238f5a9 --- /dev/null +++ b/rust/agama-server/tests/network_service.rs @@ -0,0 +1,167 @@ +pub mod common; + +use crate::common::DBusServer; +use agama_lib::error::ServiceError; +use agama_lib::network::types::{DeviceType, SSID}; +use agama_server::network::web::network_service; +use agama_server::network::{ + self, + model::{self, AccessPoint, GeneralState, Ipv4Method, Ipv6Method, StateConfig}, + Adapter, NetworkAdapterError, NetworkService, NetworkState, +}; +use agama_server::web::{generate_token, MainServiceBuilder, ServiceConfig}; + +use async_trait::async_trait; +use axum::http::header; +use axum::{ + body::Body, + http::{Method, Request, StatusCode}, + response::Response, + routing::{get, put}, + Json, Router, +}; +use common::body_to_string; +use serde_json::{json, to_string}; +use std::{error::Error, path::PathBuf}; +use tokio::{sync::broadcast::channel, test}; +use tower::ServiceExt; + +fn public_dir() -> PathBuf { + std::env::current_dir().unwrap().join("public") +} +async fn build_state() -> NetworkState { + let general_state = GeneralState::default(); + let device = model::Device { + name: String::from("eth0"), + type_: DeviceType::Ethernet, + }; + 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 dbus = DBusServer::new().start().await?.connection(); + + let adapter = NetworkTestAdapter(state); + Ok(network_service(dbus, adapter).await?) +} + +#[derive(Default)] +pub struct NetworkTestAdapter(network::NetworkState); + +#[async_trait] +impl Adapter for NetworkTestAdapter { + async fn read(&self, _: StateConfig) -> Result { + Ok(self.0.clone()) + } + + async fn write(&self, _network: &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#""wireless_enabled":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_eq!(body, to_string(&json!(["AgamaNetwork", "AgamaNetwork2"]))?); + Ok(()) +} diff --git a/rust/agama-server/tests/service.rs b/rust/agama-server/tests/service.rs index c4e48cdf55..53e06393b2 100644 --- a/rust/agama-server/tests/service.rs +++ b/rust/agama-server/tests/service.rs @@ -1,41 +1,30 @@ pub mod common; -use agama_server::{ - service, - web::{generate_token, MainServiceBuilder, ServiceConfig}, -}; +use agama_server::web::{generate_token, MainServiceBuilder, ServiceConfig}; use axum::{ body::Body, http::{Method, Request, StatusCode}, response::Response, routing::get, - Router, }; -use common::{body_to_string, DBusServer}; +use common::body_to_string; use std::{error::Error, path::PathBuf}; use tokio::{sync::broadcast::channel, test}; use tower::ServiceExt; -async fn build_service() -> Router { - let (tx, _) = channel(16); - let server = DBusServer::new().start().await.unwrap(); - service( - ServiceConfig::default(), - tx, - server.connection(), - public_dir(), - ) - .await - .unwrap() -} - fn public_dir() -> PathBuf { std::env::current_dir().unwrap().join("public") } #[test] async fn test_ping() -> Result<(), Box> { - let web_service = build_service().await; + let config = ServiceConfig::default(); + let (tx, _) = channel(16); + let web_service = MainServiceBuilder::new(tx, public_dir()) + .add_service("/protected", get(protected)) + .with_config(config) + .build(); + let request = Request::builder() .uri("/api/ping") .body(Body::empty())