Skip to content

Commit

Permalink
Adapt the Network UI to the HTTP/JSON API (#1116)
Browse files Browse the repository at this point in the history
## Problem

Recently, we have been adding an HTTP/JSON layer on top of the network
configuration. Using this new API we should be able to:

Display the active connections in the
[NetworkSection](https://github.com/openSUSE/agama/blob/master/web/src/components/overview/NetworkSection.jsx)
and the
[ConnectionsTable](https://github.com/openSUSE/agama/blob/master/web/src/components/network/ConnectionsTable.jsx).
Allow editing the configuration of those connections.

- [*Trello
Card*](https://trello.com/c/chdpfc4S/3619-8-basic-network-configuration-handling)

## Solution

The UI has been adapted using the HTTP/JSON API and additing or updating
a connections using it is now allowed. Although it was out of the scope,
adding a Wireless connection is also allowed but in that case we are not
listening to the DBUS events and not reacting properly, therefore, a
refresh would be needed in order to see the added connection.
  • Loading branch information
teclator authored Apr 9, 2024
2 parents ccea4e9 + 21e6941 commit 7df6f83
Show file tree
Hide file tree
Showing 29 changed files with 733 additions and 625 deletions.
4 changes: 3 additions & 1 deletion rust/agama-lib/src/network/settings.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Representation of the network settings
use super::types::DeviceType;
use super::types::{DeviceType, Status};
use agama_settings::error::ConversionError;
use agama_settings::{SettingObject, SettingValue, Settings};
use cidr::IpInet;
Expand Down Expand Up @@ -100,6 +100,8 @@ pub struct NetworkConnection {
pub bond: Option<BondSettings>,
#[serde(rename = "mac-address", skip_serializing_if = "Option::is_none")]
pub mac_address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<Status>,
}

impl NetworkConnection {
Expand Down
37 changes: 37 additions & 0 deletions rust/agama-lib/src/network/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,43 @@ pub enum DeviceType {
Bridge = 6,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Status {
#[default]
Up,
Down,
Removed,
}

impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match &self {
Status::Up => "up",
Status::Down => "down",
Status::Removed => "removed",
};
write!(f, "{}", name)
}
}

#[derive(Debug, Error, PartialEq)]
#[error("Invalid status: {0}")]
pub struct InvalidStatus(String);

impl TryFrom<&str> for Status {
type Error = InvalidStatus;

fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"up" => Ok(Status::Up),
"down" => Ok(Status::Down),
"removed" => Ok(Status::Removed),
_ => Err(InvalidStatus(value.to_string())),
}
}
}

/// Bond mode
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
pub enum BondMode {
Expand Down
13 changes: 6 additions & 7 deletions rust/agama-server/src/network/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@ pub enum Action {
Responder<Result<OwnedObjectPath, NetworkStateError>>,
),
/// Add a new connection
NewConnection(
Connection,
Responder<Result<OwnedObjectPath, NetworkStateError>>,
),
NewConnection(Box<Connection>, Responder<Result<(), NetworkStateError>>),
/// Gets a connection by its id
GetConnection(String, Responder<Option<Connection>>),
/// Gets a connection by its Uuid
GetConnection(Uuid, Responder<Option<Connection>>),
GetConnectionByUuid(Uuid, Responder<Option<Connection>>),
/// Gets a connection
GetConnections(Responder<Vec<Connection>>),
/// Gets a connection path
Expand Down Expand Up @@ -60,13 +59,13 @@ pub enum Action {
Responder<Result<(), NetworkStateError>>,
),
/// Updates a connection (replacing the old one).
UpdateConnection(Box<Connection>),
UpdateConnection(Box<Connection>, Responder<Result<(), NetworkStateError>>),
/// Updates the general network configuration
UpdateGeneralState(GeneralState),
/// Forces a wireless networks scan refresh
RefreshScan(Responder<Result<(), NetworkAdapterError>>),
/// Remove the connection with the given Uuid.
RemoveConnection(Uuid, Responder<Result<(), NetworkStateError>>),
RemoveConnection(String, Responder<Result<(), NetworkStateError>>),
/// Apply the current configuration.
Apply(Responder<Result<(), NetworkAdapterError>>),
}
15 changes: 10 additions & 5 deletions rust/agama-server/src/network/dbus/interfaces/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub trait ConnectionInterface {
let actions = self.actions().await;
let (tx, rx) = oneshot::channel();
actions
.send(Action::GetConnection(self.uuid(), tx))
.send(Action::GetConnectionByUuid(self.uuid(), tx))
.unwrap();
rx.await
.unwrap()
Expand All @@ -41,13 +41,15 @@ pub trait ConnectionInterface {
where
F: FnOnce(&mut NetworkConnection) + std::marker::Send,
{
let (tx, rx) = oneshot::channel();
let mut connection = self.get_connection().await?;
func(&mut connection);
let actions = self.actions().await;
actions
.send(Action::UpdateConnection(Box::new(connection)))
.send(Action::UpdateConnection(Box::new(connection), tx))
.unwrap();
Ok(())

rx.await.unwrap()
}
}

Expand All @@ -73,9 +75,12 @@ pub trait ConnectionConfigInterface: ConnectionInterface {
func(&mut config);
connection.config = config.into();
let actions = self.actions().await;

let (tx, rx) = oneshot::channel();
actions
.send(Action::UpdateConnection(Box::new(connection)))
.send(Action::UpdateConnection(Box::new(connection), tx))
.unwrap();
Ok(())

rx.await.unwrap()
}
}
2 changes: 2 additions & 0 deletions rust/agama-server/src/network/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use thiserror::Error;
pub enum NetworkStateError {
#[error("Unknown connection '{0}'")]
UnknownConnection(String),
#[error("Cannot update connection '{0}'")]
CannotUpdateConnection(String),
#[error("Invalid connection UUID: '{0}'")]
InvalidUuid(String),
#[error("Invalid IP address: '{0}'")]
Expand Down
47 changes: 25 additions & 22 deletions rust/agama-server/src/network/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! 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 agama_lib::network::types::{BondMode, DeviceType, Status, SSID};
use cidr::IpInet;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, skip_serializing_none, DisplayFromStr};
Expand Down Expand Up @@ -149,9 +149,9 @@ impl NetworkState {
/// Removes a connection from the state.
///
/// Additionally, it registers the connection to be removed when the changes are applied.
pub fn remove_connection(&mut self, uuid: Uuid) -> Result<(), NetworkStateError> {
let Some(conn) = self.get_connection_by_uuid_mut(uuid) else {
return Err(NetworkStateError::UnknownConnection(uuid.to_string()));
pub fn remove_connection(&mut self, id: &str) -> Result<(), NetworkStateError> {
let Some(conn) = self.get_connection_mut(id) else {
return Err(NetworkStateError::UnknownConnection(id.to_string()));
};

conn.remove();
Expand Down Expand Up @@ -311,15 +311,15 @@ mod tests {
let conn0 = Connection::new("eth0".to_string(), DeviceType::Ethernet);
let uuid = conn0.uuid;
state.add_connection(conn0).unwrap();
state.remove_connection(uuid).unwrap();
state.remove_connection("eth0".as_ref()).unwrap();
let found = state.get_connection("eth0").unwrap();
assert!(found.is_removed());
}

#[test]
fn test_remove_unknown_connection() {
let mut state = NetworkState::default();
let error = state.remove_connection(Uuid::new_v4()).unwrap_err();
let error = state.remove_connection("unknown".as_ref()).unwrap_err();
assert!(matches!(error, NetworkStateError::UnknownConnection(_)));
}

Expand Down Expand Up @@ -404,19 +404,23 @@ mod tests {
#[serde_as]
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)]
pub struct GeneralState {
pub hostname: String,
pub connectivity: bool,
pub wireless_enabled: bool,
pub networking_enabled: bool, // pub network_state: NMSTATE
// pub dns: GlobalDnsConfiguration <HashMap>
}

/// Access Point
#[serde_as]
#[derive(Default, Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct AccessPoint {
#[serde_as(as = "DisplayFromStr")]
pub ssid: SSID,
pub hw_address: String,
pub strength: u8,
pub security_protocols: Vec<SecurityProtocol>,
pub flags: u32,
pub rsn_flags: u32,
pub wpa_flags: u32,
}

/// Network device
Expand Down Expand Up @@ -532,6 +536,10 @@ impl TryFrom<NetworkConnection> for Connection {
connection.ip_config.method6 = method;
}

if let Some(status) = conn.status {
connection.status = status;
}

if let Some(wireless_config) = conn.wireless {
let config = WirelessConfig::try_from(wireless_config)?;
connection.config = config.into();
Expand All @@ -542,6 +550,7 @@ impl TryFrom<NetworkConnection> for Connection {
connection.config = config.into();
}

connection.ip_config.addresses = conn.addresses;
connection.ip_config.nameservers = conn.nameservers;
connection.ip_config.gateway4 = conn.gateway4;
connection.ip_config.gateway6 = conn.gateway6;
Expand All @@ -559,15 +568,17 @@ impl TryFrom<Connection> for NetworkConnection {
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 mac_address = (!mac.is_empty()).then_some(mac);
let nameservers = conn.ip_config.nameservers;
let addresses = conn.ip_config.addresses;
let gateway4 = conn.ip_config.gateway4;
let gateway6 = conn.ip_config.gateway6;
let interface = conn.interface;
let status = Some(conn.status);

let mut connection = NetworkConnection {
id,
status,
method4,
method6,
gateway4,
Expand Down Expand Up @@ -689,14 +700,6 @@ impl From<InvalidMacAddress> for zbus::fdo::Error {
}
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize)]
pub enum Status {
#[default]
Up,
Down,
Removed,
}

#[skip_serializing_none]
#[derive(Default, Debug, PartialEq, Clone, Serialize)]
pub struct IpConfig {
Expand Down
11 changes: 11 additions & 0 deletions rust/agama-server/src/network/nm/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ impl<'a> Adapter for NetworkManagerAdapter<'a> {

let mut state = NetworkState::default();

if config.general_state {
state.general_state = general_state.clone();
}

if config.devices {
state.devices = self
.client
Expand Down Expand Up @@ -119,6 +123,13 @@ impl<'a> Adapter for NetworkManagerAdapter<'a> {
if old_conn == conn {
continue;
}
} else if conn.is_removed() {
log::info!(
"Connection {} ({}) does not need to be removed",
conn.id,
conn.uuid
);
continue;
}

log::info!("Updating connection {} ({})", conn.id, conn.uuid);
Expand Down
Loading

0 comments on commit 7df6f83

Please sign in to comment.