WIP: configure-switch #159

Closed
johnride wants to merge 18 commits from configure-switch into master
Showing only changes of commit 731dc5f404 - Show all commits

View File

@ -7,7 +7,7 @@ use std::{
use async_trait::async_trait; use async_trait::async_trait;
use harmony_types::net::{IpAddress, MacAddress}; use harmony_types::net::{IpAddress, MacAddress};
use log::{debug, info}; use log::{debug, info, trace};
use russh::{ use russh::{
ChannelMsg, ChannelMsg,
client::{Handle, Handler}, client::{Handle, Handler},
@ -79,6 +79,11 @@ impl Default for SshOptions {
} }
} }
enum ExecutionMode {
Regular,
Privileged,
}
impl BrocadeClient { impl BrocadeClient {
pub async fn init( pub async fn init(
ip_addresses: &[IpAddress], ip_addresses: &[IpAddress],
@ -115,7 +120,9 @@ impl BrocadeClient {
} }
pub async fn show_mac_address_table(&self) -> Result<Vec<MacAddressEntry>, Error> { pub async fn show_mac_address_table(&self) -> Result<Vec<MacAddressEntry>, Error> {
let output = self.run_command("show mac-address").await?; let output = self
.run_command("show mac-address", ExecutionMode::Regular)
.await?;
output output
.lines() .lines()
@ -130,14 +137,15 @@ impl BrocadeClient {
let channel_id = self.find_available_channel_id().await?; let channel_id = self.find_available_channel_id().await?;
let commands = self.build_port_channel_commands(channel_id, ports); let commands = self.build_port_channel_commands(channel_id, ports);
self.run_commands(commands).await?; self.run_commands(commands, ExecutionMode::Privileged)
.await?;
Ok(channel_id) Ok(channel_id)
} }
pub async fn find_available_channel_id(&self) -> Result<u8, Error> { pub async fn find_available_channel_id(&self) -> Result<u8, Error> {
debug!("[Brocade] Finding next available channel id..."); debug!("[Brocade] Finding next available channel id...");
let output = self.run_command("show lag").await?; let output = self.run_command("show lag", ExecutionMode::Regular).await?;
let mut used_ids: Vec<u8> = output let mut used_ids: Vec<u8> = output
.lines() .lines()
.filter_map(|line| { .filter_map(|line| {
@ -158,14 +166,13 @@ impl BrocadeClient {
Ok(next_id) Ok(next_id)
} }
async fn run_command(&self, command: &str) -> Result<String, Error> { async fn run_command(&self, command: &str, mode: ExecutionMode) -> Result<String, Error> {
if self.should_skip_command(command) { if self.should_skip_command(command) {
return Ok(String::new()); return Ok(String::new());
} }
let mut channel = self.client.channel_open_session().await?; let mut channel = self.client.channel_open_session().await?;
self.setup_channel(&mut channel).await?; self.setup_channel(&mut channel, mode).await?;
self.wait_for_shell_ready(&mut channel).await?;
let output = self let output = self
.execute_command_in_session(&mut channel, command) .execute_command_in_session(&mut channel, command)
@ -178,14 +185,13 @@ impl BrocadeClient {
Ok(cleaned) Ok(cleaned)
} }
async fn run_commands(&self, commands: Vec<String>) -> Result<(), Error> { async fn run_commands(&self, commands: Vec<String>, mode: ExecutionMode) -> Result<(), Error> {
if commands.is_empty() { if commands.is_empty() {
return Ok(()); return Ok(());
} }
let mut channel = self.client.channel_open_session().await?; let mut channel = self.client.channel_open_session().await?;
self.setup_channel(&mut channel).await?; self.setup_channel(&mut channel, mode).await?;
self.wait_for_shell_ready(&mut channel).await?;
for command in commands { for command in commands {
if self.should_skip_command(&command) { if self.should_skip_command(&command) {
@ -210,13 +216,23 @@ impl BrocadeClient {
async fn setup_channel( async fn setup_channel(
&self, &self,
channel: &mut russh::Channel<russh::client::Msg>, channel: &mut russh::Channel<russh::client::Msg>,
mode: ExecutionMode,
) -> Result<(), Error> { ) -> Result<(), Error> {
// Setup PTY and shell // Setup PTY and shell
channel channel
.request_pty(false, "vt100", 80, 24, 0, 0, &[]) .request_pty(false, "vt100", 80, 24, 0, 0, &[])
.await?; .await?;
channel.request_shell(false).await?; channel.request_shell(false).await?;
Ok(())
self.wait_for_shell_ready(channel).await?;
match mode {
ExecutionMode::Regular => Ok(()),
ExecutionMode::Privileged => {
debug!("[Brocade] Attempting privilege escalation (enable mode)...");
self.try_elevate_session(channel).await
}
}
} }
async fn execute_command_in_session( async fn execute_command_in_session(
@ -235,6 +251,63 @@ impl BrocadeClient {
.map_err(|_| Error::UnexpectedError("Invalid UTF-8 in command output".to_string())) .map_err(|_| Error::UnexpectedError("Invalid UTF-8 in command output".to_string()))
} }
async fn try_elevate_session(
&self,
channel: &mut russh::Channel<russh::client::Msg>,
) -> Result<(), Error> {
channel.data(&b"enable\n"[..]).await?;
let start = Instant::now();
let mut buffer = Vec::new();
while start.elapsed() < self.options.timeouts.shell_ready {
match timeout(self.options.timeouts.message_wait, channel.wait()).await {
Ok(Some(ChannelMsg::Data { data })) => {
buffer.extend_from_slice(&data);
let output = String::from_utf8_lossy(&buffer);
if output.ends_with('#') {
debug!("[Brocade] Privileged mode established");
return Ok(());
}
if output.contains("User Name:") {
channel
.data(format!("{}\n", self.elevated_user.username).as_bytes())
.await?;
buffer.clear();
} else if output.contains("Password:") {
// Note: Brocade might not echo the password field
channel
.data(format!("{}\n", self.elevated_user.password).as_bytes())
.await?;
buffer.clear();
} else if output.contains('>') {
// Back to user mode, something failed (e.g., wrong password)
return Err(Error::AuthenticationError(
"Enable authentication failed or access denied.".to_string(),
));
}
}
Ok(Some(_)) => continue,
Ok(None) => break,
Err(_) => continue,
}
}
// Check final state if timeout was reached
let output = String::from_utf8_lossy(&buffer);
let elevated = output.ends_with('#');
match elevated {
true => {
debug!("[Brocade] Privileged mode established");
Ok(())
}
false => Err(Error::AuthenticationError(format!(
"Enable authentication failed for an unknown reason. Output was:\n{output}",
))),
}
}
async fn wait_for_shell_ready( async fn wait_for_shell_ready(
&self, &self,
channel: &mut russh::Channel<russh::client::Msg>, channel: &mut russh::Channel<russh::client::Msg>,
@ -247,7 +320,7 @@ impl BrocadeClient {
Ok(Some(ChannelMsg::Data { data })) => { Ok(Some(ChannelMsg::Data { data })) => {
buffer.extend_from_slice(&data); buffer.extend_from_slice(&data);
let output = String::from_utf8_lossy(&buffer); let output = String::from_utf8_lossy(&buffer);
if output.contains('>') || output.contains('#') { if output.ends_with('>') || output.ends_with('#') {
debug!("[Brocade] Shell ready: {}", output.trim()); debug!("[Brocade] Shell ready: {}", output.trim());
return Ok(()); return Ok(());
} }
@ -351,7 +424,7 @@ impl BrocadeClient {
} }
fn clean_brocade_output(&self, raw_output: &str, command: &str) -> String { fn clean_brocade_output(&self, raw_output: &str, command: &str) -> String {
debug!("[Brocade] Received command output:\n{raw_output}"); trace!("[Brocade] Received raw output:\n{raw_output}");
let lines: Vec<&str> = raw_output.lines().collect(); let lines: Vec<&str> = raw_output.lines().collect();
let mut cleaned_lines = Vec::new(); let mut cleaned_lines = Vec::new();