use async_trait::async_trait; use cm_dashboard_shared::{Metric, MetricValue, Status, StatusTracker}; use std::process::Command; use tracing::debug; use super::{Collector, CollectorError}; use crate::config::NixOSConfig; /// NixOS system information collector /// /// Collects NixOS-specific system information including: /// - NixOS version and build information /// - Currently active/logged in users pub struct NixOSCollector { } impl NixOSCollector { pub fn new(_config: NixOSConfig) -> Self { Self {} } /// Get NixOS build information fn get_nixos_build_info(&self) -> Result> { // Get nixos-version output directly let output = Command::new("nixos-version").output()?; if !output.status.success() { return Err("nixos-version command failed".into()); } let version_line = String::from_utf8_lossy(&output.stdout); let version = version_line.trim(); if version.is_empty() { return Err("Empty nixos-version output".into()); } // Remove codename part (e.g., "(Warbler)") let clean_version = if let Some(pos) = version.find(" (") { version[..pos].to_string() } else { version.to_string() }; Ok(clean_version) } /// Get agent hash from binary path fn get_agent_hash(&self) -> Result> { // Get the path of the current executable let exe_path = std::env::current_exe()?; let exe_str = exe_path.to_string_lossy(); // Extract Nix store hash from path like /nix/store/fn804fh332mp8gz06qawminpj20xl25h-cm-dashboard-0.1.0/bin/cm-dashboard-agent if let Some(store_path) = exe_str.strip_prefix("/nix/store/") { if let Some(dash_pos) = store_path.find('-') { return Ok(store_path[..dash_pos].to_string()); } } // Fallback to "unknown" if not in Nix store Ok("unknown".to_string()) } /// Get configuration hash from deployed nix store system fn get_config_hash(&self) -> Result> { // Read the symlink target of /run/current-system to get nix store path let output = Command::new("readlink") .arg("/run/current-system") .output()?; if !output.status.success() { return Err("readlink command failed".into()); } let binding = String::from_utf8_lossy(&output.stdout); let store_path = binding.trim(); // Extract hash from nix store path // Format: /nix/store/HASH-nixos-system-HOSTNAME-VERSION if let Some(hash_part) = store_path.strip_prefix("/nix/store/") { if let Some(hash) = hash_part.split('-').next() { if hash.len() >= 8 { // Return first 8 characters of nix store hash return Ok(hash[..8].to_string()); } } } Err("Could not extract hash from nix store path".into()) } /// Get currently active users fn get_active_users(&self) -> Result, Box> { let output = Command::new("who").output()?; if !output.status.success() { return Err("who command failed".into()); } let who_output = String::from_utf8_lossy(&output.stdout); let mut users = std::collections::HashSet::new(); for line in who_output.lines() { if let Some(username) = line.split_whitespace().next() { if !username.is_empty() { users.insert(username.to_string()); } } } Ok(users.into_iter().collect()) } } #[async_trait] impl Collector for NixOSCollector { async fn collect(&self, _status_tracker: &mut StatusTracker) -> Result, CollectorError> { debug!("Collecting NixOS system information"); let mut metrics = Vec::new(); let timestamp = chrono::Utc::now().timestamp() as u64; // Collect NixOS build information match self.get_nixos_build_info() { Ok(build_info) => { metrics.push(Metric { name: "system_nixos_build".to_string(), value: MetricValue::String(build_info), unit: None, description: Some("NixOS build information".to_string()), status: Status::Ok, timestamp, }); } Err(e) => { debug!("Failed to get NixOS build info: {}", e); metrics.push(Metric { name: "system_nixos_build".to_string(), value: MetricValue::String("unknown".to_string()), unit: None, description: Some("NixOS build (failed to detect)".to_string()), status: Status::Unknown, timestamp, }); } } // Collect active users match self.get_active_users() { Ok(users) => { let users_str = users.join(", "); metrics.push(Metric { name: "system_active_users".to_string(), value: MetricValue::String(users_str), unit: None, description: Some("Currently active users".to_string()), status: Status::Ok, timestamp, }); } Err(e) => { debug!("Failed to get active users: {}", e); metrics.push(Metric { name: "system_active_users".to_string(), value: MetricValue::String("unknown".to_string()), unit: None, description: Some("Active users (failed to detect)".to_string()), status: Status::Unknown, timestamp, }); } } // Collect config hash match self.get_config_hash() { Ok(hash) => { metrics.push(Metric { name: "system_config_hash".to_string(), value: MetricValue::String(hash), unit: None, description: Some("NixOS deployed configuration hash".to_string()), status: Status::Ok, timestamp, }); } Err(e) => { debug!("Failed to get config hash: {}", e); metrics.push(Metric { name: "system_config_hash".to_string(), value: MetricValue::String("unknown".to_string()), unit: None, description: Some("Deployed config hash (failed to detect)".to_string()), status: Status::Unknown, timestamp, }); } } // Collect agent hash match self.get_agent_hash() { Ok(hash) => { metrics.push(Metric { name: "system_agent_hash".to_string(), value: MetricValue::String(hash), unit: None, description: Some("Agent Nix store hash".to_string()), status: Status::Ok, timestamp, }); } Err(e) => { debug!("Failed to get agent hash: {}", e); metrics.push(Metric { name: "system_agent_hash".to_string(), value: MetricValue::String("unknown".to_string()), unit: None, description: Some("Agent hash (failed to detect)".to_string()), status: Status::Unknown, timestamp, }); } } debug!("Collected {} NixOS metrics", metrics.len()); Ok(metrics) } }