337 lines
11 KiB
Rust
337 lines
11 KiB
Rust
use std::str::FromStr;
|
|
|
|
use async_trait::async_trait;
|
|
use harmony_types::switch::{PortDeclaration, PortLocation};
|
|
use log::{debug, info};
|
|
use regex::Regex;
|
|
|
|
use crate::{
|
|
BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo,
|
|
InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, PortOperatingMode,
|
|
parse_brocade_mac_address, shell::BrocadeShell,
|
|
};
|
|
|
|
#[derive(Debug)]
|
|
pub struct NetworkOperatingSystemClient {
|
|
shell: BrocadeShell,
|
|
version: BrocadeInfo,
|
|
}
|
|
|
|
impl NetworkOperatingSystemClient {
|
|
pub fn init(mut shell: BrocadeShell, version_info: BrocadeInfo) -> Self {
|
|
shell.before_all(vec!["terminal length 0".into()]);
|
|
|
|
Self {
|
|
shell,
|
|
version: version_info,
|
|
}
|
|
}
|
|
|
|
fn parse_mac_entry(&self, line: &str) -> Option<Result<MacAddressEntry, Error>> {
|
|
debug!("[Brocade] Parsing mac address entry: {line}");
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
if parts.len() < 5 {
|
|
return None;
|
|
}
|
|
|
|
let (vlan, mac_address, port) = match parts.len() {
|
|
5 => (
|
|
u16::from_str(parts[0]).ok()?,
|
|
parse_brocade_mac_address(parts[1]).ok()?,
|
|
parts[4].to_string(),
|
|
),
|
|
_ => (
|
|
u16::from_str(parts[0]).ok()?,
|
|
parse_brocade_mac_address(parts[1]).ok()?,
|
|
parts[5].to_string(),
|
|
),
|
|
};
|
|
|
|
let port =
|
|
PortDeclaration::parse(&port).map_err(|e| Error::UnexpectedError(format!("{e}")));
|
|
|
|
match port {
|
|
Ok(p) => Some(Ok(MacAddressEntry {
|
|
vlan,
|
|
mac_address,
|
|
port: p,
|
|
})),
|
|
Err(e) => Some(Err(e)),
|
|
}
|
|
}
|
|
|
|
fn parse_inter_switch_link_entry(&self, line: &str) -> Option<Result<InterSwitchLink, Error>> {
|
|
debug!("[Brocade] Parsing inter switch link entry: {line}");
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
if parts.len() < 10 {
|
|
return None;
|
|
}
|
|
|
|
let local_port = PortLocation::from_str(parts[2]).ok()?;
|
|
let remote_port = PortLocation::from_str(parts[5]).ok()?;
|
|
|
|
Some(Ok(InterSwitchLink {
|
|
local_port,
|
|
remote_port: Some(remote_port),
|
|
}))
|
|
}
|
|
|
|
fn parse_interface_status_entry(&self, line: &str) -> Option<Result<InterfaceInfo, Error>> {
|
|
debug!("[Brocade] Parsing interface status entry: {line}");
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
if parts.len() < 6 {
|
|
return None;
|
|
}
|
|
|
|
let interface_type = match parts[0] {
|
|
"Fo" => InterfaceType::Ethernet("FortyGigabitEthernet".to_string()),
|
|
"Te" => InterfaceType::Ethernet("TenGigabitEthernet".to_string()),
|
|
_ => return None,
|
|
};
|
|
let port_location = PortLocation::from_str(parts[1]).ok()?;
|
|
let status = match parts[2] {
|
|
"connected" => InterfaceStatus::Connected,
|
|
"notconnected" => InterfaceStatus::NotConnected,
|
|
"sfpAbsent" => InterfaceStatus::SfpAbsent,
|
|
_ => return None,
|
|
};
|
|
let operating_mode = match parts[3] {
|
|
"ISL" => Some(PortOperatingMode::Fabric),
|
|
"Trunk" => Some(PortOperatingMode::Trunk),
|
|
"Access" => Some(PortOperatingMode::Access),
|
|
"--" => None,
|
|
_ => return None,
|
|
};
|
|
|
|
Some(Ok(InterfaceInfo {
|
|
name: format!("{interface_type} {port_location}"),
|
|
port_location,
|
|
interface_type,
|
|
operating_mode,
|
|
status,
|
|
}))
|
|
}
|
|
|
|
fn map_configure_interfaces_error(&self, err: Error) -> Error {
|
|
debug!("[Brocade] {err}");
|
|
|
|
if let Error::CommandError(message) = &err {
|
|
if message.contains("switchport")
|
|
&& message.contains("Cannot configure aggregator member")
|
|
{
|
|
let re = Regex::new(r"\(conf-if-([a-zA-Z]+)-([\d/]+)\)#").unwrap();
|
|
|
|
if let Some(caps) = re.captures(message) {
|
|
let interface_type = &caps[1];
|
|
let port_location = &caps[2];
|
|
let interface = format!("{interface_type} {port_location}");
|
|
|
|
return Error::CommandError(format!(
|
|
"Cannot configure interface '{interface}', it is a member of a port-channel (LAG)"
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
err
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl BrocadeClient for NetworkOperatingSystemClient {
|
|
async fn version(&self) -> Result<BrocadeInfo, Error> {
|
|
Ok(self.version.clone())
|
|
}
|
|
|
|
async fn get_mac_address_table(&self) -> Result<Vec<MacAddressEntry>, Error> {
|
|
let output = self
|
|
.shell
|
|
.run_command("show mac-address-table", ExecutionMode::Regular)
|
|
.await?;
|
|
|
|
output
|
|
.lines()
|
|
.skip(1)
|
|
.filter_map(|line| self.parse_mac_entry(line))
|
|
.collect()
|
|
}
|
|
|
|
async fn get_stack_topology(&self) -> Result<Vec<InterSwitchLink>, Error> {
|
|
let output = self
|
|
.shell
|
|
.run_command("show fabric isl", ExecutionMode::Regular)
|
|
.await?;
|
|
|
|
output
|
|
.lines()
|
|
.skip(6)
|
|
.filter_map(|line| self.parse_inter_switch_link_entry(line))
|
|
.collect()
|
|
}
|
|
|
|
async fn get_interfaces(&self) -> Result<Vec<InterfaceInfo>, Error> {
|
|
let output = self
|
|
.shell
|
|
.run_command(
|
|
"show interface status rbridge-id all",
|
|
ExecutionMode::Regular,
|
|
)
|
|
.await?;
|
|
|
|
output
|
|
.lines()
|
|
.skip(2)
|
|
.filter_map(|line| self.parse_interface_status_entry(line))
|
|
.collect()
|
|
}
|
|
|
|
async fn configure_interfaces(
|
|
&self,
|
|
interfaces: &Vec<(String, PortOperatingMode)>,
|
|
) -> Result<(), Error> {
|
|
info!("[Brocade] Configuring {} interface(s)...", interfaces.len());
|
|
|
|
let mut commands = vec!["configure terminal".to_string()];
|
|
|
|
for interface in interfaces {
|
|
commands.push(format!("interface {}", interface.0));
|
|
|
|
match interface.1 {
|
|
PortOperatingMode::Fabric => {
|
|
commands.push("fabric isl enable".into());
|
|
commands.push("fabric trunk enable".into());
|
|
}
|
|
PortOperatingMode::Trunk => {
|
|
commands.push("switchport".into());
|
|
commands.push("switchport mode trunk".into());
|
|
commands.push("switchport trunk allowed vlan all".into());
|
|
commands.push("no switchport trunk tag native-vlan".into());
|
|
commands.push("spanning-tree shutdown".into());
|
|
commands.push("no fabric isl enable".into());
|
|
commands.push("no fabric trunk enable".into());
|
|
commands.push("no shutdown".into());
|
|
}
|
|
PortOperatingMode::Access => {
|
|
commands.push("switchport".into());
|
|
commands.push("switchport mode access".into());
|
|
commands.push("switchport access vlan 1".into());
|
|
commands.push("no spanning-tree shutdown".into());
|
|
commands.push("no fabric isl enable".into());
|
|
commands.push("no fabric trunk enable".into());
|
|
}
|
|
}
|
|
|
|
commands.push("no shutdown".into());
|
|
commands.push("exit".into());
|
|
}
|
|
|
|
self.shell
|
|
.run_commands(commands, ExecutionMode::Regular)
|
|
.await
|
|
.map_err(|err| self.map_configure_interfaces_error(err))?;
|
|
|
|
info!("[Brocade] Interfaces configured.");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn find_available_channel_id(&self) -> Result<PortChannelId, Error> {
|
|
info!("[Brocade] Finding next available channel id...");
|
|
|
|
let output = self
|
|
.shell
|
|
.run_command("show port-channel summary", ExecutionMode::Regular)
|
|
.await?;
|
|
|
|
let used_ids: Vec<u8> = output
|
|
.lines()
|
|
.skip(6)
|
|
.filter_map(|line| {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
if parts.len() < 8 {
|
|
return None;
|
|
}
|
|
|
|
u8::from_str(parts[0]).ok()
|
|
})
|
|
.collect();
|
|
|
|
let mut next_id: u8 = 1;
|
|
loop {
|
|
if !used_ids.contains(&next_id) {
|
|
break;
|
|
}
|
|
next_id += 1;
|
|
}
|
|
|
|
info!("[Brocade] Found channel id: {next_id}");
|
|
Ok(next_id)
|
|
}
|
|
|
|
async fn create_port_channel(
|
|
&self,
|
|
channel_id: PortChannelId,
|
|
channel_name: &str,
|
|
ports: &[PortLocation],
|
|
) -> Result<(), Error> {
|
|
info!(
|
|
"[Brocade] Configuring port-channel '{channel_id} {channel_name}' with ports: {}",
|
|
ports
|
|
.iter()
|
|
.map(|p| format!("{p}"))
|
|
.collect::<Vec<String>>()
|
|
.join(", ")
|
|
);
|
|
|
|
let interfaces = self.get_interfaces().await?;
|
|
|
|
let mut commands = vec![
|
|
"configure terminal".into(),
|
|
format!("interface port-channel {}", channel_id),
|
|
"no shutdown".into(),
|
|
"exit".into(),
|
|
];
|
|
|
|
for port in ports {
|
|
let interface = interfaces.iter().find(|i| i.port_location == *port);
|
|
let Some(interface) = interface else {
|
|
continue;
|
|
};
|
|
|
|
commands.push(format!("interface {}", interface.name));
|
|
commands.push("no switchport".into());
|
|
commands.push("no ip address".into());
|
|
commands.push("no fabric isl enable".into());
|
|
commands.push("no fabric trunk enable".into());
|
|
commands.push(format!("channel-group {channel_id} mode active"));
|
|
commands.push("no shutdown".into());
|
|
commands.push("exit".into());
|
|
}
|
|
|
|
self.shell
|
|
.run_commands(commands, ExecutionMode::Regular)
|
|
.await?;
|
|
|
|
info!("[Brocade] Port-channel '{channel_name}' configured.");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn clear_port_channel(&self, channel_name: &str) -> Result<(), Error> {
|
|
info!("[Brocade] Clearing port-channel: {channel_name}");
|
|
|
|
let commands = vec![
|
|
"configure terminal".into(),
|
|
format!("no interface port-channel {}", channel_name),
|
|
"exit".into(),
|
|
];
|
|
|
|
self.shell
|
|
.run_commands(commands, ExecutionMode::Regular)
|
|
.await?;
|
|
|
|
info!("[Brocade] Port-channel '{channel_name}' cleared.");
|
|
Ok(())
|
|
}
|
|
}
|