//! systemd control abstraction for the reconciler.
//!
//! The production implementation shells out to `systemctl` rather than talking
//! to the D-Bus API directly. That is deliberate: podman quadlets are expanded
//! into transient units by a systemd *generator* at `daemon-reload` time, and
//! `systemctl` is the interface that reliably sees generated units, resolves
//! aliases, and reports `is-enabled` states like `generated`/`static`. Shelling
//! out also keeps the dependency surface tiny.
use std::io;
use std::process::Command;
/// Side-effecting systemd operations, injectable for testing.
pub trait SystemdController {
/// `systemctl daemon-reload` — re-run generators, pick up new/changed units.
fn daemon_reload(&mut self) -> io::Result<()>;
/// Whether the unit is currently active (running).
fn is_active(&self, unit: &str) -> io::Result<bool>;
/// The raw `systemctl is-active` state string (`active`, `inactive`,
/// `failed`, `activating`, …) — richer than [`Self::is_active`], used for
/// traffic-light health reporting. Default derives from `is_active` so
/// test fakes need not implement it.
fn active_state(&self, unit: &str) -> io::Result<String> {
Ok(if self.is_active(unit)? {
"active".to_string()
} else {
"inactive".to_string()
})
}
/// Whether the unit is enabled (starts on boot). Generated/static units,
/// which cannot be toggled, are reported as enabled so the reconciler
/// leaves them alone.
fn is_enabled(&self, unit: &str) -> io::Result<bool>;
fn start(&mut self, unit: &str) -> io::Result<()>;
fn stop(&mut self, unit: &str) -> io::Result<()>;
fn restart(&mut self, unit: &str) -> io::Result<()>;
fn reload(&mut self, unit: &str) -> io::Result<()>;
fn enable(&mut self, unit: &str) -> io::Result<()>;
fn disable(&mut self, unit: &str) -> io::Result<()>;
}
/// systemd management scope.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Scope {
/// System manager (`systemctl ...`). Requires root for mutations.
#[default]
System,
/// Per-user manager (`systemctl --user ...`).
User,
}
/// Production [`SystemdController`] that invokes the `systemctl` binary.
#[derive(Debug, Clone, Copy, Default)]
pub struct SystemctlController {
scope: Scope,
}
impl SystemctlController {
pub fn new(scope: Scope) -> Self {
Self { scope }
}
fn command(&self) -> Command {
let mut cmd = Command::new("systemctl");
if self.scope == Scope::User {
cmd.arg("--user");
}
cmd
}
/// Run a mutating verb and map a non-zero exit into an error carrying stderr.
fn run(&self, verb: &str, unit: Option<&str>) -> io::Result<()> {
let mut cmd = self.command();
cmd.arg(verb);
if let Some(u) = unit {
cmd.arg(u);
}
let out = cmd.output()?;
if out.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&out.stderr);
Err(io::Error::other(format!(
"systemctl {verb}{} exited with {}: {}",
unit.map(|u| format!(" {u}")).unwrap_or_default(),
out.status,
stderr.trim()
)))
}
}
/// Run a query verb, returning trimmed stdout regardless of exit status
/// (query verbs use exit code to signal state, not failure).
fn query(&self, verb: &str, unit: &str) -> io::Result<String> {
let out = self.command().arg(verb).arg(unit).output()?;
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
}
impl SystemdController for SystemctlController {
fn daemon_reload(&mut self) -> io::Result<()> {
self.run("daemon-reload", None)
}
fn is_active(&self, unit: &str) -> io::Result<bool> {
// `is-active` prints "active" (exit 0) or "inactive"/"failed"/... (exit
// non-zero). We treat only "active" as active; "activating" and
// "failed" are not the steady state we want, so drift triggers action.
Ok(self.query("is-active", unit)? == "active")
}
fn active_state(&self, unit: &str) -> io::Result<String> {
self.query("is-active", unit)
}
fn is_enabled(&self, unit: &str) -> io::Result<bool> {
let state = self.query("is-enabled", unit)?;
Ok(matches!(
state.as_str(),
"enabled"
| "enabled-runtime"
| "alias"
// Not toggleable via enable/disable — report as enabled so the
// reconciler doesn't fight them.
| "static"
| "indirect"
| "generated"
| "transient"
))
}
fn start(&mut self, unit: &str) -> io::Result<()> {
self.run("start", Some(unit))
}
fn stop(&mut self, unit: &str) -> io::Result<()> {
self.run("stop", Some(unit))
}
fn restart(&mut self, unit: &str) -> io::Result<()> {
self.run("restart", Some(unit))
}
fn reload(&mut self, unit: &str) -> io::Result<()> {
self.run("reload", Some(unit))
}
fn enable(&mut self, unit: &str) -> io::Result<()> {
self.run("enable", Some(unit))
}
fn disable(&mut self, unit: &str) -> io::Result<()> {
self.run("disable", Some(unit))
}
}
neiam
/devendra
public · Issues · Pulls · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
5.3 KiB · text
History
e832e77