diff --git a/Cargo.lock b/Cargo.lock index 0fd20fb..ba5eb1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1262,6 +1262,22 @@ dependencies = [ "url", ] +[[package]] +name = "brocade-switch-configuration" +version = "0.1.0" +dependencies = [ + "async-trait", + "brocade", + "env_logger", + "harmony", + "harmony_cli", + "harmony_macros", + "harmony_types", + "log", + "serde", + "tokio", +] + [[package]] name = "brotli" version = "8.0.2" @@ -4624,6 +4640,26 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "json-prompt" +version = "0.1.0" +dependencies = [ + "brocade", + "cidr", + "env_logger", + "harmony", + "harmony_cli", + "harmony_macros", + "harmony_secret", + "harmony_secret_derive", + "harmony_types", + "log", + "schemars 0.8.22", + "serde", + "tokio", + "url", +] + [[package]] name = "jsonpath-rust" version = "0.7.5" diff --git a/brocade/examples/env.sh b/brocade/examples/env.sh new file mode 100644 index 0000000..a864d5e --- /dev/null +++ b/brocade/examples/env.sh @@ -0,0 +1,4 @@ +export HARMONY_SECRET_NAMESPACE=brocade-example +export HARMONY_SECRET_STORE=file +export HARMONY_DATABASE_URL=sqlite://harmony_brocade_example.sqlite +export RUST_LOG=info diff --git a/brocade/examples/main.rs b/brocade/examples/main.rs index ca7d533..a26620d 100644 --- a/brocade/examples/main.rs +++ b/brocade/examples/main.rs @@ -1,6 +1,6 @@ use std::net::{IpAddr, Ipv4Addr}; -use brocade::{BrocadeOptions, ssh}; +use brocade::{BrocadeOptions, Vlan, ssh}; use harmony_secret::{Secret, SecretManager}; use harmony_types::switch::PortLocation; use schemars::JsonSchema; @@ -17,9 +17,12 @@ 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(127, 0, 0, 1)); // brocade @ sto1 + // let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); // brocade @ sto1 // let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 4, 11)); // brocade @ st - let switch_addresses = vec![ip]; + //let switch_addresses = vec![ip]; + let ip0 = IpAddr::V4(Ipv4Addr::new(192, 168, 12, 147)); // brocade @ test + let ip1 = IpAddr::V4(Ipv4Addr::new(192, 168, 12, 109)); // brocade @ test + let switch_addresses = vec![ip0, ip1]; let config = SecretManager::get_or_prompt::() .await @@ -32,7 +35,7 @@ async fn main() { &BrocadeOptions { dry_run: true, ssh: ssh::SshOptions { - port: 2222, + port: 22, ..Default::default() }, ..Default::default() @@ -58,18 +61,38 @@ async fn main() { } println!("--------------"); - todo!(); + println!("Creating VLAN 100 (test-vlan)..."); + brocade + .create_vlan(&Vlan { + id: 100, + name: "test-vlan".to_string(), + }) + .await + .unwrap(); + + println!("--------------"); + println!("Deleting VLAN 100..."); + brocade + .delete_vlan(&Vlan { + id: 100, + name: "test-vlan".to_string(), + }) + .await + .unwrap(); + + println!("--------------"); + todo!("STOP!"); let channel_name = "1"; brocade.clear_port_channel(channel_name).await.unwrap(); println!("--------------"); - let channel_id = brocade.find_available_channel_id().await.unwrap(); + let channel_id = 1; println!("--------------"); let channel_name = "HARMONY_LAG"; let ports = [PortLocation(2, 0, 35)]; brocade - .create_port_channel(channel_id, channel_name, &ports) + .create_port_channel(channel_id, channel_name, &ports, None) .await .unwrap(); } diff --git a/brocade/examples/main_vlan_demo.rs b/brocade/examples/main_vlan_demo.rs new file mode 100644 index 0000000..4669479 --- /dev/null +++ b/brocade/examples/main_vlan_demo.rs @@ -0,0 +1,242 @@ +use std::io::{self, Write}; + +use brocade::{ + BrocadeOptions, InterfaceConfig, InterfaceSpeed, InterfaceType, PortOperatingMode, + SwitchInterface, Vlan, VlanList, ssh, +}; +use harmony_secret::{Secret, SecretManager}; +use harmony_types::switch::PortLocation; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Secret, Clone, Debug, JsonSchema, Serialize, Deserialize)] +struct BrocadeSwitchAuth { + username: String, + password: String, +} + +fn wait_for_enter() { + println!("\n--- Press ENTER to continue ---"); + io::stdout().flush().unwrap(); + io::stdin().read_line(&mut String::new()).unwrap(); +} + +#[tokio::main] +async fn main() { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + let ip0 = std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 12, 147)); + let ip1 = std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 12, 109)); + let switch_addresses = vec![ip0, ip1]; + + let config = SecretManager::get_or_prompt::() + .await + .unwrap(); + + let brocade = brocade::init( + &switch_addresses, + &config.username, + &config.password, + &BrocadeOptions { + dry_run: false, + ssh: ssh::SshOptions { + port: 22, + ..Default::default() + }, + ..Default::default() + }, + ) + .await + .expect("Brocade client failed to connect"); + + println!("=== Connecting to Brocade switches ==="); + let version = brocade.version().await.unwrap(); + println!("Version: {version:?}"); + let entries = brocade.get_stack_topology().await.unwrap(); + println!("Stack topology: {entries:#?}"); + + println!("\n=== Creating VLANs 100, 200, 300 ==="); + brocade + .create_vlan(&Vlan { + id: 100, + name: "vlan100".to_string(), + }) + .await + .unwrap(); + println!("Created VLAN 100 (vlan100)"); + brocade + .create_vlan(&Vlan { + id: 200, + name: "vlan200".to_string(), + }) + .await + .unwrap(); + println!("Created VLAN 200 (vlan200)"); + brocade + .create_vlan(&Vlan { + id: 300, + name: "vlan300".to_string(), + }) + .await + .unwrap(); + println!("Created VLAN 300 (vlan300)"); + + println!("\n=== Press ENTER to continue to port configuration tests ---"); + wait_for_enter(); + + println!("\n=== TEST 1: Trunk port (all VLANs, speed 10Gbps) on TenGigabitEthernet 1/0/1 ==="); + println!("Configuring port as trunk with all VLANs and speed 10Gbps..."); + let configs = vec![InterfaceConfig { + interface: SwitchInterface::Ethernet( + InterfaceType::TenGigabitEthernet, + PortLocation(1, 0, 1), + ), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::All), + speed: Some(InterfaceSpeed::Gbps10), + }]; + brocade.configure_interfaces(&configs).await.unwrap(); + println!("Querying interfaces..."); + let interfaces = brocade.get_interfaces().await.unwrap(); + for iface in &interfaces { + if iface.name.contains("1/0/1") { + println!(" {iface:?}"); + } + } + wait_for_enter(); + + println!("\n=== TEST 2: Trunk port (specific VLANs) on TenGigabitEthernet 1/0/2 ==="); + println!("Configuring port as trunk with VLANs 100, 200..."); + let configs = vec![InterfaceConfig { + interface: SwitchInterface::Ethernet( + InterfaceType::TenGigabitEthernet, + PortLocation(1, 0, 2), + ), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::Specific(vec![ + Vlan { + id: 100, + name: "vlan100".to_string(), + }, + Vlan { + id: 200, + name: "vlan200".to_string(), + }, + ])), + speed: None, + }]; + brocade.configure_interfaces(&configs).await.unwrap(); + println!("Querying interfaces..."); + let interfaces = brocade.get_interfaces().await.unwrap(); + for iface in &interfaces { + if iface.name.contains("1/0/2") { + println!(" {iface:?}"); + } + } + wait_for_enter(); + + println!("\n=== TEST 3: Access port (default VLAN 1) on TenGigabitEthernet 1/0/3 ==="); + println!("Configuring port as access (default VLAN 1)..."); + let configs = vec![InterfaceConfig { + interface: SwitchInterface::Ethernet( + InterfaceType::TenGigabitEthernet, + PortLocation(1, 0, 3), + ), + mode: PortOperatingMode::Access, + access_vlan: None, + trunk_vlans: None, + speed: None, + }]; + brocade.configure_interfaces(&configs).await.unwrap(); + println!("Querying interfaces..."); + let interfaces = brocade.get_interfaces().await.unwrap(); + for iface in &interfaces { + if iface.name.contains("1/0/3") { + println!(" {iface:?}"); + } + } + wait_for_enter(); + + println!("\n=== TEST 4: Access port (custom VLAN 100) on TenGigabitEthernet 1/0/4 ==="); + println!("Configuring port as access with VLAN 100..."); + let configs = vec![InterfaceConfig { + interface: SwitchInterface::Ethernet( + InterfaceType::TenGigabitEthernet, + PortLocation(1, 0, 4), + ), + mode: PortOperatingMode::Access, + access_vlan: Some(100), + trunk_vlans: None, + speed: None, + }]; + brocade.configure_interfaces(&configs).await.unwrap(); + println!("Querying interfaces..."); + let interfaces = brocade.get_interfaces().await.unwrap(); + for iface in &interfaces { + if iface.name.contains("1/0/4") { + println!(" {iface:?}"); + } + } + wait_for_enter(); + + println!("\n=== TEST 5: Port-channel on TenGigabitEthernet 1/0/5 and 1/0/6 ==="); + let channel_id = 1; + println!("Using channel ID: {channel_id}"); + println!("Creating port-channel with ports 1/0/5 and 1/0/6..."); + let ports = [PortLocation(1, 0, 5), PortLocation(1, 0, 6)]; + brocade + .create_port_channel(channel_id, "HARMONY_LAG", &ports, None) + .await + .unwrap(); + println!("Port-channel created."); + println!("Querying port-channel summary..."); + let interfaces = brocade.get_interfaces().await.unwrap(); + for iface in &interfaces { + if iface.name.contains("1/0/5") || iface.name.contains("1/0/6") { + println!(" {iface:?}"); + } + } + wait_for_enter(); + + println!("\n=== TEARDOWN: Clearing port-channels and deleting VLANs ==="); + println!("Clearing port-channel {channel_id}..."); + brocade + .clear_port_channel(&channel_id.to_string()) + .await + .unwrap(); + println!("Resetting interfaces..."); + for port in 1..=6 { + let interface = format!("TenGigabitEthernet 1/0/{port}"); + println!(" Resetting {interface}..."); + brocade.reset_interface(&interface).await.unwrap(); + } + + println!("Deleting VLAN 100..."); + brocade + .delete_vlan(&Vlan { + id: 100, + name: "vlan100".to_string(), + }) + .await + .unwrap(); + println!("Deleting VLAN 200..."); + brocade + .delete_vlan(&Vlan { + id: 200, + name: "vlan200".to_string(), + }) + .await + .unwrap(); + println!("Deleting VLAN 300..."); + brocade + .delete_vlan(&Vlan { + id: 300, + name: "vlan300".to_string(), + }) + .await + .unwrap(); + + println!("\n=== DONE ==="); +} diff --git a/brocade/src/fast_iron.rs b/brocade/src/fast_iron.rs index 371c265..963101e 100644 --- a/brocade/src/fast_iron.rs +++ b/brocade/src/fast_iron.rs @@ -1,7 +1,8 @@ use super::BrocadeClient; use crate::{ - BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo, MacAddressEntry, - PortChannelId, PortOperatingMode, parse_brocade_mac_address, shell::BrocadeShell, + BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceConfig, InterfaceInfo, + InterfaceSpeed, MacAddressEntry, PortChannelId, PortOperatingMode, Vlan, + parse_brocade_mac_address, shell::BrocadeShell, }; use async_trait::async_trait; @@ -138,10 +139,15 @@ impl BrocadeClient for FastIronClient { todo!() } - async fn configure_interfaces( - &self, - _interfaces: &Vec<(String, PortOperatingMode)>, - ) -> Result<(), Error> { + async fn configure_interfaces(&self, _interfaces: &Vec) -> Result<(), Error> { + todo!() + } + + async fn create_vlan(&self, _vlan: &Vlan) -> Result<(), Error> { + todo!() + } + + async fn delete_vlan(&self, _vlan: &Vlan) -> Result<(), Error> { todo!() } @@ -180,11 +186,18 @@ impl BrocadeClient for FastIronClient { channel_id: PortChannelId, channel_name: &str, ports: &[PortLocation], + speed: Option<&InterfaceSpeed>, ) -> Result<(), Error> { info!( "[Brocade] Configuring port-channel '{channel_name} {channel_id}' with ports: {ports:?}" ); + if let Some(speed) = speed { + log::warn!( + "[Brocade] FastIron: speed override ({speed}) on port-channel is not yet implemented; ignoring" + ); + } + let commands = self.build_port_channel_commands(channel_id, channel_name, ports); self.shell .run_commands(commands, ExecutionMode::Privileged) @@ -194,6 +207,25 @@ impl BrocadeClient for FastIronClient { Ok(()) } + async fn reset_interface(&self, interface: &str) -> Result<(), Error> { + info!("[Brocade] Resetting interface: {interface}"); + + let commands = vec![ + "configure terminal".into(), + format!("interface {interface}"), + "no switchport".into(), + "no speed".into(), + "exit".into(), + ]; + + self.shell + .run_commands(commands, ExecutionMode::Privileged) + .await?; + + info!("[Brocade] Interface '{interface}' reset."); + Ok(()) + } + async fn clear_port_channel(&self, channel_name: &str) -> Result<(), Error> { info!("[Brocade] Clearing port-channel: {channel_name}"); diff --git a/brocade/src/lib.rs b/brocade/src/lib.rs index 5fc3ac9..8a5cb74 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -76,6 +76,74 @@ pub struct MacAddressEntry { pub type PortChannelId = u8; +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Vlan { + pub id: u16, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum VlanList { + All, + Specific(Vec), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum SwitchInterface { + Ethernet(InterfaceType, PortLocation), + PortChannel(PortChannelId), +} + +impl fmt::Display for SwitchInterface { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SwitchInterface::Ethernet(itype, loc) => write!(f, "{itype} {loc}"), + SwitchInterface::PortChannel(id) => write!(f, "port-channel {id}"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum InterfaceSpeed { + Mbps100, + Gbps1, + Gbps1Auto, + Gbps10, + Auto, +} + +impl fmt::Display for InterfaceSpeed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InterfaceSpeed::Mbps100 => write!(f, "100"), + InterfaceSpeed::Gbps1 => write!(f, "1000"), + InterfaceSpeed::Gbps1Auto => write!(f, "1000-auto"), + InterfaceSpeed::Gbps10 => write!(f, "10000"), + InterfaceSpeed::Auto => write!(f, "auto"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct InterfaceConfig { + pub interface: SwitchInterface, + pub mode: PortOperatingMode, + pub access_vlan: Option, + pub trunk_vlans: Option, + pub speed: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct PortChannelConfig { + pub id: PortChannelId, + pub name: String, + pub ports: Vec, + pub mode: PortOperatingMode, + pub access_vlan: Option, + pub trunk_vlans: Option, + pub speed: Option, +} + /// 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 @@ -104,16 +172,17 @@ pub struct InterfaceInfo { } /// Categorizes the functional type of a switch interface. -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize)] pub enum InterfaceType { - /// Physical or virtual Ethernet interface (e.g., TenGigabitEthernet, FortyGigabitEthernet). - Ethernet(String), + TenGigabitEthernet, + FortyGigabitEthernet, } impl fmt::Display for InterfaceType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - InterfaceType::Ethernet(name) => write!(f, "{name}"), + InterfaceType::TenGigabitEthernet => write!(f, "TenGigabitEthernet"), + InterfaceType::FortyGigabitEthernet => write!(f, "FortyGigabitEthernet"), } } } @@ -206,10 +275,13 @@ pub trait BrocadeClient: std::fmt::Debug { async fn get_interfaces(&self) -> Result, 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>; + async fn configure_interfaces(&self, interfaces: &Vec) -> Result<(), Error>; + + /// Creates a new VLAN on the switch. + async fn create_vlan(&self, vlan: &Vlan) -> Result<(), Error>; + + /// Deletes a VLAN from the switch. + async fn delete_vlan(&self, vlan: &Vlan) -> Result<(), Error>; /// Scans the existing configuration to find the next available (unused) /// Port-Channel ID (`lag` or `trunk`) for assignment. @@ -230,11 +302,16 @@ pub trait BrocadeClient: std::fmt::Debug { /// * `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. + /// * `speed`: Optional speed override applied to both the logical port-channel + /// interface and each member port. Required on Brocade when forcing a + /// non-default speed (e.g. 1G on 10G-capable ports), otherwise the LAG + /// members and the logical interface end up inconsistent. async fn create_port_channel( &self, channel_id: PortChannelId, channel_name: &str, ports: &[PortLocation], + speed: Option<&InterfaceSpeed>, ) -> Result<(), Error>; /// Enables Simple Network Management Protocol (SNMP) server for switch @@ -246,6 +323,9 @@ pub trait BrocadeClient: std::fmt::Debug { /// * `des`: The Data Encryption Standard algorithm key async fn enable_snmp(&self, user_name: &str, auth: &str, des: &str) -> Result<(), Error>; + /// Resets an interface to its default state by removing switchport configuration. + async fn reset_interface(&self, interface: &str) -> Result<(), Error>; + /// Removes all configuration associated with the specified Port-Channel name. /// /// This operation should be idempotent; attempting to clear a non-existent diff --git a/brocade/src/network_operating_system.rs b/brocade/src/network_operating_system.rs index fa99bf6..e5f1724 100644 --- a/brocade/src/network_operating_system.rs +++ b/brocade/src/network_operating_system.rs @@ -6,9 +6,10 @@ use log::{debug, info}; use regex::Regex; use crate::{ - BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo, - InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, PortOperatingMode, - parse_brocade_mac_address, shell::BrocadeShell, + BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceConfig, + InterfaceInfo, InterfaceSpeed, InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, + PortOperatingMode, SwitchInterface, Vlan, VlanList, parse_brocade_mac_address, + shell::BrocadeShell, }; #[derive(Debug)] @@ -84,8 +85,8 @@ impl NetworkOperatingSystemClient { } let interface_type = match parts[0] { - "Fo" => InterfaceType::Ethernet("FortyGigabitEthernet".to_string()), - "Te" => InterfaceType::Ethernet("TenGigabitEthernet".to_string()), + "Fo" => InterfaceType::FortyGigabitEthernet, + "Te" => InterfaceType::TenGigabitEthernet, _ => return None, }; let port_location = PortLocation::from_str(parts[1]).ok()?; @@ -185,18 +186,20 @@ impl BrocadeClient for NetworkOperatingSystemClient { .collect() } - async fn configure_interfaces( - &self, - interfaces: &Vec<(String, PortOperatingMode)>, - ) -> Result<(), Error> { + async fn configure_interfaces(&self, interfaces: &Vec) -> 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)); + debug!( + "[Brocade] Configuring interface {} as {:?}", + interface.interface, interface.mode + ); - match interface.1 { + commands.push(format!("interface {}", interface.interface)); + + match interface.mode { PortOperatingMode::Fabric => { commands.push("fabric isl enable".into()); commands.push("fabric trunk enable".into()); @@ -204,23 +207,50 @@ impl BrocadeClient for NetworkOperatingSystemClient { PortOperatingMode::Trunk => { commands.push("switchport".into()); commands.push("switchport mode trunk".into()); - commands.push("switchport trunk allowed vlan all".into()); + match &interface.trunk_vlans { + Some(VlanList::All) => { + commands.push("switchport trunk allowed vlan all".into()); + } + Some(VlanList::Specific(vlans)) => { + for vlan in vlans { + commands.push(format!("switchport trunk allowed vlan add {}", vlan.id)); + } + } + None => { + commands.push("switchport trunk allowed vlan all".into()); + } + } commands.push("no switchport trunk tag native-vlan".into()); - commands.push("spanning-tree shutdown".into()); - commands.push("no fabric isl enable".into()); - commands.push("no fabric trunk enable".into()); - commands.push("no shutdown".into()); + if matches!(interface.interface, SwitchInterface::Ethernet(..)) { + commands.push("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()); + let access_vlan = interface.access_vlan.unwrap_or(1); + commands.push(format!("switchport access vlan {access_vlan}")); + if matches!(interface.interface, SwitchInterface::Ethernet(..)) { + commands.push("no spanning-tree shutdown".into()); + commands.push("no fabric isl enable".into()); + commands.push("no fabric trunk enable".into()); + } } } + if let Some(speed) = &interface.speed { + info!( + "[Brocade] Overriding speed on {} to {speed}", + interface.interface + ); + if matches!(interface.interface, SwitchInterface::PortChannel(..)) { + commands.push("shutdown".into()); + } + commands.push(format!("speed {speed}")); + } + commands.push("no shutdown".into()); commands.push("exit".into()); } @@ -235,6 +265,40 @@ impl BrocadeClient for NetworkOperatingSystemClient { Ok(()) } + async fn create_vlan(&self, vlan: &Vlan) -> Result<(), Error> { + info!("[Brocade] Creating VLAN {} ({})", vlan.id, vlan.name); + + let commands = vec![ + "configure terminal".into(), + format!("interface Vlan {}", vlan.id), + format!("name {}", vlan.name), + "exit".into(), + ]; + + self.shell + .run_commands(commands, ExecutionMode::Regular) + .await?; + + info!("[Brocade] VLAN {} ({}) created.", vlan.id, vlan.name); + Ok(()) + } + + async fn delete_vlan(&self, vlan: &Vlan) -> Result<(), Error> { + info!("[Brocade] Deleting VLAN {}", vlan.id); + + let commands = vec![ + "configure terminal".into(), + format!("no interface Vlan {}", vlan.id), + ]; + + self.shell + .run_commands(commands, ExecutionMode::Regular) + .await?; + + info!("[Brocade] VLAN {} deleted.", vlan.id); + Ok(()) + } + async fn find_available_channel_id(&self) -> Result { info!("[Brocade] Finding next available channel id..."); @@ -273,6 +337,7 @@ impl BrocadeClient for NetworkOperatingSystemClient { channel_id: PortChannelId, channel_name: &str, ports: &[PortLocation], + speed: Option<&InterfaceSpeed>, ) -> Result<(), Error> { info!( "[Brocade] Configuring port-channel '{channel_id} {channel_name}' with ports: {}", @@ -283,27 +348,34 @@ impl BrocadeClient for NetworkOperatingSystemClient { .join(", ") ); - let interfaces = self.get_interfaces().await?; - let mut commands = vec![ "configure terminal".into(), format!("interface port-channel {}", channel_id), "no shutdown".into(), - "exit".into(), + format!("description {channel_name}"), ]; + if let Some(speed) = speed { + commands.push("shutdown".into()); + commands.push(format!("speed {speed}")); + commands.push("no shutdown".into()); + } + commands.push("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)); + debug!( + "[Brocade] Adding port TenGigabitEthernet {} to channel-group {}", + port, channel_id + ); + commands.push(format!("interface TenGigabitEthernet {}", port)); 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 {channel_id} mode active")); + commands.push("lacp timeout short".into()); + if let Some(speed) = speed { + commands.push(format!("speed {speed}")); + } commands.push("no shutdown".into()); commands.push("exit".into()); } @@ -317,6 +389,25 @@ impl BrocadeClient for NetworkOperatingSystemClient { Ok(()) } + async fn reset_interface(&self, interface: &str) -> Result<(), Error> { + info!("[Brocade] Resetting interface: {interface}"); + + let commands = vec![ + "configure terminal".into(), + format!("interface {interface}"), + "no switchport".into(), + "no speed".into(), + "exit".into(), + ]; + + self.shell + .run_commands(commands, ExecutionMode::Regular) + .await?; + + info!("[Brocade] Interface '{interface}' reset."); + Ok(()) + } + async fn clear_port_channel(&self, channel_name: &str) -> Result<(), Error> { info!("[Brocade] Clearing port-channel: {channel_name}"); diff --git a/build/check.sh b/build/check.sh index de8d6b4..751a71c 100755 --- a/build/check.sh +++ b/build/check.sh @@ -3,6 +3,9 @@ set -e cd "$(dirname "$0")/.." +git submodule init +git submodule update + rustc --version cargo check --all-targets --all-features --keep-going cargo fmt --check diff --git a/examples/brocade_switch/src/main.rs b/examples/brocade_switch/src/main.rs index 8d3da30..496cdbe 100644 --- a/examples/brocade_switch/src/main.rs +++ b/examples/brocade_switch/src/main.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use brocade::{BrocadeOptions, PortOperatingMode}; +use brocade::{BrocadeOptions, InterfaceConfig, InterfaceType, PortOperatingMode, SwitchInterface, VlanList}; use harmony::{ infra::brocade::BrocadeSwitchConfig, inventory::Inventory, @@ -9,6 +9,13 @@ use harmony::{ use harmony_macros::ip; use harmony_types::{id::Id, switch::PortLocation}; +fn tengig(stack: u8, slot: u8, port: u8) -> SwitchInterface { + SwitchInterface::Ethernet( + InterfaceType::TenGigabitEthernet, + PortLocation(stack, slot, port), + ) +} + fn get_switch_config() -> BrocadeSwitchConfig { let mut options = BrocadeOptions::default(); options.ssh.port = 2222; @@ -33,9 +40,27 @@ async fn main() { Id::from_str("18").unwrap(), ], ports_to_configure: vec![ - (PortLocation(2, 0, 17), PortOperatingMode::Trunk), - (PortLocation(2, 0, 19), PortOperatingMode::Trunk), - (PortLocation(1, 0, 18), PortOperatingMode::Trunk), + InterfaceConfig { + interface: tengig(2, 0, 17), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::All), + speed: None, + }, + InterfaceConfig { + interface: tengig(2, 0, 19), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::All), + speed: None, + }, + InterfaceConfig { + interface: tengig(1, 0, 18), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::All), + speed: None, + }, ], }; diff --git a/examples/brocade_switch_configuration/Cargo.toml b/examples/brocade_switch_configuration/Cargo.toml new file mode 100644 index 0000000..715a55b --- /dev/null +++ b/examples/brocade_switch_configuration/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "brocade-switch-configuration" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies] +harmony = { path = "../../harmony" } +harmony_cli = { path = "../../harmony_cli" } +harmony_macros = { path = "../../harmony_macros" } +harmony_types = { path = "../../harmony_types" } +tokio.workspace = true +async-trait.workspace = true +serde.workspace = true +log.workspace = true +env_logger.workspace = true +brocade = { path = "../../brocade" } diff --git a/examples/brocade_switch_configuration/env.sh b/examples/brocade_switch_configuration/env.sh new file mode 100644 index 0000000..a864d5e --- /dev/null +++ b/examples/brocade_switch_configuration/env.sh @@ -0,0 +1,4 @@ +export HARMONY_SECRET_NAMESPACE=brocade-example +export HARMONY_SECRET_STORE=file +export HARMONY_DATABASE_URL=sqlite://harmony_brocade_example.sqlite +export RUST_LOG=info diff --git a/examples/brocade_switch_configuration/src/main.rs b/examples/brocade_switch_configuration/src/main.rs new file mode 100644 index 0000000..a5a037c --- /dev/null +++ b/examples/brocade_switch_configuration/src/main.rs @@ -0,0 +1,144 @@ +use brocade::{ + BrocadeOptions, InterfaceConfig, InterfaceSpeed, InterfaceType, PortChannelConfig, + PortOperatingMode, SwitchInterface, Vlan, VlanList, +}; +use harmony::{ + infra::brocade::BrocadeSwitchConfig, + inventory::Inventory, + modules::brocade::{BrocadeSwitchAuth, BrocadeSwitchConfigurationScore, SwitchTopology}, +}; +use harmony_macros::ip; +use harmony_types::switch::PortLocation; + +fn tengig(stack: u8, slot: u8, port: u8) -> SwitchInterface { + SwitchInterface::Ethernet( + InterfaceType::TenGigabitEthernet, + PortLocation(stack, slot, port), + ) +} + +fn get_switch_config() -> BrocadeSwitchConfig { + let auth = BrocadeSwitchAuth { + username: "admin".to_string(), + password: "password".to_string(), + }; + + BrocadeSwitchConfig { + // ips: vec![ip!("192.168.12.147"), ip!("192.168.12.109")], + ips: vec![ip!("192.168.4.12"), ip!("192.168.4.11")], + auth, + options: BrocadeOptions { + dry_run: false, + ssh: brocade::ssh::SshOptions { + port: 22, + ..Default::default() + }, + ..Default::default() + }, + } +} + +#[tokio::main] +async fn main() { + harmony_cli::cli_logger::init(); + + // =================================================== + // Step 1: Define VLANs once, use them everywhere + // =================================================== + let mgmt = Vlan { + id: 100, + name: "MGMT".to_string(), + }; + let data = Vlan { + id: 200, + name: "DATA".to_string(), + }; + let storage = Vlan { + id: 300, + name: "STORAGE".to_string(), + }; + let backup = Vlan { + id: 400, + name: "BACKUP".to_string(), + }; + + // =================================================== + // Step 2: Build the score + // =================================================== + let score = BrocadeSwitchConfigurationScore { + // All VLANs that need to exist on the switch + vlans: vec![mgmt.clone(), data.clone(), storage.clone(), backup.clone()], + + // Standalone interfaces (not part of any port-channel) + interfaces: vec![ + // Trunk port with ALL VLANs, forced to 10Gbps + InterfaceConfig { + interface: tengig(1, 0, 20), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::All), + speed: Some(InterfaceSpeed::Gbps10), + }, + // Trunk port with specific VLANs (MGMT + DATA only) + InterfaceConfig { + interface: tengig(1, 0, 21), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::Specific(vec![mgmt.clone(), data.clone()])), + speed: None, + }, + // Access port on the MGMT VLAN + InterfaceConfig { + interface: tengig(1, 0, 22), + mode: PortOperatingMode::Access, + access_vlan: Some(mgmt.id), + trunk_vlans: None, + speed: None, + }, + // Access port on the STORAGE VLAN + InterfaceConfig { + interface: tengig(1, 0, 23), + mode: PortOperatingMode::Access, + access_vlan: Some(storage.id), + trunk_vlans: None, + speed: None, + }, + ], + + // Port-channels: member ports are bundled, L2 config goes on the port-channel + port_channels: vec![ + // Port-channel 1: trunk with DATA + STORAGE VLANs, forced to 1Gbps + PortChannelConfig { + id: 1, + name: "SERVER_BOND".to_string(), + ports: vec![PortLocation(1, 0, 24), PortLocation(1, 0, 25)], + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::Specific(vec![data.clone(), storage.clone()])), + speed: Some(InterfaceSpeed::Gbps1), + }, + // Port-channel 2: trunk with all VLANs, default speed + PortChannelConfig { + id: 2, + name: "BACKUP_BOND".to_string(), + ports: vec![PortLocation(1, 0, 26), PortLocation(1, 0, 27)], + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::All), + speed: None, + }, + ], + }; + + // =================================================== + // Step 3: Run + // =================================================== + harmony_cli::run( + Inventory::autoload(), + SwitchTopology::new(get_switch_config()).await, + vec![Box::new(score)], + None, + ) + .await + .unwrap(); +} diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index e68e274..ffd42ea 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use brocade::{InterfaceConfig, InterfaceSpeed, PortChannelConfig, PortChannelId, Vlan}; use harmony_k8s::K8sClient; use harmony_macros::ip; use harmony_types::{ @@ -11,7 +12,7 @@ use log::info; use crate::topology::{HelmCommand, PxeOptions}; use crate::{data::FileContent, executors::ExecutorError, topology::node_exporter::NodeExporter}; -use crate::{infra::network_manager::OpenShiftNmStateNetworkManager, topology::PortConfig}; +use crate::infra::network_manager::OpenShiftNmStateNetworkManager; use super::{ DHCPStaticEntry, DhcpServer, DnsRecord, DnsRecordType, DnsServer, Firewall, HostNetworkConfig, @@ -316,23 +317,57 @@ impl Switch for HAClusterTopology { self.switch_client.find_port(mac_address).await } - async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> { + async fn configure_port_channel( + &self, + channel_id: PortChannelId, + config: &HostNetworkConfig, + ) -> Result<(), SwitchError> { debug!("Configuring port channel: {config:#?}"); let switch_ports = config.switch_ports.iter().map(|s| s.port.clone()).collect(); self.switch_client - .configure_port_channel(&format!("Harmony_{}", config.host_id), switch_ports) + .configure_port_channel( + channel_id, + &format!("Harmony_{}", config.host_id), + switch_ports, + None, + ) .await .map_err(|e| SwitchError::new(format!("Failed to configure port-channel: {e}")))?; Ok(()) } + async fn configure_port_channel_from_config( + &self, + config: &PortChannelConfig, + ) -> Result<(), SwitchError> { + self.switch_client + .configure_port_channel( + config.id, + &config.name, + config.ports.clone(), + config.speed.as_ref(), + ) + .await + .map_err(|e| SwitchError::new(format!("Failed to create port-channel: {e}")))?; + Ok(()) + } + async fn clear_port_channel(&self, _ids: &Vec) -> Result<(), SwitchError> { todo!() } - async fn configure_interface(&self, _ports: &Vec) -> Result<(), SwitchError> { - todo!() + async fn configure_interfaces( + &self, + interfaces: &Vec, + ) -> Result<(), SwitchError> { + self.switch_client.configure_interfaces(interfaces).await + } + async fn create_vlan(&self, vlan: &Vlan) -> Result<(), SwitchError> { + self.switch_client.create_vlan(vlan).await + } + async fn delete_vlan(&self, vlan: &Vlan) -> Result<(), SwitchError> { + self.switch_client.delete_vlan(vlan).await } } @@ -592,15 +627,26 @@ impl SwitchClient for DummyInfra { async fn configure_port_channel( &self, + _channel_id: PortChannelId, _channel_name: &str, _switch_ports: Vec, + _speed: Option<&InterfaceSpeed>, ) -> Result { unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } async fn clear_port_channel(&self, _ids: &Vec) -> Result<(), SwitchError> { todo!() } - async fn configure_interface(&self, _ports: &Vec) -> Result<(), SwitchError> { + async fn configure_interfaces( + &self, + _interfaces: &Vec, + ) -> Result<(), SwitchError> { + todo!() + } + async fn create_vlan(&self, _vlan: &Vlan) -> Result<(), SwitchError> { + todo!() + } + async fn delete_vlan(&self, _vlan: &Vlan) -> Result<(), SwitchError> { todo!() } } diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index b80b3e0..433fbfc 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -7,7 +7,7 @@ use std::{ }; use async_trait::async_trait; -use brocade::PortOperatingMode; +use brocade::{InterfaceConfig, InterfaceSpeed, PortChannelConfig, PortChannelId, Vlan}; use derive_new::new; use harmony_k8s::K8sClient; use harmony_types::{ @@ -220,8 +220,6 @@ impl From for NetworkError { } } -pub type PortConfig = (PortLocation, PortOperatingMode); - #[async_trait] pub trait Switch: Send + Sync { async fn setup_switch(&self) -> Result<(), SwitchError>; @@ -231,9 +229,24 @@ pub trait Switch: Send + Sync { mac_address: &MacAddress, ) -> Result, SwitchError>; - async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError>; + async fn configure_port_channel( + &self, + channel_id: PortChannelId, + config: &HostNetworkConfig, + ) -> Result<(), SwitchError>; + /// Creates a port-channel from a PortChannelConfig (id, name, member ports). + /// Does NOT configure L2 mode — use configure_interfaces for that. + async fn configure_port_channel_from_config( + &self, + config: &PortChannelConfig, + ) -> Result<(), SwitchError>; async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError>; - async fn configure_interface(&self, ports: &Vec) -> Result<(), SwitchError>; + async fn configure_interfaces( + &self, + interfaces: &Vec, + ) -> Result<(), SwitchError>; + async fn create_vlan(&self, vlan: &Vlan) -> Result<(), SwitchError>; + async fn delete_vlan(&self, vlan: &Vlan) -> Result<(), SwitchError>; } #[derive(Clone, Debug, PartialEq)] @@ -290,12 +303,19 @@ pub trait SwitchClient: Debug + Send + Sync { async fn configure_port_channel( &self, + channel_id: PortChannelId, channel_name: &str, switch_ports: Vec, + speed: Option<&InterfaceSpeed>, ) -> Result; async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError>; - async fn configure_interface(&self, ports: &Vec) -> Result<(), SwitchError>; + async fn configure_interfaces( + &self, + interfaces: &Vec, + ) -> Result<(), SwitchError>; + async fn create_vlan(&self, vlan: &Vlan) -> Result<(), SwitchError>; + async fn delete_vlan(&self, vlan: &Vlan) -> Result<(), SwitchError>; } #[cfg(test)] diff --git a/harmony/src/infra/brocade.rs b/harmony/src/infra/brocade.rs index 8ba9b0d..fa89b92 100644 --- a/harmony/src/infra/brocade.rs +++ b/harmony/src/infra/brocade.rs @@ -1,16 +1,20 @@ use async_trait::async_trait; -use brocade::{BrocadeClient, BrocadeOptions, InterSwitchLink, InterfaceStatus, PortOperatingMode}; +use brocade::{ + BrocadeClient, BrocadeOptions, InterSwitchLink, InterfaceConfig, InterfaceSpeed, + InterfaceStatus, PortChannelId, PortOperatingMode, Vlan, +}; + use harmony_types::{ id::Id, net::{IpAddress, MacAddress}, switch::{PortDeclaration, PortLocation}, }; -use log::{info, warn}; +use log::info; use option_ext::OptionExt; use crate::{ modules::brocade::BrocadeSwitchAuth, - topology::{PortConfig, SwitchClient, SwitchError}, + topology::{SwitchClient, SwitchError}, }; #[derive(Debug, Clone)] @@ -54,7 +58,7 @@ impl SwitchClient for BrocadeSwitchClient { info!("Brocade found interfaces {interfaces:#?}"); - let interfaces: Vec<(String, PortOperatingMode)> = interfaces + let interfaces: Vec = interfaces .into_iter() .filter(|interface| { interface.operating_mode.is_none() && interface.status == InterfaceStatus::Connected @@ -65,7 +69,16 @@ impl SwitchClient for BrocadeSwitchClient { || link.remote_port.contains(&interface.port_location) }) }) - .map(|interface| (interface.name.clone(), PortOperatingMode::Trunk)) + .map(|interface| InterfaceConfig { + interface: brocade::SwitchInterface::Ethernet( + interface.interface_type.clone(), + interface.port_location.clone(), + ), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: None, + speed: None, + }) .collect(); if interfaces.is_empty() { @@ -114,50 +127,19 @@ impl SwitchClient for BrocadeSwitchClient { async fn configure_port_channel( &self, + channel_id: PortChannelId, channel_name: &str, switch_ports: Vec, + speed: Option<&InterfaceSpeed>, ) -> Result { - let mut channel_id = self - .brocade - .find_available_channel_id() + self.brocade + .create_port_channel(channel_id, channel_name, &switch_ports, speed) .await .map_err(|e| SwitchError::new(format!("{e}")))?; - info!("Found next available channel id : {channel_id}"); - - loop { - match self - .brocade - .create_port_channel(channel_id, channel_name, &switch_ports) - .await - .map_err(|e| SwitchError::new(format!("{e}"))) - { - Ok(_) => { - info!( - "Successfully configured port channel {channel_id} {channel_name} for ports {switch_ports:?}" - ); - break; - } - Err(e) => { - warn!( - "Could not configure port channel {channel_id} {channel_name} for ports {switch_ports:?}" - ); - let previous_id = channel_id; - - while previous_id == channel_id { - channel_id = inquire::Text::new( - "Type the port channel number to use (or CTRL+C to exit) :", - ) - .prompt() - .map_err(|e| { - SwitchError::new(format!("Failed to prompt for channel id : {e}")) - })? - .parse() - .unwrap_or(channel_id); - } - } - } - } + info!( + "Successfully configured port channel {channel_id} {channel_name} for ports {switch_ports:?}" + ); Ok(channel_id) } @@ -170,14 +152,28 @@ impl SwitchClient for BrocadeSwitchClient { } Ok(()) } - async fn configure_interface(&self, ports: &Vec) -> Result<(), SwitchError> { - // FIXME hardcoded TenGigabitEthernet = bad - let ports = ports - .iter() - .map(|p| (format!("TenGigabitEthernet {}", p.0), p.1.clone())) - .collect(); + async fn configure_interfaces( + &self, + interfaces: &Vec, + ) -> Result<(), SwitchError> { self.brocade - .configure_interfaces(&ports) + .configure_interfaces(interfaces) + .await + .map_err(|e| SwitchError::new(e.to_string()))?; + Ok(()) + } + + async fn create_vlan(&self, vlan: &Vlan) -> Result<(), SwitchError> { + self.brocade + .create_vlan(vlan) + .await + .map_err(|e| SwitchError::new(e.to_string()))?; + Ok(()) + } + + async fn delete_vlan(&self, vlan: &Vlan) -> Result<(), SwitchError> { + self.brocade + .delete_vlan(vlan) .await .map_err(|e| SwitchError::new(e.to_string()))?; Ok(()) @@ -208,8 +204,10 @@ impl SwitchClient for UnmanagedSwitch { async fn configure_port_channel( &self, - channel_name: &str, - switch_ports: Vec, + _channel_id: PortChannelId, + _channel_name: &str, + _switch_ports: Vec, + _speed: Option<&InterfaceSpeed>, ) -> Result { todo!("unmanaged switch. Nothing to do.") } @@ -217,8 +215,19 @@ impl SwitchClient for UnmanagedSwitch { async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError> { todo!("unmanged switch. Nothing to do.") } - async fn configure_interface(&self, ports: &Vec) -> Result<(), SwitchError> { - todo!("unmanged switch. Nothing to do.") + async fn configure_interfaces( + &self, + _interfaces: &Vec, + ) -> Result<(), SwitchError> { + todo!("unmanaged switch. Nothing to do.") + } + + async fn create_vlan(&self, _vlan: &Vlan) -> Result<(), SwitchError> { + todo!("unmanaged switch. Nothing to do.") + } + + async fn delete_vlan(&self, _vlan: &Vlan) -> Result<(), SwitchError> { + todo!("unmanaged switch. Nothing to do.") } } @@ -229,8 +238,9 @@ mod tests { use assertor::*; use async_trait::async_trait; use brocade::{ - BrocadeClient, BrocadeInfo, Error, InterSwitchLink, InterfaceInfo, InterfaceStatus, - InterfaceType, MacAddressEntry, PortChannelId, PortOperatingMode, SecurityLevel, + BrocadeClient, BrocadeInfo, Error, InterSwitchLink, InterfaceConfig, InterfaceInfo, + InterfaceSpeed, InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, + PortOperatingMode, SecurityLevel, Vlan, }; use harmony_types::switch::PortLocation; @@ -258,8 +268,26 @@ mod tests { //TODO not sure about this let configured_interfaces = brocade.configured_interfaces.lock().unwrap(); assert_that!(*configured_interfaces).contains_exactly(vec![ - (first_interface.name.clone(), PortOperatingMode::Trunk), - (second_interface.name.clone(), PortOperatingMode::Trunk), + InterfaceConfig { + interface: brocade::SwitchInterface::Ethernet( + InterfaceType::TenGigabitEthernet, + PortLocation(1, 0, 1), + ), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: None, + speed: None, + }, + InterfaceConfig { + interface: brocade::SwitchInterface::Ethernet( + InterfaceType::TenGigabitEthernet, + PortLocation(1, 0, 4), + ), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: None, + speed: None, + }, ]); } @@ -343,7 +371,7 @@ mod tests { struct FakeBrocadeClient { stack_topology: Vec, interfaces: Vec, - configured_interfaces: Arc>>, + configured_interfaces: Arc>>, } #[async_trait] @@ -366,7 +394,7 @@ mod tests { async fn configure_interfaces( &self, - interfaces: &Vec<(String, PortOperatingMode)>, + interfaces: &Vec, ) -> Result<(), Error> { let mut configured_interfaces = self.configured_interfaces.lock().unwrap(); *configured_interfaces = interfaces.clone(); @@ -374,6 +402,14 @@ mod tests { Ok(()) } + async fn create_vlan(&self, _vlan: &Vlan) -> Result<(), Error> { + todo!() + } + + async fn delete_vlan(&self, _vlan: &Vlan) -> Result<(), Error> { + todo!() + } + async fn find_available_channel_id(&self) -> Result { todo!() } @@ -383,10 +419,15 @@ mod tests { _channel_id: PortChannelId, _channel_name: &str, _ports: &[PortLocation], + _speed: Option<&InterfaceSpeed>, ) -> Result<(), Error> { todo!() } + async fn reset_interface(&self, _interface: &str) -> Result<(), Error> { + todo!() + } + async fn clear_port_channel(&self, _channel_name: &str) -> Result<(), Error> { todo!() } @@ -418,7 +459,7 @@ mod tests { let interface_type = self .interface_type .clone() - .unwrap_or(InterfaceType::Ethernet("TenGigabitEthernet".into())); + .unwrap_or(InterfaceType::TenGigabitEthernet); let port_location = self.port_location.clone().unwrap_or(PortLocation(1, 0, 1)); let name = format!("{interface_type} {port_location}"); let status = self.status.clone().unwrap_or(InterfaceStatus::Connected); diff --git a/harmony/src/modules/brocade/brocade.rs b/harmony/src/modules/brocade/brocade.rs index 67e94e5..5a4ac9c 100644 --- a/harmony/src/modules/brocade/brocade.rs +++ b/harmony/src/modules/brocade/brocade.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use brocade::{BrocadeOptions, PortOperatingMode}; +use brocade::{BrocadeOptions, InterfaceConfig, PortChannelConfig, PortChannelId, PortOperatingMode, Vlan}; use crate::{ data::Version, @@ -8,7 +8,7 @@ use crate::{ inventory::Inventory, score::Score, topology::{ - HostNetworkConfig, PortConfig, PreparationError, PreparationOutcome, Switch, SwitchClient, + HostNetworkConfig, PreparationError, PreparationOutcome, Switch, SwitchClient, SwitchError, Topology, }, }; @@ -20,7 +20,7 @@ use serde::Serialize; #[derive(Clone, Debug, Serialize)] pub struct BrocadeSwitchScore { pub port_channels_to_clear: Vec, - pub ports_to_configure: Vec, + pub ports_to_configure: Vec, } impl Score for BrocadeSwitchScore { @@ -59,7 +59,7 @@ impl Interpret for BrocadeSwitchInterpret { .map_err(|e| InterpretError::new(e.to_string()))?; debug!("Configuring interfaces {:?}", self.score.ports_to_configure); topology - .configure_interface(&self.score.ports_to_configure) + .configure_interfaces(&self.score.ports_to_configure) .await .map_err(|e| InterpretError::new(e.to_string()))?; Ok(Outcome::success("switch configured".to_string())) @@ -126,13 +126,43 @@ impl Switch for SwitchTopology { todo!() } - async fn configure_port_channel(&self, _config: &HostNetworkConfig) -> Result<(), SwitchError> { + async fn configure_port_channel( + &self, + _channel_id: PortChannelId, + _config: &HostNetworkConfig, + ) -> Result<(), SwitchError> { todo!() } + async fn configure_port_channel_from_config( + &self, + config: &PortChannelConfig, + ) -> Result<(), SwitchError> { + self.client + .configure_port_channel( + config.id, + &config.name, + config.ports.clone(), + config.speed.as_ref(), + ) + .await + .map_err(|e| SwitchError::new(format!("Failed to create port-channel: {e}")))?; + Ok(()) + } async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError> { self.client.clear_port_channel(ids).await } - async fn configure_interface(&self, ports: &Vec) -> Result<(), SwitchError> { - self.client.configure_interface(ports).await + async fn configure_interfaces( + &self, + interfaces: &Vec, + ) -> Result<(), SwitchError> { + self.client.configure_interfaces(interfaces).await + } + + async fn create_vlan(&self, vlan: &Vlan) -> Result<(), SwitchError> { + self.client.create_vlan(vlan).await + } + + async fn delete_vlan(&self, vlan: &Vlan) -> Result<(), SwitchError> { + self.client.delete_vlan(vlan).await } } diff --git a/harmony/src/modules/brocade/brocade_switch_configuration.rs b/harmony/src/modules/brocade/brocade_switch_configuration.rs new file mode 100644 index 0000000..684bd3d --- /dev/null +++ b/harmony/src/modules/brocade/brocade_switch_configuration.rs @@ -0,0 +1,179 @@ +use async_trait::async_trait; +use brocade::{InterfaceConfig, PortChannelConfig, Vlan}; +use harmony_types::id::Id; +use log::{debug, info}; +use serde::Serialize; + +use crate::{ + data::Version, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + score::Score, + topology::{Switch, SwitchError, Topology}, +}; + +#[derive(Clone, Debug, Serialize)] +pub struct BrocadeSwitchConfigurationScore { + /// VLANs to create on the switch. Define once, reference everywhere. + pub vlans: Vec, + /// Standalone interfaces (NOT members of a port-channel). + /// Each has its own VLAN/mode configuration. + pub interfaces: Vec, + /// Port-channels: bundles of ports with VLAN/mode config + /// applied on the logical port-channel interface, not on the members. + pub port_channels: Vec, +} + +impl Score for BrocadeSwitchConfigurationScore { + fn name(&self) -> String { + "BrocadeSwitchConfigurationScore".to_string() + } + + fn create_interpret(&self) -> Box> { + Box::new(BrocadeSwitchConfigurationInterpret { + score: self.clone(), + }) + } +} + +#[derive(Debug)] +struct BrocadeSwitchConfigurationInterpret { + score: BrocadeSwitchConfigurationScore, +} + +#[async_trait] +impl Interpret for BrocadeSwitchConfigurationInterpret { + async fn execute( + &self, + _inventory: &Inventory, + topology: &T, + ) -> Result { + self.create_vlans(topology).await?; + self.create_port_channels(topology).await?; + self.configure_port_channel_interfaces(topology).await?; + self.configure_standalone_interfaces(topology).await?; + + Ok(Outcome::success( + "Switch configuration applied successfully".to_string(), + )) + } + + fn get_name(&self) -> InterpretName { + InterpretName::Custom("BrocadeSwitchConfigurationInterpret") + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } +} + +impl BrocadeSwitchConfigurationInterpret { + async fn create_vlans( + &self, + topology: &T, + ) -> Result<(), InterpretError> { + for vlan in &self.score.vlans { + info!("Creating VLAN {} ({})", vlan.id, vlan.name); + topology + .create_vlan(vlan) + .await + .map_err(|e| InterpretError::new(format!("Failed to create VLAN {}: {e}", vlan.id)))?; + } + Ok(()) + } + + async fn create_port_channels( + &self, + topology: &T, + ) -> Result<(), InterpretError> { + for pc in &self.score.port_channels { + info!( + "Creating port-channel {} ({}) with ports: {:?}", + pc.id, pc.name, pc.ports + ); + topology + .configure_port_channel_from_config(pc) + .await + .map_err(|e| { + InterpretError::new(format!( + "Failed to create port-channel {} ({}): {e}", + pc.id, pc.name + )) + })?; + } + Ok(()) + } + + async fn configure_port_channel_interfaces( + &self, + topology: &T, + ) -> Result<(), InterpretError> { + let pc_interfaces: Vec = self + .score + .port_channels + .iter() + .map(|pc| InterfaceConfig { + interface: brocade::SwitchInterface::PortChannel(pc.id), + mode: pc.mode.clone(), + access_vlan: pc.access_vlan.as_ref().map(|v| v.id), + trunk_vlans: pc.trunk_vlans.clone(), + speed: pc.speed.clone(), + }) + .collect(); + + if !pc_interfaces.is_empty() { + info!( + "Configuring L2 mode on {} port-channel interface(s)", + pc_interfaces.len() + ); + for pc in &self.score.port_channels { + debug!( + " port-channel {} ({}): mode={:?}, vlans={:?}, speed={:?}", + pc.id, pc.name, pc.mode, pc.trunk_vlans, pc.speed + ); + } + topology + .configure_interfaces(&pc_interfaces) + .await + .map_err(|e| { + InterpretError::new(format!( + "Failed to configure port-channel interfaces: {e}" + )) + })?; + } + Ok(()) + } + + async fn configure_standalone_interfaces( + &self, + topology: &T, + ) -> Result<(), InterpretError> { + if !self.score.interfaces.is_empty() { + info!( + "Configuring {} standalone interface(s)", + self.score.interfaces.len() + ); + for iface in &self.score.interfaces { + debug!( + " {}: mode={:?}, speed={:?}", + iface.interface, iface.mode, iface.speed + ); + } + topology + .configure_interfaces(&self.score.interfaces) + .await + .map_err(|e| { + InterpretError::new(format!("Failed to configure interfaces: {e}")) + })?; + } + Ok(()) + } +} diff --git a/harmony/src/modules/brocade/mod.rs b/harmony/src/modules/brocade/mod.rs index 1f197b9..f9fb73c 100644 --- a/harmony/src/modules/brocade/mod.rs +++ b/harmony/src/modules/brocade/mod.rs @@ -3,3 +3,6 @@ pub use brocade::*; pub mod brocade_snmp; pub use brocade_snmp::*; + +pub mod brocade_switch_configuration; +pub use brocade_switch_configuration::*; diff --git a/harmony/src/modules/okd/host_network.rs b/harmony/src/modules/okd/host_network.rs index 98be356..75cc09b 100644 --- a/harmony/src/modules/okd/host_network.rs +++ b/harmony/src/modules/okd/host_network.rs @@ -1,6 +1,7 @@ use std::str::FromStr; use async_trait::async_trait; +use brocade::{InterfaceConfig, PortChannelConfig, PortChannelId, Vlan}; use harmony_types::{id::Id, switch::PortLocation}; use log::{error, info, warn}; use serde::Serialize; @@ -11,7 +12,10 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{HostNetworkConfig, NetworkInterface, NetworkManager, Switch, SwitchPort, Topology}, + topology::{ + HostNetworkConfig, NetworkInterface, NetworkManager, Switch, SwitchPort, + Topology, + }, }; /// Configures high-availability networking for a set of physical hosts. @@ -152,8 +156,9 @@ impl HostNetworkConfigurationInterpret { InterpretError::new(format!("Failed to configure host network: {e}")) })?; + let channel_id = todo!("Determine port-channel ID for this host"); topology - .configure_port_channel(&config) + .configure_port_channel(channel_id, &config) .await .map_err(|e| { InterpretError::new(format!("Failed to configure host network: {e}")) @@ -389,7 +394,7 @@ mod tests { use crate::{ hardware::HostCategory, topology::{ - HostNetworkConfig, NetworkError, PortConfig, PreparationError, PreparationOutcome, + HostNetworkConfig, NetworkError, PreparationError, PreparationOutcome, SwitchError, SwitchPort, }, }; @@ -836,6 +841,7 @@ mod tests { async fn configure_port_channel( &self, + _channel_id: PortChannelId, config: &HostNetworkConfig, ) -> Result<(), SwitchError> { let mut configured_port_channels = self.configured_port_channels.lock().unwrap(); @@ -843,14 +849,26 @@ mod tests { Ok(()) } + async fn configure_port_channel_from_config( + &self, + _config: &PortChannelConfig, + ) -> Result<(), SwitchError> { + todo!() + } async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError> { todo!() } - async fn configure_interface( + async fn configure_interfaces( &self, - port_config: &Vec, + _interfaces: &Vec, ) -> Result<(), SwitchError> { todo!() } + async fn create_vlan(&self, _vlan: &Vlan) -> Result<(), SwitchError> { + todo!() + } + async fn delete_vlan(&self, _vlan: &Vlan) -> Result<(), SwitchError> { + todo!() + } } }