forked from brl/citadel-tools
Refactor realm launching code into separate module.
This commit is contained in:
parent
5eb3194e5b
commit
d1f93e9f34
241
libcitadel/src/realm/launcher.rs
Normal file
241
libcitadel/src/realm/launcher.rs
Normal file
@ -0,0 +1,241 @@
|
||||
use std::fs;
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::{Realm,Result};
|
||||
use std::path::{Path, PathBuf};
|
||||
use crate::realm::network::NetworkConfig;
|
||||
|
||||
const NSPAWN_FILE_TEMPLATE: &str = "\
|
||||
[Exec]
|
||||
Boot=true
|
||||
$NETWORK_CONFIG
|
||||
|
||||
[Files]
|
||||
BindReadOnly=/opt/share
|
||||
BindReadOnly=/storage/citadel-state/resolv.conf:/etc/resolv.conf
|
||||
|
||||
$EXTRA_BIND_MOUNTS
|
||||
|
||||
$EXTRA_FILE_OPTIONS
|
||||
|
||||
";
|
||||
|
||||
const REALM_SERVICE_TEMPLATE: &str = "\
|
||||
[Unit]
|
||||
Description=Application Image $REALM_NAME instance
|
||||
|
||||
[Service]
|
||||
|
||||
DevicePolicy=closed
|
||||
$DEVICE_ALLOW
|
||||
|
||||
Environment=SYSTEMD_NSPAWN_SHARE_NS_IPC=1
|
||||
ExecStart=/usr/bin/systemd-nspawn --quiet --notify-ready=yes --keep-unit $NETNS_ARG --machine=$REALM_NAME --link-journal=auto --directory=$ROOTFS
|
||||
|
||||
KillMode=mixed
|
||||
Type=notify
|
||||
RestartForceExitStatus=133
|
||||
SuccessExitStatus=133
|
||||
";
|
||||
|
||||
const SYSTEMD_NSPAWN_PATH: &str = "/run/systemd/nspawn";
|
||||
const SYSTEMD_UNIT_PATH: &str = "/run/systemd/system";
|
||||
|
||||
pub struct RealmLauncher<'a> {
|
||||
realm: &'a Realm,
|
||||
service: String,
|
||||
devices: Vec<String>,
|
||||
}
|
||||
|
||||
impl <'a> RealmLauncher <'a> {
|
||||
pub fn new(realm: &'a Realm) -> Self {
|
||||
let service = format!("realm-{}.service", realm.name());
|
||||
RealmLauncher {
|
||||
realm, service,
|
||||
devices: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_devices(&mut self) {
|
||||
let config = self.realm.config();
|
||||
|
||||
if config.kvm() {
|
||||
self.add_device("/dev/kvm");
|
||||
}
|
||||
if config.gpu() {
|
||||
self.add_device("/dev/dri/renderD128");
|
||||
if config.gpu_card0() {
|
||||
self.add_device("/dev/dri/card0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_device(&mut self, device: &str) {
|
||||
if Path::new(device).exists() {
|
||||
self.devices.push(device.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_launch_config_files(&self) -> Result<()> {
|
||||
let nspawn_path = self.realm_nspawn_path();
|
||||
if nspawn_path.exists() {
|
||||
fs::remove_file(&nspawn_path)?;
|
||||
}
|
||||
let service_path = self.realm_service_path();
|
||||
if service_path.exists() {
|
||||
fs::remove_file(&service_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_launch_config_files(&mut self, rootfs: &Path, netconfig: &mut NetworkConfig) -> Result<()> {
|
||||
if self.devices.is_empty() {
|
||||
self.add_devices();
|
||||
}
|
||||
let nspawn_path = self.realm_nspawn_path();
|
||||
let nspawn_content = self.generate_nspawn_file(netconfig)?;
|
||||
self.write_launch_config_file(&nspawn_path, &nspawn_content)
|
||||
.map_err(|e| format_err!("failed to write nspawn config file {}: {}", nspawn_path.display(), e))?;
|
||||
|
||||
let service_path = self.realm_service_path();
|
||||
let service_content = self.generate_service_file(rootfs);
|
||||
self.write_launch_config_file(&service_path, &service_content)
|
||||
.map_err(|e| format_err!("failed to write service config file {}: {}", service_path.display(), e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn realm_service_name(&self) -> &str {
|
||||
&self.service
|
||||
}
|
||||
|
||||
/// Write the string `content` to file `path`. If the directory does
|
||||
/// not already exist, create it.
|
||||
fn write_launch_config_file(&self, path: &Path, content: &str) -> Result<()> {
|
||||
match path.parent() {
|
||||
Some(parent) => {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
},
|
||||
None => bail!("config file path {} has no parent?", path.display()),
|
||||
};
|
||||
fs::write(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_nspawn_file(&mut self, netconfig: &mut NetworkConfig) -> Result<String> {
|
||||
Ok(NSPAWN_FILE_TEMPLATE
|
||||
.replace("$EXTRA_BIND_MOUNTS", &self.generate_extra_bind_mounts()?)
|
||||
.replace("$EXTRA_FILE_OPTIONS", &self.generate_extra_file_options()?)
|
||||
.replace("$NETWORK_CONFIG", &self.generate_network_config(netconfig)?))
|
||||
}
|
||||
|
||||
fn generate_extra_bind_mounts(&self) -> Result<String> {
|
||||
let config = self.realm.config();
|
||||
let mut s = String::new();
|
||||
|
||||
if config.ephemeral_home() {
|
||||
writeln!(s, "TemporaryFileSystem=/home/user:mode=755,uid=1000,gid=1000")?;
|
||||
} else {
|
||||
writeln!(s, "Bind={}:/home/user", self.realm.base_path_file("home").display())?;
|
||||
}
|
||||
|
||||
if config.shared_dir() && Path::new("/realms/Shared").exists() {
|
||||
writeln!(s, "Bind=/realms/Shared:/home/user/Shared")?;
|
||||
}
|
||||
|
||||
for dev in &self.devices {
|
||||
writeln!(s, "Bind={}", dev)?;
|
||||
}
|
||||
|
||||
if config.sound() {
|
||||
writeln!(s, "BindReadOnly=/run/user/1000/pulse:/run/user/host/pulse")?;
|
||||
}
|
||||
|
||||
if config.x11() {
|
||||
writeln!(s, "BindReadOnly=/tmp/.X11-unix")?;
|
||||
}
|
||||
|
||||
if config.wayland() {
|
||||
writeln!(s, "BindReadOnly=/run/user/1000/wayland-0:/run/user/host/wayland-0")?;
|
||||
}
|
||||
|
||||
for bind in config.extra_bindmounts() {
|
||||
if Self::is_valid_bind_item(bind) {
|
||||
writeln!(s, "Bind={}", bind)?;
|
||||
}
|
||||
}
|
||||
|
||||
for bind in config.extra_bindmounts_ro() {
|
||||
if Self::is_valid_bind_item(bind) {
|
||||
writeln!(s, "BindReadOnly={}", bind)?;
|
||||
}
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn is_valid_bind_item(item: &str) -> bool {
|
||||
!item.contains('\n')
|
||||
}
|
||||
|
||||
fn generate_extra_file_options(&self) -> Result<String> {
|
||||
let mut s = String::new();
|
||||
if self.realm.readonly_rootfs() {
|
||||
writeln!(s, "ReadOnly=true")?;
|
||||
writeln!(s, "Overlay=+/var::/var")?;
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn generate_network_config(&mut self, netconfig: &mut NetworkConfig) -> Result<String> {
|
||||
let config = self.realm.config();
|
||||
let mut s = String::new();
|
||||
if config.network() {
|
||||
if config.has_netns() {
|
||||
return Ok(s);
|
||||
}
|
||||
let zone = config.network_zone();
|
||||
let addr = if let Some(addr) = config.reserved_ip() {
|
||||
netconfig.allocate_reserved(zone, self.realm.name(), addr)?
|
||||
} else {
|
||||
netconfig.allocate_address_for(zone, self.realm.name())?
|
||||
};
|
||||
let gw = netconfig.gateway(zone)?;
|
||||
writeln!(s, "Environment=IFCONFIG_IP={}", addr)?;
|
||||
writeln!(s, "Environment=IFCONFIG_GW={}", gw)?;
|
||||
writeln!(s, "[Network]")?;
|
||||
writeln!(s, "Zone=clear")?;
|
||||
} else {
|
||||
writeln!(s, "[Network]")?;
|
||||
writeln!(s, "Private=true")?;
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn generate_service_file(&self, rootfs: &Path) -> String {
|
||||
let rootfs = rootfs.display().to_string();
|
||||
let netns_arg = match self.realm.config().netns() {
|
||||
Some(netns) => format!("--network-namespace-path=/run/netns/{}", netns),
|
||||
None => "".into(),
|
||||
};
|
||||
|
||||
let mut s = String::new();
|
||||
for dev in &self.devices {
|
||||
writeln!(s, "DeviceAllow={}", dev).unwrap();
|
||||
}
|
||||
|
||||
REALM_SERVICE_TEMPLATE.replace("$REALM_NAME", self.realm.name())
|
||||
.replace("$ROOTFS", &rootfs)
|
||||
.replace("$NETNS_ARG", &netns_arg)
|
||||
.replace("$DEVICE_ALLOW", &s)
|
||||
}
|
||||
|
||||
fn realm_service_path(&self) -> PathBuf {
|
||||
PathBuf::from(SYSTEMD_UNIT_PATH).join(self.realm_service_name())
|
||||
}
|
||||
|
||||
fn realm_nspawn_path(&self) -> PathBuf {
|
||||
PathBuf::from(SYSTEMD_NSPAWN_PATH).join(format!("{}.nspawn", self.realm.name()))
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ pub (crate) mod network;
|
||||
pub(crate) mod create;
|
||||
pub(crate) mod events;
|
||||
mod systemd;
|
||||
mod launcher;
|
||||
|
||||
pub(crate) use self::network::BridgeAllocator;
|
||||
|
||||
|
@ -1,13 +1,9 @@
|
||||
use std::process::Command;
|
||||
use std::path::{Path,PathBuf};
|
||||
use std::fs;
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
use std::env;
|
||||
|
||||
const SYSTEMCTL_PATH: &str = "/usr/bin/systemctl";
|
||||
const MACHINECTL_PATH: &str = "/usr/bin/machinectl";
|
||||
const SYSTEMD_NSPAWN_PATH: &str = "/run/systemd/nspawn";
|
||||
const SYSTEMD_UNIT_PATH: &str = "/run/systemd/system";
|
||||
|
||||
use crate::Result;
|
||||
|
||||
@ -15,6 +11,7 @@ use crate::Realm;
|
||||
use std::sync::Mutex;
|
||||
use std::process::Stdio;
|
||||
use crate::realm::network::NetworkConfig;
|
||||
use crate::realm::launcher::RealmLauncher;
|
||||
|
||||
pub struct Systemd {
|
||||
network: Mutex<NetworkConfig>,
|
||||
@ -28,8 +25,10 @@ impl Systemd {
|
||||
}
|
||||
|
||||
pub fn start_realm(&self, realm: &Realm, rootfs: &Path) -> Result<()> {
|
||||
self.write_realm_launch_config(realm, rootfs)?;
|
||||
self.systemctl_start(&self.realm_service_name(realm))?;
|
||||
let mut lock = self.network.lock().unwrap();
|
||||
let mut launcher = RealmLauncher::new(realm);
|
||||
launcher.write_launch_config_files(rootfs, &mut lock)?;
|
||||
self.systemctl_start(&launcher.realm_service_name())?;
|
||||
if realm.config().ephemeral_home() {
|
||||
self.setup_ephemeral_home(realm)?;
|
||||
}
|
||||
@ -69,18 +68,15 @@ impl Systemd {
|
||||
}
|
||||
|
||||
pub fn stop_realm(&self, realm: &Realm) -> Result<()> {
|
||||
self.systemctl_stop(&self.realm_service_name(realm))?;
|
||||
self.remove_realm_launch_config(realm)?;
|
||||
let launcher = RealmLauncher::new(realm);
|
||||
self.systemctl_stop(&launcher.realm_service_name())?;
|
||||
launcher.remove_launch_config_files()?;
|
||||
|
||||
let mut network = self.network.lock().unwrap();
|
||||
network.free_allocation_for(realm.config().network_zone(), realm.name())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn realm_service_name(&self, realm: &Realm) -> String {
|
||||
format!("realm-{}.service", realm.name())
|
||||
}
|
||||
|
||||
fn systemctl_start(&self, name: &str) -> Result<bool> {
|
||||
self.run_systemctl("start", name)
|
||||
}
|
||||
@ -182,193 +178,4 @@ impl Systemd {
|
||||
cmd.status().map_err(|e| format_err!("failed to execute{}: {}", MACHINECTL_PATH, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn realm_service_path(&self, realm: &Realm) -> PathBuf {
|
||||
PathBuf::from(SYSTEMD_UNIT_PATH).join(self.realm_service_name(realm))
|
||||
}
|
||||
|
||||
fn realm_nspawn_path(&self, realm: &Realm) -> PathBuf {
|
||||
PathBuf::from(SYSTEMD_NSPAWN_PATH).join(format!("{}.nspawn", realm.name()))
|
||||
}
|
||||
|
||||
fn remove_realm_launch_config(&self, realm: &Realm) -> Result<()> {
|
||||
let nspawn_path = self.realm_nspawn_path(realm);
|
||||
if nspawn_path.exists() {
|
||||
fs::remove_file(&nspawn_path)?;
|
||||
}
|
||||
let service_path = self.realm_service_path(realm);
|
||||
if service_path.exists() {
|
||||
fs::remove_file(&service_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_realm_launch_config(&self, realm: &Realm, rootfs: &Path) -> Result<()> {
|
||||
let nspawn_path = self.realm_nspawn_path(realm);
|
||||
let nspawn_content = self.generate_nspawn_file(realm)?;
|
||||
self.write_launch_config_file(&nspawn_path, &nspawn_content)
|
||||
.map_err(|e| format_err!("failed to write nspawn config file {}: {}", nspawn_path.display(), e))?;
|
||||
|
||||
let service_path = self.realm_service_path(realm);
|
||||
let service_content = self.generate_service_file(realm, rootfs);
|
||||
self.write_launch_config_file(&service_path, &service_content)
|
||||
.map_err(|e| format_err!("failed to write service config file {}: {}", service_path.display(), e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write the string `content` to file `path`. If the directory does
|
||||
/// not already exist, create it.
|
||||
fn write_launch_config_file(&self, path: &Path, content: &str) -> Result<()> {
|
||||
match path.parent() {
|
||||
Some(parent) => {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
},
|
||||
None => bail!("config file path {} has no parent?", path.display()),
|
||||
};
|
||||
fs::write(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_nspawn_file(&self, realm: &Realm) -> Result<String> {
|
||||
Ok(NSPAWN_FILE_TEMPLATE
|
||||
.replace("$EXTRA_BIND_MOUNTS", &self.generate_extra_bind_mounts(realm)?)
|
||||
.replace("$EXTRA_FILE_OPTIONS", &self.generate_extra_file_options(realm)?)
|
||||
.replace("$NETWORK_CONFIG", &self.generate_network_config(realm)?))
|
||||
}
|
||||
|
||||
fn generate_extra_bind_mounts(&self, realm: &Realm) -> Result<String> {
|
||||
let config = realm.config();
|
||||
let mut s = String::new();
|
||||
|
||||
if config.ephemeral_home() {
|
||||
writeln!(s, "TemporaryFileSystem=/home/user:mode=755,uid=1000,gid=1000")?;
|
||||
} else {
|
||||
writeln!(s, "Bind={}:/home/user", realm.base_path_file("home").display())?;
|
||||
}
|
||||
|
||||
if config.shared_dir() && Path::new("/realms/Shared").exists() {
|
||||
writeln!(s, "Bind=/realms/Shared:/home/user/Shared")?;
|
||||
}
|
||||
|
||||
if config.kvm() {
|
||||
writeln!(s, "Bind=/dev/kvm")?;
|
||||
}
|
||||
|
||||
if config.gpu() {
|
||||
writeln!(s, "Bind=/dev/dri/renderD128")?;
|
||||
if config.gpu_card0() {
|
||||
writeln!(s, "Bind=/dev/dri/card0")?;
|
||||
}
|
||||
}
|
||||
|
||||
if config.sound() {
|
||||
writeln!(s, "Bind=/dev/snd")?;
|
||||
writeln!(s, "Bind=/dev/shm")?;
|
||||
writeln!(s, "BindReadOnly=/run/user/1000/pulse:/run/user/host/pulse")?;
|
||||
}
|
||||
|
||||
if config.x11() {
|
||||
writeln!(s, "BindReadOnly=/tmp/.X11-unix")?;
|
||||
}
|
||||
|
||||
if config.wayland() {
|
||||
writeln!(s, "BindReadOnly=/run/user/1000/wayland-0:/run/user/host/wayland-0")?;
|
||||
}
|
||||
|
||||
for bind in config.extra_bindmounts() {
|
||||
if self.is_valid_bind_item(bind) {
|
||||
writeln!(s, "Bind={}", bind)?;
|
||||
}
|
||||
}
|
||||
|
||||
for bind in config.extra_bindmounts_ro() {
|
||||
if self.is_valid_bind_item(bind) {
|
||||
writeln!(s, "BindReadOnly={}", bind)?;
|
||||
}
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn is_valid_bind_item(&self, item: &str) -> bool {
|
||||
!item.contains('\n')
|
||||
}
|
||||
|
||||
fn generate_extra_file_options(&self, realm: &Realm) -> Result<String> {
|
||||
let mut s = String::new();
|
||||
if realm.readonly_rootfs() {
|
||||
writeln!(s, "ReadOnly=true")?;
|
||||
writeln!(s, "Overlay=+/var::/var")?;
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn generate_network_config(&self, realm: &Realm) -> Result<String> {
|
||||
let config = realm.config();
|
||||
let mut s = String::new();
|
||||
if config.network() {
|
||||
if config.has_netns() {
|
||||
return Ok(s);
|
||||
}
|
||||
let mut netconf = self.network.lock().unwrap();
|
||||
let zone = config.network_zone();
|
||||
let addr = if let Some(addr) = config.reserved_ip() {
|
||||
netconf.allocate_reserved(zone, realm.name(), addr)?
|
||||
} else {
|
||||
netconf.allocate_address_for(zone, realm.name())?
|
||||
};
|
||||
let gw = netconf.gateway(zone)?;
|
||||
writeln!(s, "Environment=IFCONFIG_IP={}", addr)?;
|
||||
writeln!(s, "Environment=IFCONFIG_GW={}", gw)?;
|
||||
writeln!(s, "[Network]")?;
|
||||
writeln!(s, "Zone=clear")?;
|
||||
} else {
|
||||
writeln!(s, "[Network]")?;
|
||||
writeln!(s, "Private=true")?;
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn generate_service_file(&self, realm: &Realm, rootfs: &Path) -> String {
|
||||
let rootfs = rootfs.display().to_string();
|
||||
let netns_arg = match realm.config().netns() {
|
||||
Some(netns) => format!("--network-namespace-path=/run/netns/{}", netns),
|
||||
None => "".into(),
|
||||
};
|
||||
|
||||
REALM_SERVICE_TEMPLATE.replace("$REALM_NAME", realm.name()).replace("$ROOTFS", &rootfs).replace("$NETNS_ARG", &netns_arg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub const NSPAWN_FILE_TEMPLATE: &str = r###"
|
||||
[Exec]
|
||||
Boot=true
|
||||
$NETWORK_CONFIG
|
||||
|
||||
[Files]
|
||||
BindReadOnly=/opt/share
|
||||
BindReadOnly=/storage/citadel-state/resolv.conf:/etc/resolv.conf
|
||||
|
||||
$EXTRA_BIND_MOUNTS
|
||||
|
||||
$EXTRA_FILE_OPTIONS
|
||||
|
||||
"###;
|
||||
|
||||
pub const REALM_SERVICE_TEMPLATE: &str = r###"
|
||||
[Unit]
|
||||
Description=Application Image $REALM_NAME instance
|
||||
|
||||
[Service]
|
||||
Environment=SYSTEMD_NSPAWN_SHARE_NS_IPC=1
|
||||
ExecStart=/usr/bin/systemd-nspawn --quiet --notify-ready=yes --keep-unit $NETNS_ARG --machine=$REALM_NAME --link-journal=auto --directory=$ROOTFS
|
||||
|
||||
KillMode=mixed
|
||||
Type=notify
|
||||
RestartForceExitStatus=133
|
||||
SuccessExitStatus=133
|
||||
"###;
|
||||
|
Loading…
Reference in New Issue
Block a user