WIP: configure-switch #159

Closed
johnride wants to merge 18 commits from configure-switch into master
7 changed files with 171 additions and 65 deletions
Showing only changes of commit f2f55d98d4 - Show all commits

View File

@ -1,4 +1,5 @@
use std::{ use std::{
borrow::Cow,
fmt::{self, Display}, fmt::{self, Display},
sync::Arc, sync::Arc,
}; };
@ -6,8 +7,11 @@ use std::{
use async_trait::async_trait; use async_trait::async_trait;
use harmony_types::net::{IpAddress, MacAddress}; use harmony_types::net::{IpAddress, MacAddress};
use log::{debug, info}; use log::{debug, info};
use russh::client::{Handle, Handler}; use russh::{
use russh_keys::key; client::{Handle, Handler},
kex::DH_G1_SHA1,
};
use russh_keys::key::{self, SSH_RSA};
use std::str::FromStr; use std::str::FromStr;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
@ -19,6 +23,30 @@ pub struct MacAddressEntry {
pub struct BrocadeClient { pub struct BrocadeClient {
client: Handle<Client>, client: Handle<Client>,
options: BrocadeOptions,
}
#[derive(Default, Clone, Debug)]
pub struct BrocadeOptions {
pub dry_run: bool,
pub ssh: SshOptions,
}
#[derive(Clone, Debug)]
pub struct SshOptions {
pub preferred_algorithms: russh::Preferred,
}
impl Default for SshOptions {
fn default() -> Self {
Self {
preferred_algorithms: russh::Preferred {
kex: Cow::Borrowed(&[DH_G1_SHA1]),
key: Cow::Borrowed(&[SSH_RSA]),
..Default::default()
},
}
}
} }
impl BrocadeClient { impl BrocadeClient {
@ -26,14 +54,25 @@ impl BrocadeClient {
ip_addresses: &[IpAddress], ip_addresses: &[IpAddress],
username: &str, username: &str,
password: &str, password: &str,
options: Option<BrocadeOptions>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let ip = ip_addresses[0]; // FIXME: Find a better way to get master switch IP address if ip_addresses.is_empty() {
return Err(Error::ConfigurationError(
"No IP addresses provided".to_string(),
));
}
let config = russh::client::Config::default(); let ip = ip_addresses[0]; // FIXME: Find a better way to get master switch IP address
let options = options.unwrap_or_default();
let config = russh::client::Config {
preferred: options.ssh.preferred_algorithms.clone(),
..Default::default()
};
let mut client = russh::client::connect(Arc::new(config), (ip, 22), Client {}).await?; let mut client = russh::client::connect(Arc::new(config), (ip, 22), Client {}).await?;
match client.authenticate_password(username, password).await? { match client.authenticate_password(username, password).await? {
true => Ok(Self { client }), true => Ok(Self { client, options }),
false => Err(Error::AuthenticationError( false => Err(Error::AuthenticationError(
"ssh authentication failed".to_string(), "ssh authentication failed".to_string(),
)), )),
@ -41,7 +80,7 @@ impl BrocadeClient {
} }
pub async fn show_mac_address_table(&self) -> Result<Vec<MacAddressEntry>, Error> { pub async fn show_mac_address_table(&self) -> Result<Vec<MacAddressEntry>, Error> {
let output = self.run_command("show mac-address-table").await?; let output = self.run_command("show mac-address").await?;
let mut entries = Vec::new(); let mut entries = Vec::new();
// The Brocade output usually has a header and then one entry per line. // The Brocade output usually has a header and then one entry per line.
@ -104,7 +143,7 @@ impl BrocadeClient {
pub async fn find_available_channel_id(&self) -> Result<u8, Error> { pub async fn find_available_channel_id(&self) -> Result<u8, Error> {
debug!("[Brocade] Finding next available channel id..."); debug!("[Brocade] Finding next available channel id...");
let output = self.run_command("show port-channel summary").await?; let output = self.run_command("show lag").await?;
let mut used_ids = Vec::new(); let mut used_ids = Vec::new();
// Sample output line: "3 Po3(SU) LACP Eth Yes 128/128 active " // Sample output line: "3 Po3(SU) LACP Eth Yes 128/128 active "
@ -121,7 +160,7 @@ impl BrocadeClient {
// Sort the used IDs to find the next available number. // Sort the used IDs to find the next available number.
used_ids.sort(); used_ids.sort();
let mut next_id = 1; let mut next_id = 0;
for &id in &used_ids { for &id in &used_ids {
if id == next_id { if id == next_id {
next_id += 1; next_id += 1;
@ -136,6 +175,11 @@ impl BrocadeClient {
} }
async fn run_command(&self, command: &str) -> Result<String, Error> { async fn run_command(&self, command: &str) -> Result<String, Error> {
if !command.starts_with("show") && self.options.dry_run {
info!("[Brocade] Dry-run mode enabled, skipping command: {command}");
return Ok("".into());
}
debug!("[Brocade] Running command: '{command}'..."); debug!("[Brocade] Running command: '{command}'...");
let mut channel = self.client.channel_open_session().await?; let mut channel = self.client.channel_open_session().await?;
@ -161,9 +205,13 @@ impl BrocadeClient {
))); )));
} }
} }
russh::ChannelMsg::Success russh::ChannelMsg::Eof => {
| russh::ChannelMsg::WindowAdjusted { .. } channel.close().await?;
| russh::ChannelMsg::Eof => {} }
russh::ChannelMsg::Close => {
break;
}
russh::ChannelMsg::Success | russh::ChannelMsg::WindowAdjusted { .. } => {}
_ => { _ => {
return Err(Error::UnexpectedError(format!( return Err(Error::UnexpectedError(format!(
"Russh got unexpected msg {msg:?}" "Russh got unexpected msg {msg:?}"
@ -172,20 +220,25 @@ impl BrocadeClient {
} }
} }
channel.close().await?;
let output = String::from_utf8(output).expect("Output should be UTF-8 compatible"); let output = String::from_utf8(output).expect("Output should be UTF-8 compatible");
debug!("[Brocade] Command output:\n{output}");
Ok(output) Ok(output)
} }
async fn run_commands(&self, commands: Vec<String>) -> Result<(), Error> { async fn run_commands(&self, commands: Vec<String>) -> Result<(), Error> {
let mut channel = self.client.channel_open_session().await?;
// Execute commands sequentially and check for errors immediately. // Execute commands sequentially and check for errors immediately.
for command in commands { for command in commands {
if !command.starts_with("show") && self.options.dry_run {
info!("[Brocade] Dry-run mode enabled, skipping command: {command}");
continue;
}
debug!("[Brocade] Running command: '{command}'..."); debug!("[Brocade] Running command: '{command}'...");
let mut channel = self.client.channel_open_session().await?;
let mut output = Vec::new(); let mut output = Vec::new();
let mut close_received = false;
channel.exec(true, command.as_str()).await?; channel.exec(true, command.as_str()).await?;
loop { loop {
@ -206,12 +259,30 @@ impl BrocadeClient {
))); )));
} }
} }
_ => {} // Ignore other messages like success or EOF for now. russh::ChannelMsg::Eof => {
channel.close().await?;
}
russh::ChannelMsg::Close => {
close_received = true;
break;
}
russh::ChannelMsg::Success | russh::ChannelMsg::WindowAdjusted { .. } => {}
_ => {
return Err(Error::UnexpectedError(format!(
"Russh got unexpected msg {msg:?}"
)));
}
} }
} }
if !close_received {
return Err(Error::UnexpectedError(format!(
"Channel closed without receiving a final CLOSE message for command: {}",
command
)));
}
} }
channel.close().await?;
Ok(()) Ok(())
} }
} }

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>, pub network: Vec<NetworkInterface>, // FIXME: Don't use harmony_inventory_agent::NetworkInterface
Review

I understand we don't want to use the "inventory agent" data structures, but I think it's OK to have one base data structure that we try to reuse across crates for basic components like NetworkInterface and these few others.

IMO, the FIXME should be about refactoring the type into a shared types crate or something like that.

Why would that not be correct?

I understand we don't want to use the "inventory agent" data structures, but I think it's OK to have one base data structure that we try to reuse across crates for basic components like NetworkInterface and these few others. IMO, the FIXME should be about refactoring the type into a shared types crate or something like that. Why would that not be correct?
Review

that's one of the options, yes 😉 the fixme is just stating the problem: we should not use directly this type from harmony_inventory_agent

possible solutions are:

  1. (what you suggested) move it to harmony_types and both harmony_inventory_agent and harmony uses it
  2. keep the harmony_inventory_agent::NetworkInterface as is and have a different structure in harmony and map from one to the other

As of now I'm personally 50/50: I have a feeling that the needs from harmony_inventory_agent might evolve differently from harmony thus the NetworkInterface might evolve differently. So going with option 1. wouldn't be adequate. But on the other hand for now they are indeed very similar so why maintain 2 structs? 🤷

Anyway for now it's not really a big issue and won't be hard to fix. So it's more like a note for the future to be aware of.

that's one of the options, yes 😉 the `fixme` is just stating the problem: we should not use directly this type from `harmony_inventory_agent` possible solutions are: 1. (what you suggested) move it to `harmony_types` and both `harmony_inventory_agent` and `harmony` uses it 2. keep the `harmony_inventory_agent::NetworkInterface` as is and have a different structure in `harmony` and map from one to the other As of now I'm personally 50/50: I have a feeling that the needs from `harmony_inventory_agent` might evolve differently from `harmony` thus the `NetworkInterface` might evolve differently. So going with option 1. wouldn't be adequate. But on the other hand for now they are indeed very similar so why maintain 2 structs? 🤷 Anyway for now it's not really a big issue and won't be hard to fix. So it's more like a note for the future to be aware of.
pub storage: Vec<StorageDrive>, pub storage: Vec<StorageDrive>, // FIXME: Don't use harmony_inventory_agent::StorageDrive
pub labels: Vec<Label>, pub labels: Vec<Label>,
pub memory_modules: Vec<MemoryModule>, pub memory_modules: Vec<MemoryModule>, // FIXME: Don't use harmony_inventory_agent::MemoryModule
pub cpus: Vec<CPU>, pub cpus: Vec<CPU>, // FIXME: Don't use harmony_inventory_agent::CPU
} }
impl PhysicalHost { impl PhysicalHost {

View File

@ -1,4 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
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::net::MacAddress;
@ -305,9 +306,14 @@ impl HAClusterTopology {
// FIXME: We assume Brocade switches // FIXME: We assume Brocade switches
let switches: Vec<IpAddr> = self.switch.iter().map(|s| s.ip).collect(); let switches: Vec<IpAddr> = self.switch.iter().map(|s| s.ip).collect();
let client = BrocadeSwitchClient::init(&switches, &auth.username, &auth.password) let brocade_options = Some(BrocadeOptions {
.await dry_run: *crate::config::DRY_RUN,
.map_err(|e| SwitchError::new(format!("Failed to connect to switch: {e}")))?; ..Default::default()
});
let client =
BrocadeSwitchClient::init(&switches, &auth.username, &auth.password, brocade_options)
.await
.map_err(|e| SwitchError::new(format!("Failed to connect to switch: {e}")))?;
Ok(Box::new(client)) Ok(Box::new(client))
} }
@ -506,14 +512,13 @@ impl HttpServer for HAClusterTopology {
#[async_trait] #[async_trait]
impl Switch for HAClusterTopology { impl Switch for HAClusterTopology {
async fn get_port_for_mac_address(&self, mac_address: &MacAddress) -> Option<String> { async fn get_port_for_mac_address(
let client = self.get_switch_client().await; &self,
mac_address: &MacAddress,
let Ok(client) = client else { ) -> Result<Option<String>, SwitchError> {
return None; let client = self.get_switch_client().await?;
}; let port = client.find_port(mac_address).await?;
Ok(port)
client.find_port(mac_address).await
} }
async fn configure_host_network( async fn configure_host_network(

View File

@ -175,7 +175,10 @@ impl FromStr for DnsRecordType {
#[async_trait] #[async_trait]
pub trait Switch: Send + Sync { pub trait Switch: Send + Sync {
async fn get_port_for_mac_address(&self, mac_address: &MacAddress) -> Option<String>; async fn get_port_for_mac_address(
&self,
mac_address: &MacAddress,
) -> Result<Option<String>, SwitchError>;
async fn configure_host_network( async fn configure_host_network(
&self, &self,
@ -218,7 +221,7 @@ impl Error for SwitchError {}
#[async_trait] #[async_trait]
pub trait SwitchClient: Send + Sync { pub trait SwitchClient: Send + Sync {
async fn find_port(&self, mac_address: &MacAddress) -> Option<String>; async fn find_port(&self, mac_address: &MacAddress) -> Result<Option<String>, SwitchError>;
async fn configure_port_channel(&self, switch_ports: Vec<String>) -> Result<u8, SwitchError>; async fn configure_port_channel(&self, switch_ports: Vec<String>) -> Result<u8, SwitchError>;
} }

View File

@ -1,5 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use brocade::BrocadeClient; use brocade::{BrocadeClient, BrocadeOptions};
use harmony_secret::Secret; use harmony_secret::Secret;
use harmony_types::net::{IpAddress, MacAddress}; use harmony_types::net::{IpAddress, MacAddress};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -15,23 +15,26 @@ impl BrocadeSwitchClient {
ip_addresses: &[IpAddress], ip_addresses: &[IpAddress],
username: &str, username: &str,
password: &str, password: &str,
options: Option<BrocadeOptions>,
) -> Result<Self, brocade::Error> { ) -> Result<Self, brocade::Error> {
let brocade = BrocadeClient::init(ip_addresses, username, password).await?; let brocade = BrocadeClient::init(ip_addresses, username, password, options).await?;
Ok(Self { brocade }) Ok(Self { brocade })
} }
} }
#[async_trait] #[async_trait]
impl SwitchClient for BrocadeSwitchClient { impl SwitchClient for BrocadeSwitchClient {
async fn find_port(&self, mac_address: &MacAddress) -> Option<String> { async fn find_port(&self, mac_address: &MacAddress) -> Result<Option<String>, SwitchError> {
let Ok(table) = self.brocade.show_mac_address_table().await else { let table = self
return None; .brocade
}; .show_mac_address_table()
.await
.map_err(|e| SwitchError::new(format!("Failed to get mac address table: {e}")))?;
table Ok(table
.iter() .iter()
.find(|entry| entry.mac_address == *mac_address) .find(|entry| entry.mac_address == *mac_address)
.map(|entry| entry.port_name.clone()) .map(|entry| entry.port_name.clone()))
} }
async fn configure_port_channel(&self, switch_ports: Vec<String>) -> Result<u8, SwitchError> { async fn configure_port_channel(&self, switch_ports: Vec<String>) -> Result<u8, SwitchError> {

View File

@ -1,5 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use harmony_types::id::Id; use harmony_types::id::Id;
use log::{debug, info, warn};
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
@ -56,31 +57,52 @@ impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
_inventory: &Inventory, _inventory: &Inventory,
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
info!(
"Started network configuration for {} host(s)...",
self.score.hosts.len()
);
let mut configured_host_count = 0;
for host in &self.score.hosts { for host in &self.score.hosts {
let mut switch_ports = vec![]; let mut switch_ports = vec![];
for network_interface in &host.network { for network_interface in &host.network {
let mac_address = network_interface.mac_address; let mac_address = network_interface.mac_address;
if let Some(port_name) = topology.get_port_for_mac_address(&mac_address).await { match topology.get_port_for_mac_address(&mac_address).await {
switch_ports.push(SwitchPort { Ok(Some(port_name)) => {
interface: NetworkInterface { switch_ports.push(SwitchPort {
name: network_interface.name.clone(), interface: NetworkInterface {
mac_address, name: network_interface.name.clone(),
speed_mbps: network_interface.speed_mbps, mac_address,
mtu: network_interface.mtu, speed_mbps: network_interface.speed_mbps,
}, mtu: network_interface.mtu,
port_name, },
}); port_name,
});
}
Ok(None) => debug!("No port found for host '{}', skipping", host.id),
Err(e) => warn!("Failed to get port for host '{}': {}", host.id, e),
} }
} }
let _ = topology if !switch_ports.is_empty() {
.configure_host_network(host, HostNetworkConfig { switch_ports }) configured_host_count += 1;
.await; let _ = topology
.configure_host_network(host, HostNetworkConfig { switch_ports })
.await;
}
} }
Ok(Outcome::success("".into())) if configured_host_count > 0 {
Ok(Outcome::success(format!(
"Configured {configured_host_count}/{} host(s)",
self.score.hosts.len()
)))
} else {
Ok(Outcome::noop("No hosts configured".into()))
}
} }
} }
@ -221,12 +243,7 @@ mod tests {
let _ = score.interpret(&Inventory::empty(), &topology).await; let _ = score.interpret(&Inventory::empty(), &topology).await;
let configured_host_networks = topology.configured_host_networks.lock().unwrap(); let configured_host_networks = topology.configured_host_networks.lock().unwrap();
assert_that!(*configured_host_networks).contains_exactly(vec![( assert_that!(*configured_host_networks).is_empty();
HOST_ID.clone(),
HostNetworkConfig {
switch_ports: vec![],
},
)]);
} }
fn given_score(hosts: Vec<PhysicalHost>) -> HostNetworkConfigurationScore { fn given_score(hosts: Vec<PhysicalHost>) -> HostNetworkConfigurationScore {
@ -297,12 +314,15 @@ mod tests {
#[async_trait] #[async_trait]
impl Switch for TopologyWithSwitch { impl Switch for TopologyWithSwitch {
async fn get_port_for_mac_address(&self, _mac_address: &MacAddress) -> Option<String> { async fn get_port_for_mac_address(
&self,
_mac_address: &MacAddress,
) -> Result<Option<String>, SwitchError> {
let mut ports = self.available_ports.lock().unwrap(); let mut ports = self.available_ports.lock().unwrap();
if ports.is_empty() { if ports.is_empty() {
return None; return Ok(None);
} }
Some(ports.remove(0)) Ok(Some(ports.remove(0)))
} }
async fn configure_host_network( async fn configure_host_network(

View File

@ -54,6 +54,9 @@ struct DeployArgs {
#[arg(long = "profile", short = 'p', default_value = "dev")] #[arg(long = "profile", short = 'p', default_value = "dev")]
harmony_profile: HarmonyProfile, harmony_profile: HarmonyProfile,
#[arg(long = "dry-run", short = 'd', default_value = "false")]
dry_run: bool,
} }
#[derive(Args, Clone, Debug)] #[derive(Args, Clone, Debug)]
@ -178,6 +181,7 @@ async fn main() {
command command
.env("HARMONY_USE_LOCAL_K3D", format!("{use_local_k3d}")) .env("HARMONY_USE_LOCAL_K3D", format!("{use_local_k3d}"))
.env("HARMONY_PROFILE", format!("{}", args.harmony_profile)) .env("HARMONY_PROFILE", format!("{}", args.harmony_profile))
.env("HARMONY_DRY_RUN", format!("{}", args.dry_run))
.arg("-y") .arg("-y")
.arg("-a"); .arg("-a");