use std::net::IpAddr; use std::{ fmt::{self, Display}, time::Duration, }; use crate::network_operating_system::NetworkOperatingSystemClient; use crate::{ fast_iron::FastIronClient, shell::{BrocadeSession, BrocadeShell}, }; use async_trait::async_trait; use harmony_types::net::MacAddress; use harmony_types::switch::{PortDeclaration, PortLocation}; use regex::Regex; mod fast_iron; mod network_operating_system; mod shell; mod ssh; #[derive(Default, Clone, Debug)] pub struct BrocadeOptions { pub dry_run: bool, pub ssh: ssh::SshOptions, pub timeouts: TimeoutConfig, } #[derive(Clone, Debug)] pub struct TimeoutConfig { pub shell_ready: Duration, pub command_execution: Duration, pub cleanup: Duration, pub message_wait: Duration, } impl Default for TimeoutConfig { fn default() -> Self { Self { shell_ready: Duration::from_secs(10), command_execution: Duration::from_secs(60), // Commands like `deploy` (for a LAG) can take a while cleanup: Duration::from_secs(10), message_wait: Duration::from_millis(500), } } } enum ExecutionMode { Regular, Privileged, } #[derive(Clone, Debug)] pub struct BrocadeInfo { os: BrocadeOs, version: String, } #[derive(Clone, Debug)] pub enum BrocadeOs { NetworkOperatingSystem, FastIron, Unknown, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] pub struct MacAddressEntry { pub vlan: u16, pub mac_address: MacAddress, pub port: PortDeclaration, } pub type PortChannelId = u8; /// Represents a single physical or logical link connecting two switches within a stack or fabric. /// /// This structure provides a standardized view of the topology regardless of the /// underlying Brocade OS configuration (stacking vs. fabric). #[derive(Debug, PartialEq, Eq, Clone)] pub struct InterSwitchLink { /// The local port on the switch where the topology command was run. pub local_port: PortLocation, /// The port on the directly connected neighboring switch. pub remote_port: Option, } /// Represents the key running configuration status of a single switch interface. #[derive(Debug, PartialEq, Eq, Clone)] pub struct InterfaceInfo { /// The full configuration name (e.g., "TenGigabitEthernet 1/0/1", "FortyGigabitEthernet 2/0/2"). pub name: String, /// The physical location of the interface. pub port_location: PortLocation, /// The parsed type and name prefix of the interface. pub interface_type: InterfaceType, /// The primary configuration mode defining the interface's behavior (L2, L3, Fabric). pub operating_mode: Option, /// Indicates the current state of the interface. pub status: InterfaceStatus, } /// Categorizes the functional type of a switch interface. #[derive(Debug, PartialEq, Eq, Clone)] pub enum InterfaceType { /// Physical or virtual Ethernet interface (e.g., TenGigabitEthernet, FortyGigabitEthernet). Ethernet(String), } impl fmt::Display for InterfaceType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { InterfaceType::Ethernet(name) => write!(f, "{name}"), } } } /// Defines the primary configuration mode of a switch interface, representing mutually exclusive roles. #[derive(Debug, PartialEq, Eq, Clone)] pub enum PortOperatingMode { /// The interface is explicitly configured for Brocade fabric roles (ISL or Trunk enabled). Fabric, /// The interface is configured for standard Layer 2 switching as Trunk port (`switchport mode trunk`). Trunk, /// The interface is configured for standard Layer 2 switching as Access port (`switchport` without trunk mode). Access, } /// Defines the possible status of an interface. #[derive(Debug, PartialEq, Eq, Clone)] pub enum InterfaceStatus { /// The interface is connected. Connected, /// The interface is not connected and is not expected to be. NotConnected, /// The interface is not connected but is expected to be (configured with `no shutdown`). SfpAbsent, } pub async fn init( ip_addresses: &[IpAddr], port: u16, username: &str, password: &str, options: Option, ) -> Result, Error> { let shell = BrocadeShell::init(ip_addresses, port, username, password, options).await?; let version_info = shell .with_session(ExecutionMode::Regular, |session| { Box::pin(get_brocade_info(session)) }) .await?; Ok(match version_info.os { BrocadeOs::FastIron => Box::new(FastIronClient::init(shell, version_info)), BrocadeOs::NetworkOperatingSystem => { Box::new(NetworkOperatingSystemClient::init(shell, version_info)) } BrocadeOs::Unknown => todo!(), }) } #[async_trait] pub trait BrocadeClient { /// Retrieves the operating system and version details from the connected Brocade switch. /// /// This is typically the first call made after establishing a connection to determine /// the switch OS family (e.g., FastIron, NOS) for feature compatibility. /// /// # Returns /// /// A `BrocadeInfo` structure containing parsed OS type and version string. async fn version(&self) -> Result; /// Retrieves the dynamically learned MAC address table from the switch. /// /// This is crucial for discovering where specific network endpoints (MAC addresses) /// are currently located on the physical ports. /// /// # Returns /// /// A vector of `MacAddressEntry`, where each entry typically contains VLAN, MAC address, /// and the associated port name/index. async fn get_mac_address_table(&self) -> Result, Error>; /// Derives the physical connections used to link multiple switches together /// to form a single logical entity (stack, fabric, etc.). /// /// This abstracts the underlying configuration (e.g., stack ports, fabric ports) /// to return a standardized view of the topology. /// /// # Returns /// /// A vector of `InterSwitchLink` structs detailing which ports are used for stacking/fabric. /// If the switch is not stacked, returns an empty vector. async fn get_stack_topology(&self) -> Result, Error>; /// Retrieves the status for all interfaces /// /// # Returns /// /// A vector of `InterfaceInfo` structures. async fn get_interfaces(&self) -> Result, Error>; /// Configures a set of interfaces to be operated with a specified mode (access ports, ISL, etc.). async fn configure_interfaces( &self, interfaces: Vec<(String, PortOperatingMode)>, ) -> Result<(), Error>; /// Scans the existing configuration to find the next available (unused) /// Port-Channel ID (`lag` or `trunk`) for assignment. /// /// # Returns /// /// The smallest, unassigned `PortChannelId` within the supported range. async fn find_available_channel_id(&self) -> Result; /// Creates and configures a new Port-Channel (Link Aggregation Group or LAG) /// using the specified channel ID and ports. /// /// The resulting configuration must be persistent (saved to startup-config). /// Assumes a static LAG configuration mode unless specified otherwise by the implementation. /// /// # Parameters /// /// * `channel_id`: The ID (e.g., 1-128) for the logical port channel. /// * `channel_name`: A descriptive name for the LAG (used in configuration context). /// * `ports`: A slice of `PortLocation` structs defining the physical member ports. async fn create_port_channel( &self, channel_id: PortChannelId, channel_name: &str, ports: &[PortLocation], ) -> Result<(), Error>; /// Removes all configuration associated with the specified Port-Channel name. /// /// This operation should be idempotent; attempting to clear a non-existent /// channel should succeed (or return a benign error). /// /// # Parameters /// /// * `channel_name`: The name of the Port-Channel (LAG) to delete. /// async fn clear_port_channel(&self, channel_name: &str) -> Result<(), Error>; } async fn get_brocade_info(session: &mut BrocadeSession) -> Result { let output = session.run_command("show version").await?; if output.contains("Network Operating System") { let re = Regex::new(r"Network Operating System Version:\s*(?P[a-zA-Z0-9.\-]+)") .expect("Invalid regex"); let version = re .captures(&output) .and_then(|cap| cap.name("version")) .map(|m| m.as_str().to_string()) .unwrap_or_default(); return Ok(BrocadeInfo { os: BrocadeOs::NetworkOperatingSystem, version, }); } else if output.contains("ICX") { let re = Regex::new(r"(?m)^\s*SW: Version\s*(?P[a-zA-Z0-9.\-]+)") .expect("Invalid regex"); let version = re .captures(&output) .and_then(|cap| cap.name("version")) .map(|m| m.as_str().to_string()) .unwrap_or_default(); return Ok(BrocadeInfo { os: BrocadeOs::FastIron, version, }); } Err(Error::UnexpectedError("Unknown Brocade OS version".into())) } fn parse_brocade_mac_address(value: &str) -> Result { let cleaned_mac = value.replace('.', ""); if cleaned_mac.len() != 12 { return Err(format!("Invalid MAC address: {value}")); } let mut bytes = [0u8; 6]; for (i, pair) in cleaned_mac.as_bytes().chunks(2).enumerate() { let byte_str = std::str::from_utf8(pair).map_err(|_| "Invalid UTF-8")?; bytes[i] = u8::from_str_radix(byte_str, 16).map_err(|_| format!("Invalid hex in MAC: {value}"))?; } Ok(MacAddress(bytes)) } #[derive(Debug)] pub enum Error { NetworkError(String), AuthenticationError(String), ConfigurationError(String), TimeoutError(String), UnexpectedError(String), CommandError(String), } impl Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::NetworkError(msg) => write!(f, "Network error: {msg}"), Error::AuthenticationError(msg) => write!(f, "Authentication error: {msg}"), Error::ConfigurationError(msg) => write!(f, "Configuration error: {msg}"), Error::TimeoutError(msg) => write!(f, "Timeout error: {msg}"), Error::UnexpectedError(msg) => write!(f, "Unexpected error: {msg}"), Error::CommandError(msg) => write!(f, "{msg}"), } } } impl From for String { fn from(val: Error) -> Self { format!("{val}") } } impl std::error::Error for Error {} impl From for Error { fn from(value: russh::Error) -> Self { Error::NetworkError(format!("Russh client error: {value}")) } }