1
0
forked from brl/citadel-tools

14 Commits

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

Check for stale header information when activating RealmFS
2025-10-26 14:09:56 +00:00
Bruce Leidl
ba305af893 Removed some old code and added a warning.
Warn if an entry in the realmfs directory fails to process.
2025-10-26 14:08:03 +00:00
Bruce Leidl
9a273b78ff Prevent corruption of updated RealmFS
Make sure that the mounted update filesystem cannot
be written to before generating the dm-verity data.
2025-10-26 14:05:34 +00:00
isa
43f0e3ff98 Add channels and per-channel key signing 2025-09-25 22:17:33 -04:00
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
29 changed files with 2609 additions and 475 deletions

1201
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
[workspace]
members = ["citadel-realms", "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

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

View File

@@ -24,4 +24,14 @@ log = "0.4"
zbus_macros = "5.9"
event-listener = "5.4"
futures-timer = "3.0"
tokio = { version = "1", features = ["full"] }
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

@@ -114,15 +114,15 @@ fn info_signature(img: &ResourceImage) -> Result<()> {
} else {
println!("Signature: No Signature");
}
match img.header().public_key()? {
Some(pubkey) => {
match img.header().public_key() {
Ok(pubkey) => {
if img.header().verify_signature(pubkey) {
println!("Signature is valid");
} else {
println!("Signature verify FAILED");
}
},
None => { println!("No public key found for channel '{}'", img.metainfo().channel()) },
Err(_) => { println!("No public key found for channel '{}'", img.metainfo().channel()) },
}
Ok(())
}
@@ -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

@@ -16,6 +16,7 @@ mod mkimage;
mod realmfs;
mod sync;
mod update;
mod fetch;
fn main() {
let exe = match env::current_exe() {
@@ -36,9 +37,11 @@ fn main() {
} else if exe == Path::new("/usr/bin/citadel-image") {
image::main();
} else if exe == Path::new("/usr/bin/citadel-realmfs") {
realmfs::main();
realmfs::main(args);
} else if exe == Path::new("/usr/bin/citadel-update") {
update::main(args);
} else if exe == Path::new("/usr/bin/citadel-fetch") {
fetch::main();
} else if exe == Path::new("/usr/libexec/citadel-desktop-sync") {
sync::main(args);
} else if exe == Path::new("/usr/libexec/citadel-run") {
@@ -58,7 +61,7 @@ fn dispatch_command(args: Vec<String>) {
"boot" => boot::main(rebuild_args("citadel-boot", args)),
"install" => install::main(rebuild_args("citadel-install", args)),
"image" => image::main(),
"realmfs" => realmfs::main(),
"realmfs" => realmfs::main(rebuild_args("citadel-realmfs", args)),
"update" => update::main(rebuild_args("citadel-update", args)),
"mkimage" => mkimage::main(rebuild_args("citadel-mkimage", args)),
"sync" => sync::main(rebuild_args("citadel-desktop-sync", args)),

View File

@@ -3,7 +3,7 @@ use std::fs::OpenOptions;
use std::fs::{self,File};
use std::io::{self,Write};
use libcitadel::{Result, ImageHeader, devkeys, util};
use libcitadel::{Result, ImageHeader, devkeys, util, keypair_for_channel_signing};
use super::config::BuildConfig;
use std::path::Path;
@@ -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(())
}
@@ -192,6 +192,19 @@ impl UpdateBuilder {
if self.config.channel() == "dev" {
let sig = devkeys().sign(&metainfo);
hdr.set_signature(sig.to_bytes());
} else {
let private_key_path_str = match self.config.private_key_path() {
Some(path) => path,
None => bail!("private-key-path not found in config for non-dev channel"),
};
let private_key_path = Path::new(private_key_path_str);
let sig = keypair_for_channel_signing(private_key_path).sign(&metainfo);
info!("Generated signature: {}", hex::encode(sig.to_bytes()));
let generated_signature_bytes = sig.to_bytes();
if generated_signature_bytes.iter().all(|&b| b == 0) {
bail!("Generated signature is all zeros. Signing failed!");
}
hdr.set_signature(generated_signature_bytes);
}
Ok(hdr)
}
@@ -217,7 +230,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)]
@@ -22,6 +22,9 @@ pub struct BuildConfig {
#[serde(rename = "realmfs-name")]
realmfs_name: Option<String>,
#[serde(rename = "private-key-path")]
private_key_path: Option<String>,
#[serde(skip)]
basedir: PathBuf,
#[serde(skip)]
@@ -102,8 +105,8 @@ impl BuildConfig {
self.realmfs_name.as_ref().map(|s| s.as_str())
}
pub fn version(&self) -> usize {
self.version
pub fn version(&self) -> &str {
&self.version
}
pub fn channel(&self) -> &str {
@@ -117,4 +120,8 @@ impl BuildConfig {
pub fn compress(&self) -> bool {
self.compress
}
pub fn private_key_path(&self) -> Option<&str> {
self.private_key_path.as_ref().map(|s| s.as_str())
}
}

View File

@@ -6,7 +6,7 @@ use libcitadel::util::is_euid_root;
use libcitadel::ResizeSize;
use std::process::exit;
pub fn main() {
pub fn main(args: Vec<String>) {
Logger::set_log_level(LogLevel::Debug);
@@ -65,11 +65,7 @@ is the final absolute size of the image.")
.help("Path or name of RealmFS image to deactivate")
.required(true)))
.arg(Arg::new("image")
.help("Name of or path to RealmFS image to display information about")
.required(true))
.get_matches();
.get_matches_from(args);
let result = match matches.subcommand() {
Some(("resize", m)) => resize(m),

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

@@ -382,7 +382,7 @@ impl ImageHeader {
self.set_signature(&zeros);
}
pub fn public_key(&self) -> Result<Option<PublicKey>> {
pub fn public_key(&self) -> Result<PublicKey> {
public_key_for_channel(self.metainfo().channel())
}
@@ -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

@@ -37,7 +37,17 @@ impl PublicKey {
pub fn verify(&self, data: &[u8], signature: &[u8]) -> bool {
let sig = sign::Signature::try_from(signature)
.expect("Signature::from_slice() failed");
sign::verify_detached(&sig, data, &self.0)
let is_valid = sign::verify_detached(&sig, data, &self.0);
if !is_valid {
warn!("Header signature verification FAILED!");
warn!(" Public Key: {}", self.to_hex());
warn!(" Data (header): {}", hex::encode(data));
warn!(" Signature: {}", hex::encode(signature));
} else {
info!("Header signature verification SUCCESS.");
}
is_valid
}
}

View File

@@ -2,6 +2,9 @@
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate lazy_static;
use std::fs;
use std::path::Path;
#[macro_use] pub mod error;
#[macro_use] mod log;
#[macro_use] mod exec;
@@ -20,6 +23,7 @@ pub mod symlink;
mod realm;
pub mod terminal;
mod system;
pub mod updates;
pub mod flatpak;
@@ -54,28 +58,36 @@ pub fn devkeys() -> KeyPair {
.expect("Error parsing built in dev channel keys")
}
pub fn public_key_for_channel(channel: &str) -> Result<Option<PublicKey>> {
if channel == "dev" {
return Ok(Some(devkeys().public_key()));
}
pub fn keypair_for_channel_signing(private_key_path: &Path) -> KeyPair {
let hex_key = fs::read_to_string(private_key_path)
.expect(&format!("Error reading secret key from {}", private_key_path.display()));
KeyPair::from_hex(hex_key.trim())
.expect(&format!("Error parsing secret key from {}", private_key_path.display()))
}
// Look in /etc/os-release
if Some(channel) == OsRelease::citadel_channel() {
if let Some(hex) = OsRelease::citadel_image_pubkey() {
let pubkey = PublicKey::from_hex(hex)?;
return Ok(Some(pubkey));
}
}
// Does kernel command line have citadel.channel=name:[hex encoded pubkey]
pub fn public_key_for_channel(channel: &str) -> Result<PublicKey> {
// Kernel command line override for developers
if Some(channel) == CommandLine::channel_name() {
if let Some(hex) = CommandLine::channel_pubkey() {
let pubkey = PublicKey::from_hex(hex)?;
return Ok(Some(pubkey))
return Ok(pubkey);
}
}
Ok(None)
let key_path = Path::new("/usr/share/citadel/keys/").join(format!("{}.pub", channel));
if !key_path.exists() {
if channel == "dev" {
return Ok(devkeys().public_key());
}
bail!("Public key not found for channel '{}' at {}", channel, key_path.display());
}
let hex_key = fs::read_to_string(&key_path)
.map_err(context!("could not read public key from {}", key_path.display()))?;
let pubkey = PublicKey::from_hex(hex_key.trim())?;
Ok(pubkey)
}
pub use error::{Result,Error};

View File

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

View File

@@ -57,6 +57,8 @@ impl RealmFS {
// Name used to retrieve key by 'description' from kernel key storage
pub const USER_KEYNAME: &'static str = "realmfs-user";
const BLOCK_SIZE: u64 = 4096;
/// Locate a RealmFS image by name in the default location using the standard name convention
pub fn load_by_name(name: &str) -> Result<Self> {
Self::validate_name(name)?;
@@ -281,10 +283,7 @@ impl RealmFS {
let pubkey = if self.metainfo().channel() == RealmFS::USER_KEYNAME {
self.sealing_keys()?.public_key()
} else {
match self.header().public_key()? {
Some(pubkey) => pubkey,
None => bail!("No public key available for channel {}", self.metainfo().channel()),
}
self.header().public_key()?
};
Ok(pubkey)
}
@@ -313,7 +312,7 @@ impl RealmFS {
info!("forking RealmFS image '{}' to new name '{}'", self.name(), new_name);
let forked = match self.fork_to_path(new_name, &new_path, keys) {
let mut forked = match self.fork_to_path(new_name, &new_path, keys) {
Ok(forked) => forked,
Err(err) => {
if new_path.exists() {
@@ -323,7 +322,10 @@ impl RealmFS {
}
};
self.with_manager(|m| m.realmfs_added(&forked));
self.with_manager(|m| {
m.realmfs_added(&forked);
forked.set_manager(m);
});
Ok(forked)
}
@@ -368,11 +370,11 @@ impl RealmFS {
pub fn file_nblocks(&self) -> Result<usize> {
let meta = self.path.metadata()
.map_err(context!("failed to read metadata from realmfs image file {:?}", self.path))?;
let len = meta.len() as usize;
if len % 4096 != 0 {
let len = meta.len();
if len % Self::BLOCK_SIZE != 0 {
bail!("realmfs image file '{}' has size which is not a multiple of block size", self.path.display());
}
let nblocks = len / 4096;
let nblocks = (len / Self::BLOCK_SIZE) as usize;
if nblocks < (self.metainfo().nblocks() + 1) {
bail!("realmfs image file '{}' has shorter length than nblocks field of image header", self.path.display());
}
@@ -406,18 +408,20 @@ impl RealmFS {
}
pub fn free_size_blocks(&self) -> Result<usize> {
let sb = Superblock::load(self.path(), 4096)?;
let sb = Superblock::load(self.path(), Self::BLOCK_SIZE)?;
Ok(sb.free_block_count() as usize)
}
pub fn allocated_size_blocks(&self) -> Result<usize> {
let meta = self.path().metadata()
.map_err(context!("failed to read metadata from realmfs image file {:?}", self.path()))?;
Ok(meta.blocks() as usize / 8)
pub fn allocated_size_blocks(&self) -> usize {
self.metainfo().nblocks()
}
/// Activate this RealmFS image if not yet activated.
pub fn activate(&self) -> Result<()> {
// Ensure that mountpoint matches header information of image
if let Err(err) = self.check_stale_header(false) {
warn!("error reloading stale image header: {}", err);
}
self.mountpoint().activate(self)
}

View File

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

View File

@@ -108,7 +108,10 @@ impl RealmFSUpdate {
LoopDevice::with_loop(self.target(), Some(BLOCK_SIZE), false, |loopdev| {
self.resize_device(loopdev)
})
})?;
self.apply_update()?;
self.cleanup();
Ok(())
}
fn mount_update_image(&mut self) -> Result<()> {
@@ -214,22 +217,29 @@ impl RealmFSUpdate {
self.set_target_len(nblocks)
}
fn shutdown_container(&mut self) -> Result<()> {
if let Some(update) = self.container.take() {
update.stop_container()?;
fn remount_read_only(&mut self) {
if self.mountpath.exists() {
if let Err(err) = cmd!("/usr/bin/mount", "-o remount,ro {}", self.mountpath.display()) {
warn!("Failed to remount read-only directory {}: {}", self.mountpath.display(), err);
} else {
info!("Directory {} remounted as read-only", self.mountpath.display());
}
}
}
fn shutdown_container(&mut self) {
if let Some(update) = self.container.take() {
if let Err(err) = update.stop_container() {
warn!("Error shutting down update container: {}", err);
}
}
Ok(())
}
pub fn cleanup(&mut self) {
// if a container was started, stop it
if let Err(err) = self.shutdown_container() {
warn!("Error shutting down update container: {}", err);
}
self.shutdown_container();
if self.mountpath.exists() {
self.unmount_update_image();
}
self.unmount_update_image();
if self.target().exists() {
if let Err(err) = fs::remove_file(self.target()) {
@@ -304,6 +314,10 @@ impl RealmFSUpdate {
}
pub fn commit_update(&mut self) -> Result<()> {
// First shutdown container so writable mount can be removed in apply_update()
self.shutdown_container();
// Ensure no further writes
self.remount_read_only();
let result = self.apply_update();
self.cleanup();
result

View File

@@ -199,15 +199,11 @@ impl ResourceImage {
pub fn setup_verity_device(&self) -> Result<String> {
if !CommandLine::nosignatures() {
match self.header.public_key()? {
Some(pubkey) => {
if !self.header.verify_signature(pubkey) {
bail!("header signature verification failed");
}
info!("Image header signature is valid");
}
None => bail!("cannot verify header signature because no public key for channel {} is available", self.metainfo().channel())
let pubkey = self.header.public_key()?;
if !self.header.verify_signature(pubkey) {
bail!("header signature verification failed");
}
info!("Image header signature is valid");
}
info!("Setting up dm-verity device for image");
if !self.has_verity_hashtree() {
@@ -373,7 +369,10 @@ impl ResourceImage {
fn rootfs_channel() -> &'static str {
match CommandLine::channel_name() {
Some(channel) => channel,
None => "dev",
None => match OsRelease::citadel_channel() {
Some(channel) => channel,
None => "dev",
},
}
}
}
@@ -420,8 +419,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, stable ...
pub component_version: Vec<AvailableComponentVersion>,
pub publisher: String, // name of org or person who released this update
}
impl std::fmt::Display for CitadelVersionStruct {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{} image with channel {} has components:\n",
self.client, self.channel
)?;
for i in &self.component_version {
write!(
f,
"\n{} with version {} at location {}",
i.component, i.version, i.file_path
)?;
}
Ok(())
}
}
#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq, Ord)]
pub struct AvailableComponentVersion {
pub component: Component, // rootfs, kernel or extra
pub version: String, // stored as semver
pub file_path: String,
pub sha256_hash: String,
}
impl PartialOrd for AvailableComponentVersion {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
// absolutely require that the components be in the same order in all structs (rootfs, kernel, extra)
if &self.component != &other.component {
panic!("ComponentVersion comparison failed because comparing different components");
}
Some(
semver::Version::parse(&self.version)
.unwrap()
.cmp(&semver::Version::parse(&other.version).unwrap()),
)
}
}
impl std::fmt::Display for AvailableComponentVersion {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"({} image has version: {})",
self.component, self.version
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, clap::ValueEnum)]
pub enum Component {
Rootfs,
Kernel,
Extra,
}
impl Component {
pub fn iterator() -> Iter<'static, Component> {
static COMPONENTS: [Component; 3] =
[Component::Rootfs, Component::Kernel, Component::Extra];
COMPONENTS.iter()
}
}
impl fmt::Display for Component {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Component::Rootfs => write!(f, "rootfs"),
Component::Kernel => write!(f, "kernel"),
&Component::Extra => write!(f, "extra"),
}
}
}
/// Reads a specific key from an os-release formatted file.
/// The value is returned without any surrounding quotes.
pub fn get_citadel_conf(key: &str) -> anyhow::Result<Option<String>> {
// Read the entire file into a string.
let content = std::fs::read_to_string(CITADEL_CONFIG_PATH)
.context(format!("Failed to read {}", CITADEL_CONFIG_PATH))?;
// Search each line for the key.
for line in content.lines() {
// Check if the line starts with "KEY="
if let Some(value) = line.trim().strip_prefix(&format!("{}=", key)) {
// If found, trim whitespace and quotes from the value and return.
let value = value.trim().trim_matches('"').to_string();
return Ok(Some(value));
}
}
// If the loop finishes without finding the key, return None.
Ok(None)
}
pub fn get_os_release(key: &str) -> anyhow::Result<Option<String>> {
// Read the entire file into a string.
let content = std::fs::read_to_string("/etc/os-release")
.context(format!("Failed to read {}", "/etc/os-release"))?;
// Search each line for the key.
for line in content.lines() {
// Check if the line starts with "KEY="
if let Some(value) = line.trim().strip_prefix(&format!("{}=", key)) {
// If found, trim whitespace and quotes from the value and return.
let value = value.trim().trim_matches('"').to_string();
return Ok(Some(value));
}
}
// If the loop finishes without finding the key, return None.
Ok(None)
}
/// Safely modifies a key-value pair in the citadel config os-release-formated file.
/// If the key does not exist, it will be added to the end of the file.
pub fn set_citadel_conf(key: &str, value: &str) -> anyhow::Result<()> {
// Read the existing os-release file.
let content = std::fs::read_to_string(CITADEL_CONFIG_PATH)
.context(format!("Failed to read {}", CITADEL_CONFIG_PATH))?;
let mut lines: Vec<String> = Vec::new();
let mut key_updated = false;
let key_prefix = format!("{}=", key);
let new_line = format!("{}{}", key_prefix, value);
// Process each line to update the key if it exists.
for line in content.lines() {
if line.starts_with(&key_prefix) {
lines.push(new_line.clone());
key_updated = true;
} else {
lines.push(line.to_string());
}
}
// If the key was not found, add it to the end.
if !key_updated {
lines.push(new_line);
}
// Write the changes back safely using a temporary file and an atomic rename.
let mut temp_file = tempfile::Builder::new()
.prefix("citadel.conf")
.suffix(".tmp")
.tempfile_in(std::path::Path::new(CITADEL_CONFIG_PATH).parent().unwrap())?;
temp_file.write_all(lines.join("\n").as_bytes())?;
temp_file.write_all(b"\n")?; // Ensure the file ends with a newline.
temp_file.persist(CITADEL_CONFIG_PATH).context(format!(
"Failed to overwrite {}. Are you running as root?",
CITADEL_CONFIG_PATH
))?;
Ok(())
}

View File

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

@@ -7,13 +7,12 @@ use serde_repr::Serialize_repr;
use zbus::blocking::fdo::DBusProxy;
use zbus::blocking::Connection;
use zbus::names::BusName;
use zbus::zvariant::Type;
use zbus::zvariant::{ObjectPath, Type};
use zbus::{fdo, interface};
use libcitadel::{PidLookupResult, RealmManager};
use libcitadel::{PidLookupResult, Realm, RealmManager};
use libcitadel::terminal::Base16Scheme;
use crate::next::config::RealmConfigVars;
use crate::next::realm::RealmItemState;
use super::realmfs::RealmFSState;
use crate::next::state::RealmsManagerState;
pub fn failed<T>(message: String) -> fdo::Result<T> {
Err(fdo::Error::Failed(message))
@@ -43,10 +42,10 @@ impl From<PidLookupResult> for RealmFromCitadelPid {
}
}
#[derive(Clone)]
pub struct RealmsManagerServer2 {
realms: RealmItemState,
realmfs_state: RealmFSState,
state: RealmsManagerState,
manager: Arc<RealmManager>,
quit_event: Arc<Event>,
}
@@ -55,11 +54,9 @@ pub struct RealmsManagerServer2 {
impl RealmsManagerServer2 {
fn new(connection: Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> Self {
let realms = RealmItemState::new(connection.clone());
let realmfs_state = RealmFSState::new(connection.clone());
let state = RealmsManagerState::new(connection.clone());
RealmsManagerServer2 {
realms,
realmfs_state,
state,
manager,
quit_event,
}
@@ -73,13 +70,12 @@ impl RealmsManagerServer2 {
let args = sig.args()?;
match &args.name {
BusName::Unique(unique_name) if args.new_owner().is_none() => {
self.realmfs_state.client_disconnected(unique_name);
self.state.client_disconnected(unique_name);
},
_ => {},
}
}
Ok(())
}
fn listen_name_owner_changed(&self, connection: &Connection) {
@@ -94,12 +90,28 @@ impl RealmsManagerServer2 {
pub fn load(connection: &Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> zbus::Result<Self> {
let server = Self::new(connection.clone(), manager.clone(), quit_event);
server.realms.load_realms(&manager)?;
server.realmfs_state.load(&manager)?;
server.realms.populate_realmfs(&server.realmfs_state)?;
server.state.load(&manager)?;
server.listen_name_owner_changed(connection);
Ok(server)
}
fn setup_new_realm(manager: &RealmManager, realm: Realm, realmfs_name: &str) {
if let Some(realmfs) = manager.realmfs_by_name(&realmfs_name) {
realm.with_mut_config(|c| c.realmfs = Some(realmfs.name().to_string()));
} else {
warn!("Cannot set RealmFS '{}' on realm because it does not exist", realmfs_name);
}
let config = realm.config();
if let Err(err) = config.write() {
warn!("error writing config file for new realm: {}", err);
}
let scheme_name = config.terminal_scheme().unwrap_or("default-dark");
if let Some(scheme) = Base16Scheme::by_name(scheme_name) {
if let Err(e) = scheme.apply_to_realm(&manager, &realm) {
warn!("error writing scheme files: {}", e);
}
}
}
}
@@ -108,7 +120,7 @@ impl RealmsManagerServer2 {
async fn get_current(&self) -> u32 {
self.realms.get_current()
self.state.get_current()
.map(|r| r.index())
.unwrap_or(0)
}
@@ -120,11 +132,13 @@ impl RealmsManagerServer2 {
}).await
}
async fn create_realm(&self, name: &str) -> fdo::Result<()> {
async fn create_realm(&self, name: &str, realmfs: &str) -> fdo::Result<()> {
let manager = self.manager.clone();
let name = name.to_string();
let realmfs_name = realmfs.to_string();
unblock(move || {
let _ = manager.new_realm(&name).map_err(|err| fdo::Error::Failed(err.to_string()))?;
let realm = manager.new_realm(&name).map_err(|err| fdo::Error::Failed(err.to_string()))?;
RealmsManagerServer2::setup_new_realm(&manager, realm, &realmfs_name);
Ok(())
}).await
}
@@ -143,8 +157,16 @@ impl RealmsManagerServer2 {
}).await
}
async fn fork_realmfs(&self, name: &str, new_name: &str) -> fdo::Result<ObjectPath<'_>> {
let state = self.state.clone();
let name = name.to_string();
let new_name = new_name.to_string();
unblock(move || {
state.fork_realmfs(&name, &new_name)
}).await
}
async fn get_global_config(&self) -> RealmConfigVars {
RealmConfigVars::new_global()
}
}

View File

@@ -4,5 +4,7 @@ mod config;
mod realm;
mod realmfs;
mod state;
pub use manager::RealmsManagerServer2;
pub const REALMS2_SERVER_OBJECT_PATH: &str = "/com/subgraph/Realms2";

View File

@@ -1,18 +1,17 @@
use std::collections::HashMap;
use std::convert::TryInto;
use crate::next::config::{RealmConfig, RealmConfigVars};
use crate::next::REALMS2_SERVER_OBJECT_PATH;
use blocking::unblock;
use libcitadel::Realm;
use std::os::unix::process::CommandExt;
use std::process::Command;
use std::sync::{Arc, Mutex, MutexGuard};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
use blocking::unblock;
use zbus::zvariant::{OwnedObjectPath, Value};
use zbus::{interface, fdo};
use zbus::blocking::Connection;
use std::sync::Arc;
use async_io::block_on;
use zbus::{fdo, interface, Connection};
use zbus::names::{BusName, InterfaceName};
use libcitadel::{Realm, RealmEvent, RealmManager, Result};
use crate::next::config::{RealmConfig, RealmConfigVars};
use crate::next::realmfs::RealmFSState;
use crate::next::REALMS2_SERVER_OBJECT_PATH;
use zbus::zvariant::Value;
use crate::next::state::RealmFSNameToId;
#[derive(Clone)]
pub struct RealmItem {
@@ -22,12 +21,13 @@ pub struct RealmItem {
config: RealmConfig,
in_run_transition: Arc<AtomicBool>,
realmfs_index: Arc<AtomicU32>,
realmfs_name_to_id: RealmFSNameToId,
last_timestamp: Arc<AtomicI64>,
}
#[derive(Copy,Clone)]
#[repr(u32)]
enum RealmRunStatus {
pub enum RealmRunStatus {
Stopped = 0,
Starting,
Running,
@@ -48,13 +48,14 @@ impl RealmRunStatus {
}
impl RealmItem {
pub(crate) fn new_from_realm(index: u32, realm: Realm) -> RealmItem {
pub(crate) fn new_from_realm(index: u32, realm: Realm, realmfs_name_to_id: RealmFSNameToId) -> RealmItem {
let path = format!("{}/Realm{}", REALMS2_SERVER_OBJECT_PATH, index);
let in_run_transition = Arc::new(AtomicBool::new(false));
let config = RealmConfig::new(realm.clone());
let realmfs_index = Arc::new(AtomicU32::new(0));
let realmfs_index = realmfs_name_to_id.lookup(realm.config().realmfs());
let realmfs_index = Arc::new(AtomicU32::new(realmfs_index));
let last_timestamp = Arc::new(AtomicI64::new(realm.timestamp()));
RealmItem { path, index, realm, config, in_run_transition, realmfs_index, last_timestamp }
RealmItem { path, index, realm, config, in_run_transition, realmfs_name_to_id, realmfs_index, last_timestamp }
}
pub fn path(&self) -> &str {
@@ -69,10 +70,26 @@ impl RealmItem {
self.in_run_transition.load(Ordering::Relaxed)
}
fn get_run_status(&self) -> RealmRunStatus {
pub fn last_timestamp(&self) -> i64 {
self.last_timestamp.load(Ordering::Relaxed)
}
pub fn set_last_timestamp(&self, ts: i64) {
self.last_timestamp.store(ts, Ordering::Relaxed);
}
pub fn get_run_status(&self) -> RealmRunStatus {
RealmRunStatus::for_realm(&self.realm, self.in_run_transition())
}
pub fn realm(&self) -> &Realm {
&self.realm
}
pub fn set_in_run_transition(&self, in_run_transition: bool) {
self.in_run_transition.store(in_run_transition, Ordering::Relaxed);
}
async fn do_start(&mut self) -> fdo::Result<()> {
if !self.realm.is_active() {
let realm = self.realm.clone();
@@ -98,6 +115,20 @@ impl RealmItem {
}
Ok(())
}
pub fn emit_property_changed(&self, connection: &Connection, propname: &str, value: Value<'_>) -> fdo::Result<()> {
let iface_name = InterfaceName::from_str_unchecked("com.subgraph.realms.Realm");
let changed = HashMap::from([(propname.to_string(), value)]);
let inval: &[&str] = &[];
block_on(
connection.emit_signal(
None::<BusName<'_>>,
self.path(),
"org.freedesktop.DBus.Properties",
"PropertiesChanged",
&(iface_name, changed, inval)))?;
Ok(())
}
}
#[interface(
@@ -141,9 +172,20 @@ impl RealmItem {
self.config.config_vars()
}
async fn set_config(&mut self, vars: Vec<(String, String)>) -> fdo::Result<()> {
async fn set_config(&mut self,
#[zbus(connection)]
connection: &Connection,
vars: Vec<(String, String)>) -> fdo::Result<()> {
for (var, val) in &vars {
self.config.set_var(var, val)?;
if var == "realmfs" {
let index = self.realmfs_name_to_id.lookup(val);
if index != self.realmfs_index.load(Ordering::Relaxed) {
self.realmfs_index.store(index, Ordering::Relaxed);
self.emit_property_changed(connection, "RealmFS", Value::U32(index))?;
}
}
}
let config = self.config.clone();
@@ -203,193 +245,4 @@ impl RealmItem {
fn timestamp(&self) -> u64 {
self.realm.timestamp() as u64
}
}
#[derive(Clone)]
pub struct RealmItemState(Arc<Mutex<Inner>>);
struct Inner {
connection: Connection,
next_index: u32,
realms: HashMap<String, RealmItem>,
current_realm: Option<RealmItem>,
}
impl Inner {
fn new(connection: Connection) -> Self {
Inner {
connection,
next_index: 1,
realms:HashMap::new(),
current_realm: None,
}
}
fn load_realms(&mut self, manager: &RealmManager) -> zbus::Result<()> {
for realm in manager.realm_list() {
self.add_realm(realm)?;
}
Ok(())
}
pub fn populate_realmfs(&mut self, realmfs_state: &RealmFSState) -> zbus::Result<()> {
for item in self.realms.values_mut() {
if let Some(realmfs) = realmfs_state.realmfs_by_name(item.realm.config().realmfs()) {
item.realmfs_index.store(realmfs.index(), Ordering::Relaxed);
}
}
Ok(())
}
fn add_realm(&mut self, realm: Realm) -> zbus::Result<()> {
if self.realms.contains_key(realm.name()) {
warn!("Attempted to add duplicate realm '{}'", realm.name());
return Ok(())
}
let key = realm.name().to_string();
let item = RealmItem::new_from_realm(self.next_index, realm);
self.connection.object_server().at(item.path(), item.clone())?;
self.realms.insert(key, item);
self.next_index += 1;
Ok(())
}
fn remove_realm(&mut self, realm: &Realm) -> zbus::Result<()> {
if let Some(item) = self.realms.remove(realm.name()) {
self.connection.object_server().remove::<RealmItem, &str>(item.path())?;
} else {
warn!("Failed to find realm to remove with name '{}'", realm.name());
}
Ok(())
}
fn emit_property_changed(&self, object_path: OwnedObjectPath, propname: &str, value: Value<'_>) -> zbus::Result<()> {
let iface_name = InterfaceName::from_str_unchecked("com.subgraph.realms.Realm");
let changed = HashMap::from([(propname.to_string(), value)]);
let inval: &[&str] = &[];
self.connection.emit_signal(
None::<BusName<'_>>,
&object_path,
"org.freedesktop.DBus.Properties",
"PropertiesChanged",
&(iface_name, changed, inval))?;
Ok(())
}
fn realm_status_changed(&self, realm: &Realm, transition: Option<bool>) -> zbus::Result<()> {
if let Some(realm) = self.realm_by_name(realm.name()) {
if let Some(transition) = transition {
realm.in_run_transition.store(transition, Ordering::Relaxed);
}
let object_path = realm.path().try_into().unwrap();
self.emit_property_changed(object_path, "RunStatus", Value::U32(realm.get_run_status() as u32))?;
let timestamp = realm.realm.timestamp();
if realm.last_timestamp.load(Ordering::Relaxed) != realm.realm.timestamp() {
realm.last_timestamp.store(timestamp, Ordering::Relaxed);
let object_path = realm.path().try_into().unwrap();
self.emit_property_changed(object_path, "Timestamp", Value::U64(timestamp as u64))?;
}
}
Ok(())
}
fn realm_by_name(&self, name: &str) -> Option<&RealmItem> {
let res = self.realms.get(name);
if res.is_none() {
warn!("Failed to find realm with name '{}'", name);
}
res
}
fn on_starting(&self, realm: &Realm) -> zbus::Result<()>{
self.realm_status_changed(realm, Some(true))?;
Ok(())
}
fn on_started(&self, realm: &Realm) -> zbus::Result<()>{
self.realm_status_changed(realm, Some(false))
}
fn on_stopping(&self, realm: &Realm) -> zbus::Result<()> {
self.realm_status_changed(realm, Some(true))
}
fn on_stopped(&self, realm: &Realm) -> zbus::Result<()> {
self.realm_status_changed(realm, Some(false))
}
fn on_new(&mut self, realm: &Realm) -> zbus::Result<()> {
self.add_realm(realm.clone())?;
Ok(())
}
fn on_removed(&mut self, realm: &Realm) -> zbus::Result<()> {
self.remove_realm(&realm)?;
Ok(())
}
fn on_current(&mut self, realm: Option<&Realm>) -> zbus::Result<()> {
if let Some(r) = self.current_realm.take() {
self.realm_status_changed(&r.realm, None)?;
}
if let Some(realm) = realm {
self.realm_status_changed(realm, None)?;
if let Some(item) = self.realm_by_name(realm.name()) {
self.current_realm = Some(item.clone());
}
}
Ok(())
}
}
impl RealmItemState {
pub fn new(connection: Connection) -> Self {
RealmItemState(Arc::new(Mutex::new(Inner::new(connection))))
}
pub fn load_realms(&self, manager: &RealmManager) -> zbus::Result<()> {
self.inner().load_realms(manager)?;
self.add_event_handler(manager)
.map_err(|err| zbus::Error::Failure(err.to_string()))?;
Ok(())
}
pub fn populate_realmfs(&self, realmfs_state: &RealmFSState) -> zbus::Result<()> {
self.inner().populate_realmfs(realmfs_state)
}
pub fn get_current(&self) -> Option<RealmItem> {
self.inner().current_realm.clone()
}
fn inner(&self) -> MutexGuard<Inner> {
self.0.lock().unwrap()
}
fn add_event_handler(&self, manager: &RealmManager) -> Result<()> {
let state = self.clone();
manager.add_event_handler(move |ev| {
if let Err(err) = state.handle_event(ev) {
warn!("Error handling {}: {}", ev, err);
}
});
manager.start_event_task()?;
Ok(())
}
fn handle_event(&self, ev: &RealmEvent) -> zbus::Result<()> {
match ev {
RealmEvent::Started(realm) => self.inner().on_started(realm)?,
RealmEvent::Stopped(realm) => self.inner().on_stopped(realm)?,
RealmEvent::New(realm) => self.inner().on_new(realm)?,
RealmEvent::Removed(realm) => self.inner().on_removed(realm)?,
RealmEvent::Current(realm) => self.inner().on_current(realm.as_ref())?,
RealmEvent::Starting(realm) => self.inner().on_starting(realm)?,
RealmEvent::Stopping(realm) => self.inner().on_stopping(realm)?,
};
Ok(())
}
}
}

View File

@@ -1,13 +1,13 @@
use std::collections::HashMap;
use crate::next::REALMS2_SERVER_OBJECT_PATH;
use libcitadel::{RealmFS, RealmFSUpdate, ResizeSize};
use std::convert::TryInto;
use std::sync::{Arc, Mutex, MutexGuard};
use zbus::blocking::Connection;
use blocking::unblock;
use zbus::message::Header;
use zbus::names::UniqueName;
use zbus::zvariant::{ObjectPath, OwnedObjectPath};
use zbus::{fdo, interface};
use libcitadel::{RealmFS, RealmManager,RealmFSUpdate};
use crate::next::REALMS2_SERVER_OBJECT_PATH;
use zbus::object_server::SignalEmitter;
struct UpdateState(Option<(UniqueName<'static>, RealmFSUpdate)>);
@@ -40,12 +40,8 @@ impl UpdateState {
}
}
fn commit_update(&mut self) {
if let Some((_name, mut update)) = self.0.take() {
if let Err(err) = update.commit_update() {
warn!("Error committing RealmFS update: {}", err);
}
}
fn take_update(&mut self) -> Option<(RealmFSUpdate)> {
self.0.take().map(|(_,update)| update)
}
}
@@ -60,13 +56,12 @@ pub struct RealmFSItem {
impl RealmFSItem {
fn update_state(&self) -> MutexGuard<UpdateState> {
fn update_state(&self) -> MutexGuard<'_, UpdateState> {
self.update_state.lock().unwrap()
}
fn client_disconnected(&mut self, name: &UniqueName) {
//debug!("disconnect {} {}", self.object_path, name);
pub fn client_disconnected(&mut self, name: &UniqueName) {
let mut state = self.update_state();
if state.matches(name) {
@@ -84,11 +79,15 @@ impl RealmFSItem {
}
}
pub fn realmfs(&self) -> &RealmFS {
&self.realmfs
}
pub fn index(&self) -> u32 {
self.index
}
pub fn object_path(&self) -> ObjectPath {
pub fn object_path(&self) -> ObjectPath<'_> {
self.object_path.as_ref()
}
}
@@ -102,39 +101,88 @@ impl RealmFSItem {
&mut self,
#[zbus(header)]
hdr: Header<'_>,
#[zbus(signal_emitter)]
emitter: SignalEmitter<'_>,
shared_directory: bool,
) -> fdo::Result<String> {
let mut update_container = String::new();
{
let mut state = self.update_state();
let mut state = self.update_state();
if state.is_active() {
return Err(fdo::Error::Failed("An update is already in progress".to_owned()));
}
if state.is_active() {
return Err(fdo::Error::Failed("An update is already in progress".to_owned()));
let sender = match hdr.sender() {
Some(sender) => sender,
None => return Err(fdo::Error::Failed("No sender in prepare_update()".into())),
};
let mut update = self.realmfs.update()
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
update.prepare_update(shared_directory)
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
update_container.push_str(update.name());
debug!("Update from {}, container: {}", sender, update_container);
state.activate(sender.to_owned(), update);
}
let sender = match hdr.sender() {
Some(sender) => sender,
None => todo!(),
};
let mut update = self.realmfs.update()
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
update.prepare_update(shared_directory)
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
let update_container = update.name().to_string();
debug!("Update from {}, container: {}", sender, update_container);
state.activate(sender.to_owned(), update);
self.is_update_in_progress_changed(&emitter).await?;
Ok(update_container)
}
async fn commit_update(&mut self) -> fdo::Result<()> {
self.update_state().commit_update();
async fn commit_update(&mut self,
#[zbus(signal_emitter)]
emitter: SignalEmitter<'_>
) -> fdo::Result<()> {
let mut update = match self.update_state().take_update() {
None => {
warn!("CommitUpdate called when no update in progress");
return Ok(());
},
Some(update) => update,
};
unblock(move || {
if let Err(err) = update.commit_update() {
warn!("Error committing RealmFS update: {}", err);
}
}).await;
self.is_update_in_progress_changed(&emitter).await?;
self.free_space_changed(&emitter).await?;
Ok(())
}
async fn abandon_update(&mut self) -> fdo::Result<()> {
async fn abandon_update(&mut self,
#[zbus(signal_emitter)]
emitter: SignalEmitter<'_>) -> fdo::Result<()> {
self.update_state().cleanup_update();
self.is_update_in_progress_changed(&emitter).await?;
Ok(())
}
async fn resize_grow_by(&mut self,
#[zbus(signal_emitter)]
emitter: SignalEmitter<'_>,
nblocks: u64) -> fdo::Result<()> {
let nblocks = nblocks as usize;
let current = self.realmfs.allocated_size_blocks();
let new_size = current + nblocks;
let realmfs = self.realmfs.clone();
unblock(move || {
realmfs.resize_grow_to(ResizeSize::blocks(new_size))
.map_err(|err| fdo::Error::Failed(err.to_string()))
}).await?;
self.allocated_space_changed(&emitter).await?;
self.free_space_changed(&emitter).await?;
Ok(())
}
@@ -152,6 +200,12 @@ impl RealmFSItem {
fn in_use(&self) -> bool {
self.realmfs.is_activated()
}
#[zbus(property, name = "IsUpdateInProgress")]
fn is_update_in_progress(&self) -> bool {
self.update_state().is_active()
}
#[zbus(property, name = "Mountpoint")]
fn mountpoint(&self) -> String {
self.realmfs.mountpoint().to_string()
@@ -171,81 +225,7 @@ impl RealmFSItem {
#[zbus(property, name = "AllocatedSpace")]
fn allocated_space(&self) -> fdo::Result<u64> {
let blocks = self.realmfs.allocated_size_blocks()
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
let blocks = self.realmfs.allocated_size_blocks();
Ok(blocks as u64 * BLOCK_SIZE)
}
}
#[derive(Clone)]
pub struct RealmFSState(Arc<Mutex<Inner>>);
impl RealmFSState {
pub fn new(connection: Connection) -> Self {
RealmFSState(Arc::new(Mutex::new(Inner::new(connection))))
}
fn inner(&self) -> MutexGuard<Inner> {
self.0.lock().unwrap()
}
pub(crate) fn load(&self, manager: &RealmManager) -> zbus::Result<()> {
self.inner().load(manager)
}
pub fn realmfs_by_name(&self, name: &str) -> Option<RealmFSItem> {
self.inner().realmfs_by_name(name)
}
pub fn client_disconnected(&self, client_name: &UniqueName) {
let mut lock = self.inner();
for (_,v) in &mut lock.items {
v.client_disconnected(client_name);
}
println!("client disconnected: {client_name}")
}
}
struct Inner {
connection: Connection,
next_index: u32,
items: HashMap<String, RealmFSItem>,
}
impl Inner {
fn new(connection: Connection) -> Self {
Inner {
connection,
next_index: 1,
items: HashMap::new(),
}
}
pub fn load(&mut self, manager: &RealmManager) -> zbus::Result<()> {
for realmfs in manager.realmfs_list() {
self.add_realmfs(realmfs)?;
}
Ok(())
}
fn add_realmfs(&mut self, realmfs: RealmFS) -> zbus::Result<()> {
if !self.items.contains_key(realmfs.name()) {
let name = realmfs.name().to_string();
let item = RealmFSItem::new_from_realmfs(self.next_index, realmfs);
self.connection.object_server().at(item.object_path(), item.clone())?;
self.items.insert(name, item);
self.next_index += 1;
} else {
warn!("Attempted to add duplicate realmfs '{}'", realmfs.name());
}
Ok(())
}
fn realmfs_by_name(&self, name: &str) -> Option<RealmFSItem> {
let res = self.items.get(name).cloned();
if res.is_none() {
warn!("Failed to find RealmFS with name '{}'", name);
}
res
}
}
}

261
realmsd/src/next/state.rs Normal file
View File

@@ -0,0 +1,261 @@
use crate::next::manager::failed;
use crate::next::realm::RealmItem;
use crate::next::realmfs::RealmFSItem;
use libcitadel::{Realm, RealmEvent, RealmFS, RealmManager, Result};
use std::collections::HashMap;
use std::sync::{Arc, Mutex, MutexGuard};
use zbus::blocking::Connection;
use zbus::fdo;
use zbus::names::UniqueName;
use zbus::zvariant::{ObjectPath, Value};
/// Maintains a mapping of RealmFS names to the DBus object
/// index values for the corresponding RealmFS objects.
///
/// This is used in the Realm objects to look up the correct
/// realmfs object index for a realmfs name in the realm configuration.
///
#[derive(Clone)]
pub struct RealmFSNameToId(Arc<Mutex<HashMap<String, u32>>>);
impl RealmFSNameToId {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(HashMap::new())))
}
pub fn add(&mut self, name: &str, id: u32) {
self.0.lock().unwrap().insert(name.to_string(), id);
}
pub fn lookup(&self, name: &str) -> u32 {
match self.0.lock().unwrap().get(name) {
None => {
warn!("Failed to map realmfs name '{}' to an object index", name);
0
}
Some(&idx) => idx,
}
}
}
#[derive(Clone)]
pub struct RealmsManagerState(Arc<Mutex<StateInner>>);
impl RealmsManagerState {
pub fn new(connection: Connection) -> Self {
Self(Arc::new(Mutex::new(StateInner::new(connection))))
}
pub fn load(&self, manager: &RealmManager) -> zbus::Result<()> {
self.inner().load(manager)?;
self.add_event_handler(manager)
.map_err(|err| zbus::Error::Failure(err.to_string()))
}
fn inner(&self) -> MutexGuard<'_, StateInner> {
self.0.lock().unwrap()
}
fn add_event_handler(&self, manager: &RealmManager) -> Result<()> {
let state = self.clone();
manager.add_event_handler(move |ev| {
if let Err(err) = state.handle_event(ev) {
warn!("Failed to handle event {}: {:?}", ev, err);
}
});
manager.start_event_task()?;
Ok(())
}
fn on_starting(&self, realm: &Realm) -> fdo::Result<()>{
self.inner().realm_status_changed(realm, Some(true))
}
fn on_started(&self, realm: &Realm) -> fdo::Result<()>{
self.inner().realm_status_changed(realm, Some(false))
}
fn on_stopping(&self, realm: &Realm) -> fdo::Result<()> {
self.inner().realm_status_changed(realm, Some(true))
}
fn on_stopped(&self, realm: &Realm) -> fdo::Result<()>{
self.inner().realm_status_changed(realm, Some(false))
}
fn on_new(&self, realm: &Realm) -> fdo::Result<()>{
self.inner().add_realm(realm.clone())
}
fn on_removed(&self, realm: &Realm) -> fdo::Result<()>{
self.inner().remove_realm(realm)
}
fn on_current(&self, realm: Option<&Realm>) -> fdo::Result<()> {
self.inner().set_current_realm(realm)
}
fn handle_event(&self, ev: &RealmEvent) -> fdo::Result<()> {
match ev {
RealmEvent::Started(realm) => self.on_started(realm)?,
RealmEvent::Stopped(realm) => self.on_stopped(realm)?,
RealmEvent::New(realm) => self.on_new(realm)?,
RealmEvent::Removed(realm) => self.on_removed(realm)?,
RealmEvent::Current(realm) => self.on_current(realm.as_ref())?,
RealmEvent::Starting(realm) => self.on_starting(realm)?,
RealmEvent::Stopping(realm) => self.on_stopping(realm)?,
};
Ok(())
}
pub fn get_current(&self) -> Option<RealmItem> {
self.inner().current_realm.clone()
}
pub fn client_disconnected(&self, client_name: &UniqueName) {
self.inner().client_disconnected(client_name);
}
pub fn fork_realmfs(&self, name: &str, new_name: &str) -> fdo::Result<ObjectPath<'static>> {
self.inner().fork_realmfs(name, new_name)
}
}
struct StateInner {
connection: Connection,
next_realm_index: u32,
realm_items: HashMap<String, RealmItem>,
current_realm: Option<RealmItem>,
next_realmfs_index: u32,
realmfs_items: HashMap<String, RealmFSItem>,
realmfs_name_to_id: RealmFSNameToId,
}
impl StateInner {
fn new(connection: Connection) -> StateInner {
StateInner {
connection,
next_realm_index: 1,
realm_items: HashMap::new(),
current_realm: None,
next_realmfs_index: 1,
realmfs_items: HashMap::new(),
realmfs_name_to_id: RealmFSNameToId::new(),
}
}
fn load(&mut self, manager: &RealmManager) -> fdo::Result<()> {
for realmfs in manager.realmfs_list() {
self.add_realmfs(realmfs);
}
for realm in manager.realm_list() {
self.add_realm(realm)?;
}
Ok(())
}
fn add_realm(&mut self, realm: Realm) -> fdo::Result<()> {
if self.realm_items.contains_key(realm.name()) {
warn!("Attempted to add duplicate realm '{}'", realm.name());
return Ok(())
}
info!("Adding realm-{} with obj index {}", realm.name(), self.next_realm_index);
let key = realm.name().to_string();
let item = RealmItem::new_from_realm(self.next_realm_index, realm, self.realmfs_name_to_id.clone());
self.connection.object_server().at(item.path(), item.clone())?;
self.realm_items.insert(key, item);
self.next_realm_index += 1;
Ok(())
}
fn remove_realm(&mut self, realm: &Realm) -> fdo::Result<()> {
if let Some(item) = self.realm_items.remove(realm.name()) {
self.connection.object_server().remove::<RealmItem, &str>(item.path())?;
} else {
warn!("Failed to find realm to remove with name '{}'", realm.name());
}
Ok(())
}
fn add_realmfs(&mut self, realmfs: RealmFS) -> Option<RealmFSItem> {
if !self.realmfs_items.contains_key(realmfs.name()) {
info!("Adding realmfs-{} with object index {}", realmfs.name(), self.next_realmfs_index);
let name = realmfs.name().to_string();
let item = RealmFSItem::new_from_realmfs(self.next_realmfs_index, realmfs);
if let Err(err) = self.connection.object_server().at(item.object_path(), item.clone()) {
warn!("Failed to publish object at path {}: {} ", item.object_path(), err);
} else {
self.realmfs_items.insert(name.clone(), item);
self.realmfs_name_to_id.add(&name, self.next_realmfs_index);
self.next_realmfs_index += 1;
}
self.realmfs_by_name(&name)
} else {
warn!("Attempted to add duplicate realmfs '{}'", realmfs.name());
None
}
}
fn realmfs_by_name(&self, name: &str) -> Option<RealmFSItem> {
let res = self.realmfs_items.get(name).cloned();
if res.is_none() {
warn!("Failed to find RealmFS with name '{}'", name);
}
res
}
fn fork_realmfs(&mut self, name: &str, new_name: &str) -> fdo::Result<ObjectPath<'static>> {
let item = match self.realmfs_by_name(name) {
None => return Err(fdo::Error::Failed(format!("Could not fork {}-realmfs, realmfs not found", name))),
Some(item) => item,
};
let new_realmfs = item.realmfs().fork(new_name).
map_err(|err| fdo::Error::Failed(format!("Failed to fork realmfs-{} to '{}': {}", name, new_name, err)))?;
match self.add_realmfs(new_realmfs) {
None => Err(fdo::Error::Failed(format!("Failed adding new realmfs while forking realmfs-{} to {}", name, new_name))),
Some(new_item) => {
let path = new_item.object_path().to_owned();
Ok(path)
},
}
}
fn realm_by_name(&self, name: &str) -> Option<&RealmItem> {
let res = self.realm_items.get(name);
if res.is_none() {
warn!("Failed to find realm with name '{}'", name);
}
res
}
fn client_disconnected(&mut self, client_name: &UniqueName) {
for v in self.realmfs_items.values_mut() {
v.client_disconnected(client_name);
}
}
fn realm_status_changed(&self, realm: &Realm, transition: Option<bool>) -> fdo::Result<()> {
if let Some(realm) = self.realm_by_name(realm.name()) {
if let Some(transition) = transition {
realm.set_in_run_transition(transition);
}
realm.emit_property_changed(self.connection.inner(), "RunStatus", Value::U32(realm.get_run_status() as u32))?;
let timestamp = realm.realm().timestamp();
if timestamp != realm.last_timestamp() {
realm.set_last_timestamp(timestamp);
realm.emit_property_changed(self.connection.inner(), "Timestamp", Value::U64(timestamp as u64))?;
}
Ok(())
} else {
failed(format!("Unknown realm {}", realm.name()))
}
}
fn set_current_realm(&mut self, realm: Option<&Realm>) -> fdo::Result<()> {
if let Some(r) = self.current_realm.take() {
self.realm_status_changed(&r.realm(), None)?;
}
if let Some(realm) = realm {
self.realm_status_changed(realm, None)?;
if let Some(item) = self.realm_by_name(realm.name()) {
self.current_realm = Some(item.clone());
}
}
Ok(())
}
}