Merge pull request 'feat: implementation for opnsense os-node_exporter' (#173) from feat/install_opnsense_node_exporter into master
All checks were successful
Run Check Script / check (push) Successful in 59s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 6m58s

Reviewed-on: #173
This commit is contained in:
2026-01-06 19:19:34 +00:00
19 changed files with 415 additions and 39 deletions

52
Cargo.lock generated
View File

@@ -690,6 +690,24 @@ dependencies = [
"tokio", "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]] [[package]]
name = "brocade-switch" name = "brocade-switch"
version = "0.1.0" version = "0.1.0"
@@ -1885,6 +1903,25 @@ dependencies = [
"url", "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]] [[package]]
name = "example-pxe" name = "example-pxe"
version = "0.1.0" version = "0.1.0"
@@ -6095,21 +6132,6 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" 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]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"

View File

@@ -106,6 +106,7 @@ async fn main() {
name: "wk2".to_string(), name: "wk2".to_string(),
}, },
], ],
node_exporter: opnsense.clone(),
switch_client: switch_client.clone(), switch_client: switch_client.clone(),
network_manager: OnceLock::new(), network_manager: OnceLock::new(),
}; };

View File

@@ -83,6 +83,7 @@ pub async fn get_topology() -> HAClusterTopology {
name: "bootstrap".to_string(), name: "bootstrap".to_string(),
}, },
workers: vec![], workers: vec![],
node_exporter: opnsense.clone(),
switch_client: switch_client.clone(), switch_client: switch_client.clone(),
network_manager: OnceLock::new(), network_manager: OnceLock::new(),
} }

View File

@@ -78,6 +78,7 @@ pub async fn get_topology() -> HAClusterTopology {
name: "cp0".to_string(), name: "cp0".to_string(),
}, },
workers: vec![], workers: vec![],
node_exporter: opnsense.clone(),
switch_client: switch_client.clone(), switch_client: switch_client.clone(),
network_manager: OnceLock::new(), network_manager: OnceLock::new(),
} }

View File

@@ -78,6 +78,7 @@ async fn main() {
name: "cp0".to_string(), name: "cp0".to_string(),
}, },
workers: vec![], workers: vec![],
node_exporter: opnsense.clone(),
switch_client: switch_client.clone(), switch_client: switch_client.clone(),
network_manager: OnceLock::new(), network_manager: OnceLock::new(),
}; };

View File

@@ -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

View File

@@ -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<dyn NodeExporter>,
}
#[async_trait]
impl Topology for OpnSenseTopology {
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> {
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();
}

View File

@@ -9,7 +9,7 @@ use harmony_types::{
use log::debug; use log::debug;
use log::info; 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::{infra::network_manager::OpenShiftNmStateNetworkManager, topology::PortConfig};
use crate::{modules::inventory::HarmonyDiscoveryStrategy, topology::PxeOptions}; use crate::{modules::inventory::HarmonyDiscoveryStrategy, topology::PxeOptions};
@@ -19,7 +19,6 @@ use super::{
NetworkManager, PreparationError, PreparationOutcome, Router, Switch, SwitchClient, NetworkManager, PreparationError, PreparationOutcome, Router, Switch, SwitchClient,
SwitchError, TftpServer, Topology, k8s::K8sClient, SwitchError, TftpServer, Topology, k8s::K8sClient,
}; };
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -32,6 +31,7 @@ pub struct HAClusterTopology {
pub tftp_server: Arc<dyn TftpServer>, pub tftp_server: Arc<dyn TftpServer>,
pub http_server: Arc<dyn HttpServer>, pub http_server: Arc<dyn HttpServer>,
pub dns_server: Arc<dyn DnsServer>, pub dns_server: Arc<dyn DnsServer>,
pub node_exporter: Arc<dyn NodeExporter>,
pub switch_client: Arc<dyn SwitchClient>, pub switch_client: Arc<dyn SwitchClient>,
pub bootstrap_host: LogicalHost, pub bootstrap_host: LogicalHost,
pub control_plane: Vec<LogicalHost>, pub control_plane: Vec<LogicalHost>,
@@ -116,6 +116,7 @@ impl HAClusterTopology {
tftp_server: dummy_infra.clone(), tftp_server: dummy_infra.clone(),
http_server: dummy_infra.clone(), http_server: dummy_infra.clone(),
dns_server: dummy_infra.clone(), dns_server: dummy_infra.clone(),
node_exporter: dummy_infra.clone(),
switch_client: dummy_infra.clone(), switch_client: dummy_infra.clone(),
bootstrap_host: dummy_host, bootstrap_host: dummy_host,
control_plane: vec![], control_plane: vec![],
@@ -320,6 +321,23 @@ impl NetworkManager for HAClusterTopology {
async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), NetworkError> { async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), NetworkError> {
self.network_manager().await.configure_bond(config).await 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)] #[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] #[async_trait]
impl SwitchClient for DummyInfra { impl SwitchClient for DummyInfra {
async fn setup(&self) -> Result<(), SwitchError> { async fn setup(&self) -> Result<(), SwitchError> {

View File

@@ -1,6 +1,7 @@
mod failover; mod failover;
mod ha_cluster; mod ha_cluster;
pub mod ingress; pub mod ingress;
pub mod node_exporter;
pub use failover::*; pub use failover::*;
use harmony_types::net::IpAddress; use harmony_types::net::IpAddress;
mod host_binding; mod host_binding;

View File

@@ -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 ",))
// }
// }

View File

@@ -4,6 +4,7 @@ mod firewall;
mod http; mod http;
mod load_balancer; mod load_balancer;
mod management; mod management;
pub mod node_exporter;
mod tftp; mod tftp;
use std::sync::Arc; use std::sync::Arc;

View File

@@ -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()))
}
}

View File

@@ -69,13 +69,12 @@ impl<T: Topology> Interpret<T> for BrocadeEnableSnmpInterpret {
let brocade = brocade::init( let brocade = brocade::init(
&switch_addresses, &switch_addresses,
22,
&config.username, &config.username,
&config.password, &config.password,
Some(BrocadeOptions { BrocadeOptions {
dry_run: self.score.dry_run, dry_run: self.score.dry_run,
..Default::default() ..Default::default()
}), },
) )
.await .await
.expect("Brocade client failed to connect"); .expect("Brocade client failed to connect");

View File

@@ -1,3 +1,4 @@
pub mod node_exporter;
mod shell; mod shell;
mod upgrade; mod upgrade;
pub use shell::*; pub use shell::*;

View File

@@ -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<T: Topology + NodeExporter> Score<T> for NodeExporterScore {
fn name(&self) -> String {
"NodeExporterScore".to_string()
}
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(NodeExporterInterpret {})
}
}
#[derive(Debug)]
pub struct NodeExporterInterpret {}
#[async_trait]
impl<T: Topology + NodeExporter> Interpret<T> for NodeExporterInterpret {
async fn execute(
&self,
_inventory: &Inventory,
node_exporter: &T,
) -> Result<Outcome, InterpretError> {
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<Id> {
todo!()
}
}

View File

@@ -17,7 +17,7 @@ pub struct OPNsense {
pub interfaces: NamedList<Interface>, pub interfaces: NamedList<Interface>,
pub dhcpd: NamedList<DhcpInterface>, pub dhcpd: NamedList<DhcpInterface>,
pub snmpd: Snmpd, pub snmpd: Snmpd,
pub syslog: Syslog, pub syslog: Option<Syslog>,
pub nat: Nat, pub nat: Nat,
pub filter: Filters, pub filter: Filters,
pub load_balancer: Option<LoadBalancer>, pub load_balancer: Option<LoadBalancer>,
@@ -191,7 +191,7 @@ pub struct System {
pub webgui: WebGui, pub webgui: WebGui,
pub usevirtualterminal: u8, pub usevirtualterminal: u8,
pub disablenatreflection: Option<String>, pub disablenatreflection: Option<String>,
pub disableconsolemenu: u8, pub disableconsolemenu: Option<u8>,
pub disablevlanhwfilter: u8, pub disablevlanhwfilter: u8,
pub disablechecksumoffloading: u8, pub disablechecksumoffloading: u8,
pub disablesegmentationoffloading: u8, pub disablesegmentationoffloading: u8,
@@ -235,16 +235,16 @@ pub struct System {
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct Ssh { pub struct Ssh {
pub group: String, pub group: String,
pub noauto: u8, pub noauto: Option<u8>,
pub interfaces: MaybeString, pub interfaces: Option<MaybeString>,
pub kex: MaybeString, pub kex: Option<MaybeString>,
pub ciphers: MaybeString, pub ciphers: Option<MaybeString>,
pub macs: MaybeString, pub macs: Option<MaybeString>,
pub keys: MaybeString, pub keys: Option<MaybeString>,
pub enabled: String, pub enabled: Option<String>,
pub passwordauth: u8, pub passwordauth: Option<u8>,
pub keysig: MaybeString, pub keysig: Option<MaybeString>,
pub permitrootlogin: u8, pub permitrootlogin: Option<u8>,
pub rekeylimit: Option<MaybeString>, pub rekeylimit: Option<MaybeString>,
} }
@@ -308,11 +308,11 @@ pub struct WebGui {
pub protocol: String, pub protocol: String,
#[yaserde(rename = "ssl-certref")] #[yaserde(rename = "ssl-certref")]
pub ssl_certref: String, pub ssl_certref: String,
pub port: MaybeString, pub port: Option<MaybeString>,
#[yaserde(rename = "ssl-ciphers")] #[yaserde(rename = "ssl-ciphers")]
pub ssl_ciphers: MaybeString, pub ssl_ciphers: Option<MaybeString>,
pub interfaces: MaybeString, pub interfaces: Option<MaybeString>,
pub compression: MaybeString, pub compression: Option<MaybeString>,
pub nohttpreferercheck: Option<u8>, pub nohttpreferercheck: Option<u8>,
} }
@@ -436,7 +436,7 @@ pub struct OPNsenseXmlSection {
#[yaserde(rename = "Interfaces")] #[yaserde(rename = "Interfaces")]
pub interfaces: Option<ConfigInterfaces>, pub interfaces: Option<ConfigInterfaces>,
#[yaserde(rename = "NodeExporter")] #[yaserde(rename = "NodeExporter")]
pub node_exporter: Option<RawXml>, pub node_exporter: Option<NodeExporter>,
#[yaserde(rename = "Kea")] #[yaserde(rename = "Kea")]
pub kea: Option<RawXml>, pub kea: Option<RawXml>,
pub monit: Option<Monit>, pub monit: Option<Monit>,
@@ -1613,3 +1613,21 @@ pub struct Ifgroups {
#[yaserde(attribute = true)] #[yaserde(attribute = true)]
pub version: String, pub version: String,
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct NodeExporter {
pub enabled: u8,
pub listenaddress: Option<MaybeString>,
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,
}

View File

@@ -5,7 +5,8 @@ use crate::{
error::Error, error::Error,
modules::{ modules::{
caddy::CaddyConfig, dhcp_legacy::DhcpConfigLegacyISC, dns::UnboundDnsConfig, 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}; use log::{debug, info, trace, warn};
@@ -13,6 +14,7 @@ use opnsense_config_xml::OPNsense;
use russh::client; use russh::client;
use serde::Serialize; use serde::Serialize;
use sha2::Digest; use sha2::Digest;
use tokio::time::{sleep, Duration};
use super::{ConfigManager, OPNsenseShell}; use super::{ConfigManager, OPNsenseShell};
@@ -71,6 +73,10 @@ impl Config {
LoadBalancerConfig::new(&mut self.opnsense, self.shell.clone()) 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<String, Error> { pub async fn upload_files(&self, source: &str, destination: &str) -> Result<String, Error> {
self.shell.upload_folder(source, destination).await self.shell.upload_folder(source, destination).await
} }
@@ -150,7 +156,8 @@ impl Config {
async fn reload_config(&mut self) -> Result<(), Error> { async fn reload_config(&mut self) -> Result<(), Error> {
info!("Reloading opnsense live config"); 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(()) Ok(())
} }

View File

@@ -4,4 +4,5 @@ pub mod dhcp_legacy;
pub mod dns; pub mod dns;
pub mod dnsmasq; pub mod dnsmasq;
pub mod load_balancer; pub mod load_balancer;
pub mod node_exporter;
pub mod tftp; pub mod tftp;

View File

@@ -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<dyn OPNsenseShell>,
}
impl<'a> NodeExporterConfig<'a> {
pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc<dyn OPNsenseShell>) -> Self {
Self {
opnsense,
opnsense_shell,
}
}
pub fn get_full_config(&self) -> &Option<NodeExporter> {
&self.opnsense.opnsense.node_exporter
}
fn with_node_exporter<F, R>(&mut self, f: F) -> Result<R, &'static str>
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(())
}
}