use std::str::FromStr; use async_trait::async_trait; use harmony_types::switch::{PortDeclaration, PortLocation}; use log::{debug, info}; use crate::{ BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo, InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, PortOperatingMode, parse_brocade_mac_address, shell::BrocadeShell, }; 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> { 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> { 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> { 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, })) } } #[async_trait] impl BrocadeClient for NetworkOperatingSystemClient { async fn version(&self) -> Result { Ok(self.version.clone()) } async fn get_mac_address_table(&self) -> Result, 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, 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, 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("no spanning-tree shutdown".into()); commands.push("no fabric isl enable".into()); commands.push("no fabric trunk enable".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()); } commands.push("write memory".into()); self.shell .run_commands(commands, ExecutionMode::Regular) .await?; info!("[Brocade] Interfaces configured."); Ok(()) } async fn find_available_channel_id(&self) -> Result { info!("[Brocade] Finding next available channel id..."); let output = self .shell .run_command("show port-channel", ExecutionMode::Regular) .await?; let used_ids: Vec = 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_name} {channel_id}' with ports: {ports:?}" ); 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 {} mode active", channel_id)); commands.push("no shutdown".into()); commands.push("exit".into()); } commands.push("write memory".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(), "write memory".into(), ]; self.shell .run_commands(commands, ExecutionMode::Regular) .await?; info!("[Brocade] Port-channel '{channel_name}' cleared."); Ok(()) } }