feat(switch/brocade): Implement client to interact with Brocade switches #168
							
								
								
									
										60
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										60
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -665,6 +665,22 @@ dependencies = [ | |||||||
|  "serde_with", |  "serde_with", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "brocade" | ||||||
|  | version = "0.1.0" | ||||||
|  | dependencies = [ | ||||||
|  |  "async-trait", | ||||||
|  |  "env_logger", | ||||||
|  |  "harmony_secret", | ||||||
|  |  "harmony_types", | ||||||
|  |  "log", | ||||||
|  |  "regex", | ||||||
|  |  "russh", | ||||||
|  |  "russh-keys", | ||||||
|  |  "serde", | ||||||
|  |  "tokio", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "brotli" | name = "brotli" | ||||||
| version = "8.0.2" | version = "8.0.2" | ||||||
| @ -1822,18 +1838,6 @@ dependencies = [ | |||||||
|  "url", |  "url", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "example-penpot" |  | ||||||
| version = "0.1.0" |  | ||||||
| dependencies = [ |  | ||||||
|  "harmony", |  | ||||||
|  "harmony_cli", |  | ||||||
|  "harmony_macros", |  | ||||||
|  "harmony_types", |  | ||||||
|  "tokio", |  | ||||||
|  "url", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "example-pxe" | name = "example-pxe" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| @ -2428,6 +2432,17 @@ dependencies = [ | |||||||
|  "tokio", |  "tokio", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "harmony_derive" | ||||||
|  | version = "0.1.3" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "2d138bbb32bb346299c5f95fbb53532313f39927cb47c411c99c634ef8665ef7" | ||||||
|  | dependencies = [ | ||||||
|  |  "proc-macro2", | ||||||
|  |  "quote", | ||||||
|  |  "syn 1.0.109", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "harmony_inventory_agent" | name = "harmony_inventory_agent" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| @ -3874,6 +3889,19 @@ dependencies = [ | |||||||
|  "web-time", |  "web-time", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "okd_host_network" | ||||||
|  | version = "0.1.0" | ||||||
|  | dependencies = [ | ||||||
|  |  "harmony", | ||||||
|  |  "harmony_cli", | ||||||
|  |  "harmony_derive", | ||||||
|  |  "harmony_inventory_agent", | ||||||
|  |  "harmony_macros", | ||||||
|  |  "harmony_types", | ||||||
|  |  "tokio", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "once_cell" | name = "once_cell" | ||||||
| version = "1.21.3" | version = "1.21.3" | ||||||
| @ -4561,9 +4589,9 @@ dependencies = [ | |||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "regex" | name = "regex" | ||||||
| version = "1.11.2" | version = "1.11.3" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" | checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "aho-corasick 1.1.3", |  "aho-corasick 1.1.3", | ||||||
|  "memchr", |  "memchr", | ||||||
| @ -4573,9 +4601,9 @@ dependencies = [ | |||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "regex-automata" | name = "regex-automata" | ||||||
| version = "0.4.10" | version = "0.4.11" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" | checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "aho-corasick 1.1.3", |  "aho-corasick 1.1.3", | ||||||
|  "memchr", |  "memchr", | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								Cargo.toml
									
									
									
									
									
								
							| @ -14,7 +14,8 @@ members = [ | |||||||
|   "harmony_composer", |   "harmony_composer", | ||||||
|   "harmony_inventory_agent", |   "harmony_inventory_agent", | ||||||
|   "harmony_secret_derive", |   "harmony_secret_derive", | ||||||
|   "harmony_secret", "adr/agent_discovery/mdns", |   "harmony_secret", | ||||||
|  |   "adr/agent_discovery/mdns", "brocade", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [workspace.package] | [workspace.package] | ||||||
| @ -66,5 +67,12 @@ thiserror = "2.0.14" | |||||||
| serde = { version = "1.0.209", features = ["derive", "rc"] } | serde = { version = "1.0.209", features = ["derive", "rc"] } | ||||||
| serde_json = "1.0.127" | serde_json = "1.0.127" | ||||||
| askama = "0.14" | askama = "0.14" | ||||||
| sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite" ] } | sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } | ||||||
| reqwest = { version = "0.12", features = ["blocking", "stream", "rustls-tls", "http2", "json"], default-features = false } | reqwest = { version = "0.12", features = [ | ||||||
|  |   "blocking", | ||||||
|  |   "stream", | ||||||
|  |   "rustls-tls", | ||||||
|  |   "http2", | ||||||
|  |   "json", | ||||||
|  | ], default-features = false } | ||||||
|  | assertor = "0.0.4" | ||||||
|  | |||||||
							
								
								
									
										18
									
								
								brocade/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								brocade/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | [package] | ||||||
|  | name = "brocade" | ||||||
|  | edition = "2024" | ||||||
|  | version.workspace = true | ||||||
|  | readme.workspace = true | ||||||
|  | license.workspace = true | ||||||
|  | 
 | ||||||
|  | [dependencies] | ||||||
|  | async-trait.workspace = true | ||||||
|  | harmony_types = { path = "../harmony_types" } | ||||||
|  | russh.workspace = true | ||||||
|  | russh-keys.workspace = true | ||||||
|  | tokio.workspace = true | ||||||
|  | log.workspace = true | ||||||
|  | env_logger.workspace = true | ||||||
|  | regex = "1.11.3" | ||||||
|  | harmony_secret = { path = "../harmony_secret" } | ||||||
|  | serde.workspace = true | ||||||
							
								
								
									
										70
									
								
								brocade/examples/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								brocade/examples/main.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | |||||||
|  | use std::net::{IpAddr, Ipv4Addr}; | ||||||
|  | 
 | ||||||
|  | use brocade::BrocadeOptions; | ||||||
|  | use harmony_secret::{Secret, SecretManager}; | ||||||
|  | use harmony_types::switch::PortLocation; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | 
 | ||||||
|  | #[derive(Secret, Clone, Debug, Serialize, Deserialize)] | ||||||
|  | struct BrocadeSwitchAuth { | ||||||
|  |     username: String, | ||||||
|  |     password: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[tokio::main] | ||||||
|  | async fn main() { | ||||||
|  |     env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); | ||||||
|  | 
 | ||||||
|  |     // let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 250)); // old brocade @ ianlet
 | ||||||
|  |     let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 55, 101)); // brocade @ sto1
 | ||||||
|  |     // let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 4, 11)); // brocade @ st
 | ||||||
|  |     let switch_addresses = vec![ip]; | ||||||
|  | 
 | ||||||
|  |     let config = SecretManager::get_or_prompt::<BrocadeSwitchAuth>() | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  | 
 | ||||||
|  |     let brocade = brocade::init( | ||||||
|  |         &switch_addresses, | ||||||
|  |         22, | ||||||
|  |         &config.username, | ||||||
|  |         &config.password, | ||||||
|  |         Some(BrocadeOptions { | ||||||
|  |             dry_run: true, | ||||||
|  |             ..Default::default() | ||||||
|  |         }), | ||||||
|  |     ) | ||||||
|  |     .await | ||||||
|  |     .expect("Brocade client failed to connect"); | ||||||
|  | 
 | ||||||
|  |     let entries = brocade.get_stack_topology().await.unwrap(); | ||||||
|  |     println!("Stack topology: {entries:#?}"); | ||||||
|  | 
 | ||||||
|  |     let entries = brocade.get_interfaces().await.unwrap(); | ||||||
|  |     println!("Interfaces: {entries:#?}"); | ||||||
|  | 
 | ||||||
|  |     let version = brocade.version().await.unwrap(); | ||||||
|  |     println!("Version: {version:?}"); | ||||||
|  | 
 | ||||||
|  |     println!("--------------"); | ||||||
|  |     let mac_adddresses = brocade.get_mac_address_table().await.unwrap(); | ||||||
|  |     println!("VLAN\tMAC\t\t\tPORT"); | ||||||
|  |     for mac in mac_adddresses { | ||||||
|  |         println!("{}\t{}\t{}", mac.vlan, mac.mac_address, mac.port); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     println!("--------------"); | ||||||
|  |     let channel_name = "1"; | ||||||
|  |     brocade.clear_port_channel(channel_name).await.unwrap(); | ||||||
|  | 
 | ||||||
|  |     println!("--------------"); | ||||||
|  |     let channel_id = brocade.find_available_channel_id().await.unwrap(); | ||||||
|  | 
 | ||||||
|  |     println!("--------------"); | ||||||
|  |     let channel_name = "HARMONY_LAG"; | ||||||
|  |     let ports = [PortLocation(2, 0, 35)]; | ||||||
|  |     brocade | ||||||
|  |         .create_port_channel(channel_id, channel_name, &ports) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  | } | ||||||
							
								
								
									
										211
									
								
								brocade/src/fast_iron.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								brocade/src/fast_iron.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,211 @@ | |||||||
|  | use super::BrocadeClient; | ||||||
|  | use crate::{ | ||||||
|  |     BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo, MacAddressEntry, | ||||||
|  |     PortChannelId, PortOperatingMode, parse_brocade_mac_address, shell::BrocadeShell, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use async_trait::async_trait; | ||||||
|  | use harmony_types::switch::{PortDeclaration, PortLocation}; | ||||||
|  | use log::{debug, info}; | ||||||
|  | use regex::Regex; | ||||||
|  | use std::{collections::HashSet, str::FromStr}; | ||||||
|  | 
 | ||||||
|  | pub struct FastIronClient { | ||||||
|  |     shell: BrocadeShell, | ||||||
|  |     version: BrocadeInfo, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl FastIronClient { | ||||||
|  |     pub fn init(mut shell: BrocadeShell, version_info: BrocadeInfo) -> Self { | ||||||
|  |         shell.before_all(vec!["skip-page-display".into()]); | ||||||
|  |         shell.after_all(vec!["page".into()]); | ||||||
|  | 
 | ||||||
|  |         Self { | ||||||
|  |             shell, | ||||||
|  |             version: version_info, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn parse_mac_entry(&self, line: &str) -> Option<Result<MacAddressEntry, Error>> { | ||||||
|  |         debug!("[Brocade] Parsing mac address entry: {line}"); | ||||||
|  |         let parts: Vec<&str> = line.split_whitespace().collect(); | ||||||
|  |         if parts.len() < 3 { | ||||||
|  |             return None; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let (vlan, mac_address, port) = match parts.len() { | ||||||
|  |             3 => ( | ||||||
|  |                 u16::from_str(parts[0]).ok()?, | ||||||
|  |                 parse_brocade_mac_address(parts[1]).ok()?, | ||||||
|  |                 parts[2].to_string(), | ||||||
|  |             ), | ||||||
|  |             _ => ( | ||||||
|  |                 1, | ||||||
|  |                 parse_brocade_mac_address(parts[0]).ok()?, | ||||||
|  |                 parts[1].to_string(), | ||||||
|  |             ), | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let port = | ||||||
|  |             PortDeclaration::parse(&port).map_err(|e| Error::UnexpectedError(format!("{e}"))); | ||||||
|  | 
 | ||||||
|  |         match port { | ||||||
|  |             Ok(p) => Some(Ok(MacAddressEntry { | ||||||
|  |                 vlan, | ||||||
|  |                 mac_address, | ||||||
|  |                 port: p, | ||||||
|  |             })), | ||||||
|  |             Err(e) => Some(Err(e)), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn parse_stack_port_entry(&self, line: &str) -> Option<Result<InterSwitchLink, Error>> { | ||||||
|  |         debug!("[Brocade] Parsing stack port entry: {line}"); | ||||||
|  |         let parts: Vec<&str> = line.split_whitespace().collect(); | ||||||
|  |         if parts.len() < 10 { | ||||||
|  |             return None; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let local_port = PortLocation::from_str(parts[0]).ok()?; | ||||||
|  | 
 | ||||||
|  |         Some(Ok(InterSwitchLink { | ||||||
|  |             local_port, | ||||||
|  |             remote_port: None, | ||||||
|  |         })) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn build_port_channel_commands( | ||||||
|  |         &self, | ||||||
|  |         channel_id: PortChannelId, | ||||||
|  |         channel_name: &str, | ||||||
|  |         ports: &[PortLocation], | ||||||
|  |     ) -> Vec<String> { | ||||||
|  |         let mut commands = vec![ | ||||||
|  |             "configure terminal".to_string(), | ||||||
|  |             format!("lag {channel_name} static id {channel_id}"), | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         for port in ports { | ||||||
|  |             commands.push(format!("ports ethernet {port}")); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         commands.push(format!("primary-port {}", ports[0])); | ||||||
|  |         commands.push("deploy".into()); | ||||||
|  |         commands.push("exit".into()); | ||||||
|  |         commands.push("write memory".into()); | ||||||
|  |         commands.push("exit".into()); | ||||||
|  | 
 | ||||||
|  |         commands | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[async_trait] | ||||||
|  | impl BrocadeClient for FastIronClient { | ||||||
|  |     async fn version(&self) -> Result<BrocadeInfo, Error> { | ||||||
|  |         Ok(self.version.clone()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn get_mac_address_table(&self) -> Result<Vec<MacAddressEntry>, Error> { | ||||||
|  |         info!("[Brocade] Showing MAC address table..."); | ||||||
|  | 
 | ||||||
|  |         let output = self | ||||||
|  |             .shell | ||||||
|  |             .run_command("show mac-address", ExecutionMode::Regular) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         output | ||||||
|  |             .lines() | ||||||
|  |             .skip(2) | ||||||
|  |             .filter_map(|line| self.parse_mac_entry(line)) | ||||||
|  |             .collect() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn get_stack_topology(&self) -> Result<Vec<InterSwitchLink>, Error> { | ||||||
|  |         let output = self | ||||||
|  |             .shell | ||||||
|  |             .run_command("show interface stack-ports", crate::ExecutionMode::Regular) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         output | ||||||
|  |             .lines() | ||||||
|  |             .skip(1) | ||||||
|  |             .filter_map(|line| self.parse_stack_port_entry(line)) | ||||||
|  |             .collect() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn get_interfaces(&self) -> Result<Vec<InterfaceInfo>, Error> { | ||||||
|  |         todo!() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn configure_interfaces( | ||||||
|  |         &self, | ||||||
|  |         _interfaces: Vec<(String, PortOperatingMode)>, | ||||||
|  |     ) -> Result<(), Error> { | ||||||
|  |         todo!() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn find_available_channel_id(&self) -> Result<PortChannelId, Error> { | ||||||
|  |         info!("[Brocade] Finding next available channel id..."); | ||||||
|  | 
 | ||||||
|  |         let output = self | ||||||
|  |             .shell | ||||||
|  |             .run_command("show lag", ExecutionMode::Regular) | ||||||
|  |             .await?; | ||||||
|  |         let re = Regex::new(r"=== LAG .* ID\s+(\d+)").expect("Invalid regex"); | ||||||
|  | 
 | ||||||
|  |         let used_ids: HashSet<u8> = output | ||||||
|  |             .lines() | ||||||
|  |             .filter_map(|line| { | ||||||
|  |                 re.captures(line) | ||||||
|  |                     .and_then(|c| c.get(1)) | ||||||
|  |                     .and_then(|id_match| id_match.as_str().parse().ok()) | ||||||
|  |             }) | ||||||
|  |             .collect(); | ||||||
|  | 
 | ||||||
|  |         let mut next_id: u8 = 1; | ||||||
|  |         loop { | ||||||
|  |             if !used_ids.contains(&next_id) { | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             next_id += 1; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         info!("[Brocade] Found channel id: {next_id}"); | ||||||
|  |         Ok(next_id) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn create_port_channel( | ||||||
|  |         &self, | ||||||
|  |         channel_id: PortChannelId, | ||||||
|  |         channel_name: &str, | ||||||
|  |         ports: &[PortLocation], | ||||||
|  |     ) -> Result<(), Error> { | ||||||
|  |         info!( | ||||||
|  |             "[Brocade] Configuring port-channel '{channel_name} {channel_id}' with ports: {ports:?}" | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         let commands = self.build_port_channel_commands(channel_id, channel_name, ports); | ||||||
|  |         self.shell | ||||||
|  |             .run_commands(commands, ExecutionMode::Privileged) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         info!("[Brocade] Port-channel '{channel_name}' configured."); | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn clear_port_channel(&self, channel_name: &str) -> Result<(), Error> { | ||||||
|  |         info!("[Brocade] Clearing port-channel: {channel_name}"); | ||||||
|  | 
 | ||||||
|  |         let commands = vec![ | ||||||
|  |             "configure terminal".to_string(), | ||||||
|  |             format!("no lag {channel_name}"), | ||||||
|  |             "write memory".to_string(), | ||||||
|  |         ]; | ||||||
|  |         self.shell | ||||||
|  |             .run_commands(commands, ExecutionMode::Privileged) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         info!("[Brocade] Port-channel '{channel_name}' cleared."); | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										336
									
								
								brocade/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								brocade/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,336 @@ | |||||||
|  | use std::net::IpAddr; | ||||||
|  | use std::{ | ||||||
|  |     fmt::{self, Display}, | ||||||
|  |     time::Duration, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use crate::network_operating_system::NetworkOperatingSystemClient; | ||||||
|  | use crate::{ | ||||||
|  |     fast_iron::FastIronClient, | ||||||
|  |     shell::{BrocadeSession, BrocadeShell}, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use async_trait::async_trait; | ||||||
|  | use harmony_types::net::MacAddress; | ||||||
|  | use harmony_types::switch::{PortDeclaration, PortLocation}; | ||||||
|  | use regex::Regex; | ||||||
|  | 
 | ||||||
|  | mod fast_iron; | ||||||
|  | mod network_operating_system; | ||||||
|  | mod shell; | ||||||
|  | mod ssh; | ||||||
|  | 
 | ||||||
|  | #[derive(Default, Clone, Debug)] | ||||||
|  | pub struct BrocadeOptions { | ||||||
|  |     pub dry_run: bool, | ||||||
|  |     pub ssh: ssh::SshOptions, | ||||||
|  |     pub timeouts: TimeoutConfig, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone, Debug)] | ||||||
|  | pub struct TimeoutConfig { | ||||||
|  |     pub shell_ready: Duration, | ||||||
|  |     pub command_execution: Duration, | ||||||
|  |     pub cleanup: Duration, | ||||||
|  |     pub message_wait: Duration, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Default for TimeoutConfig { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         Self { | ||||||
|  |             shell_ready: Duration::from_secs(10), | ||||||
|  |             command_execution: Duration::from_secs(60), // Commands like `deploy` (for a LAG) can take a while
 | ||||||
|  |             cleanup: Duration::from_secs(10), | ||||||
|  |             message_wait: Duration::from_millis(500), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | enum ExecutionMode { | ||||||
|  |     Regular, | ||||||
|  |     Privileged, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone, Debug)] | ||||||
|  | pub struct BrocadeInfo { | ||||||
|  |     os: BrocadeOs, | ||||||
|  |     version: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone, Debug)] | ||||||
|  | pub enum BrocadeOs { | ||||||
|  |     NetworkOperatingSystem, | ||||||
|  |     FastIron, | ||||||
|  |     Unknown, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] | ||||||
|  | pub struct MacAddressEntry { | ||||||
|  |     pub vlan: u16, | ||||||
|  |     pub mac_address: MacAddress, | ||||||
|  |     pub port: PortDeclaration, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub type PortChannelId = u8; | ||||||
|  | 
 | ||||||
|  | /// Represents a single physical or logical link connecting two switches within a stack or fabric.
 | ||||||
|  | ///
 | ||||||
|  | /// This structure provides a standardized view of the topology regardless of the
 | ||||||
|  | /// underlying Brocade OS configuration (stacking vs. fabric).
 | ||||||
|  | #[derive(Debug, PartialEq, Eq, Clone)] | ||||||
|  | pub struct InterSwitchLink { | ||||||
|  |     /// The local port on the switch where the topology command was run.
 | ||||||
|  |     pub local_port: PortLocation, | ||||||
|  |     /// The port on the directly connected neighboring switch.
 | ||||||
|  |     pub remote_port: Option<PortLocation>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Represents the key running configuration status of a single switch interface.
 | ||||||
|  | #[derive(Debug, PartialEq, Eq, Clone)] | ||||||
|  | pub struct InterfaceInfo { | ||||||
|  |     /// The full configuration name (e.g., "TenGigabitEthernet 1/0/1", "FortyGigabitEthernet 2/0/2").
 | ||||||
|  |     pub name: String, | ||||||
|  |     /// The physical location of the interface.
 | ||||||
|  |     pub port_location: PortLocation, | ||||||
|  |     /// The parsed type and name prefix of the interface.
 | ||||||
|  |     pub interface_type: InterfaceType, | ||||||
|  |     /// The primary configuration mode defining the interface's behavior (L2, L3, Fabric).
 | ||||||
|  |     pub operating_mode: Option<PortOperatingMode>, | ||||||
|  |     /// Indicates the current state of the interface.
 | ||||||
|  |     pub status: InterfaceStatus, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Categorizes the functional type of a switch interface.
 | ||||||
|  | #[derive(Debug, PartialEq, Eq, Clone)] | ||||||
|  | pub enum InterfaceType { | ||||||
|  |     /// Physical or virtual Ethernet interface (e.g., TenGigabitEthernet, FortyGigabitEthernet).
 | ||||||
|  |     Ethernet(String), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl fmt::Display for InterfaceType { | ||||||
|  |     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||||
|  |         match self { | ||||||
|  |             InterfaceType::Ethernet(name) => write!(f, "{name}"), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Defines the primary configuration mode of a switch interface, representing mutually exclusive roles.
 | ||||||
|  | #[derive(Debug, PartialEq, Eq, Clone)] | ||||||
|  | pub enum PortOperatingMode { | ||||||
|  |     /// The interface is explicitly configured for Brocade fabric roles (ISL or Trunk enabled).
 | ||||||
|  |     Fabric, | ||||||
|  |     /// The interface is configured for standard Layer 2 switching as Trunk port (`switchport mode trunk`).
 | ||||||
|  |     Trunk, | ||||||
|  |     /// The interface is configured for standard Layer 2 switching as Access port (`switchport` without trunk mode).
 | ||||||
|  |     Access, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Defines the possible status of an interface.
 | ||||||
|  | #[derive(Debug, PartialEq, Eq, Clone)] | ||||||
|  | pub enum InterfaceStatus { | ||||||
|  |     /// The interface is connected.
 | ||||||
|  |     Connected, | ||||||
|  |     /// The interface is not connected and is not expected to be.
 | ||||||
|  |     NotConnected, | ||||||
|  |     /// The interface is not connected but is expected to be (configured with `no shutdown`).
 | ||||||
|  |     SfpAbsent, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub async fn init( | ||||||
|  |     ip_addresses: &[IpAddr], | ||||||
|  |     port: u16, | ||||||
|  |     username: &str, | ||||||
|  |     password: &str, | ||||||
|  |     options: Option<BrocadeOptions>, | ||||||
|  | ) -> Result<Box<dyn BrocadeClient + Send + Sync>, Error> { | ||||||
|  |     let shell = BrocadeShell::init(ip_addresses, port, username, password, options).await?; | ||||||
|  | 
 | ||||||
|  |     let version_info = shell | ||||||
|  |         .with_session(ExecutionMode::Regular, |session| { | ||||||
|  |             Box::pin(get_brocade_info(session)) | ||||||
|  |         }) | ||||||
|  |         .await?; | ||||||
|  | 
 | ||||||
|  |     Ok(match version_info.os { | ||||||
|  |         BrocadeOs::FastIron => Box::new(FastIronClient::init(shell, version_info)), | ||||||
|  |         BrocadeOs::NetworkOperatingSystem => { | ||||||
|  |             Box::new(NetworkOperatingSystemClient::init(shell, version_info)) | ||||||
|  |         } | ||||||
|  |         BrocadeOs::Unknown => todo!(), | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[async_trait] | ||||||
|  | pub trait BrocadeClient { | ||||||
|  |     /// Retrieves the operating system and version details from the connected Brocade switch.
 | ||||||
|  |     ///
 | ||||||
|  |     /// This is typically the first call made after establishing a connection to determine
 | ||||||
|  |     /// the switch OS family (e.g., FastIron, NOS) for feature compatibility.
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Returns
 | ||||||
|  |     ///
 | ||||||
|  |     /// A `BrocadeInfo` structure containing parsed OS type and version string.
 | ||||||
|  |     async fn version(&self) -> Result<BrocadeInfo, Error>; | ||||||
|  | 
 | ||||||
|  |     /// Retrieves the dynamically learned MAC address table from the switch.
 | ||||||
|  |     ///
 | ||||||
|  |     /// This is crucial for discovering where specific network endpoints (MAC addresses)
 | ||||||
|  |     /// are currently located on the physical ports.
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Returns
 | ||||||
|  |     ///
 | ||||||
|  |     /// A vector of `MacAddressEntry`, where each entry typically contains VLAN, MAC address,
 | ||||||
|  |     /// and the associated port name/index.
 | ||||||
|  |     async fn get_mac_address_table(&self) -> Result<Vec<MacAddressEntry>, Error>; | ||||||
|  | 
 | ||||||
|  |     /// Derives the physical connections used to link multiple switches together
 | ||||||
|  |     /// to form a single logical entity (stack, fabric, etc.).
 | ||||||
|  |     ///
 | ||||||
|  |     /// This abstracts the underlying configuration (e.g., stack ports, fabric ports)
 | ||||||
|  |     /// to return a standardized view of the topology.
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Returns
 | ||||||
|  |     ///
 | ||||||
|  |     /// A vector of `InterSwitchLink` structs detailing which ports are used for stacking/fabric.
 | ||||||
|  |     /// If the switch is not stacked, returns an empty vector.
 | ||||||
|  |     async fn get_stack_topology(&self) -> Result<Vec<InterSwitchLink>, Error>; | ||||||
|  | 
 | ||||||
|  |     /// Retrieves the status for all interfaces
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Returns
 | ||||||
|  |     ///
 | ||||||
|  |     /// A vector of `InterfaceInfo` structures.
 | ||||||
|  |     async fn get_interfaces(&self) -> Result<Vec<InterfaceInfo>, Error>; | ||||||
|  | 
 | ||||||
|  |     /// Configures a set of interfaces to be operated with a specified mode (access ports, ISL, etc.).
 | ||||||
|  |     async fn configure_interfaces( | ||||||
|  |         &self, | ||||||
|  |         interfaces: Vec<(String, PortOperatingMode)>, | ||||||
|  |     ) -> Result<(), Error>; | ||||||
|  | 
 | ||||||
|  |     /// Scans the existing configuration to find the next available (unused)
 | ||||||
|  |     /// Port-Channel ID (`lag` or `trunk`) for assignment.
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Returns
 | ||||||
|  |     ///
 | ||||||
|  |     /// The smallest, unassigned `PortChannelId` within the supported range.
 | ||||||
|  |     async fn find_available_channel_id(&self) -> Result<PortChannelId, Error>; | ||||||
|  | 
 | ||||||
|  |     /// Creates and configures a new Port-Channel (Link Aggregation Group or LAG)
 | ||||||
|  |     /// using the specified channel ID and ports.
 | ||||||
|  |     ///
 | ||||||
|  |     /// The resulting configuration must be persistent (saved to startup-config).
 | ||||||
|  |     /// Assumes a static LAG configuration mode unless specified otherwise by the implementation.
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Parameters
 | ||||||
|  |     ///
 | ||||||
|  |     /// * `channel_id`: The ID (e.g., 1-128) for the logical port channel.
 | ||||||
|  |     /// * `channel_name`: A descriptive name for the LAG (used in configuration context).
 | ||||||
|  |     /// * `ports`: A slice of `PortLocation` structs defining the physical member ports.
 | ||||||
|  |     async fn create_port_channel( | ||||||
|  |         &self, | ||||||
|  |         channel_id: PortChannelId, | ||||||
|  |         channel_name: &str, | ||||||
|  |         ports: &[PortLocation], | ||||||
|  |     ) -> Result<(), Error>; | ||||||
|  | 
 | ||||||
|  |     /// Removes all configuration associated with the specified Port-Channel name.
 | ||||||
|  |     ///
 | ||||||
|  |     /// This operation should be idempotent; attempting to clear a non-existent
 | ||||||
|  |     /// channel should succeed (or return a benign error).
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Parameters
 | ||||||
|  |     ///
 | ||||||
|  |     /// * `channel_name`: The name of the Port-Channel (LAG) to delete.
 | ||||||
|  |     ///
 | ||||||
|  |     async fn clear_port_channel(&self, channel_name: &str) -> Result<(), Error>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async fn get_brocade_info(session: &mut BrocadeSession) -> Result<BrocadeInfo, Error> { | ||||||
|  |     let output = session.run_command("show version").await?; | ||||||
|  | 
 | ||||||
|  |     if output.contains("Network Operating System") { | ||||||
|  |         let re = Regex::new(r"Network Operating System Version:\s*(?P<version>[a-zA-Z0-9.\-]+)") | ||||||
|  |             .expect("Invalid regex"); | ||||||
|  |         let version = re | ||||||
|  |             .captures(&output) | ||||||
|  |             .and_then(|cap| cap.name("version")) | ||||||
|  |             .map(|m| m.as_str().to_string()) | ||||||
|  |             .unwrap_or_default(); | ||||||
|  | 
 | ||||||
|  |         return Ok(BrocadeInfo { | ||||||
|  |             os: BrocadeOs::NetworkOperatingSystem, | ||||||
|  |             version, | ||||||
|  |         }); | ||||||
|  |     } else if output.contains("ICX") { | ||||||
|  |         let re = Regex::new(r"(?m)^\s*SW: Version\s*(?P<version>[a-zA-Z0-9.\-]+)") | ||||||
|  |             .expect("Invalid regex"); | ||||||
|  |         let version = re | ||||||
|  |             .captures(&output) | ||||||
|  |             .and_then(|cap| cap.name("version")) | ||||||
|  |             .map(|m| m.as_str().to_string()) | ||||||
|  |             .unwrap_or_default(); | ||||||
|  | 
 | ||||||
|  |         return Ok(BrocadeInfo { | ||||||
|  |             os: BrocadeOs::FastIron, | ||||||
|  |             version, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Err(Error::UnexpectedError("Unknown Brocade OS version".into())) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn parse_brocade_mac_address(value: &str) -> Result<MacAddress, String> { | ||||||
|  |     let cleaned_mac = value.replace('.', ""); | ||||||
|  | 
 | ||||||
|  |     if cleaned_mac.len() != 12 { | ||||||
|  |         return Err(format!("Invalid MAC address: {value}")); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let mut bytes = [0u8; 6]; | ||||||
|  |     for (i, pair) in cleaned_mac.as_bytes().chunks(2).enumerate() { | ||||||
|  |         let byte_str = std::str::from_utf8(pair).map_err(|_| "Invalid UTF-8")?; | ||||||
|  |         bytes[i] = | ||||||
|  |             u8::from_str_radix(byte_str, 16).map_err(|_| format!("Invalid hex in MAC: {value}"))?; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Ok(MacAddress(bytes)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub enum Error { | ||||||
|  |     NetworkError(String), | ||||||
|  |     AuthenticationError(String), | ||||||
|  |     ConfigurationError(String), | ||||||
|  |     TimeoutError(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::TimeoutError(msg) => write!(f, "Timeout error: {msg}"), | ||||||
|  |             Error::UnexpectedError(msg) => write!(f, "Unexpected error: {msg}"), | ||||||
|  |             Error::CommandError(msg) => write!(f, "{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(format!("Russh client error: {value}")) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										306
									
								
								brocade/src/network_operating_system.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										306
									
								
								brocade/src/network_operating_system.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,306 @@ | |||||||
|  | use std::str::FromStr; | ||||||
|  | 
 | ||||||
|  | use async_trait::async_trait; | ||||||
|  | use harmony_types::switch::{PortDeclaration, PortLocation}; | ||||||
|  | use log::{debug, info}; | ||||||
|  | 
 | ||||||
|  | use crate::{ | ||||||
|  |     BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo, | ||||||
|  |     InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, PortOperatingMode, | ||||||
|  |     parse_brocade_mac_address, shell::BrocadeShell, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | pub struct NetworkOperatingSystemClient { | ||||||
|  |     shell: BrocadeShell, | ||||||
|  |     version: BrocadeInfo, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl NetworkOperatingSystemClient { | ||||||
|  |     pub fn init(mut shell: BrocadeShell, version_info: BrocadeInfo) -> Self { | ||||||
|  |         shell.before_all(vec!["terminal length 0".into()]); | ||||||
|  | 
 | ||||||
|  |         Self { | ||||||
|  |             shell, | ||||||
|  |             version: version_info, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn parse_mac_entry(&self, line: &str) -> Option<Result<MacAddressEntry, Error>> { | ||||||
|  |         debug!("[Brocade] Parsing mac address entry: {line}"); | ||||||
|  |         let parts: Vec<&str> = line.split_whitespace().collect(); | ||||||
|  |         if parts.len() < 5 { | ||||||
|  |             return None; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let (vlan, mac_address, port) = match parts.len() { | ||||||
|  |             5 => ( | ||||||
|  |                 u16::from_str(parts[0]).ok()?, | ||||||
|  |                 parse_brocade_mac_address(parts[1]).ok()?, | ||||||
|  |                 parts[4].to_string(), | ||||||
|  |             ), | ||||||
|  |             _ => ( | ||||||
|  |                 u16::from_str(parts[0]).ok()?, | ||||||
|  |                 parse_brocade_mac_address(parts[1]).ok()?, | ||||||
|  |                 parts[5].to_string(), | ||||||
|  |             ), | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let port = | ||||||
|  |             PortDeclaration::parse(&port).map_err(|e| Error::UnexpectedError(format!("{e}"))); | ||||||
|  | 
 | ||||||
|  |         match port { | ||||||
|  |             Ok(p) => Some(Ok(MacAddressEntry { | ||||||
|  |                 vlan, | ||||||
|  |                 mac_address, | ||||||
|  |                 port: p, | ||||||
|  |             })), | ||||||
|  |             Err(e) => Some(Err(e)), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn parse_inter_switch_link_entry(&self, line: &str) -> Option<Result<InterSwitchLink, Error>> { | ||||||
|  |         debug!("[Brocade] Parsing inter switch link entry: {line}"); | ||||||
|  |         let parts: Vec<&str> = line.split_whitespace().collect(); | ||||||
|  |         if parts.len() < 10 { | ||||||
|  |             return None; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let local_port = PortLocation::from_str(parts[2]).ok()?; | ||||||
|  |         let remote_port = PortLocation::from_str(parts[5]).ok()?; | ||||||
|  | 
 | ||||||
|  |         Some(Ok(InterSwitchLink { | ||||||
|  |             local_port, | ||||||
|  |             remote_port: Some(remote_port), | ||||||
|  |         })) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn parse_interface_status_entry(&self, line: &str) -> Option<Result<InterfaceInfo, Error>> { | ||||||
|  |         debug!("[Brocade] Parsing interface status entry: {line}"); | ||||||
|  |         let parts: Vec<&str> = line.split_whitespace().collect(); | ||||||
|  |         if parts.len() < 6 { | ||||||
|  |             return None; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let interface_type = match parts[0] { | ||||||
|  |             "Fo" => InterfaceType::Ethernet("FortyGigabitEthernet".to_string()), | ||||||
|  |             "Te" => InterfaceType::Ethernet("TenGigabitEthernet".to_string()), | ||||||
|  |             _ => return None, | ||||||
|  |         }; | ||||||
|  |         let port_location = PortLocation::from_str(parts[1]).ok()?; | ||||||
|  |         let status = match parts[2] { | ||||||
|  |             "connected" => InterfaceStatus::Connected, | ||||||
|  |             "notconnected" => InterfaceStatus::NotConnected, | ||||||
|  |             "sfpAbsent" => InterfaceStatus::SfpAbsent, | ||||||
|  |             _ => return None, | ||||||
|  |         }; | ||||||
|  |         let operating_mode = match parts[3] { | ||||||
|  |             "ISL" => Some(PortOperatingMode::Fabric), | ||||||
|  |             "Trunk" => Some(PortOperatingMode::Trunk), | ||||||
|  |             "Access" => Some(PortOperatingMode::Access), | ||||||
|  |             "--" => None, | ||||||
|  |             _ => return None, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         Some(Ok(InterfaceInfo { | ||||||
|  |             name: format!("{} {}", interface_type, port_location), | ||||||
|  |             port_location, | ||||||
|  |             interface_type, | ||||||
|  |             operating_mode, | ||||||
|  |             status, | ||||||
|  |         })) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[async_trait] | ||||||
|  | impl BrocadeClient for NetworkOperatingSystemClient { | ||||||
|  |     async fn version(&self) -> Result<BrocadeInfo, Error> { | ||||||
|  |         Ok(self.version.clone()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn get_mac_address_table(&self) -> Result<Vec<MacAddressEntry>, Error> { | ||||||
|  |         let output = self | ||||||
|  |             .shell | ||||||
|  |             .run_command("show mac-address-table", ExecutionMode::Regular) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         output | ||||||
|  |             .lines() | ||||||
|  |             .skip(1) | ||||||
|  |             .filter_map(|line| self.parse_mac_entry(line)) | ||||||
|  |             .collect() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn get_stack_topology(&self) -> Result<Vec<InterSwitchLink>, Error> { | ||||||
|  |         let output = self | ||||||
|  |             .shell | ||||||
|  |             .run_command("show fabric isl", ExecutionMode::Regular) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         output | ||||||
|  |             .lines() | ||||||
|  |             .skip(6) | ||||||
|  |             .filter_map(|line| self.parse_inter_switch_link_entry(line)) | ||||||
|  |             .collect() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn get_interfaces(&self) -> Result<Vec<InterfaceInfo>, Error> { | ||||||
|  |         let output = self | ||||||
|  |             .shell | ||||||
|  |             .run_command( | ||||||
|  |                 "show interface status rbridge-id all", | ||||||
|  |                 ExecutionMode::Regular, | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         output | ||||||
|  |             .lines() | ||||||
|  |             .skip(2) | ||||||
|  |             .filter_map(|line| self.parse_interface_status_entry(line)) | ||||||
|  |             .collect() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn configure_interfaces( | ||||||
|  |         &self, | ||||||
|  |         interfaces: Vec<(String, PortOperatingMode)>, | ||||||
|  |     ) -> Result<(), Error> { | ||||||
|  |         info!("[Brocade] Configuring {} interface(s)...", interfaces.len()); | ||||||
|  | 
 | ||||||
|  |         let mut commands = vec!["configure terminal".to_string()]; | ||||||
|  | 
 | ||||||
|  |         for interface in interfaces { | ||||||
|  |             commands.push(format!("interface {}", interface.0)); | ||||||
|  | 
 | ||||||
|  |             match interface.1 { | ||||||
|  |                 PortOperatingMode::Fabric => { | ||||||
|  |                     commands.push("fabric isl enable".into()); | ||||||
|  |                     commands.push("fabric trunk enable".into()); | ||||||
|  |                 } | ||||||
|  |                 PortOperatingMode::Trunk => { | ||||||
|  |                     commands.push("switchport".into()); | ||||||
|  |                     commands.push("switchport mode trunk".into()); | ||||||
|  |                     commands.push("no spanning-tree shutdown".into()); | ||||||
|  |                     commands.push("no fabric isl enable".into()); | ||||||
|  |                     commands.push("no fabric trunk enable".into()); | ||||||
|  |                 } | ||||||
|  |                 PortOperatingMode::Access => { | ||||||
|  |                     commands.push("switchport".into()); | ||||||
|  |                     commands.push("switchport mode access".into()); | ||||||
|  |                     commands.push("switchport access vlan 1".into()); | ||||||
|  |                     commands.push("no spanning-tree shutdown".into()); | ||||||
|  |                     commands.push("no fabric isl enable".into()); | ||||||
|  |                     commands.push("no fabric trunk enable".into()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             commands.push("no shutdown".into()); | ||||||
|  |             commands.push("exit".into()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         commands.push("write memory".into()); | ||||||
|  | 
 | ||||||
|  |         self.shell | ||||||
|  |             .run_commands(commands, ExecutionMode::Regular) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         info!("[Brocade] Interfaces configured."); | ||||||
|  | 
 | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn find_available_channel_id(&self) -> Result<PortChannelId, Error> { | ||||||
|  |         info!("[Brocade] Finding next available channel id..."); | ||||||
|  | 
 | ||||||
|  |         let output = self | ||||||
|  |             .shell | ||||||
|  |             .run_command("show port-channel", ExecutionMode::Regular) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         let used_ids: Vec<u8> = output | ||||||
|  |             .lines() | ||||||
|  |             .skip(6) | ||||||
|  |             .filter_map(|line| { | ||||||
|  |                 let parts: Vec<&str> = line.split_whitespace().collect(); | ||||||
|  |                 if parts.len() < 8 { | ||||||
|  |                     return None; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 u8::from_str(parts[0]).ok() | ||||||
|  |             }) | ||||||
|  |             .collect(); | ||||||
|  | 
 | ||||||
|  |         let mut next_id: u8 = 1; | ||||||
|  |         loop { | ||||||
|  |             if !used_ids.contains(&next_id) { | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             next_id += 1; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         info!("[Brocade] Found channel id: {next_id}"); | ||||||
|  |         Ok(next_id) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn create_port_channel( | ||||||
|  |         &self, | ||||||
|  |         channel_id: PortChannelId, | ||||||
|  |         channel_name: &str, | ||||||
|  |         ports: &[PortLocation], | ||||||
|  |     ) -> Result<(), Error> { | ||||||
|  |         info!( | ||||||
|  |             "[Brocade] Configuring port-channel '{channel_name} {channel_id}' with ports: {ports:?}" | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         let interfaces = self.get_interfaces().await?; | ||||||
|  | 
 | ||||||
|  |         let mut commands = vec![ | ||||||
|  |             "configure terminal".into(), | ||||||
|  |             format!("interface port-channel {}", channel_id), | ||||||
|  |             "no shutdown".into(), | ||||||
|  |             "exit".into(), | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         for port in ports { | ||||||
|  |             let interface = interfaces.iter().find(|i| i.port_location == *port); | ||||||
|  |             let Some(interface) = interface else { | ||||||
|  |                 continue; | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  |             commands.push(format!("interface {}", interface.name)); | ||||||
|  |             commands.push("no switchport".into()); | ||||||
|  |             commands.push("no ip address".into()); | ||||||
|  |             commands.push("no fabric isl enable".into()); | ||||||
|  |             commands.push("no fabric trunk enable".into()); | ||||||
|  |             commands.push(format!("channel-group {} mode active", channel_id)); | ||||||
|  |             commands.push("no shutdown".into()); | ||||||
|  |             commands.push("exit".into()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         commands.push("write memory".into()); | ||||||
|  | 
 | ||||||
|  |         self.shell | ||||||
|  |             .run_commands(commands, ExecutionMode::Regular) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         info!("[Brocade] Port-channel '{channel_name}' configured."); | ||||||
|  | 
 | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn clear_port_channel(&self, channel_name: &str) -> Result<(), Error> { | ||||||
|  |         info!("[Brocade] Clearing port-channel: {channel_name}"); | ||||||
|  | 
 | ||||||
|  |         let commands = vec![ | ||||||
|  |             "configure terminal".into(), | ||||||
|  |             format!("no interface port-channel {}", channel_name), | ||||||
|  |             "exit".into(), | ||||||
|  |             "write memory".into(), | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         self.shell | ||||||
|  |             .run_commands(commands, ExecutionMode::Regular) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         info!("[Brocade] Port-channel '{channel_name}' cleared."); | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										367
									
								
								brocade/src/shell.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										367
									
								
								brocade/src/shell.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,367 @@ | |||||||
|  | use std::net::IpAddr; | ||||||
|  | use std::time::Duration; | ||||||
|  | use std::time::Instant; | ||||||
|  | 
 | ||||||
|  | use crate::BrocadeOptions; | ||||||
|  | use crate::Error; | ||||||
|  | use crate::ExecutionMode; | ||||||
|  | use crate::TimeoutConfig; | ||||||
|  | use crate::ssh; | ||||||
|  | 
 | ||||||
|  | use log::debug; | ||||||
|  | use log::info; | ||||||
|  | use russh::ChannelMsg; | ||||||
|  | use tokio::time::timeout; | ||||||
|  | 
 | ||||||
|  | pub struct BrocadeShell { | ||||||
|  |     ip: IpAddr, | ||||||
|  |     port: u16, | ||||||
|  |     username: String, | ||||||
|  |     password: String, | ||||||
|  |     options: BrocadeOptions, | ||||||
|  |     before_all_commands: Vec<String>, | ||||||
|  |     after_all_commands: Vec<String>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl BrocadeShell { | ||||||
|  |     pub async fn init( | ||||||
|  |         ip_addresses: &[IpAddr], | ||||||
|  |         port: u16, | ||||||
|  |         username: &str, | ||||||
|  |         password: &str, | ||||||
|  |         options: Option<BrocadeOptions>, | ||||||
|  |     ) -> Result<Self, Error> { | ||||||
|  |         let ip = ip_addresses | ||||||
|  |             .first() | ||||||
|  |             .ok_or_else(|| Error::ConfigurationError("No IP addresses provided".to_string()))?; | ||||||
|  | 
 | ||||||
|  |         let base_options = options.unwrap_or_default(); | ||||||
|  |         let options = ssh::try_init_client(username, password, ip, base_options).await?; | ||||||
|  | 
 | ||||||
|  |         Ok(Self { | ||||||
|  |             ip: *ip, | ||||||
|  |             port, | ||||||
|  |             username: username.to_string(), | ||||||
|  |             password: password.to_string(), | ||||||
|  |             before_all_commands: vec![], | ||||||
|  |             after_all_commands: vec![], | ||||||
|  |             options, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub async fn open_session(&self, mode: ExecutionMode) -> Result<BrocadeSession, Error> { | ||||||
|  |         BrocadeSession::open( | ||||||
|  |             self.ip, | ||||||
|  |             self.port, | ||||||
|  |             &self.username, | ||||||
|  |             &self.password, | ||||||
|  |             self.options.clone(), | ||||||
|  |             mode, | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub async fn with_session<F, R>(&self, mode: ExecutionMode, callback: F) -> Result<R, Error> | ||||||
|  |     where | ||||||
|  |         F: FnOnce( | ||||||
|  |             &mut BrocadeSession, | ||||||
|  |         ) -> std::pin::Pin< | ||||||
|  |             Box<dyn std::future::Future<Output = Result<R, Error>> + Send + '_>, | ||||||
|  |         >, | ||||||
|  |     { | ||||||
|  |         let mut session = self.open_session(mode).await?; | ||||||
|  | 
 | ||||||
|  |         let _ = session.run_commands(self.before_all_commands.clone()).await; | ||||||
|  |         let result = callback(&mut session).await; | ||||||
|  |         let _ = session.run_commands(self.after_all_commands.clone()).await; | ||||||
|  | 
 | ||||||
|  |         session.close().await?; | ||||||
|  |         result | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub async fn run_command(&self, command: &str, mode: ExecutionMode) -> Result<String, Error> { | ||||||
|  |         let mut session = self.open_session(mode).await?; | ||||||
|  | 
 | ||||||
|  |         let _ = session.run_commands(self.before_all_commands.clone()).await; | ||||||
|  |         let result = session.run_command(command).await; | ||||||
|  |         let _ = session.run_commands(self.after_all_commands.clone()).await; | ||||||
|  | 
 | ||||||
|  |         session.close().await?; | ||||||
|  |         result | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub async fn run_commands( | ||||||
|  |         &self, | ||||||
|  |         commands: Vec<String>, | ||||||
|  |         mode: ExecutionMode, | ||||||
|  |     ) -> Result<(), Error> { | ||||||
|  |         let mut session = self.open_session(mode).await?; | ||||||
|  | 
 | ||||||
|  |         let _ = session.run_commands(self.before_all_commands.clone()).await; | ||||||
|  |         let result = session.run_commands(commands).await; | ||||||
|  |         let _ = session.run_commands(self.after_all_commands.clone()).await; | ||||||
|  | 
 | ||||||
|  |         session.close().await?; | ||||||
|  |         result | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn before_all(&mut self, commands: Vec<String>) { | ||||||
|  |         self.before_all_commands = commands; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn after_all(&mut self, commands: Vec<String>) { | ||||||
|  |         self.after_all_commands = commands; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub struct BrocadeSession { | ||||||
|  |     pub channel: russh::Channel<russh::client::Msg>, | ||||||
|  |     pub mode: ExecutionMode, | ||||||
|  |     pub options: BrocadeOptions, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl BrocadeSession { | ||||||
|  |     pub async fn open( | ||||||
|  |         ip: IpAddr, | ||||||
|  |         port: u16, | ||||||
|  |         username: &str, | ||||||
|  |         password: &str, | ||||||
|  |         options: BrocadeOptions, | ||||||
|  |         mode: ExecutionMode, | ||||||
|  |     ) -> Result<Self, Error> { | ||||||
|  |         let client = ssh::create_client(ip, port, username, password, &options).await?; | ||||||
|  |         let mut channel = client.channel_open_session().await?; | ||||||
|  | 
 | ||||||
|  |         channel | ||||||
|  |             .request_pty(false, "vt100", 80, 24, 0, 0, &[]) | ||||||
|  |             .await?; | ||||||
|  |         channel.request_shell(false).await?; | ||||||
|  | 
 | ||||||
|  |         wait_for_shell_ready(&mut channel, &options.timeouts).await?; | ||||||
|  | 
 | ||||||
|  |         if let ExecutionMode::Privileged = mode { | ||||||
|  |             try_elevate_session(&mut channel, username, password, &options.timeouts).await?; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(Self { | ||||||
|  |             channel, | ||||||
|  |             mode, | ||||||
|  |             options, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub async fn close(&mut self) -> Result<(), Error> { | ||||||
|  |         debug!("[Brocade] Closing session..."); | ||||||
|  | 
 | ||||||
|  |         self.channel.data(&b"exit\n"[..]).await?; | ||||||
|  |         if let ExecutionMode::Privileged = self.mode { | ||||||
|  |             self.channel.data(&b"exit\n"[..]).await?; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let start = Instant::now(); | ||||||
|  |         while start.elapsed() < self.options.timeouts.cleanup { | ||||||
|  |             match timeout(self.options.timeouts.message_wait, self.channel.wait()).await { | ||||||
|  |                 Ok(Some(ChannelMsg::Close)) => break, | ||||||
|  |                 Ok(Some(_)) => continue, | ||||||
|  |                 Ok(None) | Err(_) => break, | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         debug!("[Brocade] Session closed."); | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub async fn run_command(&mut self, command: &str) -> Result<String, Error> { | ||||||
|  |         if self.should_skip_command(command) { | ||||||
|  |             return Ok(String::new()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         debug!("[Brocade] Running command: '{command}'..."); | ||||||
|  | 
 | ||||||
|  |         self.channel | ||||||
|  |             .data(format!("{}\n", command).as_bytes()) | ||||||
|  |             .await?; | ||||||
|  |         tokio::time::sleep(Duration::from_millis(100)).await; | ||||||
|  | 
 | ||||||
|  |         let output = self.collect_command_output().await?; | ||||||
|  |         let output = String::from_utf8(output) | ||||||
|  |             .map_err(|_| Error::UnexpectedError("Invalid UTF-8 in command output".to_string()))?; | ||||||
|  | 
 | ||||||
|  |         self.check_for_command_errors(&output, command)?; | ||||||
|  |         Ok(output) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub async fn run_commands(&mut self, commands: Vec<String>) -> Result<(), Error> { | ||||||
|  |         for command in commands { | ||||||
|  |             self.run_command(&command).await?; | ||||||
|  |         } | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn should_skip_command(&self, command: &str) -> bool { | ||||||
|  |         if (command.starts_with("write") || command.starts_with("deploy")) && self.options.dry_run { | ||||||
|  |             info!("[Brocade] Dry-run mode enabled, skipping command: {command}"); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn collect_command_output(&mut self) -> Result<Vec<u8>, Error> { | ||||||
|  |         let mut output = Vec::new(); | ||||||
|  |         let start = Instant::now(); | ||||||
|  |         let read_timeout = Duration::from_millis(500); | ||||||
|  |         let log_interval = Duration::from_secs(3); | ||||||
|  |         let mut last_log = Instant::now(); | ||||||
|  | 
 | ||||||
|  |         loop { | ||||||
|  |             if start.elapsed() > self.options.timeouts.command_execution { | ||||||
|  |                 return Err(Error::TimeoutError( | ||||||
|  |                     "Timeout waiting for command completion.".into(), | ||||||
|  |                 )); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if start.elapsed() > Duration::from_secs(5) && last_log.elapsed() > log_interval { | ||||||
|  |                 info!("[Brocade] Waiting for command output..."); | ||||||
|  |                 last_log = Instant::now(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             match timeout(read_timeout, self.channel.wait()).await { | ||||||
|  |                 Ok(Some(ChannelMsg::Data { data } | ChannelMsg::ExtendedData { data, .. })) => { | ||||||
|  |                     output.extend_from_slice(&data); | ||||||
|  |                     let current_output = String::from_utf8_lossy(&output); | ||||||
|  |                     if current_output.contains('>') || current_output.contains('#') { | ||||||
|  |                         return Ok(output); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 Ok(Some(ChannelMsg::Eof | ChannelMsg::Close)) => return Ok(output), | ||||||
|  |                 Ok(Some(ChannelMsg::ExitStatus { exit_status })) => { | ||||||
|  |                     debug!("[Brocade] Command exit status: {exit_status}"); | ||||||
|  |                 } | ||||||
|  |                 Ok(Some(_)) => continue, | ||||||
|  |                 Ok(None) | Err(_) => { | ||||||
|  |                     if output.is_empty() { | ||||||
|  |                         if let Ok(None) = timeout(read_timeout, self.channel.wait()).await { | ||||||
|  |                             break; | ||||||
|  |                         } | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     tokio::time::sleep(Duration::from_millis(100)).await; | ||||||
|  |                     let current_output = String::from_utf8_lossy(&output); | ||||||
|  |                     if current_output.contains('>') || current_output.contains('#') { | ||||||
|  |                         return Ok(output); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(output) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn check_for_command_errors(&self, output: &str, command: &str) -> Result<(), Error> { | ||||||
|  |         const ERROR_PATTERNS: &[&str] = &[ | ||||||
|  |             "invalid input", | ||||||
|  |             "syntax error", | ||||||
|  |             "command not found", | ||||||
|  |             "unknown command", | ||||||
|  |             "permission denied", | ||||||
|  |             "access denied", | ||||||
|  |             "authentication failed", | ||||||
|  |             "configuration error", | ||||||
|  |             "failed to", | ||||||
|  |             "error:", | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         let output_lower = output.to_lowercase(); | ||||||
|  |         if ERROR_PATTERNS.iter().any(|&p| output_lower.contains(p)) { | ||||||
|  |             return Err(Error::CommandError(format!( | ||||||
|  |                 "Command '{command}' failed: {}", | ||||||
|  |                 output.trim() | ||||||
|  |             ))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if !command.starts_with("show") && output.trim().is_empty() { | ||||||
|  |             return Err(Error::CommandError(format!( | ||||||
|  |                 "Command '{command}' produced no output" | ||||||
|  |             ))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async fn wait_for_shell_ready( | ||||||
|  |     channel: &mut russh::Channel<russh::client::Msg>, | ||||||
|  |     timeouts: &TimeoutConfig, | ||||||
|  | ) -> Result<(), Error> { | ||||||
|  |     let mut buffer = Vec::new(); | ||||||
|  |     let start = Instant::now(); | ||||||
|  | 
 | ||||||
|  |     while start.elapsed() < timeouts.shell_ready { | ||||||
|  |         match timeout(timeouts.message_wait, channel.wait()).await { | ||||||
|  |             Ok(Some(ChannelMsg::Data { data })) => { | ||||||
|  |                 buffer.extend_from_slice(&data); | ||||||
|  |                 let output = String::from_utf8_lossy(&buffer); | ||||||
|  |                 let output = output.trim(); | ||||||
|  |                 if output.ends_with('>') || output.ends_with('#') { | ||||||
|  |                     debug!("[Brocade] Shell ready"); | ||||||
|  |                     return Ok(()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Ok(Some(_)) => continue, | ||||||
|  |             Ok(None) => break, | ||||||
|  |             Err(_) => continue, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async fn try_elevate_session( | ||||||
|  |     channel: &mut russh::Channel<russh::client::Msg>, | ||||||
|  |     username: &str, | ||||||
|  |     password: &str, | ||||||
|  |     timeouts: &TimeoutConfig, | ||||||
|  | ) -> Result<(), Error> { | ||||||
|  |     channel.data(&b"enable\n"[..]).await?; | ||||||
|  |     let start = Instant::now(); | ||||||
|  |     let mut buffer = Vec::new(); | ||||||
|  | 
 | ||||||
|  |     while start.elapsed() < timeouts.shell_ready { | ||||||
|  |         match timeout(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", username).as_bytes()).await?; | ||||||
|  |                     buffer.clear(); | ||||||
|  |                 } else if output.contains("Password:") { | ||||||
|  |                     channel.data(format!("{}\n", password).as_bytes()).await?; | ||||||
|  |                     buffer.clear(); | ||||||
|  |                 } else if output.contains('>') { | ||||||
|  |                     return Err(Error::AuthenticationError( | ||||||
|  |                         "Enable authentication failed".into(), | ||||||
|  |                     )); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Ok(Some(_)) => continue, | ||||||
|  |             Ok(None) => break, | ||||||
|  |             Err(_) => continue, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let output = String::from_utf8_lossy(&buffer); | ||||||
|  |     if output.ends_with('#') { | ||||||
|  |         debug!("[Brocade] Privileged mode established"); | ||||||
|  |         Ok(()) | ||||||
|  |     } else { | ||||||
|  |         Err(Error::AuthenticationError(format!( | ||||||
|  |             "Enable failed. Output:\n{output}" | ||||||
|  |         ))) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										113
									
								
								brocade/src/ssh.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								brocade/src/ssh.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,113 @@ | |||||||
|  | use std::borrow::Cow; | ||||||
|  | use std::sync::Arc; | ||||||
|  | 
 | ||||||
|  | use async_trait::async_trait; | ||||||
|  | use russh::client::Handler; | ||||||
|  | use russh::kex::DH_G1_SHA1; | ||||||
|  | use russh::kex::ECDH_SHA2_NISTP256; | ||||||
|  | use russh_keys::key::SSH_RSA; | ||||||
|  | 
 | ||||||
|  | use super::BrocadeOptions; | ||||||
|  | use super::Error; | ||||||
|  | 
 | ||||||
|  | #[derive(Default, Clone, Debug)] | ||||||
|  | pub struct SshOptions { | ||||||
|  |     pub preferred_algorithms: russh::Preferred, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl SshOptions { | ||||||
|  |     fn ecdhsa_sha2_nistp256() -> Self { | ||||||
|  |         Self { | ||||||
|  |             preferred_algorithms: russh::Preferred { | ||||||
|  |                 kex: Cow::Borrowed(&[ECDH_SHA2_NISTP256]), | ||||||
|  |                 key: Cow::Borrowed(&[SSH_RSA]), | ||||||
|  |                 ..Default::default() | ||||||
|  |             }, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn legacy() -> Self { | ||||||
|  |         Self { | ||||||
|  |             preferred_algorithms: russh::Preferred { | ||||||
|  |                 kex: Cow::Borrowed(&[DH_G1_SHA1]), | ||||||
|  |                 key: Cow::Borrowed(&[SSH_RSA]), | ||||||
|  |                 ..Default::default() | ||||||
|  |             }, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub struct Client; | ||||||
|  | 
 | ||||||
|  | #[async_trait] | ||||||
|  | impl Handler for Client { | ||||||
|  |     type Error = Error; | ||||||
|  | 
 | ||||||
|  |     async fn check_server_key( | ||||||
|  |         &mut self, | ||||||
|  |         _server_public_key: &russh_keys::key::PublicKey, | ||||||
|  |     ) -> Result<bool, Self::Error> { | ||||||
|  |         Ok(true) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub async fn try_init_client( | ||||||
|  |     username: &str, | ||||||
|  |     password: &str, | ||||||
|  |     ip: &std::net::IpAddr, | ||||||
|  |     base_options: BrocadeOptions, | ||||||
|  | ) -> Result<BrocadeOptions, Error> { | ||||||
|  |     let ssh_options = vec![ | ||||||
|  |         SshOptions::default(), | ||||||
|  |         SshOptions::ecdhsa_sha2_nistp256(), | ||||||
|  |         SshOptions::legacy(), | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     for ssh in ssh_options { | ||||||
|  |         let opts = BrocadeOptions { | ||||||
|  |             ssh, | ||||||
|  |             ..base_options.clone() | ||||||
|  |         }; | ||||||
|  |         let client = create_client(*ip, 22, username, password, &opts).await; | ||||||
|  | 
 | ||||||
|  |         match client { | ||||||
|  |             Ok(_) => { | ||||||
|  |                 return Ok(opts); | ||||||
|  |             } | ||||||
|  |             Err(e) => match e { | ||||||
|  |                 Error::NetworkError(e) => { | ||||||
|  |                     if e.contains("No common key exchange algorithm") { | ||||||
|  |                         continue; | ||||||
|  |                     } else { | ||||||
|  |                         return Err(Error::NetworkError(e)); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 _ => return Err(e), | ||||||
|  |             }, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Err(Error::NetworkError( | ||||||
|  |         "Could not establish ssh connection: wrong key exchange algorithm)".to_string(), | ||||||
|  |     )) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub async fn create_client( | ||||||
|  |     ip: std::net::IpAddr, | ||||||
|  |     port: u16, | ||||||
|  |     username: &str, | ||||||
|  |     password: &str, | ||||||
|  |     options: &BrocadeOptions, | ||||||
|  | ) -> Result<russh::client::Handle<Client>, Error> { | ||||||
|  |     let config = russh::client::Config { | ||||||
|  |         preferred: options.ssh.preferred_algorithms.clone(), | ||||||
|  |         ..Default::default() | ||||||
|  |     }; | ||||||
|  |     let mut client = russh::client::connect(Arc::new(config), (ip, port), Client {}).await?; | ||||||
|  |     if !client.authenticate_password(username, password).await? { | ||||||
|  |         return Err(Error::AuthenticationError( | ||||||
|  |             "ssh authentication failed".to_string(), | ||||||
|  |         )); | ||||||
|  |     } | ||||||
|  |     Ok(client) | ||||||
|  | } | ||||||
| @ -19,7 +19,7 @@ use serde::{Deserialize, Serialize}; | |||||||
| ///
 | ///
 | ||||||
| /// **It is not meant to be very secure or unique**, it is suitable to generate up to 10 000 items per
 | /// **It is not meant to be very secure or unique**, it is suitable to generate up to 10 000 items per
 | ||||||
| /// second with a reasonable collision rate of 0,000014 % as calculated by this calculator : https://kevingal.com/apps/collision.html
 | /// second with a reasonable collision rate of 0,000014 % as calculated by this calculator : https://kevingal.com/apps/collision.html
 | ||||||
| #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | #[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] | ||||||
| pub struct Id { | pub struct Id { | ||||||
|     value: String, |     value: String, | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,2 +1,3 @@ | |||||||
| pub mod id; | pub mod id; | ||||||
| pub mod net; | pub mod net; | ||||||
|  | pub mod switch; | ||||||
|  | |||||||
| @ -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 { | ||||||
| @ -41,7 +41,7 @@ impl TryFrom<String> for MacAddress { | |||||||
|             bytes[i] = u8::from_str_radix(part, 16).map_err(|_| { |             bytes[i] = u8::from_str_radix(part, 16).map_err(|_| { | ||||||
|                 std::io::Error::new( |                 std::io::Error::new( | ||||||
|                     std::io::ErrorKind::InvalidInput, |                     std::io::ErrorKind::InvalidInput, | ||||||
|                     format!("Invalid hex value in part {}: '{}'", i, part), |                     format!("Invalid hex value in part {i}: '{part}'"), | ||||||
|                 ) |                 ) | ||||||
|             })?; |             })?; | ||||||
|         } |         } | ||||||
| @ -106,8 +106,8 @@ impl Serialize for Url { | |||||||
| impl std::fmt::Display for Url { | impl std::fmt::Display for Url { | ||||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|         match self { |         match self { | ||||||
|             Url::LocalFolder(path) => write!(f, "{}", path), |             Url::LocalFolder(path) => write!(f, "{path}"), | ||||||
|             Url::Url(url) => write!(f, "{}", url), |             Url::Url(url) => write!(f, "{url}"), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										176
									
								
								harmony_types/src/switch.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								harmony_types/src/switch.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,176 @@ | |||||||
|  | use std::{fmt, str::FromStr}; | ||||||
|  | 
 | ||||||
|  | /// Simple error type for port parsing failures.
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub enum PortParseError { | ||||||
|  |     /// The port string did not conform to the expected S/M/P or range format.
 | ||||||
|  |     InvalidFormat, | ||||||
|  |     /// A stack, module, or port segment could not be parsed as a number.
 | ||||||
|  |     InvalidSegment(String), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl fmt::Display for PortParseError { | ||||||
|  |     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||||
|  |         match self { | ||||||
|  |             PortParseError::InvalidFormat => write!(f, "Port string is in an unexpected format."), | ||||||
|  |             PortParseError::InvalidSegment(s) => write!(f, "Invalid segment in port string: {}", s), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Represents the atomic, physical location of a switch port: `<Stack>/<Module>/<Port>`.
 | ||||||
|  | ///
 | ||||||
|  | /// Example: `1/1/1`
 | ||||||
|  | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] | ||||||
|  | pub struct PortLocation(pub u8, pub u8, pub u8); | ||||||
|  | 
 | ||||||
|  | impl fmt::Display for PortLocation { | ||||||
|  |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|  |         write!(f, "{}/{}/{}", self.0, self.1, self.2) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl FromStr for PortLocation { | ||||||
|  |     type Err = PortParseError; | ||||||
|  | 
 | ||||||
|  |     /// Parses a string slice into a `PortLocation`.
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Examples
 | ||||||
|  |     ///
 | ||||||
|  |     /// ```rust
 | ||||||
|  |     /// use std::str::FromStr;
 | ||||||
|  |     /// use harmony_types::switch::PortLocation;
 | ||||||
|  |     ///
 | ||||||
|  |     /// assert_eq!(PortLocation::from_str("1/1/1").unwrap(), PortLocation(1, 1, 1));
 | ||||||
|  |     /// assert_eq!(PortLocation::from_str("12/5/48").unwrap(), PortLocation(12, 5, 48));
 | ||||||
|  |     /// assert!(PortLocation::from_str("1/A/1").is_err());
 | ||||||
|  |     /// ```
 | ||||||
|  |     fn from_str(s: &str) -> Result<Self, Self::Err> { | ||||||
|  |         let parts: Vec<&str> = s.split('/').collect(); | ||||||
|  | 
 | ||||||
|  |         if parts.len() != 3 { | ||||||
|  |             return Err(PortParseError::InvalidFormat); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let parse_segment = |part: &str| -> Result<u8, Self::Err> { | ||||||
|  |             u8::from_str(part).map_err(|_| PortParseError::InvalidSegment(part.to_string())) | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let stack = parse_segment(parts[0])?; | ||||||
|  |         let module = parse_segment(parts[1])?; | ||||||
|  |         let port = parse_segment(parts[2])?; | ||||||
|  | 
 | ||||||
|  |         Ok(PortLocation(stack, module, port)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Represents a Port configuration input, which can be a single port, a sequential range,
 | ||||||
|  | /// or an explicit set defined by endpoints.
 | ||||||
|  | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] | ||||||
|  | pub enum PortDeclaration { | ||||||
|  |     /// A single switch port defined by its location. Example: `PortDeclaration::Single(1/1/1)`
 | ||||||
|  |     Single(PortLocation), | ||||||
|  |     /// A strictly sequential range defined by two endpoints using the hyphen separator (`-`).
 | ||||||
|  |     /// All ports between the endpoints (inclusive) are implicitly included.
 | ||||||
|  |     /// Example: `PortDeclaration::Range(1/1/1, 1/1/4)`
 | ||||||
|  |     Range(PortLocation, PortLocation), | ||||||
|  |     /// A set of ports defined by two endpoints using the asterisk separator (`*`).
 | ||||||
|  |     /// The actual member ports must be determined contextually (e.g., from MAC tables or
 | ||||||
|  |     /// explicit configuration lists).
 | ||||||
|  |     /// Example: `PortDeclaration::Set(1/1/1, 1/1/3)` where only ports 1 and 3 might be active.
 | ||||||
|  |     Set(PortLocation, PortLocation), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl PortDeclaration { | ||||||
|  |     /// Parses a port configuration string into a structured `PortDeclaration` enum.
 | ||||||
|  |     ///
 | ||||||
|  |     /// This function performs only basic format and numerical parsing, assuming the input
 | ||||||
|  |     /// strings (e.g., from `show` commands) are semantically valid and logically ordered.
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Supported Formats
 | ||||||
|  |     ///
 | ||||||
|  |     /// * **Single Port:** `"1/1/1"`
 | ||||||
|  |     /// * **Range (Hyphen, `-`):** `"1/1/1-1/1/4"`
 | ||||||
|  |     /// * **Set (Asterisk, `*`):** `"1/1/1*1/1/4"`
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Errors
 | ||||||
|  |     ///
 | ||||||
|  |     /// Returns `PortParseError` if the string format is incorrect or numerical segments
 | ||||||
|  |     /// cannot be parsed.
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Examples
 | ||||||
|  |     ///
 | ||||||
|  |     /// ```rust
 | ||||||
|  |     /// use harmony_types::switch::{PortDeclaration, PortLocation};
 | ||||||
|  |     ///
 | ||||||
|  |     /// // Single Port
 | ||||||
|  |     /// assert_eq!(PortDeclaration::parse("3/2/15").unwrap(), PortDeclaration::Single(PortLocation(3, 2, 15)));
 | ||||||
|  |     ///
 | ||||||
|  |     /// // Range (Hyphen) - implies sequential ports
 | ||||||
|  |     /// let result_range = PortDeclaration::parse("1/1/1-1/1/4").unwrap();
 | ||||||
|  |     /// assert_eq!(result_range, PortDeclaration::Range(PortLocation(1, 1, 1), PortLocation(1, 1, 4)));
 | ||||||
|  |     ///
 | ||||||
|  |     /// // Set (Asterisk) - implies non-sequential set defined by endpoints
 | ||||||
|  |     /// let result_set = PortDeclaration::parse("1/1/48*2/1/48").unwrap();
 | ||||||
|  |     /// assert_eq!(result_set, PortDeclaration::Set(PortLocation(1, 1, 48), PortLocation(2, 1, 48)));
 | ||||||
|  |     ///
 | ||||||
|  |     /// // Invalid Format (will still fail basic parsing)
 | ||||||
|  |     /// assert!(PortDeclaration::parse("1/1/1/1").is_err());
 | ||||||
|  |     /// ```
 | ||||||
|  |     pub fn parse(port_str: &str) -> Result<Self, PortParseError> { | ||||||
|  |         if let Some((start_str, end_str)) = port_str.split_once('-') { | ||||||
|  |             let start_port = PortLocation::from_str(start_str.trim())?; | ||||||
|  |             let end_port = PortLocation::from_str(end_str.trim())?; | ||||||
|  |             return Ok(PortDeclaration::Range(start_port, end_port)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if let Some((start_str, end_str)) = port_str.split_once('*') { | ||||||
|  |             let start_port = PortLocation::from_str(start_str.trim())?; | ||||||
|  |             let end_port = PortLocation::from_str(end_str.trim())?; | ||||||
|  |             return Ok(PortDeclaration::Set(start_port, end_port)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let location = PortLocation::from_str(port_str)?; | ||||||
|  |         Ok(PortDeclaration::Single(location)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl fmt::Display for PortDeclaration { | ||||||
|  |     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||||
|  |         match self { | ||||||
|  |             PortDeclaration::Single(port) => write!(f, "{port}"), | ||||||
|  |             PortDeclaration::Range(start, end) => write!(f, "{start}-{end}"), | ||||||
|  |             PortDeclaration::Set(start, end) => write!(f, "{start}*{end}"), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use super::*; | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_port_location_invalid() { | ||||||
|  |         assert!(PortLocation::from_str("1/1").is_err()); | ||||||
|  |         assert!(PortLocation::from_str("1/A/1").is_err()); | ||||||
|  |         assert!(PortLocation::from_str("1/1/256").is_err()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_declaration_single() { | ||||||
|  |         let single_result = PortDeclaration::parse("1/1/4").unwrap(); | ||||||
|  |         assert!(matches!(single_result, PortDeclaration::Single(_))); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_declaration_range() { | ||||||
|  |         let range_result = PortDeclaration::parse("1/1/1-1/1/4").unwrap(); | ||||||
|  |         assert!(matches!(range_result, PortDeclaration::Range(_, _))); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_declaration_set() { | ||||||
|  |         let set_result = PortDeclaration::parse("1/1/48*2/1/48").unwrap(); | ||||||
|  |         assert!(matches!(set_result, PortDeclaration::Set(_, _))); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user