Files
vps-control/agent/src/services.rs
DizelBadCoder c4dc19cd41
Some checks failed
CI / containers (push) Has been cancelled
CI / agent (push) Has been cancelled
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
git init
2026-09-19 19:31:36 +03:00

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());
}
}