add option to run brocade commands in dry-run

This commit is contained in:
Ian Letourneau
2025-09-28 12:55:37 -04:00
parent 7b6ac6641a
commit f2f55d98d4
7 changed files with 171 additions and 65 deletions

View File

@@ -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<Client>,
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<BrocadeOptions>,
) -> Result<Self, Error> {
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<Vec<MacAddressEntry>, 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<u8, Error> {
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<String, Error> {
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<String>) -> 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(())
}
}