diff --git a/brocade/src/lib.rs b/brocade/src/lib.rs index 3a47d5a..552704e 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -7,7 +7,7 @@ use std::{ use async_trait::async_trait; use harmony_types::net::{IpAddress, MacAddress}; -use log::{debug, info}; +use log::{debug, info, trace}; use russh::{ ChannelMsg, client::{Handle, Handler}, @@ -79,6 +79,11 @@ impl Default for SshOptions { } } +enum ExecutionMode { + Regular, + Privileged, +} + impl BrocadeClient { pub async fn init( ip_addresses: &[IpAddress], @@ -115,7 +120,9 @@ impl BrocadeClient { } pub async fn show_mac_address_table(&self) -> Result, Error> { - let output = self.run_command("show mac-address").await?; + let output = self + .run_command("show mac-address", ExecutionMode::Regular) + .await?; output .lines() @@ -130,14 +137,15 @@ impl BrocadeClient { let channel_id = self.find_available_channel_id().await?; 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) } pub async fn find_available_channel_id(&self) -> Result { 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 = output .lines() .filter_map(|line| { @@ -158,14 +166,13 @@ impl BrocadeClient { Ok(next_id) } - async fn run_command(&self, command: &str) -> Result { + async fn run_command(&self, command: &str, mode: ExecutionMode) -> Result { if self.should_skip_command(command) { return Ok(String::new()); } let mut channel = self.client.channel_open_session().await?; - self.setup_channel(&mut channel).await?; - self.wait_for_shell_ready(&mut channel).await?; + self.setup_channel(&mut channel, mode).await?; let output = self .execute_command_in_session(&mut channel, command) @@ -178,14 +185,13 @@ impl BrocadeClient { Ok(cleaned) } - async fn run_commands(&self, commands: Vec) -> Result<(), Error> { + async fn run_commands(&self, commands: Vec, mode: ExecutionMode) -> Result<(), Error> { if commands.is_empty() { return Ok(()); } let mut channel = self.client.channel_open_session().await?; - self.setup_channel(&mut channel).await?; - self.wait_for_shell_ready(&mut channel).await?; + self.setup_channel(&mut channel, mode).await?; for command in commands { if self.should_skip_command(&command) { @@ -210,13 +216,23 @@ impl BrocadeClient { async fn setup_channel( &self, channel: &mut russh::Channel, + mode: ExecutionMode, ) -> Result<(), Error> { // Setup PTY and shell channel .request_pty(false, "vt100", 80, 24, 0, 0, &[]) .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( @@ -235,6 +251,63 @@ impl BrocadeClient { .map_err(|_| Error::UnexpectedError("Invalid UTF-8 in command output".to_string())) } + async fn try_elevate_session( + &self, + channel: &mut russh::Channel, + ) -> 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( &self, channel: &mut russh::Channel, @@ -247,7 +320,7 @@ impl BrocadeClient { Ok(Some(ChannelMsg::Data { data })) => { buffer.extend_from_slice(&data); 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()); return Ok(()); } @@ -351,7 +424,7 @@ impl BrocadeClient { } 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 mut cleaned_lines = Vec::new();