From f35ef0419adbf486e66e5a6bea4998600e4edffb Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Wed, 20 Mar 2024 14:50:32 +0000 Subject: [PATCH] Added support for modifying the network configuration through http --- rust/agama-lib/src/network/types.rs | 14 +- rust/agama-server/src/network/action.rs | 20 +- rust/agama-server/src/network/adapter.rs | 6 +- .../network/dbus/interfaces/connections.rs | 5 +- rust/agama-server/src/network/model.rs | 188 +++++++++++++++++- rust/agama-server/src/network/nm/adapter.rs | 96 ++++++++- rust/agama-server/src/network/nm/client.rs | 95 ++++++++- rust/agama-server/src/network/nm/proxies.rs | 109 ++++++++++ rust/agama-server/src/network/system.rs | 49 ++++- rust/agama-server/src/network/web.rs | 167 +++++++++++++++- 10 files changed, 715 insertions(+), 34 deletions(-) diff --git a/rust/agama-lib/src/network/types.rs b/rust/agama-lib/src/network/types.rs index 6caf9549eb..e3ec61957a 100644 --- a/rust/agama-lib/src/network/types.rs +++ b/rust/agama-lib/src/network/types.rs @@ -1,5 +1,9 @@ +use cidr::errors::NetworkParseError; use serde::{Deserialize, Serialize}; -use std::{fmt, str}; +use std::{ + fmt, + str::{self, FromStr}, +}; use thiserror::Error; use zbus; @@ -26,6 +30,14 @@ 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 diff --git a/rust/agama-server/src/network/action.rs b/rust/agama-server/src/network/action.rs index 4f05960b85..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, Device}; +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,6 +21,11 @@ pub enum Action { DeviceType, Responder>, ), + /// Add a new connection + NewConnection( + Connection, + Responder>, + ), /// Gets a connection by its Uuid GetConnection(Uuid, Responder>), /// Gets a connection @@ -36,6 +41,8 @@ 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 @@ -44,6 +51,7 @@ pub enum Action { 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( @@ -51,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..2d1a9be4ef 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::NetworkStateItems; use crate::network::NetworkState; use agama_lib::error::ServiceError; use async_trait::async_trait; @@ -16,7 +17,10 @@ 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, + items: Vec, + ) -> 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 aca6bf50ad..56a2e11363 100644 --- a/rust/agama-server/src/network/dbus/interfaces/connections.rs +++ b/rust/agama-server/src/network/dbus/interfaces/connections.rs @@ -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/model.rs b/rust/agama-server/src/network/model.rs index 18bddf85c0..5eeabda890 100644 --- a/rust/agama-server/src/network/model.rs +++ b/rust/agama-server/src/network/model.rs @@ -3,9 +3,10 @@ //! * 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::Serialize; +use serde::{Deserialize, Serialize}; use serde_with::{serde_as, skip_serializing_none, DisplayFromStr}; use std::{ collections::HashMap, @@ -18,8 +19,18 @@ use thiserror::Error; use uuid::Uuid; use zbus::zvariant::Value; +#[derive(PartialEq)] +pub enum NetworkStateItems { + AccessPoints, + Devices, + Connections, + GeneralState, +} + #[derive(Default, Clone, Debug, utoipa::ToSchema)] pub struct NetworkState { + pub general_state: GeneralState, + pub access_points: Vec, pub devices: Vec, pub connections: Vec, } @@ -27,10 +38,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, } @@ -369,6 +389,35 @@ mod tests { } } +/// Network state +#[serde_as] +#[derive(Debug, 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 +} + +impl Default for GeneralState { + fn default() -> Self { + Self { + connectivity: false, + wireless_enabled: false, + networking_enabled: false, + } + } +} + +/// Access Point +#[derive(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, Serialize, utoipa::ToSchema)] pub struct Device { @@ -465,6 +514,84 @@ impl Default for Connection { } } +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] @@ -781,6 +908,36 @@ impl TryFrom for WirelessConfig { } } +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, @@ -1008,6 +1165,33 @@ impl TryFrom for BondConfig { } } +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, diff --git a/rust/agama-server/src/network/nm/adapter.rs b/rust/agama-server/src/network/nm/adapter.rs index 0df720e5ba..c8a9882f21 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, NetworkStateItems}, 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,68 @@ impl<'a> NetworkManagerAdapter<'a> { #[async_trait] impl<'a> Adapter for NetworkManagerAdapter<'a> { - async fn read(&self) -> Result { - let devices = self - .client - .devices() - .await - .map_err(NetworkAdapterError::Read)?; - let connections = self + async fn read( + &self, + items: Vec, + ) -> Result { + let items = if items.is_empty() { + vec![ + NetworkStateItems::AccessPoints, + NetworkStateItems::Connections, + NetworkStateItems::Devices, + NetworkStateItems::GeneralState, + ] + } else { + items + }; + + let general_state = self .client - .connections() + .general_state() .await .map_err(NetworkAdapterError::Read)?; - Ok(NetworkState::new(devices, connections)) + + let devices = if items.contains(&NetworkStateItems::Devices) { + self.client + .devices() + .await + .map_err(NetworkAdapterError::Read)? + } else { + vec![] + }; + + let connections = if items.contains(&NetworkStateItems::Connections) { + self.client + .connections() + .await + .map_err(NetworkAdapterError::Read)? + } else { + vec![] + }; + + let access_points = + if items.contains(&NetworkStateItems::AccessPoints) && general_state.wireless_enabled { + if items.len() == 1 { + self.client + .request_scan() + .await + .map_err(NetworkAdapterError::Read)?; + thread::sleep(time::Duration::from_secs(1)); + }; + self.client + .access_points() + .await + .map_err(NetworkAdapterError::Read)? + } else { + vec![] + }; + + Ok(NetworkState::new( + general_state, + access_points, + devices, + connections, + )) } /// Writes the connections to NetworkManager. @@ -51,13 +103,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(vec![]).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 +161,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 2fb3838a60..00fddf9432 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::NetworkStateItems, 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(vec![]).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(vec![]).await?; let mut tree = self.tree.lock().await; tree.set_connections(&mut self.state.connections).await?; tree.set_devices(&self.state.devices).await?; @@ -75,6 +75,26 @@ impl NetworkSystem { let result = self.add_connection_action(name, ty).await; tx.send(result).unwrap(); } + Action::RefreshScan(tx) => { + let state = self + .adapter + .read(vec![NetworkStateItems::AccessPoints]) + .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(); @@ -122,10 +142,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; @@ -170,6 +195,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 index 7a42977b3b..706b880c58 100644 --- a/rust/agama-server/src/network/web.rs +++ b/rust/agama-server/src/network/web.rs @@ -2,17 +2,19 @@ use crate::error::Error; use axum::{ - extract::State, + extract::{Path, State}, http::StatusCode, response::{IntoResponse, Response}, - routing::get, + routing::{delete, get, put}, Json, Router, }; -use super::Action; +use super::{error::NetworkStateError, model::GeneralState, Action}; use crate::network::{model::Connection, model::Device, nm::NetworkManagerAdapter, NetworkSystem}; use agama_lib::error::ServiceError; +use agama_lib::network::settings::NetworkConnection; +use uuid::Uuid; use serde_json::json; use thiserror::Error; @@ -24,6 +26,10 @@ pub enum NetworkError { UnknownConnection(String), #[error("Cannot translate: {0}")] CannotTranslate(#[from] Error), + #[error("Cannot apply configuration")] + CannotApplyConfig, + #[error("Network states error: {0}")] + Error(#[from] NetworkStateError), } impl IntoResponse for NetworkError { @@ -63,11 +69,71 @@ pub async fn network_service(dbus: zbus::Connection) -> Result) -> Json { + let (tx, rx) = oneshot::channel(); + state.actions.send(Action::GetGeneralState(tx)).unwrap(); + + let state = rx.await.unwrap(); + + Json(state) +} + +#[utoipa::path(post, 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) ))] @@ -79,11 +145,98 @@ async fn devices(State(state): State) -> Json> { } #[utoipa::path(get, path = "/network/connections", responses( - (status = 200, description = "List of known connections", body = Vec) + (status = 200, description = "List of known connections", body = Vec) ))] -async fn connections(State(state): State) -> Json> { +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(rx.await.unwrap()) + 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(value): Json, +) -> Result, NetworkError> { + let (tx, rx) = oneshot::channel(); + let conn = NetworkConnection::from(value); + + state + .actions + .send(Action::AddConnection( + conn.id.clone(), + conn.device_type(), + tx, + )) + .unwrap(); + let _ = rx.await.unwrap(); + + let conn = Connection::try_from(conn)?; + + state + .actions + .send(Action::UpdateConnection(Box::new(conn.clone()))) + .unwrap(); + + Ok(Json(conn.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(value): Json, +) -> Result, NetworkError> { + let conn = NetworkConnection::from(value); + let mut conn = Connection::try_from(conn)?; + conn.uuid = id; + + state + .actions + .send(Action::UpdateConnection(Box::new(conn.clone()))) + .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(())) }