From 6237e1d8775df3f44a5246d79160752c809d9350 Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Tue, 24 Mar 2026 15:24:32 -0400 Subject: [PATCH 1/9] feat: brocade module now support vlans --- brocade/examples/env.sh | 4 + brocade/examples/main.rs | 33 +++- brocade/examples/main_vlan_demo.rs | 207 ++++++++++++++++++++++ brocade/src/fast_iron.rs | 18 +- brocade/src/lib.rs | 31 +++- brocade/src/network_operating_system.rs | 79 +++++++-- build/check.sh | 3 + examples/brocade_switch/src/main.rs | 24 ++- harmony/src/domain/topology/ha_cluster.rs | 13 ++ harmony/src/domain/topology/network.rs | 16 +- harmony/src/infra/brocade.rs | 81 +++++++-- harmony/src/modules/brocade/brocade.rs | 10 +- harmony/src/modules/okd/host_network.rs | 12 +- 13 files changed, 476 insertions(+), 55 deletions(-) create mode 100644 brocade/examples/env.sh create mode 100644 brocade/examples/main_vlan_demo.rs 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..a1ef28f 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,7 +61,27 @@ 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(); diff --git a/brocade/examples/main_vlan_demo.rs b/brocade/examples/main_vlan_demo.rs new file mode 100644 index 0000000..181194f --- /dev/null +++ b/brocade/examples/main_vlan_demo.rs @@ -0,0 +1,207 @@ +use std::io::{self, Write}; + +use brocade::{BrocadeOptions, InterfaceConfig, PortOperatingMode, 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) on TenGigabitEthernet 1/0/1 ==="); + println!("Configuring port as trunk with all VLANs..."); + let configs = vec![InterfaceConfig { + port: "TenGigabitEthernet 1/0/1".to_string(), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::All), + }]; + 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 { + port: "TenGigabitEthernet 1/0/2".to_string(), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::Specific(vec![100, 200])), + }]; + 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 { + port: "TenGigabitEthernet 1/0/3".to_string(), + mode: PortOperatingMode::Access, + access_vlan: None, + trunk_vlans: 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 { + port: "TenGigabitEthernet 1/0/4".to_string(), + mode: PortOperatingMode::Access, + access_vlan: Some(100), + trunk_vlans: 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 = brocade.find_available_channel_id().await.unwrap(); + println!("Found available 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) + .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!("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..dc34d1a 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, + 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!() } diff --git a/brocade/src/lib.rs b/brocade/src/lib.rs index 5fc3ac9..36c84db 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -76,6 +76,26 @@ pub struct MacAddressEntry { pub type PortChannelId = u8; +#[derive(Debug, Clone, PartialEq, Eq)] +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 struct InterfaceConfig { + pub port: String, + pub mode: PortOperatingMode, + pub access_vlan: Option, + pub trunk_vlans: 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 @@ -206,10 +226,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. diff --git a/brocade/src/network_operating_system.rs b/brocade/src/network_operating_system.rs index fa99bf6..253b909 100644 --- a/brocade/src/network_operating_system.rs +++ b/brocade/src/network_operating_system.rs @@ -6,9 +6,9 @@ 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, InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, + PortOperatingMode, Vlan, VlanList, parse_brocade_mac_address, shell::BrocadeShell, }; #[derive(Debug)] @@ -185,18 +185,15 @@ 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)); + commands.push(format!("interface {}", interface.port)); - match interface.1 { + match interface.mode { PortOperatingMode::Fabric => { commands.push("fabric isl enable".into()); commands.push("fabric trunk enable".into()); @@ -204,7 +201,19 @@ 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(ids)) => { + for id in ids { + commands.push(format!("switchport trunk allowed vlan add {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()); @@ -214,7 +223,8 @@ impl BrocadeClient for NetworkOperatingSystemClient { PortOperatingMode::Access => { commands.push("switchport".into()); commands.push("switchport mode access".into()); - commands.push("switchport access vlan 1".into()); + let access_vlan = interface.access_vlan.unwrap_or(1); + commands.push(format!("switchport access vlan {access_vlan}")); commands.push("no spanning-tree shutdown".into()); commands.push("no fabric isl enable".into()); commands.push("no fabric trunk enable".into()); @@ -235,6 +245,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..."); @@ -283,8 +327,6 @@ impl BrocadeClient for NetworkOperatingSystemClient { .join(", ") ); - let interfaces = self.get_interfaces().await?; - let mut commands = vec![ "configure terminal".into(), format!("interface port-channel {}", channel_id), @@ -293,12 +335,11 @@ impl BrocadeClient for NetworkOperatingSystemClient { ]; 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()); 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..87560ff 100644 --- a/examples/brocade_switch/src/main.rs +++ b/examples/brocade_switch/src/main.rs @@ -1,10 +1,11 @@ use std::str::FromStr; -use brocade::{BrocadeOptions, PortOperatingMode}; +use brocade::{BrocadeOptions, PortOperatingMode, VlanList}; use harmony::{ infra::brocade::BrocadeSwitchConfig, inventory::Inventory, modules::brocade::{BrocadeSwitchAuth, BrocadeSwitchScore, SwitchTopology}, + topology::PortConfig, }; use harmony_macros::ip; use harmony_types::{id::Id, switch::PortLocation}; @@ -33,9 +34,24 @@ 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), + PortConfig { + port: PortLocation(2, 0, 17), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::All), + }, + PortConfig { + port: PortLocation(2, 0, 19), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::All), + }, + PortConfig { + port: PortLocation(1, 0, 18), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::All), + }, ], }; diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index e68e274..e1dd472 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::Vlan; use harmony_k8s::K8sClient; use harmony_macros::ip; use harmony_types::{ @@ -334,6 +335,12 @@ impl Switch for HAClusterTopology { async fn configure_interface(&self, _ports: &Vec) -> Result<(), SwitchError> { todo!() } + 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 + } } #[async_trait] @@ -603,4 +610,10 @@ impl SwitchClient for DummyInfra { async fn configure_interface(&self, _ports: &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..0c33ece 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::{PortOperatingMode, Vlan, VlanList}; use derive_new::new; use harmony_k8s::K8sClient; use harmony_types::{ @@ -220,7 +220,15 @@ impl From for NetworkError { } } -pub type PortConfig = (PortLocation, PortOperatingMode); +pub type OldPortConfig = (PortLocation, PortOperatingMode); + +#[derive(Debug, Clone, Serialize)] +pub struct PortConfig { + pub port: PortLocation, + pub mode: PortOperatingMode, + pub access_vlan: Option, + pub trunk_vlans: Option, +} #[async_trait] pub trait Switch: Send + Sync { @@ -234,6 +242,8 @@ pub trait Switch: Send + Sync { async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError>; async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError>; async fn configure_interface(&self, ports: &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)] @@ -296,6 +306,8 @@ pub trait SwitchClient: Debug + Send + Sync { async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError>; async fn configure_interface(&self, ports: &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..438246f 100644 --- a/harmony/src/infra/brocade.rs +++ b/harmony/src/infra/brocade.rs @@ -1,5 +1,8 @@ use async_trait::async_trait; -use brocade::{BrocadeClient, BrocadeOptions, InterSwitchLink, InterfaceStatus, PortOperatingMode}; +use brocade::{ + BrocadeClient, BrocadeOptions, InterSwitchLink, InterfaceConfig, InterfaceStatus, + PortOperatingMode, Vlan, +}; use harmony_types::{ id::Id, net::{IpAddress, MacAddress}, @@ -54,7 +57,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 +68,12 @@ impl SwitchClient for BrocadeSwitchClient { || link.remote_port.contains(&interface.port_location) }) }) - .map(|interface| (interface.name.clone(), PortOperatingMode::Trunk)) + .map(|interface| InterfaceConfig { + port: interface.name.clone(), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: None, + }) .collect(); if interfaces.is_empty() { @@ -171,13 +179,33 @@ impl SwitchClient for BrocadeSwitchClient { Ok(()) } async fn configure_interface(&self, ports: &Vec) -> Result<(), SwitchError> { - // FIXME hardcoded TenGigabitEthernet = bad - let ports = ports + let configs: Vec = ports .iter() - .map(|p| (format!("TenGigabitEthernet {}", p.0), p.1.clone())) + .map(|p| InterfaceConfig { + port: format!("TenGigabitEthernet {}", p.port), + mode: p.mode.clone(), + access_vlan: p.access_vlan, + trunk_vlans: p.trunk_vlans.clone(), + }) .collect(); self.brocade - .configure_interfaces(&ports) + .configure_interfaces(&configs) + .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(()) @@ -220,6 +248,14 @@ impl SwitchClient for UnmanagedSwitch { async fn configure_interface(&self, ports: &Vec) -> Result<(), SwitchError> { todo!("unmanged 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.") + } } #[cfg(test)] @@ -229,8 +265,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, + InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, PortOperatingMode, + SecurityLevel, Vlan, }; use harmony_types::switch::PortLocation; @@ -258,8 +295,18 @@ 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 { + port: first_interface.name.clone(), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: None, + }, + InterfaceConfig { + port: second_interface.name.clone(), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: None, + }, ]); } @@ -343,7 +390,7 @@ mod tests { struct FakeBrocadeClient { stack_topology: Vec, interfaces: Vec, - configured_interfaces: Arc>>, + configured_interfaces: Arc>>, } #[async_trait] @@ -366,7 +413,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 +421,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!() } diff --git a/harmony/src/modules/brocade/brocade.rs b/harmony/src/modules/brocade/brocade.rs index 67e94e5..82ec740 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, PortOperatingMode, Vlan}; use crate::{ data::Version, @@ -135,4 +135,12 @@ impl Switch for SwitchTopology { async fn configure_interface(&self, ports: &Vec) -> Result<(), SwitchError> { self.client.configure_interface(ports).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/okd/host_network.rs b/harmony/src/modules/okd/host_network.rs index 98be356..3f7d9f3 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::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, PortConfig, Switch, SwitchPort, + Topology, + }, }; /// Configures high-availability networking for a set of physical hosts. @@ -852,5 +856,11 @@ mod tests { ) -> Result<(), SwitchError> { todo!() } + async fn create_vlan(&self, _vlan: &Vlan) -> Result<(), SwitchError> { + todo!() + } + async fn delete_vlan(&self, _vlan: &Vlan) -> Result<(), SwitchError> { + todo!() + } } } -- 2.39.5 From b67275662dcb4b42b8381d2a05f0f7abcaf5caa2 Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Tue, 24 Mar 2026 15:48:16 -0400 Subject: [PATCH 2/9] fix: use Vlan struct instance everywhere, never use an u16 to reference a Vlan --- brocade/examples/main_vlan_demo.rs | 5 ++++- brocade/src/lib.rs | 4 ++-- brocade/src/network_operating_system.rs | 6 +++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/brocade/examples/main_vlan_demo.rs b/brocade/examples/main_vlan_demo.rs index 181194f..388ce27 100644 --- a/brocade/examples/main_vlan_demo.rs +++ b/brocade/examples/main_vlan_demo.rs @@ -105,7 +105,10 @@ async fn main() { port: "TenGigabitEthernet 1/0/2".to_string(), mode: PortOperatingMode::Trunk, access_vlan: None, - trunk_vlans: Some(VlanList::Specific(vec![100, 200])), + trunk_vlans: Some(VlanList::Specific(vec![ + Vlan { id: 100, name: "vlan100".to_string() }, + Vlan { id: 200, name: "vlan200".to_string() }, + ])), }]; brocade.configure_interfaces(&configs).await.unwrap(); println!("Querying interfaces..."); diff --git a/brocade/src/lib.rs b/brocade/src/lib.rs index 36c84db..4db75c6 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -76,7 +76,7 @@ pub struct MacAddressEntry { pub type PortChannelId = u8; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct Vlan { pub id: u16, pub name: String, @@ -85,7 +85,7 @@ pub struct Vlan { #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub enum VlanList { All, - Specific(Vec), + Specific(Vec), } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] diff --git a/brocade/src/network_operating_system.rs b/brocade/src/network_operating_system.rs index 253b909..54127ad 100644 --- a/brocade/src/network_operating_system.rs +++ b/brocade/src/network_operating_system.rs @@ -205,9 +205,9 @@ impl BrocadeClient for NetworkOperatingSystemClient { Some(VlanList::All) => { commands.push("switchport trunk allowed vlan all".into()); } - Some(VlanList::Specific(ids)) => { - for id in ids { - commands.push(format!("switchport trunk allowed vlan add {id}")); + Some(VlanList::Specific(vlans)) => { + for vlan in vlans { + commands.push(format!("switchport trunk allowed vlan add {}", vlan.id)); } } None => { -- 2.39.5 From 742253401885eaf029fd69093593927d408a27c8 Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Wed, 25 Mar 2026 09:21:09 -0400 Subject: [PATCH 3/9] feat: require to specify port-channel ID instead of finding an available one --- brocade/examples/main.rs | 2 +- brocade/examples/main_vlan_demo.rs | 4 +- harmony/src/domain/topology/ha_cluster.rs | 15 +++++-- harmony/src/domain/topology/network.rs | 9 +++- harmony/src/infra/brocade.rs | 53 +++++------------------ harmony/src/modules/brocade/brocade.rs | 8 +++- harmony/src/modules/okd/host_network.rs | 6 ++- 7 files changed, 43 insertions(+), 54 deletions(-) diff --git a/brocade/examples/main.rs b/brocade/examples/main.rs index a1ef28f..50040be 100644 --- a/brocade/examples/main.rs +++ b/brocade/examples/main.rs @@ -86,7 +86,7 @@ async fn main() { 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"; diff --git a/brocade/examples/main_vlan_demo.rs b/brocade/examples/main_vlan_demo.rs index 388ce27..8bc6244 100644 --- a/brocade/examples/main_vlan_demo.rs +++ b/brocade/examples/main_vlan_demo.rs @@ -157,8 +157,8 @@ async fn main() { wait_for_enter(); println!("\n=== TEST 5: Port-channel on TenGigabitEthernet 1/0/5 and 1/0/6 ==="); - let channel_id = brocade.find_available_channel_id().await.unwrap(); - println!("Found available channel ID: {channel_id}"); + 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 diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index e1dd472..5b7618f 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use brocade::Vlan; +use brocade::{PortChannelId, Vlan}; use harmony_k8s::K8sClient; use harmony_macros::ip; use harmony_types::{ @@ -317,12 +317,20 @@ 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, + ) .await .map_err(|e| SwitchError::new(format!("Failed to configure port-channel: {e}")))?; @@ -599,6 +607,7 @@ impl SwitchClient for DummyInfra { async fn configure_port_channel( &self, + _channel_id: PortChannelId, _channel_name: &str, _switch_ports: Vec, ) -> Result { diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 0c33ece..77d56b1 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, Vlan, VlanList}; +use brocade::{PortChannelId, PortOperatingMode, Vlan, VlanList}; use derive_new::new; use harmony_k8s::K8sClient; use harmony_types::{ @@ -239,7 +239,11 @@ 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>; async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError>; async fn configure_interface(&self, ports: &Vec) -> Result<(), SwitchError>; async fn create_vlan(&self, vlan: &Vlan) -> Result<(), SwitchError>; @@ -300,6 +304,7 @@ pub trait SwitchClient: Debug + Send + Sync { async fn configure_port_channel( &self, + channel_id: PortChannelId, channel_name: &str, switch_ports: Vec, ) -> Result; diff --git a/harmony/src/infra/brocade.rs b/harmony/src/infra/brocade.rs index 438246f..d75d6c2 100644 --- a/harmony/src/infra/brocade.rs +++ b/harmony/src/infra/brocade.rs @@ -1,14 +1,14 @@ use async_trait::async_trait; use brocade::{ BrocadeClient, BrocadeOptions, InterSwitchLink, InterfaceConfig, InterfaceStatus, - PortOperatingMode, Vlan, + 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::{ @@ -122,50 +122,18 @@ impl SwitchClient for BrocadeSwitchClient { async fn configure_port_channel( &self, + channel_id: PortChannelId, channel_name: &str, switch_ports: Vec, ) -> Result { - let mut channel_id = self - .brocade - .find_available_channel_id() + self.brocade + .create_port_channel(channel_id, channel_name, &switch_ports) .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) } @@ -236,8 +204,9 @@ 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, ) -> Result { todo!("unmanaged switch. Nothing to do.") } diff --git a/harmony/src/modules/brocade/brocade.rs b/harmony/src/modules/brocade/brocade.rs index 82ec740..b00bbb8 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, Vlan}; +use brocade::{BrocadeOptions, PortChannelId, PortOperatingMode, Vlan}; use crate::{ data::Version, @@ -126,7 +126,11 @@ 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 clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError> { diff --git a/harmony/src/modules/okd/host_network.rs b/harmony/src/modules/okd/host_network.rs index 3f7d9f3..997d596 100644 --- a/harmony/src/modules/okd/host_network.rs +++ b/harmony/src/modules/okd/host_network.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use async_trait::async_trait; -use brocade::Vlan; +use brocade::{PortChannelId, Vlan}; use harmony_types::{id::Id, switch::PortLocation}; use log::{error, info, warn}; use serde::Serialize; @@ -156,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}")) @@ -840,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(); -- 2.39.5 From d8dab12834dfd68c8dd7bf8dc7d1d7874372b9e4 Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Wed, 25 Mar 2026 09:35:02 -0400 Subject: [PATCH 4/9] set the description of the port-channel interface --- brocade/src/network_operating_system.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/brocade/src/network_operating_system.rs b/brocade/src/network_operating_system.rs index 54127ad..ea554a6 100644 --- a/brocade/src/network_operating_system.rs +++ b/brocade/src/network_operating_system.rs @@ -331,6 +331,7 @@ impl BrocadeClient for NetworkOperatingSystemClient { "configure terminal".into(), format!("interface port-channel {}", channel_id), "no shutdown".into(), + format!("description {channel_name}"), "exit".into(), ]; -- 2.39.5 From a1c9bfeabddedfea78e02ee41552e1c53e6cc779 Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Wed, 25 Mar 2026 09:58:56 -0400 Subject: [PATCH 5/9] feat: add a 'reset_interface' function --- brocade/examples/main_vlan_demo.rs | 7 +++++++ brocade/src/fast_iron.rs | 18 ++++++++++++++++++ brocade/src/lib.rs | 3 +++ brocade/src/network_operating_system.rs | 18 ++++++++++++++++++ harmony/src/infra/brocade.rs | 4 ++++ 5 files changed, 50 insertions(+) diff --git a/brocade/examples/main_vlan_demo.rs b/brocade/examples/main_vlan_demo.rs index 8bc6244..375b8fd 100644 --- a/brocade/examples/main_vlan_demo.rs +++ b/brocade/examples/main_vlan_demo.rs @@ -181,6 +181,13 @@ async fn main() { .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 { diff --git a/brocade/src/fast_iron.rs b/brocade/src/fast_iron.rs index dc34d1a..2a69673 100644 --- a/brocade/src/fast_iron.rs +++ b/brocade/src/fast_iron.rs @@ -200,6 +200,24 @@ 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(), + "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 4db75c6..45a253b 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -269,6 +269,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 ea554a6..7ec1ffa 100644 --- a/brocade/src/network_operating_system.rs +++ b/brocade/src/network_operating_system.rs @@ -359,6 +359,24 @@ 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(), + "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/harmony/src/infra/brocade.rs b/harmony/src/infra/brocade.rs index d75d6c2..01418f9 100644 --- a/harmony/src/infra/brocade.rs +++ b/harmony/src/infra/brocade.rs @@ -411,6 +411,10 @@ mod tests { todo!() } + async fn reset_interface(&self, _interface: &str) -> Result<(), Error> { + todo!() + } + async fn clear_port_channel(&self, _channel_name: &str) -> Result<(), Error> { todo!() } -- 2.39.5 From 8c8baaf9ccdf0aaa9cb58b955eabf0156950fb8d Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Wed, 25 Mar 2026 11:45:57 -0400 Subject: [PATCH 6/9] feat: create new brocade configuration score --- brocade/examples/main_vlan_demo.rs | 35 +++- brocade/src/lib.rs | 29 ++- brocade/src/network_operating_system.rs | 21 ++- examples/brocade_switch/src/main.rs | 22 ++- .../brocade_switch_configuration/Cargo.toml | 18 ++ examples/brocade_switch_configuration/env.sh | 4 + .../brocade_switch_configuration/src/main.rs | 137 +++++++++++++++ harmony/src/domain/topology/ha_cluster.rs | 27 ++- harmony/src/domain/topology/network.rs | 28 +-- harmony/src/infra/brocade.rs | 41 +++-- harmony/src/modules/brocade/brocade.rs | 25 ++- .../brocade/brocade_switch_configuration.rs | 166 ++++++++++++++++++ harmony/src/modules/brocade/mod.rs | 3 + harmony/src/modules/okd/host_network.rs | 16 +- 14 files changed, 501 insertions(+), 71 deletions(-) create mode 100644 examples/brocade_switch_configuration/Cargo.toml create mode 100644 examples/brocade_switch_configuration/env.sh create mode 100644 examples/brocade_switch_configuration/src/main.rs create mode 100644 harmony/src/modules/brocade/brocade_switch_configuration.rs diff --git a/brocade/examples/main_vlan_demo.rs b/brocade/examples/main_vlan_demo.rs index 375b8fd..a82babb 100644 --- a/brocade/examples/main_vlan_demo.rs +++ b/brocade/examples/main_vlan_demo.rs @@ -1,6 +1,9 @@ use std::io::{self, Write}; -use brocade::{BrocadeOptions, InterfaceConfig, PortOperatingMode, Vlan, VlanList, ssh}; +use brocade::{ + BrocadeOptions, InterfaceConfig, InterfaceType, PortOperatingMode, SwitchInterface, Vlan, + VlanList, ssh, +}; use harmony_secret::{Secret, SecretManager}; use harmony_types::switch::PortLocation; use schemars::JsonSchema; @@ -84,7 +87,10 @@ async fn main() { println!("\n=== TEST 1: Trunk port (all VLANs) on TenGigabitEthernet 1/0/1 ==="); println!("Configuring port as trunk with all VLANs..."); let configs = vec![InterfaceConfig { - port: "TenGigabitEthernet 1/0/1".to_string(), + interface: SwitchInterface::Ethernet( + InterfaceType::Ethernet("TenGigabitEthernet".into()), + PortLocation(1, 0, 1), + ), mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: Some(VlanList::All), @@ -102,12 +108,21 @@ async fn main() { 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 { - port: "TenGigabitEthernet 1/0/2".to_string(), + interface: SwitchInterface::Ethernet( + InterfaceType::Ethernet("TenGigabitEthernet".into()), + 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() }, + Vlan { + id: 100, + name: "vlan100".to_string(), + }, + Vlan { + id: 200, + name: "vlan200".to_string(), + }, ])), }]; brocade.configure_interfaces(&configs).await.unwrap(); @@ -123,7 +138,10 @@ async fn main() { 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 { - port: "TenGigabitEthernet 1/0/3".to_string(), + interface: SwitchInterface::Ethernet( + InterfaceType::Ethernet("TenGigabitEthernet".into()), + PortLocation(1, 0, 3), + ), mode: PortOperatingMode::Access, access_vlan: None, trunk_vlans: None, @@ -141,7 +159,10 @@ async fn main() { 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 { - port: "TenGigabitEthernet 1/0/4".to_string(), + interface: SwitchInterface::Ethernet( + InterfaceType::Ethernet("TenGigabitEthernet".into()), + PortLocation(1, 0, 4), + ), mode: PortOperatingMode::Access, access_vlan: Some(100), trunk_vlans: None, diff --git a/brocade/src/lib.rs b/brocade/src/lib.rs index 45a253b..cef9b52 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -88,14 +88,39 @@ pub enum VlanList { 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 struct InterfaceConfig { - pub port: String, + pub interface: SwitchInterface, pub mode: PortOperatingMode, pub access_vlan: Option, pub trunk_vlans: 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, +} + /// 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 @@ -124,7 +149,7 @@ 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), diff --git a/brocade/src/network_operating_system.rs b/brocade/src/network_operating_system.rs index 7ec1ffa..6642e86 100644 --- a/brocade/src/network_operating_system.rs +++ b/brocade/src/network_operating_system.rs @@ -8,7 +8,8 @@ use regex::Regex; use crate::{ BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceConfig, InterfaceInfo, InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, - PortOperatingMode, Vlan, VlanList, parse_brocade_mac_address, shell::BrocadeShell, + PortOperatingMode, SwitchInterface, Vlan, VlanList, parse_brocade_mac_address, + shell::BrocadeShell, }; #[derive(Debug)] @@ -191,7 +192,7 @@ impl BrocadeClient for NetworkOperatingSystemClient { let mut commands = vec!["configure terminal".to_string()]; for interface in interfaces { - commands.push(format!("interface {}", interface.port)); + commands.push(format!("interface {}", interface.interface)); match interface.mode { PortOperatingMode::Fabric => { @@ -215,9 +216,11 @@ impl BrocadeClient for NetworkOperatingSystemClient { } } 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()); + 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()); + } commands.push("no shutdown".into()); } PortOperatingMode::Access => { @@ -225,9 +228,11 @@ impl BrocadeClient for NetworkOperatingSystemClient { commands.push("switchport mode access".into()); let access_vlan = interface.access_vlan.unwrap_or(1); commands.push(format!("switchport access vlan {access_vlan}")); - commands.push("no spanning-tree shutdown".into()); - commands.push("no fabric isl enable".into()); - commands.push("no fabric trunk enable".into()); + 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()); + } } } diff --git a/examples/brocade_switch/src/main.rs b/examples/brocade_switch/src/main.rs index 87560ff..f0ec12d 100644 --- a/examples/brocade_switch/src/main.rs +++ b/examples/brocade_switch/src/main.rs @@ -1,15 +1,21 @@ use std::str::FromStr; -use brocade::{BrocadeOptions, PortOperatingMode, VlanList}; +use brocade::{BrocadeOptions, InterfaceConfig, InterfaceType, PortOperatingMode, SwitchInterface, VlanList}; use harmony::{ infra::brocade::BrocadeSwitchConfig, inventory::Inventory, modules::brocade::{BrocadeSwitchAuth, BrocadeSwitchScore, SwitchTopology}, - topology::PortConfig, }; use harmony_macros::ip; use harmony_types::{id::Id, switch::PortLocation}; +fn tengig(stack: u8, slot: u8, port: u8) -> SwitchInterface { + SwitchInterface::Ethernet( + InterfaceType::Ethernet("TenGigabitEthernet".into()), + PortLocation(stack, slot, port), + ) +} + fn get_switch_config() -> BrocadeSwitchConfig { let mut options = BrocadeOptions::default(); options.ssh.port = 2222; @@ -34,20 +40,20 @@ async fn main() { Id::from_str("18").unwrap(), ], ports_to_configure: vec![ - PortConfig { - port: PortLocation(2, 0, 17), + InterfaceConfig { + interface: tengig(2, 0, 17), mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: Some(VlanList::All), }, - PortConfig { - port: PortLocation(2, 0, 19), + InterfaceConfig { + interface: tengig(2, 0, 19), mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: Some(VlanList::All), }, - PortConfig { - port: PortLocation(1, 0, 18), + InterfaceConfig { + interface: tengig(1, 0, 18), mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: Some(VlanList::All), 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..4a89e7e --- /dev/null +++ b/examples/brocade_switch_configuration/src/main.rs @@ -0,0 +1,137 @@ +use brocade::{ + BrocadeOptions, InterfaceConfig, 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::Ethernet("TenGigabitEthernet".into()), + 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")], + 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 + InterfaceConfig { + interface: tengig(1, 0, 1), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::All), + }, + // Trunk port with specific VLANs (MGMT + DATA only) + InterfaceConfig { + interface: tengig(1, 0, 2), + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::Specific(vec![mgmt.clone(), data.clone()])), + }, + // Access port on the MGMT VLAN + InterfaceConfig { + interface: tengig(1, 0, 3), + mode: PortOperatingMode::Access, + access_vlan: Some(mgmt.id), + trunk_vlans: None, + }, + // Access port on the STORAGE VLAN + InterfaceConfig { + interface: tengig(1, 0, 4), + mode: PortOperatingMode::Access, + access_vlan: Some(storage.id), + trunk_vlans: 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 + PortChannelConfig { + id: 1, + name: "SERVER_BOND".to_string(), + ports: vec![PortLocation(1, 0, 5), PortLocation(1, 0, 6)], + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::Specific(vec![data.clone(), storage.clone()])), + }, + // Port-channel 2: trunk with all VLANs + PortChannelConfig { + id: 2, + name: "BACKUP_BOND".to_string(), + ports: vec![PortLocation(1, 0, 7), PortLocation(1, 0, 8)], + mode: PortOperatingMode::Trunk, + access_vlan: None, + trunk_vlans: Some(VlanList::All), + }, + ], + }; + + // =================================================== + // 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 5b7618f..74dbbce 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use brocade::{PortChannelId, Vlan}; +use brocade::{InterfaceConfig, PortChannelConfig, PortChannelId, Vlan}; use harmony_k8s::K8sClient; use harmony_macros::ip; use harmony_types::{ @@ -12,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, @@ -337,11 +337,25 @@ impl Switch for HAClusterTopology { 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()) + .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 @@ -616,7 +630,10 @@ impl SwitchClient for DummyInfra { 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> { diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 77d56b1..0dcd75b 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::{PortChannelId, PortOperatingMode, Vlan, VlanList}; +use brocade::{InterfaceConfig, PortChannelConfig, PortChannelId, Vlan}; use derive_new::new; use harmony_k8s::K8sClient; use harmony_types::{ @@ -220,16 +220,6 @@ impl From for NetworkError { } } -pub type OldPortConfig = (PortLocation, PortOperatingMode); - -#[derive(Debug, Clone, Serialize)] -pub struct PortConfig { - pub port: PortLocation, - pub mode: PortOperatingMode, - pub access_vlan: Option, - pub trunk_vlans: Option, -} - #[async_trait] pub trait Switch: Send + Sync { async fn setup_switch(&self) -> Result<(), SwitchError>; @@ -244,8 +234,17 @@ pub trait Switch: Send + Sync { 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>; } @@ -310,7 +309,10 @@ pub trait SwitchClient: Debug + Send + Sync { ) -> 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>; } diff --git a/harmony/src/infra/brocade.rs b/harmony/src/infra/brocade.rs index 01418f9..c4bbde8 100644 --- a/harmony/src/infra/brocade.rs +++ b/harmony/src/infra/brocade.rs @@ -3,6 +3,7 @@ use brocade::{ BrocadeClient, BrocadeOptions, InterSwitchLink, InterfaceConfig, InterfaceStatus, PortChannelId, PortOperatingMode, Vlan, }; + use harmony_types::{ id::Id, net::{IpAddress, MacAddress}, @@ -13,7 +14,7 @@ use option_ext::OptionExt; use crate::{ modules::brocade::BrocadeSwitchAuth, - topology::{PortConfig, SwitchClient, SwitchError}, + topology::{SwitchClient, SwitchError}, }; #[derive(Debug, Clone)] @@ -69,7 +70,10 @@ impl SwitchClient for BrocadeSwitchClient { }) }) .map(|interface| InterfaceConfig { - port: interface.name.clone(), + interface: brocade::SwitchInterface::Ethernet( + interface.interface_type.clone(), + interface.port_location.clone(), + ), mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: None, @@ -146,18 +150,12 @@ impl SwitchClient for BrocadeSwitchClient { } Ok(()) } - async fn configure_interface(&self, ports: &Vec) -> Result<(), SwitchError> { - let configs: Vec = ports - .iter() - .map(|p| InterfaceConfig { - port: format!("TenGigabitEthernet {}", p.port), - mode: p.mode.clone(), - access_vlan: p.access_vlan, - trunk_vlans: p.trunk_vlans.clone(), - }) - .collect(); + async fn configure_interfaces( + &self, + interfaces: &Vec, + ) -> Result<(), SwitchError> { self.brocade - .configure_interfaces(&configs) + .configure_interfaces(interfaces) .await .map_err(|e| SwitchError::new(e.to_string()))?; Ok(()) @@ -214,8 +212,11 @@ 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> { @@ -265,13 +266,19 @@ mod tests { let configured_interfaces = brocade.configured_interfaces.lock().unwrap(); assert_that!(*configured_interfaces).contains_exactly(vec![ InterfaceConfig { - port: first_interface.name.clone(), + interface: brocade::SwitchInterface::Ethernet( + InterfaceType::Ethernet("TenGigabitEthernet".into()), + PortLocation(1, 0, 1), + ), mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: None, }, InterfaceConfig { - port: second_interface.name.clone(), + interface: brocade::SwitchInterface::Ethernet( + InterfaceType::Ethernet("TenGigabitEthernet".into()), + PortLocation(1, 0, 4), + ), mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: None, diff --git a/harmony/src/modules/brocade/brocade.rs b/harmony/src/modules/brocade/brocade.rs index b00bbb8..56b6517 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, PortChannelId, PortOperatingMode, Vlan}; +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())) @@ -133,11 +133,24 @@ impl Switch for SwitchTopology { ) -> 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()) + .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> { 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..1301c92 --- /dev/null +++ b/harmony/src/modules/brocade/brocade_switch_configuration.rs @@ -0,0 +1,166 @@ +use async_trait::async_trait; +use brocade::{InterfaceConfig, PortChannelConfig, Vlan}; +use harmony_types::id::Id; +use log::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(), + }) + .collect(); + + if !pc_interfaces.is_empty() { + info!( + "Configuring L2 mode on {} port-channel interface(s)", + pc_interfaces.len() + ); + 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() + ); + 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 997d596..75cc09b 100644 --- a/harmony/src/modules/okd/host_network.rs +++ b/harmony/src/modules/okd/host_network.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use async_trait::async_trait; -use brocade::{PortChannelId, Vlan}; +use brocade::{InterfaceConfig, PortChannelConfig, PortChannelId, Vlan}; use harmony_types::{id::Id, switch::PortLocation}; use log::{error, info, warn}; use serde::Serialize; @@ -13,7 +13,7 @@ use crate::{ inventory::Inventory, score::Score, topology::{ - HostNetworkConfig, NetworkInterface, NetworkManager, PortConfig, Switch, SwitchPort, + HostNetworkConfig, NetworkInterface, NetworkManager, Switch, SwitchPort, Topology, }, }; @@ -394,7 +394,7 @@ mod tests { use crate::{ hardware::HostCategory, topology::{ - HostNetworkConfig, NetworkError, PortConfig, PreparationError, PreparationOutcome, + HostNetworkConfig, NetworkError, PreparationError, PreparationOutcome, SwitchError, SwitchPort, }, }; @@ -849,12 +849,18 @@ 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!() } -- 2.39.5 From 2728fc8989590d345ee31de7ec8db3341f1a8a80 Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Wed, 25 Mar 2026 12:10:39 -0400 Subject: [PATCH 7/9] feat: add possibility to configure interface speed --- brocade/examples/main_vlan_demo.rs | 12 ++++++---- brocade/src/fast_iron.rs | 1 + brocade/src/lib.rs | 23 +++++++++++++++++++ brocade/src/network_operating_system.rs | 9 +++++++- examples/brocade_switch/src/main.rs | 3 +++ .../brocade_switch_configuration/src/main.rs | 16 +++++++++---- harmony/src/infra/brocade.rs | 3 +++ .../brocade/brocade_switch_configuration.rs | 1 + 8 files changed, 58 insertions(+), 10 deletions(-) diff --git a/brocade/examples/main_vlan_demo.rs b/brocade/examples/main_vlan_demo.rs index a82babb..267901f 100644 --- a/brocade/examples/main_vlan_demo.rs +++ b/brocade/examples/main_vlan_demo.rs @@ -1,8 +1,8 @@ use std::io::{self, Write}; use brocade::{ - BrocadeOptions, InterfaceConfig, InterfaceType, PortOperatingMode, SwitchInterface, Vlan, - VlanList, ssh, + BrocadeOptions, InterfaceConfig, InterfaceSpeed, InterfaceType, PortOperatingMode, + SwitchInterface, Vlan, VlanList, ssh, }; use harmony_secret::{Secret, SecretManager}; use harmony_types::switch::PortLocation; @@ -84,8 +84,8 @@ async fn main() { println!("\n=== Press ENTER to continue to port configuration tests ---"); wait_for_enter(); - println!("\n=== TEST 1: Trunk port (all VLANs) on TenGigabitEthernet 1/0/1 ==="); - println!("Configuring port as trunk with all VLANs..."); + 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::Ethernet("TenGigabitEthernet".into()), @@ -94,6 +94,7 @@ async fn main() { mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: Some(VlanList::All), + speed: Some(InterfaceSpeed::Gbps10), }]; brocade.configure_interfaces(&configs).await.unwrap(); println!("Querying interfaces..."); @@ -124,6 +125,7 @@ async fn main() { name: "vlan200".to_string(), }, ])), + speed: None, }]; brocade.configure_interfaces(&configs).await.unwrap(); println!("Querying interfaces..."); @@ -145,6 +147,7 @@ async fn main() { mode: PortOperatingMode::Access, access_vlan: None, trunk_vlans: None, + speed: None, }]; brocade.configure_interfaces(&configs).await.unwrap(); println!("Querying interfaces..."); @@ -166,6 +169,7 @@ async fn main() { mode: PortOperatingMode::Access, access_vlan: Some(100), trunk_vlans: None, + speed: None, }]; brocade.configure_interfaces(&configs).await.unwrap(); println!("Querying interfaces..."); diff --git a/brocade/src/fast_iron.rs b/brocade/src/fast_iron.rs index 2a69673..dda98a0 100644 --- a/brocade/src/fast_iron.rs +++ b/brocade/src/fast_iron.rs @@ -207,6 +207,7 @@ impl BrocadeClient for FastIronClient { "configure terminal".into(), format!("interface {interface}"), "no switchport".into(), + "no speed".into(), "exit".into(), ]; diff --git a/brocade/src/lib.rs b/brocade/src/lib.rs index cef9b52..d6b4434 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -103,12 +103,34 @@ impl fmt::Display for SwitchInterface { } } +#[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)] @@ -119,6 +141,7 @@ pub struct PortChannelConfig { 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. diff --git a/brocade/src/network_operating_system.rs b/brocade/src/network_operating_system.rs index 6642e86..273feed 100644 --- a/brocade/src/network_operating_system.rs +++ b/brocade/src/network_operating_system.rs @@ -221,7 +221,6 @@ impl BrocadeClient for NetworkOperatingSystemClient { commands.push("no fabric isl enable".into()); commands.push("no fabric trunk enable".into()); } - commands.push("no shutdown".into()); } PortOperatingMode::Access => { commands.push("switchport".into()); @@ -236,6 +235,13 @@ impl BrocadeClient for NetworkOperatingSystemClient { } } + if let Some(speed) = &interface.speed { + if matches!(interface.interface, SwitchInterface::PortChannel(..)) { + commands.push("shutdown".into()); + } + commands.push(format!("speed {speed}")); + } + commands.push("no shutdown".into()); commands.push("exit".into()); } @@ -371,6 +377,7 @@ impl BrocadeClient for NetworkOperatingSystemClient { "configure terminal".into(), format!("interface {interface}"), "no switchport".into(), + "no speed".into(), "exit".into(), ]; diff --git a/examples/brocade_switch/src/main.rs b/examples/brocade_switch/src/main.rs index f0ec12d..aa02c59 100644 --- a/examples/brocade_switch/src/main.rs +++ b/examples/brocade_switch/src/main.rs @@ -45,18 +45,21 @@ async fn main() { 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/src/main.rs b/examples/brocade_switch_configuration/src/main.rs index 4a89e7e..ae4dcec 100644 --- a/examples/brocade_switch_configuration/src/main.rs +++ b/examples/brocade_switch_configuration/src/main.rs @@ -1,6 +1,6 @@ use brocade::{ - BrocadeOptions, InterfaceConfig, InterfaceType, PortChannelConfig, PortOperatingMode, - SwitchInterface, Vlan, VlanList, + BrocadeOptions, InterfaceConfig, InterfaceSpeed, InterfaceType, PortChannelConfig, + PortOperatingMode, SwitchInterface, Vlan, VlanList, }; use harmony::{ infra::brocade::BrocadeSwitchConfig, @@ -70,12 +70,13 @@ async fn main() { // Standalone interfaces (not part of any port-channel) interfaces: vec![ - // Trunk port with ALL VLANs + // Trunk port with ALL VLANs, forced to 10Gbps InterfaceConfig { interface: tengig(1, 0, 1), mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: Some(VlanList::All), + speed: Some(InterfaceSpeed::Gbps10), }, // Trunk port with specific VLANs (MGMT + DATA only) InterfaceConfig { @@ -83,6 +84,7 @@ async fn main() { 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 { @@ -90,6 +92,7 @@ async fn main() { mode: PortOperatingMode::Access, access_vlan: Some(mgmt.id), trunk_vlans: None, + speed: None, }, // Access port on the STORAGE VLAN InterfaceConfig { @@ -97,12 +100,13 @@ async fn main() { 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 + // Port-channel 1: trunk with DATA + STORAGE VLANs, forced to 1Gbps PortChannelConfig { id: 1, name: "SERVER_BOND".to_string(), @@ -110,8 +114,9 @@ async fn main() { 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 + // Port-channel 2: trunk with all VLANs, default speed PortChannelConfig { id: 2, name: "BACKUP_BOND".to_string(), @@ -119,6 +124,7 @@ async fn main() { mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: Some(VlanList::All), + speed: None, }, ], }; diff --git a/harmony/src/infra/brocade.rs b/harmony/src/infra/brocade.rs index c4bbde8..4e7f701 100644 --- a/harmony/src/infra/brocade.rs +++ b/harmony/src/infra/brocade.rs @@ -77,6 +77,7 @@ impl SwitchClient for BrocadeSwitchClient { mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: None, + speed: None, }) .collect(); @@ -273,6 +274,7 @@ mod tests { mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: None, + speed: None, }, InterfaceConfig { interface: brocade::SwitchInterface::Ethernet( @@ -282,6 +284,7 @@ mod tests { mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: None, + speed: None, }, ]); } diff --git a/harmony/src/modules/brocade/brocade_switch_configuration.rs b/harmony/src/modules/brocade/brocade_switch_configuration.rs index 1301c92..2371ae9 100644 --- a/harmony/src/modules/brocade/brocade_switch_configuration.rs +++ b/harmony/src/modules/brocade/brocade_switch_configuration.rs @@ -125,6 +125,7 @@ impl BrocadeSwitchConfigurationInterpret { 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(); -- 2.39.5 From a646f1f4d05d2865f1078cd123a91e596ca72232 Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Wed, 25 Mar 2026 12:35:54 -0400 Subject: [PATCH 8/9] feat: use an enum for interface types, add logging --- brocade/examples/main_vlan_demo.rs | 8 ++++---- brocade/src/lib.rs | 7 ++++--- brocade/src/network_operating_system.rs | 13 +++++++++++-- examples/brocade_switch/src/main.rs | 2 +- examples/brocade_switch_configuration/src/main.rs | 2 +- harmony/src/infra/brocade.rs | 6 +++--- .../brocade/brocade_switch_configuration.rs | 14 +++++++++++++- 7 files changed, 37 insertions(+), 15 deletions(-) diff --git a/brocade/examples/main_vlan_demo.rs b/brocade/examples/main_vlan_demo.rs index 267901f..979aab0 100644 --- a/brocade/examples/main_vlan_demo.rs +++ b/brocade/examples/main_vlan_demo.rs @@ -88,7 +88,7 @@ async fn main() { println!("Configuring port as trunk with all VLANs and speed 10Gbps..."); let configs = vec![InterfaceConfig { interface: SwitchInterface::Ethernet( - InterfaceType::Ethernet("TenGigabitEthernet".into()), + InterfaceType::TenGigabitEthernet, PortLocation(1, 0, 1), ), mode: PortOperatingMode::Trunk, @@ -110,7 +110,7 @@ async fn main() { println!("Configuring port as trunk with VLANs 100, 200..."); let configs = vec![InterfaceConfig { interface: SwitchInterface::Ethernet( - InterfaceType::Ethernet("TenGigabitEthernet".into()), + InterfaceType::TenGigabitEthernet, PortLocation(1, 0, 2), ), mode: PortOperatingMode::Trunk, @@ -141,7 +141,7 @@ async fn main() { println!("Configuring port as access (default VLAN 1)..."); let configs = vec![InterfaceConfig { interface: SwitchInterface::Ethernet( - InterfaceType::Ethernet("TenGigabitEthernet".into()), + InterfaceType::TenGigabitEthernet, PortLocation(1, 0, 3), ), mode: PortOperatingMode::Access, @@ -163,7 +163,7 @@ async fn main() { println!("Configuring port as access with VLAN 100..."); let configs = vec![InterfaceConfig { interface: SwitchInterface::Ethernet( - InterfaceType::Ethernet("TenGigabitEthernet".into()), + InterfaceType::TenGigabitEthernet, PortLocation(1, 0, 4), ), mode: PortOperatingMode::Access, diff --git a/brocade/src/lib.rs b/brocade/src/lib.rs index d6b4434..243c9c7 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -174,14 +174,15 @@ pub struct InterfaceInfo { /// Categorizes the functional type of a switch interface. #[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"), } } } diff --git a/brocade/src/network_operating_system.rs b/brocade/src/network_operating_system.rs index 273feed..e55e1b5 100644 --- a/brocade/src/network_operating_system.rs +++ b/brocade/src/network_operating_system.rs @@ -85,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()?; @@ -192,6 +192,11 @@ impl BrocadeClient for NetworkOperatingSystemClient { let mut commands = vec!["configure terminal".to_string()]; for interface in interfaces { + debug!( + "[Brocade] Configuring interface {} as {:?}", + interface.interface, interface.mode + ); + commands.push(format!("interface {}", interface.interface)); match interface.mode { @@ -236,6 +241,10 @@ impl BrocadeClient for NetworkOperatingSystemClient { } 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()); } diff --git a/examples/brocade_switch/src/main.rs b/examples/brocade_switch/src/main.rs index aa02c59..496cdbe 100644 --- a/examples/brocade_switch/src/main.rs +++ b/examples/brocade_switch/src/main.rs @@ -11,7 +11,7 @@ use harmony_types::{id::Id, switch::PortLocation}; fn tengig(stack: u8, slot: u8, port: u8) -> SwitchInterface { SwitchInterface::Ethernet( - InterfaceType::Ethernet("TenGigabitEthernet".into()), + InterfaceType::TenGigabitEthernet, PortLocation(stack, slot, port), ) } diff --git a/examples/brocade_switch_configuration/src/main.rs b/examples/brocade_switch_configuration/src/main.rs index ae4dcec..ba2afdd 100644 --- a/examples/brocade_switch_configuration/src/main.rs +++ b/examples/brocade_switch_configuration/src/main.rs @@ -12,7 +12,7 @@ use harmony_types::switch::PortLocation; fn tengig(stack: u8, slot: u8, port: u8) -> SwitchInterface { SwitchInterface::Ethernet( - InterfaceType::Ethernet("TenGigabitEthernet".into()), + InterfaceType::TenGigabitEthernet, PortLocation(stack, slot, port), ) } diff --git a/harmony/src/infra/brocade.rs b/harmony/src/infra/brocade.rs index 4e7f701..4d6b31d 100644 --- a/harmony/src/infra/brocade.rs +++ b/harmony/src/infra/brocade.rs @@ -268,7 +268,7 @@ mod tests { assert_that!(*configured_interfaces).contains_exactly(vec![ InterfaceConfig { interface: brocade::SwitchInterface::Ethernet( - InterfaceType::Ethernet("TenGigabitEthernet".into()), + InterfaceType::TenGigabitEthernet, PortLocation(1, 0, 1), ), mode: PortOperatingMode::Trunk, @@ -278,7 +278,7 @@ mod tests { }, InterfaceConfig { interface: brocade::SwitchInterface::Ethernet( - InterfaceType::Ethernet("TenGigabitEthernet".into()), + InterfaceType::TenGigabitEthernet, PortLocation(1, 0, 4), ), mode: PortOperatingMode::Trunk, @@ -456,7 +456,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_switch_configuration.rs b/harmony/src/modules/brocade/brocade_switch_configuration.rs index 2371ae9..684bd3d 100644 --- a/harmony/src/modules/brocade/brocade_switch_configuration.rs +++ b/harmony/src/modules/brocade/brocade_switch_configuration.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use brocade::{InterfaceConfig, PortChannelConfig, Vlan}; use harmony_types::id::Id; -use log::info; +use log::{debug, info}; use serde::Serialize; use crate::{ @@ -134,6 +134,12 @@ impl BrocadeSwitchConfigurationInterpret { "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 @@ -155,6 +161,12 @@ impl BrocadeSwitchConfigurationInterpret { "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 -- 2.39.5 From fb72e94dbb8b75b9d3a70715282af1cb4b668caf Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Fri, 10 Apr 2026 15:41:09 -0400 Subject: [PATCH 9/9] fix: when creating a port-channel with forced speed, it needs to be set on the port-channel and its member interfaces --- Cargo.lock | 36 +++++++++++++++++++ brocade/examples/main.rs | 2 +- brocade/examples/main_vlan_demo.rs | 2 +- brocade/src/fast_iron.rs | 11 ++++-- brocade/src/lib.rs | 5 +++ brocade/src/network_operating_system.rs | 14 ++++++-- .../brocade_switch_configuration/src/main.rs | 15 ++++---- harmony/src/domain/topology/ha_cluster.rs | 11 ++++-- harmony/src/domain/topology/network.rs | 3 +- harmony/src/infra/brocade.rs | 13 ++++--- harmony/src/modules/brocade/brocade.rs | 7 +++- 11 files changed, 97 insertions(+), 22 deletions(-) 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/main.rs b/brocade/examples/main.rs index 50040be..a26620d 100644 --- a/brocade/examples/main.rs +++ b/brocade/examples/main.rs @@ -92,7 +92,7 @@ async fn main() { 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 index 979aab0..4669479 100644 --- a/brocade/examples/main_vlan_demo.rs +++ b/brocade/examples/main_vlan_demo.rs @@ -187,7 +187,7 @@ async fn main() { 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) + .create_port_channel(channel_id, "HARMONY_LAG", &ports, None) .await .unwrap(); println!("Port-channel created."); diff --git a/brocade/src/fast_iron.rs b/brocade/src/fast_iron.rs index dda98a0..963101e 100644 --- a/brocade/src/fast_iron.rs +++ b/brocade/src/fast_iron.rs @@ -1,8 +1,8 @@ use super::BrocadeClient; use crate::{ BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceConfig, InterfaceInfo, - MacAddressEntry, PortChannelId, PortOperatingMode, Vlan, parse_brocade_mac_address, - shell::BrocadeShell, + InterfaceSpeed, MacAddressEntry, PortChannelId, PortOperatingMode, Vlan, + parse_brocade_mac_address, shell::BrocadeShell, }; use async_trait::async_trait; @@ -186,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) diff --git a/brocade/src/lib.rs b/brocade/src/lib.rs index 243c9c7..8a5cb74 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -302,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 diff --git a/brocade/src/network_operating_system.rs b/brocade/src/network_operating_system.rs index e55e1b5..e5f1724 100644 --- a/brocade/src/network_operating_system.rs +++ b/brocade/src/network_operating_system.rs @@ -7,7 +7,7 @@ use regex::Regex; use crate::{ BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceConfig, - InterfaceInfo, InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, + InterfaceInfo, InterfaceSpeed, InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, PortOperatingMode, SwitchInterface, Vlan, VlanList, parse_brocade_mac_address, shell::BrocadeShell, }; @@ -337,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: {}", @@ -352,8 +353,13 @@ impl BrocadeClient for NetworkOperatingSystemClient { format!("interface port-channel {}", channel_id), "no shutdown".into(), format!("description {channel_name}"), - "exit".into(), ]; + 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 { debug!( @@ -366,6 +372,10 @@ impl BrocadeClient for NetworkOperatingSystemClient { 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()); } diff --git a/examples/brocade_switch_configuration/src/main.rs b/examples/brocade_switch_configuration/src/main.rs index ba2afdd..a5a037c 100644 --- a/examples/brocade_switch_configuration/src/main.rs +++ b/examples/brocade_switch_configuration/src/main.rs @@ -24,7 +24,8 @@ fn get_switch_config() -> BrocadeSwitchConfig { }; BrocadeSwitchConfig { - ips: vec![ip!("192.168.12.147"), ip!("192.168.12.109")], + // 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, @@ -72,7 +73,7 @@ async fn main() { interfaces: vec![ // Trunk port with ALL VLANs, forced to 10Gbps InterfaceConfig { - interface: tengig(1, 0, 1), + interface: tengig(1, 0, 20), mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: Some(VlanList::All), @@ -80,7 +81,7 @@ async fn main() { }, // Trunk port with specific VLANs (MGMT + DATA only) InterfaceConfig { - interface: tengig(1, 0, 2), + interface: tengig(1, 0, 21), mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: Some(VlanList::Specific(vec![mgmt.clone(), data.clone()])), @@ -88,7 +89,7 @@ async fn main() { }, // Access port on the MGMT VLAN InterfaceConfig { - interface: tengig(1, 0, 3), + interface: tengig(1, 0, 22), mode: PortOperatingMode::Access, access_vlan: Some(mgmt.id), trunk_vlans: None, @@ -96,7 +97,7 @@ async fn main() { }, // Access port on the STORAGE VLAN InterfaceConfig { - interface: tengig(1, 0, 4), + interface: tengig(1, 0, 23), mode: PortOperatingMode::Access, access_vlan: Some(storage.id), trunk_vlans: None, @@ -110,7 +111,7 @@ async fn main() { PortChannelConfig { id: 1, name: "SERVER_BOND".to_string(), - ports: vec![PortLocation(1, 0, 5), PortLocation(1, 0, 6)], + 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()])), @@ -120,7 +121,7 @@ async fn main() { PortChannelConfig { id: 2, name: "BACKUP_BOND".to_string(), - ports: vec![PortLocation(1, 0, 7), PortLocation(1, 0, 8)], + ports: vec![PortLocation(1, 0, 26), PortLocation(1, 0, 27)], mode: PortOperatingMode::Trunk, access_vlan: None, trunk_vlans: Some(VlanList::All), diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 74dbbce..ffd42ea 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use brocade::{InterfaceConfig, PortChannelConfig, PortChannelId, Vlan}; +use brocade::{InterfaceConfig, InterfaceSpeed, PortChannelConfig, PortChannelId, Vlan}; use harmony_k8s::K8sClient; use harmony_macros::ip; use harmony_types::{ @@ -330,6 +330,7 @@ impl Switch for HAClusterTopology { channel_id, &format!("Harmony_{}", config.host_id), switch_ports, + None, ) .await .map_err(|e| SwitchError::new(format!("Failed to configure port-channel: {e}")))?; @@ -342,7 +343,12 @@ impl Switch for HAClusterTopology { config: &PortChannelConfig, ) -> Result<(), SwitchError> { self.switch_client - .configure_port_channel(config.id, &config.name, config.ports.clone()) + .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(()) @@ -624,6 +630,7 @@ impl SwitchClient for DummyInfra { _channel_id: PortChannelId, _channel_name: &str, _switch_ports: Vec, + _speed: Option<&InterfaceSpeed>, ) -> Result { unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 0dcd75b..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::{InterfaceConfig, PortChannelConfig, PortChannelId, Vlan}; +use brocade::{InterfaceConfig, InterfaceSpeed, PortChannelConfig, PortChannelId, Vlan}; use derive_new::new; use harmony_k8s::K8sClient; use harmony_types::{ @@ -306,6 +306,7 @@ pub trait SwitchClient: Debug + Send + Sync { channel_id: PortChannelId, channel_name: &str, switch_ports: Vec, + speed: Option<&InterfaceSpeed>, ) -> Result; async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError>; diff --git a/harmony/src/infra/brocade.rs b/harmony/src/infra/brocade.rs index 4d6b31d..fa89b92 100644 --- a/harmony/src/infra/brocade.rs +++ b/harmony/src/infra/brocade.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use brocade::{ - BrocadeClient, BrocadeOptions, InterSwitchLink, InterfaceConfig, InterfaceStatus, - PortChannelId, PortOperatingMode, Vlan, + BrocadeClient, BrocadeOptions, InterSwitchLink, InterfaceConfig, InterfaceSpeed, + InterfaceStatus, PortChannelId, PortOperatingMode, Vlan, }; use harmony_types::{ @@ -130,9 +130,10 @@ impl SwitchClient for BrocadeSwitchClient { channel_id: PortChannelId, channel_name: &str, switch_ports: Vec, + speed: Option<&InterfaceSpeed>, ) -> Result { self.brocade - .create_port_channel(channel_id, channel_name, &switch_ports) + .create_port_channel(channel_id, channel_name, &switch_ports, speed) .await .map_err(|e| SwitchError::new(format!("{e}")))?; @@ -206,6 +207,7 @@ impl SwitchClient for UnmanagedSwitch { _channel_id: PortChannelId, _channel_name: &str, _switch_ports: Vec, + _speed: Option<&InterfaceSpeed>, ) -> Result { todo!("unmanaged switch. Nothing to do.") } @@ -237,8 +239,8 @@ mod tests { use async_trait::async_trait; use brocade::{ BrocadeClient, BrocadeInfo, Error, InterSwitchLink, InterfaceConfig, InterfaceInfo, - InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, PortOperatingMode, - SecurityLevel, Vlan, + InterfaceSpeed, InterfaceStatus, InterfaceType, MacAddressEntry, PortChannelId, + PortOperatingMode, SecurityLevel, Vlan, }; use harmony_types::switch::PortLocation; @@ -417,6 +419,7 @@ mod tests { _channel_id: PortChannelId, _channel_name: &str, _ports: &[PortLocation], + _speed: Option<&InterfaceSpeed>, ) -> Result<(), Error> { todo!() } diff --git a/harmony/src/modules/brocade/brocade.rs b/harmony/src/modules/brocade/brocade.rs index 56b6517..5a4ac9c 100644 --- a/harmony/src/modules/brocade/brocade.rs +++ b/harmony/src/modules/brocade/brocade.rs @@ -138,7 +138,12 @@ impl Switch for SwitchTopology { config: &PortChannelConfig, ) -> Result<(), SwitchError> { self.client - .configure_port_channel(config.id, &config.name, config.ports.clone()) + .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(()) -- 2.39.5