Add NFS/SMB share monitoring and increase disk timeouts
All checks were successful
Build and Release / build-and-release (push) Successful in 1m36s
All checks were successful
Build and Release / build-and-release (push) Successful in 1m36s
Add sub-service display for NFS exports and SMB shares under their respective services. NFS shows active exports from exportfs with options. SMB shows configured shares from smb.conf with paths. Increase disk operation timeouts to handle multiple drives: - lsblk: 2s → 10s - smartctl: 3s → 15s (critical for multi-drive systems) - df: 2s → 10s Prevents timeouts when querying SMART data from systems with multiple drives (3+ data drives plus parity).
This commit is contained in:
parent
f53df5440b
commit
c8b79576fa
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.269"
|
version = "0.1.270"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -301,7 +301,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.269"
|
version = "0.1.270"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -325,7 +325,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.269"
|
version = "0.1.270"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.270"
|
version = "0.1.271"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -114,7 +114,7 @@ impl DiskCollector {
|
|||||||
let mut cmd = TokioCommand::new("lsblk");
|
let mut cmd = TokioCommand::new("lsblk");
|
||||||
cmd.args(&["-rn", "-o", "NAME,MOUNTPOINT"]);
|
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 {
|
.map_err(|e| CollectorError::SystemRead {
|
||||||
path: "block devices".to_string(),
|
path: "block devices".to_string(),
|
||||||
error: e.to_string(),
|
error: e.to_string(),
|
||||||
@ -184,7 +184,7 @@ impl DiskCollector {
|
|||||||
/// Get filesystem info for a single mount point
|
/// Get filesystem info for a single mount point
|
||||||
fn get_filesystem_info(&self, mount_point: &str) -> Result<(u64, u64), CollectorError> {
|
fn get_filesystem_info(&self, mount_point: &str) -> Result<(u64, u64), CollectorError> {
|
||||||
let output = StdCommand::new("timeout")
|
let output = StdCommand::new("timeout")
|
||||||
.args(&["2", "df", "--block-size=1", mount_point])
|
.args(&["10", "df", "--block-size=1", mount_point])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| CollectorError::SystemRead {
|
.map_err(|e| CollectorError::SystemRead {
|
||||||
path: format!("df {}", mount_point),
|
path: format!("df {}", mount_point),
|
||||||
@ -433,7 +433,7 @@ impl DiskCollector {
|
|||||||
cmd.args(&["-a", &format!("/dev/{}", drive_name)]);
|
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 {
|
.map_err(|e| CollectorError::SystemRead {
|
||||||
path: format!("SMART data for {}", drive_name),
|
path: format!("SMART data for {}", drive_name),
|
||||||
error: e.to_string(),
|
error: e.to_string(),
|
||||||
@ -772,7 +772,7 @@ impl DiskCollector {
|
|||||||
fn get_drive_info_for_path(&self, path: &str) -> anyhow::Result<PoolDrive> {
|
fn get_drive_info_for_path(&self, path: &str) -> anyhow::Result<PoolDrive> {
|
||||||
// Use lsblk to find the backing device with timeout
|
// Use lsblk to find the backing device with timeout
|
||||||
let output = StdCommand::new("timeout")
|
let output = StdCommand::new("timeout")
|
||||||
.args(&["2", "lsblk", "-rn", "-o", "NAME,MOUNTPOINT"])
|
.args(&["10", "lsblk", "-rn", "-o", "NAME,MOUNTPOINT"])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to run lsblk: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to run lsblk: {}", e))?;
|
||||||
|
|
||||||
|
|||||||
@ -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
|
// Create complete service data
|
||||||
let service_data = ServiceData {
|
let service_data = ServiceData {
|
||||||
name: service_name.clone(),
|
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<String> = None;
|
||||||
|
let mut current_path: Option<String> = 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
|
/// Get nftables open ports grouped by protocol
|
||||||
/// Returns: (tcp_ports_string, udp_ports_string)
|
/// Returns: (tcp_ports_string, udp_ports_string)
|
||||||
fn get_nftables_open_ports(&self) -> (String, String) {
|
fn get_nftables_open_ports(&self) -> (String, String) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.270"
|
version = "0.1.271"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.270"
|
version = "0.1.271"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user