Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7362464b46 | |||
| c8b79576fa | |||
| f53df5440b | |||
| d1b0e2c431 |
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.268"
|
version = "0.1.271"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -301,7 +301,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.268"
|
version = "0.1.271"
|
||||||
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.268"
|
version = "0.1.271"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.268"
|
version = "0.1.272"
|
||||||
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,117 @@ 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_map: std::collections::HashMap<String, String> = std::collections::HashMap::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)" or "/path/to/export 192.168.1.0/24(options)"
|
||||||
|
// exportfs -v shows each export once per client/network
|
||||||
|
// We want to deduplicate by path and just show one entry
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if parts.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let export_path = parts[0].to_string();
|
||||||
|
|
||||||
|
// Extract options from parentheses (from the client specification)
|
||||||
|
let options = if parts.len() > 1 {
|
||||||
|
let client_spec = parts[1];
|
||||||
|
if let Some(start) = client_spec.find('(') {
|
||||||
|
if let Some(end) = client_spec.find(')') {
|
||||||
|
client_spec[start+1..end].to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only store the first occurrence of each path (deduplicates multiple clients)
|
||||||
|
exports_map.entry(export_path).or_insert(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert HashMap to Vec
|
||||||
|
let mut exports: Vec<(String, String)> = exports_map.into_iter().collect();
|
||||||
|
// Sort by path for consistent display
|
||||||
|
exports.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
|
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.268"
|
version = "0.1.272"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -544,9 +544,8 @@ impl SystemWidget {
|
|||||||
"unknown".to_string()
|
"unknown".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Header: "Repo <complete timestamp>"
|
// Header: just the timestamp
|
||||||
let repo_text = format!("Repo {}", time_display);
|
let repo_spans = StatusIcons::create_status_spans(self.backup_status, &time_display);
|
||||||
let repo_spans = StatusIcons::create_status_spans(self.backup_status, &repo_text);
|
|
||||||
lines.push(Line::from(repo_spans));
|
lines.push(Line::from(repo_spans));
|
||||||
|
|
||||||
// List all repositories with archive count and size
|
// List all repositories with archive count and size
|
||||||
@@ -554,8 +553,10 @@ impl SystemWidget {
|
|||||||
for (idx, repo) in self.backup_repositories.iter().enumerate() {
|
for (idx, repo) in self.backup_repositories.iter().enumerate() {
|
||||||
let tree_char = if idx == repo_count - 1 { "└─" } else { "├─" };
|
let tree_char = if idx == repo_count - 1 { "└─" } else { "├─" };
|
||||||
|
|
||||||
// Format size: use MB for < 1GB, otherwise GB
|
// Format size: use kB for < 1MB, MB for < 1GB, otherwise GB
|
||||||
let size_display = if repo.repo_size_gb < 1.0 {
|
let size_display = if repo.repo_size_gb < 0.001 {
|
||||||
|
format!("{:.0}kB", repo.repo_size_gb * 1024.0 * 1024.0)
|
||||||
|
} else if repo.repo_size_gb < 1.0 {
|
||||||
format!("{:.0}MB", repo.repo_size_gb * 1024.0)
|
format!("{:.0}MB", repo.repo_size_gb * 1024.0)
|
||||||
} else {
|
} else {
|
||||||
format!("{:.1}GB", repo.repo_size_gb)
|
format!("{:.1}GB", repo.repo_size_gb)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.268"
|
version = "0.1.272"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
Reference in New Issue
Block a user