introducing, citadel realms
This commit is contained in:
parent
69b9e8384e
commit
58047557a4
350
citadel-tools/citadel-realms/Cargo.lock
generated
Normal file
350
citadel-tools/citadel-realms/Cargo.lock
generated
Normal file
@ -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"
|
15
citadel-tools/citadel-realms/Cargo.toml
Normal file
15
citadel-tools/citadel-realms/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "citadel-realms"
|
||||
version = "0.1.0"
|
||||
authors = ["Bruce Leidl <bruce@subgraph.com>"]
|
||||
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"
|
84
citadel-tools/citadel-realms/README.md
Normal file
84
citadel-tools/citadel-realms/README.md
Normal file
@ -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
|
||||
|
43
citadel-tools/citadel-realms/src/appimg.rs
Normal file
43
citadel-tools/citadel-realms/src/appimg.rs
Normal file
@ -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(())
|
||||
}
|
||||
|
112
citadel-tools/citadel-realms/src/config.rs
Normal file
112
citadel-tools/citadel-realms/src/config.rs
Normal file
@ -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<RealmConfig> {
|
||||
if path.exists() {
|
||||
let s = load_as_string(&path)?;
|
||||
let config = toml::from_str::<RealmConfig>(&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<String> {
|
||||
let mut f = File::open(path)?;
|
||||
let mut buffer = String::new();
|
||||
f.read_to_string(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
}
|
304
citadel-tools/citadel-realms/src/main.rs
Normal file
304
citadel-tools/citadel-realms/src/main.rs
Normal file
@ -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<T> = result::Result<T,Error>;
|
||||
|
||||
thread_local! {
|
||||
pub static VERBOSE: RefCell<bool> = 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 <command>' 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()
|
||||
}
|
399
citadel-tools/citadel-realms/src/manager.rs
Normal file
399
citadel-tools/citadel-realms/src/manager.rs
Normal file
@ -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<String, Realm>,
|
||||
|
||||
/// Sorted for 'list'
|
||||
realm_list: Vec<Realm>,
|
||||
|
||||
/// track status of 'current' and 'default' symlinks
|
||||
symlinks: Rc<RefCell<RealmSymlinks>>,
|
||||
|
||||
/// finds free ip addresses to use
|
||||
network: Rc<RefCell<NetworkConfig>>,
|
||||
|
||||
/// interface to systemd
|
||||
systemd: Systemd,
|
||||
}
|
||||
|
||||
|
||||
impl RealmManager {
|
||||
fn new() -> Result<RealmManager> {
|
||||
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<Rc<RefCell<NetworkConfig>>> {
|
||||
let mut network = NetworkConfig::new();
|
||||
network.add_bridge("clear", "172.17.0.0/24")?;
|
||||
Ok(Rc::new(RefCell::new(network)))
|
||||
}
|
||||
|
||||
pub fn load() -> Result<RealmManager> {
|
||||
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<String> {
|
||||
self.symlinks.borrow().current()
|
||||
}
|
||||
|
||||
pub fn default_realm_name(&self) -> Option<String> {
|
||||
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_realm<F: Fn(&Realm)->Result<()>>(&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_realm<F: Fn(&Realm)->Result<()>>(&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<Option<String>> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
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()
|
||||
}
|
||||
}
|
210
citadel-tools/citadel-realms/src/network.rs
Normal file
210
citadel-tools/citadel-realms/src/network.rs
Normal file
@ -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<String, BridgeAllocator>,
|
||||
}
|
||||
|
||||
impl NetworkConfig {
|
||||
pub fn new() -> NetworkConfig {
|
||||
NetworkConfig {
|
||||
allocators: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_bridge(&mut self, name: &str, network: &str) -> Result<()> {
|
||||
let allocator = BridgeAllocator::for_bridge(name, network)
|
||||
.map_err(|e| format_err!("Failed to create bridge allocator: {}", e))?;
|
||||
self.allocators.insert(name.to_owned(), allocator);
|
||||
Ok(())
|
||||
}
|
||||
pub fn gateway(&self, bridge: &str) -> Result<String> {
|
||||
match self.allocators.get(bridge) {
|
||||
Some(allocator) => Ok(allocator.gateway()),
|
||||
None => bail!("Failed to return gateway address for bridge {} because it does not exist", bridge),
|
||||
}
|
||||
}
|
||||
pub fn allocate_address_for(&mut self, bridge: &str, realm_name: &str) -> Result<String> {
|
||||
match self.allocators.get_mut(bridge) {
|
||||
Some(allocator) => allocator.allocate_address_for(realm_name),
|
||||
None => bail!("Failed to allocate address for bridge {} because it does not exist", bridge),
|
||||
}
|
||||
}
|
||||
pub fn free_allocation_for(&mut self, bridge: &str, realm_name: &str) -> Result<()> {
|
||||
match self.allocators.get_mut(bridge) {
|
||||
Some(allocator) => allocator.free_allocation_for(realm_name),
|
||||
None => bail!("Failed to free address on bridge {} because it does not exist", bridge),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reserved(&self, bridge: &str) -> Result<String> {
|
||||
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<Ipv4Addr>,
|
||||
allocations: HashMap<String, Ipv4Addr>,
|
||||
}
|
||||
|
||||
impl BridgeAllocator {
|
||||
pub fn for_bridge(bridge: &str, network: &str) -> Result<BridgeAllocator> {
|
||||
let (addr_str, mask_size) = match network.find('/') {
|
||||
Some(idx) => {
|
||||
let (net,bits) = network.split_at(idx);
|
||||
(net.to_owned(), bits[1..].parse()?)
|
||||
},
|
||||
None => (network.to_owned(), 24),
|
||||
};
|
||||
if mask_size > MAX_MASK || mask_size < MIN_MASK {
|
||||
bail!("Unsupported network mask size of {}", mask_size);
|
||||
}
|
||||
|
||||
let mask = (1u32 << (32 - mask_size)) - 1;
|
||||
let ip = addr_str.parse::<Ipv4Addr>()?;
|
||||
|
||||
if (u32::from(ip) & mask) != 0 {
|
||||
bail!("network {} has masked bits with netmask /{}", addr_str, mask_size);
|
||||
}
|
||||
|
||||
let mut conf = BridgeAllocator::new(bridge, ip, mask_size);
|
||||
conf.load_state()?;
|
||||
Ok(conf)
|
||||
}
|
||||
|
||||
fn new(bridge: &str, network: Ipv4Addr, mask_size: usize) -> BridgeAllocator {
|
||||
let 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<String> {
|
||||
match self.find_free_address() {
|
||||
Some(addr) => {
|
||||
self.allocated.insert(addr.clone());
|
||||
if let Some(old) = self.allocations.insert(realm_name.to_owned(), addr.clone()) {
|
||||
self.allocated.remove(&old);
|
||||
}
|
||||
self.write_state()?;
|
||||
return Ok(format!("{}/{}", addr, self.mask_size));
|
||||
},
|
||||
None => bail!("No free IP address could be found to assign to {}", realm_name),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn find_free_address(&self) -> Option<Ipv4Addr> {
|
||||
let mask = (1u32 << (32 - self.mask_size)) - 1;
|
||||
let net = u32::from(self.network);
|
||||
for i in 2..mask {
|
||||
let addr = Ipv4Addr::from(net + i);
|
||||
if !self.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::<Ipv4Addr>()?;
|
||||
self.allocated.insert(ip.clone());
|
||||
self.allocations.insert(name.to_owned(), ip);
|
||||
},
|
||||
None => bail!("Could not parse line from network state file: {}", line),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_state(&mut self) -> Result<()> {
|
||||
let path = self.state_file_path();
|
||||
let dir = path.parent().unwrap();
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(dir)
|
||||
.map_err(|e| format_err!("failed to create directory {} for network allocation state file: {}", dir.display(), e))?;
|
||||
}
|
||||
let mut f = File::create(&path)
|
||||
.map_err(|e| format_err!("failed to open network state file {} for writing: {}", path.display(), e))?;
|
||||
|
||||
for (realm,addr) in &self.allocations {
|
||||
writeln!(f, "{}:{}", realm, addr)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
374
citadel-tools/citadel-realms/src/realm.rs
Normal file
374
citadel-tools/citadel-realms/src/realm.rs
Normal file
@ -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<i64>,
|
||||
|
||||
/// 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<RefCell<RealmSymlinks>>,
|
||||
}
|
||||
|
||||
impl Realm {
|
||||
pub fn new(name: &str, symlinks: Rc<RefCell<RealmSymlinks>>, network: Rc<RefCell<NetworkConfig>>) -> Result<Realm> {
|
||||
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<bool> {
|
||||
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<Ordering> {
|
||||
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<String>,
|
||||
default_name: Option<String>,
|
||||
}
|
||||
|
||||
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<String> {
|
||||
self.current_name.clone()
|
||||
}
|
||||
|
||||
pub fn default(&self) -> Option<String> {
|
||||
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<Option<String>> {
|
||||
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<PathBuf> {
|
||||
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(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
383
citadel-tools/citadel-realms/src/systemd.rs
Normal file
383
citadel-tools/citadel-realms/src/systemd.rs
Normal file
@ -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<RefCell<NetworkConfig>>,
|
||||
}
|
||||
|
||||
impl Systemd {
|
||||
|
||||
pub fn new(network: Rc<RefCell<NetworkConfig>>) -> Systemd {
|
||||
Systemd { network }
|
||||
}
|
||||
|
||||
pub fn realm_is_active(&self, realm: &Realm) -> Result<bool> {
|
||||
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<bool> {
|
||||
self.systemctl_restart(DESKTOPD_SERVICE)
|
||||
}
|
||||
pub fn stop_desktopd(&self) -> Result<bool> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
self.run_systemctl("restart", name)
|
||||
}
|
||||
|
||||
fn systemctl_start(&self, name: &str) -> Result<bool> {
|
||||
self.run_systemctl("start", name)
|
||||
}
|
||||
|
||||
fn systemctl_stop(&self, name: &str) -> Result<bool> {
|
||||
self.run_systemctl("stop", name)
|
||||
}
|
||||
|
||||
fn run_systemctl(&self, op: &str, name: &str) -> Result<bool> {
|
||||
Command::new(SYSTEMCTL_PATH)
|
||||
.arg(op)
|
||||
.arg(name)
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.map_err(|e| format_err!("failed to execute {}: {}", MACHINECTL_PATH, e))
|
||||
}
|
||||
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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
|
||||
"###;
|
126
citadel-tools/citadel-realms/src/util.rs
Normal file
126
citadel-tools/citadel-realms/src/util.rs
Normal file
@ -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)
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user