Compare commits

..

2 Commits

Author SHA1 Message Date
0e29e9e32d feat(host_network): configure bonds on the host and switch port channels
All checks were successful
Run Check Script / check (pull_request) Successful in 1m6s
2025-10-15 15:53:18 -04:00
c0bd8007c7 feat(switch/brocade): Implement client to interact with Brocade Switch
All checks were successful
Run Check Script / check (pull_request) Successful in 1m5s
* Expose a high-level `brocade::init()` function to connect to a Brocade switch and automatically pick the best implementation based on its OS and version
* Implement a client for Brocade switches running on Network Operating System (NOS)
* Implement a client for older Brocade switches running on FastIron (partial implementation)

The architecture for the library is based on 3 layers:
1. The `BrocadeClient` trait to describe the available capabilities to
   interact with a Brocade switch. It is partly opinionated in order to
   offer higher level features to group multiple commands into a single
   function (e.g. create a port channel). Its implementations are
   basically just the commands to run on the switch and the functions to
   parse the output.
2. The `BrocadeShell` struct to make it easier to authenticate, send commands, and interact with the switch.
3. The `ssh` module to actually connect to the switch over SSH and execute the commands.

With time, we will add support for more Brocade switches and their various OS/versions. If needed, shared behavior could be extracted into a separate module to make it easier to add new implementations.
2025-10-15 15:28:24 -04:00
7 changed files with 23 additions and 56 deletions

12
Cargo.lock generated
View File

@@ -1847,18 +1847,6 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "example-penpot"
version = "0.1.0"
dependencies = [
"harmony",
"harmony_cli",
"harmony_macros",
"harmony_types",
"tokio",
"url",
]
[[package]] [[package]]
name = "example-pxe" name = "example-pxe"
version = "0.1.0" version = "0.1.0"

View File

@@ -70,7 +70,7 @@ impl FastIronClient {
Some(Ok(InterSwitchLink { Some(Ok(InterSwitchLink {
local_port, local_port,
remote_port: None, // FIXME: Map the remote port as well remote_port: None,
})) }))
} }

View File

@@ -12,11 +12,11 @@ pub type FirewallGroup = Vec<PhysicalHost>;
pub struct PhysicalHost { pub struct PhysicalHost {
pub id: Id, pub id: Id,
pub category: HostCategory, pub category: HostCategory,
pub network: Vec<NetworkInterface>, // FIXME: Don't use harmony_inventory_agent::NetworkInterface pub network: Vec<NetworkInterface>,
pub storage: Vec<StorageDrive>, // FIXME: Don't use harmony_inventory_agent::StorageDrive pub storage: Vec<StorageDrive>,
pub labels: Vec<Label>, pub labels: Vec<Label>,
pub memory_modules: Vec<MemoryModule>, // FIXME: Don't use harmony_inventory_agent::MemoryModule pub memory_modules: Vec<MemoryModule>,
pub cpus: Vec<CPU>, // FIXME: Don't use harmony_inventory_agent::CPU pub cpus: Vec<CPU>,
} }
impl PhysicalHost { impl PhysicalHost {

View File

@@ -2,9 +2,10 @@ use async_trait::async_trait;
use brocade::BrocadeOptions; use brocade::BrocadeOptions;
use harmony_macros::ip; use harmony_macros::ip;
use harmony_secret::SecretManager; use harmony_secret::SecretManager;
use harmony_types::net::MacAddress; use harmony_types::{
use harmony_types::net::Url; net::{MacAddress, Url},
use harmony_types::switch::PortLocation; switch::PortLocation,
};
use k8s_openapi::api::core::v1::Namespace; use k8s_openapi::api::core::v1::Namespace;
use kube::api::ObjectMeta; use kube::api::ObjectMeta;
use log::debug; use log::debug;
@@ -15,40 +16,19 @@ use crate::executors::ExecutorError;
use crate::hardware::PhysicalHost; use crate::hardware::PhysicalHost;
use crate::infra::brocade::BrocadeSwitchAuth; use crate::infra::brocade::BrocadeSwitchAuth;
use crate::infra::brocade::BrocadeSwitchClient; use crate::infra::brocade::BrocadeSwitchClient;
use crate::modules::okd::crd::InstallPlanApproval; use crate::modules::okd::crd::{
use crate::modules::okd::crd::OperatorGroup; InstallPlanApproval, OperatorGroup, OperatorGroupSpec, Subscription, SubscriptionSpec,
use crate::modules::okd::crd::OperatorGroupSpec; nmstate::{self, NMState, NodeNetworkConfigurationPolicy, NodeNetworkConfigurationPolicySpec},
use crate::modules::okd::crd::Subscription; };
use crate::modules::okd::crd::SubscriptionSpec;
use crate::modules::okd::crd::nmstate;
use crate::modules::okd::crd::nmstate::NMState;
use crate::modules::okd::crd::nmstate::NodeNetworkConfigurationPolicy;
use crate::modules::okd::crd::nmstate::NodeNetworkConfigurationPolicySpec;
use crate::topology::PxeOptions; use crate::topology::PxeOptions;
use super::DHCPStaticEntry; use super::{
use super::DhcpServer; DHCPStaticEntry, DhcpServer, DnsRecord, DnsRecordType, DnsServer, Firewall, HostNetworkConfig,
use super::DnsRecord; HttpServer, IpAddress, K8sclient, LoadBalancer, LoadBalancerService, LogicalHost,
use super::DnsRecordType; PreparationError, PreparationOutcome, Router, Switch, SwitchClient, SwitchError, TftpServer,
use super::DnsServer; Topology, k8s::K8sClient,
use super::Firewall; };
use super::HostNetworkConfig;
use super::HttpServer;
use super::IpAddress;
use super::K8sclient;
use super::LoadBalancer;
use super::LoadBalancerService;
use super::LogicalHost;
use super::PreparationError;
use super::PreparationOutcome;
use super::Router;
use super::Switch;
use super::SwitchClient;
use super::SwitchError;
use super::TftpServer;
use super::Topology;
use super::k8s::K8sClient;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::net::IpAddr; use std::net::IpAddr;
use std::sync::Arc; use std::sync::Arc;
@@ -533,7 +513,7 @@ impl Switch for HAClusterTopology {
host: &PhysicalHost, host: &PhysicalHost,
config: HostNetworkConfig, config: HostNetworkConfig,
) -> Result<(), SwitchError> { ) -> Result<(), SwitchError> {
// self.configure_bond(host, &config).await?; self.configure_bond(host, &config).await?;
self.configure_port_channel(host, &config).await self.configure_port_channel(host, &config).await
} }
} }

View File

@@ -215,7 +215,7 @@ impl OKDSetup03ControlPlaneInterpret {
) -> Result<(), InterpretError> { ) -> Result<(), InterpretError> {
info!("[ControlPlane] Ensuring persistent bonding"); info!("[ControlPlane] Ensuring persistent bonding");
let score = HostNetworkConfigurationScore { let score = HostNetworkConfigurationScore {
hosts: hosts.clone(), // FIXME: Avoid clone if possible hosts: hosts.clone(),
}; };
score.interpret(inventory, topology).await?; score.interpret(inventory, topology).await?;

View File

@@ -240,7 +240,7 @@ pub struct OvsPortSpec {
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct EthtoolSpec { pub struct EthtoolSpec {
// FIXME: Properly describe this spec (https://nmstate.io/devel/yaml_api.html#ethtool) // TODO: Properly describe this spec (https://nmstate.io/devel/yaml_api.html#ethtool)
} }
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]

View File

@@ -50,6 +50,7 @@ impl HostNetworkConfigurationInterpret {
Ok(()) Ok(())
} }
async fn collect_switch_ports_for_host<T: Topology + Switch>( async fn collect_switch_ports_for_host<T: Topology + Switch>(
&self, &self,
topology: &T, topology: &T,
@@ -125,7 +126,6 @@ impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
let mut configured_host_count = 0; let mut configured_host_count = 0;
for host in &self.score.hosts { for host in &self.score.hosts {
// FIXME: Clear the previous config for host
self.configure_network_for_host(topology, host).await?; self.configure_network_for_host(topology, host).await?;
configured_host_count += 1; configured_host_count += 1;
} }
@@ -283,7 +283,6 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn port_not_found_for_mac_address_should_not_configure_interface() { async fn port_not_found_for_mac_address_should_not_configure_interface() {
// FIXME: Should it still configure an empty bond/port channel?
let score = given_score(vec![given_host(&HOST_ID, vec![UNKNOWN_INTERFACE.clone()])]); let score = given_score(vec![given_host(&HOST_ID, vec![UNKNOWN_INTERFACE.clone()])]);
let topology = TopologyWithSwitch::new_port_not_found(); let topology = TopologyWithSwitch::new_port_not_found();