diff --git a/rust/agama-dbus-server/Cargo.toml b/rust/agama-dbus-server/Cargo.toml index 25cc8ccba5..2c3200d377 100644 --- a/rust/agama-dbus-server/Cargo.toml +++ b/rust/agama-dbus-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-dbus-server/src/agama-web-server.rs b/rust/agama-dbus-server/src/agama-web-server.rs index d8a16fc565..56343bfdf5 100644 --- a/rust/agama-dbus-server/src/agama-web-server.rs +++ b/rust/agama-dbus-server/src/agama-web-server.rs @@ -45,7 +45,7 @@ async fn serve_command(address: &str) -> anyhow::Result<()> { run_monitor(tx.clone()).await?; let config = web::ServiceConfig::load().unwrap(); - let service = web::service(config, tx); + let service = web::service(config, tx).await; axum::serve(listener, service) .await .expect("could not mount app on listener"); diff --git a/rust/agama-dbus-server/src/network.rs b/rust/agama-dbus-server/src/network.rs index 536fdfdbf8..633397a3b6 100644 --- a/rust/agama-dbus-server/src/network.rs +++ b/rust/agama-dbus-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-dbus-server/src/network/action.rs b/rust/agama-dbus-server/src/network/action.rs index 3b98ee912f..5a40424372 100644 --- a/rust/agama-dbus-server/src/network/action.rs +++ b/rust/agama-dbus-server/src/network/action.rs @@ -1,4 +1,4 @@ -use crate::network::model::Connection; +use crate::network::model::{Connection, Device}; use agama_lib::network::types::DeviceType; use tokio::sync::oneshot; use uuid::Uuid; @@ -24,8 +24,10 @@ pub enum Action { /// Gets a connection 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,6 +36,11 @@ pub enum Action { Uuid, Responder>, ), + /// Gets a device + GetDevice(String, Responder>), + GetDevices(Responder>), + /// Gets a device path + GetDevicePath(String, Responder>), /// Get devices paths GetDevicesPaths(Responder>), /// Sets a controller's ports. It uses the Uuid of the controller and the IDs or interface names diff --git a/rust/agama-dbus-server/src/network/dbus/tree.rs b/rust/agama-dbus-server/src/network/dbus/tree.rs index a1872c2da9..76674d9a90 100644 --- a/rust/agama-dbus-server/src/network/dbus/tree.rs +++ b/rust/agama-dbus-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-dbus-server/src/network/model.rs b/rust/agama-dbus-server/src/network/model.rs index 551c353496..18bddf85c0 100644 --- a/rust/agama-dbus-server/src/network/model.rs +++ b/rust/agama-dbus-server/src/network/model.rs @@ -5,6 +5,8 @@ use crate::network::error::NetworkStateError; use agama_lib::network::types::{BondMode, DeviceType, SSID}; use cidr::IpInet; +use serde::Serialize; +use serde_with::{serde_as, skip_serializing_none, DisplayFromStr}; use std::{ collections::HashMap, default::Default, @@ -16,7 +18,7 @@ use thiserror::Error; use uuid::Uuid; use zbus::zvariant::Value; -#[derive(Default, Clone, Debug)] +#[derive(Default, Clone, Debug, utoipa::ToSchema)] pub struct NetworkState { pub devices: Vec, pub connections: Vec, @@ -368,17 +370,21 @@ mod tests { } /// 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 +465,7 @@ impl Default for Connection { } } -#[derive(Default, Debug, PartialEq, Clone)] +#[derive(Default, Debug, PartialEq, Clone, Serialize)] pub enum ConnectionConfig { #[default] Ethernet, @@ -471,7 +477,7 @@ pub enum ConnectionConfig { Bridge(BridgeConfig), } -#[derive(Default, Debug, PartialEq, Clone)] +#[derive(Default, Debug, PartialEq, Clone, Serialize)] pub enum PortConfig { #[default] None, @@ -494,7 +500,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 +560,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 +568,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 +583,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 +600,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 +635,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 +682,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 +710,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 +743,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 +781,7 @@ impl TryFrom for WirelessConfig { } } -#[derive(Debug, Default, Clone, Copy, PartialEq)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize)] pub enum WirelessMode { Unknown = 0, AdHoc = 1, @@ -796,7 +819,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 +865,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 +895,7 @@ impl TryFrom for WEPKeyType { } } -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub enum WEPAuthAlg { #[default] Unset, @@ -906,7 +930,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 +958,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 +991,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 +1008,25 @@ impl TryFrom for BondConfig { } } -#[derive(Debug, Default, PartialEq, Clone)] +#[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-dbus-server/src/network/system.rs b/rust/agama-dbus-server/src/network/system.rs index a347caca6a..2fb3838a60 100644 --- a/rust/agama-dbus-server/src/network/system.rs +++ b/rust/agama-dbus-server/src/network/system.rs @@ -79,6 +79,9 @@ impl NetworkSystem { 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 +95,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(); diff --git a/rust/agama-dbus-server/src/network/web.rs b/rust/agama-dbus-server/src/network/web.rs new file mode 100644 index 0000000000..e63134a38d --- /dev/null +++ b/rust/agama-dbus-server/src/network/web.rs @@ -0,0 +1,104 @@ +//! This module implements the web API for the network module. + +use crate::{error::Error, web::EventsSender}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Response}, + routing::get, + Json, Router, +}; + +use super::Action; + +use crate::network::{model::Connection, model::Device, nm::NetworkManagerAdapter, NetworkSystem}; +use agama_lib::connection; + +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), +} + +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, + events: EventsSender, +} + +/// Sets up and returns the axum service for the network module. +/// +/// * `events`: channel to send the events to the main service. +pub async fn network_service(events: EventsSender) -> Router { + let adapter = NetworkManagerAdapter::from_system() + .await + .expect("Could not connect to NetworkManager to read the configuration."); + let connection = connection().await.unwrap(); + let mut network = NetworkSystem::new(connection.clone(), adapter); + + let state = NetworkState { + actions: network.actions_tx(), + events, + }; + + tokio::spawn(async move { + network + .setup() + .await + .expect("Could not set up the D-Bus tree"); + + network.listen().await; + }); + + Router::new() + .route("/connections", get(connections)) + .route("/devices", get(devices)) + .with_state(state) +} + +#[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(); + let result = rx.await.unwrap(); + let mut devices = vec![]; + + for device in result { + devices.push(device) + } + + Json(devices) +} + +#[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 result = rx.await.unwrap(); + let mut connections = vec![]; + + for connection in result { + connections.push(connection) + } + + Json(connections) +} diff --git a/rust/agama-dbus-server/src/web.rs b/rust/agama-dbus-server/src/web.rs index 7cbb1ec6b8..8e99e73e15 100644 --- a/rust/agama-dbus-server/src/web.rs +++ b/rust/agama-dbus-server/src/web.rs @@ -21,6 +21,7 @@ pub use docs::ApiDoc; pub use event::{Event, EventsReceiver, EventsSender}; use crate::l10n::web::l10n_service; +use crate::network::web::network_service; use axum::Router; pub use service::MainServiceBuilder; @@ -30,9 +31,10 @@ use self::progress::EventsProgressPresenter; /// /// * `config`: service configuration. /// * `events`: D-Bus connection. -pub fn service(config: ServiceConfig, events: EventsSender) -> Router { +pub async fn service(config: ServiceConfig, events: EventsSender) -> Router { MainServiceBuilder::new(events.clone()) - .add_service("/l10n", l10n_service(events)) + .add_service("/l10n", l10n_service(events.clone())) + .add_service("/network", network_service(events).await) .with_config(config) .build() } diff --git a/rust/agama-dbus-server/src/web/docs.rs b/rust/agama-dbus-server/src/web/docs.rs index 58a1de6aed..e96d4b99bd 100644 --- a/rust/agama-dbus-server/src/web/docs.rs +++ b/rust/agama-dbus-server/src/web/docs.rs @@ -3,13 +3,22 @@ use utoipa::OpenApi; #[derive(OpenApi)] #[openapi( info(description = "Agama web API description"), - paths(super::http::ping, crate::l10n::web::locales), + paths( + super::http::ping, + crate::l10n::web::locales, + crate::network::web::devices, + crate::network::web::connections + ), components( schemas(super::http::PingResponse), schemas(crate::l10n::LocaleEntry), schemas(crate::l10n::web::LocaleConfig), schemas(crate::l10n::Keymap), - schemas(crate::l10n::TimezoneEntry) + schemas(crate::l10n::TimezoneEntry), + schemas(crate::network::model::NetworkState), + schemas(crate::network::model::Device), + schemas(crate::network::model::Connection), + schemas(agama_lib::network::types::DeviceType) ) )] pub struct ApiDoc; diff --git a/rust/agama-lib/Cargo.toml b/rust/agama-lib/Cargo.toml index 5ef8166609..40702e2fde 100644 --- a/rust/agama-lib/Cargo.toml +++ b/rust/agama-lib/Cargo.toml @@ -21,4 +21,5 @@ thiserror = "1.0.39" tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } tokio-stream = "0.1.14" url = "2.5.0" +utoipa = "4.2.0" zbus = { version = "3", default-features = false, features = ["tokio"] } diff --git a/rust/agama-lib/src/network/types.rs b/rust/agama-lib/src/network/types.rs index cb8f9695c8..6caf9549eb 100644 --- a/rust/agama-lib/src/network/types.rs +++ b/rust/agama-lib/src/network/types.rs @@ -4,7 +4,8 @@ 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, @@ -31,7 +32,7 @@ impl From for Vec { } } -#[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/src/web/docs.rs b/rust/agama-server/src/web/docs.rs new file mode 100644 index 0000000000..e96d4b99bd --- /dev/null +++ b/rust/agama-server/src/web/docs.rs @@ -0,0 +1,24 @@ +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi( + info(description = "Agama web API description"), + paths( + super::http::ping, + crate::l10n::web::locales, + crate::network::web::devices, + crate::network::web::connections + ), + components( + schemas(super::http::PingResponse), + schemas(crate::l10n::LocaleEntry), + schemas(crate::l10n::web::LocaleConfig), + schemas(crate::l10n::Keymap), + schemas(crate::l10n::TimezoneEntry), + schemas(crate::network::model::NetworkState), + schemas(crate::network::model::Device), + schemas(crate::network::model::Connection), + schemas(agama_lib::network::types::DeviceType) + ) +)] +pub struct ApiDoc;