WIP: configure-switch #159
1
Cargo.lock
generated
@ -680,6 +680,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"harmony_types",
|
||||
"log",
|
||||
"russh",
|
||||
"russh-keys",
|
||||
"tokio",
|
||||
|
@ -7,7 +7,8 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
harmony_types = { version = "0.1.0", path = "../harmony_types" }
|
||||
harmony_types = { path = "../harmony_types" }
|
||||
russh.workspace = true
|
||||
russh-keys.workspace = true
|
||||
tokio.workspace = true
|
||||
log.workspace = true
|
||||
|
@ -5,6 +5,7 @@ use std::{
|
||||
|
||||
use async_trait::async_trait;
|
||||
use harmony_types::net::{IpAddress, MacAddress};
|
||||
use log::{debug, info};
|
||||
use russh::client::{Handle, Handler};
|
||||
use russh_keys::key;
|
||||
use std::str::FromStr;
|
||||
@ -21,7 +22,12 @@ pub struct BrocadeClient {
|
||||
}
|
||||
|
||||
impl BrocadeClient {
|
||||
pub async fn init(ip: IpAddress, username: &str, password: &str) -> Result<Self, Error> {
|
||||
pub async fn init(
|
||||
ip_addresses: &[IpAddress],
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<Self, Error> {
|
||||
let ip = ip_addresses[0];
|
||||
let config = russh::client::Config::default();
|
||||
let mut client = russh::client::connect(Arc::new(config), (ip, 22), Client {}).await?;
|
||||
|
||||
@ -64,6 +70,8 @@ impl BrocadeClient {
|
||||
}
|
||||
|
||||
pub async fn configure_port_channel(&self, ports: &[String]) -> Result<u8, Error> {
|
||||
info!("[Brocade] Configuring port-channel with ports: {ports:?}");
|
||||
|
||||
let channel_id = self.find_available_channel_id().await?;
|
||||
let mut commands = Vec::new();
|
||||
|
||||
@ -93,7 +101,8 @@ impl BrocadeClient {
|
||||
}
|
||||
|
||||
pub async fn find_available_channel_id(&self) -> Result<u8, Error> {
|
||||
// FIXME: The command might vary slightly by Brocade OS version.
|
||||
debug!("[Brocade] Finding next available channel id...");
|
||||
|
||||
let output = self.run_command("show port-channel summary").await?;
|
||||
let mut used_ids = Vec::new();
|
||||
|
||||
@ -121,10 +130,13 @@ impl BrocadeClient {
|
||||
}
|
||||
}
|
||||
|
||||
debug!("[Brocade] Found channel id '{next_id}'");
|
||||
Ok(next_id)
|
||||
}
|
||||
|
||||
async fn run_command(&self, command: &str) -> Result<String, Error> {
|
||||
debug!("[Brocade] Running command: '{command}'...");
|
||||
|
||||
let mut channel = self.client.channel_open_session().await?;
|
||||
let mut output = Vec::new();
|
||||
|
||||
@ -142,9 +154,9 @@ impl BrocadeClient {
|
||||
}
|
||||
russh::ChannelMsg::ExitStatus { exit_status } => {
|
||||
if exit_status != 0 {
|
||||
let output_str = String::from_utf8(output).unwrap_or_default();
|
||||
return Err(Error::CommandError(format!(
|
||||
"Command failed with exit status {exit_status}, output {}",
|
||||
String::from_utf8(output).unwrap_or_default()
|
||||
"Command failed with exit status {exit_status}, output {output_str}",
|
||||
)));
|
||||
}
|
||||
}
|
||||
@ -170,6 +182,8 @@ impl BrocadeClient {
|
||||
|
||||
// Execute commands sequentially and check for errors immediately.
|
||||
for command in commands {
|
||||
debug!("[Brocade] Running command: '{command}'...");
|
||||
|
||||
let mut output = Vec::new();
|
||||
channel.exec(true, command.as_str()).await?;
|
||||
|
||||
@ -187,7 +201,7 @@ impl BrocadeClient {
|
||||
if exit_status != 0 {
|
||||
let output_str = String::from_utf8(output).unwrap_or_default();
|
||||
return Err(Error::CommandError(format!(
|
||||
"Command '{command}' failed with exit status {exit_status}: {output_str}",
|
||||
"Command failed with exit status {exit_status}: {output_str}",
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ harmony_secret = { path = "../harmony_secret" }
|
||||
askama.workspace = true
|
||||
sqlx.workspace = true
|
||||
inquire.workspace = true
|
||||
brocade = { version = "0.1.0", path = "../brocade" }
|
||||
brocade = { path = "../brocade" }
|
||||
letian marked this conversation as resolved
Outdated
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
|
@ -5,16 +5,25 @@ use harmony_secret::Secret;
|
||||
use harmony_secret::SecretManager;
|
||||
use harmony_types::net::MacAddress;
|
||||
use harmony_types::net::Url;
|
||||
use k8s_openapi::api::core::v1::Namespace;
|
||||
use kube::api::ObjectMeta;
|
||||
use log::debug;
|
||||
use log::info;
|
||||
use russh::client;
|
||||
use russh::client::Handler;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::data::FileContent;
|
||||
use crate::executors::ExecutorError;
|
||||
use crate::hardware::PhysicalHost;
|
||||
use crate::modules::okd::crd::InstallPlanApproval;
|
||||
use crate::modules::okd::crd::OperatorGroup;
|
||||
use crate::modules::okd::crd::OperatorGroupSpec;
|
||||
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 super::DHCPStaticEntry;
|
||||
@ -35,12 +44,12 @@ use super::PreparationOutcome;
|
||||
use super::Router;
|
||||
use super::Switch;
|
||||
use super::SwitchError;
|
||||
use super::SwitchPort;
|
||||
use super::TftpServer;
|
||||
|
||||
use super::Topology;
|
||||
use super::k8s::K8sClient;
|
||||
use std::error::Error;
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -102,37 +111,224 @@ impl HAClusterTopology {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn find_master_switch(&self) -> Option<LogicalHost> {
|
||||
self.switch.first().cloned() // FIXME: Should we be smarter to find the master switch?
|
||||
async fn ensure_nmstate_operator_installed(&self) -> Result<(), String> {
|
||||
// FIXME: Find a way to check nmstate is already available (get pod -n openshift-nmstate)
|
||||
debug!("Installing NMState operator...");
|
||||
let k8s_client = self.k8s_client().await?;
|
||||
|
||||
let nmstate_namespace = Namespace {
|
||||
metadata: ObjectMeta {
|
||||
name: Some("openshift-nmstate".to_string()),
|
||||
finalizers: Some(vec!["kubernetes".to_string()]),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
debug!("Creating NMState namespace: {nmstate_namespace:#?}");
|
||||
k8s_client
|
||||
.apply(&nmstate_namespace, None)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let nmstate_operator_group = OperatorGroup {
|
||||
metadata: ObjectMeta {
|
||||
name: Some("openshift-nmstate".to_string()),
|
||||
namespace: Some("openshift-nmstate".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: OperatorGroupSpec {
|
||||
target_namespaces: vec!["openshift-nmstate".to_string()],
|
||||
},
|
||||
};
|
||||
debug!("Creating NMState operator group: {nmstate_operator_group:#?}");
|
||||
k8s_client
|
||||
.apply(&nmstate_operator_group, None)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let nmstate_subscription = Subscription {
|
||||
metadata: ObjectMeta {
|
||||
name: Some("kubernetes-nmstate-operator".to_string()),
|
||||
namespace: Some("openshift-nmstate".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: SubscriptionSpec {
|
||||
channel: Some("stable".to_string()),
|
||||
install_plan_approval: Some(InstallPlanApproval::Automatic),
|
||||
name: "kubernetes-nmstate-operator".to_string(),
|
||||
source: "redhat-operators".to_string(),
|
||||
johnride
commented
redhat operators are not always available, it depends on how the OKD cluster was brought up. This method may not be as portable as we need. I think we can move forward for now but this deserves a comment here in the code to help debug potential issues. redhat operators are not always available, it depends on how the OKD cluster was brought up. This method may not be as portable as we need.
I think we can move forward for now but this deserves a comment here in the code to help debug potential issues.
|
||||
source_namespace: "openshift-marketplace".to_string(),
|
||||
},
|
||||
};
|
||||
debug!("Subscribing to NMState Operator: {nmstate_subscription:#?}");
|
||||
k8s_client
|
||||
.apply(&nmstate_subscription, None)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let nmstate = NMState {
|
||||
metadata: ObjectMeta {
|
||||
name: Some("nmstate".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
debug!("Creating NMState: {nmstate:#?}");
|
||||
k8s_client
|
||||
.apply(&nmstate, None)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
||||
fn get_next_bond_id(&self) -> u8 {
|
||||
42 // FIXME: Find a better way to declare the bond id
|
||||
johnride
commented
There may be a better way but no better answer than 42. There may be a better way but no better answer than 42.
|
||||
}
|
||||
|
||||
async fn configure_bond(
|
||||
&self,
|
||||
host: &PhysicalHost,
|
||||
config: &HostNetworkConfig,
|
||||
) -> Result<(), SwitchError> {
|
||||
self.ensure_nmstate_operator_installed()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
SwitchError::new(format!(
|
||||
"Can't configure bond, NMState operator not available: {e}"
|
||||
))
|
||||
})?;
|
||||
|
||||
let bond_config = self.create_bond_configuration(host, config);
|
||||
debug!("Configuring bond for host {host:?}: {bond_config:#?}");
|
||||
self.k8s_client()
|
||||
.await
|
||||
.unwrap()
|
||||
.apply(&bond_config, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<u8, SwitchError> {
|
||||
fn create_bond_configuration(
|
||||
&self,
|
||||
host: &PhysicalHost,
|
||||
config: &HostNetworkConfig,
|
||||
) -> NodeNetworkConfigurationPolicy {
|
||||
let host_name = host.id.clone();
|
||||
|
||||
let bond_id = self.get_next_bond_id();
|
||||
let bond_name = format!("bond{bond_id}");
|
||||
let mut bond_mtu: Option<u32> = None;
|
||||
let mut bond_mac_address: Option<String> = None;
|
||||
let mut bond_ports = Vec::new();
|
||||
let mut interfaces: Vec<nmstate::InterfaceSpec> = Vec::new();
|
||||
|
||||
for switch_port in &config.switch_ports {
|
||||
let interface_name = switch_port.interface.name.clone();
|
||||
|
||||
interfaces.push(nmstate::InterfaceSpec {
|
||||
name: interface_name.clone(),
|
||||
description: Some(format!("Member of bond {bond_name}")),
|
||||
r#type: "ethernet".to_string(),
|
||||
state: "up".to_string(),
|
||||
mtu: Some(switch_port.interface.mtu),
|
||||
mac_address: Some(switch_port.interface.mac_address.to_string()),
|
||||
ipv4: Some(nmstate::IpStackSpec {
|
||||
enabled: Some(false),
|
||||
..Default::default()
|
||||
}),
|
||||
ipv6: Some(nmstate::IpStackSpec {
|
||||
enabled: Some(false),
|
||||
..Default::default()
|
||||
}),
|
||||
link_aggregation: None,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
bond_ports.push(interface_name);
|
||||
|
||||
// Use the first port's details for the bond mtu and mac address
|
||||
if bond_mtu.is_none() {
|
||||
bond_mtu = Some(switch_port.interface.mtu);
|
||||
}
|
||||
if bond_mac_address.is_none() {
|
||||
bond_mac_address = Some(switch_port.interface.mac_address.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
interfaces.push(nmstate::InterfaceSpec {
|
||||
name: bond_name.clone(),
|
||||
description: Some(format!("Network bond for host {host_name}")),
|
||||
r#type: "bond".to_string(),
|
||||
state: "up".to_string(),
|
||||
mtu: bond_mtu,
|
||||
mac_address: bond_mac_address,
|
||||
ipv4: Some(nmstate::IpStackSpec {
|
||||
dhcp: Some(true),
|
||||
enabled: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
ipv6: Some(nmstate::IpStackSpec {
|
||||
dhcp: Some(true),
|
||||
autoconf: Some(true),
|
||||
enabled: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
link_aggregation: Some(nmstate::BondSpec {
|
||||
mode: "802.3ad".to_string(),
|
||||
ports: bond_ports,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
NodeNetworkConfigurationPolicy {
|
||||
metadata: ObjectMeta {
|
||||
name: Some(format!("{host_name}-bond-config")),
|
||||
..Default::default()
|
||||
},
|
||||
spec: NodeNetworkConfigurationPolicySpec {
|
||||
node_selector: Some(BTreeMap::from([(
|
||||
"kubernetes.io/hostname".to_string(),
|
||||
host_name.to_string(),
|
||||
)])),
|
||||
desired_state: nmstate::DesiredStateSpec { interfaces },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_switch_client(&self) -> Result<Box<dyn SwitchClient>, SwitchError> {
|
||||
let auth = SecretManager::get_or_prompt::<BrocadeSwitchAuth>()
|
||||
.await
|
||||
.map_err(|e| SwitchError::new(format!("Failed to get credentials: {e}")))?;
|
||||
|
||||
let switch = self
|
||||
.find_master_switch()
|
||||
.ok_or(SwitchError::new("No switch found in topology".to_string()))?;
|
||||
let client = BrocadeSwitchClient::init(switch.ip, &auth.username, &auth.password)
|
||||
// FIXME: We assume Brocade switches
|
||||
let switches: Vec<IpAddr> = self.switch.iter().map(|s| s.ip).collect();
|
||||
let client = BrocadeSwitchClient::init(&switches, &auth.username, &auth.password)
|
||||
.await
|
||||
.map_err(|e| SwitchError::new(format!("Failed to connect to switch: {e}")))?;
|
||||
|
||||
Ok(Box::new(client))
|
||||
}
|
||||
|
||||
async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
||||
debug!("Configuring port channel: {config:#?}");
|
||||
let client = self.get_switch_client().await?;
|
||||
|
||||
let switch_ports: Vec<String> = config
|
||||
.switch_ports
|
||||
.iter()
|
||||
.map(|s| s.port_name.clone())
|
||||
.collect();
|
||||
let channel_id = client
|
||||
|
||||
client
|
||||
.configure_port_channel(switch_ports)
|
||||
.await
|
||||
.map_err(|e| SwitchError::new(format!("Failed to configure switch: {e}")))?;
|
||||
|
||||
Ok(channel_id)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn autoload() -> Self {
|
||||
@ -312,12 +508,7 @@ impl HttpServer for HAClusterTopology {
|
||||
#[async_trait]
|
||||
impl Switch for HAClusterTopology {
|
||||
async fn get_port_for_mac_address(&self, mac_address: &MacAddress) -> Option<String> {
|
||||
let auth = SecretManager::get_or_prompt::<BrocadeSwitchAuth>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let switch = self.find_master_switch()?;
|
||||
let client = BrocadeSwitchClient::init(switch.ip, &auth.username, &auth.password).await;
|
||||
let client = self.get_switch_client().await;
|
||||
|
||||
let Ok(client) = client else {
|
||||
return None;
|
||||
@ -328,18 +519,16 @@ impl Switch for HAClusterTopology {
|
||||
|
||||
async fn configure_host_network(
|
||||
&self,
|
||||
_host: &PhysicalHost,
|
||||
host: &PhysicalHost,
|
||||
config: HostNetworkConfig,
|
||||
) -> Result<(), SwitchError> {
|
||||
let _ = self.configure_bond(&config).await;
|
||||
let channel_id = self.configure_port_channel(&config).await;
|
||||
|
||||
todo!()
|
||||
self.configure_bond(host, &config).await?;
|
||||
self.configure_port_channel(&config).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
trait SwitchClient {
|
||||
trait SwitchClient: Send + Sync {
|
||||
async fn find_port(&self, mac_address: &MacAddress) -> Option<String>;
|
||||
async fn configure_port_channel(&self, switch_ports: Vec<String>) -> Result<u8, SwitchError>;
|
||||
}
|
||||
@ -349,8 +538,12 @@ struct BrocadeSwitchClient {
|
||||
}
|
||||
|
||||
impl BrocadeSwitchClient {
|
||||
async fn init(ip: IpAddress, username: &str, password: &str) -> Result<Self, brocade::Error> {
|
||||
let brocade = BrocadeClient::init(ip, username, password).await?;
|
||||
async fn init(
|
||||
ip_addresses: &[IpAddress],
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<Self, brocade::Error> {
|
||||
let brocade = BrocadeClient::init(ip_addresses, username, password).await?;
|
||||
Ok(Self { brocade })
|
||||
}
|
||||
}
|
||||
@ -451,8 +644,8 @@ impl DhcpServer for DummyInfra {
|
||||
}
|
||||
async fn set_dhcp_range(
|
||||
&self,
|
||||
start: &IpAddress,
|
||||
end: &IpAddress,
|
||||
_start: &IpAddress,
|
||||
_end: &IpAddress,
|
||||
) -> Result<(), ExecutorError> {
|
||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||
}
|
||||
|
@ -191,9 +191,16 @@ pub struct HostNetworkConfig {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SwitchPort {
|
||||
pub mac_address: MacAddress,
|
||||
pub interface: NetworkInterface,
|
||||
pub port_name: String,
|
||||
// FIXME: Should we add speed as well? And other params
|
||||
}
|
||||
letian marked this conversation as resolved
johnride
commented
speed yes, maybe we will need something else but nothing comes to mind right now. speed yes, maybe we will need something else but nothing comes to mind right now.
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct NetworkInterface {
|
||||
pub name: String,
|
||||
pub mac_address: MacAddress,
|
||||
pub speed_mbps: Option<u32>,
|
||||
pub mtu: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, new)]
|
||||
|
@ -11,7 +11,7 @@ use crate::{
|
||||
okd::{host_network::HostNetworkConfigurationScore, templates::BootstrapIpxeTpl},
|
||||
},
|
||||
score::Score,
|
||||
topology::{self, HAClusterTopology, HostBinding},
|
||||
topology::{HAClusterTopology, HostBinding},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use derive_new::new;
|
||||
@ -30,7 +30,7 @@ pub struct OKDSetup03ControlPlaneScore {}
|
||||
|
||||
impl Score<HAClusterTopology> for OKDSetup03ControlPlaneScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||
Box::new(OKDSetup03ControlPlaneInterpret::new(self.clone()))
|
||||
Box::new(OKDSetup03ControlPlaneInterpret::new())
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
@ -40,17 +40,15 @@ impl Score<HAClusterTopology> for OKDSetup03ControlPlaneScore {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OKDSetup03ControlPlaneInterpret {
|
||||
score: OKDSetup03ControlPlaneScore,
|
||||
version: Version,
|
||||
status: InterpretStatus,
|
||||
}
|
||||
|
||||
impl OKDSetup03ControlPlaneInterpret {
|
||||
pub fn new(score: OKDSetup03ControlPlaneScore) -> Self {
|
||||
pub fn new() -> Self {
|
||||
let version = Version::from("1.0.0").unwrap();
|
||||
Self {
|
||||
version,
|
||||
score,
|
||||
status: InterpretStatus::QUEUED,
|
||||
}
|
||||
}
|
||||
@ -161,7 +159,7 @@ impl OKDSetup03ControlPlaneInterpret {
|
||||
}
|
||||
.to_string();
|
||||
|
||||
debug!("[ControlPlane] iPXE content template:\n{}", content);
|
||||
debug!("[ControlPlane] iPXE content template:\n{content}");
|
||||
|
||||
// Create and apply an iPXE boot file for each node.
|
||||
for node in nodes {
|
||||
@ -191,16 +189,13 @@ impl OKDSetup03ControlPlaneInterpret {
|
||||
/// Prompts the user to reboot the target control plane nodes.
|
||||
async fn reboot_targets(&self, nodes: &Vec<PhysicalHost>) -> Result<(), InterpretError> {
|
||||
let node_ids: Vec<String> = nodes.iter().map(|n| n.id.to_string()).collect();
|
||||
info!(
|
||||
"[ControlPlane] Requesting reboot for control plane nodes: {:?}",
|
||||
node_ids
|
||||
);
|
||||
info!("[ControlPlane] Requesting reboot for control plane nodes: {node_ids:?}",);
|
||||
|
||||
let confirmation = inquire::Confirm::new(
|
||||
&format!("Please reboot the {} control plane nodes ({}) to apply their PXE configuration. Press enter when ready.", nodes.len(), node_ids.join(", ")),
|
||||
)
|
||||
.prompt()
|
||||
.map_err(|e| InterpretError::new(format!("User prompt failed: {}", e)))?;
|
||||
.map_err(|e| InterpretError::new(format!("User prompt failed: {e}")))?;
|
||||
|
||||
if !confirmation {
|
||||
return Err(InterpretError::new(
|
||||
@ -222,19 +217,18 @@ impl OKDSetup03ControlPlaneInterpret {
|
||||
topology: &HAClusterTopology,
|
||||
hosts: &Vec<PhysicalHost>,
|
||||
johnride
commented
The clone has been bugging me too. We could use an Rc or Arc too as a balance between usability and performance. The clone has been bugging me too. We could use an Rc or Arc too as a balance between usability and performance.
|
||||
) -> Result<(), InterpretError> {
|
||||
// Generate MC or NNCP from inventory NIC data; apply via ignition or post-join.
|
||||
info!("[ControlPlane] Ensuring persistent bonding via MachineConfig/NNCP");
|
||||
let score = HostNetworkConfigurationScore {
|
||||
hosts: hosts.clone(), // FIXME: Avoid clone if possible
|
||||
};
|
||||
score.interpret(inventory, topology).await?;
|
||||
|
||||
score.interpret(inventory, topology);
|
||||
|
||||
// Generate MC or NNCP from inventory NIC data; apply via ignition or post-join.
|
||||
info!("[ControlPlane] Ensuring persistent bonding via MachineConfig/NNCP");
|
||||
inquire::Confirm::new(
|
||||
"Network configuration for control plane nodes is not automated yet. Configure it manually if needed.",
|
||||
)
|
||||
.prompt()
|
||||
.map_err(|e| InterpretError::new(format!("User prompt failed: {}", e)))?;
|
||||
.map_err(|e| InterpretError::new(format!("User prompt failed: {e}")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
43
harmony/src/modules/okd/crd/machineconfig.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use kube::CustomResource;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||
#[kube(
|
||||
group = "machineconfiguration.openshift.io",
|
||||
version = "v1",
|
||||
kind = "MachineConfig",
|
||||
namespaced
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MachineConfigSpec {
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Config {
|
||||
pub ignition: Ignition,
|
||||
pub storage: Option<Storage>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||
pub struct Ignition {
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||
pub struct Storage {
|
||||
pub files: Vec<File>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||
pub struct File {
|
||||
pub path: String,
|
||||
pub contents: FileContents,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||
pub struct FileContents {
|
||||
pub source: String,
|
||||
}
|
42
harmony/src/modules/okd/crd/mod.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use kube::CustomResource;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod machineconfig;
|
||||
pub mod nmstate;
|
||||
|
||||
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||
#[kube(
|
||||
group = "operators.coreos.com",
|
||||
version = "v1",
|
||||
kind = "OperatorGroup",
|
||||
namespaced
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OperatorGroupSpec {
|
||||
pub target_namespaces: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||
#[kube(
|
||||
group = "operators.coreos.com",
|
||||
version = "v1alpha1",
|
||||
kind = "Subscription",
|
||||
namespaced
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubscriptionSpec {
|
||||
pub name: String,
|
||||
pub source: String,
|
||||
pub source_namespace: String,
|
||||
pub channel: Option<String>,
|
||||
pub install_plan_approval: Option<InstallPlanApproval>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||
pub enum InstallPlanApproval {
|
||||
#[serde(rename = "Automatic")]
|
||||
Automatic,
|
||||
#[serde(rename = "Manual")]
|
||||
Manual,
|
||||
}
|
251
harmony/src/modules/okd/crd/nmstate.rs
Normal file
@ -0,0 +1,251 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use kube::CustomResource;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||
#[kube(group = "nmstate.io", version = "v1", kind = "NMState", namespaced)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NMStateSpec {
|
||||
pub probe_configuration: Option<ProbeConfig>,
|
||||
}
|
||||
|
||||
impl Default for NMState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
metadata: Default::default(),
|
||||
spec: NMStateSpec {
|
||||
probe_configuration: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProbeConfig {
|
||||
pub dns: ProbeDns,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProbeDns {
|
||||
pub host: String,
|
||||
}
|
||||
|
||||
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||
#[kube(
|
||||
group = "nmstate.io",
|
||||
version = "v1",
|
||||
kind = "NodeNetworkConfigurationPolicy",
|
||||
namespaced
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NodeNetworkConfigurationPolicySpec {
|
||||
pub node_selector: Option<BTreeMap<String, String>>,
|
||||
pub desired_state: DesiredStateSpec,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DesiredStateSpec {
|
||||
pub interfaces: Vec<InterfaceSpec>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct InterfaceSpec {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub r#type: String,
|
||||
pub state: String,
|
||||
pub mac_address: Option<String>,
|
||||
pub mtu: Option<u32>,
|
||||
pub controller: Option<String>,
|
||||
pub ipv4: Option<IpStackSpec>,
|
||||
pub ipv6: Option<IpStackSpec>,
|
||||
pub ethernet: Option<EthernetSpec>,
|
||||
pub link_aggregation: Option<BondSpec>,
|
||||
pub vlan: Option<VlanSpec>,
|
||||
pub vxlan: Option<VxlanSpec>,
|
||||
pub mac_vtap: Option<MacVtapSpec>,
|
||||
pub mac_vlan: Option<MacVlanSpec>,
|
||||
pub infiniband: Option<InfinibandSpec>,
|
||||
pub linux_bridge: Option<LinuxBridgeSpec>,
|
||||
pub ovs_bridge: Option<OvsBridgeSpec>,
|
||||
pub ethtool: Option<EthtoolSpec>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IpStackSpec {
|
||||
pub enabled: Option<bool>,
|
||||
pub dhcp: Option<bool>,
|
||||
pub autoconf: Option<bool>,
|
||||
pub address: Option<Vec<IpAddressSpec>>,
|
||||
pub auto_dns: Option<bool>,
|
||||
pub auto_gateway: Option<bool>,
|
||||
pub auto_routes: Option<bool>,
|
||||
pub dhcp_client_id: Option<String>,
|
||||
pub dhcp_duid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IpAddressSpec {
|
||||
pub ip: String,
|
||||
pub prefix_length: u8,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct EthernetSpec {
|
||||
pub speed: Option<u32>,
|
||||
pub duplex: Option<String>,
|
||||
pub auto_negotiation: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct BondSpec {
|
||||
pub mode: String,
|
||||
pub ports: Vec<String>,
|
||||
pub options: Option<BTreeMap<String, Value>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct VlanSpec {
|
||||
pub base_iface: String,
|
||||
pub id: u16,
|
||||
pub protocol: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct VxlanSpec {
|
||||
pub base_iface: String,
|
||||
pub id: u32,
|
||||
pub remote: String,
|
||||
pub local: Option<String>,
|
||||
pub learning: Option<bool>,
|
||||
pub destination_port: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct MacVtapSpec {
|
||||
pub base_iface: String,
|
||||
pub mode: String,
|
||||
pub promiscuous: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct MacVlanSpec {
|
||||
pub base_iface: String,
|
||||
pub mode: String,
|
||||
pub promiscuous: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct InfinibandSpec {
|
||||
pub base_iface: String,
|
||||
pub pkey: String,
|
||||
pub mode: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LinuxBridgeSpec {
|
||||
pub options: Option<LinuxBridgeOptions>,
|
||||
pub ports: Option<Vec<LinuxBridgePort>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LinuxBridgeOptions {
|
||||
pub mac_ageing_time: Option<u32>,
|
||||
pub multicast_snooping: Option<bool>,
|
||||
pub stp: Option<StpOptions>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct StpOptions {
|
||||
pub enabled: Option<bool>,
|
||||
pub forward_delay: Option<u16>,
|
||||
pub hello_time: Option<u16>,
|
||||
pub max_age: Option<u16>,
|
||||
pub priority: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LinuxBridgePort {
|
||||
pub name: String,
|
||||
pub vlan: Option<LinuxBridgePortVlan>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LinuxBridgePortVlan {
|
||||
pub mode: Option<String>,
|
||||
pub trunk_tags: Option<Vec<VlanTag>>,
|
||||
pub tag: Option<u16>,
|
||||
pub enable_native: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct VlanTag {
|
||||
pub id: u16,
|
||||
pub id_range: Option<VlanIdRange>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct VlanIdRange {
|
||||
pub min: u16,
|
||||
pub max: u16,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct OvsBridgeSpec {
|
||||
pub options: Option<OvsBridgeOptions>,
|
||||
pub ports: Option<Vec<OvsPortSpec>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct OvsBridgeOptions {
|
||||
pub stp: Option<bool>,
|
||||
pub rstp: Option<bool>,
|
||||
pub mcast_snooping_enable: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct OvsPortSpec {
|
||||
pub name: String,
|
||||
pub link_aggregation: Option<BondSpec>,
|
||||
pub vlan: Option<LinuxBridgePortVlan>,
|
||||
pub r#type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct EthtoolSpec {
|
||||
// FIXME: Properly describe this spec
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct EthtoolFecSpec {
|
||||
pub auto: Option<bool>,
|
||||
pub mode: Option<String>,
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use harmony_types::{id::Id, net::MacAddress};
|
||||
use harmony_types::id::Id;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
@ -8,7 +8,7 @@ use crate::{
|
||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||
inventory::Inventory,
|
||||
score::Score,
|
||||
topology::{HostNetworkConfig, Switch, SwitchPort, Topology},
|
||||
topology::{HostNetworkConfig, NetworkInterface, Switch, SwitchPort, Topology},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@ -59,10 +59,17 @@ impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
|
||||
for host in &self.score.hosts {
|
||||
let mut switch_ports = vec![];
|
||||
|
||||
for mac_address in host.get_mac_address() {
|
||||
for network_interface in &host.network {
|
||||
let mac_address = network_interface.mac_address;
|
||||
|
||||
if let Some(port_name) = topology.get_port_for_mac_address(&mac_address).await {
|
||||
switch_ports.push(SwitchPort {
|
||||
mac_address,
|
||||
interface: NetworkInterface {
|
||||
name: network_interface.name.clone(),
|
||||
mac_address,
|
||||
speed_mbps: network_interface.speed_mbps,
|
||||
mtu: network_interface.mtu,
|
||||
},
|
||||
port_name,
|
||||
});
|
||||
}
|
||||
@ -73,14 +80,6 @@ impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
|
||||
.await;
|
||||
}
|
||||
|
||||
// foreach hosts
|
||||
// foreach mac addresses
|
||||
// let port = topology.get_port_for_mac_address(); // si pas de port -> mac address pas connectee
|
||||
// create port channel for all ports found
|
||||
// create bond for all valid addresses (port found)
|
||||
// apply network config
|
||||
// topology.configure_host_network(host, config) <--- will create both bonds & port channels
|
||||
|
||||
Ok(Outcome::success("".into()))
|
||||
}
|
||||
}
|
||||
@ -88,7 +87,7 @@ impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assertor::*;
|
||||
use harmony_inventory_agent::hwinfo::NetworkInterface;
|
||||
use harmony_types::net::MacAddress;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use crate::{
|
||||
@ -107,19 +106,31 @@ mod tests {
|
||||
lazy_static! {
|
||||
pub static ref HOST_ID: Id = Id::from_str("host-1").unwrap();
|
||||
pub static ref ANOTHER_HOST_ID: Id = Id::from_str("host-2").unwrap();
|
||||
pub static ref EXISTING_INTERFACE: MacAddress =
|
||||
MacAddress::try_from("AA:BB:CC:DD:EE:F1".to_string()).unwrap();
|
||||
pub static ref ANOTHER_EXISTING_INTERFACE: MacAddress =
|
||||
MacAddress::try_from("AA:BB:CC:DD:EE:F2".to_string()).unwrap();
|
||||
pub static ref UNKNOWN_INTERFACE: MacAddress =
|
||||
MacAddress::try_from("11:22:33:44:55:61".to_string()).unwrap();
|
||||
pub static ref EXISTING_INTERFACE: NetworkInterface = NetworkInterface {
|
||||
mac_address: MacAddress::try_from("AA:BB:CC:DD:EE:F1".to_string()).unwrap(),
|
||||
name: "interface-1".into(),
|
||||
speed_mbps: None,
|
||||
mtu: 1,
|
||||
};
|
||||
pub static ref ANOTHER_EXISTING_INTERFACE: NetworkInterface = NetworkInterface {
|
||||
mac_address: MacAddress::try_from("AA:BB:CC:DD:EE:F2".to_string()).unwrap(),
|
||||
name: "interface-2".into(),
|
||||
speed_mbps: None,
|
||||
mtu: 1,
|
||||
};
|
||||
pub static ref UNKNOWN_INTERFACE: NetworkInterface = NetworkInterface {
|
||||
mac_address: MacAddress::try_from("11:22:33:44:55:61".to_string()).unwrap(),
|
||||
name: "unknown-interface".into(),
|
||||
speed_mbps: None,
|
||||
mtu: 1,
|
||||
};
|
||||
pub static ref PORT: String = "1/0/42".into();
|
||||
pub static ref ANOTHER_PORT: String = "2/0/42".into();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_with_one_mac_address_should_create_bond_with_one_interface() {
|
||||
let host = given_host(&HOST_ID, vec![*EXISTING_INTERFACE]);
|
||||
let host = given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]);
|
||||
let score = given_score(vec![host]);
|
||||
let topology = TopologyWithSwitch::new();
|
||||
|
||||
@ -130,7 +141,7 @@ mod tests {
|
||||
HOST_ID.clone(),
|
||||
HostNetworkConfig {
|
||||
switch_ports: vec![SwitchPort {
|
||||
mac_address: *EXISTING_INTERFACE,
|
||||
interface: EXISTING_INTERFACE.clone(),
|
||||
port_name: PORT.clone(),
|
||||
}],
|
||||
},
|
||||
@ -141,7 +152,10 @@ mod tests {
|
||||
async fn host_with_multiple_mac_addresses_should_create_one_bond_with_all_interfaces() {
|
||||
let score = given_score(vec![given_host(
|
||||
&HOST_ID,
|
||||
vec![*EXISTING_INTERFACE, *ANOTHER_EXISTING_INTERFACE],
|
||||
vec![
|
||||
EXISTING_INTERFACE.clone(),
|
||||
ANOTHER_EXISTING_INTERFACE.clone(),
|
||||
],
|
||||
)]);
|
||||
let topology = TopologyWithSwitch::new();
|
||||
|
||||
@ -153,11 +167,11 @@ mod tests {
|
||||
HostNetworkConfig {
|
||||
switch_ports: vec![
|
||||
SwitchPort {
|
||||
mac_address: *EXISTING_INTERFACE,
|
||||
interface: EXISTING_INTERFACE.clone(),
|
||||
port_name: PORT.clone(),
|
||||
},
|
||||
SwitchPort {
|
||||
mac_address: *ANOTHER_EXISTING_INTERFACE,
|
||||
interface: ANOTHER_EXISTING_INTERFACE.clone(),
|
||||
port_name: ANOTHER_PORT.clone(),
|
||||
},
|
||||
],
|
||||
@ -168,8 +182,8 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn multiple_hosts_should_create_one_bond_per_host() {
|
||||
let score = given_score(vec![
|
||||
given_host(&HOST_ID, vec![*EXISTING_INTERFACE]),
|
||||
given_host(&ANOTHER_HOST_ID, vec![*ANOTHER_EXISTING_INTERFACE]),
|
||||
given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]),
|
||||
given_host(&ANOTHER_HOST_ID, vec![ANOTHER_EXISTING_INTERFACE.clone()]),
|
||||
]);
|
||||
let topology = TopologyWithSwitch::new();
|
||||
|
||||
@ -181,7 +195,7 @@ mod tests {
|
||||
HOST_ID.clone(),
|
||||
HostNetworkConfig {
|
||||
switch_ports: vec![SwitchPort {
|
||||
mac_address: *EXISTING_INTERFACE,
|
||||
interface: EXISTING_INTERFACE.clone(),
|
||||
port_name: PORT.clone(),
|
||||
}],
|
||||
},
|
||||
@ -190,7 +204,7 @@ mod tests {
|
||||
ANOTHER_HOST_ID.clone(),
|
||||
HostNetworkConfig {
|
||||
switch_ports: vec![SwitchPort {
|
||||
mac_address: *ANOTHER_EXISTING_INTERFACE,
|
||||
interface: ANOTHER_EXISTING_INTERFACE.clone(),
|
||||
port_name: ANOTHER_PORT.clone(),
|
||||
}],
|
||||
},
|
||||
@ -201,7 +215,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
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])]);
|
||||
let score = given_score(vec![given_host(&HOST_ID, vec![UNKNOWN_INTERFACE.clone()])]);
|
||||
let topology = TopologyWithSwitch::new_port_not_found();
|
||||
|
||||
let _ = score.interpret(&Inventory::empty(), &topology).await;
|
||||
@ -219,8 +233,8 @@ mod tests {
|
||||
HostNetworkConfigurationScore { hosts }
|
||||
}
|
||||
|
||||
fn given_host(id: &Id, mac_addresses: Vec<MacAddress>) -> PhysicalHost {
|
||||
let network = mac_addresses.iter().map(|m| given_interface(*m)).collect();
|
||||
fn given_host(id: &Id, network_interfaces: Vec<NetworkInterface>) -> PhysicalHost {
|
||||
let network = network_interfaces.iter().map(given_interface).collect();
|
||||
|
||||
PhysicalHost {
|
||||
johnride
commented
If I understand correctly this has the correct behavior. It is perfectly normal for a host to have mac addresses that are not connected to the switch. Most of them do have disconnected 1gb interfaces. It is also normal to find connected interfaces in the switch that are not part of the cluster. If an admin connects to the switch for debugging, or the switch is used for other purposes than only this cluster. If I understand correctly this has the correct behavior. It is perfectly normal for a host to have mac addresses that are not connected to the switch. Most of them do have disconnected 1gb interfaces.
It is also normal to find connected interfaces in the switch that are not part of the cluster. If an admin connects to the switch for debugging, or the switch is used for other purposes than only this cluster.
letian
commented
in that case what should we do if we realize that not a single mac addresses were found on the switch? should we simply ignore it (current solution) or should we clear existing configurations to a clean slate? and actually the question will be similar when we'll approach the "update config" scenario: what to do with existing bonds? and port-channels? should we just clear everything before and just apply the new config? or a smart way to update existing bonds/port-channels? in that case what should we do if we realize that not a single mac addresses were found on the switch? should we simply ignore it (current solution) or should we clear existing configurations to a clean slate?
and actually the question will be similar when we'll approach the "update config" scenario: what to do with existing bonds? and port-channels? should we just clear everything before and just apply the new config? or a smart way to update existing bonds/port-channels?
johnride
commented
Ok, I think I figured it out and it's not as complicated as it seems. The problem is not only specific replacement logic, it is also idempotency. Does the current code currently support detecting that the mac address is connected to a port that is already in a port channel with the correct configuration (correct mac + ports assigned to it) ? I think the correct behavior here is :
This behavior should be quite easily testable too 😁 Ok, I think I figured it out and it's not as complicated as it seems.
The problem is not only specific replacement logic, it is also idempotency. Does the current code currently support detecting that the mac address is connected to a port that is already in a port channel with the correct configuration (correct mac + ports assigned to it) ?
I think the correct behavior here is :
- You have a list of mac addresses that can be bonded together (the entire list for a single host)
- Find all the port numbers in the switch stack that match the host
- Check if any of those are already in a port channel
- If more than one port channel found, crash with a clear error message that the status of the switch configuration is not supported with the actual, wrong config clearly outlined.
- Ensure the port channel ends up with the correct config (all found ports and only found ports, no foreign port allowed)
This behavior should be quite easily testable too 😁
|
||||
id: id.clone(),
|
||||
@ -233,13 +247,15 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn given_interface(mac_address: MacAddress) -> NetworkInterface {
|
||||
NetworkInterface {
|
||||
name: format!("{mac_address}"),
|
||||
mac_address,
|
||||
speed_mbps: None,
|
||||
fn given_interface(
|
||||
interface: &NetworkInterface,
|
||||
) -> harmony_inventory_agent::hwinfo::NetworkInterface {
|
||||
harmony_inventory_agent::hwinfo::NetworkInterface {
|
||||
name: interface.name.clone(),
|
||||
mac_address: interface.mac_address,
|
||||
speed_mbps: interface.speed_mbps,
|
||||
is_up: true,
|
||||
mtu: 1,
|
||||
mtu: interface.mtu,
|
||||
ipv4_addresses: vec![],
|
||||
ipv6_addresses: vec![],
|
||||
driver: "driver".into(),
|
||||
|
@ -19,4 +19,5 @@ pub use bootstrap_03_control_plane::*;
|
||||
pub use bootstrap_04_workers::*;
|
||||
pub use bootstrap_05_sanity_check::*;
|
||||
pub use bootstrap_06_installation_report::*;
|
||||
pub mod crd;
|
||||
pub mod host_network;
|
||||
|
@ -41,7 +41,7 @@ impl TryFrom<String> for MacAddress {
|
||||
bytes[i] = u8::from_str_radix(part, 16).map_err(|_| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Invalid hex value in part {}: '{}'", i, part),
|
||||
format!("Invalid hex value in part {i}: '{part}'"),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
@ -106,8 +106,8 @@ impl Serialize for Url {
|
||||
impl std::fmt::Display for Url {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Url::LocalFolder(path) => write!(f, "{}", path),
|
||||
Url::Url(url) => write!(f, "{}", url),
|
||||
Url::LocalFolder(path) => write!(f, "{path}"),
|
||||
Url::Url(url) => write!(f, "{url}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Je ne met pas la version dans les dependances locales habituellement