1
0
forked from brl/citadel-tools

8 Commits

Author SHA1 Message Date
isa
991621d489 Make text clearer when checking hash 2025-09-04 16:35:59 -04:00
isa
08460b3d5e Fix broken extra image search 2025-08-29 00:58:06 -04:00
isa
d6a93b3ded Add basic update tooling 2025-08-28 12:34:47 -04:00
isa
756520821e Convert images version to use semver 2025-08-28 00:52:07 -04:00
isa
39ac0948ef Correct timezone setting 2025-08-20 17:43:44 +00:00
isa
1b4a780225 Fix unnecessary public scope 2025-08-20 17:43:44 +00:00
isa
e39623b5a9 Add timezone install dbus method and replace dbus with zbus 2025-08-20 17:43:44 +00:00
Bruce Leidl
ea55849afb Launch terminal with gid=100 for corresponding polkit rule that permits 2025-08-20 16:36:13 +00:00
41 changed files with 2290 additions and 5367 deletions

1833
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,9 +13,25 @@ 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"
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"]}
base64ct = "=1.7.3"
reqwest = { version = "0.12", features = ["blocking"] }
sha2 = "0.10"
nix = "0.30"
dialoguer = "0.12"
indicatif = "0.18"

View File

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

View File

@@ -0,0 +1,529 @@
use crate::{update, Path};
use anyhow::{Context, Result};
use clap::ArgMatches;
use dialoguer::{theme::ColorfulTheme, Confirm, MultiSelect};
use ed25519_dalek::{pkcs8::DecodePublicKey, VerifyingKey};
use indicatif::{ProgressBar, ProgressStyle};
use libcitadel::ResourceImage;
use libcitadel::{updates, updates::CitadelVersionStruct};
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::env;
use std::fs;
use std::io::prelude::*;
use std::str::FromStr;
use tempfile::Builder;
const IMAGE_DIRECTORY_PATH: &str = "/storage/resources";
const UPDATE_SERVER_KEY_PATH: &str = "/etc/citadel/update_server_key.pub";
const LAST_RESORT_CLIENT: &str = "public";
const LAST_RESORT_CHANNEL: &str = "stable";
const LAST_RESORT_CITADEL_PUBLISHER: &str = "Subgraph";
const DEFAULT_UPDATE_SERVER_HOSTNAME: &str = "update.subgraph.com";
#[derive(Deserialize)]
struct Channels {
channels: Vec<String>,
}
fn get_update_server_hostname() -> String {
env::var("UPDATE_SERVER_HOSTNAME")
.unwrap_or_else(|_| DEFAULT_UPDATE_SERVER_HOSTNAME.to_string())
}
fn verify_hash(path: &std::path::PathBuf, expected_hash: &str) -> Result<()> {
let mut file = fs::File::open(path)?;
let mut sha256 = Sha256::new();
std::io::copy(&mut file, &mut sha256)?;
let hash = format!("{:x}", sha256.finalize());
if hash != expected_hash {
fs::remove_file(path)?;
anyhow::bail!(
"Hash mismatch for file {}. Expected {}, got {}",
path.display(),
expected_hash,
hash
);
}
Ok(())
}
pub fn check() -> Result<()> {
let current_version = get_current_os_config()?;
let server_citadel_version = fetch_and_verify_version_cbor(&current_version)?;
let components_to_upgrade =
compare_citadel_versions(&current_version, &server_citadel_version)?;
if components_to_upgrade.len() == 1 {
println!(
"We found the following component to upgrade: {}",
components_to_upgrade[0]
);
} else if components_to_upgrade.len() > 1 {
println!("We found the following components to upgrade: \n");
for component in components_to_upgrade {
println!("{}", component);
}
} else {
println!("Your system is up to date!");
}
Ok(())
}
fn get_component_info_from_args<'a>(
sub_matches: &ArgMatches,
server_version: &'a CitadelVersionStruct,
) -> Result<(&'a str, &'a str)> {
let (path, hash) = if sub_matches.get_flag("rootfs") {
(
&server_version.component_version[0].file_path,
&server_version.component_version[0].sha256_hash,
)
} else if sub_matches.get_flag("kernel") {
(
&server_version.component_version[1].file_path,
&server_version.component_version[1].sha256_hash,
)
} else if sub_matches.get_flag("extra") {
(
&server_version.component_version[2].file_path,
&server_version.component_version[2].sha256_hash,
)
} else {
anyhow::bail!("No component specified for download/reinstall.");
};
Ok((path, hash))
}
pub fn download(sub_matches: &ArgMatches) -> Result<()> {
let current_version = &get_current_os_config()?;
let server_citadel_version = &fetch_and_verify_version_cbor(current_version)?;
let (path, hash) = get_component_info_from_args(sub_matches, server_citadel_version)?;
download_file(path, hash)?;
Ok(())
}
pub fn read_remote() -> Result<()> {
let server_citadel_version = fetch_and_verify_version_cbor(&get_current_os_config()?)?;
println!("Server offers:\n{}", server_citadel_version);
Ok(())
}
pub fn upgrade() -> Result<()> {
// First, get access to the current citadel's parameters
let current_version = &get_current_os_config()?;
// Check if we are missing local version info, which happens after a channel switch
let mut missing_components = Vec::new();
for component in &current_version.component_version {
if (component.component == updates::Component::Kernel
|| component.component == updates::Component::Extra)
&& component.version == "0.0.0"
{
missing_components.push(component.component.to_string());
}
}
if !missing_components.is_empty() {
println!("WARNING: Your system is missing local version information for the following components:");
for comp in &missing_components {
println!("- {}", comp);
}
println!(
"\nThis is expected if you have recently switched to the '{}' update channel.",
current_version.channel
);
println!("However, it means we cannot verify if the server's version is newer than your installed version.");
println!(
"This could expose you to a downgrade attack if the remote channel is compromised."
);
let confirmed = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(
"Do you want to proceed with fetching and installing the versions from the server?",
)
.default(false)
.interact()?;
if !confirmed {
println!("Upgrade aborted by user.");
return Ok(());
}
}
let server_citadel_version = &fetch_and_verify_version_cbor(current_version)?;
// Find which components have updates available
let components_to_upgrade = compare_citadel_versions(current_version, server_citadel_version)?;
if components_to_upgrade.is_empty() {
println!("Your system is up to date!");
return Ok(());
}
// Create a list of formatted strings for the prompt
let prompt_items: Vec<String> = components_to_upgrade
.iter()
.map(|comp| {
// Find the currently installed version for a nice display
let current_comp_version = current_version
.component_version
.iter()
.find(|c| c.component == comp.component)
.map(|c| c.version.clone())
.unwrap_or_else(|| "N/A".to_string());
// If the current version is 0.0.0, display it as "not installed"
let display_version = if current_comp_version == "0.0.0" {
"not installed".to_string()
} else {
current_comp_version
};
format!(
"{} ({} -> {})",
comp.component, display_version, comp.version
)
})
.collect();
// Build and display the interactive checklist to the user
println!("Found available updates. Please make your selection:");
let selections = MultiSelect::with_theme(&ColorfulTheme::default())
.with_prompt("Select components to upgrade (use spacebar to select, enter to confirm)")
.items(&prompt_items)
.defaults(&vec![true; prompt_items.len()])
.interact()?;
if selections.is_empty() {
println!("No components selected. Aborting upgrade.");
return Ok(());
}
// Create a final list of only the components the user selected
let mut final_components_to_install = Vec::new();
for index in selections {
final_components_to_install.push(components_to_upgrade[index].clone());
}
println!(
"\nPreparing to install {} component(s)...",
final_components_to_install.len()
);
// Loop through the selected components and install them
for component in final_components_to_install {
println!("---");
println!("Upgrading {}", component.component);
let (_tmp_dir, save_path) = download_file(&component.file_path, &component.sha256_hash)?;
println!("Installing image...");
update::install_image(&save_path, 0)?;
println!("{} installed successfully!", component.component);
}
println!("---");
println!("Update process finished.");
Ok(())
}
pub fn reinstall(sub_matches: &ArgMatches) -> Result<()> {
let current_version = &get_current_os_config()?;
let server_citadel_version = &fetch_and_verify_version_cbor(current_version)?;
let (path, hash) = get_component_info_from_args(sub_matches, server_citadel_version)?;
let (_tmp_dir, save_path) = download_file(path, hash)?;
update::install_image(&save_path, 0)?;
Ok(())
}
pub fn status() -> Result<()> {
println!("Gathering local system information...");
let current_config = get_current_os_config()?;
println!("\n--- Citadel Update Status ---");
println!("Client: {}", current_config.client);
println!("Channel: {}", current_config.channel);
println!("Publisher: {}", current_config.publisher);
println!("\n--- Installed Components ---");
for component in current_config.component_version {
// Handle the case where a component might not be installed for the current channel
let version_display = if component.version == "0.0.0" {
"Not installed for this channel".to_string()
} else {
component.version
};
println!(
"{:<12}{}",
format!("{}:", component.component),
version_display
);
}
Ok(())
}
/// Returns a vec of ComponentVersion structs of the components which can be upgraded
fn compare_citadel_versions(
current: &CitadelVersionStruct,
offered: &CitadelVersionStruct,
) -> Result<Vec<updates::AvailableComponentVersion>> {
let mut update_vec: Vec<updates::AvailableComponentVersion> = Vec::new();
if current.channel != offered.channel {
anyhow::bail!(
"Update channel mismatch. Your system is on '{}', but the server is configured for '{}'. Please check your configuration.",
current.channel,
offered.channel
);
}
if current.client != offered.client {
anyhow::bail!(
"Update client mismatch. Your system is configured as '{}', but the server is configured for '{}'. Please check your configuration.",
current.client,
offered.client
);
}
if current.publisher != offered.publisher {
anyhow::bail!(
"Update publisher mismatch. Your system is configured for '{}', but the server is configured for '{}'. Please check your configuration.",
current.publisher,
offered.publisher
);
}
for i in 0..current.component_version.len() {
if current.component_version[i] < offered.component_version[i] {
update_vec.push(offered.component_version[i].clone());
}
}
Ok(update_vec)
}
fn get_image_version<P: AsRef<Path>>(path: &P) -> Result<String> {
let resource_image = ResourceImage::from_path(path)?;
Ok(resource_image.metainfo().version().to_string())
}
/// Reads and displays the currently configured update channel.
pub fn show_channel() -> Result<()> {
let channel = updates::get_citadel_conf("CITADEL_CHANNEL")?
.unwrap_or_else(|| LAST_RESORT_CHANNEL.to_string());
println!("Current update channel: {}", channel);
Ok(())
}
/// Sets the system's update channel by writing to the config file.
pub fn set_channel(channel_to_set: &str) -> Result<()> {
// Validate the channel name against the server's list.
let available_channels = fetch_available_channels()?;
if !available_channels.contains(&channel_to_set.to_string()) {
anyhow::bail!(
"Channel '{}' is not a valid remote channel. Use 'channel list' to see options.",
channel_to_set
);
}
// Check if the directory for the new channel already exists.
let channel_dir_path = format!("{IMAGE_DIRECTORY_PATH}/{channel_to_set}");
let path = Path::new(&channel_dir_path);
if !path.exists() {
println!(
"You are switching to a new channel ('{}') for the first time.",
channel_to_set
);
let confirmed = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(format!(
"Are you sure you want to change channels to: '{}'?",
channel_dir_path
))
.default(true)
.interact()?;
if confirmed {
fs::create_dir_all(path).context(format!(
"Failed to create directory for channel '{}'",
channel_to_set
))?;
println!("Directory created.");
} else {
println!("Channel switch aborted by user.");
return Ok(());
}
}
updates::set_citadel_conf("CITADEL_CHANNEL", channel_to_set)?;
println!("\nUpdate channel has been set to: {}", channel_to_set);
Ok(())
}
fn get_current_os_config() -> Result<updates::CitadelVersionStruct> {
let client = updates::get_citadel_conf("CITADEL_CLIENT")?
.unwrap_or_else(|| LAST_RESORT_CLIENT.to_string());
let channel = updates::get_citadel_conf("CITADEL_CHANNEL")?
.unwrap_or_else(|| LAST_RESORT_CHANNEL.to_string());
let mut kernel_version = String::from("0.0.0");
let glob_pattern = format!("{IMAGE_DIRECTORY_PATH}/{channel}/citadel-kernel*.img");
if let Ok(glob_results) = glob::glob(&glob_pattern) {
for path_result in glob_results {
if let Ok(path) = path_result {
// If we find a kernel image, try to get its version.
// If we succeed, update the version and stop looking.
if let Ok(version) = get_image_version(&path) {
kernel_version = version;
break;
}
}
}
}
// RootFS version is always available from the running system
let rootfs_version = updates::get_os_release("CITADEL_ROOTFS_VERSION")?.unwrap();
// Extra version - gracefully handle if not found
let mut extra_version = String::from("0.0.0");
let glob_pattern_extra = format!("{IMAGE_DIRECTORY_PATH}/{channel}/citadel-extra*.img");
if let Ok(glob_results) = glob::glob(&glob_pattern_extra) {
for path_result in glob_results {
if let Ok(path) = path_result {
if let Ok(version) = get_image_version(&path) {
extra_version = version;
break;
}
}
}
}
let publisher = updates::get_citadel_conf("CITADEL_PUBLISHER")?
.unwrap_or_else(|| LAST_RESORT_CITADEL_PUBLISHER.to_string());
let mut component_version = Vec::new();
component_version.push(updates::AvailableComponentVersion {
component: updates::Component::Rootfs,
version: rootfs_version.to_owned(),
file_path: "".to_owned(),
sha256_hash: "".to_owned(),
});
component_version.push(updates::AvailableComponentVersion {
component: updates::Component::Kernel,
version: kernel_version.to_owned(),
file_path: "".to_owned(),
sha256_hash: "".to_owned(),
});
component_version.push(updates::AvailableComponentVersion {
component: updates::Component::Extra,
version: extra_version.to_owned(),
file_path: "".to_owned(),
sha256_hash: "".to_owned(),
});
let current_version_struct = updates::CitadelVersionStruct {
client: client.to_owned(),
channel: channel.to_owned(),
component_version,
publisher: publisher.to_owned(),
};
Ok(current_version_struct)
}
fn fetch_and_verify_version_cbor(
current_citadel_version: &updates::CitadelVersionStruct,
) -> Result<updates::CitadelVersionStruct> {
let url = format!(
"https://{}/{}/{}/version.cbor",
get_update_server_hostname(),
current_citadel_version.client,
current_citadel_version.channel
);
let version_file_bytes = reqwest::blocking::get(&url)?
.bytes()
.context(format!("Failed to get version_file_bytes from {}", url))?;
let crypto_container: updates::CryptoContainerFile =
serde_cbor::from_slice(&version_file_bytes)
.context(format!("Failed to parse version.cbor from {}", url))?;
// find update server public key kept in the rootfs
let mut file = std::fs::File::open(UPDATE_SERVER_KEY_PATH).context(format!(
"Failed to open update_server_key file from {}",
UPDATE_SERVER_KEY_PATH
))?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let public_key = VerifyingKey::from_public_key_pem(&contents)
.context("Failed to parse public key from file.")?;
let signature = ed25519_dalek::Signature::from_str(&crypto_container.signature)?;
// verify signature
public_key.verify_strict(&crypto_container.serialized_citadel_version, &signature)?;
// construct the struct
let citadel_version_struct: updates::CitadelVersionStruct =
serde_cbor::from_slice(&crypto_container.serialized_citadel_version)?;
Ok(citadel_version_struct)
}
fn download_file(path: &str, hash: &str) -> Result<(tempfile::TempDir, std::path::PathBuf)> {
let client = reqwest::blocking::Client::new();
let url = format!("https://{}/{}", get_update_server_hostname(), path);
println!("Downloading from {}", url);
let component_download_response = client.get(&url).send()?;
// Get the total size of the file from the server's response headers.
let total_size = component_download_response
.content_length()
.context("Failed to get content length from server")?;
// Create a new progress bar and set its style.
let pb = ProgressBar::new(total_size);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")?
.tick_strings(&["", "", "", "", "", "", "", "", "", "", " "])
.progress_chars("=>-"),
);
// Create the temporary directory and destination file.
let tmp_dir = Builder::new().prefix("citadel-fetch").tempdir()?;
let file_name = Path::new(path).file_name().unwrap();
let dest_path = tmp_dir.path().join(file_name);
let mut dest_file = fs::File::create(&dest_path)?;
// Wrap the download stream with the progress bar.
let mut source = pb.wrap_read(component_download_response);
// Copy the stream to the file, which automatically updates the progress bar.
std::io::copy(&mut source, &mut dest_file)?;
println!("\nSaved file to {}\n", dest_path.display());
verify_hash(&dest_path, hash)?;
println!("File hash verified\n");
Ok((tmp_dir, dest_path))
}
pub fn list_channels() -> Result<()> {
println!("Fetching available channels...");
let channels = fetch_available_channels()?;
println!("Available channels:");
for channel in channels {
println!("- {}", channel);
}
Ok(())
}
/// Fetches the list of available channels from the remote server.
fn fetch_available_channels() -> Result<Vec<String>> {
let url = format!(
"https://{}/{}/channels.cbor",
get_update_server_hostname(),
LAST_RESORT_CLIENT
);
let response_bytes = reqwest::blocking::get(&url)?.bytes()?;
let available_channels: Channels = serde_cbor::from_slice(&response_bytes)
.context("Failed to parse channel list CBOR from server")?;
Ok(available_channels.channels)
}

View File

@@ -0,0 +1,85 @@
use clap::{arg, command, ArgAction, Command};
use std::process::exit;
mod fetch;
use clap::Arg;
pub fn main() {
let matches = command!() // requires `cargo` feature
.subcommand_required(true)
.subcommand(Command::new("check").about("Check for updates from remote server"))
.subcommand(Command::new("status").about("Show the currently installed versions and update channel"))
.subcommand(
Command::new("download")
.about("Download a specific component from the server")
.arg(arg!(-r --rootfs "rootfs component").action(ArgAction::SetTrue))
.arg(arg!(-k --kernel "kernel component").action(ArgAction::SetTrue))
.arg(arg!(-e --extra "extra component").action(ArgAction::SetTrue))
.arg_required_else_help(true),
)
.subcommand(
Command::new("read-remote")
.about("Read the remote server and print information on versions offered"),
)
.subcommand(
Command::new("upgrade")
.about("Download and install components if the server has a more recent version than currently installed on the system")
)
.subcommand(
Command::new("reinstall")
.about("Download and install a specific component even if the server's component version is not greater than currently installed")
.arg(arg!(-r --rootfs "rootfs component").action(ArgAction::SetTrue))
.arg(arg!(-k --kernel "kernel component").action(ArgAction::SetTrue))
.arg(arg!(-e --extra "extra component").action(ArgAction::SetTrue))
.arg_required_else_help(true),
)
.subcommand(
Command::new("channel")
.about("Manage the update channel")
.subcommand_required(true)
.subcommand(Command::new("list").about("List available channels from the server"))
.subcommand(Command::new("show").about("Show the current update channel"))
.subcommand(
Command::new("set")
.about("Set a new update channel (requires root)")
.arg(Arg::new("CHANNEL").required(true)),
),
)
.get_matches();
let result = match matches.subcommand() {
Some(("check", _sub_matches)) => fetch::check(),
Some(("status", _sub_matches)) => fetch::status(),
Some(("download", sub_matches)) => fetch::download(sub_matches),
Some(("read-remote", _sub_matches)) => fetch::read_remote(),
Some(("upgrade", _sub_matches)) => {
let _ = require_root(); // Check for root privileges
fetch::upgrade()
}
Some(("reinstall", sub_matches)) => {
let _ = require_root(); // Check for root privileges.
fetch::reinstall(sub_matches)
}
Some(("channel", sub_matches)) => match sub_matches.subcommand() {
Some(("list", _)) => fetch::list_channels(),
Some(("show", _)) => fetch::show_channel(),
Some(("set", set_matches)) => {
let _ = require_root();
let channel_name = set_matches.get_one::<String>("CHANNEL").unwrap();
fetch::set_channel(channel_name)
}
_ => unreachable!(),
},
_ => unreachable!("Please pass a subcommand"),
};
if let Err(ref e) = result {
eprintln!("Error: {}", e);
exit(1);
}
}
fn require_root() -> anyhow::Result<()> {
if !nix::unistd::geteuid().is_root() {
// Using anyhow::bail! is a concise way to return an error.
anyhow::bail!("This command requires root privileges. Please try again with sudo.");
}
Ok(())
}

View File

@@ -250,9 +250,9 @@ 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()) {

View File

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

View File

@@ -20,6 +20,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 +127,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 +144,7 @@ impl Installer {
target_device,
citadel_passphrase,
passphrase,
timezone,
artifact_directory: DEFAULT_ARTIFACT_DIRECTORY.to_string(),
logfile: None,
}
@@ -154,6 +158,7 @@ impl Installer {
target_device: None,
citadel_passphrase: None,
passphrase: None,
timezone: None,
artifact_directory: DEFAULT_ARTIFACT_DIRECTORY.to_string(),
logfile: None,
}
@@ -224,6 +229,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",
@@ -501,7 +519,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<()> {

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ mod mkimage;
mod realmfs;
mod sync;
mod update;
mod fetch;
fn main() {
let exe = match env::current_exe() {
@@ -32,13 +33,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();
} 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") {

View File

@@ -38,15 +38,15 @@ impl UpdateBuilder {
}
fn target_filename(&self) -> String {
format!("citadel-{}-{}-{:03}.img", self.config.img_name(), self.config.channel(), self.config.version())
format!("citadel-{}-{}-{}.img", self.config.img_name(), self.config.channel(), self.config.version())
}
fn build_filename(config: &BuildConfig) -> String {
format!("citadel-{}-{}-{:03}", config.image_type(), config.channel(), config.version())
format!("citadel-{}-{}-{}", config.image_type(), config.channel(), config.version())
}
fn verity_filename(&self) -> String {
format!("verity-hash-{}-{:03}", self.config.image_type(), self.config.version())
format!("verity-hash-{}-{}", self.config.image_type(), self.config.version())
}
pub fn build(&mut self) -> Result<()> {
@@ -154,7 +154,7 @@ impl UpdateBuilder {
bail!("failed to compress {:?}: {}", self.image(), err);
}
// Rename back to original image_data filename
util::rename(self.image().with_extension("xz"), self.image())?;
util::rename(util::append_to_path(self.image(), ".xz"), self.image())?;
}
Ok(())
}
@@ -217,7 +217,7 @@ impl UpdateBuilder {
writeln!(v, "realmfs-name = \"{}\"", name)?;
}
writeln!(v, "channel = \"{}\"", self.config.channel())?;
writeln!(v, "version = {}", self.config.version())?;
writeln!(v, "version = \"{}\"", self.config.version())?;
writeln!(v, "timestamp = \"{}\"", self.config.timestamp())?;
writeln!(v, "nblocks = {}", self.nblocks.unwrap())?;
writeln!(v, "shasum = \"{}\"", self.shasum.as_ref().unwrap())?;

View File

@@ -9,7 +9,7 @@ pub struct BuildConfig {
#[serde(rename = "image-type")]
image_type: String,
channel: String,
version: usize,
version: String,
timestamp: String,
source: String,
#[serde(default)]
@@ -102,8 +102,8 @@ impl BuildConfig {
self.realmfs_name.as_ref().map(|s| s.as_str())
}
pub fn version(&self) -> usize {
self.version
pub fn version(&self) -> &str {
&self.version
}
pub fn channel(&self) -> &str {

View File

@@ -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()?;

View File

@@ -20,6 +20,10 @@ 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"
[dependencies.inotify]
version = "0.8"

View File

@@ -453,7 +453,7 @@ pub struct MetaInfo {
realmfs_owner: Option<String>,
#[serde(default)]
version: u32,
version: String,
#[serde(default)]
timestamp: String,
@@ -508,8 +508,8 @@ impl MetaInfo {
Self::str_ref(&self.realmfs_owner)
}
pub fn version(&self) -> u32 {
self.version
pub fn version(&self) -> &str {
&self.version
}
pub fn timestamp(&self) -> &str {

View File

@@ -20,6 +20,7 @@ pub mod symlink;
mod realm;
pub mod terminal;
mod system;
pub mod updates;
pub mod flatpak;

View File

@@ -420,8 +420,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)

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

@@ -0,0 +1,183 @@
use anyhow::Context;
use std::fmt;
use std::io::Write;
use std::slice::Iter;
pub const UPDATE_SERVER_HOSTNAME: &str = "update.subgraph.com";
const CITADEL_CONFIG_PATH: &str = "/storage/citadel-state/citadel.conf";
/// This struct embeds the CitadelVersion datastruct as well as the cryptographic validation of the that information
#[derive(Debug, Serialize, Deserialize)]
pub struct CryptoContainerFile {
pub serialized_citadel_version: Vec<u8>, // we serialize CitadelVersion
pub signature: String, // serialized CitadelVersion gets signed
pub signatory: String, // name of org or person who holds the key
}
/// This struct contains the entirety of the logical information needed to decide whether to update or not
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct CitadelVersionStruct {
pub client: String,
pub channel: String, // dev, prod ...
pub component_version: Vec<AvailableComponentVersion>,
pub publisher: String, // name of org or person who released this update
}
impl std::fmt::Display for CitadelVersionStruct {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{} image with channel {} has components:\n",
self.client, self.channel
)?;
for i in &self.component_version {
write!(
f,
"\n{} with version {} at location {}",
i.component, i.version, i.file_path
)?;
}
Ok(())
}
}
#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq, Ord)]
pub struct AvailableComponentVersion {
pub component: Component, // rootfs, kernel or extra
pub version: String, // stored as semver
pub file_path: String,
pub sha256_hash: String,
}
impl PartialOrd for AvailableComponentVersion {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
// absolutely require that the components be in the same order in all structs (rootfs, kernel, extra)
if &self.component != &other.component {
panic!("ComponentVersion comparison failed because comparing different components");
}
Some(
semver::Version::parse(&self.version)
.unwrap()
.cmp(&semver::Version::parse(&other.version).unwrap()),
)
}
}
impl std::fmt::Display for AvailableComponentVersion {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"({} image has version: {})",
self.component, self.version
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, clap::ValueEnum)]
pub enum Component {
Rootfs,
Kernel,
Extra,
}
impl Component {
pub fn iterator() -> Iter<'static, Component> {
static COMPONENTS: [Component; 3] =
[Component::Rootfs, Component::Kernel, Component::Extra];
COMPONENTS.iter()
}
}
impl fmt::Display for Component {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Component::Rootfs => write!(f, "rootfs"),
Component::Kernel => write!(f, "kernel"),
&Component::Extra => write!(f, "extra"),
}
}
}
/// Reads a specific key from an os-release formatted file.
/// The value is returned without any surrounding quotes.
pub fn get_citadel_conf(key: &str) -> anyhow::Result<Option<String>> {
// Read the entire file into a string.
let content = std::fs::read_to_string(CITADEL_CONFIG_PATH)
.context(format!("Failed to read {}", CITADEL_CONFIG_PATH))?;
// Search each line for the key.
for line in content.lines() {
// Check if the line starts with "KEY="
if let Some(value) = line.trim().strip_prefix(&format!("{}=", key)) {
// If found, trim whitespace and quotes from the value and return.
let value = value.trim().trim_matches('"').to_string();
return Ok(Some(value));
}
}
// If the loop finishes without finding the key, return None.
Ok(None)
}
pub fn get_os_release(key: &str) -> anyhow::Result<Option<String>> {
// Read the entire file into a string.
let content = std::fs::read_to_string("/etc/os-release")
.context(format!("Failed to read {}", "/etc/os-release"))?;
// Search each line for the key.
for line in content.lines() {
// Check if the line starts with "KEY="
if let Some(value) = line.trim().strip_prefix(&format!("{}=", key)) {
// If found, trim whitespace and quotes from the value and return.
let value = value.trim().trim_matches('"').to_string();
return Ok(Some(value));
}
}
// If the loop finishes without finding the key, return None.
Ok(None)
}
/// Safely modifies a key-value pair in the citadel config os-release-formated file.
/// If the key does not exist, it will be added to the end of the file.
pub fn set_citadel_conf(key: &str, value: &str) -> anyhow::Result<()> {
// Read the existing os-release file.
let content = std::fs::read_to_string(CITADEL_CONFIG_PATH)
.context(format!("Failed to read {}", CITADEL_CONFIG_PATH))?;
let mut lines: Vec<String> = Vec::new();
let mut key_updated = false;
let key_prefix = format!("{}=", key);
let new_line = format!("{}{}", key_prefix, value);
// Process each line to update the key if it exists.
for line in content.lines() {
if line.starts_with(&key_prefix) {
lines.push(new_line.clone());
key_updated = true;
} else {
lines.push(line.to_string());
}
}
// If the key was not found, add it to the end.
if !key_updated {
lines.push(new_line);
}
// Write the changes back safely using a temporary file and an atomic rename.
let mut temp_file = tempfile::Builder::new()
.prefix("citadel.conf")
.suffix(".tmp")
.tempfile_in(std::path::Path::new(CITADEL_CONFIG_PATH).parent().unwrap())?;
temp_file.write_all(lines.join("\n").as_bytes())?;
temp_file.write_all(b"\n")?; // Ensure the file ends with a newline.
temp_file.persist(CITADEL_CONFIG_PATH).context(format!(
"Failed to overwrite {}. Are you running as root?",
CITADEL_CONFIG_PATH
))?;
Ok(())
}

View File

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

View File

@@ -152,9 +152,12 @@ impl RealmItem {
}
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(1000)
.gid(100) // 'users' group
.arg("-q")
.arg("-E")
.arg(format!("REALM_NAME={}", self.realm.name()))