forked from brl/citadel-tools
Compare commits
14 Commits
reproducib
...
updates
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b2a379eae | |||
|
|
3a3d5c3b9b | ||
|
|
be413476b2 | ||
|
|
87575e396c | ||
|
|
d4035cb9c3 | ||
|
|
df6e0de7c0 | ||
|
|
3966d1d753 | ||
|
|
ba305af893 | ||
|
|
9a273b78ff | ||
| 43f0e3ff98 | |||
| 991621d489 | |||
| 08460b3d5e | |||
| d6a93b3ded | |||
| 756520821e |
1201
Cargo.lock
generated
1201
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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() {
|
||||
|
||||
529
citadel-tool/src/fetch/fetch.rs
Normal file
529
citadel-tool/src/fetch/fetch.rs
Normal 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(¤t_version)?;
|
||||
let components_to_upgrade =
|
||||
compare_citadel_versions(¤t_version, &server_citadel_version)?;
|
||||
if components_to_upgrade.len() == 1 {
|
||||
println!(
|
||||
"We found the following component to upgrade: {}",
|
||||
components_to_upgrade[0]
|
||||
);
|
||||
} else if components_to_upgrade.len() > 1 {
|
||||
println!("We found the following components to upgrade: \n");
|
||||
for component in components_to_upgrade {
|
||||
println!("{}", component);
|
||||
}
|
||||
} else {
|
||||
println!("Your system is up to date!");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 ¤t_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)
|
||||
}
|
||||
85
citadel-tool/src/fetch/mod.rs
Normal file
85
citadel-tool/src/fetch/mod.rs
Normal 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(())
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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())?;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use libcitadel::util::is_euid_root;
|
||||
use libcitadel::ResizeSize;
|
||||
use std::process::exit;
|
||||
|
||||
pub fn main() {
|
||||
pub fn main(args: Vec<String>) {
|
||||
|
||||
Logger::set_log_level(LogLevel::Debug);
|
||||
|
||||
@@ -65,11 +65,7 @@ is the final absolute size of the image.")
|
||||
.help("Path or name of RealmFS image to deactivate")
|
||||
.required(true)))
|
||||
|
||||
|
||||
.arg(Arg::new("image")
|
||||
.help("Name of or path to RealmFS image to display information about")
|
||||
.required(true))
|
||||
.get_matches();
|
||||
.get_matches_from(args);
|
||||
|
||||
let result = match matches.subcommand() {
|
||||
Some(("resize", m)) => resize(m),
|
||||
|
||||
@@ -93,7 +93,7 @@ fn create_tmp_copy(path: &Path) -> Result<PathBuf> {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn install_image(path: &Path, flags: u32) -> Result<()> {
|
||||
pub fn install_image(path: &Path, flags: u32) -> Result<()> {
|
||||
if !path.exists() || path.file_name().is_none() {
|
||||
bail!("file path {} does not exist", path.display());
|
||||
}
|
||||
@@ -140,7 +140,7 @@ fn prepare_image(image: &ResourceImage, flags: u32) -> Result<()> {
|
||||
}
|
||||
|
||||
fn install_extra_image(image: &ResourceImage) -> Result<()> {
|
||||
let filename = format!("citadel-extra-{:03}.img", image.header().metainfo().version());
|
||||
let filename = format!("citadel-extra-{}.img", image.header().metainfo().version());
|
||||
install_image_file(image, filename.as_str())?;
|
||||
remove_old_extra_images(image)?;
|
||||
Ok(())
|
||||
@@ -186,7 +186,7 @@ fn install_kernel_image(image: &mut ResourceImage) -> Result<()> {
|
||||
info!("kernel version is {}", kernel_version);
|
||||
install_kernel_file(image, &kernel_version)?;
|
||||
|
||||
let filename = format!("citadel-kernel-{}-{:03}.img", kernel_version, version);
|
||||
let filename = format!("citadel-kernel-{}-{}.img", kernel_version, version);
|
||||
install_image_file(image, &filename)?;
|
||||
|
||||
let all_versions = all_boot_kernel_versions()?;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
183
libcitadel/src/updates.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use anyhow::Context;
|
||||
use std::fmt;
|
||||
use std::io::Write;
|
||||
use std::slice::Iter;
|
||||
|
||||
pub const UPDATE_SERVER_HOSTNAME: &str = "update.subgraph.com";
|
||||
const CITADEL_CONFIG_PATH: &str = "/storage/citadel-state/citadel.conf";
|
||||
|
||||
/// This struct embeds the CitadelVersion datastruct as well as the cryptographic validation of 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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
261
realmsd/src/next/state.rs
Normal file
@@ -0,0 +1,261 @@
|
||||
use crate::next::manager::failed;
|
||||
use crate::next::realm::RealmItem;
|
||||
use crate::next::realmfs::RealmFSItem;
|
||||
use libcitadel::{Realm, RealmEvent, RealmFS, RealmManager, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::fdo;
|
||||
use zbus::names::UniqueName;
|
||||
use zbus::zvariant::{ObjectPath, Value};
|
||||
|
||||
/// Maintains a mapping of RealmFS names to the DBus object
|
||||
/// index values for the corresponding RealmFS objects.
|
||||
///
|
||||
/// This is used in the Realm objects to look up the correct
|
||||
/// realmfs object index for a realmfs name in the realm configuration.
|
||||
///
|
||||
#[derive(Clone)]
|
||||
pub struct RealmFSNameToId(Arc<Mutex<HashMap<String, u32>>>);
|
||||
impl RealmFSNameToId {
|
||||
pub fn new() -> Self {
|
||||
Self(Arc::new(Mutex::new(HashMap::new())))
|
||||
}
|
||||
|
||||
pub fn add(&mut self, name: &str, id: u32) {
|
||||
self.0.lock().unwrap().insert(name.to_string(), id);
|
||||
}
|
||||
|
||||
pub fn lookup(&self, name: &str) -> u32 {
|
||||
match self.0.lock().unwrap().get(name) {
|
||||
None => {
|
||||
warn!("Failed to map realmfs name '{}' to an object index", name);
|
||||
0
|
||||
}
|
||||
Some(&idx) => idx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealmsManagerState(Arc<Mutex<StateInner>>);
|
||||
|
||||
impl RealmsManagerState {
|
||||
|
||||
pub fn new(connection: Connection) -> Self {
|
||||
Self(Arc::new(Mutex::new(StateInner::new(connection))))
|
||||
}
|
||||
|
||||
pub fn load(&self, manager: &RealmManager) -> zbus::Result<()> {
|
||||
self.inner().load(manager)?;
|
||||
|
||||
self.add_event_handler(manager)
|
||||
.map_err(|err| zbus::Error::Failure(err.to_string()))
|
||||
}
|
||||
|
||||
fn inner(&self) -> MutexGuard<'_, StateInner> {
|
||||
self.0.lock().unwrap()
|
||||
}
|
||||
|
||||
fn add_event_handler(&self, manager: &RealmManager) -> Result<()> {
|
||||
let state = self.clone();
|
||||
manager.add_event_handler(move |ev| {
|
||||
if let Err(err) = state.handle_event(ev) {
|
||||
warn!("Failed to handle event {}: {:?}", ev, err);
|
||||
}
|
||||
});
|
||||
manager.start_event_task()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_starting(&self, realm: &Realm) -> fdo::Result<()>{
|
||||
self.inner().realm_status_changed(realm, Some(true))
|
||||
}
|
||||
fn on_started(&self, realm: &Realm) -> fdo::Result<()>{
|
||||
self.inner().realm_status_changed(realm, Some(false))
|
||||
}
|
||||
fn on_stopping(&self, realm: &Realm) -> fdo::Result<()> {
|
||||
self.inner().realm_status_changed(realm, Some(true))
|
||||
}
|
||||
fn on_stopped(&self, realm: &Realm) -> fdo::Result<()>{
|
||||
self.inner().realm_status_changed(realm, Some(false))
|
||||
}
|
||||
fn on_new(&self, realm: &Realm) -> fdo::Result<()>{
|
||||
self.inner().add_realm(realm.clone())
|
||||
}
|
||||
fn on_removed(&self, realm: &Realm) -> fdo::Result<()>{
|
||||
self.inner().remove_realm(realm)
|
||||
}
|
||||
fn on_current(&self, realm: Option<&Realm>) -> fdo::Result<()> {
|
||||
self.inner().set_current_realm(realm)
|
||||
}
|
||||
|
||||
fn handle_event(&self, ev: &RealmEvent) -> fdo::Result<()> {
|
||||
match ev {
|
||||
RealmEvent::Started(realm) => self.on_started(realm)?,
|
||||
RealmEvent::Stopped(realm) => self.on_stopped(realm)?,
|
||||
RealmEvent::New(realm) => self.on_new(realm)?,
|
||||
RealmEvent::Removed(realm) => self.on_removed(realm)?,
|
||||
RealmEvent::Current(realm) => self.on_current(realm.as_ref())?,
|
||||
RealmEvent::Starting(realm) => self.on_starting(realm)?,
|
||||
RealmEvent::Stopping(realm) => self.on_stopping(realm)?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
pub fn get_current(&self) -> Option<RealmItem> {
|
||||
self.inner().current_realm.clone()
|
||||
}
|
||||
pub fn client_disconnected(&self, client_name: &UniqueName) {
|
||||
self.inner().client_disconnected(client_name);
|
||||
}
|
||||
pub fn fork_realmfs(&self, name: &str, new_name: &str) -> fdo::Result<ObjectPath<'static>> {
|
||||
self.inner().fork_realmfs(name, new_name)
|
||||
}
|
||||
}
|
||||
struct StateInner {
|
||||
connection: Connection,
|
||||
next_realm_index: u32,
|
||||
realm_items: HashMap<String, RealmItem>,
|
||||
current_realm: Option<RealmItem>,
|
||||
next_realmfs_index: u32,
|
||||
realmfs_items: HashMap<String, RealmFSItem>,
|
||||
realmfs_name_to_id: RealmFSNameToId,
|
||||
}
|
||||
|
||||
impl StateInner {
|
||||
fn new(connection: Connection) -> StateInner {
|
||||
StateInner {
|
||||
connection,
|
||||
next_realm_index: 1,
|
||||
realm_items: HashMap::new(),
|
||||
current_realm: None,
|
||||
next_realmfs_index: 1,
|
||||
realmfs_items: HashMap::new(),
|
||||
realmfs_name_to_id: RealmFSNameToId::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&mut self, manager: &RealmManager) -> fdo::Result<()> {
|
||||
for realmfs in manager.realmfs_list() {
|
||||
self.add_realmfs(realmfs);
|
||||
}
|
||||
for realm in manager.realm_list() {
|
||||
self.add_realm(realm)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_realm(&mut self, realm: Realm) -> fdo::Result<()> {
|
||||
if self.realm_items.contains_key(realm.name()) {
|
||||
warn!("Attempted to add duplicate realm '{}'", realm.name());
|
||||
return Ok(())
|
||||
}
|
||||
info!("Adding realm-{} with obj index {}", realm.name(), self.next_realm_index);
|
||||
|
||||
let key = realm.name().to_string();
|
||||
let item = RealmItem::new_from_realm(self.next_realm_index, realm, self.realmfs_name_to_id.clone());
|
||||
self.connection.object_server().at(item.path(), item.clone())?;
|
||||
self.realm_items.insert(key, item);
|
||||
self.next_realm_index += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_realm(&mut self, realm: &Realm) -> fdo::Result<()> {
|
||||
if let Some(item) = self.realm_items.remove(realm.name()) {
|
||||
self.connection.object_server().remove::<RealmItem, &str>(item.path())?;
|
||||
} else {
|
||||
warn!("Failed to find realm to remove with name '{}'", realm.name());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_realmfs(&mut self, realmfs: RealmFS) -> Option<RealmFSItem> {
|
||||
if !self.realmfs_items.contains_key(realmfs.name()) {
|
||||
info!("Adding realmfs-{} with object index {}", realmfs.name(), self.next_realmfs_index);
|
||||
let name = realmfs.name().to_string();
|
||||
let item = RealmFSItem::new_from_realmfs(self.next_realmfs_index, realmfs);
|
||||
if let Err(err) = self.connection.object_server().at(item.object_path(), item.clone()) {
|
||||
warn!("Failed to publish object at path {}: {} ", item.object_path(), err);
|
||||
} else {
|
||||
self.realmfs_items.insert(name.clone(), item);
|
||||
self.realmfs_name_to_id.add(&name, self.next_realmfs_index);
|
||||
self.next_realmfs_index += 1;
|
||||
}
|
||||
self.realmfs_by_name(&name)
|
||||
} else {
|
||||
warn!("Attempted to add duplicate realmfs '{}'", realmfs.name());
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn realmfs_by_name(&self, name: &str) -> Option<RealmFSItem> {
|
||||
let res = self.realmfs_items.get(name).cloned();
|
||||
if res.is_none() {
|
||||
warn!("Failed to find RealmFS with name '{}'", name);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn fork_realmfs(&mut self, name: &str, new_name: &str) -> fdo::Result<ObjectPath<'static>> {
|
||||
let item = match self.realmfs_by_name(name) {
|
||||
None => return Err(fdo::Error::Failed(format!("Could not fork {}-realmfs, realmfs not found", name))),
|
||||
Some(item) => item,
|
||||
};
|
||||
|
||||
let new_realmfs = item.realmfs().fork(new_name).
|
||||
map_err(|err| fdo::Error::Failed(format!("Failed to fork realmfs-{} to '{}': {}", name, new_name, err)))?;
|
||||
|
||||
match self.add_realmfs(new_realmfs) {
|
||||
None => Err(fdo::Error::Failed(format!("Failed adding new realmfs while forking realmfs-{} to {}", name, new_name))),
|
||||
Some(new_item) => {
|
||||
let path = new_item.object_path().to_owned();
|
||||
Ok(path)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn realm_by_name(&self, name: &str) -> Option<&RealmItem> {
|
||||
let res = self.realm_items.get(name);
|
||||
|
||||
if res.is_none() {
|
||||
warn!("Failed to find realm with name '{}'", name);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn client_disconnected(&mut self, client_name: &UniqueName) {
|
||||
for v in self.realmfs_items.values_mut() {
|
||||
v.client_disconnected(client_name);
|
||||
}
|
||||
}
|
||||
|
||||
fn realm_status_changed(&self, realm: &Realm, transition: Option<bool>) -> fdo::Result<()> {
|
||||
if let Some(realm) = self.realm_by_name(realm.name()) {
|
||||
if let Some(transition) = transition {
|
||||
realm.set_in_run_transition(transition);
|
||||
}
|
||||
realm.emit_property_changed(self.connection.inner(), "RunStatus", Value::U32(realm.get_run_status() as u32))?;
|
||||
let timestamp = realm.realm().timestamp();
|
||||
if timestamp != realm.last_timestamp() {
|
||||
realm.set_last_timestamp(timestamp);
|
||||
realm.emit_property_changed(self.connection.inner(), "Timestamp", Value::U64(timestamp as u64))?;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
failed(format!("Unknown realm {}", realm.name()))
|
||||
}
|
||||
}
|
||||
|
||||
fn set_current_realm(&mut self, realm: Option<&Realm>) -> fdo::Result<()> {
|
||||
if let Some(r) = self.current_realm.take() {
|
||||
self.realm_status_changed(&r.realm(), None)?;
|
||||
}
|
||||
if let Some(realm) = realm {
|
||||
self.realm_status_changed(realm, None)?;
|
||||
if let Some(item) = self.realm_by_name(realm.name()) {
|
||||
self.current_realm = Some(item.clone());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user