WIP: configure-switch #159

Closed
johnride wants to merge 18 commits from configure-switch into master
13 changed files with 658 additions and 95 deletions
Showing only changes of commit ffe3c09907 - Show all commits

1
Cargo.lock generated
View File

@ -680,6 +680,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"harmony_types", "harmony_types",
"log",
"russh", "russh",
"russh-keys", "russh-keys",
"tokio", "tokio",

View File

@ -7,7 +7,8 @@ license.workspace = true
[dependencies] [dependencies]
async-trait.workspace = true async-trait.workspace = true
harmony_types = { version = "0.1.0", path = "../harmony_types" } harmony_types = { path = "../harmony_types" }
russh.workspace = true russh.workspace = true
russh-keys.workspace = true russh-keys.workspace = true
tokio.workspace = true tokio.workspace = true
log.workspace = true

View File

@ -5,6 +5,7 @@ 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 russh::client::{Handle, Handler}; use russh::client::{Handle, Handler};
use russh_keys::key; use russh_keys::key;
use std::str::FromStr; use std::str::FromStr;
@ -21,7 +22,12 @@ pub struct BrocadeClient {
} }
impl 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 config = russh::client::Config::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?;
@ -64,6 +70,8 @@ impl BrocadeClient {
} }
pub async fn configure_port_channel(&self, ports: &[String]) -> Result<u8, Error> { 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 channel_id = self.find_available_channel_id().await?;
let mut commands = Vec::new(); let mut commands = Vec::new();
@ -93,7 +101,8 @@ impl BrocadeClient {
} }
pub async fn find_available_channel_id(&self) -> Result<u8, Error> { 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 output = self.run_command("show port-channel summary").await?;
let mut used_ids = Vec::new(); let mut used_ids = Vec::new();
@ -121,10 +130,13 @@ impl BrocadeClient {
} }
} }
debug!("[Brocade] Found channel id '{next_id}'");
Ok(next_id) Ok(next_id)
} }
async fn run_command(&self, command: &str) -> Result<String, Error> { 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 channel = self.client.channel_open_session().await?;
let mut output = Vec::new(); let mut output = Vec::new();
@ -142,9 +154,9 @@ impl BrocadeClient {
} }
russh::ChannelMsg::ExitStatus { exit_status } => { russh::ChannelMsg::ExitStatus { exit_status } => {
if exit_status != 0 { if exit_status != 0 {
let output_str = String::from_utf8(output).unwrap_or_default();
return Err(Error::CommandError(format!( return Err(Error::CommandError(format!(
"Command failed with exit status {exit_status}, output {}", "Command failed with exit status {exit_status}, output {output_str}",
String::from_utf8(output).unwrap_or_default()
))); )));
} }
} }
@ -170,6 +182,8 @@ impl BrocadeClient {
// Execute commands sequentially and check for errors immediately. // Execute commands sequentially and check for errors immediately.
for command in commands { for command in commands {
debug!("[Brocade] Running command: '{command}'...");
let mut output = Vec::new(); let mut output = Vec::new();
channel.exec(true, command.as_str()).await?; channel.exec(true, command.as_str()).await?;
@ -187,7 +201,7 @@ impl BrocadeClient {
if exit_status != 0 { if exit_status != 0 {
let output_str = String::from_utf8(output).unwrap_or_default(); let output_str = String::from_utf8(output).unwrap_or_default();
return Err(Error::CommandError(format!( return Err(Error::CommandError(format!(
"Command '{command}' failed with exit status {exit_status}: {output_str}", "Command failed with exit status {exit_status}: {output_str}",
))); )));
} }
} }

View File

@ -77,7 +77,7 @@ harmony_secret = { path = "../harmony_secret" }
askama.workspace = true askama.workspace = true
sqlx.workspace = true sqlx.workspace = true
inquire.workspace = true inquire.workspace = true
brocade = { version = "0.1.0", path = "../brocade" } brocade = { path = "../brocade" }
[dev-dependencies] [dev-dependencies]
pretty_assertions.workspace = true pretty_assertions.workspace = true

View File

@ -5,16 +5,25 @@ use harmony_secret::Secret;
use harmony_secret::SecretManager; use harmony_secret::SecretManager;
use harmony_types::net::MacAddress; use harmony_types::net::MacAddress;
use harmony_types::net::Url; use harmony_types::net::Url;
use k8s_openapi::api::core::v1::Namespace;
use kube::api::ObjectMeta;
use log::debug; use log::debug;
use log::info; use log::info;
use russh::client;
use russh::client::Handler;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use crate::data::FileContent; use crate::data::FileContent;
use crate::executors::ExecutorError; use crate::executors::ExecutorError;
use crate::hardware::PhysicalHost; 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 crate::topology::PxeOptions;
use super::DHCPStaticEntry; use super::DHCPStaticEntry;
@ -35,12 +44,12 @@ use super::PreparationOutcome;
use super::Router; use super::Router;
use super::Switch; use super::Switch;
use super::SwitchError; use super::SwitchError;
use super::SwitchPort;
use super::TftpServer; use super::TftpServer;
use super::Topology; use super::Topology;
use super::k8s::K8sClient; use super::k8s::K8sClient;
use std::error::Error; use std::collections::BTreeMap;
use std::net::IpAddr;
use std::sync::Arc; use std::sync::Arc;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -102,37 +111,224 @@ impl HAClusterTopology {
.to_string() .to_string()
} }
fn find_master_switch(&self) -> Option<LogicalHost> { async fn ensure_nmstate_operator_installed(&self) -> Result<(), String> {
self.switch.first().cloned() // FIXME: Should we be smarter to find the master switch? // 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(),
Review

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
Review

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!() 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>() let auth = SecretManager::get_or_prompt::<BrocadeSwitchAuth>()
.await .await
.map_err(|e| SwitchError::new(format!("Failed to get credentials: {e}")))?; .map_err(|e| SwitchError::new(format!("Failed to get credentials: {e}")))?;
let switch = self // FIXME: We assume Brocade switches
.find_master_switch() let switches: Vec<IpAddr> = self.switch.iter().map(|s| s.ip).collect();
.ok_or(SwitchError::new("No switch found in topology".to_string()))?; let client = BrocadeSwitchClient::init(&switches, &auth.username, &auth.password)
let client = BrocadeSwitchClient::init(switch.ip, &auth.username, &auth.password)
.await .await
.map_err(|e| SwitchError::new(format!("Failed to connect to switch: {e}")))?; .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 let switch_ports: Vec<String> = config
.switch_ports .switch_ports
.iter() .iter()
.map(|s| s.port_name.clone()) .map(|s| s.port_name.clone())
.collect(); .collect();
let channel_id = client
client
.configure_port_channel(switch_ports) .configure_port_channel(switch_ports)
.await .await
.map_err(|e| SwitchError::new(format!("Failed to configure switch: {e}")))?; .map_err(|e| SwitchError::new(format!("Failed to configure switch: {e}")))?;
Ok(channel_id) Ok(())
} }
pub fn autoload() -> Self { pub fn autoload() -> Self {
@ -312,12 +508,7 @@ 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(&self, mac_address: &MacAddress) -> Option<String> {
let auth = SecretManager::get_or_prompt::<BrocadeSwitchAuth>() let client = self.get_switch_client().await;
.await
.unwrap();
let switch = self.find_master_switch()?;
let client = BrocadeSwitchClient::init(switch.ip, &auth.username, &auth.password).await;
let Ok(client) = client else { let Ok(client) = client else {
return None; return None;
@ -328,18 +519,16 @@ impl Switch for HAClusterTopology {
async fn configure_host_network( async fn configure_host_network(
&self, &self,
_host: &PhysicalHost, host: &PhysicalHost,
config: HostNetworkConfig, config: HostNetworkConfig,
) -> Result<(), SwitchError> { ) -> Result<(), SwitchError> {
let _ = self.configure_bond(&config).await; self.configure_bond(host, &config).await?;
let channel_id = self.configure_port_channel(&config).await; self.configure_port_channel(&config).await
todo!()
} }
} }
#[async_trait] #[async_trait]
trait SwitchClient { trait SwitchClient: Send + Sync {
async fn find_port(&self, mac_address: &MacAddress) -> Option<String>; async fn find_port(&self, mac_address: &MacAddress) -> Option<String>;
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>;
} }
@ -349,8 +538,12 @@ struct BrocadeSwitchClient {
} }
impl BrocadeSwitchClient { impl BrocadeSwitchClient {
async fn init(ip: IpAddress, username: &str, password: &str) -> Result<Self, brocade::Error> { async fn init(
let brocade = BrocadeClient::init(ip, username, password).await?; ip_addresses: &[IpAddress],
username: &str,
password: &str,
) -> Result<Self, brocade::Error> {
let brocade = BrocadeClient::init(ip_addresses, username, password).await?;
Ok(Self { brocade }) Ok(Self { brocade })
} }
} }
@ -451,8 +644,8 @@ impl DhcpServer for DummyInfra {
} }
async fn set_dhcp_range( async fn set_dhcp_range(
&self, &self,
start: &IpAddress, _start: &IpAddress,
end: &IpAddress, _end: &IpAddress,
) -> Result<(), ExecutorError> { ) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
} }

View File

@ -191,9 +191,16 @@ pub struct HostNetworkConfig {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct SwitchPort { pub struct SwitchPort {
pub mac_address: MacAddress, pub interface: NetworkInterface,
pub port_name: String, pub port_name: String,
// FIXME: Should we add speed as well? And other params }
letian marked this conversation as resolved
Review

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)] #[derive(Debug, Clone, new)]

View File

@ -11,7 +11,7 @@ use crate::{
okd::{host_network::HostNetworkConfigurationScore, templates::BootstrapIpxeTpl}, okd::{host_network::HostNetworkConfigurationScore, templates::BootstrapIpxeTpl},
}, },
score::Score, score::Score,
topology::{self, HAClusterTopology, HostBinding}, topology::{HAClusterTopology, HostBinding},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
@ -30,7 +30,7 @@ pub struct OKDSetup03ControlPlaneScore {}
impl Score<HAClusterTopology> for OKDSetup03ControlPlaneScore { impl Score<HAClusterTopology> for OKDSetup03ControlPlaneScore {
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> { fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
Box::new(OKDSetup03ControlPlaneInterpret::new(self.clone())) Box::new(OKDSetup03ControlPlaneInterpret::new())
} }
fn name(&self) -> String { fn name(&self) -> String {
@ -40,17 +40,15 @@ impl Score<HAClusterTopology> for OKDSetup03ControlPlaneScore {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct OKDSetup03ControlPlaneInterpret { pub struct OKDSetup03ControlPlaneInterpret {
score: OKDSetup03ControlPlaneScore,
version: Version, version: Version,
status: InterpretStatus, status: InterpretStatus,
} }
impl OKDSetup03ControlPlaneInterpret { impl OKDSetup03ControlPlaneInterpret {
pub fn new(score: OKDSetup03ControlPlaneScore) -> Self { pub fn new() -> Self {
let version = Version::from("1.0.0").unwrap(); let version = Version::from("1.0.0").unwrap();
Self { Self {
version, version,
score,
status: InterpretStatus::QUEUED, status: InterpretStatus::QUEUED,
} }
} }
@ -161,7 +159,7 @@ impl OKDSetup03ControlPlaneInterpret {
} }
.to_string(); .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. // Create and apply an iPXE boot file for each node.
for node in nodes { for node in nodes {
@ -191,16 +189,13 @@ impl OKDSetup03ControlPlaneInterpret {
/// Prompts the user to reboot the target control plane nodes. /// Prompts the user to reboot the target control plane nodes.
async fn reboot_targets(&self, nodes: &Vec<PhysicalHost>) -> Result<(), InterpretError> { async fn reboot_targets(&self, nodes: &Vec<PhysicalHost>) -> Result<(), InterpretError> {
let node_ids: Vec<String> = nodes.iter().map(|n| n.id.to_string()).collect(); let node_ids: Vec<String> = nodes.iter().map(|n| n.id.to_string()).collect();
info!( info!("[ControlPlane] Requesting reboot for control plane nodes: {node_ids:?}",);
"[ControlPlane] Requesting reboot for control plane nodes: {:?}",
node_ids
);
let confirmation = inquire::Confirm::new( 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(", ")), &format!("Please reboot the {} control plane nodes ({}) to apply their PXE configuration. Press enter when ready.", nodes.len(), node_ids.join(", ")),
) )
.prompt() .prompt()
.map_err(|e| InterpretError::new(format!("User prompt failed: {}", e)))?; .map_err(|e| InterpretError::new(format!("User prompt failed: {e}")))?;
if !confirmation { if !confirmation {
return Err(InterpretError::new( return Err(InterpretError::new(
@ -222,19 +217,18 @@ impl OKDSetup03ControlPlaneInterpret {
topology: &HAClusterTopology, topology: &HAClusterTopology,
hosts: &Vec<PhysicalHost>, hosts: &Vec<PhysicalHost>,
Review

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> { ) -> 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 { let score = HostNetworkConfigurationScore {
hosts: hosts.clone(), // FIXME: Avoid clone if possible 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( inquire::Confirm::new(
"Network configuration for control plane nodes is not automated yet. Configure it manually if needed.", "Network configuration for control plane nodes is not automated yet. Configure it manually if needed.",
) )
.prompt() .prompt()
.map_err(|e| InterpretError::new(format!("User prompt failed: {}", e)))?; .map_err(|e| InterpretError::new(format!("User prompt failed: {e}")))?;
Ok(()) Ok(())
} }

View 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,
}

View 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,
}

View 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>,
}

View File

@ -1,5 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use harmony_types::{id::Id, net::MacAddress}; use harmony_types::id::Id;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
@ -8,7 +8,7 @@ use crate::{
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory, inventory::Inventory,
score::Score, score::Score,
topology::{HostNetworkConfig, Switch, SwitchPort, Topology}, topology::{HostNetworkConfig, NetworkInterface, Switch, SwitchPort, Topology},
}; };
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
@ -59,10 +59,17 @@ impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
for host in &self.score.hosts { for host in &self.score.hosts {
let mut switch_ports = vec![]; 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 { if let Some(port_name) = topology.get_port_for_mac_address(&mac_address).await {
switch_ports.push(SwitchPort { 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, port_name,
}); });
} }
@ -73,14 +80,6 @@ impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
.await; .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())) Ok(Outcome::success("".into()))
} }
} }
@ -88,7 +87,7 @@ impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use assertor::*; use assertor::*;
use harmony_inventory_agent::hwinfo::NetworkInterface; use harmony_types::net::MacAddress;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use crate::{ use crate::{
@ -107,19 +106,31 @@ mod tests {
lazy_static! { lazy_static! {
pub static ref HOST_ID: Id = Id::from_str("host-1").unwrap(); 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 ANOTHER_HOST_ID: Id = Id::from_str("host-2").unwrap();
pub static ref EXISTING_INTERFACE: MacAddress = pub static ref EXISTING_INTERFACE: NetworkInterface = NetworkInterface {
MacAddress::try_from("AA:BB:CC:DD:EE:F1".to_string()).unwrap(); mac_address: MacAddress::try_from("AA:BB:CC:DD:EE:F1".to_string()).unwrap(),
pub static ref ANOTHER_EXISTING_INTERFACE: MacAddress = name: "interface-1".into(),
MacAddress::try_from("AA:BB:CC:DD:EE:F2".to_string()).unwrap(); speed_mbps: None,
pub static ref UNKNOWN_INTERFACE: MacAddress = mtu: 1,
MacAddress::try_from("11:22:33:44:55:61".to_string()).unwrap(); };
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 PORT: String = "1/0/42".into();
pub static ref ANOTHER_PORT: String = "2/0/42".into(); pub static ref ANOTHER_PORT: String = "2/0/42".into();
} }
#[tokio::test] #[tokio::test]
async fn host_with_one_mac_address_should_create_bond_with_one_interface() { 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 score = given_score(vec![host]);
let topology = TopologyWithSwitch::new(); let topology = TopologyWithSwitch::new();
@ -130,7 +141,7 @@ mod tests {
HOST_ID.clone(), HOST_ID.clone(),
HostNetworkConfig { HostNetworkConfig {
switch_ports: vec![SwitchPort { switch_ports: vec![SwitchPort {
mac_address: *EXISTING_INTERFACE, interface: EXISTING_INTERFACE.clone(),
port_name: PORT.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() { async fn host_with_multiple_mac_addresses_should_create_one_bond_with_all_interfaces() {
let score = given_score(vec![given_host( let score = given_score(vec![given_host(
&HOST_ID, &HOST_ID,
vec![*EXISTING_INTERFACE, *ANOTHER_EXISTING_INTERFACE], vec![
EXISTING_INTERFACE.clone(),
ANOTHER_EXISTING_INTERFACE.clone(),
],
)]); )]);
let topology = TopologyWithSwitch::new(); let topology = TopologyWithSwitch::new();
@ -153,11 +167,11 @@ mod tests {
HostNetworkConfig { HostNetworkConfig {
switch_ports: vec![ switch_ports: vec![
SwitchPort { SwitchPort {
mac_address: *EXISTING_INTERFACE, interface: EXISTING_INTERFACE.clone(),
port_name: PORT.clone(), port_name: PORT.clone(),
}, },
SwitchPort { SwitchPort {
mac_address: *ANOTHER_EXISTING_INTERFACE, interface: ANOTHER_EXISTING_INTERFACE.clone(),
port_name: ANOTHER_PORT.clone(), port_name: ANOTHER_PORT.clone(),
}, },
], ],
@ -168,8 +182,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn multiple_hosts_should_create_one_bond_per_host() { async fn multiple_hosts_should_create_one_bond_per_host() {
let score = given_score(vec![ let score = given_score(vec![
given_host(&HOST_ID, vec![*EXISTING_INTERFACE]), given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]),
given_host(&ANOTHER_HOST_ID, vec![*ANOTHER_EXISTING_INTERFACE]), given_host(&ANOTHER_HOST_ID, vec![ANOTHER_EXISTING_INTERFACE.clone()]),
]); ]);
let topology = TopologyWithSwitch::new(); let topology = TopologyWithSwitch::new();
@ -181,7 +195,7 @@ mod tests {
HOST_ID.clone(), HOST_ID.clone(),
HostNetworkConfig { HostNetworkConfig {
switch_ports: vec![SwitchPort { switch_ports: vec![SwitchPort {
mac_address: *EXISTING_INTERFACE, interface: EXISTING_INTERFACE.clone(),
port_name: PORT.clone(), port_name: PORT.clone(),
}], }],
}, },
@ -190,7 +204,7 @@ mod tests {
ANOTHER_HOST_ID.clone(), ANOTHER_HOST_ID.clone(),
HostNetworkConfig { HostNetworkConfig {
switch_ports: vec![SwitchPort { switch_ports: vec![SwitchPort {
mac_address: *ANOTHER_EXISTING_INTERFACE, interface: ANOTHER_EXISTING_INTERFACE.clone(),
port_name: ANOTHER_PORT.clone(), port_name: ANOTHER_PORT.clone(),
}], }],
}, },
@ -201,7 +215,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn port_not_found_for_mac_address_should_not_configure_interface() { async fn port_not_found_for_mac_address_should_not_configure_interface() {
// FIXME: Should it still configure an empty bond/port channel? // 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 topology = TopologyWithSwitch::new_port_not_found();
let _ = score.interpret(&Inventory::empty(), &topology).await; let _ = score.interpret(&Inventory::empty(), &topology).await;
@ -219,8 +233,8 @@ mod tests {
HostNetworkConfigurationScore { hosts } HostNetworkConfigurationScore { hosts }
} }
fn given_host(id: &Id, mac_addresses: Vec<MacAddress>) -> PhysicalHost { fn given_host(id: &Id, network_interfaces: Vec<NetworkInterface>) -> PhysicalHost {
let network = mac_addresses.iter().map(|m| given_interface(*m)).collect(); let network = network_interfaces.iter().map(given_interface).collect();
PhysicalHost { PhysicalHost {
id: id.clone(), id: id.clone(),
@ -233,13 +247,15 @@ mod tests {
} }
} }
fn given_interface(mac_address: MacAddress) -> NetworkInterface { fn given_interface(
NetworkInterface { interface: &NetworkInterface,
name: format!("{mac_address}"), ) -> harmony_inventory_agent::hwinfo::NetworkInterface {
mac_address, harmony_inventory_agent::hwinfo::NetworkInterface {
speed_mbps: None, name: interface.name.clone(),
mac_address: interface.mac_address,
speed_mbps: interface.speed_mbps,
is_up: true, is_up: true,
mtu: 1, mtu: interface.mtu,
ipv4_addresses: vec![], ipv4_addresses: vec![],
ipv6_addresses: vec![], ipv6_addresses: vec![],
driver: "driver".into(), driver: "driver".into(),

View File

@ -19,4 +19,5 @@ pub use bootstrap_03_control_plane::*;
pub use bootstrap_04_workers::*; pub use bootstrap_04_workers::*;
pub use bootstrap_05_sanity_check::*; pub use bootstrap_05_sanity_check::*;
pub use bootstrap_06_installation_report::*; pub use bootstrap_06_installation_report::*;
pub mod crd;
pub mod host_network; pub mod host_network;

View File

@ -41,7 +41,7 @@ impl TryFrom<String> for MacAddress {
bytes[i] = u8::from_str_radix(part, 16).map_err(|_| { bytes[i] = u8::from_str_radix(part, 16).map_err(|_| {
std::io::Error::new( std::io::Error::new(
std::io::ErrorKind::InvalidInput, 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 { impl std::fmt::Display for Url {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Url::LocalFolder(path) => write!(f, "{}", path), Url::LocalFolder(path) => write!(f, "{path}"),
Url::Url(url) => write!(f, "{}", url), Url::Url(url) => write!(f, "{url}"),
} }
} }
} }