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>
194 lines
6.3 KiB
Rust
194 lines
6.3 KiB
Rust
//! 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);
|
|
}
|
|
}
|