find ports in Brocade switch & configure port-channels (blind implementation)
This commit is contained in:
parent
427009bbfe
commit
0de52aedbf
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -674,6 +674,17 @@ dependencies = [
|
|||||||
"serde_with",
|
"serde_with",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brocade"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"harmony_types",
|
||||||
|
"russh",
|
||||||
|
"russh-keys",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "8.0.2"
|
version = "8.0.2"
|
||||||
@ -2318,6 +2329,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bollard",
|
"bollard",
|
||||||
|
"brocade",
|
||||||
"chrono",
|
"chrono",
|
||||||
"cidr",
|
"cidr",
|
||||||
"convert_case",
|
"convert_case",
|
||||||
|
@ -15,7 +15,7 @@ members = [
|
|||||||
"harmony_inventory_agent",
|
"harmony_inventory_agent",
|
||||||
"harmony_secret_derive",
|
"harmony_secret_derive",
|
||||||
"harmony_secret",
|
"harmony_secret",
|
||||||
"adr/agent_discovery/mdns",
|
"adr/agent_discovery/mdns", "brocade",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
@ -77,6 +77,7 @@ harmony_secret = { path = "../harmony_secret" }
|
|||||||
askama.workspace = true
|
askama.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
inquire.workspace = true
|
inquire.workspace = true
|
||||||
|
brocade = { version = "0.1.0", path = "../brocade" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use brocade::BrocadeClient;
|
||||||
use harmony_macros::ip;
|
use harmony_macros::ip;
|
||||||
|
use harmony_secret::Secret;
|
||||||
|
use harmony_secret::SecretManager;
|
||||||
use harmony_types::net::MacAddress;
|
use harmony_types::net::MacAddress;
|
||||||
use harmony_types::net::Url;
|
use harmony_types::net::Url;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
use russh::client;
|
||||||
|
use russh::client::Handler;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::data::FileContent;
|
use crate::data::FileContent;
|
||||||
use crate::executors::ExecutorError;
|
use crate::executors::ExecutorError;
|
||||||
@ -28,10 +35,12 @@ use super::PreparationOutcome;
|
|||||||
use super::Router;
|
use super::Router;
|
||||||
use super::Switch;
|
use super::Switch;
|
||||||
use super::SwitchError;
|
use super::SwitchError;
|
||||||
|
use super::SwitchPort;
|
||||||
use super::TftpServer;
|
use super::TftpServer;
|
||||||
|
|
||||||
use super::Topology;
|
use super::Topology;
|
||||||
use super::k8s::K8sClient;
|
use super::k8s::K8sClient;
|
||||||
|
use std::error::Error;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -93,6 +102,39 @@ impl HAClusterTopology {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_master_switch(&self) -> Option<LogicalHost> {
|
||||||
|
self.switch.first().cloned() // FIXME: Should we be smarter to find the master switch?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<u8, SwitchError> {
|
||||||
|
let auth = SecretManager::get_or_prompt::<BrocadeSwitchAuth>()
|
||||||
|
.await
|
||||||
|
.map_err(|e| SwitchError::new(format!("Failed to get credentials: {e}")))?;
|
||||||
|
|
||||||
|
let switch = self
|
||||||
|
.find_master_switch()
|
||||||
|
.ok_or(SwitchError::new("No switch found in topology".to_string()))?;
|
||||||
|
let client = BrocadeSwitchClient::init(switch.ip, &auth.username, &auth.password)
|
||||||
|
.await
|
||||||
|
.map_err(|e| SwitchError::new(format!("Failed to connect to switch: {e}")))?;
|
||||||
|
|
||||||
|
let switch_ports: Vec<String> = config
|
||||||
|
.switch_ports
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.port_name.clone())
|
||||||
|
.collect();
|
||||||
|
let channel_id = client
|
||||||
|
.configure_port_channel(switch_ports)
|
||||||
|
.await
|
||||||
|
.map_err(|e| SwitchError::new(format!("Failed to configure switch: {e}")))?;
|
||||||
|
|
||||||
|
Ok(channel_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn autoload() -> Self {
|
pub fn autoload() -> Self {
|
||||||
let dummy_infra = Arc::new(DummyInfra {});
|
let dummy_infra = Arc::new(DummyInfra {});
|
||||||
let dummy_host = LogicalHost {
|
let dummy_host = LogicalHost {
|
||||||
@ -269,19 +311,77 @@ impl HttpServer for HAClusterTopology {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Switch for HAClusterTopology {
|
impl Switch for HAClusterTopology {
|
||||||
async fn get_port_for_mac_address(&self, _mac_address: &MacAddress) -> Option<String> {
|
async fn get_port_for_mac_address(&self, mac_address: &MacAddress) -> Option<String> {
|
||||||
todo!()
|
let auth = SecretManager::get_or_prompt::<BrocadeSwitchAuth>()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let switch = self.find_master_switch()?;
|
||||||
|
let client = BrocadeSwitchClient::init(switch.ip, &auth.username, &auth.password).await;
|
||||||
|
|
||||||
|
let Ok(client) = client else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
client.find_port(mac_address).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn configure_host_network(
|
async fn configure_host_network(
|
||||||
&self,
|
&self,
|
||||||
_host: &PhysicalHost,
|
_host: &PhysicalHost,
|
||||||
_config: HostNetworkConfig,
|
config: HostNetworkConfig,
|
||||||
) -> Result<(), SwitchError> {
|
) -> Result<(), SwitchError> {
|
||||||
|
let _ = self.configure_bond(&config).await;
|
||||||
|
let channel_id = self.configure_port_channel(&config).await;
|
||||||
|
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
trait SwitchClient {
|
||||||
|
async fn find_port(&self, mac_address: &MacAddress) -> Option<String>;
|
||||||
|
async fn configure_port_channel(&self, switch_ports: Vec<String>) -> Result<u8, SwitchError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BrocadeSwitchClient {
|
||||||
|
brocade: BrocadeClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BrocadeSwitchClient {
|
||||||
|
async fn init(ip: IpAddress, username: &str, password: &str) -> Result<Self, brocade::Error> {
|
||||||
|
let brocade = BrocadeClient::init(ip, username, password).await?;
|
||||||
|
Ok(Self { brocade })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SwitchClient for BrocadeSwitchClient {
|
||||||
|
async fn find_port(&self, mac_address: &MacAddress) -> Option<String> {
|
||||||
|
let Ok(table) = self.brocade.show_mac_address_table().await else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
table
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.mac_address == *mac_address)
|
||||||
|
.map(|entry| entry.port_name.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn configure_port_channel(&self, switch_ports: Vec<String>) -> Result<u8, SwitchError> {
|
||||||
|
self.brocade
|
||||||
|
.configure_port_channel(&switch_ports)
|
||||||
|
.await
|
||||||
|
.map_err(|e| SwitchError::new(format!("Failed to configure port channel: {e}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Secret, Serialize, Deserialize, Debug)]
|
||||||
|
struct BrocadeSwitchAuth {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DummyInfra;
|
pub struct DummyInfra;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
|
||||||
pub struct MacAddress(pub [u8; 6]);
|
pub struct MacAddress(pub [u8; 6]);
|
||||||
|
|
||||||
impl MacAddress {
|
impl MacAddress {
|
||||||
|
Loading…
Reference in New Issue
Block a user