diff --git a/rust/agama-lib/share/examples/network/bond.json b/rust/agama-lib/share/examples/network/bond.json new file mode 100644 index 0000000000..2ffa967169 --- /dev/null +++ b/rust/agama-lib/share/examples/network/bond.json @@ -0,0 +1,14 @@ +{ + "network": { + "connections": [ + { + "id": "bond0", + "bond": { + "ports": ["eth0", "eth1"], + "mode": "active-backup", + "options": "primary=eth1" + } + } + ] + } +} diff --git a/rust/agama-lib/share/examples/network/bridge.json b/rust/agama-lib/share/examples/network/bridge.json new file mode 100644 index 0000000000..5326e72ac8 --- /dev/null +++ b/rust/agama-lib/share/examples/network/bridge.json @@ -0,0 +1,13 @@ +{ + "network": { + "connections": [ + { + "id": "br0", + "bridge": { + "ports": ["eth0", "eth1"], + "stp": false + } + } + ] + } +} diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 5a605a1338..d23227b4a2 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -434,6 +434,44 @@ } } }, + "bridge": { + "type": "object", + "title": "Bridge configuration", + "additionalProperties": false, + "properties": { + "stp": { + "title": "whether the Spanning Tree Protocol is enabled or not", + "type": "boolean" + }, + "forwardDelay": { + "title": "Spanning Tree Protocol forward delay, in seconds", + "type": "integer", + "minimum": 0 + }, + "priority": { + "title": "Spanning Tree Protocol priority (lower values are 'better')", + "type": "integer", + "minimum": 0 + }, + "maxAge": { + "title": "Spanning Tree Protocol maximum message age, in seconds", + "type": "integer", + "minimum": 0 + }, + "helloTime": { + "title": "Spanning Tree Protocol hello time, in seconds", + "type": "integer", + "minimum": 0 + }, + "ports": { + "type": "array", + "items": { + "title": "A list of the interfaces or connections to be part of the bridge", + "type": "string" + } + } + } + }, "match": { "type": "object", "title": "Match settings", @@ -669,7 +707,11 @@ } }, "required": ["name"], - "oneOf": [{ "required": ["body"] }, { "required": ["url"] }, { "required": ["content"]}] + "oneOf": [ + { "required": ["body"] }, + { "required": ["url"] }, + { "required": ["content"] } + ] }, "postPartitioning": { "title": "User-defined installation script that runs after the partitioning finishes", @@ -697,7 +739,11 @@ } }, "required": ["name"], - "oneOf": [{ "required": ["body"] }, { "required": ["url"] }, { "required": ["content"]}] + "oneOf": [ + { "required": ["body"] }, + { "required": ["url"] }, + { "required": ["content"] } + ] }, "postScript": { "title": "User-defined installation script that runs after the installation finishes", @@ -730,7 +776,11 @@ } }, "required": ["name"], - "oneOf": [{ "required": ["body"] }, { "required": ["url"] }, { "required": ["content"]}] + "oneOf": [ + { "required": ["body"] }, + { "required": ["url"] }, + { "required": ["content"] } + ] }, "initScript": { "title": "User-defined installation script that runs during the first boot of the target system, once the installation is finished", @@ -758,7 +808,11 @@ } }, "required": ["name"], - "oneOf": [{ "required": ["body"] }, { "required": ["url"] }, { "required": ["content"]}] + "oneOf": [ + { "required": ["body"] }, + { "required": ["url"] }, + { "required": ["content"] } + ] }, "file": { "title": "User-defined file to deploy", @@ -795,7 +849,7 @@ } }, "required": ["destination"], - "oneOf": [{ "required": ["url"] }, { "required": ["content"]}] + "oneOf": [{ "required": ["url"] }, { "required": ["content"] }] } } } diff --git a/rust/agama-lib/src/hostname/client.rs b/rust/agama-lib/src/hostname/client.rs index d2d5a2b23d..adc87d53af 100644 --- a/rust/agama-lib/src/hostname/client.rs +++ b/rust/agama-lib/src/hostname/client.rs @@ -43,7 +43,6 @@ impl<'a> HostnameClient<'a> { let settings = HostnameSettings { hostname: Some(hostname), static_hostname: Some(static_hostname), - ..Default::default() }; Ok(settings) diff --git a/rust/agama-lib/src/network/store.rs b/rust/agama-lib/src/network/store.rs index 6bdd5fceeb..a591b529dd 100644 --- a/rust/agama-lib/src/network/store.rs +++ b/rust/agama-lib/src/network/store.rs @@ -94,6 +94,16 @@ fn add_ordered_connection( } } + if let Some(bridge) = &conn.bridge { + for port in &bridge.ports { + if let Some(conn) = find_connection(port, conns) { + add_ordered_connection(conn, conns, ordered); + } else if !ordered.contains(&conn.id) { + ordered.push(port.clone()); + } + } + } + if !ordered.contains(&conn.id) { ordered.push(conn.id.to_owned()) } @@ -119,7 +129,7 @@ fn default_connection(id: &str) -> NetworkConnection { #[cfg(test)] mod tests { use super::ordered_connections; - use crate::network::settings::{BondSettings, NetworkConnection}; + use crate::network::settings::{BondSettings, BridgeSettings, NetworkConnection}; #[test] fn test_ordered_connections() { @@ -132,6 +142,15 @@ mod tests { ..Default::default() }; + let bridge = NetworkConnection { + id: "br0".to_string(), + bridge: Some(BridgeSettings { + ports: vec!["eth0".to_string(), "eth1".to_string(), "eth3".to_string()], + ..Default::default() + }), + ..Default::default() + }; + let eth0 = NetworkConnection { id: "eth0".to_string(), ..Default::default() @@ -157,6 +176,18 @@ mod tests { "bond0".to_string(), "eth2".to_string() ] + ); + + let conns = vec![bridge]; + let ordered = ordered_connections(&conns); + assert_eq!( + ordered, + vec![ + "eth0".to_string(), + "eth1".to_string(), + "eth3".to_string(), + "br0".to_string() + ] ) } } diff --git a/rust/agama-network/src/model.rs b/rust/agama-network/src/model.rs index ad0a8a0ed1..031481030b 100644 --- a/rust/agama-network/src/model.rs +++ b/rust/agama-network/src/model.rs @@ -23,7 +23,9 @@ //! * 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::error::NetworkStateError; -use crate::settings::{BondSettings, IEEE8021XSettings, NetworkConnection, WirelessSettings}; +use crate::settings::{ + BondSettings, BridgeSettings, IEEE8021XSettings, NetworkConnection, WirelessSettings, +}; use crate::types::{BondMode, ConnectionState, DeviceState, DeviceType, Status, SSID}; use agama_utils::openapi::schemas; use cidr::IpInet; @@ -220,28 +222,32 @@ impl NetworkState { controller: &Connection, ports: Vec, ) -> Result<(), NetworkStateError> { - if let ConnectionConfig::Bond(_) = &controller.config { - let mut controlled = vec![]; - for port in ports { - let connection = self - .get_connection_by_interface(&port) - .or_else(|| self.get_connection(&port)) - .ok_or(NetworkStateError::UnknownConnection(port))?; - controlled.push(connection.uuid); - } + match &controller.config { + ConnectionConfig::Bond(_) | ConnectionConfig::Bridge(_) => { + let mut controlled = vec![]; + for port in ports { + let connection = self + .get_connection_by_interface(&port) + .or_else(|| self.get_connection(&port)) + .ok_or(NetworkStateError::UnknownConnection(port))?; + controlled.push(connection.uuid); + } - for conn in self.connections.iter_mut() { - if controlled.contains(&conn.uuid) { - conn.controller = Some(controller.uuid); - } else if conn.controller == Some(controller.uuid) { - conn.controller = None; + for conn in self.connections.iter_mut() { + if controlled.contains(&conn.uuid) { + conn.controller = Some(controller.uuid); + if conn.interface.is_none() { + conn.interface = Some(conn.id.clone()); + } + } else if conn.controller == Some(controller.uuid) { + conn.controller = None; + } } + Ok(()) } - Ok(()) - } else { - Err(NetworkStateError::NotControllerConnection( + _ => Err(NetworkStateError::NotControllerConnection( controller.id.to_owned(), - )) + )), } } } @@ -625,6 +631,10 @@ impl TryFrom for Connection { let config = BondConfig::try_from(bond_config)?; connection.config = config.into(); } + if let Some(bridge_config) = conn.bridge { + let config = BridgeConfig::try_from(bridge_config)?; + connection.config = config.into(); + } if let Some(ieee_8021x_config) = conn.ieee_8021x { connection.ieee_8021x_config = Some(IEEE8021XConfig::try_from(ieee_8021x_config)?); @@ -692,6 +702,9 @@ impl TryFrom for NetworkConnection { ConnectionConfig::Bond(config) => { connection.bond = Some(BondSettings::try_from(config)?); } + ConnectionConfig::Bridge(config) => { + connection.bridge = Some(BridgeSettings::try_from(config)?); + } _ => {} } @@ -720,6 +733,12 @@ pub enum PortConfig { Bridge(BridgePortConfig), } +impl From for ConnectionConfig { + fn from(value: BridgeConfig) -> Self { + Self::Bridge(value) + } +} + impl From for ConnectionConfig { fn from(value: BondConfig) -> Self { Self::Bond(value) @@ -1696,7 +1715,8 @@ impl TryFrom for BondSettings { #[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub struct BridgeConfig { - pub stp: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub stp: Option, #[serde(skip_serializing_if = "Option::is_none")] pub priority: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -1709,6 +1729,49 @@ pub struct BridgeConfig { pub ageing_time: Option, } +impl TryFrom for BridgeConfig { + type Error = NetworkStateError; + + fn try_from(value: ConnectionConfig) -> Result { + match value { + ConnectionConfig::Bridge(config) => Ok(config), + _ => Err(NetworkStateError::UnexpectedConfiguration), + } + } +} + +impl TryFrom for BridgeConfig { + type Error = NetworkStateError; + + fn try_from(settings: BridgeSettings) -> Result { + let stp = settings.stp; + let priority = settings.priority; + let forward_delay = settings.forward_delay; + let hello_time = settings.forward_delay; + + Ok(BridgeConfig { + stp, + priority, + forward_delay, + hello_time, + ..Default::default() + }) + } +} + +impl TryFrom for BridgeSettings { + type Error = NetworkStateError; + + fn try_from(bridge: BridgeConfig) -> Result { + Ok(BridgeSettings { + stp: bridge.stp, + priority: bridge.priority, + forward_delay: bridge.forward_delay, + hello_time: bridge.hello_time, + ..Default::default() + }) + } +} #[derive(Debug, Default, PartialEq, Clone, Serialize, utoipa::ToSchema)] pub struct BridgePortConfig { #[serde(skip_serializing_if = "Option::is_none")] diff --git a/rust/agama-network/src/nm/client.rs b/rust/agama-network/src/nm/client.rs index 836c88765f..93318cb261 100644 --- a/rust/agama-network/src/nm/client.rs +++ b/rust/agama-network/src/nm/client.rs @@ -182,7 +182,7 @@ impl<'a> NetworkManagerClient<'a> { /// Returns the list of network connections. pub async fn connections(&self) -> Result, NmError> { let mut controlled_by: HashMap = HashMap::new(); - let mut uuids_map: HashMap = HashMap::new(); + let mut uuids_map: HashMap = HashMap::new(); let proxy = SettingsProxy::new(&self.connection).await?; let paths = proxy.list_connections().await?; @@ -214,10 +214,10 @@ impl<'a> NetworkManagerClient<'a> { Self::add_secrets(&mut connection.config, &proxy).await?; if let Some(controller) = controller { - controlled_by.insert(connection.uuid, controller.to_string()); + controlled_by.insert(connection.uuid, controller); } if let Some(iname) = &connection.interface { - uuids_map.insert(iname.to_string(), connection.uuid); + uuids_map.insert(connection.uuid, iname.to_string()); } if self.settings_active_connection(path).await?.is_none() { connection.set_down() @@ -231,19 +231,9 @@ impl<'a> NetworkManagerClient<'a> { } for conn in connections.iter_mut() { - let Some(interface_name) = controlled_by.get(&conn.uuid) else { - continue; + if controlled_by.contains_key(&conn.uuid) { + conn.controller = Some(conn.uuid); }; - - if let Some(uuid) = uuids_map.get(interface_name) { - conn.controller = Some(*uuid); - } else { - tracing::warn!( - "Could not found a connection for the interface '{}' (required by connection '{}')", - interface_name, - conn.id - ); - } } Ok(connections) diff --git a/rust/agama-network/src/nm/dbus.rs b/rust/agama-network/src/nm/dbus.rs index 3c05395072..638e9ac087 100644 --- a/rust/agama-network/src/nm/dbus.rs +++ b/rust/agama-network/src/nm/dbus.rs @@ -66,7 +66,7 @@ pub fn connection_to_dbus<'a>( } if let Some(controller) = controller { - let slave_type = match controller.config { + let port_type = match controller.config { ConnectionConfig::Bond(_) => BOND_KEY, ConnectionConfig::Bridge(_) => BRIDGE_KEY, _ => { @@ -74,14 +74,16 @@ pub fn connection_to_dbus<'a>( "" } }; - connection_dbus.insert("slave-type", slave_type.into()); + connection_dbus.insert("port-type", port_type.into()); let master = controller .interface .as_deref() .unwrap_or(controller.id.as_str()); connection_dbus.insert("master", master.into()); + connection_dbus.remove("autoconnect"); + connection_dbus.insert("autoconnect", false.into()); } else { - connection_dbus.insert("slave-type", "".into()); + connection_dbus.insert("port-type", "".into()); connection_dbus.insert("master", "".into()); } @@ -122,6 +124,7 @@ pub fn connection_to_dbus<'a>( } ConnectionConfig::Bond(bond) => { connection_dbus.insert("type", BOND_KEY.into()); + connection_dbus.insert("autoconnect-slaves", 1.into()); if !connection_dbus.contains_key("interface-name") { connection_dbus.insert("interface-name", conn.id.as_str().into()); } @@ -136,6 +139,7 @@ pub fn connection_to_dbus<'a>( } ConnectionConfig::Bridge(bridge) => { connection_dbus.insert("type", BRIDGE_KEY.into()); + connection_dbus.insert("autoconnect-slaves", 1.into()); result.insert(BRIDGE_KEY, bridge_config_to_dbus(bridge)); } ConnectionConfig::Infiniband(infiniband) => { @@ -256,13 +260,34 @@ pub fn merge_dbus_connections<'a>( Ok(merged) } +fn is_bridge(conn: NestedHash) -> bool { + if let Some(connection) = conn.get("connection") { + if let Some(port_type) = connection.get("port-type") { + if port_type.to_string().as_str() == "bridge" { + return true; + } + } + } + + false +} + /// Cleans up the NestedHash that represents a connection. /// -/// By now it just removes the "addresses" key from the "ipv4" and "ipv6" objects, which is -/// replaced with "address-data". However, if "addresses" is present, it takes precedence. +/// If the connections is not a "bridge-port" anymore it removes the "bridge-port" key. +/// +/// It also removes empty files from the "connection" object like the "interface-name", "master", +/// "slave-type", "port-type" keys. +/// +/// Finally, it removes removes the "addresses" and "dns" keys from the "ipv4" and "ipv6" objects, +/// which are replaced with "address-data". /// /// * `conn`: connection represented as a NestedHash. pub fn cleanup_dbus_connection(conn: &mut NestedHash) { + if !is_bridge(conn.to_owned()) { + conn.remove("bridge-port"); + } + if let Some(connection) = conn.get_mut("connection") { if connection.get("interface-name").is_some_and(is_empty_value) { connection.remove("interface-name"); @@ -272,9 +297,13 @@ pub fn cleanup_dbus_connection(conn: &mut NestedHash) { connection.remove("master"); } - if connection.get("slave-type").is_some_and(is_empty_value) { + if connection.get("slave-type").is_some() { connection.remove("slave-type"); } + + if connection.get("port-type").is_some_and(is_empty_value) { + connection.remove("port-type"); + } } if let Some(ipv4) = conn.get_mut("ipv4") { @@ -547,7 +576,9 @@ fn bond_config_to_dbus(config: &BondConfig) -> HashMap<&str, zvariant::Value> { fn bridge_config_to_dbus(bridge: &BridgeConfig) -> HashMap<&str, zvariant::Value> { let mut hash = HashMap::new(); - hash.insert("stp", bridge.stp.into()); + if let Some(stp) = bridge.stp { + hash.insert("stp", stp.into()); + } if let Some(prio) = bridge.priority { hash.insert("priority", prio.into()); } @@ -573,7 +604,7 @@ fn bridge_config_from_dbus(conn: &OwnedNestedHash) -> Result anyhow::Result<()> { + dbg!("TESTING BRIDGE"); + let uuid = Uuid::new_v4().to_string(); + let connection_section = HashMap::from([hi("id", "br0")?, hi("uuid", uuid)?]); + + let bridge_config = Value::new(HashMap::from([ + ("stp".to_string(), Value::from(true)), + ("priority".to_string(), Value::from(10_u32)), + ("forward-delay".to_string(), Value::from(5_u32)), + ])); + + let dbus_conn = HashMap::from([ + ("connection".to_string(), connection_section), + (BRIDGE_KEY.to_string(), bridge_config.try_into().unwrap()), + ]); + + let connection = connection_from_dbus(dbus_conn); + if let ConnectionConfig::Bridge(config) = connection.unwrap().config { + assert_eq!(config.stp, Some(true)); + assert_eq!(config.forward_delay, Some(5_u32)); + } + + Ok(()) + } + #[test] fn test_connection_from_dbus_infiniband() -> anyhow::Result<()> { let uuid = Uuid::new_v4().to_string(); diff --git a/rust/agama-network/src/settings.rs b/rust/agama-network/src/settings.rs index 8ac8161420..4132992fc8 100644 --- a/rust/agama-network/src/settings.rs +++ b/rust/agama-network/src/settings.rs @@ -114,6 +114,35 @@ impl Default for BondSettings { } } +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] +pub struct BridgeSettings { + #[serde(skip_serializing_if = "Option::is_none")] + pub stp: Option, + #[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 = "Vec::is_empty", default)] + pub ports: Vec, +} + +impl Default for BridgeSettings { + fn default() -> Self { + Self { + stp: None, + priority: None, + forward_delay: None, + hello_time: None, + max_age: None, + ports: vec![], + } + } +} + /// IEEE 802.1x (EAP) settings #[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] @@ -215,6 +244,9 @@ pub struct NetworkConnection { /// Bonding settings if part of a bond #[serde(skip_serializing_if = "Option::is_none")] pub bond: Option, + /// Bridge settings if part of a bridge + #[serde(skip_serializing_if = "Option::is_none")] + pub bridge: Option, /// MAC address of the connection's interface #[serde(rename = "mac-address", skip_serializing_if = "Option::is_none")] pub mac_address: Option, @@ -250,6 +282,8 @@ impl NetworkConnection { DeviceType::Wireless } else if self.bond.is_some() { DeviceType::Bond + } else if self.bridge.is_some() { + DeviceType::Bridge } else { DeviceType::Ethernet } diff --git a/rust/agama-network/src/system.rs b/rust/agama-network/src/system.rs index 41f04f3e07..64a80cc623 100644 --- a/rust/agama-network/src/system.rs +++ b/rust/agama-network/src/system.rs @@ -209,6 +209,18 @@ impl NetworkSystemClient { Ok(result?) } + pub async fn set_ports( + &self, + uuid: Uuid, + ports: Vec, + ) -> Result<(), NetworkSystemError> { + let (tx, rx) = oneshot::channel(); + self.actions + .send(Action::SetPorts(uuid, Box::new(ports.clone()), tx))?; + let result = rx.await?; + Ok(result?) + } + /// Applies the network configuration. pub async fn apply(&self) -> Result<(), NetworkSystemError> { let (tx, rx) = oneshot::channel(); diff --git a/rust/agama-server/src/network/web.rs b/rust/agama-server/src/network/web.rs index f8fa1f38be..2e7eadc3a2 100644 --- a/rust/agama-server/src/network/web.rs +++ b/rust/agama-server/src/network/web.rs @@ -30,6 +30,7 @@ use axum::{ routing::{delete, get, patch, post}, Json, Router, }; +use uuid::Uuid; use agama_lib::{ error::ServiceError, @@ -210,19 +211,40 @@ async fn connections( State(state): State, ) -> Result>, NetworkError> { let connections = state.network.get_connections().await?; - let mut result = vec![]; - - for conn in connections { - let state = conn.state.clone(); - let network_connection = NetworkConnection::try_from(conn)?; - let connection_with_state = NetworkConnectionWithState { - connection: network_connection, - state, - }; - result.push(connection_with_state); - } - Ok(Json(result)) + let network_connections = connections + .iter() + .map(|c| { + let state = c.state; + let mut conn = NetworkConnection::try_from(c.clone()).unwrap(); + if let Some(ref mut bond) = conn.bond { + bond.ports = ports_for(connections.to_owned(), c.uuid); + } + if let Some(ref mut bridge) = conn.bridge { + bridge.ports = ports_for(connections.to_owned(), c.uuid); + }; + NetworkConnectionWithState { + connection: conn, + state, + } + }) + .collect(); + + Ok(Json(network_connections)) +} + +fn ports_for(connections: Vec, uuid: Uuid) -> Vec { + return connections + .iter() + .filter(|c| c.controller == Some(uuid)) + .map(|c| { + if let Some(interface) = c.interface.to_owned() { + interface + } else { + c.clone().id + } + }) + .collect(); } #[utoipa::path( @@ -235,15 +257,26 @@ async fn connections( )] async fn add_connection( State(state): State, - Json(conn): Json, + Json(net_conn): Json, ) -> Result, NetworkError> { - let conn = Connection::try_from(conn)?; + let bond = net_conn.bond.clone(); + let bridge = net_conn.bridge.clone(); + let conn = Connection::try_from(net_conn)?; let id = conn.id.clone(); - state.network.add_connection(conn).await?; + state.network.add_connection(conn.clone()).await?; + match state.network.get_connection(&id).await? { None => Err(NetworkError::CannotAddConnection(id.clone())), - Some(conn) => Ok(Json(conn)), + Some(conn) => { + if let Some(bond) = bond { + state.network.set_ports(conn.uuid, bond.ports).await?; + } + if let Some(bridge) = bridge { + state.network.set_ports(conn.uuid, bridge.ports).await?; + } + Ok(Json(conn)) + } } } @@ -307,6 +340,9 @@ async fn update_connection( .get_connection(&id) .await? .ok_or_else(|| NetworkError::UnknownConnection(id.clone()))?; + let bond = conn.bond.clone(); + let bridge = conn.bridge.clone(); + let mut conn = Connection::try_from(conn)?; if orig_conn.id != id { // FIXME: why? @@ -315,7 +351,15 @@ async fn update_connection( conn.uuid = orig_conn.uuid; } - state.network.update_connection(conn).await?; + state.network.update_connection(conn.clone()).await?; + + if let Some(bond) = bond { + state.network.set_ports(conn.uuid, bond.ports).await?; + } + if let Some(bridge) = bridge { + state.network.set_ports(conn.uuid, bridge.ports).await?; + } + Ok(StatusCode::NO_CONTENT) } diff --git a/rust/agama-server/src/web/docs/network.rs b/rust/agama-server/src/web/docs/network.rs index e65a28f5f3..8e4ecb52dd 100644 --- a/rust/agama-server/src/web/docs/network.rs +++ b/rust/agama-server/src/web/docs/network.rs @@ -50,6 +50,7 @@ impl ApiDocBuilder for NetworkApiDocBuilder { fn components(&self) -> Components { ComponentsBuilder::new() .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() diff --git a/rust/agama-server/tests/network_service.rs b/rust/agama-server/tests/network_service.rs index c23a16c8aa..292c1e7378 100644 --- a/rust/agama-server/tests/network_service.rs +++ b/rust/agama-server/tests/network_service.rs @@ -21,7 +21,7 @@ pub mod common; use agama_lib::error::ServiceError; -use agama_lib::network::settings::{BondSettings, NetworkConnection}; +use agama_lib::network::settings::{BondSettings, BridgeSettings, NetworkConnection}; use agama_lib::network::types::{DeviceType, SSID}; use agama_lib::network::{ model::{self, AccessPoint, GeneralState, NetworkState, StateConfig}, @@ -185,7 +185,7 @@ async fn test_add_bond_connection() -> Result<(), Box> { let state = build_state().await; let network_service = build_service(state.clone()).await?; - let eth0 = NetworkConnection { + let eth2 = NetworkConnection { id: "eth2".to_string(), ..Default::default() }; @@ -207,7 +207,7 @@ async fn test_add_bond_connection() -> Result<(), Box> { .uri("/connections") .header("Content-Type", "application/json") .method(Method::POST) - .body(serde_json::to_string(ð0)?) + .body(serde_json::to_string(ð2)?) .unwrap(); let response = network_service.clone().oneshot(request).await?; @@ -236,6 +236,52 @@ async fn test_add_bond_connection() -> Result<(), Box> { assert!(body.contains(r#""id":"bond0""#)); assert!(body.contains(r#""mode":"active-backup""#)); assert!(body.contains(r#""primary=eth0""#)); + assert!(body.contains(r#""ports":["eth0"]"#)); + + Ok(()) +} + +#[test] +async fn test_add_bridge_connection() -> Result<(), Box> { + let state = build_state().await; + let network_service = build_service(state.clone()).await?; + + let br0 = NetworkConnection { + id: "br0".to_string(), + method4: Some("manual".to_string()), + method6: Some("disabled".to_string()), + interface: Some("br0".to_string()), + bridge: Some(BridgeSettings { + ports: vec!["eth0".to_string()], + stp: Some(false), + ..Default::default() + }), + ..Default::default() + }; + + let request = Request::builder() + .uri("/connections") + .header("Content-Type", "application/json") + .method(Method::POST) + .body(serde_json::to_string(&br0)?) + .unwrap(); + + let response = network_service.clone().oneshot(request).await?; + assert_eq!(response.status(), StatusCode::OK); + + let request = Request::builder() + .uri("/connections") + .method(Method::GET) + .body(Body::empty()) + .unwrap(); + + let response = network_service.clone().oneshot(request).await?; + assert_eq!(response.status(), StatusCode::OK); + let body = body_to_string(response.into_body()).await; + assert!(body.contains(r#""id":"eth0""#)); + assert!(body.contains(r#""id":"br0""#)); + assert!(body.contains(r#""ports":["eth0"]"#)); + assert!(body.contains(r#""stp":false"#)); Ok(()) } diff --git a/rust/package/agama.changes b/rust/package/agama.changes index b3d2d089ad..1394c08f9b 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Wed May 14 15:20:42 UTC 2025 - Knut Anderssen + +- Add support for bridge connections (gh#openSUSE/agama#2258). + ------------------------------------------------------------------- Mon May 19 14:02:04 UTC 2025 - Imobach Gonzalez Sosa