diff --git a/citadel-tools/citadel-appimg/Cargo.lock b/citadel-tools/citadel-appimg/Cargo.lock new file mode 100644 index 0000000..32c8d57 --- /dev/null +++ b/citadel-tools/citadel-appimg/Cargo.lock @@ -0,0 +1,242 @@ +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "atty" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-demangle 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bitflags" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cc" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "citadel-appimg" +version = "0.1.0" +dependencies = [ + "clap 2.31.1 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "clap" +version = "2.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure_derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", + "synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lazy_static" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "redox_syscall" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "strsim" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synstructure" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termion" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "textwrap" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-width" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "vec_map" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +"checksum atty 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "af80143d6f7608d746df1520709e5d141c96f240b0e62b0aa41bdfb53374d9d4" +"checksum backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ebbbf59b1c43eefa8c3ede390fcc36820b4999f7914104015be25025e0d62af2" +"checksum backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "44585761d6161b0f57afc49482ab6bd067e4edef48c12a152c237eb0203f7661" +"checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf" +"checksum cc 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "9be26b24e988625409b19736d130f0c7d224f01d06454b5f81d8d23d6c1a618f" +"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" +"checksum clap 2.31.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5dc18f6f4005132120d9711636b32c46a233fad94df6217fa1d81c5e97a9f200" +"checksum failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "934799b6c1de475a012a02dab0ace1ace43789ee4b99bcfbf1a2e3e8ced5de82" +"checksum failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cdda555bb90c9bb67a3b670a0f42de8e73f5981524123ad8578aafec8ddb8b" +"checksum lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c8f31047daa365f19be14b47c29df4f7c3b581832407daabe6ae77397619237d" +"checksum libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)" = "f54263ad99207254cf58b5f701ecb432c717445ea2ee8af387334bdd1a03fdff" +"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" +"checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd" +"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +"checksum rustc-demangle 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11fb43a206a04116ffd7cfcf9bcb941f8eb6cc7ff667272246b0a1c74259a3cb" +"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" +"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +"checksum synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a761d12e6d8dcb4dcf952a7a89b475e3a9d69e4a69307e01a470977642914bd" +"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" +"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693" +"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f" +"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" +"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c" +"checksum winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/citadel-tools/citadel-appimg/Cargo.toml b/citadel-tools/citadel-appimg/Cargo.toml new file mode 100644 index 0000000..7ff7ccc --- /dev/null +++ b/citadel-tools/citadel-appimg/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "citadel-appimg" +version = "0.1.0" +authors = ["Bruce Leidl "] +homepage = "http://github.com/subgraph/citadel" + +[dependencies] +clap = "2.30.0" +failure = "0.1.1" +lazy_static = "1.0.0" diff --git a/citadel-tools/citadel-appimg/src/appimg.rs b/citadel-tools/citadel-appimg/src/appimg.rs new file mode 100644 index 0000000..7569de9 --- /dev/null +++ b/citadel-tools/citadel-appimg/src/appimg.rs @@ -0,0 +1,216 @@ + +use std::path::{Path,PathBuf}; +use std::fs::{self,File}; +use std::io::{BufRead,BufReader}; + +use Result; +use systemd; + +use util::*; + +lazy_static!{ + static ref APPIMG_BASE_PATH: PathBuf = PathBuf::from("/storage/appimg"); + static ref USERDATA_BASE_PATH: PathBuf = PathBuf::from("/storage/user-data"); + static ref SYSTEMD_UNIT_PATH: PathBuf = PathBuf::from("/run/systemd/system"); + static ref SYSTEMD_NSPAWN_PATH: PathBuf = PathBuf::from("/run/systemd/nspawn"); +} + + + +#[derive(Debug)] +pub struct AppImg { + name: String, + config: AppImgConfig, +} + +impl AppImg { + + pub fn new(name: &str) -> Result { + let mut img = AppImg { + name: name.to_string(), + config: AppImgConfig::new(name), + }; + img.load_config()?; + img.write_systemd_files()?; + Ok(img) + } + + fn load_config(&mut self) -> Result<()> { + let mut path = APPIMG_BASE_PATH.join(&self.name); + path.push("config"); + if !path.exists() { + return Ok(()); + } + self.config.load(&path)?; + Ok(()) + } + + fn write_systemd_files(&self) -> Result<()> { + if !self.nspawn_config_path().exists() { + self.write_nspawn_config()?; + } + if !self.service_unit_path().exists() { + self.write_service_unit()?; + } + Ok(()) + } + + pub fn is_running(&self) -> bool { + systemd::sysctl_is_active(&self.service_unit_name()) + } + + pub fn start(&self) -> Result<()> { + if self.is_running() { + warn!("image {} is already running", self.name); + return Ok(()); + } + self.write_nspawn_config()?; + self.write_service_unit()?; + systemd::systemctl_start(&self.service_unit_name()); + Ok(()) + + } + + pub fn stop(&self) { + if !self.is_running() { + warn!("image {} is not running", self.name); + return; + } + systemd::systemctl_stop(&self.service_unit_name()); + } + + pub fn name(&self) -> &str { + &self.name + } + + fn nspawn_config_path(&self) -> PathBuf { + SYSTEMD_NSPAWN_PATH.join(&format!("{}.nspawn", self.name)) + } + + fn service_unit_name(&self) -> String { + format!("appimg-{}.service", self.name) + } + + fn service_unit_path(&self) -> PathBuf { + SYSTEMD_UNIT_PATH.join(&format!("appimg-{}.service", self.name)) + } + + fn write_nspawn_config(&self) -> Result<()> { + let mut extra = String::new(); + extra += &format!("Bind={}:/home/user\n", self.config.home()); + if self.config.use_kvm() { + extra += "Bind=/dev/kvm\n"; + } + if self.config.use_gpu() { + extra += "Bind=/dev/dri/renderD128\n"; + } + + let content = systemd::generate_nspawn_file(&extra); + fs::create_dir_all(SYSTEMD_NSPAWN_PATH.as_path())?; + write_string_to_file(&self.nspawn_config_path(), &content)?; + + Ok(()) + } + + fn write_service_unit(&self) -> Result<()> { + let content = systemd::generate_service_file(&self.name); + fs::create_dir_all(&SYSTEMD_UNIT_PATH.as_path())?; + + write_string_to_file(&self.service_unit_path(), &content)?; + Ok(()) + } +} + +#[derive(Debug)] +pub struct AppImgConfig { + img_name: String, + home: Option, + kvm: bool, + gpu: bool, +} + +const DEFAULT_HOME_PATH: &str = "/storage/user-data/primary-home"; + +impl AppImgConfig { + fn new(img_name: &str) -> AppImgConfig { + AppImgConfig{ + img_name: img_name.to_string(), + home: None, + kvm: false, + gpu: false, + } + } + + pub fn home(&self) -> String { + if let Some(ref name) = self.home { + if is_valid_name(name) { + let home_path = USERDATA_BASE_PATH.join(name); + return home_path.to_str().unwrap().to_string(); + } + } + DEFAULT_HOME_PATH.to_string() + } + + pub fn use_kvm(&self) -> bool { + self.kvm + + } + + pub fn use_gpu(&self) -> bool { + self.gpu + } + + fn reset(&mut self) { + self.home = None; + self.kvm = false; + self.gpu = false; + } + + fn load(&mut self, path: &Path) -> Result<()> { + self.reset(); + let f = File::open(path)?; + let reader = BufReader::new(f); + for line in reader.lines() { + let line = line?; + let v = line.split('=').collect::>(); + if v.len() == 2 { + self.process_keyval(v[0].trim(), v[1].trim()); + + } + } + Ok(()) + } + + fn process_keyval(&mut self, k: &str, v: &str) { + match k { + + "home" => { + if is_valid_name(v) { + let home_path = USERDATA_BASE_PATH.join(v); + if !home_path.is_dir() { + warn!("'home' value '{}' in config file of image {} refers to directory that doesn't exist", v, self.img_name); + + } + self.home = Some(v.to_string()); + } else { + warn!("Invalid home value '{}' in config file of image {}", v, self.img_name); + } + }, + "use-kvm" => { + self.kvm = v == "yes"; + + }, + + "use-gpu" => { + self.gpu = v == "yes"; + }, + + _ => { + warn!("unknown keyword '{}' in config file for image {}", k, self.img_name); + }, + } + } + + +} + diff --git a/citadel-tools/citadel-appimg/src/main.rs b/citadel-tools/citadel-appimg/src/main.rs new file mode 100644 index 0000000..9ef393f --- /dev/null +++ b/citadel-tools/citadel-appimg/src/main.rs @@ -0,0 +1,94 @@ +#[macro_use] extern crate failure; +#[macro_use] extern crate lazy_static; + +extern crate clap; + +macro_rules! warn { + ($e:expr) => { println!("[!]: {}", $e); }; + ($fmt:expr, $($arg:tt)+) => { println!("[!]: {}", format!($fmt, $($arg)+)); }; +} + +mod appimg; +mod util; +mod systemd; +mod manager; + +use failure::Error; +use clap::{App,Arg,ArgMatches,SubCommand}; +use clap::AppSettings::*; +use std::process::exit; +use std::result; + +pub type Result = result::Result; + +pub use appimg::AppImg; +use manager::ImageManager; + +fn main() { + let matches = App::new("citadel-appimg") + .about("Subgraph Citadel application image management") + .settings(&[ArgRequiredElseHelp, ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder]) + + .subcommand(SubCommand::with_name("list") + .about("Display list of application images")) + + .subcommand(SubCommand::with_name("start") + .about("Launch an application image") + .arg(Arg::with_name("name"))) + + .subcommand(SubCommand::with_name("stop") + .about("Stop a running application image") + .arg(Arg::with_name("name").required(true))) + + .subcommand(SubCommand::with_name("default") + .about("Set an application image as the default image to boot") + .arg(Arg::with_name("name").required(true))) + + .get_matches(); + + let result = match matches.subcommand() { + ("list", _) => list_cmd(), + ("start", Some(m)) => start_cmd(m), + ("stop", Some(m)) => stop_cmd(m), + ("default", Some(m)) => default_cmd(m), + _ => Ok(()), + }; + if let Err(e) = result { + println!("{}", e); + exit(1); + } +} + + + +fn list_cmd() -> Result<()> { + let manager = ImageManager::load()?; + manager.list()?; + Ok(()) +} + +fn start_cmd(matches: &ArgMatches) -> Result<()> { + let mut manager = ImageManager::load()?; + match matches.value_of("name") { + Some(name) => manager.start_image(name), + None => manager.start_default(), + } +} + +fn stop_cmd(matches: &ArgMatches) -> Result<()> { + let name = matches.value_of("name").unwrap(); + let mut manager = ImageManager::load()?; + manager.stop_image(name)?; + Ok(()) +} + +fn default_cmd(matches: &ArgMatches) -> Result<()> { + let name = matches.value_of("name").unwrap(); + let mut manager = ImageManager::load()?; + if manager.image_exists(name) { + manager.set_default(name)?; + } else { + warn!("No image '{}' exists", name); + } + Ok(()) +} diff --git a/citadel-tools/citadel-appimg/src/manager.rs b/citadel-tools/citadel-appimg/src/manager.rs new file mode 100644 index 0000000..490c2cf --- /dev/null +++ b/citadel-tools/citadel-appimg/src/manager.rs @@ -0,0 +1,264 @@ +use std::path::{Path,PathBuf}; +use std::io::Write; +use std::fs; +use std::os::unix::fs::{OpenOptionsExt,symlink}; +use std::collections::HashMap; + +use AppImg; +use Result; +use util::*; +use systemd; + +lazy_static!{ + static ref APPIMG_BASE_PATH: PathBuf = PathBuf::from("/storage/appimg"); + static ref APPIMG_RUN_PATH: PathBuf = PathBuf::from("/run/appimg"); +} + + +pub struct ImageManager { + images: HashMap, + default: Option, + current: Option, +} + + +impl ImageManager { + fn new() -> Result { + let default = default_appimg()?; + let current = current_appimg()?; + Ok(ImageManager { + images: HashMap::new(), + default, current, + }) + } + + pub fn load() -> Result { + let mut manager = ImageManager::new()?; + for dent in fs::read_dir(APPIMG_BASE_PATH.as_path())? { + let path = dent?.path(); + manager.process_path(&path)?; + } + Ok(manager) + } + + fn process_path(&mut self, path: &Path) -> Result<()> { + let meta = path.symlink_metadata()?; + if !meta.is_dir() { + return Ok(()) + } + let name = path_filename(path); + if !is_valid_name(name) { + warn!("ignoring directory in appimg storage which has invalid appimg name: {}", name); + return Ok(()) + } + + let appimg = AppImg::new(name)?; + println!("adding: {}", appimg.name()); + self.images.insert(appimg.name().to_string(), appimg); + Ok(()) + } + + pub fn list(&self) -> Result<()> { + for img in self.images.values() { + let cur = if self.is_current(img) { "(current)" } else { "" }; + let def = if self.is_default(img) { "(default)" } else { "" }; + let run = if img.is_running() { "[running]"} else {""}; + println!(" {} {} {} {}", img.name(), run, def, cur); + } + + Ok(()) + } + + pub fn start_default(&mut self) -> Result<()> { + let name = match self.default { + Some(ref s) => s.clone(), + None => bail!("No default image to start"), + }; + self.start_image(&name) + + } + + pub fn start_image(&mut self, name: &str) -> Result<()> { + match self.images.get(name) { + Some(img) => { + img.start()?; + }, + None => { + warn!("Cannot start '{}'. Image does not exist"); + return Ok(()) + }, + } + // if current is not set, set it to this one + if self.current.is_none() { + self.set_current(name)?; + systemd::systemctl_restart("desktopd"); + } + Ok(()) + } + + pub fn stop_image(&mut self, name: &str) -> Result<()> { + match self.images.get(name) { + Some(img) => img.stop(), + None => { + warn!("Cannot stop '{}'. Image does not exist"); + return Ok(()) + }, + } + let current = match self.current { + Some(ref s) => s.clone(), + None => return Ok(()), + }; + if current == name { + systemd::systemctl_stop("desktopd"); + let path = APPIMG_RUN_PATH.join("current.appimg"); + if path.exists() { + fs::remove_file(&path)?; + } + if let Some(img_name) = self.find_running_image_name() { + self.set_current(&img_name)?; + systemd::systemctl_start("desktopd"); + } + } + Ok(()) + } + + fn find_running_image_name(&self) -> Option { + for img in self.images.values() { + if img.is_running() { + return Some(img.name().to_string()); + } + } + None + } + + fn is_current(&self, img: &AppImg) -> bool { + self.same_name(img, &self.current) + } + + fn is_default(&self, img: &AppImg) -> bool { + self.same_name(img, &self.default) + } + + fn same_name(&self, img: &AppImg, name: &Option) -> bool { + if let Some(ref name) = *name { + name == img.name() + } else { + false + } + } + pub fn image_exists(&self, name: &str) -> bool { + self.images.contains_key(name) + } + + + pub fn set_default(&mut self, name: &str) -> Result<()> { + if !is_valid_name(name) { + warn!("{} is not a valid image name", name); + return Ok(()) + } + + if let Some(ref default) = self.default { + if default == name { + warn!("{} is already default appimg", name); + return Ok(()) + } + } + + let path = APPIMG_BASE_PATH.join("default.appimg"); + if path.exists() { + fs::remove_file(&path)?; + } + symlink(name, &path)?; + self.default = Some(name.to_string()); + Ok(()) + } + + pub fn set_current(&mut self, name: &str) -> Result<()> { + if !is_valid_name(name) { + warn!("{} is not a valid image name", name); + return Ok(()) + } + if let Some(ref current) = self.current { + if current == name { + warn!("{} is already current appimg", name); + return Ok(()) + } + } + + fs::create_dir_all(APPIMG_RUN_PATH.as_path())?; + let path = APPIMG_RUN_PATH.join("current.appimg"); + let target = APPIMG_BASE_PATH.join(name); + if path.exists() { + fs::remove_file(&path)?; + } + symlink(&target, &path)?; + + let script = format!("#!/bin/bash\nmachinectl -E DESKTOP_STARTUP_ID=${{DESKTOP_STARTUP_ID}} shell user@{} /usr/libexec/launch $@\n", name); + let script_path = APPIMG_RUN_PATH.join("run-in-image"); + let mut f = fs::OpenOptions::new() + .create(true) + .write(true) + .mode(0o755) + .open(&script_path)?; + + f.write_all(script.as_bytes())?; + self.current = Some(name.to_string()); + Ok(()) + } +} + +fn appimg_symlink(symlink: &Path) -> Result> { + if !symlink.exists() { + return Ok(None); + } + + if !symlink.symlink_metadata()?.file_type().is_symlink() { + bail!("{} exists but it is not a symlink", symlink.display()); + } + + let link = fs::read_link(&symlink)?; + + let appimg_name = appimg_name_for_symlink_target(&link)?; + if !is_valid_name(&appimg_name) { + bail!("symlink {} points to a directory with a name ({}) that is not a valid appimg name", symlink.display(), appimg_name); + } + + Ok(Some(appimg_name)) +} + +fn default_appimg() -> Result> { + appimg_symlink(&APPIMG_BASE_PATH.join("default.appimg")) +} + +fn current_appimg() -> Result> { + appimg_symlink(&APPIMG_RUN_PATH.join("current.appimg")) +} + +/// +/// Returns a name only if target points to some subdirectory of APPIMG_BASE_PATH +/// +fn appimg_name_for_symlink_target(target: &Path) -> Result { + let path = if target.is_absolute() { + target.to_path_buf() + } else if target.components().count() == 1 { + APPIMG_BASE_PATH.join(target) + } else { + bail!("symlink target has invalid value: {}", target.display()) + }; + + match path.parent() { + Some(parent) => { + if parent != APPIMG_BASE_PATH.as_path() { + bail!("symlink target points outside of /storage/appimg directory"); + } + }, + None => { + bail!("symlink target has invalid value (no parent): {}", target.display()) + }, + }; + + if !path.is_dir() { + bail!("symlink target {} is not a directory", path.display()); + } + Ok(path_filename(&path).to_string()) +} diff --git a/citadel-tools/citadel-appimg/src/systemd.rs b/citadel-tools/citadel-appimg/src/systemd.rs new file mode 100644 index 0000000..0d8ca67 --- /dev/null +++ b/citadel-tools/citadel-appimg/src/systemd.rs @@ -0,0 +1,113 @@ + +use std::process::Command; + +const SYSTEMCTL_PATH: &str = "/usr/bin/systemctl"; + +pub fn sysctl_is_active(name: &str) -> bool { + let mut cmd = Command::new(SYSTEMCTL_PATH); + cmd.arg("-q"); + cmd.arg("is-active"); + cmd.arg(name); + match cmd.status() { + Ok(status) => status.success(), + Err(e) => { + warn!("failed to execute /usr/bin/systemctl: {}", e); + false + } + } +} + +pub fn systemctl_restart(name: &str) -> bool { + run_systemctl("restart", name) +} + +pub fn systemctl_start(name: &str) -> bool { + run_systemctl("start", name) +} + +pub fn systemctl_stop(name: &str) -> bool { + run_systemctl("stop", name) +} + +fn run_systemctl(op: &str, name: &str) -> bool { + let mut cmd = Command::new(SYSTEMCTL_PATH); + cmd.arg(op); + cmd.arg(name); + match cmd.output() { + Err(e) => { + warn!("failed to execute /usr/bin/systemctl: {}", e); + false + } + Ok(output) => { + if !output.status.success() { + warn!("error running systemctl {}: {}", op, String::from_utf8(output.stderr).unwrap()); + return false + } + true + } + } +} + +pub fn generate_nspawn_file(extra_bind_mounts: &str) -> String { + NSPAWN_FILE_TEMPLATE.replace("$EXTRA_BIND_MOUNTS", extra_bind_mounts) +} + +pub fn generate_service_file(appimg_name: &str) -> String { + APPIMG_SERVICE_TEMPLATE.replace("$APPIMG_NAME", appimg_name) +} + +pub const NSPAWN_FILE_TEMPLATE: &str = r###" +[Exec] +Boot=true +Environment=IFCONFIG_IP=172.17.0.2/24 +Environment=IFCONFIG_GW=172.17.0.1 + +[Files] +BindReadOnly=/usr/share/themes/Adapta +BindReadOnly=/usr/share/themes/Adapta-Eta +BindReadOnly=/usr/share/themes/Adapta-Nokto +BindReadOnly=/usr/share/themes/Adapta-Nokto-Eta +BindReadOnly=/usr/share/icons/Paper + +BindReadOnly=/storage/citadel-state/resolv.conf:/etc/resolv.conf + +# +# Bind mounts for sound and pulse audio +# +Bind=/dev/snd +Bind=/dev/shm +BindReadOnly=/run/user/1000/pulse:/run/user/host/pulse + +BindReadOnly=/tmp/.X11-unix +BindReadOnly=/run/user/1000/wayland-0:/run/user/host/wayland-0 + +$EXTRA_BIND_MOUNTS + +# +# Uncomment to enable kvm access in container +# +#Bind=/dev/kvm + +# +# Uncomment to enable GPU access in container +# +#Bind=/dev/dri/renderD128 + +[Network] +Zone=clear +"###; + +pub const APPIMG_SERVICE_TEMPLATE: &str = r###" +[Unit] +Description=Application Image $APPIMG_NAME instance +Wants=desktopd.service + +[Service] +Environment=SYSTEMD_NSPAWN_SHARE_NS_IPC=1 +ExecStart=/usr/bin/systemd-nspawn --quiet --keep-unit --machine=$APPIMG_NAME --link-journal=try-guest --directory=/storage/appimg/$APPIMG_NAME/rootfs + +KillMode=mixed +Type=notify +RestartForceExitStatus=133 +SuccessExitStatus=133 +"###; diff --git a/citadel-tools/citadel-appimg/src/util.rs b/citadel-tools/citadel-appimg/src/util.rs new file mode 100644 index 0000000..5a005e8 --- /dev/null +++ b/citadel-tools/citadel-appimg/src/util.rs @@ -0,0 +1,39 @@ +use std::io::Write; +use std::fs::File; +use std::path::Path; + +use Result; + +pub fn path_filename(path: &Path) -> &str { + if let Some(osstr) = path.file_name() { + if let Some(name) = osstr.to_str() { + return name; + } + } + "" +} + +pub fn write_string_to_file(path: &Path, s: &str) -> Result<()> { + let mut f = File::create(path)?; + f.write_all(s.as_bytes())?; + Ok(()) +} + +fn is_alphanum_or_dash(c: char) -> bool { + is_ascii(c) && (c.is_alphanumeric() || c == '-') +} + +fn is_ascii(c: char) -> bool { + c as u32 <= 0x7F +} + +fn is_first_char_alphabetic(s: &str) -> bool { + if let Some(c) = s.chars().next() { + return is_ascii(c) && c.is_alphabetic() + } + false +} + +pub fn is_valid_name(name: &str) -> bool { + is_first_char_alphabetic(name) && name.chars().all(is_alphanum_or_dash) +} diff --git a/citadel-tools/citadel-desktopd/Cargo.lock b/citadel-tools/citadel-desktopd/Cargo.lock new file mode 100644 index 0000000..05575ae --- /dev/null +++ b/citadel-tools/citadel-desktopd/Cargo.lock @@ -0,0 +1,479 @@ +[[package]] +name = "aho-corasick" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "atty" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-demangle 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bitflags" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byteorder" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bytes" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cc" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chrono" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "desktopd" +version = "0.1.0" +dependencies = [ + "env_logger 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "inotify 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "nix 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "env_logger" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "atty 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure_derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", + "synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gcc" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "inotify" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "inotify-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "inotify-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "iovec" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lazy_static" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "log" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memchr" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "nix" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", + "num-iter 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-iter" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "redox_syscall" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rustc-demangle" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_derive" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive_internals 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive_internals" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", + "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synstructure" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termcolor" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "wincolor 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termion" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "toml" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "utf8-ranges" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "wincolor" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4" +"checksum atty 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8352656fd42c30a0c3c89d26dea01e3b77c0ab2af18230835c15e2e13cd51859" +"checksum backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ebbbf59b1c43eefa8c3ede390fcc36820b4999f7914104015be25025e0d62af2" +"checksum backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "44585761d6161b0f57afc49482ab6bd067e4edef48c12a152c237eb0203f7661" +"checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf" +"checksum byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "652805b7e73fada9d85e9a6682a4abd490cb52d96aeecc12e33a0de34dfd0d23" +"checksum bytes 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1b7db437d718977f6dc9b2e3fd6fc343c02ac6b899b73fdd2179163447bd9ce9" +"checksum cc 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "deaf9ec656256bb25b404c51ef50097207b9cbb29c933d31f92cae5a8a0ffee0" +"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" +"checksum chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7c20ebe0b2b08b0aeddba49c609fe7957ba2e33449882cb186a180bc60682fa9" +"checksum env_logger 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f15f0b172cb4f52ed5dbf47f774a387cd2315d1bf7894ab5af9b083ae27efa5a" +"checksum failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "934799b6c1de475a012a02dab0ace1ace43789ee4b99bcfbf1a2e3e8ced5de82" +"checksum failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cdda555bb90c9bb67a3b670a0f42de8e73f5981524123ad8578aafec8ddb8b" +"checksum gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)" = "5e33ec290da0d127825013597dbdfc28bee4964690c7ce1166cbc2a7bd08b1bb" +"checksum inotify 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "41aaf46578a4628ff6c17c30993aed7e5188fae0817c78c558d3b7baaba1ffe5" +"checksum inotify-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7dceb94c43f70baf4c4cd6afbc1e9037d4161dbe68df8a2cd4351a23319ee4fb" +"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" +"checksum lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c8f31047daa365f19be14b47c29df4f7c3b581832407daabe6ae77397619237d" +"checksum libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "1e5d97d6708edaa407429faa671b942dc0f2727222fb6b6539bf1db936e4b121" +"checksum log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "89f010e843f2b1a31dbd316b3b8d443758bc634bed37aabade59c686d644e0a2" +"checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d" +"checksum nix 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b7fd5681d13fda646462cfbd4e5f2051279a89a544d50eb98c365b507246839f" +"checksum num 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "cc4083e14b542ea3eb9b5f33ff48bd373a92d78687e74f4cc0a30caeb754f0ca" +"checksum num-integer 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f8d26da319fb45674985c78f1d1caf99aa4941f785d384a2ae36d0740bc3e2fe" +"checksum num-iter 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "4b226df12c5a59b63569dd57fafb926d91b385dfce33d8074a412411b689d593" +"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +"checksum num-traits 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e7de20f146db9d920c45ee8ed8f71681fd9ade71909b48c3acbd766aa504cf10" +"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" +"checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd" +"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +"checksum regex 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "744554e01ccbd98fff8c457c3b092cd67af62a555a43bfe97ae8a0451f7799fa" +"checksum regex-syntax 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8e931c58b93d86f080c734bfd2bce7dd0079ae2331235818133c8be7f422e20e" +"checksum rustc-demangle 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "aee45432acc62f7b9a108cc054142dac51f979e69e71ddce7d6fc7adf29e817e" +"checksum serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = "db99f3919e20faa51bb2996057f5031d8685019b5a06139b1ce761da671b8526" +"checksum serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = "f4ba7591cfe93755e89eeecdbcc668885624829b020050e6aec99c2a03bd3fd0" +"checksum serde_derive_internals 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6e03f1c9530c3fb0a0a5c9b826bdd9246a5921ae995d75f512ac917fc4dd55b5" +"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +"checksum synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a761d12e6d8dcb4dcf952a7a89b475e3a9d69e4a69307e01a470977642914bd" +"checksum termcolor 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9065bced9c3e43453aa3d56f1e98590b8455b341d2fa191a1090c0dd0b242c75" +"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" +"checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963" +"checksum time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "a15375f1df02096fb3317256ce2cee6a1f42fc84ea5ad5fc8c421cfe40c73098" +"checksum toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "a7540f4ffc193e0d3c94121edb19b055670d369f77d5804db11ae053a45b6e7e" +"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" +"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +"checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122" +"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum wincolor 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0878187fa88838d2006c0a76f30d64797098426245b375383f60acb6aed8a203" diff --git a/citadel-tools/citadel-desktopd/Cargo.toml b/citadel-tools/citadel-desktopd/Cargo.toml new file mode 100644 index 0000000..d128b66 --- /dev/null +++ b/citadel-tools/citadel-desktopd/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "citadel-desktopd" +version = "0.1.0" +authors = ["brl@subgraph.com"] + +[dependencies] +failure = "0.1.1" +lazy_static = "1.0" +log = "0.4.0" +env_logger = "0.5.3" +inotify = "0.5" +toml = "0.4.5" +serde_derive = "1.0.27" +serde = "1.0.27" +nix = "0.10.0" diff --git a/citadel-tools/citadel-desktopd/conf/citadel-desktopd.conf b/citadel-tools/citadel-desktopd/conf/citadel-desktopd.conf new file mode 100644 index 0000000..02dc19d --- /dev/null +++ b/citadel-tools/citadel-desktopd/conf/citadel-desktopd.conf @@ -0,0 +1,6 @@ +[options] +exec_prefix="/run/appimg/run-in-image" +target_directory="/home/citadel/.local/share/applications" + +[[sources]] +path="/run/appimg/current.appimg/rootfs/usr/share/applications" diff --git a/citadel-tools/citadel-desktopd/conf/citadel-desktopd.service b/citadel-tools/citadel-desktopd/conf/citadel-desktopd.service new file mode 100644 index 0000000..9618a76 --- /dev/null +++ b/citadel-tools/citadel-desktopd/conf/citadel-desktopd.service @@ -0,0 +1,8 @@ +[Unit] +Description=Desktop Integration Manager + +[Service] +ExecStart=/usr/libexec/citadel-desktopd /usr/share/citadel/citadel-desktopd.conf + +[Install] +WantedBy=graphical.target diff --git a/citadel-tools/citadel-desktopd/src/config.rs b/citadel-tools/citadel-desktopd/src/config.rs new file mode 100644 index 0000000..5760e9d --- /dev/null +++ b/citadel-tools/citadel-desktopd/src/config.rs @@ -0,0 +1,120 @@ +use std::path::{Path,PathBuf}; +use std::fs::File; +use std::io::Read; +use Result; +use toml; + +#[derive(Clone)] +pub struct Config { + exec_prefix: String, + target_directory: PathBuf, + source_paths: Vec, +} + + +impl Config { + pub fn from_path>(path: P) -> Result { + let toml = ConfigToml::from_path(path)?; + let config = toml.to_config()?; + Ok(config) + } + + pub fn exec_prefix(&self) -> &str { + self.exec_prefix.as_ref() + } + + pub fn target_directory(&self) -> &Path { + self.target_directory.as_ref() + } + + pub fn source_paths(&self) -> &Vec { + self.source_paths.as_ref() + } +} + +#[derive(Deserialize)] +struct ConfigToml { + options: Option, + sources: Option>, +} + +#[derive(Deserialize)] +struct Options { + exec_prefix: Option, + target_directory: Option, +} + +impl Options { + fn exec_prefix(&self) -> Result<&str> { + match self.exec_prefix { + Some(ref s) => Ok(s.as_str()), + None => Err(format_err!("missing 'exec_prefix=' field")), + } + } + + fn target_directory(&self) -> Result<&str> { + match self.target_directory { + Some(ref s) => Ok(s.as_str()), + None => Err(format_err!("missing 'target_directory=' field")), + } + } +} + +#[derive(Deserialize,Clone)] +struct Source { + path: Option, +} + +impl Source { + fn path(&self) -> Result<&str> { + match self.path { + Some(ref s) => Ok(s.as_str()), + None => Err(format_err!("missing 'path=' field")), + } + } +} + +fn load_as_string(path: &Path) -> Result { + let mut f = File::open(path)?; + let mut buffer = String::new(); + f.read_to_string(&mut buffer)?; + Ok(buffer) +} + +impl ConfigToml { + fn from_path>(path: P) -> Result { + let s = load_as_string(path.as_ref())?; + let config = toml::from_str::(&s)?; + Ok(config) + } + + fn options(&self) -> Result<&Options> { + self.options.as_ref() + .ok_or(format_err!("missing '[options]' section")) + } + + fn sources(&self) -> Result> { + match self.sources { + Some(ref srcs) => Ok(srcs.clone()), + None => Err(format_err!("missing '[[sources]]' section(s)")), + } + } + + fn to_config(&self) -> Result { + let options = self.options()?; + let exec_prefix = options.exec_prefix()?.to_string() + " "; + let target_path = options.target_directory()?.to_string(); + let target_directory = PathBuf::from(target_path); + + let mut source_paths = Vec::new(); + for src in self.sources()? { + let path = src.path()?; + source_paths.push(PathBuf::from(path)); + } + Ok(Config{ + exec_prefix, + target_directory, + source_paths, + }) + } +} diff --git a/citadel-tools/citadel-desktopd/src/desktop.rs b/citadel-tools/citadel-desktopd/src/desktop.rs new file mode 100644 index 0000000..542c6a5 --- /dev/null +++ b/citadel-tools/citadel-desktopd/src/desktop.rs @@ -0,0 +1,188 @@ +use std::io::Write; +use std::fs::File; +use std::path::Path; +use std::collections::HashMap; +use Result; + + +pub struct DesktopFile { + filename: String, + lines: Vec, + // map from key of key/value pair to index of line in lines vector + main_map: HashMap, + // map from group name to map of key/value -> index + groups: HashMap>, +} + + +impl DesktopFile { + + pub fn write_to_dir>(&self, directory: P) -> Result<()> { + let mut path = directory.as_ref().to_path_buf(); + path.push(self.filename.as_str()); + let f = File::create(&path)?; + self.write_to(f)?; + Ok(()) + } + + pub fn write_to(&self, mut w: W) -> Result<()> { + for line in &self.lines { + line.write_to(&mut w)?; + } + Ok(()) + } + + pub fn filename(&self) -> &str { + self.filename.as_ref() + } + + // + // Conditions for translating a .desktop entry + // + // Type=Application // Mandatory 'Type' key must be Application + // NotShowIn= // If 'NotShowIn' key is present, must not contain GNOME + // OnlyShowIn= // If 'OnlyShowIn' key is present, must contain 'GNOME' + // Terminal=false // If 'Terminal' key is present, must be false + // Hidden=false // If 'Hidden' key is present, must be false + // + pub fn is_showable(&self) -> bool { + self.is_application_type() && self.show_in_gnome() && + !(self.needs_terminal() || self.is_hidden()) + } + + fn needs_terminal(&self) -> bool { + self.key_exists_and_not_false("Terminal") + } + + fn is_application_type(&self) -> bool { + if let Some(t) = self.get_key_val("Type") { + return t == "Application" + } + false + } + + fn show_in_gnome(&self) -> bool { + if self.key_exists("NotShowIn") && self.key_value_contains("NotShowIn", "GNOME") { + return false; + } + if self.key_exists("OnlyShowIn") && !self.key_value_contains("OnlyShowIn", "GNOME") { + return false; + } + true + } + + fn key_value_contains(&self, key: &str, s: &str) -> bool { + match self.get_key_val(key) { + Some(val) => val.contains(s), + None => false, + } + } + + fn key_exists(&self, key: &str) -> bool { + self.main_map.contains_key(key) + } + + fn is_hidden(&self) -> bool { + self.key_exists_and_not_false("Hidden") + } + + fn key_exists_and_not_false(&self, key: &str) -> bool { + if let Some(s) = self.get_key_val(key) { + if s != "false" { + return true; + } + } + false + } + + fn get_key_val(&self, key: &str) -> Option<&str> { + if let Some(idx) = self.main_map.get(key) { + match self.lines[*idx] { + Line::KeyValue(_, ref v) => return Some(v), + ref line => panic!("Key lookup on '{}' returned wrong line type: {:?}", key, line), + } + } + None + } + + + pub fn new(filename: &str) -> DesktopFile { + DesktopFile { + filename: filename.to_string(), + lines: Vec::new(), + main_map: HashMap::new(), + groups: HashMap::new(), + } + } + + pub fn add_line(&mut self, line: Line) { + if line.is_key_value_type() { + let idx = self.lines.len(); + self.main_map.insert(line.get_key_string(), idx); + } + self.lines.push(line); + } + + pub fn add_action_line(&mut self, action: &str, line: Line) { + if line.is_key_value_type() { + let idx = self.lines.len(); + let map = self.groups.entry(action.to_string()).or_insert(HashMap::new()); + map.insert(line.get_key_string(), idx); + } + self.lines.push(line); + } + + +} + + +#[derive(Debug)] +pub enum Line { + Empty, + Comment(String), + ExecLine(String), + KeyValue(String,String), + KeyLocaleValue(String,String,String), + DesktopHeader, + ActionHeader(String), + GroupHeader(String) +} + + +impl Line { + pub fn is_action_header(&self) -> bool { + match *self { + Line::ActionHeader(..) => true, + _ => false, + } + } + + fn is_key_value_type(&self) -> bool { + match *self { + Line::KeyValue(..) | Line::KeyLocaleValue(..) => true, + _ => false, + } + } + + fn get_key_string(&self) -> String { + match *self { + Line::KeyValue(ref k, ..) => k.to_string(), + Line::KeyLocaleValue(ref k, ref loc, ..) => format!("{}[{}]", k, loc), + _ => panic!("get_key_string() called on Line item which is not a key/value type"), + } + } + + fn write_to(&self, mut w: W) -> Result<()> { + match *self { + Line::Empty => writeln!(w)?, + Line::Comment(ref s) => writeln!(w, "#{}", s)?, + Line::ExecLine(ref s) => writeln!(w, "Exec={}", s)?, + Line::KeyValue(ref k, ref v) => writeln!(w, "{}={}", k, v)?, + Line::KeyLocaleValue(ref k, ref loc, ref v) => writeln!(w, "{}[{}]={}", k, loc, v)?, + Line::DesktopHeader => writeln!(w, "[Desktop Entry]")?, + Line::ActionHeader(ref action) => writeln!(w, "[Desktop Action {}]", action)?, + Line::GroupHeader(..) => {}, + } + Ok(()) + } +} diff --git a/citadel-tools/citadel-desktopd/src/desktop_file_sync.rs b/citadel-tools/citadel-desktopd/src/desktop_file_sync.rs new file mode 100644 index 0000000..573f623 --- /dev/null +++ b/citadel-tools/citadel-desktopd/src/desktop_file_sync.rs @@ -0,0 +1,120 @@ +use std::sync::{Arc,Mutex}; +use std::path::Path; +use std::fs; + +use failure::ResultExt; + +use monitor::{DirectoryMonitor,MonitorEventHandler}; +use parser::DesktopFileParser; +use config::Config; +use Result; + + +pub struct DesktopFileSync { + config: Config, + monitor: DirectoryMonitor, +} + +impl DesktopFileSync { + pub fn new(config: Config) -> DesktopFileSync { + let handler = Arc::new(Mutex::new(SyncHandler::new(config.clone()))); + let monitor = DirectoryMonitor::new(handler.clone()); + DesktopFileSync { + config, monitor + } + } + + pub fn clear_target_directory(&self) -> Result<()> { + let entries = fs::read_dir(self.config.target_directory())?; + + for entry in entries { + let path = entry?.path(); + if is_desktop_file(&path) { + fs::remove_file(&path).context(format!("remove_file({:?})", path))?; + } + } + Ok(()) + } + + pub fn sync_source(&mut self, src: &Path) -> Result<()> { + self.clear_target_directory()?; + self.sync_source_directory(src)?; + self.monitor.set_monitor_sources(&[src.to_path_buf()]); + Ok(()) + } + + fn sync_source_directory(&self, src: &Path) -> Result<()> { + let entries = fs::read_dir(src)?; + for entry in entries { + let path = entry?.path(); + if is_desktop_file(path.as_path()) { + if let Err(e) = sync_desktop_file(path.as_path(), &self.config) { + info!("error syncing desktop file {:?}: {}", path, e); + } + } + } + Ok(()) + } +} + +struct SyncHandler { + config: Config, +} + +impl SyncHandler { + fn new(config: Config) -> SyncHandler { + SyncHandler { config } + } +} + +impl MonitorEventHandler for SyncHandler { + fn file_added(&self, path: &Path) -> Result<()> { + info!("file_added: {:?}", path); + if is_desktop_file(path) { + sync_desktop_file(path, &self.config)?; + } + Ok(()) + } + + fn file_removed(&self, path: &Path) -> Result<()> { + info!("file_removed: {:?}", path); + let filename = filename_from_path(path)?; + let target_path = self.config.target_directory().join(filename); + if target_path.exists() { + fs::remove_file(target_path.as_path())?; + } + Ok(()) + } +} + +fn sync_desktop_file(source: &Path, config: &Config) -> Result<()> { + if !is_desktop_file(source) { + return Err(format_err!("source path [{:?}] is not desktop file", source)); + } + let df = DesktopFileParser::parse_from_path(source, config.exec_prefix())?; + if df.is_showable() { + df.write_to_dir(config.target_directory())?; + } else { + info!("ignoring {} as not showable", df.filename()); + } + Ok(()) +} + +fn is_desktop_file(path: &Path) -> bool { + if let Some(ext) = path.extension() { + return ext == "desktop" + } + false +} + +fn filename_from_path(path: &Path) -> Result<&str> { + let filename = match path.file_name() { + Some(name) => name, + None => return Err(format_err!("Path {:?} has no filename component", path)), + }; + match filename.to_str() { + Some(s) => Ok(s), + None => Err(format_err!("Filename has invalid utf8 encoding")), + } +} + diff --git a/citadel-tools/citadel-desktopd/src/main.rs b/citadel-tools/citadel-desktopd/src/main.rs new file mode 100644 index 0000000..8f4c252 --- /dev/null +++ b/citadel-tools/citadel-desktopd/src/main.rs @@ -0,0 +1,57 @@ +#[macro_use] extern crate failure; +#[macro_use] extern crate lazy_static; +#[macro_use] extern crate log; +#[macro_use] extern crate serde_derive; +extern crate env_logger; +extern crate serde; +extern crate toml; +extern crate inotify; +extern crate nix; + +mod desktop; +mod parser; +mod config; +mod monitor; +mod desktop_file_sync; + +use std::result; +use std::process; +use failure::Error; +use desktop_file_sync::DesktopFileSync; + +use config::Config; + +pub type Result = result::Result; + + +fn main() { + std::env::set_var("RUST_LOG", "info"); + env_logger::init(); + + let mut args = std::env::args(); + args.next(); + if args.len() != 1 { + println!("expected config file argument"); + process::exit(1); + } + + let config_path = args.next().unwrap(); + let config = match Config::from_path(&config_path) { + Err(e) => { + warn!("Failed to load configuration file: {}", e); + process::exit(1); + }, + Ok(config) => config, + }; + + let src = config.source_paths().first().unwrap().clone(); + + let mut dfs = DesktopFileSync::new(config.clone()); + if let Err(e) = dfs.sync_source(src.as_path()) { + warn!("error calling sync_source: {}", e); + } + loop { + std::thread::sleep(std::time::Duration::new(120, 0)); + } + +} diff --git a/citadel-tools/citadel-desktopd/src/monitor.rs b/citadel-tools/citadel-desktopd/src/monitor.rs new file mode 100644 index 0000000..d71641f --- /dev/null +++ b/citadel-tools/citadel-desktopd/src/monitor.rs @@ -0,0 +1,210 @@ +use std::collections::HashMap; +use std::path::{Path,PathBuf}; +use std::sync::{Arc,Mutex,Once,ONCE_INIT}; +use std::sync::atomic::{AtomicBool,Ordering}; +use std::thread::{self,JoinHandle}; +use std::os::unix::thread::JoinHandleExt; +use std::io::ErrorKind; + +use nix::libc; +use nix::sys::signal; + +use inotify::{Events,Inotify,EventMask,WatchMask,Event,WatchDescriptor}; + +use Result; + +pub trait MonitorEventHandler: Send+Sync { + fn file_added(&self, path: &Path) -> Result<()> { let _ = path; Ok(()) } + fn file_removed(&self, path: &Path) -> Result<()> { let _ = path; Ok(()) } + fn directory_added(&self, path: &Path) -> Result<()> { let _ = path; Ok(()) } + fn directory_removed(&self, path: &Path) -> Result<()> { let _ = path; Ok(()) } +} + +pub struct DirectoryMonitor { + event_handler: Arc>, + worker_handle: Option, +} + +impl DirectoryMonitor { + pub fn new(handler: Arc>) -> DirectoryMonitor { + initialize(); + DirectoryMonitor { + event_handler: handler, + worker_handle: None, + } + } + + pub fn set_monitor_sources(&mut self, sources: &[PathBuf]) { + if let Some(handle) = self.worker_handle.take() { + handle.stop(); + handle.wait(); + } + let sources = Vec::from(sources); + let h = MonitorWorker::start_worker(sources, self.event_handler.clone()); + self.worker_handle = Some(h); + } +} + +struct MonitorWorker { + descriptors: HashMap, + inotify: Inotify, + exit_flag: Arc, + watch_paths: Vec, + handler: Arc>, +} + + +impl MonitorWorker { + fn start_worker(watch_paths: Vec, handler: Arc>) -> WorkerHandle { + let exit_flag = Arc::new(AtomicBool::new(false)); + let flag_clone = exit_flag.clone(); + let jhandle = thread::spawn(move || { + + let mut worker = match MonitorWorker::new(watch_paths, flag_clone, handler) { + Ok(worker) => worker, + Err(e) => { + info!("failed to initialize inotify handle: {}", e); + return; + } + }; + if let Err(e) = worker.run() { + info!("error returned from worker thread: {}", e); + } + }); + WorkerHandle::new(jhandle, exit_flag) + } + + fn new(watch_paths: Vec, exit_flag: Arc, handler: Arc>) -> Result { + Ok(MonitorWorker { + descriptors: HashMap::new(), + inotify: Inotify::init()?, + exit_flag, + watch_paths, + handler, + }) + } + + fn add_watches(&mut self) -> Result<()> { + let watch_flags = WatchMask::CREATE | WatchMask::DELETE | WatchMask::MOVED_TO | + WatchMask::DONT_FOLLOW | WatchMask::ONLYDIR; + for p in &self.watch_paths { + let wd = self.inotify.add_watch(p, watch_flags)?; + self.descriptors.insert(wd, p.clone()); + } + Ok(()) + } + + fn read_events<'a>(&mut self, buffer: &'a mut [u8]) -> Result>> { + if self.exit_flag.load(Ordering::Relaxed) { + return Ok(None); + } + + match self.inotify.read_events_blocking(buffer) { + Ok(events) => Ok(Some(events)), + Err(e) => { + if e.kind() == ErrorKind::Interrupted { + Ok(None) + } else { + Err(e.into()) + } + } + } + } + + fn process_events(&self, events: Events) { + for ev in events { + if let Err(e) = self.handle_event(&ev) { + info!("error handling inotify event: {}", e); + } + } + } + + fn run(&mut self) -> Result<()> { + info!("running monitor event loop"); + self.add_watches()?; + let mut buffer = [0u8; 4096]; + loop { + match self.read_events(&mut buffer)? { + Some(events) => self.process_events(events), + None => break, + } + } + Ok(()) + } + + fn full_event_path(&self, ev: &Event) -> Result { + let filename = ev.name + .ok_or(format_err!("inotify event received without a filename"))?; + let path = self.descriptors.get(&ev.wd) + .ok_or(format_err!("Failed to find descriptor for received inotify event"))?; + Ok(path.join(filename)) + } + + fn handle_event(&self, ev: &Event) -> Result<()> { + let handler = self.handler.lock().unwrap(); + let pb = self.full_event_path(ev)?; + let path = pb.as_path(); + let is_create = ev.mask.intersects(EventMask::CREATE|EventMask::MOVED_TO); + if !is_create && !ev.mask.contains(EventMask::DELETE) { + return Err(format_err!("Unexpected mask value for inotify event: {:?}", ev.mask)); + } + + if ev.mask.contains(EventMask::ISDIR) { + if is_create { + handler.directory_added(path)?; + } else { + handler.directory_removed(path)?; + } + + } else { + if is_create { + handler.file_added(path)?; + } else { + handler.file_removed(path)?; + } + } + Ok(()) + } +} + +pub struct WorkerHandle { + join_handle: JoinHandle<()>, + exit_flag: Arc, +} + +impl WorkerHandle { + fn new(join_handle: JoinHandle<()>, exit_flag: Arc) -> WorkerHandle { + WorkerHandle { join_handle, exit_flag } + } + + pub fn stop(&self) { + info!("calling stop on monitor"); + let tid = self.join_handle.as_pthread_t(); + self.exit_flag.store(true, Ordering::Relaxed); + unsafe { + libc::pthread_kill(tid, signal::SIGUSR1 as libc::c_int); + } + } + + pub fn wait(self) { + if let Err(e) = self.join_handle.join() { + warn!("monitor thread panic with '{:?}'", e); + } + } +} + +static INITIALIZE_ONCE: Once = ONCE_INIT; + +fn initialize() { + INITIALIZE_ONCE.call_once(|| { + let h = signal::SigHandler::Handler(sighandler); + let sa = signal::SigAction::new(h, signal::SaFlags::empty(), signal::SigSet::empty()); + if let Err(e) = unsafe { signal::sigaction(signal::SIGUSR1, &sa) } { + warn!("Error setting signal handler: {}", e); + } + }); +} + +extern fn sighandler(_: libc::c_int) { + // do nothing, signal is only used to EINTR blocking inotify call +} \ No newline at end of file diff --git a/citadel-tools/citadel-desktopd/src/parser.rs b/citadel-tools/citadel-desktopd/src/parser.rs new file mode 100644 index 0000000..d8f8633 --- /dev/null +++ b/citadel-tools/citadel-desktopd/src/parser.rs @@ -0,0 +1,307 @@ + +use std::io::Read; +use std::fs::File; +use std::path::Path; +use std::collections::HashSet; +use desktop::{DesktopFile,Line}; +use Result; + +lazy_static! { + // These are the keys which are copied into the translated .desktop files + static ref KEY_WHITELIST: HashSet<&'static str> = [ + "Type", "Version", "Name", "GenericName", "NoDisplay", "Comment", "Icon", "Hidden", + "OnlyShowIn", "NotShowIn", "Path", "Terminal", "Actions", "MimeType", + "Categories", "Keywords", "StartupNotify", "StartupWMClass", "URL", "DocPath", + "X-GNOME-FullName", "X-GNOME-Provides", "X-Desktop-File-Install-Version", "X-GNOME-UsesNotifications", + "X-GNOME-DocPath", "X-Geoclue-Reason", "X-GNOME-SingleWindow", "X-GNOME-Gettext-Domain", + "X-MultipleArgs", + ].iter().cloned().collect(); + + // These are keys which are recognized but deliberately ignored. + static ref KEY_IGNORELIST: HashSet<&'static str> = [ + "DBusActivatable", "Implements", "TryExec", "InitialPreference", "Encoding", "X-KDE-Protocols", "X-GIO-NoFuse", "X-Gnome-Vfs-System", + "X-GNOME-Autostart-Phase", "X-GNOME-Autostart-Notify", "X-GNOME-AutoRestart", + "X-GNOME-Bugzilla-Bugzilla", "X-GNOME-Bugzilla-Product", "X-GNOME-Bugzilla-Component", "X-GNOME-Bugzilla-Version", + "X-GNOME-Bugzilla-ExtraInfoScript", "X-GNOME-Bugzilla-OtherBinaries", "X-GNOME-Autostart-enabled", + "X-AppInstall-Package", "X-KDE-SubstituteUID", "X-Ubuntu-Gettext-Domain", "X-AppInstall-Keywords", + "X-Ayatana-Desktop-Shortcuts", "X-GNOME-Settings-Panel", "X-GNOME-WMSettingsModule", "X-GNOME-WMName", + "X-GnomeWMSettingsLibrary", + ].iter().cloned().collect(); +} + +fn is_whitelisted_key(key: &str) -> bool { + KEY_WHITELIST.contains(key) +} + +fn filename_from_path(path: &Path) -> Result<&str> { + let filename = match path.file_name() { + Some(name) => name, + None => return Err(format_err!("Path {:?} has no filename component", path)), + }; + match filename.to_str() { + Some(s) => Ok(s), + None => Err(format_err!("Filename has invalid utf8 encoding")), + } +} +pub struct DesktopFileParser { + desktop_file: DesktopFile, + exec_prefix: String, + seen_header: bool, + current_action: Option, + in_ignored_group: bool, + known_actions: HashSet, +} + + +impl DesktopFileParser { + fn new(filename: &str, exec_prefix: &str) -> DesktopFileParser { + DesktopFileParser { + desktop_file: DesktopFile::new(filename), + exec_prefix: exec_prefix.to_string(), + seen_header: false, + current_action: None, + in_ignored_group: false, + known_actions: HashSet::new(), + } + } + + pub fn parse_from_path>(path: P, exec_prefix: &str) -> Result { + let filename = filename_from_path(path.as_ref())?; + let f = File::open(path.as_ref())?; + DesktopFileParser::parse_from_reader(f, filename, exec_prefix) + } + + fn parse_from_reader(mut r: T, filename: &str, exec_prefix: &str) -> Result { + let mut buffer = String::new(); + r.read_to_string(&mut buffer)?; + DesktopFileParser::parse_from_string(&buffer, filename, exec_prefix) + } + + fn parse_from_string(body: &str, filename: &str, exec_prefix: &str) -> Result { + let mut parser = DesktopFileParser::new(filename, exec_prefix); + for s in body.lines() { + match LineParser::parse(s) { + Some(line) => parser.process_line(line)?, + None => return Err(format_err!("Failed to parse line: '{}'", s)) + } + } + Ok(parser.desktop_file) + } + + fn process_initial(&mut self, line: Line) -> Result<()> { + match line { + Line::Comment(_) | Line::Empty => {}, + Line::DesktopHeader => self.seen_header = true, + _ => return Err(format_err!("Missing Desktop Entry header")) + } + self.desktop_file.add_line(line); + Ok(()) + } + + fn process_line(&mut self, mut line: Line) -> Result<()> { + if self.in_ignored_group && !line.is_action_header() { + return Ok(()) + } + if !self.seen_header { + return self.process_initial(line) + } + + if let Line::KeyValue(ref k, ref value) = line { + if k == "Actions" { + for s in value.split_terminator(";") { + self.known_actions.insert(s.trim().to_string()); + } + } + + } + + match line { + Line::ExecLine(ref mut s) => { + s.insert_str(0,self.exec_prefix.as_str()) + }, + Line::DesktopHeader => return Err(format_err!("Duplicate Desktop Entry header")), + Line::ActionHeader(ref action) => { + if self.known_actions.contains(action) { + self.current_action = Some(action.to_string()); + self.in_ignored_group = false; + } else { + return Err(format_err!("Desktop Action header with undecleared action: {}", action)) + } + }, + Line::GroupHeader(_) => { + self.in_ignored_group = true; + return Ok(()) + }, + Line::KeyLocaleValue(ref k,_,_) | Line::KeyValue(ref k,_) => { + + if !is_whitelisted_key(k) { + if !KEY_IGNORELIST.contains(k.as_str()) { + info!("Unknown key in {}: {}", self.desktop_file.filename(), k); + } + return Ok(()) + } + } + _ => {}, + } + if let Some(ref action) = self.current_action { + self.desktop_file.add_action_line(action, line) + } else { + self.desktop_file.add_line(line); + } + Ok(()) + } +} + +const DESKTOP_ACTION: &'static str = "Desktop Action "; + +struct LineParser<'a> { + s: &'a str, +} + +impl <'a> LineParser<'a> { + fn new(s: &'a str) -> LineParser<'a> { + LineParser { + s, + } + } + + fn parse(s: &'a str) -> Option { + if let Some(line) = LineParser::new(s)._parse() { + if validate_line(&line) { + return Some(line) + } + } + None + } + + fn first(&self) -> Option { + self.s.chars().next() + } + + fn last(&self) -> Option { + self.s.chars().next_back() + } + + fn _parse(&mut self) -> Option { + match self.first() { + None => Some(Line::Empty), + Some('#') => Some(Line::Comment(self.s[1..].to_string())), + Some('[') => self.parse_header(), + Some(_) => self.parse_keyval(), + } + } + + fn parse_header(&mut self) -> Option { + if self.last().unwrap() != ']' { + return None + } + let content = &self.s[1..self.s.len() - 1]; + if content.starts_with(DESKTOP_ACTION) { + let action = &content[DESKTOP_ACTION.len()..]; + return Some(Line::ActionHeader(action.to_string())) + } else if content == "Desktop Entry" { + return Some(Line::DesktopHeader) + } + return Some(Line::GroupHeader(content.to_string())) + } + + fn parse_keyval(&self) -> Option { + let parts: Vec<&str> = self.s.splitn(2, "=").collect(); + if parts.len() != 2 { + return None + } + let key = parts[0].trim(); + let val = parts[1].trim(); + if !key.contains("[") { + if key == "Exec" { + return Some(Line::ExecLine(val.to_string())) + } + return Some(Line::KeyValue(key.to_string(), val.to_string())) + } + self.parse_locale(key).map(|(key,locale)| Line::KeyLocaleValue(key, locale, val.to_string())) + } + + fn parse_locale(&self, key: &str) -> Option<(String,String)> { + let idx = key.find("[").unwrap(); + let (k,loc) = key.split_at(idx); + let mut chars = loc.chars(); + if let Some(']') = chars.next_back() { + chars.next(); + if k.trim() == "Exec" { + // Exec key with locale not allowed + return None; + } + return Some((k.trim().to_string(), chars.as_str().to_string())) + } + None + } +} + +fn is_alphanum_or_dash(c: char) -> bool { + is_ascii(c) && (c.is_alphanumeric() || c == '-') +} + +fn is_ascii(c: char) -> bool { + c as u32 <= 0x7F +} + +fn is_first_char_alphabetic(s: &str) -> bool { + if let Some(c) = s.chars().next() { + return is_ascii(c) && c.is_alphabetic() + } + false +} + +fn is_valid_key(key: &str) -> bool { + if !is_first_char_alphabetic(key) { + return false + } + key.chars().all(is_alphanum_or_dash) +} + +fn is_valid_locale(locale: &str) -> bool { + !locale.is_empty() && locale.chars().all(|c| { + is_alphanum_or_dash(c) || c == '_' || c == '.' || c == '@' + }) +} + +fn is_valid_value(value: &str) -> bool { + value.chars().all(|c| { + !(c.is_control() || c as u32 == 0 ) + }) +} + +fn is_valid_action(action: &str) -> bool { + is_first_char_alphabetic(action) && action.chars().all(is_alphanum_or_dash) +} + +fn is_valid_group(group: &str) -> bool { + is_first_char_alphabetic(group) && group.chars().all(|c| { + is_ascii(c) && !c.is_control() + }) +} + +fn is_valid_exec(val: &str) -> bool { + val.chars().all(|c| { + is_ascii(c) && !(c.is_control() || c as u32 == 0) + }) +} + +pub fn validate_line(line: &Line) -> bool { + match *line { + Line::ExecLine(ref s) => is_valid_exec(s), + Line::KeyValue(ref k, ref v) => is_valid_key(k) && is_valid_value(v), + Line::KeyLocaleValue(ref k, ref l, ref v) => is_valid_key(k) && is_valid_locale(l) && is_valid_value(v), + Line::ActionHeader(ref action) => is_valid_action(action), + Line::GroupHeader(ref group) => is_valid_group(group), + _ => true, + } +} + +#[test] +fn test_parser() { + let tests = vec!["###", "", "# hello", "[Desktop Entry]", "[Desktop Action foo]", "Foo=Bar", "Foo[hehe]=Lol"]; + for t in tests { + println!("{:?}", LineParser::parse(t)); + } +} diff --git a/citadel-tools/citadel-rootfs/.gitignore b/citadel-tools/citadel-rootfs/.gitignore new file mode 100644 index 0000000..700059f --- /dev/null +++ b/citadel-tools/citadel-rootfs/.gitignore @@ -0,0 +1,3 @@ +**/target/ +**/*.rs.bk +.idea/ diff --git a/citadel-tools/citadel-rootfs/Cargo.lock b/citadel-tools/citadel-rootfs/Cargo.lock new file mode 100644 index 0000000..6e2528b --- /dev/null +++ b/citadel-tools/citadel-rootfs/Cargo.lock @@ -0,0 +1,535 @@ +[[package]] +name = "ansi_term" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "arrayref" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "atty" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-demangle 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bitflags" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "block-buffer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayref 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "build_const" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byte-tools" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byteorder" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bytes" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cc" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "citadel-rootfs" +version = "0.1.0" +dependencies = [ + "clap 2.30.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ed25519-dalek 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "lzma-rs 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "nix 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "sha2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "clap" +version = "2.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "clear_on_drop" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crc" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "build_const 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "curve25519-dalek" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayref 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "subtle 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "digest" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ed25519-dalek" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "curve25519-dalek 0.14.4 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "subtle 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure_derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", + "synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "gcc" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "generic-array" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "typenum 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "iovec" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libc" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "log" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lzma-rs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "crc 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "nix" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rand" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_syscall" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_derive" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive_internals 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive_internals" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", + "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sha2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "strsim" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "subtle" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "subtle" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synstructure" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termion" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "textwrap" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "toml" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "typenum" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-width" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "vec_map" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b3568b48b7cefa6b8ce125f9bb4989e52fbcc29ebea88df04cc7c5f12f70455" +"checksum arrayref 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "0fd1479b7c29641adbd35ff3b5c293922d696a92f25c8c975da3e0acbc87258f" +"checksum atty 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8352656fd42c30a0c3c89d26dea01e3b77c0ab2af18230835c15e2e13cd51859" +"checksum backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ebbbf59b1c43eefa8c3ede390fcc36820b4999f7914104015be25025e0d62af2" +"checksum backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "44585761d6161b0f57afc49482ab6bd067e4edef48c12a152c237eb0203f7661" +"checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf" +"checksum block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a076c298b9ecdb530ed9d967e74a6027d6a7478924520acddcddc24c1c8ab3ab" +"checksum build_const 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e90dc84f5e62d2ebe7676b83c22d33b6db8bd27340fb6ffbff0a364efa0cb9c9" +"checksum byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40" +"checksum byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "652805b7e73fada9d85e9a6682a4abd490cb52d96aeecc12e33a0de34dfd0d23" +"checksum bytes 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1b7db437d718977f6dc9b2e3fd6fc343c02ac6b899b73fdd2179163447bd9ce9" +"checksum cc 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "deaf9ec656256bb25b404c51ef50097207b9cbb29c933d31f92cae5a8a0ffee0" +"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" +"checksum clap 2.30.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1c07b9257a00f3fc93b7f3c417fc15607ec7a56823bc2c37ec744e266387de5b" +"checksum clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "97276801e127ffb46b66ce23f35cc96bd454fa311294bced4bbace7baa8b1d17" +"checksum crc 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bd5d02c0aac6bd68393ed69e00bbc2457f3e89075c6349db7189618dc4ddc1d7" +"checksum curve25519-dalek 0.14.4 (registry+https://github.com/rust-lang/crates.io-index)" = "6734ff1a930d90b3ee54b7d6eba1b520f8724a1f353cf4f2b4b171a9ce63d814" +"checksum digest 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "00a49051fef47a72c9623101b19bd71924a45cca838826caae3eaa4d00772603" +"checksum ed25519-dalek 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3692ef38cc617236a39120ef0b91794e5e4d5c96227607a6740bfaaab53ac3c" +"checksum failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "934799b6c1de475a012a02dab0ace1ace43789ee4b99bcfbf1a2e3e8ced5de82" +"checksum failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cdda555bb90c9bb67a3b670a0f42de8e73f5981524123ad8578aafec8ddb8b" +"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +"checksum gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)" = "5e33ec290da0d127825013597dbdfc28bee4964690c7ce1166cbc2a7bd08b1bb" +"checksum generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d" +"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" +"checksum libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "1e5d97d6708edaa407429faa671b942dc0f2727222fb6b6539bf1db936e4b121" +"checksum log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "89f010e843f2b1a31dbd316b3b8d443758bc634bed37aabade59c686d644e0a2" +"checksum lzma-rs 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5d49cc18d2e4235afb294250b5eae3c8ac150fc3662fd46fbb7f48c6cb6567b7" +"checksum nix 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b7fd5681d13fda646462cfbd4e5f2051279a89a544d50eb98c365b507246839f" +"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +"checksum num-traits 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e7de20f146db9d920c45ee8ed8f71681fd9ade71909b48c3acbd766aa504cf10" +"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" +"checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" +"checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd" +"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +"checksum rustc-demangle 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f312457f8a4fa31d3581a6f423a70d6c33a10b95291985df55f1ff670ec10ce8" +"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" +"checksum serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = "db99f3919e20faa51bb2996057f5031d8685019b5a06139b1ce761da671b8526" +"checksum serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = "f4ba7591cfe93755e89eeecdbcc668885624829b020050e6aec99c2a03bd3fd0" +"checksum serde_derive_internals 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6e03f1c9530c3fb0a0a5c9b826bdd9246a5921ae995d75f512ac917fc4dd55b5" +"checksum sha2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7daca11f2fdb8559c4f6c588386bed5e2ad4b6605c1442935a7f08144a918688" +"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" +"checksum subtle 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c7a6bab57c3efd01ebd3d750f4244ae0af4cdd1fc505a7904a41603192b803c5" +"checksum subtle 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc7f6353c2ee5407358d063a14cccc1630804527090a6fb5a9489ce4924280fb" +"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +"checksum synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a761d12e6d8dcb4dcf952a7a89b475e3a9d69e4a69307e01a470977642914bd" +"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" +"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693" +"checksum toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "a7540f4ffc193e0d3c94121edb19b055670d369f77d5804db11ae053a45b6e7e" +"checksum typenum 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a99dc6780ef33c78780b826cf9d2a78840b72cae9474de4bcaf9051e60ebbd" +"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f" +"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" +"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c" +"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/citadel-tools/citadel-rootfs/Cargo.toml b/citadel-tools/citadel-rootfs/Cargo.toml new file mode 100644 index 0000000..437bc46 --- /dev/null +++ b/citadel-tools/citadel-rootfs/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "citadel-rootfs" +version = "0.1.0" +authors = ["Bruce Leidl "] +homepage = "https://github.com/subgraph/citadel" + +[dependencies] +clap = "2.30.0" +failure = "0.1.1" +libc = "0.2" +nix = "0.10.0" +ed25519-dalek = "^0.6" +rand = "0.4.2" +sha2 = "0.7.0" +toml = "0.4.5" +serde_derive = "1.0.27" +serde = "1.0.27" +rustc-serialize = "0.3.24" +lzma-rs = "0.1.0" diff --git a/citadel-tools/citadel-rootfs/conf/citadel-rootfs.conf b/citadel-tools/citadel-rootfs/conf/citadel-rootfs.conf new file mode 100644 index 0000000..f44d12f --- /dev/null +++ b/citadel-tools/citadel-rootfs/conf/citadel-rootfs.conf @@ -0,0 +1,13 @@ + + +# This is where images to be installed are stored +citadel_updates = "/storage/citadel-updates" + +kernel_updates = "/storage/kernel-updates" + +# This is where update images are built +image_builds = "/storage/image-builds" + +[channel.test] +update_server = "" +pubkey = "7a6743a61cff946083f2496c4df0e5afb958c84eec3185e63cbe7a695c20e732" diff --git a/citadel-tools/citadel-rootfs/src/blockdev.rs b/citadel-tools/citadel-rootfs/src/blockdev.rs new file mode 100644 index 0000000..862af77 --- /dev/null +++ b/citadel-tools/citadel-rootfs/src/blockdev.rs @@ -0,0 +1,154 @@ +use std::path::Path; +use std::fs::File; +use std::io::{Read,Write,Seek,SeekFrom}; +use std::os::unix::io::AsRawFd; +use std::fs::OpenOptions; +use std::os::unix::fs::OpenOptionsExt; +use libc; + +use Result; +use util::path_str; + +const REQUIRED_ALIGNMENT: usize = 4096; +const DEFAULT_ALIGNMENT: usize = REQUIRED_ALIGNMENT; + +pub struct AlignedBuffer { + buffer: Vec, + alignment: usize, + size: usize, + align_offset: usize, +} + +impl AlignedBuffer { + + pub fn new(size: usize) -> AlignedBuffer { + AlignedBuffer::new_with_alignment(size, DEFAULT_ALIGNMENT) + } + + pub fn from_slice(bytes: &[u8]) -> AlignedBuffer { + AlignedBuffer::from_slice_with_alignment(bytes, DEFAULT_ALIGNMENT) + } + + pub fn new_with_alignment(size: usize, alignment: usize) -> AlignedBuffer { + AlignedBuffer { + alignment, size, + buffer: vec![0; size + alignment], + align_offset: 0, + } + } + + pub fn from_slice_with_alignment(bytes: &[u8], alignment: usize) -> AlignedBuffer { + let mut ab = AlignedBuffer::new_with_alignment(bytes.len(), alignment); + ab.as_mut().copy_from_slice(bytes); + ab + } + + /// + /// Calculates an offset into `self.buffer` array that is physically + /// located at a 4096 byte alignment boundary and returns slice at + /// this offset. `self.align_offset` is set so that access functions + /// will use the right offset. + /// + /// I/O on block devices must use 4k aligned memory: + /// + /// https://people.redhat.com/msnitzer/docs/io-limits.txt + /// + /// Or maybe just 512 byte aligned memory: + /// + /// https://www.quora.com/Why-does-O_DIRECT-require-I-O-to-be-512-byte-aligned + /// + fn align_buffer(&mut self) { + let addr = self.buffer.as_ptr() as usize; + let offset = self.alignment - (addr & (self.alignment - 1)); + self.align_offset = offset; + } +} + +impl AsRef<[u8]> for AlignedBuffer { + fn as_ref(&self) -> &[u8] { + let start = self.align_offset; + let end = start + self.size; + &(self.buffer.as_slice())[start..end] + } +} + +impl AsMut<[u8]> for AlignedBuffer { + fn as_mut(&mut self) -> &mut [u8] { + self.align_buffer(); + let start = self.align_offset; + let end = start + self.size; + &mut self.buffer.as_mut_slice()[start..end] + } +} + +pub const SECTOR_SIZE: usize = 512; +pub const ALIGNMENT_MASK: usize = 4095; + +ioctl!(read blk_getsize64 with 0x12, 114; u64); + +pub struct BlockDev { + file: File, +} + +impl BlockDev { + pub fn open_ro>(path: P) -> Result { + BlockDev::open(path.as_ref(), false) + } + pub fn open_rw>(path: P) -> Result { + BlockDev::open(path.as_ref(), true) + } + + fn open(path: &Path, write: bool) -> Result { + let mut oo = OpenOptions::new(); + oo.read(true); + oo.custom_flags(libc::O_DIRECT | libc::O_SYNC); + if write { + oo.write(true); + } + let file = oo.open(path) + .map_err(|e| format_err!("Failed to open block device {}: {}", path_str(path), e))?; + Ok(BlockDev{file}) + } + + pub fn size(&self) -> Result { + let mut sz = 0u64; + unsafe { + blk_getsize64(self.file.as_raw_fd(), &mut sz) + .map_err(|e| format_err!("Error calling getsize ioctl on block device: {}", e))?; + } + Ok(sz) + } + + pub fn nsectors(&self) -> Result { + Ok((self.size()? as usize) >> 9) + } + + fn setup_io(&mut self, offset: usize, buffer: &[u8]) -> Result<()> { + let addr = buffer.as_ptr() as usize; + if addr & ALIGNMENT_MASK != 0 { + bail!("block device i/o attempted with incorrectly aligned buffer: {:p}", buffer); + } + if buffer.len() % SECTOR_SIZE != 0 { + bail!("buffer length {} is not a multiple of sector size", buffer.len()); + } + let count = buffer.len() / SECTOR_SIZE; + if offset + count > self.nsectors()? { + bail!("sector_io({}, {}) is past end of device", offset, buffer.len()); + } + self.file.seek(SeekFrom::Start((offset * SECTOR_SIZE) as u64))?; + Ok(()) + } + + pub fn read_sectors(&mut self, offset: usize, buffer: &mut [u8]) -> Result<()> { + self.setup_io(offset, buffer)?; + self.file.read_exact(buffer)?; + Ok(()) + } + + pub fn write_sectors(&mut self, offset: usize, buffer: &[u8]) -> Result<()> { + self.setup_io(offset, buffer)?; + self.file.write_all(buffer)?; + Ok(()) + } + +} diff --git a/citadel-tools/citadel-rootfs/src/boot.rs b/citadel-tools/citadel-rootfs/src/boot.rs new file mode 100644 index 0000000..cce4fac --- /dev/null +++ b/citadel-tools/citadel-rootfs/src/boot.rs @@ -0,0 +1,100 @@ + + +use {Result,Partition,Config}; + +pub struct BootSelection { + partitions: Vec, +} + +impl BootSelection { + pub fn load_partitions() -> Result { + let partitions = Partition::rootfs_partitions() + .map_err(|e| format_err!("Could not load rootfs partition info: {}", e))?; + + Ok(BootSelection { + partitions + }) + } + + pub fn choose_install_partition(&self) -> Option<&Partition> { + self.choose(|p| { + // first pass, if there is a partition which is not mounted and + // not initialized use that one + !p.is_mounted() && !p.is_initialized() + }).or_else(|| self.choose(|p| { + // second pass, just find one that's not mounted + !p.is_mounted() + })) + } + + fn choose(&self, pred: F) -> Option<&Partition> + where F: Sized + Fn(&&Partition) -> bool + { + self.partitions.iter().find(pred) + } + + /// Find the best rootfs partition to boot from + pub fn choose_boot_partition(&self) -> Option<&Partition> { + let mut best: Option<&Partition> = None; + + for p in &self.partitions { + if is_better(&best, p) { + best = Some(p); + } + } + best + } + + + /// Perform checks for error states at boot time. + pub fn scan_boot_partitions(&self, config: &Config) -> Result<()> { + for p in &self.partitions { + if let Err(e) = p.boot_scan(config) { + warn!("error in bootscan of partition {}: {}", p.path_str(), e); + } + } + Ok(()) + } +} + +fn is_better<'a>(current_best: &Option<&'a Partition>, other: &'a Partition) -> bool { + + // Only consider partitions in state NEW or state GOOD + if !other.is_good() && !other.is_new() { + return false; + } + // If metainfo is broken, then no, it's not better + if !other.metainfo().is_ok() { + return false; + } + + let best = match *current_best { + Some(p) => p, + // No current 'best', so 'other' is better, whatever it is. + None => return true, + }; + + // First parition with PREFER flag trumps everything else + if best.is_preferred() { + return false; + } + + // These are guaranteed to unwrap() + let best_version = best.metainfo().unwrap().version(); + let other_version = other.metainfo().unwrap().version(); + + if best_version > other_version { + return false; + } + + if other_version > best_version { + return true; + } + + // choose NEW over GOOD if versions are the same + if other.is_new() && best.is_good() { + return true; + } + // ... but if all things otherwise match, return first match + false +} diff --git a/citadel-tools/citadel-rootfs/src/config.rs b/citadel-tools/citadel-rootfs/src/config.rs new file mode 100644 index 0000000..e5bd10e --- /dev/null +++ b/citadel-tools/citadel-rootfs/src/config.rs @@ -0,0 +1,138 @@ +use std::path::Path; +use std::collections::HashMap; + +use ed25519_dalek::{Signature,PublicKey,Keypair}; +use rustc_serialize::hex::FromHex; +use sha2::Sha512; +use toml; + +use util::{read_file_as_string,path_str}; +use Result; + + +const DEFAULT_CONFIG_PATH: &str = "/usr/share/citadel/citadel-rootfs.conf"; +fn default_citadel_updates() -> String { "/storage/citadel-updates".to_string() } +fn default_kernel_updates() -> String { "/storage/kernel-updates".to_string() } +fn default_image_builds() -> String { "/storage/image-builds".to_string() } + +#[derive(Deserialize)] +pub struct Config { + default_channel: Option, + + #[serde (default= "default_citadel_updates")] + citadel_updates: String, + + #[serde (default = "default_kernel_updates")] + kernel_updates: String, + + #[serde (default = "default_image_builds")] + image_builds: String, + + channel: HashMap, +} + +impl Config { + + pub fn load_default() -> Result { + Config::load(DEFAULT_CONFIG_PATH) + } + + pub fn load>(path: P) -> Result { + let config = match Config::from_path(path.as_ref()) { + Ok(config) => config, + Err(e) => bail!("Failed to load config file {}: {}", path_str(path.as_ref()), e), + }; + Ok(config) + } + + fn from_path(path: &Path) -> Result { + let s = read_file_as_string(path.as_ref())?; + let mut config = toml::from_str::(&s)?; + for (k,v) in config.channel.iter_mut() { + v.name = k.to_string(); + } + + Ok(config) + } + + pub fn get_default_channel(&self) -> Option { + + if let Some(ref name) = self.default_channel { + if let Some(c) = self.channel(name) { + return Some(c); + } + } + + if self.channel.len() == 1 { + return self.channel.values().next().map(|c| c.clone()); + } + None + } + + pub fn channel(&self, name: &str) -> Option { + self.channel.get(name).map(|c| c.clone() ) + } + + pub fn citadel_updates_base(&self) -> &str { + &self.citadel_updates + } + + pub fn kernel_updates_base(&self) -> &str { + &self.kernel_updates + } + + pub fn image_builds_base(&self) -> &str { + &self.image_builds + } + + pub fn get_private_key(&self, channel: &str) -> Option { + if let Some(channel_config) = self.channel.get(channel) { + if let Some(ref key) = channel_config.keypair { + return Some(key.clone()); + } + } + None + } + + pub fn get_public_key(&self, channel: &str) -> Option { + if let Some(channel_config) = self.channel.get(channel) { + return Some(channel_config.pubkey.clone()); + } + None + } +} + +#[derive(Deserialize,Clone)] +pub struct Channel { + update_server: Option, + pubkey: String, + keypair: Option, + + #[serde(skip)] + name: String, +} + +impl Channel { + pub fn name(&self) -> &str { + &self.name + } + + pub fn sign(&self, data: &[u8]) -> Result { + let keybytes = match self.keypair { + Some(ref hex) => hex.from_hex()?, + None => bail!("No private signing key available for channel {}", self.name), + }; + let privkey = Keypair::from_bytes(&keybytes)?; + let sig = privkey.sign::(data); + Ok(sig) + } + + pub fn verify(&self, data: &[u8], sigbytes: &[u8]) -> Result { + let keybytes = self.pubkey.from_hex()?; + let pubkey = PublicKey::from_bytes(&keybytes)?; + let sig = Signature::from_bytes(sigbytes)?; + Ok(pubkey.verify::(data, &sig)) + } + +} + diff --git a/citadel-tools/citadel-rootfs/src/main.rs b/citadel-tools/citadel-rootfs/src/main.rs new file mode 100644 index 0000000..1f9af7d --- /dev/null +++ b/citadel-tools/citadel-rootfs/src/main.rs @@ -0,0 +1,290 @@ +#[macro_use] extern crate failure; +#[macro_use] extern crate nix; +#[macro_use] extern crate serde_derive; + + +extern crate libc; +extern crate clap; +extern crate serde; +extern crate toml; +extern crate ed25519_dalek; +extern crate sha2; +extern crate rand; +extern crate rustc_serialize; + + +thread_local! { + pub static VERBOSE: RefCell = RefCell::new(false); + pub static SYSOP: RefCell = RefCell::new(false); +} + +pub fn verbose() -> bool { + VERBOSE.with(|f| { + *f.borrow() + }) +} + +fn sysop() -> bool { + SYSOP.with(|f| { + *f.borrow() + }) +} + +macro_rules! info { + ($e:expr) => { if ::verbose() { println!("[+] {}", $e);} }; + ($fmt:expr, $($arg:tt)+) => { if ::verbose() { println!("[+] {}", format!($fmt, $($arg)+));} }; +} +macro_rules! warn { + ($e:expr) => { if ::verbose() { println!("WARNING: {}", $e);} }; + ($fmt:expr, $($arg:tt)+) => { if ::verbose() { println!("WARNING: {}", format!($fmt, $($arg)+));} }; +} + +macro_rules! notify { + ($e:expr) => { println!("[+] {}", $e); }; + ($fmt:expr, $($arg:tt)+) => { println!("[+] {}", format!($fmt, $($arg)+)); }; +} + +use std::result; +use std::process::exit; + +use failure::Error; +use clap::{App,Arg,ArgMatches, SubCommand}; +use clap::AppSettings::*; +use sha2::Sha512; +use rand::OsRng; +use ed25519_dalek::Keypair; +use rustc_serialize::hex::ToHex; +use std::cell::RefCell; +use std::env; + + +pub use config::Config; +pub use metainfo::Metainfo; +pub use blockdev::BlockDev; +pub use partition::{Partition,MAX_METAINFO_LEN}; +use unpacker::UpdateImageUnpacker; +use packer::UpdateImagePacker; +use boot::BootSelection; + +mod boot; +mod metainfo; +mod partition; +mod blockdev; +mod config; +mod packer; +mod unpacker; +mod util; + +pub type Result = result::Result; + +fn main() { + match env::var("CITADEL_SYSOP") { + Ok(_) => SYSOP.with(|f| *f.borrow_mut() = true), + _ => {}, + }; + + let mut app = App::new("citadel-rootfs") + .about("Subgraph Citadel rootfs partition management") + .settings(&[ArgRequiredElseHelp, ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder]) + .arg(Arg::with_name("v") + .help("Verbose output") + .short("v") + .long("verbose")) + .arg(Arg::with_name("config") + .help("Optionally specify an alternate config file") + .takes_value(true) + .short("c") .long("config")) + + .subcommand(SubCommand::with_name("list") + .about("Show information about all rootfs partitions")) + + .subcommand(SubCommand::with_name("which-boot") + .about("Show which rootfs paritition would currently boot according to the boot selection algorithm")) + + .subcommand(SubCommand::with_name("verify-update") + .about("Verify the signature of an update image")) + + .subcommand(SubCommand::with_name("update") + .about("Download update if available and install") + .arg(Arg::with_name("download-only") + .help("Only download available update, don't install") + .long("download"))) + + .subcommand(SubCommand::with_name("install-update") + .about("Install an update image") + .arg(Arg::with_name("image") + .required(true))); + if sysop() { + + app = app.subcommand(SubCommand::with_name("build-update") + .about("Create an update image from a raw citadel-image.ext2 file") + .arg(Arg::with_name("image") + .required(true))) + + .subcommand(SubCommand::with_name("genkeys") + .about("Generate a new update keypair")); + + } + + let matches = app.get_matches(); + + + let config = load_config(&matches); + + if matches.is_present("v") { + VERBOSE.with(|f| *f.borrow_mut() = true); + } + + let result = match matches.subcommand() { + ("list", Some(_)) => list_cmd(&config), + ("which-boot", Some(_)) => { which_boot_cmd(&config)}, + ("update", Some(m)) => update_cmd(&config, m), + ("verify-update", Some(m)) => verify_update_cmd(&config, m), + ("install-update", Some(m)) => install_update_cmd(&config, m), + ("build-update", Some(m)) => build_update_cmd(&config, m), + ("genkeys", Some(_)) => genkeys_cmd(), + ("mount-rootfs", Some(m)) => mount_rootfs_cmd(&config, m), + (s, Some(_)) => {info!("subcommand: {}", s); Ok(())}, + _ => Ok(()), + }; + + if let Err(e) = result { + println!("{}", e); + exit(1); + } +} + +fn load_config(arg_matches: &ArgMatches) -> Config { + let config_load = match arg_matches.value_of("config") { + Some(path) => Config::load(path), + None => Config::load_default(), + }; + match config_load { + Ok(config) => config, + Err(e) => { + println!("{}", e); + exit(1); + } + } +} + +fn list_cmd(_config: &Config) -> Result<()> { + println!("{:^30} {:^14} {:^8} {:^8} {:^12}", "DEVICE PATH", "MOUNTED", "CHANNEL", "VERSION", "STATUS"); + for p in Partition::rootfs_partitions()? { + let info = partition_info(&p); + println!("{:^30} {:^14} {:^8} {:^8} {:^12}", info.0, info.1, info.2, info.3, info.4); + } + Ok(()) +} + + +fn partition_info(part: &Partition) -> (String,String,String,String,String) { + let mounted = if part.is_mounted() { "[Yes]".to_string() } else { String::new() }; + let status = if part.is_initialized() { part.status_label() } else { "Not Initialized".to_string() }; + let (channel, version) = match part.metainfo() { + Ok(meta) => (meta.channel().to_string(), meta.version().to_string()), + _ => (String::new(), String::new()), + }; + (part.path_str().to_string(), mounted, channel, version, status) +} + +fn mount_rootfs_cmd(_config: &Config, _matches: &ArgMatches) -> Result<()> { + // mounting installer rootfs should happen here too? + // perhaps based on cmd line flag + // maybe define guid like the gpt generator looks for + // for: rootfs inside luks, rootfs outside luks + let bs = BootSelection::load_partitions()?; + let _p = match bs.choose_boot_partition() { + Some(p) => p, + None => bail!("None of the rootfs partitions have a bootable image"), + }; + Ok(()) +} + +fn which_boot_cmd(_config: &Config) -> Result<()> { + let select = BootSelection::load_partitions()?; + match select.choose_boot_partition() { + Some(part) => { + notify!("Next boot will be from partition: {}", part.path_str()); + }, + None => { + warn!("None of the rootfs partitions are currently in bootable state."); + warn!("Unless a valid image is installed, computer will fail to boot"); + } + } + Ok(()) +} + +fn verify_update_cmd(_config: &Config, _matches: &ArgMatches) -> Result<()> { + println!("do verify_update"); + Ok(()) +} + +fn update_cmd(_config: &Config, _matches: &ArgMatches) -> Result<()> { + // note to self, remember to verify that downloaded update image version matches the version + // which is expected. + Ok(()) + +} + +fn install_update_cmd(config: &Config, matches: &ArgMatches) -> Result<()> { + let image_path = match matches.value_of("image") { + Some(val) => val, + None => bail!("install-update requires an image path"), + }; + let unpack = UpdateImageUnpacker::open(image_path, config)?; + info!("unpacking image channel: {} version: {}", unpack.metainfo().channel(), unpack.metainfo().version()); + unpack.unpack_disk_image()?; + info!("decompressing image"); + unpack.decompress_disk_image()?; + info!("verifying shasum"); + unpack.verify_shasum()?; + + let bs = BootSelection::load_partitions()?; + let p = match bs.choose_install_partition() { + Some(p) => p, + None => bail!("None of the rootfs partitions are available to install update to"), + }; + info!("installing to {}", p.path_str()); + unpack.write_partition(p)?; + notify!("Update image successfully installed to {}", p.path_str()); + + + //update::UpdateImage::new(path); + // 1) read header, extract metainfo + // 2) verify signature on metainfo + // 3) determine if this version/channel makes sense to be installed + // 4) xtrat image data to temporary file with https://crates.io/crates/lzma-rs + // 5) verify sha256 on image data + // + // 6) choose rootfs partition + // 7) write partition info block, setting status to INVALID + // 8) write update image to device + // 9) run verifyupdate using provided --salt + // 10) re-rewrite parition info block with status NEW + Ok(()) +} + +fn build_update_cmd(config: &Config, matches: &ArgMatches) -> Result<()> { + let image_path = match matches.value_of("image") { + Some(val) => val, + None => bail!("build-update requires an image path"), + }; + let channel = match config.get_default_channel() { + Some(ch) => ch, + None => bail!("Could not determine default channel from config file"), + }; + let mut builder = UpdateImagePacker::new(config, channel, image_path)?; + builder.build()?; + Ok(()) +} + +fn genkeys_cmd() -> Result<()> { + let mut rng = OsRng::new()?; + let keypair = Keypair::generate::(&mut rng); + + println!("pubkey = \"{}\"", keypair.public.to_bytes().to_hex()); + println!("privkey = \"{}\"", keypair.to_bytes().to_hex()); + + Ok(()) +} diff --git a/citadel-tools/citadel-rootfs/src/metainfo.rs b/citadel-tools/citadel-rootfs/src/metainfo.rs new file mode 100644 index 0000000..54f743a --- /dev/null +++ b/citadel-tools/citadel-rootfs/src/metainfo.rs @@ -0,0 +1,48 @@ +#[derive(Deserialize,Serialize,Clone)] +pub struct Metainfo { + channel: String, + version: u32, + base_version: u32, + date: String, + gitrev: String, + nsectors: u32, + shasum: String, + verity_salt: String, + verity_root: String, +} + + +impl Metainfo { + + pub fn channel(&self) -> &str { + &self.channel + } + + pub fn version(&self) -> u32 { + self.version + } + + pub fn date(&self) -> &str { + &self.date + } + + pub fn gitrev(&self) -> &str { + &self.gitrev + } + + pub fn nsectors(&self) -> usize { + self.nsectors as usize + } + + pub fn shasum(&self) -> &str { + &self.shasum + } + + pub fn verity_root(&self) -> &str { + &self.verity_root + } + + pub fn verity_salt(&self) -> &str { + &self.verity_salt + } +} diff --git a/citadel-tools/citadel-rootfs/src/packer.rs b/citadel-tools/citadel-rootfs/src/packer.rs new file mode 100644 index 0000000..0185f16 --- /dev/null +++ b/citadel-tools/citadel-rootfs/src/packer.rs @@ -0,0 +1,209 @@ + +use std::path::Path; +use std::io::{self,Write}; +use std::fs::{File,OpenOptions}; +use std::collections::HashMap; + +use config::{Config,Channel}; +use util::*; + +use Result; + +const IMAGE_FILENAME: &str = "citadel-image.ext2"; + +/// +/// +pub struct UpdateImagePacker { + workdir: Workdir, + version: usize, + channel: Channel, + nsectors: usize, + verity_salt: String, + verity_root: String, + shasum: String, + header: Vec, +} + +impl UpdateImagePacker { + pub fn new(config: &Config, channel: Channel, image_path: &str) -> Result { + let mut workdir = Workdir::new(config.image_builds_base(), channel.name()); + let version = workdir.find_next_version()?; + + sanity_check_source(image_path)?; + let mut from = File::open(image_path)?; + let mut to = File::create(workdir.filepath(IMAGE_FILENAME))?; + io::copy(&mut from, &mut to)?; + + Ok(UpdateImagePacker { + workdir, version, + channel: channel.to_owned(), + nsectors: 0, + verity_salt: String::new(), + verity_root: String::new(), + shasum: String::new(), + header: Vec::new(), + }) + } + + pub fn build(&mut self) -> Result<()> { + self.pad_image(4096)?; + let meta = self.workdir.filepath(IMAGE_FILENAME).metadata()?; + self.nsectors = (meta.len() / 512) as usize; + self.build_verity()?; + self.calculate_image_shasum()?; + self.build_update_header()?; + self.compress_image()?; + self.write_update_image()?; + + Ok(()) + } + + fn pad_image(&self, size: usize) -> Result<()> { + let path = self.workdir.filepath(IMAGE_FILENAME); + let meta = path.metadata()?; + let rem = (meta.len() as usize) % size; + if rem == 0 { + return Ok(()); + } + let padlen = size - rem; + info!("padding image with {} bytes", padlen); + let zeros = vec![0u8; padlen]; + + let mut file = OpenOptions::new().append(true).open(&path)?; + file.write_all(&zeros)?; + Ok(()) + } + + fn build_verity(&mut self) -> Result<()> { + info!("Building dm-verity hash tree"); + let verity_output = run_verityformat_command(&self.workdir.filepath(IMAGE_FILENAME), &self.workdir.filepath("verifyhash.out"))?; + write_string_to_file(&self.workdir.filepath("verityinfo"), &verity_output)?; + + let map = UpdateImagePacker::parse_verity_output(&verity_output); + + self.verity_root = match map.get("Root hash") { + Some(v) => v.to_owned(), + None => bail!("No root hash found in veritysetup output"), + }; + + self.verity_salt = match map.get("Salt") { + Some(v) => v.to_owned(), + None => bail!("No Salt found in veritysetup output"), + }; + + info!("Verity root: {}", self.verity_root); + Ok(()) + } + + fn calculate_image_shasum(&mut self) -> Result<()> { + info!("Calculating sha256 digest over image file"); + self.shasum = run_sha256_command(&self.workdir.filepath(IMAGE_FILENAME))?; + Ok(()) + } + + fn parse_verity_output(output: &str) -> HashMap { + let mut map = HashMap::new(); + for line in output.lines() { + if let Some((k,v)) = UpdateImagePacker::parse_verity_line(line) { + map.insert(k, v); + } + } + map + } + + fn parse_verity_line(line: &str) -> Option<(String,String)> { + let v = line.split(':').map(|s| s.trim()) + .collect::>(); + + if v.len() == 2 { + Some((v[0].to_string(), v[1].to_string())) + } else { + None + } + } + + + fn build_update_header(&mut self) -> Result<()> { + info!("Creating update image header"); + let metainfo = self.generate_metainfo()?; + + let mut f = File::create(self.workdir.filepath("metainfo"))?; + f.write_all(&metainfo)?; + + let sig = self.channel.sign(&metainfo)?; + + let mut szbuf = [0u8; 2]; + szbuf[0] = (metainfo.len() >> 8) as u8; + szbuf[1] = metainfo.len() as u8; + + self.header.write_all(b"UPDT")?; + self.header.write_all(&szbuf)?; + self.header.write_all(&metainfo)?; + self.header.write_all(&sig.to_bytes())?; + Ok(()) + } + + fn generate_metainfo(&self) -> Result> { + let mut v = Vec::new(); + writeln!(v, "channel = \"{}\"", self.channel.name())?; + writeln!(v, "version = {}", self.version)?; + writeln!(v, "base_version = {}", self.version)?; + writeln!(v, "date = \"\"")?; + writeln!(v, "gitrev = \"\"")?; + writeln!(v, "nsectors = {}", self.nsectors)?; + + writeln!(v, "shasum = \"{}\"", self.shasum)?; + writeln!(v, "verity_salt = \"{}\"", self.verity_salt)?; + writeln!(v, "verity_root = \"{}\"", self.verity_root)?; + Ok(v) + } + + fn compress_image(&self) -> Result<()> { + info!("Compressing image file"); + run_xz_command(&self.workdir.filepath(IMAGE_FILENAME), false)?; + Ok(()) + } + + fn write_update_image(&self) -> Result<()> { + let img_path = self.workdir.filepath("citadel-update.img"); + info!("writing update image to {}", path_str(&img_path)); + let mut out = File::create(&img_path)?; + + out.write_all(self.header.as_slice())?; + + let mut image = File::open(self.workdir.filepath("citadel-image.ext2.xz"))?; + io::copy(&mut image, &mut out)?; + + Ok(()) + } +} + + +fn sanity_check_source>(src: P) -> Result<()> { + let src: &Path = src.as_ref(); + let meta = match src.metadata() { + Ok(md) => md, + Err(e) => bail!("Could not load image file {}: {}", path_str(src), e), + }; + + if !meta.file_type().is_file() { + bail!("Image file {} exists but is not a regular file"); + } + + let filetype = match run_file_command(&src) { + Ok(s) => s, + Err(e) => bail!("{}", e), + }; + + if filetype.starts_with("XZ") { + bail!("Image file is compressed, decompress first"); + } else if !filetype.starts_with("Linux rev 1.0 ext2 filesystem data") { + bail!("Image file is not an ext2 filesystem as expected:\n {}", + filetype.trim_right()); + } + + if meta.len() % 512 != 0 { + bail!("Image file size is not a multiple of sector size (512 bytes)"); + } + Ok(()) +} diff --git a/citadel-tools/citadel-rootfs/src/partition.rs b/citadel-tools/citadel-rootfs/src/partition.rs new file mode 100644 index 0000000..c2808dc --- /dev/null +++ b/citadel-tools/citadel-rootfs/src/partition.rs @@ -0,0 +1,481 @@ +use std::path::{Path,PathBuf}; +use std::str; +use std::cell::{RefCell,Ref}; + +use toml; +use ed25519_dalek::SIGNATURE_LENGTH; + +use Result; +use Metainfo; +use BlockDev; +use Config; +use blockdev::AlignedBuffer; +use util::*; + +const MAGIC: &[u8] = b"CTDL"; + +/// Size in bytes of the Partition Info Block +const BLOCK_SIZE: usize = 4096; + +/// Size of the PIB header up to the metainfo data +const HEADER_SIZE: usize = 8; + +/// The maximum length of metainfo data in a Partition Info Block. +/// This size is the entire block minus the header and signature. +pub const MAX_METAINFO_LEN: usize = BLOCK_SIZE - (HEADER_SIZE + SIGNATURE_LENGTH); + +/// +/// Flag to override the algorithm which selects a partition to mount during boot +/// +const FLAG_PREFER: u8 = 1; + +/// +/// The last 4096 bytes of a rootfs block device stores a structure +/// called the Partition Info Block. +/// +/// The layout of this structure is the following: +/// +/// field size (bytes) offset +/// ----- ------------ ------ +/// +/// magic 4 0 +/// status 1 4 +/// flags 1 5 +/// length 2 6 +/// +/// metainfo 8 +/// +/// signature 64 8 + length +/// +/// magic : Must match ascii bytes 'CTDL' for the block to be considered valid +/// +/// status : See `PartitionStatus` for description of defined valid values +/// +/// flags : Only one flag is defined, `FLAG_PREFER` +/// +/// length : Big endian 16 bit size in bytes of metainfo field +/// +/// metainfo : A utf-8 TOML document with various fields describing the rootfs image +/// on this partition. +/// +/// signature : ed25519 signature of the content of metainfo field +/// +struct Infoblock(RefCell>); + +impl Infoblock { + fn new() -> Infoblock { + let v = vec![0; BLOCK_SIZE]; + Infoblock(RefCell::new(v)) + } + + fn reset(&self) { + for b in &mut self.0.borrow_mut()[..] { + *b = 0; + } + self.write_bytes(0, MAGIC); + } + + fn w8(&self, idx: usize, val: u8) { + self.0.borrow_mut()[idx] = val; + } + + fn r8(&self, idx: usize) -> u8 { + self.0.borrow()[idx] + } + + fn write_bytes(&self, offset: usize, data: &[u8]) { + self.0.borrow_mut()[offset..offset+data.len()].copy_from_slice(data) + } + + fn read_bytes(&self, offset: usize, len: usize) -> Vec { + Vec::from(&self.0.borrow()[offset..offset+len]) + } + + fn status(&self) -> u8 { + self.r8(4) + } + + fn set_status(&self, status: u8) { + self.w8(4, status); + } + + fn has_status(&self, status: u8) -> bool { + self.is_valid() && self.status() == status + } + + fn flags(&self) -> u8 { + self.r8(5) + } + + fn has_flag(&self, flag: u8) -> bool { + self.is_valid() && (self.flags() & flag) == flag + } + + /// Returns `true` if flag value changed + fn set_flag(&self, flag: u8, value: bool) -> bool { + let old = self.flags(); + + if value { + self.w8(5, old | flag); + } else { + self.w8(5, old & !flag); + } + self.flags() == old + } + + fn metainfo_len(&self) -> usize { + let high = self.r8(6) as usize; + let low = self.r8(7) as usize; + (high << 8) | low + } + + fn set_metainfo_len(&self, mlen: usize) { + assert!(mlen <= MAX_METAINFO_LEN); + let high = (mlen >> 8) as u8; + let low = mlen as u8; + self.w8(6, high); + self.w8(7, low); + } + + fn write_metainfo(&self, metainfo: &[u8]) { + self.set_metainfo_len(metainfo.len()); + self.write_bytes(8, metainfo); + } + + fn write_signature(&self, signature: &[u8]) { + assert_eq!(signature.len(), SIGNATURE_LENGTH); + self.write_bytes(8 + self.metainfo_len(), signature); + } + + fn read_metainfo(&self) -> Vec { + assert!(self.is_valid()); + self.read_bytes(8, self.metainfo_len()) + } + + fn read_signature(&self) -> Vec { + assert!(self.is_valid()); + self.read_bytes(8 + self.metainfo_len(), SIGNATURE_LENGTH) + } + + fn is_valid(&self) -> bool { + &self.0.borrow()[0..4] == MAGIC && + is_valid_status_code(self.status()) && + self.flags() & !FLAG_PREFER == 0 && + self.metainfo_len() > 0 && self.metainfo_len() <= MAX_METAINFO_LEN + } + + fn from_slice(&self, bytes: &[u8]) { + self.0.borrow_mut().copy_from_slice(bytes); + } + + fn as_ref(&self) -> Ref> { + self.0.borrow() + } +} + +pub struct Partition { + path: PathBuf, + is_mounted: bool, + infoblock: Infoblock, +} + +impl Partition { + /// + /// Return a `Vec` of all rootfs partitions on the system. Usually + /// there are two (rootfsA and rootfsB). + /// + pub fn rootfs_partitions() -> Result> { + let mut v = Vec::new(); + for path in rootfs_partition_paths()? { + v.push(Partition::load(&path)?); + } + Ok(v) + } + + /// Construct a new `Partition` object for the device `dev` and load + /// the Partition Info Block structure from the block device. + fn load(dev: &Path) -> Result { + let is_mounted = is_path_mounted(dev)?; + let part = Partition::new(dev, is_mounted); + part.read_infoblock()?; + Ok(part) + } + + fn new(path: &Path, is_mounted: bool) -> Partition { + Partition { + path: path.to_path_buf(), + is_mounted, + infoblock: Infoblock::new(), + } + } + + /// + /// For the passed in `BlockDev` instance calculate and return + /// the sector offset of the Partition Info Block, which is + /// located 8 sectors (4096 bytes) from the end of the partition. + /// + fn infoblock_offset(&self, bdev: &BlockDev) -> Result { + let nsectors = bdev.nsectors()?; + if nsectors < 8 { + bail!("{} is a block device but it's very short, {} sectors", + self.path_str(), nsectors); + } + Ok(nsectors - 8) + } + + /// + /// Open the block device for this partition and load the + /// Partition Info Block into the internal buffer `self.infoblock`. + /// + fn read_infoblock(&self) -> Result<()> { + let mut dev = BlockDev::open_ro(&self.path)?; + let off = self.infoblock_offset(&dev)?; + let mut buffer = AlignedBuffer::new(BLOCK_SIZE); + dev.read_sectors(off, buffer.as_mut())?; + self.infoblock.from_slice(buffer.as_ref()); + Ok(()) + } + + /// + /// Open the block device for this partition and write the + /// internal buffer `self.infoblock` into the Partition Info + /// Block. + /// + fn write_infoblock(&self) -> Result<()> { + let mut dev = BlockDev::open_rw(&self.path)?; + let off = self.infoblock_offset(&dev)?; + let buffer = AlignedBuffer::from_slice(self.infoblock.as_ref().as_slice()); + dev.write_sectors(off, buffer.as_ref())?; + Ok(()) + } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn path_str(&self) -> &str { + self.path.to_str().unwrap() + } + + /// + /// Returns true if this partition is currently mounted and + /// cannot be written to. + /// + pub fn is_mounted(&self) -> bool { + self.is_mounted + } + + /// + /// Update the Partition Info Block for this partition with a new + /// status field. + /// + pub fn write_status(&self, status: u8) -> Result<()> { + self.infoblock.set_status(status); + self.write_infoblock()?; + Ok(()) + } + + pub fn set_prefer_flag(&self, value: bool) -> Result<()> { + if self.infoblock.set_flag(FLAG_PREFER, value) { + self.write_infoblock()?; + } + Ok(()) + } + + /// + /// Write metainfo and signature to the Partition Info Block of this partition. + /// + /// This also sets the internal buffer `self.infoblock` to the block contents + /// written to disk. + /// + /// Writing new partition info also set status to `STATUS_INVALID` as this function + /// is meant to be called in preparation for writing a raw disk image to partition. + /// + /// Caller should write status as `STATUS_NEW` after raw image has been successfully + /// written to partition. + /// + pub fn write_partition_info(&self, metainfo: &[u8], signature: &[u8]) -> Result<()> { + let mlen = metainfo.len(); + + if mlen > MAX_METAINFO_LEN { + bail!("cannot write partition because metainfo field is too long ({} bytes)", metainfo.len()); + } + + if mlen == 0 { + bail!("cannot write partition because metainfo is empty"); + } + + if signature.len() != SIGNATURE_LENGTH { + bail!("cannot write partition info because signature has wrong length {} != {}", + signature.len(), SIGNATURE_LENGTH); + } + + self.infoblock.reset(); + self.infoblock.write_metainfo(metainfo); + self.infoblock.write_signature(signature); + + self.write_infoblock()?; + + Ok(()) + } + + /// + /// Returns true only if this partition has a valid Partition Information + /// Block and the signature on the metainfo field can be verified with + /// the provided `PublicKey`. + /// + pub fn verify_signature(&self, config: &Config) -> Result { + if !self.infoblock.is_valid() { + bail!("Cannot verify signature because partition is invalid"); + } + let metainfo = self.metainfo()?; + let channel = match config.channel(metainfo.channel()) { + Some(ch) => ch, + None => bail!("No public key configured for channel '{}'", metainfo.channel()), + }; + + let data = self.infoblock.read_metainfo(); + let signature = self.infoblock.read_signature(); + Ok(channel.verify(&data, &signature)?) + } + + /// + /// Parse the bytes from the metainfo section of the Partition Information + /// Block and return a `Metainfo` structure. + /// + pub fn metainfo(&self) -> Result { + if !self.infoblock.is_valid() { + bail!("partition is invalid"); + } + let bytes = self.infoblock.read_metainfo(); + let metainfo = toml::from_slice::(&bytes)?; + Ok(metainfo) + } + + pub fn is_new(&self) -> bool { + self.infoblock.has_status(STATUS_NEW) + } + + pub fn is_good(&self) -> bool { + self.infoblock.has_status(STATUS_GOOD) + } + + pub fn is_preferred(&self) -> bool { + self.infoblock.has_flag(FLAG_PREFER) + } + + pub fn status_label(&self) -> String { + status_code_label(self.infoblock.status()) + } + + /// `true` if the Partition Info Block fields + /// contain legal values. `false` indicates that + /// the data is corrupted or was never written to + /// this partition. + pub fn is_initialized(&self) -> bool { + self.infoblock.is_valid() + } + + pub fn write_image(&self, image_path: &Path, metainfo: &[u8], signature: &[u8]) -> Result<()> { + if self.is_mounted { + bail!("Cannot write to mounted device {}", self.path_str()); + } + info!("Writing raw rootfs disk image to {}", self.path_str()); + self.write_partition_info(metainfo, signature)?; + run_write_image_dd(image_path, &self.path)?; + + let meta = image_path.metadata()?; + let len = meta.len() as usize; + let nblocks = len / 4096; + + info!("Generating dm-verity hash tree"); + let mut verityinfo = image_path.to_path_buf(); + verityinfo.pop(); + verityinfo.push("verity-format.out"); + let out = run_verityinstall_command(self.path(), self.metainfo()?.verity_salt(), nblocks, len)?; + write_string_to_file(&verityinfo, &out)?; + + info!("Setting parition status field to STATUS_NEW"); + self.write_status(STATUS_NEW)?; + + Ok(()) + } + + /// Called at boot to perform various checks and possibly + /// update the status field to an error state. + /// + /// Mark `STATUS_TRY_BOOT` partition as `STATUS_FAILED`. + /// + /// If metainfo cannot be parsed, mark as `STATUS_BAD_META`. + /// + /// Verify metainfo signature and mark `STATUS_BAD_SIG` if + /// signature verification fails. + /// + pub fn boot_scan(&self, config: &Config) -> Result<()> { + if !self.is_initialized() { + return Ok(()); + } + if self.infoblock.status() == STATUS_TRY_BOOT { + warn!("Partition {} has STATUS_TRY_BOOT, assuming it failed boot attempt and marking STATUS_FAILED", self.path_str()); + self.write_status(STATUS_FAILED)?; + } + + if let Err(_) = self.metainfo() { + warn!("Partition {} has invalid metainfo, setting STATUS_BAD_META", self.path_str()); + self.write_status(STATUS_BAD_META)?; + return Ok(()); + } + + match self.verify_signature(config) { + Err(e) => { + warn!("Error verifying parition signature on {}: {}", self.path_str(), e); + warn!("Partition {} has bad signature, marking STATUS_BAD_SIG", self.path_str()); + self.write_status(STATUS_BAD_SIG)?; + }, + Ok(false) => { + warn!("Partition {} has bad signature, marking STATUS_BAD_SIG", self.path_str()); + self.write_status(STATUS_BAD_SIG)?; + }, + Ok(true) => { /* signature good */ }, + }; + Ok(()) + } +} + + + +/// Set on partition before writing a new rootfs disk image +const STATUS_INVALID : u8 = 0; + +/// Set on partition after write of new rootfs disk image completes successfully +const STATUS_NEW : u8 = 1; + +/// Set on boot selected partition if in `STATUS_NEW` state. +const STATUS_TRY_BOOT: u8 = 2; + +/// Set on boot when a `STATUS_TRY_BOOT` partition successfully launches desktop +const STATUS_GOOD : u8 = 3; + +/// Set on boot for any partition in state `STATUS_TRY_BOOT` +const STATUS_FAILED : u8 = 4; + +/// Set on boot selected partition when signature fails to verify +const STATUS_BAD_SIG : u8 = 5; + +/// Set on boot selected partition when Metainfo cannot be parsed from Partition Info Block +const STATUS_BAD_META: u8 = 6; + +const CODE_TO_LABEL: [&str; 7] = ["Invalid", "New", "Try Boot", "Good", "Failed Boot", "Bad Signature", "Bad Metainfo"]; + +fn is_valid_status_code(code: u8) -> bool { + code <= STATUS_BAD_META +} + +fn status_code_label(code: u8) -> String { + if is_valid_status_code(code) { + CODE_TO_LABEL[code as usize].to_string() + } else { + format!("Invalid status code: {}", code) + } +} + diff --git a/citadel-tools/citadel-rootfs/src/unpacker.rs b/citadel-tools/citadel-rootfs/src/unpacker.rs new file mode 100644 index 0000000..33aa56f --- /dev/null +++ b/citadel-tools/citadel-rootfs/src/unpacker.rs @@ -0,0 +1,129 @@ + +use std::path::{Path,PathBuf}; +use std::fs::{self,File}; +use std::io::{self,Read}; + +use toml; +use ed25519_dalek::SIGNATURE_LENGTH; + +use Result; +use Config; +use Metainfo; +use Partition; +use util::*; +use MAX_METAINFO_LEN; + +pub struct UpdateImageUnpacker { + path: PathBuf, + workdir: Workdir, + metainfo: Metainfo, + metainfo_bytes: Vec, + signature_bytes: Vec, + header_len: usize, +} + +impl UpdateImageUnpacker { + pub fn open>(path: P, config: &Config) -> Result { + let mut f = File::open(path.as_ref())?; + + UpdateImageUnpacker::read_magic(&mut f)?; + let metainfo_bytes = UpdateImageUnpacker::read_metainfo(&mut f)?; + let metainfo = toml::from_slice::(&metainfo_bytes)?; + + let mut signature_bytes = vec![0; SIGNATURE_LENGTH]; + f.read_exact(&mut signature_bytes)?; + + let channel = match config.channel(metainfo.channel()) { + Some(ch) => ch, + None => bail!("Channel '{}' not found in configuration", metainfo.channel()), + }; + + if !channel.verify(metainfo_bytes.as_slice(), &signature_bytes)? { + bail!("Signature verification failed"); + } + + + let mut workdir = Workdir::new(config.citadel_updates_base(), metainfo.channel()); + workdir.set_version(metainfo.version() as usize)?; + + let mlen = metainfo_bytes.len(); + + Ok(UpdateImageUnpacker { + path: PathBuf::from(path.as_ref()), + workdir, metainfo, metainfo_bytes, + signature_bytes, + + header_len: 6 + SIGNATURE_LENGTH + mlen, + }) + } + + fn read_magic(r: &mut File) -> Result<()> { + let mut buf = [0u8; 4]; + r.read_exact(&mut buf)?; + + if &buf != b"UPDT" { + bail!("not an update image, bad magic value"); + } + Ok(()) + } + fn read_metainfo(r: &mut File) -> Result> { + let mut lenbuf = [0u8; 2]; + r.read_exact(&mut lenbuf)?; + let len = (lenbuf[0] as usize) << 8 | (lenbuf[1] as usize); + if len == 0 || len > MAX_METAINFO_LEN { + bail!("metainfo length field has invalid value: {}", len); + } + let mut bytes = vec![0u8; len]; + r.read_exact(bytes.as_mut_slice())?; + Ok(bytes) + } + + pub fn metainfo(&self) -> &Metainfo { + &self.metainfo + } + + pub fn unpack_disk_image(&self) -> Result<()> { + let mut from = File::open(&self.path)?; + info!("{} -> {}", path_str(&self.path), path_str(&self.workdir.filepath("citadel-image.ext2.xz"))); + let mut to = File::create(&self.workdir.filepath("citadel-image.ext2.xz"))?; + let mut discard = vec![0u8; self.header_len]; + from.read_exact(&mut discard)?; + io::copy(&mut from, &mut to)?; + Ok(()) + } + + pub fn decompress_disk_image(&self) -> Result<()> { + let output = self.workdir.filepath("citadel-image.ext2"); + if output.exists() { + fs::remove_file(&output)?; + } + run_xz_command(&self.workdir.filepath("citadel-image.ext2.xz"), true)?; + + let file_sz = fs::metadata(&output)?.len() as usize; + let meta_sz = self.metainfo.nsectors() * 512; + if file_sz != meta_sz { + bail!("Uncompressed images size {} does not match size declared in metainfo {}", file_sz, meta_sz); + } + Ok(()) + } + + pub fn verify_shasum(&self) -> Result<()> { + let path = self.workdir.filepath("citadel-image.ext2"); + let shasum = run_sha256_command(&path)?; + if shasum != self.metainfo.shasum() { + let mut bad = path.clone(); + bad.pop(); bad.push("citadel-image.ext2.badsum"); + fs::rename(&path, &bad)?; + bail!("Failed sha256 sum of {}: {}", path_str(&path), shasum); + } + info!("GOOD: {}", shasum); + Ok(()) + } + + pub fn write_partition(&self, part: &Partition) -> Result<()> { + let path = self.workdir.filepath("citadel-image.ext2"); + part.write_image(&path, &self.metainfo_bytes, &self.signature_bytes)?; + Ok(()) + } + +} diff --git a/citadel-tools/citadel-rootfs/src/util.rs b/citadel-tools/citadel-rootfs/src/util.rs new file mode 100644 index 0000000..89a345f --- /dev/null +++ b/citadel-tools/citadel-rootfs/src/util.rs @@ -0,0 +1,168 @@ +use std::path::{Path,PathBuf}; +use std::fs::{self,File}; +use std::process::Command; +use std::io::{Read,Write,BufReader,BufRead}; + +use Result; + +pub struct Workdir(PathBuf); + +impl Workdir { + pub fn new(base: &str, channel: &str) -> Workdir { + let mut pb = PathBuf::from(base); + pb.push(channel); + Workdir(pb) + } + + pub fn find_next_version(&mut self) -> Result { + let mut version = 1; + loop { + let path = self.0.join(version.to_string()); + if !path.exists() { + self.set_version(version)?; + return Ok(version); + } + version += 1; + } + } + + pub fn set_version(&mut self, version: usize) -> Result<()> { + self.0.push(version.to_string()); + fs::create_dir_all(&self.0)?; + Ok(()) + } + + pub fn filepath(&self, name: &str) -> PathBuf { + self.0.join(name) + } + +} + + +/// +/// Returns `true` if `path` matches the source field (first field) +/// of any of the mount lines listed in /proc/mounts +/// +pub fn is_path_mounted(path: &Path) -> Result { + let path_str = path.to_str().unwrap(); + let f = File::open("/proc/mounts")?; + let reader = BufReader::new(f); + for line in reader.lines() { + if let Some(s) = line?.split_whitespace().next() { + if s == path_str { + return Ok(true); + } + } + } + Ok(false) +} + +/// +/// Converts a `Path` into `&str` representation, assuming +/// that it contains valid utf-8 +/// +pub fn path_str(path: &Path) -> &str { + path.to_str().unwrap() +} + +pub fn rootfs_partition_paths() -> Result> { + let mut rootfs_paths = Vec::new(); + for dent in fs::read_dir("/dev/mapper")? { + let path = dent?.path(); + if is_path_rootfs(&path) { + rootfs_paths.push(path); + } + } + Ok(rootfs_paths) +} +pub fn is_path_rootfs(path: &Path) -> bool { + path_filename(path).starts_with("citadel-rootfs") +} + +fn path_filename(path: &Path) -> &str { + if let Some(osstr) = path.file_name() { + if let Some(name) = osstr.to_str() { + return name; + } + } + "" +} + +pub fn write_string_to_file(path: &Path, s: &str) -> Result<()> { + let mut f = File::create(path)?; + f.write_all(s.as_bytes())?; + Ok(()) +} + +pub fn read_file_as_string(path: &Path) -> Result { + let mut f = File::open(path)?; + let mut buffer = String::new(); + f.read_to_string(&mut buffer)?; + Ok(buffer) +} + +pub fn run_file_command(path: &Path) -> Result { + let path = path_str(path); + let output = try_run_command("/usr/bin/file", &["-b", path])?; + Ok(output) +} + +pub fn run_xz_command(path: &Path, decompress: bool) -> Result<()> { + let path = path_str(path); + if decompress { + let _ = try_run_command("/usr/bin/xz", &["-d", path])?; + } else { + let _ = try_run_command("/usr/bin/xz", &["-T0", path])?; + } + Ok(()) +} + +pub fn run_sha256_command(path: &Path) -> Result { + let path = path_str(path); + let output = try_run_command("/usr/bin/sha256sum", &[path])?; + + let v: Vec<&str> = output.split_whitespace().collect(); + Ok(v[0].trim().to_owned()) +} + +pub fn run_verityformat_command(srcfile: &Path, hashfile: &Path) -> Result { + let srcfile = path_str(srcfile); + let hashfile = path_str(hashfile); + let output = try_run_command("/usr/sbin/veritysetup", + &["format", srcfile, hashfile])?; + Ok(output) +} + +pub fn run_verityinstall_command(block_device: &Path, salt: &str, data_blocks: usize, hash_offset: usize) -> Result { + let data_device = path_str(block_device).to_owned(); + let hash_device = path_str(block_device).to_owned(); + let arg1 = format!("--data-blocks={}", data_blocks); + let arg2 = format!("--hash-offset={}", hash_offset); + let arg3 = format!("--salt={}", salt); + let output = try_run_command("/usr/sbin/veritysetup", &[&arg1, &arg2, &arg3, "format", &data_device, &hash_device])?; + Ok(output) +} + +pub fn run_write_image_dd(image_src: &Path, block_device: &Path) -> Result<()> { + let src = format!("if={}", path_str(image_src)); + let dst = format!("of={}", path_str(block_device)); + let _ = try_run_command("/bin/dd", &[&src, &dst, "bs=4M"])?; + Ok(()) +} + +fn try_run_command(cmd_path: &str, args: &[&str]) -> Result { + let mut cmd = Command::new(cmd_path); + for arg in args { + cmd.arg(arg); + } + + let result = cmd.output()?; + + if !result.status.success() { + let err = String::from_utf8(result.stderr)?; + let argstr = args.join(" "); + bail!("{} {} command failed: {}", cmd_path, argstr, err); + } + let output = String::from_utf8(result.stdout)?; + Ok(output) +} diff --git a/meta-citadel/recipes-core/citadel-tools/citadel-appimg.bb b/meta-citadel/recipes-core/citadel-tools/citadel-appimg.bb new file mode 100644 index 0000000..3b1c646 --- /dev/null +++ b/meta-citadel/recipes-core/citadel-tools/citadel-appimg.bb @@ -0,0 +1,36 @@ +SUMMARY = "citadel-appimg" + +SRC_URI = "\ +crate://crates.io/ansi_term/0.11.0 \ +crate://crates.io/atty/0.2.8 \ +crate://crates.io/backtrace-sys/0.1.16 \ +crate://crates.io/backtrace/0.3.5 \ +crate://crates.io/bitflags/1.0.1 \ +crate://crates.io/cc/1.0.5 \ +crate://crates.io/cfg-if/0.1.2 \ +crate://crates.io/clap/2.31.1 \ +crate://crates.io/failure/0.1.1 \ +crate://crates.io/failure_derive/0.1.1 \ +crate://crates.io/lazy_static/1.0.0 \ +crate://crates.io/libc/0.2.39 \ +crate://crates.io/quote/0.3.15 \ +crate://crates.io/redox_syscall/0.1.37 \ +crate://crates.io/redox_termios/0.1.1 \ +crate://crates.io/rustc-demangle/0.1.7 \ +crate://crates.io/strsim/0.7.0 \ +crate://crates.io/syn/0.11.11 \ +crate://crates.io/synom/0.11.3 \ +crate://crates.io/synstructure/0.6.1 \ +crate://crates.io/termion/1.5.1 \ +crate://crates.io/textwrap/0.9.0 \ +crate://crates.io/unicode-width/0.1.4 \ +crate://crates.io/unicode-xid/0.0.4 \ +crate://crates.io/vec_map/0.8.0 \ +crate://crates.io/winapi-i686-pc-windows-gnu/0.4.0 \ +crate://crates.io/winapi-x86_64-pc-windows-gnu/0.4.0 \ +crate://crates.io/winapi/0.3.4 \ +" + +inherit cargo + +require citadel-tools.inc diff --git a/meta-citadel/recipes-core/citadel-tools/citadel-desktopd.bb b/meta-citadel/recipes-core/citadel-tools/citadel-desktopd.bb new file mode 100644 index 0000000..7de51b2 --- /dev/null +++ b/meta-citadel/recipes-core/citadel-tools/citadel-desktopd.bb @@ -0,0 +1,75 @@ +SUMMARY = "citadel-desktopd" + +SRC_URI = "\ +crate://crates.io/aho-corasick/0.6.4 \ +crate://crates.io/atty/0.2.6 \ +crate://crates.io/backtrace-sys/0.1.16 \ +crate://crates.io/backtrace/0.3.5 \ +crate://crates.io/bitflags/1.0.1 \ +crate://crates.io/byteorder/1.2.1 \ +crate://crates.io/bytes/0.4.6 \ +crate://crates.io/cc/1.0.4 \ +crate://crates.io/cfg-if/0.1.2 \ +crate://crates.io/chrono/0.4.0 \ +crate://crates.io/env_logger/0.5.3 \ +crate://crates.io/failure/0.1.1 \ +crate://crates.io/failure_derive/0.1.1 \ +crate://crates.io/gcc/0.3.54 \ +crate://crates.io/inotify-sys/0.1.2 \ +crate://crates.io/inotify/0.5.0 \ +crate://crates.io/iovec/0.1.2 \ +crate://crates.io/lazy_static/1.0.0 \ +crate://crates.io/libc/0.2.36 \ +crate://crates.io/log/0.4.1 \ +crate://crates.io/memchr/2.0.1 \ +crate://crates.io/nix/0.10.0 \ +crate://crates.io/num-integer/0.1.36 \ +crate://crates.io/num-iter/0.1.35 \ +crate://crates.io/num-traits/0.1.43 \ +crate://crates.io/num-traits/0.2.0 \ +crate://crates.io/num/0.1.41 \ +crate://crates.io/quote/0.3.15 \ +crate://crates.io/redox_syscall/0.1.37 \ +crate://crates.io/redox_termios/0.1.1 \ +crate://crates.io/regex-syntax/0.4.2 \ +crate://crates.io/regex/0.2.5 \ +crate://crates.io/rustc-demangle/0.1.5 \ +crate://crates.io/serde/1.0.27 \ +crate://crates.io/serde_derive/1.0.27 \ +crate://crates.io/serde_derive_internals/0.19.0 \ +crate://crates.io/syn/0.11.11 \ +crate://crates.io/synom/0.11.3 \ +crate://crates.io/synstructure/0.6.1 \ +crate://crates.io/termcolor/0.3.3 \ +crate://crates.io/termion/1.5.1 \ +crate://crates.io/thread_local/0.3.5 \ +crate://crates.io/time/0.1.39 \ +crate://crates.io/toml/0.4.5 \ +crate://crates.io/unicode-xid/0.0.4 \ +crate://crates.io/unreachable/1.0.0 \ +crate://crates.io/utf8-ranges/1.0.0 \ +crate://crates.io/void/1.0.2 \ +crate://crates.io/winapi-i686-pc-windows-gnu/0.4.0 \ +crate://crates.io/winapi-x86_64-pc-windows-gnu/0.4.0 \ +crate://crates.io/winapi/0.2.8 \ +crate://crates.io/winapi/0.3.4 \ +crate://crates.io/wincolor/0.1.5 \ +" + +inherit cargo systemd + +SYSTEMD_SERVICE_${PN} = "citadel-desktopd.service" + +do_install() { + install -d ${D}${libexecdir} + install -d ${D}${datadir}/citadel + install -d ${D}${systemd_system_unitdir} + + install -m 755 ${B}/target/${CARGO_TARGET_SUBDIR}/citadel-desktopd ${D}${libexecdir} + install -m 644 ${B}/conf/citadel-desktopd.conf ${D}${datadir}/citadel + install -m 644 ${B}/conf/citadel-desktopd.service ${D}${systemd_system_unitdir} +} + +FILES_${PN} += "${datadir}/citadel" + +require citadel-tools.inc diff --git a/meta-citadel/recipes-core/citadel-tools/citadel-rootfs.bb b/meta-citadel/recipes-core/citadel-tools/citadel-rootfs.bb new file mode 100644 index 0000000..56fae68 --- /dev/null +++ b/meta-citadel/recipes-core/citadel-tools/citadel-rootfs.bb @@ -0,0 +1,76 @@ +SUMMARY = "citadel-rootfs" + +SRC_URI = "\ +crate://crates.io/ansi_term/0.10.2 \ +crate://crates.io/arrayref/0.3.4 \ +crate://crates.io/atty/0.2.6 \ +crate://crates.io/backtrace-sys/0.1.16 \ +crate://crates.io/backtrace/0.3.5 \ +crate://crates.io/bitflags/1.0.1 \ +crate://crates.io/block-buffer/0.3.3 \ +crate://crates.io/build_const/0.2.0 \ +crate://crates.io/byte-tools/0.2.0 \ +crate://crates.io/byteorder/1.2.1 \ +crate://crates.io/bytes/0.4.6 \ +crate://crates.io/cc/1.0.4 \ +crate://crates.io/cfg-if/0.1.2 \ +crate://crates.io/clap/2.30.0 \ +crate://crates.io/clear_on_drop/0.2.3 \ +crate://crates.io/crc/1.7.0 \ +crate://crates.io/curve25519-dalek/0.14.4 \ +crate://crates.io/digest/0.7.2 \ +crate://crates.io/ed25519-dalek/0.6.1 \ +crate://crates.io/failure/0.1.1 \ +crate://crates.io/failure_derive/0.1.1 \ +crate://crates.io/fake-simd/0.1.2 \ +crate://crates.io/fuchsia-zircon-sys/0.3.3 \ +crate://crates.io/fuchsia-zircon/0.3.3 \ +crate://crates.io/gcc/0.3.54 \ +crate://crates.io/generic-array/0.9.0 \ +crate://crates.io/iovec/0.1.2 \ +crate://crates.io/libc/0.2.36 \ +crate://crates.io/log/0.4.1 \ +crate://crates.io/lzma-rs/0.1.0 \ +crate://crates.io/nix/0.10.0 \ +crate://crates.io/num-traits/0.1.43 \ +crate://crates.io/num-traits/0.2.0 \ +crate://crates.io/quote/0.3.15 \ +crate://crates.io/rand/0.4.2 \ +crate://crates.io/redox_syscall/0.1.37 \ +crate://crates.io/redox_termios/0.1.1 \ +crate://crates.io/rustc-demangle/0.1.6 \ +crate://crates.io/rustc-serialize/0.3.24 \ +crate://crates.io/serde/1.0.27 \ +crate://crates.io/serde_derive/1.0.27 \ +crate://crates.io/serde_derive_internals/0.19.0 \ +crate://crates.io/sha2/0.7.0 \ +crate://crates.io/strsim/0.7.0 \ +crate://crates.io/subtle/0.3.0 \ +crate://crates.io/subtle/0.5.1 \ +crate://crates.io/syn/0.11.11 \ +crate://crates.io/synom/0.11.3 \ +crate://crates.io/synstructure/0.6.1 \ +crate://crates.io/termion/1.5.1 \ +crate://crates.io/textwrap/0.9.0 \ +crate://crates.io/toml/0.4.5 \ +crate://crates.io/typenum/1.9.0 \ +crate://crates.io/unicode-width/0.1.4 \ +crate://crates.io/unicode-xid/0.0.4 \ +crate://crates.io/vec_map/0.8.0 \ +crate://crates.io/void/1.0.2 \ +crate://crates.io/winapi-i686-pc-windows-gnu/0.4.0 \ +crate://crates.io/winapi-x86_64-pc-windows-gnu/0.4.0 \ +crate://crates.io/winapi/0.2.8 \ +crate://crates.io/winapi/0.3.4 \ +" + +do_install() { + install -d ${D}${bindir} + install -d ${D}${datadir}/citadel + + install -m 755 ${B}/target/${CARGO_TARGET_SUBDIR}/citadel-rootfs ${D}${bindir} + install -m 644 ${B}/conf/citadel-rootfs.conf ${D}${datadir}/citadel +} + +inherit cargo +require citadel-tools.inc diff --git a/meta-citadel/recipes-core/citadel-tools/citadel-tools.inc b/meta-citadel/recipes-core/citadel-tools/citadel-tools.inc new file mode 100644 index 0000000..e3ead11 --- /dev/null +++ b/meta-citadel/recipes-core/citadel-tools/citadel-tools.inc @@ -0,0 +1,16 @@ +HOMEPAGE = "http://github.com/subgraph/citadel" +LICENSE = "CLOSED" +LIC_FILES_CHKSUM="" + +# what is this even for? +CARGO_SRC_DIR="" + +S = "${WORKDIR}/${PN}" +do_unpack_src() { + rm -rf ${WORKDIR}/${PN} + cp -a ${TOPDIR}/../citadel-tools/${PN} ${WORKDIR} + rm -rf ${WORKDIR}$/${PN}/target +} +# don't use cached task +#do_unpack_src[nostamp] = "1" +addtask unpack_src after do_unpack before do_patch diff --git a/meta-citadel/recipes-core/primary-user-appimg/files/primary-user-appimg.path b/meta-citadel/recipes-core/launch-default-appimg/files/launch-default-appimg.path similarity index 100% rename from meta-citadel/recipes-core/primary-user-appimg/files/primary-user-appimg.path rename to meta-citadel/recipes-core/launch-default-appimg/files/launch-default-appimg.path diff --git a/meta-citadel/recipes-core/launch-default-appimg/files/launch-default-appimg.service b/meta-citadel/recipes-core/launch-default-appimg/files/launch-default-appimg.service new file mode 100644 index 0000000..4e3d686 --- /dev/null +++ b/meta-citadel/recipes-core/launch-default-appimg/files/launch-default-appimg.service @@ -0,0 +1,8 @@ +[Unit] +Description=Launch default appimg +ConditionPathExists=!/run/appimg + +[Service] +ExecStart=/usr/bin/citadel-appimg start +Type=oneshot +RemainAfterExit=yes diff --git a/meta-citadel/recipes-core/primary-user-appimg/files/watch-run-user.path b/meta-citadel/recipes-core/launch-default-appimg/files/watch-run-user.path similarity index 73% rename from meta-citadel/recipes-core/primary-user-appimg/files/watch-run-user.path rename to meta-citadel/recipes-core/launch-default-appimg/files/watch-run-user.path index 7b4876b..a58447a 100644 --- a/meta-citadel/recipes-core/primary-user-appimg/files/watch-run-user.path +++ b/meta-citadel/recipes-core/launch-default-appimg/files/watch-run-user.path @@ -1,5 +1,5 @@ [Unit] -Description=Watch Just watching Run User +Description=Watch for creation of /run/user/1000 After=run-user-1000.mount Requires=run-user-1000.mount diff --git a/meta-citadel/recipes-core/launch-default-appimg/files/watch-run-user.service b/meta-citadel/recipes-core/launch-default-appimg/files/watch-run-user.service new file mode 100644 index 0000000..c491b91 --- /dev/null +++ b/meta-citadel/recipes-core/launch-default-appimg/files/watch-run-user.service @@ -0,0 +1,6 @@ +[Unit] +Description=Watch run-user service + +[Service] +Type=oneshot +ExecStart=/usr/bin/systemctl --no-block start launch-default-appimg.path diff --git a/meta-citadel/recipes-core/launch-default-appimg/launch-default-appimg.bb b/meta-citadel/recipes-core/launch-default-appimg/launch-default-appimg.bb new file mode 100644 index 0000000..98b7cf6 --- /dev/null +++ b/meta-citadel/recipes-core/launch-default-appimg/launch-default-appimg.bb @@ -0,0 +1,29 @@ + +DESCRIPTION = "Install systemd unit file to automatically start default appimg" +LICENSE = "MIT" +LIC_FILES_CHKSUM = "file://${COREBASE}/meta/COPYING.MIT;md5=3da9cfbcb788c80a0384361b4de20420" +SECTION = "" +DEPENDS = "" + +inherit systemd + +SRC_URI = "file://launch-default-appimg.path file://launch-default-appimg.service file://watch-run-user.path file://watch-run-user.service" + +S = "${WORKDIR}" + +SYSTEMD_SERVICE_${PN} = "watch-run-user.path" +RDEPENDS_${PN} = "bash" + +FILES_${PN} += "\ + ${systemd_system_unitdir}/watch-run-user.service \ + ${systemd_system_unitdir}/launch-default-appimg.path \ + ${systemd_system_unitdir}/launch-default-appimg.service \ +" + +do_install() { + install -d ${D}${systemd_system_unitdir} + install -m 644 ${WORKDIR}/launch-default-appimg.path ${D}${systemd_system_unitdir} + install -m 644 ${WORKDIR}/launch-default-appimg.service ${D}${systemd_system_unitdir} + install -m 644 ${WORKDIR}/watch-run-user.path ${D}${systemd_system_unitdir} + install -m 644 ${WORKDIR}/watch-run-user.service ${D}${systemd_system_unitdir} +} diff --git a/meta-citadel/recipes-core/packagegroups/packagegroup-citadel-desktop.bb b/meta-citadel/recipes-core/packagegroups/packagegroup-citadel-desktop.bb index 8b943c9..e40e0ce 100644 --- a/meta-citadel/recipes-core/packagegroups/packagegroup-citadel-desktop.bb +++ b/meta-citadel/recipes-core/packagegroups/packagegroup-citadel-desktop.bb @@ -75,8 +75,10 @@ RDEPENDS_${PN} = "\ e2fsprogs \ dosfstools \ btrfs-tools \ - primary-user-appimg\ dash-to-panel \ - desktopd \ + launch-default-appimg \ + citadel-desktopd \ + citadel-rootfs \ + citadel-appimg \ iproute2 \ " diff --git a/meta-citadel/recipes-core/primary-user-appimg/files/primary-user-appimg.service b/meta-citadel/recipes-core/primary-user-appimg/files/primary-user-appimg.service deleted file mode 100644 index de28182..0000000 --- a/meta-citadel/recipes-core/primary-user-appimg/files/primary-user-appimg.service +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Default User appimg -ConditionPathExists=/storage/appimg/primary/rootfs - -[Service] -Environment=SYSTEMD_NSPAWN_SHARE_NS_IPC=1 -ExecStart=/usr/bin/systemd-nspawn --quiet --keep-unit --machine=primary --link-journal=try-guest --directory=/storage/appimg/default.appimg/rootfs - -KillMode=mixed -Type=notify -RestartForceExitStatus=133 -SuccessExitStatus=133 diff --git a/meta-citadel/recipes-core/primary-user-appimg/files/primary.nspawn b/meta-citadel/recipes-core/primary-user-appimg/files/primary.nspawn deleted file mode 100644 index 0e75caf..0000000 --- a/meta-citadel/recipes-core/primary-user-appimg/files/primary.nspawn +++ /dev/null @@ -1,37 +0,0 @@ -[Exec] -Boot=true -Environment=IFCONFIG_IP=172.17.0.2/24 -Environment=IFCONFIG_GW=172.17.0.1 - -[Files] -BindReadOnly=/usr/share/themes/Adapta -BindReadOnly=/usr/share/themes/Adapta-Eta -BindReadOnly=/usr/share/themes/Adapta-Nokto -BindReadOnly=/usr/share/themes/Adapta-Nokto-Eta -BindReadOnly=/usr/share/icons/Paper - -Bind=/storage/user-data/primary-home:/home/user -BindReadOnly=/storage/citadel-state/resolv.conf:/etc/resolv.conf - -# -# Bind mounts for sound and pulse audio -# -Bind=/dev/snd -Bind=/dev/shm -BindReadOnly=/run/user/1000/pulse:/run/user/host/pulse - -BindReadOnly=/tmp/.X11-unix -BindReadOnly=/run/user/1000/wayland-0:/run/user/host/wayland-0 - -# -# Uncomment to enable kvm access in container -# -#Bind=/dev/kvm - -# -# Uncomment to enable GPU access in container -# -#Bind=/dev/dri/renderD128 - -[Network] -Zone=clear diff --git a/meta-citadel/recipes-core/primary-user-appimg/files/run-in-image b/meta-citadel/recipes-core/primary-user-appimg/files/run-in-image deleted file mode 100644 index af965b4..0000000 --- a/meta-citadel/recipes-core/primary-user-appimg/files/run-in-image +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -machinectl -E DESKTOP_STARTUP_ID=${DESKTOP_STARTUP_ID} shell user@primary /usr/libexec/launch $@ diff --git a/meta-citadel/recipes-core/primary-user-appimg/files/watch-run-user.service b/meta-citadel/recipes-core/primary-user-appimg/files/watch-run-user.service deleted file mode 100644 index a3b67b0..0000000 --- a/meta-citadel/recipes-core/primary-user-appimg/files/watch-run-user.service +++ /dev/null @@ -1,6 +0,0 @@ -[Unit] -Description=Watch run-user service - -[Service] -Type=oneshot -ExecStart=/usr/bin/systemctl --no-block start primary-user-appimg.path diff --git a/meta-citadel/recipes-core/primary-user-appimg/primary-user-appimg.bb b/meta-citadel/recipes-core/primary-user-appimg/primary-user-appimg.bb deleted file mode 100644 index 8d8b230..0000000 --- a/meta-citadel/recipes-core/primary-user-appimg/primary-user-appimg.bb +++ /dev/null @@ -1,33 +0,0 @@ - -DESCRIPTION = "Install systemd unit file to automatically start primary-user-appimg container" -LICENSE = "MIT" -LIC_FILES_CHKSUM = "file://${COREBASE}/meta/COPYING.MIT;md5=3da9cfbcb788c80a0384361b4de20420" -SECTION = "" -DEPENDS = "" - -inherit systemd - -SRC_URI = "file://primary-user-appimg.path file://primary-user-appimg.service file://primary.nspawn file://watch-run-user.path file://watch-run-user.service file://run-in-image" - -S = "${WORKDIR}" - -SYSTEMD_SERVICE_${PN} = "watch-run-user.path" -RDEPENDS_${PN} = "bash" - -FILES_${PN} += "\ - ${systemd_system_unitdir}/watch-run-user.service \ - ${systemd_system_unitdir}/primary-user-appimg.path \ - ${systemd_system_unitdir}/primary-user-appimg.service \ -" - -do_install() { - install -d ${D}${systemd_system_unitdir} - install -m 644 ${WORKDIR}/primary-user-appimg.path ${D}${systemd_system_unitdir} - install -m 644 ${WORKDIR}/primary-user-appimg.service ${D}${systemd_system_unitdir} - install -m 644 ${WORKDIR}/watch-run-user.path ${D}${systemd_system_unitdir} - install -m 644 ${WORKDIR}/watch-run-user.service ${D}${systemd_system_unitdir} - install -d ${D}${sysconfdir}/systemd/nspawn - install -m 644 ${WORKDIR}/primary.nspawn ${D}${sysconfdir}/systemd/nspawn - install -d ${D}${libexecdir} - install -m 755 ${WORKDIR}/run-in-image ${D}${libexecdir} -}