762bf95dfd
Nuevo crate `crates/init/arje-absorb`: lee la configuración de un init clásico y la traduce a una Tarjeta Semilla (Card JSON) con cada servicio como hija genesis de arje-zero. El paso «absorber» de la migración a arje — para no perder los servicios al cambiar de init. - Absorbers: sysvinit (/etc/inittab), runit (runsvdir o /etc/sv), dinit (/etc/dinit.d), openrc (/etc/runlevels). Autodetección. - Modelo intermedio ForeignService → Card vía brahman-card (validado). - `--with-carmen`: agrega carmen-dm (gestor de login gráfico). - CLI: --from/--root/--output/--label/--with-carmen. 24 tests, clippy limpio. `scripts/migrate-to-arje.sh`: orquesta absorber → validar → (carmen: compila+instala mirada dinámico) → install-arje-as-init.sh. El init viejo queda intacto; arje se elige en GRUB. --dry-run no toca nada. systemd no se absorbe (units no son texto trivial) — para systemd sigue la capa de shims + seeds/arje-host.card.json. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
87 lines
2.9 KiB
Rust
87 lines
2.9 KiB
Rust
//! Absorbe **OpenRC**: descubre los servicios habilitados por runlevel.
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
use crate::model::{ForeignService, ServiceKind};
|
|
|
|
/// Descubre los servicios OpenRC habilitados en `<root>`.
|
|
///
|
|
/// OpenRC habilita un servicio con un symlink en `/etc/runlevels/<rl>/`.
|
|
/// Los scripts de `/etc/init.d/` son shell completo —no se parsean—; se
|
|
/// absorben como tarea one-shot `/etc/init.d/<svc> start`, que arranca
|
|
/// el daemon (OpenRC mismo lo lleva a segundo plano). Recorremos los
|
|
/// runlevels `sysinit` → `boot` → `default` y deduplicamos.
|
|
pub fn absorb(root: &Path) -> anyhow::Result<Vec<ForeignService>> {
|
|
let runlevels = ["sysinit", "boot", "default"];
|
|
let mut found_any = false;
|
|
let mut seen: BTreeSet<String> = BTreeSet::new();
|
|
let mut out = Vec::new();
|
|
for rl in runlevels {
|
|
let dir = root.join("etc/runlevels").join(rl);
|
|
if !dir.is_dir() {
|
|
continue;
|
|
}
|
|
found_any = true;
|
|
let mut names: Vec<String> = fs::read_dir(&dir)?
|
|
.flatten()
|
|
.map(|e| e.file_name().to_string_lossy().into_owned())
|
|
.filter(|n| !n.starts_with('.'))
|
|
.collect();
|
|
names.sort();
|
|
for svc in names {
|
|
if !seen.insert(svc.clone()) {
|
|
continue; // ya absorbido en un runlevel anterior
|
|
}
|
|
out.push(ForeignService {
|
|
name: format!("openrc-{svc}"),
|
|
exec: format!("/etc/init.d/{svc}"),
|
|
argv: vec!["start".to_string()],
|
|
env: Vec::new(),
|
|
kind: ServiceKind::OneShot,
|
|
});
|
|
}
|
|
}
|
|
anyhow::ensure!(
|
|
found_any,
|
|
"no encontré /etc/runlevels/* en {}",
|
|
root.display()
|
|
);
|
|
Ok(out)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn absorbs_enabled_services_deduped() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
for (rl, svcs) in [
|
|
("boot", ["bootmisc", "hostname"].as_slice()),
|
|
("default", ["sshd", "hostname"].as_slice()),
|
|
] {
|
|
let d = tmp.path().join("etc/runlevels").join(rl);
|
|
fs::create_dir_all(&d).unwrap();
|
|
for s in svcs {
|
|
// Un archivo simple basta: sólo leemos los nombres.
|
|
fs::write(d.join(s), "").unwrap();
|
|
}
|
|
}
|
|
let out = absorb(tmp.path()).unwrap();
|
|
// bootmisc, hostname, sshd — `hostname` deduplicado entre runlevels.
|
|
assert_eq!(out.len(), 3);
|
|
let sshd = out.iter().find(|s| s.name == "openrc-sshd").unwrap();
|
|
assert_eq!(sshd.exec, "/etc/init.d/sshd");
|
|
assert_eq!(sshd.argv, ["start"]);
|
|
assert_eq!(sshd.kind, ServiceKind::OneShot);
|
|
}
|
|
|
|
#[test]
|
|
fn errors_without_runlevels() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
assert!(absorb(tmp.path()).is_err());
|
|
}
|
|
}
|