refactor of Realm into a module with various components

This commit is contained in:
Bruce Leidl 2019-04-02 15:08:55 -04:00
parent 4b4e5f31e7
commit fcbf63db8e
10 changed files with 3056 additions and 0 deletions

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

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

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

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

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

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

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

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

View 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(&current_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
}
}

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