forked from brl/citadel-tools
Compare commits
31 Commits
update_too
...
tuf_update
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e80644d4c | |||
|
|
3a3d5c3b9b | ||
|
|
be413476b2 | ||
|
|
87575e396c | ||
|
|
d4035cb9c3 | ||
|
|
df6e0de7c0 | ||
|
|
3966d1d753 | ||
|
|
ba305af893 | ||
|
|
9a273b78ff | ||
| 39ac0948ef | |||
| 1b4a780225 | |||
| e39623b5a9 | |||
|
|
ea55849afb | ||
|
|
c864490dd0 | ||
|
|
97be5b5793 | ||
|
|
ecefb17c82 | ||
|
|
53f87ae338 | ||
|
|
d44847dec6 | ||
|
|
7d3b002dab | ||
|
|
ba268016a6 | ||
|
|
65aa521118 | ||
|
|
3f38cbe099 | ||
|
|
bbcfe9540c | ||
|
|
533fb462f9 | ||
|
|
0aa5c36eee | ||
|
|
3879feb998 | ||
|
|
87dc7b9668 | ||
|
|
d34deab087 | ||
|
|
af75d3ce4a | ||
|
|
11ec3441c2 | ||
|
|
11b3e8a016 |
2627
Cargo.lock
generated
2627
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
[workspace]
|
||||
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui", "launch-gnome-software" ]
|
||||
members = ["citadel-realms", "citadel-tool", "realmsd", "launch-gnome-software", "update-realmfs" ]
|
||||
resolver = "2"
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
||||
1
citadel-installer-ui/.gitignore
vendored
1
citadel-installer-ui/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/target
|
||||
2854
citadel-installer-ui/Cargo.lock
generated
2854
citadel-installer-ui/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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 <b>Apply</b> 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 <b>Next</b>.
|
||||
|
||||
To continue trying Citadel without installing it, press <b>Cancel</b>.
|
||||
|
||||
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>
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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::<>k::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();
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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: >k::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: >k::Box, first_entry: >k::Entry, second_entry: >k::Entry, status_label: >k::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(); }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,29 @@ clap = { version = "4.5", features = ["cargo", "derive"] }
|
||||
lazy_static = "1.4"
|
||||
serde_derive = "1.0"
|
||||
serde = "1.0"
|
||||
toml = "0.8"
|
||||
toml = "0.9"
|
||||
hex = "0.4"
|
||||
byteorder = "1"
|
||||
dbus = "0.8.4"
|
||||
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"
|
||||
|
||||
@@ -136,8 +136,10 @@ fn compare_boot_partitions(a: Option<Partition>, b: Partition) -> Option<Partiti
|
||||
}
|
||||
|
||||
// Compare versions and channels
|
||||
let a_v = a.metainfo().version();
|
||||
let b_v = b.metainfo().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() {
|
||||
|
||||
949
citadel-tool/src/fetch/client.rs
Normal file
949
citadel-tool/src/fetch/client.rs
Normal 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, ¤t_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(×tamp, "timestamp")?;
|
||||
check_expiry(×tamp.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,
|
||||
¤t_version
|
||||
);
|
||||
if version_gt(&custom.version, ¤t_version) {
|
||||
// Check min_version requirement
|
||||
if let Some(min) = &custom.min_version {
|
||||
if version_gt(min, ¤t_version) {
|
||||
log::warn!("Update for {} available, but current version {} is less than minimum required version {}. Skipping.", component, ¤t_version, min);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Found update for {}: {} -> {}",
|
||||
component,
|
||||
¤t_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()
|
||||
}
|
||||
110
citadel-tool/src/fetch/config.rs
Normal file
110
citadel-tool/src/fetch/config.rs
Normal 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)
|
||||
}
|
||||
210
citadel-tool/src/fetch/keyring.rs
Normal file
210
citadel-tool/src/fetch/keyring.rs
Normal 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])
|
||||
}
|
||||
121
citadel-tool/src/fetch/metadata.rs
Normal file
121
citadel-tool/src/fetch/metadata.rs
Normal 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,
|
||||
}
|
||||
699
citadel-tool/src/fetch/mod.rs
Normal file
699
citadel-tool/src/fetch/mod.rs
Normal 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(())
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
use clap::{command, ArgAction, Command};
|
||||
use clap::{Arg, ArgMatches};
|
||||
use hex;
|
||||
use std::path::Path;
|
||||
use std::process::exit;
|
||||
use clap::{Arg,ArgMatches};
|
||||
use clap::{command, ArgAction, Command};
|
||||
use hex;
|
||||
|
||||
use libcitadel::{Result, ResourceImage, Logger, LogLevel, Partition, KeyPair, ImageHeader, util};
|
||||
use libcitadel::public_key_for_channel;
|
||||
use libcitadel::{util, ImageHeader, KeyPair, Partition, ResourceImage, Result};
|
||||
|
||||
pub fn main() {
|
||||
let matches = command!()
|
||||
@@ -103,7 +104,7 @@ pub fn main() {
|
||||
|
||||
fn info(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = load_image(arg_matches)?;
|
||||
print!("{}",String::from_utf8(img.header().metainfo_bytes())?);
|
||||
print!("{}", String::from_utf8(img.header().metainfo_bytes())?);
|
||||
info_signature(&img)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -113,22 +114,22 @@ fn info_signature(img: &ResourceImage) -> Result<()> {
|
||||
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(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn metainfo(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = load_image(arg_matches)?;
|
||||
print!("{}",String::from_utf8(img.header().metainfo_bytes())?);
|
||||
print!("{}", String::from_utf8(img.header().metainfo_bytes())?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -167,7 +168,8 @@ fn verify_shasum(arg_matches: &ArgMatches) -> Result<()> {
|
||||
}
|
||||
|
||||
fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage> {
|
||||
let path = arg_matches.get_one::<String>("path")
|
||||
let path = arg_matches
|
||||
.get_one::<String>("path")
|
||||
.expect("path argument missing");
|
||||
|
||||
if !Path::new(path).exists() {
|
||||
@@ -183,7 +185,7 @@ fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage> {
|
||||
fn install_rootfs(arg_matches: &ArgMatches) -> Result<()> {
|
||||
if arg_matches.get_flag("choose") {
|
||||
let _ = choose_install_partition(true)?;
|
||||
return Ok(())
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let img = load_image(arg_matches)?;
|
||||
@@ -222,14 +224,18 @@ fn sign_image(arg_matches: &ArgMatches) -> Result<()> {
|
||||
}
|
||||
|
||||
fn install_image(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let source = arg_matches.get_one::<String>("path")
|
||||
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") {
|
||||
bail!("Cannot install image type {}", metainfo.image_type());
|
||||
@@ -250,13 +256,20 @@ fn install_image(arg_matches: &ArgMatches) -> Result<()> {
|
||||
if kernel_version.chars().any(|c| c == '/') {
|
||||
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()) {
|
||||
bail!("Refusing to build path from strange channel name {}", metainfo.channel());
|
||||
bail!(
|
||||
"Refusing to build path from strange channel name {}",
|
||||
metainfo.channel()
|
||||
);
|
||||
}
|
||||
let image_dir = Path::new("/storage/resources").join(metainfo.channel());
|
||||
let image_dest = image_dir.join(filename);
|
||||
|
||||
@@ -56,7 +56,7 @@ pub fn run_cli_install_with<P: AsRef<Path>>(target: P) -> Result<bool> {
|
||||
}
|
||||
|
||||
fn run_install(disk: Disk, citadel_passphrase: String, passphrase: String) -> Result<()> {
|
||||
let mut install = Installer::new(disk.path(), &citadel_passphrase, &passphrase);
|
||||
let mut install = Installer::new(disk.path(), &citadel_passphrase, &passphrase, None);
|
||||
install.set_install_syslinux(true);
|
||||
install.verify()?;
|
||||
install.run()
|
||||
|
||||
@@ -12,6 +12,7 @@ use libcitadel::RealmFS;
|
||||
use libcitadel::Result;
|
||||
use libcitadel::OsRelease;
|
||||
use libcitadel::KeyRing;
|
||||
use libcitadel::ResourceImage;
|
||||
use libcitadel::terminal::Base16Scheme;
|
||||
use libcitadel::UtsName;
|
||||
|
||||
@@ -20,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";
|
||||
@@ -125,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());
|
||||
@@ -141,6 +145,7 @@ impl Installer {
|
||||
target_device,
|
||||
citadel_passphrase,
|
||||
passphrase,
|
||||
timezone,
|
||||
artifact_directory: DEFAULT_ARTIFACT_DIRECTORY.to_string(),
|
||||
logfile: None,
|
||||
}
|
||||
@@ -154,6 +159,7 @@ impl Installer {
|
||||
target_device: None,
|
||||
citadel_passphrase: None,
|
||||
passphrase: None,
|
||||
timezone: None,
|
||||
artifact_directory: DEFAULT_ARTIFACT_DIRECTORY.to_string(),
|
||||
logfile: None,
|
||||
}
|
||||
@@ -224,6 +230,19 @@ impl Installer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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",
|
||||
@@ -461,11 +480,29 @@ impl Installer {
|
||||
}
|
||||
|
||||
fn setup_storage_resources(&self) -> Result<()> {
|
||||
let channel = match OsRelease::citadel_channel() {
|
||||
Some(channel) => channel,
|
||||
None => "dev",
|
||||
// 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)?;
|
||||
@@ -501,7 +538,11 @@ impl Installer {
|
||||
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<()> {
|
||||
|
||||
@@ -1,278 +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 libcitadel::{Result};
|
||||
// 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> {
|
||||
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<()> {
|
||||
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<()> {
|
||||
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) {
|
||||
bail!("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 = ();
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
use libcitadel::Result;
|
||||
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() {
|
||||
if CommandLine::install_mode() {
|
||||
if let Err(e) = run_dbus_server() {
|
||||
warn!("Error: {}", e);
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
println!("Citadel installer backend will only run in install or live mode");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_dbus_server() -> Result<()> {
|
||||
let server = dbus::DbusServer::connect()?;
|
||||
server.start()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
161
citadel-tool/src/install_backend/zbus.rs
Normal file
161
citadel-tool/src/install_backend/zbus.rs
Normal 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(())
|
||||
}
|
||||
@@ -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;
|
||||
@@ -22,7 +26,7 @@ fn main() {
|
||||
Ok(path) => path,
|
||||
Err(_e) => {
|
||||
return;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let args = env::args().collect::<Vec<String>>();
|
||||
@@ -32,13 +36,15 @@ fn main() {
|
||||
} else if exe == Path::new("/usr/libexec/citadel-install") {
|
||||
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();
|
||||
} else if exe == Path::new("/usr/bin/citadel-realmfs") {
|
||||
realmfs::main();
|
||||
realmfs::main(args);
|
||||
} else if exe == Path::new("/usr/bin/citadel-update") {
|
||||
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);
|
||||
} else if exe == Path::new("/usr/libexec/citadel-run") {
|
||||
@@ -58,8 +64,9 @@ fn dispatch_command(args: Vec<String>) {
|
||||
"boot" => boot::main(rebuild_args("citadel-boot", args)),
|
||||
"install" => install::main(rebuild_args("citadel-install", args)),
|
||||
"image" => image::main(),
|
||||
"realmfs" => realmfs::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)),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use libcitadel::util::is_euid_root;
|
||||
use libcitadel::ResizeSize;
|
||||
use std::process::exit;
|
||||
|
||||
pub fn main() {
|
||||
pub fn main(args: Vec<String>) {
|
||||
|
||||
Logger::set_log_level(LogLevel::Debug);
|
||||
|
||||
@@ -65,11 +65,7 @@ is the final absolute size of the image.")
|
||||
.help("Path or name of RealmFS image to deactivate")
|
||||
.required(true)))
|
||||
|
||||
|
||||
.arg(Arg::new("image")
|
||||
.help("Name of or path to RealmFS image to display information about")
|
||||
.required(true))
|
||||
.get_matches();
|
||||
.get_matches_from(args);
|
||||
|
||||
let result = match matches.subcommand() {
|
||||
Some(("resize", m)) => resize(m),
|
||||
|
||||
@@ -93,7 +93,7 @@ fn create_tmp_copy(path: &Path) -> Result<PathBuf> {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn install_image(path: &Path, flags: u32) -> Result<()> {
|
||||
pub fn install_image(path: &Path, flags: u32) -> Result<()> {
|
||||
if !path.exists() || path.file_name().is_none() {
|
||||
bail!("file path {} does not exist", path.display());
|
||||
}
|
||||
@@ -140,7 +140,7 @@ fn prepare_image(image: &ResourceImage, flags: u32) -> Result<()> {
|
||||
}
|
||||
|
||||
fn install_extra_image(image: &ResourceImage) -> Result<()> {
|
||||
let filename = format!("citadel-extra-{:03}.img", image.header().metainfo().version());
|
||||
let filename = format!("citadel-extra-{}.img", image.header().metainfo().version());
|
||||
install_image_file(image, filename.as_str())?;
|
||||
remove_old_extra_images(image)?;
|
||||
Ok(())
|
||||
@@ -186,7 +186,7 @@ fn install_kernel_image(image: &mut ResourceImage) -> Result<()> {
|
||||
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()?;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Name=RealmConfig
|
||||
Type=Application
|
||||
Icon=org.gnome.Settings
|
||||
NoDisplay=true
|
||||
@@ -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"
|
||||
|
||||
@@ -20,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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,7 @@ pub mod symlink;
|
||||
mod realm;
|
||||
pub mod terminal;
|
||||
mod system;
|
||||
pub mod updates;
|
||||
|
||||
pub mod flatpak;
|
||||
|
||||
@@ -34,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;
|
||||
@@ -52,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};
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -266,12 +266,28 @@ 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
|
||||
///
|
||||
@@ -279,6 +295,14 @@ impl RealmConfig {
|
||||
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.
|
||||
///
|
||||
@@ -287,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
|
||||
@@ -301,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:
|
||||
@@ -321,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> {
|
||||
@@ -343,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 {
|
||||
@@ -351,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 {
|
||||
@@ -369,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> {
|
||||
@@ -435,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())
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
|
||||
@@ -235,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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
200
libcitadel/src/realm/liveconfig.rs
Normal file
200
libcitadel/src/realm/liveconfig.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -194,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)
|
||||
@@ -292,6 +298,7 @@ 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) {
|
||||
@@ -300,8 +307,12 @@ impl RealmManager {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -113,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)
|
||||
}
|
||||
|
||||
|
||||
174
libcitadel/src/realmfs/launcher.rs
Normal file
174
libcitadel/src/realmfs/launcher.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +81,9 @@ 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())?;
|
||||
|
||||
Ok(())
|
||||
@@ -98,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)?;
|
||||
@@ -169,6 +186,7 @@ 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();
|
||||
|
||||
@@ -187,6 +205,8 @@ impl <'a> Update<'a> {
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
info!("Resizing target file to {} blocks", nblocks);
|
||||
|
||||
if nblocks < self.metainfo_nblock_size() {
|
||||
bail!("Cannot shrink image")
|
||||
}
|
||||
@@ -197,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()) {
|
||||
@@ -265,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");
|
||||
@@ -296,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()?;
|
||||
@@ -345,7 +418,7 @@ impl <'a> Update<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl <'a> Drop for Update<'a> {
|
||||
impl Drop for RealmFSUpdate {
|
||||
fn drop(&mut self) {
|
||||
self.cleanup();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
183
libcitadel/src/updates.rs
Normal 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(())
|
||||
}
|
||||
@@ -48,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() {
|
||||
@@ -404,4 +410,4 @@ pub fn drop_privileges(uid: u32, gid: u32) -> Result<()> {
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"] }
|
||||
@@ -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>
|
||||
@@ -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: >k::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
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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(¤t));
|
||||
}
|
||||
|
||||
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 {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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: >k::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)
|
||||
}
|
||||
}
|
||||
@@ -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: >k::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: >k::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: >k::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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="NewRealmDialog" parent="GtkDialog">
|
||||
<property name="title">Create New Realm</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
|
||||
<!-- GtkInfoBar -->
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="infobar">
|
||||
<property name="revealed">False</property>
|
||||
<property name="message-type">warning</property>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<child>
|
||||
<object class="GtkLabel" id="infolabel">
|
||||
<property name="label">Name already exists</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<!-- GtkLabel -->
|
||||
<child>
|
||||
<object class="GtkLabel" id="label">
|
||||
<property name="label">Enter name for new realm:</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-start">20</property>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<!-- GtkEntry-->
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<child>
|
||||
<object class="GtkEntry" id="entry">
|
||||
<property name="hexpand">True</property>
|
||||
<property name="placeholder-text">Enter name of new realm</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">20</property>
|
||||
<property name="margin-start">20</property>
|
||||
<property name="margin-end">5</property>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<!-- GtkButton -->
|
||||
<child>
|
||||
<object class="GtkButton" id="config-button">
|
||||
<property name="sensitive">False</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">20</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">20</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">emblem-system-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</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">Create</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>
|
||||
</interface>
|
||||
@@ -1,76 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
|
||||
use libcitadel::Realm;
|
||||
|
||||
use crate::new_realm::dialog::NewRealmDialog;
|
||||
|
||||
pub struct RealmNameVerifier {
|
||||
ok: gtk::Widget,
|
||||
infobar: gtk::InfoBar,
|
||||
infolabel: gtk::Label,
|
||||
label: gtk::Label,
|
||||
config: gtk::Button,
|
||||
realms: Rc<RefCell<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl RealmNameVerifier {
|
||||
pub fn new(dialog: &NewRealmDialog) -> Self {
|
||||
let ok = dialog.instance().widget_for_response(gtk::ResponseType::Ok).expect("No Ok Widget found");
|
||||
RealmNameVerifier {
|
||||
ok,
|
||||
infobar: dialog.infobar.clone(),
|
||||
infolabel: dialog.infolabel.clone(),
|
||||
label: dialog.label.clone(),
|
||||
config: dialog.config_button.clone(),
|
||||
realms: dialog.realm_names.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_insert(&self, entry: >k::Entry, text: &str, pos: i32) -> bool {
|
||||
let mut s = entry.text().to_string();
|
||||
s.insert_str(pos as usize, text);
|
||||
Realm::is_valid_name(&s)
|
||||
}
|
||||
|
||||
pub fn verify_delete(&self, entry: >k::Entry, start: i32, end: i32) -> bool {
|
||||
let mut s = entry.text().to_string();
|
||||
let start = start as usize;
|
||||
let end = end as usize;
|
||||
s.replace_range(start..end, "");
|
||||
s.is_empty() || Realm::is_valid_name(&s)
|
||||
}
|
||||
|
||||
fn verify_name (&self, name: &String) -> bool {
|
||||
if self.realms.borrow().contains(name) {
|
||||
self.infolabel.set_markup(&format!("Realm already exists with name <b>realm-{}</b>", name));
|
||||
self.infobar.set_revealed(true);
|
||||
false
|
||||
} else {
|
||||
self.infobar.set_revealed(false);
|
||||
self.infolabel.set_markup("");
|
||||
!name.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn changed(&self, entry: >k::Entry) {
|
||||
let s = entry.text().to_string();
|
||||
|
||||
if self.verify_name(&s) {
|
||||
self.ok.set_sensitive(true);
|
||||
self.config.set_sensitive(true);
|
||||
self.label.set_markup(&format!("<b>realm-{}</b>", s));
|
||||
} else {
|
||||
self.ok.set_sensitive(false);
|
||||
self.config.set_sensitive(false);
|
||||
if s.is_empty() {
|
||||
self.label.set_markup("Enter name for new realm:");
|
||||
} else {
|
||||
self.label.set_markup("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use zbus::dbus_proxy;
|
||||
use zvariant::derive::Type;
|
||||
use serde::{Serialize,Deserialize};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
#[derive(Deserialize,Serialize,Type)]
|
||||
pub struct RealmItem {
|
||||
name: String,
|
||||
description: String,
|
||||
realmfs: String,
|
||||
namespace: u64,
|
||||
status: u8,
|
||||
}
|
||||
|
||||
impl RealmItem {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone)]
|
||||
pub struct RealmConfig {
|
||||
options: Rc<HashMap<String,String>>,
|
||||
realmfs_list: Rc<Vec<String>>,
|
||||
}
|
||||
|
||||
impl RealmConfig {
|
||||
pub fn new_default(realmfs_list: Vec<String>) -> Self {
|
||||
let config = libcitadel::RealmConfig::default();
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert("use-gpu".to_string(), config.gpu().to_string());
|
||||
vars.insert("use-wayland".to_string(), config.wayland().to_string());
|
||||
vars.insert("use-x11".to_string(), config.x11().to_string());
|
||||
vars.insert("use-sound".to_string(), config.sound().to_string());
|
||||
vars.insert("use-shared-dir".to_string(), config.shared_dir().to_string());
|
||||
vars.insert("use-network".to_string(), config.network().to_string());
|
||||
vars.insert("use-kvm".to_string(), config.kvm().to_string());
|
||||
vars.insert("use-ephemeral-home".to_string(), config.ephemeral_home().to_string());
|
||||
|
||||
if realmfs_list.contains(&String::from("main")) {
|
||||
vars.insert("realmfs".to_string(), String::from("main"));
|
||||
} else if let Some(first) = realmfs_list.first() {
|
||||
vars.insert("realmfs".to_string(), first.clone());
|
||||
}
|
||||
Self::new(vars, realmfs_list)
|
||||
}
|
||||
|
||||
fn new(options: HashMap<String, String>, realmfs_list: Vec<String>) -> Self {
|
||||
RealmConfig {
|
||||
options: Rc::new(options),
|
||||
realmfs_list: Rc::new(realmfs_list),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_string(&self, id: &str) -> Option<&str> {
|
||||
self.options.get(id).map(|s| s.as_str())
|
||||
}
|
||||
|
||||
fn parse_bool(val: &str) -> bool {
|
||||
match val.parse::<bool>() {
|
||||
Ok(v) => v,
|
||||
_ => {
|
||||
warn!("Failed to parse value '{}' as bool", val);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_bool(&self, id: &str) -> bool {
|
||||
match self.get_string(id) {
|
||||
Some(val) => Self::parse_bool(val),
|
||||
None => {
|
||||
warn!("No value found for option '{}'", id);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn realmfs_list(&self) -> &[String] {
|
||||
&self.realmfs_list
|
||||
}
|
||||
}
|
||||
|
||||
#[dbus_proxy(
|
||||
default_service = "com.subgraph.realms",
|
||||
interface = "com.subgraph.realms.Manager",
|
||||
default_path = "/com/subgraph/realms"
|
||||
)]
|
||||
pub trait RealmsManager {
|
||||
fn get_current(&self) -> zbus::Result<String>;
|
||||
fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) -> zbus::Result<()>;
|
||||
fn list(&self) -> zbus::Result<Vec<RealmItem>>;
|
||||
fn realm_config(&self, name: &str) -> zbus::Result<HashMap<String,String>>;
|
||||
fn realm_exists(&self, name: &str) -> zbus::Result<bool>;
|
||||
fn list_realm_f_s(&self) -> zbus::Result<Vec<String>>;
|
||||
fn create_realm(&self, name: &str) -> zbus::Result<bool>;
|
||||
}
|
||||
|
||||
impl RealmsManagerProxy<'_> {
|
||||
pub fn connect() -> Result<Self> {
|
||||
let connection = zbus::Connection::new_system()?;
|
||||
|
||||
let proxy = RealmsManagerProxy::new(&connection)
|
||||
.map_err(|_| Error::ManagerConnect)?;
|
||||
|
||||
// Test connection
|
||||
proxy.get_current().map_err(|_| Error::ManagerConnect)?;
|
||||
|
||||
Ok(proxy)
|
||||
}
|
||||
|
||||
pub fn realm_names(&self) -> Result<Vec<String>> {
|
||||
let realms = self.list()?;
|
||||
let names = realms.iter()
|
||||
.map(|r| r.name().to_string())
|
||||
.collect();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
pub fn default_config(&self) -> Result<RealmConfig> {
|
||||
let realmfs_list = self.list_realm_f_s()?;
|
||||
Ok(RealmConfig::new_default(realmfs_list))
|
||||
}
|
||||
|
||||
pub fn config(&self, realm: &str) -> Result<RealmConfig> {
|
||||
if !self.realm_exists(realm)? {
|
||||
return Err(Error::NoSuchRealm(realm.to_string()));
|
||||
}
|
||||
|
||||
let options = self.realm_config(realm)?;
|
||||
let realmfs_list = self.list_realm_f_s()?;
|
||||
Ok(RealmConfig::new(options, realmfs_list))
|
||||
}
|
||||
|
||||
pub fn configure_realm(&self, realm: &str, config: Vec<(String, String)>) -> Result<()> {
|
||||
self.realm_set_config(realm, config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_new_realm(&self, realm: &str, config: Vec<(String, String)>) -> Result<()> {
|
||||
if self.create_realm(realm)? {
|
||||
if !config.is_empty() {
|
||||
self.realm_set_config(realm, config)?;
|
||||
}
|
||||
} else {
|
||||
return Err(Error::CreateRealmFailed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,10 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
zbus = "=2.0.0-beta.5"
|
||||
zvariant = "2.7.0"
|
||||
async-io = "2.3.2"
|
||||
blocking = "1.6.1"
|
||||
event-listener = "5.3.1"
|
||||
zbus = "5.7.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_repr = "0.1.8"
|
||||
serde_repr = "0.1.20"
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use zbus::{Connection, ObjectServer};
|
||||
use async_io::block_on;
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use crate::realms_manager::{RealmsManagerServer, REALMS_SERVER_OBJECT_PATH, realm_status};
|
||||
use libcitadel::{RealmEvent, Realm};
|
||||
|
||||
pub struct EventHandler {
|
||||
connection: Connection,
|
||||
realms_server: RealmsManagerServer,
|
||||
}
|
||||
|
||||
impl EventHandler {
|
||||
pub fn new(connection: Connection, realms_server: RealmsManagerServer) -> Self {
|
||||
EventHandler { connection, realms_server }
|
||||
pub fn new(connection: Connection) -> Self {
|
||||
EventHandler { connection }
|
||||
}
|
||||
|
||||
pub fn handle_event(&self, ev: &RealmEvent) {
|
||||
@@ -25,44 +26,49 @@ impl EventHandler {
|
||||
RealmEvent::New(realm) => self.on_new(realm),
|
||||
RealmEvent::Removed(realm) => self.on_removed(realm),
|
||||
RealmEvent::Current(realm) => self.on_current(realm.as_ref()),
|
||||
RealmEvent::Starting(_) => Ok(()),
|
||||
RealmEvent::Stopping(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_server<F>(&self, func: F) -> zbus::Result<()>
|
||||
fn with_signal_emitter<F>(&self, func: F) -> zbus::Result<()>
|
||||
where
|
||||
F: Fn(&RealmsManagerServer) -> zbus::Result<()>,
|
||||
F: Fn(&SignalEmitter) -> zbus::Result<()>,
|
||||
{
|
||||
let mut object_server = ObjectServer::new(&self.connection);
|
||||
object_server.at(REALMS_SERVER_OBJECT_PATH, self.realms_server.clone())?;
|
||||
object_server.with(REALMS_SERVER_OBJECT_PATH, |iface: &RealmsManagerServer| func(iface))
|
||||
let object_server = self.connection.object_server();
|
||||
let iface = object_server.interface::<_, RealmsManagerServer>(REALMS_SERVER_OBJECT_PATH)?;
|
||||
|
||||
let emitter = iface.signal_emitter();
|
||||
func(emitter)
|
||||
}
|
||||
|
||||
fn on_started(&self, realm: &Realm) -> zbus::Result<()> {
|
||||
let pid_ns = realm.pid_ns().unwrap_or(0);
|
||||
let status = realm_status(realm);
|
||||
self.with_server(|server| server.realm_started(realm.name(), pid_ns, status))
|
||||
self.with_signal_emitter(|ctx| block_on(RealmsManagerServer::realm_started(ctx, realm.name(), pid_ns, status)))
|
||||
}
|
||||
|
||||
fn on_stopped(&self, realm: &Realm) -> zbus::Result<()> {
|
||||
let status = realm_status(realm);
|
||||
self.with_server(|server| server.realm_stopped(realm.name(), status))
|
||||
self.with_signal_emitter(|ctx| block_on(RealmsManagerServer::realm_stopped(ctx, realm.name(), status)))
|
||||
}
|
||||
|
||||
fn on_new(&self, realm: &Realm) -> zbus::Result<()> {
|
||||
let status = realm_status(realm);
|
||||
let description = realm.notes().unwrap_or(String::new());
|
||||
self.with_server(|server| server.realm_new(realm.name(), &description, status))
|
||||
self.with_signal_emitter(|ctx| block_on(RealmsManagerServer::realm_new(ctx, realm.name(), &description, status)))
|
||||
}
|
||||
|
||||
fn on_removed(&self, realm: &Realm) -> zbus::Result<()> {
|
||||
self.with_server(|server| server.realm_removed(realm.name()))
|
||||
self.with_signal_emitter(|ctx| block_on(RealmsManagerServer::realm_removed(ctx, realm.name())))
|
||||
}
|
||||
|
||||
fn on_current(&self, realm: Option<&Realm>) -> zbus::Result<()> {
|
||||
self.with_server(|server| {
|
||||
self.with_signal_emitter(|ctx| {
|
||||
match realm {
|
||||
Some(realm) => server.realm_current(realm.name(), realm_status(realm)),
|
||||
None => server.realm_current("", 0),
|
||||
Some(realm) => block_on(RealmsManagerServer::realm_current(ctx, realm.name(), realm_status(realm))),
|
||||
None => block_on(RealmsManagerServer::realm_current(ctx, "", 0)),
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
#[macro_use] extern crate libcitadel;
|
||||
|
||||
use zbus::{Connection, fdo};
|
||||
|
||||
use libcitadel::{Logger, LogLevel, Result};
|
||||
|
||||
use crate::realms_manager::RealmsManagerServer;
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use event_listener::{Event, Listener};
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::fdo::ObjectManager;
|
||||
use libcitadel::{Logger, LogLevel, Result, RealmManager};
|
||||
use crate::next::{RealmsManagerServer2, REALMS2_SERVER_OBJECT_PATH};
|
||||
use crate::realms_manager::{RealmsManagerServer, REALMS_SERVER_OBJECT_PATH};
|
||||
|
||||
mod realms_manager;
|
||||
mod events;
|
||||
|
||||
mod next;
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = run_realm_manager() {
|
||||
@@ -16,24 +20,43 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_system_connection() -> zbus::Result<Connection> {
|
||||
let connection = zbus::Connection::new_system()?;
|
||||
fdo::DBusProxy::new(&connection)?.request_name("com.subgraph.realms", fdo::RequestNameFlags::AllowReplacement.into())?;
|
||||
Ok(connection)
|
||||
fn register_realms_manager_server(connection: &Connection, realm_manager: &Arc<RealmManager>, quit_event: &Arc<Event>) -> Result<()> {
|
||||
let server = RealmsManagerServer::load(&connection, realm_manager.clone(), quit_event.clone())
|
||||
.map_err(context!("Loading realms server"))?;
|
||||
connection.object_server().at(REALMS_SERVER_OBJECT_PATH, server).map_err(context!("registering realms manager object"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_realms2_manager_server(connection: &Connection, realm_manager: &Arc<RealmManager>, quit_event: &Arc<Event>) -> Result<()> {
|
||||
let server2 = RealmsManagerServer2::load(&connection, realm_manager.clone(), quit_event.clone())
|
||||
.map_err(context!("Loading realms2 server"))?;
|
||||
connection.object_server().at(REALMS2_SERVER_OBJECT_PATH, server2).map_err(context!("registering realms manager object"))?;
|
||||
connection.object_server().at(REALMS2_SERVER_OBJECT_PATH, ObjectManager).map_err(context!("registering ObjectManager"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_realm_manager() -> Result<()> {
|
||||
Logger::set_log_level(LogLevel::Verbose);
|
||||
|
||||
let connection = create_system_connection()
|
||||
.map_err(context!("ZBus Connection error"))?;
|
||||
let testing = env::args().skip(1).any(|s| s == "--testing");
|
||||
|
||||
let mut object_server = RealmsManagerServer::register(&connection)?;
|
||||
let connection = Connection::system()
|
||||
.map_err(context!("ZBus Connection error"))?;
|
||||
|
||||
loop {
|
||||
if let Err(err) = object_server.try_handle_next() {
|
||||
warn!("Error handling DBus message: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let realm_manager = RealmManager::load()?;
|
||||
let quit_event = Arc::new(Event::new());
|
||||
|
||||
if testing {
|
||||
register_realms2_manager_server(&connection, &realm_manager, &quit_event)?;
|
||||
connection.request_name("com.subgraph.Realms2")
|
||||
.map_err(context!("acquiring realms manager name"))?;
|
||||
} else {
|
||||
register_realms_manager_server(&connection, &realm_manager, &quit_event)?;
|
||||
register_realms2_manager_server(&connection, &realm_manager, &quit_event)?;
|
||||
connection.request_name("com.subgraph.realms")
|
||||
.map_err(context!("acquiring realms manager name"))?;
|
||||
};
|
||||
quit_event.listen().wait();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
227
realmsd/src/next/config.rs
Normal file
227
realmsd/src/next/config.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zbus::fdo;
|
||||
use libcitadel::{LiveConfig, OverlayType, Realm, GLOBAL_CONFIG};
|
||||
use libcitadel::terminal::Base16Scheme;
|
||||
use zbus::zvariant::Type;
|
||||
use crate::next::manager::failed;
|
||||
|
||||
const BOOL_CONFIG_VARS: &[&str] = &[
|
||||
"use-gpu", "use-gpu-card0", "use-wayland", "use-x11", "use-sound",
|
||||
"use-shared-dir", "use-network", "use-kvm", "use-ephemeral-home",
|
||||
"use-media-dir", "use-fuse", "use-flatpak",
|
||||
];
|
||||
|
||||
fn is_bool_config_variable(variable: &str) -> bool {
|
||||
BOOL_CONFIG_VARS.iter().any(|&s| s == variable)
|
||||
}
|
||||
|
||||
fn add_boolean_vars(config: &libcitadel::RealmConfig, vars: &mut RealmConfigVars) {
|
||||
vars.add_bool("use-gpu", config.gpu());
|
||||
vars.add_bool("use-gpu-card0", config.gpu_card0());
|
||||
vars.add_bool("use-wayland", config.wayland());
|
||||
vars.add_bool("use-x11", config.x11());
|
||||
vars.add_bool("use-sound", config.sound());
|
||||
vars.add_bool("use-shared-dir", config.shared_dir());
|
||||
vars.add_bool("use-network", config.network());
|
||||
vars.add_bool("use-kvm", config.kvm());
|
||||
vars.add_bool("use-ephemeral-home", config.ephemeral_home());
|
||||
vars.add_bool("use-media-dir", config.media_dir());
|
||||
vars.add_bool("use-fuse", config.fuse());
|
||||
vars.add_bool("use-flatpak", config.flatpak());
|
||||
}
|
||||
|
||||
fn set_boolean_config_var(config: &mut libcitadel::RealmConfig, var: &str, value: bool) -> bool {
|
||||
match var {
|
||||
"use-gpu" => config.set_gpu(value),
|
||||
"use-gpu-card0" => config.set_gpu_card0(value),
|
||||
"use-wayland" => config.set_wayland(value),
|
||||
"use-x11" => config.set_x11(value),
|
||||
"use-sound" => config.set_sound(value),
|
||||
"use-shared-dir" => config.set_shared_dir(value),
|
||||
"use-network" => config.set_network(value),
|
||||
"use-kvm" => config.set_kvm(value),
|
||||
"use-ephemeral-home" => config.set_ephemeral_home(value),
|
||||
"use-media-dir" => config.set_media_dir(value),
|
||||
"use-fuse" => config.set_fuse(value),
|
||||
"use-flatpak" => config.set_flatpak(value),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize,Serialize,Type)]
|
||||
pub struct RealmConfigVars {
|
||||
items: HashMap<String,String>,
|
||||
}
|
||||
|
||||
impl RealmConfigVars {
|
||||
fn new() -> Self {
|
||||
RealmConfigVars { items: HashMap::new() }
|
||||
}
|
||||
|
||||
pub fn new_global() -> Self {
|
||||
Self::new_from_config(&GLOBAL_CONFIG)
|
||||
}
|
||||
|
||||
fn new_from_realm(realm: &Realm) -> Self {
|
||||
let config = realm.config();
|
||||
Self::new_from_config(&config)
|
||||
}
|
||||
|
||||
fn new_from_config(config: &libcitadel::RealmConfig) -> Self {
|
||||
let mut vars = RealmConfigVars::new();
|
||||
add_boolean_vars(config, &mut vars);
|
||||
|
||||
let overlay = match config.overlay() {
|
||||
OverlayType::None => "none",
|
||||
OverlayType::TmpFS => "tmpfs",
|
||||
OverlayType::Storage => "storage",
|
||||
};
|
||||
vars.add("overlay", overlay);
|
||||
|
||||
let scheme = match config.terminal_scheme() {
|
||||
Some(name) => name.to_string(),
|
||||
None => String::new(),
|
||||
};
|
||||
vars.add("terminal-scheme", scheme);
|
||||
vars.add("realmfs", config.realmfs());
|
||||
vars
|
||||
}
|
||||
|
||||
fn add_bool(&mut self, name: &str, val: bool) {
|
||||
let valstr = if val { "true".to_string() } else { "false".to_string() };
|
||||
self.add(name, valstr);
|
||||
}
|
||||
|
||||
fn add<S,T>(&mut self, k: S, v: T) where S: Into<String>, T: Into<String> {
|
||||
self.items.insert(k.into(), v.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealmConfig {
|
||||
realm: Realm,
|
||||
changed: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl RealmConfig {
|
||||
|
||||
pub fn new(realm: Realm) -> Self {
|
||||
let changed = Arc::new(AtomicBool::new(false));
|
||||
RealmConfig { realm, changed }
|
||||
}
|
||||
|
||||
fn mark_changed(&self) {
|
||||
self.changed.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn is_changed(&self) -> bool {
|
||||
self.changed.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn config_vars(&self) -> RealmConfigVars {
|
||||
RealmConfigVars::new_from_realm(&self.realm)
|
||||
}
|
||||
|
||||
fn set_bool_var(&mut self, var: &str, value: &str) -> fdo::Result<()> {
|
||||
let v = match value {
|
||||
"true" => true,
|
||||
"false" => false,
|
||||
_ => return failed(format!("Invalid boolean value '{}' for realm config variable '{}'", value, var)),
|
||||
};
|
||||
|
||||
let mut has_changed = true;
|
||||
self.realm.with_mut_config(|c| {
|
||||
has_changed = set_boolean_config_var(c, var, v);
|
||||
});
|
||||
if has_changed {
|
||||
self.mark_changed();
|
||||
if self.realm.is_active() && LiveConfig::is_live_configurable(var) {
|
||||
let lc = LiveConfig::new(&self.realm);
|
||||
if let Err(err) = lc.configure(var, v) {
|
||||
warn!("Error live setting {} = {}: {}", var, value, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_overlay(&mut self, value: &str) -> fdo::Result<()> {
|
||||
let val = match value {
|
||||
"tmpfs" => Some("tmpfs".to_string()),
|
||||
"storage" => Some("storage".to_string()),
|
||||
"none" => None,
|
||||
_ => return failed(format!("Invalid value '{}' for overlay config", value)),
|
||||
};
|
||||
if self.realm.config().overlay != val {
|
||||
self.realm.with_mut_config(|c| {
|
||||
c.overlay = Some(value.to_string());
|
||||
});
|
||||
self.mark_changed();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_terminal_scheme(&mut self, value: &str) -> fdo::Result<()> {
|
||||
if Some(value) == self.realm.config().terminal_scheme() {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
let scheme = match Base16Scheme::by_name(value) {
|
||||
Some(scheme) => scheme,
|
||||
None => return failed(format!("Invalid terminal color scheme '{}'", value)),
|
||||
};
|
||||
|
||||
let manager = self.realm.manager();
|
||||
if let Err(err) = scheme.apply_to_realm(&manager, &self.realm) {
|
||||
return failed(format!("Error applying terminal color scheme: {}", err));
|
||||
}
|
||||
|
||||
self.realm.with_mut_config(|c| {
|
||||
c.terminal_scheme = Some(value.to_string());
|
||||
});
|
||||
self.mark_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_realmfs(&mut self, value: &str) -> fdo::Result<()> {
|
||||
let manager = self.realm.manager();
|
||||
if manager.realmfs_by_name(value).is_none() {
|
||||
return failed(format!("Failed to set 'realmfs' config for realm-{}: RealmFS named '{}' does not exist", self.realm.name(), value));
|
||||
}
|
||||
if self.realm.config().realmfs() != value {
|
||||
self.realm.with_mut_config(|c| {
|
||||
c.realmfs = Some(value.to_string())
|
||||
});
|
||||
self.mark_changed();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_config(&self) -> fdo::Result<()> {
|
||||
if self.is_changed() {
|
||||
self.realm.config()
|
||||
.write()
|
||||
.map_err(|err| fdo::Error::Failed(format!("Error writing config file for realm-{}: {}", self.realm.name(), err)))?;
|
||||
|
||||
self.changed.store(false, Ordering::Relaxed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_var(&mut self, var: &str, value: &str) -> fdo::Result<()> {
|
||||
if is_bool_config_variable(var) {
|
||||
self.set_bool_var(var, value)
|
||||
} else if var == "overlay" {
|
||||
self.set_overlay(value)
|
||||
} else if var == "terminal-scheme" {
|
||||
self.set_terminal_scheme(value)
|
||||
} else if var == "realmfs" {
|
||||
self.set_realmfs(value)
|
||||
} else {
|
||||
failed(format!("Unknown realm configuration variable '{}'", var))
|
||||
}
|
||||
}
|
||||
}
|
||||
172
realmsd/src/next/manager.rs
Normal file
172
realmsd/src/next/manager.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use blocking::unblock;
|
||||
use event_listener::Event;
|
||||
use serde::Serialize;
|
||||
use serde_repr::Serialize_repr;
|
||||
use zbus::blocking::fdo::DBusProxy;
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::names::BusName;
|
||||
use zbus::zvariant::{ObjectPath, Type};
|
||||
use zbus::{fdo, interface};
|
||||
use libcitadel::{PidLookupResult, Realm, RealmManager};
|
||||
use libcitadel::terminal::Base16Scheme;
|
||||
use crate::next::config::RealmConfigVars;
|
||||
use crate::next::state::RealmsManagerState;
|
||||
|
||||
pub fn failed<T>(message: String) -> fdo::Result<T> {
|
||||
Err(fdo::Error::Failed(message))
|
||||
}
|
||||
|
||||
#[derive(Serialize_repr, Type, Debug, PartialEq)]
|
||||
#[repr(u32)]
|
||||
pub enum PidLookupResultCode {
|
||||
Unknown = 1,
|
||||
Realm = 2,
|
||||
Citadel = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Type, Serialize)]
|
||||
pub struct RealmFromCitadelPid {
|
||||
code: PidLookupResultCode,
|
||||
realm: String,
|
||||
}
|
||||
|
||||
impl From<PidLookupResult> for RealmFromCitadelPid {
|
||||
fn from(result: PidLookupResult) -> Self {
|
||||
match result {
|
||||
PidLookupResult::Unknown => RealmFromCitadelPid { code: PidLookupResultCode::Unknown, realm: String::new() },
|
||||
PidLookupResult::Realm(realm) => RealmFromCitadelPid { code: PidLookupResultCode::Realm, realm: realm.name().to_string() },
|
||||
PidLookupResult::Citadel => RealmFromCitadelPid { code: PidLookupResultCode::Citadel, realm: String::new() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealmsManagerServer2 {
|
||||
state: RealmsManagerState,
|
||||
manager: Arc<RealmManager>,
|
||||
quit_event: Arc<Event>,
|
||||
}
|
||||
|
||||
|
||||
impl RealmsManagerServer2 {
|
||||
|
||||
fn new(connection: Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> Self {
|
||||
let state = RealmsManagerState::new(connection.clone());
|
||||
RealmsManagerServer2 {
|
||||
state,
|
||||
manager,
|
||||
quit_event,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn name_owner_changed_loop(&self, connection: &Connection) -> zbus::Result<()> {
|
||||
let dbus = DBusProxy::new(connection)?;
|
||||
|
||||
for sig in dbus.receive_name_owner_changed()? {
|
||||
let args = sig.args()?;
|
||||
match &args.name {
|
||||
BusName::Unique(unique_name) if args.new_owner().is_none() => {
|
||||
self.state.client_disconnected(unique_name);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn listen_name_owner_changed(&self, connection: &Connection) {
|
||||
let connection = connection.clone();
|
||||
let server = self.clone();
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = server.name_owner_changed_loop(&connection) {
|
||||
warn!("error: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn load(connection: &Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> zbus::Result<Self> {
|
||||
let server = Self::new(connection.clone(), manager.clone(), quit_event);
|
||||
server.state.load(&manager)?;
|
||||
server.listen_name_owner_changed(connection);
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
fn setup_new_realm(manager: &RealmManager, realm: Realm, realmfs_name: &str) {
|
||||
if let Some(realmfs) = manager.realmfs_by_name(&realmfs_name) {
|
||||
realm.with_mut_config(|c| c.realmfs = Some(realmfs.name().to_string()));
|
||||
} else {
|
||||
warn!("Cannot set RealmFS '{}' on realm because it does not exist", realmfs_name);
|
||||
}
|
||||
let config = realm.config();
|
||||
if let Err(err) = config.write() {
|
||||
warn!("error writing config file for new realm: {}", err);
|
||||
}
|
||||
let scheme_name = config.terminal_scheme().unwrap_or("default-dark");
|
||||
if let Some(scheme) = Base16Scheme::by_name(scheme_name) {
|
||||
if let Err(e) = scheme.apply_to_realm(&manager, &realm) {
|
||||
warn!("error writing scheme files: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[interface(name = "com.subgraph.realms.Manager2")]
|
||||
impl RealmsManagerServer2 {
|
||||
|
||||
async fn get_current(&self) -> u32 {
|
||||
|
||||
self.state.get_current()
|
||||
.map(|r| r.index())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
async fn realm_from_citadel_pid(&self, pid: u32) -> RealmFromCitadelPid {
|
||||
let manager = self.manager.clone();
|
||||
unblock(move || {
|
||||
manager.realm_by_pid(pid).into()
|
||||
}).await
|
||||
}
|
||||
|
||||
async fn create_realm(&self, name: &str, realmfs: &str) -> fdo::Result<()> {
|
||||
let manager = self.manager.clone();
|
||||
let name = name.to_string();
|
||||
let realmfs_name = realmfs.to_string();
|
||||
unblock(move || {
|
||||
let realm = manager.new_realm(&name).map_err(|err| fdo::Error::Failed(err.to_string()))?;
|
||||
RealmsManagerServer2::setup_new_realm(&manager, realm, &realmfs_name);
|
||||
Ok(())
|
||||
}).await
|
||||
}
|
||||
|
||||
async fn remove_realm(&self, name: &str, save_home: bool) -> fdo::Result<()> {
|
||||
let manager = self.manager.clone();
|
||||
let realm = match manager.realm_by_name(name) {
|
||||
Some(realm) => realm,
|
||||
None => return Err(fdo::Error::Failed(format!("No realm named {} exists", name))),
|
||||
};
|
||||
|
||||
unblock(move || {
|
||||
manager.delete_realm(&realm, save_home)
|
||||
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
|
||||
Ok(())
|
||||
}).await
|
||||
}
|
||||
|
||||
async fn fork_realmfs(&self, name: &str, new_name: &str) -> fdo::Result<ObjectPath<'_>> {
|
||||
let state = self.state.clone();
|
||||
let name = name.to_string();
|
||||
let new_name = new_name.to_string();
|
||||
unblock(move || {
|
||||
state.fork_realmfs(&name, &new_name)
|
||||
}).await
|
||||
}
|
||||
|
||||
async fn get_global_config(&self) -> RealmConfigVars {
|
||||
RealmConfigVars::new_global()
|
||||
}
|
||||
}
|
||||
10
realmsd/src/next/mod.rs
Normal file
10
realmsd/src/next/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
mod manager;
|
||||
mod config;
|
||||
mod realm;
|
||||
mod realmfs;
|
||||
|
||||
mod state;
|
||||
|
||||
pub use manager::RealmsManagerServer2;
|
||||
pub const REALMS2_SERVER_OBJECT_PATH: &str = "/com/subgraph/Realms2";
|
||||
248
realmsd/src/next/realm.rs
Normal file
248
realmsd/src/next/realm.rs
Normal file
@@ -0,0 +1,248 @@
|
||||
use std::collections::HashMap;
|
||||
use crate::next::config::{RealmConfig, RealmConfigVars};
|
||||
use crate::next::REALMS2_SERVER_OBJECT_PATH;
|
||||
use blocking::unblock;
|
||||
use libcitadel::Realm;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use async_io::block_on;
|
||||
use zbus::{fdo, interface, Connection};
|
||||
use zbus::names::{BusName, InterfaceName};
|
||||
use zbus::zvariant::Value;
|
||||
use crate::next::state::RealmFSNameToId;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealmItem {
|
||||
path: String,
|
||||
index: u32,
|
||||
realm: Realm,
|
||||
config: RealmConfig,
|
||||
in_run_transition: Arc<AtomicBool>,
|
||||
realmfs_index: Arc<AtomicU32>,
|
||||
realmfs_name_to_id: RealmFSNameToId,
|
||||
last_timestamp: Arc<AtomicI64>,
|
||||
}
|
||||
|
||||
#[derive(Copy,Clone)]
|
||||
#[repr(u32)]
|
||||
pub enum RealmRunStatus {
|
||||
Stopped = 0,
|
||||
Starting,
|
||||
Running,
|
||||
Current,
|
||||
Stopping,
|
||||
}
|
||||
|
||||
impl RealmRunStatus {
|
||||
fn for_realm(realm: &Realm, in_transition: bool) -> Self {
|
||||
if in_transition {
|
||||
if realm.is_active() { Self::Stopping } else { Self::Starting }
|
||||
} else if realm.is_active() {
|
||||
if realm.is_current() { Self::Current } else {Self::Running }
|
||||
} else {
|
||||
Self::Stopped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RealmItem {
|
||||
pub(crate) fn new_from_realm(index: u32, realm: Realm, realmfs_name_to_id: RealmFSNameToId) -> RealmItem {
|
||||
let path = format!("{}/Realm{}", REALMS2_SERVER_OBJECT_PATH, index);
|
||||
let in_run_transition = Arc::new(AtomicBool::new(false));
|
||||
let config = RealmConfig::new(realm.clone());
|
||||
let realmfs_index = realmfs_name_to_id.lookup(realm.config().realmfs());
|
||||
let realmfs_index = Arc::new(AtomicU32::new(realmfs_index));
|
||||
let last_timestamp = Arc::new(AtomicI64::new(realm.timestamp()));
|
||||
RealmItem { path, index, realm, config, in_run_transition, realmfs_name_to_id, realmfs_index, last_timestamp }
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn index(&self) -> u32 {
|
||||
self.index
|
||||
}
|
||||
|
||||
fn in_run_transition(&self) -> bool {
|
||||
self.in_run_transition.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn last_timestamp(&self) -> i64 {
|
||||
self.last_timestamp.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn set_last_timestamp(&self, ts: i64) {
|
||||
self.last_timestamp.store(ts, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn get_run_status(&self) -> RealmRunStatus {
|
||||
RealmRunStatus::for_realm(&self.realm, self.in_run_transition())
|
||||
}
|
||||
|
||||
pub fn realm(&self) -> &Realm {
|
||||
&self.realm
|
||||
}
|
||||
|
||||
pub fn set_in_run_transition(&self, in_run_transition: bool) {
|
||||
self.in_run_transition.store(in_run_transition, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
async fn do_start(&mut self) -> fdo::Result<()> {
|
||||
if !self.realm.is_active() {
|
||||
let realm = self.realm.clone();
|
||||
|
||||
let res = unblock(move || realm.start()).await;
|
||||
|
||||
if let Err(err) = res {
|
||||
return Err(fdo::Error::Failed(format!("Failed to start realm: {}", err)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn do_stop(&mut self) -> fdo::Result<()> {
|
||||
if self.realm.is_active() {
|
||||
let realm = self.realm.clone();
|
||||
|
||||
let res = unblock(move || realm.stop()).await;
|
||||
|
||||
if let Err(err) = res {
|
||||
return Err(fdo::Error::Failed(format!("Failed to stop realm: {}", err)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn emit_property_changed(&self, connection: &Connection, propname: &str, value: Value<'_>) -> fdo::Result<()> {
|
||||
let iface_name = InterfaceName::from_str_unchecked("com.subgraph.realms.Realm");
|
||||
let changed = HashMap::from([(propname.to_string(), value)]);
|
||||
let inval: &[&str] = &[];
|
||||
block_on(
|
||||
connection.emit_signal(
|
||||
None::<BusName<'_>>,
|
||||
self.path(),
|
||||
"org.freedesktop.DBus.Properties",
|
||||
"PropertiesChanged",
|
||||
&(iface_name, changed, inval)))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[interface(
|
||||
name = "com.subgraph.realms.Realm"
|
||||
)]
|
||||
impl RealmItem {
|
||||
|
||||
async fn start(
|
||||
&mut self,
|
||||
) -> fdo::Result<()> {
|
||||
|
||||
self.do_start().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop(
|
||||
&mut self,
|
||||
) -> fdo::Result<()> {
|
||||
self.do_stop().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn restart(
|
||||
&mut self,
|
||||
) -> fdo::Result<()> {
|
||||
self.do_stop().await?;
|
||||
self.do_start().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_current(&mut self) -> fdo::Result<()> {
|
||||
let realm = self.realm.clone();
|
||||
let res = unblock(move || realm.set_current()).await;
|
||||
if let Err(err) = res {
|
||||
return Err(fdo::Error::Failed(format!("Failed to set realm {} as current: {}", self.realm.name(), err)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_config(&self) -> RealmConfigVars {
|
||||
self.config.config_vars()
|
||||
}
|
||||
|
||||
async fn set_config(&mut self,
|
||||
#[zbus(connection)]
|
||||
connection: &Connection,
|
||||
vars: Vec<(String, String)>) -> fdo::Result<()> {
|
||||
for (var, val) in &vars {
|
||||
self.config.set_var(var, val)?;
|
||||
if var == "realmfs" {
|
||||
let index = self.realmfs_name_to_id.lookup(val);
|
||||
if index != self.realmfs_index.load(Ordering::Relaxed) {
|
||||
self.realmfs_index.store(index, Ordering::Relaxed);
|
||||
self.emit_property_changed(connection, "RealmFS", Value::U32(index))?;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
let config = self.config.clone();
|
||||
unblock(move || config.save_config()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn open_terminal(&self) -> fdo::Result<()> {
|
||||
// Run machinectl with gid=100 so that a policykit rule can be
|
||||
// written to allow this call without allowing every unprivileged
|
||||
// call.
|
||||
let _res = Command::new("/usr/bin/machinectl")
|
||||
.uid(1000)
|
||||
.gid(100) // 'users' group
|
||||
.arg("-q")
|
||||
.arg("-E")
|
||||
.arg(format!("REALM_NAME={}", self.realm.name()))
|
||||
.arg("shell")
|
||||
.arg(format!("user@{}", self.realm.name()))
|
||||
.arg("/usr/bin/gnome-terminal")
|
||||
.spawn();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[zbus(property, name = "RunStatus")]
|
||||
fn run_status(&self) -> u32 {
|
||||
self.get_run_status() as u32
|
||||
}
|
||||
|
||||
#[zbus(property, name="IsSystemRealm")]
|
||||
fn is_system_realm(&self) -> bool {
|
||||
self.realm.is_system()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Name")]
|
||||
fn name(&self) -> &str {
|
||||
self.realm.name()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Description")]
|
||||
fn description(&self) -> String {
|
||||
self.realm.notes()
|
||||
.unwrap_or(String::new())
|
||||
}
|
||||
|
||||
#[zbus(property, name = "PidNS")]
|
||||
fn pid_ns(&self) -> u64 {
|
||||
self.realm.pid_ns().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "RealmFS")]
|
||||
fn realmfs(&self) -> u32 {
|
||||
self.realmfs_index.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Timestamp")]
|
||||
fn timestamp(&self) -> u64 {
|
||||
self.realm.timestamp() as u64
|
||||
}
|
||||
}
|
||||
231
realmsd/src/next/realmfs.rs
Normal file
231
realmsd/src/next/realmfs.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use crate::next::REALMS2_SERVER_OBJECT_PATH;
|
||||
use libcitadel::{RealmFS, RealmFSUpdate, ResizeSize};
|
||||
use std::convert::TryInto;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use blocking::unblock;
|
||||
use zbus::message::Header;
|
||||
use zbus::names::UniqueName;
|
||||
use zbus::zvariant::{ObjectPath, OwnedObjectPath};
|
||||
use zbus::{fdo, interface};
|
||||
use zbus::object_server::SignalEmitter;
|
||||
|
||||
struct UpdateState(Option<(UniqueName<'static>, RealmFSUpdate)>);
|
||||
|
||||
impl UpdateState {
|
||||
|
||||
fn new() -> Self {
|
||||
UpdateState(None)
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
self.0.is_some()
|
||||
}
|
||||
|
||||
fn matches(&self, name: &UniqueName) -> bool {
|
||||
match &self.0{
|
||||
Some((sender, _update)) => sender == name,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn activate(&mut self, sender: UniqueName<'static>, update: RealmFSUpdate) {
|
||||
if self.0.is_none() {
|
||||
self.0 = Some((sender, update));
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_update(&mut self) {
|
||||
if let Some((_name, mut update)) = self.0.take() {
|
||||
update.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
fn take_update(&mut self) -> Option<(RealmFSUpdate)> {
|
||||
self.0.take().map(|(_,update)| update)
|
||||
}
|
||||
}
|
||||
|
||||
const BLOCK_SIZE: u64 = 4096;
|
||||
#[derive(Clone)]
|
||||
pub struct RealmFSItem {
|
||||
update_state: Arc<Mutex<UpdateState>>,
|
||||
object_path: OwnedObjectPath,
|
||||
index: u32,
|
||||
realmfs: RealmFS,
|
||||
}
|
||||
|
||||
impl RealmFSItem {
|
||||
|
||||
fn update_state(&self) -> MutexGuard<'_, UpdateState> {
|
||||
self.update_state.lock().unwrap()
|
||||
}
|
||||
|
||||
|
||||
pub fn client_disconnected(&mut self, name: &UniqueName) {
|
||||
let mut state = self.update_state();
|
||||
|
||||
if state.matches(name) {
|
||||
state.cleanup_update();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_from_realmfs(index: u32, realmfs: RealmFS) -> RealmFSItem {
|
||||
let object_path = format!("{}/RealmFS{}", REALMS2_SERVER_OBJECT_PATH, index).try_into().unwrap();
|
||||
RealmFSItem {
|
||||
update_state: Arc::new(Mutex::new(UpdateState::new())),
|
||||
object_path,
|
||||
index,
|
||||
realmfs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn realmfs(&self) -> &RealmFS {
|
||||
&self.realmfs
|
||||
}
|
||||
|
||||
pub fn index(&self) -> u32 {
|
||||
self.index
|
||||
}
|
||||
|
||||
pub fn object_path(&self) -> ObjectPath<'_> {
|
||||
self.object_path.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[interface(
|
||||
name = "com.subgraph.realms.RealmFS"
|
||||
)]
|
||||
impl RealmFSItem {
|
||||
|
||||
async fn prepare_update(
|
||||
&mut self,
|
||||
#[zbus(header)]
|
||||
hdr: Header<'_>,
|
||||
#[zbus(signal_emitter)]
|
||||
emitter: SignalEmitter<'_>,
|
||||
shared_directory: bool,
|
||||
) -> fdo::Result<String> {
|
||||
let mut update_container = String::new();
|
||||
{
|
||||
let mut state = self.update_state();
|
||||
|
||||
if state.is_active() {
|
||||
return Err(fdo::Error::Failed("An update is already in progress".to_owned()));
|
||||
}
|
||||
|
||||
let sender = match hdr.sender() {
|
||||
Some(sender) => sender,
|
||||
None => return Err(fdo::Error::Failed("No sender in prepare_update()".into())),
|
||||
};
|
||||
|
||||
let mut update = self.realmfs.update()
|
||||
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
|
||||
|
||||
update.prepare_update(shared_directory)
|
||||
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
|
||||
|
||||
|
||||
update_container.push_str(update.name());
|
||||
|
||||
debug!("Update from {}, container: {}", sender, update_container);
|
||||
state.activate(sender.to_owned(), update);
|
||||
}
|
||||
self.is_update_in_progress_changed(&emitter).await?;
|
||||
Ok(update_container)
|
||||
}
|
||||
|
||||
async fn commit_update(&mut self,
|
||||
#[zbus(signal_emitter)]
|
||||
emitter: SignalEmitter<'_>
|
||||
) -> fdo::Result<()> {
|
||||
|
||||
let mut update = match self.update_state().take_update() {
|
||||
None => {
|
||||
warn!("CommitUpdate called when no update in progress");
|
||||
return Ok(());
|
||||
},
|
||||
Some(update) => update,
|
||||
};
|
||||
|
||||
unblock(move || {
|
||||
if let Err(err) = update.commit_update() {
|
||||
warn!("Error committing RealmFS update: {}", err);
|
||||
}
|
||||
}).await;
|
||||
|
||||
self.is_update_in_progress_changed(&emitter).await?;
|
||||
self.free_space_changed(&emitter).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn abandon_update(&mut self,
|
||||
#[zbus(signal_emitter)]
|
||||
emitter: SignalEmitter<'_>) -> fdo::Result<()> {
|
||||
self.update_state().cleanup_update();
|
||||
self.is_update_in_progress_changed(&emitter).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn resize_grow_by(&mut self,
|
||||
#[zbus(signal_emitter)]
|
||||
emitter: SignalEmitter<'_>,
|
||||
nblocks: u64) -> fdo::Result<()> {
|
||||
let nblocks = nblocks as usize;
|
||||
let current = self.realmfs.allocated_size_blocks();
|
||||
let new_size = current + nblocks;
|
||||
let realmfs = self.realmfs.clone();
|
||||
|
||||
unblock(move || {
|
||||
realmfs.resize_grow_to(ResizeSize::blocks(new_size))
|
||||
.map_err(|err| fdo::Error::Failed(err.to_string()))
|
||||
}).await?;
|
||||
|
||||
self.allocated_space_changed(&emitter).await?;
|
||||
self.free_space_changed(&emitter).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Name")]
|
||||
fn name(&self) -> &str {
|
||||
self.realmfs.name()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Activated")]
|
||||
fn activated(&self) -> bool {
|
||||
self.realmfs.is_activated()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "InUse")]
|
||||
fn in_use(&self) -> bool {
|
||||
self.realmfs.is_activated()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "IsUpdateInProgress")]
|
||||
fn is_update_in_progress(&self) -> bool {
|
||||
self.update_state().is_active()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Mountpoint")]
|
||||
fn mountpoint(&self) -> String {
|
||||
self.realmfs.mountpoint().to_string()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Path")]
|
||||
fn path(&self) -> String {
|
||||
format!("{}", self.realmfs.path().display())
|
||||
}
|
||||
|
||||
#[zbus(property, name = "FreeSpace")]
|
||||
fn free_space(&self) -> fdo::Result<u64> {
|
||||
let blocks = self.realmfs.free_size_blocks()
|
||||
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
|
||||
Ok(blocks as u64 * BLOCK_SIZE)
|
||||
}
|
||||
|
||||
#[zbus(property, name = "AllocatedSpace")]
|
||||
fn allocated_space(&self) -> fdo::Result<u64> {
|
||||
let blocks = self.realmfs.allocated_size_blocks();
|
||||
Ok(blocks as u64 * BLOCK_SIZE)
|
||||
}
|
||||
}
|
||||
261
realmsd/src/next/state.rs
Normal file
261
realmsd/src/next/state.rs
Normal file
@@ -0,0 +1,261 @@
|
||||
use crate::next::manager::failed;
|
||||
use crate::next::realm::RealmItem;
|
||||
use crate::next::realmfs::RealmFSItem;
|
||||
use libcitadel::{Realm, RealmEvent, RealmFS, RealmManager, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::fdo;
|
||||
use zbus::names::UniqueName;
|
||||
use zbus::zvariant::{ObjectPath, Value};
|
||||
|
||||
/// Maintains a mapping of RealmFS names to the DBus object
|
||||
/// index values for the corresponding RealmFS objects.
|
||||
///
|
||||
/// This is used in the Realm objects to look up the correct
|
||||
/// realmfs object index for a realmfs name in the realm configuration.
|
||||
///
|
||||
#[derive(Clone)]
|
||||
pub struct RealmFSNameToId(Arc<Mutex<HashMap<String, u32>>>);
|
||||
impl RealmFSNameToId {
|
||||
pub fn new() -> Self {
|
||||
Self(Arc::new(Mutex::new(HashMap::new())))
|
||||
}
|
||||
|
||||
pub fn add(&mut self, name: &str, id: u32) {
|
||||
self.0.lock().unwrap().insert(name.to_string(), id);
|
||||
}
|
||||
|
||||
pub fn lookup(&self, name: &str) -> u32 {
|
||||
match self.0.lock().unwrap().get(name) {
|
||||
None => {
|
||||
warn!("Failed to map realmfs name '{}' to an object index", name);
|
||||
0
|
||||
}
|
||||
Some(&idx) => idx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealmsManagerState(Arc<Mutex<StateInner>>);
|
||||
|
||||
impl RealmsManagerState {
|
||||
|
||||
pub fn new(connection: Connection) -> Self {
|
||||
Self(Arc::new(Mutex::new(StateInner::new(connection))))
|
||||
}
|
||||
|
||||
pub fn load(&self, manager: &RealmManager) -> zbus::Result<()> {
|
||||
self.inner().load(manager)?;
|
||||
|
||||
self.add_event_handler(manager)
|
||||
.map_err(|err| zbus::Error::Failure(err.to_string()))
|
||||
}
|
||||
|
||||
fn inner(&self) -> MutexGuard<'_, StateInner> {
|
||||
self.0.lock().unwrap()
|
||||
}
|
||||
|
||||
fn add_event_handler(&self, manager: &RealmManager) -> Result<()> {
|
||||
let state = self.clone();
|
||||
manager.add_event_handler(move |ev| {
|
||||
if let Err(err) = state.handle_event(ev) {
|
||||
warn!("Failed to handle event {}: {:?}", ev, err);
|
||||
}
|
||||
});
|
||||
manager.start_event_task()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_starting(&self, realm: &Realm) -> fdo::Result<()>{
|
||||
self.inner().realm_status_changed(realm, Some(true))
|
||||
}
|
||||
fn on_started(&self, realm: &Realm) -> fdo::Result<()>{
|
||||
self.inner().realm_status_changed(realm, Some(false))
|
||||
}
|
||||
fn on_stopping(&self, realm: &Realm) -> fdo::Result<()> {
|
||||
self.inner().realm_status_changed(realm, Some(true))
|
||||
}
|
||||
fn on_stopped(&self, realm: &Realm) -> fdo::Result<()>{
|
||||
self.inner().realm_status_changed(realm, Some(false))
|
||||
}
|
||||
fn on_new(&self, realm: &Realm) -> fdo::Result<()>{
|
||||
self.inner().add_realm(realm.clone())
|
||||
}
|
||||
fn on_removed(&self, realm: &Realm) -> fdo::Result<()>{
|
||||
self.inner().remove_realm(realm)
|
||||
}
|
||||
fn on_current(&self, realm: Option<&Realm>) -> fdo::Result<()> {
|
||||
self.inner().set_current_realm(realm)
|
||||
}
|
||||
|
||||
fn handle_event(&self, ev: &RealmEvent) -> fdo::Result<()> {
|
||||
match ev {
|
||||
RealmEvent::Started(realm) => self.on_started(realm)?,
|
||||
RealmEvent::Stopped(realm) => self.on_stopped(realm)?,
|
||||
RealmEvent::New(realm) => self.on_new(realm)?,
|
||||
RealmEvent::Removed(realm) => self.on_removed(realm)?,
|
||||
RealmEvent::Current(realm) => self.on_current(realm.as_ref())?,
|
||||
RealmEvent::Starting(realm) => self.on_starting(realm)?,
|
||||
RealmEvent::Stopping(realm) => self.on_stopping(realm)?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
pub fn get_current(&self) -> Option<RealmItem> {
|
||||
self.inner().current_realm.clone()
|
||||
}
|
||||
pub fn client_disconnected(&self, client_name: &UniqueName) {
|
||||
self.inner().client_disconnected(client_name);
|
||||
}
|
||||
pub fn fork_realmfs(&self, name: &str, new_name: &str) -> fdo::Result<ObjectPath<'static>> {
|
||||
self.inner().fork_realmfs(name, new_name)
|
||||
}
|
||||
}
|
||||
struct StateInner {
|
||||
connection: Connection,
|
||||
next_realm_index: u32,
|
||||
realm_items: HashMap<String, RealmItem>,
|
||||
current_realm: Option<RealmItem>,
|
||||
next_realmfs_index: u32,
|
||||
realmfs_items: HashMap<String, RealmFSItem>,
|
||||
realmfs_name_to_id: RealmFSNameToId,
|
||||
}
|
||||
|
||||
impl StateInner {
|
||||
fn new(connection: Connection) -> StateInner {
|
||||
StateInner {
|
||||
connection,
|
||||
next_realm_index: 1,
|
||||
realm_items: HashMap::new(),
|
||||
current_realm: None,
|
||||
next_realmfs_index: 1,
|
||||
realmfs_items: HashMap::new(),
|
||||
realmfs_name_to_id: RealmFSNameToId::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&mut self, manager: &RealmManager) -> fdo::Result<()> {
|
||||
for realmfs in manager.realmfs_list() {
|
||||
self.add_realmfs(realmfs);
|
||||
}
|
||||
for realm in manager.realm_list() {
|
||||
self.add_realm(realm)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_realm(&mut self, realm: Realm) -> fdo::Result<()> {
|
||||
if self.realm_items.contains_key(realm.name()) {
|
||||
warn!("Attempted to add duplicate realm '{}'", realm.name());
|
||||
return Ok(())
|
||||
}
|
||||
info!("Adding realm-{} with obj index {}", realm.name(), self.next_realm_index);
|
||||
|
||||
let key = realm.name().to_string();
|
||||
let item = RealmItem::new_from_realm(self.next_realm_index, realm, self.realmfs_name_to_id.clone());
|
||||
self.connection.object_server().at(item.path(), item.clone())?;
|
||||
self.realm_items.insert(key, item);
|
||||
self.next_realm_index += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_realm(&mut self, realm: &Realm) -> fdo::Result<()> {
|
||||
if let Some(item) = self.realm_items.remove(realm.name()) {
|
||||
self.connection.object_server().remove::<RealmItem, &str>(item.path())?;
|
||||
} else {
|
||||
warn!("Failed to find realm to remove with name '{}'", realm.name());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_realmfs(&mut self, realmfs: RealmFS) -> Option<RealmFSItem> {
|
||||
if !self.realmfs_items.contains_key(realmfs.name()) {
|
||||
info!("Adding realmfs-{} with object index {}", realmfs.name(), self.next_realmfs_index);
|
||||
let name = realmfs.name().to_string();
|
||||
let item = RealmFSItem::new_from_realmfs(self.next_realmfs_index, realmfs);
|
||||
if let Err(err) = self.connection.object_server().at(item.object_path(), item.clone()) {
|
||||
warn!("Failed to publish object at path {}: {} ", item.object_path(), err);
|
||||
} else {
|
||||
self.realmfs_items.insert(name.clone(), item);
|
||||
self.realmfs_name_to_id.add(&name, self.next_realmfs_index);
|
||||
self.next_realmfs_index += 1;
|
||||
}
|
||||
self.realmfs_by_name(&name)
|
||||
} else {
|
||||
warn!("Attempted to add duplicate realmfs '{}'", realmfs.name());
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn realmfs_by_name(&self, name: &str) -> Option<RealmFSItem> {
|
||||
let res = self.realmfs_items.get(name).cloned();
|
||||
if res.is_none() {
|
||||
warn!("Failed to find RealmFS with name '{}'", name);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn fork_realmfs(&mut self, name: &str, new_name: &str) -> fdo::Result<ObjectPath<'static>> {
|
||||
let item = match self.realmfs_by_name(name) {
|
||||
None => return Err(fdo::Error::Failed(format!("Could not fork {}-realmfs, realmfs not found", name))),
|
||||
Some(item) => item,
|
||||
};
|
||||
|
||||
let new_realmfs = item.realmfs().fork(new_name).
|
||||
map_err(|err| fdo::Error::Failed(format!("Failed to fork realmfs-{} to '{}': {}", name, new_name, err)))?;
|
||||
|
||||
match self.add_realmfs(new_realmfs) {
|
||||
None => Err(fdo::Error::Failed(format!("Failed adding new realmfs while forking realmfs-{} to {}", name, new_name))),
|
||||
Some(new_item) => {
|
||||
let path = new_item.object_path().to_owned();
|
||||
Ok(path)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn realm_by_name(&self, name: &str) -> Option<&RealmItem> {
|
||||
let res = self.realm_items.get(name);
|
||||
|
||||
if res.is_none() {
|
||||
warn!("Failed to find realm with name '{}'", name);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn client_disconnected(&mut self, client_name: &UniqueName) {
|
||||
for v in self.realmfs_items.values_mut() {
|
||||
v.client_disconnected(client_name);
|
||||
}
|
||||
}
|
||||
|
||||
fn realm_status_changed(&self, realm: &Realm, transition: Option<bool>) -> fdo::Result<()> {
|
||||
if let Some(realm) = self.realm_by_name(realm.name()) {
|
||||
if let Some(transition) = transition {
|
||||
realm.set_in_run_transition(transition);
|
||||
}
|
||||
realm.emit_property_changed(self.connection.inner(), "RunStatus", Value::U32(realm.get_run_status() as u32))?;
|
||||
let timestamp = realm.realm().timestamp();
|
||||
if timestamp != realm.last_timestamp() {
|
||||
realm.set_last_timestamp(timestamp);
|
||||
realm.emit_property_changed(self.connection.inner(), "Timestamp", Value::U64(timestamp as u64))?;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
failed(format!("Unknown realm {}", realm.name()))
|
||||
}
|
||||
}
|
||||
|
||||
fn set_current_realm(&mut self, realm: Option<&Realm>) -> fdo::Result<()> {
|
||||
if let Some(r) = self.current_realm.take() {
|
||||
self.realm_status_changed(&r.realm(), None)?;
|
||||
}
|
||||
if let Some(realm) = realm {
|
||||
self.realm_status_changed(realm, None)?;
|
||||
if let Some(item) = self.realm_by_name(realm.name()) {
|
||||
self.current_realm = Some(item.clone());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
use libcitadel::{RealmManager, Realm, OverlayType, Result, PidLookupResult};
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::Type;
|
||||
use std::sync::Arc;
|
||||
use zbus::{dbus_interface, ObjectServer,Connection};
|
||||
use zvariant::derive::Type;
|
||||
use zbus::blocking::Connection;
|
||||
use std::thread;
|
||||
use std::collections::HashMap;
|
||||
use serde::{Serialize,Deserialize};
|
||||
use blocking::unblock;
|
||||
use event_listener::Event;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_repr::Serialize_repr;
|
||||
use zbus::interface;
|
||||
use crate::events::EventHandler;
|
||||
use libcitadel::terminal::Base16Scheme;
|
||||
|
||||
@@ -39,6 +43,7 @@ impl From<PidLookupResult> for RealmFromCitadelPid {
|
||||
#[derive(Clone)]
|
||||
pub struct RealmsManagerServer {
|
||||
manager: Arc<RealmManager>,
|
||||
quit_event: Arc<Event>,
|
||||
}
|
||||
|
||||
const BOOL_CONFIG_VARS: &[&str] = &[
|
||||
@@ -121,40 +126,40 @@ fn configure_realm(manager: &RealmManager, realm: &Realm, variable: &str, value:
|
||||
|
||||
impl RealmsManagerServer {
|
||||
|
||||
fn register_events(&self, connection: &Connection) -> Result<()> {
|
||||
let events = EventHandler::new(connection.clone(), self.clone());
|
||||
self.manager.add_event_handler(move |ev| events.handle_event(ev));
|
||||
self.manager.start_event_task()
|
||||
pub fn load(connection: &Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> Result<RealmsManagerServer> {
|
||||
let server = RealmsManagerServer { manager, quit_event };
|
||||
let events = EventHandler::new(connection.clone());
|
||||
server.manager.add_event_handler(move |ev| events.handle_event(ev));
|
||||
server.manager.start_event_task()?;
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
pub fn register(connection: &Connection) -> Result<ObjectServer> {
|
||||
let manager = RealmManager::load()?;
|
||||
let iface = RealmsManagerServer { manager };
|
||||
iface.register_events(connection)?;
|
||||
let mut object_server = ObjectServer::new(connection);
|
||||
object_server.at(REALMS_SERVER_OBJECT_PATH, iface).map_err(context!("ZBus error"))?;
|
||||
Ok(object_server)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
#[dbus_interface(name = "com.subgraph.realms.Manager")]
|
||||
#[interface(name = "com.subgraph.realms.Manager")]
|
||||
impl RealmsManagerServer {
|
||||
|
||||
fn set_current(&self, name: &str) {
|
||||
if let Some(realm) = self.manager.realm_by_name(name) {
|
||||
if let Err(err) = self.manager.set_current_realm(&realm) {
|
||||
warn!("set_current_realm({}) failed: {}", name, err);
|
||||
async fn set_current(&self, name: &str) {
|
||||
|
||||
let manager = self.manager.clone();
|
||||
let name = name.to_string();
|
||||
unblock(move || {
|
||||
if let Some(realm) = manager.realm_by_name(&name) {
|
||||
if let Err(err) = manager.set_current_realm(&realm) {
|
||||
warn!("set_current_realm({}) failed: {}", name, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).await
|
||||
}
|
||||
|
||||
fn get_current(&self) -> String {
|
||||
match self.manager.current_realm() {
|
||||
Some(realm) => realm.name().to_string(),
|
||||
None => String::new(),
|
||||
}
|
||||
async fn get_current(&self) -> String {
|
||||
let manager = self.manager.clone();
|
||||
unblock(move || {
|
||||
match manager.current_realm() {
|
||||
Some(realm) => realm.name().to_string(),
|
||||
None => String::new(),
|
||||
}
|
||||
}).await
|
||||
}
|
||||
|
||||
fn list(&self) -> Vec<RealmItem> {
|
||||
@@ -249,8 +254,12 @@ impl RealmsManagerServer {
|
||||
});
|
||||
}
|
||||
|
||||
fn realm_from_citadel_pid(&self, pid: u32) -> RealmFromCitadelPid {
|
||||
self.manager.realm_by_pid(pid).into()
|
||||
async fn realm_from_citadel_pid(&self, pid: u32) -> RealmFromCitadelPid {
|
||||
let manager = self.manager.clone();
|
||||
unblock(move || {
|
||||
manager.realm_by_pid(pid).into()
|
||||
|
||||
}).await
|
||||
}
|
||||
|
||||
fn realm_config(&self, name: &str) -> RealmConfig {
|
||||
@@ -261,7 +270,7 @@ impl RealmsManagerServer {
|
||||
RealmConfig::new_from_realm(&realm)
|
||||
}
|
||||
|
||||
fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) {
|
||||
async fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) {
|
||||
let realm = match self.manager.realm_by_name(name) {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
@@ -270,8 +279,12 @@ impl RealmsManagerServer {
|
||||
},
|
||||
};
|
||||
|
||||
for var in &vars {
|
||||
configure_realm(&self.manager, &realm, &var.0, &var.1);
|
||||
for var in vars {
|
||||
let manager = self.manager.clone();
|
||||
let realm = realm.clone();
|
||||
unblock( move || {
|
||||
configure_realm(&manager, &realm, &var.0, &var.1);
|
||||
}).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,13 +292,18 @@ impl RealmsManagerServer {
|
||||
Realm::is_valid_name(name) && self.manager.realm_by_name(name).is_some()
|
||||
}
|
||||
|
||||
fn create_realm(&self, name: &str) -> bool {
|
||||
if let Err(err) = self.manager.new_realm(name) {
|
||||
warn!("Error creating realm ({}): {}", name, err);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
async fn create_realm(&self, name: &str) -> bool {
|
||||
|
||||
let manager = self.manager.clone();
|
||||
let name = name.to_string();
|
||||
unblock(move || {
|
||||
if let Err(err) = manager.new_realm(&name) {
|
||||
warn!("Error creating realm ({}): {}", name, err);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}).await
|
||||
}
|
||||
|
||||
fn list_realm_f_s(&self) -> Vec<String> {
|
||||
@@ -299,23 +317,23 @@ impl RealmsManagerServer {
|
||||
|
||||
}
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_started(&self, realm: &str, pid_ns: u64, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_started(ctx: &SignalEmitter<'_>, realm: &str, pid_ns: u64, status: u8) -> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_stopped(&self, realm: &str, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_stopped(ctx: &SignalEmitter<'_>, realm: &str, status: u8) -> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_new(&self, realm: &str, description: &str, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_new(ctx: &SignalEmitter<'_>, realm: &str, description: &str, status: u8) -> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_removed(&self, realm: &str) -> zbus::Result<()> { Ok(()) }
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_removed(ctx: &SignalEmitter<'_>, realm: &str) -> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_current(&self, realm: &str, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_current(ctx: &SignalEmitter<'_>, realm: &str, status: u8) -> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
pub fn service_started(&self) -> zbus::Result<()> { Ok(()) }
|
||||
#[zbus(signal)]
|
||||
pub async fn service_started(ctx: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||
|
||||
}
|
||||
|
||||
|
||||
9
update-realmfs/Cargo.toml
Normal file
9
update-realmfs/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "update-realmfs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
zbus = "5.7.1"
|
||||
anyhow = "1.0"
|
||||
139
update-realmfs/src/main.rs
Normal file
139
update-realmfs/src/main.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use std::io::Write;
|
||||
use std::{env, io};
|
||||
use std::process::Command;
|
||||
|
||||
use libcitadel::terminal::{AnsiTerminal, Color};
|
||||
use libcitadel::warn;
|
||||
use zbus::blocking::Connection;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use self::realmsd::RealmFS;
|
||||
|
||||
mod realmsd;
|
||||
|
||||
|
||||
fn open_update_shell(machine_name: &str, realmfs_name: &str) -> Result<()> {
|
||||
let mut child = Command::new("/usr/bin/machinectl")
|
||||
.env("PS1", format!("(Update realmfs-{}) # ", realmfs_name))
|
||||
.arg("--quiet")
|
||||
.arg("--setenv=PS1")
|
||||
|
||||
// dumb hack to avoid PS1 being overwritten by /etc/bash.bashrc
|
||||
.arg("--setenv=SUDO_PS1=foo")
|
||||
.arg("--setenv=SUDO_USER=user")
|
||||
|
||||
.arg("shell")
|
||||
.arg(machine_name)
|
||||
.spawn()?;
|
||||
|
||||
let _status = child.wait()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_realmfs(name: &str, shared_dir: bool) -> Result<()> {
|
||||
let connection = Connection::system()?;
|
||||
let realmfs = RealmFS::lookup(&connection, name)?;
|
||||
|
||||
|
||||
print!("\x1B[2J\x1B[1;1H");
|
||||
println!();
|
||||
println!("Starting update container to modify '{name}-realmfs.img'");
|
||||
println!();
|
||||
|
||||
let machine_name = realmfs.prepare_update(shared_dir)
|
||||
.context(format!("Failed to prepare update for RealmFS ({name})"))?;
|
||||
|
||||
|
||||
println!("The root filesystem can be updated from this shell to upgrade packages,");
|
||||
println!("install new software, or make other changes to the filesystem.");
|
||||
println!();
|
||||
if shared_dir {
|
||||
println!("The Shared directory has been mounted at /run/Shared");
|
||||
println!();
|
||||
}
|
||||
println!("Exit update shell with ctrl-d or 'exit'");
|
||||
println!();
|
||||
|
||||
open_update_shell(&machine_name, name)?;
|
||||
|
||||
println!();
|
||||
print!("Save changes (Y/N)? ");
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer)?;
|
||||
|
||||
let response = buffer.trim();
|
||||
|
||||
if response == "y" || response == "Y" {
|
||||
println!("Saving changes");
|
||||
realmfs.commit_update()?;
|
||||
} else {
|
||||
println!("Discarding changes");
|
||||
realmfs.abandon_update()?;
|
||||
}
|
||||
|
||||
println!("Done...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn realmfs_arg(args: &[String]) -> Option<&str> {
|
||||
if args.len() < 2 {
|
||||
None
|
||||
} else {
|
||||
Some(&args[args.len() - 1])
|
||||
}
|
||||
}
|
||||
|
||||
fn has_arg(args: &[String], arg: &str) -> bool {
|
||||
for a in args {
|
||||
if a == arg {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn set_background() -> Result<Color> {
|
||||
let mut ansi = AnsiTerminal::new()?;
|
||||
let saved_bg = ansi.read_palette_bg()?;
|
||||
let bg = Color::new(0xd0, 0xd0, 0xff);
|
||||
ansi.set_palette_bg(bg)?;
|
||||
Ok(saved_bg)
|
||||
}
|
||||
|
||||
fn restore_background(bg: Color) -> Result<()> {
|
||||
let mut ansi = AnsiTerminal::new()?;
|
||||
ansi.set_palette_bg(bg)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
|
||||
let args = env::args().collect::<Vec<_>>();
|
||||
|
||||
let realmfs_name = match realmfs_arg(&args) {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
println!("Need a realmfs name");
|
||||
return Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
let shared_dir = has_arg(&args, "--shared-dir");
|
||||
|
||||
println!("realmfs name is {realmfs_name}");
|
||||
|
||||
let saved_bg = set_background()?;
|
||||
|
||||
if let Err(err) = update_realmfs(&realmfs_name, shared_dir) {
|
||||
warn!("{}", err);
|
||||
}
|
||||
restore_background(saved_bg)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
72
update-realmfs/src/realmsd.rs
Normal file
72
update-realmfs/src/realmsd.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use zbus::blocking::fdo::ObjectManagerProxy;
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::{fdo, proxy};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
#[proxy(
|
||||
interface = "com.subgraph.realms.RealmFS",
|
||||
default_service = "com.subgraph.realms",
|
||||
gen_async = false
|
||||
)]
|
||||
trait RealmFS {
|
||||
|
||||
fn prepare_update(&self, shared_dir: bool) -> zbus::Result<String>;
|
||||
fn commit_update(&self) -> zbus::Result<()>;
|
||||
fn abandon_update(&self) -> zbus::Result<()>;
|
||||
|
||||
#[zbus(property, name = "Name")]
|
||||
fn name(&self) -> fdo::Result<String>;
|
||||
|
||||
#[zbus(property, name = "FreeSpace")]
|
||||
fn free_space(&self) -> fdo::Result<u64> ;
|
||||
|
||||
#[zbus(property, name = "AllocatedSpace")]
|
||||
fn allocated_space(&self) -> fdo::Result<u64>;
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealmFS<'a> {
|
||||
proxy: RealmFSProxy<'a>,
|
||||
}
|
||||
|
||||
impl <'a> RealmFS <'a> {
|
||||
pub fn lookup(connection: &Connection, name: &str) -> Result<Self> {
|
||||
|
||||
let obj_mgr = ObjectManagerProxy::new(
|
||||
connection,
|
||||
"com.subgraph.realms",
|
||||
"/com/subgraph/Realms2")?;
|
||||
|
||||
for (path, map) in obj_mgr.get_managed_objects()? {
|
||||
if map.contains_key("com.subgraph.realms.RealmFS") {
|
||||
let proxy = RealmFSProxy::builder(&connection)
|
||||
.path(path.to_owned())?
|
||||
.build()?;
|
||||
|
||||
let realmfs_name = proxy.name()?;
|
||||
|
||||
if &realmfs_name == name {
|
||||
return Ok(RealmFS{ proxy });
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow::bail!("No RealmFS named '{name}' found.");
|
||||
}
|
||||
|
||||
pub fn prepare_update(&self, shared_dir: bool) -> Result<String> {
|
||||
let name = self.proxy.prepare_update(shared_dir)?;
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
pub fn abandon_update(&self) -> Result<()> {
|
||||
self.proxy.abandon_update()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn commit_update(&self) -> Result<()> {
|
||||
self.proxy.commit_update()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user