diff --git a/brocade/examples/main.rs b/brocade/examples/main.rs index 756240f..d13cf50 100644 --- a/brocade/examples/main.rs +++ b/brocade/examples/main.rs @@ -1,25 +1,33 @@ use std::net::{IpAddr, Ipv4Addr}; +use brocade::BrocadeOptions; use harmony_types::switch::PortLocation; #[tokio::main] async fn main() { - env_logger::init(); + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 250)); // old brocade @ ianlet // let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 55, 101)); // brocade @ sto1 let switch_addresses = vec![ip]; - let brocade = brocade::init(&switch_addresses, 22, "admin", "password", None) - .await - .expect("Brocade client failed to connect"); + let brocade = brocade::init( + &switch_addresses, + 22, + "admin", + "password", + Some(BrocadeOptions { + dry_run: true, + ..Default::default() + }), + ) + .await + .expect("Brocade client failed to connect"); let version = brocade.version().await.unwrap(); println!("Version: {version:?}"); println!("--------------"); - println!("Showing MAC Address table..."); - let mac_adddresses = brocade.show_mac_address_table().await.unwrap(); println!("VLAN\tMAC\t\t\tPORT"); for mac in mac_adddresses { @@ -28,27 +36,16 @@ async fn main() { println!("--------------"); let channel_name = "HARMONY_LAG"; - println!("Clearing port channel '{channel_name}'..."); - brocade.clear_port_channel(channel_name).await.unwrap(); - println!("Cleared"); - println!("--------------"); - println!("Finding next available channel..."); - let channel_id = brocade.find_available_channel_id().await.unwrap(); - println!("Channel id: {channel_id}"); println!("--------------"); let channel_name = "HARMONY_LAG"; let ports = [PortLocation(1, 1, 3), PortLocation(1, 1, 4)]; - println!("Creating port channel '{channel_name}' with ports {ports:?}'..."); - brocade .create_port_channel(channel_id, channel_name, &ports) .await .unwrap(); - - println!("Created"); } diff --git a/brocade/src/fast_iron.rs b/brocade/src/fast_iron.rs index 95db621..258b326 100644 --- a/brocade/src/fast_iron.rs +++ b/brocade/src/fast_iron.rs @@ -145,7 +145,7 @@ impl BrocadeClient for FastIronClient { } async fn clear_port_channel(&self, channel_name: &str) -> Result<(), Error> { - debug!("[Brocade] Clearing port-channel: {channel_name}"); + info!("[Brocade] Clearing port-channel: {channel_name}"); let commands = vec![ "configure terminal".to_string(), @@ -156,6 +156,7 @@ impl BrocadeClient for FastIronClient { .run_commands(commands, ExecutionMode::Privileged) .await?; + info!("[Brocade] Port-channel '{channel_name}' cleared."); Ok(()) } } diff --git a/brocade/src/lib.rs b/brocade/src/lib.rs index 6dc4436..0517292 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -4,6 +4,7 @@ use std::{ time::Duration, }; +use crate::network_operating_system::NetworkOperatingSystemClient; use crate::{ fast_iron::FastIronClient, shell::{BrocadeSession, BrocadeShell}, @@ -15,6 +16,7 @@ use harmony_types::switch::{PortDeclaration, PortLocation}; use regex::Regex; mod fast_iron; +mod network_operating_system; mod shell; mod ssh; @@ -36,7 +38,7 @@ pub struct TimeoutConfig { impl Default for TimeoutConfig { fn default() -> Self { Self { - shell_ready: Duration::from_secs(3), + 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), @@ -91,7 +93,10 @@ pub async fn init( shell, version: version_info, }), - BrocadeOs::NetworkOperatingSystem => todo!(), + BrocadeOs::NetworkOperatingSystem => Box::new(NetworkOperatingSystemClient { + shell, + version: version_info, + }), BrocadeOs::Unknown => todo!(), }) } diff --git a/brocade/src/network_operating_system.rs b/brocade/src/network_operating_system.rs new file mode 100644 index 0000000..a0a90e6 --- /dev/null +++ b/brocade/src/network_operating_system.rs @@ -0,0 +1,87 @@ +use std::str::FromStr; + +use async_trait::async_trait; +use harmony_types::switch::{PortDeclaration, PortLocation}; +use log::debug; + +use crate::{ + BrocadeClient, BrocadeInfo, Error, MacAddressEntry, PortChannelId, parse_brocade_mac_address, + shell::BrocadeShell, +}; + +pub struct NetworkOperatingSystemClient { + pub shell: BrocadeShell, + pub version: BrocadeInfo, +} + +impl NetworkOperatingSystemClient { + pub 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)), + } + } +} + +#[async_trait] +impl BrocadeClient for NetworkOperatingSystemClient { + async fn version(&self) -> Result { + Ok(self.version.clone()) + } + + async fn show_mac_address_table(&self) -> Result, Error> { + let output = self + .shell + .run_command("show mac-address-table", crate::ExecutionMode::Regular) + .await?; + + output + .lines() + .skip(1) + .filter_map(|line| self.parse_mac_entry(line)) + .collect() + } + + async fn find_available_channel_id(&self) -> Result { + 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!() + } +} diff --git a/brocade/src/shell.rs b/brocade/src/shell.rs index 643caa6..8f9d1fc 100644 --- a/brocade/src/shell.rs +++ b/brocade/src/shell.rs @@ -148,6 +148,10 @@ impl BrocadeSession { } pub async fn run_command(&mut self, command: &str) -> Result { + if self.should_skip_command(command) { + return Ok(String::new()); + } + debug!("[Brocade] Running command: '{command}'..."); self.channel @@ -170,7 +174,15 @@ impl BrocadeSession { Ok(()) } - pub async fn collect_command_output(&mut self) -> Result, Error> { + fn should_skip_command(&self, command: &str) -> bool { + if (command.starts_with("write") || command.starts_with("deploy")) && self.options.dry_run { + info!("[Brocade] Dry-run mode enabled, skipping command: {command}"); + return true; + } + false + } + + async fn collect_command_output(&mut self) -> Result, Error> { let mut output = Vec::new(); let start = Instant::now(); let read_timeout = Duration::from_millis(500); @@ -222,7 +234,7 @@ impl BrocadeSession { Ok(output) } - pub fn check_for_command_errors(&self, output: &str, command: &str) -> Result<(), Error> { + fn check_for_command_errors(&self, output: &str, command: &str) -> Result<(), Error> { const ERROR_PATTERNS: &[&str] = &[ "invalid input", "syntax error", @@ -254,7 +266,7 @@ impl BrocadeSession { } } -pub async fn wait_for_shell_ready( +async fn wait_for_shell_ready( channel: &mut russh::Channel, timeouts: &TimeoutConfig, ) -> Result<(), Error> { @@ -266,6 +278,7 @@ pub async fn wait_for_shell_ready( Ok(Some(ChannelMsg::Data { data })) => { buffer.extend_from_slice(&data); let output = String::from_utf8_lossy(&buffer); + let output = output.trim(); if output.ends_with('>') || output.ends_with('#') { debug!("[Brocade] Shell ready"); return Ok(()); @@ -279,7 +292,7 @@ pub async fn wait_for_shell_ready( Ok(()) } -pub async fn try_elevate_session( +async fn try_elevate_session( channel: &mut russh::Channel, username: &str, password: &str,