106 lines
2.9 KiB
Rust
106 lines
2.9 KiB
Rust
use crate::{command, config::valid_service_name};
|
|
use anyhow::{bail, Result};
|
|
use serde::Serialize;
|
|
use std::{collections::HashMap, time::Duration};
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ServiceStatus {
|
|
pub name: String,
|
|
pub active_state: String,
|
|
pub sub_state: String,
|
|
pub pid: u32,
|
|
pub memory_bytes: Option<u64>,
|
|
pub active_since_monotonic_us: Option<u64>,
|
|
}
|
|
|
|
pub async fn list(allowed: &[String], timeout: Duration) -> Vec<ServiceStatus> {
|
|
let mut result = Vec::with_capacity(allowed.len());
|
|
for name in allowed {
|
|
match inspect(name, timeout).await {
|
|
Ok(status) => result.push(status),
|
|
Err(_) => result.push(ServiceStatus {
|
|
name: name.clone(),
|
|
active_state: "unavailable".into(),
|
|
sub_state: "unknown".into(),
|
|
pid: 0,
|
|
memory_bytes: None,
|
|
active_since_monotonic_us: None,
|
|
}),
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
pub async fn restart(name: &str, allowed: &[String], timeout: Duration) -> Result<()> {
|
|
if !valid_service_name(name) || !allowed.iter().any(|item| item == name) {
|
|
bail!("service is not allow-listed")
|
|
}
|
|
command::run("systemctl", &["restart", "--", name], timeout).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn inspect(name: &str, timeout: Duration) -> Result<ServiceStatus> {
|
|
if !valid_service_name(name) {
|
|
bail!("invalid service name")
|
|
}
|
|
let output = command::run(
|
|
"systemctl",
|
|
&[
|
|
"show",
|
|
"--no-pager",
|
|
"--property=ActiveState,SubState,MainPID,MemoryCurrent,ActiveEnterTimestampMonotonic",
|
|
"--",
|
|
name,
|
|
],
|
|
timeout,
|
|
)
|
|
.await?;
|
|
let values: HashMap<_, _> = output
|
|
.stdout
|
|
.lines()
|
|
.filter_map(|line| line.split_once('='))
|
|
.collect();
|
|
Ok(ServiceStatus {
|
|
name: name.to_owned(),
|
|
active_state: values
|
|
.get("ActiveState")
|
|
.copied()
|
|
.unwrap_or("unknown")
|
|
.to_owned(),
|
|
sub_state: values
|
|
.get("SubState")
|
|
.copied()
|
|
.unwrap_or("unknown")
|
|
.to_owned(),
|
|
pid: values
|
|
.get("MainPID")
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or_default(),
|
|
memory_bytes: values.get("MemoryCurrent").and_then(|v| v.parse().ok()),
|
|
active_since_monotonic_us: values
|
|
.get("ActiveEnterTimestampMonotonic")
|
|
.and_then(|v| v.parse().ok()),
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn rejects_service_outside_allowlist_without_execution() {
|
|
assert!(
|
|
restart("postgresql", &["nginx".into()], Duration::from_secs(1))
|
|
.await
|
|
.is_err()
|
|
);
|
|
assert!(restart(
|
|
"nginx;reboot",
|
|
&["nginx;reboot".into()],
|
|
Duration::from_secs(1)
|
|
)
|
|
.await
|
|
.is_err());
|
|
}
|
|
}
|