feat(arje): arje-absorb — absorbe otros inits a una Semilla brahman
1//! Absorbe **OpenRC**: descubre los servicios habilitados por runlevel.
2
3use std::collections::BTreeSet;
4use std::fs;
5use std::path::Path;
6
7use crate::model::{ForeignService, ServiceKind};
8
9/// Descubre los servicios OpenRC habilitados en `<root>`.
10///
11/// OpenRC habilita un servicio con un symlink en `/etc/runlevels/<rl>/`.
12/// Los scripts de `/etc/init.d/` son shell completo —no se parsean—; se
13/// absorben como tarea one-shot `/etc/init.d/<svc> start`, que arranca
14/// el daemon (OpenRC mismo lo lleva a segundo plano). Recorremos los
15/// runlevels `sysinit` → `boot` → `default` y deduplicamos.
16pub fn absorb(root: &Path) -> anyhow::Result<Vec<ForeignService>> {
17 let runlevels = ["sysinit", "boot", "default"];
18 let mut found_any = false;
19 let mut seen: BTreeSet<String> = BTreeSet::new();
20 let mut out = Vec::new();
21 for rl in runlevels {
22 let dir = root.join("etc/runlevels").join(rl);
23 if !dir.is_dir() {
24 continue;
25 }
26 found_any = true;
27 let mut names: Vec<String> = fs::read_dir(&dir)?
28 .flatten()
29 .map(|e| e.file_name().to_string_lossy().into_owned())
30 .filter(|n| !n.starts_with('.'))
31 .collect();
32 names.sort();
33 for svc in names {
34 if !seen.insert(svc.clone()) {
35 continue; // ya absorbido en un runlevel anterior
36 }
37 out.push(ForeignService {
38 name: format!("openrc-{svc}"),
39 exec: format!("/etc/init.d/{svc}"),
40 argv: vec!["start".to_string()],
41 env: Vec::new(),
42 kind: ServiceKind::OneShot,
43 });
44 }
45 }
46 anyhow::ensure!(
47 found_any,
48 "no encontré /etc/runlevels/* en {}",
49 root.display()
50 );
51 Ok(out)
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 #[test]
59 fn absorbs_enabled_services_deduped() {
60 let tmp = tempfile::tempdir().unwrap();
61 for (rl, svcs) in [
62 ("boot", ["bootmisc", "hostname"].as_slice()),
63 ("default", ["sshd", "hostname"].as_slice()),
64 ] {
65 let d = tmp.path().join("etc/runlevels").join(rl);
66 fs::create_dir_all(&d).unwrap();
67 for s in svcs {
68 // Un archivo simple basta: sólo leemos los nombres.
69 fs::write(d.join(s), "").unwrap();
70 }
71 }
72 let out = absorb(tmp.path()).unwrap();
73 // bootmisc, hostname, sshd — `hostname` deduplicado entre runlevels.
74 assert_eq!(out.len(), 3);
75 let sshd = out.iter().find(|s| s.name == "openrc-sshd").unwrap();
76 assert_eq!(sshd.exec, "/etc/init.d/sshd");
77 assert_eq!(sshd.argv, ["start"]);
78 assert_eq!(sshd.kind, ServiceKind::OneShot);
79 }
80
81 #[test]
82 fn errors_without_runlevels() {
83 let tmp = tempfile::tempdir().unwrap();
84 assert!(absorb(tmp.path()).is_err());
85 }
86}