Add NFS export permissions and network display, fix SMB service detection
All checks were successful
Build and Release / build-and-release (push) Successful in 1m13s
All checks were successful
Build and Release / build-and-release (push) Successful in 1m13s
Display NFS exports with ro/rw permissions and network ranges for better visibility into share configuration. Support both smbd and samba-smbd service names for SMB share detection across different distributions.
This commit is contained in:
parent
a34b095857
commit
dcd350ec2c
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.272"
|
version = "0.1.273"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -301,7 +301,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.272"
|
version = "0.1.273"
|
||||||
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.272"
|
version = "0.1.273"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.273"
|
version = "0.1.274"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -233,23 +233,22 @@ impl SystemdCollector {
|
|||||||
if service_name == "nfs-server" && status_info.active_state == "active" {
|
if service_name == "nfs-server" && status_info.active_state == "active" {
|
||||||
// Add NFS exports as sub-services
|
// Add NFS exports as sub-services
|
||||||
let exports = self.get_nfs_exports();
|
let exports = self.get_nfs_exports();
|
||||||
for (export_path, options) in exports {
|
for (export_path, info) in exports {
|
||||||
let metrics = Vec::new();
|
let display = if !info.is_empty() {
|
||||||
let display = if !options.is_empty() {
|
format!("{} {}", export_path, info)
|
||||||
format!("{} ({})", export_path, options)
|
|
||||||
} else {
|
} else {
|
||||||
export_path
|
export_path
|
||||||
};
|
};
|
||||||
sub_services.push(SubServiceData {
|
sub_services.push(SubServiceData {
|
||||||
name: display,
|
name: display,
|
||||||
service_status: Status::Info,
|
service_status: Status::Info,
|
||||||
metrics,
|
metrics: Vec::new(),
|
||||||
service_type: "nfs_export".to_string(),
|
service_type: "nfs_export".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if service_name == "smbd" && status_info.active_state == "active" {
|
if (service_name == "smbd" || service_name == "samba-smbd") && status_info.active_state == "active" {
|
||||||
// Add SMB shares as sub-services
|
// Add SMB shares as sub-services
|
||||||
let shares = self.get_smb_shares();
|
let shares = self.get_smb_shares();
|
||||||
for (share_name, share_path) in shares {
|
for (share_name, share_path) in shares {
|
||||||
@ -1045,79 +1044,71 @@ impl SystemdCollector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get NFS exports from exportfs
|
/// Get NFS exports from exportfs
|
||||||
/// Returns a list of (export_path, options) tuples
|
/// Returns a list of (export_path, info_string) tuples
|
||||||
fn get_nfs_exports(&self) -> Vec<(String, String)> {
|
fn get_nfs_exports(&self) -> Vec<(String, String)> {
|
||||||
match Command::new("timeout")
|
let output = match Command::new("timeout")
|
||||||
.args(["2", "exportfs", "-v"])
|
.args(["2", "exportfs", "-v"])
|
||||||
.output()
|
.output()
|
||||||
{
|
{
|
||||||
Ok(output) if output.status.success() => {
|
Ok(output) if output.status.success() => output,
|
||||||
let exports_output = String::from_utf8_lossy(&output.stdout);
|
_ => return Vec::new(),
|
||||||
let mut exports_map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
};
|
||||||
|
|
||||||
for line in exports_output.lines() {
|
let exports_output = String::from_utf8_lossy(&output.stdout);
|
||||||
let line = line.trim();
|
let mut exports_map: std::collections::HashMap<String, Vec<(String, String)>> =
|
||||||
if line.is_empty() || line.starts_with('#') {
|
std::collections::HashMap::new();
|
||||||
continue;
|
let mut current_path: Option<String> = None;
|
||||||
}
|
|
||||||
|
|
||||||
// Format: "/path/to/export hostname(options)" or "/path/to/export 192.168.1.0/24(options)"
|
for line in exports_output.lines() {
|
||||||
// Split only at first whitespace to get path and rest
|
let trimmed = line.trim();
|
||||||
let parts: Vec<&str> = line.splitn(2, char::is_whitespace).collect();
|
|
||||||
if parts.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let export_path = parts[0].trim().to_string();
|
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Find options in parentheses from the entire rest of the line
|
if trimmed.starts_with('/') {
|
||||||
let options = if parts.len() > 1 {
|
// New export path line
|
||||||
let rest = parts[1].trim();
|
current_path = trimmed.split_whitespace().next().map(|s| s.to_string());
|
||||||
// Find all text in parentheses (there might be spaces before the parentheses)
|
} else if let Some(ref path) = current_path {
|
||||||
if let Some(start) = rest.find('(') {
|
// Continuation line with network and options
|
||||||
if let Some(end) = rest.find(')') {
|
// Format: "192.168.0.0/16(options...)"
|
||||||
// Extract key options only: rw/ro, sync/async
|
if let Some(paren_pos) = trimmed.find('(') {
|
||||||
let opts = rest[start+1..end].to_string();
|
let network = trimmed[..paren_pos].trim();
|
||||||
// Simplify options to just show key ones
|
|
||||||
let mut key_opts = Vec::new();
|
// Extract ro/rw from options
|
||||||
if opts.contains("rw") {
|
if let Some(end_paren) = trimmed.find(')') {
|
||||||
key_opts.push("rw");
|
let options = &trimmed[paren_pos+1..end_paren];
|
||||||
} else if opts.contains("ro") {
|
let mode = if options.contains(",rw,") || options.ends_with(",rw") {
|
||||||
key_opts.push("ro");
|
"rw"
|
||||||
}
|
|
||||||
if opts.contains("sync") {
|
|
||||||
key_opts.push("sync");
|
|
||||||
} else if opts.contains("async") {
|
|
||||||
key_opts.push("async");
|
|
||||||
}
|
|
||||||
if !key_opts.is_empty() {
|
|
||||||
key_opts.join(",")
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
"ro"
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only store the first occurrence of each path (deduplicates multiple clients)
|
exports_map.entry(path.clone())
|
||||||
exports_map.entry(export_path).or_insert(options);
|
.or_insert_with(Vec::new)
|
||||||
|
.push((network.to_string(), mode.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build display strings: "path: mode [networks]"
|
||||||
|
let mut exports: Vec<(String, String)> = exports_map
|
||||||
|
.into_iter()
|
||||||
|
.map(|(path, mut entries)| {
|
||||||
|
if entries.is_empty() {
|
||||||
|
return (path, String::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert HashMap to Vec
|
let mode = entries[0].1.clone();
|
||||||
let mut exports: Vec<(String, String)> = exports_map.into_iter().collect();
|
let networks: Vec<String> = entries.drain(..).map(|(n, _)| n).collect();
|
||||||
// Sort by path for consistent display
|
let info = format!("{} [{}]", mode, networks.join(", "));
|
||||||
exports.sort_by(|a, b| a.0.cmp(&b.0));
|
(path, info)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
exports
|
exports.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
}
|
exports
|
||||||
_ => Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get SMB shares from smb.conf
|
/// Get SMB shares from smb.conf
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.273"
|
version = "0.1.274"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.273"
|
version = "0.1.274"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user