diff --git a/brocade/src/lib.rs b/brocade/src/lib.rs index 0df2ec4..411d9de 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, fmt::{self, Display}, sync::Arc, }; @@ -6,8 +7,11 @@ use std::{ use async_trait::async_trait; use harmony_types::net::{IpAddress, MacAddress}; use log::{debug, info}; -use russh::client::{Handle, Handler}; -use russh_keys::key; +use russh::{ + client::{Handle, Handler}, + kex::DH_G1_SHA1, +}; +use russh_keys::key::{self, SSH_RSA}; use std::str::FromStr; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] @@ -19,6 +23,30 @@ pub struct MacAddressEntry { pub struct BrocadeClient { client: Handle, + options: BrocadeOptions, +} + +#[derive(Default, Clone, Debug)] +pub struct BrocadeOptions { + pub dry_run: bool, + pub ssh: SshOptions, +} + +#[derive(Clone, Debug)] +pub struct SshOptions { + pub preferred_algorithms: russh::Preferred, +} + +impl Default for SshOptions { + fn default() -> Self { + Self { + preferred_algorithms: russh::Preferred { + kex: Cow::Borrowed(&[DH_G1_SHA1]), + key: Cow::Borrowed(&[SSH_RSA]), + ..Default::default() + }, + } + } } impl BrocadeClient { @@ -26,14 +54,25 @@ impl BrocadeClient { ip_addresses: &[IpAddress], username: &str, password: &str, + options: Option, ) -> Result { - let ip = ip_addresses[0]; // FIXME: Find a better way to get master switch IP address + if ip_addresses.is_empty() { + return Err(Error::ConfigurationError( + "No IP addresses provided".to_string(), + )); + } - let config = russh::client::Config::default(); + let ip = ip_addresses[0]; // FIXME: Find a better way to get master switch IP address + let options = options.unwrap_or_default(); + + let config = russh::client::Config { + preferred: options.ssh.preferred_algorithms.clone(), + ..Default::default() + }; let mut client = russh::client::connect(Arc::new(config), (ip, 22), Client {}).await?; match client.authenticate_password(username, password).await? { - true => Ok(Self { client }), + true => Ok(Self { client, options }), false => Err(Error::AuthenticationError( "ssh authentication failed".to_string(), )), @@ -41,7 +80,7 @@ impl BrocadeClient { } pub async fn show_mac_address_table(&self) -> Result, Error> { - let output = self.run_command("show mac-address-table").await?; + let output = self.run_command("show mac-address").await?; let mut entries = Vec::new(); // The Brocade output usually has a header and then one entry per line. @@ -104,7 +143,7 @@ impl BrocadeClient { pub async fn find_available_channel_id(&self) -> Result { debug!("[Brocade] Finding next available channel id..."); - let output = self.run_command("show port-channel summary").await?; + let output = self.run_command("show lag").await?; let mut used_ids = Vec::new(); // Sample output line: "3 Po3(SU) LACP Eth Yes 128/128 active " @@ -121,7 +160,7 @@ impl BrocadeClient { // Sort the used IDs to find the next available number. used_ids.sort(); - let mut next_id = 1; + let mut next_id = 0; for &id in &used_ids { if id == next_id { next_id += 1; @@ -136,6 +175,11 @@ impl BrocadeClient { } async fn run_command(&self, command: &str) -> Result { + if !command.starts_with("show") && self.options.dry_run { + info!("[Brocade] Dry-run mode enabled, skipping command: {command}"); + return Ok("".into()); + } + debug!("[Brocade] Running command: '{command}'..."); let mut channel = self.client.channel_open_session().await?; @@ -161,9 +205,13 @@ impl BrocadeClient { ))); } } - russh::ChannelMsg::Success - | russh::ChannelMsg::WindowAdjusted { .. } - | russh::ChannelMsg::Eof => {} + russh::ChannelMsg::Eof => { + channel.close().await?; + } + russh::ChannelMsg::Close => { + break; + } + russh::ChannelMsg::Success | russh::ChannelMsg::WindowAdjusted { .. } => {} _ => { return Err(Error::UnexpectedError(format!( "Russh got unexpected msg {msg:?}" @@ -172,20 +220,25 @@ impl BrocadeClient { } } - channel.close().await?; - let output = String::from_utf8(output).expect("Output should be UTF-8 compatible"); + debug!("[Brocade] Command output:\n{output}"); Ok(output) } async fn run_commands(&self, commands: Vec) -> Result<(), Error> { - let mut channel = self.client.channel_open_session().await?; - // Execute commands sequentially and check for errors immediately. for command in commands { + if !command.starts_with("show") && self.options.dry_run { + info!("[Brocade] Dry-run mode enabled, skipping command: {command}"); + continue; + } + debug!("[Brocade] Running command: '{command}'..."); + let mut channel = self.client.channel_open_session().await?; let mut output = Vec::new(); + let mut close_received = false; + channel.exec(true, command.as_str()).await?; loop { @@ -206,12 +259,30 @@ impl BrocadeClient { ))); } } - _ => {} // Ignore other messages like success or EOF for now. + russh::ChannelMsg::Eof => { + channel.close().await?; + } + russh::ChannelMsg::Close => { + close_received = true; + break; + } + russh::ChannelMsg::Success | russh::ChannelMsg::WindowAdjusted { .. } => {} + _ => { + return Err(Error::UnexpectedError(format!( + "Russh got unexpected msg {msg:?}" + ))); + } } } + + if !close_received { + return Err(Error::UnexpectedError(format!( + "Channel closed without receiving a final CLOSE message for command: {}", + command + ))); + } } - channel.close().await?; Ok(()) } } diff --git a/harmony/src/domain/hardware/mod.rs b/harmony/src/domain/hardware/mod.rs index 1b1a72c..a070b83 100644 --- a/harmony/src/domain/hardware/mod.rs +++ b/harmony/src/domain/hardware/mod.rs @@ -12,11 +12,11 @@ pub type FirewallGroup = Vec; pub struct PhysicalHost { pub id: Id, pub category: HostCategory, - pub network: Vec, - pub storage: Vec, + pub network: Vec, // FIXME: Don't use harmony_inventory_agent::NetworkInterface + pub storage: Vec, // FIXME: Don't use harmony_inventory_agent::StorageDrive pub labels: Vec