find ports in Brocade switch & configure port-channels (blind implementation)
This commit is contained in:
13
brocade/Cargo.toml
Normal file
13
brocade/Cargo.toml
Normal 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
251
brocade/src/lib.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user