feat(arje): arje-absorb — absorbe otros inits a una Semilla brahman
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>
This commit is contained in:
Generated
+9
@@ -342,6 +342,15 @@ dependencies = [
|
|||||||
"password-hash",
|
"password-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arje-absorb"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"brahman-card",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arje-binfmt-compat"
|
name = "arje-binfmt-compat"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ members = [
|
|||||||
"crates/init/arje-soma",
|
"crates/init/arje-soma",
|
||||||
"crates/init/arje-snapshot",
|
"crates/init/arje-snapshot",
|
||||||
"crates/init/arje-incarnate",
|
"crates/init/arje-incarnate",
|
||||||
|
"crates/init/arje-absorb",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# runtime/ — Infraestructura de ejecución (bus + cas + wasm + brain)
|
# runtime/ — Infraestructura de ejecución (bus + cas + wasm + brain)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "arje-absorb"
|
||||||
|
version = "0.0.1"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "arje-absorb — lee la configuración de otro init (sysvinit/runit/dinit/openrc) y la traduce a una Tarjeta Semilla brahman."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "arje-absorb"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
brahman-card = { path = "../../protocol/brahman-card" }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = { workspace = true }
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# arje-absorb
|
||||||
|
|
||||||
|
Absorbe la configuración de **otro init** y la traduce a una **Tarjeta
|
||||||
|
Semilla** brahman: una `Card` JSON con cada servicio como hija `genesis`
|
||||||
|
de `arje-zero`.
|
||||||
|
|
||||||
|
Es el paso «absorber» de la migración a arje — para no perder los
|
||||||
|
servicios del sistema al cambiar de init. Lo usa `scripts/migrate-to-arje.sh`.
|
||||||
|
|
||||||
|
## Inits soportados
|
||||||
|
|
||||||
|
| init | qué lee | servicio → Card |
|
||||||
|
| ---------- | ----------------------------------------- | --------------- |
|
||||||
|
| `sysvinit` | `/etc/inittab` | `respawn` → daemon supervisado; `wait`/`once`/`sysinit` → one-shot |
|
||||||
|
| `runit` | el `runsvdir` activo (o `/etc/sv`) | cada script `run` → daemon supervisado (calce 1:1) |
|
||||||
|
| `dinit` | `/etc/dinit.d/*` | `type=process`/`bgprocess` → daemon; `scripted` → one-shot |
|
||||||
|
| `openrc` | `/etc/runlevels/{sysinit,boot,default}` | `/etc/init.d/<svc> start` → one-shot |
|
||||||
|
|
||||||
|
`systemd` no se absorbe — sus units no son un formato de texto trivial.
|
||||||
|
Para systemd ya existe la capa de shims (`crates/compat/`) y la seed
|
||||||
|
`seeds/arje-host.card.json`.
|
||||||
|
|
||||||
|
## Uso
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# autodetecta el init y emite la Semilla a stdout
|
||||||
|
arje-absorb
|
||||||
|
|
||||||
|
# explícito, a un archivo, agregando el gestor de login gráfico
|
||||||
|
arje-absorb --from openrc --output /ente/seed.card.json --with-carmen
|
||||||
|
|
||||||
|
# absorber otra raíz (un chroot, una imagen montada)
|
||||||
|
arje-absorb --root /mnt/sistema --from runit
|
||||||
|
```
|
||||||
|
|
||||||
|
Opciones: `--from <init>` (def. `auto`), `--root <dir>` (def. `/`),
|
||||||
|
`--output <f>` (def. `-` = stdout), `--label <s>`, `--with-carmen`.
|
||||||
|
|
||||||
|
## Lo que NO hace
|
||||||
|
|
||||||
|
La Semilla absorbida **conserva el comportamiento** del init viejo: los
|
||||||
|
servicios corren sin aislar (namespaces en `false`, FS read-write, red
|
||||||
|
`full`). No endurece el sandbox — eso se hace después, Card por Card.
|
||||||
|
|
||||||
|
La absorción de OpenRC es **superficial**: envuelve `/etc/init.d/<svc>
|
||||||
|
start` como one-shot (los scripts de OpenRC son shell completo, no se
|
||||||
|
parsean — el daemon lo deja en segundo plano OpenRC mismo, no arje).
|
||||||
|
runit y dinit, en cambio, dan supervisión real porque su `run`/`command`
|
||||||
|
es un proceso en primer plano.
|
||||||
|
|
||||||
|
Revisá la Semilla antes de instalarla como `/ente/seed.card.json`.
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
//! Traducción del modelo intermedio a Tarjetas Semilla brahman.
|
||||||
|
//!
|
||||||
|
//! Cada [`ForeignService`] se vuelve una Card hija; el conjunto cuelga
|
||||||
|
//! como `genesis` de una Card raíz que `arje-zero` encarna al arrancar.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use brahman_card::{
|
||||||
|
Capability, Card, CgroupSpec, FsPolicy, Lifecycle, NetworkingPolicy, Payload, Permissions,
|
||||||
|
Priority, SomaSpec, Supervision,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::model::{ForeignService, ServiceKind};
|
||||||
|
|
||||||
|
/// Convierte un servicio absorbido en una Card hija (genesis child).
|
||||||
|
fn service_to_card(svc: &ForeignService) -> Card {
|
||||||
|
let (lifecycle, supervision) = match svc.kind {
|
||||||
|
ServiceKind::Daemon => (
|
||||||
|
Lifecycle::Daemon,
|
||||||
|
Supervision::Restart {
|
||||||
|
initial: Duration::from_millis(1_000),
|
||||||
|
max: Duration::from_millis(60_000),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ServiceKind::OneShot => (Lifecycle::Oneshot, Supervision::OneShot),
|
||||||
|
};
|
||||||
|
Card {
|
||||||
|
payload: Payload::Native {
|
||||||
|
exec: svc.exec.clone(),
|
||||||
|
argv: svc.argv.clone(),
|
||||||
|
envp: svc.env.clone(),
|
||||||
|
},
|
||||||
|
supervision,
|
||||||
|
lifecycle,
|
||||||
|
priority: Priority::Normal,
|
||||||
|
// Servicio de sistema absorbido: sin aislar (namespaces en
|
||||||
|
// `false` por defecto), con FS de escritura, red y subprocesos.
|
||||||
|
// La migración conserva el comportamiento; endurecer el sandbox
|
||||||
|
// queda como trabajo posterior, Card por Card.
|
||||||
|
permissions: Permissions {
|
||||||
|
filesystem: FsPolicy::ReadWrite,
|
||||||
|
networking: NetworkingPolicy::Full,
|
||||||
|
processes: true,
|
||||||
|
..Permissions::default()
|
||||||
|
},
|
||||||
|
soma: SomaSpec {
|
||||||
|
cgroup: CgroupSpec {
|
||||||
|
path: "ente.slice/absorbed".to_string(),
|
||||||
|
..CgroupSpec::default()
|
||||||
|
},
|
||||||
|
..SomaSpec::default()
|
||||||
|
},
|
||||||
|
..Card::new(svc.name.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// La Card de `carmen-dm`: el compositor `mirada` en modo greeter,
|
||||||
|
/// como gestor de login gráfico. Para agregar a una Semilla absorbida
|
||||||
|
/// al migrar un escritorio (flag `--with-carmen`). Idéntica a la
|
||||||
|
/// entrada `carmen-dm` de `seeds/arje-host.card.json`.
|
||||||
|
pub fn carmen_dm_card() -> Card {
|
||||||
|
Card {
|
||||||
|
payload: Payload::Native {
|
||||||
|
exec: "/usr/bin/mirada-compositor".to_string(),
|
||||||
|
argv: vec!["--greeter".to_string(), "--drm".to_string()],
|
||||||
|
envp: vec![
|
||||||
|
(
|
||||||
|
"PATH".to_string(),
|
||||||
|
"/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin".to_string(),
|
||||||
|
),
|
||||||
|
("XDG_RUNTIME_DIR".to_string(), "/run".to_string()),
|
||||||
|
(
|
||||||
|
"MIRADA_GREETER_BIN".to_string(),
|
||||||
|
"/usr/bin/mirada-greeter".to_string(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
supervision: Supervision::Restart {
|
||||||
|
initial: Duration::from_millis(2_000),
|
||||||
|
max: Duration::from_millis(60_000),
|
||||||
|
},
|
||||||
|
lifecycle: Lifecycle::Daemon,
|
||||||
|
priority: Priority::High,
|
||||||
|
permissions: Permissions {
|
||||||
|
filesystem: FsPolicy::ReadWrite,
|
||||||
|
networking: NetworkingPolicy::None,
|
||||||
|
processes: true,
|
||||||
|
..Permissions::default()
|
||||||
|
},
|
||||||
|
soma: SomaSpec {
|
||||||
|
cgroup: CgroupSpec {
|
||||||
|
path: "ente.slice/carmen".to_string(),
|
||||||
|
..CgroupSpec::default()
|
||||||
|
},
|
||||||
|
..SomaSpec::default()
|
||||||
|
},
|
||||||
|
..Card::new("carmen-dm")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arma una Tarjeta Semilla raíz que encarna todos los servicios
|
||||||
|
/// absorbidos como hijas `genesis` de `arje-zero`.
|
||||||
|
pub fn build_seed(label: &str, services: &[ForeignService]) -> Card {
|
||||||
|
let genesis: Vec<Card> = services.iter().map(service_to_card).collect();
|
||||||
|
Card {
|
||||||
|
provides: [Capability::Spawn, Capability::Journal]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
permissions: Permissions {
|
||||||
|
filesystem: FsPolicy::ReadWrite,
|
||||||
|
networking: NetworkingPolicy::Full,
|
||||||
|
processes: true,
|
||||||
|
..Permissions::default()
|
||||||
|
},
|
||||||
|
genesis,
|
||||||
|
..Card::new(label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn svc(name: &str, kind: ServiceKind) -> ForeignService {
|
||||||
|
ForeignService {
|
||||||
|
name: name.to_string(),
|
||||||
|
exec: "/usr/bin/foo".to_string(),
|
||||||
|
argv: vec!["-x".to_string()],
|
||||||
|
env: Vec::new(),
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seed_validates() {
|
||||||
|
let seed = build_seed(
|
||||||
|
"arje.seed.absorbed",
|
||||||
|
&[svc("a", ServiceKind::Daemon), svc("b", ServiceKind::OneShot)],
|
||||||
|
);
|
||||||
|
seed.validate().expect("la Semilla absorbida debe validar");
|
||||||
|
assert_eq!(seed.genesis.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn daemon_maps_to_restart() {
|
||||||
|
let c = service_to_card(&svc("d", ServiceKind::Daemon));
|
||||||
|
assert!(matches!(c.supervision, Supervision::Restart { .. }));
|
||||||
|
assert_eq!(c.lifecycle, Lifecycle::Daemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oneshot_maps_to_oneshot() {
|
||||||
|
let c = service_to_card(&svc("o", ServiceKind::OneShot));
|
||||||
|
assert!(matches!(c.supervision, Supervision::OneShot));
|
||||||
|
assert_eq!(c.lifecycle, Lifecycle::Oneshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn children_get_distinct_ids() {
|
||||||
|
let seed = build_seed(
|
||||||
|
"x",
|
||||||
|
&[svc("a", ServiceKind::Daemon), svc("b", ServiceKind::Daemon)],
|
||||||
|
);
|
||||||
|
assert_ne!(seed.genesis[0].id, seed.genesis[1].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_service_list_still_validates() {
|
||||||
|
let seed = build_seed("arje.seed.absorbed", &[]);
|
||||||
|
seed.validate().expect("una Semilla sin hijas es válida");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn carmen_card_is_valid_greeter() {
|
||||||
|
let c = carmen_dm_card();
|
||||||
|
c.validate().expect("la Card de carmen-dm debe validar");
|
||||||
|
match &c.payload {
|
||||||
|
brahman_card::Payload::Native { exec, argv, .. } => {
|
||||||
|
assert_eq!(exec, "/usr/bin/mirada-compositor");
|
||||||
|
assert!(argv.contains(&"--greeter".to_string()));
|
||||||
|
}
|
||||||
|
_ => panic!("carmen-dm debe ser un payload Native"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn carmen_can_be_appended_to_absorbed_seed() {
|
||||||
|
let mut seed = build_seed("arje.seed.absorbed", &[svc("a", ServiceKind::Daemon)]);
|
||||||
|
seed.genesis.push(carmen_dm_card());
|
||||||
|
seed.validate().expect("la Semilla con carmen debe validar");
|
||||||
|
assert_eq!(seed.genesis.len(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
//! Absorbe **dinit**: parsea los archivos de `/etc/dinit.d`.
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::model::{split_command, ForeignService, ServiceKind};
|
||||||
|
|
||||||
|
/// Descubre los servicios dinit de `<root>/etc/dinit.d`.
|
||||||
|
///
|
||||||
|
/// Cada archivo es un servicio con líneas `clave = valor`. Absorbemos
|
||||||
|
/// los de `type` = `process`/`bgprocess` (daemon) y `scripted`
|
||||||
|
/// (one-shot) que declaren un `command`. `internal`/`triggered` no
|
||||||
|
/// encarnan un proceso y se omiten — entre ellos el servicio `boot`.
|
||||||
|
pub fn absorb(root: &Path) -> anyhow::Result<Vec<ForeignService>> {
|
||||||
|
let dir = root.join("etc/dinit.d");
|
||||||
|
anyhow::ensure!(
|
||||||
|
dir.is_dir(),
|
||||||
|
"no encontré /etc/dinit.d en {}",
|
||||||
|
root.display()
|
||||||
|
);
|
||||||
|
let mut names: Vec<String> = fs::read_dir(&dir)?
|
||||||
|
.flatten()
|
||||||
|
.filter(|e| e.file_type().map(|t| t.is_file()).unwrap_or(false))
|
||||||
|
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||||
|
.filter(|n| !n.starts_with('.'))
|
||||||
|
.collect();
|
||||||
|
names.sort();
|
||||||
|
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for name in names {
|
||||||
|
let text = fs::read_to_string(dir.join(&name))?;
|
||||||
|
if let Some(svc) = parse_service(&name, &text) {
|
||||||
|
out.push(svc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsea un archivo de servicio dinit. `None` si no encarna un proceso.
|
||||||
|
fn parse_service(name: &str, text: &str) -> Option<ForeignService> {
|
||||||
|
let mut ty = String::new();
|
||||||
|
let mut command = String::new();
|
||||||
|
for raw in text.lines() {
|
||||||
|
let line = raw.trim();
|
||||||
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some((key, val)) = line.split_once('=') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match key.trim() {
|
||||||
|
"type" => ty = val.trim().to_string(),
|
||||||
|
"command" => command = val.trim().to_string(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let kind = match ty.as_str() {
|
||||||
|
"process" | "bgprocess" => ServiceKind::Daemon,
|
||||||
|
"scripted" => ServiceKind::OneShot,
|
||||||
|
_ => return None, // internal, triggered, o sin `type`
|
||||||
|
};
|
||||||
|
let (exec, argv) = split_command(&command)?;
|
||||||
|
Some(ForeignService {
|
||||||
|
name: format!("dinit-{name}"),
|
||||||
|
exec,
|
||||||
|
argv,
|
||||||
|
env: Vec::new(),
|
||||||
|
kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn absorbs_process_and_scripted_skips_internal() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let d = tmp.path().join("etc/dinit.d");
|
||||||
|
fs::create_dir_all(&d).unwrap();
|
||||||
|
fs::write(d.join("boot"), "type = internal\nwaits-for = sshd\n").unwrap();
|
||||||
|
fs::write(
|
||||||
|
d.join("sshd"),
|
||||||
|
"# servicio ssh\ntype = process\ncommand = /usr/sbin/sshd -D\nrestart = true\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fs::write(d.join("fsck"), "type = scripted\ncommand = /sbin/fsck -A\n").unwrap();
|
||||||
|
|
||||||
|
let svcs = absorb(tmp.path()).unwrap();
|
||||||
|
// sshd + fsck — boot (internal) se omite.
|
||||||
|
assert_eq!(svcs.len(), 2);
|
||||||
|
let sshd = svcs.iter().find(|s| s.name == "dinit-sshd").unwrap();
|
||||||
|
assert_eq!(sshd.kind, ServiceKind::Daemon);
|
||||||
|
assert_eq!(sshd.exec, "/usr/sbin/sshd");
|
||||||
|
assert_eq!(sshd.argv, ["-D"]);
|
||||||
|
let fsck = svcs.iter().find(|s| s.name == "dinit-fsck").unwrap();
|
||||||
|
assert_eq!(fsck.kind, ServiceKind::OneShot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn errors_without_dinit_d() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
assert!(absorb(tmp.path()).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
//! `arje-absorb` — traduce la configuración de otro init a una Semilla.
|
||||||
|
//!
|
||||||
|
//! Lee la configuración de un init clásico (sysvinit, runit, dinit,
|
||||||
|
//! OpenRC) y emite una Tarjeta Semilla brahman (`Card` JSON) con cada
|
||||||
|
//! servicio como hija `genesis` de `arje-zero`. Es el paso «absorber»
|
||||||
|
//! de la migración: `scripts/migrate-to-arje.sh` lo usa para no perder
|
||||||
|
//! los servicios del sistema al cambiar de init.
|
||||||
|
//!
|
||||||
|
//! La Semilla resultante NO endurece el sandbox — conserva el
|
||||||
|
//! comportamiento del init viejo. Revisala antes de instalarla.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
|
mod card;
|
||||||
|
mod dinit;
|
||||||
|
mod model;
|
||||||
|
mod openrc;
|
||||||
|
mod runit;
|
||||||
|
mod sysvinit;
|
||||||
|
|
||||||
|
const HELP: &str = "\
|
||||||
|
arje-absorb — absorbe la configuración de otro init en una Semilla brahman.
|
||||||
|
|
||||||
|
USO:
|
||||||
|
arje-absorb [--from <init>] [--root <dir>] [--output <archivo>] [--label <s>]
|
||||||
|
|
||||||
|
OPCIONES:
|
||||||
|
--from <init> sysvinit | runit | dinit | openrc | auto (def: auto)
|
||||||
|
--root <dir> raíz del sistema a leer (def: /)
|
||||||
|
--output <f> archivo de salida, o '-' para stdout (def: -)
|
||||||
|
--label <s> label de la Semilla raíz (def: arje.seed.absorbed)
|
||||||
|
--with-carmen agrega carmen-dm (gestor de login gráfico) a la Semilla
|
||||||
|
-h, --help esta ayuda
|
||||||
|
|
||||||
|
Emite una Tarjeta Semilla con cada servicio del init ajeno como hija
|
||||||
|
genesis de arje-zero. Revisala antes de instalarla como
|
||||||
|
/ente/seed.card.json.";
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if let Err(e) = run() {
|
||||||
|
eprintln!("arje-absorb: error: {e:#}");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> anyhow::Result<()> {
|
||||||
|
let mut from = "auto".to_string();
|
||||||
|
let mut root = PathBuf::from("/");
|
||||||
|
let mut output = "-".to_string();
|
||||||
|
let mut label = "arje.seed.absorbed".to_string();
|
||||||
|
let mut with_carmen = false;
|
||||||
|
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
while let Some(arg) = args.next() {
|
||||||
|
match arg.as_str() {
|
||||||
|
"-h" | "--help" => {
|
||||||
|
println!("{HELP}");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
"--from" => {
|
||||||
|
from = args.next().ok_or_else(|| anyhow::anyhow!("--from necesita un valor"))?
|
||||||
|
}
|
||||||
|
"--root" => {
|
||||||
|
root = PathBuf::from(
|
||||||
|
args.next().ok_or_else(|| anyhow::anyhow!("--root necesita un valor"))?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"--output" => {
|
||||||
|
output =
|
||||||
|
args.next().ok_or_else(|| anyhow::anyhow!("--output necesita un valor"))?
|
||||||
|
}
|
||||||
|
"--label" => {
|
||||||
|
label =
|
||||||
|
args.next().ok_or_else(|| anyhow::anyhow!("--label necesita un valor"))?
|
||||||
|
}
|
||||||
|
"--with-carmen" => with_carmen = true,
|
||||||
|
other => anyhow::bail!("opción desconocida «{other}» (usá --help)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let init = if from == "auto" {
|
||||||
|
detect(&root).ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"no pude autodetectar el init en {} — pasá --from <init>",
|
||||||
|
root.display()
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
from.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let services = match init.as_str() {
|
||||||
|
"sysvinit" => sysvinit::absorb(&root)?,
|
||||||
|
"runit" => runit::absorb(&root)?,
|
||||||
|
"dinit" => dinit::absorb(&root)?,
|
||||||
|
"openrc" => openrc::absorb(&root)?,
|
||||||
|
other => anyhow::bail!(
|
||||||
|
"init «{other}» no soportado (sysvinit | runit | dinit | openrc | auto)"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"arje-absorb: init «{init}» → {} servicio(s) absorbido(s).",
|
||||||
|
services.len()
|
||||||
|
);
|
||||||
|
if services.is_empty() {
|
||||||
|
eprintln!("arje-absorb: aviso: 0 servicios — la Semilla quedará sin hijas.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut seed = card::build_seed(&label, &services);
|
||||||
|
if with_carmen {
|
||||||
|
seed.genesis.push(card::carmen_dm_card());
|
||||||
|
eprintln!("arje-absorb: agregado carmen-dm (gestor de login gráfico).");
|
||||||
|
}
|
||||||
|
seed.validate()
|
||||||
|
.map_err(|e| anyhow::anyhow!("la Semilla generada no valida: {e}"))?;
|
||||||
|
let json = seed.to_json_pretty()?;
|
||||||
|
|
||||||
|
if output == "-" {
|
||||||
|
println!("{json}");
|
||||||
|
} else {
|
||||||
|
std::fs::write(&output, json)
|
||||||
|
.map_err(|e| anyhow::anyhow!("escribiendo {output}: {e}"))?;
|
||||||
|
eprintln!("arje-absorb: Semilla escrita en {output}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Autodetecta el init presente en `root`. El orden importa: lo más
|
||||||
|
/// específico primero — un sistema OpenRC suele llevar también un
|
||||||
|
/// `/etc/inittab` (para las consolas), así que sysvinit va último.
|
||||||
|
fn detect(root: &Path) -> Option<String> {
|
||||||
|
if root.join("etc/dinit.d").is_dir() {
|
||||||
|
return Some("dinit".to_string());
|
||||||
|
}
|
||||||
|
if root.join("etc/runlevels").is_dir() || root.join("sbin/openrc").exists() {
|
||||||
|
return Some("openrc".to_string());
|
||||||
|
}
|
||||||
|
if root.join("etc/runit").is_dir() || root.join("etc/sv").is_dir() {
|
||||||
|
return Some("runit".to_string());
|
||||||
|
}
|
||||||
|
if root.join("etc/inittab").exists() {
|
||||||
|
return Some("sysvinit".to_string());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_prefers_dinit() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
std::fs::create_dir_all(tmp.path().join("etc/dinit.d")).unwrap();
|
||||||
|
std::fs::create_dir_all(tmp.path().join("etc/runlevels")).unwrap();
|
||||||
|
assert_eq!(detect(tmp.path()).as_deref(), Some("dinit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_openrc_over_sysvinit() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
std::fs::create_dir_all(tmp.path().join("etc/runlevels/default")).unwrap();
|
||||||
|
std::fs::write(tmp.path().join("etc/inittab"), "").unwrap();
|
||||||
|
assert_eq!(detect(tmp.path()).as_deref(), Some("openrc"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_none_on_empty_root() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
assert!(detect(tmp.path()).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
//! El modelo intermedio: un servicio descubierto en el init ajeno,
|
||||||
|
//! independiente de su formato de origen. Cada absorber
|
||||||
|
//! (`sysvinit`, `runit`, `dinit`, `openrc`) produce [`ForeignService`]s;
|
||||||
|
//! `card` los traduce a Cards brahman.
|
||||||
|
|
||||||
|
/// Un servicio leído de la configuración de otro init.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ForeignService {
|
||||||
|
/// Etiqueta del servicio — pasa a ser el `label` de su Card.
|
||||||
|
pub name: String,
|
||||||
|
/// Ejecutable (path absoluto, tal como existirá en el sistema).
|
||||||
|
pub exec: String,
|
||||||
|
/// Argumentos del ejecutable.
|
||||||
|
pub argv: Vec<String>,
|
||||||
|
/// Variables de entorno (clave, valor) a inyectarle.
|
||||||
|
pub env: Vec<(String, String)>,
|
||||||
|
/// Daemon supervisado, o tarea de una sola ejecución.
|
||||||
|
pub kind: ServiceKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cómo trata arje a un servicio absorbido.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ServiceKind {
|
||||||
|
/// Servicio de larga duración: arje lo supervisa y reinicia.
|
||||||
|
Daemon,
|
||||||
|
/// Tarea puntual: corre una vez y termina.
|
||||||
|
OneShot,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parte una línea de comando en `(exec, argv)`, respetando comillas
|
||||||
|
/// simples y dobles. Best-effort: no expande variables ni globs —
|
||||||
|
/// suficiente para los campos `process`/`command` de los inits
|
||||||
|
/// clásicos. `None` si la línea no tiene ningún token.
|
||||||
|
pub fn split_command(line: &str) -> Option<(String, Vec<String>)> {
|
||||||
|
let mut tokens: Vec<String> = Vec::new();
|
||||||
|
let mut cur = String::new();
|
||||||
|
let mut in_token = false;
|
||||||
|
let mut chars = line.chars().peekable();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
match c {
|
||||||
|
'"' | '\'' => {
|
||||||
|
in_token = true;
|
||||||
|
let quote = c;
|
||||||
|
for q in chars.by_ref() {
|
||||||
|
if q == quote {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cur.push(q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c if c.is_whitespace() => {
|
||||||
|
if in_token {
|
||||||
|
tokens.push(std::mem::take(&mut cur));
|
||||||
|
in_token = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c => {
|
||||||
|
in_token = true;
|
||||||
|
cur.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if in_token {
|
||||||
|
tokens.push(cur);
|
||||||
|
}
|
||||||
|
if tokens.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let exec = tokens.remove(0);
|
||||||
|
Some((exec, tokens))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn splits_plain_command() {
|
||||||
|
let (exec, argv) = split_command("/usr/sbin/sshd -D -e").unwrap();
|
||||||
|
assert_eq!(exec, "/usr/sbin/sshd");
|
||||||
|
assert_eq!(argv, ["-D", "-e"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn respects_double_quotes() {
|
||||||
|
let (exec, argv) = split_command(r#"/bin/foo "arg con espacios" bar"#).unwrap();
|
||||||
|
assert_eq!(exec, "/bin/foo");
|
||||||
|
assert_eq!(argv, ["arg con espacios", "bar"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn respects_single_quotes() {
|
||||||
|
let (exec, argv) = split_command("/bin/sh -c 'echo hola mundo'").unwrap();
|
||||||
|
assert_eq!(exec, "/bin/sh");
|
||||||
|
assert_eq!(argv, ["-c", "echo hola mundo"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_line_yields_none() {
|
||||||
|
assert!(split_command(" ").is_none());
|
||||||
|
assert!(split_command("").is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
//! 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
//! Absorbe **runit**: descubre los servicios bajo supervisión.
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::model::{ForeignService, ServiceKind};
|
||||||
|
|
||||||
|
/// Descubre los servicios runit de `<root>`.
|
||||||
|
///
|
||||||
|
/// runit supervisa un directorio de servicios activos (el `runsvdir`
|
||||||
|
/// del runlevel). Cada entrada apunta a `/etc/sv/<nombre>`, cuyo script
|
||||||
|
/// `run` es un daemon en primer plano — el calce con arje es 1:1: arje
|
||||||
|
/// supervisa ese mismo `run`. Si no hay `runsvdir`, cae a `/etc/sv`
|
||||||
|
/// (todos los servicios definidos).
|
||||||
|
pub fn absorb(root: &Path) -> anyhow::Result<Vec<ForeignService>> {
|
||||||
|
let runsvdir = [
|
||||||
|
"etc/runit/runsvdir/default",
|
||||||
|
"etc/runit/runsvdir/current",
|
||||||
|
"service",
|
||||||
|
"var/service",
|
||||||
|
"etc/service",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| root.join(c))
|
||||||
|
.find(|p| p.is_dir());
|
||||||
|
|
||||||
|
let scan = match runsvdir {
|
||||||
|
Some(d) => d,
|
||||||
|
None => {
|
||||||
|
let sv = root.join("etc/sv");
|
||||||
|
anyhow::ensure!(
|
||||||
|
sv.is_dir(),
|
||||||
|
"no encontré servicios runit en {}",
|
||||||
|
root.display()
|
||||||
|
);
|
||||||
|
sv
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut names: Vec<String> = fs::read_dir(&scan)?
|
||||||
|
.flatten()
|
||||||
|
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||||
|
.filter(|n| !n.starts_with('.'))
|
||||||
|
.collect();
|
||||||
|
names.sort();
|
||||||
|
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for name in names {
|
||||||
|
// El servicio vive en /etc/sv/<name>; su run-script es el exec.
|
||||||
|
let run = root.join("etc/sv").join(&name).join("run");
|
||||||
|
if !run.exists() {
|
||||||
|
continue; // entrada sin run-script real — la saltamos
|
||||||
|
}
|
||||||
|
out.push(ForeignService {
|
||||||
|
name: format!("runit-{name}"),
|
||||||
|
exec: format!("/etc/sv/{name}/run"),
|
||||||
|
argv: Vec::new(),
|
||||||
|
env: Vec::new(),
|
||||||
|
kind: ServiceKind::Daemon, // runit siempre supervisa
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn make_sv(root: &Path, name: &str) {
|
||||||
|
let d = root.join("etc/sv").join(name);
|
||||||
|
fs::create_dir_all(&d).unwrap();
|
||||||
|
fs::write(d.join("run"), "#!/bin/sh\nexec daemon\n").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn absorbs_from_etc_sv_fallback() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
make_sv(tmp.path(), "sshd");
|
||||||
|
make_sv(tmp.path(), "dhcpcd");
|
||||||
|
let svcs = absorb(tmp.path()).unwrap();
|
||||||
|
assert_eq!(svcs.len(), 2);
|
||||||
|
// Salida ordenada por nombre.
|
||||||
|
assert_eq!(svcs[0].name, "runit-dhcpcd");
|
||||||
|
assert_eq!(svcs[0].exec, "/etc/sv/dhcpcd/run");
|
||||||
|
assert_eq!(svcs[0].kind, ServiceKind::Daemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn absorbs_only_enabled_from_runsvdir() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
for s in ["sshd", "dhcpcd", "apagado"] {
|
||||||
|
make_sv(tmp.path(), s);
|
||||||
|
}
|
||||||
|
let rsv = tmp.path().join("etc/runit/runsvdir/default");
|
||||||
|
fs::create_dir_all(&rsv).unwrap();
|
||||||
|
// Sólo sshd y dhcpcd están habilitados (symlink en el runsvdir).
|
||||||
|
for s in ["sshd", "dhcpcd"] {
|
||||||
|
std::os::unix::fs::symlink(tmp.path().join("etc/sv").join(s), rsv.join(s))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let svcs = absorb(tmp.path()).unwrap();
|
||||||
|
assert_eq!(svcs.len(), 2);
|
||||||
|
assert!(svcs.iter().all(|s| s.name != "runit-apagado"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn errors_without_services() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
assert!(absorb(tmp.path()).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
//! Absorbe **sysvinit**: parsea `/etc/inittab`.
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
|
||||||
|
use crate::model::{split_command, ForeignService, ServiceKind};
|
||||||
|
|
||||||
|
/// Lee `<root>/etc/inittab` y devuelve sus servicios.
|
||||||
|
///
|
||||||
|
/// Formato de cada línea: `id:runlevels:action:process`. Tomamos las
|
||||||
|
/// que tienen un `process` real: `respawn` → daemon supervisado;
|
||||||
|
/// `wait`/`once`/`boot`/`bootwait`/`sysinit` → one-shot. El resto de
|
||||||
|
/// acciones (`initdefault`, `ctrlaltdel`, `power*`, `off`, …) no lanzan
|
||||||
|
/// un servicio y se ignoran.
|
||||||
|
pub fn absorb(root: &Path) -> anyhow::Result<Vec<ForeignService>> {
|
||||||
|
let path = root.join("etc/inittab");
|
||||||
|
let text =
|
||||||
|
fs::read_to_string(&path).with_context(|| format!("leyendo {}", path.display()))?;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for raw in text.lines() {
|
||||||
|
let line = raw.trim();
|
||||||
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut f = line.splitn(4, ':');
|
||||||
|
let (id, action, process) = match (f.next(), f.next(), f.next(), f.next()) {
|
||||||
|
(Some(id), Some(_rl), Some(action), Some(process)) => (id, action, process),
|
||||||
|
_ => continue, // línea malformada
|
||||||
|
};
|
||||||
|
let kind = match action.trim() {
|
||||||
|
"respawn" => ServiceKind::Daemon,
|
||||||
|
"wait" | "once" | "boot" | "bootwait" | "sysinit" => ServiceKind::OneShot,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
// El proceso puede empezar con `+` (sysvinit: no escribir utmp).
|
||||||
|
let process = process.trim().trim_start_matches('+').trim();
|
||||||
|
let Some((exec, argv)) = split_command(process) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
out.push(ForeignService {
|
||||||
|
name: format!("sysv-{}", id.trim()),
|
||||||
|
exec,
|
||||||
|
argv,
|
||||||
|
env: Vec::new(),
|
||||||
|
kind,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn with_inittab(content: &str) -> tempfile::TempDir {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
fs::create_dir_all(tmp.path().join("etc")).unwrap();
|
||||||
|
fs::write(tmp.path().join("etc/inittab"), content).unwrap();
|
||||||
|
tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_respawn_and_oneshot() {
|
||||||
|
let tmp = with_inittab(
|
||||||
|
"# consolas del sistema\n\
|
||||||
|
id:3:initdefault:\n\
|
||||||
|
si::sysinit:/etc/rc.d/rc.sysinit\n\
|
||||||
|
1:2345:respawn:/sbin/agetty 38400 tty1 linux\n\
|
||||||
|
rc::wait:/etc/rc.d/rc 3\n",
|
||||||
|
);
|
||||||
|
let svcs = absorb(tmp.path()).unwrap();
|
||||||
|
// sysinit + respawn + wait — initdefault no cuenta.
|
||||||
|
assert_eq!(svcs.len(), 3);
|
||||||
|
let agetty = svcs.iter().find(|s| s.name == "sysv-1").unwrap();
|
||||||
|
assert_eq!(agetty.kind, ServiceKind::Daemon);
|
||||||
|
assert_eq!(agetty.exec, "/sbin/agetty");
|
||||||
|
assert_eq!(agetty.argv, ["38400", "tty1", "linux"]);
|
||||||
|
let si = svcs.iter().find(|s| s.name == "sysv-si").unwrap();
|
||||||
|
assert_eq!(si.kind, ServiceKind::OneShot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skips_comments_and_blank_lines() {
|
||||||
|
let tmp = with_inittab("\n \n# sólo comentarios\n");
|
||||||
|
assert!(absorb(tmp.path()).unwrap().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strips_leading_plus() {
|
||||||
|
let tmp = with_inittab("x:2:respawn:+/sbin/getty tty2\n");
|
||||||
|
let svcs = absorb(tmp.path()).unwrap();
|
||||||
|
assert_eq!(svcs[0].exec, "/sbin/getty");
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+116
@@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# migrate-to-arje.sh — migra una máquina de su init actual a arje,
|
||||||
|
# absorbiendo su configuración de servicios.
|
||||||
|
#
|
||||||
|
# QUÉ HACE:
|
||||||
|
# 1. Compila arje-absorb (host).
|
||||||
|
# 2. Autodetecta el init actual (o --from) y absorbe sus servicios en
|
||||||
|
# una Tarjeta Semilla (out/arje-absorbed.card.json por default).
|
||||||
|
# 3. (--with-carmen) agrega carmen-dm —el gestor de login gráfico— a
|
||||||
|
# la Semilla y compila+instala mirada-compositor y mirada-greeter.
|
||||||
|
# 4. Pasa la Semilla a install-arje-as-init.sh, que instala arje en
|
||||||
|
# paralelo a tu init actual, con su propia entrada GRUB.
|
||||||
|
#
|
||||||
|
# Tu init actual queda INTACTO y sigue siendo el default — arje se elige
|
||||||
|
# en GRUB. Revertir: sudo scripts/uninstall-arje.sh
|
||||||
|
#
|
||||||
|
# systemd NO se absorbe (sus units no son un formato de texto trivial);
|
||||||
|
# para systemd usá install-arje-as-init.sh con seeds/arje-host.card.json.
|
||||||
|
# Este script cubre sysvinit / runit / dinit / OpenRC.
|
||||||
|
#
|
||||||
|
# USO:
|
||||||
|
# sudo scripts/migrate-to-arje.sh [opciones]
|
||||||
|
# --from <init> sysvinit|runit|dinit|openrc|auto (def: auto)
|
||||||
|
# --with-carmen agrega el escritorio carmen como gestor de login
|
||||||
|
# --seed-out <f> dónde escribir la Semilla absorbida
|
||||||
|
# (def: out/arje-absorbed.card.json)
|
||||||
|
# --dry-run absorbe y valida, sin instalar nada (no necesita root)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
FROM="auto"
|
||||||
|
WITH_CARMEN=0
|
||||||
|
DRY_RUN=0
|
||||||
|
SEED_OUT="$REPO_DIR/out/arje-absorbed.card.json"
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--from) FROM="${2:?--from necesita un valor}"; shift 2 ;;
|
||||||
|
--with-carmen) WITH_CARMEN=1; shift ;;
|
||||||
|
--seed-out) SEED_OUT="${2:?--seed-out necesita un valor}"; shift 2 ;;
|
||||||
|
--dry-run) DRY_RUN=1; shift ;;
|
||||||
|
-h|--help)
|
||||||
|
cat <<'USAGE'
|
||||||
|
migrate-to-arje.sh — migra una máquina a arje absorbiendo su init actual.
|
||||||
|
--from <init> sysvinit|runit|dinit|openrc|auto (def: auto)
|
||||||
|
--with-carmen agrega el escritorio carmen como gestor de login
|
||||||
|
--seed-out <f> dónde escribir la Semilla absorbida
|
||||||
|
--dry-run absorbe y valida sin instalar (no necesita root)
|
||||||
|
USAGE
|
||||||
|
exit 0 ;;
|
||||||
|
*) echo "[migrate] opción desconocida: $1 (usá --help)" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" -eq 0 ] && [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo "[migrate] hace falta root para instalar — o corré con --dry-run" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[migrate] paso 1/4: compilar arje-absorb"
|
||||||
|
cargo build --quiet --release -p arje-absorb
|
||||||
|
ABSORB="$REPO_DIR/target/release/arje-absorb"
|
||||||
|
|
||||||
|
echo "[migrate] paso 2/4: absorber el init actual (--from $FROM)"
|
||||||
|
mkdir -p "$(dirname "$SEED_OUT")"
|
||||||
|
ABSORB_ARGS=(--from "$FROM" --output "$SEED_OUT" --label "arje.seed.migrated")
|
||||||
|
[ "$WITH_CARMEN" -eq 1 ] && ABSORB_ARGS+=(--with-carmen)
|
||||||
|
"$ABSORB" "${ABSORB_ARGS[@]}"
|
||||||
|
echo "[migrate] Semilla escrita en: $SEED_OUT"
|
||||||
|
bash "$REPO_DIR/seeds/validate.sh" "$SEED_OUT"
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
[migrate] --dry-run: Semilla absorbida y validada en
|
||||||
|
$SEED_OUT
|
||||||
|
Revisala y, cuando quieras instalar:
|
||||||
|
sudo scripts/install-arje-as-init.sh "$SEED_OUT"
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$WITH_CARMEN" -eq 1 ]; then
|
||||||
|
echo "[migrate] paso 3/4: compilar e instalar carmen (compositor + greeter)"
|
||||||
|
# carmen enlaza mesa/libdrm/libinput/libseat — se compila con el
|
||||||
|
# target del host (glibc dinámico), NO musl: esta máquina ya tiene
|
||||||
|
# esas librerías. Sólo el PID 1 y los shims van musl-static al initrd.
|
||||||
|
cargo build --quiet --release -p mirada-compositor -p mirada-greeter
|
||||||
|
install -m 0755 "$REPO_DIR/target/release/mirada-compositor" /usr/bin/mirada-compositor
|
||||||
|
install -m 0755 "$REPO_DIR/target/release/mirada-greeter" /usr/bin/mirada-greeter
|
||||||
|
echo " mirada-compositor y mirada-greeter instalados en /usr/bin/"
|
||||||
|
else
|
||||||
|
echo "[migrate] paso 3/4: carmen omitido (sin --with-carmen)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[migrate] paso 4/4: instalar arje como init"
|
||||||
|
"$SCRIPT_DIR/install-arje-as-init.sh" "$SEED_OUT"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
[migrate] HECHO. arje quedó instalado en paralelo a tu init actual.
|
||||||
|
- Semilla migrada: $SEED_OUT → /ente/seed.card.json
|
||||||
|
- tu init actual sigue siendo el default; arje se elige en GRUB.
|
||||||
|
|
||||||
|
Para arrancar arje una vez en el próximo boot:
|
||||||
|
sudo grub-reboot "arje (init=/sbin/arje-zero) — kernel \$(uname -r)"
|
||||||
|
sudo reboot
|
||||||
|
|
||||||
|
Revertir todo:
|
||||||
|
sudo scripts/uninstall-arje.sh
|
||||||
|
EOF
|
||||||
Reference in New Issue
Block a user