find ports in Brocade switch & configure port-channels (blind implementation)

This commit is contained in:
2025-09-16 17:19:32 -04:00
parent 427009bbfe
commit 0de52aedbf
7 changed files with 382 additions and 5 deletions

13
brocade/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "brocade"
edition = "2024"
version.workspace = true
readme.workspace = true
license.workspace = true
[dependencies]
async-trait.workspace = true
harmony_types = { version = "0.1.0", path = "../harmony_types" }
russh.workspace = true
russh-keys.workspace = true
tokio.workspace = true

251
brocade/src/lib.rs Normal file
View File

@@ -0,0 +1,251 @@
use std::{
fmt::{self, Display},
sync::Arc,
};
use async_trait::async_trait;
use harmony_types::net::{IpAddress, MacAddress};
use russh::client::{Handle, Handler};
use russh_keys::key;
use std::str::FromStr;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct MacAddressEntry {
pub vlan: u16,
pub mac_address: MacAddress,
pub port_name: String,
}
pub struct BrocadeClient {
client: Handle<Client>,
}
impl BrocadeClient {
pub async fn init(ip: IpAddress, username: &str, password: &str) -> Result<Self, Error> {
let config = russh::client::Config::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 }),
false => Err(Error::AuthenticationError(
"ssh authentication failed".to_string(),
)),
}
}
pub async fn show_mac_address_table(&self) -> Result<Vec<MacAddressEntry>, Error> {
let output = self.run_command("show mac-address-table").await?;
let mut entries = Vec::new();
// The Brocade output usually has a header and then one entry per line.
// We will skip the header and parse each line.
// Sample line: "1234 AA:BB:CC:DD:EE:F1 GigabitEthernet1/1/1"
for line in output.lines().skip(1) {
// Skip the header row
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
// Assuming the format is: <VLAN> <MAC> <Port>
if let Ok(vlan) = u16::from_str(parts[0]) {
let mac = MacAddress::try_from(parts[1].to_string());
let port = parts[2].to_string();
if let Ok(mac_address) = mac {
entries.push(MacAddressEntry {
vlan,
mac_address,
port_name: port,
});
}
}
}
}
Ok(entries)
}
pub async fn configure_port_channel(&self, ports: &[String]) -> Result<u8, Error> {
let channel_id = self.find_available_channel_id().await?;
let mut commands = Vec::new();
// Start configuration mode.
commands.push("configure terminal".to_string());
// Create the port channel interface.
commands.push(format!("interface Port-channel {channel_id}"));
commands.push("no ip address".to_string());
commands.push("exit".to_string());
// Configure each physical port to join the channel.
for port in ports {
commands.push(format!("interface {port}"));
// 'channel-group' command to add the interface to the port channel.
// Using 'mode active' enables LACP.
commands.push(format!("channel-group {channel_id} mode active"));
commands.push("exit".to_string());
}
// Save the configuration.
commands.push("write memory".to_string());
self.run_commands(commands).await?;
Ok(channel_id)
}
pub async fn find_available_channel_id(&self) -> Result<u8, Error> {
// FIXME: The command might vary slightly by Brocade OS version.
let output = self.run_command("show port-channel summary").await?;
let mut used_ids = Vec::new();
// Sample output line: "3 Po3(SU) LACP Eth Yes 128/128 active "
// We're looking for the ID, which is the first number.
for line in output.lines() {
if line.trim().starts_with(|c: char| c.is_ascii_digit()) {
let parts: Vec<&str> = line.split_whitespace().collect();
if let Ok(id) = u8::from_str(parts[0]) {
used_ids.push(id);
}
}
}
// Sort the used IDs to find the next available number.
used_ids.sort();
let mut next_id = 1;
for &id in &used_ids {
if id == next_id {
next_id += 1;
} else {
// Found a gap, so this is our ID.
return Ok(next_id);
}
}
Ok(next_id)
}
async fn run_command(&self, command: &str) -> Result<String, Error> {
let mut channel = self.client.channel_open_session().await?;
let mut output = Vec::new();
channel.exec(true, command).await?;
loop {
let Some(msg) = channel.wait().await else {
break;
};
match msg {
russh::ChannelMsg::ExtendedData { ref data, .. }
| russh::ChannelMsg::Data { ref data } => {
output.append(&mut data.to_vec());
}
russh::ChannelMsg::ExitStatus { exit_status } => {
if exit_status != 0 {
return Err(Error::CommandError(format!(
"Command failed with exit status {exit_status}, output {}",
String::from_utf8(output).unwrap_or_default()
)));
}
}
russh::ChannelMsg::Success
| russh::ChannelMsg::WindowAdjusted { .. }
| russh::ChannelMsg::Eof => {}
_ => {
return Err(Error::UnexpectedError(format!(
"Russh got unexpected msg {msg:?}"
)));
}
}
}
channel.close().await?;
let output = String::from_utf8(output).expect("Output should be UTF-8 compatible");
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 {
let mut output = Vec::new();
channel.exec(true, command.as_str()).await?;
loop {
let Some(msg) = channel.wait().await else {
break;
};
match msg {
russh::ChannelMsg::ExtendedData { ref data, .. }
| russh::ChannelMsg::Data { ref data } => {
output.append(&mut data.to_vec());
}
russh::ChannelMsg::ExitStatus { exit_status } => {
if exit_status != 0 {
let output_str = String::from_utf8(output).unwrap_or_default();
return Err(Error::CommandError(format!(
"Command '{command}' failed with exit status {exit_status}: {output_str}",
)));
}
}
_ => {} // Ignore other messages like success or EOF for now.
}
}
}
channel.close().await?;
Ok(())
}
}
struct Client {}
#[async_trait]
impl Handler for Client {
type Error = Error;
async fn check_server_key(
&mut self,
_server_public_key: &key::PublicKey,
) -> Result<bool, Self::Error> {
Ok(true)
}
}
#[derive(Debug)]
pub enum Error {
NetworkError(String),
AuthenticationError(String),
ConfigurationError(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::UnexpectedError(msg) => write!(f, "Unexpected error: {msg}"),
Error::CommandError(msg) => write!(f, "Command failed: {msg}"),
}
}
}
impl From<Error> for String {
fn from(val: Error) -> Self {
format!("{val}")
}
}
impl std::error::Error for Error {}
impl From<russh::Error> for Error {
fn from(_value: russh::Error) -> Self {
Error::NetworkError("Russh client error".to_string())
}
}