Compare commits

...

5 Commits

45 changed files with 988 additions and 183 deletions

31
Cargo.lock generated
View File

@@ -690,6 +690,23 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "brocade-switch"
version = "0.1.0"
dependencies = [
"async-trait",
"brocade",
"env_logger",
"harmony",
"harmony_cli",
"harmony_macros",
"harmony_types",
"log",
"serde",
"tokio",
"url",
]
[[package]] [[package]]
name = "brotli" name = "brotli"
version = "8.0.2" version = "8.0.2"
@@ -2479,6 +2496,19 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "harmony_inventory_builder"
version = "0.1.0"
dependencies = [
"cidr",
"harmony",
"harmony_cli",
"harmony_macros",
"harmony_types",
"tokio",
"url",
]
[[package]] [[package]]
name = "harmony_macros" name = "harmony_macros"
version = "0.1.0" version = "0.1.0"
@@ -2544,6 +2574,7 @@ dependencies = [
name = "harmony_types" name = "harmony_types"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"log",
"rand 0.9.2", "rand 0.9.2",
"serde", "serde",
"url", "url",

View File

@@ -1,6 +1,6 @@
use std::net::{IpAddr, Ipv4Addr}; use std::net::{IpAddr, Ipv4Addr};
use brocade::BrocadeOptions; use brocade::{BrocadeOptions, ssh};
use harmony_secret::{Secret, SecretManager}; use harmony_secret::{Secret, SecretManager};
use harmony_types::switch::PortLocation; use harmony_types::switch::PortLocation;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -16,23 +16,28 @@ async fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
// let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 250)); // old brocade @ ianlet // let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 250)); // old brocade @ ianlet
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 55, 101)); // brocade @ sto1 let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); // brocade @ sto1
// let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 4, 11)); // brocade @ st // let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 4, 11)); // brocade @ st
let switch_addresses = vec![ip]; let switch_addresses = vec![ip];
let config = SecretManager::get_or_prompt::<BrocadeSwitchAuth>() // let config = SecretManager::get_or_prompt::<BrocadeSwitchAuth>()
.await // .await
.unwrap(); // .unwrap();
let brocade = brocade::init( let brocade = brocade::init(
&switch_addresses, &switch_addresses,
22, // &config.username,
&config.username, // &config.password,
&config.password, "admin",
Some(BrocadeOptions { "password",
BrocadeOptions {
dry_run: true, dry_run: true,
ssh: ssh::SshOptions {
port: 2222,
..Default::default()
},
..Default::default() ..Default::default()
}), },
) )
.await .await
.expect("Brocade client failed to connect"); .expect("Brocade client failed to connect");
@@ -54,6 +59,7 @@ async fn main() {
} }
println!("--------------"); println!("--------------");
todo!();
let channel_name = "1"; let channel_name = "1";
brocade.clear_port_channel(channel_name).await.unwrap(); brocade.clear_port_channel(channel_name).await.unwrap();

View File

@@ -140,7 +140,7 @@ impl BrocadeClient for FastIronClient {
async fn configure_interfaces( async fn configure_interfaces(
&self, &self,
_interfaces: Vec<(String, PortOperatingMode)>, _interfaces: &Vec<(String, PortOperatingMode)>,
) -> Result<(), Error> { ) -> Result<(), Error> {
todo!() todo!()
} }

View File

@@ -14,11 +14,12 @@ use async_trait::async_trait;
use harmony_types::net::MacAddress; use harmony_types::net::MacAddress;
use harmony_types::switch::{PortDeclaration, PortLocation}; use harmony_types::switch::{PortDeclaration, PortLocation};
use regex::Regex; use regex::Regex;
use serde::Serialize;
mod fast_iron; mod fast_iron;
mod network_operating_system; mod network_operating_system;
mod shell; mod shell;
mod ssh; pub mod ssh;
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub struct BrocadeOptions { pub struct BrocadeOptions {
@@ -118,7 +119,7 @@ impl fmt::Display for InterfaceType {
} }
/// Defines the primary configuration mode of a switch interface, representing mutually exclusive roles. /// Defines the primary configuration mode of a switch interface, representing mutually exclusive roles.
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone, Serialize)]
pub enum PortOperatingMode { pub enum PortOperatingMode {
/// The interface is explicitly configured for Brocade fabric roles (ISL or Trunk enabled). /// The interface is explicitly configured for Brocade fabric roles (ISL or Trunk enabled).
Fabric, Fabric,
@@ -141,12 +142,11 @@ pub enum InterfaceStatus {
pub async fn init( pub async fn init(
ip_addresses: &[IpAddr], ip_addresses: &[IpAddr],
port: u16,
username: &str, username: &str,
password: &str, password: &str,
options: Option<BrocadeOptions>, options: BrocadeOptions,
) -> Result<Box<dyn BrocadeClient + Send + Sync>, Error> { ) -> Result<Box<dyn BrocadeClient + Send + Sync>, Error> {
let shell = BrocadeShell::init(ip_addresses, port, username, password, options).await?; let shell = BrocadeShell::init(ip_addresses, username, password, options).await?;
let version_info = shell let version_info = shell
.with_session(ExecutionMode::Regular, |session| { .with_session(ExecutionMode::Regular, |session| {
@@ -208,7 +208,7 @@ pub trait BrocadeClient: std::fmt::Debug {
/// Configures a set of interfaces to be operated with a specified mode (access ports, ISL, etc.). /// Configures a set of interfaces to be operated with a specified mode (access ports, ISL, etc.).
async fn configure_interfaces( async fn configure_interfaces(
&self, &self,
interfaces: Vec<(String, PortOperatingMode)>, interfaces: &Vec<(String, PortOperatingMode)>,
) -> Result<(), Error>; ) -> Result<(), Error>;
/// Scans the existing configuration to find the next available (unused) /// Scans the existing configuration to find the next available (unused)

View File

@@ -187,7 +187,7 @@ impl BrocadeClient for NetworkOperatingSystemClient {
async fn configure_interfaces( async fn configure_interfaces(
&self, &self,
interfaces: Vec<(String, PortOperatingMode)>, interfaces: &Vec<(String, PortOperatingMode)>,
) -> Result<(), Error> { ) -> Result<(), Error> {
info!("[Brocade] Configuring {} interface(s)...", interfaces.len()); info!("[Brocade] Configuring {} interface(s)...", interfaces.len());
@@ -204,9 +204,12 @@ impl BrocadeClient for NetworkOperatingSystemClient {
PortOperatingMode::Trunk => { PortOperatingMode::Trunk => {
commands.push("switchport".into()); commands.push("switchport".into());
commands.push("switchport mode trunk".into()); commands.push("switchport mode trunk".into());
commands.push("no spanning-tree shutdown".into()); commands.push("switchport trunk allowed vlan all".into());
commands.push("no switchport trunk tag native-vlan".into());
commands.push("spanning-tree shutdown".into());
commands.push("no fabric isl enable".into()); commands.push("no fabric isl enable".into());
commands.push("no fabric trunk enable".into()); commands.push("no fabric trunk enable".into());
commands.push("no shutdown".into());
} }
PortOperatingMode::Access => { PortOperatingMode::Access => {
commands.push("switchport".into()); commands.push("switchport".into());

View File

@@ -16,7 +16,6 @@ use tokio::time::timeout;
#[derive(Debug)] #[derive(Debug)]
pub struct BrocadeShell { pub struct BrocadeShell {
ip: IpAddr, ip: IpAddr,
port: u16,
username: String, username: String,
password: String, password: String,
options: BrocadeOptions, options: BrocadeOptions,
@@ -27,33 +26,31 @@ pub struct BrocadeShell {
impl BrocadeShell { impl BrocadeShell {
pub async fn init( pub async fn init(
ip_addresses: &[IpAddr], ip_addresses: &[IpAddr],
port: u16,
username: &str, username: &str,
password: &str, password: &str,
options: Option<BrocadeOptions>, options: BrocadeOptions,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let ip = ip_addresses let ip = ip_addresses
.first() .first()
.ok_or_else(|| Error::ConfigurationError("No IP addresses provided".to_string()))?; .ok_or_else(|| Error::ConfigurationError("No IP addresses provided".to_string()))?;
let base_options = options.unwrap_or_default(); let brocade_ssh_client_options =
let options = ssh::try_init_client(username, password, ip, base_options).await?; ssh::try_init_client(username, password, ip, options).await?;
Ok(Self { Ok(Self {
ip: *ip, ip: *ip,
port,
username: username.to_string(), username: username.to_string(),
password: password.to_string(), password: password.to_string(),
before_all_commands: vec![], before_all_commands: vec![],
after_all_commands: vec![], after_all_commands: vec![],
options, options: brocade_ssh_client_options,
}) })
} }
pub async fn open_session(&self, mode: ExecutionMode) -> Result<BrocadeSession, Error> { pub async fn open_session(&self, mode: ExecutionMode) -> Result<BrocadeSession, Error> {
BrocadeSession::open( BrocadeSession::open(
self.ip, self.ip,
self.port, self.options.ssh.port,
&self.username, &self.username,
&self.password, &self.password,
self.options.clone(), self.options.clone(),

View File

@@ -2,6 +2,7 @@ use std::borrow::Cow;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use log::debug;
use russh::client::Handler; use russh::client::Handler;
use russh::kex::DH_G1_SHA1; use russh::kex::DH_G1_SHA1;
use russh::kex::ECDH_SHA2_NISTP256; use russh::kex::ECDH_SHA2_NISTP256;
@@ -10,29 +11,43 @@ use russh_keys::key::SSH_RSA;
use super::BrocadeOptions; use super::BrocadeOptions;
use super::Error; use super::Error;
#[derive(Default, Clone, Debug)] #[derive(Clone, Debug)]
pub struct SshOptions { pub struct SshOptions {
pub preferred_algorithms: russh::Preferred, pub preferred_algorithms: russh::Preferred,
pub port: u16,
}
impl Default for SshOptions {
fn default() -> Self {
Self {
preferred_algorithms: Default::default(),
port: 22,
}
}
} }
impl SshOptions { impl SshOptions {
fn ecdhsa_sha2_nistp256() -> Self { fn ecdhsa_sha2_nistp256(port: u16) -> Self {
Self { Self {
preferred_algorithms: russh::Preferred { preferred_algorithms: russh::Preferred {
kex: Cow::Borrowed(&[ECDH_SHA2_NISTP256]), kex: Cow::Borrowed(&[ECDH_SHA2_NISTP256]),
key: Cow::Borrowed(&[SSH_RSA]), key: Cow::Borrowed(&[SSH_RSA]),
..Default::default() ..Default::default()
}, },
port,
..Default::default()
} }
} }
fn legacy() -> Self { fn legacy(port: u16) -> Self {
Self { Self {
preferred_algorithms: russh::Preferred { preferred_algorithms: russh::Preferred {
kex: Cow::Borrowed(&[DH_G1_SHA1]), kex: Cow::Borrowed(&[DH_G1_SHA1]),
key: Cow::Borrowed(&[SSH_RSA]), key: Cow::Borrowed(&[SSH_RSA]),
..Default::default() ..Default::default()
}, },
port,
..Default::default()
} }
} }
} }
@@ -57,18 +72,21 @@ pub async fn try_init_client(
ip: &std::net::IpAddr, ip: &std::net::IpAddr,
base_options: BrocadeOptions, base_options: BrocadeOptions,
) -> Result<BrocadeOptions, Error> { ) -> Result<BrocadeOptions, Error> {
let mut default = SshOptions::default();
default.port = base_options.ssh.port;
let ssh_options = vec![ let ssh_options = vec![
SshOptions::default(), default,
SshOptions::ecdhsa_sha2_nistp256(), SshOptions::ecdhsa_sha2_nistp256(base_options.ssh.port),
SshOptions::legacy(), SshOptions::legacy(base_options.ssh.port),
]; ];
for ssh in ssh_options { for ssh in ssh_options {
let opts = BrocadeOptions { let opts = BrocadeOptions {
ssh, ssh: ssh.clone(),
..base_options.clone() ..base_options.clone()
}; };
let client = create_client(*ip, 22, username, password, &opts).await; debug!("Creating client {ip}:{} {username}", ssh.port);
let client = create_client(*ip, ssh.port, username, password, &opts).await;
match client { match client {
Ok(_) => { Ok(_) => {

Binary file not shown.

BIN
empty_database.sqlite Normal file

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[package]
name = "brocade-switch"
edition = "2024"
version.workspace = true
readme.workspace = true
license.workspace = true
[dependencies]
harmony = { path = "../../harmony" }
harmony_cli = { path = "../../harmony_cli" }
harmony_macros = { path = "../../harmony_macros" }
harmony_types = { path = "../../harmony_types" }
tokio.workspace = true
url.workspace = true
async-trait.workspace = true
serde.workspace = true
log.workspace = true
env_logger.workspace = true
brocade = { path = "../../brocade" }

View File

@@ -0,0 +1,157 @@
use std::str::FromStr;
use async_trait::async_trait;
use brocade::{BrocadeOptions, PortOperatingMode};
use harmony::{
data::Version,
infra::brocade::BrocadeSwitchClient,
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
score::Score,
topology::{
HostNetworkConfig, PortConfig, PreparationError, PreparationOutcome, Switch, SwitchClient,
SwitchError, Topology,
},
};
use harmony_macros::ip;
use harmony_types::{id::Id, net::MacAddress, switch::PortLocation};
use log::{debug, info};
use serde::Serialize;
#[tokio::main]
async fn main() {
let switch_score = BrocadeSwitchScore {
port_channels_to_clear: vec![
Id::from_str("17").unwrap(),
Id::from_str("19").unwrap(),
Id::from_str("18").unwrap(),
],
ports_to_configure: vec![
(PortLocation(2, 0, 17), PortOperatingMode::Trunk),
(PortLocation(2, 0, 19), PortOperatingMode::Trunk),
(PortLocation(1, 0, 18), PortOperatingMode::Trunk),
],
};
harmony_cli::run(
Inventory::autoload(),
SwitchTopology::new().await,
vec![Box::new(switch_score)],
None,
)
.await
.unwrap();
}
#[derive(Clone, Debug, Serialize)]
struct BrocadeSwitchScore {
port_channels_to_clear: Vec<Id>,
ports_to_configure: Vec<PortConfig>,
}
impl<T: Topology + Switch> Score<T> for BrocadeSwitchScore {
fn name(&self) -> String {
"BrocadeSwitchScore".to_string()
}
#[doc(hidden)]
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(BrocadeSwitchInterpret {
score: self.clone(),
})
}
}
#[derive(Debug)]
struct BrocadeSwitchInterpret {
score: BrocadeSwitchScore,
}
#[async_trait]
impl<T: Topology + Switch> Interpret<T> for BrocadeSwitchInterpret {
async fn execute(
&self,
_inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
info!("Applying switch configuration {:?}", self.score);
debug!(
"Clearing port channel {:?}",
self.score.port_channels_to_clear
);
topology
.clear_port_channel(&self.score.port_channels_to_clear)
.await
.map_err(|e| InterpretError::new(e.to_string()))?;
debug!("Configuring interfaces {:?}", self.score.ports_to_configure);
topology
.configure_interface(&self.score.ports_to_configure)
.await
.map_err(|e| InterpretError::new(e.to_string()))?;
Ok(Outcome::success("switch configured".to_string()))
}
fn get_name(&self) -> InterpretName {
InterpretName::Custom("BrocadeSwitchInterpret")
}
fn get_version(&self) -> Version {
todo!()
}
fn get_status(&self) -> InterpretStatus {
todo!()
}
fn get_children(&self) -> Vec<Id> {
todo!()
}
}
struct SwitchTopology {
client: Box<dyn SwitchClient>,
}
#[async_trait]
impl Topology for SwitchTopology {
fn name(&self) -> &str {
"SwitchTopology"
}
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> {
Ok(PreparationOutcome::Noop)
}
}
impl SwitchTopology {
async fn new() -> Self {
let mut options = BrocadeOptions::default();
options.ssh.port = 2222;
let client =
BrocadeSwitchClient::init(&vec![ip!("127.0.0.1")], &"admin", &"password", options)
.await
.expect("Failed to connect to switch");
let client = Box::new(client);
Self { client }
}
}
#[async_trait]
impl Switch for SwitchTopology {
async fn setup_switch(&self) -> Result<(), SwitchError> {
todo!()
}
async fn get_port_for_mac_address(
&self,
_mac_address: &MacAddress,
) -> Result<Option<PortLocation>, SwitchError> {
todo!()
}
async fn configure_port_channel(&self, _config: &HostNetworkConfig) -> Result<(), SwitchError> {
todo!()
}
async fn clear_port_channel(&self, ids: &Vec<Id>) -> Result<(), SwitchError> {
self.client.clear_port_channel(ids).await
}
async fn configure_interface(&self, ports: &Vec<PortConfig>) -> Result<(), SwitchError> {
self.client.configure_interface(ports).await
}
}

View File

@@ -2,7 +2,7 @@ use harmony::{
inventory::Inventory, inventory::Inventory,
modules::{ modules::{
dummy::{ErrorScore, PanicScore, SuccessScore}, dummy::{ErrorScore, PanicScore, SuccessScore},
inventory::LaunchDiscoverInventoryAgentScore, inventory::{HarmonyDiscoveryStrategy, LaunchDiscoverInventoryAgentScore},
}, },
topology::LocalhostTopology, topology::LocalhostTopology,
}; };
@@ -18,6 +18,7 @@ async fn main() {
Box::new(PanicScore {}), Box::new(PanicScore {}),
Box::new(LaunchDiscoverInventoryAgentScore { Box::new(LaunchDiscoverInventoryAgentScore {
discovery_timeout: Some(10), discovery_timeout: Some(10),
discovery_strategy: HarmonyDiscoveryStrategy::MDNS,
}), }),
], ],
None, None,

View File

@@ -0,0 +1,15 @@
[package]
name = "harmony_inventory_builder"
edition = "2024"
version.workspace = true
readme.workspace = true
license.workspace = true
[dependencies]
harmony = { path = "../../harmony" }
harmony_cli = { path = "../../harmony_cli" }
harmony_macros = { path = "../../harmony_macros" }
harmony_types = { path = "../../harmony_types" }
tokio.workspace = true
url.workspace = true
cidr.workspace = true

View File

@@ -0,0 +1,11 @@
cargo build -p harmony_inventory_builder --release --target x86_64-unknown-linux-musl
SCRIPT_DIR="$(dirname ${0})"
cd "${SCRIPT_DIR}/docker/"
cp ../../../target/x86_64-unknown-linux-musl/release/harmony_inventory_builder .
docker build . -t hub.nationtech.io/harmony/harmony_inventory_builder
docker push hub.nationtech.io/harmony/harmony_inventory_builder

View File

@@ -0,0 +1,10 @@
FROM debian:12-slim
RUN mkdir /app
WORKDIR /app/
COPY harmony_inventory_builder /app/
ENV RUST_LOG=info
CMD ["sleep", "infinity"]

View File

@@ -0,0 +1,36 @@
use harmony::{
inventory::{HostRole, Inventory},
modules::inventory::{DiscoverHostForRoleScore, HarmonyDiscoveryStrategy},
topology::LocalhostTopology,
};
use harmony_macros::cidrv4;
#[tokio::main]
async fn main() {
let discover_worker = DiscoverHostForRoleScore {
role: HostRole::Worker,
number_desired_hosts: 3,
discovery_strategy: HarmonyDiscoveryStrategy::SUBNET {
cidr: cidrv4!("192.168.0.1/25"),
port: 25000,
},
};
let discover_control_plane = DiscoverHostForRoleScore {
role: HostRole::ControlPlane,
number_desired_hosts: 3,
discovery_strategy: HarmonyDiscoveryStrategy::SUBNET {
cidr: cidrv4!("192.168.0.1/25"),
port: 25000,
},
};
harmony_cli::run(
Inventory::autoload(),
LocalhostTopology::new(),
vec![Box::new(discover_worker), Box::new(discover_control_plane)],
None,
)
.await
.unwrap();
}

View File

@@ -39,10 +39,10 @@ async fn main() {
.expect("Failed to get credentials"); .expect("Failed to get credentials");
let switches: Vec<IpAddr> = vec![ip!("192.168.33.101")]; let switches: Vec<IpAddr> = vec![ip!("192.168.33.101")];
let brocade_options = Some(BrocadeOptions { let brocade_options = BrocadeOptions {
dry_run: *harmony::config::DRY_RUN, dry_run: *harmony::config::DRY_RUN,
..Default::default() ..Default::default()
}); };
let switch_client = BrocadeSwitchClient::init( let switch_client = BrocadeSwitchClient::init(
&switches, &switches,
&switch_auth.username, &switch_auth.username,

View File

@@ -31,10 +31,10 @@ pub async fn get_topology() -> HAClusterTopology {
.expect("Failed to get credentials"); .expect("Failed to get credentials");
let switches: Vec<IpAddr> = vec![ip!("192.168.1.101")]; // TODO: Adjust me let switches: Vec<IpAddr> = vec![ip!("192.168.1.101")]; // TODO: Adjust me
let brocade_options = Some(BrocadeOptions { let brocade_options = BrocadeOptions {
dry_run: *harmony::config::DRY_RUN, dry_run: *harmony::config::DRY_RUN,
..Default::default() ..Default::default()
}); };
let switch_client = BrocadeSwitchClient::init( let switch_client = BrocadeSwitchClient::init(
&switches, &switches,
&switch_auth.username, &switch_auth.username,

View File

@@ -26,10 +26,10 @@ pub async fn get_topology() -> HAClusterTopology {
.expect("Failed to get credentials"); .expect("Failed to get credentials");
let switches: Vec<IpAddr> = vec![ip!("192.168.1.101")]; // TODO: Adjust me let switches: Vec<IpAddr> = vec![ip!("192.168.1.101")]; // TODO: Adjust me
let brocade_options = Some(BrocadeOptions { let brocade_options = BrocadeOptions {
dry_run: *harmony::config::DRY_RUN, dry_run: *harmony::config::DRY_RUN,
..Default::default() ..Default::default()
}); };
let switch_client = BrocadeSwitchClient::init( let switch_client = BrocadeSwitchClient::init(
&switches, &switches,
&switch_auth.username, &switch_auth.username,

View File

@@ -35,10 +35,10 @@ async fn main() {
.expect("Failed to get credentials"); .expect("Failed to get credentials");
let switches: Vec<IpAddr> = vec![ip!("192.168.5.101")]; // TODO: Adjust me let switches: Vec<IpAddr> = vec![ip!("192.168.5.101")]; // TODO: Adjust me
let brocade_options = Some(BrocadeOptions { let brocade_options = BrocadeOptions {
dry_run: *harmony::config::DRY_RUN, dry_run: *harmony::config::DRY_RUN,
..Default::default() ..Default::default()
}); };
let switch_client = BrocadeSwitchClient::init( let switch_client = BrocadeSwitchClient::init(
&switches, &switches,
&switch_auth.username, &switch_auth.username,

View File

@@ -152,10 +152,10 @@ impl PhysicalHost {
pub fn parts_list(&self) -> String { pub fn parts_list(&self) -> String {
let PhysicalHost { let PhysicalHost {
id, id,
category, category: _,
network, network,
storage, storage,
labels, labels: _,
memory_modules, memory_modules,
cpus, cpus,
} = self; } = self;
@@ -226,8 +226,8 @@ impl PhysicalHost {
speed_mhz, speed_mhz,
manufacturer, manufacturer,
part_number, part_number,
serial_number, serial_number: _,
rank, rank: _,
} = mem; } = mem;
parts_list.push_str(&format!( parts_list.push_str(&format!(
"\n{}Gb, {}Mhz, Manufacturer ({}), Part Number ({})", "\n{}Gb, {}Mhz, Manufacturer ({}), Part Number ({})",

View File

@@ -4,6 +4,8 @@ use std::error::Error;
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
use crate::inventory::HostRole;
use super::{ use super::{
data::Version, executors::ExecutorError, inventory::Inventory, topology::PreparationError, data::Version, executors::ExecutorError, inventory::Inventory, topology::PreparationError,
}; };

View File

@@ -1,4 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use brocade::PortOperatingMode;
use harmony_macros::ip; use harmony_macros::ip;
use harmony_types::{ use harmony_types::{
id::Id, id::Id,
@@ -8,7 +9,7 @@ use harmony_types::{
use log::debug; use log::debug;
use log::info; use log::info;
use crate::infra::network_manager::OpenShiftNmStateNetworkManager; use crate::{infra::network_manager::OpenShiftNmStateNetworkManager, topology::PortConfig};
use crate::topology::PxeOptions; use crate::topology::PxeOptions;
use crate::{data::FileContent, executors::ExecutorError}; use crate::{data::FileContent, executors::ExecutorError};
@@ -298,6 +299,16 @@ impl Switch for HAClusterTopology {
Ok(()) Ok(())
} }
async fn clear_port_channel(&self, ids: &Vec<Id>) -> Result<(), SwitchError> {
todo!()
}
async fn configure_interface(
&self,
ports: &Vec<PortConfig>,
) -> Result<(), SwitchError> {
todo!()
}
} }
#[async_trait] #[async_trait]
@@ -521,4 +532,6 @@ impl SwitchClient for DummyInfra {
) -> Result<u8, SwitchError> { ) -> Result<u8, SwitchError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
} }
async fn clear_port_channel(&self, ids: &Vec<Id>) -> Result<(), SwitchError> {todo!()}
async fn configure_interface(&self, ports: &Vec<PortConfig>) -> Result<(), SwitchError> {todo!()}
} }

View File

@@ -7,6 +7,7 @@ use std::{
}; };
use async_trait::async_trait; use async_trait::async_trait;
use brocade::PortOperatingMode;
use derive_new::new; use derive_new::new;
use harmony_types::{ use harmony_types::{
id::Id, id::Id,
@@ -214,6 +215,8 @@ impl From<String> for NetworkError {
} }
} }
pub type PortConfig = (PortLocation, PortOperatingMode);
#[async_trait] #[async_trait]
pub trait Switch: Send + Sync { pub trait Switch: Send + Sync {
async fn setup_switch(&self) -> Result<(), SwitchError>; async fn setup_switch(&self) -> Result<(), SwitchError>;
@@ -224,6 +227,8 @@ pub trait Switch: Send + Sync {
) -> Result<Option<PortLocation>, SwitchError>; ) -> Result<Option<PortLocation>, SwitchError>;
async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError>; async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError>;
async fn clear_port_channel(&self, ids: &Vec<Id>) -> Result<(), SwitchError>;
async fn configure_interface(&self, ports: &Vec<PortConfig>) -> Result<(), SwitchError>;
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@@ -283,6 +288,9 @@ pub trait SwitchClient: Debug + Send + Sync {
channel_name: &str, channel_name: &str,
switch_ports: Vec<PortLocation>, switch_ports: Vec<PortLocation>,
) -> Result<u8, SwitchError>; ) -> Result<u8, SwitchError>;
async fn clear_port_channel(&self, ids: &Vec<Id>) -> Result<(), SwitchError>;
async fn configure_interface(&self, ports: &Vec<PortConfig>) -> Result<(), SwitchError>;
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -14,7 +14,7 @@ use k8s_openapi::{
}, },
apimachinery::pkg::util::intstr::IntOrString, apimachinery::pkg::util::intstr::IntOrString,
}; };
use kube::Resource; use kube::{Resource, api::DynamicObject};
use log::debug; use log::debug;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde_json::json; use serde_json::json;

View File

@@ -1,12 +1,13 @@
use async_trait::async_trait; use async_trait::async_trait;
use brocade::{BrocadeClient, BrocadeOptions, InterSwitchLink, InterfaceStatus, PortOperatingMode}; use brocade::{BrocadeClient, BrocadeOptions, InterSwitchLink, InterfaceStatus, PortOperatingMode};
use harmony_types::{ use harmony_types::{
id::Id,
net::{IpAddress, MacAddress}, net::{IpAddress, MacAddress},
switch::{PortDeclaration, PortLocation}, switch::{PortDeclaration, PortLocation},
}; };
use option_ext::OptionExt; use option_ext::OptionExt;
use crate::topology::{SwitchClient, SwitchError}; use crate::topology::{PortConfig, SwitchClient, SwitchError};
#[derive(Debug)] #[derive(Debug)]
pub struct BrocadeSwitchClient { pub struct BrocadeSwitchClient {
@@ -18,9 +19,9 @@ impl BrocadeSwitchClient {
ip_addresses: &[IpAddress], ip_addresses: &[IpAddress],
username: &str, username: &str,
password: &str, password: &str,
options: Option<BrocadeOptions>, options: BrocadeOptions,
) -> Result<Self, brocade::Error> { ) -> Result<Self, brocade::Error> {
let brocade = brocade::init(ip_addresses, 22, username, password, options).await?; let brocade = brocade::init(ip_addresses, username, password, options).await?;
Ok(Self { brocade }) Ok(Self { brocade })
} }
} }
@@ -59,7 +60,7 @@ impl SwitchClient for BrocadeSwitchClient {
} }
self.brocade self.brocade
.configure_interfaces(interfaces) .configure_interfaces(&interfaces)
.await .await
.map_err(|e| SwitchError::new(e.to_string()))?; .map_err(|e| SwitchError::new(e.to_string()))?;
@@ -111,6 +112,24 @@ impl SwitchClient for BrocadeSwitchClient {
Ok(channel_id) Ok(channel_id)
} }
async fn clear_port_channel(&self, ids: &Vec<Id>) -> Result<(), SwitchError> {
for i in ids {
self.brocade
.clear_port_channel(&i.to_string())
.await
.map_err(|e| SwitchError::new(e.to_string()))?;
}
Ok(())
}
async fn configure_interface(&self, ports: &Vec<PortConfig>) -> Result<(), SwitchError> {
// FIXME hardcoded TenGigabitEthernet = bad
let ports = ports.iter().map(|p| (format!("TenGigabitEthernet {}", p.0), p.1.clone())).collect();
self.brocade
.configure_interfaces(&ports)
.await
.map_err(|e| SwitchError::new(e.to_string()))?;
Ok(())
}
} }
#[cfg(test)] #[cfg(test)]
@@ -147,8 +166,8 @@ mod tests {
let configured_interfaces = brocade.configured_interfaces.lock().unwrap(); let configured_interfaces = brocade.configured_interfaces.lock().unwrap();
assert_that!(*configured_interfaces).contains_exactly(vec![ assert_that!(*configured_interfaces).contains_exactly(vec![
(first_interface.name.clone(), PortOperatingMode::Access), (first_interface.port_location, PortOperatingMode::Access),
(second_interface.name.clone(), PortOperatingMode::Access), (second_interface.port_location, PortOperatingMode::Access),
]); ]);
} }
@@ -255,10 +274,10 @@ mod tests {
async fn configure_interfaces( async fn configure_interfaces(
&self, &self,
interfaces: Vec<(String, PortOperatingMode)>, interfaces: &Vec<(String, PortOperatingMode)>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut configured_interfaces = self.configured_interfaces.lock().unwrap(); let mut configured_interfaces = self.configured_interfaces.lock().unwrap();
*configured_interfaces = interfaces; *configured_interfaces = interfaces.clone();
Ok(()) Ok(())
} }

View File

@@ -121,7 +121,7 @@ mod test {
#[test] #[test]
fn deployment_to_dynamic_roundtrip() { fn deployment_to_dynamic_roundtrip() {
// Create a sample Deployment with nested structures // Create a sample Deployment with nested structures
let mut deployment = Deployment { let deployment = Deployment {
metadata: ObjectMeta { metadata: ObjectMeta {
name: Some("my-deployment".to_string()), name: Some("my-deployment".to_string()),
labels: Some({ labels: Some({

View File

@@ -8,7 +8,6 @@ mod tftp;
use std::sync::Arc; use std::sync::Arc;
pub use management::*; pub use management::*;
use opnsense_config_xml::Host;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::{executors::ExecutorError, topology::LogicalHost}; use crate::{executors::ExecutorError, topology::LogicalHost};

View File

@@ -19,8 +19,11 @@ pub struct DhcpScore {
pub host_binding: Vec<HostBinding>, pub host_binding: Vec<HostBinding>,
pub next_server: Option<IpAddress>, pub next_server: Option<IpAddress>,
pub boot_filename: Option<String>, pub boot_filename: Option<String>,
/// Boot filename to be provided to PXE clients identifying as BIOS
pub filename: Option<String>, pub filename: Option<String>,
/// Boot filename to be provided to PXE clients identifying as uefi but NOT iPXE
pub filename64: Option<String>, pub filename64: Option<String>,
/// Boot filename to be provided to PXE clients identifying as iPXE
pub filenameipxe: Option<String>, pub filenameipxe: Option<String>,
pub dhcp_range: (IpAddress, IpAddress), pub dhcp_range: (IpAddress, IpAddress),
pub domain: Option<String>, pub domain: Option<String>,

View File

@@ -5,11 +5,10 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
data::Version, data::Version,
hardware::PhysicalHost,
infra::inventory::InventoryRepositoryFactory, infra::inventory::InventoryRepositoryFactory,
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::{HostRole, Inventory}, inventory::{HostRole, Inventory},
modules::inventory::LaunchDiscoverInventoryAgentScore, modules::inventory::{HarmonyDiscoveryStrategy, LaunchDiscoverInventoryAgentScore},
score::Score, score::Score,
topology::Topology, topology::Topology,
}; };
@@ -17,11 +16,13 @@ use crate::{
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoverHostForRoleScore { pub struct DiscoverHostForRoleScore {
pub role: HostRole, pub role: HostRole,
pub number_desired_hosts: i16,
pub discovery_strategy : HarmonyDiscoveryStrategy,
} }
impl<T: Topology> Score<T> for DiscoverHostForRoleScore { impl<T: Topology> Score<T> for DiscoverHostForRoleScore {
fn name(&self) -> String { fn name(&self) -> String {
"DiscoverInventoryAgentScore".to_string() format!("DiscoverHostForRoleScore({:?})", self.role)
} }
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
@@ -48,13 +49,15 @@ impl<T: Topology> Interpret<T> for DiscoverHostForRoleInterpret {
); );
LaunchDiscoverInventoryAgentScore { LaunchDiscoverInventoryAgentScore {
discovery_timeout: None, discovery_timeout: None,
discovery_strategy: self.score.discovery_strategy.clone(),
} }
.interpret(inventory, topology) .interpret(inventory, topology)
.await?; .await?;
let host: PhysicalHost; let mut chosen_hosts = vec![];
let host_repo = InventoryRepositoryFactory::build().await?; let host_repo = InventoryRepositoryFactory::build().await?;
let mut assigned_hosts = 0;
loop { loop {
let all_hosts = host_repo.get_all_hosts().await?; let all_hosts = host_repo.get_all_hosts().await?;
@@ -75,15 +78,24 @@ impl<T: Topology> Interpret<T> for DiscoverHostForRoleInterpret {
match ans { match ans {
Ok(choice) => { Ok(choice) => {
info!( info!(
"Selected {} as the {:?} node.", "Assigned role {:?} for node {}",
choice.summary(), self.score.role,
self.score.role choice.summary()
); );
host_repo host_repo
.save_role_mapping(&self.score.role, &choice) .save_role_mapping(&self.score.role, &choice)
.await?; .await?;
host = choice; chosen_hosts.push(choice);
break; assigned_hosts += 1;
info!(
"Found {assigned_hosts} hosts for role {:?}",
self.score.role
);
if assigned_hosts == self.score.number_desired_hosts {
break;
}
} }
Err(inquire::InquireError::OperationCanceled) => { Err(inquire::InquireError::OperationCanceled) => {
info!("Refresh requested. Fetching list of discovered hosts again..."); info!("Refresh requested. Fetching list of discovered hosts again...");
@@ -100,8 +112,13 @@ impl<T: Topology> Interpret<T> for DiscoverHostForRoleInterpret {
} }
Ok(Outcome::success(format!( Ok(Outcome::success(format!(
"Successfully discovered host {} for role {:?}", "Successfully discovered {} hosts {} for role {:?}",
host.summary(), self.score.number_desired_hosts,
chosen_hosts
.iter()
.map(|h| h.summary())
.collect::<Vec<String>>()
.join(", "),
self.score.role self.score.role
))) )))
} }

View File

@@ -1,6 +1,10 @@
mod discovery; mod discovery;
pub mod inspect; pub mod inspect;
use std::net::Ipv4Addr;
use cidr::{Ipv4Cidr, Ipv4Inet};
pub use discovery::*; pub use discovery::*;
use tokio::time::{Duration, timeout};
use async_trait::async_trait; use async_trait::async_trait;
use harmony_inventory_agent::local_presence::DiscoveryEvent; use harmony_inventory_agent::local_presence::DiscoveryEvent;
@@ -24,6 +28,7 @@ use harmony_types::id::Id;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LaunchDiscoverInventoryAgentScore { pub struct LaunchDiscoverInventoryAgentScore {
pub discovery_timeout: Option<u64>, pub discovery_timeout: Option<u64>,
pub discovery_strategy: HarmonyDiscoveryStrategy,
} }
impl<T: Topology> Score<T> for LaunchDiscoverInventoryAgentScore { impl<T: Topology> Score<T> for LaunchDiscoverInventoryAgentScore {
@@ -43,6 +48,12 @@ struct DiscoverInventoryAgentInterpret {
score: LaunchDiscoverInventoryAgentScore, score: LaunchDiscoverInventoryAgentScore,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HarmonyDiscoveryStrategy {
MDNS,
SUBNET { cidr: cidr::Ipv4Cidr, port: u16 },
}
#[async_trait] #[async_trait]
impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret { impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret {
async fn execute( async fn execute(
@@ -57,6 +68,37 @@ impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret {
), ),
}; };
match self.score.discovery_strategy {
HarmonyDiscoveryStrategy::MDNS => self.launch_mdns_discovery().await,
HarmonyDiscoveryStrategy::SUBNET { cidr, port } => {
self.launch_cidr_discovery(&cidr, port).await
}
};
Ok(Outcome::success(
"Discovery process completed successfully".to_string(),
))
}
fn get_name(&self) -> InterpretName {
InterpretName::DiscoverInventoryAgent
}
fn get_version(&self) -> Version {
todo!()
}
fn get_status(&self) -> InterpretStatus {
todo!()
}
fn get_children(&self) -> Vec<Id> {
todo!()
}
}
impl DiscoverInventoryAgentInterpret {
async fn launch_mdns_discovery(&self) {
harmony_inventory_agent::local_presence::discover_agents( harmony_inventory_agent::local_presence::discover_agents(
self.score.discovery_timeout, self.score.discovery_timeout,
|event: DiscoveryEvent| -> Result<(), String> { |event: DiscoveryEvent| -> Result<(), String> {
@@ -112,6 +154,8 @@ impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret {
cpus, cpus,
}; };
// FIXME only save the host when it is new or something changed in it.
// we currently are saving the host every time it is discovered.
let repo = InventoryRepositoryFactory::build() let repo = InventoryRepositoryFactory::build()
.await .await
.map_err(|e| format!("Could not build repository : {e}")) .map_err(|e| format!("Could not build repository : {e}"))
@@ -132,25 +176,111 @@ impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret {
Ok(()) Ok(())
}, },
) )
.await; .await
Ok(Outcome::success(
"Discovery process completed successfully".to_string(),
))
} }
fn get_name(&self) -> InterpretName { // async fn launch_cidr_discovery(&self, cidr : &Ipv4Cidr, port: u16) {
InterpretName::DiscoverInventoryAgent // todo!("launnch cidr discovery for {cidr} : {port}
} // - Iterate over all possible addresses in cidr
// - make calls in batches of 20 attempting to reach harmony inventory agent on <addr, port> using same as above harmony_inventory_agent::client::get_host_inventory(&address, port)
// - Log warn when response is 404, it means the port was used by something else unexpected
// - Log error when response is 5xx
// - Log debug when no response (timeout 15 seconds)
// - Log info when found and response is 2xx
// ");
// }
async fn launch_cidr_discovery(&self, cidr: &Ipv4Cidr, port: u16) {
let addrs: Vec<Ipv4Inet> = cidr.iter().collect();
let total = addrs.len();
info!(
"Starting CIDR discovery for {} hosts on {}/{} (port {})",
total,
cidr.network_length(),
cidr,
port
);
fn get_version(&self) -> Version { let batch_size: usize = 20;
todo!() let timeout_secs = 5;
} let request_timeout = Duration::from_secs(timeout_secs);
fn get_status(&self) -> InterpretStatus { let mut current_batch = 0;
todo!() let num_batches = addrs.len() / batch_size;
}
fn get_children(&self) -> Vec<Id> { for batch in addrs.chunks(batch_size) {
todo!() current_batch += 1;
info!("Starting query batch {current_batch} of {num_batches}, timeout {timeout_secs}");
let mut tasks = Vec::with_capacity(batch.len());
for addr in batch {
let addr = addr.address().to_string();
let port = port;
let task = tokio::spawn(async move {
match timeout(
request_timeout,
harmony_inventory_agent::client::get_host_inventory(&addr, port),
)
.await
{
Ok(Ok(host)) => {
info!("Found and response is 2xx for {addr}:{port}");
// Reuse the same conversion to PhysicalHost as MDNS flow
let harmony_inventory_agent::hwinfo::PhysicalHost {
storage_drives,
storage_controller,
memory_modules,
cpus,
chipset,
network_interfaces,
management_interface,
host_uuid,
} = host;
let host = PhysicalHost {
id: Id::from(host_uuid),
category: HostCategory::Server,
network: network_interfaces,
storage: storage_drives,
labels: vec![Label {
name: "discovered-by".to_string(),
value: "harmony-inventory-agent".to_string(),
}],
memory_modules,
cpus,
};
// Save host to inventory
let repo = InventoryRepositoryFactory::build()
.await
.map_err(|e| format!("Could not build repository : {e}"))
.unwrap();
if let Err(e) = repo.save(&host).await {
log::debug!("Failed to save host {}: {e}", host.id);
} else {
info!("Saved host id {}, summary : {}", host.id, host.summary());
}
}
Ok(Err(e)) => {
log::info!("Error querying inventory agent on {addr}:{port} : {e}");
}
Err(_) => {
// Timeout for this host
log::debug!("No response (timeout) for {addr}:{port}");
}
}
});
tasks.push(task);
}
// Wait for this batch to complete
for t in tasks {
let _ = t.await;
}
}
info!("CIDR discovery completed");
} }
} }

View File

@@ -4,7 +4,7 @@ use crate::{
infra::inventory::InventoryRepositoryFactory, infra::inventory::InventoryRepositoryFactory,
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::{HostRole, Inventory}, inventory::{HostRole, Inventory},
modules::inventory::DiscoverHostForRoleScore, modules::inventory::{DiscoverHostForRoleScore, HarmonyDiscoveryStrategy},
score::Score, score::Score,
topology::HAClusterTopology, topology::HAClusterTopology,
}; };
@@ -104,6 +104,8 @@ When you can dig them, confirm to continue.
bootstrap_host = hosts.into_iter().next().to_owned(); bootstrap_host = hosts.into_iter().next().to_owned();
DiscoverHostForRoleScore { DiscoverHostForRoleScore {
role: HostRole::Bootstrap, role: HostRole::Bootstrap,
number_desired_hosts: 1,
discovery_strategy: HarmonyDiscoveryStrategy::MDNS,
} }
.interpret(inventory, topology) .interpret(inventory, topology)
.await?; .await?;

View File

@@ -6,7 +6,7 @@ use crate::{
inventory::{HostRole, Inventory}, inventory::{HostRole, Inventory},
modules::{ modules::{
dhcp::DhcpHostBindingScore, http::IPxeMacBootFileScore, dhcp::DhcpHostBindingScore, http::IPxeMacBootFileScore,
inventory::DiscoverHostForRoleScore, okd::templates::BootstrapIpxeTpl, inventory::{DiscoverHostForRoleScore, HarmonyDiscoveryStrategy}, okd::templates::BootstrapIpxeTpl,
}, },
score::Score, score::Score,
topology::{HAClusterTopology, HostBinding}, topology::{HAClusterTopology, HostBinding},
@@ -58,38 +58,39 @@ impl OKDSetup03ControlPlaneInterpret {
inventory: &Inventory, inventory: &Inventory,
topology: &HAClusterTopology, topology: &HAClusterTopology,
) -> Result<Vec<PhysicalHost>, InterpretError> { ) -> Result<Vec<PhysicalHost>, InterpretError> {
const REQUIRED_HOSTS: usize = 3; const REQUIRED_HOSTS: i16 = 3;
let repo = InventoryRepositoryFactory::build().await?; let repo = InventoryRepositoryFactory::build().await?;
let mut control_plane_hosts = repo.get_host_for_role(&HostRole::ControlPlane).await?; let control_plane_hosts = repo.get_host_for_role(&HostRole::ControlPlane).await?;
while control_plane_hosts.len() < REQUIRED_HOSTS { info!(
info!( "Discovery of {} control plane hosts in progress, current number {}",
"Discovery of {} control plane hosts in progress, current number {}", REQUIRED_HOSTS,
REQUIRED_HOSTS, control_plane_hosts.len()
control_plane_hosts.len() );
); // This score triggers the discovery agent for a specific role.
// This score triggers the discovery agent for a specific role. DiscoverHostForRoleScore {
DiscoverHostForRoleScore { role: HostRole::ControlPlane,
role: HostRole::ControlPlane, number_desired_hosts: REQUIRED_HOSTS,
} discovery_strategy: HarmonyDiscoveryStrategy::MDNS,
.interpret(inventory, topology)
.await?;
control_plane_hosts = repo.get_host_for_role(&HostRole::ControlPlane).await?;
} }
.interpret(inventory, topology)
.await?;
if control_plane_hosts.len() < REQUIRED_HOSTS { let control_plane_hosts = repo.get_host_for_role(&HostRole::ControlPlane).await?;
Err(InterpretError::new(format!(
if control_plane_hosts.len() < REQUIRED_HOSTS as usize {
return Err(InterpretError::new(format!(
"OKD Requires at least {} control plane hosts, but only found {}. Cannot proceed.", "OKD Requires at least {} control plane hosts, but only found {}. Cannot proceed.",
REQUIRED_HOSTS, REQUIRED_HOSTS,
control_plane_hosts.len() control_plane_hosts.len()
))) )));
} else {
// Take exactly the number of required hosts to ensure consistency.
Ok(control_plane_hosts
.into_iter()
.take(REQUIRED_HOSTS)
.collect())
} }
// Take exactly the number of required hosts to ensure consistency.
Ok(control_plane_hosts
.into_iter()
.take(REQUIRED_HOSTS as usize)
.collect())
} }
/// Configures DHCP host bindings for all control plane nodes. /// Configures DHCP host bindings for all control plane nodes.

View File

@@ -251,14 +251,14 @@ impl<T: Topology + NetworkManager + Switch> Interpret<T> for HostNetworkConfigur
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use assertor::*; use assertor::*;
use brocade::PortOperatingMode;
use harmony_types::{net::MacAddress, switch::PortLocation}; use harmony_types::{net::MacAddress, switch::PortLocation};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use crate::{ use crate::{
hardware::HostCategory, hardware::HostCategory,
topology::{ topology::{
HostNetworkConfig, NetworkError, PreparationError, PreparationOutcome, SwitchError, HostNetworkConfig, NetworkError, PortConfig, PreparationError, PreparationOutcome, SwitchError, SwitchPort
SwitchPort,
}, },
}; };
use std::{ use std::{
@@ -692,5 +692,14 @@ mod tests {
Ok(()) Ok(())
} }
async fn clear_port_channel(&self, ids: &Vec<Id>) -> Result<(), SwitchError> {
todo!()
}
async fn configure_interface(
&self,
port_config: &Vec<PortConfig>,
) -> Result<(), SwitchError> {
todo!()
}
} }
} }

View File

@@ -0,0 +1,11 @@
cargo build -p harmony_inventory_agent --release --target x86_64-unknown-linux-musl
SCRIPT_DIR="$(dirname ${0})"
cd "${SCRIPT_DIR}/docker/"
cp ../../target/x86_64-unknown-linux-musl/release/harmony_inventory_agent .
docker build . -t hub.nationtech.io/harmony/harmony_inventory_agent
docker push hub.nationtech.io/harmony/harmony_inventory_agent

View File

@@ -0,0 +1 @@
harmony_inventory_agent

View File

@@ -0,0 +1,17 @@
FROM debian:12-slim
# install packages required to make these commands available : lspci, lsmod, dmidecode, smartctl, ip
RUN apt-get update && \
apt-get install -y --no-install-recommends pciutils kmod dmidecode smartmontools iproute2 && \
rm -rf /var/lib/apt/lists/*
RUN mkdir /app
WORKDIR /app/
COPY harmony_inventory_agent /app/
ENV RUST_LOG=info
CMD [ "/app/harmony_inventory_agent" ]

View File

@@ -0,0 +1,117 @@
apiVersion: v1
kind: Namespace
metadata:
name: harmony-inventory-agent
labels:
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/audit: privileged
pod-security.kubernetes.io/warn: privileged
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: harmony-inventory-agent
namespace: harmony-inventory-agent
---
# Grant the built-in "privileged" SCC to the SA
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: use-privileged-scc
namespace: harmony-inventory-agent
rules:
- apiGroups: ["security.openshift.io"]
resources: ["securitycontextconstraints"]
resourceNames: ["privileged"]
verbs: ["use"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: use-privileged-scc
namespace: harmony-inventory-agent
subjects:
- kind: ServiceAccount
name: harmony-inventory-agent
namespace: harmony-inventory-agent
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: use-privileged-scc
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: harmony-inventory-agent
namespace: harmony-inventory-agent
spec:
selector:
matchLabels:
app: harmony-inventory-agent
template:
metadata:
labels:
app: harmony-inventory-agent
spec:
serviceAccountName: harmony-inventory-agent
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
tolerations:
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: inventory-agent
image: hub.nationtech.io/harmony/harmony_inventory_agent
imagePullPolicy: Always
env:
- name: RUST_LOG
value: "harmony_inventory_agent=trace,info"
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
securityContext:
privileged: true
# optional: leave the rest unset since privileged SCC allows it
#
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: harmony-inventory-builder
namespace: harmony-inventory-agent
spec:
replicas: 1
strategy: {}
selector:
matchLabels:
app: harmony-inventory-builder
template:
metadata:
labels:
app: harmony-inventory-builder
spec:
serviceAccountName: harmony-inventory-agent
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: inventory-agent
image: hub.nationtech.io/harmony/harmony_inventory_builder
imagePullPolicy: Always
env:
- name: RUST_LOG
value: "harmony_inventory_builder=trace,info"
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
securityContext:
privileged: true
# optional: leave the rest unset since privileged SCC allows it

View File

@@ -1,5 +1,5 @@
use harmony_types::net::MacAddress; use harmony_types::net::MacAddress;
use log::{debug, warn}; use log::{debug, trace, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::fs; use std::fs;
@@ -121,20 +121,48 @@ pub struct ManagementInterface {
impl PhysicalHost { impl PhysicalHost {
pub fn gather() -> Result<Self, String> { pub fn gather() -> Result<Self, String> {
trace!("Start gathering physical host information");
let mut sys = System::new_all(); let mut sys = System::new_all();
trace!("System new_all called");
sys.refresh_all(); sys.refresh_all();
trace!("System refresh_all called");
Self::all_tools_available()?; Self::all_tools_available()?;
trace!("All tools_available success");
let storage_drives = Self::gather_storage_drives()?;
trace!("got storage drives");
let storage_controller = Self::gather_storage_controller()?;
trace!("got storage controller");
let memory_modules = Self::gather_memory_modules()?;
trace!("got memory_modules");
let cpus = Self::gather_cpus(&sys)?;
trace!("got cpus");
let chipset = Self::gather_chipset()?;
trace!("got chipsets");
let network_interfaces = Self::gather_network_interfaces()?;
trace!("got network_interfaces");
let management_interface = Self::gather_management_interface()?;
trace!("got management_interface");
let host_uuid = Self::get_host_uuid()?;
Ok(Self { Ok(Self {
storage_drives: Self::gather_storage_drives()?, storage_drives,
storage_controller: Self::gather_storage_controller()?, storage_controller,
memory_modules: Self::gather_memory_modules()?, memory_modules,
cpus: Self::gather_cpus(&sys)?, cpus,
chipset: Self::gather_chipset()?, chipset,
network_interfaces: Self::gather_network_interfaces()?, network_interfaces,
management_interface: Self::gather_management_interface()?, management_interface,
host_uuid: Self::get_host_uuid()?, host_uuid,
}) })
} }
@@ -208,6 +236,8 @@ impl PhysicalHost {
)); ));
} }
debug!("All tools found!");
Ok(()) Ok(())
} }
@@ -231,7 +261,10 @@ impl PhysicalHost {
fn gather_storage_drives() -> Result<Vec<StorageDrive>, String> { fn gather_storage_drives() -> Result<Vec<StorageDrive>, String> {
let mut drives = Vec::new(); let mut drives = Vec::new();
trace!("Starting storage drive discovery using lsblk");
// Use lsblk with JSON output for robust parsing // Use lsblk with JSON output for robust parsing
trace!("Executing 'lsblk -d -o NAME,MODEL,SERIAL,SIZE,ROTA,WWN -n -e 7 --json'");
let output = Command::new("lsblk") let output = Command::new("lsblk")
.args([ .args([
"-d", "-d",
@@ -245,13 +278,18 @@ impl PhysicalHost {
.output() .output()
.map_err(|e| format!("Failed to execute lsblk: {}", e))?; .map_err(|e| format!("Failed to execute lsblk: {}", e))?;
trace!(
"lsblk command executed successfully (status: {:?})",
output.status
);
if !output.status.success() { if !output.status.success() {
return Err(format!( let stderr_str = String::from_utf8_lossy(&output.stderr);
"lsblk command failed: {}", debug!("lsblk command failed: {stderr_str}");
String::from_utf8_lossy(&output.stderr) return Err(format!("lsblk command failed: {stderr_str}"));
));
} }
trace!("Parsing lsblk JSON output");
let json: Value = serde_json::from_slice(&output.stdout) let json: Value = serde_json::from_slice(&output.stdout)
.map_err(|e| format!("Failed to parse lsblk JSON output: {}", e))?; .map_err(|e| format!("Failed to parse lsblk JSON output: {}", e))?;
@@ -260,6 +298,8 @@ impl PhysicalHost {
.and_then(|v| v.as_array()) .and_then(|v| v.as_array())
.ok_or("Invalid lsblk JSON: missing 'blockdevices' array")?; .ok_or("Invalid lsblk JSON: missing 'blockdevices' array")?;
trace!("Found {} blockdevices in lsblk output", blockdevices.len());
for device in blockdevices { for device in blockdevices {
let name = device let name = device
.get("name") .get("name")
@@ -268,52 +308,72 @@ impl PhysicalHost {
.to_string(); .to_string();
if name.is_empty() { if name.is_empty() {
trace!("Skipping unnamed device entry: {:?}", device);
continue; continue;
} }
trace!("Inspecting block device: {name}");
// Extract metadata fields
let model = device let model = device
.get("model") .get("model")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.map(|s| s.trim().to_string()) .map(|s| s.trim().to_string())
.unwrap_or_default(); .unwrap_or_default();
trace!("Model for {name}: '{}'", model);
let serial = device let serial = device
.get("serial") .get("serial")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.map(|s| s.trim().to_string()) .map(|s| s.trim().to_string())
.unwrap_or_default(); .unwrap_or_default();
trace!("Serial for {name}: '{}'", serial);
let size_str = device let size_str = device
.get("size") .get("size")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.ok_or("Missing 'size' in lsblk device")?; .ok_or("Missing 'size' in lsblk device")?;
trace!("Reported size for {name}: {}", size_str);
let size_bytes = Self::parse_size(size_str)?; let size_bytes = Self::parse_size(size_str)?;
trace!("Parsed size for {name}: {} bytes", size_bytes);
let rotational = device let rotational = device
.get("rota") .get("rota")
.and_then(|v| v.as_bool()) .and_then(|v| v.as_bool())
.ok_or("Missing 'rota' in lsblk device")?; .ok_or("Missing 'rota' in lsblk device")?;
trace!("Rotational flag for {name}: {}", rotational);
let wwn = device let wwn = device
.get("wwn") .get("wwn")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.map(|s| s.trim().to_string()) .map(|s| s.trim().to_string())
.filter(|s| !s.is_empty() && s != "null"); .filter(|s| !s.is_empty() && s != "null");
trace!("WWN for {name}: {:?}", wwn);
let device_path = Path::new("/sys/block").join(&name); let device_path = Path::new("/sys/block").join(&name);
trace!("Sysfs path for {name}: {:?}", device_path);
trace!("Reading logical block size for {name}");
let logical_block_size = Self::read_sysfs_u32( let logical_block_size = Self::read_sysfs_u32(
&device_path.join("queue/logical_block_size"), &device_path.join("queue/logical_block_size"),
) )
.map_err(|e| format!("Failed to read logical block size for {}: {}", name, e))?; .map_err(|e| format!("Failed to read logical block size for {}: {}", name, e))?;
trace!("Logical block size for {name}: {}", logical_block_size);
trace!("Reading physical block size for {name}");
let physical_block_size = Self::read_sysfs_u32( let physical_block_size = Self::read_sysfs_u32(
&device_path.join("queue/physical_block_size"), &device_path.join("queue/physical_block_size"),
) )
.map_err(|e| format!("Failed to read physical block size for {}: {}", name, e))?; .map_err(|e| format!("Failed to read physical block size for {}: {}", name, e))?;
trace!("Physical block size for {name}: {}", physical_block_size);
trace!("Determining interface type for {name}");
let interface_type = Self::get_interface_type(&name, &device_path)?; let interface_type = Self::get_interface_type(&name, &device_path)?;
trace!("Interface type for {name}: {}", interface_type);
trace!("Getting SMART status for {name}");
let smart_status = Self::get_smart_status(&name)?; let smart_status = Self::get_smart_status(&name)?;
trace!("SMART status for {name}: {:?}", smart_status);
let mut drive = StorageDrive { let mut drive = StorageDrive {
name: name.clone(), name: name.clone(),
@@ -330,19 +390,31 @@ impl PhysicalHost {
// Enhance with additional sysfs info if available // Enhance with additional sysfs info if available
if device_path.exists() { if device_path.exists() {
trace!("Enhancing drive {name} with extra sysfs metadata");
if drive.model.is_empty() { if drive.model.is_empty() {
trace!("Reading model from sysfs for {name}");
drive.model = Self::read_sysfs_string(&device_path.join("device/model")) drive.model = Self::read_sysfs_string(&device_path.join("device/model"))
.unwrap_or(format!("Failed to read model for {}", name)); .unwrap_or_else(|_| format!("Failed to read model for {}", name));
} }
if drive.serial.is_empty() { if drive.serial.is_empty() {
trace!("Reading serial from sysfs for {name}");
drive.serial = Self::read_sysfs_string(&device_path.join("device/serial")) drive.serial = Self::read_sysfs_string(&device_path.join("device/serial"))
.unwrap_or(format!("Failed to read serial for {}", name)); .unwrap_or_else(|_| format!("Failed to read serial for {}", name));
} }
} else {
trace!(
"Sysfs path {:?} not found for drive {name}, skipping extra metadata",
device_path
);
} }
debug!("Discovered storage drive: {drive:?}");
drives.push(drive); drives.push(drive);
} }
debug!("Discovered total {} storage drives", drives.len());
trace!("All discovered dives: {drives:?}");
Ok(drives) Ok(drives)
} }
@@ -418,6 +490,8 @@ impl PhysicalHost {
} }
} }
debug!("Found storage controller {controller:?}");
Ok(controller) Ok(controller)
} }
@@ -486,6 +560,7 @@ impl PhysicalHost {
} }
} }
debug!("Found memory modules {modules:?}");
Ok(modules) Ok(modules)
} }
@@ -501,22 +576,30 @@ impl PhysicalHost {
frequency_mhz: global_cpu.frequency(), frequency_mhz: global_cpu.frequency(),
}); });
debug!("Found cpus {cpus:?}");
Ok(cpus) Ok(cpus)
} }
fn gather_chipset() -> Result<Chipset, String> { fn gather_chipset() -> Result<Chipset, String> {
Ok(Chipset { let chipset = Chipset {
name: Self::read_dmi("baseboard-product-name")?, name: Self::read_dmi("baseboard-product-name")?,
vendor: Self::read_dmi("baseboard-manufacturer")?, vendor: Self::read_dmi("baseboard-manufacturer")?,
}) };
debug!("Found chipset {chipset:?}");
Ok(chipset)
} }
fn gather_network_interfaces() -> Result<Vec<NetworkInterface>, String> { fn gather_network_interfaces() -> Result<Vec<NetworkInterface>, String> {
let mut interfaces = Vec::new(); let mut interfaces = Vec::new();
let sys_net_path = Path::new("/sys/class/net"); let sys_net_path = Path::new("/sys/class/net");
trace!("Reading /sys/class/net");
let entries = fs::read_dir(sys_net_path) let entries = fs::read_dir(sys_net_path)
.map_err(|e| format!("Failed to read /sys/class/net: {}", e))?; .map_err(|e| format!("Failed to read /sys/class/net: {}", e))?;
trace!("Got entries {entries:?}");
for entry in entries { for entry in entries {
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
@@ -525,6 +608,7 @@ impl PhysicalHost {
.into_string() .into_string()
.map_err(|_| "Invalid UTF-8 in interface name")?; .map_err(|_| "Invalid UTF-8 in interface name")?;
let iface_path = entry.path(); let iface_path = entry.path();
trace!("Inspecting interface {iface_name} path {iface_path:?}");
// Skip virtual interfaces // Skip virtual interfaces
if iface_name.starts_with("lo") if iface_name.starts_with("lo")
@@ -535,70 +619,101 @@ impl PhysicalHost {
|| iface_name.starts_with("tun") || iface_name.starts_with("tun")
|| iface_name.starts_with("wg") || iface_name.starts_with("wg")
{ {
trace!(
"Skipping interface {iface_name} because it appears to be virtual/unsupported"
);
continue; continue;
} }
// Check if it's a physical interface by looking for device directory // Check if it's a physical interface by looking for device directory
if !iface_path.join("device").exists() { if !iface_path.join("device").exists() {
trace!(
"Skipping interface {iface_name} since {iface_path:?}/device does not exist"
);
continue; continue;
} }
trace!("Reading MAC address for {iface_name}");
let mac_address = Self::read_sysfs_string(&iface_path.join("address")) let mac_address = Self::read_sysfs_string(&iface_path.join("address"))
.map_err(|e| format!("Failed to read MAC address for {}: {}", iface_name, e))?; .map_err(|e| format!("Failed to read MAC address for {}: {}", iface_name, e))?;
let mac_address = MacAddress::try_from(mac_address).map_err(|e| e.to_string())?; let mac_address = MacAddress::try_from(mac_address).map_err(|e| e.to_string())?;
trace!("MAC address for {iface_name}: {mac_address}");
let speed_mbps = if iface_path.join("speed").exists() { let speed_path = iface_path.join("speed");
match Self::read_sysfs_u32(&iface_path.join("speed")) { let speed_mbps = if speed_path.exists() {
Ok(speed) => Some(speed), trace!("Reading speed for {iface_name} from {:?}", speed_path);
match Self::read_sysfs_u32(&speed_path) {
Ok(speed) => {
trace!("Speed for {iface_name}: {speed} Mbps");
Some(speed)
}
Err(e) => { Err(e) => {
debug!( debug!(
"Failed to read speed for {}: {} . This is expected to fail on wifi interfaces.", "Failed to read speed for {}: {} (this may be expected on WiFi interfaces)",
iface_name, e iface_name, e
); );
None None
} }
} }
} else { } else {
trace!("Speed file not found for {iface_name}, skipping");
None None
}; };
trace!("Reading operstate for {iface_name}");
let operstate = Self::read_sysfs_string(&iface_path.join("operstate")) let operstate = Self::read_sysfs_string(&iface_path.join("operstate"))
.map_err(|e| format!("Failed to read operstate for {}: {}", iface_name, e))?; .map_err(|e| format!("Failed to read operstate for {}: {}", iface_name, e))?;
trace!("Operstate for {iface_name}: {operstate}");
trace!("Reading MTU for {iface_name}");
let mtu = Self::read_sysfs_u32(&iface_path.join("mtu")) let mtu = Self::read_sysfs_u32(&iface_path.join("mtu"))
.map_err(|e| format!("Failed to read MTU for {}: {}", iface_name, e))?; .map_err(|e| format!("Failed to read MTU for {}: {}", iface_name, e))?;
trace!("MTU for {iface_name}: {mtu}");
trace!("Reading driver for {iface_name}");
let driver = let driver =
Self::read_sysfs_symlink_basename(&iface_path.join("device/driver/module")) Self::read_sysfs_symlink_basename(&iface_path.join("device/driver/module"))
.map_err(|e| format!("Failed to read driver for {}: {}", iface_name, e))?; .map_err(|e| format!("Failed to read driver for {}: {}", iface_name, e))?;
trace!("Driver for {iface_name}: {driver}");
trace!("Reading firmware version for {iface_name}");
let firmware_version = Self::read_sysfs_opt_string( let firmware_version = Self::read_sysfs_opt_string(
&iface_path.join("device/firmware_version"), &iface_path.join("device/firmware_version"),
) )
.map_err(|e| format!("Failed to read firmware version for {}: {}", iface_name, e))?; .map_err(|e| format!("Failed to read firmware version for {}: {}", iface_name, e))?;
trace!("Firmware version for {iface_name}: {firmware_version:?}");
// Get IP addresses using ip command with JSON output trace!("Fetching IP addresses for {iface_name}");
let (ipv4_addresses, ipv6_addresses) = Self::get_interface_ips_json(&iface_name) let (ipv4_addresses, ipv6_addresses) = Self::get_interface_ips_json(&iface_name)
.map_err(|e| format!("Failed to get IP addresses for {}: {}", iface_name, e))?; .map_err(|e| format!("Failed to get IP addresses for {}: {}", iface_name, e))?;
trace!("Interface {iface_name} has IPv4: {ipv4_addresses:?}, IPv6: {ipv6_addresses:?}");
interfaces.push(NetworkInterface { let is_up = operstate == "up";
name: iface_name, trace!("Constructing NetworkInterface for {iface_name} (is_up={is_up})");
let iface = NetworkInterface {
name: iface_name.clone(),
mac_address, mac_address,
speed_mbps, speed_mbps,
is_up: operstate == "up", is_up,
mtu, mtu,
ipv4_addresses, ipv4_addresses,
ipv6_addresses, ipv6_addresses,
driver, driver,
firmware_version, firmware_version,
}); };
debug!("Discovered interface: {iface:?}");
interfaces.push(iface);
} }
debug!("Discovered total {} network interfaces", interfaces.len());
trace!("Interfaces collected: {interfaces:?}");
Ok(interfaces) Ok(interfaces)
} }
fn gather_management_interface() -> Result<Option<ManagementInterface>, String> { fn gather_management_interface() -> Result<Option<ManagementInterface>, String> {
if Path::new("/dev/ipmi0").exists() { let mgmt = if Path::new("/dev/ipmi0").exists() {
Ok(Some(ManagementInterface { Ok(Some(ManagementInterface {
kind: "IPMI".to_string(), kind: "IPMI".to_string(),
address: None, address: None,
@@ -612,11 +727,16 @@ impl PhysicalHost {
})) }))
} else { } else {
Ok(None) Ok(None)
} };
debug!("Found management interface {mgmt:?}");
mgmt
} }
fn get_host_uuid() -> Result<String, String> { fn get_host_uuid() -> Result<String, String> {
Self::read_dmi("system-uuid") let uuid = Self::read_dmi("system-uuid");
debug!("Found uuid {uuid:?}");
uuid
} }
// Helper methods // Helper methods
@@ -709,7 +829,8 @@ impl PhysicalHost {
Ok("Ramdisk".to_string()) Ok("Ramdisk".to_string())
} else { } else {
// Try to determine from device path // Try to determine from device path
let subsystem = Self::read_sysfs_string(&device_path.join("device/subsystem"))?; let subsystem = Self::read_sysfs_string(&device_path.join("device/subsystem"))
.unwrap_or(String::new());
Ok(subsystem Ok(subsystem
.split('/') .split('/')
.next_back() .next_back()
@@ -779,6 +900,8 @@ impl PhysicalHost {
size.map(|s| s as u64) size.map(|s| s as u64)
} }
// FIXME when scanning an interface that is part of a bond/bridge we won't get an address on the
// interface, we should be looking at the bond/bridge device. For example, br-ex on k8s nodes.
fn get_interface_ips_json(iface_name: &str) -> Result<(Vec<String>, Vec<String>), String> { fn get_interface_ips_json(iface_name: &str) -> Result<(Vec<String>, Vec<String>), String> {
let mut ipv4 = Vec::new(); let mut ipv4 = Vec::new();
let mut ipv6 = Vec::new(); let mut ipv6 = Vec::new();

View File

@@ -1,4 +1,4 @@
use log::{debug, error, info, warn}; use log::{debug, error, info, trace, warn};
use mdns_sd::{ServiceDaemon, ServiceInfo}; use mdns_sd::{ServiceDaemon, ServiceInfo};
use std::collections::HashMap; use std::collections::HashMap;
@@ -12,6 +12,7 @@ use crate::{
/// This function is synchronous and non-blocking. It spawns a background Tokio task /// This function is synchronous and non-blocking. It spawns a background Tokio task
/// to handle the mDNS advertisement for the lifetime of the application. /// to handle the mDNS advertisement for the lifetime of the application.
pub fn advertise(service_port: u16) -> Result<(), PresenceError> { pub fn advertise(service_port: u16) -> Result<(), PresenceError> {
trace!("starting advertisement process for port {service_port}");
let host_id = match PhysicalHost::gather() { let host_id = match PhysicalHost::gather() {
Ok(host) => Some(host.host_uuid), Ok(host) => Some(host.host_uuid),
Err(e) => { Err(e) => {
@@ -20,11 +21,15 @@ pub fn advertise(service_port: u16) -> Result<(), PresenceError> {
} }
}; };
trace!("Found host id {host_id:?}");
let instance_name = format!( let instance_name = format!(
"inventory-agent-{}", "inventory-agent-{}",
host_id.clone().unwrap_or("unknown".to_string()) host_id.clone().unwrap_or("unknown".to_string())
); );
trace!("Found host id {host_id:?}, name : {instance_name}");
let spawned_msg = format!("Spawned local presence advertisement task for '{instance_name}'."); let spawned_msg = format!("Spawned local presence advertisement task for '{instance_name}'.");
tokio::spawn(async move { tokio::spawn(async move {

View File

@@ -28,7 +28,7 @@ async fn inventory() -> impl Responder {
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
env_logger::init(); env_logger::init();
let port = env::var("HARMONY_INVENTORY_AGENT_PORT").unwrap_or_else(|_| "8080".to_string()); let port = env::var("HARMONY_INVENTORY_AGENT_PORT").unwrap_or_else(|_| "25000".to_string());
let port = port let port = port
.parse::<u16>() .parse::<u16>()
.expect(&format!("Invalid port number, cannot parse to u16 {port}")); .expect(&format!("Invalid port number, cannot parse to u16 {port}"));

View File

@@ -9,3 +9,4 @@ license.workspace = true
serde.workspace = true serde.workspace = true
url.workspace = true url.workspace = true
rand.workspace = true rand.workspace = true
log.workspace = true

View File

@@ -1,4 +1,6 @@
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
use log::trace;
use serde::Serialize;
/// Simple error type for port parsing failures. /// Simple error type for port parsing failures.
#[derive(Debug)] #[derive(Debug)]
@@ -21,7 +23,7 @@ impl fmt::Display for PortParseError {
/// Represents the atomic, physical location of a switch port: `<Stack>/<Module>/<Port>`. /// Represents the atomic, physical location of a switch port: `<Stack>/<Module>/<Port>`.
/// ///
/// Example: `1/1/1` /// Example: `1/1/1`
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize)]
pub struct PortLocation(pub u8, pub u8, pub u8); pub struct PortLocation(pub u8, pub u8, pub u8);
impl fmt::Display for PortLocation { impl fmt::Display for PortLocation {
@@ -70,6 +72,11 @@ impl FromStr for PortLocation {
pub enum PortDeclaration { pub enum PortDeclaration {
/// A single switch port defined by its location. Example: `PortDeclaration::Single(1/1/1)` /// A single switch port defined by its location. Example: `PortDeclaration::Single(1/1/1)`
Single(PortLocation), Single(PortLocation),
/// A Named port, often used for virtual ports such as PortChannels. Example
/// ```rust
/// PortDeclaration::Named("1".to_string())
/// ```
Named(String),
/// A strictly sequential range defined by two endpoints using the hyphen separator (`-`). /// A strictly sequential range defined by two endpoints using the hyphen separator (`-`).
/// All ports between the endpoints (inclusive) are implicitly included. /// All ports between the endpoints (inclusive) are implicitly included.
/// Example: `PortDeclaration::Range(1/1/1, 1/1/4)` /// Example: `PortDeclaration::Range(1/1/1, 1/1/4)`
@@ -130,8 +137,14 @@ impl PortDeclaration {
return Ok(PortDeclaration::Set(start_port, end_port)); return Ok(PortDeclaration::Set(start_port, end_port));
} }
let location = PortLocation::from_str(port_str)?; match PortLocation::from_str(port_str) {
Ok(PortDeclaration::Single(location)) Ok(loc) => Ok(PortDeclaration::Single(loc)),
Err(e) => {
trace!("Failed to parse PortLocation {port_str} : {e}");
trace!("Falling back on named port");
Ok(PortDeclaration::Named(port_str.to_string()))
}
}
} }
} }
@@ -141,6 +154,7 @@ impl fmt::Display for PortDeclaration {
PortDeclaration::Single(port) => write!(f, "{port}"), PortDeclaration::Single(port) => write!(f, "{port}"),
PortDeclaration::Range(start, end) => write!(f, "{start}-{end}"), PortDeclaration::Range(start, end) => write!(f, "{start}-{end}"),
PortDeclaration::Set(start, end) => write!(f, "{start}*{end}"), PortDeclaration::Set(start, end) => write!(f, "{start}*{end}"),
PortDeclaration::Named(name) => write!(f, "{name}"),
} }
} }
} }

View File

@@ -195,7 +195,7 @@ pub struct System {
pub disablechecksumoffloading: u8, pub disablechecksumoffloading: u8,
pub disablesegmentationoffloading: u8, pub disablesegmentationoffloading: u8,
pub disablelargereceiveoffloading: u8, pub disablelargereceiveoffloading: u8,
pub ipv6allow: u8, pub ipv6allow: Option<u8>,
pub powerd_ac_mode: String, pub powerd_ac_mode: String,
pub powerd_battery_mode: String, pub powerd_battery_mode: String,
pub powerd_normal_mode: String, pub powerd_normal_mode: String,
@@ -226,6 +226,7 @@ pub struct System {
pub dns6gw: Option<String>, pub dns6gw: Option<String>,
pub dns7gw: Option<String>, pub dns7gw: Option<String>,
pub dns8gw: Option<String>, pub dns8gw: Option<String>,
pub prefer_ipv4: Option<String>,
pub dnsallowoverride: u8, pub dnsallowoverride: u8,
pub dnsallowoverride_exclude: Option<MaybeString>, pub dnsallowoverride_exclude: Option<MaybeString>,
} }
@@ -329,6 +330,7 @@ pub struct Range {
pub struct StaticMap { pub struct StaticMap {
pub mac: String, pub mac: String,
pub ipaddr: String, pub ipaddr: String,
pub cid: Option<MaybeString>,
pub hostname: String, pub hostname: String,
pub descr: Option<MaybeString>, pub descr: Option<MaybeString>,
pub winsserver: MaybeString, pub winsserver: MaybeString,
@@ -764,9 +766,19 @@ pub struct Jobs {
pub struct Job { pub struct Job {
#[yaserde(attribute = true)] #[yaserde(attribute = true)]
pub uuid: MaybeString, pub uuid: MaybeString,
#[yaserde(rename = "name")] pub name: Option<MaybeString>,
pub name: MaybeString,
// Add other fields as needed // Add other fields as needed
pub origin: Option<MaybeString>,
pub enabled: Option<MaybeString>,
pub minutes: Option<MaybeString>,
pub hours: Option<MaybeString>,
pub days: Option<MaybeString>,
pub months: Option<MaybeString>,
pub weekdays: Option<MaybeString>,
pub who: Option<MaybeString>,
pub command: Option<MaybeString>,
pub parameters: Option<MaybeString>,
pub description: Option<MaybeString>,
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
@@ -895,28 +907,28 @@ pub struct Proxy {
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct ProxyGeneral { pub struct ProxyGeneral {
pub enabled: i8, pub enabled: i8,
pub error_pages: String, pub error_pages: Option<MaybeString>,
#[yaserde(rename = "icpPort")] #[yaserde(rename = "icpPort")]
pub icp_port: MaybeString, pub icp_port: MaybeString,
pub logging: Logging, pub logging: Logging,
#[yaserde(rename = "alternateDNSservers")] #[yaserde(rename = "alternateDNSservers")]
pub alternate_dns_servers: MaybeString, pub alternate_dns_servers: MaybeString,
#[yaserde(rename = "dnsV4First")] #[yaserde(rename = "dnsV4First")]
pub dns_v4_first: i8, pub dns_v4_first: Option<MaybeString>,
#[yaserde(rename = "forwardedForHandling")] #[yaserde(rename = "forwardedForHandling")]
pub forwarded_for_handling: String, pub forwarded_for_handling: Option<MaybeString>,
#[yaserde(rename = "uriWhitespaceHandling")] #[yaserde(rename = "uriWhitespaceHandling")]
pub uri_whitespace_handling: String, pub uri_whitespace_handling: Option<MaybeString>,
#[yaserde(rename = "enablePinger")] #[yaserde(rename = "enablePinger")]
pub enable_pinger: i8, pub enable_pinger: i8,
#[yaserde(rename = "useViaHeader")] #[yaserde(rename = "useViaHeader")]
pub use_via_header: i8, pub use_via_header: Option<MaybeString>,
#[yaserde(rename = "suppressVersion")] #[yaserde(rename = "suppressVersion")]
pub suppress_version: i32, pub suppress_version: Option<MaybeString>,
#[yaserde(rename = "connecttimeout")] #[yaserde(rename = "connecttimeout")]
pub connect_timeout: MaybeString, pub connect_timeout: Option<MaybeString>,
#[yaserde(rename = "VisibleEmail")] #[yaserde(rename = "VisibleEmail")]
pub visible_email: String, pub visible_email: Option<MaybeString>,
#[yaserde(rename = "VisibleHostname")] #[yaserde(rename = "VisibleHostname")]
pub visible_hostname: MaybeString, pub visible_hostname: MaybeString,
pub cache: Cache, pub cache: Cache,
@@ -953,7 +965,7 @@ pub struct LocalCache {
pub cache_mem: i32, pub cache_mem: i32,
pub maximum_object_size: MaybeString, pub maximum_object_size: MaybeString,
pub maximum_object_size_in_memory: MaybeString, pub maximum_object_size_in_memory: MaybeString,
pub memory_cache_mode: String, pub memory_cache_mode: MaybeString,
pub size: i32, pub size: i32,
pub l1: i32, pub l1: i32,
pub l2: i32, pub l2: i32,
@@ -965,13 +977,13 @@ pub struct LocalCache {
pub struct Traffic { pub struct Traffic {
pub enabled: i32, pub enabled: i32,
#[yaserde(rename = "maxDownloadSize")] #[yaserde(rename = "maxDownloadSize")]
pub max_download_size: i32, pub max_download_size: MaybeString,
#[yaserde(rename = "maxUploadSize")] #[yaserde(rename = "maxUploadSize")]
pub max_upload_size: i32, pub max_upload_size: MaybeString,
#[yaserde(rename = "OverallBandwidthTrotteling")] #[yaserde(rename = "OverallBandwidthTrotteling")]
pub overall_bandwidth_trotteling: i32, pub overall_bandwidth_trotteling: MaybeString,
#[yaserde(rename = "perHostTrotteling")] #[yaserde(rename = "perHostTrotteling")]
pub per_host_trotteling: i32, pub per_host_trotteling: MaybeString,
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
@@ -988,7 +1000,7 @@ pub struct ParentProxy {
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct Forward { pub struct Forward {
pub interfaces: String, pub interfaces: MaybeString,
pub port: i32, pub port: i32,
pub sslbumpport: i32, pub sslbumpport: i32,
pub sslbump: i32, pub sslbump: i32,
@@ -1033,9 +1045,9 @@ pub struct Acl {
pub google_apps: MaybeString, pub google_apps: MaybeString,
pub youtube: MaybeString, pub youtube: MaybeString,
#[yaserde(rename = "safePorts")] #[yaserde(rename = "safePorts")]
pub safe_ports: String, pub safe_ports: MaybeString,
#[yaserde(rename = "sslPorts")] #[yaserde(rename = "sslPorts")]
pub ssl_ports: String, pub ssl_ports: MaybeString,
#[yaserde(rename = "remoteACLs")] #[yaserde(rename = "remoteACLs")]
pub remote_acls: RemoteAcls, pub remote_acls: RemoteAcls,
} }
@@ -1051,9 +1063,9 @@ pub struct RemoteAcls {
pub struct Icap { pub struct Icap {
pub enable: i32, pub enable: i32,
#[yaserde(rename = "RequestURL")] #[yaserde(rename = "RequestURL")]
pub request_url: String, pub request_url: MaybeString,
#[yaserde(rename = "ResponseURL")] #[yaserde(rename = "ResponseURL")]
pub response_url: String, pub response_url: MaybeString,
#[yaserde(rename = "SendClientIP")] #[yaserde(rename = "SendClientIP")]
pub send_client_ip: i32, pub send_client_ip: i32,
#[yaserde(rename = "SendUsername")] #[yaserde(rename = "SendUsername")]
@@ -1061,7 +1073,7 @@ pub struct Icap {
#[yaserde(rename = "EncodeUsername")] #[yaserde(rename = "EncodeUsername")]
pub encode_username: i32, pub encode_username: i32,
#[yaserde(rename = "UsernameHeader")] #[yaserde(rename = "UsernameHeader")]
pub username_header: String, pub username_header: MaybeString,
#[yaserde(rename = "EnablePreview")] #[yaserde(rename = "EnablePreview")]
pub enable_preview: i32, pub enable_preview: i32,
#[yaserde(rename = "PreviewSize")] #[yaserde(rename = "PreviewSize")]
@@ -1076,9 +1088,9 @@ pub struct Authentication {
pub method: MaybeString, pub method: MaybeString,
#[yaserde(rename = "authEnforceGroup")] #[yaserde(rename = "authEnforceGroup")]
pub auth_enforce_group: MaybeString, pub auth_enforce_group: MaybeString,
pub realm: String, pub realm: MaybeString,
pub credentialsttl: i32, // This field is already in snake_case pub credentialsttl: MaybeString, // This field is already in snake_case
pub children: i32, pub children: MaybeString,
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
@@ -1293,6 +1305,7 @@ pub struct WireguardServerItem {
pub peers: String, pub peers: String,
pub endpoint: MaybeString, pub endpoint: MaybeString,
pub peer_dns: MaybeString, pub peer_dns: MaybeString,
pub debug: Option<MaybeString>,
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
@@ -1477,6 +1490,7 @@ pub struct Ppp {
pub ports: Option<MaybeString>, pub ports: Option<MaybeString>,
pub username: Option<MaybeString>, pub username: Option<MaybeString>,
pub password: Option<MaybeString>, pub password: Option<MaybeString>,
pub provider: Option<MaybeString>,
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]

View File

@@ -86,10 +86,7 @@ impl<'a> DhcpConfigLegacyISC<'a> {
mac, mac,
ipaddr: ipaddr.to_string(), ipaddr: ipaddr.to_string(),
hostname, hostname,
descr: Default::default(), ..Default::default()
winsserver: Default::default(),
dnsserver: Default::default(),
ntpserver: Default::default(),
}; };
existing_mappings.push(static_map); existing_mappings.push(static_map);
@@ -126,9 +123,7 @@ impl<'a> DhcpConfigLegacyISC<'a> {
ipaddr: entry["ipaddr"].as_str().unwrap_or_default().to_string(), ipaddr: entry["ipaddr"].as_str().unwrap_or_default().to_string(),
hostname: entry["hostname"].as_str().unwrap_or_default().to_string(), hostname: entry["hostname"].as_str().unwrap_or_default().to_string(),
descr: entry["descr"].as_str().map(MaybeString::from), descr: entry["descr"].as_str().map(MaybeString::from),
winsserver: MaybeString::default(), ..Default::default()
dnsserver: MaybeString::default(),
ntpserver: MaybeString::default(),
}) })
.collect(); .collect();