git init
This commit is contained in:
105
agent/src/services.rs
Normal file
105
agent/src/services.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user