## Description * Replace the CatalogSource approach to install the OperatorHub.io catalog by a more simple & straightforward way to install NMState * Improve logging * Add report summarizing the host network configuration that was applied (which host, bonds, port-channels) * Fix command to find next available port channel id ## Extra info Using the `apply_url` approach to install the NMState operator isn't the best approach: it's harder to maintain and upgrade. But it helps us achieve waht we wanted for now: install the NMState Operator to configure bonds on a host. The preferred approach, installing an operator from the OperatorHub.io catalog, didn't work for now. We had a timeout error with DeadlineExceeded probably caused by an insufficient CPU/Memory allocation to query such a big catalog, even though we tweaked the RAM allocation (we couldn't find a way to do it for CPU). Spent too much time on this so we stopped these efforts for now. It would be good to get back to it when we need to install something else from a custom catalog. Reviewed-on: #175
334 lines
10 KiB
Rust
334 lines
10 KiB
Rust
use std::str::FromStr;
|
|
|
|
use async_trait::async_trait;
|
|
use harmony_types::switch::{PortDeclaration, PortLocation};
|
|
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,
|
|
};
|
|
|
|
#[derive(Debug)]
|
|
pub struct NetworkOperatingSystemClient {
|
|
shell: BrocadeShell,
|
|
version: BrocadeInfo,
|
|
}
|
|
|
|
impl NetworkOperatingSystemClient {
|
|
pub fn init(mut shell: BrocadeShell, version_info: BrocadeInfo) -> Self {
|
|
shell.before_all(vec!["terminal length 0".into()]);
|
|
|
|
Self {
|
|
shell,
|
|
version: version_info,
|
|
}
|
|
}
|
|
|
|
fn parse_mac_entry(&self, line: &str) -> Option<Result<MacAddressEntry, Error>> {
|
|
debug!("[Brocade] Parsing mac address entry: {line}");
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
if parts.len() < 5 {
|
|
return None;
|
|
}
|
|
|
|
let (vlan, mac_address, port) = match parts.len() {
|
|
5 => (
|
|
u16::from_str(parts[0]).ok()?,
|
|
parse_brocade_mac_address(parts[1]).ok()?,
|
|
parts[4].to_string(),
|
|
),
|
|
_ => (
|
|
u16::from_str(parts[0]).ok()?,
|
|
parse_brocade_mac_address(parts[1]).ok()?,
|
|
parts[5].to_string(),
|
|
),
|
|
};
|
|
|
|
let port =
|
|
PortDeclaration::parse(&port).map_err(|e| Error::UnexpectedError(format!("{e}")));
|
|
|
|
match port {
|
|
Ok(p) => Some(Ok(MacAddressEntry {
|
|
vlan,
|
|
mac_address,
|
|
port: p,
|
|
})),
|
|
Err(e) => Some(Err(e)),
|
|
}
|
|
}
|
|
|
|
fn parse_inter_switch_link_entry(&self, line: &str) -> Option<Result<InterSwitchLink, Error>> {
|
|
debug!("[Brocade] Parsing inter switch link entry: {line}");
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
if parts.len() < 10 {
|
|
return None;
|
|
}
|
|
|
|
let local_port = PortLocation::from_str(parts[2]).ok()?;
|
|
let remote_port = PortLocation::from_str(parts[5]).ok()?;
|
|
|
|
Some(Ok(InterSwitchLink {
|
|
local_port,
|
|
remote_port: Some(remote_port),
|
|
}))
|
|
}
|
|
|
|
fn parse_interface_status_entry(&self, line: &str) -> Option<Result<InterfaceInfo, Error>> {
|
|
debug!("[Brocade] Parsing interface status entry: {line}");
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
if parts.len() < 6 {
|
|
return None;
|
|
}
|
|
|
|
let interface_type = match parts[0] {
|
|
"Fo" => InterfaceType::Ethernet("FortyGigabitEthernet".to_string()),
|
|
"Te" => InterfaceType::Ethernet("TenGigabitEthernet".to_string()),
|
|
_ => return None,
|
|
};
|
|
let port_location = PortLocation::from_str(parts[1]).ok()?;
|
|
let status = match parts[2] {
|
|
"connected" => InterfaceStatus::Connected,
|
|
"notconnected" => InterfaceStatus::NotConnected,
|
|
"sfpAbsent" => InterfaceStatus::SfpAbsent,
|
|
_ => return None,
|
|
};
|
|
let operating_mode = match parts[3] {
|
|
"ISL" => Some(PortOperatingMode::Fabric),
|
|
"Trunk" => Some(PortOperatingMode::Trunk),
|
|
"Access" => Some(PortOperatingMode::Access),
|
|
"--" => None,
|
|
_ => return None,
|
|
};
|
|
|
|
Some(Ok(InterfaceInfo {
|
|
name: format!("{interface_type} {port_location}"),
|
|
port_location,
|
|
interface_type,
|
|
operating_mode,
|
|
status,
|
|
}))
|
|
}
|
|
|
|
fn map_configure_interfaces_error(&self, err: Error) -> Error {
|
|
debug!("[Brocade] {err}");
|
|
|
|
if let Error::CommandError(message) = &err {
|
|
if message.contains("switchport")
|
|
&& message.contains("Cannot configure aggregator member")
|
|
{
|
|
let re = Regex::new(r"\(conf-if-([a-zA-Z]+)-([\d/]+)\)#").unwrap();
|
|
|
|
if let Some(caps) = re.captures(message) {
|
|
let interface_type = &caps[1];
|
|
let port_location = &caps[2];
|
|
let interface = format!("{interface_type} {port_location}");
|
|
|
|
return Error::CommandError(format!(
|
|
"Cannot configure interface '{interface}', it is a member of a port-channel (LAG)"
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
err
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl BrocadeClient for NetworkOperatingSystemClient {
|
|
async fn version(&self) -> Result<BrocadeInfo, Error> {
|
|
Ok(self.version.clone())
|
|
}
|
|
|
|
async fn get_mac_address_table(&self) -> Result<Vec<MacAddressEntry>, Error> {
|
|
let output = self
|
|
.shell
|
|
.run_command("show mac-address-table", ExecutionMode::Regular)
|
|
.await?;
|
|
|
|
output
|
|
.lines()
|
|
.skip(1)
|
|
.filter_map(|line| self.parse_mac_entry(line))
|
|
.collect()
|
|
}
|
|
|
|
async fn get_stack_topology(&self) -> Result<Vec<InterSwitchLink>, Error> {
|
|
let output = self
|
|
.shell
|
|
.run_command("show fabric isl", ExecutionMode::Regular)
|
|
.await?;
|
|
|
|
output
|
|
.lines()
|
|
.skip(6)
|
|
.filter_map(|line| self.parse_inter_switch_link_entry(line))
|
|
.collect()
|
|
}
|
|
|
|
async fn get_interfaces(&self) -> Result<Vec<InterfaceInfo>, Error> {
|
|
let output = self
|
|
.shell
|
|
.run_command(
|
|
"show interface status rbridge-id all",
|
|
ExecutionMode::Regular,
|
|
)
|
|
.await?;
|
|
|
|
output
|
|
.lines()
|
|
.skip(2)
|
|
.filter_map(|line| self.parse_interface_status_entry(line))
|
|
.collect()
|
|
}
|
|
|
|
async fn configure_interfaces(
|
|
&self,
|
|
interfaces: Vec<(String, PortOperatingMode)>,
|
|
) -> Result<(), Error> {
|
|
info!("[Brocade] Configuring {} interface(s)...", interfaces.len());
|
|
|
|
let mut commands = vec!["configure terminal".to_string()];
|
|
|
|
for interface in interfaces {
|
|
commands.push(format!("interface {}", interface.0));
|
|
|
|
match interface.1 {
|
|
PortOperatingMode::Fabric => {
|
|
commands.push("fabric isl enable".into());
|
|
commands.push("fabric trunk enable".into());
|
|
}
|
|
PortOperatingMode::Trunk => {
|
|
commands.push("switchport".into());
|
|
commands.push("switchport mode trunk".into());
|
|
commands.push("no spanning-tree shutdown".into());
|
|
commands.push("no fabric isl enable".into());
|
|
commands.push("no fabric trunk enable".into());
|
|
}
|
|
PortOperatingMode::Access => {
|
|
commands.push("switchport".into());
|
|
commands.push("switchport mode access".into());
|
|
commands.push("switchport access vlan 1".into());
|
|
commands.push("no spanning-tree shutdown".into());
|
|
commands.push("no fabric isl enable".into());
|
|
commands.push("no fabric trunk enable".into());
|
|
}
|
|
}
|
|
|
|
commands.push("no shutdown".into());
|
|
commands.push("exit".into());
|
|
}
|
|
|
|
self.shell
|
|
.run_commands(commands, ExecutionMode::Regular)
|
|
.await
|
|
.map_err(|err| self.map_configure_interfaces_error(err))?;
|
|
|
|
info!("[Brocade] Interfaces configured.");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn find_available_channel_id(&self) -> Result<PortChannelId, Error> {
|
|
info!("[Brocade] Finding next available channel id...");
|
|
|
|
let output = self
|
|
.shell
|
|
.run_command("show port-channel summary", ExecutionMode::Regular)
|
|
.await?;
|
|
|
|
let used_ids: Vec<u8> = output
|
|
.lines()
|
|
.skip(6)
|
|
.filter_map(|line| {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
if parts.len() < 8 {
|
|
return None;
|
|
}
|
|
|
|
u8::from_str(parts[0]).ok()
|
|
})
|
|
.collect();
|
|
|
|
let mut next_id: u8 = 1;
|
|
loop {
|
|
if !used_ids.contains(&next_id) {
|
|
break;
|
|
}
|
|
next_id += 1;
|
|
}
|
|
|
|
info!("[Brocade] Found channel id: {next_id}");
|
|
Ok(next_id)
|
|
}
|
|
|
|
async fn create_port_channel(
|
|
&self,
|
|
channel_id: PortChannelId,
|
|
channel_name: &str,
|
|
ports: &[PortLocation],
|
|
) -> Result<(), Error> {
|
|
info!(
|
|
"[Brocade] Configuring port-channel '{channel_id} {channel_name}' with ports: {}",
|
|
ports
|
|
.iter()
|
|
.map(|p| format!("{p}"))
|
|
.collect::<Vec<String>>()
|
|
.join(", ")
|
|
);
|
|
|
|
let interfaces = self.get_interfaces().await?;
|
|
|
|
let mut commands = vec![
|
|
"configure terminal".into(),
|
|
format!("interface port-channel {}", channel_id),
|
|
"no shutdown".into(),
|
|
"exit".into(),
|
|
];
|
|
|
|
for port in ports {
|
|
let interface = interfaces.iter().find(|i| i.port_location == *port);
|
|
let Some(interface) = interface else {
|
|
continue;
|
|
};
|
|
|
|
commands.push(format!("interface {}", interface.name));
|
|
commands.push("no switchport".into());
|
|
commands.push("no ip address".into());
|
|
commands.push("no fabric isl enable".into());
|
|
commands.push("no fabric trunk enable".into());
|
|
commands.push(format!("channel-group {channel_id} mode active"));
|
|
commands.push("no shutdown".into());
|
|
commands.push("exit".into());
|
|
}
|
|
|
|
self.shell
|
|
.run_commands(commands, ExecutionMode::Regular)
|
|
.await?;
|
|
|
|
info!("[Brocade] Port-channel '{channel_name}' configured.");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn clear_port_channel(&self, channel_name: &str) -> Result<(), Error> {
|
|
info!("[Brocade] Clearing port-channel: {channel_name}");
|
|
|
|
let commands = vec![
|
|
"configure terminal".into(),
|
|
format!("no interface port-channel {}", channel_name),
|
|
"exit".into(),
|
|
];
|
|
|
|
self.shell
|
|
.run_commands(commands, ExecutionMode::Regular)
|
|
.await?;
|
|
|
|
info!("[Brocade] Port-channel '{channel_name}' cleared.");
|
|
Ok(())
|
|
}
|
|
}
|