forked from brl/citadel-tools
Compare commits
5 Commits
error_hand
...
update_too
Author | SHA1 | Date | |
---|---|---|---|
0ff03cb273 | |||
ae41a71b60 | |||
24f786cf75 | |||
2dc8bf2922 | |||
2a16bd4c41 |
5
.cargo/config.toml
Normal file
5
.cargo/config.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[env]
|
||||
# overide this with your name to send to a different path on the server
|
||||
UPDATES_CLIENT = "public"
|
||||
UPDATES_CHANNEL = "prod"
|
||||
|
2426
Cargo.lock
generated
2426
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui" ]
|
||||
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui", "launch-gnome-software", "update-generator" ]
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
750
citadel-installer-ui/Cargo.lock
generated
750
citadel-installer-ui/Cargo.lock
generated
@ -1,750 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b"
|
||||
|
||||
[[package]]
|
||||
name = "atk"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/gtk-rs/atk#9e3eb26374a4156297280769bd64102a4bebcad7"
|
||||
dependencies = [
|
||||
"atk-sys",
|
||||
"bitflags",
|
||||
"glib",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atk-sys"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/gtk-rs/sys#56e03c021c393e1cf3f148005b348ac5a8a0ab72"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/gtk-rs/cairo#2d5a1cb0003176224004c4e826929520bd1227f9"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cairo-sys-rs",
|
||||
"glib",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-sys-rs"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/gtk-rs/cairo#2d5a1cb0003176224004c4e826929520bd1227f9"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||
|
||||
[[package]]
|
||||
name = "citadel-installer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dbus",
|
||||
"failure",
|
||||
"gdk",
|
||||
"gdk-pixbuf",
|
||||
"gio",
|
||||
"glib",
|
||||
"glib-sys",
|
||||
"gtk",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cd9e78c210146a1860f897db03412fd5091fd73100778e43ee255cca252cf32"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libdbus-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f"
|
||||
|
||||
[[package]]
|
||||
name = "failure"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"failure_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "failure_derive"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39"
|
||||
dependencies = [
|
||||
"proc-macro-hack",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project",
|
||||
"pin-utils",
|
||||
"proc-macro-hack",
|
||||
"proc-macro-nested",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk"
|
||||
version = "0.13.0"
|
||||
source = "git+https://github.com/gtk-rs/gdk#fa2fb7819b13daa4fac55bd93ac217691cbd9dc8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cairo-rs",
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf",
|
||||
"gdk-sys",
|
||||
"gio",
|
||||
"gio-sys",
|
||||
"glib",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk-pixbuf"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/gtk-rs/gdk-pixbuf#2502779ebc0a81c8a03a64e1fbf576b16eb8b91d"
|
||||
dependencies = [
|
||||
"gdk-pixbuf-sys",
|
||||
"gio",
|
||||
"gio-sys",
|
||||
"glib",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk-pixbuf-sys"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/gtk-rs/sys#56e03c021c393e1cf3f148005b348ac5a8a0ab72"
|
||||
dependencies = [
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk-sys"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/gtk-rs/sys#56e03c021c393e1cf3f148005b348ac5a8a0ab72"
|
||||
dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"pkg-config",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724"
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/gtk-rs/gio#809e580c56f1434327a3c81af49cb75baea5faf7"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"futures",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"gio-sys",
|
||||
"glib",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio-sys"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/gtk-rs/sys#56e03c021c393e1cf3f148005b348ac5a8a0ab72"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/gtk-rs/glib#200d34eab3421b53d0688f02093932e14540f411"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
"glib-macros",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-macros"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/gtk-rs/glib#200d34eab3421b53d0688f02093932e14540f411"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"itertools",
|
||||
"proc-macro-crate",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-sys"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/gtk-rs/sys#56e03c021c393e1cf3f148005b348ac5a8a0ab72"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/gtk-rs/sys#56e03c021c393e1cf3f148005b348ac5a8a0ab72"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/gtk-rs/gtk#9b9cf0f623bd74bf71459f639beac2f6f26c163e"
|
||||
dependencies = [
|
||||
"atk",
|
||||
"bitflags",
|
||||
"cairo-rs",
|
||||
"cairo-sys-rs",
|
||||
"cc",
|
||||
"gdk",
|
||||
"gdk-pixbuf",
|
||||
"gdk-pixbuf-sys",
|
||||
"gdk-sys",
|
||||
"gio",
|
||||
"gio-sys",
|
||||
"glib",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gtk-sys",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pango",
|
||||
"pango-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk-sys"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/gtk-rs/sys#56e03c021c393e1cf3f148005b348ac5a8a0ab72"
|
||||
dependencies = [
|
||||
"atk-sys",
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gdk-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2f02823cf78b754822df5f7f268fb59822e7296276d3e069d8e8cb26a14bd10"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc12a3bc971424edbbf7edaf6e5740483444db63aa8e23d3751ff12a30f306f0"
|
||||
dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be0f75932c1f6cfae3c04000e40114adf955636e19040f9c0a2c380702aa1c7f"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d"
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/gtk-rs/pango#b98784c9881f0def29c8e3bda6d2754e41f96b0f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"glib",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pango-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango-sys"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/gtk-rs/sys#56e03c021c393e1cf3f148005b348ac5a8a0ab72"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "0.4.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "0.4.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
|
||||
dependencies = [
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-hack"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-nested"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.115"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e69abc24912995b3038597a7a593be5053eb0fb44f3cc5beec0deb421790c1f4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"pkg-config",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
|
@ -8,14 +8,20 @@ homepage = "https://subgraph.com"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
rpassword = "4.0"
|
||||
clap = "2.33"
|
||||
rpassword = "7.3"
|
||||
clap = { version = "4.5", features = ["cargo", "derive"] }
|
||||
lazy_static = "1.4"
|
||||
serde_derive = "1.0"
|
||||
serde = "1.0"
|
||||
toml = "0.5"
|
||||
toml = "0.8"
|
||||
hex = "0.4"
|
||||
byteorder = "1"
|
||||
dbus = "0.8.4"
|
||||
pwhash = "0.3.1"
|
||||
pwhash = "1.0"
|
||||
tempfile = "3"
|
||||
ed25519-dalek = {version = "2.1", features = ["pem"]}
|
||||
anyhow = "1.0"
|
||||
rs-release = "0.1"
|
||||
reqwest = {version = "0.12", features = ["blocking"]}
|
||||
glob = "0.3"
|
||||
serde_cbor = "0.11"
|
@ -4,7 +4,7 @@ use std::fs;
|
||||
use std::thread::{self,JoinHandle};
|
||||
use std::time::{self,Instant};
|
||||
|
||||
use libcitadel::{UtsName, util};
|
||||
use libcitadel::{Result, UtsName, util};
|
||||
use libcitadel::ResourceImage;
|
||||
|
||||
use crate::boot::disks;
|
||||
@ -13,33 +13,34 @@ use crate::install::installer::Installer;
|
||||
|
||||
const IMAGE_DIRECTORY: &str = "/run/citadel/images";
|
||||
|
||||
pub fn live_rootfs() -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn live_rootfs() -> Result<()> {
|
||||
copy_artifacts()?;
|
||||
let rootfs = find_rootfs_image()?;
|
||||
setup_rootfs_resource(&rootfs)
|
||||
}
|
||||
|
||||
pub fn live_setup() -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn live_setup() -> Result<()> {
|
||||
decompress_images(true)?;
|
||||
info!("Starting live setup");
|
||||
let live = Installer::new_livesetup();
|
||||
live.run()
|
||||
}
|
||||
|
||||
fn copy_artifacts() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn copy_artifacts() -> Result<()> {
|
||||
for _ in 0..3 {
|
||||
if try_copy_artifacts()? {
|
||||
//decompress_images()?;
|
||||
return Ok(());
|
||||
return Ok(())
|
||||
}
|
||||
// Try again after waiting for more devices to be discovered
|
||||
info!("Failed to find partition with images, trying again in 2 seconds");
|
||||
thread::sleep(time::Duration::from_secs(2));
|
||||
}
|
||||
Result::Err("could not find partition containing resource images".into())
|
||||
bail!("could not find partition containing resource images")
|
||||
|
||||
}
|
||||
|
||||
fn try_copy_artifacts() -> Result<bool, Box<dyn std::error::Error>> {
|
||||
fn try_copy_artifacts() -> Result<bool> {
|
||||
let rootfs_image = Path::new("/boot/images/citadel-rootfs.img");
|
||||
// Already mounted?
|
||||
if rootfs_image.exists() {
|
||||
@ -59,13 +60,13 @@ fn try_copy_artifacts() -> Result<bool, Box<dyn std::error::Error>> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn kernel_version() -> Result<String, Box<dyn std::error::Error>> {
|
||||
fn kernel_version() -> String {
|
||||
let utsname = UtsName::uname();
|
||||
let v = utsname.release().split('-').collect::<Vec<_>>();
|
||||
Ok(v[0].to_string())
|
||||
v[0].to_string()
|
||||
}
|
||||
|
||||
fn deploy_artifacts() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn deploy_artifacts() -> Result<()> {
|
||||
let run_images = Path::new(IMAGE_DIRECTORY);
|
||||
if !run_images.exists() {
|
||||
util::create_dir(run_images)?;
|
||||
@ -77,7 +78,7 @@ fn deploy_artifacts() -> Result<(), Box<dyn std::error::Error>> {
|
||||
util::copy_file(dent.path(), run_images.join(dent.file_name()))
|
||||
})?;
|
||||
|
||||
let kv = kernel_version()?;
|
||||
let kv = kernel_version();
|
||||
println!("Copying bzImage-{} to /run/citadel/images", kv);
|
||||
let from = format!("/boot/bzImage-{}", kv);
|
||||
let to = format!("/run/citadel/images/bzImage-{}", kv);
|
||||
@ -91,7 +92,7 @@ fn deploy_artifacts() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn deploy_syslinux_artifacts() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn deploy_syslinux_artifacts() -> Result<()> {
|
||||
let boot_syslinux = Path::new("/boot/syslinux");
|
||||
|
||||
if !boot_syslinux.exists() {
|
||||
@ -111,11 +112,10 @@ fn deploy_syslinux_artifacts() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn find_rootfs_image() -> Result<ResourceImage, Box<dyn std::error::Error>> {
|
||||
fn find_rootfs_image() -> Result<ResourceImage> {
|
||||
let entries = fs::read_dir(IMAGE_DIRECTORY)
|
||||
.map_err(context!("error reading directory {}", IMAGE_DIRECTORY))?;
|
||||
for entry in entries {
|
||||
@ -123,21 +123,15 @@ fn find_rootfs_image() -> Result<ResourceImage, Box<dyn std::error::Error>> {
|
||||
if entry.path().extension() == Some(OsStr::new("img")) {
|
||||
if let Ok(image) = ResourceImage::from_path(&entry.path()) {
|
||||
if image.metainfo().image_type() == "rootfs" {
|
||||
return Ok(image);
|
||||
return Ok(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Result::Err(
|
||||
format!(
|
||||
"unable to find rootfs resource image in {}",
|
||||
IMAGE_DIRECTORY
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
bail!("unable to find rootfs resource image in {}", IMAGE_DIRECTORY)
|
||||
}
|
||||
|
||||
fn decompress_images(sync: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn decompress_images(sync: bool) -> Result<()> {
|
||||
info!("Decompressing images");
|
||||
let mut threads = Vec::new();
|
||||
util::read_directory("/run/citadel/images", |dent| {
|
||||
@ -146,8 +140,9 @@ fn decompress_images(sync: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if image.is_compressed() {
|
||||
if sync {
|
||||
if let Err(err) = decompress_one_image_sync(image) {
|
||||
warn!("Error decompressing image: {}", err);
|
||||
warn!("Error: {}", err);
|
||||
}
|
||||
|
||||
} else {
|
||||
threads.push(decompress_one_image(image));
|
||||
}
|
||||
@ -166,11 +161,9 @@ fn decompress_images(sync: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decompress_one_image_sync(
|
||||
image: ResourceImage,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let start = Instant::now();
|
||||
info!("Decompressing {}", image.path().display());
|
||||
fn decompress_one_image_sync(image: ResourceImage) -> Result<()> {
|
||||
let start = Instant::now();
|
||||
info!("Decompressing {}", image.path().display());
|
||||
image.decompress(true)
|
||||
.map_err(|e| format_err!("Failed to decompress image file {}: {}", image.path().display(), e))?;
|
||||
cmd!("/usr/bin/du", "-h {}", image.path().display())?;
|
||||
@ -180,7 +173,8 @@ fn decompress_one_image_sync(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decompress_one_image(image: ResourceImage,) ->
|
||||
JoinHandle<Result<(), Box<dyn std::error::Error + Send + Sync>>> {
|
||||
thread::spawn(move || decompress_one_image_sync(image))
|
||||
fn decompress_one_image(image: ResourceImage) -> JoinHandle<Result<()>> {
|
||||
thread::spawn(move || {
|
||||
decompress_one_image_sync(image)
|
||||
})
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::fs;
|
||||
use std::process::exit;
|
||||
|
||||
use libcitadel::{ResourceImage, CommandLine, KeyRing, LogLevel, Logger, util};
|
||||
use libcitadel::{Result, ResourceImage, CommandLine, KeyRing, LogLevel, Logger, util};
|
||||
use libcitadel::RealmManager;
|
||||
use crate::boot::disks::DiskPartition;
|
||||
use std::path::Path;
|
||||
@ -9,40 +10,44 @@ mod live;
|
||||
mod disks;
|
||||
mod rootfs;
|
||||
|
||||
pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn main(args: Vec<String>) {
|
||||
if CommandLine::debug() {
|
||||
Logger::set_log_level(LogLevel::Debug);
|
||||
} else if CommandLine::verbose() {
|
||||
Logger::set_log_level(LogLevel::Info);
|
||||
}
|
||||
|
||||
match args.get(1) {
|
||||
let result = match args.get(1) {
|
||||
Some(s) if s == "rootfs" => do_rootfs(),
|
||||
Some(s) if s == "setup" => do_setup(),
|
||||
Some(s) if s == "boot-automount" => do_boot_automount(),
|
||||
Some(s) if s == "start-realms" => do_start_realms(),
|
||||
_ => Err(format_err!("Bad or missing argument").into()),
|
||||
}?;
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_rootfs() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if CommandLine::live_mode() || CommandLine::install_mode() {
|
||||
live::live_rootfs()
|
||||
} else {
|
||||
Ok(rootfs::setup_rootfs()?)
|
||||
if let Err(ref e) = result {
|
||||
warn!("Failed: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_keyring() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
fn do_rootfs() -> Result<()> {
|
||||
if CommandLine::live_mode() || CommandLine::install_mode() {
|
||||
live::live_rootfs()
|
||||
} else {
|
||||
rootfs::setup_rootfs()
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_keyring() -> Result<()> {
|
||||
ResourceImage::ensure_storage_mounted()?;
|
||||
let keyring = KeyRing::load_with_cryptsetup_passphrase("/sysroot/storage/keyring")?;
|
||||
keyring.add_keys_to_kernel()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_setup() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn do_setup() -> Result<()> {
|
||||
if CommandLine::live_mode() || CommandLine::install_mode() {
|
||||
live::live_setup()?;
|
||||
} else if let Err(err) = setup_keyring() {
|
||||
@ -59,7 +64,8 @@ fn do_setup() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_overlay() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
fn mount_overlay() -> Result<()> {
|
||||
info!("Creating rootfs overlay");
|
||||
|
||||
info!("Moving /sysroot mount to /rootfs.ro");
|
||||
@ -83,13 +89,13 @@ fn mount_overlay() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_start_realms() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn do_start_realms() -> Result<()> {
|
||||
let manager = RealmManager::load()?;
|
||||
Ok(manager.start_boot_realms()?)
|
||||
manager.start_boot_realms()
|
||||
}
|
||||
|
||||
// Write automount unit for /boot partition
|
||||
fn do_boot_automount() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn do_boot_automount() -> Result<()> {
|
||||
Logger::set_log_level(LogLevel::Info);
|
||||
|
||||
if CommandLine::live_mode() || CommandLine::install_mode() {
|
||||
@ -99,10 +105,10 @@ fn do_boot_automount() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
let boot_partition = find_boot_partition()?;
|
||||
info!("Creating /boot automount units for boot partition {}", boot_partition);
|
||||
Ok(cmd!("/usr/bin/systemd-mount", "-A --timeout-idle-sec=300 {} /boot", boot_partition)?)
|
||||
cmd!("/usr/bin/systemd-mount", "-A --timeout-idle-sec=300 {} /boot", boot_partition)
|
||||
}
|
||||
|
||||
fn find_boot_partition() -> Result<String, Box<dyn std::error::Error>> {
|
||||
fn find_boot_partition() -> Result<String> {
|
||||
let loader_dev = read_loader_dev_efi_var()?;
|
||||
let boot_partitions = DiskPartition::boot_partitions(true)?
|
||||
.into_iter()
|
||||
@ -110,7 +116,7 @@ fn find_boot_partition() -> Result<String, Box<dyn std::error::Error>> {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if boot_partitions.len() != 1 {
|
||||
return Result::Err("Cannot uniquely determine boot partition".into());
|
||||
return Err(format_err!("Cannot uniquely determine boot partition"));
|
||||
}
|
||||
|
||||
Ok(boot_partitions[0].path().display().to_string())
|
||||
@ -135,7 +141,7 @@ fn matches_loader_dev(partition: &DiskPartition, dev: &Option<String>) -> bool {
|
||||
const LOADER_EFI_VAR_PATH: &str =
|
||||
"/sys/firmware/efi/efivars/LoaderDevicePartUUID-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
|
||||
|
||||
fn read_loader_dev_efi_var() -> Result<Option<String>, Box<dyn std::error::Error>> {
|
||||
fn read_loader_dev_efi_var() -> Result<Option<String>> {
|
||||
let efi_var = Path::new(LOADER_EFI_VAR_PATH);
|
||||
if efi_var.exists() {
|
||||
let s = fs::read(efi_var)
|
||||
|
@ -1,10 +1,10 @@
|
||||
use std::path::Path;
|
||||
use std::process::{Command,Stdio};
|
||||
|
||||
use libcitadel::{BlockDev, ResourceImage, CommandLine, ImageHeader, Partition, LoopDevice};
|
||||
use libcitadel::{BlockDev, ResourceImage, CommandLine, ImageHeader, Partition, Result, LoopDevice};
|
||||
use libcitadel::verity::Verity;
|
||||
|
||||
pub fn setup_rootfs() -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn setup_rootfs() -> Result<()> {
|
||||
let mut p = choose_boot_partiton(true, CommandLine::revert_rootfs())?;
|
||||
if CommandLine::noverity() {
|
||||
setup_partition_unverified(&p)
|
||||
@ -13,7 +13,7 @@ pub fn setup_rootfs() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_rootfs_resource(rootfs: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn setup_rootfs_resource(rootfs: &ResourceImage) -> Result<()> {
|
||||
if CommandLine::noverity() {
|
||||
setup_resource_unverified(&rootfs)
|
||||
} else {
|
||||
@ -21,7 +21,7 @@ pub fn setup_rootfs_resource(rootfs: &ResourceImage) -> Result<(), Box<dyn std::
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_resource_unverified(img: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn setup_resource_unverified(img: &ResourceImage) -> Result<()> {
|
||||
if img.is_compressed() {
|
||||
img.decompress(false)?;
|
||||
}
|
||||
@ -30,31 +30,25 @@ fn setup_resource_unverified(img: &ResourceImage) -> Result<(), Box<dyn std::err
|
||||
setup_linear_mapping(loopdev.device())
|
||||
}
|
||||
|
||||
fn setup_resource_verified(img: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn setup_resource_verified(img: &ResourceImage) -> Result<()> {
|
||||
let _ = img.setup_verity_device()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_partition_unverified(p: &Partition) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn setup_partition_unverified(p: &Partition) -> Result<()> {
|
||||
info!("Creating /dev/mapper/rootfs device with linear device mapping of partition (no verity)");
|
||||
setup_linear_mapping(p.path())
|
||||
}
|
||||
|
||||
fn setup_partition_verified(p: &mut Partition) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn setup_partition_verified(p: &mut Partition) -> Result<()> {
|
||||
info!("Creating /dev/mapper/rootfs dm-verity device");
|
||||
if !CommandLine::nosignatures() {
|
||||
if !p.has_public_key() {
|
||||
return Result::Err(
|
||||
format!(
|
||||
"no public key available for channel {}",
|
||||
p.metainfo().channel()
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
bail!("no public key available for channel {}", p.metainfo().channel())
|
||||
}
|
||||
if !p.is_signature_valid() {
|
||||
p.write_status(ImageHeader::STATUS_BAD_SIG)?;
|
||||
return Result::Err("signature verification failed on partition".into());
|
||||
bail!("signature verification failed on partition");
|
||||
}
|
||||
info!("Image signature is valid for channel {}", p.metainfo().channel());
|
||||
}
|
||||
@ -62,7 +56,7 @@ fn setup_partition_verified(p: &mut Partition) -> Result<(), Box<dyn std::error:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_linear_mapping(blockdev: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn setup_linear_mapping(blockdev: &Path) -> Result<()> {
|
||||
let dev = BlockDev::open_ro(blockdev)?;
|
||||
let table = format!("0 {} linear {} 0", dev.nsectors()?, blockdev.display());
|
||||
|
||||
@ -76,9 +70,7 @@ fn setup_linear_mapping(blockdev: &Path) -> Result<(), Box<dyn std::error::Error
|
||||
.success();
|
||||
|
||||
if !ok {
|
||||
return Result::Err(
|
||||
"failed to set up linear identity mapping with /usr/sbin/dmsetup".into(),
|
||||
);
|
||||
bail!("failed to set up linear identity mapping with /usr/sbin/dmsetup");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -102,8 +94,7 @@ fn choose_revert_partition(best: Option<Partition>) -> Option<Partition> {
|
||||
best
|
||||
}
|
||||
|
||||
fn choose_boot_partiton(scan: bool, revert_rootfs: bool,
|
||||
) -> Result<Partition, Box<dyn std::error::Error>> {
|
||||
fn choose_boot_partiton(scan: bool, revert_rootfs: bool) -> Result<Partition> {
|
||||
let mut partitions = Partition::rootfs_partitions()?;
|
||||
|
||||
if scan {
|
||||
@ -145,17 +136,17 @@ fn compare_boot_partitions(a: Option<Partition>, b: Partition) -> Option<Partiti
|
||||
}
|
||||
|
||||
// Compare versions and channels
|
||||
let meta_a = a.metainfo();
|
||||
let meta_b = b.metainfo();
|
||||
let bind_a = a.metainfo();
|
||||
let bind_b = b.metainfo();
|
||||
|
||||
let ver_a = meta_a.version();
|
||||
let ver_b = meta_b.version();
|
||||
let a_v = bind_a.version();
|
||||
let b_v = bind_b.version();
|
||||
|
||||
// Compare versions only if channels match
|
||||
if a.metainfo().channel() == b.metainfo().channel() {
|
||||
if ver_a > ver_b {
|
||||
if a_v > b_v {
|
||||
return Some(a);
|
||||
} else if ver_b > ver_a {
|
||||
} else if b_v > a_v {
|
||||
return Some(b);
|
||||
}
|
||||
}
|
||||
|
328
citadel-tool/src/fetch/fetch.rs
Normal file
328
citadel-tool/src/fetch/fetch.rs
Normal file
@ -0,0 +1,328 @@
|
||||
use crate::{update, Path};
|
||||
use anyhow::{Context, Result};
|
||||
use clap::ArgMatches;
|
||||
use ed25519_dalek::{pkcs8::DecodePublicKey, VerifyingKey};
|
||||
use libcitadel::updates::UPDATE_SERVER_HOSTNAME;
|
||||
use libcitadel::ResourceImage;
|
||||
use libcitadel::{updates, updates::CitadelVersionStruct};
|
||||
|
||||
use std::io::prelude::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
const OS_RELEASE_PATH: &str = "/etc/os-release";
|
||||
const IMAGE_DIRECTORY_PATH: &str = "/run/citadel/images/";
|
||||
const EXTRA_IMAGE_PATH: &str = "/run/citadel/images/citadel-extra.img";
|
||||
const ROOTFS_IMAGE_PATH: &str = "/run/citadel/images/citadel-rootfs.img";
|
||||
const UPDATE_SERVER_KEY_PATH: &str = "/etc/citadel/update_server_key.pub";
|
||||
const LAST_RESORT_CLIENT: &str = "public";
|
||||
|
||||
pub fn check() -> Result<()> {
|
||||
let current_version = get_current_os_config()?;
|
||||
|
||||
let server_citadel_version = fetch_and_verify_version_cbor(¤t_version)?;
|
||||
|
||||
let components_to_upgrade =
|
||||
compare_citadel_versions(¤t_version, &server_citadel_version)?;
|
||||
|
||||
if components_to_upgrade.len() == 1 {
|
||||
println!(
|
||||
"We found the following component to upgrade: {}",
|
||||
components_to_upgrade[0]
|
||||
);
|
||||
} else if components_to_upgrade.len() > 1 {
|
||||
println!("We found the following components to upgrade: \n");
|
||||
|
||||
for component in components_to_upgrade {
|
||||
println!("{}", component);
|
||||
}
|
||||
} else {
|
||||
println!("Your system is up to date!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn download(sub_matches: &ArgMatches) -> Result<()> {
|
||||
let current_version = &get_current_os_config()?;
|
||||
let server_citadel_version = &fetch_and_verify_version_cbor(¤t_version)?;
|
||||
|
||||
let mut path = "";
|
||||
if sub_matches.get_flag("rootfs") {
|
||||
path = &server_citadel_version.component_version[0].file_path;
|
||||
} else if sub_matches.get_flag("kernel") {
|
||||
path = &server_citadel_version.component_version[1].file_path;
|
||||
} else if sub_matches.get_flag("extra") {
|
||||
path = &server_citadel_version.component_version[2].file_path;
|
||||
}
|
||||
|
||||
download_file(path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_remote() -> Result<()> {
|
||||
let server_citadel_version = fetch_and_verify_version_cbor(&get_current_os_config()?)?;
|
||||
|
||||
println!("Server offers:\n{}", server_citadel_version);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn upgrade() -> Result<()> {
|
||||
// First get access to the current citadel's parameters
|
||||
let current_version = &get_current_os_config()?;
|
||||
let server_citadel_version = &fetch_and_verify_version_cbor(¤t_version)?;
|
||||
|
||||
// What do we need to upgrade?
|
||||
let components_to_upgrade =
|
||||
compare_citadel_versions(¤t_version, &server_citadel_version)?;
|
||||
|
||||
if components_to_upgrade.len() == 1 {
|
||||
println!("We found a component to upgrade!");
|
||||
let allow_download = prompt_user_for_permission_to_download(&components_to_upgrade[0])?;
|
||||
|
||||
if allow_download {
|
||||
let save_path = download_file(&components_to_upgrade[0].file_path)?;
|
||||
|
||||
// run citadel-update to upgrade
|
||||
println!("Installing image");
|
||||
update::install_image(&save_path, 0)?;
|
||||
println!("Image installed correctly");
|
||||
} else {
|
||||
println!("Ok! Maybe later");
|
||||
}
|
||||
} else if components_to_upgrade.len() > 1 {
|
||||
println!("We found some components to upgrade!");
|
||||
for component in components_to_upgrade {
|
||||
let allow_download = prompt_user_for_permission_to_download(&component)?;
|
||||
|
||||
if allow_download {
|
||||
let save_path = download_file(&component.file_path)?;
|
||||
|
||||
println!("Installing image");
|
||||
update::install_image(&save_path, 0)?;
|
||||
println!("Image installed correctly");
|
||||
} else {
|
||||
println!("Ok! Maybe later");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Your system is up to date!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reinstall(sub_matches: &ArgMatches) -> Result<()> {
|
||||
let current_version = &get_current_os_config()?;
|
||||
let server_citadel_version = &fetch_and_verify_version_cbor(¤t_version)?;
|
||||
|
||||
let mut path = "";
|
||||
if sub_matches.get_flag("rootfs") {
|
||||
path = &server_citadel_version.component_version[0].file_path;
|
||||
} else if sub_matches.get_flag("kernel") {
|
||||
path = &server_citadel_version.component_version[1].file_path;
|
||||
} else if sub_matches.get_flag("extra") {
|
||||
path = &server_citadel_version.component_version[2].file_path;
|
||||
}
|
||||
|
||||
let save_path = download_file(path)?;
|
||||
update::install_image(&save_path, 0)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a vec of ComponentVersion structs of the components which can be upgraded
|
||||
fn compare_citadel_versions(
|
||||
current: &CitadelVersionStruct,
|
||||
offered: &CitadelVersionStruct,
|
||||
) -> Result<Vec<updates::AvailableComponentVersion>> {
|
||||
let mut update_vec: Vec<updates::AvailableComponentVersion> = Vec::new();
|
||||
|
||||
// safety checks
|
||||
if current.channel != offered.channel {
|
||||
panic!("Error: channels do not match");
|
||||
} else if current.client != offered.client {
|
||||
panic!("Error: clients do not match");
|
||||
} else if current.publisher != offered.publisher {
|
||||
panic!("Error: publishers do not match");
|
||||
}
|
||||
|
||||
for i in 0..current.component_version.len() {
|
||||
if current.component_version[i] < offered.component_version[i] {
|
||||
update_vec.push(offered.component_version[i].clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(update_vec)
|
||||
}
|
||||
|
||||
fn get_image_version<P: AsRef<Path>>(path: &P) -> Result<String> {
|
||||
let resource_image = ResourceImage::from_path(path)?;
|
||||
|
||||
Ok(resource_image.metainfo().version().to_string())
|
||||
}
|
||||
|
||||
fn get_current_os_config() -> Result<updates::CitadelVersionStruct> {
|
||||
let os_release = rs_release::parse_os_release(OS_RELEASE_PATH).context(format!(
|
||||
"Failed to get OS_RELEASE_PATH from {}",
|
||||
OS_RELEASE_PATH
|
||||
))?;
|
||||
|
||||
// We need to get the version of the rootfs, kernel and extra images
|
||||
// We first check if there is an env var called UPDATES_CLIENT
|
||||
let client = match std::env::var("UPDATES_CLIENT") {
|
||||
Ok(a) => a,
|
||||
Err(_) => os_release
|
||||
.get("CLIENT")
|
||||
.unwrap_or(&LAST_RESORT_CLIENT.to_string())
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
let channel = os_release.get("CITADEL_CHANNEL");
|
||||
|
||||
// Search image dir for kernel image and extract version
|
||||
let mut kernel_version = String::from("0.0.0");
|
||||
for path in glob::glob(&format!("{}citadel-kernel*.img", IMAGE_DIRECTORY_PATH))
|
||||
.expect("Failed to read glob pattern")
|
||||
{
|
||||
match path {
|
||||
Ok(path) => {
|
||||
kernel_version = get_image_version(&path).context(format!(
|
||||
"Failed to get image version at path: {}",
|
||||
path.display()
|
||||
))?
|
||||
}
|
||||
Err(e) => anyhow::bail!("Failed to find kernel image. Error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
let rootfs_version = get_image_version(&ROOTFS_IMAGE_PATH).context(format!(
|
||||
"Failed to get rootfs img version at path: {}",
|
||||
ROOTFS_IMAGE_PATH
|
||||
))?;
|
||||
let extra_version = get_image_version(&EXTRA_IMAGE_PATH).context(format!(
|
||||
"Failed to get extra img version at path: {}",
|
||||
EXTRA_IMAGE_PATH
|
||||
))?;
|
||||
|
||||
let publisher = os_release.get("CITADEL_PUBLISHER");
|
||||
|
||||
let mut component_version = Vec::new();
|
||||
component_version.push(updates::AvailableComponentVersion {
|
||||
component: updates::Component::Rootfs,
|
||||
version: rootfs_version.to_owned(),
|
||||
file_path: "".to_owned(),
|
||||
});
|
||||
component_version.push(updates::AvailableComponentVersion {
|
||||
component: updates::Component::Kernel,
|
||||
version: kernel_version.to_owned(),
|
||||
file_path: "".to_owned(),
|
||||
});
|
||||
component_version.push(updates::AvailableComponentVersion {
|
||||
component: updates::Component::Extra,
|
||||
version: extra_version.to_owned(),
|
||||
file_path: "".to_owned(),
|
||||
});
|
||||
|
||||
let current_version_struct = updates::CitadelVersionStruct {
|
||||
client: client.to_owned(),
|
||||
channel: channel.unwrap_or(&"prod".to_owned()).to_owned(),
|
||||
component_version,
|
||||
publisher: publisher.unwrap_or(&"Subgraph".to_string()).to_owned(),
|
||||
};
|
||||
|
||||
Ok(current_version_struct)
|
||||
}
|
||||
|
||||
fn fetch_and_verify_version_cbor(
|
||||
current_citadel_version: &updates::CitadelVersionStruct,
|
||||
) -> Result<updates::CitadelVersionStruct> {
|
||||
let url = format!(
|
||||
"https://{}/{}/{}/version.cbor",
|
||||
UPDATE_SERVER_HOSTNAME, current_citadel_version.client, current_citadel_version.channel
|
||||
);
|
||||
|
||||
let version_file_bytes = reqwest::blocking::get(&url)?
|
||||
.bytes()
|
||||
.context(format!("Failed to get version_file_bytes from {}", url))?;
|
||||
|
||||
let crypto_container: updates::CryptoContainerFile =
|
||||
serde_cbor::from_slice(&version_file_bytes)
|
||||
.context(format!("Failed to parse version.cbor from {}", url))?;
|
||||
|
||||
// find update server public key kept in the rootfs
|
||||
let mut file = std::fs::File::open(UPDATE_SERVER_KEY_PATH).context(format!(
|
||||
"Failed to open update_server_key file from {}",
|
||||
UPDATE_SERVER_KEY_PATH
|
||||
))?;
|
||||
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
|
||||
let public_key = VerifyingKey::from_public_key_pem(&contents)
|
||||
.context("Failed to parse public key from file.")?;
|
||||
|
||||
let signature = ed25519_dalek::Signature::from_str(&crypto_container.signature)?;
|
||||
|
||||
// verify signature
|
||||
public_key.verify_strict(&crypto_container.serialized_citadel_version, &signature)?;
|
||||
|
||||
// construct the struct
|
||||
let citadel_version_struct: updates::CitadelVersionStruct =
|
||||
serde_cbor::from_slice(&crypto_container.serialized_citadel_version)?;
|
||||
|
||||
Ok(citadel_version_struct)
|
||||
}
|
||||
|
||||
fn prompt_user_for_permission_to_download(
|
||||
component: &updates::AvailableComponentVersion,
|
||||
) -> Result<bool> {
|
||||
println!(
|
||||
"Would you like to download the new version of the {} image with version {}? (y/n)",
|
||||
component.component, component.version
|
||||
);
|
||||
|
||||
loop {
|
||||
let stdin = std::io::stdin();
|
||||
let mut user_input = String::new();
|
||||
|
||||
stdin.read_line(&mut user_input)?;
|
||||
|
||||
if user_input.trim() == "y" {
|
||||
return Ok(true);
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn download_file(path: &str) -> Result<std::path::PathBuf> {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
|
||||
let url = format!("https://{}/{}", UPDATE_SERVER_HOSTNAME, path);
|
||||
|
||||
println!("Downloading from {}", url);
|
||||
|
||||
let component_download_response = client.get(&url).send()?;
|
||||
|
||||
if !component_download_response.status().is_success() {
|
||||
anyhow::bail!(
|
||||
"Failed to download image from {}. Server returned error {}",
|
||||
path,
|
||||
component_download_response.status()
|
||||
);
|
||||
}
|
||||
|
||||
let path = Path::new(path);
|
||||
let path = format!("/tmp/{}", path.file_name().unwrap().to_str().unwrap());
|
||||
|
||||
let mut content = std::io::Cursor::new(component_download_response.bytes()?);
|
||||
let mut file =
|
||||
std::fs::File::create(&path).context(format!("Failed to create file at {}", path))?;
|
||||
std::io::copy(&mut content, &mut file)?;
|
||||
|
||||
println!("Saved file to {}", path);
|
||||
|
||||
Ok(std::path::PathBuf::from(path))
|
||||
}
|
49
citadel-tool/src/fetch/mod.rs
Normal file
49
citadel-tool/src/fetch/mod.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use clap::{arg, command, ArgAction, Command};
|
||||
use std::process::exit;
|
||||
|
||||
mod fetch;
|
||||
|
||||
pub fn main() {
|
||||
let matches = command!() // requires `cargo` feature
|
||||
.subcommand_required(true)
|
||||
.subcommand(Command::new("check").about("Check for updates from remote server"))
|
||||
.subcommand(
|
||||
Command::new("download")
|
||||
.about("Download a specific file from the server")
|
||||
.arg(arg!(-r --rootfs "rootfs component").action(ArgAction::SetTrue))
|
||||
.arg(arg!(-k --kernel "kernel component").action(ArgAction::SetTrue))
|
||||
.arg(arg!(-e --extra "extra component").action(ArgAction::SetTrue))
|
||||
.arg_required_else_help(true),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("read-remote")
|
||||
.about("Read the remote server and print information on versions offered"),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("upgrade")
|
||||
.about("Download and install all components found on the server to be more recent than currently installed on system")
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("reinstall")
|
||||
.about("Download and install a specific component even if the server's component version is not greater than currently installed")
|
||||
.arg(arg!(-r --rootfs "rootfs component").action(ArgAction::SetTrue))
|
||||
.arg(arg!(-k --kernel "kernel component").action(ArgAction::SetTrue))
|
||||
.arg(arg!(-e --extra "extra component").action(ArgAction::SetTrue))
|
||||
.arg_required_else_help(true),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let result = match matches.subcommand() {
|
||||
Some(("check", _sub_matches)) => fetch::check(),
|
||||
Some(("download", sub_matches)) => fetch::download(sub_matches),
|
||||
Some(("read-remote", _sub_matches)) => fetch::read_remote(),
|
||||
Some(("upgrade", _sub_matches)) => fetch::upgrade(),
|
||||
Some(("reinstall", sub_matches)) => fetch::reinstall(sub_matches),
|
||||
_ => unreachable!("Please pass a subcommand"),
|
||||
};
|
||||
|
||||
if let Err(ref e) = result {
|
||||
println!("Error: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
@ -1,88 +1,97 @@
|
||||
use std::path::Path;
|
||||
use std::process::exit;
|
||||
|
||||
use clap::{App,Arg,SubCommand,ArgMatches};
|
||||
use clap::AppSettings::*;
|
||||
use libcitadel::{ResourceImage, Logger, LogLevel, Partition, KeyPair, ImageHeader, util};
|
||||
use clap::{Arg,ArgMatches};
|
||||
use clap::{command, ArgAction, Command};
|
||||
use hex;
|
||||
|
||||
pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let app = App::new("citadel-image")
|
||||
use libcitadel::{Result, ResourceImage, Logger, LogLevel, Partition, KeyPair, ImageHeader, util};
|
||||
|
||||
pub fn main() {
|
||||
let matches = command!()
|
||||
.about("Citadel update image builder")
|
||||
.settings(&[ArgRequiredElseHelp,ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder])
|
||||
.arg_required_else_help(true)
|
||||
.disable_help_subcommand(true)
|
||||
.subcommand(
|
||||
Command::new("metainfo")
|
||||
.about("Display metainfo variables for an image file")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("info")
|
||||
.about("Display metainfo variables for an image file")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("generate-verity")
|
||||
.about("Generate dm-verity hash tree for an image file")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("verify")
|
||||
.about("Verify dm-verity hash tree for an image file")
|
||||
.arg(
|
||||
Arg::new("option")
|
||||
.long("option")
|
||||
.required(true)
|
||||
.help("Path to image file"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("install-rootfs")
|
||||
.about("Install rootfs image file to a partition")
|
||||
.arg(
|
||||
Arg::new("choose")
|
||||
.long("just-choose")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Don't install anything, just show which partition would be chosen"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("skip-sha")
|
||||
.long("skip-sha")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Skip verification of header sha256 value"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-prefer")
|
||||
.long("no-prefer")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Don't set PREFER_BOOT flag"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("path")
|
||||
.required_unless_present("choose")
|
||||
.help("Path to image file"),
|
||||
),
|
||||
)
|
||||
.subcommand(Command::new("genkeys").about("Generate a pair of keys"))
|
||||
.subcommand(
|
||||
Command::new("decompress")
|
||||
.about("Decompress a compressed image file")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("bless")
|
||||
.about("Mark currently mounted rootfs partition as successfully booted"),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("verify-shasum")
|
||||
.about("Verify the sha256 sum of the image")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
.subcommand(SubCommand::with_name("metainfo")
|
||||
.about("Display metainfo variables for an image file")
|
||||
.arg(Arg::with_name("path")
|
||||
.required(true)
|
||||
.help("Path to image file")))
|
||||
|
||||
.subcommand(SubCommand::with_name("info")
|
||||
.about("Display metainfo variables for an image file")
|
||||
.arg(Arg::with_name("path")
|
||||
.required(true)
|
||||
.help("Path to image file")))
|
||||
|
||||
.subcommand(SubCommand::with_name("generate-verity")
|
||||
.about("Generate dm-verity hash tree for an image file")
|
||||
.arg(Arg::with_name("path")
|
||||
.required(true)
|
||||
.help("Path to image file")))
|
||||
|
||||
.subcommand(SubCommand::with_name("verify")
|
||||
.about("Verify dm-verity hash tree for an image file")
|
||||
.arg(Arg::with_name("path")
|
||||
.required(true)
|
||||
.help("Path to image file")))
|
||||
|
||||
.subcommand(SubCommand::with_name("install-rootfs")
|
||||
.about("Install rootfs image file to a partition")
|
||||
.arg(Arg::with_name("choose")
|
||||
.long("just-choose")
|
||||
.help("Don't install anything, just show which partition would be chosen"))
|
||||
.arg(Arg::with_name("skip-sha")
|
||||
.long("skip-sha")
|
||||
.help("Skip verification of header sha256 value"))
|
||||
.arg(Arg::with_name("no-prefer")
|
||||
.long("no-prefer")
|
||||
.help("Don't set PREFER_BOOT flag"))
|
||||
.arg(Arg::with_name("path")
|
||||
.required_unless("choose")
|
||||
.help("Path to image file")))
|
||||
|
||||
.subcommand(SubCommand::with_name("genkeys")
|
||||
.about("Generate a pair of keys"))
|
||||
|
||||
.subcommand(SubCommand::with_name("decompress")
|
||||
.about("Decompress a compressed image file")
|
||||
.arg(Arg::with_name("path")
|
||||
.required(true)
|
||||
.help("Path to image file")))
|
||||
|
||||
.subcommand(SubCommand::with_name("bless")
|
||||
.about("Mark currently mounted rootfs partition as successfully booted"))
|
||||
|
||||
.subcommand(SubCommand::with_name("verify-shasum")
|
||||
.about("Verify the sha256 sum of the image")
|
||||
.arg(Arg::with_name("path")
|
||||
.required(true)
|
||||
.help("Path to image file")));
|
||||
|
||||
Logger::set_log_level(LogLevel::Debug);
|
||||
|
||||
let matches = app.get_matches_from(args);
|
||||
let result = match matches.subcommand() {
|
||||
("metainfo", Some(m)) => metainfo(m),
|
||||
("info", Some(m)) => info(m),
|
||||
("generate-verity", Some(m)) => generate_verity(m),
|
||||
("verify", Some(m)) => verify(m),
|
||||
("sign-image", Some(m)) => sign_image(m),
|
||||
("genkeys", Some(_)) => genkeys(),
|
||||
("decompress", Some(m)) => decompress(m),
|
||||
("verify-shasum", Some(m)) => verify_shasum(m),
|
||||
("install-rootfs", Some(m)) => install_rootfs(m),
|
||||
("install", Some(m)) => install_image(m),
|
||||
("bless", Some(_)) => bless(),
|
||||
Some(("metainfo", sub_m)) => metainfo(sub_m),
|
||||
Some(("info", sub_m)) => info(sub_m),
|
||||
Some(("generate-verity", sub_m)) => generate_verity(sub_m),
|
||||
Some(("verify", sub_m)) => verify(sub_m),
|
||||
Some(("sign-image", sub_m)) => sign_image(sub_m),
|
||||
Some(("genkeys", _)) => genkeys(),
|
||||
Some(("decompress", sub_m)) => decompress(sub_m),
|
||||
Some(("verify-shasum", sub_m)) => verify_shasum(sub_m),
|
||||
Some(("install-rootfs", sub_m)) => install_rootfs(sub_m),
|
||||
Some(("install", sub_m)) => install_image(sub_m),
|
||||
Some(("bless", _)) => bless(),
|
||||
_ => Ok(()),
|
||||
};
|
||||
|
||||
@ -90,18 +99,16 @@ pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Error: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn info(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn info(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = load_image(arg_matches)?;
|
||||
print!("{}", String::from_utf8(img.header().metainfo_bytes())?);
|
||||
print!("{}",String::from_utf8(img.header().metainfo_bytes())?);
|
||||
info_signature(&img)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn info_signature(img: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn info_signature(img: &ResourceImage) -> Result<()> {
|
||||
if img.header().has_signature() {
|
||||
println!("Signature: {}", hex::encode(&img.header().signature()));
|
||||
} else {
|
||||
@ -117,15 +124,15 @@ fn info_signature(img: &ResourceImage) -> Result<(), Box<dyn std::error::Error>>
|
||||
},
|
||||
None => { println!("No public key found for channel '{}'", img.metainfo().channel()) },
|
||||
}
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
fn metainfo(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn metainfo(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = load_image(arg_matches)?;
|
||||
print!("{}", String::from_utf8(img.header().metainfo_bytes())?);
|
||||
print!("{}",String::from_utf8(img.header().metainfo_bytes())?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_verity(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn generate_verity(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = load_image(arg_matches)?;
|
||||
if img.has_verity_hashtree() {
|
||||
info!("Image already has dm-verity hashtree appended, doing nothing.");
|
||||
@ -135,7 +142,7 @@ fn generate_verity(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::E
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn verify(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = load_image(arg_matches)?;
|
||||
let ok = img.verify_verity()?;
|
||||
if ok {
|
||||
@ -146,7 +153,7 @@ fn verify(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_shasum(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn verify_shasum(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = load_image(arg_matches)?;
|
||||
let shasum = img.generate_shasum()?;
|
||||
if shasum == img.metainfo().shasum() {
|
||||
@ -159,37 +166,39 @@ fn verify_shasum(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Err
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage, Box<dyn std::error::Error>> {
|
||||
let path = arg_matches.value_of("path").expect("path argument missing");
|
||||
fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage> {
|
||||
let path = arg_matches.get_one::<String>("path")
|
||||
.expect("path argument missing");
|
||||
|
||||
if !Path::new(path).exists() {
|
||||
panic!("Cannot load image {}: File does not exist", path);
|
||||
bail!("Cannot load image {}: File does not exist", path);
|
||||
}
|
||||
let img = ResourceImage::from_path(path)?;
|
||||
if !img.is_valid_image() {
|
||||
panic!("File {} is not a valid image file", path);
|
||||
bail!("File {} is not a valid image file", path);
|
||||
}
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
fn install_rootfs(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if arg_matches.is_present("choose") {
|
||||
fn install_rootfs(arg_matches: &ArgMatches) -> Result<()> {
|
||||
if arg_matches.get_flag("choose") {
|
||||
let _ = choose_install_partition(true)?;
|
||||
return Ok(());
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
let img = load_image(arg_matches)?;
|
||||
|
||||
if !arg_matches.is_present("skip-sha") {
|
||||
if !arg_matches.get_flag("skip-sha") {
|
||||
info!("Verifying sha256 hash of image");
|
||||
let shasum = img.generate_shasum()?;
|
||||
if shasum != img.metainfo().shasum() {
|
||||
panic!("image file does not have expected sha256 value");
|
||||
bail!("image file does not have expected sha256 value");
|
||||
}
|
||||
}
|
||||
|
||||
let partition = choose_install_partition(true)?;
|
||||
|
||||
if !arg_matches.is_present("no-prefer") {
|
||||
if !arg_matches.get_flag("no-prefer") {
|
||||
clear_prefer_boot()?;
|
||||
img.header().set_flag(ImageHeader::FLAG_PREFER_BOOT);
|
||||
}
|
||||
@ -197,7 +206,7 @@ fn install_rootfs(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Er
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_prefer_boot() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn clear_prefer_boot() -> Result<()> {
|
||||
for mut p in Partition::rootfs_partitions()? {
|
||||
if p.is_initialized() && p.header().has_flag(ImageHeader::FLAG_PREFER_BOOT) {
|
||||
p.clear_flag_and_write(ImageHeader::FLAG_PREFER_BOOT)?;
|
||||
@ -206,14 +215,16 @@ fn clear_prefer_boot() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sign_image(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn sign_image(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let _img = load_image(arg_matches)?;
|
||||
info!("Not implemented yet");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_image(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let source = arg_matches.value_of("path").expect("path argument missing");
|
||||
fn install_image(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let source = arg_matches.get_one::<String>("path")
|
||||
.expect("path argument missing");
|
||||
|
||||
let img = load_image(arg_matches)?;
|
||||
let _hdr = img.header();
|
||||
let metainfo = img.metainfo();
|
||||
@ -221,12 +232,12 @@ fn install_image(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Err
|
||||
// XXX verify signature?
|
||||
|
||||
if !(metainfo.image_type() == "kernel" || metainfo.image_type() == "extra") {
|
||||
panic!("Cannot install image type {}", metainfo.image_type());
|
||||
bail!("Cannot install image type {}", metainfo.image_type());
|
||||
}
|
||||
|
||||
let shasum = img.generate_shasum()?;
|
||||
if shasum != img.metainfo().shasum() {
|
||||
panic!("Image shasum does not match metainfo");
|
||||
bail!("Image shasum does not match metainfo");
|
||||
}
|
||||
|
||||
img.generate_verity_hashtree()?;
|
||||
@ -234,49 +245,44 @@ fn install_image(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Err
|
||||
let filename = if metainfo.image_type() == "kernel" {
|
||||
let kernel_version = match metainfo.kernel_version() {
|
||||
Some(version) => version,
|
||||
None => panic!("Kernel image does not have a kernel version field in metainfo"),
|
||||
None => bail!("Kernel image does not have a kernel version field in metainfo"),
|
||||
};
|
||||
if kernel_version.chars().any(|c| c == '/') {
|
||||
panic!("Kernel version field has / char");
|
||||
bail!("Kernel version field has / char");
|
||||
}
|
||||
format!("citadel-kernel-{}-{:03}.img", kernel_version, metainfo.version())
|
||||
format!("citadel-kernel-{}-{}.img", kernel_version, metainfo.version())
|
||||
} else {
|
||||
format!("citadel-extra-{:03}.img", metainfo.version())
|
||||
format!("citadel-extra-{}.img", metainfo.version())
|
||||
};
|
||||
|
||||
if !metainfo.channel().chars().all(|c| c.is_ascii_lowercase()) {
|
||||
panic!(
|
||||
"Refusing to build path from strange channel name {}",
|
||||
metainfo.channel()
|
||||
);
|
||||
bail!("Refusing to build path from strange channel name {}", metainfo.channel());
|
||||
}
|
||||
let image_dir = Path::new("/storage/resources").join(metainfo.channel());
|
||||
let image_dest = image_dir.join(filename);
|
||||
if image_dest.exists() {
|
||||
rotate(&image_dest)?;
|
||||
}
|
||||
util::rename(source, &image_dest)?;
|
||||
Ok(())
|
||||
util::rename(source, &image_dest)
|
||||
}
|
||||
|
||||
fn rotate(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn rotate(path: &Path) -> Result<()> {
|
||||
if !path.exists() || path.file_name().is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let filename = path.file_name().unwrap();
|
||||
let dot_zero = path.with_file_name(format!("{}.0", filename.to_string_lossy()));
|
||||
util::remove_file(&dot_zero)?;
|
||||
util::rename(path, &dot_zero).unwrap();
|
||||
Ok(())
|
||||
util::rename(path, &dot_zero)
|
||||
}
|
||||
|
||||
fn genkeys() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn genkeys() -> Result<()> {
|
||||
let keypair = KeyPair::generate();
|
||||
println!("keypair = \"{}\"", keypair.to_hex());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decompress(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn decompress(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = load_image(arg_matches)?;
|
||||
if !img.is_compressed() {
|
||||
info!("Image is not compressed, not decompressing.");
|
||||
@ -286,7 +292,7 @@ fn decompress(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bless() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn bless() -> Result<()> {
|
||||
for mut p in Partition::rootfs_partitions()? {
|
||||
if p.is_initialized() && p.is_mounted() {
|
||||
p.bless()?;
|
||||
@ -305,7 +311,7 @@ fn bool_to_yesno(val: bool) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn choose_install_partition(verbose: bool) -> Result<Partition, Box<dyn std::error::Error>> {
|
||||
fn choose_install_partition(verbose: bool) -> Result<Partition> {
|
||||
let partitions = Partition::rootfs_partitions()?;
|
||||
|
||||
if verbose {
|
||||
@ -320,12 +326,9 @@ fn choose_install_partition(verbose: bool) -> Result<Partition, Box<dyn std::err
|
||||
for p in &partitions {
|
||||
if !p.is_mounted() && !p.is_initialized() {
|
||||
if verbose {
|
||||
info!(
|
||||
"Choosing {} because it is empty and not mounted",
|
||||
p.path().display()
|
||||
);
|
||||
info!("Choosing {} because it is empty and not mounted", p.path().display());
|
||||
}
|
||||
return Ok(p.clone());
|
||||
return Ok(p.clone())
|
||||
}
|
||||
}
|
||||
for p in &partitions {
|
||||
@ -333,10 +336,10 @@ fn choose_install_partition(verbose: bool) -> Result<Partition, Box<dyn std::err
|
||||
if verbose {
|
||||
info!("Choosing {} because it is not mounted", p.path().display());
|
||||
info!("Header metainfo:");
|
||||
print!("{}", String::from_utf8(p.header().metainfo_bytes())?);
|
||||
print!("{}",String::from_utf8(p.header().metainfo_bytes())?);
|
||||
}
|
||||
return Ok(p.clone());
|
||||
return Ok(p.clone())
|
||||
}
|
||||
}
|
||||
panic!("No suitable install partition found")
|
||||
bail!("No suitable install partition found")
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::io::{self,Write};
|
||||
use std::path::Path;
|
||||
use libcitadel::Result;
|
||||
use super::disk::Disk;
|
||||
use rpassword;
|
||||
use crate::install::installer::Installer;
|
||||
@ -7,7 +8,7 @@ use crate::install::installer::Installer;
|
||||
const CITADEL_PASSPHRASE_PROMPT: &str = "Enter a password for the Citadel user (or 'q' to quit)";
|
||||
const LUKS_PASSPHRASE_PROMPT: &str = "Enter a disk encryption passphrase (or 'q' to quit";
|
||||
|
||||
pub fn run_cli_install() -> Result<bool, Box<dyn std::error::Error>> {
|
||||
pub fn run_cli_install() -> Result<bool> {
|
||||
let disk = match choose_disk()? {
|
||||
Some(disk) => disk,
|
||||
None => return Ok(false),
|
||||
@ -32,7 +33,7 @@ pub fn run_cli_install() -> Result<bool, Box<dyn std::error::Error>> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn run_cli_install_with<P: AsRef<Path>>(target: P) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
pub fn run_cli_install_with<P: AsRef<Path>>(target: P) -> Result<bool> {
|
||||
let disk = find_disk_by_path(target.as_ref())?;
|
||||
display_disk(&disk);
|
||||
|
||||
@ -54,15 +55,11 @@ pub fn run_cli_install_with<P: AsRef<Path>>(target: P) -> Result<bool, Box<dyn s
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn run_install(
|
||||
disk: Disk,
|
||||
citadel_passphrase: String,
|
||||
passphrase: String,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn run_install(disk: Disk, citadel_passphrase: String, passphrase: String) -> Result<()> {
|
||||
let mut install = Installer::new(disk.path(), &citadel_passphrase, &passphrase);
|
||||
install.set_install_syslinux(true);
|
||||
install.verify()?;
|
||||
Ok(install.run()?)
|
||||
install.run()
|
||||
}
|
||||
|
||||
fn display_disk(disk: &Disk) {
|
||||
@ -73,22 +70,22 @@ fn display_disk(disk: &Disk) {
|
||||
println!();
|
||||
}
|
||||
|
||||
fn find_disk_by_path(path: &Path) -> Result<Disk, Box<dyn std::error::Error>> {
|
||||
fn find_disk_by_path(path: &Path) -> Result<Disk> {
|
||||
if !path.exists() {
|
||||
panic!("Target disk path {} does not exist", path.display());
|
||||
bail!("Target disk path {} does not exist", path.display());
|
||||
}
|
||||
for disk in Disk::probe_all()? {
|
||||
if disk.path() == path {
|
||||
return Ok(disk.clone());
|
||||
}
|
||||
}
|
||||
panic!("installation target {} is not a valid disk", path.display())
|
||||
bail!("installation target {} is not a valid disk", path.display())
|
||||
}
|
||||
|
||||
fn choose_disk() -> Result<Option<Disk>, Box<dyn std::error::Error>> {
|
||||
fn choose_disk() -> Result<Option<Disk>> {
|
||||
let disks = Disk::probe_all()?;
|
||||
if disks.is_empty() {
|
||||
panic!("no disks found.");
|
||||
bail!("no disks found.");
|
||||
}
|
||||
|
||||
loop {
|
||||
@ -99,7 +96,7 @@ fn choose_disk() -> Result<Option<Disk>, Box<dyn std::error::Error>> {
|
||||
}
|
||||
if let Ok(n) = line.parse::<usize>() {
|
||||
if n > 0 && n <= disks.len() {
|
||||
return Ok(Some(disks[n - 1].clone()));
|
||||
return Ok(Some(disks[n-1].clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -114,7 +111,7 @@ fn prompt_choose_disk(disks: &[Disk]) {
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
|
||||
fn read_line() -> Result<String, Box<dyn std::error::Error>> {
|
||||
fn read_line() -> Result<String> {
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)
|
||||
.map_err(context!("error reading line from stdin"))?;
|
||||
@ -128,7 +125,7 @@ fn read_passphrase(prompt: &str) -> io::Result<Option<String>> {
|
||||
loop {
|
||||
println!("{}", prompt);
|
||||
println!();
|
||||
let passphrase = rpassword::read_password_from_tty(Some(" Passphrase : "))?;
|
||||
let passphrase = rpassword::prompt_password(" Passphrase : ")?;
|
||||
if passphrase.is_empty() {
|
||||
println!("Passphrase cannot be empty");
|
||||
continue;
|
||||
@ -136,7 +133,7 @@ fn read_passphrase(prompt: &str) -> io::Result<Option<String>> {
|
||||
if passphrase == "q" || passphrase == "Q" {
|
||||
return Ok(None);
|
||||
}
|
||||
let confirm = rpassword::read_password_from_tty(Some(" Confirm : "))?;
|
||||
let confirm = rpassword::prompt_password(" Confirm : ")?;
|
||||
if confirm == "q" || confirm == "Q" {
|
||||
return Ok(None);
|
||||
}
|
||||
@ -149,7 +146,7 @@ fn read_passphrase(prompt: &str) -> io::Result<Option<String>> {
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_install(disk: &Disk) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
fn confirm_install(disk: &Disk) -> Result<bool> {
|
||||
println!("Are you sure you want to completely erase this this device?");
|
||||
println!();
|
||||
println!(" Device: {}", disk.path().display());
|
||||
@ -161,3 +158,4 @@ fn confirm_install(disk: &Disk) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
let answer = read_line()?;
|
||||
Ok(answer == "YES")
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ use pwhash::sha512_crypt;
|
||||
|
||||
use libcitadel::util;
|
||||
use libcitadel::RealmFS;
|
||||
use libcitadel::Result;
|
||||
use libcitadel::OsRelease;
|
||||
use libcitadel::KeyRing;
|
||||
use libcitadel::terminal::Base16Scheme;
|
||||
@ -182,7 +183,7 @@ impl Installer {
|
||||
self.install_syslinux = val;
|
||||
}
|
||||
|
||||
pub fn verify(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn verify(&self) -> Result<()> {
|
||||
let kernel_img = self.kernel_imagename();
|
||||
let bzimage = format!("bzImage-{}", self.kernel_version());
|
||||
let artifacts = vec![
|
||||
@ -191,29 +192,26 @@ impl Installer {
|
||||
];
|
||||
|
||||
if !self.target().exists() {
|
||||
panic!("target device {:?} does not exist", self.target());
|
||||
bail!("target device {:?} does not exist", self.target());
|
||||
}
|
||||
|
||||
for a in artifacts {
|
||||
if !self.artifact_path(a).exists() {
|
||||
panic!(
|
||||
"required install artifact {} does not exist in {}",
|
||||
a, self.artifact_directory
|
||||
);
|
||||
bail!("required install artifact {} does not exist in {}", a, self.artifact_directory);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn run(&self) -> Result<()> {
|
||||
match self._type {
|
||||
InstallType::Install => self.run_install(),
|
||||
InstallType::LiveSetup => self.run_live_setup(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_install(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn run_install(&self) -> Result<()> {
|
||||
let start = Instant::now();
|
||||
self.partition_disk()?;
|
||||
self.setup_luks()?;
|
||||
@ -226,7 +224,7 @@ impl Installer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_live_setup(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn run_live_setup(&self) -> Result<()> {
|
||||
self.cmd_list(&[
|
||||
"/bin/mount -t tmpfs var-tmpfs /sysroot/var",
|
||||
"/bin/mount -t tmpfs home-tmpfs /sysroot/home",
|
||||
@ -244,7 +242,8 @@ impl Installer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_live_realm(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn setup_live_realm(&self) -> Result<()> {
|
||||
|
||||
let realmfs_dir = self.storage().join("realms/realmfs-images");
|
||||
let base_realmfs = realmfs_dir.join("base-realmfs.img");
|
||||
|
||||
@ -262,14 +261,14 @@ impl Installer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn partition_disk(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn partition_disk(&self) -> Result<()> {
|
||||
self.header("Partitioning target disk")?;
|
||||
self.cmd_list(PARTITION_COMMANDS, &[
|
||||
("$TARGET", self.target_str())
|
||||
])
|
||||
}
|
||||
|
||||
pub fn setup_luks(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn setup_luks(&self) -> Result<()> {
|
||||
self.header("Setting up LUKS disk encryption")?;
|
||||
util::create_dir(INSTALL_MOUNT)?;
|
||||
util::write_file(LUKS_PASSPHRASE_FILE, self.passphrase().as_bytes())?;
|
||||
@ -282,16 +281,15 @@ impl Installer {
|
||||
("$LUKS_PASSFILE", LUKS_PASSPHRASE_FILE),
|
||||
])?;
|
||||
|
||||
util::remove_file(LUKS_PASSPHRASE_FILE)?;
|
||||
Ok(())
|
||||
util::remove_file(LUKS_PASSPHRASE_FILE)
|
||||
}
|
||||
|
||||
pub fn setup_lvm(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn setup_lvm(&self) -> Result<()> {
|
||||
self.header("Setting up LVM volumes")?;
|
||||
self.cmd_list(LVM_COMMANDS, &[])
|
||||
}
|
||||
|
||||
pub fn setup_boot(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn setup_boot(&self) -> Result<()> {
|
||||
self.header("Setting up /boot partition")?;
|
||||
let boot_partition = self.target_partition(1);
|
||||
self.cmd(format!("/sbin/mkfs.vfat -F 32 {}", boot_partition))?;
|
||||
@ -325,11 +323,11 @@ impl Installer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_syslinux(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn setup_syslinux(&self) -> Result<()> {
|
||||
self.header("Installing syslinux")?;
|
||||
let syslinux_src = self.artifact_path("syslinux");
|
||||
if !syslinux_src.exists() {
|
||||
panic!("no syslinux directory found in artifact directory, cannot install syslinux");
|
||||
bail!("no syslinux directory found in artifact directory, cannot install syslinux");
|
||||
}
|
||||
let dst = Path::new(INSTALL_MOUNT).join("syslinux");
|
||||
util::create_dir(&dst)?;
|
||||
@ -347,17 +345,17 @@ impl Installer {
|
||||
self.cmd(format!("/sbin/extlinux --install {}", dst.display()))
|
||||
}
|
||||
|
||||
fn setup_syslinux_post_umount(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn setup_syslinux_post_umount(&self) -> Result<()> {
|
||||
let mbrbin = self.artifact_path("syslinux/gptmbr.bin");
|
||||
if !mbrbin.exists() {
|
||||
panic!("could not find MBR image: {:?}", mbrbin);
|
||||
bail!("could not find MBR image: {:?}", mbrbin);
|
||||
}
|
||||
self.cmd(format!("/bin/dd bs=440 count=1 conv=notrunc if={} of={}", mbrbin.display(), self.target().display()))?;
|
||||
self.cmd(format!("/sbin/parted -s {} set 1 legacy_boot on", self.target_str()))
|
||||
|
||||
}
|
||||
|
||||
pub fn create_storage(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn create_storage(&self) -> Result<()> {
|
||||
self.header("Setting up /storage partition")?;
|
||||
|
||||
self.cmd_list(CREATE_STORAGE_COMMANDS,
|
||||
@ -367,7 +365,7 @@ impl Installer {
|
||||
self.cmd(format!("/bin/umount {}", INSTALL_MOUNT))
|
||||
}
|
||||
|
||||
fn setup_storage(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn setup_storage(&self) -> Result<()> {
|
||||
if self._type == InstallType::Install {
|
||||
self.create_keyring()?;
|
||||
self.setup_storage_resources()?;
|
||||
@ -391,33 +389,33 @@ impl Installer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_keyring(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn create_keyring(&self) -> Result<()> {
|
||||
self.info("Creating initial keyring")?;
|
||||
let keyring = KeyRing::create_new();
|
||||
Ok(keyring.write(self.storage().join("keyring"), self.passphrase.as_ref().unwrap())?)
|
||||
keyring.write(self.storage().join("keyring"), self.passphrase.as_ref().unwrap())
|
||||
}
|
||||
|
||||
fn setup_base_realmfs(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
fn setup_base_realmfs(&self) -> Result<()> {
|
||||
let realmfs_dir = self.storage().join("realms/realmfs-images");
|
||||
util::create_dir(&realmfs_dir)?;
|
||||
self.sparse_copy_artifact("base-realmfs.img", &realmfs_dir)?;
|
||||
self.cmd(format!("/usr/bin/citadel-image decompress {}/base-realmfs.img", realmfs_dir.display()))
|
||||
}
|
||||
|
||||
fn setup_realm_skel(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn setup_realm_skel(&self) -> Result<()> {
|
||||
let realm_skel = self.storage().join("realms/skel");
|
||||
util::create_dir(&realm_skel)?;
|
||||
util::copy_tree_with_chown(&self.skel(), &realm_skel, (1000, 1000))?;
|
||||
Ok(())
|
||||
util::copy_tree_with_chown(&self.skel(), &realm_skel, (1000,1000))
|
||||
}
|
||||
|
||||
fn create_realmlock(&self, dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn create_realmlock(&self, dir: &Path) -> Result<()> {
|
||||
fs::File::create(dir.join(".realmlock"))
|
||||
.map_err(context!("failed to create {:?}/.realmlock file", dir))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_main_realm(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn setup_main_realm(&self) -> Result<()> {
|
||||
self.header("Creating main realm")?;
|
||||
|
||||
let realm = self.storage().join("realms/realm-main");
|
||||
@ -442,7 +440,7 @@ impl Installer {
|
||||
self.create_realmlock(&realm)
|
||||
}
|
||||
|
||||
fn setup_apt_cacher_realm(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn setup_apt_cacher_realm(&self) -> Result<()> {
|
||||
self.header("Creating apt-cacher realm")?;
|
||||
let realm_base = self.storage().join("realms/realm-apt-cacher");
|
||||
|
||||
@ -462,7 +460,7 @@ impl Installer {
|
||||
self.create_realmlock(&realm_base)
|
||||
}
|
||||
|
||||
fn setup_storage_resources(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn setup_storage_resources(&self) -> Result<()> {
|
||||
let channel = match OsRelease::citadel_channel() {
|
||||
Some(channel) => channel,
|
||||
None => "dev",
|
||||
@ -476,7 +474,7 @@ impl Installer {
|
||||
self.sparse_copy_artifact(&kernel_img, &resources)
|
||||
}
|
||||
|
||||
fn setup_citadel_passphrase(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn setup_citadel_passphrase(&self) -> Result<()> {
|
||||
if self._type == InstallType::LiveSetup {
|
||||
self.info("Creating temporary citadel passphrase file for live mode")?;
|
||||
let path = self.storage().join("citadel-state/passwd");
|
||||
@ -499,15 +497,17 @@ impl Installer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_rootfs_partitions(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn install_rootfs_partitions(&self) -> Result<()> {
|
||||
self.header("Installing rootfs partitions")?;
|
||||
let rootfs = self.artifact_path("citadel-rootfs.img");
|
||||
self.cmd(format!("/usr/bin/citadel-image install-rootfs --skip-sha {}", rootfs.display()))?;
|
||||
self.cmd(format!("/usr/bin/citadel-image install-rootfs --skip-sha --no-prefer {}", rootfs.display()))
|
||||
}
|
||||
|
||||
pub fn finish_install(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.cmd_list(FINISH_COMMANDS, &[("$TARGET", self.target_str())])
|
||||
pub fn finish_install(&self) -> Result<()> {
|
||||
self.cmd_list(FINISH_COMMANDS, &[
|
||||
("$TARGET", self.target_str())
|
||||
])
|
||||
}
|
||||
|
||||
fn global_realm_config(&self) -> &str {
|
||||
@ -546,33 +546,16 @@ impl Installer {
|
||||
Path::new(&self.artifact_directory).join(filename)
|
||||
}
|
||||
|
||||
fn copy_artifact<P: AsRef<Path>>(
|
||||
&self,
|
||||
filename: &str,
|
||||
target: P,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn copy_artifact<P: AsRef<Path>>(&self, filename: &str, target: P) -> Result<()> {
|
||||
self._copy_artifact(filename, target, false)
|
||||
}
|
||||
|
||||
fn sparse_copy_artifact<P: AsRef<Path>>(
|
||||
&self,
|
||||
filename: &str,
|
||||
target: P,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn sparse_copy_artifact<P: AsRef<Path>>(&self, filename: &str, target: P) -> Result<()> {
|
||||
self._copy_artifact(filename, target, true)
|
||||
}
|
||||
|
||||
fn _copy_artifact<P: AsRef<Path>>(
|
||||
&self,
|
||||
filename: &str,
|
||||
target: P,
|
||||
sparse: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.info(format!(
|
||||
"Copying {} to {}",
|
||||
filename,
|
||||
target.as_ref().display()
|
||||
))?;
|
||||
fn _copy_artifact<P: AsRef<Path>>(&self, filename: &str, target: P, sparse: bool) -> Result<()> {
|
||||
self.info(format!("Copying {} to {}", filename, target.as_ref().display()))?;
|
||||
let src = self.artifact_path(filename);
|
||||
let target = target.as_ref();
|
||||
util::create_dir(target)?;
|
||||
@ -585,19 +568,20 @@ impl Installer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn header<S: AsRef<str>>(&self, s: S) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn header<S: AsRef<str>>(&self, s: S) -> Result<()> {
|
||||
self.output(format!("\n[+] {}\n", s.as_ref()))
|
||||
}
|
||||
|
||||
fn info<S: AsRef<str>>(&self, s: S) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn info<S: AsRef<str>>(&self, s: S) -> Result<()> {
|
||||
self.output(format!(" [>] {}", s.as_ref()))
|
||||
}
|
||||
|
||||
fn output<S: AsRef<str>>(&self, s: S) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.write_output(s.as_ref())
|
||||
|
||||
fn output<S: AsRef<str>>(&self, s: S) -> Result<()> {
|
||||
self.write_output(s.as_ref()).map_err(context!("error writing output"))
|
||||
}
|
||||
|
||||
fn write_output(&self, s: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn write_output(&self, s: &str) -> io::Result<()> {
|
||||
println!("{}", s);
|
||||
io::stdout().flush()?;
|
||||
|
||||
@ -608,11 +592,7 @@ impl Installer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_list<I: IntoIterator<Item = S>, S: AsRef<str>>(
|
||||
&self,
|
||||
cmd_lines: I,
|
||||
subs: &[(&str, &str)],
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn cmd_list<I: IntoIterator<Item=S>, S: AsRef<str>>(&self, cmd_lines: I, subs: &[(&str,&str)]) -> Result<()> {
|
||||
for line in cmd_lines {
|
||||
let line = line.as_ref();
|
||||
let line = subs.iter().fold(line.to_string(), |acc, (from,to)| acc.replace(from,to));
|
||||
@ -622,12 +602,12 @@ impl Installer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd<S: AsRef<str>>(&self, args: S) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn cmd<S: AsRef<str>>(&self, args: S) -> Result<()> {
|
||||
let args: Vec<&str> = args.as_ref().split_whitespace().collect::<Vec<_>>();
|
||||
self.run_cmd(args, false)
|
||||
}
|
||||
|
||||
fn run_cmd(&self, args: Vec<&str>, as_user: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn run_cmd(&self, args: Vec<&str>, as_user: bool) -> Result<()> {
|
||||
self.output(format!(" # {}", args.join(" ")))?;
|
||||
|
||||
let mut command = Command::new(args[0]);
|
||||
@ -652,8 +632,8 @@ impl Installer {
|
||||
|
||||
if !result.status.success() {
|
||||
match result.status.code() {
|
||||
Some(code) => panic!("command {} failed with exit code: {}", args[0], code),
|
||||
None => panic!("command {} failed with no exit code", args[0]),
|
||||
Some(code) => bail!("command {} failed with exit code: {}", args[0], code),
|
||||
None => bail!("command {} failed with no exit code", args[0]),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -4,7 +4,7 @@ pub(crate) mod installer;
|
||||
mod cli;
|
||||
mod disk;
|
||||
|
||||
pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn main(args: Vec<String>) {
|
||||
let mut args = args.iter().skip(1);
|
||||
let result = if let Some(dev) = args.next() {
|
||||
cli::run_cli_install_with(dev)
|
||||
@ -17,11 +17,10 @@ pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err(ref err) => {
|
||||
println!("Install failed: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
},
|
||||
};
|
||||
if !ok {
|
||||
println!("Install cancelled...");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ use std::sync::mpsc::{Sender};
|
||||
use dbus::tree::{self, Factory, MTFn, MethodResult, Tree};
|
||||
use dbus::{Message};
|
||||
use dbus::blocking::LocalConnection;
|
||||
use libcitadel::{Result};
|
||||
// Use local version of disk.rs since we added some methods
|
||||
use crate::install_backend::disk::*;
|
||||
use crate::install::installer::*;
|
||||
@ -14,6 +15,7 @@ use std::fmt;
|
||||
|
||||
type MethodInfo<'a> = tree::MethodInfo<'a, MTFn<TData>, TData>;
|
||||
|
||||
|
||||
const OBJECT_PATH: &str = "/com/subgraph/installer";
|
||||
const INTERFACE_NAME: &str = "com.subgraph.installer.Manager";
|
||||
const BUS_NAME: &str = "com.subgraph.installer";
|
||||
@ -35,7 +37,8 @@ pub struct DbusServer {
|
||||
}
|
||||
|
||||
impl DbusServer {
|
||||
pub fn connect() -> Result<DbusServer, Box<dyn std::error::Error>> {
|
||||
|
||||
pub fn connect() -> Result<DbusServer> {
|
||||
let connection = LocalConnection::new_system()
|
||||
.map_err(|e| format_err!("Failed to connect to DBUS system bus: {}", e))?;
|
||||
let connection = Arc::new(connection);
|
||||
@ -77,7 +80,7 @@ impl DbusServer {
|
||||
Ok(vec![m.msg.method_return().append1(list)])
|
||||
}
|
||||
|
||||
fn run_install(path: String, citadel_passphrase: String, luks_passphrase: String, sender: Sender<Msg>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn run_install(path: String, citadel_passphrase: String, luks_passphrase: String, sender: Sender<Msg>) -> Result<()> {
|
||||
let mut install = Installer::new(path, &citadel_passphrase, &luks_passphrase);
|
||||
install.set_install_syslinux(true);
|
||||
install.verify()?;
|
||||
@ -171,12 +174,12 @@ impl DbusServer {
|
||||
Message::signal(&path, &iface, &member).append1(text)
|
||||
}
|
||||
|
||||
pub fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (sender, receiver) = mpsc::channel::<Msg>();
|
||||
pub fn start(&self) -> Result<()> {
|
||||
let (sender, receiver) = mpsc::channel::<Msg>();
|
||||
let sender_clone = sender.clone();
|
||||
let tree = self.build_tree(sender);
|
||||
if let Err(_err) = self.connection.request_name(BUS_NAME, false, true, false) {
|
||||
panic!("Failed to request name");
|
||||
bail!("Failed to request name");
|
||||
}
|
||||
|
||||
tree.start_receive(self.connection.as_ref());
|
||||
@ -228,6 +231,7 @@ impl DbusServer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -239,6 +243,7 @@ impl TreeData {
|
||||
TreeData {}
|
||||
}
|
||||
|
||||
|
||||
fn disks(&self) -> HashMap<String, Vec<String>> {
|
||||
let disks = Disk::probe_all().unwrap();
|
||||
|
||||
@ -252,6 +257,7 @@ impl TreeData {
|
||||
}
|
||||
disk_map
|
||||
}
|
||||
|
||||
}
|
||||
impl fmt::Debug for TreeData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
|
@ -1,20 +1,24 @@
|
||||
use libcitadel::Result;
|
||||
use std::process::exit;
|
||||
|
||||
mod disk;
|
||||
mod dbus;
|
||||
use libcitadel::CommandLine;
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn main() {
|
||||
if CommandLine::install_mode() {
|
||||
run_dbus_server()
|
||||
if let Err(e) = run_dbus_server() {
|
||||
warn!("Error: {}", e);
|
||||
}
|
||||
} else {
|
||||
println!("Citadel installer backend will only run in install or live mode");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_dbus_server() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn run_dbus_server() -> Result<()> {
|
||||
let server = dbus::DbusServer::connect()?;
|
||||
server.start()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -16,75 +16,72 @@ mod mkimage;
|
||||
mod realmfs;
|
||||
mod sync;
|
||||
mod update;
|
||||
mod fetch;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let exe = env::current_exe()?;
|
||||
fn main() {
|
||||
let exe = match env::current_exe() {
|
||||
Ok(path) => path,
|
||||
Err(_e) => {
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
let args = env::args().collect::<Vec<String>>();
|
||||
|
||||
if exe == Path::new("/usr/libexec/citadel-boot") {
|
||||
boot::main(args)
|
||||
boot::main(args);
|
||||
} else if exe == Path::new("/usr/libexec/citadel-install") {
|
||||
install::main(args)
|
||||
install::main(args);
|
||||
} else if exe == Path::new("/usr/libexec/citadel-install-backend") {
|
||||
install_backend::main()
|
||||
install_backend::main();
|
||||
} else if exe == Path::new("/usr/bin/citadel-image") {
|
||||
image::main(args)
|
||||
image::main();
|
||||
} else if exe == Path::new("/usr/bin/citadel-realmfs") {
|
||||
realmfs::main(args)
|
||||
realmfs::main();
|
||||
} else if exe == Path::new("/usr/bin/citadel-update") {
|
||||
update::main(args)
|
||||
update::main(args);
|
||||
} else if exe == Path::new("/usr/bin/citadel-fetch") {
|
||||
fetch::main();
|
||||
} else if exe == Path::new("/usr/libexec/citadel-desktop-sync") {
|
||||
sync::main(args)
|
||||
sync::main(args);
|
||||
} else if exe == Path::new("/usr/libexec/citadel-run") {
|
||||
do_citadel_run(args)
|
||||
do_citadel_run(args);
|
||||
} else if exe.file_name() == Some(OsStr::new("citadel-mkimage")) {
|
||||
mkimage::main(args)
|
||||
mkimage::main(args);
|
||||
} else if exe.file_name() == Some(OsStr::new("citadel-tool")) {
|
||||
dispatch_command(args)
|
||||
dispatch_command(args);
|
||||
} else {
|
||||
Result::Err(format!("Error: unknown executable {}", exe.display()).into())
|
||||
println!("Error: unknown executable {}", exe.display());
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_command(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn dispatch_command(args: Vec<String>) {
|
||||
if let Some(command) = args.get(1) {
|
||||
match command.as_str() {
|
||||
"boot" => boot::main(rebuild_args("citadel-boot", args)),
|
||||
"install" => install::main(rebuild_args("citadel-install", args)),
|
||||
"image" => image::main(rebuild_args("citadel-image", args)),
|
||||
"realmfs" => realmfs::main(rebuild_args("citadel-realmfs", args)),
|
||||
"image" => image::main(),
|
||||
"realmfs" => realmfs::main(),
|
||||
"update" => update::main(rebuild_args("citadel-update", args)),
|
||||
"mkimage" => mkimage::main(rebuild_args("citadel-mkimage", args)),
|
||||
"sync" => sync::main(rebuild_args("citadel-desktop-sync", args)),
|
||||
"run" => do_citadel_run(rebuild_args("citadel-run", args)),
|
||||
_ => throw_err(command),
|
||||
_ => println!("Error: unknown command {}", command),
|
||||
}
|
||||
} else {
|
||||
Result::Err("Must provide an argument".into())
|
||||
println!("Must provide an argument");
|
||||
}
|
||||
}
|
||||
|
||||
fn throw_err(command: &String) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Result::Err(format!("Error: unknown command {}", command).into())
|
||||
}
|
||||
|
||||
fn rebuild_args(command: &str, args: Vec<String>) -> Vec<String> {
|
||||
iter::once(command.to_string())
|
||||
.chain(args.into_iter().skip(2))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn do_citadel_run(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn do_citadel_run(args: Vec<String>) {
|
||||
if let Err(e) = RealmManager::run_in_current(&args[1..], true) {
|
||||
return Result::Err(
|
||||
format!(
|
||||
"RealmManager::run_in_current({:?}) failed: {}",
|
||||
&args[1..],
|
||||
e
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
println!("RealmManager::run_in_current({:?}) failed: {}", &args[1..], e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -38,15 +38,15 @@ impl UpdateBuilder {
|
||||
}
|
||||
|
||||
fn target_filename(&self) -> String {
|
||||
format!("citadel-{}-{}-{:03}.img", self.config.img_name(), self.config.channel(), self.config.version())
|
||||
format!("citadel-{}-{}-{:}.img", self.config.img_name(), self.config.channel(), self.config.version())
|
||||
}
|
||||
|
||||
fn build_filename(config: &BuildConfig) -> String {
|
||||
format!("citadel-{}-{}-{:03}", config.image_type(), config.channel(), config.version())
|
||||
format!("citadel-{}-{}-{}", config.image_type(), config.channel(), config.version())
|
||||
}
|
||||
|
||||
fn verity_filename(&self) -> String {
|
||||
format!("verity-hash-{}-{:03}", self.config.image_type(), self.config.version())
|
||||
format!("verity-hash-{}-{}", self.config.image_type(), self.config.version())
|
||||
}
|
||||
|
||||
pub fn build(&mut self) -> Result<()> {
|
||||
@ -154,7 +154,7 @@ impl UpdateBuilder {
|
||||
bail!("failed to compress {:?}: {}", self.image(), err);
|
||||
}
|
||||
// Rename back to original image_data filename
|
||||
util::rename(self.image().with_extension("xz"), self.image())?;
|
||||
util::rename(util::append_to_path(self.image(), ".xz"), self.image())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -217,7 +217,7 @@ impl UpdateBuilder {
|
||||
writeln!(v, "realmfs-name = \"{}\"", name)?;
|
||||
}
|
||||
writeln!(v, "channel = \"{}\"", self.config.channel())?;
|
||||
writeln!(v, "version = {}", self.config.version())?;
|
||||
writeln!(v, "version = \"{}\"", self.config.version())?;
|
||||
writeln!(v, "timestamp = \"{}\"", self.config.timestamp())?;
|
||||
writeln!(v, "nblocks = {}", self.nblocks.unwrap())?;
|
||||
writeln!(v, "shasum = \"{}\"", self.shasum.as_ref().unwrap())?;
|
||||
|
@ -9,7 +9,7 @@ pub struct BuildConfig {
|
||||
#[serde(rename = "image-type")]
|
||||
image_type: String,
|
||||
channel: String,
|
||||
version: usize,
|
||||
version: String,
|
||||
timestamp: String,
|
||||
source: String,
|
||||
#[serde(default)]
|
||||
@ -102,8 +102,8 @@ impl BuildConfig {
|
||||
self.realmfs_name.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
pub fn version(&self) -> usize {
|
||||
self.version
|
||||
pub fn version(&self) -> &str {
|
||||
&self.version
|
||||
}
|
||||
|
||||
pub fn channel(&self) -> &str {
|
||||
|
@ -1,10 +1,13 @@
|
||||
|
||||
use std::process::exit;
|
||||
|
||||
use libcitadel::Result;
|
||||
|
||||
mod config;
|
||||
mod build;
|
||||
|
||||
pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn main(args: Vec<String>) {
|
||||
|
||||
let config_path = match args.get(1) {
|
||||
Some(arg) => arg,
|
||||
None => {
|
||||
@ -18,11 +21,11 @@ pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
}
|
||||
|
||||
fn build_image(config_path: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn build_image(config_path: &str) -> Result<()> {
|
||||
let conf = config::BuildConfig::load(config_path)?;
|
||||
let mut builder = build::UpdateBuilder::new(conf);
|
||||
Ok(builder.build()?)
|
||||
}
|
||||
builder.build()
|
||||
}
|
@ -1,26 +1,24 @@
|
||||
use clap::App;
|
||||
use clap::ArgMatches;
|
||||
use clap::{command, Command};
|
||||
use clap::{Arg, ArgMatches};
|
||||
|
||||
use libcitadel::{RealmFS, Logger, LogLevel};
|
||||
use libcitadel::{Result,RealmFS,Logger,LogLevel};
|
||||
use libcitadel::util::is_euid_root;
|
||||
use clap::SubCommand;
|
||||
use clap::AppSettings::*;
|
||||
use clap::Arg;
|
||||
use libcitadel::ResizeSize;
|
||||
use std::process::exit;
|
||||
|
||||
pub fn main() {
|
||||
|
||||
pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Logger::set_log_level(LogLevel::Debug);
|
||||
|
||||
let app = App::new("citadel-realmfs")
|
||||
.about("Citadel realmfs image tool")
|
||||
.settings(&[ArgRequiredElseHelp,ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder,SubcommandsNegateReqs])
|
||||
|
||||
.subcommand(SubCommand::with_name("resize")
|
||||
let matches = command!()
|
||||
.about("citadel-realmfs")
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(Command::new("resize")
|
||||
.about("Resize an existing RealmFS image. If the image is currently sealed, it will also be unsealed.")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image to resize")
|
||||
.required(true))
|
||||
.arg(Arg::with_name("size")
|
||||
.arg(Arg::new("size")
|
||||
.help("Size to increase RealmFS image to (or by if prefixed with '+')")
|
||||
.long_help("\
|
||||
The size can be followed by a 'g' or 'm' character \
|
||||
@ -33,63 +31,66 @@ is the final absolute size of the image.")
|
||||
.required(true)))
|
||||
|
||||
|
||||
.subcommand(SubCommand::with_name("fork")
|
||||
.subcommand(Command::new("fork")
|
||||
.about("Create a new RealmFS image as an unsealed copy of an existing image")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image to fork")
|
||||
.required(true))
|
||||
|
||||
.arg(Arg::with_name("forkname")
|
||||
.arg(Arg::new("forkname")
|
||||
.help("Name of new image to create")
|
||||
.required(true)))
|
||||
|
||||
.subcommand(SubCommand::with_name("autoresize")
|
||||
.subcommand(Command::new("autoresize")
|
||||
.about("Increase size of RealmFS image if not enough free space remains")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image")
|
||||
.required(true)))
|
||||
|
||||
.subcommand(SubCommand::with_name("update")
|
||||
.subcommand(Command::new("update")
|
||||
.about("Open an update shell on the image")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image")
|
||||
.required(true)))
|
||||
|
||||
.subcommand(SubCommand::with_name("activate")
|
||||
.subcommand(Command::new("activate")
|
||||
.about("Activate a RealmFS by creating a block device for the image and mounting it.")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image to activate")
|
||||
.required(true)))
|
||||
|
||||
.subcommand(SubCommand::with_name("deactivate")
|
||||
.subcommand(Command::new("deactivate")
|
||||
.about("Deactivate a RealmFS by unmounting it and removing block device created during activation.")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image to deactivate")
|
||||
.required(true)))
|
||||
|
||||
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Name of or path to RealmFS image to display information about")
|
||||
.required(true));
|
||||
.required(true))
|
||||
.get_matches();
|
||||
|
||||
let matches = app.get_matches_from(args);
|
||||
let result = match matches.subcommand() {
|
||||
("resize", Some(m)) => resize(m),
|
||||
("autoresize", Some(m)) => autoresize(m),
|
||||
("fork", Some(m)) => fork(m),
|
||||
("update", Some(m)) => update(m),
|
||||
("activate", Some(m)) => activate(m),
|
||||
("deactivate", Some(m)) => deactivate(m),
|
||||
Some(("resize", m)) => resize(m),
|
||||
Some(("autoresize", m)) => autoresize(m),
|
||||
Some(("fork", m)) => fork(m),
|
||||
Some(("update", m)) => update(m),
|
||||
Some(("activate", m)) => activate(m),
|
||||
Some(("deactivate", m)) => deactivate(m),
|
||||
_ => image_info(&matches),
|
||||
}?;
|
||||
};
|
||||
|
||||
Ok(())
|
||||
if let Err(ref e) = result {
|
||||
eprintln!("Error: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn realmfs_image(arg_matches: &ArgMatches) -> Result<RealmFS, Box<dyn std::error::Error>> {
|
||||
let image = match arg_matches.value_of("image") {
|
||||
fn realmfs_image(arg_matches: &ArgMatches) -> Result<RealmFS> {
|
||||
let image = match arg_matches.get_one::<String>("image") {
|
||||
Some(s) => s,
|
||||
None => panic!("Image argument required."),
|
||||
None => bail!("Image argument required."),
|
||||
};
|
||||
|
||||
let realmfs = if RealmFS::is_valid_name(image) {
|
||||
@ -97,45 +98,41 @@ fn realmfs_image(arg_matches: &ArgMatches) -> Result<RealmFS, Box<dyn std::error
|
||||
} else if RealmFS::is_valid_realmfs_image(image) {
|
||||
RealmFS::load_from_path(image)?
|
||||
} else {
|
||||
panic!(
|
||||
"Not a valid realmfs name or path to realmfs image file: {}",
|
||||
image
|
||||
);
|
||||
bail!("Not a valid realmfs name or path to realmfs image file: {}", image);
|
||||
};
|
||||
Ok(realmfs)
|
||||
}
|
||||
|
||||
fn image_info(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn image_info(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
print!("{}", String::from_utf8(img.header().metainfo_bytes())?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_resize_size(s: &str) -> Result<ResizeSize, Box<dyn std::error::Error>> {
|
||||
fn parse_resize_size(s: &str) -> Result<ResizeSize> {
|
||||
let unit = s.chars().last().filter(|c| c.is_alphabetic());
|
||||
|
||||
let skip = if s.starts_with('+') { 1 } else { 0 };
|
||||
let size = s
|
||||
.chars()
|
||||
let size = s.chars()
|
||||
.skip(skip)
|
||||
.take_while(|c| c.is_numeric())
|
||||
.collect::<String>()
|
||||
.parse::<usize>()
|
||||
.map_err(|_| format_err!("Unable to parse size value '{}'", s))?;
|
||||
.map_err(|_| format_err!("Unable to parse size value '{}'",s))?;
|
||||
|
||||
let sz = match unit {
|
||||
Some('g') | Some('G') => ResizeSize::gigs(size),
|
||||
Some('m') | Some('M') => ResizeSize::megs(size),
|
||||
Some(c) => panic!("Unknown size unit '{}'", c),
|
||||
Some(c) => bail!("Unknown size unit '{}'", c),
|
||||
None => ResizeSize::blocks(size),
|
||||
};
|
||||
Ok(sz)
|
||||
}
|
||||
|
||||
fn resize(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn resize(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
info!("image is {}", img.path().display());
|
||||
let size_arg = match arg_matches.value_of("size") {
|
||||
let size_arg = match arg_matches.get_one::<String>("size") {
|
||||
Some(size) => size,
|
||||
None => "No size argument",
|
||||
};
|
||||
@ -144,55 +141,51 @@ fn resize(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let size = parse_resize_size(size_arg)?;
|
||||
|
||||
if mode_add {
|
||||
img.resize_grow_by(size)?;
|
||||
img.resize_grow_by(size)
|
||||
} else {
|
||||
img.resize_grow_to(size)?;
|
||||
img.resize_grow_to(size)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn autoresize(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn autoresize(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
|
||||
if let Some(size) = img.auto_resize_size() {
|
||||
img.resize_grow_to(size)?;
|
||||
img.resize_grow_to(size)
|
||||
} else {
|
||||
info!(
|
||||
"RealmFS image {} has sufficient free space, doing nothing",
|
||||
img.path().display()
|
||||
);
|
||||
info!("RealmFS image {} has sufficient free space, doing nothing", img.path().display());
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fork(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn fork(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
let forkname = match arg_matches.value_of("forkname") {
|
||||
let forkname = match arg_matches.get_one::<String>("forkname") {
|
||||
Some(name) => name,
|
||||
None => panic!("No fork name argument"),
|
||||
None => bail!("No fork name argument"),
|
||||
};
|
||||
if !RealmFS::is_valid_name(forkname) {
|
||||
panic!("Not a valid RealmFS image name '{}'", forkname);
|
||||
bail!("Not a valid RealmFS image name '{}'", forkname);
|
||||
}
|
||||
if RealmFS::named_image_exists(forkname) {
|
||||
panic!("A RealmFS image named '{}' already exists", forkname);
|
||||
bail!("A RealmFS image named '{}' already exists", forkname);
|
||||
}
|
||||
img.fork(forkname)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn update(arg_matches: &ArgMatches) -> Result<()> {
|
||||
if !is_euid_root() {
|
||||
panic!("RealmFS updates must be run as root");
|
||||
bail!("RealmFS updates must be run as root");
|
||||
}
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
img.interactive_update(Some("icy"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn activate(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn activate(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
let img_arg = arg_matches.value_of("image").unwrap();
|
||||
let img_arg = arg_matches.get_one::<String>("image").unwrap();
|
||||
|
||||
if img.is_activated() {
|
||||
info!("RealmFS image {} is already activated", img_arg);
|
||||
@ -203,9 +196,9 @@ fn activate(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn deactivate(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn deactivate(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
let img_arg = arg_matches.value_of("image").unwrap();
|
||||
let img_arg = arg_matches.get_one::<String>("image").unwrap();
|
||||
if !img.is_activated() {
|
||||
info!("RealmFS image {} is not activated", img_arg);
|
||||
} else if img.is_in_use() {
|
||||
|
@ -1,9 +1,9 @@
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::{Path,PathBuf};
|
||||
use std::time::SystemTime;
|
||||
|
||||
use libcitadel::{Realm, Realms, util};
|
||||
use libcitadel::{Realm, Realms, Result, util};
|
||||
use crate::sync::parser::DesktopFileParser;
|
||||
use std::fs::DirEntry;
|
||||
use crate::sync::desktop_file::DesktopFile;
|
||||
@ -17,13 +17,14 @@ pub struct DesktopFileSync {
|
||||
icons: Option<IconSync>,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash)]
|
||||
#[derive(Eq,PartialEq,Hash)]
|
||||
struct DesktopItem {
|
||||
path: PathBuf,
|
||||
mtime: SystemTime,
|
||||
}
|
||||
|
||||
impl DesktopItem {
|
||||
|
||||
fn new(path: PathBuf, mtime: SystemTime) -> Self {
|
||||
DesktopItem { path, mtime }
|
||||
}
|
||||
@ -45,7 +46,7 @@ impl DesktopItem {
|
||||
impl DesktopFileSync {
|
||||
pub const CITADEL_APPLICATIONS: &'static str = "/home/citadel/.local/share/applications";
|
||||
|
||||
pub fn sync_active_realms() -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn sync_active_realms() -> Result<()> {
|
||||
let realms = Realms::load()?;
|
||||
for realm in realms.active(true) {
|
||||
let mut sync = DesktopFileSync::new(realm);
|
||||
@ -71,7 +72,7 @@ impl DesktopFileSync {
|
||||
DesktopFileSync { realm, items: HashSet::new(), icons }
|
||||
}
|
||||
|
||||
pub fn run_sync(&mut self, clear: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn run_sync(&mut self, clear: bool) -> Result<()> {
|
||||
|
||||
IconSync::ensure_theme_index_exists()?;
|
||||
|
||||
@ -97,8 +98,8 @@ impl DesktopFileSync {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_source_files(&mut self, directory: impl AsRef<Path>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut directory = Realms::current_realm_symlink().join(directory.as_ref());
|
||||
fn collect_source_files(&mut self, directory: impl AsRef<Path>) -> Result<()> {
|
||||
let mut directory = self.realm.run_path().join(directory.as_ref());
|
||||
directory.push("share/applications");
|
||||
if directory.exists() {
|
||||
util::read_directory(&directory, |dent| {
|
||||
@ -118,16 +119,20 @@ impl DesktopFileSync {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_target_files() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(util::read_directory(Self::CITADEL_APPLICATIONS, |dent| {
|
||||
pub fn clear_target_files() -> Result<()> {
|
||||
util::read_directory(Self::CITADEL_APPLICATIONS, |dent| {
|
||||
util::remove_file(dent.path())
|
||||
})?)
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_missing_target_files(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let sources = self.source_filenames();
|
||||
fn remove_missing_target_files(&mut self) -> Result<()> {
|
||||
let mut sources = self.source_filenames();
|
||||
// If flatpak is enabled, don't remove the generated GNOME Software desktop file
|
||||
if self.realm.config().flatpak() {
|
||||
sources.insert(format!("realm-{}.org.gnome.Software.desktop", self.realm.name()));
|
||||
}
|
||||
let prefix = format!("realm-{}.", self.realm.name());
|
||||
Ok(util::read_directory(Self::CITADEL_APPLICATIONS, |dent| {
|
||||
util::read_directory(Self::CITADEL_APPLICATIONS, |dent| {
|
||||
if let Some(filename) = dent.file_name().to_str() {
|
||||
if filename.starts_with(&prefix) && !sources.contains(filename) {
|
||||
let path = dent.path();
|
||||
@ -136,7 +141,7 @@ impl DesktopFileSync {
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?)
|
||||
})
|
||||
}
|
||||
|
||||
fn mtime(path: &Path) -> Option<SystemTime> {
|
||||
@ -155,7 +160,7 @@ impl DesktopFileSync {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn synchronize_items(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn synchronize_items(&self) -> Result<()> {
|
||||
for item in &self.items {
|
||||
let target = Path::new(Self::CITADEL_APPLICATIONS).join(item.filename());
|
||||
if item.is_newer_than(&target) {
|
||||
@ -179,9 +184,11 @@ impl DesktopFileSync {
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_item(&self, item: &DesktopItem) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn sync_item(&self, item: &DesktopItem) -> Result<()> {
|
||||
let mut dfp = DesktopFileParser::parse_from_path(&item.path, "/usr/libexec/citadel-run ")?;
|
||||
if dfp.is_showable() {
|
||||
// When use-flatpak is enabled a gnome-software desktop file will be generated
|
||||
let flatpak_gs_hide = dfp.filename() == "org.gnome.Software.desktop" && self.realm.config().flatpak();
|
||||
if dfp.is_showable() && !flatpak_gs_hide {
|
||||
self.sync_item_icon(&mut dfp);
|
||||
dfp.write_to_dir(Self::CITADEL_APPLICATIONS, Some(&self.realm))?;
|
||||
} else {
|
||||
|
@ -4,7 +4,6 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use libcitadel::{Result, util, Realm};
|
||||
use std::cell::{RefCell, Cell};
|
||||
use std::fs;
|
||||
use crate::sync::desktop_file::DesktopFile;
|
||||
use crate::sync::REALM_BASE_PATHS;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
use libcitadel::{Logger, LogLevel};
|
||||
use libcitadel::{Result, Logger, LogLevel};
|
||||
|
||||
mod desktop_file;
|
||||
mod parser;
|
||||
@ -14,12 +14,13 @@ fn has_arg(args: &[String], arg: &str) -> bool {
|
||||
|
||||
pub const REALM_BASE_PATHS:&[&str] = &[
|
||||
"rootfs/usr",
|
||||
"rootfs/var/lib/flatpak/exports",
|
||||
"flatpak/exports",
|
||||
"home/.local",
|
||||
"home/.local/share/flatpak/exports"
|
||||
];
|
||||
|
||||
pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn main(args: Vec<String>) {
|
||||
|
||||
if has_arg(&args, "-v") {
|
||||
Logger::set_log_level(LogLevel::Debug);
|
||||
} else {
|
||||
@ -36,14 +37,12 @@ pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Desktop file sync failed: {}", e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync(clear: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn sync(clear: bool) -> Result<()> {
|
||||
if let Some(mut sync) = DesktopFileSync::new_current() {
|
||||
sync.run_sync(clear)?
|
||||
sync.run_sync(clear)
|
||||
} else {
|
||||
DesktopFileSync::clear_target_files()?
|
||||
DesktopFileSync::clear_target_files()
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use libcitadel::{Partition, ResourceImage, ImageHeader, LogLevel, Logger, util};
|
||||
use libcitadel::{Result, Partition, ResourceImage, ImageHeader, LogLevel, Logger, util};
|
||||
use crate::update::kernel::{KernelInstaller, KernelVersion};
|
||||
use std::collections::HashSet;
|
||||
use std::fs::{DirEntry, File};
|
||||
@ -16,7 +16,7 @@ const FLAG_QUIET: u32 = 0x04;
|
||||
const RESOURCES_DIRECTORY: &str = "/storage/resources";
|
||||
const TEMP_DIRECTORY: &str = "/storage/resources/tmp";
|
||||
|
||||
pub fn main(args: Vec<String>) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn main(args: Vec<String>) {
|
||||
let mut args = args.iter().skip(1);
|
||||
let mut flags = 0;
|
||||
|
||||
@ -34,7 +34,7 @@ pub fn main(args: Vec<String>) -> std::result::Result<(), Box<dyn std::error::Er
|
||||
Logger::set_log_level(LogLevel::Debug);
|
||||
} else if arg == "--choose-rootfs" {
|
||||
let _ = choose_install_partition(true);
|
||||
return Ok(())
|
||||
return;
|
||||
} else {
|
||||
let path = Path::new(arg);
|
||||
if let Err(e) = install_image(path, flags) {
|
||||
@ -42,13 +42,12 @@ pub fn main(args: Vec<String>) -> std::result::Result<(), Box<dyn std::error::Er
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Search directory containing installed image files for an
|
||||
// image file that has an identical shasum and abort the installation
|
||||
// if a duplicate is found.
|
||||
fn detect_duplicates(header: &ImageHeader) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn detect_duplicates(header: &ImageHeader) -> Result<()> {
|
||||
let metainfo = header.metainfo();
|
||||
let channel = metainfo.channel();
|
||||
let shasum = metainfo.shasum();
|
||||
@ -62,20 +61,17 @@ fn detect_duplicates(header: &ImageHeader) -> Result<(), Box<dyn std::error::Err
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
Ok(util::read_directory(&resource_dir, |dent| {
|
||||
util::read_directory(&resource_dir, |dent| {
|
||||
if let Ok(hdr) = ImageHeader::from_file(dent.path()) {
|
||||
if hdr.metainfo().shasum() == shasum {
|
||||
panic!(
|
||||
"A duplicate image file with the same shasum already exists at {}",
|
||||
dent.path().display()
|
||||
);
|
||||
bail!("A duplicate image file with the same shasum already exists at {}", dent.path().display());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?)
|
||||
})
|
||||
}
|
||||
|
||||
fn create_tmp_copy(path: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
fn create_tmp_copy(path: &Path) -> Result<PathBuf> {
|
||||
if !Path::new(TEMP_DIRECTORY).exists() {
|
||||
util::create_dir(TEMP_DIRECTORY)?;
|
||||
}
|
||||
@ -97,12 +93,12 @@ fn create_tmp_copy(path: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn install_image(path: &Path, flags: u32) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn install_image(path: &Path, flags: u32) -> Result<()> {
|
||||
if !path.exists() || path.file_name().is_none() {
|
||||
panic!("file path {} does not exist", path.display());
|
||||
bail!("file path {} does not exist", path.display());
|
||||
}
|
||||
if !util::is_euid_root() {
|
||||
panic!("Image updates must be installed by root user");
|
||||
bail!("Image updates must be installed by root user");
|
||||
}
|
||||
|
||||
let header = ImageHeader::from_file(path)?;
|
||||
@ -117,14 +113,14 @@ fn install_image(path: &Path, flags: u32) -> Result<(), Box<dyn std::error::Erro
|
||||
match image.metainfo().image_type() {
|
||||
"kernel" => install_kernel_image(&mut image),
|
||||
"extra" => install_extra_image(&image),
|
||||
"rootfs" => install_rootfs_image(&image, flags),
|
||||
image_type => panic!("Unknown image type: {}", image_type),
|
||||
"rootfs" => install_rootfs_image(&image, flags),
|
||||
image_type => bail!("Unknown image type: {}", image_type),
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare the image file for installation by decompressing and generating
|
||||
// dmverity hash tree.
|
||||
fn prepare_image(image: &ResourceImage, flags: u32) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn prepare_image(image: &ResourceImage, flags: u32) -> Result<()> {
|
||||
if image.is_compressed() {
|
||||
image.decompress(false)?;
|
||||
}
|
||||
@ -133,7 +129,7 @@ fn prepare_image(image: &ResourceImage, flags: u32) -> Result<(), Box<dyn std::e
|
||||
info!("Verifying sha256 hash of image");
|
||||
let shasum = image.generate_shasum()?;
|
||||
if shasum != image.metainfo().shasum() {
|
||||
panic!("image file does not have expected sha256 value");
|
||||
bail!("image file does not have expected sha256 value");
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,28 +139,24 @@ fn prepare_image(image: &ResourceImage, flags: u32) -> Result<(), Box<dyn std::e
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_extra_image(image: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let filename = format!("citadel-extra-{:03}.img", image.header().metainfo().version());
|
||||
fn install_extra_image(image: &ResourceImage) -> Result<()> {
|
||||
let filename = format!("citadel-extra-{}.img", image.header().metainfo().version());
|
||||
install_image_file(image, filename.as_str())?;
|
||||
remove_old_extra_images(image)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_old_extra_images(image: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn remove_old_extra_images(image: &ResourceImage) -> Result<()> {
|
||||
let new_meta = image.header().metainfo();
|
||||
let shasum = new_meta.shasum();
|
||||
let target_dir = target_directory(image)?;
|
||||
Ok(util::read_directory(&target_dir, |dent| {
|
||||
util::read_directory(&target_dir, |dent| {
|
||||
let path = dent.path();
|
||||
maybe_remove_old_extra_image(&path, shasum).unwrap();
|
||||
Ok(())
|
||||
})?)
|
||||
maybe_remove_old_extra_image(&path, shasum)
|
||||
})
|
||||
}
|
||||
|
||||
fn maybe_remove_old_extra_image(
|
||||
path: &Path,
|
||||
shasum: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn maybe_remove_old_extra_image(path: &Path, shasum: &str) -> Result<()> {
|
||||
let header = ImageHeader::from_file(&path)?;
|
||||
if !header.is_magic_valid() {
|
||||
return Ok(());
|
||||
@ -180,21 +172,21 @@ fn maybe_remove_old_extra_image(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_kernel_image(image: &mut ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn install_kernel_image(image: &mut ResourceImage) -> Result<()> {
|
||||
if !Path::new("/boot/loader/loader.conf").exists() {
|
||||
panic!("failed to automount /boot partition. Please manually mount correct partition.");
|
||||
bail!("failed to automount /boot partition. Please manually mount correct partition.");
|
||||
}
|
||||
|
||||
let metainfo = image.header().metainfo();
|
||||
let version = metainfo.version();
|
||||
let kernel_version = match metainfo.kernel_version() {
|
||||
Some(kv) => kv,
|
||||
None => panic!("kernel image does not have kernel version field"),
|
||||
None => bail!("kernel image does not have kernel version field"),
|
||||
};
|
||||
info!("kernel version is {}", kernel_version);
|
||||
install_kernel_file(image, &kernel_version)?;
|
||||
|
||||
let filename = format!("citadel-kernel-{}-{:03}.img", kernel_version, version);
|
||||
let filename = format!("citadel-kernel-{}-{}.img", kernel_version, version);
|
||||
install_image_file(image, &filename)?;
|
||||
|
||||
let all_versions = all_boot_kernel_versions()?;
|
||||
@ -202,7 +194,7 @@ fn install_kernel_image(image: &mut ResourceImage) -> Result<(), Box<dyn std::er
|
||||
let mut remove_paths = Vec::new();
|
||||
util::read_directory(&image_dir, |dent| {
|
||||
let path = dent.path();
|
||||
if is_unused_kernel_image(&path, &all_versions).unwrap() {
|
||||
if is_unused_kernel_image(&path, &all_versions)? {
|
||||
remove_paths.push(path);
|
||||
}
|
||||
Ok(())
|
||||
@ -214,7 +206,7 @@ fn install_kernel_image(image: &mut ResourceImage) -> Result<(), Box<dyn std::er
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_unused_kernel_image(path: &Path, versions: &HashSet<String>) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
fn is_unused_kernel_image(path: &Path, versions: &HashSet<String>) -> Result<bool> {
|
||||
let header = ImageHeader::from_file(path)?;
|
||||
if !header.is_magic_valid() {
|
||||
return Ok(false);
|
||||
@ -234,25 +226,23 @@ fn is_unused_kernel_image(path: &Path, versions: &HashSet<String>) -> Result<boo
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn install_kernel_file(
|
||||
image: &mut ResourceImage,
|
||||
kernel_version: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn install_kernel_file(image: &mut ResourceImage, kernel_version: &str) -> Result<()> {
|
||||
let mountpoint = Path::new("/run/citadel/images/kernel-install.mountpoint");
|
||||
info!("Temporarily mounting kernel resource image");
|
||||
let mut handle = image.mount_at(mountpoint)?;
|
||||
let kernel_path = mountpoint.join("kernel/bzImage");
|
||||
if !kernel_path.exists() {
|
||||
handle.unmount()?;
|
||||
panic!("kernel not found in kernel resource image at /kernel/bzImage")
|
||||
bail!("kernel not found in kernel resource image at /kernel/bzImage")
|
||||
}
|
||||
|
||||
KernelInstaller::install_kernel(&kernel_path, kernel_version)?;
|
||||
let result = KernelInstaller::install_kernel(&kernel_path, kernel_version);
|
||||
info!("Unmounting kernel resource image");
|
||||
Ok(handle.unmount()?)
|
||||
handle.unmount()?;
|
||||
result
|
||||
}
|
||||
|
||||
fn all_boot_kernel_versions() -> Result<HashSet<String>, Box<dyn std::error::Error>> {
|
||||
fn all_boot_kernel_versions() -> Result<HashSet<String>> {
|
||||
let mut result = HashSet::new();
|
||||
util::read_directory("/boot", |dent| {
|
||||
if is_kernel_dirent(&dent) {
|
||||
@ -274,10 +264,7 @@ fn is_kernel_dirent(dirent: &DirEntry) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn install_image_file(
|
||||
image: &ResourceImage,
|
||||
filename: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn install_image_file(image: &ResourceImage, filename: &str) -> Result<()> {
|
||||
let image_dir = target_directory(image)?;
|
||||
let image_dest = image_dir.join(filename);
|
||||
if image_dest.exists() {
|
||||
@ -288,14 +275,14 @@ fn install_image_file(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn target_directory(image: &ResourceImage) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
fn target_directory(image: &ResourceImage) -> Result<PathBuf> {
|
||||
let metainfo = image.header().metainfo();
|
||||
let channel = metainfo.channel();
|
||||
validate_channel_name(channel)?;
|
||||
Ok(Path::new("/storage/resources").join(channel))
|
||||
}
|
||||
|
||||
fn rotate(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn rotate(path: &Path) -> Result<()> {
|
||||
if !path.exists() || path.file_name().is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
@ -306,17 +293,14 @@ fn rotate(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_channel_name(channel: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn validate_channel_name(channel: &str) -> Result<()> {
|
||||
if !channel.chars().all(|c| c.is_ascii_lowercase()) {
|
||||
panic!("image has invalid channel name '{}'", channel);
|
||||
bail!("image has invalid channel name '{}'", channel);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_rootfs_image(
|
||||
image: &ResourceImage,
|
||||
flags: u32,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn install_rootfs_image(image: &ResourceImage, flags: u32) -> Result<()> {
|
||||
let quiet = flags & FLAG_QUIET != 0;
|
||||
let partition = choose_install_partition(!quiet)?;
|
||||
|
||||
@ -331,7 +315,7 @@ fn install_rootfs_image(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_prefer_boot() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn clear_prefer_boot() -> Result<()> {
|
||||
for mut p in Partition::rootfs_partitions()? {
|
||||
if p.is_initialized() && p.header().has_flag(ImageHeader::FLAG_PREFER_BOOT) {
|
||||
p.clear_flag_and_write(ImageHeader::FLAG_PREFER_BOOT)?;
|
||||
@ -348,7 +332,7 @@ fn bool_to_yesno(val: bool) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn choose_install_partition(verbose: bool) -> Result<Partition, Box<dyn std::error::Error>> {
|
||||
fn choose_install_partition(verbose: bool) -> Result<Partition> {
|
||||
let partitions = Partition::rootfs_partitions()?;
|
||||
|
||||
if verbose {
|
||||
@ -378,5 +362,5 @@ fn choose_install_partition(verbose: bool) -> Result<Partition, Box<dyn std::err
|
||||
return Ok(p.clone())
|
||||
}
|
||||
}
|
||||
panic!("no suitable install partition found")
|
||||
bail!("no suitable install partition found")
|
||||
}
|
||||
|
8
launch-gnome-software/Cargo.toml
Normal file
8
launch-gnome-software/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "launch-gnome-software"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
anyhow = "1.0"
|
67
launch-gnome-software/src/main.rs
Normal file
67
launch-gnome-software/src/main.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use std::env;
|
||||
|
||||
use libcitadel::{Logger, LogLevel, Realm, Realms, util};
|
||||
use libcitadel::flatpak::GnomeSoftwareLauncher;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
|
||||
fn realm_arg() -> Option<String> {
|
||||
let mut args = env::args();
|
||||
while let Some(arg) = args.next() {
|
||||
if arg == "--realm" {
|
||||
return args.next();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn choose_realm() -> Result<Realm> {
|
||||
let mut realms = Realms::load()?;
|
||||
if let Some(realm_name) = realm_arg() {
|
||||
match realms.by_name(&realm_name) {
|
||||
None => bail!("realm '{}' not found", realm_name),
|
||||
Some(realm) => return Ok(realm),
|
||||
}
|
||||
}
|
||||
let realm = match realms.current() {
|
||||
Some(realm) => realm,
|
||||
None => bail!("no current realm"),
|
||||
};
|
||||
|
||||
Ok(realm)
|
||||
}
|
||||
|
||||
fn has_arg(arg: &str) -> bool {
|
||||
env::args()
|
||||
.skip(1)
|
||||
.any(|s| s == arg)
|
||||
}
|
||||
|
||||
fn launch() -> Result<()> {
|
||||
let realm = choose_realm()?;
|
||||
if !util::is_euid_root() {
|
||||
bail!("Must be run with root euid");
|
||||
}
|
||||
let mut launcher = GnomeSoftwareLauncher::new(realm)?;
|
||||
|
||||
if has_arg("--quit") {
|
||||
launcher.quit()?;
|
||||
} else {
|
||||
if has_arg("--shell") {
|
||||
launcher.set_run_shell();
|
||||
}
|
||||
launcher.launch()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if has_arg("--verbose") {
|
||||
Logger::set_log_level(LogLevel::Verbose);
|
||||
}
|
||||
|
||||
if let Err(err) = launch() {
|
||||
eprintln!("Error: {}", err);
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ nix = "0.17.0"
|
||||
toml = "0.5"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "=1.0.1"
|
||||
lazy_static = "1.4"
|
||||
sodiumoxide = "0.2"
|
||||
hex = "0.4"
|
||||
@ -19,6 +20,9 @@ walkdir = "2"
|
||||
dbus = "0.6"
|
||||
posix-acl = "1.0.0"
|
||||
procfs = "0.12.0"
|
||||
semver = "1.0"
|
||||
anyhow = "1.0"
|
||||
clap = "4.5"
|
||||
|
||||
[dependencies.inotify]
|
||||
version = "0.8"
|
||||
|
221
libcitadel/src/flatpak/bubblewrap.rs
Normal file
221
libcitadel/src/flatpak/bubblewrap.rs
Normal file
@ -0,0 +1,221 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::{fs, io};
|
||||
use std::fs::File;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::{Logger, LogLevel, Result, verbose};
|
||||
|
||||
const BWRAP_PATH: &str = "/usr/libexec/flatpak-bwrap";
|
||||
|
||||
pub struct BubbleWrap {
|
||||
command: Command,
|
||||
}
|
||||
|
||||
impl BubbleWrap {
|
||||
|
||||
pub fn new() -> Self {
|
||||
BubbleWrap {
|
||||
command: Command::new(BWRAP_PATH),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
|
||||
self.command.arg(arg);
|
||||
self
|
||||
}
|
||||
|
||||
fn add_args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
|
||||
for arg in args {
|
||||
self.add_arg(arg.as_ref());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ro_bind(&mut self, path_list: &[&str]) -> &mut Self {
|
||||
for &path in path_list {
|
||||
self.add_args(&["--ro-bind", path, path]);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ro_bind_to(&mut self, src: &str, dest: &str) -> &mut Self {
|
||||
self.add_args(&["--ro-bind", src, dest])
|
||||
}
|
||||
|
||||
pub fn bind_to(&mut self, src: &str, dest: &str) -> &mut Self {
|
||||
self.add_args(&["--bind", src, dest])
|
||||
}
|
||||
|
||||
pub fn create_dirs(&mut self, dir_list: &[&str]) -> &mut Self {
|
||||
for &dir in dir_list {
|
||||
self.add_args(&["--dir", dir]);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn create_symlinks(&mut self, links: &[(&str, &str)]) -> &mut Self {
|
||||
for (src,dest) in links {
|
||||
self.add_args(&["--symlink", src, dest]);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mount_dev(&mut self) -> &mut Self {
|
||||
self.add_args(&["--dev", "/dev"])
|
||||
}
|
||||
|
||||
pub fn mount_proc(&mut self) -> &mut Self {
|
||||
self.add_args(&["--proc", "/proc"])
|
||||
}
|
||||
|
||||
pub fn dev_bind(&mut self, path: &str) -> &mut Self {
|
||||
self.add_args(&["--dev-bind", path, path])
|
||||
}
|
||||
|
||||
pub fn clear_env(&mut self) -> &mut Self {
|
||||
self.add_arg("--clearenv")
|
||||
}
|
||||
|
||||
pub fn set_env_list(&mut self, env_list: &[&str]) -> &mut Self {
|
||||
for line in env_list {
|
||||
if let Some((k,v)) = line.split_once("=") {
|
||||
self.add_args(&["--setenv", k, v]);
|
||||
} else {
|
||||
eprintln!("Warning: environment variable '{}' does not have = delimiter. Ignoring", line);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unshare_all(&mut self) -> &mut Self {
|
||||
self.add_arg("--unshare-all")
|
||||
}
|
||||
|
||||
pub fn share_net(&mut self) -> &mut Self {
|
||||
self.add_arg("--share-net")
|
||||
}
|
||||
|
||||
pub fn log_command(&self) {
|
||||
let mut buffer = String::new();
|
||||
verbose!("{}", BWRAP_PATH);
|
||||
for arg in self.command.get_args() {
|
||||
if let Some(s) = arg.to_str() {
|
||||
if s.starts_with("-") {
|
||||
if !buffer.is_empty() {
|
||||
verbose!(" {}", buffer);
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
if !buffer.is_empty() {
|
||||
buffer.push(' ');
|
||||
}
|
||||
buffer.push_str(s);
|
||||
}
|
||||
}
|
||||
if !buffer.is_empty() {
|
||||
verbose!(" {}", buffer);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status_file(&mut self, status_file: &File) -> &mut Self {
|
||||
// Rust sets O_CLOEXEC when opening files so we create
|
||||
// a new descriptor that will remain open across exec()
|
||||
let dup_fd = unsafe {
|
||||
libc::dup(status_file.as_raw_fd())
|
||||
};
|
||||
if dup_fd == -1 {
|
||||
warn!("Failed to dup() status file descriptor: {}", io::Error::last_os_error());
|
||||
warn!("Skipping --json-status-fd argument");
|
||||
self
|
||||
} else {
|
||||
self.add_arg("--json-status-fd")
|
||||
.add_arg(dup_fd.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn launch<S: AsRef<OsStr>>(&mut self, cmd: &[S]) -> Result<()> {
|
||||
if Logger::is_log_level(LogLevel::Verbose) {
|
||||
self.log_command();
|
||||
let s = cmd.iter().map(|s| format!("{} ", s.as_ref().to_str().unwrap())).collect::<String>();
|
||||
verbose!(" {}", s)
|
||||
}
|
||||
self.add_args(cmd);
|
||||
let err = self.command.exec();
|
||||
bail!("failed to exec bubblewrap: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize,Clone)]
|
||||
#[serde(rename_all="kebab-case")]
|
||||
pub struct BubbleWrapRunningStatus {
|
||||
pub child_pid: u64,
|
||||
pub cgroup_namespace: u64,
|
||||
pub ipc_namespace: u64,
|
||||
pub mnt_namespace: u64,
|
||||
pub pid_namespace: u64,
|
||||
pub uts_namespace: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize,Clone)]
|
||||
#[serde(rename_all="kebab-case")]
|
||||
pub struct BubbleWrapExitStatus {
|
||||
pub exit_code: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BubbleWrapStatus {
|
||||
running: BubbleWrapRunningStatus,
|
||||
exit: Option<BubbleWrapExitStatus>,
|
||||
}
|
||||
|
||||
impl BubbleWrapStatus {
|
||||
pub fn parse_file(path: impl AsRef<Path>) -> Result<Option<Self>> {
|
||||
if !path.as_ref().exists() {
|
||||
return Ok(None)
|
||||
}
|
||||
|
||||
let s = fs::read_to_string(path)
|
||||
.map_err(context!("error reading status file"))?;
|
||||
|
||||
let mut lines = s.lines();
|
||||
let running = match lines.next() {
|
||||
None => return Ok(None),
|
||||
Some(s) => serde_json::from_str::<BubbleWrapRunningStatus>(s)
|
||||
.map_err(context!("failed to parse status line ({})", s))?
|
||||
};
|
||||
|
||||
let exit = match lines.next() {
|
||||
None => None,
|
||||
Some(s) => Some(serde_json::from_str::<BubbleWrapExitStatus>(s)
|
||||
.map_err(context!("failed to parse exit line ({})", s))?)
|
||||
};
|
||||
|
||||
Ok(Some(BubbleWrapStatus {
|
||||
running,
|
||||
exit
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.exit.is_none()
|
||||
}
|
||||
|
||||
pub fn running_status(&self) -> &BubbleWrapRunningStatus {
|
||||
&self.running
|
||||
}
|
||||
|
||||
pub fn child_pid(&self) -> u64 {
|
||||
self.running.child_pid
|
||||
}
|
||||
|
||||
pub fn pid_namespace(&self) -> u64 {
|
||||
self.running.pid_namespace
|
||||
}
|
||||
|
||||
pub fn exit_status(&self) -> Option<&BubbleWrapExitStatus> {
|
||||
self.exit.as_ref()
|
||||
}
|
||||
}
|
259
libcitadel/src/flatpak/launcher.rs
Normal file
259
libcitadel/src/flatpak/launcher.rs
Normal file
@ -0,0 +1,259 @@
|
||||
use std::{fs, io};
|
||||
use std::fs::File;
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::os::unix::prelude::CommandExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::{Realm, Result, util};
|
||||
use crate::flatpak::{BubbleWrap, BubbleWrapStatus, SANDBOX_STATUS_FILE_DIRECTORY, SandboxStatus};
|
||||
use crate::flatpak::netns::NetNS;
|
||||
|
||||
|
||||
const FLATPAK_PATH: &str = "/usr/bin/flatpak";
|
||||
|
||||
const ENVIRONMENT: &[&str; 7] = &[
|
||||
"HOME=/home/citadel",
|
||||
"USER=citadel",
|
||||
"XDG_RUNTIME_DIR=/run/user/1000",
|
||||
"XDG_DATA_DIRS=/home/citadel/.local/share/flatpak/exports/share:/usr/share",
|
||||
"TERM=xterm-256color",
|
||||
"GTK_A11Y=none",
|
||||
"FLATPAK_USER_DIR=/home/citadel/realm-flatpak",
|
||||
];
|
||||
|
||||
const FLATHUB_URL: &str = "https://dl.flathub.org/repo/flathub.flatpakrepo";
|
||||
|
||||
pub struct GnomeSoftwareLauncher {
|
||||
realm: Realm,
|
||||
status: Option<BubbleWrapStatus>,
|
||||
netns: NetNS,
|
||||
run_shell: bool,
|
||||
}
|
||||
impl GnomeSoftwareLauncher {
|
||||
|
||||
pub fn new(realm: Realm) -> Result<Self> {
|
||||
let sandbox_status = SandboxStatus::load(SANDBOX_STATUS_FILE_DIRECTORY)?;
|
||||
let status = sandbox_status.realm_status(&realm);
|
||||
let netns = NetNS::new(NetNS::GS_NETNS_NAME);
|
||||
Ok(GnomeSoftwareLauncher {
|
||||
realm,
|
||||
status,
|
||||
netns,
|
||||
run_shell: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_run_shell(&mut self) {
|
||||
self.run_shell = true;
|
||||
}
|
||||
|
||||
fn ensure_flatpak_dir(&self) -> Result<()> {
|
||||
let flatpak_user_dir = self.realm.base_path_file("flatpak");
|
||||
if !flatpak_user_dir.exists() {
|
||||
if let Err(err) = fs::create_dir(&flatpak_user_dir) {
|
||||
bail!("failed to create realm flatpak directory ({}): {}", flatpak_user_dir.display(), err);
|
||||
}
|
||||
util::chown_user(&flatpak_user_dir)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_flathub(&self) -> Result<()> {
|
||||
let flatpak_user_dir = self.realm.base_path_file("flatpak");
|
||||
|
||||
match Command::new(FLATPAK_PATH)
|
||||
.env("FLATPAK_USER_DIR", flatpak_user_dir)
|
||||
.arg("remote-add")
|
||||
.arg("--user")
|
||||
.arg("--if-not-exists")
|
||||
.arg("flathub")
|
||||
.arg(FLATHUB_URL)
|
||||
.status() {
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
Ok(())
|
||||
|
||||
} else {
|
||||
bail!("failed to add flathub repo")
|
||||
}
|
||||
},
|
||||
Err(err) => bail!("error running flatpak command: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_tmp_directory(path: &Path) -> io::Result<Option<String>> {
|
||||
for entry in fs::read_dir(&path)? {
|
||||
let entry = entry?;
|
||||
if entry.file_type()?.is_socket() {
|
||||
if let Some(filename) = entry.path().file_name() {
|
||||
if let Some(filename) = filename.to_str() {
|
||||
if filename.starts_with("dbus-") {
|
||||
return Ok(Some(format!("/tmp/{}", filename)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn find_dbus_socket(&self) -> Result<String> {
|
||||
let pid = self.running_pid()?;
|
||||
let tmp_dir = PathBuf::from(format!("/proc/{}/root/tmp", pid));
|
||||
if !tmp_dir.is_dir() {
|
||||
bail!("no /tmp directory found for process pid={}", pid);
|
||||
}
|
||||
|
||||
if let Some(s) = Self::scan_tmp_directory(&tmp_dir)
|
||||
.map_err(context!("error reading directory {}", tmp_dir.display()))? {
|
||||
Ok(s)
|
||||
|
||||
} else {
|
||||
bail!("no dbus socket found in /tmp directory for process pid={}", pid);
|
||||
}
|
||||
}
|
||||
|
||||
fn launch_sandbox(&self, status_file: &File) -> Result<()> {
|
||||
self.ensure_flatpak_dir()?;
|
||||
if let Err(err) = self.netns.nsenter() {
|
||||
bail!("Failed to enter 'gnome-software' network namespace: {}", err);
|
||||
}
|
||||
verbose!("Entered network namespace ({})", NetNS::GS_NETNS_NAME);
|
||||
|
||||
if let Err(err) = util::drop_privileges(1000, 1000) {
|
||||
bail!("Failed to drop privileges to uid = gid = 1000: {}", err);
|
||||
}
|
||||
verbose!("Dropped privileges (uid=1000, gid=1000)");
|
||||
|
||||
self.add_flathub()?;
|
||||
let flatpak_user_dir = self.realm.base_path_file("flatpak");
|
||||
let flatpak_user_dir = flatpak_user_dir.to_str().unwrap();
|
||||
|
||||
let cmd = if self.run_shell { "/usr/bin/bash" } else { "/usr/bin/gnome-software"};
|
||||
verbose!("Running command in sandbox: {}", cmd);
|
||||
|
||||
BubbleWrap::new()
|
||||
.ro_bind(&[
|
||||
"/usr/bin",
|
||||
"/usr/lib",
|
||||
"/usr/libexec",
|
||||
"/usr/share/dbus-1",
|
||||
"/usr/share/icons",
|
||||
"/usr/share/mime",
|
||||
"/usr/share/X11",
|
||||
"/usr/share/glib-2.0",
|
||||
"/usr/share/xml",
|
||||
"/usr/share/drirc.d",
|
||||
"/usr/share/fontconfig",
|
||||
"/usr/share/fonts",
|
||||
"/usr/share/zoneinfo",
|
||||
"/usr/share/swcatalog",
|
||||
|
||||
"/etc/passwd",
|
||||
"/etc/machine-id",
|
||||
"/etc/nsswitch.conf",
|
||||
"/etc/fonts",
|
||||
"/etc/ssl",
|
||||
"/sys/dev/char", "/sys/devices",
|
||||
"/run/user/1000/wayland-0",
|
||||
])
|
||||
.ro_bind_to("/run/NetworkManager/resolv.conf", "/etc/resolv.conf")
|
||||
.bind_to(flatpak_user_dir, "/home/citadel/realm-flatpak")
|
||||
.create_symlinks(&[
|
||||
("usr/lib", "/lib64"),
|
||||
("usr/bin", "/bin"),
|
||||
("/tmp", "/var/tmp"),
|
||||
])
|
||||
.create_dirs(&[
|
||||
"/var/lib/flatpak",
|
||||
"/home/citadel",
|
||||
"/tmp",
|
||||
"/sys/block", "/sys/bus", "/sys/class",
|
||||
])
|
||||
|
||||
|
||||
.mount_dev()
|
||||
.dev_bind("/dev/dri")
|
||||
.mount_proc()
|
||||
.unshare_all()
|
||||
.share_net()
|
||||
|
||||
.clear_env()
|
||||
.set_env_list(ENVIRONMENT)
|
||||
|
||||
.status_file(status_file)
|
||||
|
||||
.launch(&["dbus-run-session", "--", cmd])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn new_realm_status_file(&self) -> Result<File> {
|
||||
let path = Path::new(SANDBOX_STATUS_FILE_DIRECTORY).join(self.realm.name());
|
||||
File::create(&path)
|
||||
.map_err(context!("failed to open sandbox status file {}", path.display()))
|
||||
}
|
||||
|
||||
pub fn launch(&self) -> Result<()> {
|
||||
self.netns.ensure_exists()?;
|
||||
if self.is_running() {
|
||||
let cmd = if self.run_shell { "/usr/bin/bash" } else { "/usr/bin/gnome-software"};
|
||||
self.launch_in_running_sandbox(&[cmd])?;
|
||||
} else {
|
||||
let status_file = self.new_realm_status_file()?;
|
||||
self.ensure_flatpak_dir()?;
|
||||
self.launch_sandbox(&status_file)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn quit(&self) -> Result<()> {
|
||||
if self.is_running() {
|
||||
self.launch_in_running_sandbox(&["/usr/bin/gnome-software", "--quit"])?;
|
||||
} else {
|
||||
warn!("No running sandbox found for realm {}", self.realm.name());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.status.as_ref()
|
||||
.map(|s| s.is_running())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn running_pid(&self) -> Result<u64> {
|
||||
self.status.as_ref()
|
||||
.map(|s| s.child_pid())
|
||||
.ok_or(format_err!("no sandbox status available for realm '{}',", self.realm.name()))
|
||||
}
|
||||
|
||||
fn dbus_session_address(&self) -> Result<String> {
|
||||
let dbus_socket = Self::find_dbus_socket(&self)?;
|
||||
Ok(format!("unix:path={}", dbus_socket))
|
||||
}
|
||||
|
||||
fn launch_in_running_sandbox(&self, command: &[&str]) -> Result<()> {
|
||||
let dbus_address = self.dbus_session_address()?;
|
||||
|
||||
let pid = self.running_pid()?.to_string();
|
||||
let mut env = ENVIRONMENT.iter()
|
||||
.map(|s| s.split_once('=').unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
env.push(("DBUS_SESSION_BUS_ADDRESS", dbus_address.as_str()));
|
||||
|
||||
let err = Command::new("/usr/bin/nsenter")
|
||||
.env_clear()
|
||||
.envs( env )
|
||||
.args(&[
|
||||
"--all",
|
||||
"--target", pid.as_str(),
|
||||
"--setuid", "1000",
|
||||
"--setgid", "1000",
|
||||
])
|
||||
.args(command)
|
||||
.exec();
|
||||
|
||||
Err(format_err!("failed to execute nsenter: {}", err))
|
||||
}
|
||||
}
|
16
libcitadel/src/flatpak/mod.rs
Normal file
16
libcitadel/src/flatpak/mod.rs
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
pub(crate) mod setup;
|
||||
pub(crate) mod status;
|
||||
|
||||
pub(crate) mod bubblewrap;
|
||||
|
||||
pub(crate) mod launcher;
|
||||
|
||||
pub(crate) mod netns;
|
||||
|
||||
pub use status::SandboxStatus;
|
||||
|
||||
pub use bubblewrap::{BubbleWrap,BubbleWrapStatus,BubbleWrapRunningStatus,BubbleWrapExitStatus};
|
||||
pub use launcher::GnomeSoftwareLauncher;
|
||||
|
||||
pub const SANDBOX_STATUS_FILE_DIRECTORY: &str = "/run/citadel/realms/gs-sandbox-status";
|
132
libcitadel/src/flatpak/netns.rs
Normal file
132
libcitadel/src/flatpak/netns.rs
Normal file
@ -0,0 +1,132 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::{util,Result};
|
||||
|
||||
|
||||
const BRIDGE_NAME: &str = "vz-clear";
|
||||
const VETH0: &str = "gs-veth0";
|
||||
const VETH1: &str = "gs-veth1";
|
||||
const IP_ADDRESS: &str = "172.17.0.222/24";
|
||||
const GW_ADDRESS: &str = "172.17.0.1";
|
||||
|
||||
|
||||
pub struct NetNS {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl NetNS {
|
||||
pub const GS_NETNS_NAME: &'static str = "gnome-software";
|
||||
pub fn new(name: &str) -> Self {
|
||||
NetNS {
|
||||
name: name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create(&self) -> crate::Result<()> {
|
||||
Ip::new().link_add_veth(VETH0, VETH1).run()?;
|
||||
Ip::new().link_set_netns(VETH0, &self.name).run()?;
|
||||
Ip::new().link_set_master(VETH1, BRIDGE_NAME).run()?;
|
||||
Ip::new().link_set_dev_up(VETH1).run()?;
|
||||
|
||||
Ip::new().ip_netns_exec_ip(&self.name).addr_add(IP_ADDRESS, VETH0).run()?;
|
||||
Ip::new().ip_netns_exec_ip(&self.name).link_set_dev_up(VETH0).run()?;
|
||||
Ip::new().ip_netns_exec_ip(&self.name).route_add_default(GW_ADDRESS).run()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn ensure_exists(&self) -> Result<()> {
|
||||
if Path::new(&format!("/run/netns/{}", self.name)).exists() {
|
||||
verbose!("Network namespace ({}) exists", self.name);
|
||||
return Ok(())
|
||||
}
|
||||
verbose!("Setting up network namespace ({})", self.name);
|
||||
|
||||
Ip::new().netns_add(&self.name).run()
|
||||
.map_err(context!("Failed to add network namespace '{}'", self.name))?;
|
||||
|
||||
if let Err(err) = self.create() {
|
||||
Ip::new().netns_delete(&self.name).run()?;
|
||||
Err(err)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nsenter(&self) -> Result<()> {
|
||||
util::nsenter_netns(&self.name)
|
||||
}
|
||||
}
|
||||
const IP_PATH: &str = "/usr/sbin/ip";
|
||||
struct Ip {
|
||||
command: Command,
|
||||
}
|
||||
|
||||
impl Ip {
|
||||
|
||||
fn new() -> Self {
|
||||
let mut command = Command::new(IP_PATH);
|
||||
command.env_clear();
|
||||
Ip { command }
|
||||
}
|
||||
|
||||
fn add_args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
|
||||
for arg in args {
|
||||
self.command.arg(arg);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn netns_add(&mut self, name: &str) -> &mut Self {
|
||||
self.add_args(&["netns", "add", name])
|
||||
}
|
||||
|
||||
pub fn netns_delete(&mut self, name: &str) -> &mut Self {
|
||||
self.add_args(&["netns", "delete", name])
|
||||
}
|
||||
|
||||
pub fn link_add_veth(&mut self, name: &str, peer_name: &str) -> &mut Self {
|
||||
self.add_args(&["link", "add", name, "type", "veth", "peer", "name", peer_name])
|
||||
}
|
||||
|
||||
pub fn link_set_netns(&mut self, iface: &str, netns_name: &str) -> &mut Self {
|
||||
self.add_args(&["link", "set", iface, "netns", netns_name])
|
||||
}
|
||||
|
||||
pub fn link_set_master(&mut self, iface: &str, bridge_name: &str) -> &mut Self {
|
||||
self.add_args(&["link", "set", iface, "master", bridge_name])
|
||||
}
|
||||
|
||||
pub fn link_set_dev_up(&mut self, iface: &str) -> &mut Self {
|
||||
self.add_args(&["link", "set", "dev", iface, "up"])
|
||||
}
|
||||
|
||||
pub fn ip_netns_exec_ip(&mut self, netns_name: &str) -> &mut Self {
|
||||
self.add_args(&["netns", "exec", netns_name, IP_PATH])
|
||||
}
|
||||
|
||||
pub fn addr_add(&mut self, ip_address: &str, dev: &str) -> &mut Self {
|
||||
self.add_args(&["addr", "add", ip_address, "dev", dev])
|
||||
}
|
||||
|
||||
pub fn route_add_default(&mut self, gateway: &str) -> &mut Self {
|
||||
self.add_args(&["route", "add", "default", "via", gateway])
|
||||
}
|
||||
|
||||
fn run(&mut self) -> crate::Result<()> {
|
||||
verbose!("{:?}", self.command);
|
||||
match self.command.status() {
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("IP command ({:?}) did not succeeed.", self.command);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("error running ip command ({:?}): {}", self.command, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
58
libcitadel/src/flatpak/setup.rs
Normal file
58
libcitadel/src/flatpak/setup.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use std::path::Path;
|
||||
use crate::{Realm, Result, util};
|
||||
|
||||
const GNOME_SOFTWARE_DESKTOP_TEMPLATE: &str = "\
|
||||
[Desktop Entry]
|
||||
Name=Software
|
||||
Comment=Add, remove or update software on this computer
|
||||
Icon=org.gnome.Software
|
||||
Exec=/usr/libexec/launch-gnome-software --realm $REALM_NAME
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=GNOME;GTK;System;PackageManager;
|
||||
Keywords=Updates;Upgrade;Sources;Repositories;Preferences;Install;Uninstall;Program;Software;App;Store;
|
||||
StartupNotify=true
|
||||
";
|
||||
|
||||
const APPLICATION_DIRECTORY: &str = "/home/citadel/.local/share/applications";
|
||||
|
||||
pub struct FlatpakSetup<'a> {
|
||||
realm: &'a Realm,
|
||||
}
|
||||
|
||||
impl <'a> FlatpakSetup<'a> {
|
||||
|
||||
pub fn new(realm: &'a Realm) -> Self {
|
||||
Self { realm }
|
||||
}
|
||||
|
||||
pub fn setup(&self) -> Result<()> {
|
||||
self.write_desktop_file()?;
|
||||
self.ensure_flatpak_directory()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn write_desktop_file(&self) -> Result<()> {
|
||||
let appdir = Path::new(APPLICATION_DIRECTORY);
|
||||
if !appdir.exists() {
|
||||
util::create_dir(appdir)?;
|
||||
if let Some(parent) = appdir.parent().and_then(|p| p.parent()) {
|
||||
util::chown_tree(parent, (1000,1000), true)?;
|
||||
}
|
||||
}
|
||||
let path = appdir.join(format!("realm-{}.org.gnome.Software.desktop", self.realm.name()));
|
||||
util::write_file(path, GNOME_SOFTWARE_DESKTOP_TEMPLATE.replace("$REALM_NAME", self.realm.name()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_flatpak_directory(&self) -> Result<()> {
|
||||
let path = self.realm.base_path_file("flatpak");
|
||||
if !path.exists() {
|
||||
util::create_dir(&path)?;
|
||||
util::chown_user(&path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
144
libcitadel/src/flatpak/status.rs
Normal file
144
libcitadel/src/flatpak/status.rs
Normal file
@ -0,0 +1,144 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
use crate::flatpak::bubblewrap::BubbleWrapStatus;
|
||||
use crate::{Realm, Result, util};
|
||||
|
||||
|
||||
/// Utility function to read modified time from a path.
|
||||
fn modified_time(path: &Path) -> Result<SystemTime> {
|
||||
path.metadata().and_then(|meta| meta.modified())
|
||||
.map_err(context!("failed to read modified time from '{}'", path.display()))
|
||||
}
|
||||
|
||||
/// Utility function to detect if current modified time of a path
|
||||
/// matches an earlier recorded modified time.
|
||||
fn modified_changed(path: &Path, old_modified: SystemTime) -> bool {
|
||||
if !path.exists() {
|
||||
// Path existed at some earlier point, so something changed
|
||||
return true;
|
||||
}
|
||||
|
||||
match modified_time(path) {
|
||||
Ok(modified) => old_modified != modified,
|
||||
Err(err) => {
|
||||
// Print a warning but assume change
|
||||
warn!("{}", err);
|
||||
true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Records the content of single entry in a sandbox status directory.
|
||||
///
|
||||
/// The path to the status file as well as the last modified time are
|
||||
/// recorded so that changes in status of a sandbox can be detected.
|
||||
struct StatusEntry {
|
||||
status: BubbleWrapStatus,
|
||||
path: PathBuf,
|
||||
modified: SystemTime,
|
||||
}
|
||||
|
||||
|
||||
impl StatusEntry {
|
||||
|
||||
fn load_timestamp_and_status(path: &Path) -> Result<Option<(SystemTime, BubbleWrapStatus)>> {
|
||||
if path.exists() {
|
||||
let modified = modified_time(path)?;
|
||||
if let Some(status) = BubbleWrapStatus::parse_file(path)? {
|
||||
return Ok(Some((modified, status)));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn load(base_dir: &Path, name: &str) -> Result<Option<Self>> {
|
||||
let path = base_dir.join(name);
|
||||
let result = StatusEntry::load_timestamp_and_status(&path)?
|
||||
.map(|(modified, status)| StatusEntry { status, path, modified });
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn is_modified(&self) -> bool {
|
||||
modified_changed(&self.path, self.modified)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Holds information about entries in a sandbox status directory.
|
||||
///
|
||||
/// Bubblewrap accepts a command line argument that asks for status
|
||||
/// information to be written as a json structure to a file descriptor.
|
||||
///
|
||||
pub struct SandboxStatus {
|
||||
base_dir: PathBuf,
|
||||
base_modified: SystemTime,
|
||||
entries: HashMap<String, StatusEntry>,
|
||||
}
|
||||
|
||||
impl SandboxStatus {
|
||||
|
||||
pub fn need_reload(&self) -> bool {
|
||||
if modified_changed(&self.base_dir, self.base_modified) {
|
||||
return true;
|
||||
}
|
||||
self.entries.values().any(|entry| entry.is_modified())
|
||||
}
|
||||
|
||||
fn process_dir_entry(&mut self, dir_entry: PathBuf) -> Result<()> {
|
||||
fn realm_name_for_path(path: &Path) -> Option<&str> {
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.filter(|name| Realm::is_valid_name(name))
|
||||
|
||||
}
|
||||
if dir_entry.is_file() {
|
||||
if let Some(name) = realm_name_for_path(&dir_entry) {
|
||||
if let Some(entry) = StatusEntry::load(&self.base_dir, name)? {
|
||||
self.entries.insert(name.to_string(), entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reload(&mut self) -> Result<()> {
|
||||
self.entries.clear();
|
||||
self.base_modified = modified_time(&self.base_dir)?;
|
||||
let base_dir = self.base_dir.clone();
|
||||
util::read_directory(&base_dir, |entry| {
|
||||
self.process_dir_entry(entry.path())
|
||||
})
|
||||
}
|
||||
|
||||
fn new(base_dir: &Path) -> Result<Self> {
|
||||
let base_dir = base_dir.to_owned();
|
||||
let base_modified = modified_time(&base_dir)?;
|
||||
Ok(SandboxStatus {
|
||||
base_dir,
|
||||
base_modified,
|
||||
entries: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(directory: impl AsRef<Path>) -> Result<SandboxStatus> {
|
||||
let base_dir = directory.as_ref();
|
||||
if !base_dir.exists() {
|
||||
util::create_dir(base_dir)?;
|
||||
}
|
||||
let mut status = SandboxStatus::new(base_dir)?;
|
||||
status.reload()?;
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
pub fn realm_status(&self, realm: &Realm) -> Option<BubbleWrapStatus> {
|
||||
self.entries.get(realm.name()).map(|entry| entry.status.clone())
|
||||
}
|
||||
|
||||
pub fn new_realm_status_file(&self, realm: &Realm) -> Result<File> {
|
||||
let path = self.base_dir.join(realm.name());
|
||||
File::create(&path)
|
||||
.map_err(context!("failed to open sandbox status file {}", path.display()))
|
||||
}
|
||||
}
|
@ -453,7 +453,7 @@ pub struct MetaInfo {
|
||||
realmfs_owner: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
version: u32,
|
||||
version: String,
|
||||
|
||||
#[serde(default)]
|
||||
timestamp: String,
|
||||
@ -508,8 +508,8 @@ impl MetaInfo {
|
||||
Self::str_ref(&self.realmfs_owner)
|
||||
}
|
||||
|
||||
pub fn version(&self) -> u32 {
|
||||
self.version
|
||||
pub fn version(&self) -> &str {
|
||||
&self.version
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> &str {
|
||||
|
@ -20,6 +20,9 @@ pub mod symlink;
|
||||
mod realm;
|
||||
pub mod terminal;
|
||||
mod system;
|
||||
pub mod updates;
|
||||
|
||||
pub mod flatpak;
|
||||
|
||||
pub use crate::config::OsRelease;
|
||||
pub use crate::blockdev::BlockDev;
|
||||
|
@ -62,6 +62,11 @@ impl Logger {
|
||||
logger.level = level;
|
||||
}
|
||||
|
||||
pub fn is_log_level(level: LogLevel) -> bool {
|
||||
let logger = LOGGER.lock().unwrap();
|
||||
logger.level >= level
|
||||
}
|
||||
|
||||
pub fn set_log_output(output: Box<dyn LogOutput>) {
|
||||
let mut logger = LOGGER.lock().unwrap();
|
||||
logger.output = output;
|
||||
|
@ -77,6 +77,9 @@ pub struct RealmConfig {
|
||||
#[serde(rename="use-fuse")]
|
||||
pub use_fuse: Option<bool>,
|
||||
|
||||
#[serde(rename="use-flatpak")]
|
||||
pub use_flatpak: Option<bool>,
|
||||
|
||||
#[serde(rename="use-gpu")]
|
||||
pub use_gpu: Option<bool>,
|
||||
|
||||
@ -201,6 +204,7 @@ impl RealmConfig {
|
||||
wayland_socket: Some("wayland-0".to_string()),
|
||||
use_kvm: Some(false),
|
||||
use_fuse: Some(false),
|
||||
use_flatpak: Some(false),
|
||||
use_gpu: Some(false),
|
||||
use_gpu_card0: Some(false),
|
||||
use_network: Some(true),
|
||||
@ -233,6 +237,7 @@ impl RealmConfig {
|
||||
wayland_socket: None,
|
||||
use_kvm: None,
|
||||
use_fuse: None,
|
||||
use_flatpak: None,
|
||||
use_gpu: None,
|
||||
use_gpu_card0: None,
|
||||
use_network: None,
|
||||
@ -267,6 +272,14 @@ impl RealmConfig {
|
||||
self.bool_value(|c| c.use_fuse)
|
||||
}
|
||||
|
||||
/// If `true` flatpak directory will be mounted into realm
|
||||
/// and a desktop file will be created to launch gnome-software
|
||||
///
|
||||
pub fn flatpak(&self) -> bool {
|
||||
self.bool_value(|c| c.use_flatpak)
|
||||
}
|
||||
|
||||
|
||||
/// If `true` render node device /dev/dri/renderD128 will be added to realm.
|
||||
///
|
||||
/// This enables hardware graphics acceleration in realm.
|
||||
|
@ -60,7 +60,7 @@ impl <'a> RealmLauncher <'a> {
|
||||
if config.kvm() {
|
||||
self.add_device("/dev/kvm");
|
||||
}
|
||||
if config.fuse() {
|
||||
if config.fuse() || config.flatpak() {
|
||||
self.add_device("/dev/fuse");
|
||||
}
|
||||
|
||||
@ -153,6 +153,10 @@ impl <'a> RealmLauncher <'a> {
|
||||
writeln!(s, "BindReadOnly=/run/user/1000/{}:/run/user/host/wayland-0", config.wayland_socket())?;
|
||||
}
|
||||
|
||||
if config.flatpak() {
|
||||
writeln!(s, "BindReadOnly={}:/var/lib/flatpak", self.realm.base_path_file("flatpak").display())?;
|
||||
}
|
||||
|
||||
for bind in config.extra_bindmounts() {
|
||||
if Self::is_valid_bind_item(bind) {
|
||||
writeln!(s, "Bind={}", bind)?;
|
||||
|
@ -4,6 +4,8 @@ use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use posix_acl::{ACL_EXECUTE, ACL_READ, PosixACL, Qualifier};
|
||||
|
||||
use crate::{Mountpoint, Result, Realms, RealmFS, Realm, util};
|
||||
use crate::flatpak::GnomeSoftwareLauncher;
|
||||
use crate::flatpak::setup::FlatpakSetup;
|
||||
use crate::realm::pidmapper::{PidLookupResult, PidMapper};
|
||||
use crate::realmfs::realmfs_set::RealmFSSet;
|
||||
|
||||
@ -21,6 +23,7 @@ struct Inner {
|
||||
events: RealmEventListener,
|
||||
realms: Realms,
|
||||
realmfs_set: RealmFSSet,
|
||||
pid_mapper: PidMapper,
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
@ -28,7 +31,8 @@ impl Inner {
|
||||
let events = RealmEventListener::new();
|
||||
let realms = Realms::load()?;
|
||||
let realmfs_set = RealmFSSet::load()?;
|
||||
Ok(Inner { events, realms, realmfs_set })
|
||||
let pid_mapper = PidMapper::new()?;
|
||||
Ok(Inner { events, realms, realmfs_set, pid_mapper })
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,6 +234,10 @@ impl RealmManager {
|
||||
self.ensure_run_media_directory()?;
|
||||
}
|
||||
|
||||
if realm.config().flatpak() {
|
||||
FlatpakSetup::new(realm).setup()?;
|
||||
}
|
||||
|
||||
self.systemd.start_realm(realm, &rootfs)?;
|
||||
|
||||
self.create_realm_namefile(realm)?;
|
||||
@ -268,6 +276,15 @@ impl RealmManager {
|
||||
self.run_in_realm(realm, &["/usr/bin/ln", "-s", "/run/user/host/wayland-0", "/run/user/1000/wayland-0"], false)
|
||||
}
|
||||
|
||||
fn stop_gnome_software_sandbox(&self, realm: &Realm) -> Result<()> {
|
||||
let launcher = GnomeSoftwareLauncher::new(realm.clone())?;
|
||||
if launcher.is_running() {
|
||||
info!("Stopping GNOME Software sandbox for {}", realm.name());
|
||||
launcher.quit()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop_realm(&self, realm: &Realm) -> Result<()> {
|
||||
if !realm.is_active() {
|
||||
info!("ignoring stop request on realm '{}' which is not running", realm.name());
|
||||
@ -276,6 +293,12 @@ impl RealmManager {
|
||||
|
||||
info!("Stopping realm {}", realm.name());
|
||||
|
||||
if realm.config().flatpak() {
|
||||
if let Err(err) = self.stop_gnome_software_sandbox(realm) {
|
||||
warn!("Error stopping GNOME Software sandbox: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
realm.set_active(false);
|
||||
self.systemd.stop_realm(realm)?;
|
||||
realm.cleanup_rootfs();
|
||||
@ -335,8 +358,8 @@ impl RealmManager {
|
||||
}
|
||||
|
||||
pub fn realm_by_pid(&self, pid: u32) -> PidLookupResult {
|
||||
let mapper = PidMapper::new(self.active_realms(false));
|
||||
mapper.lookup_pid(pid as libc::pid_t)
|
||||
let realms = self.realm_list();
|
||||
self.inner_mut().pid_mapper.lookup_pid(pid as libc::pid_t, realms)
|
||||
}
|
||||
|
||||
pub fn rescan_realms(&self) -> Result<(Vec<Realm>,Vec<Realm>)> {
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::ffi::OsStr;
|
||||
use procfs::process::Process;
|
||||
use crate::Realm;
|
||||
use crate::{Result, Realm};
|
||||
use crate::flatpak::{SANDBOX_STATUS_FILE_DIRECTORY, SandboxStatus};
|
||||
|
||||
pub enum PidLookupResult {
|
||||
Unknown,
|
||||
@ -9,14 +10,15 @@ pub enum PidLookupResult {
|
||||
}
|
||||
|
||||
pub struct PidMapper {
|
||||
active_realms: Vec<Realm>,
|
||||
sandbox_status: SandboxStatus,
|
||||
my_pid_ns_id: Option<u64>,
|
||||
}
|
||||
|
||||
impl PidMapper {
|
||||
pub fn new(active_realms: Vec<Realm>) -> Self {
|
||||
pub fn new() -> Result<Self> {
|
||||
let sandbox_status = SandboxStatus::load(SANDBOX_STATUS_FILE_DIRECTORY)?;
|
||||
let my_pid_ns_id = Self::self_pid_namespace_id();
|
||||
PidMapper { active_realms, my_pid_ns_id }
|
||||
Ok(PidMapper { sandbox_status, my_pid_ns_id })
|
||||
}
|
||||
|
||||
fn read_process(pid: libc::pid_t) -> Option<Process> {
|
||||
@ -72,7 +74,30 @@ impl PidMapper {
|
||||
Self::read_process(ppid)
|
||||
}
|
||||
|
||||
pub fn lookup_pid(&self, pid: libc::pid_t) -> PidLookupResult {
|
||||
fn refresh_sandbox_status(&mut self) -> Result<()> {
|
||||
if self.sandbox_status.need_reload() {
|
||||
self.sandbox_status.reload()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn search_sandbox_realms(&mut self, pid_ns: u64, realms: &[Realm]) -> Option<Realm> {
|
||||
if let Err(err) = self.refresh_sandbox_status() {
|
||||
warn!("error reloading sandbox status directory: {}", err);
|
||||
return None;
|
||||
}
|
||||
|
||||
for r in realms {
|
||||
if let Some(status) = self.sandbox_status.realm_status(r) {
|
||||
if status.pid_namespace() == pid_ns {
|
||||
return Some(r.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn lookup_pid(&mut self, pid: libc::pid_t, realms: Vec<Realm>) -> PidLookupResult {
|
||||
const MAX_PARENT_SEARCH: i32 = 8;
|
||||
let mut n = 0;
|
||||
|
||||
@ -92,13 +117,17 @@ impl PidMapper {
|
||||
return PidLookupResult::Citadel;
|
||||
}
|
||||
|
||||
if let Some(realm) = self.active_realms.iter()
|
||||
.find(|r| r.has_pid_ns(pid_ns_id))
|
||||
if let Some(realm) = realms.iter()
|
||||
.find(|r| r.is_active() && r.has_pid_ns(pid_ns_id))
|
||||
.cloned()
|
||||
{
|
||||
return PidLookupResult::Realm(realm)
|
||||
}
|
||||
|
||||
if let Some(r) = self.search_sandbox_realms(pid_ns_id, &realms) {
|
||||
return PidLookupResult::Realm(r)
|
||||
}
|
||||
|
||||
proc = match Self::parent_process(proc) {
|
||||
Some(proc) => proc,
|
||||
None => return PidLookupResult::Unknown,
|
||||
@ -108,5 +137,4 @@ impl PidMapper {
|
||||
}
|
||||
PidLookupResult::Unknown
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -279,6 +279,9 @@ impl Realm {
|
||||
symlink::write(&rootfs, self.rootfs_symlink(), false)?;
|
||||
symlink::write(mountpoint.path(), self.realmfs_mountpoint_symlink(), false)?;
|
||||
symlink::write(self.base_path().join("home"), self.run_path().join("home"), false)?;
|
||||
if self.config().flatpak() {
|
||||
symlink::write(self.base_path().join("flatpak"), self.run_path().join("flatpak"), false)?;
|
||||
}
|
||||
|
||||
Ok(rootfs)
|
||||
}
|
||||
@ -300,6 +303,9 @@ impl Realm {
|
||||
Self::remove_symlink(self.realmfs_mountpoint_symlink());
|
||||
Self::remove_symlink(self.rootfs_symlink());
|
||||
Self::remove_symlink(self.run_path().join("home"));
|
||||
if self.config().flatpak() {
|
||||
Self::remove_symlink(self.run_path().join("flatpak"));
|
||||
}
|
||||
|
||||
if let Err(e) = fs::remove_dir(self.run_path()) {
|
||||
warn!("failed to remove run directory {}: {}", self.run_path().display(), e);
|
||||
|
@ -31,6 +31,28 @@ impl Systemd {
|
||||
if realm.config().ephemeral_home() {
|
||||
self.setup_ephemeral_home(realm)?;
|
||||
}
|
||||
if realm.config().flatpak() {
|
||||
self.setup_flatpak_workaround(realm)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// What even is this??
|
||||
//
|
||||
// Good question.
|
||||
//
|
||||
// https://bugzilla.redhat.com/show_bug.cgi?id=2210335#c10
|
||||
//
|
||||
fn setup_flatpak_workaround(&self, realm: &Realm) -> Result<()> {
|
||||
let commands = &[
|
||||
vec!["/usr/bin/mount", "-m", "-t","proc", "proc", "/run/flatpak-workaround/proc"],
|
||||
vec!["/usr/bin/chmod", "700", "/run/flatpak-workaround"],
|
||||
];
|
||||
|
||||
for cmd in commands {
|
||||
Self::machinectl_shell(realm, cmd, "root", false, true)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -23,8 +23,8 @@ impl ResizeSize {
|
||||
|
||||
pub fn gigs(n: usize) -> Self {
|
||||
ResizeSize(BLOCKS_PER_GIG * n)
|
||||
|
||||
}
|
||||
|
||||
pub fn megs(n: usize) -> Self {
|
||||
ResizeSize(BLOCKS_PER_MEG * n)
|
||||
}
|
||||
@ -45,8 +45,8 @@ impl ResizeSize {
|
||||
self.0 / BLOCKS_PER_MEG
|
||||
}
|
||||
|
||||
/// If the RealmFS needs to be resized to a larger size, returns the
|
||||
/// recommended size.
|
||||
/// If the RealmFS has less than `AUTO_RESIZE_MINIMUM_FREE` blocks free then choose a new
|
||||
/// size to resize the filesystem to and return it. Otherwise, return `None`
|
||||
pub fn auto_resize_size(realmfs: &RealmFS) -> Option<ResizeSize> {
|
||||
let sb = match Superblock::load(realmfs.path(), 4096) {
|
||||
Ok(sb) => sb,
|
||||
@ -56,22 +56,37 @@ impl ResizeSize {
|
||||
},
|
||||
};
|
||||
|
||||
sb.free_block_count();
|
||||
let free_blocks = sb.free_block_count() as usize;
|
||||
if free_blocks < AUTO_RESIZE_MINIMUM_FREE.nblocks() {
|
||||
let metainfo_nblocks = realmfs.metainfo().nblocks() + 1;
|
||||
let increase_multiple = metainfo_nblocks / AUTO_RESIZE_INCREASE_SIZE.nblocks();
|
||||
let grow_size = (increase_multiple + 1) * AUTO_RESIZE_INCREASE_SIZE.nblocks();
|
||||
let mask = grow_size - 1;
|
||||
let grow_blocks = (free_blocks + mask) & !mask;
|
||||
Some(ResizeSize::blocks(grow_blocks))
|
||||
if free_blocks >= AUTO_RESIZE_MINIMUM_FREE.nblocks() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let metainfo_nblocks = realmfs.metainfo().nblocks();
|
||||
|
||||
if metainfo_nblocks >= AUTO_RESIZE_INCREASE_SIZE.nblocks() {
|
||||
return Some(ResizeSize::blocks(metainfo_nblocks + AUTO_RESIZE_INCREASE_SIZE.nblocks()))
|
||||
}
|
||||
|
||||
// If current size is under 4GB (AUTO_RESIZE_INCREASE_SIZE) and raising size to 4GB will create more than the
|
||||
// minimum free space (1GB) then just do that.
|
||||
if free_blocks + (AUTO_RESIZE_INCREASE_SIZE.nblocks() - metainfo_nblocks) >= AUTO_RESIZE_MINIMUM_FREE.nblocks() {
|
||||
Some(AUTO_RESIZE_INCREASE_SIZE)
|
||||
} else {
|
||||
None
|
||||
// Otherwise for original size under 4GB, since raising to 4GB is not enough,
|
||||
// raise size to 8GB
|
||||
Some(ResizeSize::blocks(AUTO_RESIZE_INCREASE_SIZE.nblocks() * 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SUPERBLOCK_SIZE: usize = 1024;
|
||||
|
||||
/// An EXT4 superblock structure.
|
||||
///
|
||||
/// A class for reading the first superblock from an EXT4 filesystem
|
||||
/// and parsing the Free Block Count field. No other fields are parsed
|
||||
/// since this is the only information needed for the resize operation.
|
||||
///
|
||||
pub struct Superblock([u8; SUPERBLOCK_SIZE]);
|
||||
|
||||
impl Superblock {
|
||||
|
@ -76,8 +76,6 @@ impl <'a> Update<'a> {
|
||||
}
|
||||
self.realmfs.copy_image_file(self.target())?;
|
||||
|
||||
self.truncate_verity()?;
|
||||
self.resize_image_file()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -115,9 +113,8 @@ impl <'a> Update<'a> {
|
||||
}
|
||||
|
||||
// Return size of image file in blocks based on metainfo `nblocks` field.
|
||||
// Include header block in count so add one block
|
||||
fn metainfo_nblock_size(&self) -> usize {
|
||||
self.realmfs.metainfo().nblocks() + 1
|
||||
self.realmfs.metainfo().nblocks()
|
||||
}
|
||||
|
||||
fn unmount_update_image(&mut self) {
|
||||
@ -159,7 +156,8 @@ impl <'a> Update<'a> {
|
||||
}
|
||||
|
||||
fn set_target_len(&self, nblocks: usize) -> Result<()> {
|
||||
let len = (nblocks * BLOCK_SIZE) as u64;
|
||||
// add one block for header block
|
||||
let len = ((nblocks + 1) * BLOCK_SIZE) as u64;
|
||||
let f = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&self.target)
|
||||
@ -176,7 +174,7 @@ impl <'a> Update<'a> {
|
||||
|
||||
if self.realmfs.header().has_flag(ImageHeader::FLAG_HASH_TREE) {
|
||||
self.set_target_len(metainfo_nblocks)?;
|
||||
} else if file_nblocks > metainfo_nblocks {
|
||||
} else if file_nblocks > (metainfo_nblocks + 1) {
|
||||
warn!("RealmFS image size was greater than length indicated by metainfo.nblocks but FLAG_HASH_TREE not set");
|
||||
}
|
||||
Ok(())
|
||||
@ -185,7 +183,7 @@ impl <'a> Update<'a> {
|
||||
// If resize was requested, adjust size of update copy of image file.
|
||||
fn resize_image_file(&self) -> Result<()> {
|
||||
let nblocks = match self.resize {
|
||||
Some(rs) => rs.nblocks() + 1,
|
||||
Some(rs) => rs.nblocks(),
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
@ -224,7 +222,7 @@ impl <'a> Update<'a> {
|
||||
fn seal(&mut self) -> Result<()> {
|
||||
let nblocks = match self.resize {
|
||||
Some(rs) => rs.nblocks(),
|
||||
None => self.metainfo_nblock_size() - 1,
|
||||
None => self.metainfo_nblock_size(),
|
||||
};
|
||||
|
||||
let salt = hex::encode(randombytes(32));
|
||||
@ -232,20 +230,11 @@ impl <'a> Update<'a> {
|
||||
.map_err(context!("failed to create verity context for realmfs update image {:?}", self.target()))?;
|
||||
let output = verity.generate_image_hashtree_with_salt(&salt, nblocks)
|
||||
.map_err(context!("failed to generate dm-verity hashtree for realmfs update image {:?}", self.target()))?;
|
||||
// XXX passes metainfo for nblocks
|
||||
//let output = Verity::new(&self.target).generate_image_hashtree_with_salt(&self.realmfs.metainfo(), &salt)?;
|
||||
|
||||
let root_hash = output.root_hash()
|
||||
.ok_or_else(|| format_err!("no root hash returned from verity format operation"))?;
|
||||
info!("root hash is {}", output.root_hash().unwrap());
|
||||
|
||||
/*
|
||||
let nblocks = match self.resize {
|
||||
Some(rs) => rs.nblocks(),
|
||||
None => self.metainfo_nblock_size() - 1,
|
||||
};
|
||||
|
||||
*/
|
||||
|
||||
info!("Signing new image with user realmfs keys");
|
||||
let metainfo_bytes = RealmFS::generate_metainfo(self.realmfs.name(), nblocks, salt.as_str(), root_hash);
|
||||
let keys = self.realmfs.sealing_keys().expect("No sealing keys");
|
||||
|
@ -420,8 +420,11 @@ fn compare_images(a: Option<ResourceImage>, b: ResourceImage) -> Result<Resource
|
||||
None => return Ok(b),
|
||||
};
|
||||
|
||||
let ver_a = a.metainfo().version();
|
||||
let ver_b = b.metainfo().version();
|
||||
let bind_a = a.metainfo();
|
||||
let bind_b = b.metainfo();
|
||||
|
||||
let ver_a = bind_a.version();
|
||||
let ver_b = bind_b.version();
|
||||
|
||||
if ver_a > ver_b {
|
||||
Ok(a)
|
||||
|
97
libcitadel/src/updates.rs
Normal file
97
libcitadel/src/updates.rs
Normal file
@ -0,0 +1,97 @@
|
||||
use std::fmt;
|
||||
use std::slice::Iter;
|
||||
|
||||
pub const UPDATE_SERVER_HOSTNAME: &str = "update.subgraph.com";
|
||||
|
||||
/// This struct embeds the CitadelVersion datastruct as well as the cryptographic validation of the that information
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CryptoContainerFile {
|
||||
pub serialized_citadel_version: Vec<u8>, // we serialize CitadelVersion
|
||||
pub signature: String, // serialized CitadelVersion gets signed
|
||||
pub signatory: String, // name of org or person who holds the key
|
||||
}
|
||||
|
||||
/// This struct contains the entirety of the logical information needed to decide whether to update or not
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct CitadelVersionStruct {
|
||||
pub client: String,
|
||||
pub channel: String, // dev, prod ...
|
||||
pub component_version: Vec<AvailableComponentVersion>,
|
||||
pub publisher: String, // name of org or person who released this update
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CitadelVersionStruct {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} image with channel {} has components:\n",
|
||||
self.client, self.channel
|
||||
)?;
|
||||
|
||||
for i in &self.component_version {
|
||||
write!(
|
||||
f,
|
||||
"\n{} with version {} at location {}",
|
||||
i.component, i.version, i.file_path
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq, Ord)]
|
||||
pub struct AvailableComponentVersion {
|
||||
pub component: Component, // rootfs, kernel or extra
|
||||
pub version: String, // stored as semver
|
||||
pub file_path: String,
|
||||
}
|
||||
|
||||
impl PartialOrd for AvailableComponentVersion {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
// absolutely require that the components be in the same order in all structs (rootfs, kernel, extra)
|
||||
if &self.component != &other.component {
|
||||
panic!("ComponentVersion comparison failed because comparing different components");
|
||||
}
|
||||
|
||||
Some(
|
||||
semver::Version::parse(&self.version)
|
||||
.unwrap()
|
||||
.cmp(&semver::Version::parse(&other.version).unwrap()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AvailableComponentVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"({} image has version: {})",
|
||||
self.component, self.version
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, clap::ValueEnum)]
|
||||
pub enum Component {
|
||||
Rootfs,
|
||||
Kernel,
|
||||
Extra,
|
||||
}
|
||||
|
||||
impl Component {
|
||||
pub fn iterator() -> Iter<'static, Component> {
|
||||
static COMPONENTS: [Component; 3] =
|
||||
[Component::Rootfs, Component::Kernel, Component::Extra];
|
||||
COMPONENTS.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Component {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Component::Rootfs => write!(f, "rootfs"),
|
||||
Component::Kernel => write!(f, "kernel"),
|
||||
&Component::Extra => write!(f, "extra"),
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ use std::env;
|
||||
use std::fs::{self, File, DirEntry};
|
||||
use std::ffi::CString;
|
||||
use std::io::{self, Seek, Read, BufReader, SeekFrom};
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use walkdir::WalkDir;
|
||||
@ -47,6 +48,12 @@ fn search_path(filename: &str) -> Result<PathBuf> {
|
||||
bail!("could not find {} in $PATH", filename)
|
||||
}
|
||||
|
||||
pub fn append_to_path(p: &Path, s: &str) -> PathBuf {
|
||||
let mut p_osstr = p.as_os_str().to_owned();
|
||||
p_osstr.push(s);
|
||||
p_osstr.into()
|
||||
}
|
||||
|
||||
pub fn ensure_command_exists(cmd: &str) -> Result<()> {
|
||||
let path = Path::new(cmd);
|
||||
if !path.is_absolute() {
|
||||
@ -217,7 +224,8 @@ where
|
||||
///
|
||||
pub fn remove_file(path: impl AsRef<Path>) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
if path.exists() {
|
||||
let is_symlink = fs::symlink_metadata(path).is_ok();
|
||||
if is_symlink || path.exists() {
|
||||
fs::remove_file(path)
|
||||
.map_err(context!("failed to remove file {:?}", path))?;
|
||||
}
|
||||
@ -336,7 +344,6 @@ pub fn is_euid_root() -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn utimes(path: &Path, atime: i64, mtime: i64) -> Result<()> {
|
||||
let cstr = CString::new(path.as_os_str().as_bytes())
|
||||
.expect("path contains null byte");
|
||||
@ -368,9 +375,38 @@ pub fn touch_mtime(path: &Path) -> Result<()> {
|
||||
|
||||
utimes(path, meta.atime(),mtime)?;
|
||||
Ok(())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
pub fn nsenter_netns(netns: &str) -> Result<()> {
|
||||
let mut path = PathBuf::from("/run/netns");
|
||||
path.push(netns);
|
||||
if !path.exists() {
|
||||
bail!("Network namespace '{}' does not exist", netns);
|
||||
}
|
||||
let f = File::open(&path)
|
||||
.map_err(context!("error opening netns file {}", path.display()))?;
|
||||
|
||||
let fd = f.as_raw_fd();
|
||||
|
||||
unsafe {
|
||||
if libc::setns(fd, libc::CLONE_NEWNET) == -1 {
|
||||
let err = io::Error::last_os_error();
|
||||
bail!("failed to setns() into network namespace '{}': {}", netns, err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn drop_privileges(uid: u32, gid: u32) -> Result<()> {
|
||||
unsafe {
|
||||
if libc::setgid(gid) == -1 {
|
||||
let err = io::Error::last_os_error();
|
||||
bail!("failed to call setgid({}): {}", gid, err);
|
||||
|
||||
} else if libc::setuid(uid) == -1 {
|
||||
let err = io::Error::last_os_error();
|
||||
bail!("failed to call setuid({}): {}", uid, err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
@ -4,6 +4,6 @@ StartLimitIntervalSec=0
|
||||
|
||||
[Path]
|
||||
PathChanged=/run/citadel/realms/current/current.realm/rootfs/usr/share/applications
|
||||
PathChanged=/run/citadel/realms/current/current.realm/rootfs/var/lib/flatpak/exports/share/applications
|
||||
PathChanged=/run/citadel/realms/current/current.realm/flatpak/exports/share/applications
|
||||
PathChanged=/run/citadel/realms/current/current.realm/home/.local/share/applications
|
||||
PathChanged=/run/citadel/realms/current/current.realm/home/.local/share/flatpak/exports/share/applications
|
||||
|
21
update-generator/Cargo.toml
Normal file
21
update-generator/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "update_generator"
|
||||
description = "Utility to create updates for citadel's components"
|
||||
version = "0.1.0"
|
||||
authors = ["isa <isa@subgraph.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
ed25519-dalek = {version = "2.1", features = ["rand_core", "pem"]}
|
||||
serde_cbor = "0.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_derive = "1.0"
|
||||
rand = "0.8"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
rpassword = "7.3"
|
||||
semver = "1.0"
|
||||
zeroize = "1.8"
|
||||
sodiumoxide = "0.2"
|
||||
anyhow = "1.0"
|
||||
glob = "0.3"
|
63
update-generator/README.md
Normal file
63
update-generator/README.md
Normal file
@ -0,0 +1,63 @@
|
||||
# Introduction
|
||||
|
||||
This code is used to produce the files needed to update citadel.
|
||||
The citadel-fetch command is installed in citadel and may be run manually to check for updates.
|
||||
|
||||
N.B.
|
||||
This update framework is at the moment not production ready. It defends against arbitrary installation and rollback attacks. It partially defends against endless data attacks.
|
||||
It does not defend against fast-forward or indefinite freeze attacks. This system is vulnerable to key compromise and does not provide a method to update the root key through the update mechanism.
|
||||
Further work needs to be done to prevent these issues. Consider using libraries from the Update Framework (TUF).
|
||||
|
||||
# Usage
|
||||
|
||||
Within the citadel-tools directory, run `cargo run --bin update_generator generate-keypair` to generate a keypair. Replace the existing public key file by moving update_server_key.pub to citadel/meta-citadel/recipes-citadel/citadel-config/files/citadel-fetch/update_server_key.pub .
|
||||
The private key is available in the same directory as keypair.priv.
|
||||
|
||||
Run `cargo run --bin update_generator create-signed-file -r 1.0.0 -k 6.5.3 -e 1.0.0` replacing the version numbers with yours and a version.cbor file will be generated.
|
||||
Alternatively, the `create-signed-file` can be run with no parameters in which case it will read the version number of the available images in build/images and generate a version.cbor automagically.
|
||||
|
||||
Finally, the files generated (version.cbor and images) must be uploaded/ Run `cargo run --bin update_generator upload-to-server` to automatically upload all the files to the server.
|
||||
To perform the upload manually, put this version.cbor file to the server at: {UPDATE_SERVER_NAME}/{CLIENT}/{CHANNEL}/. For example: https://update.subgraph.com/public/dev/ .
|
||||
Upload the image files to {UPDATE_SERVER_NAME}/{CLIENT}/{CHANNEL}/. For example: https://update.subgraph.com/public/dev/rootfs_1.0.0.img
|
||||
|
||||
More information is available in the `update_generator` help menu.
|
||||
|
||||
For debugging, testing and internal dev use, the config.toml file provides a way to change the path on the server where the files are saved by changing the CLIENT and CHANNEL.
|
||||
|
||||
# Basic structure
|
||||
|
||||
We host the files used by this program as a file server. The current hostname is update.subgraph.com.
|
||||
|
||||
An installed citadel server will automatically check for updates by reading the file at https://update.subgraph.com/{CLIENT}/{CHANNEL}/version.cbor
|
||||
|
||||
{CLIENT} will default to "public" but is read from the currently running system.
|
||||
|
||||
{CHANNEL} will default to "prod" but is read from the currently running system.
|
||||
|
||||
The version.cbor file is signed with Subgraph-controlled keys or equivalent client-controlled keys. The corresponding public key will be embedded in the rootfs image during build. The public key is called update_server_key.pub and is stored in /etc/citadel/ .
|
||||
|
||||
# Update File Structure
|
||||
|
||||
## version.cbor file
|
||||
|
||||
The version.cbor file is the only file read during update and contains all information required for the user's system to decide to update or not as well as where the update files are located on the server.
|
||||
This file is merely a container which provides a cryptographic guarantee that the serialized_citadel_version struct (which contains the actual information we need to disseminate) is authentic.
|
||||
|
||||
1. serialized_citadel_version (CitadelVersionStruct): the serialized data containing the actionable information we need to read to make decisions on the update
|
||||
2. signature: the ed25519 signature of the above serialized data
|
||||
3. signatory: the name of the org or person who produced the version.cbor file
|
||||
|
||||
|
||||
## CitadelVersionStruct
|
||||
|
||||
1. client: the Subgraph client making the request
|
||||
2. channel: whether this is a production release or other
|
||||
3. component_version (Vec<ComponentVersion>): a vector containing structures called ComponentVersion which contain the information on each component's version number and the location of the download file of the component
|
||||
4. publisher: the name of the org or person who released this update
|
||||
|
||||
|
||||
## ComponentVersion
|
||||
|
||||
1. component: the name of the image we may update. Either the rootfs, the kernel or the extra image
|
||||
2. version: a string which contains the semver describing the version of the component
|
||||
3. file_path: where on the update server can the file be downloaded from. This is relative to the domain name we are currently fetching from
|
530
update-generator/src/main.rs
Normal file
530
update-generator/src/main.rs
Normal file
@ -0,0 +1,530 @@
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use ed25519_dalek::pkcs8::DecodePublicKey;
|
||||
use ed25519_dalek::pkcs8::EncodePublicKey;
|
||||
use ed25519_dalek::Signature;
|
||||
use ed25519_dalek::Signer;
|
||||
use ed25519_dalek::SigningKey;
|
||||
use ed25519_dalek::VerifyingKey;
|
||||
use ed25519_dalek::KEYPAIR_LENGTH;
|
||||
use glob::glob;
|
||||
use libcitadel::updates::Component;
|
||||
use libcitadel::{updates, ImageHeader};
|
||||
use rand::rngs::OsRng;
|
||||
use sodiumoxide::crypto::pwhash;
|
||||
use sodiumoxide::crypto::secretbox;
|
||||
use sodiumoxide::crypto::stream;
|
||||
use std::env;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
const SALSA_NONCE: &[u8] = &[
|
||||
116, 138, 142, 103, 234, 105, 192, 48, 117, 29, 150, 214, 106, 116, 195, 64, 120, 251, 94, 20,
|
||||
212, 118, 125, 189,
|
||||
];
|
||||
|
||||
const PASSWORD_SALT: &[u8] = &[
|
||||
18, 191, 168, 237, 156, 199, 54, 43, 122, 165, 35, 9, 89, 106, 36, 209, 145, 161, 90, 2, 121,
|
||||
51, 242, 182, 14, 245, 47, 253, 237, 153, 251, 219,
|
||||
];
|
||||
|
||||
const LAST_RESORT_CLIENT: &str = "public";
|
||||
const LAST_RESORT_CHANNEL: &str = "prod";
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Generate the file we use to inform clients of updates")]
|
||||
#[command(author, about, long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[command(arg_required_else_help = true)]
|
||||
enum Commands {
|
||||
/// Generate a keypair to be used to sign version file. You will be asked to provide a mandatory password.
|
||||
GenerateKeypair {
|
||||
/// keypair filepath to save to
|
||||
#[arg(short, long)]
|
||||
keypair_filepath: Option<String>,
|
||||
},
|
||||
|
||||
/// Generate the complete cbor file. If no components are passed, generate by reading image of each component
|
||||
CreateSignedFile {
|
||||
/// rootfs image semver version
|
||||
#[arg(short, long)]
|
||||
rootfs_image_version: Option<String>,
|
||||
/// kernel image semver version
|
||||
#[arg(short, long)]
|
||||
kernel_image_version: Option<String>,
|
||||
#[arg(short, long)]
|
||||
extra_image_version: Option<String>,
|
||||
/// keypair filepath
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
path_keypair: Option<String>,
|
||||
/// command output complete filepath
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
versionfile_filepath: Option<String>,
|
||||
},
|
||||
|
||||
/// Verify that the version file is correctly signed
|
||||
VerifySignature {
|
||||
/// public key filepath
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
publickey_filepath: Option<String>,
|
||||
/// command output complete filepath
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
versionfile_filepath: Option<String>,
|
||||
},
|
||||
|
||||
UploadToServer {
|
||||
#[arg(long)]
|
||||
component: Option<updates::Component>,
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match &cli.command {
|
||||
Some(Commands::GenerateKeypair { keypair_filepath }) => {
|
||||
generate_keypair(keypair_filepath).context("Failed to generate keypair")?
|
||||
}
|
||||
Some(Commands::CreateSignedFile {
|
||||
path_keypair,
|
||||
rootfs_image_version,
|
||||
kernel_image_version,
|
||||
extra_image_version,
|
||||
versionfile_filepath,
|
||||
}) => create_signed_version_file(
|
||||
path_keypair,
|
||||
rootfs_image_version,
|
||||
kernel_image_version,
|
||||
extra_image_version,
|
||||
versionfile_filepath,
|
||||
)
|
||||
.context("Failed to create signed file")?,
|
||||
Some(Commands::VerifySignature {
|
||||
publickey_filepath,
|
||||
versionfile_filepath,
|
||||
}) => verify_version_signature(publickey_filepath, versionfile_filepath)
|
||||
.context("Failed to verify signature")?,
|
||||
Some(Commands::UploadToServer { component, path }) => {
|
||||
upload_components_to_server(component, path)
|
||||
.context("Failed to upload to the server")?
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_keypair(keypair_filepath: &Option<String>) -> Result<()> {
|
||||
// if the user did not pass a path, we save key files to current directory
|
||||
let keypair_filepath = &keypair_filepath.clone().unwrap_or_else(|| ".".to_string());
|
||||
let path = std::path::Path::new(keypair_filepath);
|
||||
|
||||
let mut password;
|
||||
|
||||
loop {
|
||||
// get passphrase used to encrypt key from user
|
||||
password = rpassword::prompt_password(
|
||||
"Please enter the passphrase we will use to encrypt the private key with: ",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let password_confirm = rpassword::prompt_password("Retype same passphrase: ").unwrap();
|
||||
|
||||
if password != password_confirm {
|
||||
println!("\nPassphrases did not match. Please try again")
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// generate keypair
|
||||
let mut csprng = OsRng;
|
||||
let signing_key: SigningKey = SigningKey::generate(&mut csprng);
|
||||
|
||||
let keypair_fp: PathBuf;
|
||||
let publickey_fp: PathBuf;
|
||||
|
||||
if path.is_dir() {
|
||||
keypair_fp = path.join("keypair.priv");
|
||||
publickey_fp = path.join("update_server_key.pub");
|
||||
} else {
|
||||
keypair_fp = path.to_path_buf();
|
||||
publickey_fp = path.parent().unwrap().join("update_server_key.pub");
|
||||
}
|
||||
|
||||
let mut keyfile = std::fs::File::create(&keypair_fp)?;
|
||||
let mut public_key = std::fs::File::create(&publickey_fp)?;
|
||||
|
||||
// encrypt private key
|
||||
let mut k = secretbox::Key([0; secretbox::KEYBYTES]);
|
||||
|
||||
let secretbox::Key(ref mut kb) = k;
|
||||
let password_hash = pwhash::derive_key(
|
||||
kb,
|
||||
password.as_bytes(),
|
||||
&sodiumoxide::crypto::pwhash::scryptsalsa208sha256::Salt::from_slice(PASSWORD_SALT)
|
||||
.unwrap(),
|
||||
pwhash::OPSLIMIT_INTERACTIVE,
|
||||
pwhash::MEMLIMIT_INTERACTIVE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let plaintext = signing_key.to_keypair_bytes();
|
||||
let key = sodiumoxide::crypto::stream::xsalsa20::Key::from_slice(password_hash)
|
||||
.expect("failed to unwrap key");
|
||||
let nonce = sodiumoxide::crypto::stream::xsalsa20::Nonce::from_slice(SALSA_NONCE)
|
||||
.expect("failed to unwrap nonce");
|
||||
|
||||
// encrypt the plaintext
|
||||
let ciphertext = stream::stream_xor(&plaintext, &nonce, &key);
|
||||
|
||||
keyfile.write_all(&ciphertext)?;
|
||||
public_key.write_all(
|
||||
&signing_key
|
||||
.verifying_key()
|
||||
.to_public_key_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
password.zeroize();
|
||||
|
||||
println!(
|
||||
"Generated the keypair and wrote to {}. The public key is here: {}",
|
||||
keypair_fp.display(),
|
||||
publickey_fp.display()
|
||||
);
|
||||
println!(
|
||||
"You may now move the public key from {} to the citadel build path at citadel/meta-citadel/recipes-citadel/citadel-config/files/citadel-fetch/update_server_key.pub",
|
||||
publickey_fp.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_signed_version_file(
|
||||
signing_key_path: &Option<String>,
|
||||
citadel_rootfs_version: &Option<String>,
|
||||
citadel_kernel_version: &Option<String>,
|
||||
citadel_extra_version: &Option<String>,
|
||||
versionfile_filepath: &Option<String>,
|
||||
) -> Result<()> {
|
||||
let rootfs_version = match citadel_rootfs_version {
|
||||
Some(v) => semver::Version::parse(v)
|
||||
.expect("Error: Failed to parse rootfs semver version")
|
||||
.to_string(),
|
||||
None => get_imageheader_version(&Component::Rootfs).unwrap_or(String::from("0.0.0")),
|
||||
};
|
||||
|
||||
let kernel_version = match citadel_kernel_version {
|
||||
Some(v) => semver::Version::parse(v)
|
||||
.expect("Error: Failed to parse kernel semver version")
|
||||
.to_string(),
|
||||
None => get_imageheader_version(&Component::Kernel).unwrap_or(String::from("0.0.0")),
|
||||
};
|
||||
|
||||
let extra_version = match citadel_extra_version {
|
||||
Some(v) => semver::Version::parse(v)
|
||||
.expect("Error: Failed to parse extra semver version")
|
||||
.to_string(),
|
||||
None => get_imageheader_version(&Component::Extra).unwrap_or(String::from("0.0.0")),
|
||||
};
|
||||
|
||||
let rootfs_path = get_component_path(&Component::Rootfs);
|
||||
let channel;
|
||||
|
||||
if rootfs_path.is_ok() {
|
||||
channel = match ImageHeader::from_file(rootfs_path?) {
|
||||
Ok(image_header) => image_header.metainfo().channel().to_string(),
|
||||
Err(_) => env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
|
||||
};
|
||||
} else {
|
||||
channel = env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string());
|
||||
}
|
||||
|
||||
let component_version_vector = vec![
|
||||
updates::AvailableComponentVersion {
|
||||
component: updates::Component::Rootfs,
|
||||
version: rootfs_version.clone(),
|
||||
file_path: format!(
|
||||
"{}/{}/{}_{}.img",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
channel,
|
||||
"rootfs",
|
||||
rootfs_version
|
||||
)
|
||||
.to_string(),
|
||||
},
|
||||
updates::AvailableComponentVersion {
|
||||
component: updates::Component::Kernel,
|
||||
version: kernel_version.clone(),
|
||||
file_path: format!(
|
||||
"{}/{}/{}_{}.img",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
channel,
|
||||
"kernel",
|
||||
kernel_version
|
||||
)
|
||||
.to_string(),
|
||||
},
|
||||
updates::AvailableComponentVersion {
|
||||
component: updates::Component::Extra,
|
||||
version: extra_version.clone(),
|
||||
file_path: format!(
|
||||
"{}/{}/{}_{}.img",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
channel,
|
||||
"extra",
|
||||
extra_version
|
||||
)
|
||||
.to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
// generate version file
|
||||
let citadel_version = updates::CitadelVersionStruct {
|
||||
client: env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
channel: channel.to_string(),
|
||||
component_version: component_version_vector,
|
||||
publisher: "Subgraph".to_string(),
|
||||
};
|
||||
|
||||
let fp = match signing_key_path {
|
||||
Some(fp) => Path::new(fp),
|
||||
None => Path::new("keypair.priv"),
|
||||
};
|
||||
|
||||
// serialized to cbor
|
||||
let serialized_citadel_version = serde_cbor::to_vec(&citadel_version)?;
|
||||
|
||||
// get signing_key_bytes from the file passed
|
||||
let mut keyfile = std::fs::File::open(&fp)?;
|
||||
let mut buf = [0; KEYPAIR_LENGTH];
|
||||
keyfile.read_exact(&mut buf)?;
|
||||
|
||||
// prompt user for keypair decryption password
|
||||
let mut password =
|
||||
rpassword::prompt_password("Please enter the passphrase used to decrypt the private key: ")
|
||||
.unwrap();
|
||||
|
||||
// decrypt private key
|
||||
let mut k = secretbox::Key([0; secretbox::KEYBYTES]);
|
||||
|
||||
let secretbox::Key(ref mut kb) = k;
|
||||
let password_hash = pwhash::derive_key(
|
||||
kb,
|
||||
password.as_bytes(),
|
||||
&sodiumoxide::crypto::pwhash::scryptsalsa208sha256::Salt::from_slice(PASSWORD_SALT)
|
||||
.unwrap(),
|
||||
pwhash::OPSLIMIT_INTERACTIVE,
|
||||
pwhash::MEMLIMIT_INTERACTIVE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let key = sodiumoxide::crypto::stream::xsalsa20::Key::from_slice(password_hash)
|
||||
.expect("failed to unwrap key");
|
||||
let nonce = sodiumoxide::crypto::stream::xsalsa20::Nonce::from_slice(SALSA_NONCE)
|
||||
.expect("failed to unwrap nonce");
|
||||
|
||||
// decrypt the ciphertext
|
||||
let plaintext = stream::stream_xor(&buf, &nonce, &key);
|
||||
|
||||
let signing_key = SigningKey::from_keypair_bytes(plaintext[0..64].try_into()?)?;
|
||||
|
||||
// sign serialized_citadel_version for inclusion in version_file
|
||||
let signature: Signature = signing_key.sign(&serialized_citadel_version);
|
||||
|
||||
// generate signature of citadel_version cbor format (signed)
|
||||
let version_file = updates::CryptoContainerFile {
|
||||
serialized_citadel_version,
|
||||
signature: signature.to_string(),
|
||||
signatory: "Subgraph".to_string(),
|
||||
};
|
||||
|
||||
let vf_fp = match versionfile_filepath {
|
||||
Some(vf_fp) => {
|
||||
if Path::new(vf_fp).is_dir() {
|
||||
Path::new(vf_fp).join("version.cbor")
|
||||
} else {
|
||||
Path::new(vf_fp).to_path_buf()
|
||||
}
|
||||
}
|
||||
None => Path::new("version.cbor").to_path_buf(),
|
||||
};
|
||||
|
||||
let outfile = std::fs::File::create(&vf_fp)?;
|
||||
|
||||
serde_cbor::to_writer(outfile, &version_file)?;
|
||||
|
||||
password.zeroize();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate that the completed version file correctly verifies given the self-embedded signature and public key
|
||||
fn verify_version_signature(
|
||||
pubkey_filepath: &Option<String>,
|
||||
versionfile_filepath: &Option<String>,
|
||||
) -> Result<()> {
|
||||
let pub_fp = match pubkey_filepath {
|
||||
Some(pub_fp) => Path::new(pub_fp),
|
||||
None => Path::new("update_server_key.pub"),
|
||||
};
|
||||
|
||||
let version_fp = match versionfile_filepath {
|
||||
Some(version_fp) => Path::new(version_fp),
|
||||
None => Path::new("version.cbor"),
|
||||
};
|
||||
|
||||
let mut pubkey_file = std::fs::File::open(&pub_fp)?;
|
||||
|
||||
let mut buf = String::new();
|
||||
pubkey_file.read_to_string(&mut buf)?;
|
||||
|
||||
let verifying_key = VerifyingKey::from_public_key_pem(&buf)?;
|
||||
|
||||
let version_file = &std::fs::File::open(version_fp)?;
|
||||
let crypto_container_struct: updates::CryptoContainerFile =
|
||||
serde_cbor::from_reader(version_file)?;
|
||||
|
||||
let citadel_version_struct: updates::CitadelVersionStruct =
|
||||
serde_cbor::from_slice(&crypto_container_struct.serialized_citadel_version)?;
|
||||
|
||||
let signature = ed25519_dalek::Signature::from_str(&crypto_container_struct.signature)?;
|
||||
|
||||
match verifying_key.verify_strict(
|
||||
&crypto_container_struct.serialized_citadel_version,
|
||||
&signature,
|
||||
) {
|
||||
Ok(_) => println!("Everythin ok. Signature verified correctly"),
|
||||
Err(e) => panic!(
|
||||
"Error: Signature was not able to be verified! Threw error: {}",
|
||||
e
|
||||
),
|
||||
}
|
||||
|
||||
println!(
|
||||
"The destructured version file contains the following information:\n{}",
|
||||
citadel_version_struct
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Make sure to add your ssh key to the "updates" user on the server
|
||||
fn send_with_scp(from: &PathBuf, to: &PathBuf) -> Result<()> {
|
||||
Command::new("scp")
|
||||
.arg("-P 22514")
|
||||
.arg(from)
|
||||
.arg(format!(
|
||||
"updates@{}:/updates/files/{}",
|
||||
updates::UPDATE_SERVER_HOSTNAME,
|
||||
to.to_string_lossy()
|
||||
))
|
||||
.spawn()
|
||||
.context("scp command failed to run")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_imageheader_version(component: &Component) -> Result<String> {
|
||||
let version = match ImageHeader::from_file(get_component_path(component)?) {
|
||||
Ok(image_header) => image_header.metainfo().version().to_string(),
|
||||
Err(_) => String::from("0.0.0"),
|
||||
};
|
||||
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
fn get_component_path(component: &updates::Component) -> Result<PathBuf> {
|
||||
let mut gl = match component {
|
||||
Component::Rootfs => glob("../build/images/citadel-rootfs*.img")?,
|
||||
Component::Kernel => glob("../build/images/citadel-kernel*.img")?,
|
||||
Component::Extra => glob("../build/images/citadel-extra*.img")?,
|
||||
};
|
||||
|
||||
let first = gl.nth(0).context(format!(
|
||||
"Failed to find citadel-{}*.img in ../build/images/",
|
||||
component
|
||||
))?;
|
||||
|
||||
Ok(PathBuf::from(first?))
|
||||
}
|
||||
|
||||
fn upload_all_components() -> Result<()> {
|
||||
for component in updates::Component::iterator() {
|
||||
upload_component(component, &None)?
|
||||
}
|
||||
upload_cbor()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn upload_component(component: &updates::Component, path: &Option<PathBuf>) -> Result<()> {
|
||||
let final_path;
|
||||
|
||||
// uggliest if statement i've ever written
|
||||
// if path was passed to this function
|
||||
if let Some(p) = path {
|
||||
final_path = p.to_path_buf();
|
||||
} else {
|
||||
// if the path wasn't passed to this function, search for the component path
|
||||
if let Ok(p) = get_component_path(&component) {
|
||||
final_path = p;
|
||||
} else {
|
||||
// if path was not passed and we failed to locate the component's path
|
||||
println!(
|
||||
"We failed to find the {} image we were looking for... Skipping...",
|
||||
component
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let image_header = libcitadel::ImageHeader::from_file(&final_path)?;
|
||||
|
||||
let to = PathBuf::from(format!(
|
||||
"{}/{}/{}_{}.img",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
|
||||
component,
|
||||
image_header.metainfo().version()
|
||||
));
|
||||
send_with_scp(&final_path, &to)
|
||||
}
|
||||
|
||||
// If no parameters are passed to this command, upload all components regardless of version on the server
|
||||
// If a path is passed, upload that image to the server regardless version.
|
||||
fn upload_components_to_server(
|
||||
component: &Option<updates::Component>,
|
||||
path: &Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
// check if a component is passed to this function:
|
||||
match component {
|
||||
Some(comp) => upload_component(comp, path)?, // This function handles the option of not passing a path
|
||||
None => upload_all_components()?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn upload_cbor() -> Result<()> {
|
||||
send_with_scp(
|
||||
&PathBuf::from("version.cbor"),
|
||||
&PathBuf::from(format!(
|
||||
"{}/{}/version.cbor",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
|
||||
)),
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user