Files
brahman/crates/init/arje-absorb/src/card.rs
T
sergio 762bf95dfd 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>
2026-05-22 00:40:34 +00:00

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);
}
}