Discover applications on the cluster and the host for backups
Kubernetes workloads are grouped by their Helm instance label into applications, each offering what is worth backing up: data volumes, a PostgreSQL dump instead of the database's own volume, and optionally the namespace manifests. Caches are listed but not preselected. Host applications come from running systemd services that declare a state or working directory. Selecting components creates one strategy each. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@ -2,7 +2,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use domain::host::{OsInfo, Package, PackageSource};
|
||||
use domain::host::{HostService, OsInfo, Package, PackageSource};
|
||||
use domain::ports::HostInspector;
|
||||
use domain::DomainError;
|
||||
|
||||
@ -39,6 +39,50 @@ impl HostInspector for DebianInspector {
|
||||
})
|
||||
}
|
||||
|
||||
async fn services(&self) -> Result<Vec<HostService>, DomainError> {
|
||||
let out = self
|
||||
.runner
|
||||
.run(
|
||||
"systemctl",
|
||||
&[
|
||||
"list-units",
|
||||
"--type=service",
|
||||
"--state=running",
|
||||
"--no-legend",
|
||||
"--no-pager",
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
if !out.success {
|
||||
return Err(DomainError::Unavailable("systemctl failed".into()));
|
||||
}
|
||||
let mut services = Vec::new();
|
||||
for (unit, description) in parse_service_units(&out.stdout) {
|
||||
let props = self
|
||||
.runner
|
||||
.run(
|
||||
"systemctl",
|
||||
&[
|
||||
"show",
|
||||
&unit,
|
||||
"-p",
|
||||
"WorkingDirectory",
|
||||
"-p",
|
||||
"StateDirectory",
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
let (working_dir, state_dir) = parse_service_properties(&props.stdout);
|
||||
services.push(HostService {
|
||||
unit,
|
||||
description,
|
||||
working_dir,
|
||||
state_dir,
|
||||
});
|
||||
}
|
||||
Ok(services)
|
||||
}
|
||||
|
||||
async fn packages(&self) -> Result<Vec<Package>, DomainError> {
|
||||
let r = &self.runner;
|
||||
let dpkg = r
|
||||
@ -139,6 +183,34 @@ pub fn parse_snap_list(out: &str) -> Vec<(String, String)> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `systemctl list-units --type=service --state=running --no-legend` → (unit, description).
|
||||
pub fn parse_service_units(out: &str) -> Vec<(String, String)> {
|
||||
out.lines()
|
||||
.filter_map(|l| {
|
||||
let mut parts = l.split_whitespace();
|
||||
let unit = parts.next()?.to_string();
|
||||
if !unit.ends_with(".service") {
|
||||
return None;
|
||||
}
|
||||
// loaded / active / running, then the description
|
||||
let description = parts.skip(3).collect::<Vec<_>>().join(" ");
|
||||
Some((unit, description))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `systemctl show -p WorkingDirectory -p StateDirectory` → (working dir, state dir).
|
||||
pub fn parse_service_properties(out: &str) -> (Option<String>, Option<String>) {
|
||||
let value = |key: &str| {
|
||||
out.lines()
|
||||
.find_map(|l| l.strip_prefix(&format!("{key}=")))
|
||||
.map(str::trim)
|
||||
.filter(|v| !v.is_empty())
|
||||
.map(String::from)
|
||||
};
|
||||
(value("WorkingDirectory"), value("StateDirectory"))
|
||||
}
|
||||
|
||||
/// `/etc/os-release` → (PRETTY_NAME, VERSION_ID).
|
||||
pub fn parse_os_release(content: &str) -> (String, String) {
|
||||
let get = |key: &str| {
|
||||
@ -194,6 +266,36 @@ Conf openssl (3.0.16-1~deb12u1 Debian-Security:12/stable-security [amd64])\n";
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_running_services_and_their_directories() {
|
||||
// `systemctl list-units --type=service --state=running --no-legend`
|
||||
let units = " monitoring.service loaded active running SoftVisor Infrastructure Monitoring\n ssh.service loaded active running OpenBSD Secure Shell server\n";
|
||||
assert_eq!(
|
||||
parse_service_units(units),
|
||||
vec![
|
||||
(
|
||||
"monitoring.service".to_string(),
|
||||
"SoftVisor Infrastructure Monitoring".to_string()
|
||||
),
|
||||
(
|
||||
"ssh.service".to_string(),
|
||||
"OpenBSD Secure Shell server".to_string()
|
||||
)
|
||||
]
|
||||
);
|
||||
// `systemctl show -p WorkingDirectory -p StateDirectory <unit>`
|
||||
let props = "WorkingDirectory=/opt/monitoring\nStateDirectory=\n";
|
||||
assert_eq!(
|
||||
parse_service_properties(props),
|
||||
(Some("/opt/monitoring".to_string()), None)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_service_properties("WorkingDirectory=\nStateDirectory=tailscale\n"),
|
||||
(None, Some("tailscale".to_string()))
|
||||
);
|
||||
assert_eq!(parse_service_properties(""), (None, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn os_release_strips_quotes() {
|
||||
let c = "PRETTY_NAME=\"Debian GNU/Linux 12 (bookworm)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"12\"\n";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
//! Sample data for development machines without apt (FAKE_HOST=true).
|
||||
use async_trait::async_trait;
|
||||
use domain::host::{OsInfo, Package, PackageSource};
|
||||
use domain::host::{HostService, OsInfo, Package, PackageSource};
|
||||
use domain::ports::HostInspector;
|
||||
use domain::DomainError;
|
||||
|
||||
@ -19,6 +19,23 @@ impl HostInspector for FakeHostInspector {
|
||||
})
|
||||
}
|
||||
|
||||
async fn services(&self) -> Result<Vec<HostService>, DomainError> {
|
||||
Ok(vec![
|
||||
HostService {
|
||||
unit: "monitoring.service".into(),
|
||||
description: "SoftVisor Infrastructure Monitoring".into(),
|
||||
working_dir: Some("/opt/monitoring".into()),
|
||||
state_dir: None,
|
||||
},
|
||||
HostService {
|
||||
unit: "ssh.service".into(),
|
||||
description: "OpenBSD Secure Shell server".into(),
|
||||
working_dir: None,
|
||||
state_dir: None,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
async fn packages(&self) -> Result<Vec<Package>, DomainError> {
|
||||
let apt = |n: &str, i: &str, c: Option<&str>, s: bool| Package {
|
||||
name: n.into(),
|
||||
|
||||
Reference in New Issue
Block a user