forked from brl/citadel-tools
refactor of Realm into a module with various components
This commit is contained in:
parent
4b4e5f31e7
commit
fcbf63db8e
451
libcitadel/src/realm/config.rs
Normal file
451
libcitadel/src/realm/config.rs
Normal file
@ -0,0 +1,451 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::fs;
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
use toml;
|
||||||
|
use crate::{Result, Realms};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref GLOBAL_CONFIG: RealmConfig = RealmConfig::load_global_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_ZONE: &str = "clear";
|
||||||
|
const DEFAULT_REALMFS: &str = "base";
|
||||||
|
const DEFAULT_OVERLAY: &str = "storage";
|
||||||
|
|
||||||
|
/// Type of rootfs overlay a Realm is configured to use
|
||||||
|
#[derive(PartialEq,Debug,Copy,Clone)]
|
||||||
|
pub enum OverlayType {
|
||||||
|
/// Don't use a rootfs overlay
|
||||||
|
None,
|
||||||
|
/// Use a rootfs overlay stored on tmpfs
|
||||||
|
TmpFS,
|
||||||
|
/// Use a rootfs overlay stored in a btrfs subvolume
|
||||||
|
Storage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OverlayType {
|
||||||
|
pub fn from_str_value(value: &str) -> OverlayType {
|
||||||
|
if value == "tmpfs" {
|
||||||
|
OverlayType::TmpFS
|
||||||
|
} else if value == "storage" {
|
||||||
|
OverlayType::Storage
|
||||||
|
} else {
|
||||||
|
warn!("Invalid overlay type: '{}'", value);
|
||||||
|
OverlayType::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_str_value(&self) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
OverlayType::None => None,
|
||||||
|
OverlayType::TmpFS => Some("tmpfs"),
|
||||||
|
OverlayType::Storage => Some("storage"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Content of a Realm configuration file
|
||||||
|
#[derive (Serialize,Deserialize,Clone)]
|
||||||
|
pub struct RealmConfig {
|
||||||
|
#[serde(rename="use-shared-dir")]
|
||||||
|
pub use_shared_dir: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(rename="use-ephemeral-home")]
|
||||||
|
pub use_ephemeral_home: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(rename="ephemeral-persistent-dirs")]
|
||||||
|
pub ephemeral_persistent_dirs: Option<Vec<String>>,
|
||||||
|
|
||||||
|
#[serde(rename="use-sound")]
|
||||||
|
pub use_sound: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(rename="use-x11")]
|
||||||
|
pub use_x11: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(rename="use-wayland")]
|
||||||
|
pub use_wayland: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(rename="use-kvm")]
|
||||||
|
pub use_kvm: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(rename="use-gpu")]
|
||||||
|
pub use_gpu: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(rename="use-gpu-card0")]
|
||||||
|
pub use_gpu_card0: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(rename="use-network")]
|
||||||
|
pub use_network: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(rename="network-zone")]
|
||||||
|
pub network_zone: Option<String>,
|
||||||
|
|
||||||
|
#[serde(rename="reserved-ip")]
|
||||||
|
pub reserved_ip: Option<u32>,
|
||||||
|
|
||||||
|
#[serde(rename="system-realm")]
|
||||||
|
pub system_realm: Option<bool>,
|
||||||
|
|
||||||
|
pub autostart: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(rename="extra-bindmounts")]
|
||||||
|
pub extra_bindmounts: Option<Vec<String>>,
|
||||||
|
|
||||||
|
#[serde(rename="extra-bindmounts-ro")]
|
||||||
|
pub extra_bindmounts_ro: Option<Vec<String>>,
|
||||||
|
|
||||||
|
#[serde(rename="realm-depends")]
|
||||||
|
pub realm_depends: Option<Vec<String>>,
|
||||||
|
|
||||||
|
pub realmfs: Option<String>,
|
||||||
|
|
||||||
|
#[serde(rename="realmfs-write")]
|
||||||
|
pub realmfs_write: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(rename="terminal-scheme")]
|
||||||
|
pub terminal_scheme: Option<String>,
|
||||||
|
|
||||||
|
pub overlay: Option<String>,
|
||||||
|
|
||||||
|
pub netns: Option<String>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub parent: Option<Box<RealmConfig>>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
loaded: Option<i64>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RealmConfig {
|
||||||
|
|
||||||
|
/// Return an 'unloaded' realm config instance.
|
||||||
|
pub fn unloaded_realm_config(realm_name: &str) -> RealmConfig {
|
||||||
|
let path = Path::new(Realms::BASE_PATH)
|
||||||
|
.join(format!("realm-{}", realm_name))
|
||||||
|
.join("config");
|
||||||
|
|
||||||
|
let mut config = RealmConfig::empty();
|
||||||
|
config.path = path;
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_global_config() -> RealmConfig {
|
||||||
|
if let Some(mut global) = RealmConfig::load_config("/storage/realms/config") {
|
||||||
|
global.parent = Some(Box::new(RealmConfig::default()));
|
||||||
|
return global;
|
||||||
|
}
|
||||||
|
RealmConfig::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_config<P: AsRef<Path>>(path: P) -> Option<RealmConfig> {
|
||||||
|
if path.as_ref().exists() {
|
||||||
|
match fs::read_to_string(path.as_ref()) {
|
||||||
|
Ok(s) => return toml::from_str::<RealmConfig>(&s).ok(),
|
||||||
|
Err(e) => warn!("Error reading config file: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_config<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
||||||
|
let serialized = toml::to_string(self)?;
|
||||||
|
fs::write(path.as_ref(), serialized)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(&self) -> Result<()> {
|
||||||
|
let serialized = toml::to_string(self)?;
|
||||||
|
fs::write(&self.path, serialized)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_mtime(&self) -> i64 {
|
||||||
|
self.path.metadata().map(|meta| meta.mtime()).unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_stale(&self) -> bool {
|
||||||
|
Some(self.read_mtime()) != self.loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reload(&mut self) -> Result<()> {
|
||||||
|
let path = self.path.clone();
|
||||||
|
|
||||||
|
if self.path.exists() {
|
||||||
|
let s = fs::read_to_string(&self.path)?;
|
||||||
|
*self = toml::from_str(&s)?;
|
||||||
|
} else {
|
||||||
|
*self = RealmConfig::empty();
|
||||||
|
}
|
||||||
|
self.path = path;
|
||||||
|
self.loaded = Some(self.read_mtime());
|
||||||
|
self.parent = Some(Box::new(GLOBAL_CONFIG.clone()));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default() -> RealmConfig {
|
||||||
|
RealmConfig {
|
||||||
|
use_shared_dir: Some(true),
|
||||||
|
use_ephemeral_home: Some(false),
|
||||||
|
use_sound: Some(true),
|
||||||
|
use_x11: Some(true),
|
||||||
|
use_wayland: Some(true),
|
||||||
|
use_kvm: Some(false),
|
||||||
|
use_gpu: Some(false),
|
||||||
|
use_gpu_card0: Some(false),
|
||||||
|
use_network: Some(true),
|
||||||
|
ephemeral_persistent_dirs: Some(vec!["Documents".to_string()]),
|
||||||
|
network_zone: Some(DEFAULT_ZONE.into()),
|
||||||
|
reserved_ip: None,
|
||||||
|
system_realm: Some(false),
|
||||||
|
autostart: Some(false),
|
||||||
|
extra_bindmounts: None,
|
||||||
|
extra_bindmounts_ro: None,
|
||||||
|
realm_depends: None,
|
||||||
|
realmfs: Some(DEFAULT_REALMFS.into()),
|
||||||
|
realmfs_write: Some(false),
|
||||||
|
overlay: Some(DEFAULT_OVERLAY.into()),
|
||||||
|
terminal_scheme: None,
|
||||||
|
netns: None,
|
||||||
|
parent: None,
|
||||||
|
loaded: None,
|
||||||
|
path: PathBuf::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty() -> RealmConfig {
|
||||||
|
RealmConfig {
|
||||||
|
use_shared_dir: None,
|
||||||
|
use_ephemeral_home: None,
|
||||||
|
use_sound: None,
|
||||||
|
use_x11: None,
|
||||||
|
use_wayland: None,
|
||||||
|
use_kvm: None,
|
||||||
|
use_gpu: None,
|
||||||
|
use_gpu_card0: None,
|
||||||
|
use_network: None,
|
||||||
|
network_zone: None,
|
||||||
|
reserved_ip: None,
|
||||||
|
system_realm: None,
|
||||||
|
autostart: None,
|
||||||
|
extra_bindmounts: None,
|
||||||
|
extra_bindmounts_ro: None,
|
||||||
|
realm_depends: None,
|
||||||
|
ephemeral_persistent_dirs: None,
|
||||||
|
realmfs: None,
|
||||||
|
realmfs_write: None,
|
||||||
|
overlay: None,
|
||||||
|
terminal_scheme: None,
|
||||||
|
netns: None,
|
||||||
|
parent: None,
|
||||||
|
loaded: None,
|
||||||
|
path: PathBuf::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `true` device /dev/kvm will be added to realm
|
||||||
|
///
|
||||||
|
/// This allows use of tools such as Qemu.
|
||||||
|
pub fn kvm(&self) -> bool {
|
||||||
|
self.bool_value(|c| c.use_kvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// If `true` render node device /dev/dri/renderD128 will be added to realm.
|
||||||
|
///
|
||||||
|
/// This enables hardware graphics acceleration in realm.
|
||||||
|
pub fn gpu(&self) -> bool {
|
||||||
|
self.bool_value(|c| c.use_gpu)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `true` and `self.gpu()` is also true, privileged device /dev/dri/card0 will be
|
||||||
|
/// added to realm.
|
||||||
|
pub fn gpu_card0(&self) -> bool {
|
||||||
|
self.bool_value(|c| c.use_gpu_card0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `true` the /Shared directory will be mounted in home directory of realm.
|
||||||
|
///
|
||||||
|
/// This directory is shared between all running realms and is an easy way to move files
|
||||||
|
/// between realms.
|
||||||
|
pub fn shared_dir(&self) -> bool {
|
||||||
|
self.bool_value(|c| c.use_shared_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `true` the home directory of this realm will be set up in ephemeral mode.
|
||||||
|
///
|
||||||
|
/// The ephemeral home directory is set up with the following steps:
|
||||||
|
///
|
||||||
|
/// 1. Home directory is mounted as tmpfs
|
||||||
|
/// 2. Any files in /realms/skel are copied into home directory
|
||||||
|
/// 3. Any files in /realms/realm-${name}/skel are copied into home directory
|
||||||
|
/// 4. Any directories listed in `self.ephemeral_psersistent_dirs()` are bind
|
||||||
|
/// mounted from /realms/realm-${name}/home into ephemeral home directory.
|
||||||
|
///
|
||||||
|
pub fn ephemeral_home(&self) -> bool {
|
||||||
|
self.bool_value(|c| c.use_ephemeral_home)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list of subdirectories of /realms/realm-${name}/home to bind mount into realm
|
||||||
|
/// home directory when ephemeral-home is enabled.
|
||||||
|
pub fn ephemeral_persistent_dirs(&self) -> Vec<String> {
|
||||||
|
if let Some(ref dirs) = self.ephemeral_persistent_dirs {
|
||||||
|
return dirs.clone()
|
||||||
|
}
|
||||||
|
if let Some(ref parent) = self.parent {
|
||||||
|
return parent.ephemeral_persistent_dirs();
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `true` allows use of sound inside realm. The following items will be
|
||||||
|
/// added to realm:
|
||||||
|
///
|
||||||
|
/// /dev/snd
|
||||||
|
/// /dev/shm
|
||||||
|
/// /run/user/1000/pulse
|
||||||
|
pub fn sound(&self) -> bool {
|
||||||
|
self.bool_value(|c| c.use_sound)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `true` access to the X11 server will be added to realm by bind mounting
|
||||||
|
/// directory /tmp/.X11-unix
|
||||||
|
pub fn x11(&self) -> bool {
|
||||||
|
self.bool_value(|c| {
|
||||||
|
c.use_x11
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `true` access to Wayland display will be permitted in realm by adding
|
||||||
|
/// wayland socket /run/user/1000/wayland-0
|
||||||
|
pub fn wayland(&self) -> bool {
|
||||||
|
self.bool_value(|c| c.use_wayland)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `true` the realm will have access to the network through the zone specified
|
||||||
|
/// by `self.network_zone()`
|
||||||
|
pub fn network(&self) -> bool {
|
||||||
|
self.bool_value(|c| c.use_network)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The name of the network zone this realm will use if `self.network()` is `true`.
|
||||||
|
pub fn network_zone(&self) -> &str {
|
||||||
|
self.str_value(|c| c.network_zone.as_ref()).unwrap_or(DEFAULT_ZONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// If configured, this realm uses a fixed IP address on the zone subnet. The last
|
||||||
|
/// octet of the network address for this realm will be set to the provided value.
|
||||||
|
pub fn reserved_ip(&self) -> Option<u8> {
|
||||||
|
if let Some(n) = self.reserved_ip {
|
||||||
|
Some(n as u8)
|
||||||
|
} else if let Some(ref parent) = self.parent {
|
||||||
|
parent.reserved_ip()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `true` this realm is a system utility realm and should not be displayed
|
||||||
|
/// in the usual list of user realms.
|
||||||
|
pub fn system_realm(&self) -> bool {
|
||||||
|
self.bool_value(|c| c.system_realm)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `true` this realm will be automatically started at boot.
|
||||||
|
pub fn autostart(&self) -> bool {
|
||||||
|
self.bool_value(|c| c.autostart)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list of additional directories to read-write bind mount into realm.
|
||||||
|
pub fn extra_bindmounts(&self) -> Vec<&str> {
|
||||||
|
self.str_vec_value(|c| c.extra_bindmounts.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list of additional directories to read-only bind mount into realm.
|
||||||
|
pub fn extra_bindmounts_ro(&self) -> Vec<&str> {
|
||||||
|
self.str_vec_value(|c| c.extra_bindmounts_ro.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list of names of realms this realm depends on. When this realm is started
|
||||||
|
/// these realms will also be started if not already running.
|
||||||
|
pub fn realm_depends(&self) -> Vec<&str> {
|
||||||
|
self.str_vec_value(|c| c.realm_depends.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The name of a RealmFS to use as the root filesystem for this realm.
|
||||||
|
pub fn realmfs(&self) -> &str {
|
||||||
|
self.str_value(|c| c.realmfs.as_ref()).unwrap_or(DEFAULT_REALMFS)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn realmfs_write(&self) -> bool {
|
||||||
|
self.bool_value(|c| c.realmfs_write)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Name of a terminal color scheme to use in this realm.
|
||||||
|
pub fn terminal_scheme(&self) -> Option<&str> {
|
||||||
|
self.str_value(|c| c.terminal_scheme.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of overlay on root filesystem to set up for this realm.
|
||||||
|
pub fn overlay(&self) -> OverlayType {
|
||||||
|
self.str_value(|c| c.overlay.as_ref())
|
||||||
|
.map(OverlayType::from_str_value)
|
||||||
|
.unwrap_or(OverlayType::None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the overlay string variable according to the `OverlayType` argument.
|
||||||
|
pub fn set_overlay(&mut self, overlay: OverlayType) {
|
||||||
|
self.overlay = overlay.to_str_value().map(String::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn netns(&self) -> Option<&str> {
|
||||||
|
self.str_value(|c| c.netns.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_netns(&self) -> bool {
|
||||||
|
self.netns().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn str_vec_value<F>(&self, get: F) -> Vec<&str>
|
||||||
|
where F: Fn(&RealmConfig) -> Option<&Vec<String>>
|
||||||
|
{
|
||||||
|
if let Some(val) = get(self) {
|
||||||
|
val.iter().map(|s| s.as_str()).collect()
|
||||||
|
} else if let Some(ref parent) = self.parent {
|
||||||
|
parent.str_vec_value(get)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn str_value<F>(&self, get: F) -> Option<&str>
|
||||||
|
where F: Fn(&RealmConfig) -> Option<&String>
|
||||||
|
{
|
||||||
|
if let Some(val) = get(self) {
|
||||||
|
return Some(val)
|
||||||
|
}
|
||||||
|
if let Some(ref parent) = self.parent {
|
||||||
|
return parent.str_value(get);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bool_value<F>(&self, get: F) -> bool
|
||||||
|
where F: Fn(&RealmConfig) -> Option<bool>
|
||||||
|
{
|
||||||
|
if let Some(val) = get(self) {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref parent) = self.parent {
|
||||||
|
return parent.bool_value(get)
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
139
libcitadel/src/realm/create.rs
Normal file
139
libcitadel/src/realm/create.rs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
use std::path::{PathBuf, Path};
|
||||||
|
use crate::{Realms, Result, util};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
/// Creation and removal of a Realm
|
||||||
|
pub struct RealmCreateDestroy {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RealmCreateDestroy {
|
||||||
|
|
||||||
|
pub fn new(name: &str) -> Self {
|
||||||
|
let name = name.to_string();
|
||||||
|
RealmCreateDestroy { name }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tmpdir() -> PathBuf {
|
||||||
|
Path::new(Realms::BASE_PATH).join(".tmp")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn temp_basepath(&self) -> PathBuf {
|
||||||
|
Self::tmpdir().join(self.dirname())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn basepath(&self) -> PathBuf {
|
||||||
|
Path::new(Realms::BASE_PATH)
|
||||||
|
.join(self.dirname())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dirname(&self) -> String {
|
||||||
|
format!("realm-{}", self.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new realm with the name `self.name`
|
||||||
|
pub fn create(&self) -> Result<()> {
|
||||||
|
if self.basepath().exists() {
|
||||||
|
bail!("realm directory {} already exists", self.basepath().display());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = self.create_realm_directory() {
|
||||||
|
let tmpdir = self.temp_basepath();
|
||||||
|
if tmpdir.exists() {
|
||||||
|
let _ = fs::remove_dir_all(tmpdir);
|
||||||
|
}
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_realm_directory(&self) -> Result<()> {
|
||||||
|
self.create_home()?;
|
||||||
|
self.move_from_temp()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_home(&self) -> Result<()> {
|
||||||
|
let home = self.temp_basepath().join("home");
|
||||||
|
|
||||||
|
fs::create_dir_all(&home)
|
||||||
|
.map_err(|e| format_err!("failed to create directory {}: {}", home.display(), e))?;
|
||||||
|
util::chown(&home, 1000, 1000)
|
||||||
|
.map_err(|e| format_err!("failed to change ownership of {} to 1000:1000: {}", home.display(), e))?;
|
||||||
|
|
||||||
|
let skel = Path::new(Realms::BASE_PATH).join("skel");
|
||||||
|
|
||||||
|
if skel.exists() {
|
||||||
|
info!("Populating realm home directory with files from {}", skel.display());
|
||||||
|
util::copy_tree(&skel, &home)
|
||||||
|
.map_err(|e| format_err!("failed to copy tree of files from {} to {}: {}", skel.display(), home.display(), e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_from_temp(&self) -> Result<()> {
|
||||||
|
let from = self.temp_basepath();
|
||||||
|
let to = self.basepath();
|
||||||
|
if to.exists() {
|
||||||
|
bail!("Cannot move temporary directory {} to {} because the target already exists", from.display(), to.display());
|
||||||
|
}
|
||||||
|
fs::rename(from, to)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_to_temp(&self) -> Result<()> {
|
||||||
|
let from = self.basepath();
|
||||||
|
let to = self.temp_basepath();
|
||||||
|
if to.exists() {
|
||||||
|
bail!("Cannot move realm directory {} to {} because the target already exists", from.display(), to.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Self::tmpdir().exists() {
|
||||||
|
fs::create_dir_all(Self::tmpdir())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::rename(from, to)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_realm(&self, save_home: bool) -> Result<()> {
|
||||||
|
|
||||||
|
self.move_to_temp()?;
|
||||||
|
if save_home {
|
||||||
|
self.save_home_for_delete()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("removing realm directory {}", self.temp_basepath().display());
|
||||||
|
fs::remove_dir_all(self.temp_basepath())?;
|
||||||
|
Ok(())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_home_for_delete(&self) -> Result<()> {
|
||||||
|
if !Path::new("/realms/removed").exists() {
|
||||||
|
fs::create_dir("/realms/removed")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = self.home_save_directory();
|
||||||
|
let home = self.temp_basepath().join("home");
|
||||||
|
fs::rename(&home, &target)
|
||||||
|
.map_err(|e| format_err!("unable to move realm home directory to {}: {}", target.display(), e))?;
|
||||||
|
|
||||||
|
info!("home directory been moved to {}, delete it at your leisure", target.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn home_save_directory(&self) -> PathBuf {
|
||||||
|
let mut n = 1;
|
||||||
|
let mut save_dir= PathBuf::from(&format!("/realms/removed/home-{}", self.name));
|
||||||
|
while save_dir.exists() {
|
||||||
|
save_dir.set_extension(n.to_string());
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
save_dir
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
369
libcitadel/src/realm/events.rs
Normal file
369
libcitadel/src/realm/events.rs
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::fmt::{Display,self};
|
||||||
|
use std::sync::{Arc, RwLock, Weak, RwLockWriteGuard, RwLockReadGuard};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::thread::{self,JoinHandle};
|
||||||
|
use std::path;
|
||||||
|
|
||||||
|
use crate::{RealmManager, Result, Realm};
|
||||||
|
use dbus::{Connection, BusType, ConnectionItem, Message, Path};
|
||||||
|
use inotify::{Inotify, WatchMask, WatchDescriptor, Event};
|
||||||
|
|
||||||
|
pub enum RealmEvent {
|
||||||
|
Started(Realm),
|
||||||
|
Stopped(Realm),
|
||||||
|
New(Realm),
|
||||||
|
Removed(Realm),
|
||||||
|
Current(Option<Realm>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for RealmEvent {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
RealmEvent::Started(ref realm) => write!(f, "RealmStarted({})", realm.name()),
|
||||||
|
RealmEvent::Stopped(ref realm) => write!(f, "RealmStopped({})", realm.name()),
|
||||||
|
RealmEvent::New(ref realm) => write!(f, "RealmNew({})", realm.name()),
|
||||||
|
RealmEvent::Removed(ref realm) => write!(f, "RealmRemoved({})", realm.name()),
|
||||||
|
RealmEvent::Current(Some(realm)) => write!(f, "RealmCurrent({})", realm.name()),
|
||||||
|
RealmEvent::Current(None) => write!(f, "RealmCurrent(None)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type RealmEventHandler = Fn(&RealmEvent)+Send+Sync;
|
||||||
|
|
||||||
|
pub struct RealmEventListener {
|
||||||
|
inner: Arc<RwLock<Inner>>,
|
||||||
|
running: Arc<AtomicBool>,
|
||||||
|
join: Vec<JoinHandle<Result<()>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Inner {
|
||||||
|
manager: Weak<RealmManager>,
|
||||||
|
handlers: Vec<Box<RealmEventHandler>>,
|
||||||
|
quit: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Inner {
|
||||||
|
fn new() -> Self {
|
||||||
|
Inner {
|
||||||
|
manager: Weak::new(),
|
||||||
|
handlers: Vec::new(),
|
||||||
|
quit: Arc::new(AtomicBool::new(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_manager(&mut self, manager: Arc<RealmManager>) {
|
||||||
|
self.manager = Arc::downgrade(&manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_handler<F>(&mut self, handler: F)
|
||||||
|
where F: Fn(&RealmEvent),
|
||||||
|
F: 'static + Send + Sync
|
||||||
|
{
|
||||||
|
self.handlers.push(Box::new(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_event(&self, event: RealmEvent) {
|
||||||
|
self.handlers.iter().for_each(|cb| (cb)(&event));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quit_flag(&self) -> bool {
|
||||||
|
self.quit.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_quit_flag(&self, val: bool) {
|
||||||
|
self.quit.store(val, Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_manager<F>(&self, f: F)
|
||||||
|
where F: Fn(&RealmManager)
|
||||||
|
{
|
||||||
|
if let Some(manager) = self.manager.upgrade() {
|
||||||
|
f(&manager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RealmEventListener {
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
RealmEventListener {
|
||||||
|
inner: Arc::new(RwLock::new(Inner::new())),
|
||||||
|
running: Arc::new(AtomicBool::new(false)),
|
||||||
|
join: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_manager(&self, manager: Arc<RealmManager>) {
|
||||||
|
self.inner_mut().set_manager(manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_running(&self) -> bool {
|
||||||
|
self.running.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_running(&self, val: bool) -> bool {
|
||||||
|
self.running.swap(val, Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_handler<F>(&self, handler: F)
|
||||||
|
where F: Fn(&RealmEvent),
|
||||||
|
F: 'static + Send + Sync
|
||||||
|
{
|
||||||
|
self.inner_mut().add_handler(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner_mut(&self) -> RwLockWriteGuard<Inner> {
|
||||||
|
self.inner.write().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner(&self) -> RwLockReadGuard<Inner> {
|
||||||
|
self.inner.read().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_event_task(&mut self) -> Result<()> {
|
||||||
|
if self.set_running(true) {
|
||||||
|
warn!("RealmEventListener already running");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let inotify_handle = match InotifyEventListener::create(self.inner.clone()) {
|
||||||
|
Ok(inotify) => inotify.spawn(),
|
||||||
|
Err(e) => {
|
||||||
|
self.set_running(false);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let dbus_handle = DbusEventListener::new(self.inner.clone()).spawn();
|
||||||
|
|
||||||
|
self.join.clear();
|
||||||
|
self.join.push(inotify_handle);
|
||||||
|
self.join.push(dbus_handle);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_stop(&self) -> bool {
|
||||||
|
let lock = self.inner();
|
||||||
|
|
||||||
|
let can_stop = self.is_running() && !lock.quit_flag();
|
||||||
|
|
||||||
|
if can_stop {
|
||||||
|
lock.set_quit_flag(true);
|
||||||
|
}
|
||||||
|
can_stop
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&mut self) {
|
||||||
|
if !self.notify_stop() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Stopping event listening task");
|
||||||
|
|
||||||
|
if let Err(e) = InotifyEventListener::wake_inotify() {
|
||||||
|
warn!("error signaling inotify task by creating a file: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
thread::spawn({
|
||||||
|
let handles: Vec<_> = self.join.drain(..).collect();
|
||||||
|
let running = self.running.clone();
|
||||||
|
let quit = self.inner().quit.clone();
|
||||||
|
move || {
|
||||||
|
for join in handles {
|
||||||
|
if let Err(err) = join.join().unwrap() {
|
||||||
|
warn!("error from event task: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
running.store(false, Ordering::SeqCst);
|
||||||
|
quit.store(false, Ordering::SeqCst);
|
||||||
|
info!("Event listening task stopped");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for RealmEventListener {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.inner().set_quit_flag(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct DbusEventListener {
|
||||||
|
inner: Arc<RwLock<Inner>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DbusEventListener {
|
||||||
|
fn new(inner: Arc<RwLock<Inner>>) -> Self {
|
||||||
|
DbusEventListener { inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn(self) -> JoinHandle<Result<()>> {
|
||||||
|
thread::spawn(move || {
|
||||||
|
if let Err(err) = self.dbus_event_loop() {
|
||||||
|
warn!("dbus_event_loop(): {}", err);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dbus_event_loop(&self) -> Result<()> {
|
||||||
|
let connection = Connection::get_private(BusType::System)?;
|
||||||
|
connection.add_match("interface='org.freedesktop.machine1.Manager',type='signal'")?;
|
||||||
|
for item in connection.iter(1000) {
|
||||||
|
if self.inner().quit_flag() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self.handle_item(item);
|
||||||
|
}
|
||||||
|
info!("Exiting dbus event loop");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner(&self) -> RwLockReadGuard<Inner> {
|
||||||
|
self.inner.read().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_item(&self, item: ConnectionItem) {
|
||||||
|
if let ConnectionItem::Signal(message) = item {
|
||||||
|
if let Some(interface) = message.interface() {
|
||||||
|
if &(*interface) == "org.freedesktop.machine1.Manager" {
|
||||||
|
if let Err(e) = self.handle_signal(message) {
|
||||||
|
warn!("Error handling signal: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_signal(&self, message: Message) -> Result<()> {
|
||||||
|
|
||||||
|
let member = message.member()
|
||||||
|
.ok_or(format_err!("invalid signal"))?;
|
||||||
|
let (name, _path): (String, Path) = message.read2()?;
|
||||||
|
if let (Some(interface),Some(member)) = (message.interface(),message.member()) {
|
||||||
|
verbose!("DBUS: {}:[{}({})]", interface, member,name);
|
||||||
|
}
|
||||||
|
match &*member {
|
||||||
|
"MachineNew" => self.on_machine_new(&name),
|
||||||
|
"MachineRemoved" => self.on_machine_removed(&name),
|
||||||
|
_ => {},
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_machine_new(&self, name: &str) {
|
||||||
|
self.inner().with_manager(|m| {
|
||||||
|
if let Some(realm) = m.realm_by_name(name) {
|
||||||
|
realm.set_active(true);
|
||||||
|
self.inner().send_event(RealmEvent::Started(realm))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_machine_removed(&self, name: &str) {
|
||||||
|
self.inner().with_manager(|m| {
|
||||||
|
if let Some(realm) = m.on_machine_removed(name) {
|
||||||
|
self.inner().send_event(RealmEvent::Stopped(realm))
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InotifyEventListener {
|
||||||
|
inner: Arc<RwLock<Inner>>,
|
||||||
|
inotify: Inotify,
|
||||||
|
realms_watch: WatchDescriptor,
|
||||||
|
current_watch: WatchDescriptor,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InotifyEventListener {
|
||||||
|
|
||||||
|
fn create(inner: Arc<RwLock<Inner>>) -> Result<Self> {
|
||||||
|
let mut inotify = Inotify::init()?;
|
||||||
|
let realms_watch = inotify.add_watch("/realms", WatchMask::MOVED_FROM|WatchMask::MOVED_TO)?;
|
||||||
|
let current_watch = inotify.add_watch("/run/citadel/realms/current", WatchMask::CREATE|WatchMask::MOVED_TO)?;
|
||||||
|
|
||||||
|
Ok(InotifyEventListener { inner, inotify, realms_watch, current_watch, })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wake_inotify() -> Result<()> {
|
||||||
|
let path = "/run/citadel/realms/current/stop-events";
|
||||||
|
fs::File::create(path)?;
|
||||||
|
fs::remove_file(path)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn(mut self) -> JoinHandle<Result<()>> {
|
||||||
|
thread::spawn(move || self.inotify_event_loop())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inotify_event_loop(&mut self) -> Result<()> {
|
||||||
|
let mut buffer = [0; 1024];
|
||||||
|
while !self.inner().quit_flag() {
|
||||||
|
let events = self.inotify.read_events_blocking(&mut buffer)?;
|
||||||
|
|
||||||
|
if !self.inner().quit_flag() {
|
||||||
|
for event in events {
|
||||||
|
self.handle_event(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("Exiting inotify event loop");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_event(&self, event: Event<&OsStr>) {
|
||||||
|
self.log_event(&event);
|
||||||
|
if event.wd == self.current_watch {
|
||||||
|
self.handle_current_event();
|
||||||
|
} else if event.wd == self.realms_watch {
|
||||||
|
self.handle_realm_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_event(&self, event: &Event<&OsStr>) {
|
||||||
|
if let Some(name) = event.name {
|
||||||
|
let path = path::Path::new("/realms").join(name);
|
||||||
|
verbose!("INOTIFY: {} ({:?})", path.display(), event.mask);
|
||||||
|
} else {
|
||||||
|
verbose!("INOTIFY: ({:?})", event.mask);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner(&self) -> RwLockReadGuard<Inner> {
|
||||||
|
self.inner.read().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_current_event(&self) {
|
||||||
|
self.inner().with_manager(|m| {
|
||||||
|
if let Some(current) = m.has_current_changed() {
|
||||||
|
self.inner().send_event(RealmEvent::Current(current));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_realm_event(&self) {
|
||||||
|
self.inner().with_manager(|m| {
|
||||||
|
let (added,removed) = match m.rescan_realms() {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("error rescanning realms: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for realm in added {
|
||||||
|
self.inner().send_event(RealmEvent::New(realm));
|
||||||
|
}
|
||||||
|
for realm in removed {
|
||||||
|
self.inner().send_event(RealmEvent::Removed(realm));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
371
libcitadel/src/realm/manager.rs
Normal file
371
libcitadel/src/realm/manager.rs
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
|
||||||
|
use crate::{Mountpoint, Activation,Result, Realms, RealmFS, Realm, util};
|
||||||
|
use crate::realmfs::realmfs_set::RealmFSSet;
|
||||||
|
|
||||||
|
use super::systemd::Systemd;
|
||||||
|
use super::network::NetworkConfig;
|
||||||
|
use super::events::{RealmEventListener, RealmEvent};
|
||||||
|
|
||||||
|
pub struct RealmManager {
|
||||||
|
inner: RwLock<Inner>,
|
||||||
|
systemd: Systemd,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Inner {
|
||||||
|
events: RealmEventListener,
|
||||||
|
realms: Realms,
|
||||||
|
realmfs_set: RealmFSSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Inner {
|
||||||
|
fn new() -> Result<Self> {
|
||||||
|
let events = RealmEventListener::new();
|
||||||
|
let realms = Realms::load()?;
|
||||||
|
let realmfs_set = RealmFSSet::load()?;
|
||||||
|
Ok(Inner { events, realms, realmfs_set })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RealmManager {
|
||||||
|
|
||||||
|
fn create_network_config() -> Result<NetworkConfig> {
|
||||||
|
let mut network = NetworkConfig::new();
|
||||||
|
network.add_bridge("clear", "172.17.0.0/24")?;
|
||||||
|
Ok(network)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load() -> Result<Arc<RealmManager>> {
|
||||||
|
let inner = Inner::new()?;
|
||||||
|
let inner = RwLock::new(inner);
|
||||||
|
|
||||||
|
let network = RealmManager::create_network_config()?;
|
||||||
|
let systemd = Systemd::new(network);
|
||||||
|
|
||||||
|
let manager = RealmManager{ inner, systemd };
|
||||||
|
let manager = Arc::new(manager);
|
||||||
|
|
||||||
|
manager.set_manager(manager.clone());
|
||||||
|
|
||||||
|
Ok(manager)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_manager(&self, manager: Arc<RealmManager>) {
|
||||||
|
let mut inner = self.inner_mut();
|
||||||
|
inner.events.set_manager(manager.clone());
|
||||||
|
inner.realms.set_manager(manager.clone());
|
||||||
|
inner.realmfs_set.set_manager(manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_event_handler<F>(&self, handler: F)
|
||||||
|
where F: Fn(&RealmEvent),
|
||||||
|
F: 'static + Send + Sync
|
||||||
|
{
|
||||||
|
self.inner_mut().events.add_handler(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_event_task(&self) -> Result<()> {
|
||||||
|
self.inner_mut().events.start_event_task()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop_event_task(&self) {
|
||||||
|
self.inner_mut().events.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Execute shell in a realm. If `realm_name` is `None` then exec
|
||||||
|
/// shell in current realm, otherwise look up realm by name.
|
||||||
|
///
|
||||||
|
/// If `root_shell` is true, open a root shell, otherwise open
|
||||||
|
/// a user (uid = 1000) shell.
|
||||||
|
///
|
||||||
|
pub fn launch_shell(&self, realm: &Realm, root_shell: bool) -> Result<()> {
|
||||||
|
Systemd::machinectl_exec_shell(realm, root_shell, true)?;
|
||||||
|
info!("exiting shell in realm '{}'", realm.name());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn launch_terminal(&self, realm: &Realm) -> Result<()> {
|
||||||
|
info!("opening terminal in realm '{}'", realm.name());
|
||||||
|
let title_arg = format!("Realm: {}", realm.name());
|
||||||
|
let args = &["/usr/bin/gnome-terminal".to_owned(), "--title".to_owned(), title_arg];
|
||||||
|
Systemd::machinectl_shell(realm, args, "user", true, true)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_in_realm<S: AsRef<str>>(&self, realm: &Realm, args: &[S], use_launcher: bool) -> Result<()> {
|
||||||
|
Systemd::machinectl_shell(realm, args, "user", use_launcher, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_in_current<S: AsRef<str>>(args: &[S], use_launcher: bool) -> Result<()> {
|
||||||
|
let realm = Realms::load_current_realm()
|
||||||
|
.ok_or(format_err!("Could not find current realm"))?;
|
||||||
|
|
||||||
|
if !realm.is_active() {
|
||||||
|
bail!("Current realm {} is not active?", realm.name());
|
||||||
|
}
|
||||||
|
Systemd::machinectl_shell(&realm, args, "user", use_launcher, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_to_realm<P: AsRef<Path>, Q:AsRef<Path>>(&self, realm: &Realm, from: P, to: Q) -> Result<()> {
|
||||||
|
let from = from.as_ref().to_string_lossy();
|
||||||
|
let to = to.as_ref().to_string_lossy();
|
||||||
|
self.systemd.machinectl_copy_to(realm, from.as_ref(), to.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn realm_list(&self) -> Vec<Realm> {
|
||||||
|
self.inner_mut().realms.sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active_realms(&self, ignore_system: bool) -> Vec<Realm> {
|
||||||
|
self.inner().realms.active(ignore_system)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a list of Realms that are using the `activation`
|
||||||
|
pub fn realms_for_activation(&self, activation: &Activation) -> Vec<Realm> {
|
||||||
|
self.active_realms(false)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|r| {
|
||||||
|
r.realmfs_mountpoint()
|
||||||
|
.map(|mp| activation.is_mountpoint(&mp))
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn realmfs_list(&self) -> Vec<RealmFS> {
|
||||||
|
self.inner().realmfs_set.realmfs_list()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn realmfs_name_exists(&self, name: &str) -> bool {
|
||||||
|
self.inner().realmfs_set.name_exists(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn realmfs_by_name(&self, name: &str) -> Option<RealmFS> {
|
||||||
|
self.inner().realmfs_set.by_name(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Notify `RealmManager` that `mountpoint` has been released by a
|
||||||
|
/// `Realm`.
|
||||||
|
pub fn release_mountpoint(&self, mountpoint: Mountpoint) {
|
||||||
|
info!("releasing mountpoint: {}", mountpoint);
|
||||||
|
if !mountpoint.is_valid() {
|
||||||
|
warn!("bad mountpoint {} passed to release_mountpoint()", mountpoint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(realmfs) = self.realmfs_by_name(mountpoint.realmfs()) {
|
||||||
|
if realmfs.release_mountpoint(&mountpoint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(activation) = Activation::for_mountpoint(&mountpoint) {
|
||||||
|
let active = self.active_mountpoints();
|
||||||
|
if let Err(e) = activation.deactivate(&active) {
|
||||||
|
warn!("error on detached deactivation for {}: {}",activation.device(), e);
|
||||||
|
} else {
|
||||||
|
info!("Deactivated detached activation for device {}", activation.device());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("No activation found for released mountpoint {}", mountpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a `Mountpoint` set containing all the mountpoints which are being used
|
||||||
|
/// by some running `Realm`.
|
||||||
|
pub fn active_mountpoints(&self) -> HashSet<Mountpoint> {
|
||||||
|
self.active_realms(false)
|
||||||
|
.iter()
|
||||||
|
.flat_map(Realm::realmfs_mountpoint)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_boot_realms(&self) -> Result<()> {
|
||||||
|
if let Some(realm) = self.default_realm() {
|
||||||
|
if let Err(e) = self.start_realm(&realm) {
|
||||||
|
bail!("Failed to start default realm '{}': {}", realm.name(), e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bail!("No default realm to start");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_realm(&self, realm: &Realm) -> Result<()> {
|
||||||
|
if realm.is_active() {
|
||||||
|
info!("ignoring start request on already running realm '{}'", realm.name());
|
||||||
|
}
|
||||||
|
info!("Starting realm {}", realm.name());
|
||||||
|
self._start_realm(realm, &mut HashSet::new())?;
|
||||||
|
|
||||||
|
if !Realms::is_some_realm_current() {
|
||||||
|
self.inner_mut().realms.set_realm_current(realm)
|
||||||
|
.unwrap_or_else(|e| warn!("Failed to set realm as current: {}", e));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _start_realm(&self, realm: &Realm, starting: &mut HashSet<String>) -> Result<()> {
|
||||||
|
|
||||||
|
self.start_realm_dependencies(realm, starting)?;
|
||||||
|
|
||||||
|
let home = realm.base_path_file("home");
|
||||||
|
if !home.exists() {
|
||||||
|
warn!("No home directory exists at {}, creating an empty directory", home.display());
|
||||||
|
fs::create_dir_all(&home)?;
|
||||||
|
util::chown_user(&home)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rootfs = realm.setup_rootfs()?;
|
||||||
|
|
||||||
|
realm.update_timestamp()?;
|
||||||
|
|
||||||
|
self.systemd.start_realm(realm, &rootfs)?;
|
||||||
|
|
||||||
|
self.create_realm_namefile(realm)?;
|
||||||
|
|
||||||
|
if realm.config().wayland() {
|
||||||
|
self.link_wayland_socket(realm)
|
||||||
|
.unwrap_or_else(|e| warn!("Error linking wayland socket: {}", e));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_realm_namefile(&self, realm: &Realm) -> Result<()> {
|
||||||
|
let namefile = realm.run_path_file("realm-name");
|
||||||
|
fs::write(&namefile, realm.name())?;
|
||||||
|
self.systemd.machinectl_copy_to(realm, &namefile, "/run/realm-name")?;
|
||||||
|
fs::remove_file(&namefile)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_realm_dependencies(&self, realm: &Realm, starting: &mut HashSet<String>) -> Result<()> {
|
||||||
|
starting.insert(realm.name().to_string());
|
||||||
|
|
||||||
|
for realm_name in realm.config().realm_depends() {
|
||||||
|
if let Some(r) = self.realm_by_name(realm_name) {
|
||||||
|
if !r.is_active() && !starting.contains(r.name()) {
|
||||||
|
info!("Starting realm dependency realm-{}", realm.name());
|
||||||
|
self._start_realm(&r, starting)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("Realm dependency '{}' not found", realm_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link_wayland_socket(&self, realm: &Realm) -> Result<()> {
|
||||||
|
self.run_in_realm(realm, &["/usr/bin/ln", "-s", "/run/user/host/wayland-0", "/run/user/1000/wayland-0"], false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop_realm(&self, realm: &Realm) -> Result<()> {
|
||||||
|
if !realm.is_active() {
|
||||||
|
info!("ignoring stop request on realm '{}' which is not running", realm.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Stopping realm {}", realm.name());
|
||||||
|
|
||||||
|
realm.set_active(false);
|
||||||
|
self.systemd.stop_realm(realm)?;
|
||||||
|
realm.cleanup_rootfs();
|
||||||
|
|
||||||
|
if realm.is_current() {
|
||||||
|
self.choose_some_current_realm();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner(&self) -> RwLockReadGuard<Inner> {
|
||||||
|
self.inner.read().unwrap()
|
||||||
|
}
|
||||||
|
fn inner_mut(&self) -> RwLockWriteGuard<Inner> {
|
||||||
|
self.inner.write().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn on_machine_removed(&self, name: &str) -> Option<Realm> {
|
||||||
|
let realm = match self.inner().realms.by_name(name) {
|
||||||
|
Some(ref realm) if realm.is_active() => realm.clone(),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// XXX do something to detect realmfs/overlay that is not cleaned up
|
||||||
|
realm.set_active(false);
|
||||||
|
|
||||||
|
if realm.is_current() {
|
||||||
|
self.choose_some_current_realm();
|
||||||
|
}
|
||||||
|
Some(realm)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn choose_some_current_realm(&self) {
|
||||||
|
if let Err(e) = self.inner_mut().realms.choose_some_current() {
|
||||||
|
warn!("error choosing new current realm: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_current_changed(&self) -> Option<Option<Realm>> {
|
||||||
|
self.inner_mut().realms.has_current_changed()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_realm(&self) -> Option<Realm> {
|
||||||
|
self.inner().realms.default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_default_realm(&self, realm: &Realm) -> Result<()> {
|
||||||
|
self.inner().realms.set_realm_default(realm)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn realm_by_name(&self, name: &str) -> Option<Realm> {
|
||||||
|
self.inner().realms.by_name(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rescan_realms(&self) -> Result<(Vec<Realm>,Vec<Realm>)> {
|
||||||
|
self.inner_mut().realms.rescan_realms()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_realm(&self, realm: &Realm) -> Result<()> {
|
||||||
|
if realm.is_current() {
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
if !realm.is_active() {
|
||||||
|
self.start_realm(realm)?;
|
||||||
|
}
|
||||||
|
self.inner_mut().realms.set_realm_current(realm)?;
|
||||||
|
info!("Realm '{}' set as current realm", realm.name());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_realm(&self, name: &str) -> Result<Realm> {
|
||||||
|
self.inner_mut().realms.create_realm(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_realm(&self, realm: &Realm, save_home: bool) -> Result<()> {
|
||||||
|
if realm.is_active() {
|
||||||
|
self.stop_realm(realm)?;
|
||||||
|
}
|
||||||
|
self.inner_mut().realms.delete_realm(realm.name(), save_home)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn realmfs_added(&self, realmfs: &RealmFS) {
|
||||||
|
self.inner_mut().realmfs_set.add(realmfs);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_realmfs(&self, realmfs: &RealmFS) -> Result<()> {
|
||||||
|
if realmfs.is_in_use() {
|
||||||
|
bail!("Cannot delete realmfs because it is in use");
|
||||||
|
}
|
||||||
|
realmfs.deactivate()?;
|
||||||
|
if realmfs.is_activated() {
|
||||||
|
bail!("Unable to deactive Realmfs, cannot delete");
|
||||||
|
}
|
||||||
|
self.inner_mut().realmfs_set.remove(realmfs.name());
|
||||||
|
info!("Removing RealmFS image file {}", realmfs.path().display());
|
||||||
|
fs::remove_file(realmfs.path())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
13
libcitadel/src/realm/mod.rs
Normal file
13
libcitadel/src/realm/mod.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
pub(crate) mod overlay;
|
||||||
|
pub(crate) mod config;
|
||||||
|
pub(crate) mod realms;
|
||||||
|
pub(crate) mod manager;
|
||||||
|
pub(crate) mod realm;
|
||||||
|
pub (crate) mod network;
|
||||||
|
pub(crate) mod create;
|
||||||
|
pub(crate) mod events;
|
||||||
|
mod systemd;
|
||||||
|
|
||||||
|
pub(crate) use self::network::BridgeAllocator;
|
||||||
|
|
241
libcitadel/src/realm/network.rs
Normal file
241
libcitadel/src/realm/network.rs
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
use std::path::{Path,PathBuf};
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
use std::collections::{HashSet,HashMap};
|
||||||
|
use std::io::{BufReader,BufRead,Write};
|
||||||
|
use std::fs::{self,File};
|
||||||
|
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
const REALMS_RUN_PATH: &str = "/run/citadel/realms";
|
||||||
|
|
||||||
|
const CLEAR_BRIDGE_NETWORK: &str = "172.17.0.0/24";
|
||||||
|
|
||||||
|
const MIN_MASK: usize = 16;
|
||||||
|
const MAX_MASK: usize = 24;
|
||||||
|
const RESERVED_START: u8 = 200;
|
||||||
|
|
||||||
|
/// Manage ip address assignment for bridges
|
||||||
|
pub struct NetworkConfig {
|
||||||
|
allocators: HashMap<String, BridgeAllocator>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkConfig {
|
||||||
|
pub fn new() -> NetworkConfig {
|
||||||
|
NetworkConfig {
|
||||||
|
allocators: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_bridge(&mut self, name: &str, network: &str) -> Result<()> {
|
||||||
|
let allocator = BridgeAllocator::for_bridge(name, network)
|
||||||
|
.map_err(|e| format_err!("Failed to create bridge allocator: {}", e))?;
|
||||||
|
self.allocators.insert(name.to_owned(), allocator);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gateway(&self, bridge: &str) -> Result<String> {
|
||||||
|
match self.allocators.get(bridge) {
|
||||||
|
Some(allocator) => Ok(allocator.gateway()),
|
||||||
|
None => bail!("Failed to return gateway address for bridge {} because it does not exist", bridge),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allocate_address_for(&mut self, bridge: &str, realm_name: &str) -> Result<String> {
|
||||||
|
match self.allocators.get_mut(bridge) {
|
||||||
|
Some(allocator) => allocator.allocate_address_for(realm_name),
|
||||||
|
None => bail!("Failed to allocate address for bridge {} because it does not exist", bridge),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn free_allocation_for(&mut self, bridge: &str, realm_name: &str) -> Result<()> {
|
||||||
|
match self.allocators.get_mut(bridge) {
|
||||||
|
Some(allocator) => allocator.free_allocation_for(realm_name),
|
||||||
|
None => bail!("Failed to free address on bridge {} because it does not exist", bridge),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allocate_reserved(&mut self, bridge: &str, realm_name: &str, octet: u8) -> Result<String> {
|
||||||
|
match self.allocators.get_mut(bridge) {
|
||||||
|
Some(allocator) => allocator.allocate_reserved(realm_name, octet),
|
||||||
|
None => bail!("Failed to allocate address for bridge {} because it does not exist", bridge),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Allocates IP addresses for a bridge shared by multiple realms.
|
||||||
|
///
|
||||||
|
/// State information is stored in /run/citadel/realms/network-$bridge as
|
||||||
|
/// colon ':' separated pairs of realm name and allocated ip address
|
||||||
|
///
|
||||||
|
/// realm-a:172.17.0.2
|
||||||
|
/// realm-b:172.17.0.3
|
||||||
|
///
|
||||||
|
pub struct BridgeAllocator {
|
||||||
|
bridge: String,
|
||||||
|
network: Ipv4Addr,
|
||||||
|
mask_size: usize,
|
||||||
|
allocated: HashSet<Ipv4Addr>,
|
||||||
|
allocations: HashMap<String, Ipv4Addr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BridgeAllocator {
|
||||||
|
|
||||||
|
|
||||||
|
pub fn default_bridge() -> Result<BridgeAllocator> {
|
||||||
|
BridgeAllocator::for_bridge("clear", CLEAR_BRIDGE_NETWORK)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_bridge(bridge: &str, network: &str) -> Result<BridgeAllocator> {
|
||||||
|
let (addr_str, mask_size) = match network.find('/') {
|
||||||
|
Some(idx) => {
|
||||||
|
let (net,bits) = network.split_at(idx);
|
||||||
|
(net.to_owned(), bits[1..].parse()?)
|
||||||
|
},
|
||||||
|
None => (network.to_owned(), 24),
|
||||||
|
};
|
||||||
|
if mask_size > MAX_MASK || mask_size < MIN_MASK {
|
||||||
|
bail!("Unsupported network mask size of {}", mask_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mask = (1u32 << (32 - mask_size)) - 1;
|
||||||
|
let ip = addr_str.parse::<Ipv4Addr>()?;
|
||||||
|
|
||||||
|
if (u32::from(ip) & mask) != 0 {
|
||||||
|
bail!("network {} has masked bits with netmask /{}", addr_str, mask_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut conf = BridgeAllocator::new(bridge, ip, mask_size);
|
||||||
|
conf.load_state()?;
|
||||||
|
Ok(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(bridge: &str, network: Ipv4Addr, mask_size: usize) -> BridgeAllocator {
|
||||||
|
let allocator = BridgeAllocator {
|
||||||
|
bridge: bridge.to_owned(),
|
||||||
|
allocated: HashSet::new(),
|
||||||
|
allocations: HashMap::new(),
|
||||||
|
network, mask_size,
|
||||||
|
};
|
||||||
|
allocator
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allocate_address_for(&mut self, realm_name: &str) -> Result<String> {
|
||||||
|
match self.find_free_address() {
|
||||||
|
Some(addr) => {
|
||||||
|
self.allocated.insert(addr.clone());
|
||||||
|
if let Some(old) = self.allocations.insert(realm_name.to_owned(), addr.clone()) {
|
||||||
|
self.allocated.remove(&old);
|
||||||
|
}
|
||||||
|
self.write_state()?;
|
||||||
|
return Ok(format!("{}/{}", addr, self.mask_size));
|
||||||
|
},
|
||||||
|
None => bail!("No free IP address could be found to assign to {}", realm_name),
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store_allocation(&mut self, realm_name: &str, address: Ipv4Addr) -> Result<()> {
|
||||||
|
self.allocated.insert(address.clone());
|
||||||
|
if let Some(old) = self.allocations.insert(realm_name.to_string(), address) {
|
||||||
|
self.allocated.remove(&old);
|
||||||
|
}
|
||||||
|
self.write_state()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_free_address(&self) -> Option<Ipv4Addr> {
|
||||||
|
let mask = (1u32 << (32 - self.mask_size)) - 1;
|
||||||
|
let net = u32::from(self.network);
|
||||||
|
for i in 2..mask {
|
||||||
|
let addr = Ipv4Addr::from(net + i);
|
||||||
|
if !Self::is_reserved(addr) && !self.allocated.contains(&addr) {
|
||||||
|
return Some(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_reserved(addr: Ipv4Addr) -> bool {
|
||||||
|
addr.octets()[3] >= RESERVED_START
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gateway(&self) -> String {
|
||||||
|
let gw = u32::from(self.network) + 1;
|
||||||
|
let addr = Ipv4Addr::from(gw);
|
||||||
|
addr.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allocate_reserved(&mut self, realm_name: &str, octet: u8) -> Result<String> {
|
||||||
|
if octet < RESERVED_START {
|
||||||
|
bail!("Not a reserved octet: {}", octet);
|
||||||
|
}
|
||||||
|
let rsv = u32::from(self.network) | octet as u32;
|
||||||
|
let addr = Ipv4Addr::from(rsv);
|
||||||
|
let s = format!("{}/{}", addr, self.mask_size);
|
||||||
|
if self.allocated.contains(&addr) {
|
||||||
|
bail!("Already in use: {}", s);
|
||||||
|
}
|
||||||
|
self.store_allocation(realm_name, addr)?;
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn free_allocation_for(&mut self, realm_name: &str) -> Result<()> {
|
||||||
|
match self.allocations.remove(realm_name) {
|
||||||
|
Some(ip) => {
|
||||||
|
self.allocated.remove(&ip);
|
||||||
|
self.write_state()?;
|
||||||
|
}
|
||||||
|
None => warn!("No address allocation found for realm {}", realm_name),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_file_path(&self) -> PathBuf {
|
||||||
|
Path::new(REALMS_RUN_PATH).with_file_name(format!("network-{}", self.bridge))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn load_state(&mut self) -> Result<()> {
|
||||||
|
let path = self.state_file_path();
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
let f = File::open(path)?;
|
||||||
|
let reader = BufReader::new(f);
|
||||||
|
for line in reader.lines() {
|
||||||
|
let line = &line?;
|
||||||
|
self.parse_state_line(line)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_state_line(&mut self, line: &str) -> Result<()> {
|
||||||
|
match line.find(":") {
|
||||||
|
Some(idx) => {
|
||||||
|
let (name,addr) = line.split_at(idx);
|
||||||
|
let ip = addr[1..].parse::<Ipv4Addr>()?;
|
||||||
|
self.allocated.insert(ip.clone());
|
||||||
|
self.allocations.insert(name.to_owned(), ip);
|
||||||
|
},
|
||||||
|
None => bail!("Could not parse line from network state file: {}", line),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_state(&mut self) -> Result<()> {
|
||||||
|
let path = self.state_file_path();
|
||||||
|
let dir = path.parent().unwrap();
|
||||||
|
if !dir.exists() {
|
||||||
|
fs::create_dir_all(dir)
|
||||||
|
.map_err(|e| format_err!("failed to create directory {} for network allocation state file: {}", dir.display(), e))?;
|
||||||
|
}
|
||||||
|
let mut f = File::create(&path)
|
||||||
|
.map_err(|e| format_err!("failed to open network state file {} for writing: {}", path.display(), e))?;
|
||||||
|
|
||||||
|
for (realm,addr) in &self.allocations {
|
||||||
|
writeln!(f, "{}:{}", realm, addr)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
170
libcitadel/src/realm/overlay.rs
Normal file
170
libcitadel/src/realm/overlay.rs
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::os::unix;
|
||||||
|
use std::path::{Path,PathBuf};
|
||||||
|
|
||||||
|
use crate::{Realm,Result};
|
||||||
|
use crate::Exec;
|
||||||
|
use crate::realm::config::OverlayType;
|
||||||
|
|
||||||
|
const REALMS_BASE_PATH: &str = "/realms";
|
||||||
|
const REALMS_RUN_PATH: &str = "/run/citadel/realms";
|
||||||
|
|
||||||
|
pub struct RealmOverlay {
|
||||||
|
realm: String,
|
||||||
|
overlay: OverlayType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RealmOverlay {
|
||||||
|
|
||||||
|
pub fn remove_any_overlay(realm: &Realm) {
|
||||||
|
Self::try_remove(realm, OverlayType::Storage);
|
||||||
|
Self::try_remove(realm, OverlayType::TmpFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_remove(realm: &Realm, overlay: OverlayType) {
|
||||||
|
let ov = Self::new(realm.name(), overlay);
|
||||||
|
if !ov.exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = ov.remove() {
|
||||||
|
warn!("Error removing {:?} overlay for realm '{}': {}", overlay, realm.name(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_realm(realm: &Realm) -> Option<RealmOverlay> {
|
||||||
|
match realm.config().overlay() {
|
||||||
|
OverlayType::None => None,
|
||||||
|
overlay => Some(RealmOverlay::new(realm.name(), overlay)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(realm: &str, overlay: OverlayType) -> RealmOverlay {
|
||||||
|
let realm = realm.to_string();
|
||||||
|
RealmOverlay { realm, overlay }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Set up an overlayfs for a realm root filesystem either on tmpfs
|
||||||
|
/// or in a btrfs subvolume. Create the overlay over `lower` and
|
||||||
|
/// return the overlay mountpoint.
|
||||||
|
pub fn create(&self, lower: impl AsRef<Path>) -> Result<PathBuf> {
|
||||||
|
let lower = lower.as_ref();
|
||||||
|
info!("Creating overlay [{:?}] over rootfs mounted at {}", self.overlay, lower.display());
|
||||||
|
match self.overlay {
|
||||||
|
OverlayType::TmpFS => self.create_tmpfs(lower),
|
||||||
|
OverlayType::Storage => self.create_btrfs(lower),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a previously created realm overlay and return the
|
||||||
|
/// initial `lower` directory.
|
||||||
|
pub fn remove(&self) -> Result<PathBuf> {
|
||||||
|
let base = self.overlay_directory();
|
||||||
|
let mountpoint = base.join("mountpoint");
|
||||||
|
if !self.umount_overlay() {
|
||||||
|
warn!("Failed to unmount overlay mountpoint {}",mountpoint.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let lower = base.join("lower").read_link()
|
||||||
|
.map_err(|e| format_err!("Unable to read link to 'lower' directory of overlay: {}", e));
|
||||||
|
|
||||||
|
match self.overlay {
|
||||||
|
OverlayType::TmpFS => self.remove_tmpfs(&base)?,
|
||||||
|
OverlayType::Storage => self.remove_btrfs(&base)?,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
Ok(lower?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exists(&self) -> bool {
|
||||||
|
self.overlay_directory().exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lower(&self) -> Option<PathBuf> {
|
||||||
|
let path = self.overlay_directory().join("lower");
|
||||||
|
if path.exists() {
|
||||||
|
fs::read_link(path).ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_tmpfs(&self, base: &Path) -> Result<()> {
|
||||||
|
fs::remove_dir_all(base)
|
||||||
|
.map_err(|e| format_err!("Could not remove overlay directory {}: {}", base.display(), e))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_btrfs(&self, base: &Path) -> Result<()> {
|
||||||
|
Exec::new("/usr/bin/btrfs")
|
||||||
|
.quiet()
|
||||||
|
.run(format!("subvolume delete {}", base.display()))
|
||||||
|
.map_err(|e| format_err!("Could not remove btrfs subvolume {}: {}", base.display(), e))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_tmpfs(&self, lower: &Path) -> Result<PathBuf> {
|
||||||
|
let base = self.overlay_directory();
|
||||||
|
if base.exists() {
|
||||||
|
info!("tmpfs overlay directory already exists, removing it before setting up overlay");
|
||||||
|
self.umount_overlay();
|
||||||
|
self.remove_tmpfs(&base)?;
|
||||||
|
}
|
||||||
|
self.setup_overlay(&base, lower)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn umount_overlay(&self) -> bool {
|
||||||
|
let mountpoint = self.overlay_directory().join("mountpoint");
|
||||||
|
match cmd_ok!("/usr/bin/umount", "{}", mountpoint.display()) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Could not run /usr/bin/umount on {}: {}", mountpoint.display(), e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_btrfs(&self, lower: &Path) -> Result<PathBuf> {
|
||||||
|
let subvolume = self.overlay_directory();
|
||||||
|
if subvolume.exists() {
|
||||||
|
info!("btrfs overlay subvolume already exists, removing it before setting up overlay");
|
||||||
|
self.umount_overlay();
|
||||||
|
self.remove_btrfs(&subvolume)?;
|
||||||
|
}
|
||||||
|
Exec::new("/usr/bin/btrfs").quiet().run(format!("subvolume create {}", subvolume.display()))?;
|
||||||
|
self.setup_overlay(&subvolume, lower)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_overlay(&self, base: &Path, lower: &Path) -> Result<PathBuf> {
|
||||||
|
let upper = self.mkdir(base, "upperdir")?;
|
||||||
|
let work = self.mkdir(base, "workdir")?;
|
||||||
|
let mountpoint = self.mkdir(base, "mountpoint")?;
|
||||||
|
unix::fs::symlink(lower, base.join("lower"))?;
|
||||||
|
cmd!("/usr/bin/mount",
|
||||||
|
"-t overlay realm-{}-overlay -olowerdir={},upperdir={},workdir={} {}",
|
||||||
|
self.realm,
|
||||||
|
lower.display(),
|
||||||
|
upper.display(),
|
||||||
|
work.display(),
|
||||||
|
mountpoint.display())?;
|
||||||
|
Ok(mountpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mkdir(&self, base: &Path, dirname: &str) -> Result<PathBuf> {
|
||||||
|
let path = base.join(dirname);
|
||||||
|
fs::create_dir_all(&path)
|
||||||
|
.map_err(|e| format_err!("failed to create directory {}: {}", path.display(), e))?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay_directory(&self) -> PathBuf {
|
||||||
|
let base = match self.overlay {
|
||||||
|
OverlayType::TmpFS => REALMS_RUN_PATH,
|
||||||
|
OverlayType::Storage => REALMS_BASE_PATH,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
Path::new(base)
|
||||||
|
.join(format!("realm-{}", self.realm))
|
||||||
|
.join("overlay")
|
||||||
|
}
|
||||||
|
}
|
512
libcitadel/src/realm/realm.rs
Normal file
512
libcitadel/src/realm/realm.rs
Normal file
@ -0,0 +1,512 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{PathBuf, Path};
|
||||||
|
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, Weak};
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
|
||||||
|
|
||||||
|
use super::overlay::RealmOverlay;
|
||||||
|
use super::config::{RealmConfig,GLOBAL_CONFIG,OverlayType};
|
||||||
|
use super::realms::Realms;
|
||||||
|
use super::systemd::Systemd;
|
||||||
|
|
||||||
|
use crate::realmfs::{Mountpoint, Activation};
|
||||||
|
use crate::{symlink, util, Result, RealmFS, CommandLine, RealmManager};
|
||||||
|
|
||||||
|
|
||||||
|
const MAX_REALM_NAME_LEN:usize = 128;
|
||||||
|
const ALWAYS_LOAD_TIMESTAMP: bool = true;
|
||||||
|
|
||||||
|
#[derive(Clone,Copy,PartialEq)]
|
||||||
|
enum RealmActiveState {
|
||||||
|
Active,
|
||||||
|
Inactive,
|
||||||
|
Unknown,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RealmActiveState {
|
||||||
|
fn from_sysctl_output(line: &str) -> RealmActiveState {
|
||||||
|
match line {
|
||||||
|
"active" => RealmActiveState::Active,
|
||||||
|
"inactive" => RealmActiveState::Inactive,
|
||||||
|
_ => RealmActiveState::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Inner {
|
||||||
|
config: Arc<RealmConfig>,
|
||||||
|
timestamp: i64,
|
||||||
|
leader_pid: Option<u32>,
|
||||||
|
active: RealmActiveState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Inner {
|
||||||
|
fn new(config: RealmConfig) -> Inner {
|
||||||
|
Inner {
|
||||||
|
config: Arc::new(config),
|
||||||
|
timestamp: 0,
|
||||||
|
leader_pid: None,
|
||||||
|
active: RealmActiveState::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Realm {
|
||||||
|
name: Arc<String>,
|
||||||
|
manager: Weak<RealmManager>,
|
||||||
|
inner: Arc<RwLock<Inner>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Realm {
|
||||||
|
|
||||||
|
pub(crate) fn new(name: &str) -> Realm {
|
||||||
|
let config = RealmConfig::unloaded_realm_config(name);
|
||||||
|
let inner = Inner::new(config);
|
||||||
|
let inner = Arc::new(RwLock::new(inner));
|
||||||
|
let name = Arc::new(name.to_string());
|
||||||
|
let manager = Weak::new();
|
||||||
|
Realm { name, manager, inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_manager(&mut self, manager: Arc<RealmManager>) {
|
||||||
|
self.manager = Arc::downgrade(&manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn manager(&self) -> Arc<RealmManager> {
|
||||||
|
if let Some(manager) = self.manager.upgrade() {
|
||||||
|
manager
|
||||||
|
} else {
|
||||||
|
panic!("No manager set on realm {}", self.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner(&self) -> RwLockReadGuard<Inner> {
|
||||||
|
self.inner.read().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner_mut(&self) -> RwLockWriteGuard<Inner> {
|
||||||
|
self.inner.write().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
if self.inner().active == RealmActiveState::Unknown {
|
||||||
|
self.reload_active_state();
|
||||||
|
}
|
||||||
|
self.inner().active == RealmActiveState::Active
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_active(&self, is_active: bool) {
|
||||||
|
let state = if is_active {
|
||||||
|
RealmActiveState::Active
|
||||||
|
} else {
|
||||||
|
RealmActiveState::Inactive
|
||||||
|
};
|
||||||
|
self.set_active_state(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_system(&self) -> bool {
|
||||||
|
self.config().system_realm()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_active_state(&self, state: RealmActiveState) {
|
||||||
|
let mut inner = self.inner_mut();
|
||||||
|
if state != RealmActiveState::Active {
|
||||||
|
inner.leader_pid = None;
|
||||||
|
}
|
||||||
|
inner.active = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reload_active_state(&self) {
|
||||||
|
match Systemd::is_active(self) {
|
||||||
|
Ok(active) => self.set_active(active),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Failed to run systemctl to determine realm is-active state: {}", err);
|
||||||
|
self.set_active_state(RealmActiveState::Failed)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_active_from_systemctl(&self, output: &str) {
|
||||||
|
self.set_active_state(RealmActiveState::from_sysctl_output(output));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn realmfs_mountpoint(&self) -> Option<Mountpoint> {
|
||||||
|
symlink::read(self.realmfs_mountpoint_symlink())
|
||||||
|
.map(Into::into)
|
||||||
|
.filter(Mountpoint::is_valid)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rootfs(&self) -> Option<PathBuf> {
|
||||||
|
symlink::read(self.rootfs_symlink())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn timestamp(&self) -> i64 {
|
||||||
|
if ALWAYS_LOAD_TIMESTAMP {
|
||||||
|
return self.load_timestamp();
|
||||||
|
}
|
||||||
|
let ts = self._timestamp();
|
||||||
|
if ts == 0 {
|
||||||
|
self.inner_mut().timestamp = self.load_timestamp();
|
||||||
|
}
|
||||||
|
self._timestamp()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _timestamp(&self) -> i64 {
|
||||||
|
self.inner().timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_timestamp(&self) -> i64 {
|
||||||
|
let tstamp = self.base_path().join(".tstamp");
|
||||||
|
if tstamp.exists() {
|
||||||
|
if let Ok(meta) = tstamp.metadata() {
|
||||||
|
return meta.mtime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// create an empty file which is used to track the time at which
|
||||||
|
/// this realm was last made 'current'. These times are used
|
||||||
|
/// to order the output when listing realms.
|
||||||
|
pub fn update_timestamp(&self) -> Result<()> {
|
||||||
|
let tstamp = self.base_path().join(".tstamp");
|
||||||
|
if tstamp.exists() {
|
||||||
|
fs::remove_file(&tstamp)?;
|
||||||
|
}
|
||||||
|
fs::File::create(&tstamp)
|
||||||
|
.map_err(|e| format_err!("failed to create timestamp file {}: {}", tstamp.display(), e))?;
|
||||||
|
// also load the new value
|
||||||
|
self.inner_mut().timestamp = self.load_timestamp();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_realmlock(&self) -> bool {
|
||||||
|
self.base_path_file(".realmlock").exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rootfs_symlink(&self) -> PathBuf {
|
||||||
|
self.run_path().join("rootfs")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn realmfs_mountpoint_symlink(&self) -> PathBuf {
|
||||||
|
self.run_path().join("realmfs-mountpoint")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set up rootfs for a realm that is about to be started.
|
||||||
|
///
|
||||||
|
/// 1) Find the RealmFS for this realm and activate it if not yet activated.
|
||||||
|
/// 2) If this realm is configured to use an overlay, set it up.
|
||||||
|
/// 3) If the RealmFS is unsealed, choose between ro/rw mountpoints
|
||||||
|
/// 4) create 'rootfs' symlink in realm run path pointing to rootfs base
|
||||||
|
/// 5) create 'realmfs-mountpoint' symlink pointing to realmfs mount
|
||||||
|
///
|
||||||
|
pub fn setup_rootfs(&self) -> Result<PathBuf> {
|
||||||
|
let realmfs = self.get_named_realmfs(self.config().realmfs())?;
|
||||||
|
|
||||||
|
let activation = realmfs.activate()?;
|
||||||
|
let writeable = self.use_writable_mountpoint(&realmfs);
|
||||||
|
let mountpoint = self.choose_mountpoint(writeable, &activation)?;
|
||||||
|
|
||||||
|
let rootfs = match RealmOverlay::for_realm(self) {
|
||||||
|
Some(ref overlay) if !writeable => overlay.create(mountpoint.path())?,
|
||||||
|
_ => mountpoint.path().to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
symlink::write(&rootfs, self.rootfs_symlink(), false)?;
|
||||||
|
symlink::write(mountpoint.path(), self.realmfs_mountpoint_symlink(), false)?;
|
||||||
|
symlink::write(self.base_path().join("home"), self.run_path().join("home"), false)?;
|
||||||
|
|
||||||
|
Ok(rootfs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn choose_mountpoint<'a>(&self, writeable: bool, activation: &'a Activation) -> Result<&'a Mountpoint> {
|
||||||
|
if !writeable {
|
||||||
|
Ok(activation.mountpoint())
|
||||||
|
} else if let Some(mountpoint) = activation.mountpoint_rw() {
|
||||||
|
Ok(mountpoint)
|
||||||
|
} else {
|
||||||
|
Err(format_err!("RealmFS activation does not have writable mountpoint as expected"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clean up the rootfs created when starting this realm.
|
||||||
|
///
|
||||||
|
/// 1) If an overlay was created, remove it.
|
||||||
|
/// 2) Notify RealmFS that mountpoint has been released
|
||||||
|
/// 2) Remove the run path rootfs and mountpoint symlinks
|
||||||
|
/// 3) Remove realm run path directory
|
||||||
|
///
|
||||||
|
pub fn cleanup_rootfs(&self) {
|
||||||
|
RealmOverlay::remove_any_overlay(self);
|
||||||
|
|
||||||
|
if let Some(mountpoint) = self.realmfs_mountpoint() {
|
||||||
|
self.manager().release_mountpoint(mountpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::remove_symlink(self.realmfs_mountpoint_symlink());
|
||||||
|
Self::remove_symlink(self.rootfs_symlink());
|
||||||
|
Self::remove_symlink(self.run_path().join("home"));
|
||||||
|
|
||||||
|
if let Err(e) = fs::remove_dir(self.run_path()) {
|
||||||
|
warn!("failed to remove run directory {}: {}", self.run_path().display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_symlink(path: PathBuf) {
|
||||||
|
if let Err(e) = symlink::remove(&path) {
|
||||||
|
warn!("failed to remove symlink {}: {}", path.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_writable_mountpoint(&self, realmfs: &RealmFS) -> bool {
|
||||||
|
match realmfs.metainfo().realmfs_owner() {
|
||||||
|
Some(name) => !realmfs.is_sealed() && name == self.name(),
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return named RealmFS instance if it already exists.
|
||||||
|
///
|
||||||
|
/// Otherwise, create it as a fork of the 'default' image.
|
||||||
|
/// The default image is either 'base' or some other name
|
||||||
|
/// from the global realm config file.
|
||||||
|
///
|
||||||
|
fn get_named_realmfs(&self, name: &str) -> Result<RealmFS> {
|
||||||
|
|
||||||
|
if let Some(realmfs) = self.manager().realmfs_by_name(name) {
|
||||||
|
return Ok(realmfs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if CommandLine::sealed() {
|
||||||
|
bail!("Realm {} needs RealmFS {} which does not exist and cannot be created in sealed realmfs mode", self.name(), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fork_default_realmfs(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create named RealmFS instance as a fork of 'default' RealmFS instance
|
||||||
|
fn fork_default_realmfs(&self, name: &str) -> Result<RealmFS> {
|
||||||
|
let default = self.get_default_realmfs()?;
|
||||||
|
// Requested name might be the default image, if so return it.
|
||||||
|
if name == default.name() {
|
||||||
|
Ok(default)
|
||||||
|
} else {
|
||||||
|
default.fork(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return 'default' RealmFS instance as listed in global realms config file.
|
||||||
|
///
|
||||||
|
/// If the default image does not exist, then create it as a fork
|
||||||
|
/// of 'base' image.
|
||||||
|
fn get_default_realmfs(&self) -> Result<RealmFS> {
|
||||||
|
let default = GLOBAL_CONFIG.realmfs();
|
||||||
|
|
||||||
|
let manager = self.manager();
|
||||||
|
|
||||||
|
if let Some(realmfs) = manager.realmfs_by_name(default) {
|
||||||
|
Ok(realmfs)
|
||||||
|
} else if let Some(base) = manager.realmfs_by_name("base") {
|
||||||
|
// If default image name is something other than 'base' and does
|
||||||
|
// not exist, create it as a fork of 'base'
|
||||||
|
base.fork(default)
|
||||||
|
} else {
|
||||||
|
Err(format_err!("Default RealmFS '{}' does not exist and neither does 'base'", default))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dir_name(&self) -> String {
|
||||||
|
format!("realm-{}", self.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the base path directory of this realm.
|
||||||
|
///
|
||||||
|
/// The base path of a realm with name 'main' would be:
|
||||||
|
///
|
||||||
|
/// /realms/realm-main
|
||||||
|
///
|
||||||
|
pub fn base_path(&self) -> PathBuf {
|
||||||
|
Path::new(Realms::BASE_PATH).join(self.dir_name())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join a filename to the base path of this realm and return it
|
||||||
|
pub fn base_path_file(&self, name: &str) -> PathBuf {
|
||||||
|
self.base_path().join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the run path directory of this realm.
|
||||||
|
///
|
||||||
|
/// The run path of a realm with name 'main' would be:
|
||||||
|
///
|
||||||
|
/// /run/citadel/realms/realm-main
|
||||||
|
///
|
||||||
|
pub fn run_path(&self) -> PathBuf {
|
||||||
|
Path::new(Realms::RUN_PATH).join(self.dir_name())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join a filename to the run path of this realm and return it.
|
||||||
|
pub fn run_path_file(&self, name: &str) -> PathBuf {
|
||||||
|
self.run_path().join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `Arc<RealmConfig>` containing the configuration of this realm.
|
||||||
|
/// If the config file has not yet been loaded from disk, it is lazy loaded
|
||||||
|
/// the first time this method is called.
|
||||||
|
pub fn config(&self) -> Arc<RealmConfig> {
|
||||||
|
if self.inner_config().is_stale() {
|
||||||
|
if let Err(err) = self.with_mut_config(|config| config.reload()) {
|
||||||
|
warn!("error loading config file for realm {}: {}", self.name(), err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.inner_config()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner_config(&self) -> Arc<RealmConfig> {
|
||||||
|
self.inner().config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_mut_config<F,R>(&self, f: F) -> R
|
||||||
|
where F: FnOnce(&mut RealmConfig) -> R
|
||||||
|
{
|
||||||
|
|
||||||
|
let mut lock = self.inner_mut();
|
||||||
|
|
||||||
|
let mut config = lock.config.as_ref().clone();
|
||||||
|
let result = f(&mut config);
|
||||||
|
lock.config = Arc::new(config);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if this realm is configured to use a read-only RealmFS mount.
|
||||||
|
pub fn readonly_rootfs(&self) -> bool {
|
||||||
|
if self.config().overlay() != OverlayType::None {
|
||||||
|
false
|
||||||
|
} else if CommandLine::sealed() {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
!self.config().realmfs_write()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return path to root directory as seen by mount namespace inside the realm container
|
||||||
|
/// This is the path to /proc/PID/root where PID is the 'outside' process id of the
|
||||||
|
/// systemd instance (pid 1) inside the realm pid namespace.
|
||||||
|
pub fn proc_rootfs(&self) -> Option<PathBuf> {
|
||||||
|
self.leader_pid().map(|pid| PathBuf::from(format!("/proc/{}/root", pid)))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Query for 'leader pid' of realm nspawn instance with machinectl.
|
||||||
|
/// The leader pid is the 'pid 1' of the realm container as seen from
|
||||||
|
/// outside the PID namespace.
|
||||||
|
pub fn leader_pid(&self) -> Option<u32> {
|
||||||
|
if !self.is_active() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut lock = self.inner_mut();
|
||||||
|
|
||||||
|
if let Some(pid) = lock.leader_pid {
|
||||||
|
return Some(pid);
|
||||||
|
}
|
||||||
|
match self.query_leader_pid() {
|
||||||
|
Ok(pid) => lock.leader_pid = Some(pid),
|
||||||
|
Err(e) => warn!("error retrieving leader pid for realm: {}", e)
|
||||||
|
}
|
||||||
|
lock.leader_pid
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_leader_pid(&self) -> Result<u32> {
|
||||||
|
let output = cmd_with_output!("/usr/bin/machinectl", "show --value {} -p Leader", self.name())?;
|
||||||
|
let pid = output.parse::<u32>()
|
||||||
|
.map_err(|_| format_err!("Failed to parse leader pid output from machinectl: {}", output))?;
|
||||||
|
Ok(pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if `name` is a valid name for a realm.
|
||||||
|
///
|
||||||
|
/// Valid realm names:
|
||||||
|
///
|
||||||
|
/// * must start with an alphabetic ascii letter character
|
||||||
|
/// * may only contain ascii characters which are letters, numbers, or the dash '-' symbol
|
||||||
|
/// * must not be empty or have a length exceeding 128 characters
|
||||||
|
///
|
||||||
|
pub fn is_valid_name(name: &str) -> bool {
|
||||||
|
util::is_valid_name(name, MAX_REALM_NAME_LEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if this realm is the current realm.
|
||||||
|
///
|
||||||
|
/// A realm is current if the target of the current.realm symlink
|
||||||
|
/// is the run path of the realm.
|
||||||
|
pub fn is_current(&self) -> bool {
|
||||||
|
Realms::read_current_realm_symlink() == Some(self.run_path())
|
||||||
|
//Realms::current_realm_name().as_ref() == Some(&self.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if this realm is the default realm.
|
||||||
|
///
|
||||||
|
/// A realm is the default realm if the target of the
|
||||||
|
/// default.realm symlink is the base path of the realm.
|
||||||
|
pub fn is_default(&self) -> bool {
|
||||||
|
Realms::default_realm_name().as_ref() == Some(&self.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notes(&self) -> Option<String> {
|
||||||
|
let path = self.base_path_file("notes");
|
||||||
|
if path.exists() {
|
||||||
|
return fs::read_to_string(path).ok();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_notes(&self, notes: impl AsRef<str>) -> Result<()> {
|
||||||
|
let path = self.base_path_file("notes");
|
||||||
|
let notes = notes.as_ref();
|
||||||
|
if path.exists() && notes.is_empty() {
|
||||||
|
fs::remove_file(path)?;
|
||||||
|
} else {
|
||||||
|
fs::write(path, notes)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Realm {}
|
||||||
|
impl PartialOrd for Realm {
|
||||||
|
fn partial_cmp(&self, other: &Realm) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl PartialEq for Realm {
|
||||||
|
fn eq(&self, other: &Realm) -> bool {
|
||||||
|
self.partial_cmp(other) == Some(Ordering::Equal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Ord for Realm {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
if !self.is_system() && other.is_system() {
|
||||||
|
Ordering::Less
|
||||||
|
} else if self.is_system() && !other.is_system() {
|
||||||
|
Ordering::Greater
|
||||||
|
} else if self.is_active() && !other.is_active() {
|
||||||
|
Ordering::Less
|
||||||
|
} else if !self.is_active() && other.is_active() {
|
||||||
|
Ordering::Greater
|
||||||
|
} else if self.timestamp() == other.timestamp() {
|
||||||
|
self.name().cmp(other.name())
|
||||||
|
} else {
|
||||||
|
other.timestamp().cmp(&self.timestamp())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
416
libcitadel/src/realm/realms.rs
Normal file
416
libcitadel/src/realm/realms.rs
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::path::{Path,PathBuf};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use crate::{Realm, Result, symlink, RealmManager,FileLock};
|
||||||
|
use std::sync::{Arc, Weak};
|
||||||
|
use super::create::RealmCreateDestroy;
|
||||||
|
use crate::realm::systemd::Systemd;
|
||||||
|
|
||||||
|
struct RealmMapList {
|
||||||
|
manager: Weak<RealmManager>,
|
||||||
|
map: HashMap<String, Realm>,
|
||||||
|
list: Vec<Realm>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RealmMapList {
|
||||||
|
fn new() -> Self {
|
||||||
|
let map = HashMap::new();
|
||||||
|
let list = Vec::new();
|
||||||
|
let manager = Weak::new();
|
||||||
|
RealmMapList { manager, map, list }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_manager(&mut self, manager: Arc<RealmManager>) {
|
||||||
|
self.manager = Arc::downgrade(&manager);
|
||||||
|
self.list.iter_mut().for_each(|r| r.set_manager(manager.clone()));
|
||||||
|
self.map.iter_mut().for_each(|(_,r)| r.set_manager(manager.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert(&mut self, realm: Realm) -> Realm {
|
||||||
|
let mut realm = realm;
|
||||||
|
if let Some(manager) = self.manager.upgrade() {
|
||||||
|
realm.set_manager(manager);
|
||||||
|
}
|
||||||
|
let key = realm.name().to_string();
|
||||||
|
self.map.insert(key, realm.clone());
|
||||||
|
self.list.push(realm.clone());
|
||||||
|
self.sort();
|
||||||
|
realm
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take(&mut self, name: &str) -> Option<Realm> {
|
||||||
|
self.list.retain(|r| r.name() != name);
|
||||||
|
let result = self.map.remove(name);
|
||||||
|
assert_eq!(self.list.len(), self.map.len());
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sort(&mut self) {
|
||||||
|
self.list.sort_unstable();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.list.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Realms {
|
||||||
|
manager: Weak<RealmManager>,
|
||||||
|
realms: RealmMapList,
|
||||||
|
last_current: Option<Realm>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Realms {
|
||||||
|
|
||||||
|
pub const BASE_PATH: &'static str = "/realms";
|
||||||
|
pub const RUN_PATH: &'static str = "/run/citadel/realms";
|
||||||
|
|
||||||
|
pub fn load() -> Result<Self> {
|
||||||
|
let _lock = Self::realmslock()?;
|
||||||
|
|
||||||
|
let mut realms = RealmMapList::new();
|
||||||
|
|
||||||
|
for realm in Self::all_realms(true)? {
|
||||||
|
realms.insert(realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
let manager = Weak::new();
|
||||||
|
|
||||||
|
Ok( Realms { realms, manager, last_current: None })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn all_realms(mark_active: bool) -> Result<Vec<Realm>> {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
for entry in fs::read_dir(Realms::BASE_PATH)? {
|
||||||
|
if let Some(realm) = Realms::entry_to_realm(entry?) {
|
||||||
|
v.push(realm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mark_active {
|
||||||
|
Realms::mark_active_realms(&mut v)?;
|
||||||
|
}
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_manager(&mut self, manager: Arc<RealmManager>) {
|
||||||
|
self.manager = Arc::downgrade(&manager);
|
||||||
|
self.realms.set_manager(manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Examine a directory entry and if it looks like a legit realm directory
|
||||||
|
// extract realm name and return a `Realm` instance.
|
||||||
|
fn entry_to_realm(entry: fs::DirEntry) -> Option<Realm> {
|
||||||
|
match entry.path().symlink_metadata() {
|
||||||
|
Ok(ref meta) if meta.is_dir() => {},
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(filename) = entry.file_name().into_string() {
|
||||||
|
if filename.starts_with("realm-") {
|
||||||
|
let (_, name) = filename.split_at(6);
|
||||||
|
if Realm::is_valid_name(name) {
|
||||||
|
return Some(Realm::new(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which realms are running with a single 'systemctl is-active' call.
|
||||||
|
fn mark_active_realms(realms: &mut Vec<Realm>) -> Result<()> {
|
||||||
|
|
||||||
|
let output = Systemd::are_realms_active(realms)?;
|
||||||
|
|
||||||
|
// process the lines of output together with the list of realms with .zip()
|
||||||
|
realms.iter_mut()
|
||||||
|
.zip(output.lines())
|
||||||
|
.for_each(|(r,line)| r.set_active_from_systemctl(line));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(&self) -> Vec<Realm> {
|
||||||
|
self.realms.list.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sorted(&mut self) -> Vec<Realm> {
|
||||||
|
self.realms.sort();
|
||||||
|
self.list()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn realm_count(&self) -> usize {
|
||||||
|
self.realms.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active(&self, ignore_system: bool) -> Vec<Realm> {
|
||||||
|
self.realms.list.iter()
|
||||||
|
.filter(|r| r.is_active() && !(ignore_system && r.is_system()))
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_realm(&mut self, name: &str) -> Realm {
|
||||||
|
self.realms.insert(Realm::new(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name_set<'a, T>(realms: T) -> HashSet<String>
|
||||||
|
where T: IntoIterator<Item=&'a Realm>
|
||||||
|
{
|
||||||
|
realms.into_iter().map(|r| r.name().to_string()).collect()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Read the /realms directory for the current set of Realms
|
||||||
|
/// that exist on disk. Compare this to the collection of
|
||||||
|
/// realms in `self.realms` to determine if realms have been
|
||||||
|
/// added or removed and update the collection with current
|
||||||
|
/// information.
|
||||||
|
///
|
||||||
|
/// Returns a pair of vectors `(added,removed)` containing
|
||||||
|
/// realms that have been added or removed by the operation.
|
||||||
|
///
|
||||||
|
pub fn rescan_realms(&mut self) -> Result<(Vec<Realm>,Vec<Realm>)> {
|
||||||
|
let _lock = Self::realmslock()?;
|
||||||
|
|
||||||
|
let mut added = Vec::new();
|
||||||
|
let mut removed = Vec::new();
|
||||||
|
|
||||||
|
let current_realms = Self::all_realms(false)?;
|
||||||
|
let new_names = Self::name_set(¤t_realms);
|
||||||
|
let old_names = Self::name_set(&self.realms.list);
|
||||||
|
|
||||||
|
//
|
||||||
|
// names that used to exist and now don't exist have
|
||||||
|
// been removed. Pull those realms out of collection of
|
||||||
|
// realms known to exist (self.realms).
|
||||||
|
//
|
||||||
|
// Set(old_names) - Set(new_names) = Set(removed)
|
||||||
|
//
|
||||||
|
for name in old_names.difference(&new_names) {
|
||||||
|
if let Some(realm) = self.realms.take(name) {
|
||||||
|
removed.push(realm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Set(new_names) - Set(old_names) = Set(added)
|
||||||
|
//
|
||||||
|
for name in new_names.difference(&old_names) {
|
||||||
|
added.push(self.add_realm(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((added, removed))
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create a locking file /realms/.realmslock and lock it with
|
||||||
|
// with flock(2). FileLock will drop the lock when it goes
|
||||||
|
// out of scope.
|
||||||
|
//
|
||||||
|
// Lock is held when iterating over realm instance directories
|
||||||
|
// or when adding or removing a realm directory.
|
||||||
|
//
|
||||||
|
fn realmslock() -> Result<FileLock> {
|
||||||
|
let lockpath = Path::new(Realms::BASE_PATH)
|
||||||
|
.join(".realmslock");
|
||||||
|
|
||||||
|
FileLock::acquire(lockpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_realm(&mut self, name: &str) -> Result<Realm> {
|
||||||
|
let _lock = Self::realmslock()?;
|
||||||
|
|
||||||
|
if !Realm::is_valid_name(name) {
|
||||||
|
bail!("'{}' is not a valid realm name. Only letters, numbers and dash '-' symbol allowed in name. First character must be a letter", name);
|
||||||
|
} else if self.by_name(name).is_some() {
|
||||||
|
bail!("A realm with name '{}' already exists", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
RealmCreateDestroy::new(name).create()?;
|
||||||
|
|
||||||
|
Ok(self.add_realm(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_realm(&mut self, name: &str, save_home: bool) -> Result<()> {
|
||||||
|
let _lock = Self::realmslock()?;
|
||||||
|
|
||||||
|
let realm = match self.realms.take(name) {
|
||||||
|
Some(realm) => realm,
|
||||||
|
None => bail!("Cannot remove realm '{}' because it doesn't seem to exist", name),
|
||||||
|
};
|
||||||
|
|
||||||
|
if realm.is_active() {
|
||||||
|
bail!("Cannot remove active realm. Stop realm {} before deleting", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
RealmCreateDestroy::new(name).delete_realm(save_home)?;
|
||||||
|
|
||||||
|
if realm.is_default() {
|
||||||
|
Self::clear_default_realm()?;
|
||||||
|
self.set_arbitrary_default()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_none_current(&mut self) -> Result<()> {
|
||||||
|
Self::clear_current_realm()?;
|
||||||
|
self.last_current = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_realm_current(&mut self, realm: &Realm) -> Result<()> {
|
||||||
|
symlink::write(realm.run_path(), Realms::current_realm_symlink(), true)?;
|
||||||
|
self.last_current = Some(realm.clone());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_realm_default(&self, realm: &Realm) -> Result<()> {
|
||||||
|
symlink::write(realm.base_path(), Realms::default_symlink(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_arbitrary_default(&mut self) -> Result<()> {
|
||||||
|
// Prefer a recently used realm and don't choose a system realm
|
||||||
|
let choice = self.sorted()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|r| !r.is_system())
|
||||||
|
.next();
|
||||||
|
|
||||||
|
if let Some(realm) = choice {
|
||||||
|
info!("Setting '{}' as new default realm", realm.name());
|
||||||
|
self.set_realm_default(&realm)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_arbitrary_current(&mut self) -> Result<()> {
|
||||||
|
self.realms.sort();
|
||||||
|
if let Some(realm) = self.active(true).first() {
|
||||||
|
self.set_realm_current(realm)?;
|
||||||
|
} else {
|
||||||
|
self.set_none_current()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn choose_some_current(&mut self) -> Result<()> {
|
||||||
|
self.set_arbitrary_current()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn by_name(&self, name: &str) -> Option<Realm> {
|
||||||
|
self.realms.map.get(name).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current(&mut self) -> Option<Realm> {
|
||||||
|
let current = Realms::current_realm_name().and_then(|name| self.by_name(&name));
|
||||||
|
self.last_current = current.clone();
|
||||||
|
current
|
||||||
|
}
|
||||||
|
|
||||||
|
// None : no it's the same
|
||||||
|
// Some() : yes and here's the new value
|
||||||
|
pub fn has_current_changed(&mut self) -> Option<Option<Realm>> {
|
||||||
|
let old = self.last_current.clone();
|
||||||
|
let current = self.current();
|
||||||
|
if current == old {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default(&self) -> Option<Realm> {
|
||||||
|
Realms::default_realm_name().and_then(|name| self.by_name(&name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the `Realm` marked as current, or `None` if no realm is current.
|
||||||
|
///
|
||||||
|
/// This should only be used when not instantiating a `RealmManager`
|
||||||
|
/// otherwise the current realm should be accessed through the manager.
|
||||||
|
///
|
||||||
|
/// The current realm is determined by reading symlink at path:
|
||||||
|
///
|
||||||
|
/// /run/citadel/realms/current/current.realm
|
||||||
|
///
|
||||||
|
/// If the symlink exists it will point to run path of the current realm.
|
||||||
|
///
|
||||||
|
pub fn load_current_realm() -> Option<Realm> {
|
||||||
|
Realms::current_realm_name().map(|ref name| Realm::new(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if some realm has been marked as current.
|
||||||
|
///
|
||||||
|
/// Whether or not a realm has been marked as current is determined
|
||||||
|
/// by checking for the existence of the symlink at path:
|
||||||
|
///
|
||||||
|
/// /run/citadel/realms/current/current.realm
|
||||||
|
///
|
||||||
|
pub fn is_some_realm_current() -> bool {
|
||||||
|
Realms::current_realm_symlink().exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set no realm as current by removing the current.realm symlink.
|
||||||
|
fn clear_current_realm() -> Result<()> {
|
||||||
|
symlink::remove(Realms::current_realm_symlink())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set no realm as default by removing the default.realm symlink.
|
||||||
|
pub fn clear_default_realm() -> Result<()> {
|
||||||
|
symlink::remove(Realms::default_symlink())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path of 'current.realm' symlink
|
||||||
|
pub fn current_realm_symlink() -> PathBuf {
|
||||||
|
Path::new(Realms::RUN_PATH)
|
||||||
|
.join("current")
|
||||||
|
.join("current.realm")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_realm_name() -> Option<String> {
|
||||||
|
Realms::read_current_realm_symlink().as_ref().and_then(Realms::path_to_realm_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_current_realm_symlink() -> Option<PathBuf> {
|
||||||
|
symlink::read(Realms::current_realm_symlink())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path of 'default.realm' symlink
|
||||||
|
pub fn default_symlink() -> PathBuf {
|
||||||
|
Path::new(Realms::BASE_PATH)
|
||||||
|
.join("default.realm")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_realm_name() -> Option<String> {
|
||||||
|
Realms::read_default_symlink().as_ref().and_then(Realms::path_to_realm_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_default_symlink() -> Option<PathBuf> {
|
||||||
|
symlink::read(Realms::default_symlink())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_to_realm_name(path: impl AsRef<Path>) -> Option<String> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
if path.starts_with(Realms::BASE_PATH) {
|
||||||
|
path.strip_prefix(Realms::BASE_PATH).ok()
|
||||||
|
} else if path.starts_with(Realms::RUN_PATH) {
|
||||||
|
path.strip_prefix(Realms::RUN_PATH).ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}.and_then(Realms::dir_to_realm_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dir_to_realm_name(dir: &Path) -> Option<String> {
|
||||||
|
let dirname = dir.to_string_lossy();
|
||||||
|
if dirname.starts_with("realm-") {
|
||||||
|
let (_,name) = dirname.split_at(6);
|
||||||
|
if Realm::is_valid_name(name) {
|
||||||
|
return Some(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
374
libcitadel/src/realm/systemd.rs
Normal file
374
libcitadel/src/realm/systemd.rs
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
use std::path::{Path,PathBuf};
|
||||||
|
use std::fs;
|
||||||
|
use std::fmt::Write;
|
||||||
|
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;
|
||||||
|
|
||||||
|
use crate::Realm;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::process::Stdio;
|
||||||
|
use crate::realm::network::NetworkConfig;
|
||||||
|
|
||||||
|
pub struct Systemd {
|
||||||
|
network: Mutex<NetworkConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Systemd {
|
||||||
|
|
||||||
|
pub fn new(network: NetworkConfig) -> Systemd {
|
||||||
|
let network = Mutex::new(network);
|
||||||
|
Systemd { network }
|
||||||
|
}
|
||||||
|
|
||||||
|
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))?;
|
||||||
|
if realm.config().ephemeral_home() {
|
||||||
|
self.setup_ephemeral_home(realm)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_ephemeral_home(&self, realm: &Realm) -> Result<()> {
|
||||||
|
|
||||||
|
// 1) if exists: machinectl copy-to /realms/skel /home/user
|
||||||
|
if Path::new("/realms/skel").exists() {
|
||||||
|
self.machinectl_copy_to(realm, "/realms/skel", "/home/user")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) if exists: machinectl copy-to /realms/realm-$name /home/user
|
||||||
|
let realm_skel = realm.base_path_file("skel");
|
||||||
|
if realm_skel.exists() {
|
||||||
|
self.machinectl_copy_to(realm, realm_skel.to_str().unwrap(), "/home/user")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let home = realm.base_path_file("home");
|
||||||
|
if !home.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
for dir in realm.config().ephemeral_persistent_dirs() {
|
||||||
|
let src = home.join(&dir);
|
||||||
|
if src.exists() {
|
||||||
|
let src = src.canonicalize()?;
|
||||||
|
if src.starts_with(&home) && src.exists() {
|
||||||
|
let dst = Path::new("/home/user").join(&dir);
|
||||||
|
self.machinectl_bind(realm, &src, &dst)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop_realm(&self, realm: &Realm) -> Result<()> {
|
||||||
|
self.systemctl_stop(&self.realm_service_name(realm))?;
|
||||||
|
self.remove_realm_launch_config(realm)?;
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn systemctl_stop(&self, name: &str) -> Result<bool> {
|
||||||
|
self.run_systemctl("stop", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_systemctl(&self, op: &str, name: &str) -> Result<bool> {
|
||||||
|
Command::new(SYSTEMCTL_PATH)
|
||||||
|
.arg(op)
|
||||||
|
.arg(name)
|
||||||
|
.status()
|
||||||
|
.map(|status| status.success())
|
||||||
|
.map_err(|e| format_err!("failed to execute {}: {}", MACHINECTL_PATH, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn machinectl_copy_to(&self, realm: &Realm, from: impl AsRef<Path>, to: &str) -> Result<()> {
|
||||||
|
let from = from.as_ref().to_str().unwrap();
|
||||||
|
info!("calling machinectl copy-to {} {} {}", realm.name(), from, to);
|
||||||
|
Command::new(MACHINECTL_PATH)
|
||||||
|
.args(&["copy-to", realm.name(), from, to ])
|
||||||
|
.status()
|
||||||
|
.map_err(|e| format_err!("failed to machinectl copy-to {} {} {}: {}", realm.name(), from, to, e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn machinectl_bind(&self, realm: &Realm, from: &Path, to: &Path) -> Result<()> {
|
||||||
|
let from = from.display().to_string();
|
||||||
|
let to = to.display().to_string();
|
||||||
|
Command::new(MACHINECTL_PATH)
|
||||||
|
.args(&["--mkdir", "bind", realm.name(), from.as_str(), to.as_str() ])
|
||||||
|
.status()
|
||||||
|
.map_err(|e| format_err!("failed to machinectl bind {} {} {}: {}", realm.name(), from, to, e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_active(realm: &Realm) -> Result<bool> {
|
||||||
|
Command::new(SYSTEMCTL_PATH)
|
||||||
|
.args(&["--quiet", "is-active"])
|
||||||
|
.arg(format!("realm-{}", realm.name()))
|
||||||
|
.status()
|
||||||
|
.map(|status| status.success())
|
||||||
|
.map_err(|e| format_err!("failed to execute {}: {}", SYSTEMCTL_PATH, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn are_realms_active(realms: &mut Vec<Realm>) -> Result<String> {
|
||||||
|
let args: Vec<String> = realms.iter()
|
||||||
|
.map(|r| format!("realm-{}", r.name()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let result = Command::new("/usr/bin/systemctl")
|
||||||
|
.arg("is-active")
|
||||||
|
.args(args)
|
||||||
|
.stderr(Stdio::inherit())
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
Ok(String::from_utf8(result.stdout).unwrap().trim().to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn machinectl_exec_shell(realm: &Realm, as_root: bool, launcher: bool) -> Result<()> {
|
||||||
|
let username = if as_root { "root" } else { "user" };
|
||||||
|
let args = ["/bin/bash".to_string()];
|
||||||
|
Self::machinectl_shell(realm, &args, username, launcher, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn machinectl_shell<S: AsRef<str>>(realm: &Realm, args: &[S], user: &str, launcher: bool, quiet: bool) -> Result<()> {
|
||||||
|
let mut cmd = Command::new(MACHINECTL_PATH);
|
||||||
|
cmd.arg("--quiet");
|
||||||
|
|
||||||
|
cmd.arg(format!("--setenv=REALM_NAME={}", realm.name()));
|
||||||
|
|
||||||
|
if let Ok(val) = env::var("DESKTOP_STARTUP_ID") {
|
||||||
|
cmd.arg(format!("--setenv=DESKTOP_STARTUP_ID={}", val));
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = realm.config();
|
||||||
|
if config.wayland() && !config.x11() {
|
||||||
|
cmd.arg("--setenv=GDK_BACKEND=wayland");
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.arg("shell");
|
||||||
|
cmd.arg(format!("{}@{}", user, realm.name()));
|
||||||
|
|
||||||
|
if launcher {
|
||||||
|
cmd.arg("/usr/libexec/launch");
|
||||||
|
}
|
||||||
|
|
||||||
|
if quiet {
|
||||||
|
cmd.stdin(Stdio::null());
|
||||||
|
cmd.stdout(Stdio::null());
|
||||||
|
cmd.stderr(Stdio::null());
|
||||||
|
}
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
cmd.arg(arg.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
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