379 lines
12 KiB
Rust
379 lines
12 KiB
Rust
use async_trait::async_trait;
|
|
use brocade::{BrocadeClient, BrocadeOptions, InterSwitchLink, InterfaceStatus, PortOperatingMode};
|
|
use harmony_types::{
|
|
net::{IpAddress, MacAddress},
|
|
switch::{PortDeclaration, PortLocation},
|
|
};
|
|
use option_ext::OptionExt;
|
|
|
|
use crate::topology::{SwitchClient, SwitchError};
|
|
|
|
#[derive(Debug)]
|
|
pub struct BrocadeSwitchClient {
|
|
brocade: Box<dyn BrocadeClient + Send + Sync>,
|
|
}
|
|
|
|
impl BrocadeSwitchClient {
|
|
pub async fn init(
|
|
ip_addresses: &[IpAddress],
|
|
username: &str,
|
|
password: &str,
|
|
options: Option<BrocadeOptions>,
|
|
) -> Result<Self, brocade::Error> {
|
|
let brocade = brocade::init(ip_addresses, 22, username, password, options).await?;
|
|
Ok(Self { brocade })
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl SwitchClient for BrocadeSwitchClient {
|
|
async fn setup(&self) -> Result<(), SwitchError> {
|
|
let stack_topology = self
|
|
.brocade
|
|
.get_stack_topology()
|
|
.await
|
|
.map_err(|e| SwitchError::new(e.to_string()))?;
|
|
|
|
let interfaces = self
|
|
.brocade
|
|
.get_interfaces()
|
|
.await
|
|
.map_err(|e| SwitchError::new(e.to_string()))?;
|
|
|
|
let interfaces: Vec<(String, PortOperatingMode)> = interfaces
|
|
.into_iter()
|
|
.filter(|interface| {
|
|
interface.operating_mode.is_none() && interface.status == InterfaceStatus::Connected
|
|
})
|
|
.filter(|interface| {
|
|
!stack_topology.iter().any(|link: &InterSwitchLink| {
|
|
link.local_port == interface.port_location
|
|
|| link.remote_port.contains(&interface.port_location)
|
|
})
|
|
})
|
|
.map(|interface| (interface.name.clone(), PortOperatingMode::Access))
|
|
.collect();
|
|
|
|
if interfaces.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
self.brocade
|
|
.configure_interfaces(interfaces)
|
|
.await
|
|
.map_err(|e| SwitchError::new(e.to_string()))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn find_port(
|
|
&self,
|
|
mac_address: &MacAddress,
|
|
) -> Result<Option<PortLocation>, SwitchError> {
|
|
let table = self
|
|
.brocade
|
|
.get_mac_address_table()
|
|
.await
|
|
.map_err(|e| SwitchError::new(format!("{e}")))?;
|
|
|
|
let port = table
|
|
.iter()
|
|
.find(|entry| entry.mac_address == *mac_address)
|
|
.map(|entry| match &entry.port {
|
|
PortDeclaration::Single(port_location) => Ok(port_location.clone()),
|
|
_ => Err(SwitchError::new(
|
|
"Multiple ports found for MAC address".into(),
|
|
)),
|
|
});
|
|
|
|
match port {
|
|
Some(Ok(p)) => Ok(Some(p)),
|
|
Some(Err(e)) => Err(e),
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
|
|
async fn configure_port_channel(
|
|
&self,
|
|
channel_name: &str,
|
|
switch_ports: Vec<PortLocation>,
|
|
) -> Result<u8, SwitchError> {
|
|
let channel_id = self
|
|
.brocade
|
|
.find_available_channel_id()
|
|
.await
|
|
.map_err(|e| SwitchError::new(format!("{e}")))?;
|
|
|
|
self.brocade
|
|
.create_port_channel(channel_id, channel_name, &switch_ports)
|
|
.await
|
|
.map_err(|e| SwitchError::new(format!("{e}")))?;
|
|
|
|
Ok(channel_id)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use assertor::*;
|
|
use async_trait::async_trait;
|
|
use brocade::{
|
|
BrocadeClient, BrocadeInfo, Error, InterSwitchLink, InterfaceInfo, InterfaceStatus,
|
|
InterfaceType, MacAddressEntry, PortChannelId, PortOperatingMode,
|
|
};
|
|
use harmony_types::switch::PortLocation;
|
|
|
|
use crate::{infra::brocade::BrocadeSwitchClient, topology::SwitchClient};
|
|
|
|
#[tokio::test]
|
|
async fn setup_should_configure_ethernet_interfaces_as_access_ports() {
|
|
let first_interface = given_interface()
|
|
.with_port_location(PortLocation(1, 0, 1))
|
|
.build();
|
|
let second_interface = given_interface()
|
|
.with_port_location(PortLocation(1, 0, 4))
|
|
.build();
|
|
let brocade = Box::new(FakeBrocadeClient::new(
|
|
vec![],
|
|
vec![first_interface.clone(), second_interface.clone()],
|
|
));
|
|
let client = BrocadeSwitchClient {
|
|
brocade: brocade.clone(),
|
|
};
|
|
|
|
client.setup().await.unwrap();
|
|
|
|
let configured_interfaces = brocade.configured_interfaces.lock().unwrap();
|
|
assert_that!(*configured_interfaces).contains_exactly(vec![
|
|
(first_interface.name.clone(), PortOperatingMode::Access),
|
|
(second_interface.name.clone(), PortOperatingMode::Access),
|
|
]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn setup_with_an_already_configured_interface_should_skip_configuration() {
|
|
let brocade = Box::new(FakeBrocadeClient::new(
|
|
vec![],
|
|
vec![
|
|
given_interface()
|
|
.with_operating_mode(Some(PortOperatingMode::Access))
|
|
.build(),
|
|
],
|
|
));
|
|
let client = BrocadeSwitchClient {
|
|
brocade: brocade.clone(),
|
|
};
|
|
|
|
client.setup().await.unwrap();
|
|
|
|
let configured_interfaces = brocade.configured_interfaces.lock().unwrap();
|
|
assert_that!(*configured_interfaces).is_empty();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn setup_with_a_disconnected_interface_should_skip_configuration() {
|
|
let brocade = Box::new(FakeBrocadeClient::new(
|
|
vec![],
|
|
vec![
|
|
given_interface()
|
|
.with_status(InterfaceStatus::SfpAbsent)
|
|
.build(),
|
|
given_interface()
|
|
.with_status(InterfaceStatus::NotConnected)
|
|
.build(),
|
|
],
|
|
));
|
|
let client = BrocadeSwitchClient {
|
|
brocade: brocade.clone(),
|
|
};
|
|
|
|
client.setup().await.unwrap();
|
|
|
|
let configured_interfaces = brocade.configured_interfaces.lock().unwrap();
|
|
assert_that!(*configured_interfaces).is_empty();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn setup_with_inter_switch_links_should_not_configure_interfaces_used_to_form_stack() {
|
|
let brocade = Box::new(FakeBrocadeClient::new(
|
|
vec![
|
|
given_inter_switch_link()
|
|
.between(PortLocation(1, 0, 1), PortLocation(2, 0, 1))
|
|
.build(),
|
|
given_inter_switch_link()
|
|
.between(PortLocation(2, 0, 2), PortLocation(3, 0, 1))
|
|
.build(),
|
|
],
|
|
vec![
|
|
given_interface()
|
|
.with_port_location(PortLocation(1, 0, 1))
|
|
.build(),
|
|
given_interface()
|
|
.with_port_location(PortLocation(2, 0, 1))
|
|
.build(),
|
|
given_interface()
|
|
.with_port_location(PortLocation(3, 0, 1))
|
|
.build(),
|
|
],
|
|
));
|
|
let client = BrocadeSwitchClient {
|
|
brocade: brocade.clone(),
|
|
};
|
|
|
|
client.setup().await.unwrap();
|
|
|
|
let configured_interfaces = brocade.configured_interfaces.lock().unwrap();
|
|
assert_that!(*configured_interfaces).is_empty();
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct FakeBrocadeClient {
|
|
stack_topology: Vec<InterSwitchLink>,
|
|
interfaces: Vec<InterfaceInfo>,
|
|
configured_interfaces: Arc<Mutex<Vec<(String, PortOperatingMode)>>>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl BrocadeClient for FakeBrocadeClient {
|
|
async fn version(&self) -> Result<BrocadeInfo, Error> {
|
|
todo!()
|
|
}
|
|
|
|
async fn get_mac_address_table(&self) -> Result<Vec<MacAddressEntry>, Error> {
|
|
todo!()
|
|
}
|
|
|
|
async fn get_stack_topology(&self) -> Result<Vec<InterSwitchLink>, Error> {
|
|
Ok(self.stack_topology.clone())
|
|
}
|
|
|
|
async fn get_interfaces(&self) -> Result<Vec<InterfaceInfo>, Error> {
|
|
Ok(self.interfaces.clone())
|
|
}
|
|
|
|
async fn configure_interfaces(
|
|
&self,
|
|
interfaces: Vec<(String, PortOperatingMode)>,
|
|
) -> Result<(), Error> {
|
|
let mut configured_interfaces = self.configured_interfaces.lock().unwrap();
|
|
*configured_interfaces = interfaces;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn find_available_channel_id(&self) -> Result<PortChannelId, Error> {
|
|
todo!()
|
|
}
|
|
|
|
async fn create_port_channel(
|
|
&self,
|
|
_channel_id: PortChannelId,
|
|
_channel_name: &str,
|
|
_ports: &[PortLocation],
|
|
) -> Result<(), Error> {
|
|
todo!()
|
|
}
|
|
|
|
async fn clear_port_channel(&self, _channel_name: &str) -> Result<(), Error> {
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
impl FakeBrocadeClient {
|
|
fn new(stack_topology: Vec<InterSwitchLink>, interfaces: Vec<InterfaceInfo>) -> Self {
|
|
Self {
|
|
stack_topology,
|
|
interfaces,
|
|
configured_interfaces: Arc::new(Mutex::new(vec![])),
|
|
}
|
|
}
|
|
}
|
|
|
|
struct InterfaceInfoBuilder {
|
|
port_location: Option<PortLocation>,
|
|
interface_type: Option<InterfaceType>,
|
|
operating_mode: Option<PortOperatingMode>,
|
|
status: Option<InterfaceStatus>,
|
|
}
|
|
|
|
impl InterfaceInfoBuilder {
|
|
fn build(&self) -> InterfaceInfo {
|
|
let interface_type = self
|
|
.interface_type
|
|
.clone()
|
|
.unwrap_or(InterfaceType::Ethernet("TenGigabitEthernet".into()));
|
|
let port_location = self.port_location.clone().unwrap_or(PortLocation(1, 0, 1));
|
|
let name = format!("{interface_type} {port_location}");
|
|
let status = self.status.clone().unwrap_or(InterfaceStatus::Connected);
|
|
|
|
InterfaceInfo {
|
|
name,
|
|
port_location,
|
|
interface_type,
|
|
operating_mode: self.operating_mode.clone(),
|
|
status,
|
|
}
|
|
}
|
|
|
|
fn with_port_location(self, port_location: PortLocation) -> Self {
|
|
Self {
|
|
port_location: Some(port_location),
|
|
..self
|
|
}
|
|
}
|
|
|
|
fn with_operating_mode(self, operating_mode: Option<PortOperatingMode>) -> Self {
|
|
Self {
|
|
operating_mode,
|
|
..self
|
|
}
|
|
}
|
|
|
|
fn with_status(self, status: InterfaceStatus) -> Self {
|
|
Self {
|
|
status: Some(status),
|
|
..self
|
|
}
|
|
}
|
|
}
|
|
|
|
struct InterSwitchLinkBuilder {
|
|
link: Option<(PortLocation, PortLocation)>,
|
|
}
|
|
|
|
impl InterSwitchLinkBuilder {
|
|
fn build(&self) -> InterSwitchLink {
|
|
let link = self
|
|
.link
|
|
.clone()
|
|
.unwrap_or((PortLocation(1, 0, 1), PortLocation(2, 0, 1)));
|
|
|
|
InterSwitchLink {
|
|
local_port: link.0,
|
|
remote_port: Some(link.1),
|
|
}
|
|
}
|
|
|
|
fn between(self, local_port: PortLocation, remote_port: PortLocation) -> Self {
|
|
Self {
|
|
link: Some((local_port, remote_port)),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn given_interface() -> InterfaceInfoBuilder {
|
|
InterfaceInfoBuilder {
|
|
port_location: None,
|
|
interface_type: None,
|
|
operating_mode: None,
|
|
status: None,
|
|
}
|
|
}
|
|
|
|
fn given_inter_switch_link() -> InterSwitchLinkBuilder {
|
|
InterSwitchLinkBuilder { link: None }
|
|
}
|
|
}
|