From 755a4b7749de39c441ce0e4bba97e460ce61370b Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Mon, 10 Nov 2025 22:14:39 -0500 Subject: [PATCH 1/5] feat(inventory-agent): Discover algorithm by scanning a subnet of ips, slower than mdns but more reliable and versatile --- Cargo.lock | 25 +++ empty_database.sqlite | Bin 0 -> 32768 bytes examples/cli/src/main.rs | 3 +- examples/harmony_inventory_builder/Cargo.toml | 15 ++ .../harmony_inventory_builder/build_docker.sh | 11 ++ .../docker/Dockerfile | 10 + .../harmony_inventory_builder/src/main.rs | 36 ++++ harmony/src/domain/interpret/mod.rs | 2 + harmony/src/domain/topology/tenant/k8s.rs | 2 +- harmony/src/modules/inventory/discovery.rs | 39 ++-- harmony/src/modules/inventory/mod.rs | 160 +++++++++++++-- .../src/modules/okd/bootstrap_01_prepare.rs | 4 +- .../modules/okd/bootstrap_03_control_plane.rs | 51 ++--- harmony_inventory_agent/build_docker.sh | 11 ++ harmony_inventory_agent/docker/.gitignore | 1 + harmony_inventory_agent/docker/Dockerfile | 17 ++ .../harmony-inventory-agent-daemonset.yaml | 117 +++++++++++ harmony_inventory_agent/src/hwinfo.rs | 183 +++++++++++++++--- .../src/local_presence/advertise.rs | 7 +- harmony_inventory_agent/src/main.rs | 2 +- 20 files changed, 610 insertions(+), 86 deletions(-) create mode 100644 empty_database.sqlite create mode 100644 examples/harmony_inventory_builder/Cargo.toml create mode 100755 examples/harmony_inventory_builder/build_docker.sh create mode 100644 examples/harmony_inventory_builder/docker/Dockerfile create mode 100644 examples/harmony_inventory_builder/src/main.rs create mode 100755 harmony_inventory_agent/build_docker.sh create mode 100644 harmony_inventory_agent/docker/.gitignore create mode 100644 harmony_inventory_agent/docker/Dockerfile create mode 100644 harmony_inventory_agent/harmony-inventory-agent-daemonset.yaml diff --git a/Cargo.lock b/Cargo.lock index 7d9cdcf..f0504aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1853,6 +1853,18 @@ dependencies = [ "url", ] +[[package]] +name = "example-penpot" +version = "0.1.0" +dependencies = [ + "harmony", + "harmony_cli", + "harmony_macros", + "harmony_types", + "tokio", + "url", +] + [[package]] name = "example-pxe" version = "0.1.0" @@ -2479,6 +2491,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "harmony_inventory_builder" +version = "0.1.0" +dependencies = [ + "cidr", + "harmony", + "harmony_cli", + "harmony_macros", + "harmony_types", + "tokio", + "url", +] + [[package]] name = "harmony_macros" version = "0.1.0" diff --git a/empty_database.sqlite b/empty_database.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..a59fb387613059b9a265cc4acac6045b5fd7bd9d GIT binary patch literal 32768 zcmeI&PiWIn90%~ET^d(6D?3>9vUixU(ABLSI-TfjXPnipoozZcJ%rds7uSC_sZ1y0 zh9c@g@XtZfvnZZCs8>agdYF^p(TRExkBT6OFKIiMI=x%I57Or4y}Z2N=e-u*Ym*0} znxT>-g;Gv2NH4RNVOeGuAq>NC^jJlYT56&v=21&*?c^Wt9JA?EvDNdP;aj#cYc6{p zw?0@CYwd5jM6*Z`fB*y_009U<00Izzz<(kz!n?daA3JU+hqJ1FJWCf5vaTL4tNC<& zcGX})3`ru9LgA=L>gsmpm7Ll|?-XYIF5cVS&CWJkZB7()LoOAvs+?1bMJ<2yj|;r5 z-9L&+r<>5EWk@6@i9=$7j3gq%p~N`ZFOHK?N{UBfw1r_YCUsdgtrlo?NgR_%EKZM9 zG+HS#`(eNC7K$A2^$YBbSIcMADJ?T4R~wKGEvFVIPU>1($yyh6SGB8=*lux*cFA7X z9gh)vebT9Ay2uH&q-%w|O#8H3Ob!Y&?M~h+&_|xMgzUH2b4@lIds!7`S?gBB%XRG0 zuWXW042A4V(!SSfyM^sdE^l058H=Pk(n{GVSb5pr4LM-XbF%_VMg8nK&I(Va_fN?= z?Py6csDfUfci2L#FSQ3r!jU053zpA-%D`q+Jzdg@<^$~GTPfD^x}jvVYDPB8Bg0}+ z3Js4e1x$x!Pbd|Y$Y3gwpz}yxs#3XKFQ@6JK_}sOJSv8MLsd%C6KZ-=FXu=&8V@hE zSt(Pe)O6YGl{J}a-Tw_e;Ba}9{=Y%jW5|K}D^B_~W3BC5>50SB$9V407bFNk00Izz z00bZa0SG_<0uX=z1eObA96Zteuan2xULT%*(S9uWZT{-Q zp^dNKjXvA_CZXNydOmyQ(Yn=N<{n-%Mh8+q-83qP=QHE^MqiL1009U<00Izz00bZa z0SG_<0ucD$1>8=Kt!!5~nw?y8+oZXtVE+D3|MZUp0SG_<0uX=z1Rwwb2tWV=5P(1f z3z)zEWBuR21B}oh009U<00Izz00bZa0SG_<0#pF&e@p`iKmY;|fB*y_009U<00Izz zK;sMG|No6Y#)uFC5P$##AOHafKmY;|fB*y_fb~D-00bZa0SG_<0uX=z1Rwwb2tc6m G1%3g_wwIIu literal 0 HcmV?d00001 diff --git a/examples/cli/src/main.rs b/examples/cli/src/main.rs index a8bc901..6edd536 100644 --- a/examples/cli/src/main.rs +++ b/examples/cli/src/main.rs @@ -2,7 +2,7 @@ use harmony::{ inventory::Inventory, modules::{ dummy::{ErrorScore, PanicScore, SuccessScore}, - inventory::LaunchDiscoverInventoryAgentScore, + inventory::{HarmonyDiscoveryStrategy, LaunchDiscoverInventoryAgentScore}, }, topology::LocalhostTopology, }; @@ -18,6 +18,7 @@ async fn main() { Box::new(PanicScore {}), Box::new(LaunchDiscoverInventoryAgentScore { discovery_timeout: Some(10), + discovery_strategy: HarmonyDiscoveryStrategy::MDNS, }), ], None, diff --git a/examples/harmony_inventory_builder/Cargo.toml b/examples/harmony_inventory_builder/Cargo.toml new file mode 100644 index 0000000..19002e1 --- /dev/null +++ b/examples/harmony_inventory_builder/Cargo.toml @@ -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 diff --git a/examples/harmony_inventory_builder/build_docker.sh b/examples/harmony_inventory_builder/build_docker.sh new file mode 100755 index 0000000..f952175 --- /dev/null +++ b/examples/harmony_inventory_builder/build_docker.sh @@ -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 diff --git a/examples/harmony_inventory_builder/docker/Dockerfile b/examples/harmony_inventory_builder/docker/Dockerfile new file mode 100644 index 0000000..ea06187 --- /dev/null +++ b/examples/harmony_inventory_builder/docker/Dockerfile @@ -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"] diff --git a/examples/harmony_inventory_builder/src/main.rs b/examples/harmony_inventory_builder/src/main.rs new file mode 100644 index 0000000..cfd017f --- /dev/null +++ b/examples/harmony_inventory_builder/src/main.rs @@ -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(); +} diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index f9a509d..0c9b326 100644 --- a/harmony/src/domain/interpret/mod.rs +++ b/harmony/src/domain/interpret/mod.rs @@ -4,6 +4,8 @@ use std::error::Error; use async_trait::async_trait; use derive_new::new; +use crate::inventory::HostRole; + use super::{ data::Version, executors::ExecutorError, inventory::Inventory, topology::PreparationError, }; diff --git a/harmony/src/domain/topology/tenant/k8s.rs b/harmony/src/domain/topology/tenant/k8s.rs index 8085127..d7d99c0 100644 --- a/harmony/src/domain/topology/tenant/k8s.rs +++ b/harmony/src/domain/topology/tenant/k8s.rs @@ -14,7 +14,7 @@ use k8s_openapi::{ }, apimachinery::pkg::util::intstr::IntOrString, }; -use kube::Resource; +use kube::{Resource, api::DynamicObject}; use log::debug; use serde::de::DeserializeOwned; use serde_json::json; diff --git a/harmony/src/modules/inventory/discovery.rs b/harmony/src/modules/inventory/discovery.rs index b02078b..3890a1a 100644 --- a/harmony/src/modules/inventory/discovery.rs +++ b/harmony/src/modules/inventory/discovery.rs @@ -5,11 +5,10 @@ use serde::{Deserialize, Serialize}; use crate::{ data::Version, - hardware::PhysicalHost, infra::inventory::InventoryRepositoryFactory, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::{HostRole, Inventory}, - modules::inventory::LaunchDiscoverInventoryAgentScore, + modules::inventory::{HarmonyDiscoveryStrategy, LaunchDiscoverInventoryAgentScore}, score::Score, topology::Topology, }; @@ -17,11 +16,13 @@ use crate::{ #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DiscoverHostForRoleScore { pub role: HostRole, + pub number_desired_hosts: i16, + pub discovery_strategy : HarmonyDiscoveryStrategy, } impl Score for DiscoverHostForRoleScore { fn name(&self) -> String { - "DiscoverInventoryAgentScore".to_string() + format!("DiscoverHostForRoleScore({:?})", self.role) } fn create_interpret(&self) -> Box> { @@ -48,13 +49,15 @@ impl Interpret for DiscoverHostForRoleInterpret { ); LaunchDiscoverInventoryAgentScore { discovery_timeout: None, + discovery_strategy: self.score.discovery_strategy.clone(), } .interpret(inventory, topology) .await?; - let host: PhysicalHost; + let mut chosen_hosts = vec![]; let host_repo = InventoryRepositoryFactory::build().await?; + let mut assigned_hosts = 0; loop { let all_hosts = host_repo.get_all_hosts().await?; @@ -75,15 +78,24 @@ impl Interpret for DiscoverHostForRoleInterpret { match ans { Ok(choice) => { info!( - "Selected {} as the {:?} node.", - choice.summary(), - self.score.role + "Assigned role {:?} for node {}", + self.score.role, + choice.summary() ); host_repo .save_role_mapping(&self.score.role, &choice) .await?; - host = choice; - break; + chosen_hosts.push(choice); + 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) => { info!("Refresh requested. Fetching list of discovered hosts again..."); @@ -100,8 +112,13 @@ impl Interpret for DiscoverHostForRoleInterpret { } Ok(Outcome::success(format!( - "Successfully discovered host {} for role {:?}", - host.summary(), + "Successfully discovered {} hosts {} for role {:?}", + self.score.number_desired_hosts, + chosen_hosts + .iter() + .map(|h| h.summary()) + .collect::>() + .join(", "), self.score.role ))) } diff --git a/harmony/src/modules/inventory/mod.rs b/harmony/src/modules/inventory/mod.rs index 174231b..8cf92f1 100644 --- a/harmony/src/modules/inventory/mod.rs +++ b/harmony/src/modules/inventory/mod.rs @@ -1,6 +1,10 @@ mod discovery; pub mod inspect; +use std::net::Ipv4Addr; + +use cidr::{Ipv4Cidr, Ipv4Inet}; pub use discovery::*; +use tokio::time::{Duration, timeout}; use async_trait::async_trait; use harmony_inventory_agent::local_presence::DiscoveryEvent; @@ -24,6 +28,7 @@ use harmony_types::id::Id; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LaunchDiscoverInventoryAgentScore { pub discovery_timeout: Option, + pub discovery_strategy: HarmonyDiscoveryStrategy, } impl Score for LaunchDiscoverInventoryAgentScore { @@ -43,6 +48,12 @@ struct DiscoverInventoryAgentInterpret { score: LaunchDiscoverInventoryAgentScore, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum HarmonyDiscoveryStrategy { + MDNS, + SUBNET { cidr: cidr::Ipv4Cidr, port: u16 }, +} + #[async_trait] impl Interpret for DiscoverInventoryAgentInterpret { async fn execute( @@ -57,6 +68,37 @@ impl Interpret 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 { + todo!() + } +} + +impl DiscoverInventoryAgentInterpret { + async fn launch_mdns_discovery(&self) { harmony_inventory_agent::local_presence::discover_agents( self.score.discovery_timeout, |event: DiscoveryEvent| -> Result<(), String> { @@ -112,6 +154,8 @@ impl Interpret for DiscoverInventoryAgentInterpret { 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() .await .map_err(|e| format!("Could not build repository : {e}")) @@ -132,25 +176,111 @@ impl Interpret for DiscoverInventoryAgentInterpret { Ok(()) }, ) - .await; - Ok(Outcome::success( - "Discovery process completed successfully".to_string(), - )) + .await } - fn get_name(&self) -> InterpretName { - InterpretName::DiscoverInventoryAgent - } + // async fn launch_cidr_discovery(&self, cidr : &Ipv4Cidr, port: u16) { + // 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 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 = 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 { - todo!() - } + let batch_size: usize = 20; + let timeout_secs = 5; + let request_timeout = Duration::from_secs(timeout_secs); - fn get_status(&self) -> InterpretStatus { - todo!() - } + let mut current_batch = 0; + let num_batches = addrs.len() / batch_size; - fn get_children(&self) -> Vec { - todo!() + for batch in addrs.chunks(batch_size) { + 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"); } } diff --git a/harmony/src/modules/okd/bootstrap_01_prepare.rs b/harmony/src/modules/okd/bootstrap_01_prepare.rs index 57b71d9..1bf1b40 100644 --- a/harmony/src/modules/okd/bootstrap_01_prepare.rs +++ b/harmony/src/modules/okd/bootstrap_01_prepare.rs @@ -4,7 +4,7 @@ use crate::{ infra::inventory::InventoryRepositoryFactory, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::{HostRole, Inventory}, - modules::inventory::DiscoverHostForRoleScore, + modules::inventory::{DiscoverHostForRoleScore, HarmonyDiscoveryStrategy}, score::Score, topology::HAClusterTopology, }; @@ -104,6 +104,8 @@ When you can dig them, confirm to continue. bootstrap_host = hosts.into_iter().next().to_owned(); DiscoverHostForRoleScore { role: HostRole::Bootstrap, + number_desired_hosts: 1, + discovery_strategy: HarmonyDiscoveryStrategy::MDNS, } .interpret(inventory, topology) .await?; diff --git a/harmony/src/modules/okd/bootstrap_03_control_plane.rs b/harmony/src/modules/okd/bootstrap_03_control_plane.rs index 5abe848..2ad3bf5 100644 --- a/harmony/src/modules/okd/bootstrap_03_control_plane.rs +++ b/harmony/src/modules/okd/bootstrap_03_control_plane.rs @@ -6,7 +6,7 @@ use crate::{ inventory::{HostRole, Inventory}, modules::{ dhcp::DhcpHostBindingScore, http::IPxeMacBootFileScore, - inventory::DiscoverHostForRoleScore, okd::templates::BootstrapIpxeTpl, + inventory::{DiscoverHostForRoleScore, HarmonyDiscoveryStrategy}, okd::templates::BootstrapIpxeTpl, }, score::Score, topology::{HAClusterTopology, HostBinding}, @@ -58,38 +58,39 @@ impl OKDSetup03ControlPlaneInterpret { inventory: &Inventory, topology: &HAClusterTopology, ) -> Result, InterpretError> { - const REQUIRED_HOSTS: usize = 3; + const REQUIRED_HOSTS: i16 = 3; 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!( - "Discovery of {} control plane hosts in progress, current number {}", - REQUIRED_HOSTS, - control_plane_hosts.len() - ); - // This score triggers the discovery agent for a specific role. - DiscoverHostForRoleScore { - role: HostRole::ControlPlane, - } - .interpret(inventory, topology) - .await?; - control_plane_hosts = repo.get_host_for_role(&HostRole::ControlPlane).await?; + info!( + "Discovery of {} control plane hosts in progress, current number {}", + REQUIRED_HOSTS, + control_plane_hosts.len() + ); + // This score triggers the discovery agent for a specific role. + DiscoverHostForRoleScore { + role: HostRole::ControlPlane, + number_desired_hosts: REQUIRED_HOSTS, + discovery_strategy: HarmonyDiscoveryStrategy::MDNS, } + .interpret(inventory, topology) + .await?; - if control_plane_hosts.len() < REQUIRED_HOSTS { - Err(InterpretError::new(format!( + let control_plane_hosts = repo.get_host_for_role(&HostRole::ControlPlane).await?; + + 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.", REQUIRED_HOSTS, 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. diff --git a/harmony_inventory_agent/build_docker.sh b/harmony_inventory_agent/build_docker.sh new file mode 100755 index 0000000..521ea9b --- /dev/null +++ b/harmony_inventory_agent/build_docker.sh @@ -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 diff --git a/harmony_inventory_agent/docker/.gitignore b/harmony_inventory_agent/docker/.gitignore new file mode 100644 index 0000000..052a676 --- /dev/null +++ b/harmony_inventory_agent/docker/.gitignore @@ -0,0 +1 @@ +harmony_inventory_agent diff --git a/harmony_inventory_agent/docker/Dockerfile b/harmony_inventory_agent/docker/Dockerfile new file mode 100644 index 0000000..0c4e066 --- /dev/null +++ b/harmony_inventory_agent/docker/Dockerfile @@ -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" ] + diff --git a/harmony_inventory_agent/harmony-inventory-agent-daemonset.yaml b/harmony_inventory_agent/harmony-inventory-agent-daemonset.yaml new file mode 100644 index 0000000..e9fa401 --- /dev/null +++ b/harmony_inventory_agent/harmony-inventory-agent-daemonset.yaml @@ -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 diff --git a/harmony_inventory_agent/src/hwinfo.rs b/harmony_inventory_agent/src/hwinfo.rs index 5fffb61..960c1f0 100644 --- a/harmony_inventory_agent/src/hwinfo.rs +++ b/harmony_inventory_agent/src/hwinfo.rs @@ -1,5 +1,5 @@ use harmony_types::net::MacAddress; -use log::{debug, warn}; +use log::{debug, trace, warn}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::fs; @@ -121,20 +121,48 @@ pub struct ManagementInterface { impl PhysicalHost { pub fn gather() -> Result { + trace!("Start gathering physical host information"); let mut sys = System::new_all(); + trace!("System new_all called"); sys.refresh_all(); + trace!("System refresh_all called"); 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 { - storage_drives: Self::gather_storage_drives()?, - storage_controller: Self::gather_storage_controller()?, - memory_modules: Self::gather_memory_modules()?, - cpus: Self::gather_cpus(&sys)?, - chipset: Self::gather_chipset()?, - network_interfaces: Self::gather_network_interfaces()?, - management_interface: Self::gather_management_interface()?, - host_uuid: Self::get_host_uuid()?, + storage_drives, + storage_controller, + memory_modules, + cpus, + chipset, + network_interfaces, + management_interface, + host_uuid, }) } @@ -208,6 +236,8 @@ impl PhysicalHost { )); } + debug!("All tools found!"); + Ok(()) } @@ -231,7 +261,10 @@ impl PhysicalHost { fn gather_storage_drives() -> Result, String> { let mut drives = Vec::new(); + trace!("Starting storage drive discovery using lsblk"); + // 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") .args([ "-d", @@ -245,13 +278,18 @@ impl PhysicalHost { .output() .map_err(|e| format!("Failed to execute lsblk: {}", e))?; + trace!( + "lsblk command executed successfully (status: {:?})", + output.status + ); + if !output.status.success() { - return Err(format!( - "lsblk command failed: {}", - String::from_utf8_lossy(&output.stderr) - )); + let stderr_str = String::from_utf8_lossy(&output.stderr); + debug!("lsblk command failed: {stderr_str}"); + return Err(format!("lsblk command failed: {stderr_str}")); } + trace!("Parsing lsblk JSON output"); let json: Value = serde_json::from_slice(&output.stdout) .map_err(|e| format!("Failed to parse lsblk JSON output: {}", e))?; @@ -260,6 +298,8 @@ impl PhysicalHost { .and_then(|v| v.as_array()) .ok_or("Invalid lsblk JSON: missing 'blockdevices' array")?; + trace!("Found {} blockdevices in lsblk output", blockdevices.len()); + for device in blockdevices { let name = device .get("name") @@ -268,52 +308,72 @@ impl PhysicalHost { .to_string(); if name.is_empty() { + trace!("Skipping unnamed device entry: {:?}", device); continue; } + trace!("Inspecting block device: {name}"); + + // Extract metadata fields let model = device .get("model") .and_then(|v| v.as_str()) .map(|s| s.trim().to_string()) .unwrap_or_default(); + trace!("Model for {name}: '{}'", model); let serial = device .get("serial") .and_then(|v| v.as_str()) .map(|s| s.trim().to_string()) .unwrap_or_default(); + trace!("Serial for {name}: '{}'", serial); let size_str = device .get("size") .and_then(|v| v.as_str()) .ok_or("Missing 'size' in lsblk device")?; + trace!("Reported size for {name}: {}", size_str); let size_bytes = Self::parse_size(size_str)?; + trace!("Parsed size for {name}: {} bytes", size_bytes); let rotational = device .get("rota") .and_then(|v| v.as_bool()) .ok_or("Missing 'rota' in lsblk device")?; + trace!("Rotational flag for {name}: {}", rotational); let wwn = device .get("wwn") .and_then(|v| v.as_str()) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty() && s != "null"); + trace!("WWN for {name}: {:?}", wwn); 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( &device_path.join("queue/logical_block_size"), ) .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( &device_path.join("queue/physical_block_size"), ) .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)?; + trace!("Interface type for {name}: {}", interface_type); + + trace!("Getting SMART status for {name}"); let smart_status = Self::get_smart_status(&name)?; + trace!("SMART status for {name}: {:?}", smart_status); let mut drive = StorageDrive { name: name.clone(), @@ -330,19 +390,31 @@ impl PhysicalHost { // Enhance with additional sysfs info if available if device_path.exists() { + trace!("Enhancing drive {name} with extra sysfs metadata"); if drive.model.is_empty() { + trace!("Reading model from sysfs for {name}"); 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() { + trace!("Reading serial from sysfs for {name}"); 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); } + debug!("Discovered total {} storage drives", drives.len()); + trace!("All discovered dives: {drives:?}"); + Ok(drives) } @@ -418,6 +490,8 @@ impl PhysicalHost { } } + debug!("Found storage controller {controller:?}"); + Ok(controller) } @@ -486,6 +560,7 @@ impl PhysicalHost { } } + debug!("Found memory modules {modules:?}"); Ok(modules) } @@ -501,22 +576,30 @@ impl PhysicalHost { frequency_mhz: global_cpu.frequency(), }); + debug!("Found cpus {cpus:?}"); + Ok(cpus) } fn gather_chipset() -> Result { - Ok(Chipset { + let chipset = Chipset { name: Self::read_dmi("baseboard-product-name")?, vendor: Self::read_dmi("baseboard-manufacturer")?, - }) + }; + + debug!("Found chipset {chipset:?}"); + + Ok(chipset) } fn gather_network_interfaces() -> Result, String> { let mut interfaces = Vec::new(); let sys_net_path = Path::new("/sys/class/net"); + trace!("Reading /sys/class/net"); let entries = fs::read_dir(sys_net_path) .map_err(|e| format!("Failed to read /sys/class/net: {}", e))?; + trace!("Got entries {entries:?}"); for entry in entries { let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; @@ -525,6 +608,7 @@ impl PhysicalHost { .into_string() .map_err(|_| "Invalid UTF-8 in interface name")?; let iface_path = entry.path(); + trace!("Inspecting interface {iface_name} path {iface_path:?}"); // Skip virtual interfaces if iface_name.starts_with("lo") @@ -535,70 +619,101 @@ impl PhysicalHost { || iface_name.starts_with("tun") || iface_name.starts_with("wg") { + trace!( + "Skipping interface {iface_name} because it appears to be virtual/unsupported" + ); continue; } // Check if it's a physical interface by looking for device directory if !iface_path.join("device").exists() { + trace!( + "Skipping interface {iface_name} since {iface_path:?}/device does not exist" + ); continue; } + trace!("Reading MAC address for {iface_name}"); let mac_address = Self::read_sysfs_string(&iface_path.join("address")) .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())?; + trace!("MAC address for {iface_name}: {mac_address}"); - let speed_mbps = if iface_path.join("speed").exists() { - match Self::read_sysfs_u32(&iface_path.join("speed")) { - Ok(speed) => Some(speed), + let speed_path = iface_path.join("speed"); + let speed_mbps = if speed_path.exists() { + 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) => { debug!( - "Failed to read speed for {}: {} . This is expected to fail on wifi interfaces.", + "Failed to read speed for {}: {} (this may be expected on Wi‑Fi interfaces)", iface_name, e ); None } } } else { + trace!("Speed file not found for {iface_name}, skipping"); None }; + trace!("Reading operstate for {iface_name}"); let operstate = Self::read_sysfs_string(&iface_path.join("operstate")) .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")) .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 = Self::read_sysfs_symlink_basename(&iface_path.join("device/driver/module")) .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( &iface_path.join("device/firmware_version"), ) .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) .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 { - name: iface_name, + let is_up = operstate == "up"; + trace!("Constructing NetworkInterface for {iface_name} (is_up={is_up})"); + + let iface = NetworkInterface { + name: iface_name.clone(), mac_address, speed_mbps, - is_up: operstate == "up", + is_up, mtu, ipv4_addresses, ipv6_addresses, driver, firmware_version, - }); + }; + + debug!("Discovered interface: {iface:?}"); + interfaces.push(iface); } + debug!("Discovered total {} network interfaces", interfaces.len()); + trace!("Interfaces collected: {interfaces:?}"); Ok(interfaces) } fn gather_management_interface() -> Result, String> { - if Path::new("/dev/ipmi0").exists() { + let mgmt = if Path::new("/dev/ipmi0").exists() { Ok(Some(ManagementInterface { kind: "IPMI".to_string(), address: None, @@ -612,11 +727,16 @@ impl PhysicalHost { })) } else { Ok(None) - } + }; + + debug!("Found management interface {mgmt:?}"); + mgmt } fn get_host_uuid() -> Result { - Self::read_dmi("system-uuid") + let uuid = Self::read_dmi("system-uuid"); + debug!("Found uuid {uuid:?}"); + uuid } // Helper methods @@ -709,7 +829,8 @@ impl PhysicalHost { Ok("Ramdisk".to_string()) } else { // 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 .split('/') .next_back() @@ -779,6 +900,8 @@ impl PhysicalHost { 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, Vec), String> { let mut ipv4 = Vec::new(); let mut ipv6 = Vec::new(); diff --git a/harmony_inventory_agent/src/local_presence/advertise.rs b/harmony_inventory_agent/src/local_presence/advertise.rs index 3ccb4f6..d26b619 100644 --- a/harmony_inventory_agent/src/local_presence/advertise.rs +++ b/harmony_inventory_agent/src/local_presence/advertise.rs @@ -1,4 +1,4 @@ -use log::{debug, error, info, warn}; +use log::{debug, error, info, trace, warn}; use mdns_sd::{ServiceDaemon, ServiceInfo}; use std::collections::HashMap; @@ -12,6 +12,7 @@ use crate::{ /// This function is synchronous and non-blocking. It spawns a background Tokio task /// to handle the mDNS advertisement for the lifetime of the application. pub fn advertise(service_port: u16) -> Result<(), PresenceError> { + trace!("starting advertisement process for port {service_port}"); let host_id = match PhysicalHost::gather() { Ok(host) => Some(host.host_uuid), Err(e) => { @@ -20,11 +21,15 @@ pub fn advertise(service_port: u16) -> Result<(), PresenceError> { } }; + trace!("Found host id {host_id:?}"); + let instance_name = format!( "inventory-agent-{}", 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}'."); tokio::spawn(async move { diff --git a/harmony_inventory_agent/src/main.rs b/harmony_inventory_agent/src/main.rs index d93ea4f..f66de78 100644 --- a/harmony_inventory_agent/src/main.rs +++ b/harmony_inventory_agent/src/main.rs @@ -28,7 +28,7 @@ async fn inventory() -> impl Responder { async fn main() -> std::io::Result<()> { 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 .parse::() .expect(&format!("Invalid port number, cannot parse to u16 {port}")); From 43b04edbaeada2e31465204f8fa8fb5f0c98d78d Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Mon, 10 Nov 2025 22:59:37 -0500 Subject: [PATCH 2/5] feat(brocade): Add feature and example to remove port channel and configure switchport --- brocade/examples/main.rs | 5 +- brocade/src/fast_iron.rs | 2 +- brocade/src/lib.rs | 10 +- brocade/src/network_operating_system.rs | 7 +- brocade/src/shell.rs | 13 +- brocade/src/ssh.rs | 34 +++-- examples/brocade_switch/Cargo.toml | 19 +++ examples/brocade_switch/src/main.rs | 157 ++++++++++++++++++++++ harmony/src/domain/topology/ha_cluster.rs | 15 ++- harmony/src/domain/topology/network.rs | 8 ++ harmony/src/infra/brocade.rs | 35 +++-- harmony/src/modules/dhcp.rs | 3 + harmony/src/modules/okd/host_network.rs | 13 +- harmony_types/src/switch.rs | 4 +- 14 files changed, 286 insertions(+), 39 deletions(-) create mode 100644 examples/brocade_switch/Cargo.toml create mode 100644 examples/brocade_switch/src/main.rs diff --git a/brocade/examples/main.rs b/brocade/examples/main.rs index 34dec21..de8623d 100644 --- a/brocade/examples/main.rs +++ b/brocade/examples/main.rs @@ -26,13 +26,12 @@ async fn main() { let brocade = brocade::init( &switch_addresses, - 22, &config.username, &config.password, - Some(BrocadeOptions { + BrocadeOptions { dry_run: true, ..Default::default() - }), + }, ) .await .expect("Brocade client failed to connect"); diff --git a/brocade/src/fast_iron.rs b/brocade/src/fast_iron.rs index 5a3474e..9c260d1 100644 --- a/brocade/src/fast_iron.rs +++ b/brocade/src/fast_iron.rs @@ -140,7 +140,7 @@ impl BrocadeClient for FastIronClient { async fn configure_interfaces( &self, - _interfaces: Vec<(String, PortOperatingMode)>, + _interfaces: &Vec<(String, PortOperatingMode)>, ) -> Result<(), Error> { todo!() } diff --git a/brocade/src/lib.rs b/brocade/src/lib.rs index c0b5b70..6a6afa4 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -14,6 +14,7 @@ use async_trait::async_trait; use harmony_types::net::MacAddress; use harmony_types::switch::{PortDeclaration, PortLocation}; use regex::Regex; +use serde::Serialize; mod fast_iron; mod network_operating_system; @@ -118,7 +119,7 @@ impl fmt::Display for InterfaceType { } /// 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 { /// The interface is explicitly configured for Brocade fabric roles (ISL or Trunk enabled). Fabric, @@ -141,12 +142,11 @@ pub enum InterfaceStatus { pub async fn init( ip_addresses: &[IpAddr], - port: u16, username: &str, password: &str, - options: Option, + options: BrocadeOptions, ) -> Result, 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 .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.). async fn configure_interfaces( &self, - interfaces: Vec<(String, PortOperatingMode)>, + interfaces: &Vec<(String, PortOperatingMode)>, ) -> Result<(), Error>; /// Scans the existing configuration to find the next available (unused) diff --git a/brocade/src/network_operating_system.rs b/brocade/src/network_operating_system.rs index f4db713..ccd9544 100644 --- a/brocade/src/network_operating_system.rs +++ b/brocade/src/network_operating_system.rs @@ -187,7 +187,7 @@ impl BrocadeClient for NetworkOperatingSystemClient { async fn configure_interfaces( &self, - interfaces: Vec<(String, PortOperatingMode)>, + interfaces: &Vec<(String, PortOperatingMode)>, ) -> Result<(), Error> { info!("[Brocade] Configuring {} interface(s)...", interfaces.len()); @@ -204,9 +204,12 @@ impl BrocadeClient for NetworkOperatingSystemClient { PortOperatingMode::Trunk => { commands.push("switchport".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 trunk enable".into()); + commands.push("no shutdown".into()); } PortOperatingMode::Access => { commands.push("switchport".into()); diff --git a/brocade/src/shell.rs b/brocade/src/shell.rs index 9cd94a9..f72c31b 100644 --- a/brocade/src/shell.rs +++ b/brocade/src/shell.rs @@ -16,7 +16,6 @@ use tokio::time::timeout; #[derive(Debug)] pub struct BrocadeShell { ip: IpAddr, - port: u16, username: String, password: String, options: BrocadeOptions, @@ -27,33 +26,31 @@ pub struct BrocadeShell { impl BrocadeShell { pub async fn init( ip_addresses: &[IpAddr], - port: u16, username: &str, password: &str, - options: Option, + options: BrocadeOptions, ) -> Result { let ip = ip_addresses .first() .ok_or_else(|| Error::ConfigurationError("No IP addresses provided".to_string()))?; - let base_options = options.unwrap_or_default(); - let options = ssh::try_init_client(username, password, ip, base_options).await?; + let brocade_ssh_client_options = + ssh::try_init_client(username, password, ip, options).await?; Ok(Self { ip: *ip, - port, username: username.to_string(), password: password.to_string(), before_all_commands: vec![], after_all_commands: vec![], - options, + options: brocade_ssh_client_options, }) } pub async fn open_session(&self, mode: ExecutionMode) -> Result { BrocadeSession::open( self.ip, - self.port, + self.options.ssh.port, &self.username, &self.password, self.options.clone(), diff --git a/brocade/src/ssh.rs b/brocade/src/ssh.rs index 08ff96f..cb804c7 100644 --- a/brocade/src/ssh.rs +++ b/brocade/src/ssh.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::sync::Arc; use async_trait::async_trait; +use log::debug; use russh::client::Handler; use russh::kex::DH_G1_SHA1; use russh::kex::ECDH_SHA2_NISTP256; @@ -10,29 +11,43 @@ use russh_keys::key::SSH_RSA; use super::BrocadeOptions; use super::Error; -#[derive(Default, Clone, Debug)] +#[derive(Clone, Debug)] pub struct SshOptions { pub preferred_algorithms: russh::Preferred, + pub port: u16, +} + +impl Default for SshOptions { + fn default() -> Self { + Self { + preferred_algorithms: Default::default(), + port: 22, + } + } } impl SshOptions { - fn ecdhsa_sha2_nistp256() -> Self { + fn ecdhsa_sha2_nistp256(port: u16) -> Self { Self { preferred_algorithms: russh::Preferred { kex: Cow::Borrowed(&[ECDH_SHA2_NISTP256]), key: Cow::Borrowed(&[SSH_RSA]), ..Default::default() }, + port, + ..Default::default() } } - fn legacy() -> Self { + fn legacy(port: u16) -> Self { Self { preferred_algorithms: russh::Preferred { kex: Cow::Borrowed(&[DH_G1_SHA1]), key: Cow::Borrowed(&[SSH_RSA]), ..Default::default() }, + port, + ..Default::default() } } } @@ -57,18 +72,21 @@ pub async fn try_init_client( ip: &std::net::IpAddr, base_options: BrocadeOptions, ) -> Result { + let mut default = SshOptions::default(); + default.port = base_options.ssh.port; let ssh_options = vec![ - SshOptions::default(), - SshOptions::ecdhsa_sha2_nistp256(), - SshOptions::legacy(), + default, + SshOptions::ecdhsa_sha2_nistp256(base_options.ssh.port), + SshOptions::legacy(base_options.ssh.port), ]; for ssh in ssh_options { let opts = BrocadeOptions { - ssh, + ssh: ssh.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 { Ok(_) => { diff --git a/examples/brocade_switch/Cargo.toml b/examples/brocade_switch/Cargo.toml new file mode 100644 index 0000000..6ac6a6c --- /dev/null +++ b/examples/brocade_switch/Cargo.toml @@ -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" } diff --git a/examples/brocade_switch/src/main.rs b/examples/brocade_switch/src/main.rs new file mode 100644 index 0000000..a99263d --- /dev/null +++ b/examples/brocade_switch/src/main.rs @@ -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, + ports_to_configure: Vec, +} + +impl Score for BrocadeSwitchScore { + fn name(&self) -> String { + "BrocadeSwitchScore".to_string() + } + + #[doc(hidden)] + fn create_interpret(&self) -> Box> { + Box::new(BrocadeSwitchInterpret { + score: self.clone(), + }) + } +} + +#[derive(Debug)] +struct BrocadeSwitchInterpret { + score: BrocadeSwitchScore, +} + +#[async_trait] +impl Interpret for BrocadeSwitchInterpret { + async fn execute( + &self, + _inventory: &Inventory, + topology: &T, + ) -> Result { + 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 { + todo!() + } +} + +struct SwitchTopology { + client: Box, +} + +#[async_trait] +impl Topology for SwitchTopology { + fn name(&self) -> &str { + "SwitchTopology" + } + + async fn ensure_ready(&self) -> Result { + 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, SwitchError> { + todo!() + } + + async fn configure_port_channel(&self, _config: &HostNetworkConfig) -> Result<(), SwitchError> { + todo!() + } + async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError> { + self.client.clear_port_channel(ids).await + } + async fn configure_interface(&self, ports: &Vec) -> Result<(), SwitchError> { + self.client.configure_interface(ports).await + } +} diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 558f97c..dc6d4ac 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use brocade::PortOperatingMode; use harmony_macros::ip; use harmony_types::{ id::Id, @@ -8,7 +9,7 @@ use harmony_types::{ use log::debug; use log::info; -use crate::infra::network_manager::OpenShiftNmStateNetworkManager; +use crate::{infra::network_manager::OpenShiftNmStateNetworkManager, topology::PortConfig}; use crate::topology::PxeOptions; use crate::{data::FileContent, executors::ExecutorError}; @@ -298,6 +299,16 @@ impl Switch for HAClusterTopology { Ok(()) } + + async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError> { + todo!() + } + async fn configure_interface( + &self, + ports: &Vec, + ) -> Result<(), SwitchError> { + todo!() + } } #[async_trait] @@ -521,4 +532,6 @@ impl SwitchClient for DummyInfra { ) -> Result { unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } + async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError> {todo!()} + async fn configure_interface(&self, ports: &Vec) -> Result<(), SwitchError> {todo!()} } diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index d9e4f72..ad2efe0 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -7,6 +7,7 @@ use std::{ }; use async_trait::async_trait; +use brocade::PortOperatingMode; use derive_new::new; use harmony_types::{ id::Id, @@ -214,6 +215,8 @@ impl From for NetworkError { } } +pub type PortConfig = (PortLocation, PortOperatingMode); + #[async_trait] pub trait Switch: Send + Sync { async fn setup_switch(&self) -> Result<(), SwitchError>; @@ -224,6 +227,8 @@ pub trait Switch: Send + Sync { ) -> Result, SwitchError>; async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError>; + async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError>; + async fn configure_interface(&self, ports: &Vec) -> Result<(), SwitchError>; } #[derive(Clone, Debug, PartialEq)] @@ -283,6 +288,9 @@ pub trait SwitchClient: Debug + Send + Sync { channel_name: &str, switch_ports: Vec, ) -> Result; + + async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError>; + async fn configure_interface(&self, ports: &Vec) -> Result<(), SwitchError>; } #[cfg(test)] diff --git a/harmony/src/infra/brocade.rs b/harmony/src/infra/brocade.rs index 774c8f8..6e45ce7 100644 --- a/harmony/src/infra/brocade.rs +++ b/harmony/src/infra/brocade.rs @@ -1,12 +1,13 @@ use async_trait::async_trait; use brocade::{BrocadeClient, BrocadeOptions, InterSwitchLink, InterfaceStatus, PortOperatingMode}; use harmony_types::{ + id::Id, net::{IpAddress, MacAddress}, switch::{PortDeclaration, PortLocation}, }; use option_ext::OptionExt; -use crate::topology::{SwitchClient, SwitchError}; +use crate::topology::{PortConfig, SwitchClient, SwitchError}; #[derive(Debug)] pub struct BrocadeSwitchClient { @@ -18,9 +19,9 @@ impl BrocadeSwitchClient { ip_addresses: &[IpAddress], username: &str, password: &str, - options: Option, + options: BrocadeOptions, ) -> Result { - let brocade = brocade::init(ip_addresses, 22, username, password, options).await?; + let brocade = brocade::init(ip_addresses, username, password, options).await?; Ok(Self { brocade }) } } @@ -59,7 +60,7 @@ impl SwitchClient for BrocadeSwitchClient { } self.brocade - .configure_interfaces(interfaces) + .configure_interfaces(&interfaces) .await .map_err(|e| SwitchError::new(e.to_string()))?; @@ -111,6 +112,24 @@ impl SwitchClient for BrocadeSwitchClient { Ok(channel_id) } + async fn clear_port_channel(&self, ids: &Vec) -> 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) -> 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)] @@ -147,8 +166,8 @@ mod tests { let configured_interfaces = brocade.configured_interfaces.lock().unwrap(); assert_that!(*configured_interfaces).contains_exactly(vec![ - (first_interface.name.clone(), PortOperatingMode::Access), - (second_interface.name.clone(), PortOperatingMode::Access), + (first_interface.port_location, PortOperatingMode::Access), + (second_interface.port_location, PortOperatingMode::Access), ]); } @@ -255,10 +274,10 @@ mod tests { async fn configure_interfaces( &self, - interfaces: Vec<(String, PortOperatingMode)>, + interfaces: &Vec<(String, PortOperatingMode)>, ) -> Result<(), Error> { let mut configured_interfaces = self.configured_interfaces.lock().unwrap(); - *configured_interfaces = interfaces; + *configured_interfaces = interfaces.clone(); Ok(()) } diff --git a/harmony/src/modules/dhcp.rs b/harmony/src/modules/dhcp.rs index e261220..149b1c0 100644 --- a/harmony/src/modules/dhcp.rs +++ b/harmony/src/modules/dhcp.rs @@ -19,8 +19,11 @@ pub struct DhcpScore { pub host_binding: Vec, pub next_server: Option, pub boot_filename: Option, + /// Boot filename to be provided to PXE clients identifying as BIOS pub filename: Option, + /// Boot filename to be provided to PXE clients identifying as uefi but NOT iPXE pub filename64: Option, + /// Boot filename to be provided to PXE clients identifying as iPXE pub filenameipxe: Option, pub dhcp_range: (IpAddress, IpAddress), pub domain: Option, diff --git a/harmony/src/modules/okd/host_network.rs b/harmony/src/modules/okd/host_network.rs index ee68942..a84cf19 100644 --- a/harmony/src/modules/okd/host_network.rs +++ b/harmony/src/modules/okd/host_network.rs @@ -251,14 +251,14 @@ impl Interpret for HostNetworkConfigur #[cfg(test)] mod tests { use assertor::*; + use brocade::PortOperatingMode; use harmony_types::{net::MacAddress, switch::PortLocation}; use lazy_static::lazy_static; use crate::{ hardware::HostCategory, topology::{ - HostNetworkConfig, NetworkError, PreparationError, PreparationOutcome, SwitchError, - SwitchPort, + HostNetworkConfig, NetworkError, PortConfig, PreparationError, PreparationOutcome, SwitchError, SwitchPort }, }; use std::{ @@ -692,5 +692,14 @@ mod tests { Ok(()) } + async fn clear_port_channel(&self, ids: &Vec) -> Result<(), SwitchError> { + todo!() + } + async fn configure_interface( + &self, + port_config: &Vec, + ) -> Result<(), SwitchError> { + todo!() + } } } diff --git a/harmony_types/src/switch.rs b/harmony_types/src/switch.rs index 2d32754..359e776 100644 --- a/harmony_types/src/switch.rs +++ b/harmony_types/src/switch.rs @@ -1,5 +1,7 @@ use std::{fmt, str::FromStr}; +use serde::Serialize; + /// Simple error type for port parsing failures. #[derive(Debug)] pub enum PortParseError { @@ -21,7 +23,7 @@ impl fmt::Display for PortParseError { /// Represents the atomic, physical location of a switch port: `//`. /// /// 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); impl fmt::Display for PortLocation { From a0a8d5277c3adbab92df6571d6050b0532a437e1 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Tue, 11 Nov 2025 09:06:36 -0500 Subject: [PATCH 3/5] fix: opnsense definitions more accurate for various resources such as ProxyGeneral, System, StaticMap, Job, etc. Also fixed brocade crate export and some warnings --- brocade/src/lib.rs | 2 +- harmony/src/domain/hardware/mod.rs | 8 +-- harmony/src/infra/kube.rs | 2 +- harmony/src/infra/opnsense/mod.rs | 1 - opnsense-config-xml/src/data/opnsense.rs | 64 +++++++++++++--------- opnsense-config/src/modules/dhcp_legacy.rs | 9 +-- 6 files changed, 47 insertions(+), 39 deletions(-) diff --git a/brocade/src/lib.rs b/brocade/src/lib.rs index 6a6afa4..3dfb4d4 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -19,7 +19,7 @@ use serde::Serialize; mod fast_iron; mod network_operating_system; mod shell; -mod ssh; +pub mod ssh; #[derive(Default, Clone, Debug)] pub struct BrocadeOptions { diff --git a/harmony/src/domain/hardware/mod.rs b/harmony/src/domain/hardware/mod.rs index 1b1a72c..5b590e7 100644 --- a/harmony/src/domain/hardware/mod.rs +++ b/harmony/src/domain/hardware/mod.rs @@ -152,10 +152,10 @@ impl PhysicalHost { pub fn parts_list(&self) -> String { let PhysicalHost { id, - category, + category: _, network, storage, - labels, + labels: _, memory_modules, cpus, } = self; @@ -226,8 +226,8 @@ impl PhysicalHost { speed_mhz, manufacturer, part_number, - serial_number, - rank, + serial_number: _, + rank: _, } = mem; parts_list.push_str(&format!( "\n{}Gb, {}Mhz, Manufacturer ({}), Part Number ({})", diff --git a/harmony/src/infra/kube.rs b/harmony/src/infra/kube.rs index 9fb1247..de4d4ea 100644 --- a/harmony/src/infra/kube.rs +++ b/harmony/src/infra/kube.rs @@ -121,7 +121,7 @@ mod test { #[test] fn deployment_to_dynamic_roundtrip() { // Create a sample Deployment with nested structures - let mut deployment = Deployment { + let deployment = Deployment { metadata: ObjectMeta { name: Some("my-deployment".to_string()), labels: Some({ diff --git a/harmony/src/infra/opnsense/mod.rs b/harmony/src/infra/opnsense/mod.rs index 3878cfc..b5223e4 100644 --- a/harmony/src/infra/opnsense/mod.rs +++ b/harmony/src/infra/opnsense/mod.rs @@ -8,7 +8,6 @@ mod tftp; use std::sync::Arc; pub use management::*; -use opnsense_config_xml::Host; use tokio::sync::RwLock; use crate::{executors::ExecutorError, topology::LogicalHost}; diff --git a/opnsense-config-xml/src/data/opnsense.rs b/opnsense-config-xml/src/data/opnsense.rs index fa5f985..8a2f64f 100644 --- a/opnsense-config-xml/src/data/opnsense.rs +++ b/opnsense-config-xml/src/data/opnsense.rs @@ -195,7 +195,7 @@ pub struct System { pub disablechecksumoffloading: u8, pub disablesegmentationoffloading: u8, pub disablelargereceiveoffloading: u8, - pub ipv6allow: u8, + pub ipv6allow: Option, pub powerd_ac_mode: String, pub powerd_battery_mode: String, pub powerd_normal_mode: String, @@ -226,6 +226,7 @@ pub struct System { pub dns6gw: Option, pub dns7gw: Option, pub dns8gw: Option, + pub prefer_ipv4: Option, pub dnsallowoverride: u8, pub dnsallowoverride_exclude: Option, } @@ -329,6 +330,7 @@ pub struct Range { pub struct StaticMap { pub mac: String, pub ipaddr: String, + pub cid: Option, pub hostname: String, pub descr: Option, pub winsserver: MaybeString, @@ -764,9 +766,19 @@ pub struct Jobs { pub struct Job { #[yaserde(attribute = true)] pub uuid: MaybeString, - #[yaserde(rename = "name")] - pub name: MaybeString, + pub name: Option, // Add other fields as needed + pub origin: Option, + pub enabled: Option, + pub minutes: Option, + pub hours: Option, + pub days: Option, + pub months: Option, + pub weekdays: Option, + pub who: Option, + pub command: Option, + pub parameters: Option, + pub description: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -895,28 +907,28 @@ pub struct Proxy { #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct ProxyGeneral { pub enabled: i8, - pub error_pages: String, + pub error_pages: Option, #[yaserde(rename = "icpPort")] pub icp_port: MaybeString, pub logging: Logging, #[yaserde(rename = "alternateDNSservers")] pub alternate_dns_servers: MaybeString, #[yaserde(rename = "dnsV4First")] - pub dns_v4_first: i8, + pub dns_v4_first: Option, #[yaserde(rename = "forwardedForHandling")] - pub forwarded_for_handling: String, + pub forwarded_for_handling: Option, #[yaserde(rename = "uriWhitespaceHandling")] - pub uri_whitespace_handling: String, + pub uri_whitespace_handling: Option, #[yaserde(rename = "enablePinger")] pub enable_pinger: i8, #[yaserde(rename = "useViaHeader")] - pub use_via_header: i8, + pub use_via_header: Option, #[yaserde(rename = "suppressVersion")] - pub suppress_version: i32, + pub suppress_version: Option, #[yaserde(rename = "connecttimeout")] - pub connect_timeout: MaybeString, + pub connect_timeout: Option, #[yaserde(rename = "VisibleEmail")] - pub visible_email: String, + pub visible_email: Option, #[yaserde(rename = "VisibleHostname")] pub visible_hostname: MaybeString, pub cache: Cache, @@ -953,7 +965,7 @@ pub struct LocalCache { pub cache_mem: i32, pub maximum_object_size: MaybeString, pub maximum_object_size_in_memory: MaybeString, - pub memory_cache_mode: String, + pub memory_cache_mode: MaybeString, pub size: i32, pub l1: i32, pub l2: i32, @@ -965,13 +977,13 @@ pub struct LocalCache { pub struct Traffic { pub enabled: i32, #[yaserde(rename = "maxDownloadSize")] - pub max_download_size: i32, + pub max_download_size: MaybeString, #[yaserde(rename = "maxUploadSize")] - pub max_upload_size: i32, + pub max_upload_size: MaybeString, #[yaserde(rename = "OverallBandwidthTrotteling")] - pub overall_bandwidth_trotteling: i32, + pub overall_bandwidth_trotteling: MaybeString, #[yaserde(rename = "perHostTrotteling")] - pub per_host_trotteling: i32, + pub per_host_trotteling: MaybeString, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -988,7 +1000,7 @@ pub struct ParentProxy { #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct Forward { - pub interfaces: String, + pub interfaces: MaybeString, pub port: i32, pub sslbumpport: i32, pub sslbump: i32, @@ -1033,9 +1045,9 @@ pub struct Acl { pub google_apps: MaybeString, pub youtube: MaybeString, #[yaserde(rename = "safePorts")] - pub safe_ports: String, + pub safe_ports: MaybeString, #[yaserde(rename = "sslPorts")] - pub ssl_ports: String, + pub ssl_ports: MaybeString, #[yaserde(rename = "remoteACLs")] pub remote_acls: RemoteAcls, } @@ -1051,9 +1063,9 @@ pub struct RemoteAcls { pub struct Icap { pub enable: i32, #[yaserde(rename = "RequestURL")] - pub request_url: String, + pub request_url: MaybeString, #[yaserde(rename = "ResponseURL")] - pub response_url: String, + pub response_url: MaybeString, #[yaserde(rename = "SendClientIP")] pub send_client_ip: i32, #[yaserde(rename = "SendUsername")] @@ -1061,7 +1073,7 @@ pub struct Icap { #[yaserde(rename = "EncodeUsername")] pub encode_username: i32, #[yaserde(rename = "UsernameHeader")] - pub username_header: String, + pub username_header: MaybeString, #[yaserde(rename = "EnablePreview")] pub enable_preview: i32, #[yaserde(rename = "PreviewSize")] @@ -1076,9 +1088,9 @@ pub struct Authentication { pub method: MaybeString, #[yaserde(rename = "authEnforceGroup")] pub auth_enforce_group: MaybeString, - pub realm: String, - pub credentialsttl: i32, // This field is already in snake_case - pub children: i32, + pub realm: MaybeString, + pub credentialsttl: MaybeString, // This field is already in snake_case + pub children: MaybeString, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -1293,6 +1305,7 @@ pub struct WireguardServerItem { pub peers: String, pub endpoint: MaybeString, pub peer_dns: MaybeString, + pub debug: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -1477,6 +1490,7 @@ pub struct Ppp { pub ports: Option, pub username: Option, pub password: Option, + pub provider: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] diff --git a/opnsense-config/src/modules/dhcp_legacy.rs b/opnsense-config/src/modules/dhcp_legacy.rs index 1d36ac6..b644073 100644 --- a/opnsense-config/src/modules/dhcp_legacy.rs +++ b/opnsense-config/src/modules/dhcp_legacy.rs @@ -86,10 +86,7 @@ impl<'a> DhcpConfigLegacyISC<'a> { mac, ipaddr: ipaddr.to_string(), hostname, - descr: Default::default(), - winsserver: Default::default(), - dnsserver: Default::default(), - ntpserver: Default::default(), + ..Default::default() }; existing_mappings.push(static_map); @@ -126,9 +123,7 @@ impl<'a> DhcpConfigLegacyISC<'a> { ipaddr: entry["ipaddr"].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), - winsserver: MaybeString::default(), - dnsserver: MaybeString::default(), - ntpserver: MaybeString::default(), + ..Default::default() }) .collect(); From d3634a631397bc7a799a4c188107f747ef42474a Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Tue, 11 Nov 2025 09:53:59 -0500 Subject: [PATCH 4/5] fix(types): Switch port location failed on port channel interfaces --- Cargo.lock | 30 ++++++++++++++--------- brocade/examples/main.rs | 21 ++++++++++------ examples/nanodc/src/main.rs | 4 +-- examples/okd_installation/src/topology.rs | 4 +-- examples/okd_pxe/src/topology.rs | 4 +-- examples/opnsense/src/main.rs | 4 +-- harmony_types/Cargo.toml | 1 + harmony_types/src/switch.rs | 18 +++++++++++--- 8 files changed, 56 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0504aa..321b2b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -690,6 +690,23 @@ dependencies = [ "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]] name = "brotli" version = "8.0.2" @@ -1853,18 +1870,6 @@ dependencies = [ "url", ] -[[package]] -name = "example-penpot" -version = "0.1.0" -dependencies = [ - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "tokio", - "url", -] - [[package]] name = "example-pxe" version = "0.1.0" @@ -2569,6 +2574,7 @@ dependencies = [ name = "harmony_types" version = "0.1.0" dependencies = [ + "log", "rand 0.9.2", "serde", "url", diff --git a/brocade/examples/main.rs b/brocade/examples/main.rs index de8623d..47d4a63 100644 --- a/brocade/examples/main.rs +++ b/brocade/examples/main.rs @@ -1,6 +1,6 @@ use std::net::{IpAddr, Ipv4Addr}; -use brocade::BrocadeOptions; +use brocade::{BrocadeOptions, ssh}; use harmony_secret::{Secret, SecretManager}; use harmony_types::switch::PortLocation; use serde::{Deserialize, Serialize}; @@ -16,20 +16,26 @@ async fn main() { 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(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 switch_addresses = vec![ip]; - let config = SecretManager::get_or_prompt::() - .await - .unwrap(); + // let config = SecretManager::get_or_prompt::() + // .await + // .unwrap(); let brocade = brocade::init( &switch_addresses, - &config.username, - &config.password, + // &config.username, + // &config.password, + "admin", + "password", BrocadeOptions { dry_run: true, + ssh: ssh::SshOptions { + port: 2222, + ..Default::default() + }, ..Default::default() }, ) @@ -53,6 +59,7 @@ async fn main() { } println!("--------------"); + todo!(); let channel_name = "1"; brocade.clear_port_channel(channel_name).await.unwrap(); diff --git a/examples/nanodc/src/main.rs b/examples/nanodc/src/main.rs index e487fab..ee19074 100644 --- a/examples/nanodc/src/main.rs +++ b/examples/nanodc/src/main.rs @@ -39,10 +39,10 @@ async fn main() { .expect("Failed to get credentials"); let switches: Vec = vec![ip!("192.168.33.101")]; - let brocade_options = Some(BrocadeOptions { + let brocade_options = BrocadeOptions { dry_run: *harmony::config::DRY_RUN, ..Default::default() - }); + }; let switch_client = BrocadeSwitchClient::init( &switches, &switch_auth.username, diff --git a/examples/okd_installation/src/topology.rs b/examples/okd_installation/src/topology.rs index 2bc9fd2..6af30fd 100644 --- a/examples/okd_installation/src/topology.rs +++ b/examples/okd_installation/src/topology.rs @@ -31,10 +31,10 @@ pub async fn get_topology() -> HAClusterTopology { .expect("Failed to get credentials"); let switches: Vec = vec![ip!("192.168.1.101")]; // TODO: Adjust me - let brocade_options = Some(BrocadeOptions { + let brocade_options = BrocadeOptions { dry_run: *harmony::config::DRY_RUN, ..Default::default() - }); + }; let switch_client = BrocadeSwitchClient::init( &switches, &switch_auth.username, diff --git a/examples/okd_pxe/src/topology.rs b/examples/okd_pxe/src/topology.rs index 72695fa..c32bf16 100644 --- a/examples/okd_pxe/src/topology.rs +++ b/examples/okd_pxe/src/topology.rs @@ -26,10 +26,10 @@ pub async fn get_topology() -> HAClusterTopology { .expect("Failed to get credentials"); let switches: Vec = vec![ip!("192.168.1.101")]; // TODO: Adjust me - let brocade_options = Some(BrocadeOptions { + let brocade_options = BrocadeOptions { dry_run: *harmony::config::DRY_RUN, ..Default::default() - }); + }; let switch_client = BrocadeSwitchClient::init( &switches, &switch_auth.username, diff --git a/examples/opnsense/src/main.rs b/examples/opnsense/src/main.rs index e74f5da..9cfd4cf 100644 --- a/examples/opnsense/src/main.rs +++ b/examples/opnsense/src/main.rs @@ -35,10 +35,10 @@ async fn main() { .expect("Failed to get credentials"); let switches: Vec = vec![ip!("192.168.5.101")]; // TODO: Adjust me - let brocade_options = Some(BrocadeOptions { + let brocade_options = BrocadeOptions { dry_run: *harmony::config::DRY_RUN, ..Default::default() - }); + }; let switch_client = BrocadeSwitchClient::init( &switches, &switch_auth.username, diff --git a/harmony_types/Cargo.toml b/harmony_types/Cargo.toml index f02874e..d1ac556 100644 --- a/harmony_types/Cargo.toml +++ b/harmony_types/Cargo.toml @@ -9,3 +9,4 @@ license.workspace = true serde.workspace = true url.workspace = true rand.workspace = true +log.workspace = true diff --git a/harmony_types/src/switch.rs b/harmony_types/src/switch.rs index 359e776..611207d 100644 --- a/harmony_types/src/switch.rs +++ b/harmony_types/src/switch.rs @@ -1,5 +1,5 @@ use std::{fmt, str::FromStr}; - +use log::trace; use serde::Serialize; /// Simple error type for port parsing failures. @@ -72,6 +72,11 @@ impl FromStr for PortLocation { pub enum PortDeclaration { /// A single switch port defined by its location. Example: `PortDeclaration::Single(1/1/1)` 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 (`-`). /// All ports between the endpoints (inclusive) are implicitly included. /// Example: `PortDeclaration::Range(1/1/1, 1/1/4)` @@ -132,8 +137,14 @@ impl PortDeclaration { return Ok(PortDeclaration::Set(start_port, end_port)); } - let location = PortLocation::from_str(port_str)?; - Ok(PortDeclaration::Single(location)) + match PortLocation::from_str(port_str) { + 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())) + } + } } } @@ -143,6 +154,7 @@ impl fmt::Display for PortDeclaration { PortDeclaration::Single(port) => write!(f, "{port}"), PortDeclaration::Range(start, end) => write!(f, "{start}-{end}"), PortDeclaration::Set(start, end) => write!(f, "{start}*{end}"), + PortDeclaration::Named(name) => write!(f, "{name}"), } } } From 8ee3f8a4adbbc762dded2f1287ea9db0507749ea Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Tue, 11 Nov 2025 11:32:42 -0500 Subject: [PATCH 5/5] chore: Update harmony-inventory-agent binary as some fixes were introduced : port is 25000 now and nbd devices wont make the inventory crash --- data/pxe/okd/http_files/harmony_inventory_agent | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/pxe/okd/http_files/harmony_inventory_agent b/data/pxe/okd/http_files/harmony_inventory_agent index 1d802f7..47c7aaa 100755 --- a/data/pxe/okd/http_files/harmony_inventory_agent +++ b/data/pxe/okd/http_files/harmony_inventory_agent @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5244fa8968fe15c2415de6cc487e6112f8aedd9989951e018f9bdb536b1016d2 -size 8139216 +oid sha256:78b2cf5b2faa1a6b637c0d6ba3d37f427ee9f1b087e8605b95acce83dc417aa1 +size 8187248