diff --git a/citadel-tools/citadel-realms/Cargo.lock b/citadel-tools/citadel-realms/Cargo.lock new file mode 100644 index 0000000..2ee093f --- /dev/null +++ b/citadel-tools/citadel-realms/Cargo.lock @@ -0,0 +1,350 @@ +[[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.6 (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.6" +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-realms" +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)", + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 2.1.4 (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 = "libc" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (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 = "quote" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.2.3 (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 = "same-file" +version = "1.0.2" +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 = "serde" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_derive" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive_internals 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.12.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive_internals" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.12.14 (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 = "syn" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "wincolor 0.1.6 (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 = "toml" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.30 (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 = "unicode-xid" +version = "0.1.0" +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 = "walkdir" +version = "2.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "same-file 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (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.6" +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 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.6 (registry+https://github.com/rust-lang/crates.io-index)" = "fedf677519ac9e865c4ff43ef8f930773b37ed6e6ea61b6b83b400a7b5787f49" +"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 libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)" = "f54263ad99207254cf58b5f701ecb432c717445ea2ee8af387334bdd1a03fdff" +"checksum proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cd07deb3c6d1d9ff827999c7f9b04cdfd66b1b17ae508e14fe47b620f2282ae0" +"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" +"checksum quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1eca14c727ad12702eb4b6bfb5a232287dcf8385cb8ca83a3eeaf6519c44c408" +"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 same-file 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cfb6eded0b06a0b512c8ddbcf04089138c9b4362c2f696f3c3d76039d68f3637" +"checksum serde 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)" = "1f4d6340aa5fcdac490a1aa3511ff079b1cdaa45ffb766b2fd83395dae085cd5" +"checksum serde_derive 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)" = "a5c7e2a9833bb397a3284b55e208b895e2486b8e6c6682a428e309204cd9d75a" +"checksum serde_derive_internals 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1fc848d073be32cd982380c06587ea1d433bc1a4c4a111de07ec2286a3ddade8" +"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 syn 0.12.14 (registry+https://github.com/rust-lang/crates.io-index)" = "8c5bc2d6ff27891209efa5f63e9de78648d7801f085e4653701a692ce938d6fd" +"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.5 (registry+https://github.com/rust-lang/crates.io-index)" = "56c456352e44f9f91f774ddeeed27c1ec60a2455ed66d692059acfb1d731bda1" +"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 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 unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c" +"checksum walkdir 2.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "63636bd0eb3d00ccb8b9036381b526efac53caf112b7783b730ab3f8e44da369" +"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.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eeb06499a3a4d44302791052df005d5232b927ed1a9658146d842165c4de7767" diff --git a/citadel-tools/citadel-realms/Cargo.toml b/citadel-tools/citadel-realms/Cargo.toml new file mode 100644 index 0000000..2dd374c --- /dev/null +++ b/citadel-tools/citadel-realms/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "citadel-realms" +version = "0.1.0" +authors = ["Bruce Leidl "] +homepage = "http://github.com/subgraph/citadel" + +[dependencies] +libc = "0.2" +clap = "2.30.0" +failure = "0.1.1" +toml = "0.4.5" +serde_derive = "1.0.27" +serde = "1.0.27" +termcolor = "0.3" +walkdir = "2" diff --git a/citadel-tools/citadel-realms/README.md b/citadel-tools/citadel-realms/README.md new file mode 100644 index 0000000..b61777a --- /dev/null +++ b/citadel-tools/citadel-realms/README.md @@ -0,0 +1,84 @@ + + + +## `default` realm + +One realm is always selected to be the `default` realm. The default realm +starts automatically when the system boots. The `realms` utility can be used +to change which realm is the default realm. Switching the default realm changes +the symlink `/realm/default.realm` to point to a different realm instance directory. + + citadel:~# realms default + Default Realm: main + + citadel:~# realms default project + [+] default realm changed from 'main' to 'project' + + citadel:~# realms default + Default Realm: project + +## `current` realm + +If any realms are running, then one realm is always the `current` realm. The current +realm is a realm that is being monitored by the `citadel-desktopd` daemon. This +daemon is responsible for safely copying application `.desktop` files from the running +realm instance to a temporary directory where they will be read by the GNOME desktop to +to display a menu of applications that can be launched. + +Changing the `current` realm, changes the set of applications which are visible to +gnome-shell to only the applications installed in this realm. Also, any applications +started by gnome-shell will run in the `current` realm. + + citadel:~# realms + Current Realm: main + +## Realms base directory layout + +The realms base directory is stored on the storage partition at `/storage/realms` and is bind mounted to `/realms` on the root filesystem for convenience. + + /realms + config + /Shared + /skel + /default.realm -> realm-main + /realm-main + /realm-project + /realm-testing + +### `/realms/config` file + +This file is a template of the configuration file for individual realms. When a new realm is created this file in copied into the new realm instance directory. By modifying this file, the default configuration for new realm instances can be changed. + +### `/realms/Shared` directory + +This directory is bind mounted to `/home/user/Shared` of each running realm that has the option `use-shared-dir` enabled. It's a convenient way to move files between different realms and between citadel and realms. + +### `/realms/skel` directory + +Files which are added to this directory will be copied into the home directory of any newly created realm. The directory is copied as a tree of files and may contain subdirectories. + +### `/realms/default.realm` + +A symlink which points to a realm instance directory of the default realm. The default realm is the realm which starts when the system is booted. + +### `/realms/realm-$name` + +This is a realm instance directory, for a realm with $name as the realm name. + + /realm-main + config + /home + /rootfs + + * `config` : configuration file copied from `/realms/config` + * `/home` : directory mounted to `/home/user` in the realm, populated from `/realms/skel` + * `/rootfs` : btrfs subvolume clone (snapshot) of an application image. + + +### Realm instance directory layout + + /realm-main + config + /home + /rootfs + diff --git a/citadel-tools/citadel-realms/src/appimg.rs b/citadel-tools/citadel-realms/src/appimg.rs new file mode 100644 index 0000000..0a93e1a --- /dev/null +++ b/citadel-tools/citadel-realms/src/appimg.rs @@ -0,0 +1,43 @@ +use std::path::Path; +use std::process::Command; + +use Realm; +use Result; + +const BASE_APPIMG_PATH: &str = "/storage/appimg/base.appimg"; +const BTRFS_COMMAND: &str = "/usr/bin/btrfs"; + +pub fn clone_base_appimg(target_realm: &Realm) -> Result<()> { + if !Path::new(BASE_APPIMG_PATH).exists() { + bail!("base appimg does not exist at {}", BASE_APPIMG_PATH); + } + let target = format!("/realms/realm-{}/rootfs", target_realm.name()); + let target_path = Path::new(&target); + + if target_path.exists() { + bail!("cannot create clone of base appimg for realm '{}' because rootfs directory already exists at {}", + target_realm.name(), target); + } + + if !target_path.parent().unwrap().exists() { + bail!("cannot create clone of base appimg for realm '{}' because realm directory /realms/realm-{} does not exist.", + target_realm.name(), target_realm.name()); + } + + Command::new(BTRFS_COMMAND) + .args(&["subvolume", "snapshot", BASE_APPIMG_PATH, &target ]) + .status() + .map_err(|e| format_err!("failed to execute {}: {}", BTRFS_COMMAND, e))?; + Ok(()) + +} + +pub fn delete_rootfs_subvolume(realm: &Realm) -> Result<()> { + let path = realm.base_path().join("rootfs"); + Command::new(BTRFS_COMMAND) + .args(&["subvolume", "delete", path.to_str().unwrap() ]) + .status() + .map_err(|e| format_err!("failed to execute {}: {}", BTRFS_COMMAND, e))?; + Ok(()) +} + diff --git a/citadel-tools/citadel-realms/src/config.rs b/citadel-tools/citadel-realms/src/config.rs new file mode 100644 index 0000000..bcda2af --- /dev/null +++ b/citadel-tools/citadel-realms/src/config.rs @@ -0,0 +1,112 @@ +use std::path::Path; +use std::fs::File; +use std::io::Read; +use toml; +use Result; + +fn default_true() -> bool { + true +} + +fn default_zone() -> String { + "clear".to_owned() +} + +#[derive (Deserialize,Clone)] +pub struct RealmConfig { + #[serde(default = "default_true", rename="add-shared-dir")] + add_shared_dir: bool, + + #[serde(default, rename="use-ephemeral-home")] + use_ephemeral_home: bool, + + #[serde(default = "default_true", rename="use-sound")] + use_sound: bool, + + #[serde(default = "default_true", rename="use-x11")] + use_x11: bool, + + #[serde(default = "default_true", rename="use-wayland")] + use_wayland: bool, + + #[serde(default, rename="use-kvm")] + use_kvm: bool, + + #[serde(default,rename="use-gpu")] + use_gpu: bool, + + #[serde(default = "default_true", rename="use-network")] + use_network: bool, + + #[serde(default = "default_zone", rename="network-zone")] + network_zone: String, +} + +impl RealmConfig { + pub fn load_or_default(path: &Path) -> Result { + if path.exists() { + let s = load_as_string(&path)?; + let config = toml::from_str::(&s)?; + Ok(config) + } else { + Ok(RealmConfig::default()) + } + } + + pub fn default() -> RealmConfig { + RealmConfig { + add_shared_dir: true, + use_ephemeral_home: false, + use_sound: true, + use_x11: true, + use_wayland: true, + use_kvm: false, + use_gpu: false, + use_network: true, + network_zone: default_zone(), + } + } + + pub fn kvm(&self) -> bool { + self.use_kvm + } + + pub fn gpu(&self) -> bool { + self.use_gpu + } + + pub fn shared_dir(&self) -> bool { + self.add_shared_dir + } + + pub fn emphemeral_home(&self) -> bool { + self.use_ephemeral_home + } + + pub fn sound(&self) -> bool { + self.use_sound + } + + pub fn x11(&self) -> bool { + self.use_x11 + } + + pub fn wayland(&self) -> bool { + self.use_wayland + } + + pub fn network(&self) -> bool { + self.use_network + } + + pub fn network_zone(&self) -> &str { + &self.network_zone + } +} + +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) +} diff --git a/citadel-tools/citadel-realms/src/main.rs b/citadel-tools/citadel-realms/src/main.rs new file mode 100644 index 0000000..a395d93 --- /dev/null +++ b/citadel-tools/citadel-realms/src/main.rs @@ -0,0 +1,304 @@ +#[macro_use] extern crate failure; +#[macro_use] extern crate serde_derive; + +extern crate libc; +extern crate clap; +extern crate toml; +extern crate termcolor; +extern crate walkdir; + +use failure::Error; +use clap::{App,Arg,ArgMatches,SubCommand}; +use clap::AppSettings::*; +use std::process::exit; +use std::cell::RefCell; +use std::result; + +pub type Result = result::Result; + +thread_local! { + pub static VERBOSE: RefCell = RefCell::new(true); +} +pub fn verbose() -> bool { + VERBOSE.with(|f| *f.borrow()) +} + +macro_rules! warn { + ($e:expr) => { println!("[!]: {}", $e); }; + ($fmt:expr, $($arg:tt)+) => { println!("[!]: {}", format!($fmt, $($arg)+)); }; +} + +macro_rules! info { + ($e:expr) => { if ::verbose() { println!("[+]: {}", $e); } }; + ($fmt:expr, $($arg:tt)+) => { if ::verbose() { println!("[+]: {}", format!($fmt, $($arg)+)); } }; +} + +mod manager; +mod realm; +mod util; +mod systemd; +mod config; +mod network; +mod appimg; + +use realm::{Realm,RealmSymlinks}; +use manager::RealmManager; +use config::RealmConfig; +use systemd::Systemd; +use network::NetworkConfig; + +fn main() { + let app = App::new("citadel-realms") + .about("Subgraph Citadel realm management") + .after_help("'realms help ' to display help for an individual subcommand\n") + .global_settings(&[ColoredHelp, DisableVersion, DeriveDisplayOrder, AllowMissingPositional, VersionlessSubcommands ]) + .arg(Arg::with_name("help").long("help").hidden(true)) + .arg(Arg::with_name("quiet") + .long("quiet") + .help("Don't display extra output")) + + .subcommand(SubCommand::with_name("list") + .arg(Arg::with_name("help").long("help").hidden(true)) + .about("Display list of all realms")) + + .subcommand(SubCommand::with_name("shell") + .arg(Arg::with_name("help").long("help").hidden(true)) + .about("Open shell in current or named realm") + + .arg(Arg::with_name("realm-name") + .help("Name of a realm to open shell in. Use current realm if omitted.")) + + .arg(Arg::with_name("root-shell") + .long("root") + .help("Open shell as root instead of user account."))) + + .subcommand(SubCommand::with_name("terminal") + .arg(Arg::with_name("help").long("help").hidden(true)) + .about("Launch terminal in current or named realm") + + .arg(Arg::with_name("realm-name") + .help("Name of realm to open terminal in. Use current realm if omitted."))) + + .subcommand(SubCommand::with_name("start") + .arg(Arg::with_name("help").long("help").hidden(true)) + .about("Start named realm or default realm") + .arg(Arg::with_name("realm-name") + .help("Name of realm to start. Use default realm if omitted."))) + + .subcommand(SubCommand::with_name("stop") + .arg(Arg::with_name("help").long("help").hidden(true)) + .about("Stop a running realm by name") + .arg(Arg::with_name("realm-name") + .required(true) + .help("Name of realm to stop."))) + + .subcommand(SubCommand::with_name("default") + .arg(Arg::with_name("help").long("help").hidden(true)) + .about("Choose a realm to start automatically on boot") + .arg(Arg::with_name("realm-name") + .help("Name of a realm to set as default. Display current default realm if omitted."))) + + .subcommand(SubCommand::with_name("current") + .arg(Arg::with_name("help").long("help").hidden(true)) + .about("Choose a realm to set as 'current' realm") + .arg(Arg::with_name("realm-name") + .help("Name of a realm to set as current, will start if necessary. Display current realm name if omitted."))) + + .subcommand(SubCommand::with_name("run") + .arg(Arg::with_name("help").long("help").hidden(true)) + .about("Execute a command in named realm or current realm") + + .arg(Arg::with_name("realm-name") + .help("Name of realm to run command in, start if necessary. Use current realm if omitted.")) + .arg(Arg::with_name("args") + .required(true) + .last(true) + .allow_hyphen_values(true) + .multiple(true))) + + .subcommand(SubCommand::with_name("update-appimg") + .arg(Arg::with_name("help").long("help").hidden(true)) + .about("Launch shell to update application image") + + .arg(Arg::with_name("appimg-name") + .long("appimg") + .help("Name of application image in /storage/appimg directory. Default is to use base.appimg") + .takes_value(true))) + + + .subcommand(SubCommand::with_name("new") + .arg(Arg::with_name("help").long("help").hidden(true)) + .about("Create a new realm with the name provided") + .arg(Arg::with_name("realm-name") + .required(true) + .help("Name to assign to newly created realm"))) + + .subcommand(SubCommand::with_name("remove") + .arg(Arg::with_name("help").long("help").hidden(true)) + .about("Remove realm by name") + + .arg(Arg::with_name("no-confirm") + .long("no-confirm") + .help("Do not prompt for confirmation.")) + .arg(Arg::with_name("remove-home") + .long("remove-home") + .help("Also remove home directory with --no-confirm rather than moving it to /realms/removed-homes")) + + .arg(Arg::with_name("realm-name") + .help("Name of realm to remove") + .required(true))); + + + + let matches = app.get_matches(); + + if matches.is_present("quiet") { + VERBOSE.with(|f| *f.borrow_mut() = false); + } + + let result = match matches.subcommand() { + ("list", _) => do_list(), + ("start", Some(m)) => do_start(m), + ("stop", Some(m)) => do_stop(m), + ("default", Some(m)) => do_default(m), + ("current", Some(m)) => do_current(m), + ("run", Some(m)) => do_run(m), + ("shell", Some(m)) => do_shell(m), + ("terminal", Some(m)) => do_terminal(m), + ("new", Some(m)) => do_new(m), + ("remove", Some(m)) => do_remove(m), + ("base-update", _) => do_base_update(), + _ => do_list(), + }; + + if let Err(e) = result { + warn!("{}", e); + exit(1); + } +} + +fn is_root() -> bool { + unsafe { + libc::geteuid() == 0 + } +} + +fn require_root() -> Result<()> { + if !is_root() { + bail!("You need to do that as root") + } + Ok(()) +} + +fn do_list() -> Result<()> { + let manager = RealmManager::load()?; + println!(); + manager.list()?; + println!("\n 'realms help' for list of commands\n"); + Ok(()) +} + +fn do_start(matches: &ArgMatches) -> Result<()> { + require_root()?; + let mut manager = RealmManager::load()?; + match matches.value_of("realm-name") { + Some(name) => manager.start_named_realm(name)?, + None => manager.start_default()?, + }; + Ok(()) +} + +fn do_stop(matches: &ArgMatches) -> Result<()> { + require_root()?; + let name = matches.value_of("realm-name").unwrap(); + let mut manager = RealmManager::load()?; + manager.stop_realm(name)?; + Ok(()) +} + +fn do_default(matches: &ArgMatches) -> Result<()> { + let manager = RealmManager::load()?; + + match matches.value_of("realm-name") { + Some(name) => { + require_root()?; + manager.set_default_by_name(name)?; + }, + None => { + if let Some(name) = manager.default_realm_name() { + println!("Default Realm: {}", name); + } else { + println!("No default realm."); + } + }, + } + Ok(()) +} + +fn do_current(matches: &ArgMatches) -> Result<()> { + let manager = RealmManager::load()?; + + match matches.value_of("realm-name") { + Some(name) => { + require_root()?; + manager.set_current_by_name(name)?; + }, + None => { + if let Some(name) = manager.current_realm_name() { + println!("Current Realm: {}", name); + } else { + println!("No current realm."); + } + }, + } + Ok(()) +} + + +fn do_run(matches: &ArgMatches) -> Result<()> { + let args: Vec<&str> = matches.values_of("args").unwrap().collect(); + let mut v = Vec::new(); + for arg in args { + v.push(arg.to_string()); + } + let manager = RealmManager::load()?; + manager.run_in_realm(matches.value_of("realm-name"), &v, true)?; + Ok(()) +} + +fn do_shell(matches: &ArgMatches) -> Result<()> { + let manager = RealmManager::load()?; + let root = matches.is_present("root-shell"); + manager.launch_shell(matches.value_of("realm-name"), root)?; + Ok(()) +} + +fn do_terminal(matches: &ArgMatches) -> Result<()> { + let manager = RealmManager::load()?; + manager.launch_terminal(matches.value_of("realm-name"))?; + Ok(()) +} + +fn do_new(matches: &ArgMatches) -> Result<()> { + require_root()?; + let name = matches.value_of("realm-name").unwrap(); + let mut manager = RealmManager::load()?; + manager.new_realm(name)?; + Ok(()) +} + +fn do_remove(matches: &ArgMatches) -> Result<()> { + require_root()?; + let confirm = !matches.is_present("no-confirm"); + let save_home = !matches.is_present("remove-home"); + let name = matches.value_of("realm-name").unwrap(); + let mut manager = RealmManager::load()?; + manager.remove_realm(name, confirm, save_home)?; + Ok(()) +} + +fn do_base_update() -> Result<()> { + require_root()?; + let manager = RealmManager::load()?; + manager.base_appimg_update() +} diff --git a/citadel-tools/citadel-realms/src/manager.rs b/citadel-tools/citadel-realms/src/manager.rs new file mode 100644 index 0000000..867f074 --- /dev/null +++ b/citadel-tools/citadel-realms/src/manager.rs @@ -0,0 +1,399 @@ +use std::rc::Rc; +use std::cell::RefCell; +use std::path::{Path,PathBuf}; +use std::fs; +use std::collections::HashMap; +use std::io::Write; + + +use Realm; +use Result; +use Systemd; +use RealmSymlinks; +use NetworkConfig; +use util::*; + +const REALMS_BASE_PATH: &str = "/realms"; + +pub struct RealmManager { + /// Map from realm name -> realm + realm_map: HashMap, + + /// Sorted for 'list' + realm_list: Vec, + + /// track status of 'current' and 'default' symlinks + symlinks: Rc>, + + /// finds free ip addresses to use + network: Rc>, + + /// interface to systemd + systemd: Systemd, +} + + +impl RealmManager { + fn new() -> Result { + let network = RealmManager::create_network_config()?; + + Ok(RealmManager { + realm_map: HashMap::new(), + realm_list: Vec::new(), + symlinks: Rc::new(RefCell::new(RealmSymlinks::new())), + network: network.clone(), + systemd: Systemd::new(network), + }) + } + + fn create_network_config() -> Result>> { + let mut network = NetworkConfig::new(); + network.add_bridge("clear", "172.17.0.0/24")?; + Ok(Rc::new(RefCell::new(network))) + } + + pub fn load() -> Result { + let mut manager = RealmManager::new()?; + manager.symlinks.borrow_mut().load_symlinks()?; + if ! PathBuf::from(REALMS_BASE_PATH).exists() { + bail!("realms base directory {} does not exist", REALMS_BASE_PATH); + } + for dent in fs::read_dir(REALMS_BASE_PATH)? { + let path = dent?.path(); + manager.process_realm_path(&path) + .map_err(|e| format_err!("error processing entry {} in realm base dir: {}", path.display(), e))?; + } + manager.realm_list.sort_unstable(); + Ok(manager) + } + + /// + /// Process `path` as an entry from the base realms directory and + /// if `path` is a directory, and directory name has prefix "realm-" + /// extract chars after prefix as realm name and add a new `Realm` + /// instance + /// + fn process_realm_path(&mut self, path: &Path) -> Result<()> { + let meta = path.symlink_metadata()?; + if !meta.is_dir() { + return Ok(()) + } + + let fname = path_filename(path); + if !fname.starts_with("realm-") { + return Ok(()) + } + + let (_, realm_name) = fname.split_at(6); + if !is_valid_realm_name(realm_name) { + warn!("ignoring directory in realm storage which has invalid realm name: {}", realm_name); + return Ok(()) + } + let rootfs = path.join("rootfs"); + if !rootfs.exists() { + warn!("realm directory {} does not have a rootfs, ignoring", path.display()); + return Ok(()) + } + + match Realm::new(realm_name, self.symlinks.clone(), self.network.clone()) { + Ok(realm) => { self.add_realm_entry(realm);} , + Err(e) => warn!("Ignoring '{}': {}", realm_name, e), + }; + Ok(()) + + } + + fn add_realm_entry(&mut self, realm: Realm) -> &Realm { + self.realm_map.insert(realm.name().to_owned(), realm.clone()); + self.realm_list.push(realm.clone()); + self.realm_map.get(realm.name()).expect("cannot find realm we just added to map") + } + + fn remove_realm_entry(&mut self, name: &str) -> Result<()> { + self.realm_map.remove(name); + let list = self.realm_list.clone(); + let mut have_default = false; + self.realm_list.clear(); + for realm in list { + if realm.name() != name { + if realm.is_default() { + have_default = true; + } + self.realm_list.push(realm); + } + } + if !have_default && !self.realm_list.is_empty() { + self.symlinks.borrow_mut().set_default_symlink(self.realm_list[0].name())?; + } + Ok(()) + } + + pub fn current_realm_name(&self) -> Option { + self.symlinks.borrow().current() + } + + pub fn default_realm_name(&self) -> Option { + self.symlinks.borrow().default() + } + + /// + /// Execute shell in a realm. If `realm_name` is `None` then exec + /// shell in current realm, otherwise look up realm by name. + /// + /// If `root_shell` is true, open a root shell, otherwise open + /// a user (uid = 1000) shell. + /// + pub fn launch_shell(&self, realm_name: Option<&str>, root_shell: bool) -> Result<()> { + let run_shell = |realm: &Realm| { + info!("opening shell in realm '{}'", realm.name()); + realm.exec_shell(root_shell)?; + info!("exiting shell in realm '{}'", realm.name()); + Ok(()) + }; + + if let Some(name) = realm_name { + self.with_named_realm(name, true, run_shell) + } else { + self.with_current_realm(run_shell) + } + } + + pub fn launch_terminal(&self, name: Option<&str>) -> Result<()> { + let run_terminal = |realm: &Realm| { + info!("opening terminal in realm '{}'", realm.name()); + let title_arg = format!("Realm: {}", realm.name()); + realm.run(&["/usr/bin/gnome-terminal".to_owned(), "--title".to_owned(), title_arg], true) + }; + + if let Some(name) = name { + self.with_named_realm(name, true, run_terminal) + } else { + self.with_current_realm(run_terminal) + } + + } + + pub fn run_in_realm(&self, realm_name: Option<&str>, args: &[String], use_launcher: bool) -> Result<()> { + + if let Some(name) = realm_name { + self.with_named_realm(name, true, |realm| realm.run(args, use_launcher)) + } else { + self.with_current_realm(|realm| realm.run(args, use_launcher)) + } + } + + fn with_current_realmResult<()>>(&self, f: F) -> Result<()> { + match self.symlinks.borrow().current() { + Some(ref name) => { + self.with_named_realm(name, false, f)?; + }, + None => { + warn!("No current realm instance to run command in"); + } + } + Ok(()) + } + + fn with_named_realmResult<()>>(&self, name: &str, want_start: bool, f: F) -> Result<()> { + match self.realm(name) { + Some(realm) => { + if want_start && !realm.is_running()? { + info!("realm '{}' is not running, starting it.", realm.name()); + self.start_realm(realm)?; + } + f(realm) + }, + None => bail!("no realm with name '{}' exists", name), + } + } + + pub fn list(&self) -> Result<()> { + let mut out = ColoredOutput::new(); + self.print_realm_header(&mut out); + for realm in &self.realm_list { + self.print_realm(realm, &mut out)?; + } + Ok(()) + } + + fn print_realm_header(&self, out: &mut ColoredOutput) { + out.write(" REALMS ").bold("bold").write(": current, ").bright("colored") + .write(": running, (default) starts on boot\n").write(" ------\n\n"); + } + + fn print_realm(&self, realm: &Realm, out: &mut ColoredOutput) -> Result<()> { + let name = format!("{:12}", realm.name()); + if realm.is_current() { + out.write(" > ").bold(&name); + } else if realm.is_running()? { + out.write(" ").bright(&name); + } else { + out.write(" ").dim(&name); + } + + if realm.is_default() { + out.write(" (default)"); + } + out.write("\n"); + Ok(()) + } + + pub fn start_default(&mut self) -> Result<()> { + let default = self.symlinks.borrow().default(); + if let Some(ref realm_name) = default { + self.start_named_realm(realm_name)?; + return Ok(()); + } + bail!("No default realm to start"); + } + + pub fn start_named_realm(&mut self, realm_name: &str) -> Result<()> { + info!("starting realm '{}'", realm_name); + self.with_named_realm(realm_name, false, |realm| self.start_realm(realm)) + } + + fn start_realm(&self, realm: &Realm) -> Result<()> { + let mut symlinks = self.symlinks.borrow_mut(); + let no_current_realm = symlinks.current().is_none(); + // no realm is current, so make this realm the current one + // service file for realm will also start desktopd, so this symlink + // must be created before launching realm. + if no_current_realm { + symlinks.set_current_symlink(Some(realm.name()))?; + } + if let Err(e) = realm.start() { + if no_current_realm { + // oops realm failed to start, need to reset symlink we changed + symlinks.set_current_symlink(None)?; + } + return Err(e); + } + Ok(()) + } + + + pub fn stop_realm(&mut self, name: &str) -> Result<()> { + match self.realm_map.get(name) { + Some(realm) => { + realm.stop()?; + self.set_current_if_none()?; + }, + None => { + warn!("Cannot stop '{}'. Realm does not exist", name); + return Ok(()) + }, + }; + Ok(()) + } + + fn set_current_if_none(&self) -> Result<()> { + let mut symlinks = self.symlinks.borrow_mut(); + if symlinks.current().is_some() { + return Ok(()); + } + + if let Some(ref name) = self.find_running_realm_name()? { + symlinks.set_current_symlink(Some(name))?; + self.systemd.restart_desktopd()?; + } else { + self.systemd.stop_desktopd()?; + } + Ok(()) + } + + fn find_running_realm_name(&self) -> Result> { + for realm in self.realm_map.values() { + if realm.is_running()? { + return Ok(Some(realm.name().to_string())); + } + } + Ok(None) + } + + pub fn set_current_by_name(&self, realm_name: &str) -> Result<()> { + self.with_named_realm(realm_name, false, |realm| realm.set_current()) + } + + pub fn set_default_by_name(&self, realm_name: &str) -> Result<()> { + self.with_named_realm(realm_name, false, |realm| realm.set_default()) + } + pub fn realm_name_exists(&self, name: &str) -> bool { + self.realm_map.contains_key(name) + } + + pub fn realm(&self, name: &str) -> Option<&Realm> { + self.realm_map.get(name) + } + + pub fn new_realm(&mut self, name: &str) -> Result<&Realm> { + if !is_valid_realm_name(name) { + bail!("'{}' is not a valid realm name. Only letters, numbers and dash '-' symbol allowed in name. First character must be a letter", name); + } else if self.realm_name_exists(name) { + bail!("A realm with name '{}' already exists", name); + } + + let realm = Realm::new(name, self.symlinks.clone(), self.network.clone())?; + + match realm.create_realm_directory() { + Ok(()) => Ok(self.add_realm_entry(realm)), + Err(e) => { + fs::remove_dir_all(realm.base_path())?; + Err(e) + }, + } + + } + + pub fn remove_realm(&mut self, realm_name: &str, confirm: bool, save_home: bool) -> Result<()> { + self.with_named_realm(realm_name, false, |realm| { + if realm.base_path().join(".realmlock").exists() { + warn!("Realm '{}' has .realmlock file in base directory to protect it from deletion.", realm.name()); + warn!("Remove this file from {} before running 'realms remove {}' if you really want to delete it", realm.base_path().display(), realm.name()); + return Ok(()); + } + let mut save_home = save_home; + if confirm { + if !RealmManager::confirm_delete(realm.name(), &mut save_home)? { + return Ok(()); + } + } + realm.delete_realm(save_home)?; + self.set_current_if_none() + })?; + + self.remove_realm_entry(realm_name)?; + Ok(()) + } + + fn confirm_delete(realm_name: &str, save_home: &mut bool) -> Result { + let you_sure = RealmManager::prompt_user(&format!("Are you sure you want to remove realm '{}'?", realm_name), false)?; + if !you_sure { + info!("Ok, not removing"); + return Ok(false); + } + + println!("\nThe home directory for this realm can be saved in /realms/removed/home-{}\n", realm_name); + *save_home = RealmManager::prompt_user("Would you like to save the home directory?", true)?; + Ok(true) + } + + fn prompt_user(prompt: &str, default_y: bool) -> Result { + let yn = if default_y { "(Y/n)" } else { "(y/N)" }; + use std::io::{stdin,stdout}; + print!("{} {} : ", prompt, yn); + stdout().flush()?; + let mut line = String::new(); + stdin().read_line(&mut line)?; + + let yes = match line.trim().chars().next() { + Some(c) => c == 'Y' || c == 'y', + None => default_y, + }; + Ok(yes) + } + + pub fn base_appimg_update(&self) -> Result<()> { + info!("Entering root shell on base appimg"); + self.systemd.base_image_update_shell() + } +} diff --git a/citadel-tools/citadel-realms/src/network.rs b/citadel-tools/citadel-realms/src/network.rs new file mode 100644 index 0000000..ab8a35c --- /dev/null +++ b/citadel-tools/citadel-realms/src/network.rs @@ -0,0 +1,210 @@ +use std::path::PathBuf; +use std::net::Ipv4Addr; +use std::collections::{HashSet,HashMap}; +use std::io::{BufReader,BufRead,Write}; +use std::fs::{self,File}; + +use Result; + +const MIN_MASK: usize = 16; +const MAX_MASK: usize = 24; +const RESERVED_OCTET: u32 = 213; + +/// Manage ip address assignment for bridges +pub struct NetworkConfig { + allocators: HashMap, +} + +impl NetworkConfig { + pub fn new() -> NetworkConfig { + NetworkConfig { + allocators: HashMap::new(), + } + } + + pub fn add_bridge(&mut self, name: &str, network: &str) -> Result<()> { + let allocator = BridgeAllocator::for_bridge(name, network) + .map_err(|e| format_err!("Failed to create bridge allocator: {}", e))?; + self.allocators.insert(name.to_owned(), allocator); + Ok(()) + } + pub fn gateway(&self, bridge: &str) -> Result { + match self.allocators.get(bridge) { + Some(allocator) => Ok(allocator.gateway()), + None => bail!("Failed to return gateway address for bridge {} because it does not exist", bridge), + } + } + pub fn allocate_address_for(&mut self, bridge: &str, realm_name: &str) -> Result { + match self.allocators.get_mut(bridge) { + Some(allocator) => allocator.allocate_address_for(realm_name), + None => bail!("Failed to allocate address for bridge {} because it does not exist", bridge), + } + } + pub fn free_allocation_for(&mut self, bridge: &str, realm_name: &str) -> Result<()> { + match self.allocators.get_mut(bridge) { + Some(allocator) => allocator.free_allocation_for(realm_name), + None => bail!("Failed to free address on bridge {} because it does not exist", bridge), + } + } + + pub fn reserved(&self, bridge: &str) -> Result { + match self.allocators.get(bridge) { + Some(allocator) => Ok(allocator.reserved()), + None => bail!("Failed to return reserved address for bridge {} because it does not exist", bridge), + } + } +} + +/// +/// Allocates IP addresses for a bridge shared by multiple realms. +/// +/// State information is stored in /run/realms/network-$bridge as +/// colon ':' separated pairs of realm name and allocated ip address +/// +/// realm-a:172.17.0.2 +/// realm-b:172.17.0.3 +/// +struct BridgeAllocator { + bridge: String, + network: Ipv4Addr, + mask_size: usize, + allocated: HashSet, + allocations: HashMap, +} + +impl BridgeAllocator { + pub fn for_bridge(bridge: &str, network: &str) -> Result { + let (addr_str, mask_size) = match network.find('/') { + Some(idx) => { + let (net,bits) = network.split_at(idx); + (net.to_owned(), bits[1..].parse()?) + }, + None => (network.to_owned(), 24), + }; + if mask_size > MAX_MASK || mask_size < MIN_MASK { + bail!("Unsupported network mask size of {}", mask_size); + } + + let mask = (1u32 << (32 - mask_size)) - 1; + let ip = addr_str.parse::()?; + + if (u32::from(ip) & mask) != 0 { + bail!("network {} has masked bits with netmask /{}", addr_str, mask_size); + } + + let mut conf = BridgeAllocator::new(bridge, ip, mask_size); + conf.load_state()?; + Ok(conf) + } + + fn new(bridge: &str, network: Ipv4Addr, mask_size: usize) -> BridgeAllocator { + let mut allocator = BridgeAllocator { + bridge: bridge.to_owned(), + allocated: HashSet::new(), + allocations: HashMap::new(), + network, mask_size, + }; + let rsv = u32::from(network) | RESERVED_OCTET; + allocator.allocated.insert(Ipv4Addr::from(rsv)); + allocator + } + + fn allocate_address_for(&mut self, realm_name: &str) -> Result { + match self.find_free_address() { + Some(addr) => { + self.allocated.insert(addr.clone()); + if let Some(old) = self.allocations.insert(realm_name.to_owned(), addr.clone()) { + self.allocated.remove(&old); + } + self.write_state()?; + return Ok(format!("{}/{}", addr, self.mask_size)); + }, + None => bail!("No free IP address could be found to assign to {}", realm_name), + } + + } + + fn find_free_address(&self) -> Option { + let mask = (1u32 << (32 - self.mask_size)) - 1; + let net = u32::from(self.network); + for i in 2..mask { + let addr = Ipv4Addr::from(net + i); + if !self.allocated.contains(&addr) { + return Some(addr); + } + } + None + } + + fn gateway(&self) -> String { + let gw = u32::from(self.network) + 1; + let addr = Ipv4Addr::from(gw); + addr.to_string() + } + + fn reserved(&self) -> String { + let rsv = u32::from(self.network) | RESERVED_OCTET; + let addr = Ipv4Addr::from(rsv); + format!("{}/{}", addr, self.mask_size) + } + + fn free_allocation_for(&mut self, realm_name: &str) -> Result<()> { + match self.allocations.remove(realm_name) { + Some(ip) => { + self.allocated.remove(&ip); + self.write_state()?; + } + None => warn!("No address allocation found for realm {}", realm_name), + }; + Ok(()) + } + + fn state_file_path(&self) -> PathBuf { + PathBuf::from(format!("/run/realms/network-{}", self.bridge)) + } + + + fn load_state(&mut self) -> Result<()> { + let path = self.state_file_path(); + if !path.exists() { + return Ok(()) + } + let f = File::open(path)?; + let reader = BufReader::new(f); + for line in reader.lines() { + let line = &line?; + self.parse_state_line(line)?; + } + + Ok(()) + } + + fn parse_state_line(&mut self, line: &str) -> Result<()> { + match line.find(":") { + Some(idx) => { + let (name,addr) = line.split_at(idx); + let ip = addr[1..].parse::()?; + self.allocated.insert(ip.clone()); + self.allocations.insert(name.to_owned(), ip); + }, + None => bail!("Could not parse line from network state file: {}", line), + } + Ok(()) + } + + fn write_state(&mut self) -> Result<()> { + let path = self.state_file_path(); + let dir = path.parent().unwrap(); + if !dir.exists() { + fs::create_dir_all(dir) + .map_err(|e| format_err!("failed to create directory {} for network allocation state file: {}", dir.display(), e))?; + } + let mut f = File::create(&path) + .map_err(|e| format_err!("failed to open network state file {} for writing: {}", path.display(), e))?; + + for (realm,addr) in &self.allocations { + writeln!(f, "{}:{}", realm, addr)?; + } + Ok(()) + } +} diff --git a/citadel-tools/citadel-realms/src/realm.rs b/citadel-tools/citadel-realms/src/realm.rs new file mode 100644 index 0000000..05e6bfd --- /dev/null +++ b/citadel-tools/citadel-realms/src/realm.rs @@ -0,0 +1,374 @@ +use std::path::{PathBuf,Path}; +use std::rc::Rc; +use std::cmp::Ordering; +use std::cell::{RefCell,Cell}; +use std::fs::{self,File}; +use std::os::unix::fs::{symlink,MetadataExt}; + +use {RealmConfig,Result,Systemd,NetworkConfig}; +use util::*; +use appimg::*; + +const REALMS_BASE_PATH: &str = "/realms"; +const REALMS_RUN_PATH: &str = "/run/realms"; + +#[derive(Clone)] +pub struct Realm { + /// The realm name. Corresponds to a directory with path /realms/realm-$name/ + name: String, + + /// modify time of timestamp file which is updated when realm is set to current. + ts: Cell, + + /// Configuration options, either default values or values read from file /realms/realm-$name/config + config: RealmConfig, + + /// wrapper around various calls to systemd utilities + systemd: Systemd, + + /// reads and manages 'current' and 'default' symlinks, shared between all instances + symlinks: Rc>, +} + +impl Realm { + pub fn new(name: &str, symlinks: Rc>, network: Rc>) -> Result { + let mut realm = Realm { + name: name.to_string(), + ts: Cell::new(0), + systemd: Systemd::new(network), + config: RealmConfig::default(), symlinks, + }; + realm.load_config()?; + realm.load_timestamp()?; + Ok(realm) + } + + fn load_config(&mut self) -> Result<()> { + let path = self.base_path().join("config"); + self.config = RealmConfig::load_or_default(&path) + .map_err(|e| format_err!("failed to load realm config file {}: {}", path.display(), e))?; + Ok(()) + } + + pub fn config(&self) -> &RealmConfig { + &self.config + } + + pub fn base_path(&self) -> PathBuf { + PathBuf::from(REALMS_BASE_PATH).join(format!("realm-{}", self.name)) + } + + pub fn set_default(&self) -> Result<()> { + if self.is_default() { + info!("Realm '{}' is already default realm", self.name()); + return Ok(()) + } + self.symlinks.borrow_mut().set_default_symlink(&self.name)?; + info!("Realm '{}' set as default realm", self.name()); + Ok(()) + } + + pub fn set_current(&self) -> Result<()> { + if self.is_current() { + info!("Realm '{}' is already current realm", self.name()); + return Ok(()) + } + if !self.is_running()? { + self.start()?; + } + self.symlinks.borrow_mut().set_current_symlink(Some(&self.name))?; + self.systemd.restart_desktopd()?; + self.update_timestamp()?; + info!("Realm '{}' set as current realm", self.name()); + Ok(()) + } + + pub fn is_default(&self) -> bool { + self.symlinks.borrow().is_name_default(&self.name) + } + + pub fn is_current(&self) -> bool { + self.symlinks.borrow().is_name_current(&self.name) + } + + pub fn is_running(&self) -> Result { + self.systemd.realm_is_active(self) + } + + pub fn run(&self, args: &[String], use_launcher: bool) -> Result<()> { + self.systemd.machinectl_shell(self, args, use_launcher)?; + Ok(()) + } + + pub fn exec_shell(&self, as_root: bool) -> Result<()> { + self.systemd.machinectl_exec_shell(self, as_root) + } + + pub fn start(&self) -> Result<()> { + self.systemd.start_realm(self)?; + info!("Started realm '{}'", self.name()); + Ok(()) + } + + pub fn stop(&self) -> Result<()> { + self.systemd.stop_realm(self)?; + if self.is_current() { + self.symlinks.borrow_mut().set_current_symlink(None)?; + } + info!("Stopped realm '{}'", self.name()); + Ok(()) + } + + pub fn name(&self) -> &str { + &self.name + } + + fn load_timestamp(&self) -> Result<()> { + let tstamp = self.base_path().join(".tstamp"); + if tstamp.exists() { + let meta = tstamp.metadata()?; + self.ts.set(meta.mtime()); + } + Ok(()) + } + + /// create an empty file which is used to track the time at which + /// this realm was last made 'current'. These times are used + /// to order the output when listing realms. + fn update_timestamp(&self) -> Result<()> { + let tstamp = self.base_path().join(".tstamp"); + if tstamp.exists() { + fs::remove_file(&tstamp)?; + } + File::create(&tstamp) + .map_err(|e| format_err!("failed to create timestamp file {}: {}", tstamp.display(), e))?; + // also load the new value + self.load_timestamp()?; + Ok(()) + } + + pub fn create_realm_directory(&self) -> Result<()> { + if self.base_path().exists() { + bail!("realm base directory {} already exists, cannot create", self.base_path().display()); + } + + fs::create_dir(self.base_path()) + .map_err(|e| format_err!("failed to create realm base directory {}: {}", self.base_path().display(), e))?; + + self.create_home_directory() + .map_err(|e| format_err!("failed to create realm home directory {}: {}", self.base_path().join("home").display(), e))?; + + // This must be last step because if an error is returned caller assumes that subvolume was + // never created and does not need to be removed. + clone_base_appimg(self)?; + Ok(()) + } + + fn create_home_directory(&self) -> Result<()> { + let home = self.base_path().join("home"); + mkdir_chown(&home, 1000, 1000)?; + let skel = PathBuf::from(REALMS_BASE_PATH).join("skel"); + if skel.exists() { + info!("Populating realm home directory with files from {}", skel.display()); + copy_tree(&skel, &home)?; + } + Ok(()) + } + + pub fn delete_realm(&self, save_home: bool) -> Result<()> { + if save_home { + self.save_home_for_delete()?; + } + if self.is_running()? { + self.stop()?; + } + info!("removing rootfs subvolume for '{}'", self.name()); + delete_rootfs_subvolume(self)?; + + info!("removing realm directory {}", self.base_path().display()); + fs::remove_dir_all(self.base_path())?; + + info!("realm '{}' has been removed", self.name()); + Ok(()) + } + + fn save_home_for_delete(&self) -> Result<()> { + let target = PathBuf::from(&format!("/realms/removed/home-{}", self.name())); + if !Path::new("/realms/removed").exists() { + fs::create_dir("/realms/removed")?; + } + + fs::rename(self.base_path().join("home"), &target) + .map_err(|e| format_err!("unable to move realm home directory to {}: {}", target.display(), e))?; + info!("home directory been moved to /realms/removed/home-{}, delete it at your leisure", self.name()); + Ok(()) + } + +} + +impl Ord for Realm { + fn cmp(&self, other: &Realm) -> Ordering { + let self_run = self.is_running().unwrap_or(false); + let other_run = other.is_running().unwrap_or(false); + + if self_run && !other_run { + Ordering::Less + } else if !self_run && other_run { + Ordering::Greater + } else { + let self_ts = self.ts.get(); + let other_ts = other.ts.get(); + other_ts.cmp(&self_ts) + } + } +} + +impl PartialOrd for Realm { + fn partial_cmp(&self, other: &Realm) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for Realm { + fn eq(&self, other: &Realm) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +impl Eq for Realm {} + +pub struct RealmSymlinks { + current_name: Option, + default_name: Option, +} + +impl RealmSymlinks { + pub fn new() -> RealmSymlinks { + RealmSymlinks { + current_name: None, + default_name: None, + } + } + + pub fn load_symlinks(&mut self) -> Result<()> { + self.current_name = self.resolve_realm_name(&PathBuf::from(REALMS_RUN_PATH).join("current.realm"))?; + self.default_name = self.resolve_realm_name(&PathBuf::from(REALMS_BASE_PATH).join("default.realm"))?; + Ok(()) + } + + fn is_name_default(&self, name: &str) -> bool { + match self.default() { + Some(dname) => dname == name, + None => false, + } + } + + fn is_name_current(&self, name: &str) -> bool { + match self.current() { + Some(cname) => cname == name, + None => false, + } + } + + pub fn current(&self) -> Option { + self.current_name.clone() + } + + pub fn default(&self) -> Option { + self.default_name.clone() + } + + + pub fn set_current_symlink(&mut self, name: Option<&str>) -> Result<()> { + let runpath = Path::new(REALMS_RUN_PATH); + if !runpath.exists() { + fs::create_dir_all(runpath) + .map_err(|e| format_err!("failed to create realm runtime directory {}: {}", runpath.display(), e))?; + } + + let path = runpath.join("current.realm"); + if let Some(n) = name { + let tmp = Path::new("/run/current.realm.tmp"); + let target = PathBuf::from(REALMS_BASE_PATH).join(format!("realm-{}", n)); + symlink(&target, tmp) + .map_err(|e| format_err!("failed to create symlink from {} to {}: {}", tmp.display(), target.display(), e))?; + + fs::rename(tmp, &path) + .map_err(|e| format_err!("failed to rename symlink from {} to {}: {}", tmp.display(), path.display(), e))?; + + self.current_name = Some(n.to_owned()); + } else { + if path.exists() { + fs::remove_file(&path) + .map_err(|e| format_err!("failed to remove current symlink {}: {}", path.display(), e))?; + } + self.current_name = None; + } + Ok(()) + } + + pub fn set_default_symlink(&mut self, name: &str) -> Result<()> { + let path = PathBuf::from(REALMS_BASE_PATH).join("default.realm"); + let tmp = Path::new("/realms/default.realm.tmp"); + let target = format!("realm-{}", name); + symlink(&target, tmp) + .map_err(|e| format_err!("failed to create symlink from {} to {}: {}", tmp.display(), target, e))?; + fs::rename(tmp, &path) + .map_err(|e| format_err!("failed to rename symlink from {} to {}: {}", tmp.display(), path.display(), e))?; + + self.default_name = Some(name.to_owned()); + Ok(()) + } + + fn resolve_realm_name(&self, path: &Path) -> Result> { + if !path.exists() { + return Ok(None); + } + let meta = path.symlink_metadata()?; + if !meta.file_type().is_symlink() { + bail!("{} exists but it is not a symlink", path.display()); + } + let target = RealmSymlinks::absolute_target(path)?; + RealmSymlinks::ensure_subdir_of_base(path, &target)?; + if !target.is_dir() { + bail!("target of symlink {} is not a directory", path.display()); + } + let filename = path_filename(&target); + if !filename.starts_with("realm-") { + bail!("target of symlink {} is not a realm directory", path.display()); + } + Ok(Some(filename[6..].to_string())) + } + + /// Read target of symlink `path` and resolve it to an absolute + /// path + fn absolute_target(path: &Path) -> Result { + let target = fs::read_link(path)?; + if target.is_absolute() { + Ok(target) + } else if target.components().count() == 1 { + match path.parent() { + Some(parent) => return Ok(parent.join(target)), + None => bail!("Cannot resolve absolute path of symlink target because symlink path has no parent"), + } + } else { + bail!("symlink target has invalid value: {}", target.display()) + } + } + + fn ensure_subdir_of_base(path: &Path, target: &Path) -> Result<()> { + let realms_base = PathBuf::from(REALMS_BASE_PATH); + match target.parent() { + Some(parent) => { + if parent != realms_base.as_path() { + bail!("target of symlink {} points outside of {} directory: {}", path.display(), REALMS_BASE_PATH, target.display()); + } + }, + None => bail!("target of symlink {} has invalid value (no parent): {}", path.display(), target.display()), + } + Ok(()) + } + +} + + diff --git a/citadel-tools/citadel-realms/src/systemd.rs b/citadel-tools/citadel-realms/src/systemd.rs new file mode 100644 index 0000000..79ae428 --- /dev/null +++ b/citadel-tools/citadel-realms/src/systemd.rs @@ -0,0 +1,383 @@ +use std::rc::Rc; +use std::cell::RefCell; +use std::process::Command; +use std::path::{Path,PathBuf}; +use std::fs::{self,File}; +use std::fmt::Write; +use std::io::Write as IoWrite; +use std::env; + +const SYSTEMCTL_PATH: &str = "/usr/bin/systemctl"; +const MACHINECTL_PATH: &str = "/usr/bin/machinectl"; +const SYSTEMD_NSPAWN_PATH: &str = "/run/systemd/nspawn"; +const SYSTEMD_UNIT_PATH: &str = "/run/systemd/system"; + +const DESKTOPD_SERVICE: &str = "citadel-desktopd.service"; + +use Realm; +use NetworkConfig; +use Result; +use util::{path_filename,is_first_char_alphabetic}; + +#[derive(Clone)] +pub struct Systemd { + network: Rc>, +} + +impl Systemd { + + pub fn new(network: Rc>) -> Systemd { + Systemd { network } + } + + pub fn realm_is_active(&self, realm: &Realm) -> Result { + let active = self.is_active(&self.realm_service_name(realm))?; + let has_config = self.realm_config_exists(realm); + if active && !has_config { + bail!("Realm {} is running, but config files are missing", realm.name()); + } + if !active && has_config { + bail!("Realm {} is not running, but config files are present", realm.name()); + } + Ok(active) + } + + pub fn start_realm(&self, realm: &Realm) -> Result<()> { + if self.realm_is_active(realm)? { + warn!("Realm {} is already running", realm.name()); + return Ok(()) + } + self.write_realm_launch_config(realm)?; + self.systemctl_start(&self.realm_service_name(realm))?; + if realm.config().emphemeral_home() { + self.setup_ephemeral_home(realm)?; + } + + Ok(()) + + } + + pub fn base_image_update_shell(&self) -> Result<()> { + let netconf = self.network.borrow_mut(); + let gw = netconf.gateway("clear")?; + let addr = netconf.reserved("clear")?; + let gw_env = format!("--setenv=IFCONFIG_GW={}", gw); + let addr_env = format!("--setenv=IFCONFIG_IP={}", addr); + + Command::new("/usr/bin/systemd-nspawn") + .args(&[ + &addr_env, &gw_env, + "--quiet", + "--machine=base-appimg-update", + "--directory=/storage/appimg/base.appimg", + "--network-zone=clear", + "/bin/bash", "-c", "/usr/libexec/configure-host0.sh && exec /bin/bash" + ]).status()?; + Ok(()) + } + + fn setup_ephemeral_home(&self, realm: &Realm) -> Result<()> { + + // 1) if exists: machinectl copy-to /realms/skel /home/user + if Path::new("/realms/skel").exists() { + self.machinectl_copy_to(realm, "/realms/skel", "/home/user")?; + } + + // 2) if exists: machinectl copy-to /realms/realm-$name /home/user + let realm_skel = realm.base_path().join("skel"); + if realm_skel.exists() { + self.machinectl_copy_to(realm, realm_skel.to_str().unwrap(), "/home/user")?; + } + + let home = realm.base_path().join("home"); + if !home.exists() { + return Ok(()); + } + + // 3) for each : machinectl bind /realms/realm-$name/home/$dir /home/user/$dir + for dent in fs::read_dir(home)? { + let path = dent?.path(); + self.bind_mount_home_subdir(realm, &path)?; + } + + Ok(()) + } + + fn bind_mount_home_subdir(&self, realm: &Realm, path: &Path) -> Result<()> { + let path = path.canonicalize()?; + if !path.is_dir() { + return Ok(()); + } + let fname = path_filename(&path); + if !is_first_char_alphabetic(fname) { + return Ok(()); + } + let from = format!("/realms/realm-{}/home/{}", realm.name(), fname); + let to = format!("/home/user/{}", fname); + self.machinectl_bind(realm, &from, &to)?; + Ok(()) + } + + pub fn stop_realm(&self, realm: &Realm) -> Result<()> { + if !self.realm_is_active(realm)? { + warn!("Realm {} is not running", realm.name()); + return Ok(()); + } + self.systemctl_stop(&self.realm_service_name(realm))?; + self.remove_realm_launch_config(realm)?; + self.network.borrow_mut().free_allocation_for(realm.config().network_zone(), realm.name())?; + Ok(()) + } + + pub fn restart_desktopd(&self) -> Result { + self.systemctl_restart(DESKTOPD_SERVICE) + } + pub fn stop_desktopd(&self) -> Result { + self.systemctl_stop(DESKTOPD_SERVICE) + } + + fn realm_service_name(&self, realm: &Realm) -> String { + format!("realm-{}.service", realm.name()) + } + + fn is_active(&self, name: &str) -> Result { + Command::new(SYSTEMCTL_PATH) + .args(&["--quiet", "is-active", name]) + .status() + .map(|status| status.success()) + .map_err(|e| format_err!("failed to execute{}: {}", MACHINECTL_PATH, e)) + + } + + + fn systemctl_restart(&self, name: &str) -> Result { + self.run_systemctl("restart", name) + } + + fn systemctl_start(&self, name: &str) -> Result { + self.run_systemctl("start", name) + } + + fn systemctl_stop(&self, name: &str) -> Result { + self.run_systemctl("stop", name) + } + + fn run_systemctl(&self, op: &str, name: &str) -> Result { + Command::new(SYSTEMCTL_PATH) + .arg(op) + .arg(name) + .status() + .map(|status| status.success()) + .map_err(|e| format_err!("failed to execute {}: {}", MACHINECTL_PATH, e)) + } + + fn machinectl_copy_to(&self, realm: &Realm, from: &str, to: &str) -> Result<()> { + Command::new(MACHINECTL_PATH) + .args(&["copy-to", realm.name(), from, to ]) + .status() + .map_err(|e| format_err!("failed to machinectl copy-to {} {} {}: {}", realm.name(), from, to, e))?; + Ok(()) + } + + fn machinectl_bind(&self, realm: &Realm, from: &str, to: &str) -> Result<()> { + Command::new(MACHINECTL_PATH) + .args(&["--mkdir", "bind", realm.name(), from, to ]) + .status() + .map_err(|e| format_err!("failed to machinectl bind {} {} {}: {}", realm.name(), from, to, e))?; + Ok(()) + + } + + pub fn machinectl_exec_shell(&self, realm: &Realm, as_root: bool) -> Result<()> { + let namevar = format!("--setenv=REALM_NAME={}", realm.name()); + let user = if as_root { "root" } else { "user" }; + let user_at_host = format!("{}@{}", user, realm.name()); + Command::new(MACHINECTL_PATH) + .args(&[ &namevar, "--quiet", "shell", &user_at_host, "/bin/bash"]) + .status() + .map_err(|e| format_err!("failed to execute{}: {}", MACHINECTL_PATH, e))?; + + Ok(()) + } + + pub fn machinectl_shell(&self, realm: &Realm, args: &[String], launcher: bool) -> Result<()> { + let namevar = format!("--setenv=REALM_NAME={}", realm.name()); + let mut cmd = Command::new(MACHINECTL_PATH); + cmd.arg("--quiet"); + match env::var("DESKTOP_STARTUP_ID") { + Ok(val) => { + cmd.arg("-E"); + cmd.arg(&format!("DESKTOP_STARTUP_ID={}", val)); + }, + Err(_) => {}, + }; + cmd.arg(&namevar); + cmd.arg("shell"); + cmd.arg(format!("user@{}", realm.name())); + + if launcher { + cmd.arg("/usr/libexec/launch"); + } + + for arg in args { + cmd.arg(&arg); + } + cmd.status().map_err(|e| format_err!("failed to execute{}: {}", MACHINECTL_PATH, e))?; + Ok(()) + } + + + fn realm_service_path(&self, realm: &Realm) -> PathBuf { + PathBuf::from(SYSTEMD_UNIT_PATH).join(self.realm_service_name(realm)) + } + + fn realm_nspawn_path(&self, realm: &Realm) -> PathBuf { + PathBuf::from(SYSTEMD_NSPAWN_PATH).join(format!("{}.nspawn", realm.name())) + } + + fn realm_config_exists(&self, realm: &Realm) -> bool { + self.realm_service_path(realm).exists() || self.realm_nspawn_path(realm).exists() + } + + fn remove_realm_launch_config(&self, realm: &Realm) -> Result<()> { + let nspawn_path = self.realm_nspawn_path(realm); + if nspawn_path.exists() { + fs::remove_file(&nspawn_path)?; + } + let service_path = self.realm_service_path(realm); + if service_path.exists() { + fs::remove_file(&service_path)?; + } + Ok(()) + } + + fn write_realm_launch_config(&self, realm: &Realm) -> Result<()> { + let nspawn_path = self.realm_nspawn_path(realm); + let nspawn_content = self.generate_nspawn_file(realm)?; + self.write_launch_config_file(&nspawn_path, &nspawn_content) + .map_err(|e| format_err!("failed to write nspawn config file {}: {}", nspawn_path.display(), e))?; + + let service_path = self.realm_service_path(realm); + let service_content = self.generate_service_file(realm); + self.write_launch_config_file(&service_path, &service_content) + .map_err(|e| format_err!("failed to write service config file {}: {}", service_path.display(), e))?; + + Ok(()) + } + + /// Write the string `content` to file `path`. If the directory does + /// not already exist, create it. + fn write_launch_config_file(&self, path: &Path, content: &str) -> Result<()> { + match path.parent() { + Some(parent) => { + if !parent.exists() { + fs::create_dir_all(parent)?; + } + }, + None => bail!("config file path {} has no parent?", path.display()), + }; + let mut f = File::create(path)?; + f.write_all(content.as_bytes())?; + Ok(()) + } + + fn generate_nspawn_file(&self, realm: &Realm) -> Result { + Ok(NSPAWN_FILE_TEMPLATE + .replace("$EXTRA_BIND_MOUNTS", &self.generate_extra_bind_mounts(realm)?) + + .replace("$NETWORK_CONFIG", &self.generate_network_config(realm)?)) + } + + fn generate_extra_bind_mounts(&self, realm: &Realm) -> Result { + let config = realm.config(); + let mut s = String::new(); + + if config.emphemeral_home() { + writeln!(s, "TemporaryFileSystem=/home/user:mode=755,uid=1000,gid=1000")?; + } else { + writeln!(s, "Bind={}/home:/home/user", realm.base_path().display())?; + } + + if config.shared_dir() && Path::new("/realms/Shared").exists() { + writeln!(s, "Bind=/realms/Shared:/home/user/Shared")?; + } + + if config.kvm() { + writeln!(s, "Bind=/dev/kvm")?; + } + + if config.gpu() { + writeln!(s, "Bind=/dev/dri/renderD128")?; + } + + if config.sound() { + writeln!(s, "Bind=/dev/snd")?; + writeln!(s, "Bind=/dev/shm")?; + writeln!(s, "BindReadOnly=/run/user/1000/pulse:/run/user/host/pulse")?; + } + + if config.x11() { + writeln!(s, "BindReadOnly=/tmp/.X11-unix")?; + } + + if config.wayland() { + writeln!(s, "BindReadOnly=/run/user/1000/wayland-0:/run/user/host/wayland-0")?; + } + + Ok(s) + } + + fn generate_network_config(&self, realm: &Realm) -> Result { + let mut s = String::new(); + if realm.config().network() { + let mut netconf = self.network.borrow_mut(); + let zone = realm.config().network_zone(); + let addr = netconf.allocate_address_for(zone, realm.name())?; + let gw = netconf.gateway(zone)?; + writeln!(s, "Environment=IFCONFIG_IP={}", addr)?; + writeln!(s, "Environment=IFCONFIG_GW={}", gw)?; + writeln!(s, "[Network]")?; + writeln!(s, "Zone=clear")?; + } else { + writeln!(s, "[Network]")?; + writeln!(s, "Private=true")?; + } + Ok(s) + } + + fn generate_service_file(&self, realm: &Realm) -> String { + let rootfs = format!("/realms/realm-{}/rootfs", realm.name()); + REALM_SERVICE_TEMPLATE.replace("$REALM_NAME", realm.name()).replace("$ROOTFS", &rootfs) + } +} + + +pub const NSPAWN_FILE_TEMPLATE: &str = r###" +[Exec] +Boot=true +$NETWORK_CONFIG + +[Files] +BindReadOnly=/usr/share/themes +BindReadOnly=/usr/share/icons/Paper + +BindReadOnly=/storage/citadel-state/resolv.conf:/etc/resolv.conf + +$EXTRA_BIND_MOUNTS + +"###; + +pub const REALM_SERVICE_TEMPLATE: &str = r###" +[Unit] +Description=Application Image $REALM_NAME instance +Wants=citadel-desktopd.service + +[Service] +Environment=SYSTEMD_NSPAWN_SHARE_NS_IPC=1 +ExecStart=/usr/bin/systemd-nspawn --quiet --notify-ready=yes --keep-unit --machine=$REALM_NAME --link-journal=try-guest --directory=$ROOTFS + +KillMode=mixed +Type=notify +RestartForceExitStatus=133 +SuccessExitStatus=133 +"###; diff --git a/citadel-tools/citadel-realms/src/util.rs b/citadel-tools/citadel-realms/src/util.rs new file mode 100644 index 0000000..ede3671 --- /dev/null +++ b/citadel-tools/citadel-realms/src/util.rs @@ -0,0 +1,126 @@ +use std::path::Path; +use std::fs; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::MetadataExt; +use std::ffi::CString; +use std::io::{self,Write}; + +use libc; +use walkdir::WalkDir; + +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; + } + } + "" +} + +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 +} + +pub fn is_first_char_alphabetic(s: &str) -> bool { + if let Some(c) = s.chars().next() { + return is_ascii(c) && c.is_alphabetic() + } + false +} + +const MAX_REALM_NAME_LEN:usize = 128; + +/// Valid realm names: +/// * must start with an alphabetic ascii letter character +/// * may only contain ascii characters which are letters, numbers, or the dash '-' symbol +/// * must not be empty or have a length exceeding 128 characters +pub fn is_valid_realm_name(name: &str) -> bool { + name.len() <= MAX_REALM_NAME_LEN && + // Also false on empty string + is_first_char_alphabetic(name) && + name.chars().all(is_alphanum_or_dash) +} + +pub fn mkdir_chown(path: &Path, uid: u32, gid: u32) -> io::Result<()> { + if !path.exists() { + fs::create_dir(path)?; + } + let cstr = CString::new(path.as_os_str().as_bytes())?; + unsafe { + if libc::chown(cstr.as_ptr(), uid, gid) == -1 { + return Err(io::Error::last_os_error()); + } + } + Ok(()) +} + +pub fn copy_tree(from_base: &Path, to_base: &Path) -> Result<()> { + for entry in WalkDir::new(from_base) { + let path = entry?.path().to_owned(); + let to = to_base.join(path.strip_prefix(from_base)?); + if path.is_dir() { + let meta = path.metadata()?; + mkdir_chown(&to, meta.uid(), meta.gid())?; + } else { + fs::copy(&path, &to) + .map_err(|e| format_err!("failed to copy {} to {}: {}", path.display(), to.display(), e))?; + } + } + Ok(()) +} + +use termcolor::{ColorChoice,Color,ColorSpec,WriteColor,StandardStream}; + +pub struct ColoredOutput { + color_bright: ColorSpec, + color_bold: ColorSpec, + color_dim: ColorSpec, + stream: StandardStream, +} + + +impl ColoredOutput { + pub fn new() -> ColoredOutput { + ColoredOutput::new_with_colors(Color::Rgb(0, 110, 180), Color::Rgb(100, 100, 80)) + } + + pub fn new_with_colors(bright: Color, dim: Color) -> ColoredOutput { + let mut out = ColoredOutput { + color_bright: ColorSpec::new(), + color_bold: ColorSpec::new(), + color_dim: ColorSpec::new(), + stream: StandardStream::stdout(ColorChoice::AlwaysAnsi), + }; + out.color_bright.set_fg(Some(bright.clone())); + out.color_bold.set_fg(Some(bright)).set_bold(true); + out.color_dim.set_fg(Some(dim)); + + out + } + + pub fn write(&mut self, s: &str) -> &mut Self { + write!(&mut self.stream, "{}", s).unwrap(); + self.stream.reset().unwrap(); + self + } + pub fn bright(&mut self, s: &str) -> &mut Self { + self.stream.set_color(&self.color_bright).unwrap(); + self.write(s) + } + pub fn bold(&mut self, s: &str) -> &mut Self { + self.stream.set_color(&self.color_bold).unwrap(); + self.write(s) + } + pub fn dim(&mut self, s: &str) -> &mut Self { + self.stream.set_color(&self.color_dim).unwrap(); + self.write(s) + } + +}