Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions rust/agama-network/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ pub enum Action {
GetConnections(Responder<Vec<Connection>>),
/// Gets all scanned access points
GetAccessPoints(Responder<Vec<AccessPoint>>),
/// Adds a new access point.
AddAccessPoint(Box<AccessPoint>),
/// Removes an access point by its hardware address.
RemoveAccessPoint(String),
/// Adds a new device.
AddDevice(Box<Device>),
/// Updates a device by its `name`.
Expand All @@ -66,7 +70,7 @@ pub enum Action {
GetDevices(Responder<Vec<Device>>),
GetGeneralState(Responder<GeneralState>),
/// Connection state changed
ChangeConnectionState(String, ConnectionState),
ChangeConnectionState(Uuid, ConnectionState),
/// Persists existing connections if none exist and the network copy is not disabled.
ProposeDefault(Responder<Result<(), NetworkStateError>>),
// Copies persistent connections to the target system
Expand All @@ -77,8 +81,8 @@ pub enum Action {
UpdateGeneralState(GeneralState),
/// Forces a wireless networks scan refresh
RefreshScan(Responder<Result<(), NetworkAdapterError>>),
/// Remove the connection with the given ID.
RemoveConnection(String),
/// Remove the connection with the given UUID.
RemoveConnection(Uuid),
/// Apply the current configuration.
Apply(Responder<Result<(), NetworkAdapterError>>),
}
2 changes: 2 additions & 0 deletions rust/agama-network/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ pub enum NetworkStateError {
CannotUpdateConnection(String),
#[error("Unknown device '{0}'")]
UnknownDevice(String),
#[error("Unknown access point '{0}'")]
UnknownAccessPoint(String),
#[error("Invalid connection UUID: '{0}'")]
InvalidUuid(String),
#[error("Invalid IP address: '{0}'")]
Expand Down
124 changes: 106 additions & 18 deletions rust/agama-network/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,12 +296,10 @@ impl NetworkState {

/// Updates a connection with a new one.
///
/// It uses the `id` to decide which connection to update.
///
/// Additionally, it registers the connection to be removed when the changes are applied.
/// It uses the `uuid` to decide which connection to update.
pub fn update_connection(&mut self, conn: Connection) -> Result<(), NetworkStateError> {
let Some(old_conn) = self.get_connection_mut(&conn.id) else {
return Err(NetworkStateError::UnknownConnection(conn.id.clone()));
let Some(old_conn) = self.get_connection_by_uuid_mut(conn.uuid) else {
return Err(NetworkStateError::UnknownConnection(conn.uuid.to_string()));
};
*old_conn = conn;

Expand All @@ -311,9 +309,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, id: &str) -> Result<(), NetworkStateError> {
let Some(position) = self.connections.iter().position(|d| d.id == id) else {
return Err(NetworkStateError::UnknownConnection(id.to_string()));
pub fn remove_connection(&mut self, uuid: Uuid) -> Result<(), NetworkStateError> {
let Some(position) = self.connections.iter().position(|d| d.uuid == uuid) else {
return Err(NetworkStateError::UnknownConnection(uuid.to_string()));
};

self.connections.remove(position);
Expand Down Expand Up @@ -343,6 +341,34 @@ impl NetworkState {
Ok(())
}

pub fn add_access_point(&mut self, ap: AccessPoint) -> Result<(), NetworkStateError> {
if let Some(position) = self
.access_points
.iter()
.position(|a| a.hw_address == ap.hw_address)
{
self.access_points.remove(position);
}
self.access_points.push(ap);

Ok(())
}

pub fn remove_access_point(&mut self, hw_address: &str) -> Result<(), NetworkStateError> {
let Some(position) = self
.access_points
.iter()
.position(|a| a.hw_address == hw_address)
else {
return Err(NetworkStateError::UnknownAccessPoint(
hw_address.to_string(),
));
};

self.access_points.remove(position);
Ok(())
}

/// Sets a controller's ports.
///
/// If the connection is not a controller, returns an error.
Expand Down Expand Up @@ -417,22 +443,23 @@ mod tests {
#[test]
fn test_update_connection() {
let mut state = NetworkState::default();
let uuid = Uuid::new_v4();
let conn0 = Connection {
id: "eth0".to_string(),
uuid: Uuid::new_v4(),
uuid,
..Default::default()
};
state.add_connection(conn0).unwrap();

let uuid = Uuid::new_v4();
let conn1 = Connection {
id: "eth0".to_string(),
uuid,
firewall_zone: Some("public".to_string()),
..Default::default()
};
state.update_connection(conn1).unwrap();
let found = state.get_connection("eth0").unwrap();
assert_eq!(found.uuid, uuid);
let found = state.get_connection_by_uuid(uuid).unwrap();
assert_eq!(found.firewall_zone, Some("public".to_string()));
}

#[test]
Expand All @@ -446,20 +473,77 @@ mod tests {
#[test]
fn test_remove_connection() {
let mut state = NetworkState::default();
let conn0 = Connection::new("eth0".to_string(), DeviceType::Ethernet);
let uuid = Uuid::new_v4();
let conn0 = Connection {
id: "eth0".to_string(),
uuid,
..Default::default()
};
state.add_connection(conn0).unwrap();
state.remove_connection("eth0".as_ref()).unwrap();
let found = state.get_connection("eth0");
state.remove_connection(uuid).unwrap();
let found = state.get_connection_by_uuid(uuid);
assert!(found.is_none());
}

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

#[test]
fn test_remove_device() {
let mut state = NetworkState::default();
let device = Device {
name: "eth0".to_string(),
..Default::default()
};
state.add_device(device).unwrap();
state.remove_device("eth0").unwrap();
assert!(state.get_device("eth0").is_none());
}

#[test]
fn test_add_access_point() {
let mut state = NetworkState::default();
let ap = AccessPoint {
hw_address: "AA:BB:CC:DD:EE:FF".to_string(),
ssid: SSID(b"test".to_vec()),
..Default::default()
};
state.add_access_point(ap.clone()).unwrap();
assert_eq!(state.access_points.len(), 1);
assert_eq!(state.access_points[0].hw_address, "AA:BB:CC:DD:EE:FF");

// Adding same AP should replace it (in our implementation we remove and push)
let mut ap2 = ap.clone();
ap2.strength = 80;
state.add_access_point(ap2).unwrap();
assert_eq!(state.access_points.len(), 1);
assert_eq!(state.access_points[0].strength, 80);
}

#[test]
fn test_remove_access_point() {
let mut state = NetworkState::default();
let ap = AccessPoint {
hw_address: "AA:BB:CC:DD:EE:FF".to_string(),
..Default::default()
};
state.add_access_point(ap).unwrap();
state.remove_access_point("AA:BB:CC:DD:EE:FF").unwrap();
assert_eq!(state.access_points.len(), 0);
}

#[test]
fn test_remove_unknown_access_point() {
let mut state = NetworkState::default();
let error = state.remove_access_point("unknown").unwrap_err();
assert!(matches!(error, NetworkStateError::UnknownAccessPoint(_)));
}

#[test]
fn test_is_loopback() {
let conn = Connection::new("eth0".to_string(), DeviceType::Ethernet);
Expand Down Expand Up @@ -1726,7 +1810,7 @@ pub struct TunConfig {
#[serde(rename_all = "camelCase")]
pub enum NetworkChange {
ConnectionAdded(Connection),
ConnectionRemoved(String),
ConnectionRemoved(Uuid),
/// A new device has been added.
DeviceAdded(Device),
/// A device has been removed.
Expand All @@ -1737,9 +1821,13 @@ pub enum NetworkChange {
DeviceUpdated(String, Device),
/// A connection state has changed.
ConnectionStateChanged {
id: String,
uuid: Uuid,
state: ConnectionState,
},
/// A new access point has been added.
AccessPointAdded(AccessPoint),
/// An access point has been removed.
AccessPointRemoved(String),
}

#[derive(Default, Debug, PartialEq, Clone, Deserialize, Serialize, utoipa::ToSchema)]
Expand Down
39 changes: 37 additions & 2 deletions rust/agama-network/src/nm/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ use crate::{
nm::{
dbus::connection_from_dbus,
model::NmDeviceType,
proxies::{ConnectionProxy, DeviceProxy, IP4ConfigProxy, IP6ConfigProxy},
proxies::{AccessPointProxy, ConnectionProxy, DeviceProxy, IP4ConfigProxy, IP6ConfigProxy},
},
types::{
AccessPoint, ConnectionFlags, Device, DeviceState, DeviceType, IpConfig, IpRoute,
MacAddress, SSID,
},
types::{ConnectionFlags, Device, DeviceState, DeviceType, IpConfig, IpRoute, MacAddress},
};
use cidr::IpInet;
use std::{collections::HashMap, net::IpAddr, str::FromStr};
Expand All @@ -44,6 +47,38 @@ pub struct DeviceFromProxyBuilder<'a> {
proxy: &'a DeviceProxy<'a>,
}

/// Builder to create an [AccessPoint] from its corresponding NetworkManager D-Bus representation.
pub struct AccessPointFromProxyBuilder<'a> {
device_name: String,
proxy: &'a AccessPointProxy<'a>,
}

impl<'a> AccessPointFromProxyBuilder<'a> {
pub fn new(device_name: String, proxy: &'a AccessPointProxy<'a>) -> Self {
Self { device_name, proxy }
}

/// Creates an [AccessPoint] starting on the [AccessPointProxy].
pub async fn build(&self) -> Result<AccessPoint, NmError> {
let ssid = SSID(self.proxy.ssid().await?);
let hw_address = self.proxy.hw_address().await?;
let strength = self.proxy.strength().await?;
let flags = self.proxy.flags().await?;
let rsn_flags = self.proxy.rsn_flags().await?;
let wpa_flags = self.proxy.wpa_flags().await?;

Ok(AccessPoint {
device: self.device_name.clone(),
ssid,
hw_address,
strength,
flags,
rsn_flags,
wpa_flags,
})
}
}

impl<'a> ConnectionFromProxyBuilder<'a> {
pub fn new(_connection: &zbus::Connection, proxy: &'a ConnectionProxy<'a>) -> Self {
Self { proxy }
Expand Down
16 changes: 16 additions & 0 deletions rust/agama-network/src/nm/streams/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub enum NmChange {
ActiveConnectionAdded(OwnedObjectPath),
ActiveConnectionUpdated(OwnedObjectPath),
ActiveConnectionRemoved(OwnedObjectPath),
AccessPointAdded(OwnedObjectPath, OwnedObjectPath),
AccessPointRemoved(OwnedObjectPath, OwnedObjectPath),
}

pub async fn build_added_and_removed_stream(
Expand Down Expand Up @@ -63,3 +65,17 @@ pub async fn build_properties_changed_stream(
let stream = MessageStream::for_match_rule(rule, connection, Some(1)).await?;
Ok(stream)
}

/// Returns a stream of wireless signals to be used by DeviceChangedStream.
///
/// It listens for AccessPointAdded and AccessPointRemoved signals.
pub async fn build_wireless_signals_stream(
connection: &zbus::Connection,
) -> Result<MessageStream, NmError> {
let rule = MatchRule::builder()
.msg_type(MessageType::Signal)
.interface("org.freedesktop.NetworkManager.Device.Wireless")?
.build();
let stream = MessageStream::for_match_rule(rule, connection, Some(1)).await?;
Ok(stream)
}
28 changes: 27 additions & 1 deletion rust/agama-network/src/nm/streams/devices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ use zbus::{
Message, MessageStream,
};

use super::common::{build_added_and_removed_stream, build_properties_changed_stream, NmChange};
use super::common::{
build_added_and_removed_stream, build_properties_changed_stream, build_wireless_signals_stream,
NmChange,
};
use crate::nm::error::NmError;

/// Stream of device-related events.
Expand Down Expand Up @@ -64,6 +67,10 @@ impl DeviceChangedStream {
"properties",
build_properties_changed_stream(&connection).await?,
);
inner.insert(
"wireless",
build_wireless_signals_stream(&connection).await?,
);
Ok(Self { connection, inner })
}

Expand Down Expand Up @@ -161,6 +168,25 @@ impl DeviceChangedStream {
return Self::handle_changed(changed);
}

let header = message.header();
let interface = header.interface()?;
if interface == "org.freedesktop.NetworkManager.Device.Wireless" {
let path = OwnedObjectPath::from(header.path()?.to_owned());
let member = header.member()?;

match member.as_str() {
"AccessPointAdded" => {
let ap_path: OwnedObjectPath = message.body().deserialize().ok()?;
return Some(NmChange::AccessPointAdded(path, ap_path));
}
"AccessPointRemoved" => {
let ap_path: OwnedObjectPath = message.body().deserialize().ok()?;
return Some(NmChange::AccessPointRemoved(path, ap_path));
}
_ => {}
}
}

None
}
}
Expand Down
Loading
Loading