1
0
forked from brl/citadel-tools

34 Commits

Author SHA1 Message Date
isa
8e80644d4c Implement TUF updates 2026-01-13 23:25:08 -05:00
Bruce Leidl
3a3d5c3b9b Improvements to RealmFS updates and resizing
1. Ensure resized image is resealed again
2. Add property to RealmFS dbus object to indicate if update is in
   progress
3. Notify that free space may have changed when committing an update
4. Implement ResizeGrowBy dbus method for RealmFS
2025-11-03 15:41:48 +00:00
Bruce Leidl
be413476b2 Big refactor and changes to fork realmfs and create realm 2025-10-26 19:51:40 +00:00
Bruce Leidl
87575e396c Correctly report the number of allocated blocks 2025-10-26 19:49:27 +00:00
Bruce Leidl
d4035cb9c3 Add a constant for block size 2025-10-26 19:45:36 +00:00
Bruce Leidl
df6e0de7c0 Fix arguments to realmfs tool 2025-10-26 14:12:27 +00:00
Bruce Leidl
3966d1d753 Fixed a couple of bugs related to forking realmfs.
Make sure new RealmFS instance has manager assigned.

Check for stale header information when activating RealmFS
2025-10-26 14:09:56 +00:00
Bruce Leidl
ba305af893 Removed some old code and added a warning.
Warn if an entry in the realmfs directory fails to process.
2025-10-26 14:08:03 +00:00
Bruce Leidl
9a273b78ff Prevent corruption of updated RealmFS
Make sure that the mounted update filesystem cannot
be written to before generating the dm-verity data.
2025-10-26 14:05:34 +00:00
isa
39ac0948ef Correct timezone setting 2025-08-20 17:43:44 +00:00
isa
1b4a780225 Fix unnecessary public scope 2025-08-20 17:43:44 +00:00
isa
e39623b5a9 Add timezone install dbus method and replace dbus with zbus 2025-08-20 17:43:44 +00:00
Bruce Leidl
ea55849afb Launch terminal with gid=100 for corresponding polkit rule that permits 2025-08-20 16:36:13 +00:00
Bruce Leidl
c864490dd0 RealmFS Updates 2025-07-23 17:57:12 +00:00
Bruce Leidl
97be5b5793 Open terminal 2025-07-23 17:56:29 +00:00
Bruce Leidl
ecefb17c82 Implement setting realm config variables 2025-07-23 17:43:27 +00:00
Bruce Leidl
53f87ae338 Fix resolver warning 2025-07-23 17:41:45 +00:00
Bruce Leidl
d44847dec6 Remove realm-config-ui 2025-07-23 17:41:05 +00:00
Bruce Leidl
7d3b002dab Updated dependencies 2025-07-23 17:39:21 +00:00
Bruce Leidl
ba268016a6 Use correct service name 2025-07-23 17:38:45 +00:00
Bruce Leidl
65aa521118 Upgrade zbus 2025-07-23 15:07:10 +00:00
Bruce Leidl
3f38cbe099 realmfs update helper binary 2025-07-23 15:01:53 +00:00
Bruce Leidl
bbcfe9540c New system for realmfs updates 2025-07-23 14:57:18 +00:00
Bruce Leidl
533fb462f9 Fix I/O safety issue 2025-07-23 14:54:29 +00:00
Bruce Leidl
0aa5c36eee A module for changing options on running realms without restarting them. 2025-07-23 14:52:15 +00:00
Bruce Leidl
3879feb998 Fixed bug causing event to not be fired 2025-07-23 14:49:22 +00:00
Bruce Leidl
87dc7b9668 Fixed typo 2025-07-23 14:49:08 +00:00
Bruce Leidl
d34deab087 Added long comment explaining use of SYSTEMD_NSPAWN_SHARE_NS_IPC 2025-07-23 14:43:06 +00:00
Bruce Leidl
af75d3ce4a Added accessors for setting boolean options. 2025-07-23 14:41:01 +00:00
Bruce Leidl
11ec3441c2 impl From<fmt::Error> for crate::Error 2025-07-23 14:40:26 +00:00
Bruce Leidl
11b3e8a016 New improved realms daemon implementation 2024-11-13 11:54:40 -05:00
Bruce Leidl
24f786cf75 Fix broken realmfs autoresize 2024-09-06 10:24:53 -04:00
Bruce Leidl
2dc8bf2922 Support for flatpak and GNOME Software in Realms
When a realm has enabled 'use-flatpak' a .desktop file for GNOME
Software will be automatically generated while that realm is running.

This .desktop file will launch GNOME Software from Citadel inside a
bubblewrap sandbox. The sandbox has been prepared so that GNOME
Software will install flatpak applications into a directory that belongs
to the realm associated with the .desktop file.

When a realm has enabled 'use-flatpak' this directory will be bind
mounted (read-only) into the root filesystem of the realm so that
applications installed by GNOME Software are visible and can be launched.
2024-09-06 10:24:28 -04:00
isa
2a16bd4c41 Upgrade clap, rpassword and pwhash to prepare for new code using them 2024-08-30 13:11:47 -04:00
117 changed files with 8344 additions and 6758 deletions

2840
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
[workspace]
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui" ]
members = ["citadel-realms", "citadel-tool", "realmsd", "launch-gnome-software", "update-realmfs" ]
resolver = "2"
[profile.release]
lto = true
codegen-units = 1

View File

@@ -1 +0,0 @@
/target

View File

@@ -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"

View File

@@ -1,19 +0,0 @@
[package]
name = "citadel-installer-ui"
version = "0.1.0"
authors = ["David McKinney <mckinney@subgraph.com>"]
edition = "2018"
description = "Citadel Installer UI"
homepage = "https://subgraph.com"
[dependencies]
libcitadel = { path = "../libcitadel" }
failure = "0.1.8"
dbus = "0.8.4"
gtk = { version = "0.14.0", features = ["v3_24"] }
gio = "0.14.0"
glib = "0.14.0"
glib-macros = "0.14.0"
gdk = "0.14.0"
pango = "0.9.1"
once_cell = "1.0"

View File

@@ -1,23 +0,0 @@
# Citadel Installer UI design
The installer is required to run in Wayland but also perform privileged
operations. This necessitated splitting the installer into the following
pieces:
- A user interface that can be run by a non-privileged user in their Wayland
session
- A back-end server that runs in the background to perform the privileged
operations on behalf of the user
The user interface communicates with the back-end over DBUS. There are a simple
set of messages/signals to initiate the install process and provide updates to
the interface about the success/failure of each install stage.
Both the user interface can only be run in install/live mode. The user
interface will start automatically when the computer is booted in install/live
mode, however, the user can close the interface and test out the system in
live mode to determine if it is compatible with their hardware, if they want to
actually perform an install, etc. If the user decides to install the system,
they can simply re-open the user interface while still in live mode.

View File

@@ -1,126 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="citadel_password_page">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="citadel_password_page_header">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">24</property>
<property name="margin_bottom">40</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkImage" id="citadel_password_header_icon">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">24</property>
<property name="pixel_size">96</property>
<property name="icon_name">dialog-password-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="citadel_password_header_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Set Citadel User Password</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="citadel_password_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">40</property>
<property name="row_spacing">6</property>
<property name="column_spacing">12</property>
<child>
<object class="GtkLabel" id="citadel_password_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Password: </property>
<property name="justify">right</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="citadel_password_confirm_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Confirm Password:</property>
<property name="justify">right</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="citadel_password_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="visibility">False</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="citadel_password_confirm_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="visibility">False</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="citadel_password_status_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View File

@@ -1,101 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="confirm_install_page">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="confirm_install_page_header">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">24</property>
<property name="margin_bottom">40</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkImage" id="confirm_install_page_header_icon">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixel_size">96</property>
<property name="margin_top">24</property>
<property name="icon_name">system-os-installer</property>
<property name="icon_size">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="confirm_install_page_header_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Install Citadel</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="confirm_install_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="confirm_install_label_1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">You are about to install Citadel to the following destination:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="confirm_install_label_3">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="confirm_install_label_2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Press the &lt;b&gt;Apply&lt;/b&gt; button to continue with the installation. </property>
<property name="use_markup">True</property>
<property name="margin_top">20</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View File

@@ -1,80 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="install_destination_page">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="install_destination_page_header">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">24</property>
<property name="margin_bottom">40</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkImage">
<property name="name">install_destination_header_icon</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixel_size">96</property>
<property name="margin_top">24</property>
<property name="icon_name">drive-harddisk-symbolic</property>
<property name="icon_size">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="name">install_destination_header_label</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Choose an installation destination</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="install_destination_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">40</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkListBox" id="install_destination_listbox">
<property name="width_request">600</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">18</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View File

@@ -1,88 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkTextBuffer" id="install_textbuffer">
<property name="text" translatable="yes">
</property>
</object>
<object class="GtkBox" id="install_page">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_top">24</property>
<property name="margin_bottom">40</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="margin_top">24</property>
<property name="margin_bottom">96</property>
<child>
<object class="GtkLabel" id="install_header_label">
<property name="visible">True</property>
<property name="margin_top">24</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Installing Citadel</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkProgressBar" id="install_progress">
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_bottom">40</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="install_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="valign">center</property>
<property name="hscrollbar_policy">never</property>
<property name="shadow_type">in</property>
<property name="min_content_width">200</property>
<property name="min_content_height">200</property>
<child>
<object class="GtkTextView" id="install_textview">
<property name="width_request">600</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="wrap_mode">word-char</property>
<property name="indent">10</property>
<property name="cursor_visible">False</property>
<property name="buffer">install_textbuffer</property>
<property name="accepts_tab">False</property>
<property name="monospace">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</interface>

View File

@@ -1,126 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="luks_password_page">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="luks_password_page_header">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">24</property>
<property name="margin_bottom">40</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkImage" id="luks_password_header_icon">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">24</property>
<property name="pixel_size">96</property>
<property name="icon_name">dialog-password-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="luks_password_header_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Set a disk encryption password</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="luks_password_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">40</property>
<property name="row_spacing">6</property>
<property name="column_spacing">12</property>
<child>
<object class="GtkLabel" id="luks_password_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Password: </property>
<property name="justify">right</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="luks_password_confirm_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Confirm Password:</property>
<property name="justify">right</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="luks_password_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="visibility">False</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="luks_password_confirm_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="visibility">False</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="luks_password_status_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View File

@@ -1,22 +0,0 @@
button.default:not(disabled) {
background: #027b40;
color: #D1D7d7;
}
button.default:disabled {
background: #D1D7d7;
color: #929595;
}
button {
background: #D1D7d7;
}
headerbar {
background: #3C4141;
}
text {
background: #4C575C;
color: #D1D7D7;
}

View File

@@ -1,58 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="welcome_page">
<property name="name">welcome_page</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="welcome_header">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_top">120</property>
<property name="margin_bottom">40</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="welcome_header_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Welcome to the Citadel installer.</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="welcome_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">40</property>
<property name="label" translatable="yes">To install Citadel on your computer, press &lt;b&gt;Next&lt;/b&gt;.
To continue trying Citadel without installing it, press &lt;b&gt;Cancel&lt;/b&gt;.
If you decide to install Citadel after trying it out, just re-open this application.
</property>
<property name="use_markup">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View File

@@ -1,48 +0,0 @@
use gtk::prelude::*;
use crate::{Error, Result};
pub struct Builder {
builder: gtk::Builder,
}
impl Builder {
pub fn new(source: &str) -> Self {
let builder = gtk::Builder::from_string(source);
Builder { builder }
}
fn ok_or_err<T>(type_name: &str, name: &str, object: Option<T>) -> Result<T> {
object.ok_or(Error::Builder(format!("failed to load {} {}", type_name, name)))
}
pub fn get_entry(&self, name: &str) -> Result<gtk::Entry> {
Self::ok_or_err("GtkEntry", name, self.builder.object(name))
}
pub fn get_box(&self, name: &str) -> Result<gtk::Box> {
Self::ok_or_err("GtkBox", name, self.builder.object(name))
}
pub fn get_label(&self, name: &str) -> Result<gtk::Label> {
Self::ok_or_err("GtkLabel", name, self.builder.object(name))
}
pub fn get_listbox(&self, name: &str) -> Result<gtk::ListBox> {
Self::ok_or_err("GtkListBox", name, self.builder.object(name))
}
pub fn get_progress_bar(&self, name: &str) -> Result<gtk::ProgressBar> {
Self::ok_or_err("GtkProgressBar", name, self.builder.object(name))
}
pub fn get_textview(&self, name: &str) -> Result<gtk::TextView> {
Self::ok_or_err("GtkTextView", name, self.builder.object(name))
}
pub fn get_scrolled_window(&self, name: &str) -> Result<gtk::ScrolledWindow> {
Self::ok_or_err("GtkScrolledWindow", name, self.builder.object(name))
}
}

View File

@@ -1,207 +0,0 @@
use dbus::arg;
#[derive(Debug)]
pub struct ComSubgraphInstallerManagerInstallCompleted {
}
impl arg::AppendAll for ComSubgraphInstallerManagerInstallCompleted {
fn append(&self, _: &mut arg::IterAppend) {
}
}
impl arg::ReadAll for ComSubgraphInstallerManagerInstallCompleted {
fn read(_i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(ComSubgraphInstallerManagerInstallCompleted {})
}
}
impl dbus::message::SignalArgs for ComSubgraphInstallerManagerInstallCompleted {
const NAME: &'static str = "InstallCompleted";
const INTERFACE: &'static str = "com.subgraph.installer.Manager";
}
#[derive(Debug)]
pub struct ComSubgraphInstallerManagerRunInstallStarted {
pub text: String,
}
impl arg::AppendAll for ComSubgraphInstallerManagerRunInstallStarted {
fn append(&self, _: &mut arg::IterAppend) {
}
}
impl arg::ReadAll for ComSubgraphInstallerManagerRunInstallStarted {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(ComSubgraphInstallerManagerRunInstallStarted {
text: i.read()?,
})
}
}
impl dbus::message::SignalArgs for ComSubgraphInstallerManagerRunInstallStarted {
const NAME: &'static str = "RunInstallStarted";
const INTERFACE: &'static str = "com.subgraph.installer.Manager";
}
#[derive(Debug)]
pub struct ComSubgraphInstallerManagerDiskPartitioned {
pub text: String,
}
impl arg::AppendAll for ComSubgraphInstallerManagerDiskPartitioned {
fn append(&self, _: &mut arg::IterAppend) {
}
}
impl arg::ReadAll for ComSubgraphInstallerManagerDiskPartitioned {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(ComSubgraphInstallerManagerDiskPartitioned {
text: i.read()?
//sender,
})
}
}
impl dbus::message::SignalArgs for ComSubgraphInstallerManagerDiskPartitioned {
const NAME: &'static str = "DiskPartitioned";
const INTERFACE: &'static str = "com.subgraph.installer.Manager";
}
#[derive(Debug)]
pub struct ComSubgraphInstallerManagerLvmSetup {
pub text: String,
}
impl arg::AppendAll for ComSubgraphInstallerManagerLvmSetup {
fn append(&self, _: &mut arg::IterAppend) {
}
}
impl arg::ReadAll for ComSubgraphInstallerManagerLvmSetup {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(ComSubgraphInstallerManagerLvmSetup {
text: i.read()?
})
}
}
impl dbus::message::SignalArgs for ComSubgraphInstallerManagerLvmSetup {
const NAME: &'static str = "LvmSetup";
const INTERFACE: &'static str = "com.subgraph.installer.Manager";
}
#[derive(Debug)]
pub struct ComSubgraphInstallerManagerLuksSetup {
pub text: String,
}
impl arg::AppendAll for ComSubgraphInstallerManagerLuksSetup {
fn append(&self, _: &mut arg::IterAppend) {
}
}
impl arg::ReadAll for ComSubgraphInstallerManagerLuksSetup {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(ComSubgraphInstallerManagerLuksSetup {
text: i.read()?
})
}
}
impl dbus::message::SignalArgs for ComSubgraphInstallerManagerLuksSetup {
const NAME: &'static str = "LuksSetup";
const INTERFACE: &'static str = "com.subgraph.installer.Manager";
}
#[derive(Debug)]
pub struct ComSubgraphInstallerManagerBootSetup {
pub text: String,
}
impl arg::AppendAll for ComSubgraphInstallerManagerBootSetup {
fn append(&self, _: &mut arg::IterAppend) {
}
}
impl arg::ReadAll for ComSubgraphInstallerManagerBootSetup {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(ComSubgraphInstallerManagerBootSetup {
text: i.read()?
})
}
}
impl dbus::message::SignalArgs for ComSubgraphInstallerManagerBootSetup {
const NAME: &'static str = "BootSetup";
const INTERFACE: &'static str = "com.subgraph.installer.Manager";
}
#[derive(Debug)]
pub struct ComSubgraphInstallerManagerStorageCreated {
pub text: String,
}
impl arg::AppendAll for ComSubgraphInstallerManagerStorageCreated {
fn append(&self, _: &mut arg::IterAppend) {
}
}
impl arg::ReadAll for ComSubgraphInstallerManagerStorageCreated {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(ComSubgraphInstallerManagerStorageCreated {
//sender,
text: i.read()?
})
}
}
impl dbus::message::SignalArgs for ComSubgraphInstallerManagerStorageCreated {
const NAME: &'static str = "StorageCreated";
const INTERFACE: &'static str = "com.subgraph.installer.Manager";
}
#[derive(Debug)]
pub struct ComSubgraphInstallerManagerRootfsInstalled {
pub text: String,
}
impl arg::AppendAll for ComSubgraphInstallerManagerRootfsInstalled {
fn append(&self, _: &mut arg::IterAppend) {
}
}
impl arg::ReadAll for ComSubgraphInstallerManagerRootfsInstalled {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(ComSubgraphInstallerManagerRootfsInstalled {
text: i.read()?
})
}
}
impl dbus::message::SignalArgs for ComSubgraphInstallerManagerRootfsInstalled {
const NAME: &'static str = "RootfsInstalled";
const INTERFACE: &'static str = "com.subgraph.installer.Manager";
}
#[derive(Debug)]
pub struct ComSubgraphInstallerManagerInstallFailed {
pub text: String,
}
impl arg::AppendAll for ComSubgraphInstallerManagerInstallFailed {
fn append(&self, _: &mut arg::IterAppend) {
}
}
impl arg::ReadAll for ComSubgraphInstallerManagerInstallFailed {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(ComSubgraphInstallerManagerInstallFailed {
text: i.read()?,
})
}
}
impl dbus::message::SignalArgs for ComSubgraphInstallerManagerInstallFailed {
const NAME: &'static str = "InstallFailed";
const INTERFACE: &'static str = "com.subgraph.installer.Manager";
}

View File

@@ -1,11 +0,0 @@
use std::result;
use dbus;
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
Dbus(dbus::Error),
Builder(String),
}

View File

@@ -1,40 +0,0 @@
#![allow(deprecated)]
use gtk::prelude::*;
mod ui;
mod builder;
mod error;
mod rowdata;
mod dbus_client;
use libcitadel::CommandLine;
use ui::Ui;
pub use error::{Result,Error};
fn main() {
let application =
gtk::Application::new(Some("com.subgraph.citadel-installer"), Default::default());
application.connect_activate(|app| {
if !(CommandLine::live_mode() || CommandLine::install_mode()) {
let dialog = gtk::MessageDialog::new(
None::<&gtk::Window>,
gtk::DialogFlags::empty(),
gtk::MessageType::Error,
gtk::ButtonsType::Cancel,
"Citadel Installer can only be run during install mode");
dialog.run();
} else {
match Ui::build(app) {
Ok(ui) => {
ui.assistant.show_all();
ui.start();
},
Err(err) => {
println!("Could not start application: {:?}", err);
}
}
}
});
application.run();
}

View File

@@ -1,130 +0,0 @@
use gio::prelude::*;
use std::fmt;
use glib::subclass::prelude::*;
use glib::ParamSpec;
use gtk::glib;
use std::cell::RefCell;
#[derive(Default)]
pub struct RowDataImpl {
model: RefCell<Option<String>>,
path: RefCell<Option<String>>,
size: RefCell<Option<String>>,
removable: RefCell<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for RowDataImpl {
const NAME: &'static str = "RowData";
type Type = RowData;
type ParentType = glib::Object;
}
impl ObjectImpl for RowDataImpl {
fn properties() -> &'static [ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpec::new_string(
"model",
"Model",
"Model",
None,
glib::ParamFlags::READWRITE,
),
glib::ParamSpec::new_string(
"path",
"Path",
"Path",
None,
glib::ParamFlags::READWRITE,
),
glib::ParamSpec::new_string(
"size",
"Size",
"Size",
None,
glib::ParamFlags::READWRITE,
),
glib::ParamSpec::new_boolean(
"removable",
"Removable",
"Removable",
false,
glib::ParamFlags::READWRITE,
),
]
});
PROPERTIES.as_ref()
}
fn set_property(
&self,
_obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"model" => {
let model = value
.get()
.expect("type conformity checked by `Object::set_property`");
self.model.replace(model);
}
"path" => {
let path = value
.get()
.expect("type conformity checked by `Object::set_property`");
self.path.replace(path);
}
"size" => {
let size = value
.get()
.expect("type conformity checked by `Object::set_property`");
self.size.replace(size);
}
"removable" => {
let removable = value
.get()
.expect("type conformity checked by `Object::set_property`");
self.removable.replace(removable);
}
_ => unimplemented!(),
}
}
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"model" => self.model.borrow().to_value(),
"path" => self.path.borrow().to_value(),
"size" => self.size.borrow().to_value(),
"removable" => self.removable.borrow().to_value(),
_ => unimplemented!(),
}
}
}
glib::wrapper! {
pub struct RowData(ObjectSubclass<RowDataImpl>);
}
impl RowData {
pub fn new(model: &str, path: &str, size: &str, removable: bool) -> RowData {
glib::Object::new(&[
("model", &model),
("path", &path),
("size", &size),
("removable", &removable),
])
.expect("Failed to create row data")
}
}
impl fmt::Display for RowData {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}

View File

@@ -1,421 +0,0 @@
use gtk::prelude::*;
use gtk::glib;
use dbus::Message;
use std::time::Duration;
use std::thread;
use std::collections::HashMap;
use dbus::blocking::{Connection, Proxy};
use crate::builder::*;
use crate::rowdata::RowData;
use crate::{Result, Error};
use crate::dbus_client::*;
const STYLE: &str = include_str!("../data/style.css");
const WELCOME_UI: &str = include_str!("../data/welcome_page.ui");
const CITADEL_PASSWORD_UI: &str = include_str!("../data/citadel_password_page.ui");
const LUKS_PASSWORD_UI: &str = include_str!("../data/luks_password_page.ui");
const INSTALL_DESTINATION_UI: &str = include_str!("../data/install_destination_page.ui");
const CONFIRM_INSTALL_UI: &str = include_str!("../data/confirm_install_page.ui");
const INSTALL_UI: &str = include_str!("../data/install_page.ui");
pub enum Msg {
InstallStarted,
LvmSetup(String),
LuksSetup(String),
BootSetup(String),
StorageCreated(String),
RootfsInstalled(String),
InstallCompleted,
InstallFailed(String)
}
#[derive(Clone)]
pub struct Ui {
pub assistant: gtk::Assistant,
pub citadel_password_page: gtk::Box,
pub citadel_password_entry: gtk::Entry,
pub citadel_password_confirm_entry: gtk::Entry,
pub citadel_password_status_label: gtk::Label,
pub luks_password_page: gtk::Box,
pub luks_password_entry: gtk::Entry,
pub luks_password_confirm_entry: gtk::Entry,
pub luks_password_status_label: gtk::Label,
pub disks_listbox: gtk::ListBox,
pub disks_model: gio::ListStore,
pub disk_rows: Vec<RowData>,
pub confirm_install_label: gtk::Label,
pub install_page: gtk::Box,
pub install_progress: gtk::ProgressBar,
pub install_scrolled_window: gtk::ScrolledWindow,
pub install_textview: gtk::TextView,
pub sender: glib::Sender<Msg>
}
impl Ui {
pub fn build(application: &gtk::Application) -> Result<Self> {
let disks = Self::get_disks()?;
let assistant = gtk::Assistant::new();
assistant.set_default_size(800, 600);
assistant.set_position(gtk::WindowPosition::CenterAlways);
assistant.set_application(Some(application));
assistant.connect_delete_event(glib::clone!(@strong application => move |_, _| {
application.quit();
gtk::Inhibit(false)
}));
assistant.connect_cancel(glib::clone!(@strong application => move |_| {
application.quit();
}));
let welcome_builder = Builder::new(WELCOME_UI);
let welcome_page: gtk::Box = welcome_builder.get_box("welcome_page")?;
let citadel_password_builder = Builder::new(CITADEL_PASSWORD_UI);
let citadel_password_page: gtk::Box = citadel_password_builder.get_box("citadel_password_page")?;
let citadel_password_entry: gtk::Entry = citadel_password_builder.get_entry("citadel_password_entry")?;
let citadel_password_confirm_entry: gtk::Entry = citadel_password_builder.get_entry("citadel_password_confirm_entry")?;
let citadel_password_status_label: gtk::Label = citadel_password_builder.get_label("citadel_password_status_label")?;
let luks_password_builder = Builder::new(LUKS_PASSWORD_UI);
let luks_password_page: gtk::Box = luks_password_builder.get_box("luks_password_page")?;
let luks_password_entry: gtk::Entry = luks_password_builder.get_entry("luks_password_entry")?;
let luks_password_confirm_entry: gtk::Entry = luks_password_builder.get_entry("luks_password_confirm_entry")?;
let luks_password_status_label: gtk::Label = luks_password_builder.get_label("luks_password_status_label")?;
let install_destination_builder = Builder::new(INSTALL_DESTINATION_UI);
let install_destination_page: gtk::Box = install_destination_builder.get_box("install_destination_page")?;
let disks_listbox = install_destination_builder.get_listbox("install_destination_listbox")?;
let confirm_install_builder = Builder::new(CONFIRM_INSTALL_UI);
let confirm_install_page: gtk::Box = confirm_install_builder.get_box("confirm_install_page")?;
let confirm_install_label: gtk::Label = confirm_install_builder.get_label("confirm_install_label_3")?;
let disks_model = gio::ListStore::new(RowData::static_type());
disks_listbox.bind_model(Some(&disks_model), move |item| {
let row = gtk::ListBoxRow::new();
let item = item.downcast_ref::<RowData>().expect("Row data is of wrong type");
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 5);
hbox.set_homogeneous(true);
let removable= item.property("removable").unwrap().get::<bool>().unwrap();
let icon_name = Self::get_disk_icon(removable);
let disk_icon = gtk::Image::from_icon_name(Some(&icon_name), gtk::IconSize::LargeToolbar);
disk_icon.set_halign(gtk::Align::Start);
let model_label = gtk::Label::new(None);
model_label.set_halign(gtk::Align::Start);
model_label.set_justify(gtk::Justification::Left);
item.bind_property("model", &model_label, "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
let path_label = gtk::Label::new(None);
path_label.set_halign(gtk::Align::Start);
path_label.set_justify(gtk::Justification::Left);
item.bind_property("path", &path_label, "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
let size_label = gtk::Label::new(None);
size_label.set_halign(gtk::Align::Start);
size_label.set_justify(gtk::Justification::Left);
item.bind_property("size", &size_label, "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
hbox.pack_start(&disk_icon, true, true, 0);
hbox.pack_start(&path_label, true, true, 0);
hbox.pack_start(&model_label, true, true, 0);
hbox.pack_start(&size_label, true, true, 0);
row.add(&hbox);
row.show_all();
row.upcast::<gtk::Widget>()
});
disks_listbox.connect_row_selected(glib::clone!(@strong assistant, @strong install_destination_page => move |_, listbox_row | {
if let Some(_) = listbox_row {
assistant.set_page_complete(&install_destination_page, true);
}
}));
let install_builder = Builder::new(INSTALL_UI);
let install_page: gtk::Box = install_builder.get_box("install_page")?;
let install_progress: gtk::ProgressBar = install_builder.get_progress_bar("install_progress")?;
let install_scrolled_window: gtk::ScrolledWindow = install_builder.get_scrolled_window("install_scrolled_window")?;
let install_textview: gtk::TextView = install_builder.get_textview("install_textview")?;
assistant.append_page(&welcome_page);
assistant.set_page_type(&welcome_page, gtk::AssistantPageType::Intro);
assistant.set_page_complete(&welcome_page, true);
assistant.append_page(&citadel_password_page);
assistant.append_page(&luks_password_page);
assistant.append_page(&install_destination_page);
assistant.append_page(&confirm_install_page);
assistant.set_page_type(&confirm_install_page, gtk::AssistantPageType::Confirm);
assistant.set_page_complete(&confirm_install_page, true);
assistant.append_page(&install_page);
assistant.set_page_type(&install_page, gtk::AssistantPageType::Progress);
let disks_model_clone = disks_model.clone();
let (sender, receiver) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let ui = Self {
assistant,
citadel_password_page,
citadel_password_entry,
citadel_password_confirm_entry,
citadel_password_status_label,
luks_password_page,
luks_password_entry,
luks_password_confirm_entry,
luks_password_status_label,
disks_listbox,
disks_model,
disk_rows: disks.clone(),
confirm_install_label,
install_page,
install_progress,
install_scrolled_window,
install_textview,
sender,
};
receiver.attach(None,glib::clone!(@strong ui, @strong application => move |msg| {
match msg {
Msg::InstallStarted => {
ui.install_progress.set_fraction(0.1428);
let buffer = ui.install_textview.buffer().unwrap();
let mut iter = buffer.end_iter();
let text = format!(
"+ Installing Citadel to {}. \nFor a full log, consult the systemd journal by running the following command:\n <i>sudo journalctl -u citadel-installer-backend.service</i>\n",
ui.get_install_destination());
buffer.insert_markup(&mut iter, &text);
},
Msg::LuksSetup(text) => {
ui.install_progress.set_fraction(0.1428 * 2.0);
let buffer = ui.install_textview.buffer().unwrap();
let mut iter = buffer.end_iter();
buffer.insert(&mut iter, &text);
},
Msg::LvmSetup(text) => {
ui.install_progress.set_fraction(0.1428 * 3.0);
let buffer = ui.install_textview.buffer().unwrap();
let mut iter = buffer.end_iter();
buffer.insert(&mut iter, &text);
},
Msg::BootSetup(text) => {
ui.install_progress.set_fraction(0.1428 * 4.0);
let buffer = ui.install_textview.buffer().unwrap();
let mut iter = buffer.end_iter();
buffer.insert(&mut iter, &text);
},
Msg::StorageCreated(text) => {
ui.install_progress.set_fraction(0.1428 * 5.0);
let buffer = ui.install_textview.buffer().unwrap();
let mut iter = buffer.end_iter();
buffer.insert(&mut iter, &text);
},
Msg::RootfsInstalled(text) => {
ui.install_progress.set_fraction(0.1428 * 6.0);
let buffer = ui.install_textview.buffer().unwrap();
let mut iter = buffer.end_iter();
buffer.insert(&mut iter, &text);
},
Msg::InstallCompleted => {
ui.install_progress.set_fraction(1.0);
let buffer = ui.install_textview.buffer().unwrap();
let mut iter = buffer.end_iter();
buffer.insert(&mut iter, "+ Completed the installation successfully\n");
let quit_button = gtk::Button::with_label("Quit");
quit_button.connect_clicked(glib::clone!(@strong application => move |_| {
application.quit();
}));
quit_button.set_sensitive(true);
ui.assistant.add_action_widget(&quit_button);
ui.assistant.show_all();
},
Msg::InstallFailed(error) => {
ui.install_progress.set_fraction(100.0);
let buffer = ui.install_textview.buffer().unwrap();
let mut iter = buffer.end_iter();
let text = format!("+ Install failed with error:\n<i>{}</i>\n", error);
buffer.insert_markup(&mut iter, &text);
let quit_button = gtk::Button::with_label("Quit");
quit_button.connect_clicked(glib::clone!(@strong application => move |_| {
application.quit();
}));
quit_button.set_sensitive(true);
ui.assistant.add_action_widget(&quit_button);
ui.assistant.show_all();
}
}
glib::Continue(true)
}));
ui.setup_style();
ui.setup_signals();
for disk in disks {
disks_model_clone.append(&disk);
}
Ok(ui)
}
fn get_disks() -> Result<Vec<RowData>> {
let mut disks = vec![];
let conn = Connection::new_system().unwrap();
let proxy = conn.with_proxy("com.subgraph.installer",
"/com/subgraph/installer", Duration::from_millis(5000));
let (devices,): (HashMap<String, Vec<String>>,) = proxy.method_call("com.subgraph.installer.Manager", "GetDisks", ()).map_err(Error::Dbus)?;
for device in devices {
let disk = RowData::new(
&device.1[0].clone(),
&device.0,
&device.1[1].clone(),
device.1[2].parse().unwrap());
disks.push(disk);
}
Ok(disks)
}
fn get_disk_icon(removable: bool) -> String {
if removable {
return "drive-harddisk-usb-symbolic".to_string();
}
"drive-harddisk-system-symbolic".to_string()
}
pub fn setup_entry_signals(&self, page: &gtk::Box, first_entry: &gtk::Entry, second_entry: &gtk::Entry, status_label: &gtk::Label) {
let ui = self.clone();
let assistant = ui.assistant.clone();
first_entry.connect_changed(glib::clone!(@weak assistant, @weak page, @weak second_entry, @weak status_label => move |entry| {
let password = entry.text();
let confirm = second_entry.text();
if password != "" && confirm != "" {
let matches = password == confirm;
if !matches {
status_label.set_text("Passwords do not match");
} else {
status_label.set_text("");
}
assistant.set_page_complete(&page, matches);
}
}));
first_entry.connect_activate(glib::clone!(@weak second_entry => move |_| {
second_entry.grab_focus();
}));
}
pub fn setup_prepare_signal(&self) {
let ui = self.clone();
ui.assistant.connect_prepare(glib::clone!(@strong ui => move |assistant, page| {
let page_type = assistant.page_type(page);
if page_type == gtk::AssistantPageType::Confirm {
let path = ui.get_install_destination();
let text = format!("<i>{}</i>", path);
ui.confirm_install_label.set_markup(&text);
}
}));
}
pub fn setup_apply_signal(&self) {
let ui = self.clone();
ui.assistant.connect_apply(glib::clone!(@strong ui => move |_| {
let citadel_password = ui.get_citadel_password();
let luks_password = ui.get_luks_password();
let destination = ui.get_install_destination();
let conn = Connection::new_system().unwrap();
let proxy = conn.with_proxy("com.subgraph.installer",
"/com/subgraph/installer", Duration::from_millis(5000));
let (_,): (bool,) = proxy.method_call("com.subgraph.installer.Manager",
"RunInstall", (destination, citadel_password, luks_password)).unwrap();
let _= ui.sender.send(Msg::InstallStarted);
}));
}
pub fn setup_autoscroll_signal(&self) {
let ui = self.clone();
let scrolled_window = ui.install_scrolled_window;
ui.install_textview.connect_size_allocate(glib::clone!(@weak scrolled_window => move |_, _| {
let adjustment = scrolled_window.vadjustment();
adjustment.set_value(adjustment.upper() - adjustment.page_size());
}));
}
pub fn setup_signals(&self) {
let ui = self.clone();
self.setup_entry_signals(&ui.citadel_password_page, &ui.citadel_password_entry,
&ui.citadel_password_confirm_entry, &ui.citadel_password_status_label);
self.setup_entry_signals(&ui.citadel_password_page, &ui.citadel_password_confirm_entry,
&ui.citadel_password_entry, &ui.citadel_password_status_label);
self.setup_entry_signals(&ui.luks_password_page, &ui.luks_password_entry,
&ui.luks_password_confirm_entry, &ui.luks_password_status_label);
self.setup_entry_signals(&ui.luks_password_page, &ui.luks_password_confirm_entry,
&ui.luks_password_entry, &ui.luks_password_status_label);
self.setup_prepare_signal();
self.setup_apply_signal();
self.setup_autoscroll_signal();
}
fn setup_style(&self) {
let css = gtk::CssProvider::new();
if let Err(err) = css.load_from_data(STYLE.as_bytes()) {
println!("Error parsing CSS style: {}", err);
return;
}
if let Some(screen) = gdk::Screen::default() {
gtk::StyleContext::add_provider_for_screen(&screen, &css, gtk::STYLE_PROVIDER_PRIORITY_USER);
}
}
pub fn get_citadel_password(&self) -> String {
let ui = self.clone();
let password = ui.citadel_password_entry.text();
password.to_string()
}
pub fn get_luks_password(&self) -> String {
let ui = self.clone();
let password = ui.luks_password_entry.text();
password.to_string()
}
pub fn get_install_destination(&self) -> String {
let ui = self.clone();
if let Some(row) = ui.disks_listbox.selected_row() {
let index = row.index() as usize;
if ui.disk_rows.len() > index {
let data = &ui.disk_rows[index];
let path: String = data.property("path").unwrap().get::<String>().unwrap();
return path.to_string();
}
}
"".to_string()
}
fn setup_signal_matchers(&self, proxy: Proxy<&Connection>) {
let sender = self.sender.clone();
let _ = proxy.match_signal(glib::clone!(@strong sender => move |_: ComSubgraphInstallerManagerInstallCompleted, _: &Connection, _: &Message| {
let _ = sender.send(Msg::InstallCompleted);
true
}));
let _ = proxy.match_signal(glib::clone!(@strong sender => move |h: ComSubgraphInstallerManagerLvmSetup, _: &Connection, _: &Message| {
let _ = sender.send(Msg::LvmSetup(h.text));
true
}));
let _ = proxy.match_signal(glib::clone!(@strong sender => move |h: ComSubgraphInstallerManagerLuksSetup, _: &Connection, _: &Message| {
let _ = sender.send(Msg::LuksSetup(h.text));
true
}));
let _ = proxy.match_signal(glib::clone!(@strong sender => move |h: ComSubgraphInstallerManagerBootSetup, _: &Connection, _: &Message| {
let _ = sender.send(Msg::BootSetup(h.text));
true
}));
let _ = proxy.match_signal(glib::clone!(@strong sender => move |h: ComSubgraphInstallerManagerStorageCreated, _: &Connection, _: &Message| {
let _ = sender.send(Msg::StorageCreated(h.text));
true
}));
let _ = proxy.match_signal(glib::clone!(@strong sender => move |h: ComSubgraphInstallerManagerRootfsInstalled, _: &Connection, _: &Message| {
let _ = sender.send(Msg::RootfsInstalled(h.text));
true
}));
let _ = proxy.match_signal(glib::clone!(@strong sender => move |h: ComSubgraphInstallerManagerInstallFailed, _: &Connection, _: &Message| {
let _ = sender.send(Msg::InstallFailed(h.text));
true
}));
}
pub fn start(&self) {
let c = Connection::new_system().unwrap();
let proxy = c.with_proxy("com.subgraph.installer", "/com/subgraph/installer", Duration::from_millis(5000));
self.setup_signal_matchers(proxy);
thread::spawn(move || {
loop {
c.process(Duration::from_millis(1000)).unwrap(); }
});
}
}

View File

@@ -21,6 +21,8 @@ use cursive::vec::Vec2;
use std::cell::{Cell, RefCell};
use std::fs::File;
use std::io::{self,BufWriter,Write};
use std::os::fd::AsFd;
use std::os::fd::OwnedFd;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
@@ -49,19 +51,10 @@ pub struct Backend {
input_receiver: Receiver<TEvent>,
resize_receiver: Receiver<()>,
tty_fd: RawFd,
tty_fd: OwnedFd,
input_thread: JoinHandle<()>,
}
fn close_fd(fd: RawFd) -> io::Result<()> {
unsafe {
if libc::close(fd) != 0 {
return Err(io::Error::last_os_error());
}
};
Ok(())
}
fn pthread_kill(tid: libc::pthread_t, sig: libc::c_int) -> io::Result<()> {
unsafe {
if libc::pthread_kill(tid, sig) != 0 {
@@ -98,7 +91,7 @@ impl Backend {
// Read input from a separate thread
let input = std::fs::File::open("/dev/tty").unwrap();
let tty_fd = input.as_raw_fd();
let tty_fd = input.as_fd().try_clone_to_owned().unwrap();
let input_thread = thread::spawn(move || {
let mut events = input.events();
@@ -127,12 +120,6 @@ impl Backend {
Ok(Box::new(c))
}
fn close_tty(&self) {
if let Err(e) = close_fd(self.tty_fd) {
warn!("error closing tty fd: {}", e);
}
}
fn kill_thread(&self) {
if let Err(e) = pthread_kill(self.input_thread.as_pthread_t(), libc::SIGWINCH) {
warn!("error sending signal to input thread: {}", e);
@@ -245,7 +232,6 @@ impl backend::Backend for Backend {
)
.unwrap();
self.close_tty();
self.kill_thread();
}

View File

@@ -159,7 +159,7 @@ impl <'a> RealmFSInfoRender <'a> {
fn render_image(&mut self) {
fn sizes(r: &RealmFS) -> Result<(usize,usize)> {
let free = r.free_size_blocks()?;
let allocated = r.allocated_size_blocks()?;
let allocated = r.allocated_size_blocks();
Ok((free,allocated))
}

View File

@@ -8,14 +8,34 @@ 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.9"
hex = "0.4"
byteorder = "1"
dbus = "0.8.4"
pwhash = "0.3.1"
pwhash = "1.0"
rand = "0.8"
tempfile = "3"
zbus = "5.9.0"
anyhow = "1.0"
log = "0.4"
zbus_macros = "5.9"
event-listener = "5.4"
futures-timer = "3.0"
tokio = { version = "1", features = ["full"] }
rs-release = "0.1"
glob = "0.3"
serde_cbor = "0.11"
ed25519-dalek = { version = "2.2", features = ["pem", "rand_core"] }
base64ct = "=1.7.3"
ureq = { version = "3.1" }
sha2 = "0.10"
nix = "0.30"
dialoguer = "0.12"
indicatif = "0.18"
serde_json = "1.0"
chrono = "0.4"
env_logger = "0.11"

View File

@@ -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)
})
}

View File

@@ -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)

View File

@@ -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,16 @@ 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 ver_a = meta_a.version();
let ver_b = meta_b.version();
let bind_a = a.metainfo();
let bind_b = b.metainfo();
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);
}
}

View File

@@ -0,0 +1,949 @@
use super::config::Config;
use super::keyring::ChannelKey;
use super::metadata::*;
use anyhow::{Context, Result};
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use indicatif::{ProgressBar, ProgressStyle};
use libcitadel::ImageHeader;
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::convert::TryInto;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
const METADATA_CACHE_DIR: &str = "/storage/citadel-state/tuf-cache/metadata";
const TARGETS_CACHE_DIR: &str = "/storage/citadel-state/tuf-cache/targets";
const EMBEDDED_ROOT: &str = "/etc/citadel/root.json";
pub struct TufClient {
config: Config,
metadata_dir: PathBuf,
targets_dir: PathBuf,
root: Option<SignedMetadata<RootMetadata>>,
timestamp: Option<SignedMetadata<TimestampMetadata>>,
snapshot: Option<SignedMetadata<SnapshotMetadata>>,
targets: Option<SignedMetadata<TargetsMetadata>>,
channel_targets: HashMap<String, SignedMetadata<TargetsMetadata>>,
os_release_info: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct UpdateInfo {
pub component: String,
pub current_version: String,
pub new_version: String,
pub target_path: String,
pub download_size: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct ChannelInfo {
pub name: String,
pub key_id: String,
}
impl TufClient {
pub fn new(config: &Config) -> Result<Self> {
let metadata_dir = PathBuf::from(METADATA_CACHE_DIR);
let targets_dir = PathBuf::from(TARGETS_CACHE_DIR);
fs::create_dir_all(&metadata_dir)?;
fs::create_dir_all(&targets_dir)?;
let root = load_or_bootstrap_root(&metadata_dir)?;
let os_release_info =
super::config::parse_conf_file(Path::new("/etc/os-release")).unwrap_or_default();
Ok(TufClient {
config: config.clone(),
metadata_dir,
targets_dir,
root: Some(root),
timestamp: None,
snapshot: None,
targets: None,
channel_targets: HashMap::new(),
os_release_info,
})
}
pub fn refresh_metadata(&mut self) -> Result<()> {
log::info!("Refreshing all metadata");
self.update_root()?;
self.update_timestamp()?;
self.update_snapshot()?;
self.update_targets()?;
log::info!("Metadata refresh complete");
Ok(())
}
fn update_root(&mut self) -> Result<()> {
log::info!("Updating root metadata");
let mut current_root = self.root.clone().unwrap();
let mut version = current_root.signed.version;
loop {
let next_version = version + 1;
log::debug!("Attempting to fetch root metadata version {}", next_version);
let url = format!(
"{}/{}.root.json",
self.config.repository_url(),
next_version
);
match fetch_url(&url) {
Ok(content) => {
log::debug!(
"Successfully fetched root metadata version {}",
next_version
);
let new_root: SignedMetadata<RootMetadata> =
serde_json::from_str(&content).context("Failed to parse root.json")?;
if new_root.signed.version != next_version {
anyhow::bail!("Root version mismatch");
}
check_expiry(&new_root.signed.expires)?;
// Verify with current root's keys
log::debug!("Verifying new root with old root keys");
self.verify_root_signature(&new_root, &current_root)?;
// Update for next iteration
current_root = new_root.clone();
version = next_version;
// Save to cache
let cache_path = self.metadata_dir.join("root.json");
fs::write(&cache_path, &content)?;
log::debug!("Saved new root metadata to cache");
// Update self.root
self.root = Some(new_root);
}
Err(e) => {
log::debug!(
"Failed to fetch root version {}: {}. Assuming this is the latest version.",
next_version,
e
);
break;
}
}
}
Ok(())
}
fn update_timestamp(&mut self) -> Result<()> {
log::info!("Updating timestamp metadata");
let url = format!("{}/timestamp.json", self.config.repository_url());
let content = fetch_url(&url).context("Failed to fetch timestamp.json")?;
let timestamp: SignedMetadata<TimestampMetadata> =
serde_json::from_str(&content).context("Failed to parse timestamp.json")?;
log::debug!("Verifying timestamp metadata signature");
self.verify_role_signature(&timestamp, "timestamp")?;
check_expiry(&timestamp.signed.expires)?;
if let Some(cached) = &self.timestamp {
if timestamp.signed.version < cached.signed.version {
log::error!(
"Timestamp rollback detected! Cached version: {}, new version: {}",
cached.signed.version,
timestamp.signed.version
);
anyhow::bail!("Timestamp rollback detected");
}
}
let cache_path = self.metadata_dir.join("timestamp.json");
fs::write(&cache_path, &content)?;
log::debug!("Saved new timestamp metadata to cache");
self.timestamp = Some(timestamp);
Ok(())
}
fn update_snapshot(&mut self) -> Result<()> {
log::info!("Updating snapshot metadata");
let timestamp = self.timestamp.as_ref().unwrap();
let snapshot_meta = timestamp
.signed
.meta
.get("snapshot.json")
.ok_or_else(|| anyhow::anyhow!("No snapshot.json in timestamp"))?;
let url = format!("{}/snapshot.json", self.config.repository_url());
let content = fetch_url(&url).context("Failed to fetch snapshot.json")?;
if let Some(hashes) = &snapshot_meta.hashes {
if let Some(expected) = hashes.get("sha256") {
let actual = hex::encode(Sha256::digest(content.as_bytes()));
if &actual != expected {
log::error!(
"Snapshot hash mismatch! Expected: {}, actual: {}",
expected,
actual
);
anyhow::bail!("Snapshot hash mismatch");
}
log::debug!("Snapshot hash verified");
}
}
let snapshot: SignedMetadata<SnapshotMetadata> =
serde_json::from_str(&content).context("Failed to parse snapshot.json")?;
log::debug!("Verifying snapshot metadata signature");
self.verify_role_signature(&snapshot, "snapshot")?;
check_expiry(&snapshot.signed.expires)?;
let cache_path = self.metadata_dir.join("snapshot.json");
fs::write(&cache_path, &content)?;
log::debug!("Saved new snapshot metadata to cache");
self.snapshot = Some(snapshot);
Ok(())
}
fn update_targets(&mut self) -> Result<()> {
log::info!("Updating targets metadata");
let snapshot = self.snapshot.as_ref().unwrap();
let _targets_meta = snapshot
.signed
.meta
.get("targets.json")
.ok_or_else(|| anyhow::anyhow!("No targets.json in snapshot"))?;
let url = format!("{}/targets.json", self.config.repository_url());
let content = fetch_url(&url).context("Failed to fetch targets.json")?;
let targets: SignedMetadata<TargetsMetadata> =
serde_json::from_str(&content).context("Failed to parse targets.json")?;
log::debug!("Verifying targets metadata signature");
self.verify_role_signature(&targets, "targets")?;
check_expiry(&targets.signed.expires)?;
let cache_path = self.metadata_dir.join("targets.json");
fs::write(&cache_path, &content)?;
log::debug!("Saved new targets metadata to cache");
self.targets = Some(targets);
Ok(())
}
fn load_channel_targets(&mut self, channel: &str) -> Result<()> {
if self.channel_targets.contains_key(channel) {
log::debug!("Channel targets for '{}' already loaded", channel);
return Ok(());
}
log::info!("Loading channel targets for '{}'", channel);
let url = format!("{}/{}.json", self.config.repository_url(), channel);
let content =
fetch_url(&url).with_context(|| format!("Failed to fetch {}.json", channel))?;
let channel_targets: SignedMetadata<TargetsMetadata> =
serde_json::from_str(&content).context("Failed to parse channel targets")?;
log::debug!(
"Verifying channel targets metadata signature for '{}'",
channel
);
self.verify_delegation_signature(&channel_targets, channel)?;
check_expiry(&channel_targets.signed.expires)?;
let cache_path = self.metadata_dir.join(format!("{}.json", channel));
fs::write(&cache_path, &content)?;
log::debug!(
"Saved new channel targets metadata for '{}' to cache",
channel
);
self.channel_targets
.insert(channel.to_string(), channel_targets);
Ok(())
}
pub fn check_for_updates(&mut self, channel: &str) -> Result<Vec<UpdateInfo>> {
log::info!("Checking for updates for channel '{}'", channel);
self.load_channel_targets(channel)?;
let channel_targets = self.channel_targets.get(channel).unwrap();
let mut updates = Vec::new();
for (target_path, info) in &channel_targets.signed.targets {
log::debug!("Checking target: {}", target_path);
let custom = match &info.custom {
Some(c) => c,
None => {
log::debug!("Target {} has no custom info, skipping", target_path);
continue;
}
};
let component = &custom.image_type;
log::debug!("Target component: {}", component);
let current_version = match component.as_str() {
"rootfs" => {
let version = self
.os_release_info
.get("CITADEL_ROOTFS_VERSION")
.map(|s| s.as_str())
.unwrap_or("0.0.0")
.to_string();
log::debug!("Current rootfs version from os-release: {}", version);
version
}
"kernel" => {
let base_path = format!("/storage/resources/{}", self.config.channel);
// Try to find any citadel-kernel*.img file and pick the highest version
if let Some((image_path, version)) =
find_highest_version_image(&base_path, "citadel-kernel")
{
log::debug!(
"Found kernel image at {} with version {}",
image_path.display(),
version
);
version
} else {
log::debug!("No kernel image found in {}", base_path);
"0.0.0".to_string()
}
}
"extra" => {
let base_path = format!("/storage/resources/{}", self.config.channel);
// Try to find any citadel-extra*.img file and pick the highest version
if let Some((image_path, version)) =
find_highest_version_image(&base_path, "citadel-extra")
{
log::debug!(
"Found extra image at {} with version {}",
image_path.display(),
version
);
version
} else {
log::debug!("No extra image found in {}", base_path);
"0.0.0".to_string()
}
}
_ => {
log::warn!("Unknown component type: {}", component);
"0.0.0".to_string()
}
};
log::debug!(
"Comparing new version {} with current version {}",
&custom.version,
&current_version
);
if version_gt(&custom.version, &current_version) {
// Check min_version requirement
if let Some(min) = &custom.min_version {
if version_gt(min, &current_version) {
log::warn!("Update for {} available, but current version {} is less than minimum required version {}. Skipping.", component, &current_version, min);
continue;
}
}
log::info!(
"Found update for {}: {} -> {}",
component,
&current_version,
&custom.version
);
updates.push(UpdateInfo {
component: component.to_string(),
current_version: current_version.to_string(),
new_version: custom.version.clone(),
target_path: target_path.clone(),
download_size: Some(info.length),
});
} else {
log::debug!("Component {} is up to date", component);
}
}
Ok(updates)
}
pub fn download_target(&mut self, channel: &str, target_path: &str) -> Result<PathBuf> {
log::info!(
"Downloading target '{}' from channel '{}'",
target_path,
channel
);
self.load_channel_targets(channel)?;
let channel_targets = self.channel_targets.get(channel).unwrap();
let target_info = channel_targets
.signed
.targets
.get(target_path)
.ok_or_else(|| anyhow::anyhow!("Target not found: {}", target_path))?;
let expected_hash = target_info
.hashes
.get("sha256")
.ok_or_else(|| anyhow::anyhow!("No sha256 hash for target"))?;
let filename = Path::new(target_path)
.file_name()
.unwrap()
.to_string_lossy();
let url = format!("{}/targets/{}", self.config.repository_url(), target_path);
let local_path = self.targets_dir.join(&*filename);
log::debug!("Downloading from {} to {}", url, local_path.display());
download_file(&url, &local_path, target_info.length)?;
// Verify hash
log::debug!(
"Verifying hash for downloaded file {}",
local_path.display()
);
let actual_hash = hash_file(&local_path)?;
if actual_hash != *expected_hash {
fs::remove_file(&local_path).ok();
log::error!(
"Hash mismatch for {}! Expected: {}, actual: {}",
local_path.display(),
expected_hash,
actual_hash
);
anyhow::bail!(
"Hash mismatch!\n Expected: {}\n Actual: {}",
expected_hash,
actual_hash
);
}
log::info!("Hash verified for {}", local_path.display());
Ok(local_path)
}
pub fn list_channels(&self) -> Result<Vec<ChannelInfo>> {
log::info!("Listing available channels");
let targets = self
.targets
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Targets not loaded"))?;
let mut channels = Vec::new();
if let Some(delegations) = &targets.signed.delegations {
for role in &delegations.roles {
let key_id = role.keyids.first().cloned().unwrap_or_default();
log::debug!("Found channel: {} with key ID: {}", role.name, key_id);
channels.push(ChannelInfo {
name: role.name.clone(),
key_id,
});
}
}
log::debug!("Found {} channels", channels.len());
Ok(channels)
}
pub fn get_channel_key(&self, channel: &str) -> Result<ChannelKey> {
log::info!("Getting channel key for '{}'", channel);
let targets = self
.targets
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Targets not loaded"))?;
let delegations = targets
.signed
.delegations
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No delegations in targets"))?;
let role = delegations
.roles
.iter()
.find(|r| r.name == channel)
.ok_or_else(|| anyhow::anyhow!("Channel '{}' not found", channel))?;
let key_id = role
.keyids
.first()
.ok_or_else(|| anyhow::anyhow!("No key for channel '{}'", channel))?;
let key = delegations
.keys
.get(key_id)
.ok_or_else(|| anyhow::anyhow!("Key {} not found", key_id))?;
log::debug!(
"Retrieved key for channel '{}': key ID = {}, public key = {}...",
channel,
key_id,
&key.keyval.public[..16]
);
Ok(ChannelKey {
key_id: key_id.clone(),
public_key: key.keyval.public.clone(),
})
}
fn verify_root_signature(
&self,
new_root: &SignedMetadata<RootMetadata>,
old_root: &SignedMetadata<RootMetadata>,
) -> Result<()> {
log::debug!("Verifying root signature");
// Verify with old root's keys first
let role_def = old_root
.signed
.roles
.get("root")
.ok_or_else(|| anyhow::anyhow!("No root role in root.json"))?;
log::debug!("Verifying new root with old root's keys");
verify_signatures(
new_root,
&role_def.keyids,
role_def.threshold,
&old_root.signed.keys,
)?;
// Then verify with new root's keys
let new_role_def = new_root
.signed
.roles
.get("root")
.ok_or_else(|| anyhow::anyhow!("No root role in new root.json"))?;
log::debug!("Verifying new root with new root's keys");
verify_signatures(
new_root,
&new_role_def.keyids,
new_role_def.threshold,
&new_root.signed.keys,
)
}
fn verify_role_signature<T: Serialize>(
&self,
signed: &SignedMetadata<T>,
role: &str,
) -> Result<()> {
log::debug!("Verifying role signature for role: {}", role);
let root = self
.root
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Root not loaded"))?;
let role_def = root
.signed
.roles
.get(role)
.ok_or_else(|| anyhow::anyhow!("Role '{}' not found", role))?;
log::debug!(
"Role '{}' found. Key IDs: {:?}, Threshold: {}",
role,
role_def.keyids,
role_def.threshold
);
verify_signatures(
signed,
&role_def.keyids,
role_def.threshold,
&root.signed.keys,
)
}
fn verify_delegation_signature<T: Serialize>(
&self,
signed: &SignedMetadata<T>,
delegation_name: &str,
) -> Result<()> {
log::debug!(
"Verifying delegation signature for delegation: {}",
delegation_name
);
let targets = self
.targets
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Targets not loaded"))?;
let delegations = targets
.signed
.delegations
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No delegations"))?;
let role = delegations
.roles
.iter()
.find(|r| r.name == delegation_name)
.ok_or_else(|| anyhow::anyhow!("Delegation '{}' not found", delegation_name))?;
log::debug!(
"Delegation '{}' found. Key IDs: {:?}, Threshold: {}",
delegation_name,
role.keyids,
role.threshold
);
log::debug!(
"Available delegation keys: {:?}",
delegations.keys.keys().collect::<Vec<_>>()
);
verify_signatures(signed, &role.keyids, role.threshold, &delegations.keys)
}
}
fn verify_signatures<T: Serialize>(
signed: &SignedMetadata<T>,
authorized_keyids: &[String],
threshold: u32,
keys: &HashMap<String, TufKey>,
) -> Result<()> {
let canonical_bytes = canonicalize_json(&signed.signed)?;
log::debug!(
"Canonical JSON (first 200 bytes): {:?}",
&canonical_bytes[..std::cmp::min(200, canonical_bytes.len())]
);
let authorized_set: std::collections::HashSet<&String> = authorized_keyids.iter().collect();
let mut valid_count = 0u32;
log::debug!("Verifying signatures. Threshold: {}", threshold);
log::debug!("Authorized key IDs: {:?}", authorized_keyids);
for sig in &signed.signatures {
log::debug!("Processing signature with key ID: {}", sig.keyid);
if !authorized_set.contains(&sig.keyid) {
log::debug!(
"Signature with key ID {} is not in the authorized set, skipping.",
sig.keyid
);
continue;
}
let tuf_key = match keys.get(&sig.keyid) {
Some(k) => {
log::debug!("Found key for ID {}", sig.keyid);
k
}
None => {
log::warn!("Key not found for ID {}", sig.keyid);
continue;
}
};
if tuf_key.keytype != "ed25519" {
log::warn!(
"Unsupported key type for key ID {}: {}",
sig.keyid,
tuf_key.keytype
);
continue;
}
let pub_bytes = match hex::decode(&tuf_key.keyval.public) {
Ok(b) => b,
Err(e) => {
log::warn!(
"Failed to decode public key for key ID {}: {}",
sig.keyid,
e
);
continue;
}
};
let pub_array: [u8; 32] = match pub_bytes.try_into() {
Ok(a) => a,
Err(_) => {
log::warn!("Public key for key ID {} has incorrect length", sig.keyid);
continue;
}
};
let verifying_key = match VerifyingKey::from_bytes(&pub_array) {
Ok(k) => k,
Err(e) => {
log::warn!(
"Failed to construct verifying key from public key for key ID {}: {}",
sig.keyid,
e
);
continue;
}
};
let sig_bytes = match hex::decode(&sig.sig) {
Ok(b) => b,
Err(e) => {
log::warn!("Failed to decode signature for key ID {}: {}", sig.keyid, e);
continue;
}
};
let sig_array: [u8; 64] = match sig_bytes.try_into() {
Ok(a) => a,
Err(_) => {
log::warn!("Signature for key ID {} has incorrect length", sig.keyid);
continue;
}
};
let signature = Signature::from_bytes(&sig_array);
if verifying_key.verify(&canonical_bytes, &signature).is_ok() {
log::debug!("Signature verified successfully for key ID {}", sig.keyid);
valid_count += 1;
} else {
log::warn!("Signature verification failed for key ID {}", sig.keyid);
}
}
log::debug!(
"Final verification count: valid_count={}, threshold={}",
valid_count,
threshold
);
if valid_count >= threshold {
log::debug!(
"Signature verification successful. Got {} valid signatures, threshold is {}",
valid_count,
threshold
);
Ok(())
} else {
log::error!(
"Signature verification failed: got {} valid, need {}",
valid_count,
threshold
);
anyhow::bail!(
"Signature verification failed: got {} valid, need {}",
valid_count,
threshold
)
}
}
fn canonicalize_json<T: Serialize>(value: &T) -> Result<Vec<u8>> {
let json_value = serde_json::to_value(value)?;
let canonical = sort_json_keys(json_value);
Ok(serde_json::to_vec(&canonical)?)
}
fn sort_json_keys(value: serde_json::Value) -> serde_json::Value {
use serde_json::Value;
use std::collections::BTreeMap;
match value {
Value::Object(map) => {
let sorted: BTreeMap<String, Value> = map
.into_iter()
.map(|(k, v)| (k, sort_json_keys(v)))
.collect();
Value::Object(sorted.into_iter().collect())
}
Value::Array(arr) => Value::Array(arr.into_iter().map(sort_json_keys).collect()),
other => other,
}
}
fn load_or_bootstrap_root(metadata_dir: &Path) -> Result<SignedMetadata<RootMetadata>> {
let cached_path = metadata_dir.join("root.json");
if cached_path.exists() {
let content = fs::read_to_string(&cached_path)?;
if let Ok(root) = serde_json::from_str(&content) {
return Ok(root);
}
}
if Path::new(EMBEDDED_ROOT).exists() {
let content = fs::read_to_string(EMBEDDED_ROOT)?;
let root: SignedMetadata<RootMetadata> = serde_json::from_str(&content)?;
fs::write(&cached_path, &content)?;
return Ok(root);
}
anyhow::bail!(
"No TUF root.json found at {} or {}",
cached_path.display(),
EMBEDDED_ROOT
)
}
fn check_expiry(expires: &str) -> Result<()> {
use chrono::{DateTime, Utc};
let expiry: DateTime<Utc> = expires.parse().context("Failed to parse expiry")?;
if expiry < Utc::now() {
anyhow::bail!("Metadata expired: {}", expires);
}
Ok(())
}
fn fetch_url(url: &str) -> Result<String> {
let mut response = ureq::get(url)
.call()
.with_context(|| format!("Failed to fetch {}", url))?;
response
.body_mut()
.read_to_string()
.with_context(|| format!("Failed to read response from {}", url))
}
fn download_file(url: &str, dest: &Path, expected_size: u64) -> Result<()> {
println!(" Downloading from {}", url);
// Create progress bar
let pb = ProgressBar::new(expected_size);
pb.set_style(
ProgressStyle::default_bar()
.template(" [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
.unwrap()
.progress_chars("#>-"),
);
let mut response = ureq::get(url).call().context("Failed to download file")?;
let mut file =
File::create(dest).with_context(|| format!("Failed to create file: {}", dest.display()))?;
let mut downloaded = 0u64;
let mut buffer = [0u8; 8192];
let mut reader = response.body_mut().as_reader();
loop {
match reader.read(&mut buffer) {
Ok(0) => break,
Ok(n) => {
file.write_all(&buffer[..n])?;
downloaded += n as u64;
pb.set_position(downloaded);
}
Err(e) => return Err(e.into()),
}
}
pb.finish_and_clear();
// Verify size
if downloaded != expected_size {
fs::remove_file(dest).ok();
anyhow::bail!(
"Size mismatch: expected {} bytes, got {}",
expected_size,
downloaded
);
}
println!(" ✓ Downloaded {} bytes", downloaded);
Ok(())
}
fn hash_file(path: &Path) -> Result<String> {
let mut file = File::open(path)?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 8192];
loop {
let n = file.read(&mut buffer)?;
if n == 0 {
break;
}
hasher.update(&buffer[..n]);
}
Ok(hex::encode(hasher.finalize()))
}
/// Find the highest versioned image file matching the pattern
/// e.g., citadel-kernel-*.img or citadel-extra-*.img
fn find_highest_version_image(
base_path: &str,
component_prefix: &str,
) -> Option<(PathBuf, String)> {
use glob::glob;
let pattern = format!("{}/{}*.img", base_path, component_prefix);
log::debug!("Searching for images with pattern: {}", pattern);
let mut best_path: Option<PathBuf> = None;
let mut best_version = "0.0.0".to_string();
if let Ok(entries) = glob(&pattern) {
for entry in entries.flatten() {
log::debug!("Found image file: {}", entry.display());
// Try to read version from the image header
if let Ok(header) = ImageHeader::from_file(&entry) {
let version = if component_prefix.contains("kernel") {
header
.metainfo()
.kernel_version()
.map(|v| v.to_string())
.unwrap_or_else(|| "0.0.0".to_string())
} else {
header.metainfo().version().to_string()
};
log::debug!(" Image version: {}", version);
if version_gt(&version, &best_version) {
best_version = version;
best_path = Some(entry);
}
}
}
}
best_path.map(|path| {
log::debug!(
"Selected highest version image: {} (version: {})",
path.display(),
best_version
);
(path, best_version)
})
}
fn version_gt(a: &str, b: &str) -> bool {
let parse = |s: &str| -> Vec<u32> { s.split('.').filter_map(|p| p.parse().ok()).collect() };
let va = parse(a);
let vb = parse(b);
for (a, b) in va.iter().zip(vb.iter()) {
if a > b {
return true;
}
if a < b {
return false;
}
}
va.len() > vb.len()
}

View File

@@ -0,0 +1,110 @@
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
const UPDATE_CONF_PATH: &str = "/etc/citadel/update.conf";
const USER_CONF_PATH: &str = "/storage/citadel-state/citadel.conf";
const OS_RELEASE_PATH: &str = "/etc/os-release";
#[derive(Debug, Clone)]
pub struct Config {
pub base_url: String,
pub client: String,
pub channel: String,
pub min_root_version: u32,
pub require_signatures: bool,
}
impl Config {
pub fn load() -> Result<Self> {
let system_conf = parse_conf_file(Path::new(UPDATE_CONF_PATH)).unwrap_or_default();
let user_conf = parse_conf_file(Path::new(USER_CONF_PATH)).unwrap_or_default();
let os_release = parse_conf_file(Path::new(OS_RELEASE_PATH)).unwrap_or_default();
// User config overrides system config, which overrides os-release
let channel = user_conf
.get("UPDATE_CHANNEL")
.or_else(|| system_conf.get("DEFAULT_CHANNEL"))
.or_else(|| os_release.get("CITADEL_CHANNEL"))
.cloned()
.unwrap_or_else(|| "stable".to_string());
let client = system_conf
.get("UPDATE_CLIENT")
.cloned()
.unwrap_or_else(|| "public".to_string());
let base_url = system_conf
.get("UPDATE_BASE_URL")
.cloned()
.unwrap_or_else(|| "https://update.subgraph.com".to_string());
let min_root_version = system_conf
.get("MIN_ROOT_VERSION")
.and_then(|s| s.parse().ok())
.unwrap_or(1);
let require_signatures = system_conf
.get("REQUIRE_SIGNATURES")
.map(|s| s == "true" || s == "1")
.unwrap_or(true);
Ok(Config {
base_url,
client,
channel,
min_root_version,
require_signatures,
})
}
pub fn save(&self) -> Result<()> {
let dir = Path::new(USER_CONF_PATH).parent().unwrap();
fs::create_dir_all(dir)?;
let mut content = String::new();
// Read existing config to preserve other settings
if let Ok(existing) = fs::read_to_string(USER_CONF_PATH) {
for line in existing.lines() {
if !line.starts_with("UPDATE_CHANNEL=") {
content.push_str(line);
content.push('\n');
}
}
}
content.push_str(&format!("UPDATE_CHANNEL=\"{}\"\n", self.channel));
fs::write(USER_CONF_PATH, content)?;
Ok(())
}
pub fn repository_url(&self) -> String {
format!("{}/{}", self.base_url, self.client)
}
}
pub fn parse_conf_file(path: &Path) -> Result<HashMap<String, String>> {
let content =
fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display()))?;
let mut conf = HashMap::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(pos) = line.find('=') {
let key = line[..pos].trim().to_string();
let value = line[pos + 1..].trim().trim_matches('"').to_string();
conf.insert(key, value);
}
}
Ok(conf)
}

View File

@@ -0,0 +1,210 @@
use anyhow::{Context, Result};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
const KEYRING_PATH: &str = "/storage/citadel-state/trusted-keys/keyring.json";
const BUILTIN_KEYS_DIR: &str = "/usr/share/citadel/keys";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Keyring {
pub version: u32,
pub trusted_keys: HashMap<String, TrustedKey>,
#[serde(default)]
pub default_channels: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrustedKey {
pub key_id: String,
pub public_key: String,
pub added_at: String,
pub trust_level: TrustLevel,
pub comment: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TrustLevel {
Default,
UserApproved,
Pending,
}
#[derive(Debug, Clone)]
pub enum TrustStatus {
TrustedDefault,
TrustedUser,
Pending,
Unknown,
KeyMismatch { expected: String, actual: String },
}
#[derive(Debug, Clone)]
pub struct ChannelKey {
pub key_id: String,
pub public_key: String,
}
impl Keyring {
pub fn load() -> Result<Self> {
let mut keyring = if Path::new(KEYRING_PATH).exists() {
let content = fs::read_to_string(KEYRING_PATH).context("Failed to read keyring")?;
serde_json::from_str(&content).context("Failed to parse keyring")?
} else {
Keyring {
version: 1,
trusted_keys: HashMap::new(),
default_channels: Vec::new(),
}
};
// Load built-in keys from rootfs
keyring.load_builtin_keys()?;
Ok(keyring)
}
fn load_builtin_keys(&mut self) -> Result<()> {
let keys_dir = Path::new(BUILTIN_KEYS_DIR);
if !keys_dir.exists() {
return Ok(());
}
for entry in fs::read_dir(keys_dir)? {
let entry = entry?;
let path = entry.path();
// Look for channel public keys (not image keys)
let filename = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
// Skip image signing keys (*_image.pub)
if filename.ends_with("_image") {
continue;
}
if path.extension().map_or(false, |e| e == "pub") {
let channel = filename.to_string();
let public_key = fs::read_to_string(&path)?.trim().to_string();
let key_id = compute_key_id(&public_key);
// Built-in keys don't override user keys
if !self.trusted_keys.contains_key(&channel) {
self.trusted_keys.insert(
channel.clone(),
TrustedKey {
key_id,
public_key,
added_at: "built-in".to_string(),
trust_level: TrustLevel::Default,
comment: Some("Shipped with OS".to_string()),
},
);
if !self.default_channels.contains(&channel) {
self.default_channels.push(channel);
}
}
}
}
Ok(())
}
pub fn save(&self) -> Result<()> {
let dir = Path::new(KEYRING_PATH).parent().unwrap();
fs::create_dir_all(dir)?;
// Only save non-default keys (default keys come from rootfs)
let saveable: HashMap<_, _> = self
.trusted_keys
.iter()
.filter(|(_, k)| k.trust_level != TrustLevel::Default)
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let save_keyring = Keyring {
version: self.version,
trusted_keys: saveable,
default_channels: Vec::new(), // Don't save default list
};
let content = serde_json::to_string_pretty(&save_keyring)?;
fs::write(KEYRING_PATH, content)?;
Ok(())
}
pub fn check_trust(&self, channel: &str, key_id: &str) -> TrustStatus {
match self.trusted_keys.get(channel) {
Some(key) => {
if key.key_id == key_id {
match key.trust_level {
TrustLevel::Default => TrustStatus::TrustedDefault,
TrustLevel::UserApproved => TrustStatus::TrustedUser,
TrustLevel::Pending => TrustStatus::Pending,
}
} else {
TrustStatus::KeyMismatch {
expected: key.key_id.clone(),
actual: key_id.to_string(),
}
}
}
None => TrustStatus::Unknown,
}
}
pub fn get_key(&self, channel: &str) -> Option<&TrustedKey> {
self.trusted_keys.get(channel)
}
pub fn is_default(&self, channel: &str) -> bool {
self.default_channels.contains(&channel.to_string())
}
pub fn add_trusted(
&mut self,
channel: &str,
key: &ChannelKey,
level: TrustLevel,
) -> Result<()> {
self.trusted_keys.insert(
channel.to_string(),
TrustedKey {
key_id: key.key_id.clone(),
public_key: key.public_key.clone(),
added_at: Utc::now().to_rfc3339(),
trust_level: level,
comment: None,
},
);
self.save()
}
pub fn add_pending(&mut self, channel: &str, key: &ChannelKey) -> Result<()> {
self.add_trusted(channel, key, TrustLevel::Pending)
}
pub fn approve(&mut self, channel: &str) -> Result<()> {
if let Some(key) = self.trusted_keys.get_mut(channel) {
key.trust_level = TrustLevel::UserApproved;
key.added_at = Utc::now().to_rfc3339();
key.comment = Some("Approved by user".to_string());
}
self.save()
}
pub fn remove(&mut self, channel: &str) -> Result<()> {
self.trusted_keys.remove(channel);
self.save()
}
}
fn compute_key_id(public_key: &str) -> String {
let hash = Sha256::digest(public_key.as_bytes());
hex::encode(&hash[..8])
}

View File

@@ -0,0 +1,121 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedMetadata<T> {
pub signatures: Vec<Signature>,
pub signed: T,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Signature {
pub keyid: String,
pub sig: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RootMetadata {
#[serde(rename = "_type")]
pub metadata_type: String,
pub spec_version: String,
pub version: u32,
pub expires: String,
pub keys: HashMap<String, TufKey>,
pub roles: HashMap<String, RoleDefinition>,
pub consistent_snapshot: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TufKey {
pub keytype: String,
pub scheme: String,
pub keyval: KeyValue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyValue {
pub public: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoleDefinition {
pub keyids: Vec<String>,
pub threshold: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimestampMetadata {
#[serde(rename = "_type")]
pub metadata_type: String,
pub spec_version: String,
pub version: u32,
pub expires: String,
pub meta: HashMap<String, MetaFile>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetaFile {
pub version: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub length: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hashes: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotMetadata {
#[serde(rename = "_type")]
pub metadata_type: String,
pub spec_version: String,
pub version: u32,
pub expires: String,
pub meta: HashMap<String, MetaFile>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetsMetadata {
#[serde(rename = "_type")]
pub metadata_type: String,
pub spec_version: String,
pub version: u32,
pub expires: String,
pub targets: HashMap<String, TargetInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delegations: Option<Delegations>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetInfo {
pub length: u64,
pub hashes: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom: Option<TargetCustom>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetCustom {
pub version: String,
pub image_type: String,
pub channel: String,
pub timestamp: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub release_notes_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Delegations {
pub keys: HashMap<String, TufKey>,
pub roles: Vec<DelegatedRole>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DelegatedRole {
pub name: String,
pub keyids: Vec<String>,
pub threshold: u32,
pub paths: Vec<String>,
#[serde(default)]
pub terminating: bool,
}

View File

@@ -0,0 +1,699 @@
mod client;
mod config;
mod keyring;
mod metadata;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::path::Path;
#[derive(Parser)]
#[command(name = "citadel-fetch")]
#[command(about = "Citadel update client")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
/// Check for available updates
Check,
/// Download and install updates
Update {
/// Don't prompt for confirmation
#[arg(short, long)]
yes: bool,
/// Only download specific component
#[arg(short, long)]
component: Option<String>,
},
/// Show current update status
Status,
/// Manage update channels
Channel {
#[command(subcommand)]
command: ChannelCommands,
},
/// Manage trusted signing keys
Keyring {
#[command(subcommand)]
command: KeyringCommands,
},
/// Refresh TUF metadata from server
Refresh,
}
#[derive(Debug, Subcommand)]
enum ChannelCommands {
/// Show current channel
Show,
/// List available channels
List,
/// Switch to a different channel
Set {
/// Channel name to switch to
channel: String,
},
}
#[derive(Debug, Subcommand)]
enum KeyringCommands {
/// List all trusted keys
List,
/// Show details of a specific channel's key
Show {
/// Channel name
channel: String,
},
/// Add a key from the remote repository
Add {
/// Channel name
channel: String,
},
/// Remove a key from the keyring
Remove {
/// Channel name
channel: String,
},
/// Discover available channels from remote
Discover,
}
pub fn main() {
env_logger::init();
if let Err(e) = run() {
eprintln!("Error: {:#}", e);
std::process::exit(1);
}
}
fn run() -> Result<()> {
let cli = Cli::parse();
log::debug!("Executing command: {:?}", cli.command);
match cli.command {
Commands::Check => cmd_check(),
Commands::Update { yes, component } => cmd_update(yes, component),
Commands::Status => cmd_status(),
Commands::Channel { command } => match command {
ChannelCommands::Show => cmd_channel_show(),
ChannelCommands::List => cmd_channel_list(),
ChannelCommands::Set { channel } => cmd_channel_set(&channel),
},
Commands::Keyring { command } => match command {
KeyringCommands::List => cmd_keyring_list(),
KeyringCommands::Show { channel } => cmd_keyring_show(&channel),
KeyringCommands::Add { channel } => cmd_keyring_add(&channel),
KeyringCommands::Remove { channel } => cmd_keyring_remove(&channel),
KeyringCommands::Discover => cmd_keyring_discover(),
},
Commands::Refresh => cmd_refresh(),
}
}
fn cmd_check() -> Result<()> {
log::debug!("Executing cmd_check");
let config = config::Config::load()?;
let mut client = client::TufClient::new(&config)?;
println!("Checking for updates...");
println!(" Repository: {}", config.repository_url());
println!(" Channel: {}", config.channel);
client.refresh_metadata()?;
let updates = client.check_for_updates(&config.channel)?;
if updates.is_empty() {
println!("✓ System is up to date");
} else {
println!("Updates available:");
for update in &updates {
println!(
" {} {}{}",
update.component, update.current_version, update.new_version
);
if let Some(size) = update.download_size {
println!(" Download size: {:.1} MB", size as f64 / 1_048_576.0);
}
}
println!("Run 'citadel-fetch update' to install updates");
}
Ok(())
}
fn cmd_update(yes: bool, component: Option<String>) -> Result<()> {
log::debug!(
"Executing cmd_update with yes={}, component={:?}",
yes,
component
);
let config = config::Config::load()?;
let mut keyring = keyring::Keyring::load()?;
let mut client = client::TufClient::new(&config)?;
println!("Checking for updates...");
client.refresh_metadata()?;
// Verify channel key trust
let channel_key = client.get_channel_key(&config.channel)?;
let trust_status = keyring.check_trust(&config.channel, &channel_key.key_id);
println!(
"Channel '{}' trust status: {:?}",
config.channel, trust_status
);
match &trust_status {
keyring::TrustStatus::TrustedDefault => {
println!(
" Channel '{}' signed by: {} (default key)",
config.channel,
&channel_key.key_id[..16]
);
}
keyring::TrustStatus::TrustedUser => {
println!(
" Channel '{}' signed by: {} (user approved)",
config.channel,
&channel_key.key_id[..16]
);
}
keyring::TrustStatus::Unknown => {
println!(
"\n⚠ WARNING: Channel '{}' key not in trusted keyring",
config.channel
);
println!(" Key ID: {}", channel_key.key_id);
println!(" Public Key: {}...", &channel_key.public_key[..48]);
println!("\n This key was verified by the TUF root certificate,");
println!(" but you haven't explicitly trusted it yet.");
if !yes && !prompt_yes_no("\nTrust this key and continue?")? {
println!("Update cancelled by user");
return Ok(());
}
keyring.add_trusted(
&config.channel,
&channel_key,
keyring::TrustLevel::UserApproved,
)?;
println!(
"✓ Channel key for '{}' added to trusted keyring",
config.channel
);
}
keyring::TrustStatus::KeyMismatch { expected, actual } => {
println!("\n🚨 WARNING: Channel key has changed!");
println!(" Expected: {}", expected);
println!(" Received: {}", actual);
if !yes {
println!("\nThis could indicate a security issue. Contact channel maintainer.");
if !prompt_yes_no("Accept new key anyway?")? {
println!("Update cancelled by user due to key mismatch");
return Ok(());
}
}
keyring.add_trusted(
&config.channel,
&channel_key,
keyring::TrustLevel::UserApproved,
)?;
println!(
"✓ New channel key for '{}' accepted and added to trusted keyring",
config.channel
);
}
keyring::TrustStatus::Pending => {
println!("\n⚠ Channel key pending approval");
if !yes && !prompt_yes_no("Approve this key and continue?")? {
println!("Update cancelled by user");
return Ok(());
}
keyring.approve(&config.channel)?;
println!("✓ Channel key for '{}' approved", config.channel);
}
}
let updates = client.check_for_updates(&config.channel)?;
// Filter by component if specified
let updates: Vec<_> = if let Some(ref comp) = component {
println!("Filtering updates for component: {}", comp);
updates
.into_iter()
.filter(|u| &u.component == comp)
.collect()
} else {
updates
};
if updates.is_empty() {
println!("\n✓ System is up to date");
return Ok(());
}
println!("\nUpdates to install:");
let mut total_size: u64 = 0;
for update in &updates {
println!(
" {} {}{}",
update.component, update.current_version, update.new_version
);
if let Some(size) = update.download_size {
total_size += size;
}
}
println!(
"\nTotal download: {:.1} MB",
total_size as f64 / 1_048_576.0
);
if !yes && !prompt_yes_no("\nProceed with update?")? {
println!("Update cancelled by user");
return Ok(());
}
println!("Initiating download and installation...");
for update in &updates {
println!("Downloading {}...", update.component);
let path = client.download_target(&config.channel, &update.target_path)?;
println!(" ✓ Downloaded to {}", path.display());
println!("Installing {}...", update.component);
install_update(&update.component, &path)?;
println!(" ✓ Installed");
}
println!("\n═══════════════════════════════════════════════════════════");
println!(" ✓ UPDATE COMPLETE");
println!(" Reboot to apply changes");
println!("═══════════════════════════════════════════════════════════");
Ok(())
}
fn cmd_status() -> Result<()> {
log::debug!("Executing cmd_status");
let config = config::Config::load()?;
let os_release_info = config::parse_conf_file(Path::new("/etc/os-release")).unwrap_or_default();
println!("Citadel Update Status");
println!("═══════════════════════════════════════════════════════════");
println!("System Information:");
println!(
" Distro: {} {}",
os_release_info
.get("NAME")
.unwrap_or(&"Unknown".to_string()),
os_release_info
.get("VERSION_ID")
.unwrap_or(&"Unknown".to_string())
);
println!(" Channel: {}", config.channel);
println!("Update Server:");
println!(" URL: {}", config.repository_url());
Ok(())
}
fn cmd_channel_show() -> Result<()> {
log::debug!("Executing cmd_channel_show");
let config = config::Config::load()?;
let keyring = keyring::Keyring::load()?;
println!("Current channel: {}", config.channel);
if let Some(key) = keyring.get_key(&config.channel) {
println!(" Key ID: {}", key.key_id);
println!(" Trust: {:?}", key.trust_level);
} else {
println!(" No key found for current channel in keyring.");
}
Ok(())
}
fn cmd_channel_list() -> Result<()> {
log::debug!("Executing cmd_channel_list");
let config = config::Config::load()?;
let mut client = client::TufClient::new(&config)?;
let keyring = keyring::Keyring::load()?;
client.refresh_metadata()?;
let channels = client.list_channels()?;
println!("Available channels:");
println!("───────────────────────────────────────────────────────────");
if channels.is_empty() {
println!(" No channels found.");
} else {
for channel in channels {
let current = if channel.name == config.channel {
" (current)"
} else {
""
};
let trust = match keyring.get_key(&channel.name) {
Some(k) => match k.trust_level {
keyring::TrustLevel::Default => "✓ default",
keyring::TrustLevel::UserApproved => "✓ trusted",
keyring::TrustLevel::Pending => "○ pending",
},
None => "○ unknown",
};
println!(
" {:<12} {} [{}]{}",
channel.name,
&channel.key_id[..16],
trust,
current
);
}
}
Ok(())
}
fn cmd_channel_set(channel: &str) -> Result<()> {
log::debug!("Executing cmd_channel_set for channel '{}'", channel);
let mut config = config::Config::load()?;
let mut keyring = keyring::Keyring::load()?;
let mut client = client::TufClient::new(&config)?;
println!("Switching to channel '{}'...", channel);
// Refresh metadata to get channel info
client.refresh_metadata()?;
// Get the channel's signing key
let channel_key = client
.get_channel_key(channel)
.with_context(|| format!("Channel '{}' not found", channel))?;
let trust_status = keyring.check_trust(channel, &channel_key.key_id);
println!("Channel '{}' trust status: {:?}", channel, trust_status);
match trust_status {
keyring::TrustStatus::TrustedDefault => {
println!(
"\n Key: {} (default, shipped with OS)",
&channel_key.key_id[..16]
);
}
keyring::TrustStatus::TrustedUser => {
println!(
"\n Key: {} (previously approved)",
&channel_key.key_id[..16]
);
}
keyring::TrustStatus::Pending => {
println!("\n════════════════════════════════════════════════════════");
println!(" PENDING KEY APPROVAL");
println!("════════════════════════════════════════════════════════");
println!("\n Channel: {}", channel);
println!(" Key ID: {}", channel_key.key_id);
println!("\n This key was fetched but not yet approved.\n");
if !prompt_yes_no("Approve this key?")? {
println!("Channel switch cancelled by user");
return Ok(());
}
keyring.approve(channel)?;
println!("Channel key for '{}' approved", channel);
}
keyring::TrustStatus::Unknown => {
println!("\n════════════════════════════════════════════════════════");
println!(" NEW CHANNEL KEY");
println!("════════════════════════════════════════════════════════");
println!("\n Channel: {}", channel);
println!(" Key ID: {}", channel_key.key_id);
println!(" Key: {}...", &channel_key.public_key[..48]);
println!("\n ⚠ This key is not in your trusted keyring.");
println!(" Only trust keys from verified sources.\n");
if !prompt_yes_no("Trust this key?")? {
println!("Channel switch cancelled by user");
return Ok(());
}
keyring.add_trusted(channel, &channel_key, keyring::TrustLevel::UserApproved)?;
println!(
"New channel key for '{}' trusted and added to keyring",
channel
);
}
keyring::TrustStatus::KeyMismatch { expected, actual } => {
println!("\n════════════════════════════════════════════════════════");
println!(" 🚨 KEY MISMATCH WARNING");
println!("════════════════════════════════════════════════════════");
println!("\n Channel: {}", channel);
println!(" Expected: {}", expected);
println!(" Received: {}", actual);
println!("\n The signing key for this channel has changed.");
println!(" This could indicate:");
println!(" - Legitimate key rotation");
println!(" - A potential security issue");
println!("\n Verify with channel maintainer before accepting.\n");
if !prompt_yes_no("Accept new key?")? {
println!("Channel switch cancelled by user due to key mismatch");
return Ok(());
}
keyring.add_trusted(channel, &channel_key, keyring::TrustLevel::UserApproved)?;
println!(
"New key for channel '{}' accepted despite mismatch",
channel
);
}
}
// Save new channel to config
config.channel = channel.to_string();
config.save()?;
println!("\n✓ Channel switched to '{}'", channel);
Ok(())
}
fn cmd_keyring_list() -> Result<()> {
println!("Executing cmd_keyring_list");
let keyring = keyring::Keyring::load()?;
println!("Trusted Channel Keys");
println!("═══════════════════════════════════════════════════════════");
if keyring.trusted_keys.is_empty() {
println!(" No keys in keyring");
return Ok(());
}
for (channel, key) in &keyring.trusted_keys {
let trust = match key.trust_level {
keyring::TrustLevel::Default => "[default]",
keyring::TrustLevel::UserApproved => "[user] ",
keyring::TrustLevel::Pending => "[pending]",
};
let comment = key.comment.as_deref().unwrap_or("");
println!(
" {:<12} {} {} {}",
channel,
&key.key_id[..16],
trust,
comment
);
}
Ok(())
}
fn cmd_keyring_show(channel: &str) -> Result<()> {
log::debug!("Executing cmd_keyring_show for channel '{}'", channel);
let keyring = keyring::Keyring::load()?;
match keyring.get_key(channel) {
Some(key) => {
println!("Channel: {}", channel);
println!("═══════════════════════════════════════════════════════════");
println!(" Key ID: {}", key.key_id);
println!(" Public Key: {}", key.public_key);
println!(" Trust: {:?}", key.trust_level);
println!(" Added: {}", key.added_at);
if let Some(comment) = &key.comment {
println!(" Comment: {}", comment);
}
}
None => {
println!("No key found for channel '{}'", channel);
println!("Use 'citadel-fetch keyring add {}' to add it", channel);
}
}
Ok(())
}
fn cmd_keyring_add(channel: &str) -> Result<()> {
log::debug!("Executing cmd_keyring_add for channel '{}'", channel);
let config = config::Config::load()?;
let mut keyring = keyring::Keyring::load()?;
let mut client = client::TufClient::new(&config)?;
println!("Fetching key for channel '{}'...", channel);
client.refresh_metadata()?;
let channel_key = client
.get_channel_key(channel)
.with_context(|| format!("Channel '{}' not found", channel))?;
println!("\n Channel: {}", channel);
println!(" Key ID: {}", channel_key.key_id);
println!(" Key: {}...", &channel_key.public_key[..48]);
if keyring.get_key(channel).is_some() {
println!("\n ⚠ A key for this channel already exists.");
if !prompt_yes_no("Replace existing key?")? {
println!("Key add cancelled by user");
return Ok(());
}
}
if !prompt_yes_no("\nTrust this key?")? {
println!("Key add cancelled by user");
return Ok(());
}
keyring.add_trusted(channel, &channel_key, keyring::TrustLevel::UserApproved)?;
println!("\n✓ Key added to keyring");
Ok(())
}
fn cmd_keyring_remove(channel: &str) -> Result<()> {
log::debug!("Executing cmd_keyring_remove for channel '{}'", channel);
let mut keyring = keyring::Keyring::load()?;
if keyring.get_key(channel).is_none() {
println!("No key found for channel '{}'", channel);
return Ok(());
}
if keyring.is_default(channel) {
println!("⚠ '{}' is a default key shipped with the OS.", channel);
println!(" Removing it may prevent updates from this channel.");
}
if !prompt_yes_no(&format!("Remove key for '{}'?", channel))? {
println!("Key removal cancelled by user");
return Ok(());
}
keyring.remove(channel)?;
println!("✓ Key removed");
Ok(())
}
fn cmd_keyring_discover() -> Result<()> {
log::debug!("Executing cmd_keyring_discover");
let config = config::Config::load()?;
let mut client = client::TufClient::new(&config)?;
let keyring = keyring::Keyring::load()?;
println!("Discovering channels from {}...\n", config.base_url);
client.refresh_metadata()?;
let channels = client.list_channels()?;
println!("Available Channels");
println!("═══════════════════════════════════════════════════════════");
if channels.is_empty() {
println!(" No channels found in repository.");
} else {
for channel in channels {
let status = match keyring.get_key(&channel.name) {
Some(k) => match k.trust_level {
keyring::TrustLevel::Default => "✓ trusted (default)",
keyring::TrustLevel::UserApproved => "✓ trusted (user)",
keyring::TrustLevel::Pending => "○ pending approval",
},
None => "○ not in keyring",
};
println!(
" {:<12} {} {}",
channel.name,
&channel.key_id[..16],
status
);
}
}
println!("\nAdd a key: citadel-fetch keyring add <channel>");
Ok(())
}
fn cmd_refresh() -> Result<()> {
log::debug!("Executing cmd_refresh");
let config = config::Config::load()?;
let mut client = client::TufClient::new(&config)?;
println!("Refreshing TUF metadata...");
client.refresh_metadata()?;
println!("✓ Metadata refreshed");
Ok(())
}
fn prompt_yes_no(prompt: &str) -> Result<bool> {
use std::io::{self, Write};
print!("{} (y/N): ", prompt);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let answer = input.trim().to_lowercase();
Ok(answer == "y" || answer == "yes")
}
fn install_update(component: &str, image_path: &std::path::Path) -> Result<()> {
// This integrates with libcitadel's image installation
// For now, just copy to the appropriate location
use std::process::Command;
let dest = match component {
"rootfs" => "/run/citadel/images/citadel-rootfs.img",
"kernel" => "/run/citadel/images/citadel-kernel.img",
"extra" => "/run/citadel/images/citadel-extra.img",
_ => return Err(anyhow::anyhow!("Unknown component: {}", component)),
};
// Use citadel-image to install
let status = Command::new("/usr/bin/citadel-image")
.args(["install", &image_path.to_string_lossy(), dest])
.status()?;
if !status.success() {
anyhow::bail!("Failed to install {} image", component);
}
Ok(())
}

View File

@@ -1,88 +1,98 @@
use clap::{command, ArgAction, Command};
use clap::{Arg, ArgMatches};
use hex;
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 hex;
use libcitadel::public_key_for_channel;
use libcitadel::{util, ImageHeader, KeyPair, Partition, ResourceImage, Result};
pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
let app = App::new("citadel-image")
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,42 +100,40 @@ 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())?);
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 {
println!("Signature: No Signature");
return Ok(());
}
match img.header().public_key()? {
Some(pubkey) => {
if img.header().verify_signature(pubkey) {
println!("Signature is valid");
} else {
println!("Signature verify FAILED");
}
},
None => { println!("No public key found for channel '{}'", img.metainfo().channel()) },
let pubkey = public_key_for_channel(img.metainfo().channel())?;
if img.header().verify_signature(pubkey) {
println!("Signature is valid");
} else {
println!("Signature verify FAILED");
}
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())?);
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 +143,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 +154,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 +167,40 @@ 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(());
}
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 +208,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,27 +217,33 @@ 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();
// XXX verify signature?
// Use existing function from libcitadel
let pubkey = public_key_for_channel(metainfo.channel())?;
if !img.header().verify_signature(pubkey) {
bail!("Image signature verification failed");
}
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,18 +251,22 @@ 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!(
bail!(
"Refusing to build path from strange channel name {}",
metainfo.channel()
);
@@ -255,28 +276,26 @@ fn install_image(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Err
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 +305,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 +324,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 +339,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 +349,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")
}

View File

@@ -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>> {
let mut install = Installer::new(disk.path(), &citadel_passphrase, &passphrase);
fn run_install(disk: Disk, citadel_passphrase: String, passphrase: String) -> Result<()> {
let mut install = Installer::new(disk.path(), &citadel_passphrase, &passphrase, None);
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")
}

View File

@@ -9,8 +9,10 @@ use pwhash::sha512_crypt;
use libcitadel::util;
use libcitadel::RealmFS;
use libcitadel::Result;
use libcitadel::OsRelease;
use libcitadel::KeyRing;
use libcitadel::ResourceImage;
use libcitadel::terminal::Base16Scheme;
use libcitadel::UtsName;
@@ -19,6 +21,8 @@ const LUKS_UUID: &str = "683a17fc-4457-42cc-a946-cde67195a101";
const EXTRA_IMAGE_NAME: &str = "citadel-extra.img";
const INSTALL_MOUNT: &str = "/run/installer/mnt";
const STORAGE_MOUNT: &str = "/run/installer/storage";
const LUKS_PASSPHRASE_FILE: &str = "/run/installer/luks-passphrase";
const DEFAULT_ARTIFACT_DIRECTORY: &str = "/run/citadel/images";
@@ -124,12 +128,13 @@ pub struct Installer {
target_device: Option<PathBuf>,
citadel_passphrase: Option<String>,
passphrase: Option<String>,
timezone: Option<String>,
artifact_directory: String,
logfile: Option<RefCell<File>>,
}
impl Installer {
pub fn new<P: AsRef<Path>>(target_device: P, citadel_passphrase: &str, passphrase: &str) -> Installer {
pub fn new<P: AsRef<Path>>(target_device: P, citadel_passphrase: &str, passphrase: &str, timezone: Option<String>) -> Installer {
let target_device = Some(target_device.as_ref().to_owned());
let citadel_passphrase = Some(citadel_passphrase.to_owned());
let passphrase = Some(passphrase.to_owned());
@@ -140,6 +145,7 @@ impl Installer {
target_device,
citadel_passphrase,
passphrase,
timezone,
artifact_directory: DEFAULT_ARTIFACT_DIRECTORY.to_string(),
logfile: None,
}
@@ -153,6 +159,7 @@ impl Installer {
target_device: None,
citadel_passphrase: None,
passphrase: None,
timezone: None,
artifact_directory: DEFAULT_ARTIFACT_DIRECTORY.to_string(),
logfile: None,
}
@@ -182,7 +189,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 +198,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 +230,20 @@ impl Installer {
Ok(())
}
pub fn run_live_setup(&self) -> Result<(), Box<dyn std::error::Error>> {
pub fn setup_localtime_symlink(&self, timezone: Option<String>) -> Result<()> {
self.header("Setting up localtime symlink")?;
util::create_dir(STORAGE_MOUNT)?;
let storage_path = "/dev/mapper/citadel-storage";
self.cmd(format!("/bin/mount {} {}", storage_path, STORAGE_MOUNT))?;
let rootfs_localtime_path = Path::new("/usr/share/zoneinfo/").join(timezone.unwrap_or("Canada/Eastern".to_string()));
let storage_localtime_link = Path::new(STORAGE_MOUNT).join("citadel-state/localtime");
self.info(format!("Creating symlink from {} to {}", storage_localtime_link.display(), rootfs_localtime_path.display()))?;
util::symlink(&rootfs_localtime_path, storage_localtime_link)?;
self.cmd(format!("/bin/umount {}", STORAGE_MOUNT))?;
Ok(())
}
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 +261,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 +280,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 +300,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 +342,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 +364,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 +384,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 +408,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 +459,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,12 +479,30 @@ impl Installer {
self.create_realmlock(&realm_base)
}
fn setup_storage_resources(&self) -> Result<(), Box<dyn std::error::Error>> {
let channel = match OsRelease::citadel_channel() {
Some(channel) => channel,
None => "dev",
fn setup_storage_resources(&self) -> Result<()> {
// Get the channel from the extra image metadata rather than OsRelease
// because during installation in live mode, OsRelease reads from initramfs
// which doesn't have CITADEL_CHANNEL set, but the images have it in metadata
let extra_path = self.artifact_path(EXTRA_IMAGE_NAME);
let channel = if extra_path.exists() {
match ResourceImage::from_path(&extra_path) {
Ok(img) => img.metainfo().channel().to_string(),
Err(_) => {
// Fallback to OsRelease if we can't read the image
OsRelease::citadel_channel()
.ok_or_else(|| {
format_err!("Failed to determine channel from extra image or OsRelease")
})?
.to_string()
}
}
} else {
OsRelease::citadel_channel()
.ok_or_else(|| format_err!("Failed to determine channel from OsRelease"))?
.to_string()
};
let resources = self.storage().join("resources").join(channel);
let resources = self.storage().join("resources").join(&channel);
util::create_dir(&resources)?;
self.sparse_copy_artifact(EXTRA_IMAGE_NAME, &resources)?;
@@ -476,7 +511,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 +534,21 @@ 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()))
self.cmd(format!("/usr/bin/citadel-image install-rootfs --skip-sha --no-prefer {}", rootfs.display()))?;
if self.timezone.is_some() {
self.setup_localtime_symlink(self.timezone.clone())?;
}
Ok(())
}
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 +587,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 +609,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 +633,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 +643,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 +673,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(())

View File

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

View File

@@ -1,272 +0,0 @@
use std::sync::Arc;
use std::collections::HashMap;
use std::time::Duration;
use std::sync::mpsc;
use std::sync::mpsc::{Sender};
use dbus::tree::{self, Factory, MTFn, MethodResult, Tree};
use dbus::{Message};
use dbus::blocking::LocalConnection;
// Use local version of disk.rs since we added some methods
use crate::install_backend::disk::*;
use crate::install::installer::*;
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";
pub enum Msg {
RunInstall(String, String, String),
LvmSetup(String),
LuksSetup(String),
BootSetup(String),
StorageCreated(String),
RootfsInstalled(String),
InstallCompleted,
InstallFailed(String)
}
pub struct DbusServer {
connection: Arc<LocalConnection>,
//events: EventHandler,
}
impl DbusServer {
pub fn connect() -> Result<DbusServer, Box<dyn std::error::Error>> {
let connection = LocalConnection::new_system()
.map_err(|e| format_err!("Failed to connect to DBUS system bus: {}", e))?;
let connection = Arc::new(connection);
//let events = EventHandler::new(connection.clone());
let server = DbusServer { connection };
Ok(server)
}
fn build_tree(&self, sender: mpsc::Sender<Msg>) -> Tree<MTFn<TData>, TData> {
let f = Factory::new_fn::<TData>();
let data = TreeData::new();
let interface = f.interface(INTERFACE_NAME, ())
// Methods
.add_m(f.method("GetDisks", (), Self::do_get_disks)
.in_arg(("name", "a{sas}")))
.add_m(f.method("RunInstall", (),move |m| {
let (device, citadel_passphrase, luks_passphrase): (String, String, String) = m.msg.read3()?;
println!("Device: {} Citadel Passphrase: {} Luks Passphrase: {}", device, citadel_passphrase, luks_passphrase);
let _ = sender.send(Msg::RunInstall(device, citadel_passphrase, luks_passphrase));
Ok(vec![m.msg.method_return().append1(true)])
})
.in_arg(("device", "s")).in_arg(("citadel_passphrase", "s")).in_arg(("luks_passphrase", "s")))
.add_s(f.signal("RunInstallStarted", ()))
.add_s(f.signal("InstallCompleted", ()))
.add_s(f.signal("CitadelPasswordSet", ()));
let obpath = f.object_path(OBJECT_PATH, ())
.introspectable()
.add(interface);
f.tree(data).add(obpath)
}
fn do_get_disks(m: &MethodInfo) -> MethodResult {
let list = m.tree.get_data().disks();
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>> {
let mut install = Installer::new(path, &citadel_passphrase, &luks_passphrase);
install.set_install_syslinux(true);
install.verify()?;
install.partition_disk()?;
install.setup_luks()?;
let _ = sender.send(Msg::LuksSetup("+ Setup LUKS disk encryption password successfully\n".to_string()));
install.setup_lvm()?;
let _ = sender.send(Msg::LvmSetup("+ Setup LVM volumes successfully\n".to_string()));
install.setup_boot()?;
let _ = sender.send(Msg::BootSetup("+ Setup /boot partition successfully\n".to_string()));
install.create_storage()?;
let _ = sender.send(Msg::StorageCreated("+ Setup /storage partition successfully\n".to_string()));
install.install_rootfs_partitions()?;
let _ = sender.send(Msg::RootfsInstalled("+ Installed rootfs partitions successfully\n".to_string()));
install.finish_install()?;
Ok(())
}
/*fn process_message(&self, _msg: Message) -> Result<()> {
// add handlers for expected signals here
Ok(())
}*/
fn send_service_started(&self) {
let signal = Self::create_signal("ServiceStarted");
if self.connection.channel().send(signal).is_err() {
warn!("Failed to send ServiceStarted signal");
}
}
fn send_install_completed(&self) {
let signal = Self::create_signal("InstallCompleted");
if self.connection.channel().send(signal).is_err() {
warn!("Failed to send InstallCompleted signal");
}
}
fn send_lvm_setup(&self, text: String) {
let signal = Self::create_signal_with_text("LvmSetup", text);
if self.connection.channel().send(signal).is_err() {
warn!("Failed to send LvmSetup signal");
}
}
fn send_luks_setup(&self, text: String) {
let signal = Self::create_signal_with_text("LuksSetup", text);
if self.connection.channel().send(signal).is_err() {
warn!("Failed to send LuksSetup signal");
}
}
fn send_boot_setup(&self, text: String) {
let signal = Self::create_signal_with_text("BootSetup", text);
if self.connection.channel().send(signal).is_err() {
warn!("Failed to send BootSetup signal");
}
}
fn send_storage_created(&self, text: String) {
let signal = Self::create_signal_with_text("StorageCreated", text);
if self.connection.channel().send(signal).is_err() {
warn!("Failed to send StorageCreated signal");
}
}
fn send_rootfs_installed(&self, text: String) {
let signal = Self::create_signal_with_text("RootfsInstalled", text);
if self.connection.channel().send(signal).is_err() {
warn!("Failed to send StorageCreated signal");
}
}
fn send_install_failed(&self, error: String) {
let signal = Self::create_signal_with_text("InstallFailed", error);
if self.connection.channel().send(signal).is_err() {
warn!("Failed to send StorageCreated signal");
}
}
fn create_signal(name: &str) -> Message {
let path = dbus::Path::new(OBJECT_PATH).unwrap();
let iface = dbus::strings::Interface::new(INTERFACE_NAME).unwrap();
let member = dbus::strings::Member::new(name).unwrap();
Message::signal(&path, &iface, &member)
}
fn create_signal_with_text(name: &str, text: String) -> Message {
let path = dbus::Path::new(OBJECT_PATH).unwrap();
let iface = dbus::strings::Interface::new(INTERFACE_NAME).unwrap();
let member = dbus::strings::Member::new(name).unwrap();
Message::signal(&path, &iface, &member).append1(text)
}
pub fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
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");
}
tree.start_receive(self.connection.as_ref());
self.send_service_started();
loop {
self.connection
.process(Duration::from_millis(1000))
.map_err(context!("Error handling dbus messages"))?;
if let Ok(msg) = receiver.try_recv() {
match msg {
Msg::RunInstall(device, citadel_passphrase, luks_passphrase) => {
let install_sender = sender_clone.clone();
// TODO: Implement more stages
match Self::run_install(device, citadel_passphrase, luks_passphrase, install_sender) {
Ok(_) => {
println!("Install completed");
let _ = sender_clone.send(Msg::InstallCompleted);
},
Err(err) => {
println!("Install error: {}", err);
let _ = sender_clone.send(Msg::InstallFailed(err.to_string()));
}
}
},
Msg::LvmSetup(text) => {
self.send_lvm_setup(text);
},
Msg::LuksSetup(text) => {
self.send_luks_setup(text);
},
Msg::BootSetup(text) => {
self.send_boot_setup(text);
},
Msg::StorageCreated(text) => {
self.send_storage_created(text);
},
Msg::RootfsInstalled(text) => {
self.send_rootfs_installed(text);
},
Msg::InstallCompleted => {
self.send_install_completed();
},
Msg::InstallFailed(text) => {
self.send_install_failed(text);
}
}
}
}
}
}
#[derive(Clone)]
struct TreeData {
}
impl TreeData {
fn new() -> TreeData {
TreeData {}
}
fn disks(&self) -> HashMap<String, Vec<String>> {
let disks = Disk::probe_all().unwrap();
let mut disk_map = HashMap::new();
for d in disks {
let mut fields = vec![];
fields.push(d.model().to_string());
fields.push(d.size_str().to_string());
fields.push(d.removable().to_string());
disk_map.insert(d.path().to_string_lossy().to_string(), fields);
}
disk_map
}
}
impl fmt::Debug for TreeData {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "<TreeData>")
}
}
#[derive(Copy, Clone, Default, Debug)]
struct TData;
impl tree::DataType for TData {
type Tree = TreeData;
type ObjectPath = ();
type Property = ();
type Interface = ();
type Method = ();
type Signal = ();
}

View File

@@ -1,80 +0,0 @@
use std::path::{Path,PathBuf};
use std::fs;
use libcitadel::{Result, util};
#[derive(Debug, Clone)]
pub struct Disk {
path: PathBuf,
size: usize,
size_str: String,
model: String,
removable: bool,
}
impl Disk {
pub fn probe_all() -> Result<Vec<Disk>> {
let mut v = Vec::new();
util::read_directory("/sys/block", |dent| {
let path = dent.path();
if Disk::is_disk_device(&path) {
let disk = Disk::read_device(&path)?;
v.push(disk);
}
Ok(())
})?;
Ok(v)
}
fn is_disk_device(device: &Path) -> bool {
device.join("device/model").exists()
}
fn is_disk_removable(device: &Path) -> bool {
if let Ok(removable) = util::read_to_string(device.join("removable")) {
if removable.trim() == "1" {
return true
}
}
false
}
fn read_device(device: &Path) -> Result<Disk> {
let path = Path::new("/dev/").join(device.file_name().unwrap());
let size = fs::read_to_string(device.join("size"))
.map_err(context!("failed to read device size for {:?}", device))?
.trim()
.parse::<usize>()
.map_err(context!("error parsing device size for {:?}", device))?;
let size_str = format!("{}G", size >> 21);
let model = fs::read_to_string(device.join("device/model"))
.map_err(context!("failed to read device/model for {:?}", device))?
.trim()
.to_string();
let removable = Disk::is_disk_removable(device);
Ok(Disk { path, size, size_str, model, removable })
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn size_str(&self) -> &str {
&self.size_str
}
pub fn model(&self) -> &str {
&self.model
}
pub fn removable(&self) -> &bool {
&self.removable
}
}

View File

@@ -1,20 +1,24 @@
use std::process::exit;
use anyhow::Result;
use log::info;
use tokio::runtime::Runtime;
mod disk;
mod dbus;
use libcitadel::CommandLine;
mod zbus;
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
if CommandLine::install_mode() {
run_dbus_server()
} else {
println!("Citadel installer backend will only run in install or live mode");
exit(1);
pub fn main() -> Result<()> {
let rt = Runtime::new().unwrap();
let res = rt.block_on(zbus::start_zbus_server());
match res {
Ok(_) => {
info!("Starting Installer Zbus server");
}
Err(e) => {
warn!(
"The Zbus server failed to start or encountered a fatal error: {:?}",
e
);
return Err(e);
}
}
}
fn run_dbus_server() -> Result<(), Box<dyn std::error::Error>> {
let server = dbus::DbusServer::connect()?;
server.start()?;
Ok(())
}

View File

@@ -0,0 +1,161 @@
use crate::install::installer::*;
use event_listener::{Event, Listener};
use zbus::connection::Builder;
use zbus::fdo;
use zbus::interface;
use zbus::object_server::SignalEmitter;
use zbus::Result;
const OBJECT_PATH: &str = "/com/subgraph/installer";
const BUS_NAME: &str = "com.subgraph.installer";
pub struct DbusServer {
done: Event,
}
pub async fn start_zbus_server() -> anyhow::Result<()> {
let dbus_server = DbusServer {
done: event_listener::Event::new(),
};
let done_listener = dbus_server.done.listen();
let _connection = Builder::system()?
.name(BUS_NAME)?
.serve_at(OBJECT_PATH, dbus_server)?
.build()
.await?;
done_listener.wait();
Ok(())
}
#[interface(name = "com.subgraph.installer.Manager")]
impl DbusServer {
async fn run_install(
&self,
#[zbus(signal_emitter)] emitter: SignalEmitter<'_>,
device_path: &str,
citadel_passphrase: &str,
luks_passphrase: &str,
) -> fdo::Result<()> {
run_install_task(
emitter,
device_path,
citadel_passphrase,
luks_passphrase,
None,
)
.await
}
async fn run_install_with_timezone(
&self,
#[zbus(signal_emitter)] emitter: SignalEmitter<'_>,
device_path: &str,
citadel_passphrase: &str,
luks_passphrase: &str,
timezone: &str,
) -> fdo::Result<()> {
run_install_task(
emitter,
device_path,
citadel_passphrase,
luks_passphrase,
Some(timezone.to_string()),
)
.await
}
#[zbus(signal)]
async fn run_install_started(emitter: &SignalEmitter<'_>, message: &str) -> Result<()>;
#[zbus(signal)]
async fn citadel_password_set(emitter: &SignalEmitter<'_>) -> Result<()>;
#[zbus(signal)]
async fn disk_partitioned(emitter: &SignalEmitter<'_>, message: &str) -> Result<()>;
#[zbus(signal)]
async fn lvm_setup(emitter: &SignalEmitter<'_>, message: &str) -> Result<()>;
#[zbus(signal)]
async fn luks_setup(emitter: &SignalEmitter<'_>, message: &str) -> Result<()>;
#[zbus(signal)]
async fn boot_setup(emitter: &SignalEmitter<'_>, message: &str) -> Result<()>;
#[zbus(signal)]
async fn storage_created(emitter: &SignalEmitter<'_>, message: &str) -> Result<()>;
#[zbus(signal)]
async fn rootfs_installed(emitter: &SignalEmitter<'_>, message: &str) -> Result<()>;
#[zbus(signal)]
async fn install_completed(emitter: &SignalEmitter<'_>) -> Result<()>;
#[zbus(signal)]
async fn install_failed(emitter: &SignalEmitter<'_>, reason: &str) -> Result<()>;
}
async fn run_install_task(
emitter: SignalEmitter<'_>,
device_path: &str,
citadel_passphrase: &str,
luks_passphrase: &str,
timezone: Option<String>, // Accepts an owned String
) -> fdo::Result<()> {
println!(
"-> Installation process started for device '{}'...",
device_path
);
let msg1 = "Installation process has begun...";
DbusServer::run_install_started(&emitter, msg1).await?;
// We convert the `Option<String>` to an `Option<&str>` for `Installer::new`
let mut install = Installer::new(device_path, citadel_passphrase, luks_passphrase, timezone);
install.set_install_syslinux(true);
install
.verify()
.map_err(|e| fdo::Error::Failed(e.to_string()))?;
install
.partition_disk()
.map_err(|e| fdo::Error::Failed(e.to_string()))?;
install
.setup_luks()
.map_err(|e| fdo::Error::Failed(e.to_string()))?;
let msg2 = "Setup LUKS disk encryption password successfully\n";
DbusServer::luks_setup(&emitter, msg2).await?;
install
.setup_lvm()
.map_err(|e| fdo::Error::Failed(e.to_string()))?;
let msg3 = "Disk has been partitioned successfully.";
DbusServer::lvm_setup(&emitter, msg3).await?;
install
.setup_boot()
.map_err(|e| fdo::Error::Failed(e.to_string()))?;
let msg4 = "Setup /boot partition successfully\n";
DbusServer::boot_setup(&emitter, msg4).await?;
install
.create_storage()
.map_err(|e| fdo::Error::Failed(e.to_string()))?;
let msg5 = "Setup /storage partition successfully\n";
DbusServer::storage_created(&emitter, msg5).await?;
install
.install_rootfs_partitions()
.map_err(|e| fdo::Error::Failed(e.to_string()))?;
let msg6 = "Installed rootfs partitions successfully\n";
DbusServer::rootfs_installed(&emitter, msg6).await?;
install
.finish_install()
.map_err(|e| fdo::Error::Failed(e.to_string()))?;
DbusServer::install_completed(&emitter).await?;
Ok(())
}

View File

@@ -1,14 +1,18 @@
#[macro_use] extern crate libcitadel;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate lazy_static;
#[macro_use]
extern crate libcitadel;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate lazy_static;
use libcitadel::RealmManager;
use std::env;
use std::path::Path;
use std::ffi::OsStr;
use std::iter;
use libcitadel::RealmManager;
use std::path::Path;
mod boot;
mod fetch;
mod image;
mod install;
mod install_backend;
@@ -17,74 +21,71 @@ mod realmfs;
mod sync;
mod update;
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().unwrap();
} 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(args);
} 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)),
"image" => image::main(),
"realmfs" => realmfs::main(rebuild_args("citadel-realmfs", args)),
"update" => update::main(rebuild_args("citadel-update", args)),
"fetch" => fetch::main(),
"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(())
}

View File

@@ -1,13 +1,13 @@
use std::path::PathBuf;
use std::fs::OpenOptions;
use std::fs::{self,File};
use std::io::{self,Write};
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::PathBuf;
use libcitadel::{Result, ImageHeader, devkeys, util};
use libcitadel::{devkeys, keypair_for_channel_signing, util, ImageHeader, Result};
use super::config::BuildConfig;
use std::path::Path;
use libcitadel::verity::Verity;
use std::path::Path;
pub struct UpdateBuilder {
config: BuildConfig,
@@ -19,15 +19,12 @@ pub struct UpdateBuilder {
verity_root: Option<String>,
}
const BLOCK_SIZE: usize = 4096;
fn align(sz: usize, n: usize) -> usize {
(sz + (n - 1)) & !(n - 1)
}
impl UpdateBuilder {
pub fn new(config: BuildConfig) -> UpdateBuilder {
let image_data = config.workdir_path(UpdateBuilder::build_filename(&config));
UpdateBuilder {
@@ -38,15 +35,29 @@ 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 +165,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(())
}
@@ -192,6 +203,19 @@ impl UpdateBuilder {
if self.config.channel() == "dev" {
let sig = devkeys().sign(&metainfo);
hdr.set_signature(sig.to_bytes());
} else {
let private_key_path_str = match self.config.private_key_path() {
Some(path) => path,
None => bail!("private-key-path not found in config for non-dev channel"),
};
let private_key_path = Path::new(private_key_path_str);
let sig = keypair_for_channel_signing(private_key_path).sign(&metainfo);
info!("Generated signature: {}", hex::encode(sig.to_bytes()));
let generated_signature_bytes = sig.to_bytes();
if generated_signature_bytes.iter().all(|&b| b == 0) {
bail!("Generated signature is all zeros. Signing failed!");
}
hdr.set_signature(&generated_signature_bytes);
}
Ok(hdr)
}
@@ -217,12 +241,33 @@ 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())?;
writeln!(v, "verity-salt = \"{}\"", self.verity_salt.as_ref().unwrap())?;
writeln!(v, "verity-root = \"{}\"", self.verity_root.as_ref().unwrap())?;
writeln!(
v,
"verity-salt = \"{}\"",
self.verity_salt.as_ref().unwrap()
)?;
writeln!(
v,
"verity-root = \"{}\"",
self.verity_root.as_ref().unwrap()
)?;
if let Some(path) = self.config.public_key_path() {
if let Ok(key) = fs::read_to_string(path) {
writeln!(v, "public-key = \"{}\"", key.trim())?;
}
}
if let Some(path) = self.config.certificate_path() {
if let Ok(cert) = fs::read_to_string(path) {
writeln!(v, "authorizing-signature = \"{}\"", cert.trim())?;
}
}
Ok(v)
}
}

View File

@@ -1,7 +1,5 @@
use std::path::{Path, PathBuf};
use toml;
use libcitadel::{Result, util};
#[derive(Deserialize)]
@@ -9,7 +7,7 @@ pub struct BuildConfig {
#[serde(rename = "image-type")]
image_type: String,
channel: String,
version: usize,
version: String,
timestamp: String,
source: String,
#[serde(default)]
@@ -22,6 +20,15 @@ pub struct BuildConfig {
#[serde(rename = "realmfs-name")]
realmfs_name: Option<String>,
#[serde(rename = "private-key-path")]
private_key_path: Option<String>,
#[serde(rename = "public-key-path")]
public_key_path: Option<String>,
#[serde(rename = "certificate-path")]
certificate_path: Option<String>,
#[serde(skip)]
basedir: PathBuf,
#[serde(skip)]
@@ -46,6 +53,43 @@ impl BuildConfig {
Some(ref version) => format!("{}-{}", &config.image_type, version),
None => config.image_type.to_owned(),
};
// Auto-detect private key path if not specified
if config.private_key_path.is_none() && config.channel != "dev" {
let default_key_path = format!("/usr/share/citadel/keys/{}_image.key", config.channel);
info!("No private-key-path specified, using default: {}", default_key_path);
config.private_key_path = Some(default_key_path);
}
// Auto-detect public key and certificate paths if not specified
if config.public_key_path.is_none() && config.channel != "dev" {
// First check relative to config file (for build machine)
let local_path = config.basedir.join(format!("{}_image.pub", config.channel));
if local_path.exists() {
config.public_key_path = Some(local_path.to_string_lossy().into_owned());
} else {
// Fallback to runtime path
let path = format!("/usr/share/citadel/keys/{}_image.pub", config.channel);
if Path::new(&path).exists() {
config.public_key_path = Some(path);
}
}
}
if config.certificate_path.is_none() && config.channel != "dev" {
// First check relative to config file (for build machine)
let local_path = config.basedir.join(format!("{}_image.cert", config.channel));
if local_path.exists() {
config.certificate_path = Some(local_path.to_string_lossy().into_owned());
} else {
// Fallback to runtime path
let path = format!("/usr/share/citadel/keys/{}_image.cert", config.channel);
if Path::new(&path).exists() {
config.certificate_path = Some(path);
}
}
}
Ok(config)
}
@@ -102,8 +146,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 {
@@ -117,4 +161,16 @@ impl BuildConfig {
pub fn compress(&self) -> bool {
self.compress
}
pub fn private_key_path(&self) -> Option<&str> {
self.private_key_path.as_ref().map(|s| s.as_str())
}
pub fn public_key_path(&self) -> Option<&str> {
self.public_key_path.as_ref().map(|s| s.as_str())
}
pub fn certificate_path(&self) -> Option<&str> {
self.certificate_path.as_ref().map(|s| s.as_str())
}
}

View File

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

View File

@@ -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(args: Vec<String>) {
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,62 @@ 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)))
.get_matches_from(args);
.arg(Arg::with_name("image")
.help("Name of or path to RealmFS image to display information about")
.required(true));
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 +94,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 +137,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 +192,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() {

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
[Desktop Entry]
Name=RealmConfig
Type=Application
Icon=org.gnome.Settings
NoDisplay=true

View File

@@ -7,10 +7,12 @@
<busconfig>
<policy user="root">
<allow own="com.subgraph.realms"/>
<allow own="com.subgraph.Realms2"/>
</policy>
<policy context="default">
<allow send_destination="com.subgraph.realms"/>
<allow send_destination="com.subgraph.Realms2"/>
<allow send_destination="com.subgraph.realms"
send_interface="org.freedesktop.DBus.Properties"/>
<allow send_destination="com.subgraph.realms"

View File

@@ -0,0 +1,8 @@
[package]
name = "launch-gnome-software"
version = "0.1.0"
edition = "2021"
[dependencies]
libcitadel = { path = "../libcitadel" }
anyhow = "1.0"

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

View File

@@ -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,12 @@ walkdir = "2"
dbus = "0.6"
posix-acl = "1.0.0"
procfs = "0.12.0"
anyhow = "1.0"
clap = "4.5"
tempfile = "3.21"
semver = "1.0"
sha2 = "0.10"
ed25519-dalek = { version = "2.1", features = ["pkcs8"] }
[dependencies.inotify]
version = "0.8"

View File

@@ -78,3 +78,9 @@ impl Display for Error {
}
}
}
impl From<fmt::Error> for crate::Error {
fn from(e: fmt::Error) -> Self {
format_err!("Error formatting string: {}", e).into()
}
}

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

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

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

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

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

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

View File

@@ -382,8 +382,33 @@ impl ImageHeader {
self.set_signature(&zeros);
}
pub fn public_key(&self) -> Result<Option<PublicKey>> {
public_key_for_channel(self.metainfo().channel())
pub fn public_key(&self) -> Result<PublicKey> {
let metainfo = self.metainfo();
// 1. Try Hierarchical Verification if fields are present
if let (Some(pk_hex), Some(sig_hex)) = (metainfo.public_key(), metainfo.authorizing_signature()) {
let root_pubkey = match crate::root_image_public_key() {
Ok(rk) => rk,
Err(e) => {
warn!("Could not load Root Image Key for hierarchical verification: {}", e);
return public_key_for_channel(metainfo.channel());
}
};
let channel_pubkey = PublicKey::from_hex(pk_hex)?;
let sig_bytes = hex::decode(sig_hex).map_err(context!("Invalid authorizing-signature hex"))?;
let pk_bytes = hex::decode(pk_hex).map_err(context!("Invalid public-key hex"))?;
if root_pubkey.verify(&pk_bytes, &sig_bytes) {
info!("Channel key authorization SUCCESS...");
return Ok(channel_pubkey);
} else {
bail!("Security Critical: Channel key signature verification FAILED. Aborting.");
}
}
// 2. Fallback to standard channel-based lookup (key must be on disk)
public_key_for_channel(metainfo.channel())
}
pub fn verify_signature(&self, pubkey: PublicKey) -> bool {
@@ -453,7 +478,7 @@ pub struct MetaInfo {
realmfs_owner: Option<String>,
#[serde(default)]
version: u32,
version: String,
#[serde(default)]
timestamp: String,
@@ -469,6 +494,12 @@ pub struct MetaInfo {
#[serde(default, rename = "verity-root")]
verity_root: String,
#[serde(rename = "public-key")]
public_key: Option<String>,
#[serde(rename = "authorizing-signature")]
authorizing_signature: Option<String>,
}
impl MetaInfo {
@@ -508,8 +539,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 {
@@ -535,5 +566,13 @@ impl MetaInfo {
pub fn verity_tag(&self) -> &str {
&self.verity_root()[..8]
}
pub fn public_key(&self) -> Option<&str> {
self.public_key.as_deref()
}
pub fn authorizing_signature(&self) -> Option<&str> {
self.authorizing_signature.as_deref()
}
}

View File

@@ -35,9 +35,24 @@ impl PublicKey {
}
pub fn verify(&self, data: &[u8], signature: &[u8]) -> bool {
let sig = sign::Signature::try_from(signature)
.expect("Signature::from_slice() failed");
sign::verify_detached(&sig, data, &self.0)
let sig = match sign::Signature::try_from(signature) {
Ok(s) => s,
Err(_) => {
warn!("Invalid signature length: {}", signature.len());
return false;
}
};
let is_valid = sign::verify_detached(&sig, data, &self.0);
if !is_valid {
warn!("Header signature verification FAILED!");
warn!(" Public Key: {}", self.to_hex());
warn!(" Data (header): {}", hex::encode(data));
warn!(" Signature: {}", hex::encode(signature));
} else {
info!("Header signature verification SUCCESS.");
}
is_valid
}
}

View File

@@ -2,6 +2,9 @@
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate lazy_static;
use std::fs;
use std::path::Path;
#[macro_use] pub mod error;
#[macro_use] mod log;
#[macro_use] mod exec;
@@ -20,6 +23,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;
@@ -32,10 +38,12 @@ pub use crate::realmfs::{RealmFS,Mountpoint};
pub use crate::keyring::{KeyRing,KernelKey};
pub use crate::exec::{Exec,FileRange};
pub use crate::realmfs::resizer::ResizeSize;
pub use crate::realmfs::update::RealmFSUpdate;
pub use crate::realm::overlay::RealmOverlay;
pub use crate::realm::realm::Realm;
pub use crate::realm::pidmapper::PidLookupResult;
pub use crate::realm::config::{RealmConfig,OverlayType,GLOBAL_CONFIG};
pub use crate::realm::liveconfig::LiveConfig;
pub use crate::realm::events::RealmEvent;
pub use crate::realm::realms::Realms;
pub use crate::realm::manager::RealmManager;
@@ -50,28 +58,51 @@ pub fn devkeys() -> KeyPair {
.expect("Error parsing built in dev channel keys")
}
pub fn public_key_for_channel(channel: &str) -> Result<Option<PublicKey>> {
if channel == "dev" {
return Ok(Some(devkeys().public_key()));
pub fn keypair_for_channel_signing(private_key_path: &Path) -> KeyPair {
let hex_key = fs::read_to_string(private_key_path)
.expect(&format!("Error reading secret key from {}", private_key_path.display()));
KeyPair::from_hex(hex_key.trim())
.expect(&format!("Error parsing secret key from {}", private_key_path.display()))
}
pub fn public_key_for_channel(channel: &str) -> Result<PublicKey> {
// Validate input first
if !channel.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
bail!("Invalid channel name: {}", channel);
}
// Look in /etc/os-release
if Some(channel) == OsRelease::citadel_channel() {
if let Some(hex) = OsRelease::citadel_image_pubkey() {
let pubkey = PublicKey::from_hex(hex)?;
return Ok(Some(pubkey));
}
}
// Does kernel command line have citadel.channel=name:[hex encoded pubkey]
// Kernel command line override for developers
if Some(channel) == CommandLine::channel_name() {
if let Some(hex) = CommandLine::channel_pubkey() {
let pubkey = PublicKey::from_hex(hex)?;
return Ok(Some(pubkey))
return Ok(pubkey);
}
}
Ok(None)
let key_path = Path::new("/usr/share/citadel/keys/").join(format!("{}_image.pub", channel));
if !key_path.exists() {
if channel == "dev" {
return Ok(devkeys().public_key());
}
bail!("Public key not found for channel '{}' at {}", channel, key_path.display());
}
let hex_key = fs::read_to_string(&key_path)
.map_err(context!("could not read public key from {}", key_path.display()))?;
let pubkey = PublicKey::from_hex(hex_key.trim())?;
Ok(pubkey)
}
pub fn root_image_public_key() -> Result<PublicKey> {
let key_path = Path::new("/usr/share/citadel/keys/root_image.pub");
if !key_path.exists() {
bail!("Root image public key not found at {}", key_path.display());
}
let hex_key = fs::read_to_string(&key_path)
.map_err(context!("could not read root public key from {}", key_path.display()))?;
PublicKey::from_hex(hex_key.trim())
}
pub use error::{Result,Error};

View File

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

View File

@@ -15,8 +15,7 @@ pub struct Partition {
#[derive(Clone)]
struct HeaderInfo {
header: Arc<ImageHeader>,
// None if no public key available for channel named in metainfo
pubkey: Option<PublicKey>,
pubkey: PublicKey,
}
impl Partition {
@@ -43,13 +42,7 @@ impl Partition {
}
let metainfo = header.metainfo();
let pubkey = match public_key_for_channel(metainfo.channel()) {
Ok(result) => result,
Err(err) => {
warn!("Error parsing pubkey for channel '{}': {}", metainfo.channel(), err);
None
}
};
let pubkey = public_key_for_channel(metainfo.channel())?;
let header = Arc::new(header);
Ok(Some(HeaderInfo {
@@ -104,21 +97,15 @@ impl Partition {
pub fn is_signature_valid(&self) -> bool {
if let Some(ref hinfo) = self.hinfo {
if let Some(ref pubkey) = hinfo.pubkey {
return pubkey.verify(
&self.header().metainfo_bytes(),
&self.header().signature())
}
return hinfo.pubkey.verify(
&self.header().metainfo_bytes(),
&self.header().signature())
}
false
}
pub fn has_public_key(&self) -> bool {
if let Some(ref h) = self.hinfo {
h.pubkey.is_some()
} else {
false
}
self.hinfo.is_some()
}
pub fn write_status(&mut self, status: u8) -> Result<()> {

View File

@@ -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,
@@ -261,12 +266,44 @@ impl RealmConfig {
self.bool_value(|c| c.use_kvm)
}
pub fn set_kvm(&mut self, value: bool) -> bool {
if self.kvm() == value {
return false;
}
self.use_kvm = Some(value);
true
}
/// If `true` device /dev/fuse will be added to realm
///
pub fn fuse(&self) -> bool {
self.bool_value(|c| c.use_fuse)
}
pub fn set_fuse(&mut self, value: bool) -> bool {
if self.fuse() == value {
return false;
}
self.use_fuse = Some(value);
true
}
/// 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)
}
pub fn set_flatpak(&mut self, value: bool) -> bool {
if self.flatpak() == value {
return false;
}
self.use_flatpak = Some(value);
true
}
/// If `true` render node device /dev/dri/renderD128 will be added to realm.
///
/// This enables hardware graphics acceleration in realm.
@@ -274,12 +311,28 @@ impl RealmConfig {
self.bool_value(|c| c.use_gpu)
}
pub fn set_gpu(&mut self, value: bool) -> bool {
if self.gpu() == value {
return false;
}
self.use_gpu = Some(value);
true
}
/// If `true` and `self.gpu()` is also true, privileged device /dev/dri/card0 will be
/// added to realm.
pub fn gpu_card0(&self) -> bool {
self.bool_value(|c| c.use_gpu_card0)
}
pub fn set_gpu_card0(&mut self, value: bool) -> bool {
if self.gpu_card0() == value {
return false;
}
self.use_gpu_card0 = Some(value);
true
}
/// If `true` the /Shared directory will be mounted in home directory of realm.
///
/// This directory is shared between all running realms and is an easy way to move files
@@ -288,12 +341,28 @@ impl RealmConfig {
self.bool_value(|c| c.use_shared_dir)
}
pub fn set_shared_dir(&mut self, value: bool) -> bool {
if self.shared_dir() == value {
return false;
}
self.use_shared_dir = Some(value);
true
}
/// If `true` the mount directory for external storage devices will be bind mounted as /Media
///
pub fn media_dir(&self) -> bool {
self.bool_value(|c| c.use_media_dir)
}
pub fn set_media_dir(&mut self, value: bool) -> bool {
if self.media_dir() == value {
return false;
}
self.use_media_dir = Some(value);
true
}
/// If `true` the home directory of this realm will be set up in ephemeral mode.
///
/// The ephemeral home directory is set up with the following steps:
@@ -308,6 +377,14 @@ impl RealmConfig {
self.bool_value(|c| c.use_ephemeral_home)
}
pub fn set_ephemeral_home(&mut self, value: bool) -> bool {
if self.ephemeral_home() == value {
return false;
}
self.use_ephemeral_home = Some(value);
true
}
/// A list of subdirectories of /realms/realm-${name}/home to bind mount into realm
/// home directory when ephemeral-home is enabled.
pub fn ephemeral_persistent_dirs(&self) -> Vec<String> {
@@ -330,6 +407,14 @@ impl RealmConfig {
self.bool_value(|c| c.use_sound)
}
pub fn set_sound(&mut self, value: bool) -> bool {
if self.sound() == value {
return false;
}
self.use_sound = Some(value);
true
}
/// If `true` access to the X11 server will be added to realm by bind mounting
/// directory /tmp/.X11-unix
pub fn x11(&self) -> bool {
@@ -338,12 +423,28 @@ impl RealmConfig {
})
}
pub fn set_x11(&mut self, value: bool) -> bool {
if self.x11() == value {
return false;
}
self.use_x11 = Some(value);
true
}
/// If `true` access to Wayland display will be permitted in realm by adding
/// wayland socket /run/user/1000/wayland-0
pub fn wayland(&self) -> bool {
self.bool_value(|c| c.use_wayland)
}
pub fn set_wayland(&mut self, value: bool) -> bool {
if self.wayland() == value {
return false;
}
self.use_wayland = Some(value);
true
}
/// The name of the wayland socket to use if `self.wayland()` is `true`
/// defaults to wayland-0, will appear in the realm as wayland-0 regardless of value
pub fn wayland_socket(&self) -> &str {
@@ -356,12 +457,19 @@ impl RealmConfig {
self.bool_value(|c| c.use_network)
}
pub fn set_network(&mut self, value: bool) -> bool {
if self.network() == value {
return false;
}
self.use_network = Some(value);
true
}
/// The name of the network zone this realm will use if `self.network()` is `true`.
pub fn network_zone(&self) -> &str {
self.str_value(|c| c.network_zone.as_ref()).unwrap_or(DEFAULT_ZONE)
}
/// If configured, this realm uses a fixed IP address on the zone subnet. The last
/// octet of the network address for this realm will be set to the provided value.
pub fn reserved_ip(&self) -> Option<u8> {
@@ -422,7 +530,6 @@ impl RealmConfig {
self.overlay = overlay.to_str_value().map(String::from)
}
pub fn netns(&self) -> Option<&str> {
self.str_value(|c| c.netns.as_ref())
}

View File

@@ -12,7 +12,9 @@ use dbus::{Connection, BusType, ConnectionItem, Message, Path};
use inotify::{Inotify, WatchMask, WatchDescriptor, Event};
pub enum RealmEvent {
Starting(Realm),
Started(Realm),
Stopping(Realm),
Stopped(Realm),
New(Realm),
Removed(Realm),
@@ -22,7 +24,9 @@ pub enum RealmEvent {
impl Display for RealmEvent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
RealmEvent::Starting(ref realm) => write!(f, "RealmStarting({})", realm.name()),
RealmEvent::Started(ref realm) => write!(f, "RealmStarted({})", realm.name()),
RealmEvent::Stopping(ref realm) => write!(f, "RealmStopping({})", realm.name()),
RealmEvent::Stopped(ref realm) => write!(f, "RealmStopped({})", realm.name()),
RealmEvent::New(ref realm) => write!(f, "RealmNew({})", realm.name()),
RealmEvent::Removed(ref realm) => write!(f, "RealmRemoved({})", realm.name()),

View File

@@ -1,4 +1,4 @@
use std::fmt::{self,Write};
use std::fmt::Write;
use std::path::{Path, PathBuf};
use crate::{Realm, Result, util, realm::network::NetworkConfig};
@@ -18,6 +18,23 @@ $EXTRA_FILE_OPTIONS
";
// SYSTEMD_NSPAWN_SHARE_NS_IPC is a secret flag that allows sharing IPC namespace between
// nspawn container and host. This is needed so that the X11 MIT-SHM extension will work
// correctly. Sharing the IPC namespace is not ideal, but also not obviously harmful.
//
// If this patch was applied to Mutter, then the MIT-SHM extension could be disabled for
// XWayland which would break some applications and degrade performance of others.
//
// https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/2136
//
// Setting QT_X11_NO_MITSHM=1 should at least prevent QT applications from crashing if
// extension is not available.
//
// Another approach would be to use LD_PRELOAD to disable visibility of the extension for
// applications inside of container:
//
// https://github.com/jessfraz/dockerfiles/issues/359#issuecomment-828714848
//
const REALM_SERVICE_TEMPLATE: &str = "\
[Unit]
Description=Application Image $REALM_NAME instance
@@ -60,7 +77,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 +170,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)?;
@@ -231,9 +252,4 @@ impl <'a> RealmLauncher <'a> {
}
}
impl From<fmt::Error> for crate::Error {
fn from(e: fmt::Error) -> Self {
format_err!("Error formatting string: {}", e).into()
}
}

View File

@@ -0,0 +1,200 @@
use std::process::Command;
use crate::{Realm,Result};
pub struct LiveConfig<'a> {
realm: &'a Realm,
}
const LIVE_VARS: &[&str] = &[
"use-gpu", "use-gpu-card0", "use-wayland", "use-x11",
"use-sound", "use-shared-dir", "use-kvm", "use-media-dir",
"use-fuse", "use-flatpak"
];
const SYSTEMCTL_PATH: &str = "/usr/bin/systemctl";
const SYSTEMD_RUN_PATH: &str = "/usr/bin/systemd-run";
const MACHINECTL_PATH: &str = "/usr/bin/machinectl";
impl <'a> LiveConfig<'a> {
pub fn is_live_configurable(varname: &str) -> bool {
LIVE_VARS.contains(&varname)
}
pub fn new(realm: &'a Realm) -> Self {
LiveConfig {
realm
}
}
pub fn configure(&self, varname: &str, enabled: bool) -> Result<()> {
match varname {
"use-gpu" => self.configure_gpu(enabled),
"use-gpu-card0" => self.configure_gpu_card0(enabled),
"use-wayland" => self.configure_wayland(enabled),
"use-x11" => self.configure_x11(enabled),
"use-sound" => self.configure_sound(enabled),
"use-shared-dir" => self.configure_shared(enabled),
"use-kvm" => self.configure_kvm(enabled),
"use-media-dir" => self.configure_media(enabled),
"use-fuse" => self.configure_fuse(enabled),
"use-flatpak" => self.configure_flatpak(enabled),
_ => bail!("Unknown live configuration variable '{}'", varname),
}
}
fn configure_gpu(&self, enabled: bool) -> Result<()> {
self.enable_device("/dev/dri/renderD128", enabled)
}
fn configure_gpu_card0(&self, enabled: bool) -> Result<()> {
self.enable_device("/dev/dri/card0", enabled)
}
fn configure_kvm(&self, enabled: bool) -> Result<()> {
self.enable_device("/dev/kvm", enabled)
}
fn configure_fuse(&self, enabled: bool) -> Result<()> {
self.enable_device("/dev/fuse", enabled)
}
fn configure_shared(&self, enabled: bool) -> Result<()> {
self.enable_bind_mount(
"/realms/Shared",
Some("/home/user/Shared"),
false,
enabled)
}
fn configure_media(&self, enabled: bool) -> Result<()> {
self.enable_bind_mount(
"/run/media/citadel",
Some("/home/user/Media"),
false,
enabled)
}
fn configure_sound(&self, enabled: bool) -> Result<()> {
self.enable_bind_mount(
"/run/user/1000/pulse",
Some("/run/user/host/pulse"),
true,
enabled)
}
fn configure_wayland(&self, enabled: bool) -> Result<()> {
self.enable_bind_mount(
"/run/user/1000/wayland-0",
Some("/run/user/host/wayland-0"),
true,
enabled)
}
fn configure_x11(&self, enabled: bool) -> Result<()> {
self.enable_bind_mount(
"/tmp/.X11-unix",
None,
true,
enabled)
}
fn configure_flatpak(&self, enabled: bool) -> Result<()> {
let path = self.realm.base_path_file("flatpak")
.display()
.to_string();
self.enable_bind_mount(
&path,
Some("/var/lib/flatpak"),
true,
enabled)
}
fn enable_bind_mount(&self, path: &str, target: Option<&str>, readonly: bool, enabled: bool) -> Result<()> {
if enabled {
self.machinectl_bind(path, target, readonly)
} else if let Some(target) = target {
self.unmount(target, false)
} else {
self.unmount(path, false)
}
}
fn enable_device(&self, device_path: &str, enabled: bool) -> Result<()> {
if enabled {
self.machinectl_bind(device_path, None, false)?;
self.systemctl_device_allow(device_path)?;
} else {
self.unmount(device_path, true)?;
}
Ok(())
}
fn unmount(&self, path: &str, delete: bool) -> Result<()> {
// systemd-run --machine={name} umount {path}
self.systemd_run(&["umount", path])?;
if delete {
self.systemd_run(&["rm", path])?;
}
Ok(())
}
fn systemctl_device_allow(&self, device_path: &str) -> Result<()> {
let status = Command::new(SYSTEMCTL_PATH)
.arg("set-property")
.arg(format!("realm-{}.service", self.realm.name()))
.arg(format!("DeviceAllow={}", device_path))
.status()
.map_err(context!("failed to execute {}", SYSTEMCTL_PATH))?;
if !status.success() {
bail!("'systemctl set-property realm-{}.service DeviceAllow={}' command did not complete successfully status: {:?}", self.realm.name(), device_path, status.code());
}
Ok(())
}
fn machinectl_bind(&self, path: &str, target: Option<&str>, readonly: bool) -> Result<()> {
let mut cmd = Command::new(MACHINECTL_PATH);
cmd.arg("--mkdir");
if readonly {
cmd.arg("--read-only");
}
cmd.arg("bind")
.arg(self.realm.name())
.arg(path);
if let Some(target) = target {
cmd.arg(target);
}
let status = cmd.status()
.map_err(context!("failed to execute {}", MACHINECTL_PATH))?;
if !status.success() {
bail!("machinectl bind {} {} {:?} did not complete successfully status: {:?}", self.realm.name(), path, target, status.code());
}
Ok(())
}
fn systemd_run(&self, args: &[&str]) -> Result<()> {
let status = Command::new(SYSTEMD_RUN_PATH)
.arg("--quiet")
.arg(format!("--machine={}", self.realm.name()))
.args(args)
.status()
.map_err(context!("failed to execute {}", MACHINECTL_PATH))?;
if !status.success() {
bail!("systemd-run --machine={} command did not complete successfully args: {:?} status: {:?}", self.realm.name(), args, status.code());
}
Ok(())
}
}

View File

@@ -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 })
}
}
@@ -190,7 +194,13 @@ impl RealmManager {
return Ok(());
}
info!("Starting realm {}", realm.name());
self._start_realm(realm, &mut HashSet::new())?;
self.inner().events.send_event(RealmEvent::Starting(realm.clone()));
if let Err(err) = self._start_realm(realm, &mut HashSet::new()) {
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
return Err(err);
}
self.inner().events.send_event(RealmEvent::Started(realm.clone()));
if !Realms::is_some_realm_current() {
self.inner_mut().realms.set_realm_current(realm)
@@ -230,6 +240,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 +282,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());
@@ -275,10 +298,21 @@ impl RealmManager {
}
info!("Stopping realm {}", realm.name());
self.inner().events.send_event(RealmEvent::Stopping(realm.clone()));
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)?;
if let Err(err) = self.systemd.stop_realm(realm) {
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
return Err(err);
}
realm.cleanup_rootfs();
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
if realm.is_current() {
self.choose_some_current_realm();
@@ -335,8 +369,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>)> {

View File

@@ -1,6 +1,7 @@
pub(crate) mod overlay;
pub(crate) mod config;
pub(crate) mod liveconfig;
pub(crate) mod realms;
pub(crate) mod manager;
#[allow(clippy::module_inception)]

View File

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

View File

@@ -169,6 +169,20 @@ impl Realm {
self.inner.write().unwrap()
}
pub fn start(&self) -> Result<()> {
warn!("Realm({})::start()", self.name());
self.manager().start_realm(self)
}
pub fn stop(&self) -> Result<()> {
self.manager().stop_realm(self)
}
pub fn set_current(&self) -> Result<()> {
self.manager().set_current_realm(self)
}
pub fn is_active(&self) -> bool {
self.inner_mut().is_active()
}
@@ -279,6 +293,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 +317,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);

View File

@@ -244,7 +244,7 @@ impl Realms {
pub fn delete_realm(&mut self, name: &str, save_home: bool) -> Result<()> {
let _lock = Self::realmslock()?;
let realm = match self.realms.take(name) {
let realm = match self.by_name(name) {
Some(realm) => realm,
None => bail!("Cannot remove realm '{}' because it doesn't seem to exist", name),
};

View File

@@ -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(())
}
@@ -91,7 +113,7 @@ impl Systemd {
.arg(name)
.status()
.map(|status| status.success())
.map_err(context!("failed to execute {}", MACHINECTL_PATH))?;
.map_err(context!("failed to execute {}", SYSTEMCTL_PATH))?;
Ok(ok)
}

View File

@@ -0,0 +1,174 @@
use std::cell::Cell;
use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::realm::BridgeAllocator;
use crate::{util, Result};
const NSPAWN_FILE_TEMPLATE: &str = "\
[Exec]
Boot=true
$NETWORK_CONFIG
[Files]
BindReadOnly=/storage/citadel-state/resolv.conf:/etc/resolv.conf
$BIND_MOUNTS
";
const SERVICE_TEMPLATE: &str = "\
[Unit]
Description=Update RealmFS $MACHINE_NAME instance
[Service]
DevicePolicy=closed
ExecStart=/usr/bin/systemd-nspawn --quiet --console=passive --notify-ready=yes --keep-unit --machine=$MACHINE_NAME --link-journal=auto --directory=$ROOTFS
KillMode=mixed
Type=notify
RestartForceExitStatus=133
SuccessExitStatus=133
";
const SYSTEMCTL_PATH: &str = "/usr/bin/systemctl";
const SYSTEMD_NSPAWN_PATH: &str = "/run/systemd/nspawn";
const SYSTEMD_UNIT_PATH: &str = "/run/systemd/system";
/// Launcher for RealmFS update containers
pub struct RealmFSUpdateLauncher {
machine_name: String,
shared_directory: bool,
running: Cell<bool>,
rootfs: PathBuf,
service_path: PathBuf,
nspawn_path: PathBuf,
}
impl RealmFSUpdateLauncher {
fn new(machine_name: &str, rootfs: &Path, shared_directory: bool) -> Self {
let machine_name = machine_name.to_string();
let running = Cell::new(false);
let rootfs = rootfs.to_owned();
let service_path = PathBuf::from(SYSTEMD_UNIT_PATH).join(format!("realmfs-{machine_name}.service"));
let nspawn_path= PathBuf::from(SYSTEMD_NSPAWN_PATH).join(format!("{machine_name}.nspawn"));
RealmFSUpdateLauncher {
machine_name, shared_directory, running, rootfs, service_path, nspawn_path,
}
}
pub fn launch_update_container(machine_name: &str, rootfs: &Path, shared_directory: bool) -> Result<Self> {
let launcher = Self::new(machine_name, rootfs, shared_directory);
launcher.start_container()?;
Ok(launcher)
}
fn systemctl_start(&self) -> Result<bool> {
self.run_systemctl("start")
}
fn systemctl_stop(&self) -> Result<bool> {
self.run_systemctl("stop")
}
fn run_systemctl(&self, op: &str) -> Result<bool> {
let service_name = format!("realmfs-{}", self.machine_name);
let ok = Command::new(SYSTEMCTL_PATH)
.arg(op)
.arg(service_name)
.status()
.map(|status| status.success())
.map_err(context!("failed to execute {}", SYSTEMCTL_PATH))?;
Ok(ok)
}
fn start_container(&self) -> Result<()> {
self.write_launch_config_files()?;
let ok = self.systemctl_start()?;
self.running.set(ok);
Ok(())
}
pub fn stop_container(&self) -> Result<()> {
if self.running.replace(false) {
self.systemctl_stop()?;
self.remove_launch_config_files()?;
// XXX remove IP address allocation?
}
Ok(())
}
fn remove_launch_config_files(&self) -> Result<()> {
util::remove_file(&self.nspawn_path)?;
util::remove_file(&self.service_path)?;
Ok(())
}
fn generate_nspawn_file(&self) -> Result<String> {
Ok(NSPAWN_FILE_TEMPLATE
.replace("$BIND_MOUNTS", &self.generate_bind_mounts()?)
.replace("$NETWORK_CONFIG", &self.generate_network_config()?))
}
fn generate_service_file(&self) -> String {
let rootfs = self.rootfs.display().to_string();
SERVICE_TEMPLATE
.replace("$MACHINE_NAME", &self.machine_name)
.replace("$ROOTFS", &rootfs)
}
/// Write the string `content` to file `path`. If the directory does
/// not already exist, create it.
fn write_launch_config_file(&self, path: &Path, content: &str) -> Result<()> {
match path.parent() {
Some(parent) => util::create_dir(parent)?,
None => bail!("config file path {} has no parent?", path.display()),
};
util::write_file(path, content)
}
fn generate_bind_mounts(&self) -> Result<String> {
let mut s = String::new();
if self.shared_directory && Path::new("/realms/Shared").exists() {
writeln!(s, "Bind=/realms/Shared:/run/Shared")?;
}
Ok(s)
}
fn generate_network_config(&self) -> Result<String> {
let mut s = String::new();
let mut alloc = BridgeAllocator::default_bridge()?;
let addr = alloc.allocate_address_for(&self.machine_name)?;
let gw = alloc.gateway();
writeln!(s, "Environment=IFCONFIG_IP={}", addr)?;
writeln!(s, "Environment=IFCONFIG_GW={}", gw)?;
writeln!(s, "[Network]")?;
writeln!(s, "Zone=clear")?;
Ok(s)
}
fn write_launch_config_files(&self) -> Result<()> {
let nspawn_content = self.generate_nspawn_file()?;
self.write_launch_config_file(&self.nspawn_path, &nspawn_content)?;
let service_content = self.generate_service_file();
self.write_launch_config_file(&self.service_path, &service_content)?;
Ok(())
}
}
impl Drop for RealmFSUpdateLauncher {
fn drop(&mut self) {
if let Err(err) = self.stop_container() {
warn!("Error stopping RealmFS update container: {}", err);
}
}
}

View File

@@ -1,9 +1,10 @@
pub(crate) mod resizer;
mod mountpoint;
mod update;
pub(crate) mod update;
pub(crate) mod realmfs_set;
#[allow(clippy::module_inception)]
mod realmfs;
mod launcher;
pub use self::realmfs::RealmFS;
pub use self::mountpoint::Mountpoint;

View File

@@ -1,13 +1,12 @@
use std::ffi::OsStr;
use std::fs;
use std::io::Write;
use std::os::unix::fs::MetadataExt;
use std::path::{Path,PathBuf};
use std::sync::{Arc, Weak, RwLock};
use crate::{ImageHeader, MetaInfo, Result, KeyRing, KeyPair, util, RealmManager, PublicKey, ResizeSize};
use crate::realmfs::resizer::Superblock;
use crate::realmfs::update::Update;
use crate::realmfs::update::RealmFSUpdate;
use super::mountpoint::Mountpoint;
// Maximum length of a RealmFS name
@@ -57,6 +56,8 @@ impl RealmFS {
// Name used to retrieve key by 'description' from kernel key storage
pub const USER_KEYNAME: &'static str = "realmfs-user";
const BLOCK_SIZE: u64 = 4096;
/// Locate a RealmFS image by name in the default location using the standard name convention
pub fn load_by_name(name: &str) -> Result<Self> {
Self::validate_name(name)?;
@@ -266,8 +267,13 @@ impl RealmFS {
}
}
pub fn update(&self) -> Result<RealmFSUpdate> {
let update = RealmFSUpdate::create(self)?;
Ok(update)
}
pub fn interactive_update(&self, scheme: Option<&str>) -> Result<()> {
let mut update = Update::create(self)?;
let mut update = RealmFSUpdate::create(self)?;
update.run_interactive_update(scheme)
}
@@ -276,10 +282,7 @@ impl RealmFS {
let pubkey = if self.metainfo().channel() == RealmFS::USER_KEYNAME {
self.sealing_keys()?.public_key()
} else {
match self.header().public_key()? {
Some(pubkey) => pubkey,
None => bail!("No public key available for channel {}", self.metainfo().channel()),
}
self.header().public_key()?
};
Ok(pubkey)
}
@@ -308,7 +311,7 @@ impl RealmFS {
info!("forking RealmFS image '{}' to new name '{}'", self.name(), new_name);
let forked = match self.fork_to_path(new_name, &new_path, keys) {
let mut forked = match self.fork_to_path(new_name, &new_path, keys) {
Ok(forked) => forked,
Err(err) => {
if new_path.exists() {
@@ -318,7 +321,10 @@ impl RealmFS {
}
};
self.with_manager(|m| m.realmfs_added(&forked));
self.with_manager(|m| {
m.realmfs_added(&forked);
forked.set_manager(m);
});
Ok(forked)
}
@@ -363,11 +369,11 @@ impl RealmFS {
pub fn file_nblocks(&self) -> Result<usize> {
let meta = self.path.metadata()
.map_err(context!("failed to read metadata from realmfs image file {:?}", self.path))?;
let len = meta.len() as usize;
if len % 4096 != 0 {
let len = meta.len();
if len % Self::BLOCK_SIZE != 0 {
bail!("realmfs image file '{}' has size which is not a multiple of block size", self.path.display());
}
let nblocks = len / 4096;
let nblocks = (len / Self::BLOCK_SIZE) as usize;
if nblocks < (self.metainfo().nblocks() + 1) {
bail!("realmfs image file '{}' has shorter length than nblocks field of image header", self.path.display());
}
@@ -388,31 +394,33 @@ impl RealmFS {
pub fn resize_grow_to(&self, size: ResizeSize) -> Result<()> {
info!("Resizing to {} blocks", size.nblocks());
let mut update = Update::create(self)?;
let mut update = RealmFSUpdate::create(self)?;
update.grow_to(size);
update.resize()
}
pub fn resize_grow_by(&self, size: ResizeSize) -> Result<()> {
info!("Resizing to an increase of {} blocks", size.nblocks());
let mut update = Update::create(self)?;
let mut update = RealmFSUpdate::create(self)?;
update.grow_by(size);
update.resize()
}
pub fn free_size_blocks(&self) -> Result<usize> {
let sb = Superblock::load(self.path(), 4096)?;
let sb = Superblock::load(self.path(), Self::BLOCK_SIZE)?;
Ok(sb.free_block_count() as usize)
}
pub fn allocated_size_blocks(&self) -> Result<usize> {
let meta = self.path().metadata()
.map_err(context!("failed to read metadata from realmfs image file {:?}", self.path()))?;
Ok(meta.blocks() as usize / 8)
pub fn allocated_size_blocks(&self) -> usize {
self.metainfo().nblocks()
}
/// Activate this RealmFS image if not yet activated.
pub fn activate(&self) -> Result<()> {
// Ensure that mountpoint matches header information of image
if let Err(err) = self.check_stale_header(false) {
warn!("error reloading stale image header: {}", err);
}
self.mountpoint().activate(self)
}

View File

@@ -27,17 +27,6 @@ impl RealmFSSet {
}
Ok(())
})?;
/*
let entries = fs::read_dir(RealmFS::BASE_PATH)
.map_err(context!("error reading realmfs directory {}", RealmFS::BASE_PATH))?;
for entry in entries {
let entry = entry.map_err(context!("error reading directory entry"))?;
if let Some(realmfs) = Self::entry_to_realmfs(&entry) {
v.push(realmfs)
}
}
*/
Ok(v)
}
@@ -47,6 +36,8 @@ impl RealmFSSet {
let name = filename.trim_end_matches("-realmfs.img");
if RealmFS::is_valid_name(name) && RealmFS::named_image_exists(name) {
return RealmFS::load_by_name(name).ok();
} else {
warn!("Rejecting realmfs '{}' as invalid name or invalid image", name);
}
}
}

View File

@@ -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 {

View File

@@ -11,6 +11,8 @@ use crate::util::is_euid_root;
use crate::terminal::TerminalRestorer;
use crate::verity::Verity;
use super::launcher::RealmFSUpdateLauncher;
const BLOCK_SIZE: usize = 4096;
// The maximum number of backup copies the rotate() method will create
@@ -21,36 +23,41 @@ const RESIZE2FS: &str = "resize2fs";
/// Manages the process of updating or resizing a `RealmFS` image file.
///
pub struct Update<'a> {
realmfs: &'a RealmFS, // RealmFS being updated
pub struct RealmFSUpdate {
realmfs: RealmFS, // RealmFS being updated
name: String, // name for nspawn instance
target: PathBuf, // Path to the update copy of realmfs image
mountpath: PathBuf, // Path at which update copy is mounted
container: Option<RealmFSUpdateLauncher>,
_lock: FileLock,
resize: Option<ResizeSize>, // If the image needs to be resized, the resize size is stored here
network_allocated: bool,
}
impl <'a> Update<'a> {
fn new(realmfs: &'a RealmFS, lock: FileLock) -> Self {
impl RealmFSUpdate {
fn new(realmfs: RealmFS, lock: FileLock) -> Self {
let metainfo = realmfs.metainfo();
let tag = metainfo.verity_tag();
let mountpath = Path::new(RealmFS::RUN_DIRECTORY)
.join(format!("realmfs-{}-{}.update", realmfs.name(), tag));
Update {
let name = format!("{}-{}-update", realmfs.name(), tag);
let resize = ResizeSize::auto_resize_size(&realmfs);
let target = realmfs.path().with_extension("update");
RealmFSUpdate {
realmfs,
name: format!("{}-{}-update", realmfs.name(), tag),
target: realmfs.path().with_extension("update"),
name,
target,
mountpath,
_lock: lock,
resize: ResizeSize::auto_resize_size(realmfs),
container: None,
resize,
network_allocated: false,
}
}
pub fn create(realmfs: &'a RealmFS) -> Result<Self> {
pub fn create(realmfs: &RealmFS) -> Result<Self> {
let lock = FileLock::nonblocking_acquire(realmfs.path().with_extension("lock"))?
.ok_or(format_err!("Unable to obtain file lock to update realmfs image: {}", realmfs.name()))?;
@@ -58,10 +65,10 @@ impl <'a> Update<'a> {
bail!("Cannot seal realmfs image, no sealing keys available");
}
Ok(Update::new(realmfs, lock))
Ok(RealmFSUpdate::new(realmfs.clone(), lock))
}
fn name(&self) -> &str {
pub fn name(&self) -> &str {
&self.name
}
@@ -74,10 +81,11 @@ impl <'a> Update<'a> {
info!("Update file {} already exists, removing it", self.target.display());
util::remove_file(&self.target)?;
}
info!("Creating update copy of realmfs {} -> {}",
self.realmfs.path().display(),
self.target().display());
self.realmfs.copy_image_file(self.target())?;
self.truncate_verity()?;
self.resize_image_file()?;
Ok(())
}
@@ -100,10 +108,17 @@ impl <'a> Update<'a> {
LoopDevice::with_loop(self.target(), Some(BLOCK_SIZE), false, |loopdev| {
self.resize_device(loopdev)
})
})?;
self.apply_update()?;
self.cleanup();
Ok(())
}
fn mount_update_image(&mut self) -> Result<()> {
info!("Loop device mounting {} at {}",
self.target().display(),
self.mountpath.display());
LoopDevice::with_loop(self.target(), Some(BLOCK_SIZE), false, |loopdev| {
if self.resize.is_some() {
self.resize_device(loopdev)?;
@@ -115,9 +130,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 +173,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)
@@ -171,12 +186,13 @@ impl <'a> Update<'a> {
// Remove dm-verity hash tree from update copy of image file.
fn truncate_verity(&self) -> Result<()> {
info!("Truncating dm-verity hash tree from {}", self.target().display());
let file_nblocks = self.realmfs.file_nblocks()?;
let metainfo_nblocks = self.metainfo_nblock_size();
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,10 +201,12 @@ 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(()),
};
info!("Resizing target file to {} blocks", nblocks);
if nblocks < self.metainfo_nblock_size() {
bail!("Cannot shrink image")
}
@@ -199,10 +217,29 @@ impl <'a> Update<'a> {
self.set_target_len(nblocks)
}
pub fn cleanup(&mut self) {
fn remount_read_only(&mut self) {
if self.mountpath.exists() {
self.unmount_update_image();
if let Err(err) = cmd!("/usr/bin/mount", "-o remount,ro {}", self.mountpath.display()) {
warn!("Failed to remount read-only directory {}: {}", self.mountpath.display(), err);
} else {
info!("Directory {} remounted as read-only", self.mountpath.display());
}
}
}
fn shutdown_container(&mut self) {
if let Some(update) = self.container.take() {
if let Err(err) = update.stop_container() {
warn!("Error shutting down update container: {}", err);
}
}
}
pub fn cleanup(&mut self) {
// if a container was started, stop it
self.shutdown_container();
self.unmount_update_image();
if self.target().exists() {
if let Err(err) = fs::remove_file(self.target()) {
@@ -224,7 +261,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 +269,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");
@@ -276,6 +304,25 @@ impl <'a> Update<'a> {
Ok(yes)
}
pub fn prepare_update(&mut self, shared_directory: bool) -> Result<()> {
if !is_euid_root() {
bail!("RealmFS updates must be prepared as root");
}
self.setup()?;
self.launch_update_container(shared_directory)?;
Ok(())
}
pub fn commit_update(&mut self) -> Result<()> {
// First shutdown container so writable mount can be removed in apply_update()
self.shutdown_container();
// Ensure no further writes
self.remount_read_only();
let result = self.apply_update();
self.cleanup();
result
}
pub fn run_interactive_update(&mut self, scheme: Option<&str>) -> Result<()> {
if !is_euid_root() {
bail!("RealmFS updates must be run as root");
@@ -307,6 +354,21 @@ impl <'a> Update<'a> {
Ok(())
}
fn launch_update_container(&mut self, shared_directory: bool) -> Result<()> {
if self.container.is_some() {
bail!("Update container is already running");
}
info!("Launching update container '{}'", self.name());
let update = RealmFSUpdateLauncher::launch_update_container(self.name(), &self.mountpath, shared_directory)?;
self.container = Some(update);
Ok(())
}
pub fn run_update_shell(&mut self, command: &str) -> Result<()> {
let mut alloc = BridgeAllocator::default_bridge()?;
@@ -356,7 +418,7 @@ impl <'a> Update<'a> {
}
}
impl <'a> Drop for Update<'a> {
impl Drop for RealmFSUpdate {
fn drop(&mut self) {
self.cleanup();
}

View File

@@ -42,7 +42,21 @@ impl ResourceImage {
info!("Searching run directory for image {} with channel {}", image_type, channel);
if let Some(image) = search_directory(RUN_DIRECTORY, image_type, Some(&channel))? {
let run_channel = if CommandLine::live_mode() || CommandLine::install_mode() {
info!("Live/Install mode: searching {} without channel filter", RUN_DIRECTORY);
None
} else {
info!("Normal mode: searching {} with channel filter: {}", RUN_DIRECTORY, channel);
Some(channel)
};
// In live/install mode, skip strict kernel version/id matching since we're
// using whatever kernel image was provided on the boot media
let skip_kernel_match = CommandLine::live_mode() || CommandLine::install_mode();
info!("Searching in {}", RUN_DIRECTORY);
if let Some(image) = search_directory(RUN_DIRECTORY, image_type, run_channel, skip_kernel_match)? {
info!("Found image in {}: {}", RUN_DIRECTORY, image.path().display());
return Ok(image);
}
@@ -52,8 +66,9 @@ impl ResourceImage {
let storage_path = Path::new(STORAGE_BASEDIR).join(&channel);
if let Some(image) = search_directory(storage_path, image_type, Some(&channel))? {
return Ok(image);
if let Some(image) = search_directory(&storage_path, image_type, Some(&channel), false)? {
info!("Found image in storage: {}", image.path().display());
return Ok(image);
}
bail!("failed to find resource image of type: {}", image_type)
@@ -66,7 +81,7 @@ impl ResourceImage {
/// Locate a rootfs image in /run/citadel/images and return it
pub fn find_rootfs() -> Result<Self> {
match search_directory(RUN_DIRECTORY, "rootfs", None)? {
match search_directory(RUN_DIRECTORY, "rootfs", None, false)? {
Some(image) => Ok(image),
None => bail!("failed to find rootfs resource image"),
}
@@ -199,15 +214,11 @@ impl ResourceImage {
pub fn setup_verity_device(&self) -> Result<String> {
if !CommandLine::nosignatures() {
match self.header.public_key()? {
Some(pubkey) => {
if !self.header.verify_signature(pubkey) {
bail!("header signature verification failed");
}
info!("Image header signature is valid");
}
None => bail!("cannot verify header signature because no public key for channel {} is available", self.metainfo().channel())
let pubkey = self.header.public_key()?;
if !self.header.verify_signature(pubkey) {
bail!("header signature verification failed");
}
info!("Image header signature is valid");
}
info!("Setting up dm-verity device for image");
if !self.has_verity_hashtree() {
@@ -350,6 +361,13 @@ impl ResourceImage {
// If the /storage directory is not mounted, attempt to mount it.
// Return true if already mounted or if the attempt to mount it succeeds.
pub fn ensure_storage_mounted() -> Result<bool> {
// In live/install mode, storage is a tmpfs and /dev/mapper/citadel-storage
// doesn't exist. All resources should be in /run/citadel/images.
if CommandLine::live_mode() || CommandLine::install_mode() {
info!("Live/Install mode: skipping storage mount (using tmpfs)");
return Ok(false);
}
if Mounts::is_source_mounted("/dev/mapper/citadel-storage")? {
return Ok(true);
}
@@ -371,9 +389,30 @@ impl ResourceImage {
}
fn rootfs_channel() -> &'static str {
match CommandLine::channel_name() {
Some(channel) => channel,
None => "dev",
let cmdline_channel = CommandLine::channel_name();
info!("CommandLine::channel_name() = {:?}", cmdline_channel);
match cmdline_channel {
Some(channel) => {
info!("Using channel from kernel command line: {}", channel);
channel
},
None => {
let osrelease_channel = OsRelease::citadel_channel();
info!("OsRelease::citadel_channel() = {:?}", osrelease_channel);
match osrelease_channel {
Some(channel) => {
info!("Using channel from OsRelease: {}", channel);
channel
},
None => {
info!("No channel found, defaulting to 'dev'");
"dev"
}
}
},
}
}
}
@@ -383,15 +422,24 @@ impl ResourceImage {
// in the image header metainfo. If multiple matches are found, return the image
// with the highest version number. If multiple images have the same highest version
// number, return the image with the newest file creation time.
fn search_directory<P: AsRef<Path>>(dir: P, image_type: &str, channel: Option<&str>) -> Result<Option<ResourceImage>> {
// If skip_kernel_match is true, kernel version/id matching is skipped for kernel images.
fn search_directory<P: AsRef<Path>>(dir: P, image_type: &str, channel: Option<&str>, skip_kernel_match: bool) -> Result<Option<ResourceImage>> {
info!("search_directory: dir={}, image_type={}, channel={:?}, skip_kernel_match={}",
dir.as_ref().display(), image_type, channel, skip_kernel_match);
if !dir.as_ref().exists() {
return Ok(None)
}
let mut best = None;
let mut matches = all_matching_images(dir.as_ref(), image_type, channel)?;
debug!("Found {} matching images", matches.len());
let mut matches = all_matching_images(dir.as_ref(), image_type, channel, skip_kernel_match)?;
info!("Found {} matching images in {}", matches.len(), dir.as_ref().display());
for (i, img) in matches.iter().enumerate() {
info!(" Match {}: {} (channel: {}, version: {})",
i, img.path().display(), img.metainfo().channel(), img.metainfo().version());
}
if channel.is_none() {
if matches.is_empty() {
@@ -420,8 +468,10 @@ 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)
@@ -455,17 +505,21 @@ fn current_kernel_version() -> String {
//
// Read a directory search for ResourceImages which match the channel
// and image_type.
// and image_type. If skip_kernel_match is true, skip kernel version/id matching.
//
fn all_matching_images(dir: &Path, image_type: &str, channel: Option<&str>) -> Result<Vec<ResourceImage>> {
fn all_matching_images(dir: &Path, image_type: &str, channel: Option<&str>, skip_kernel_match: bool) -> Result<Vec<ResourceImage>> {
let kernel_version = current_kernel_version();
let kv = if image_type == "kernel" {
let kv = if image_type == "kernel" && !skip_kernel_match {
Some(kernel_version.as_str())
} else {
None
};
let kernel_id = OsRelease::citadel_kernel_id();
let kernel_id = if !skip_kernel_match {
OsRelease::citadel_kernel_id()
} else {
None
};
let mut v = Vec::new();
util::read_directory(dir, |dent| {
@@ -477,8 +531,9 @@ fn all_matching_images(dir: &Path, image_type: &str, channel: Option<&str>) -> R
// Examine a directory entry to determine if it is a resource image which
// matches a given channel and image_type. If the image_type is "kernel"
// then also match the kernel-version and kernel-id fields. If channel
// is None then don't consider the channel in the match.
// then also match the kernel-version and kernel-id fields (unless those
// parameters are None, in which case skip version/id checking).
// If channel is None then don't consider the channel in the match.
//
// If the entry is a match, then instantiate a ResourceImage and add it to
// the images vector.
@@ -490,9 +545,12 @@ fn maybe_add_dir_entry(entry: &DirEntry,
images: &mut Vec<ResourceImage>) -> Result<()> {
let path = entry.path();
info!("Examining directory entry: {}", path.display());
let meta = entry.metadata()
.map_err(context!("failed to read metadata for {:?}", entry.path()))?;
if !meta.is_file() || meta.len() < ImageHeader::HEADER_SIZE as u64 {
if !meta.is_file() {
info!(" Skipping: not a regular file");
return Ok(())
}
let header = match ImageHeader::from_file(&path) {
@@ -504,25 +562,38 @@ fn maybe_add_dir_entry(entry: &DirEntry,
};
if !header.is_magic_valid() {
info!(" Skipping: invalid magic header");
return Ok(())
}
let metainfo = header.metainfo();
debug!("Found an image type={} channel={} kernel={:?}", metainfo.image_type(), metainfo.channel(), metainfo.kernel_version());
info!("Found an image type={} channel={} kernel={:?}", metainfo.image_type(), metainfo.channel(), metainfo.kernel_version());
if let Some(channel) = channel {
if metainfo.channel() != channel {
info!(" Skipping: channel mismatch (want {}, got {})", channel, metainfo.channel());
return Ok(());
}
}
if image_type != metainfo.image_type() {
info!(" Skipping: image_type mismatch (want {}, got {})", image_type, metainfo.image_type());
return Ok(())
}
if image_type == "kernel" && (metainfo.kernel_version() != kernel_version || metainfo.kernel_id() != kernel_id) {
return Ok(());
// Only check kernel version/id if they are specified (Some)
// kernel_version must match if specified
// kernel_id must match only if specified (if None, any kernel_id is acceptable)
if image_type == "kernel" && kernel_version.is_some() {
let version_matches = metainfo.kernel_version() == kernel_version;
let id_matches = kernel_id.is_none() || metainfo.kernel_id() == kernel_id;
if !version_matches || !id_matches {
info!(" Skipping: kernel version/id mismatch (want version={:?}, id={:?}; got version={:?}, id={:?})",
kernel_version, kernel_id, metainfo.kernel_version(), metainfo.kernel_id());
return Ok(());
}
}
images.push(ResourceImage::new(&path, header));

View File

@@ -5,5 +5,5 @@ mod uname;
pub use self::uname::UtsName;
pub use self::loopdev::LoopDevice;
pub use self::mounts::{Mounts,MountLine};
pub use self::mounts::Mounts;
pub use self::lock::FileLock;

183
libcitadel/src/updates.rs Normal file
View File

@@ -0,0 +1,183 @@
use anyhow::Context;
use std::fmt;
use std::io::Write;
use std::slice::Iter;
pub const UPDATE_SERVER_HOSTNAME: &str = "update.subgraph.com";
const CITADEL_CONFIG_PATH: &str = "/storage/citadel-state/citadel.conf";
/// This struct embeds the CitadelVersion datastruct as well as the cryptographic validation of 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, stable ...
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,
pub sha256_hash: 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"),
}
}
}
/// Reads a specific key from an os-release formatted file.
/// The value is returned without any surrounding quotes.
pub fn get_citadel_conf(key: &str) -> anyhow::Result<Option<String>> {
// Read the entire file into a string.
let content = std::fs::read_to_string(CITADEL_CONFIG_PATH)
.context(format!("Failed to read {}", CITADEL_CONFIG_PATH))?;
// Search each line for the key.
for line in content.lines() {
// Check if the line starts with "KEY="
if let Some(value) = line.trim().strip_prefix(&format!("{}=", key)) {
// If found, trim whitespace and quotes from the value and return.
let value = value.trim().trim_matches('"').to_string();
return Ok(Some(value));
}
}
// If the loop finishes without finding the key, return None.
Ok(None)
}
pub fn get_os_release(key: &str) -> anyhow::Result<Option<String>> {
// Read the entire file into a string.
let content = std::fs::read_to_string("/etc/os-release")
.context(format!("Failed to read {}", "/etc/os-release"))?;
// Search each line for the key.
for line in content.lines() {
// Check if the line starts with "KEY="
if let Some(value) = line.trim().strip_prefix(&format!("{}=", key)) {
// If found, trim whitespace and quotes from the value and return.
let value = value.trim().trim_matches('"').to_string();
return Ok(Some(value));
}
}
// If the loop finishes without finding the key, return None.
Ok(None)
}
/// Safely modifies a key-value pair in the citadel config os-release-formated file.
/// If the key does not exist, it will be added to the end of the file.
pub fn set_citadel_conf(key: &str, value: &str) -> anyhow::Result<()> {
// Read the existing os-release file.
let content = std::fs::read_to_string(CITADEL_CONFIG_PATH)
.context(format!("Failed to read {}", CITADEL_CONFIG_PATH))?;
let mut lines: Vec<String> = Vec::new();
let mut key_updated = false;
let key_prefix = format!("{}=", key);
let new_line = format!("{}{}", key_prefix, value);
// Process each line to update the key if it exists.
for line in content.lines() {
if line.starts_with(&key_prefix) {
lines.push(new_line.clone());
key_updated = true;
} else {
lines.push(line.to_string());
}
}
// If the key was not found, add it to the end.
if !key_updated {
lines.push(new_line);
}
// Write the changes back safely using a temporary file and an atomic rename.
let mut temp_file = tempfile::Builder::new()
.prefix("citadel.conf")
.suffix(".tmp")
.tempfile_in(std::path::Path::new(CITADEL_CONFIG_PATH).parent().unwrap())?;
temp_file.write_all(lines.join("\n").as_bytes())?;
temp_file.write_all(b"\n")?; // Ensure the file ends with a newline.
temp_file.persist(CITADEL_CONFIG_PATH).context(format!(
"Failed to overwrite {}. Are you running as root?",
CITADEL_CONFIG_PATH
))?;
Ok(())
}

View File

@@ -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))?;
}
@@ -368,9 +376,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(())
}

View File

@@ -1,15 +0,0 @@
[package]
name = "realm-config-ui"
version = "0.1.0"
authors = ["Bruce Leidl <bruce@subgraph.com>"]
edition = "2018"
description = "Realm Configuration Tool"
homepage = "https://subgraph.com"
[dependencies]
libcitadel = { path = "../libcitadel" }
rand = "0.8"
zvariant = "2.7.0"
serde = { version = "1.0", features = ["derive"] }
zbus = "=2.0.0-beta.5"
gtk = { version = "0.14.0", features = ["v3_24"] }

View File

@@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ColorSchemeDialog" parent="GtkDialog">
<property name="title">Choose Terminal Colors</property>
<property name="modal">True</property>
<child internal-child="vbox">
<object class="GtkBox">
<property name="orientation">horizontal</property>
<child>
<object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property>
<child>
<object class="GtkTreeView" id="colorscheme-tree">
<property name="headers-visible">False</property>
<property name="model">treemodel</property>
<child>
<object class="GtkTreeViewColumn">
<property name="expand">True</property>
<child>
<object class="GtkCellRendererText"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkSeparator">
<property name="orientation">vertical</property>
</object>
</child>
<child>
<object class="GtkLabel" id="colorscheme-label">
<property name="hexpand">True</property>
<property name="halign">fill</property>
</object>
</child>
</object>
</child>
<child type="action">
<object class="GtkButton" id="cancel_button">
<property name="use-underline">1</property>
<property name="label">Cancel</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="ok_button">
<property name="use-underline">1</property>
<property name="label">_Choose</property>
<property name="can-default">True</property>
</object>
</child>
<action-widgets>
<action-widget response="cancel">cancel_button</action-widget>
<action-widget response="ok" default="true">ok_button</action-widget>
</action-widgets>
</template>
<object class="GtkTreeStore" id="treemodel">
<columns>
<column type="gchararray" />
<column type="gchararray" />
</columns>
</object>
</interface>

View File

@@ -1,216 +0,0 @@
use std::rc::Rc;
use gtk::prelude::*;
use gtk::glib;
use libcitadel::terminal::{Base16Scheme, Color};
enum RootEntry {
Scheme(Base16Scheme),
Category(String, Vec<Base16Scheme>),
}
impl RootEntry {
fn key(&self) -> &str {
match self {
RootEntry::Scheme(scheme) => scheme.slug(),
RootEntry::Category(name, _) => name.as_str(),
}
}
fn add_to_category(list: &mut Vec<RootEntry>, category: &str, scheme: &Base16Scheme) {
let scheme = scheme.clone();
for entry in list.iter_mut() {
if let RootEntry::Category(name, schemes) = entry {
if name == category {
schemes.push(scheme);
return;
}
}
}
list.push(RootEntry::Category(category.to_string(), vec![scheme]))
}
fn build_list() -> Vec<RootEntry> {
let mut list = Vec::new();
for scheme in Base16Scheme::all_schemes() {
if let Some(category) = scheme.category() {
Self::add_to_category(&mut list,category, &scheme);
} else {
list.push(RootEntry::Scheme(scheme));
}
}
list.sort_by(|a, b| a.key().cmp(b.key()));
list
}
}
#[derive(Clone)]
pub struct ColorSchemes {
entries: Rc<Vec<RootEntry>>,
}
impl ColorSchemes {
pub fn new() -> Self {
ColorSchemes {
entries: Rc::new(RootEntry::build_list()),
}
}
pub fn populate_tree_model(&self, store: &gtk::TreeStore) {
for entry in self.entries.iter() {
match entry {
RootEntry::Scheme(scheme) => {
let first = scheme.slug().to_string();
let second = scheme.name().to_string();
store.insert_with_values(None, None, &[(0, &first), (1, &second)]);
}
RootEntry::Category(name, list) => {
let first = String::new();
let second = name.to_string();
let iter = store.insert_with_values(None, None, &[(0, &first), (1, &second)]);
for scheme in list {
let first = scheme.slug().to_string();
let second = scheme.name().to_string();
store.insert_with_values(Some(&iter), None, &[(0, &first), (1, &second)]);
}
}
}
}
}
pub fn preview_scheme(&self, id: &str) -> Option<(String, Color)> {
let scheme = Base16Scheme::by_name(id)?;
let bg = scheme.terminal_background();
let text = PreviewRender::new(scheme).render_preview();
Some((text, bg))
}
}
struct PreviewRender {
buffer: String,
scheme: Base16Scheme,
}
impl PreviewRender {
fn new(scheme: &Base16Scheme) -> Self {
let scheme = scheme.clone();
PreviewRender {
buffer: String::new(),
scheme,
}
}
fn print(mut self, color_idx: usize, text: &str) -> Self {
let s = glib::markup_escape_text(text);
let color = self.scheme.terminal_palette_color(color_idx);
self.color_span(Some(color), None);
self.buffer.push_str(s.as_str());
self.end_span();
self
}
fn vtype(self, text: &str) -> Self {
self.print(3, text)
}
fn konst(self, text: &str) -> Self {
self.print(1, text)
}
fn func(self, text: &str) -> Self {
self.print(4, text)
}
fn string(self, text: &str) -> Self {
self.print(2, text)
}
fn keyword(self, text: &str) -> Self {
self.print(5, text)
}
fn comment(self, text: &str) -> Self {
self.print(8, text)
}
fn text(mut self, text: &str) -> Self {
let color = self.scheme.terminal_foreground();
self.color_span(Some(color), None);
self.buffer.push_str(text);
self.end_span();
self
}
fn color_attrib(&mut self, name: &str, color: Color) {
let (r,g,b) = color.rgb();
self.buffer.push_str(&format!(" {}='#{:02X}{:02X}{:02X}'", name, r, g, b));
}
fn color_span(&mut self, fg: Option<Color>, bg: Option<Color>) {
self.buffer.push_str("<span");
if let Some(color) = fg {
self.color_attrib("foreground", color);
}
if let Some(color) = bg {
self.color_attrib("background", color);
}
self.buffer.push_str(">");
}
fn end_span(&mut self) {
self.buffer.push_str("</span>");
}
fn nl(mut self) -> Self {
self.buffer.push_str(" \n ");
self
}
fn render_colorbar(&mut self) {
self.buffer.push_str("\n ");
let color = self.scheme.terminal_foreground();
self.color_span(Some(color), None);
for i in 0..16 {
self.buffer.push_str(&format!(" {:X} ", i));
}
self.end_span();
self.buffer.push_str(" \n ");
for i in 0..16 {
let c = self.scheme.color(i);
self.color_span(None, Some(c));
self.buffer.push_str(" ");
self.end_span();
}
self.buffer.push_str(" \n ");
for i in 8..16 {
let c = self.scheme.terminal_palette_color(i);
self.color_span(None, Some(c));
self.buffer.push_str(" ");
self.end_span();
}
self.buffer.push_str(" \n ");
}
fn render_preview(mut self) -> String {
let name = self.scheme.name().to_string();
self.render_colorbar();
self.nl()
.comment("/**").nl()
.comment(" * An example of how this color scheme").nl()
.comment(" * might look in a text editor with syntax").nl()
.comment(" * highlighting.").nl()
.comment(" */").nl()
.nl()
.func("#include ").string("<stdio.h>").nl()
.func("#include ").string("<stdlib.h>").nl()
.nl()
.vtype("static char").text(" theme[] = ").string(&format!("\"{}\"", name)).text(";").nl()
.nl()
.vtype("int").text(" main(").vtype("int").text(" argc, ").vtype("char").text(" **argv) {").nl()
.text(" printf(").string("\"Hello, ").keyword("%s").text("!").keyword("\\n").string("\"").text(", theme);").nl()
.text(" exit(").konst("0").text(");").nl()
.text("}")
.nl()
.nl().buffer
}
}

View File

@@ -1,153 +0,0 @@
use std::cell::RefCell;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
use libcitadel::terminal::{Base16Scheme, Color};
use crate::colorscheme::colorschemes::ColorSchemes;
#[derive(CompositeTemplate)]
#[template(file = "colorscheme-dialog.ui")]
pub struct ColorSchemeDialog {
#[template_child(id="colorscheme-tree")]
tree: TemplateChild<gtk::TreeView>,
#[template_child]
treemodel: TemplateChild<gtk::TreeStore>,
#[template_child(id="colorscheme-label")]
preview: TemplateChild<gtk::Label>,
css_provider: gtk::CssProvider,
colorschemes: ColorSchemes,
tracker: RefCell<Option<SelectionTracker>>,
}
#[derive(Clone)]
struct SelectionTracker {
model: gtk::TreeStore,
selection: gtk::TreeSelection,
preview: gtk::Label,
colorschemes: ColorSchemes,
css_provider: gtk::CssProvider,
}
impl SelectionTracker {
fn new(dialog: &ColorSchemeDialog) -> Self {
let tracker = SelectionTracker {
model: dialog.treemodel.clone(),
selection: dialog.tree.selection(),
preview: dialog.preview.clone(),
colorschemes: dialog.colorschemes.clone(),
css_provider: dialog.css_provider.clone(),
};
tracker.selection.connect_changed(glib::clone!(@strong tracker => move |_| {
if let Some(id) = tracker.selected_id() {
if let Some((text, background)) = tracker.colorschemes.preview_scheme(&id) {
tracker.set_preview_background(background);
tracker.preview.set_markup(&text);
}
}
}));
tracker
}
fn selected_id(&self) -> Option<String> {
self.selection.selected().and_then(|(model,iter)| {
model.value(&iter, 0).get::<String>().ok()
})
}
fn set_preview_background(&self, color: Color) {
const CSS: &str =
r##"
#colorscheme-label {
background-color: $COLOR;
font-family: monospace;
font-size: 14pt;
}
"##;
let (r, g, b) = color.rgb();
let css = CSS.replace("$COLOR", &format!("#{:02x}{:02x}{:02x}", r, g, b));
if let Err(e) = self.css_provider.load_from_data(css.as_bytes()) {
warn!("Error loading CSS provider data: {}", e);
}
}
fn set_selected_id(&self, id: &str) {
self.model.foreach(glib::clone!(@strong self.selection as selection => move |model, _path, iter| {
if let Ok(ref s) = model.value(iter, 0).get::<String>() {
if s == id {
selection.select_iter(iter);
return true;
}
}
false
}))
}
}
impl ColorSchemeDialog {
pub fn set_selected_id(&self, colorscheme_id: &str) {
let tracker = self.tracker.borrow();
if let Some(tracker) = tracker.as_ref() {
tracker.set_selected_id(colorscheme_id);
}
}
pub fn get_selected_scheme (&self) -> Option<Base16Scheme> {
let tracker = self.tracker.borrow();
tracker.as_ref().and_then(|t| t.selected_id())
.and_then(|id| Base16Scheme::by_name(&id))
.cloned()
}
}
impl Default for ColorSchemeDialog {
fn default() -> Self {
ColorSchemeDialog {
tree: Default::default(),
treemodel: Default::default(),
preview: Default::default(),
css_provider: gtk::CssProvider::new(),
colorschemes: ColorSchemes::new(),
tracker: RefCell::new(None),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for ColorSchemeDialog {
const NAME: &'static str = "ColorSchemeDialog";
type Type = super::ColorSchemeDialog;
type ParentType = gtk::Dialog;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ColorSchemeDialog {
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
self.preview.set_widget_name("colorscheme-label");
self.preview.style_context().add_provider(&self.css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION);
self.colorschemes.populate_tree_model(&self.treemodel);
let tracker = SelectionTracker::new(self);
self.tracker.borrow_mut().replace(tracker);
}
}
impl DialogImpl for ColorSchemeDialog {}
impl WindowImpl for ColorSchemeDialog {}
impl BinImpl for ColorSchemeDialog {}
impl ContainerImpl for ColorSchemeDialog {}
impl WidgetImpl for ColorSchemeDialog {}

View File

@@ -1,31 +0,0 @@
use gtk::glib;
use glib::subclass::prelude::*;
use libcitadel::terminal::Base16Scheme;
mod dialog;
mod colorschemes;
glib::wrapper! {
pub struct ColorSchemeDialog(ObjectSubclass<dialog::ColorSchemeDialog>)
@extends gtk::Dialog, gtk::Window, gtk::Bin, gtk::Container, gtk::Widget,
@implements gtk::Buildable;
}
impl ColorSchemeDialog {
pub fn new() -> Self {
glib::Object::new(&[("use-header-bar", &1)])
.expect("Failed to create ColorSchemeDialog")
}
fn instance(&self) -> &dialog::ColorSchemeDialog {
dialog::ColorSchemeDialog::from_instance(self)
}
pub fn get_selected_scheme(&self) -> Option<Base16Scheme> {
self.instance().get_selected_scheme()
}
pub fn set_selected_scheme(&self, id: &str) {
self.instance().set_selected_id(id);
}
}

View File

@@ -1,155 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ConfigureDialog" parent="GtkDialog">
<property name="title">Configure Realm</property>
<property name="modal">True</property>
<child internal-child="vbox">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="margin">20</property>
<child>
<object class="GtkLabel">
<property name="label">Options</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="margin-bottom">20</property>
<child>
<!-- -->
<object class="GtkListBox" id="bool-options-box">
<property name="margin">10</property>
<property name="selection_mode">none</property>
<property name="activate_on_single_click">False</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="tooltip-markup"><![CDATA[<b><big>Overlay</big></b>
Type of rootfs overlay realm is configured to use.
<b>None</b> Don't use a rootfs overlay
<b>TmpFS</b> Use a rootfs overlay stored on tmpfs
<b>Storage</b> Use a rootfs overlay stored on disk in storage partition
]]></property>
<child>
<object class="GtkLabel">
<property name="label">Overlay</property>
<property name="hexpand">True</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkComboBoxText" id="overlay-combo">
<property name="active">0</property>
<items>
<item id="storage">Storage</item>
<item id="tmpfs">TmpFS</item>
<item id="none">None</item>
</items>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="tooltip-markup"><![CDATA[<b><big>RealmFS</big></b>
Root filesystem image to use for realm.
]]></property>
<child>
<object class="GtkLabel">
<property name="label">RealmFS</property>
<property name="hexpand">True</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkComboBoxText" id="realmfs-combo">
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="tooltip-markup"><![CDATA[<b><big>Terminal Color Scheme</big></b>
Choose a color scheme to use in terminals in this realm.
]]></property>
<child>
<object class="GtkLabel">
<property name="label">Color Scheme</property>
<property name="hexpand">True</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkButton" id="color-scheme-button">
<property name="label">Default Dark</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="tooltip-markup"><![CDATA[<b><big>Window Frame Color</big></b>
Set a color to be used when frames are drawn around application windows for this realm.
]]></property>
<child>
<object class="GtkLabel">
<property name="label">Frame Color</property>
<property name="hexpand">True</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkColorButton" id="frame-color-button">
<property name="color">#ffff00000000</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="action">
<object class="GtkButton" id="cancel_button">
<property name="use-underline">1</property>
<property name="label">Cancel</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="ok_button">
<property name="use-underline">1</property>
<property name="label">Apply</property>
<property name="can-default">True</property>
</object>
</child>
<action-widgets>
<action-widget response="cancel">cancel_button</action-widget>
<action-widget response="ok" default="true">ok_button</action-widget>
</action-widgets>
</template>
<object class="GtkSizeGroup">
<widgets>
<widget name="overlay-combo" />
<widget name="realmfs-combo" />
<widget name="color-scheme-button" />
<widget name="frame-color-button" />
</widgets>
</object>
</interface>

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ConfigureOption" parent="GtkListBoxRow">
<property name="width_request">100</property>
<property name="activatable">False</property>
<property name="selectable">False</property>
<child>
<object class="GtkBox">
<property name="margin-bottom">5</property>
<property name="spacing">30</property>
<child>
<object class="GtkLabel" id="name">
<property name="hexpand">True</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkSwitch" id="switch">
<property name="halign">end</property>
</object>
</child>
</object>
</child>
</template>
</interface>

View File

@@ -1,203 +0,0 @@
use std::cell::{Ref, RefCell};
use std::rc::Rc;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
use crate::colorscheme::ColorSchemeDialog;
use crate::configure_dialog::ConfigOptions;
use crate::configure_dialog::settings::CitadelSettings;
use crate::realmsd::RealmConfig;
#[derive(CompositeTemplate)]
#[template(file = "configure-dialog.ui")]
pub struct ConfigureDialog {
#[template_child(id="bool-options-box")]
bool_option_list: TemplateChild<gtk::ListBox>,
#[template_child(id="overlay-combo")]
overlay: TemplateChild<gtk::ComboBoxText>,
#[template_child(id="realmfs-combo")]
realmfs: TemplateChild<gtk::ComboBoxText>,
#[template_child(id="color-scheme-button")]
colorscheme: TemplateChild<gtk::Button>,
#[template_child(id="frame-color-button")]
frame_color: TemplateChild<gtk::ColorButton>,
options: Rc<RefCell<ConfigOptions>>,
bool_option_rows: RefCell<Vec<super::ConfigureOption>>,
colorscheme_dialog: ColorSchemeDialog,
settings: RefCell<CitadelSettings>,
}
impl ConfigureDialog {
pub fn set_realm_name(&self, name: &str) {
let color = self.settings.borrow().get_realm_color(Some(name));
self.frame_color.set_rgba(&color);
}
pub fn reset_options(&self) {
self.options.borrow_mut().reset();
self.update_options();
}
pub fn set_config(&self, config: &RealmConfig) {
self.options.borrow_mut().configure(config);
self.realmfs.remove_all();
self.update_options();
}
pub fn changes(&self) -> Vec<(String,String)> {
self.options.borrow().changes()
}
pub fn store_settings(&self, realm_name: &str) {
let color = self.frame_color.rgba();
self.settings.borrow_mut().store_realm_color(realm_name, color);
}
pub fn options(&self) -> Ref<ConfigOptions> {
self.options.borrow()
}
fn update_realmfs(&self) {
self.realmfs.remove_all();
for realmfs in self.options().realmfs_list() {
self.realmfs.append(Some(realmfs.as_str()), realmfs.as_str());
}
let current = self.options().realmfs();
self.realmfs.set_active_id(Some(&current));
}
fn update_options(&self) {
let rows = self.bool_option_rows.borrow();
for row in rows.iter() {
row.update();
}
let overlay_id = self.options().overlay_id();
self.overlay.set_active_id(Some(&overlay_id));
self.update_realmfs();
let scheme = self.options().colorscheme();
self.colorscheme.set_label(scheme.name());
}
fn create_option_rows(&self) {
let mut rows = self.bool_option_rows.borrow_mut();
let options = self.options.borrow();
for op in options.bool_options() {
let w = super::ConfigureOption::new(op);
self.bool_option_list.add(&w);
rows.push(w);
}
}
fn setup_overlay(&self) {
let options = self.options.clone();
self.overlay.connect_changed(move |combo| {
if let Some(text) = combo.active_id() {
options.borrow_mut().set_overlay_id(text.as_str());
}
});
}
fn setup_realmfs(&self) {
let options = self.options.clone();
self.realmfs.connect_changed(move |combo| {
if let Some(text) = combo.active_text() {
options.borrow_mut().set_realmfs(text.as_str());
}
});
}
fn setup_colorscheme(&self) {
let dialog = self.colorscheme_dialog.clone();
let options = self.options.clone();
self.colorscheme.connect_clicked(move |b| {
dialog.show_all();
let scheme = options.borrow().colorscheme();
dialog.set_selected_scheme(scheme.slug());
match dialog.run() {
gtk::ResponseType::Ok => {
if let Some(scheme) = dialog.get_selected_scheme() {
options.borrow_mut().set_colorscheme_id(scheme.slug());
b.set_label(scheme.name());
}
},
_ => {},
}
dialog.hide();
});
}
fn setup_frame_color(&self) {
let color = self.settings.borrow().get_realm_color(None);
self.frame_color.set_rgba(&color);
}
fn setup_widgets(&self) {
self.create_option_rows();
self.setup_overlay();
self.setup_realmfs();
self.setup_colorscheme();
self.setup_frame_color();
}
}
impl Default for ConfigureDialog {
fn default() -> Self {
ConfigureDialog {
bool_option_list: Default::default(),
overlay: Default::default(),
realmfs: Default::default(),
colorscheme: Default::default(),
frame_color: Default::default(),
colorscheme_dialog: ColorSchemeDialog::new(),
options: Rc::new(RefCell::new(ConfigOptions::new())),
settings: RefCell::new(CitadelSettings::new()),
bool_option_rows: RefCell::new(Vec::new()),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for ConfigureDialog {
const NAME: &'static str = "ConfigureDialog";
type Type = super::ConfigureDialog;
type ParentType = gtk::Dialog;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ConfigureDialog {
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
self.colorscheme_dialog.set_transient_for(Some(&self.instance()));
self.setup_widgets();
}
}
impl DialogImpl for ConfigureDialog {}
impl WindowImpl for ConfigureDialog {}
impl BinImpl for ConfigureDialog {}
impl ContainerImpl for ConfigureDialog {}
impl WidgetImpl for ConfigureDialog {}

View File

@@ -1,78 +0,0 @@
use gtk::glib;
use gtk::prelude::*;
use glib::subclass::prelude::*;
use crate::realmsd::RealmConfig;
pub use crate::configure_dialog::options::{ConfigOptions,BoolOption};
mod dialog;
mod option_row;
mod options;
mod settings;
glib::wrapper! {
pub struct ConfigureDialog(ObjectSubclass<dialog::ConfigureDialog>)
@extends gtk::Dialog, gtk::Window, gtk::Bin, gtk::Container, gtk::Widget,
@implements gtk::Buildable;
}
impl ConfigureDialog {
pub fn new() -> Self {
glib::Object::new(&[("use-header-bar", &1)])
.expect("Failed to create ConfigureDialog")
}
fn instance(&self) -> &dialog::ConfigureDialog {
dialog::ConfigureDialog::from_instance(self)
}
pub fn changes(&self) -> Vec<(String,String)> {
self.instance().changes()
}
pub fn store_settings(&self, realm_name: &str) {
self.instance().store_settings(realm_name);
}
pub fn reset_options(&self) {
self.instance().reset_options();
}
pub fn set_realm_name(&self, name: &str) {
self.set_title(&format!("Configure realm-{}", name));
self.instance().set_realm_name(name);
}
pub fn set_config(&self, config: &RealmConfig) {
self.instance().set_config(config);
}
}
glib::wrapper! {
pub struct ConfigureOption(ObjectSubclass<option_row::ConfigureOption>)
@extends gtk::Widget, gtk::Bin, gtk::Container,
@implements gtk::Buildable, gtk::Actionable;
}
impl ConfigureOption {
pub fn new(option: &BoolOption) -> Self {
let widget :Self = glib::Object::new(&[])
.expect("Failed to create ConfigureOption");
widget.set_bool_option(option);
widget
}
fn instance(&self) -> &option_row::ConfigureOption {
option_row::ConfigureOption::from_instance(self)
}
pub fn update(&self) {
self.instance().update();
}
fn set_bool_option(&self, option: &BoolOption) {
self.set_tooltip_markup(Some(option.tooltip()));
self.instance().set_bool_option(option);
}
}

View File

@@ -1,68 +0,0 @@
use std::cell::RefCell;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
use crate::configure_dialog::BoolOption;
#[derive(CompositeTemplate)]
#[template(file = "configure-option-switch.ui")]
pub struct ConfigureOption {
#[template_child]
pub name: TemplateChild<gtk::Label>,
#[template_child]
pub switch: TemplateChild<gtk::Switch>,
pub option: RefCell<Option<BoolOption>>,
}
impl Default for ConfigureOption {
fn default() -> Self {
ConfigureOption {
name: Default::default(),
switch: Default::default(),
option: RefCell::new(None),
}
}
}
impl ConfigureOption {
pub fn set_bool_option(&self, option: &BoolOption) {
self.name.set_text(option.description());
self.switch.set_state(option.value());
self.switch.connect_state_set(glib::clone!(@strong option => move |_b,v| {
option.set_value(v);
Inhibit(false)
}));
self.option.borrow_mut().replace(option.clone());
}
pub fn update(&self) {
let option = self.option.borrow();
if let Some(option) = option.as_ref() {
self.switch.set_state(option.value());
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for ConfigureOption {
const NAME: &'static str = "ConfigureOption";
type Type = super::ConfigureOption;
type ParentType = gtk::ListBoxRow;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ConfigureOption {}
impl WidgetImpl for ConfigureOption {}
impl ContainerImpl for ConfigureOption {}
impl BinImpl for ConfigureOption {}
impl ListBoxRowImpl for ConfigureOption {}

View File

@@ -1,384 +0,0 @@
use std::cell::Cell;
use std::rc::Rc;
use libcitadel::OverlayType;
use libcitadel::terminal::Base16Scheme;
use crate::realmsd::RealmConfig;
const GPU_TOOLTIP: &str = r#"If enabled the render node device <tt><b>/dev/dri/renderD128</b></tt> will be mounted into the realm container.
If privileged device <tt><b>/dev/dri/card0</b></tt> is also needed set
additional variable in realm configuration file:
<tt><b>use-gpu-card0 = true</b></tt>
"#;
const WAYLAND_TOOLTIP: &str = "\
If enabled access to Wayland display will be permitted in realm by adding wayland socket to realm.
<tt><b>/run/user/1000/wayland-0</b></tt>
";
const X11_TOOLTIP: &str = "\
If enabled access to X11 server will be added by mounting directory X11 directory into realm.
<tt><b>/tmp/.X11-unix</b></tt>
";
const SOUND_TOOLTIP: &str = r#"If enabled allows use of sound inside of realm. The following items will be added:
<tt><b>/dev/snd</b></tt>
<tt><b>/dev/shm</b></tt>
<tt><b>/run/user/1000/pulse</b></tt>
"#;
const SHARED_DIR_TOOLTIP: &str = r#"If enabled the shared directory will be mounted as <tt><b>/Shared</b></tt> in home directory of realm.
This directory is shared between all realms with this option enabled and is an easy way to move files between realms.
"#;
const NETWORK_TOOLTIP: &str = "\
If enabled the realm will have access to the network.
";
const KVM_TOOLTIP: &str = r#"If enabled device <tt><b>/dev/kvm</b></tt> will be added to realm.
This allows use of applications such as Qemu inside of realms.
"#;
const EPHERMERAL_HOME_TOOLTIP: &str = r#"If enabled the home directory of realm will be set up in ephemeral mode.
The ephemeral home directory is set up with the following steps:
1. Home directory is mounted as tmpfs filesystem
2. Any files in <tt><b>/realms/skel</b></tt> are copied into home directory
3. Any files in <tt><b>/realms/realm-$name/skel</b></tt> are copied into home directory.
4. Any directories listed in config file variable <tt><b>ephemeral_persistent_dirs</b></tt>
are bind mounted from <tt><b>/realms/realm-$name/home</b></tt> into ephemeral
home directory.
"#;
const BOOL_OPTIONS: &[(&str, &str, &str)] = &[
("use-gpu", "Use GPU in Realm", GPU_TOOLTIP),
("use-wayland", "Use Wayland in Realm", WAYLAND_TOOLTIP),
("use-x11", "Use X11 in Realm", X11_TOOLTIP),
("use-sound", "Use Sound in Realm", SOUND_TOOLTIP),
("use-shared-dir", "Mount /Shared directory in Realm", SHARED_DIR_TOOLTIP),
("use-network", "Realm has network access", NETWORK_TOOLTIP),
("use-kvm", "Use KVM (/dev/kvm) in Realm", KVM_TOOLTIP),
("use-ephemeral-home", "Use ephemeral tmpfs mount for home directory", EPHERMERAL_HOME_TOOLTIP),
];
#[derive(Clone)]
pub struct BoolOption {
id: String,
description: String,
tooltip: String,
original: Rc<Cell<bool>>,
value: Rc<Cell<bool>>,
}
impl BoolOption {
fn create_options() -> Vec<BoolOption> {
let mut bools = Vec::new();
for (id, description, tooltip) in BOOL_OPTIONS {
bools.push(BoolOption::new(id, description, tooltip));
}
bools
}
fn new(id: &str, description: &str, tooltip: &str) -> Self {
let id = id.to_string();
let description = description.to_string();
let tooltip = format!("<b><big>{}</big></b>\n\n{}", description, tooltip);
let value = Rc::new(Cell::new(false));
let original = Rc::new(Cell::new(false));
BoolOption { id, description, tooltip, original, value }
}
pub fn value(&self) -> bool {
self.value.get()
}
fn has_changed(&self) -> bool {
self.value() != self.original.get()
}
pub fn set_value(&self, v: bool) {
self.value.set(v);
}
pub fn id(&self) -> &str {
&self.id
}
pub fn description(&self) -> &str {
&self.description
}
pub fn tooltip(&self) -> &str {
&self.tooltip
}
fn configure(&self, config: &RealmConfig) {
let v = config.get_bool(self.id());
self.original.set(v);
self.value.set(v);
}
fn reset(&self) {
self.set_value(self.original.get());
}
fn add_changes(&self, result: &mut Vec<(String, String)>) {
if self.has_changed() {
let k = self.id.clone();
let v = self.value().to_string();
result.push((k, v))
}
}
}
struct OverlayOption {
original: OverlayType,
current: OverlayType,
}
impl OverlayOption {
fn new() -> Self {
OverlayOption {
original: OverlayType::None,
current: OverlayType::None,
}
}
fn overlay_str_to_enum(str: Option<&str>) -> OverlayType {
match str {
Some("storage") => OverlayType::Storage,
Some("tmpfs") => OverlayType::TmpFS,
Some("none") => OverlayType::None,
None => OverlayType::None,
Some(s) => {
warn!("Unexpected overlay type: {}", s);
OverlayType::None
},
}
}
fn set_overlay(&mut self, overlay: &str) {
self.current = Self::overlay_str_to_enum(Some(overlay));
}
fn str_value(&self) -> String {
self.current.to_str_value()
.unwrap_or("none").to_string()
}
fn configure(&mut self, config: &RealmConfig) {
let overlay = Self::overlay_str_to_enum(config.get_string("overlay"));
self.original = overlay;
self.current = overlay;
}
fn reset(&mut self) {
self.current = self.original;
}
fn add_changes(&self, result: &mut Vec<(String, String)>) {
if self.original != self.current {
let k = "overlay".to_string();
let v = self.str_value();
result.push((k, v));
}
}
}
struct RealmFsOption {
original: String,
current: String,
realmfs_list: Vec<String>,
}
impl RealmFsOption {
fn new() -> Self {
let base = String::from("base");
RealmFsOption {
original: base.clone(),
current: base.clone(),
realmfs_list: vec![base],
}
}
fn realmfs_list(&self) -> Vec<String> {
self.realmfs_list.clone()
}
fn current(&self) -> String {
self.current.clone()
}
fn set_current(&mut self, realmfs: &str) {
self.current = realmfs.to_string();
}
fn configure(&mut self, config: &RealmConfig) {
if let Some(realmfs) = config.get_string("realmfs") {
self.realmfs_list.clear();
self.realmfs_list.extend(config.realmfs_list().iter().cloned());
self.original = realmfs.to_string();
self.current = realmfs.to_string();
}
}
fn reset(&mut self) {
self.current = self.original.clone();
}
fn add_changes(&self, result: &mut Vec<(String, String)>) {
if self.current.is_empty() {
return;
}
if self.current != self.original {
result.push(("realmfs".to_string(), self.current.clone()))
}
}
}
const DEFAULT_SCHEME: &str = "default-dark";
struct ColorSchemeOption {
original: Base16Scheme,
current: Base16Scheme,
}
impl ColorSchemeOption {
fn new() -> Self {
let scheme = Base16Scheme::by_name(DEFAULT_SCHEME)
.expect("default Base16Scheme");
ColorSchemeOption {
original: scheme.clone(),
current: scheme.clone(),
}
}
fn configure(&mut self, config: &RealmConfig) {
if let Some(scheme) = config.get_string("terminal-scheme") {
if let Some(scheme) = Base16Scheme::by_name(scheme) {
self.original = scheme.clone();
self.current = scheme.clone();
}
}
}
fn reset(&mut self) {
self.set_current(self.original.clone());
}
fn set_current(&mut self, scheme: Base16Scheme) {
self.current = scheme;
}
fn set_current_id(&mut self, id: &str) {
if let Some(scheme) = Base16Scheme::by_name(id) {
self.set_current(scheme.clone());
}
}
fn current(&self) -> Base16Scheme {
self.current.clone()
}
fn add_changes(&self, result: &mut Vec<(String, String)>) {
if self.original.slug() != self.current.slug() {
result.push(("terminal-scheme".to_string(), self.current.slug().to_string()));
}
}
}
pub struct ConfigOptions {
bool_options: Vec<BoolOption>,
overlay: OverlayOption,
realmfs: RealmFsOption,
colorscheme: ColorSchemeOption,
}
impl ConfigOptions {
pub fn configure(&mut self, config: &RealmConfig) {
for op in &self.bool_options {
op.configure(config);
}
self.overlay.configure(config);
self.realmfs.configure(config);
self.colorscheme.configure(config);
}
pub fn reset(&mut self) {
for op in &self.bool_options {
op.reset();
}
self.overlay.reset();
self.realmfs.reset();
self.colorscheme.reset();
}
pub fn changes(&self) -> Vec<(String,String)> {
let mut changes = Vec::new();
for op in &self.bool_options {
op.add_changes(&mut changes);
}
self.overlay.add_changes(&mut changes);
self.realmfs.add_changes(&mut changes);
self.colorscheme.add_changes(&mut changes);
changes
}
pub fn new() -> Self {
let bool_options = BoolOption::create_options();
let overlay = OverlayOption::new();
let realmfs = RealmFsOption::new();
let colorscheme = ColorSchemeOption::new();
ConfigOptions {
bool_options, overlay, realmfs, colorscheme,
}
}
pub fn bool_options(&self) -> &[BoolOption] {
&self.bool_options
}
pub fn realmfs_list(&self) -> Vec<String> {
self.realmfs.realmfs_list()
}
pub fn overlay_id(&self) -> String {
self.overlay.str_value()
}
pub fn set_overlay_id(&mut self, id: &str) {
self.overlay.set_overlay(id);
}
pub fn realmfs(&self) -> String {
self.realmfs.current()
}
pub fn set_realmfs(&mut self, realmfs: &str) {
self.realmfs.set_current(realmfs);
}
pub fn colorscheme(&self) -> Base16Scheme {
self.colorscheme.current()
}
pub fn set_colorscheme_id(&mut self, id: &str) {
self.colorscheme.set_current_id(id);
}
}

View File

@@ -1,126 +0,0 @@
use std::collections::HashSet;
use std::convert::TryFrom;
use gtk::{gdk,gio};
use gtk::gio::prelude::*;
use rand::Rng;
use libcitadel::Realm;
pub struct CitadelSettings {
settings: gio::Settings,
frame_colors: Vec<gdk::RGBA>,
realms: Vec<RealmFrameColor>,
used_colors: HashSet<gdk::RGBA>,
}
#[derive(Clone)]
struct RealmFrameColor(String,gdk::RGBA);
impl RealmFrameColor {
fn new(realm: &str, color: &gdk::RGBA) -> Self {
RealmFrameColor(realm.to_string(), color.clone())
}
fn realm(&self) -> &str {
&self.0
}
fn color(&self) -> &gdk::RGBA {
&self.1
}
fn set_color(&mut self, color: gdk::RGBA) {
self.1 = color;
}
}
impl CitadelSettings {
fn choose_random_color(&self) -> gdk::RGBA {
if !self.frame_colors.is_empty() {
let n = rand::thread_rng().gen_range(0..self.frame_colors.len());
self.frame_colors[n].clone()
} else {
gdk::RGBA::blue()
}
}
fn allocate_color(&self) -> gdk::RGBA {
self.frame_colors.iter()
.find(|&c| !self.used_colors.contains(c))
.cloned()
.unwrap_or_else(|| self.choose_random_color())
}
pub fn get_realm_color(&self, name: Option<&str>) -> gdk::RGBA {
name.and_then(|name| self.get_realm_frame_color(name))
.cloned()
.unwrap_or_else(|| self.allocate_color())
}
pub fn store_realm_color(&mut self, name: &str, color: gdk::RGBA) -> bool {
if let Some(realm) = self.realms.iter_mut().find(|r| r.realm() == name) {
realm.set_color(color);
} else {
self.realms.push(RealmFrameColor::new(name, &color));
}
let list = self.realms.iter().map(|r| r.to_string()).collect::<Vec<String>>();
let realms = list.iter().map(|s| s.as_str()).collect::<Vec<&str>>();
self.settings.set_strv("realm-label-colors", &realms).is_ok()
}
pub fn new() -> Self {
let settings = gio::Settings::new("com.subgraph.citadel");
let realms = settings.strv("realm-label-colors")
.into_iter()
.flat_map(|gs| RealmFrameColor::try_from(gs.as_str()).ok())
.collect::<Vec<RealmFrameColor>>();
let frame_colors = settings.strv("label-color-list").into_iter()
.flat_map(|gs| gs.as_str().parse().ok())
.collect();
let used_colors = realms.iter()
.map(|rfc| rfc.1.clone()).collect();
CitadelSettings {
settings,
frame_colors,
realms,
used_colors,
}
}
fn get_realm_frame_color(&self, name: &str) -> Option<&gdk::RGBA> {
self.realms.iter()
.find(|r| r.realm() == name)
.map(|r| r.color())
}
}
impl TryFrom<&str> for RealmFrameColor {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
let idx = value.find(':').ok_or(())?;
let (realm, color_str) = value.split_at(idx);
let rgba = &color_str[1..].parse::<gdk::RGBA>()
.map_err(|_| ())?;
if Realm::is_valid_name(realm) {
Ok(RealmFrameColor::new(realm, rgba))
} else {
Err(())
}
}
}
impl ToString for RealmFrameColor {
fn to_string(&self) -> String {
format!("{}:{}", self.realm(), self.color())
}
}

View File

@@ -1,60 +0,0 @@
use std::result;
use std::fmt;
use crate::error::Error::Zbus;
use std::fmt::Formatter;
use gtk::prelude::*;
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
Zbus(zbus::Error),
ManagerConnect,
NoSuchRealm(String),
CreateRealmFailed,
}
impl Error {
fn create_dialog(&self) -> gtk::MessageDialog {
let title = "Error";
let message = self.to_string();
gtk::MessageDialog::builder()
.message_type(gtk::MessageType::Error)
.title(title)
.text(&message)
.buttons(gtk::ButtonsType::Close)
.build()
}
pub fn error_dialog<P: IsA<gtk::Window>>(&self, parent: Option<&P>) {
let dialog = self.create_dialog();
dialog.set_transient_for(parent);
dialog.run();
dialog.close();
}
pub fn app_error_dialog(&self, app: &gtk::Application) {
let dialog = self.create_dialog();
app.add_window(&dialog);
dialog.run();
dialog.close();
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Error::Zbus(e) => write!(f, "ZBus error: {}", e),
Error::ManagerConnect => write!(f, "Unable to connect to Realms Manager"),
Error::NoSuchRealm(name) => write!(f, "Realm '{}' does not exist", name),
Error::CreateRealmFailed => write!(f, "Failed to create new realm"),
}
}
}
impl From<zbus::Error> for Error {
fn from(e: zbus::Error) -> Self {
Zbus(e)
}
}

View File

@@ -1,120 +0,0 @@
#[macro_use] extern crate libcitadel;
use std::env;
use gtk::prelude::*;
use gtk::gio;
use crate::configure_dialog::ConfigureDialog;
use crate::new_realm::NewRealmDialog;
use crate::error::Result;
use crate::realmsd::{RealmConfig, RealmsManagerProxy};
mod realmsd;
mod error;
mod colorscheme;
mod configure_dialog;
mod new_realm;
fn load_realm_names() -> Result<(RealmsManagerProxy<'static>, Vec<String>, RealmConfig)> {
let manager = RealmsManagerProxy::connect()?;
let names = manager.realm_names()?;
let config = manager.default_config()?;
Ok((manager, names, config))
}
fn new_realm_ui(app: &gtk::Application) {
let (manager, realms, config) = match load_realm_names() {
Ok(v) => v,
Err(err) => {
err.app_error_dialog(app);
return;
}
};
let dialog = NewRealmDialog::new();
dialog.set_realm_names(&realms);
dialog.set_config(&config);
app.add_window(&dialog);
dialog.show_all();
if dialog.run() == gtk::ResponseType::Ok {
let realm = dialog.get_realm_name();
dialog.store_config_settings();
let changes = dialog.config_changes();
if let Err(err) = manager.create_new_realm(&realm, changes) {
err.error_dialog(Some(&dialog));
}
}
dialog.close();
}
fn load_realm_config(realm_name: &str) -> Result<(RealmsManagerProxy<'static>, RealmConfig)> {
let manager = RealmsManagerProxy::connect()?;
let config = manager.config(realm_name)?;
Ok((manager, config))
}
fn configure_realm_ui(app: &gtk::Application, name: &str) {
let (manager, config) = match load_realm_config(name) {
Ok(val) => val,
Err(err) => {
err.app_error_dialog(app);
return;
}
};
let dialog = ConfigureDialog::new();
app.add_window(&dialog);
dialog.set_config(&config);
dialog.set_realm_name(name);
dialog.show_all();
if dialog.run() == gtk::ResponseType::Ok {
dialog.store_settings(name);
let changes = dialog.changes();
if !changes.is_empty() {
if let Err(err) = manager.configure_realm(name, changes) {
err.error_dialog(Some(&dialog));
}
}
}
dialog.close();
}
fn test_ui(app: &gtk::Application) {
let config = RealmConfig::new_default(vec![String::from("main"), String::from("foo")]);
let dialog = ConfigureDialog::new();
app.add_window(&dialog);
dialog.set_config(&config);
dialog.set_title("Configure realm-testing");
dialog.show_all();
if dialog.run() == gtk::ResponseType::Ok {
let changes = dialog.changes();
println!("Changes: {:?}", changes);
}
dialog.close();
}
fn main() {
let mut args = env::args().collect::<Vec<String>>();
if args.len() > 1 {
let first = args.remove(1);
let application = gtk::Application::new(Some("com.subgraph.RealmConfig"), gio::ApplicationFlags::empty());
if first.as_str() == "--new" {
application.connect_activate(new_realm_ui);
} else if first.as_str() == "--test" {
application.connect_activate(test_ui);
} else {
application.connect_activate(move |app| {
configure_realm_ui(app, &first);
});
}
application.run_with_args(&args);
}
}

View File

@@ -1,134 +0,0 @@
use std::cell::RefCell;
use std::rc::Rc;
use gtk::glib;
use gtk::CompositeTemplate;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use crate::configure_dialog::ConfigureDialog;
use crate::new_realm::verifier::RealmNameVerifier;
use crate::realmsd::RealmConfig;
#[derive(CompositeTemplate)]
#[template(file = "new-realm-dialog.ui")]
pub struct NewRealmDialog {
#[template_child]
pub infobar: TemplateChild<gtk::InfoBar>,
#[template_child]
pub infolabel: TemplateChild<gtk::Label>,
#[template_child]
pub label: TemplateChild<gtk::Label>,
#[template_child]
entry: TemplateChild<gtk::Entry>,
#[template_child (id="config-button")]
pub config_button: TemplateChild<gtk::Button>,
pub realm_names: Rc<RefCell<Vec<String>>>,
configure_dialog: ConfigureDialog,
}
impl Default for NewRealmDialog {
fn default() -> Self {
NewRealmDialog {
infobar: Default::default(),
infolabel: Default::default(),
label: Default::default(),
entry: Default::default(),
config_button: Default::default(),
realm_names: Default::default(),
configure_dialog: ConfigureDialog::new(),
}
}
}
impl NewRealmDialog {
pub fn set_realm_names(&self, names: &[String]) {
let mut lock = self.realm_names.borrow_mut();
lock.clear();
lock.extend_from_slice(&names)
}
pub fn set_config(&self, config: &RealmConfig) {
self.configure_dialog.set_config(config);
}
pub fn get_realm_name(&self) -> String {
self.entry.text().to_string()
}
pub fn config_changes(&self) -> Vec<(String,String)> {
self.configure_dialog.changes()
}
pub fn store_config_settings(&self) {
let realm_name = self.get_realm_name();
if !realm_name.is_empty() {
self.configure_dialog.store_settings(&realm_name);
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for NewRealmDialog {
const NAME: &'static str = "NewRealmDialog";
type Type = super::NewRealmDialog;
type ParentType = gtk::Dialog;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for NewRealmDialog {
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
self.configure_dialog.set_transient_for(Some(&self.instance()));
let verifier = Rc::new(RealmNameVerifier::new(self));
self.entry.connect_insert_text(glib::clone!(@strong verifier => move |entry, text, pos|{
if !verifier.verify_insert(entry, text, *pos) {
entry.stop_signal_emission("insert-text");
}
}));
self.entry.connect_delete_text(glib::clone!(@strong verifier => move |entry, start, end| {
if !verifier.verify_delete(entry, start, end) {
entry.stop_signal_emission("delete-text");
}
}));
self.entry.connect_changed(glib::clone!(@strong verifier => move |entry| {
verifier.changed(entry);
}));
let config_dialog = self.configure_dialog.clone();
let entry = self.entry.clone();
self.config_button.connect_clicked(move |_b| {
let name = entry.text().to_string();
config_dialog.set_title(&format!("Configure realm-{}", name));
config_dialog.show_all();
match config_dialog.run() {
gtk::ResponseType::Ok => {},
_ => config_dialog.reset_options(),
}
config_dialog.hide();
});
}
}
impl DialogImpl for NewRealmDialog {}
impl WindowImpl for NewRealmDialog {}
impl BinImpl for NewRealmDialog {}
impl ContainerImpl for NewRealmDialog {}
impl WidgetImpl for NewRealmDialog {}

View File

@@ -1,44 +0,0 @@
use gtk::glib;
use glib::subclass::prelude::*;
use crate::realmsd::RealmConfig;
mod dialog;
mod verifier;
glib::wrapper! {
pub struct NewRealmDialog(ObjectSubclass<dialog::NewRealmDialog>)
@extends gtk::Dialog, gtk::Window, gtk::Bin, gtk::Container, gtk::Widget,
@implements gtk::Buildable;
}
impl NewRealmDialog {
pub fn new() -> Self {
glib::Object::new(&[("use-header-bar", &1)])
.expect("Failed to create NewRealmDialog")
}
fn instance(&self) -> &dialog::NewRealmDialog {
dialog::NewRealmDialog::from_instance(self)
}
pub fn set_realm_names(&self, names: &[String]) {
self.instance().set_realm_names(names);
}
pub fn set_config(&self, config: &RealmConfig) {
self.instance().set_config(config);
}
pub fn get_realm_name(&self) -> String {
self.instance().get_realm_name()
}
pub fn config_changes(&self) -> Vec<(String,String)> {
self.instance().config_changes()
}
pub fn store_config_settings(&self) {
self.instance().store_config_settings();
}
}

Some files were not shown because too many files have changed in this diff Show More