diff --git a/examples/nanodc/src/main.rs b/examples/nanodc/src/main.rs index 57574d2..95b16a6 100644 --- a/examples/nanodc/src/main.rs +++ b/examples/nanodc/src/main.rs @@ -39,8 +39,7 @@ async fn main() { let gateway_ipv4 = Ipv4Addr::new(192, 168, 33, 1); let gateway_ip = IpAddr::V4(gateway_ipv4); let topology = harmony::topology::HAClusterTopology { - domain_name: "ncd0.harmony.mcd".to_string(), // TODO this must be set manually correctly - // when setting up the opnsense firewall + domain_name: "ncd0.harmony.mcd".to_string(), router: Arc::new(UnmanagedRouter::new( gateway_ip, Ipv4Cidr::new(lan_subnet, 24).unwrap(), @@ -84,6 +83,7 @@ async fn main() { }, ], switch: vec![], + node_exporter: opnsense.clone(), }; let inventory = Inventory { diff --git a/examples/okd_installation/src/topology.rs b/examples/okd_installation/src/topology.rs index 31062f5..4df6ab5 100644 --- a/examples/okd_installation/src/topology.rs +++ b/examples/okd_installation/src/topology.rs @@ -59,6 +59,7 @@ pub async fn get_topology() -> HAClusterTopology { }, workers: vec![], switch: vec![], + node_exporter: opnsense.clone(), } } diff --git a/examples/okd_pxe/src/topology.rs b/examples/okd_pxe/src/topology.rs index 707969a..63e3613 100644 --- a/examples/okd_pxe/src/topology.rs +++ b/examples/okd_pxe/src/topology.rs @@ -53,6 +53,7 @@ pub async fn get_topology() -> HAClusterTopology { }, workers: vec![], switch: vec![], + node_exporter: opnsense.clone(), } } diff --git a/examples/opnsense/src/main.rs b/examples/opnsense/src/main.rs index fcfaf09..8f4039d 100644 --- a/examples/opnsense/src/main.rs +++ b/examples/opnsense/src/main.rs @@ -55,6 +55,7 @@ async fn main() { }, workers: vec![], switch: vec![], + node_exporter: opnsense.clone(), }; let inventory = Inventory { diff --git a/examples/opnsense_node_exporter/Cargo.toml b/examples/opnsense_node_exporter/Cargo.toml new file mode 100644 index 0000000..957bdd9 --- /dev/null +++ b/examples/opnsense_node_exporter/Cargo.toml @@ -0,0 +1,20 @@ +[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 diff --git a/examples/opnsense_node_exporter/src/main.rs b/examples/opnsense_node_exporter/src/main.rs new file mode 100644 index 0000000..4f1219d --- /dev/null +++ b/examples/opnsense_node_exporter/src/main.rs @@ -0,0 +1,110 @@ +use std::{ + net::{IpAddr, Ipv4Addr}, + sync::Arc, +}; + +use cidr::Ipv4Cidr; +use harmony::{ + hardware::{HostCategory, Location, PhysicalHost, SwitchGroup}, + infra::opnsense::OPNSenseManagementInterface, + inventory::Inventory, + modules::opnsense::node_exporter::NodeExporterScore, + topology::{HAClusterTopology, LogicalHost, UnmanagedRouter}, +}; +use harmony_macros::{ip, ipv4, mac_address}; + +#[tokio::main] +async fn main() { + let firewall = harmony::topology::LogicalHost { + ip: ip!("192.168.33.1"), + name: String::from("fw0"), + }; + + let opnsense = Arc::new( + harmony::infra::opnsense::OPNSenseFirewall::new(firewall, None, "root", "opnsense").await, + ); + let lan_subnet = Ipv4Addr::new(192, 168, 33, 0); + let gateway_ipv4 = Ipv4Addr::new(192, 168, 33, 1); + let gateway_ip = IpAddr::V4(gateway_ipv4); + let topology = harmony::topology::HAClusterTopology { + domain_name: "ncd0.harmony.mcd".to_string(), + router: Arc::new(UnmanagedRouter::new( + gateway_ip, + Ipv4Cidr::new(lan_subnet, 24).unwrap(), + )), + load_balancer: opnsense.clone(), + firewall: opnsense.clone(), + tftp_server: opnsense.clone(), + http_server: opnsense.clone(), + dhcp_server: opnsense.clone(), + dns_server: opnsense.clone(), + control_plane: vec![ + LogicalHost { + ip: ip!("192.168.33.20"), + name: "cp0".to_string(), + }, + LogicalHost { + ip: ip!("192.168.33.21"), + name: "cp1".to_string(), + }, + LogicalHost { + ip: ip!("192.168.33.22"), + name: "cp2".to_string(), + }, + ], + bootstrap_host: LogicalHost { + ip: ip!("192.168.33.66"), + name: "bootstrap".to_string(), + }, + workers: vec![ + LogicalHost { + ip: ip!("192.168.33.30"), + name: "wk0".to_string(), + }, + LogicalHost { + ip: ip!("192.168.33.31"), + name: "wk1".to_string(), + }, + LogicalHost { + ip: ip!("192.168.33.32"), + name: "wk2".to_string(), + }, + ], + switch: vec![], + node_exporter: opnsense.clone(), + }; + + let inventory = Inventory { + location: Location::new("I am mobile".to_string(), "earth".to_string()), + switch: SwitchGroup::from([]), + firewall_mgmt: Box::new(OPNSenseManagementInterface::new()), + storage_host: vec![], + worker_host: vec![ + PhysicalHost::empty(HostCategory::Server) + .mac_address(mac_address!("C4:62:37:02:61:0F")), + PhysicalHost::empty(HostCategory::Server) + .mac_address(mac_address!("C4:62:37:02:61:26")), + PhysicalHost::empty(HostCategory::Server) + .mac_address(mac_address!("C4:62:37:02:61:70")), + ], + control_plane_host: vec![ + PhysicalHost::empty(HostCategory::Server) + .mac_address(mac_address!("C4:62:37:02:60:FA")), + PhysicalHost::empty(HostCategory::Server) + .mac_address(mac_address!("C4:62:37:02:61:1A")), + PhysicalHost::empty(HostCategory::Server) + .mac_address(mac_address!("C4:62:37:01:BC:68")), + ], + }; + + 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 7be2725..a3e650d 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -11,7 +11,6 @@ use kube::api::ObjectMeta; use log::debug; use log::info; -use crate::data::FileContent; use crate::executors::ExecutorError; use crate::hardware::PhysicalHost; use crate::infra::brocade::BrocadeSwitchAuth; @@ -21,6 +20,7 @@ use crate::modules::okd::crd::{ nmstate::{self, NMState, NodeNetworkConfigurationPolicy, NodeNetworkConfigurationPolicySpec}, }; use crate::topology::PxeOptions; +use crate::{data::FileContent, topology::node_exporter::NodeExporter}; use super::{ DHCPStaticEntry, DhcpServer, DnsRecord, DnsRecordType, DnsServer, Firewall, HostNetworkConfig, @@ -43,6 +43,7 @@ pub struct HAClusterTopology { pub tftp_server: Arc, pub http_server: Arc, pub dns_server: Arc, + pub node_exporter: Arc, pub bootstrap_host: LogicalHost, pub control_plane: Vec, pub workers: Vec, @@ -333,6 +334,7 @@ impl HAClusterTopology { tftp_server: dummy_infra.clone(), http_server: dummy_infra.clone(), dns_server: dummy_infra.clone(), + node_exporter: dummy_infra.clone(), bootstrap_host: dummy_host, control_plane: vec![], workers: vec![], @@ -516,6 +518,23 @@ impl Switch for HAClusterTopology { self.configure_bond(host, &config).await?; self.configure_port_channel(host, &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)] @@ -704,3 +723,18 @@ impl DnsServer for DummyInfra { unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } } + +#[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) + } +} diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index 85e57d7..08c1c15 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -1,5 +1,6 @@ mod ha_cluster; pub mod ingress; +pub mod node_exporter; use harmony_types::net::IpAddress; mod host_binding; mod http; diff --git a/harmony/src/domain/topology/node_exporter.rs b/harmony/src/domain/topology/node_exporter.rs new file mode 100644 index 0000000..88e3cc9 --- /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 { + 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 3878cfc..102d2b6 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..2c27b26 --- /dev/null +++ b/harmony/src/infra/opnsense/node_exporter.rs @@ -0,0 +1,44 @@ +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); + 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/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 fa5f985..4b384d4 100644 --- a/opnsense-config-xml/src/data/opnsense.rs +++ b/opnsense-config-xml/src/data/opnsense.rs @@ -433,7 +433,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, @@ -1595,3 +1595,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..30240e4 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}; @@ -71,6 +72,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 } 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(()) + } +}