diff --git a/Cargo.lock b/Cargo.lock index c7240f9..76f7d97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.269" +version = "0.1.270" dependencies = [ "anyhow", "chrono", @@ -301,7 +301,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.269" +version = "0.1.270" dependencies = [ "anyhow", "async-trait", @@ -325,7 +325,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.269" +version = "0.1.270" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index dd892dc..2136920 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.270" +version = "0.1.271" edition = "2021" [dependencies] diff --git a/agent/src/collectors/disk.rs b/agent/src/collectors/disk.rs index a3c92df..5dc82f5 100644 --- a/agent/src/collectors/disk.rs +++ b/agent/src/collectors/disk.rs @@ -114,7 +114,7 @@ impl DiskCollector { let mut cmd = TokioCommand::new("lsblk"); cmd.args(&["-rn", "-o", "NAME,MOUNTPOINT"]); - let output = run_command_with_timeout(cmd, 2).await + let output = run_command_with_timeout(cmd, 10).await .map_err(|e| CollectorError::SystemRead { path: "block devices".to_string(), error: e.to_string(), @@ -184,7 +184,7 @@ impl DiskCollector { /// Get filesystem info for a single mount point fn get_filesystem_info(&self, mount_point: &str) -> Result<(u64, u64), CollectorError> { let output = StdCommand::new("timeout") - .args(&["2", "df", "--block-size=1", mount_point]) + .args(&["10", "df", "--block-size=1", mount_point]) .output() .map_err(|e| CollectorError::SystemRead { path: format!("df {}", mount_point), @@ -433,7 +433,7 @@ impl DiskCollector { cmd.args(&["-a", &format!("/dev/{}", drive_name)]); } - let output = run_command_with_timeout(cmd, 3).await + let output = run_command_with_timeout(cmd, 15).await .map_err(|e| CollectorError::SystemRead { path: format!("SMART data for {}", drive_name), error: e.to_string(), @@ -772,7 +772,7 @@ impl DiskCollector { fn get_drive_info_for_path(&self, path: &str) -> anyhow::Result { // Use lsblk to find the backing device with timeout let output = StdCommand::new("timeout") - .args(&["2", "lsblk", "-rn", "-o", "NAME,MOUNTPOINT"]) + .args(&["10", "lsblk", "-rn", "-o", "NAME,MOUNTPOINT"]) .output() .map_err(|e| anyhow::anyhow!("Failed to run lsblk: {}", e))?; diff --git a/agent/src/collectors/systemd.rs b/agent/src/collectors/systemd.rs index 78a4892..eba2d1e 100644 --- a/agent/src/collectors/systemd.rs +++ b/agent/src/collectors/systemd.rs @@ -230,6 +230,39 @@ impl SystemdCollector { } } + if service_name == "nfs-server" && status_info.active_state == "active" { + // Add NFS exports as sub-services + let exports = self.get_nfs_exports(); + for (export_path, options) in exports { + let metrics = Vec::new(); + let display = if !options.is_empty() { + format!("{} ({})", export_path, options) + } else { + export_path + }; + sub_services.push(SubServiceData { + name: display, + service_status: Status::Info, + metrics, + service_type: "nfs_export".to_string(), + }); + } + } + + if service_name == "smbd" && status_info.active_state == "active" { + // Add SMB shares as sub-services + let shares = self.get_smb_shares(); + for (share_name, share_path) in shares { + let metrics = Vec::new(); + sub_services.push(SubServiceData { + name: format!("{}: {}", share_name, share_path), + service_status: Status::Info, + metrics, + service_type: "smb_share".to_string(), + }); + } + } + // Create complete service data let service_data = ServiceData { name: service_name.clone(), @@ -1011,6 +1044,110 @@ impl SystemdCollector { } } + /// Get NFS exports from exportfs + /// Returns a list of (export_path, options) tuples + fn get_nfs_exports(&self) -> Vec<(String, String)> { + match Command::new("timeout") + .args(["2", "exportfs", "-v"]) + .output() + { + Ok(output) if output.status.success() => { + let exports_output = String::from_utf8_lossy(&output.stdout); + let mut exports = Vec::new(); + + for line in exports_output.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + + // Format: "/path/to/export hostname(options)" + // We want just the path and a summary of options + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.is_empty() { + continue; + } + + let export_path = parts[0].to_string(); + + // Extract options from parentheses (simplified) + let options = if parts.len() > 1 { + let opts_str = parts[1..].join(" "); + if let Some(start) = opts_str.find('(') { + if let Some(end) = opts_str.find(')') { + opts_str[start+1..end].to_string() + } else { + String::new() + } + } else { + String::new() + } + } else { + String::new() + }; + + exports.push((export_path, options)); + } + + exports + } + _ => Vec::new(), + } + } + + /// Get SMB shares from smb.conf + /// Returns a list of (share_name, share_path) tuples + fn get_smb_shares(&self) -> Vec<(String, String)> { + match std::fs::read_to_string("/etc/samba/smb.conf") { + Ok(config) => { + let mut shares = Vec::new(); + let mut current_share: Option = None; + let mut current_path: Option = None; + + for line in config.lines() { + let line = line.trim(); + + // Skip comments and empty lines + if line.is_empty() || line.starts_with('#') || line.starts_with(';') { + continue; + } + + // Detect share section [sharename] + if line.starts_with('[') && line.ends_with(']') { + // Save previous share if we have both name and path + if let (Some(name), Some(path)) = (current_share.take(), current_path.take()) { + // Skip special sections + if name != "global" && name != "homes" && name != "printers" { + shares.push((name, path)); + } + } + + // Start new share + let share_name = line[1..line.len()-1].trim().to_string(); + current_share = Some(share_name); + current_path = None; + } + // Look for path = /some/path + else if line.starts_with("path") && line.contains('=') { + if let Some(path_value) = line.split('=').nth(1) { + current_path = Some(path_value.trim().to_string()); + } + } + } + + // Don't forget the last share + if let (Some(name), Some(path)) = (current_share, current_path) { + if name != "global" && name != "homes" && name != "printers" { + shares.push((name, path)); + } + } + + shares + } + _ => Vec::new(), + } + } + /// Get nftables open ports grouped by protocol /// Returns: (tcp_ports_string, udp_ports_string) fn get_nftables_open_ports(&self) -> (String, String) { diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index 6da30f5..30fdd2e 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.270" +version = "0.1.271" edition = "2021" [dependencies] diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 4b8ea59..f69d2c4 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-shared" -version = "0.1.270" +version = "0.1.271" edition = "2021" [dependencies]