diff --git a/Cargo.lock b/Cargo.lock index 65f2d8b..5c45111 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -690,6 +690,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "brocade-snmp-server" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "brocade", + "env_logger", + "harmony", + "harmony_cli", + "harmony_macros", + "harmony_secret", + "harmony_types", + "log", + "serde", + "tokio", + "url", +] + [[package]] name = "brocade-switch" version = "0.1.0" @@ -1885,6 +1903,25 @@ dependencies = [ "url", ] +[[package]] +name = "example-opnsense-node-exporter" +version = "0.1.0" +dependencies = [ + "async-trait", + "cidr", + "env_logger", + "harmony", + "harmony_cli", + "harmony_macros", + "harmony_secret", + "harmony_secret_derive", + "harmony_types", + "log", + "serde", + "tokio", + "url", +] + [[package]] name = "example-pxe" version = "0.1.0" @@ -6095,21 +6132,6 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" -[[package]] -name = "test-score" -version = "0.1.0" -dependencies = [ - "base64 0.22.1", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "tokio", - "url", -] - [[package]] name = "thiserror" version = "1.0.69" diff --git a/examples/nanodc/src/main.rs b/examples/nanodc/src/main.rs index ee19074..bffd659 100644 --- a/examples/nanodc/src/main.rs +++ b/examples/nanodc/src/main.rs @@ -106,6 +106,7 @@ async fn main() { name: "wk2".to_string(), }, ], + node_exporter: opnsense.clone(), switch_client: switch_client.clone(), network_manager: OnceLock::new(), }; diff --git a/examples/okd_installation/src/topology.rs b/examples/okd_installation/src/topology.rs index 6af30fd..ce43f6b 100644 --- a/examples/okd_installation/src/topology.rs +++ b/examples/okd_installation/src/topology.rs @@ -83,6 +83,7 @@ pub async fn get_topology() -> HAClusterTopology { name: "bootstrap".to_string(), }, workers: vec![], + node_exporter: opnsense.clone(), switch_client: switch_client.clone(), network_manager: OnceLock::new(), } diff --git a/examples/okd_pxe/src/topology.rs b/examples/okd_pxe/src/topology.rs index c32bf16..73e5ba3 100644 --- a/examples/okd_pxe/src/topology.rs +++ b/examples/okd_pxe/src/topology.rs @@ -78,6 +78,7 @@ pub async fn get_topology() -> HAClusterTopology { name: "cp0".to_string(), }, workers: vec![], + node_exporter: opnsense.clone(), switch_client: switch_client.clone(), network_manager: OnceLock::new(), } diff --git a/examples/opnsense/src/main.rs b/examples/opnsense/src/main.rs index 9cfd4cf..316c4af 100644 --- a/examples/opnsense/src/main.rs +++ b/examples/opnsense/src/main.rs @@ -78,6 +78,7 @@ async fn main() { name: "cp0".to_string(), }, workers: vec![], + node_exporter: opnsense.clone(), switch_client: switch_client.clone(), network_manager: OnceLock::new(), }; diff --git a/examples/opnsense_node_exporter/Cargo.toml b/examples/opnsense_node_exporter/Cargo.toml new file mode 100644 index 0000000..5cc5c10 --- /dev/null +++ b/examples/opnsense_node_exporter/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "example-opnsense-node-exporter" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies] +harmony = { path = "../../harmony" } +harmony_cli = { path = "../../harmony_cli" } +harmony_types = { path = "../../harmony_types" } +harmony_secret = { path = "../../harmony_secret" } +harmony_secret_derive = { path = "../../harmony_secret_derive" } +cidr = { workspace = true } +tokio = { workspace = true } +harmony_macros = { path = "../../harmony_macros" } +log = { workspace = true } +env_logger = { workspace = true } +url = { workspace = true } +serde.workspace = true +async-trait.workspace = true diff --git a/examples/opnsense_node_exporter/src/main.rs b/examples/opnsense_node_exporter/src/main.rs new file mode 100644 index 0000000..d71d2ed --- /dev/null +++ b/examples/opnsense_node_exporter/src/main.rs @@ -0,0 +1,80 @@ +use std::{ + net::{IpAddr, Ipv4Addr}, + sync::Arc, +}; + +use async_trait::async_trait; +use cidr::Ipv4Cidr; +use harmony::{ + executors::ExecutorError, + hardware::{HostCategory, Location, PhysicalHost, SwitchGroup}, + infra::opnsense::OPNSenseManagementInterface, + inventory::Inventory, + modules::opnsense::node_exporter::NodeExporterScore, + topology::{ + HAClusterTopology, LogicalHost, PreparationError, PreparationOutcome, Topology, + UnmanagedRouter, node_exporter::NodeExporter, + }, +}; +use harmony_macros::{ip, ipv4, mac_address}; + +#[derive(Debug)] +struct OpnSenseTopology { + node_exporter: Arc, +} + +#[async_trait] +impl Topology for OpnSenseTopology { + async fn ensure_ready(&self) -> Result { + Ok(PreparationOutcome::Success { + details: "Success".to_string(), + }) + } + fn name(&self) -> &str { + "OpnsenseTopology" + } +} + +#[async_trait] +impl NodeExporter for OpnSenseTopology { + async fn ensure_initialized(&self) -> Result<(), ExecutorError> { + self.node_exporter.ensure_initialized().await + } + + async fn commit_config(&self) -> Result<(), ExecutorError> { + self.node_exporter.commit_config().await + } + + async fn reload_restart(&self) -> Result<(), ExecutorError> { + self.node_exporter.reload_restart().await + } +} + +#[tokio::main] +async fn main() { + let firewall = harmony::topology::LogicalHost { + ip: ip!("192.168.1.1"), + name: String::from("fw0"), + }; + + let opnsense = Arc::new( + harmony::infra::opnsense::OPNSenseFirewall::new(firewall, None, "root", "opnsense").await, + ); + + let topology = OpnSenseTopology { + node_exporter: opnsense.clone(), + }; + + let inventory = Inventory::empty(); + + let node_exporter_score = NodeExporterScore {}; + + harmony_cli::run( + inventory, + topology, + vec![Box::new(node_exporter_score)], + None, + ) + .await + .unwrap(); +} diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 2f9e348..532fdda 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -9,7 +9,7 @@ use harmony_types::{ use log::debug; use log::info; -use crate::{data::FileContent, executors::ExecutorError}; +use crate::{data::FileContent, executors::ExecutorError, topology::node_exporter::NodeExporter}; use crate::{infra::network_manager::OpenShiftNmStateNetworkManager, topology::PortConfig}; use crate::{modules::inventory::HarmonyDiscoveryStrategy, topology::PxeOptions}; @@ -19,7 +19,6 @@ use super::{ NetworkManager, PreparationError, PreparationOutcome, Router, Switch, SwitchClient, SwitchError, TftpServer, Topology, k8s::K8sClient, }; - use std::sync::{Arc, OnceLock}; #[derive(Debug, Clone)] @@ -32,6 +31,7 @@ pub struct HAClusterTopology { pub tftp_server: Arc, pub http_server: Arc, pub dns_server: Arc, + pub node_exporter: Arc, pub switch_client: Arc, pub bootstrap_host: LogicalHost, pub control_plane: Vec, @@ -116,6 +116,7 @@ impl HAClusterTopology { tftp_server: dummy_infra.clone(), http_server: dummy_infra.clone(), dns_server: dummy_infra.clone(), + node_exporter: dummy_infra.clone(), switch_client: dummy_infra.clone(), bootstrap_host: dummy_host, control_plane: vec![], @@ -320,6 +321,23 @@ impl NetworkManager for HAClusterTopology { async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), NetworkError> { self.network_manager().await.configure_bond(config).await } + + //TODO add snmp here +} + +#[async_trait] +impl NodeExporter for HAClusterTopology { + async fn ensure_initialized(&self) -> Result<(), ExecutorError> { + self.node_exporter.ensure_initialized().await + } + + async fn commit_config(&self) -> Result<(), ExecutorError> { + self.node_exporter.commit_config().await + } + + async fn reload_restart(&self) -> Result<(), ExecutorError> { + self.node_exporter.reload_restart().await + } } #[derive(Debug)] @@ -509,6 +527,21 @@ impl DnsServer for DummyInfra { } } +#[async_trait] +impl NodeExporter for DummyInfra { + async fn ensure_initialized(&self) -> Result<(), ExecutorError> { + unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + } + + async fn commit_config(&self) -> Result<(), ExecutorError> { + unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + } + + async fn reload_restart(&self) -> Result<(), ExecutorError> { + unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + } +} + #[async_trait] impl SwitchClient for DummyInfra { async fn setup(&self) -> Result<(), SwitchError> { diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index 64e12d0..a3c246a 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -1,6 +1,7 @@ mod failover; mod ha_cluster; pub mod ingress; +pub mod node_exporter; pub use failover::*; use harmony_types::net::IpAddress; mod host_binding; diff --git a/harmony/src/domain/topology/node_exporter.rs b/harmony/src/domain/topology/node_exporter.rs new file mode 100644 index 0000000..1e6ef67 --- /dev/null +++ b/harmony/src/domain/topology/node_exporter.rs @@ -0,0 +1,17 @@ +use async_trait::async_trait; + +use crate::executors::ExecutorError; + +#[async_trait] +pub trait NodeExporter: Send + Sync + std::fmt::Debug { + async fn ensure_initialized(&self) -> Result<(), ExecutorError>; + async fn commit_config(&self) -> Result<(), ExecutorError>; + async fn reload_restart(&self) -> Result<(), ExecutorError>; +} + +// //TODO complete this impl +// impl std::fmt::Debug for dyn NodeExporter { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// f.write_fmt(format_args!("NodeExporter ",)) +// } +// } diff --git a/harmony/src/infra/opnsense/mod.rs b/harmony/src/infra/opnsense/mod.rs index b5223e4..efa6ea1 100644 --- a/harmony/src/infra/opnsense/mod.rs +++ b/harmony/src/infra/opnsense/mod.rs @@ -4,6 +4,7 @@ mod firewall; mod http; mod load_balancer; mod management; +pub mod node_exporter; mod tftp; use std::sync::Arc; diff --git a/harmony/src/infra/opnsense/node_exporter.rs b/harmony/src/infra/opnsense/node_exporter.rs new file mode 100644 index 0000000..97d2a09 --- /dev/null +++ b/harmony/src/infra/opnsense/node_exporter.rs @@ -0,0 +1,47 @@ +use async_trait::async_trait; +use log::debug; + +use crate::{ + executors::ExecutorError, infra::opnsense::OPNSenseFirewall, + topology::node_exporter::NodeExporter, +}; + +#[async_trait] +impl NodeExporter for OPNSenseFirewall { + async fn ensure_initialized(&self) -> Result<(), ExecutorError> { + let mut config = self.opnsense_config.write().await; + let node_exporter = config.node_exporter(); + if let Some(config) = node_exporter.get_full_config() { + debug!( + "Node exporter available in opnsense config, assuming it is already installed. {config:?}" + ); + } else { + config + .install_package("os-node_exporter") + .await + .map_err(|e| { + ExecutorError::UnexpectedError(format!("Executor failed when trying to install os-node_exporter package with error {e:?}" + )) + })?; + } + + config + .node_exporter() + .enable(true) + .map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?; + Ok(()) + } + async fn commit_config(&self) -> Result<(), ExecutorError> { + OPNSenseFirewall::commit_config(self).await + } + + async fn reload_restart(&self) -> Result<(), ExecutorError> { + self.opnsense_config + .write() + .await + .node_exporter() + .reload_restart() + .await + .map_err(|e| ExecutorError::UnexpectedError(e.to_string())) + } +} diff --git a/harmony/src/modules/brocade.rs b/harmony/src/modules/brocade.rs index de6b957..a52dd6e 100644 --- a/harmony/src/modules/brocade.rs +++ b/harmony/src/modules/brocade.rs @@ -69,13 +69,12 @@ impl Interpret for BrocadeEnableSnmpInterpret { let brocade = brocade::init( &switch_addresses, - 22, &config.username, &config.password, - Some(BrocadeOptions { + BrocadeOptions { dry_run: self.score.dry_run, ..Default::default() - }), + }, ) .await .expect("Brocade client failed to connect"); diff --git a/harmony/src/modules/opnsense/mod.rs b/harmony/src/modules/opnsense/mod.rs index 28b52cf..8988205 100644 --- a/harmony/src/modules/opnsense/mod.rs +++ b/harmony/src/modules/opnsense/mod.rs @@ -1,3 +1,4 @@ +pub mod node_exporter; mod shell; mod upgrade; pub use shell::*; diff --git a/harmony/src/modules/opnsense/node_exporter.rs b/harmony/src/modules/opnsense/node_exporter.rs new file mode 100644 index 0000000..d17f67a --- /dev/null +++ b/harmony/src/modules/opnsense/node_exporter.rs @@ -0,0 +1,70 @@ +use async_trait::async_trait; +use harmony_types::id::Id; +use log::info; +use serde::Serialize; + +use crate::{ + data::Version, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + score::Score, + topology::{Topology, node_exporter::NodeExporter}, +}; + +#[derive(Debug, Clone, Serialize)] +pub struct NodeExporterScore {} + +impl Score for NodeExporterScore { + fn name(&self) -> String { + "NodeExporterScore".to_string() + } + + fn create_interpret(&self) -> Box> { + Box::new(NodeExporterInterpret {}) + } +} + +#[derive(Debug)] +pub struct NodeExporterInterpret {} + +#[async_trait] +impl Interpret for NodeExporterInterpret { + async fn execute( + &self, + _inventory: &Inventory, + node_exporter: &T, + ) -> Result { + info!( + "Making sure node exporter is initiailized: {:?}", + node_exporter.ensure_initialized().await? + ); + + info!("Applying Node Exporter configuration"); + + node_exporter.commit_config().await?; + + info!("Reloading and restarting Node Exporter"); + + node_exporter.reload_restart().await?; + + Ok(Outcome::success(format!( + "NodeExporter successfully configured" + ))) + } + + fn get_name(&self) -> InterpretName { + InterpretName::Custom("NodeExporter") + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } +} diff --git a/opnsense-config-xml/src/data/opnsense.rs b/opnsense-config-xml/src/data/opnsense.rs index bf4e652..b92657f 100644 --- a/opnsense-config-xml/src/data/opnsense.rs +++ b/opnsense-config-xml/src/data/opnsense.rs @@ -17,7 +17,7 @@ pub struct OPNsense { pub interfaces: NamedList, pub dhcpd: NamedList, pub snmpd: Snmpd, - pub syslog: Syslog, + pub syslog: Option, pub nat: Nat, pub filter: Filters, pub load_balancer: Option, @@ -191,7 +191,7 @@ pub struct System { pub webgui: WebGui, pub usevirtualterminal: u8, pub disablenatreflection: Option, - pub disableconsolemenu: u8, + pub disableconsolemenu: Option, pub disablevlanhwfilter: u8, pub disablechecksumoffloading: u8, pub disablesegmentationoffloading: u8, @@ -235,16 +235,16 @@ pub struct System { #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct Ssh { pub group: String, - pub noauto: u8, - pub interfaces: MaybeString, - pub kex: MaybeString, - pub ciphers: MaybeString, - pub macs: MaybeString, - pub keys: MaybeString, - pub enabled: String, - pub passwordauth: u8, - pub keysig: MaybeString, - pub permitrootlogin: u8, + pub noauto: Option, + pub interfaces: Option, + pub kex: Option, + pub ciphers: Option, + pub macs: Option, + pub keys: Option, + pub enabled: Option, + pub passwordauth: Option, + pub keysig: Option, + pub permitrootlogin: Option, pub rekeylimit: Option, } @@ -308,11 +308,11 @@ pub struct WebGui { pub protocol: String, #[yaserde(rename = "ssl-certref")] pub ssl_certref: String, - pub port: MaybeString, + pub port: Option, #[yaserde(rename = "ssl-ciphers")] - pub ssl_ciphers: MaybeString, - pub interfaces: MaybeString, - pub compression: MaybeString, + pub ssl_ciphers: Option, + pub interfaces: Option, + pub compression: Option, pub nohttpreferercheck: Option, } @@ -436,7 +436,7 @@ pub struct OPNsenseXmlSection { #[yaserde(rename = "Interfaces")] pub interfaces: Option, #[yaserde(rename = "NodeExporter")] - pub node_exporter: Option, + pub node_exporter: Option, #[yaserde(rename = "Kea")] pub kea: Option, pub monit: Option, @@ -1613,3 +1613,21 @@ pub struct Ifgroups { #[yaserde(attribute = true)] pub version: String, } + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct NodeExporter { + pub enabled: u8, + pub listenaddress: Option, + pub listenport: u16, + pub cpu: u8, + pub exec: u8, + pub filesystem: u8, + pub loadavg: u8, + pub meminfo: u8, + pub netdev: u8, + pub time: u8, + pub devstat: u8, + pub interrupts: u8, + pub ntp: u8, + pub zfs: u8, +} diff --git a/opnsense-config/src/config/config.rs b/opnsense-config/src/config/config.rs index c2d0f60..7c292c8 100644 --- a/opnsense-config/src/config/config.rs +++ b/opnsense-config/src/config/config.rs @@ -5,7 +5,8 @@ use crate::{ error::Error, modules::{ caddy::CaddyConfig, dhcp_legacy::DhcpConfigLegacyISC, dns::UnboundDnsConfig, - dnsmasq::DhcpConfigDnsMasq, load_balancer::LoadBalancerConfig, tftp::TftpConfig, + dnsmasq::DhcpConfigDnsMasq, load_balancer::LoadBalancerConfig, + node_exporter::NodeExporterConfig, tftp::TftpConfig, }, }; use log::{debug, info, trace, warn}; @@ -13,6 +14,7 @@ use opnsense_config_xml::OPNsense; use russh::client; use serde::Serialize; use sha2::Digest; +use tokio::time::{sleep, Duration}; use super::{ConfigManager, OPNsenseShell}; @@ -71,6 +73,10 @@ impl Config { LoadBalancerConfig::new(&mut self.opnsense, self.shell.clone()) } + pub fn node_exporter(&mut self) -> NodeExporterConfig<'_> { + NodeExporterConfig::new(&mut self.opnsense, self.shell.clone()) + } + pub async fn upload_files(&self, source: &str, destination: &str) -> Result { self.shell.upload_folder(source, destination).await } @@ -150,7 +156,8 @@ impl Config { async fn reload_config(&mut self) -> Result<(), Error> { info!("Reloading opnsense live config"); - let (opnsense, sha2) = Self::get_opnsense_instance(self.repository.clone()).await?; + let (opnsense, _sha2) = Self::get_opnsense_instance(self.repository.clone()).await?; + self.opnsense = opnsense; Ok(()) } diff --git a/opnsense-config/src/modules/mod.rs b/opnsense-config/src/modules/mod.rs index 3448075..eec16a2 100644 --- a/opnsense-config/src/modules/mod.rs +++ b/opnsense-config/src/modules/mod.rs @@ -4,4 +4,5 @@ pub mod dhcp_legacy; pub mod dns; pub mod dnsmasq; pub mod load_balancer; +pub mod node_exporter; pub mod tftp; diff --git a/opnsense-config/src/modules/node_exporter.rs b/opnsense-config/src/modules/node_exporter.rs new file mode 100644 index 0000000..fd7ee5c --- /dev/null +++ b/opnsense-config/src/modules/node_exporter.rs @@ -0,0 +1,54 @@ +use std::sync::Arc; + +use opnsense_config_xml::{NodeExporter, OPNsense}; + +use crate::{config::OPNsenseShell, Error}; + +pub struct NodeExporterConfig<'a> { + opnsense: &'a mut OPNsense, + opnsense_shell: Arc, +} + +impl<'a> NodeExporterConfig<'a> { + pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc) -> Self { + Self { + opnsense, + opnsense_shell, + } + } + + pub fn get_full_config(&self) -> &Option { + &self.opnsense.opnsense.node_exporter + } + + fn with_node_exporter(&mut self, f: F) -> Result + where + F: FnOnce(&mut NodeExporter) -> R, + { + match &mut self.opnsense.opnsense.node_exporter.as_mut() { + Some(node_exporter) => Ok(f(node_exporter)), + None => Err("node exporter is not yet installed"), + } + } + + pub fn enable(&mut self, enabled: bool) -> Result<(), &'static str> { + self.with_node_exporter(|node_exporter| node_exporter.enabled = enabled as u8) + .map(|_| ()) + } + + pub async fn reload_restart(&self) -> Result<(), Error> { + self.opnsense_shell + .exec("configctl node_exporter stop") + .await?; + self.opnsense_shell + .exec("configctl template reload OPNsense/NodeExporter") + .await?; + self.opnsense_shell + .exec("configctl node_exporter configtest") + .await?; + self.opnsense_shell + .exec("configctl node_exporter start") + .await?; + Ok(()) + } +}