Refactor realm launching code into separate module.

This commit is contained in:
Bruce Leidl 2019-08-20 13:42:35 -04:00
parent 5eb3194e5b
commit d1f93e9f34
3 changed files with 251 additions and 202 deletions

View 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()))
}
}

View File

@ -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;

View File

@ -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
"###;