configure bond with NMState
This commit is contained in:
parent
0de52aedbf
commit
ffe3c09907
1
Cargo.lock
generated
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" }
|
||||
|
||||
[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(),
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
#[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>,
|
||||
) -> 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
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
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
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 {
|
||||
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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user