Compare commits
2 Commits
master
...
update_too
Author | SHA1 | Date | |
---|---|---|---|
0ff03cb273 | |||
ae41a71b60 |
5
.cargo/config.toml
Normal file
5
.cargo/config.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[env]
|
||||
# overide this with your name to send to a different path on the server
|
||||
UPDATES_CLIENT = "public"
|
||||
UPDATES_CHANNEL = "prod"
|
||||
|
1836
Cargo.lock
generated
1836
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui", "launch-gnome-software" ]
|
||||
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui", "launch-gnome-software", "update-generator" ]
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
2854
citadel-installer-ui/Cargo.lock
generated
2854
citadel-installer-ui/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -19,3 +19,9 @@ byteorder = "1"
|
||||
dbus = "0.8.4"
|
||||
pwhash = "1.0"
|
||||
tempfile = "3"
|
||||
ed25519-dalek = {version = "2.1", features = ["pem"]}
|
||||
anyhow = "1.0"
|
||||
rs-release = "0.1"
|
||||
reqwest = {version = "0.12", features = ["blocking"]}
|
||||
glob = "0.3"
|
||||
serde_cbor = "0.11"
|
@ -136,8 +136,11 @@ 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() {
|
||||
|
328
citadel-tool/src/fetch/fetch.rs
Normal file
328
citadel-tool/src/fetch/fetch.rs
Normal file
@ -0,0 +1,328 @@
|
||||
use crate::{update, Path};
|
||||
use anyhow::{Context, Result};
|
||||
use clap::ArgMatches;
|
||||
use ed25519_dalek::{pkcs8::DecodePublicKey, VerifyingKey};
|
||||
use libcitadel::updates::UPDATE_SERVER_HOSTNAME;
|
||||
use libcitadel::ResourceImage;
|
||||
use libcitadel::{updates, updates::CitadelVersionStruct};
|
||||
|
||||
use std::io::prelude::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
const OS_RELEASE_PATH: &str = "/etc/os-release";
|
||||
const IMAGE_DIRECTORY_PATH: &str = "/run/citadel/images/";
|
||||
const EXTRA_IMAGE_PATH: &str = "/run/citadel/images/citadel-extra.img";
|
||||
const ROOTFS_IMAGE_PATH: &str = "/run/citadel/images/citadel-rootfs.img";
|
||||
const UPDATE_SERVER_KEY_PATH: &str = "/etc/citadel/update_server_key.pub";
|
||||
const LAST_RESORT_CLIENT: &str = "public";
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
pub fn download(sub_matches: &ArgMatches) -> Result<()> {
|
||||
let current_version = &get_current_os_config()?;
|
||||
let server_citadel_version = &fetch_and_verify_version_cbor(¤t_version)?;
|
||||
|
||||
let mut path = "";
|
||||
if sub_matches.get_flag("rootfs") {
|
||||
path = &server_citadel_version.component_version[0].file_path;
|
||||
} else if sub_matches.get_flag("kernel") {
|
||||
path = &server_citadel_version.component_version[1].file_path;
|
||||
} else if sub_matches.get_flag("extra") {
|
||||
path = &server_citadel_version.component_version[2].file_path;
|
||||
}
|
||||
|
||||
download_file(path)?;
|
||||
|
||||
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()?;
|
||||
let server_citadel_version = &fetch_and_verify_version_cbor(¤t_version)?;
|
||||
|
||||
// What do we need to upgrade?
|
||||
let components_to_upgrade =
|
||||
compare_citadel_versions(¤t_version, &server_citadel_version)?;
|
||||
|
||||
if components_to_upgrade.len() == 1 {
|
||||
println!("We found a component to upgrade!");
|
||||
let allow_download = prompt_user_for_permission_to_download(&components_to_upgrade[0])?;
|
||||
|
||||
if allow_download {
|
||||
let save_path = download_file(&components_to_upgrade[0].file_path)?;
|
||||
|
||||
// run citadel-update to upgrade
|
||||
println!("Installing image");
|
||||
update::install_image(&save_path, 0)?;
|
||||
println!("Image installed correctly");
|
||||
} else {
|
||||
println!("Ok! Maybe later");
|
||||
}
|
||||
} else if components_to_upgrade.len() > 1 {
|
||||
println!("We found some components to upgrade!");
|
||||
for component in components_to_upgrade {
|
||||
let allow_download = prompt_user_for_permission_to_download(&component)?;
|
||||
|
||||
if allow_download {
|
||||
let save_path = download_file(&component.file_path)?;
|
||||
|
||||
println!("Installing image");
|
||||
update::install_image(&save_path, 0)?;
|
||||
println!("Image installed correctly");
|
||||
} else {
|
||||
println!("Ok! Maybe later");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Your system is up to date!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reinstall(sub_matches: &ArgMatches) -> Result<()> {
|
||||
let current_version = &get_current_os_config()?;
|
||||
let server_citadel_version = &fetch_and_verify_version_cbor(¤t_version)?;
|
||||
|
||||
let mut path = "";
|
||||
if sub_matches.get_flag("rootfs") {
|
||||
path = &server_citadel_version.component_version[0].file_path;
|
||||
} else if sub_matches.get_flag("kernel") {
|
||||
path = &server_citadel_version.component_version[1].file_path;
|
||||
} else if sub_matches.get_flag("extra") {
|
||||
path = &server_citadel_version.component_version[2].file_path;
|
||||
}
|
||||
|
||||
let save_path = download_file(path)?;
|
||||
update::install_image(&save_path, 0)?;
|
||||
|
||||
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();
|
||||
|
||||
// safety checks
|
||||
if current.channel != offered.channel {
|
||||
panic!("Error: channels do not match");
|
||||
} else if current.client != offered.client {
|
||||
panic!("Error: clients do not match");
|
||||
} else if current.publisher != offered.publisher {
|
||||
panic!("Error: publishers do not match");
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
fn get_current_os_config() -> Result<updates::CitadelVersionStruct> {
|
||||
let os_release = rs_release::parse_os_release(OS_RELEASE_PATH).context(format!(
|
||||
"Failed to get OS_RELEASE_PATH from {}",
|
||||
OS_RELEASE_PATH
|
||||
))?;
|
||||
|
||||
// We need to get the version of the rootfs, kernel and extra images
|
||||
// We first check if there is an env var called UPDATES_CLIENT
|
||||
let client = match std::env::var("UPDATES_CLIENT") {
|
||||
Ok(a) => a,
|
||||
Err(_) => os_release
|
||||
.get("CLIENT")
|
||||
.unwrap_or(&LAST_RESORT_CLIENT.to_string())
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
let channel = os_release.get("CITADEL_CHANNEL");
|
||||
|
||||
// Search image dir for kernel image and extract version
|
||||
let mut kernel_version = String::from("0.0.0");
|
||||
for path in glob::glob(&format!("{}citadel-kernel*.img", IMAGE_DIRECTORY_PATH))
|
||||
.expect("Failed to read glob pattern")
|
||||
{
|
||||
match path {
|
||||
Ok(path) => {
|
||||
kernel_version = get_image_version(&path).context(format!(
|
||||
"Failed to get image version at path: {}",
|
||||
path.display()
|
||||
))?
|
||||
}
|
||||
Err(e) => anyhow::bail!("Failed to find kernel image. Error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
let rootfs_version = get_image_version(&ROOTFS_IMAGE_PATH).context(format!(
|
||||
"Failed to get rootfs img version at path: {}",
|
||||
ROOTFS_IMAGE_PATH
|
||||
))?;
|
||||
let extra_version = get_image_version(&EXTRA_IMAGE_PATH).context(format!(
|
||||
"Failed to get extra img version at path: {}",
|
||||
EXTRA_IMAGE_PATH
|
||||
))?;
|
||||
|
||||
let publisher = os_release.get("CITADEL_PUBLISHER");
|
||||
|
||||
let mut component_version = Vec::new();
|
||||
component_version.push(updates::AvailableComponentVersion {
|
||||
component: updates::Component::Rootfs,
|
||||
version: rootfs_version.to_owned(),
|
||||
file_path: "".to_owned(),
|
||||
});
|
||||
component_version.push(updates::AvailableComponentVersion {
|
||||
component: updates::Component::Kernel,
|
||||
version: kernel_version.to_owned(),
|
||||
file_path: "".to_owned(),
|
||||
});
|
||||
component_version.push(updates::AvailableComponentVersion {
|
||||
component: updates::Component::Extra,
|
||||
version: extra_version.to_owned(),
|
||||
file_path: "".to_owned(),
|
||||
});
|
||||
|
||||
let current_version_struct = updates::CitadelVersionStruct {
|
||||
client: client.to_owned(),
|
||||
channel: channel.unwrap_or(&"prod".to_owned()).to_owned(),
|
||||
component_version,
|
||||
publisher: publisher.unwrap_or(&"Subgraph".to_string()).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",
|
||||
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 prompt_user_for_permission_to_download(
|
||||
component: &updates::AvailableComponentVersion,
|
||||
) -> Result<bool> {
|
||||
println!(
|
||||
"Would you like to download the new version of the {} image with version {}? (y/n)",
|
||||
component.component, component.version
|
||||
);
|
||||
|
||||
loop {
|
||||
let stdin = std::io::stdin();
|
||||
let mut user_input = String::new();
|
||||
|
||||
stdin.read_line(&mut user_input)?;
|
||||
|
||||
if user_input.trim() == "y" {
|
||||
return Ok(true);
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn download_file(path: &str) -> Result<std::path::PathBuf> {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
|
||||
let url = format!("https://{}/{}", UPDATE_SERVER_HOSTNAME, path);
|
||||
|
||||
println!("Downloading from {}", url);
|
||||
|
||||
let component_download_response = client.get(&url).send()?;
|
||||
|
||||
if !component_download_response.status().is_success() {
|
||||
anyhow::bail!(
|
||||
"Failed to download image from {}. Server returned error {}",
|
||||
path,
|
||||
component_download_response.status()
|
||||
);
|
||||
}
|
||||
|
||||
let path = Path::new(path);
|
||||
let path = format!("/tmp/{}", path.file_name().unwrap().to_str().unwrap());
|
||||
|
||||
let mut content = std::io::Cursor::new(component_download_response.bytes()?);
|
||||
let mut file =
|
||||
std::fs::File::create(&path).context(format!("Failed to create file at {}", path))?;
|
||||
std::io::copy(&mut content, &mut file)?;
|
||||
|
||||
println!("Saved file to {}", path);
|
||||
|
||||
Ok(std::path::PathBuf::from(path))
|
||||
}
|
49
citadel-tool/src/fetch/mod.rs
Normal file
49
citadel-tool/src/fetch/mod.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use clap::{arg, command, ArgAction, Command};
|
||||
use std::process::exit;
|
||||
|
||||
mod fetch;
|
||||
|
||||
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("download")
|
||||
.about("Download a specific file 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 all components found on the server to be more recent than currently installed on 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),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let result = match matches.subcommand() {
|
||||
Some(("check", _sub_matches)) => fetch::check(),
|
||||
Some(("download", sub_matches)) => fetch::download(sub_matches),
|
||||
Some(("read-remote", _sub_matches)) => fetch::read_remote(),
|
||||
Some(("upgrade", _sub_matches)) => fetch::upgrade(),
|
||||
Some(("reinstall", sub_matches)) => fetch::reinstall(sub_matches),
|
||||
_ => unreachable!("Please pass a subcommand"),
|
||||
};
|
||||
|
||||
if let Err(ref e) = result {
|
||||
println!("Error: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
@ -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() {
|
||||
@ -39,6 +40,8 @@ fn main() {
|
||||
realmfs::main();
|
||||
} else if exe == Path::new("/usr/bin/citadel-update") {
|
||||
update::main(args);
|
||||
} else if exe == Path::new("/usr/bin/citadel-fetch") {
|
||||
fetch::main();
|
||||
} else if exe == Path::new("/usr/libexec/citadel-desktop-sync") {
|
||||
sync::main(args);
|
||||
} else if exe == Path::new("/usr/libexec/citadel-run") {
|
||||
|
@ -38,15 +38,15 @@ impl UpdateBuilder {
|
||||
}
|
||||
|
||||
fn target_filename(&self) -> String {
|
||||
format!("citadel-{}-{}-{:03}.img", self.config.img_name(), self.config.channel(), self.config.version())
|
||||
format!("citadel-{}-{}-{:}.img", self.config.img_name(), self.config.channel(), self.config.version())
|
||||
}
|
||||
|
||||
fn build_filename(config: &BuildConfig) -> String {
|
||||
format!("citadel-{}-{}-{:03}", config.image_type(), config.channel(), config.version())
|
||||
format!("citadel-{}-{}-{}", config.image_type(), config.channel(), config.version())
|
||||
}
|
||||
|
||||
fn verity_filename(&self) -> String {
|
||||
format!("verity-hash-{}-{:03}", self.config.image_type(), self.config.version())
|
||||
format!("verity-hash-{}-{}", self.config.image_type(), self.config.version())
|
||||
}
|
||||
|
||||
pub fn build(&mut self) -> Result<()> {
|
||||
@ -154,7 +154,7 @@ impl UpdateBuilder {
|
||||
bail!("failed to compress {:?}: {}", self.image(), err);
|
||||
}
|
||||
// Rename back to original image_data filename
|
||||
util::rename(self.image().with_extension("xz"), self.image())?;
|
||||
util::rename(util::append_to_path(self.image(), ".xz"), self.image())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -217,7 +217,7 @@ impl UpdateBuilder {
|
||||
writeln!(v, "realmfs-name = \"{}\"", name)?;
|
||||
}
|
||||
writeln!(v, "channel = \"{}\"", self.config.channel())?;
|
||||
writeln!(v, "version = {}", self.config.version())?;
|
||||
writeln!(v, "version = \"{}\"", self.config.version())?;
|
||||
writeln!(v, "timestamp = \"{}\"", self.config.timestamp())?;
|
||||
writeln!(v, "nblocks = {}", self.nblocks.unwrap())?;
|
||||
writeln!(v, "shasum = \"{}\"", self.shasum.as_ref().unwrap())?;
|
||||
|
@ -9,7 +9,7 @@ pub struct BuildConfig {
|
||||
#[serde(rename = "image-type")]
|
||||
image_type: String,
|
||||
channel: String,
|
||||
version: usize,
|
||||
version: String,
|
||||
timestamp: String,
|
||||
source: String,
|
||||
#[serde(default)]
|
||||
@ -102,8 +102,8 @@ impl BuildConfig {
|
||||
self.realmfs_name.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
pub fn version(&self) -> usize {
|
||||
self.version
|
||||
pub fn version(&self) -> &str {
|
||||
&self.version
|
||||
}
|
||||
|
||||
pub fn channel(&self) -> &str {
|
||||
|
@ -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()?;
|
||||
|
@ -7,12 +7,10 @@
|
||||
<busconfig>
|
||||
<policy user="root">
|
||||
<allow own="com.subgraph.realms"/>
|
||||
<allow own="com.subgraph.Realms2"/>
|
||||
</policy>
|
||||
|
||||
<policy context="default">
|
||||
<allow send_destination="com.subgraph.realms"/>
|
||||
<allow send_destination="com.subgraph.Realms2"/>
|
||||
<allow send_destination="com.subgraph.realms"
|
||||
send_interface="org.freedesktop.DBus.Properties"/>
|
||||
<allow send_destination="com.subgraph.realms"
|
||||
|
@ -20,6 +20,9 @@ walkdir = "2"
|
||||
dbus = "0.6"
|
||||
posix-acl = "1.0.0"
|
||||
procfs = "0.12.0"
|
||||
semver = "1.0"
|
||||
anyhow = "1.0"
|
||||
clap = "4.5"
|
||||
|
||||
[dependencies.inotify]
|
||||
version = "0.8"
|
||||
|
@ -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 {
|
||||
|
@ -20,6 +20,7 @@ pub mod symlink;
|
||||
mod realm;
|
||||
pub mod terminal;
|
||||
mod system;
|
||||
pub mod updates;
|
||||
|
||||
pub mod flatpak;
|
||||
|
||||
|
@ -12,9 +12,7 @@ use dbus::{Connection, BusType, ConnectionItem, Message, Path};
|
||||
use inotify::{Inotify, WatchMask, WatchDescriptor, Event};
|
||||
|
||||
pub enum RealmEvent {
|
||||
Starting(Realm),
|
||||
Started(Realm),
|
||||
Stopping(Realm),
|
||||
Stopped(Realm),
|
||||
New(Realm),
|
||||
Removed(Realm),
|
||||
@ -24,9 +22,7 @@ pub enum RealmEvent {
|
||||
impl Display for RealmEvent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
RealmEvent::Starting(ref realm) => write!(f, "RealmStarting({})", realm.name()),
|
||||
RealmEvent::Started(ref realm) => write!(f, "RealmStarted({})", realm.name()),
|
||||
RealmEvent::Stopping(ref realm) => write!(f, "RealmStopping({})", realm.name()),
|
||||
RealmEvent::Stopped(ref realm) => write!(f, "RealmStopped({})", realm.name()),
|
||||
RealmEvent::New(ref realm) => write!(f, "RealmNew({})", realm.name()),
|
||||
RealmEvent::Removed(ref realm) => write!(f, "RealmRemoved({})", realm.name()),
|
||||
|
@ -194,13 +194,7 @@ impl RealmManager {
|
||||
return Ok(());
|
||||
}
|
||||
info!("Starting realm {}", realm.name());
|
||||
self.inner().events.send_event(RealmEvent::Starting(realm.clone()));
|
||||
if let Err(err) = self._start_realm(realm, &mut HashSet::new()) {
|
||||
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
self.inner().events.send_event(RealmEvent::Started(realm.clone()));
|
||||
self._start_realm(realm, &mut HashSet::new())?;
|
||||
|
||||
if !Realms::is_some_realm_current() {
|
||||
self.inner_mut().realms.set_realm_current(realm)
|
||||
@ -298,7 +292,6 @@ impl RealmManager {
|
||||
}
|
||||
|
||||
info!("Stopping realm {}", realm.name());
|
||||
self.inner().events.send_event(RealmEvent::Stopping(realm.clone()));
|
||||
|
||||
if realm.config().flatpak() {
|
||||
if let Err(err) = self.stop_gnome_software_sandbox(realm) {
|
||||
@ -307,12 +300,8 @@ impl RealmManager {
|
||||
}
|
||||
|
||||
realm.set_active(false);
|
||||
if let Err(err) = self.systemd.stop_realm(realm) {
|
||||
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
self.systemd.stop_realm(realm)?;
|
||||
realm.cleanup_rootfs();
|
||||
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
|
||||
|
||||
if realm.is_current() {
|
||||
self.choose_some_current_realm();
|
||||
|
@ -169,20 +169,6 @@ impl Realm {
|
||||
self.inner.write().unwrap()
|
||||
}
|
||||
|
||||
|
||||
pub fn start(&self) -> Result<()> {
|
||||
warn!("Realm({})::start()", self.name());
|
||||
self.manager().start_realm(self)
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<()> {
|
||||
self.manager().stop_realm(self)
|
||||
}
|
||||
|
||||
pub fn set_current(&self) -> Result<()> {
|
||||
self.manager().set_current_realm(self)
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.inner_mut().is_active()
|
||||
}
|
||||
|
@ -420,8 +420,11 @@ 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)
|
||||
|
97
libcitadel/src/updates.rs
Normal file
97
libcitadel/src/updates.rs
Normal file
@ -0,0 +1,97 @@
|
||||
use std::fmt;
|
||||
use std::slice::Iter;
|
||||
|
||||
pub const UPDATE_SERVER_HOSTNAME: &str = "update.subgraph.com";
|
||||
|
||||
/// This struct embeds the CitadelVersion datastruct as well as the cryptographic validation of the that information
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CryptoContainerFile {
|
||||
pub serialized_citadel_version: Vec<u8>, // we serialize CitadelVersion
|
||||
pub signature: String, // serialized CitadelVersion gets signed
|
||||
pub signatory: String, // name of org or person who holds the key
|
||||
}
|
||||
|
||||
/// This struct contains the entirety of the logical information needed to decide whether to update or not
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct CitadelVersionStruct {
|
||||
pub client: String,
|
||||
pub channel: String, // dev, prod ...
|
||||
pub component_version: Vec<AvailableComponentVersion>,
|
||||
pub publisher: String, // name of org or person who released this update
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CitadelVersionStruct {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} image with channel {} has components:\n",
|
||||
self.client, self.channel
|
||||
)?;
|
||||
|
||||
for i in &self.component_version {
|
||||
write!(
|
||||
f,
|
||||
"\n{} with version {} at location {}",
|
||||
i.component, i.version, i.file_path
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq, Ord)]
|
||||
pub struct AvailableComponentVersion {
|
||||
pub component: Component, // rootfs, kernel or extra
|
||||
pub version: String, // stored as semver
|
||||
pub file_path: String,
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
@ -338,7 +344,6 @@ pub fn is_euid_root() -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn utimes(path: &Path, atime: i64, mtime: i64) -> Result<()> {
|
||||
let cstr = CString::new(path.as_os_str().as_bytes())
|
||||
.expect("path contains null byte");
|
||||
|
@ -9,7 +9,7 @@ homepage = "https://subgraph.com"
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
rand = "0.8"
|
||||
zvariant = "4.2.0"
|
||||
zvariant = "2.7.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
zbus = "4.4.0"
|
||||
zbus = "=2.0.0-beta.5"
|
||||
gtk = { version = "0.14.0", features = ["v3_24"] }
|
||||
|
@ -1,10 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use zvariant::Type;
|
||||
use zbus::dbus_proxy;
|
||||
use zvariant::derive::Type;
|
||||
use serde::{Serialize,Deserialize};
|
||||
use zbus::proxy;
|
||||
use zbus::blocking::Connection;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
#[derive(Deserialize,Serialize,Type)]
|
||||
@ -85,12 +85,10 @@ impl RealmConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[proxy(
|
||||
#[dbus_proxy(
|
||||
default_service = "com.subgraph.realms",
|
||||
interface = "com.subgraph.realms.Manager",
|
||||
default_path = "/com/subgraph/realms",
|
||||
gen_blocking = true,
|
||||
gen_async = false,
|
||||
default_path = "/com/subgraph/realms"
|
||||
)]
|
||||
pub trait RealmsManager {
|
||||
fn get_current(&self) -> zbus::Result<String>;
|
||||
@ -104,7 +102,7 @@ pub trait RealmsManager {
|
||||
|
||||
impl RealmsManagerProxy<'_> {
|
||||
pub fn connect() -> Result<Self> {
|
||||
let connection = Connection::system()?;
|
||||
let connection = zbus::Connection::new_system()?;
|
||||
|
||||
let proxy = RealmsManagerProxy::new(&connection)
|
||||
.map_err(|_| Error::ManagerConnect)?;
|
||||
|
@ -6,11 +6,8 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
async-io = "2.3.2"
|
||||
blocking = "1.6.1"
|
||||
event-listener = "5.3.1"
|
||||
zbus = "4.4.0"
|
||||
zvariant = "4.2.0"
|
||||
zbus = "=2.0.0-beta.5"
|
||||
zvariant = "2.7.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_repr = "0.1.8"
|
||||
|
||||
|
@ -1,16 +1,15 @@
|
||||
use async_io::block_on;
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::SignalContext;
|
||||
use zbus::{Connection, ObjectServer};
|
||||
use crate::realms_manager::{RealmsManagerServer, REALMS_SERVER_OBJECT_PATH, realm_status};
|
||||
use libcitadel::{RealmEvent, Realm};
|
||||
|
||||
pub struct EventHandler {
|
||||
connection: Connection,
|
||||
realms_server: RealmsManagerServer,
|
||||
}
|
||||
|
||||
impl EventHandler {
|
||||
pub fn new(connection: Connection) -> Self {
|
||||
EventHandler { connection }
|
||||
pub fn new(connection: Connection, realms_server: RealmsManagerServer) -> Self {
|
||||
EventHandler { connection, realms_server }
|
||||
}
|
||||
|
||||
pub fn handle_event(&self, ev: &RealmEvent) {
|
||||
@ -26,49 +25,44 @@ impl EventHandler {
|
||||
RealmEvent::New(realm) => self.on_new(realm),
|
||||
RealmEvent::Removed(realm) => self.on_removed(realm),
|
||||
RealmEvent::Current(realm) => self.on_current(realm.as_ref()),
|
||||
RealmEvent::Starting(_) => Ok(()),
|
||||
RealmEvent::Stopping(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_signal_context<F>(&self, func: F) -> zbus::Result<()>
|
||||
fn with_server<F>(&self, func: F) -> zbus::Result<()>
|
||||
where
|
||||
F: Fn(&SignalContext) -> zbus::Result<()>,
|
||||
F: Fn(&RealmsManagerServer) -> zbus::Result<()>,
|
||||
{
|
||||
let object_server = self.connection.object_server();
|
||||
let iface = object_server.interface::<_, RealmsManagerServer>(REALMS_SERVER_OBJECT_PATH)?;
|
||||
|
||||
let ctx = iface.signal_context();
|
||||
func(ctx)
|
||||
let mut object_server = ObjectServer::new(&self.connection);
|
||||
object_server.at(REALMS_SERVER_OBJECT_PATH, self.realms_server.clone())?;
|
||||
object_server.with(REALMS_SERVER_OBJECT_PATH, |iface: &RealmsManagerServer| func(iface))
|
||||
}
|
||||
|
||||
fn on_started(&self, realm: &Realm) -> zbus::Result<()> {
|
||||
let pid_ns = realm.pid_ns().unwrap_or(0);
|
||||
let status = realm_status(realm);
|
||||
self.with_signal_context(|ctx| block_on(RealmsManagerServer::realm_started(ctx, realm.name(), pid_ns, status)))
|
||||
self.with_server(|server| server.realm_started(realm.name(), pid_ns, status))
|
||||
}
|
||||
|
||||
fn on_stopped(&self, realm: &Realm) -> zbus::Result<()> {
|
||||
let status = realm_status(realm);
|
||||
self.with_signal_context(|ctx| block_on(RealmsManagerServer::realm_stopped(ctx, realm.name(), status)))
|
||||
self.with_server(|server| server.realm_stopped(realm.name(), status))
|
||||
}
|
||||
|
||||
fn on_new(&self, realm: &Realm) -> zbus::Result<()> {
|
||||
let status = realm_status(realm);
|
||||
let description = realm.notes().unwrap_or(String::new());
|
||||
self.with_signal_context(|ctx| block_on(RealmsManagerServer::realm_new(ctx, realm.name(), &description, status)))
|
||||
self.with_server(|server| server.realm_new(realm.name(), &description, status))
|
||||
}
|
||||
|
||||
fn on_removed(&self, realm: &Realm) -> zbus::Result<()> {
|
||||
self.with_signal_context(|ctx| block_on(RealmsManagerServer::realm_removed(ctx, realm.name())))
|
||||
self.with_server(|server| server.realm_removed(realm.name()))
|
||||
}
|
||||
|
||||
fn on_current(&self, realm: Option<&Realm>) -> zbus::Result<()> {
|
||||
self.with_signal_context(|ctx| {
|
||||
self.with_server(|server| {
|
||||
match realm {
|
||||
Some(realm) => block_on(RealmsManagerServer::realm_current(ctx, realm.name(), realm_status(realm))),
|
||||
None => block_on(RealmsManagerServer::realm_current(ctx, "", 0)),
|
||||
|
||||
Some(realm) => server.realm_current(realm.name(), realm_status(realm)),
|
||||
None => server.realm_current("", 0),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,18 +1,14 @@
|
||||
#[macro_use] extern crate libcitadel;
|
||||
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use event_listener::{Event, Listener};
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::fdo::ObjectManager;
|
||||
use libcitadel::{Logger, LogLevel, Result, RealmManager};
|
||||
use crate::next::{RealmsManagerServer2, REALMS2_SERVER_OBJECT_PATH};
|
||||
use crate::realms_manager::{RealmsManagerServer, REALMS_SERVER_OBJECT_PATH};
|
||||
use zbus::{Connection, fdo};
|
||||
|
||||
use libcitadel::{Logger, LogLevel, Result};
|
||||
|
||||
use crate::realms_manager::RealmsManagerServer;
|
||||
|
||||
mod realms_manager;
|
||||
mod events;
|
||||
|
||||
mod next;
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = run_realm_manager() {
|
||||
@ -20,43 +16,24 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
fn register_realms_manager_server(connection: &Connection, realm_manager: &Arc<RealmManager>, quit_event: &Arc<Event>) -> Result<()> {
|
||||
let server = RealmsManagerServer::load(&connection, realm_manager.clone(), quit_event.clone())
|
||||
.map_err(context!("Loading realms server"))?;
|
||||
connection.object_server().at(REALMS_SERVER_OBJECT_PATH, server).map_err(context!("registering realms manager object"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_realms2_manager_server(connection: &Connection, realm_manager: &Arc<RealmManager>, quit_event: &Arc<Event>) -> Result<()> {
|
||||
let server2 = RealmsManagerServer2::load(&connection, realm_manager.clone(), quit_event.clone())
|
||||
.map_err(context!("Loading realms2 server"))?;
|
||||
connection.object_server().at(REALMS2_SERVER_OBJECT_PATH, server2).map_err(context!("registering realms manager object"))?;
|
||||
connection.object_server().at(REALMS2_SERVER_OBJECT_PATH, ObjectManager).map_err(context!("registering ObjectManager"))?;
|
||||
Ok(())
|
||||
fn create_system_connection() -> zbus::Result<Connection> {
|
||||
let connection = zbus::Connection::new_system()?;
|
||||
fdo::DBusProxy::new(&connection)?.request_name("com.subgraph.realms", fdo::RequestNameFlags::AllowReplacement.into())?;
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
fn run_realm_manager() -> Result<()> {
|
||||
Logger::set_log_level(LogLevel::Verbose);
|
||||
|
||||
let testing = env::args().skip(1).any(|s| s == "--testing");
|
||||
let connection = create_system_connection()
|
||||
.map_err(context!("ZBus Connection error"))?;
|
||||
|
||||
let connection = Connection::system()
|
||||
.map_err(context!("ZBus Connection error"))?;
|
||||
let mut object_server = RealmsManagerServer::register(&connection)?;
|
||||
|
||||
loop {
|
||||
if let Err(err) = object_server.try_handle_next() {
|
||||
warn!("Error handling DBus message: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let realm_manager = RealmManager::load()?;
|
||||
let quit_event = Arc::new(Event::new());
|
||||
|
||||
if testing {
|
||||
register_realms2_manager_server(&connection, &realm_manager, &quit_event)?;
|
||||
connection.request_name("com.subgraph.Realms2")
|
||||
.map_err(context!("acquiring realms manager name"))?;
|
||||
} else {
|
||||
register_realms_manager_server(&connection, &realm_manager, &quit_event)?;
|
||||
register_realms2_manager_server(&connection, &realm_manager, &quit_event)?;
|
||||
connection.request_name("com.subgraph.realms")
|
||||
.map_err(context!("acquiring realms manager name"))?;
|
||||
};
|
||||
quit_event.listen().wait();
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,201 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zbus::fdo;
|
||||
use zvariant::Type;
|
||||
use libcitadel::{OverlayType, Realm, GLOBAL_CONFIG};
|
||||
use libcitadel::terminal::Base16Scheme;
|
||||
use crate::next::manager::failed;
|
||||
|
||||
const BOOL_CONFIG_VARS: &[&str] = &[
|
||||
"use-gpu", "use-wayland", "use-x11", "use-sound",
|
||||
"use-shared-dir", "use-network", "use-kvm", "use-ephemeral-home",
|
||||
"use-media-dir", "use-fuse", "use-flatpak", "use-gpu-card0"
|
||||
];
|
||||
|
||||
fn is_bool_config_variable(variable: &str) -> bool {
|
||||
BOOL_CONFIG_VARS.iter().any(|&s| s == variable)
|
||||
}
|
||||
|
||||
#[derive(Deserialize,Serialize,Type)]
|
||||
pub struct RealmConfigVars {
|
||||
items: HashMap<String,String>,
|
||||
}
|
||||
|
||||
impl RealmConfigVars {
|
||||
fn new() -> Self {
|
||||
RealmConfigVars { items: HashMap::new() }
|
||||
}
|
||||
|
||||
pub fn new_global() -> Self {
|
||||
Self::new_from_config(&GLOBAL_CONFIG)
|
||||
}
|
||||
|
||||
fn new_from_realm(realm: &Realm) -> Self {
|
||||
let config = realm.config();
|
||||
Self::new_from_config(&config)
|
||||
}
|
||||
|
||||
fn new_from_config(config: &libcitadel::RealmConfig) -> Self {
|
||||
let mut vars = RealmConfigVars::new();
|
||||
vars.add_bool("use-gpu", config.gpu());
|
||||
vars.add_bool("use-gpu-card0", config.gpu_card0());
|
||||
vars.add_bool("use-wayland", config.wayland());
|
||||
vars.add_bool("use-x11", config.x11());
|
||||
vars.add_bool("use-sound", config.sound());
|
||||
vars.add_bool("use-shared-dir", config.shared_dir());
|
||||
vars.add_bool("use-network", config.network());
|
||||
vars.add_bool("use-kvm", config.kvm());
|
||||
vars.add_bool("use-ephemeral-home", config.ephemeral_home());
|
||||
vars.add_bool("use-media-dir", config.media_dir());
|
||||
vars.add_bool("use-fuse", config.fuse());
|
||||
vars.add_bool("use-flatpak", config.flatpak());
|
||||
|
||||
let overlay = match config.overlay() {
|
||||
OverlayType::None => "none",
|
||||
OverlayType::TmpFS => "tmpfs",
|
||||
OverlayType::Storage => "storage",
|
||||
};
|
||||
vars.add("overlay", overlay);
|
||||
|
||||
let scheme = match config.terminal_scheme() {
|
||||
Some(name) => name.to_string(),
|
||||
None => String::new(),
|
||||
};
|
||||
vars.add("terminal-scheme", scheme);
|
||||
vars.add("realmfs", config.realmfs());
|
||||
vars
|
||||
}
|
||||
|
||||
fn add_bool(&mut self, name: &str, val: bool) {
|
||||
let valstr = if val { "true".to_string() } else { "false".to_string() };
|
||||
self.add(name, valstr);
|
||||
}
|
||||
|
||||
fn add<S,T>(&mut self, k: S, v: T) where S: Into<String>, T: Into<String> {
|
||||
self.items.insert(k.into(), v.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealmConfig {
|
||||
realm: Realm,
|
||||
changed: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl RealmConfig {
|
||||
pub fn new(realm: Realm) -> Self {
|
||||
let changed = Arc::new(AtomicBool::new(false));
|
||||
RealmConfig { realm, changed }
|
||||
}
|
||||
|
||||
fn mark_changed(&self) {
|
||||
self.changed.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn is_changed(&self) -> bool {
|
||||
self.changed.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn config_vars(&self) -> RealmConfigVars {
|
||||
RealmConfigVars::new_from_realm(&self.realm)
|
||||
}
|
||||
|
||||
fn set_bool_var(&mut self, var: &str, value: &str) -> fdo::Result<()> {
|
||||
let v = match value {
|
||||
"true" => true,
|
||||
"false" => false,
|
||||
_ => return failed(format!("Invalid boolean value '{}' for realm config variable '{}'", value, var)),
|
||||
};
|
||||
|
||||
let mut has_changed = true;
|
||||
self.realm.with_mut_config(|c| {
|
||||
match var {
|
||||
"use-gpu" if c.gpu() != v => c.use_gpu = Some(v),
|
||||
_ => has_changed = false,
|
||||
}
|
||||
});
|
||||
if has_changed {
|
||||
self.mark_changed();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_overlay(&mut self, value: &str) -> fdo::Result<()> {
|
||||
let val = match value {
|
||||
"tmpfs" => Some("tmpfs".to_string()),
|
||||
"storage" => Some("storage".to_string()),
|
||||
"none" => None,
|
||||
_ => return failed(format!("Invalid value '{}' for overlay config", value)),
|
||||
};
|
||||
if self.realm.config().overlay != val {
|
||||
self.realm.with_mut_config(|c| {
|
||||
c.overlay = Some(value.to_string());
|
||||
});
|
||||
self.mark_changed();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_terminal_scheme(&mut self, value: &str) -> fdo::Result<()> {
|
||||
if Some(value) == self.realm.config().terminal_scheme() {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
let scheme = match Base16Scheme::by_name(value) {
|
||||
Some(scheme) => scheme,
|
||||
None => return failed(format!("Invalid terminal color scheme '{}'", value)),
|
||||
};
|
||||
|
||||
let manager = self.realm.manager();
|
||||
if let Err(err) = scheme.apply_to_realm(&manager, &self.realm) {
|
||||
return failed(format!("Error applying terminal color scheme: {}", err));
|
||||
}
|
||||
|
||||
self.realm.with_mut_config(|c| {
|
||||
c.terminal_scheme = Some(value.to_string());
|
||||
});
|
||||
self.mark_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_realmfs(&mut self, value: &str) -> fdo::Result<()> {
|
||||
let manager = self.realm.manager();
|
||||
if manager.realmfs_by_name(value).is_none() {
|
||||
return failed(format!("Failed to set 'realmfs' config for realm-{}: RealmFS named '{}' does not exist", self.realm.name(), value));
|
||||
}
|
||||
if self.realm.config().realmfs() != value {
|
||||
self.realm.with_mut_config(|c| {
|
||||
c.realmfs = Some(value.to_string())
|
||||
});
|
||||
self.mark_changed();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_config(&self) -> fdo::Result<()> {
|
||||
if self.is_changed() {
|
||||
self.realm.config()
|
||||
.write()
|
||||
.map_err(|err| fdo::Error::Failed(format!("Error writing config file for realm-{}: {}", self.realm.name(), err)))?;
|
||||
|
||||
self.changed.store(false, Ordering::Relaxed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_var(&mut self, var: &str, value: &str) -> fdo::Result<()> {
|
||||
if is_bool_config_variable(var) {
|
||||
self.set_bool_var(var, value)
|
||||
} else if var == "overlay" {
|
||||
self.set_overlay(value)
|
||||
} else if var == "terminal-scheme" {
|
||||
self.set_terminal_scheme(value)
|
||||
} else if var == "realmfs" {
|
||||
self.set_realmfs(value)
|
||||
} else {
|
||||
failed(format!("Unknown realm configuration variable '{}'", var))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use blocking::unblock;
|
||||
use event_listener::{Event, EventListener};
|
||||
use serde::Serialize;
|
||||
use serde_repr::Serialize_repr;
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::{fdo, interface};
|
||||
use zvariant::Type;
|
||||
use libcitadel::{PidLookupResult, RealmManager};
|
||||
use crate::next::config::RealmConfigVars;
|
||||
use crate::next::realm::RealmItemState;
|
||||
use crate::next::realmfs::RealmFSState;
|
||||
|
||||
pub fn failed<T>(message: String) -> fdo::Result<T> {
|
||||
Err(fdo::Error::Failed(message))
|
||||
}
|
||||
|
||||
#[derive(Serialize_repr, Type, Debug, PartialEq)]
|
||||
#[repr(u32)]
|
||||
pub enum PidLookupResultCode {
|
||||
Unknown = 1,
|
||||
Realm = 2,
|
||||
Citadel = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Type, Serialize)]
|
||||
pub struct RealmFromCitadelPid {
|
||||
code: PidLookupResultCode,
|
||||
realm: String,
|
||||
}
|
||||
|
||||
impl From<PidLookupResult> for RealmFromCitadelPid {
|
||||
fn from(result: PidLookupResult) -> Self {
|
||||
match result {
|
||||
PidLookupResult::Unknown => RealmFromCitadelPid { code: PidLookupResultCode::Unknown, realm: String::new() },
|
||||
PidLookupResult::Realm(realm) => RealmFromCitadelPid { code: PidLookupResultCode::Realm, realm: realm.name().to_string() },
|
||||
PidLookupResult::Citadel => RealmFromCitadelPid { code: PidLookupResultCode::Citadel, realm: String::new() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RealmsManagerServer2 {
|
||||
realms: RealmItemState,
|
||||
realmfs_state: RealmFSState,
|
||||
manager: Arc<RealmManager>,
|
||||
quit_event: Arc<Event>,
|
||||
}
|
||||
|
||||
|
||||
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());
|
||||
RealmsManagerServer2 {
|
||||
realms,
|
||||
realmfs_state,
|
||||
manager,
|
||||
quit_event,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(connection: &Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> zbus::Result<Self> {
|
||||
let mut 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)?;
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[interface(name = "com.subgraph.realms.Manager2")]
|
||||
impl RealmsManagerServer2 {
|
||||
|
||||
async fn get_current(&self) -> u32 {
|
||||
|
||||
self.realms.get_current()
|
||||
.map(|r| r.index())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
async fn realm_from_citadel_pid(&self, pid: u32) -> RealmFromCitadelPid {
|
||||
let manager = self.manager.clone();
|
||||
unblock(move || {
|
||||
manager.realm_by_pid(pid).into()
|
||||
}).await
|
||||
}
|
||||
|
||||
async fn create_realm(&self, name: &str) -> fdo::Result<()> {
|
||||
let manager = self.manager.clone();
|
||||
let name = name.to_string();
|
||||
unblock(move || {
|
||||
let _ = manager.new_realm(&name).map_err(|err| fdo::Error::Failed(err.to_string()))?;
|
||||
Ok(())
|
||||
}).await
|
||||
}
|
||||
|
||||
async fn get_global_config(&self) -> RealmConfigVars {
|
||||
RealmConfigVars::new_global()
|
||||
}
|
||||
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
|
||||
mod manager;
|
||||
mod config;
|
||||
mod realm;
|
||||
mod realmfs;
|
||||
|
||||
pub use manager::RealmsManagerServer2;
|
||||
pub const REALMS2_SERVER_OBJECT_PATH: &str = "/com/subgraph/Realms2";
|
@ -1,374 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
|
||||
use blocking::unblock;
|
||||
use zbus::{interface, fdo};
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::names::{BusName, InterfaceName};
|
||||
use zvariant::{OwnedObjectPath, Value};
|
||||
use libcitadel::{Realm, RealmEvent, RealmManager, Result};
|
||||
use crate::next::config::{RealmConfig, RealmConfigVars};
|
||||
use crate::next::realmfs::RealmFSState;
|
||||
use crate::next::REALMS2_SERVER_OBJECT_PATH;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealmItem {
|
||||
path: String,
|
||||
index: u32,
|
||||
realm: Realm,
|
||||
config: RealmConfig,
|
||||
in_run_transition: Arc<AtomicBool>,
|
||||
realmfs_index: Arc<AtomicU32>,
|
||||
last_timestamp: Arc<AtomicI64>,
|
||||
}
|
||||
|
||||
#[derive(Copy,Clone)]
|
||||
#[repr(u32)]
|
||||
enum RealmRunStatus {
|
||||
Stopped = 0,
|
||||
Starting,
|
||||
Running,
|
||||
Current,
|
||||
Stopping,
|
||||
}
|
||||
|
||||
impl RealmRunStatus {
|
||||
fn for_realm(realm: &Realm, in_transition: bool) -> Self {
|
||||
if in_transition {
|
||||
if realm.is_active() { Self::Stopping } else { Self::Starting }
|
||||
} else if realm.is_active() {
|
||||
if realm.is_current() { Self::Current } else {Self::Running }
|
||||
} else {
|
||||
Self::Stopped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RealmItem {
|
||||
pub(crate) fn new_from_realm(index: u32, realm: Realm) -> 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 last_timestamp = Arc::new(AtomicI64::new(realm.timestamp()));
|
||||
RealmItem { path, index, realm, config, in_run_transition, realmfs_index, last_timestamp }
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn index(&self) -> u32 {
|
||||
self.index
|
||||
}
|
||||
|
||||
fn in_run_transition(&self) -> bool {
|
||||
self.in_run_transition.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn get_run_status(&self) -> RealmRunStatus {
|
||||
RealmRunStatus::for_realm(&self.realm, self.in_run_transition())
|
||||
}
|
||||
|
||||
async fn do_start(&mut self) -> fdo::Result<()> {
|
||||
if !self.realm.is_active() {
|
||||
let realm = self.realm.clone();
|
||||
|
||||
let res = unblock(move || realm.start()).await;
|
||||
|
||||
if let Err(err) = res {
|
||||
return Err(fdo::Error::Failed(format!("Failed to start realm: {}", err)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn do_stop(&mut self) -> fdo::Result<()> {
|
||||
if self.realm.is_active() {
|
||||
let realm = self.realm.clone();
|
||||
|
||||
let res = unblock(move || realm.stop()).await;
|
||||
|
||||
if let Err(err) = res {
|
||||
return Err(fdo::Error::Failed(format!("Failed to stop realm: {}", err)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[interface(
|
||||
name = "com.subgraph.realms.Realm"
|
||||
)]
|
||||
impl RealmItem {
|
||||
|
||||
async fn start(
|
||||
&mut self,
|
||||
) -> fdo::Result<()> {
|
||||
|
||||
self.do_start().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop(
|
||||
&mut self,
|
||||
) -> fdo::Result<()> {
|
||||
self.do_stop().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn restart(
|
||||
&mut self,
|
||||
) -> fdo::Result<()> {
|
||||
self.do_stop().await?;
|
||||
self.do_start().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_current(&mut self) -> fdo::Result<()> {
|
||||
let realm = self.realm.clone();
|
||||
let res = unblock(move || realm.set_current()).await;
|
||||
if let Err(err) = res {
|
||||
return Err(fdo::Error::Failed(format!("Failed to set realm {} as current: {}", self.realm.name(), err)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_config(&self) -> RealmConfigVars {
|
||||
self.config.config_vars()
|
||||
}
|
||||
|
||||
async fn set_config(&mut self, vars: Vec<(String, String)>) -> fdo::Result<()> {
|
||||
for (var, val) in &vars {
|
||||
self.config.set_var(var, val)?;
|
||||
}
|
||||
let config = self.config.clone();
|
||||
unblock(move || config.save_config()).await
|
||||
}
|
||||
|
||||
#[zbus(property, name = "RunStatus")]
|
||||
fn run_status(&self) -> u32 {
|
||||
self.get_run_status() as u32
|
||||
}
|
||||
|
||||
#[zbus(property, name="IsSystemRealm")]
|
||||
fn is_system_realm(&self) -> bool {
|
||||
self.realm.is_system()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Name")]
|
||||
fn name(&self) -> &str {
|
||||
self.realm.name()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Description")]
|
||||
fn description(&self) -> String {
|
||||
self.realm.notes()
|
||||
.unwrap_or(String::new())
|
||||
}
|
||||
|
||||
#[zbus(property, name = "PidNS")]
|
||||
fn pid_ns(&self) -> u64 {
|
||||
self.realm.pid_ns().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "RealmFS")]
|
||||
fn realmfs(&self) -> u32 {
|
||||
self.realmfs_index.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Timestamp")]
|
||||
fn timestamp(&self) -> u64 {
|
||||
self.realm.timestamp() as u64
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[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,120 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::{fdo, interface};
|
||||
use zvariant::{ObjectPath, OwnedObjectPath};
|
||||
use libcitadel::{RealmFS, RealmManager};
|
||||
use crate::next::REALMS2_SERVER_OBJECT_PATH;
|
||||
|
||||
const BLOCK_SIZE: u64 = 4096;
|
||||
#[derive(Clone)]
|
||||
pub struct RealmFSItem {
|
||||
object_path: OwnedObjectPath,
|
||||
index: u32,
|
||||
realmfs: RealmFS,
|
||||
}
|
||||
impl RealmFSItem {
|
||||
pub(crate) fn new_from_realmfs(index: u32, realmfs: RealmFS) -> RealmFSItem {
|
||||
let object_path = format!("{}/RealmFS{}", REALMS2_SERVER_OBJECT_PATH, index).try_into().unwrap();
|
||||
RealmFSItem {
|
||||
object_path,
|
||||
index,
|
||||
realmfs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index(&self) -> u32 {
|
||||
self.index
|
||||
}
|
||||
|
||||
pub fn object_path(&self) -> ObjectPath {
|
||||
self.object_path.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[interface(
|
||||
name = "com.subgraph.realms.RealmFS"
|
||||
)]
|
||||
impl RealmFSItem {
|
||||
|
||||
#[zbus(property, name = "Name")]
|
||||
fn name(&self) -> &str {
|
||||
self.realmfs.name()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Activated")]
|
||||
fn activated(&self) -> bool {
|
||||
self.realmfs.is_activated()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "InUse")]
|
||||
fn in_use(&self) -> bool {
|
||||
self.realmfs.is_activated()
|
||||
}
|
||||
#[zbus(property, name = "Mountpoint")]
|
||||
fn mountpoint(&self) -> String {
|
||||
self.realmfs.mountpoint().to_string()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Path")]
|
||||
fn path(&self) -> String {
|
||||
format!("{}", self.realmfs.path().display())
|
||||
}
|
||||
|
||||
#[zbus(property, name = "FreeSpace")]
|
||||
fn free_space(&self) -> fdo::Result<u64> {
|
||||
let blocks = self.realmfs.free_size_blocks()
|
||||
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
|
||||
Ok(blocks as u64 * BLOCK_SIZE)
|
||||
}
|
||||
|
||||
#[zbus(property, name = "AllocatedSpace")]
|
||||
fn allocated_space(&self) -> fdo::Result<u64> {
|
||||
let blocks = self.realmfs.allocated_size_blocks()
|
||||
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
|
||||
Ok(blocks as u64 * BLOCK_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RealmFSState {
|
||||
connection: Connection,
|
||||
next_index: u32,
|
||||
items: HashMap<String, RealmFSItem>,
|
||||
}
|
||||
|
||||
impl RealmFSState {
|
||||
pub fn new(connection: Connection) -> Self {
|
||||
RealmFSState {
|
||||
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(())
|
||||
}
|
||||
pub fn realmfs_by_name(&self, name: &str) -> Option<&RealmFSItem> {
|
||||
let res = self.items.get(name);
|
||||
if res.is_none() {
|
||||
warn!("Failed to find RealmFS with name '{}'", name);
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
@ -1,14 +1,11 @@
|
||||
use libcitadel::{RealmManager, Realm, OverlayType, Result, PidLookupResult};
|
||||
use std::sync::Arc;
|
||||
use zbus::blocking::Connection;
|
||||
use zvariant::Type;
|
||||
use zbus::{dbus_interface, ObjectServer,Connection};
|
||||
use zvariant::derive::Type;
|
||||
use std::thread;
|
||||
use std::collections::HashMap;
|
||||
use blocking::unblock;
|
||||
use event_listener::{Event, EventListener};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde::{Serialize,Deserialize};
|
||||
use serde_repr::Serialize_repr;
|
||||
use zbus::{interface, SignalContext};
|
||||
use crate::events::EventHandler;
|
||||
use libcitadel::terminal::Base16Scheme;
|
||||
|
||||
@ -42,7 +39,6 @@ impl From<PidLookupResult> for RealmFromCitadelPid {
|
||||
#[derive(Clone)]
|
||||
pub struct RealmsManagerServer {
|
||||
manager: Arc<RealmManager>,
|
||||
quit_event: Arc<Event>,
|
||||
}
|
||||
|
||||
const BOOL_CONFIG_VARS: &[&str] = &[
|
||||
@ -125,40 +121,40 @@ fn configure_realm(manager: &RealmManager, realm: &Realm, variable: &str, value:
|
||||
|
||||
impl RealmsManagerServer {
|
||||
|
||||
pub fn load(connection: &Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> Result<RealmsManagerServer> {
|
||||
let server = RealmsManagerServer { manager, quit_event };
|
||||
let events = EventHandler::new(connection.clone());
|
||||
server.manager.add_event_handler(move |ev| events.handle_event(ev));
|
||||
server.manager.start_event_task()?;
|
||||
Ok(server)
|
||||
fn register_events(&self, connection: &Connection) -> Result<()> {
|
||||
let events = EventHandler::new(connection.clone(), self.clone());
|
||||
self.manager.add_event_handler(move |ev| events.handle_event(ev));
|
||||
self.manager.start_event_task()
|
||||
}
|
||||
|
||||
pub fn register(connection: &Connection) -> Result<ObjectServer> {
|
||||
let manager = RealmManager::load()?;
|
||||
let iface = RealmsManagerServer { manager };
|
||||
iface.register_events(connection)?;
|
||||
let mut object_server = ObjectServer::new(connection);
|
||||
object_server.at(REALMS_SERVER_OBJECT_PATH, iface).map_err(context!("ZBus error"))?;
|
||||
Ok(object_server)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
#[interface(name = "com.subgraph.realms.Manager")]
|
||||
#[dbus_interface(name = "com.subgraph.realms.Manager")]
|
||||
impl RealmsManagerServer {
|
||||
|
||||
async fn set_current(&self, name: &str) {
|
||||
|
||||
let manager = self.manager.clone();
|
||||
let name = name.to_string();
|
||||
unblock(move || {
|
||||
if let Some(realm) = manager.realm_by_name(&name) {
|
||||
if let Err(err) = manager.set_current_realm(&realm) {
|
||||
warn!("set_current_realm({}) failed: {}", name, err);
|
||||
}
|
||||
fn set_current(&self, name: &str) {
|
||||
if let Some(realm) = self.manager.realm_by_name(name) {
|
||||
if let Err(err) = self.manager.set_current_realm(&realm) {
|
||||
warn!("set_current_realm({}) failed: {}", name, err);
|
||||
}
|
||||
}).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_current(&self) -> String {
|
||||
let manager = self.manager.clone();
|
||||
unblock(move || {
|
||||
match manager.current_realm() {
|
||||
Some(realm) => realm.name().to_string(),
|
||||
None => String::new(),
|
||||
}
|
||||
}).await
|
||||
fn get_current(&self) -> String {
|
||||
match self.manager.current_realm() {
|
||||
Some(realm) => realm.name().to_string(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn list(&self) -> Vec<RealmItem> {
|
||||
@ -253,12 +249,8 @@ impl RealmsManagerServer {
|
||||
});
|
||||
}
|
||||
|
||||
async fn realm_from_citadel_pid(&self, pid: u32) -> RealmFromCitadelPid {
|
||||
let manager = self.manager.clone();
|
||||
unblock(move || {
|
||||
manager.realm_by_pid(pid).into()
|
||||
|
||||
}).await
|
||||
fn realm_from_citadel_pid(&self, pid: u32) -> RealmFromCitadelPid {
|
||||
self.manager.realm_by_pid(pid).into()
|
||||
}
|
||||
|
||||
fn realm_config(&self, name: &str) -> RealmConfig {
|
||||
@ -269,7 +261,7 @@ impl RealmsManagerServer {
|
||||
RealmConfig::new_from_realm(&realm)
|
||||
}
|
||||
|
||||
async fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) {
|
||||
fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) {
|
||||
let realm = match self.manager.realm_by_name(name) {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
@ -278,12 +270,8 @@ impl RealmsManagerServer {
|
||||
},
|
||||
};
|
||||
|
||||
for var in vars {
|
||||
let manager = self.manager.clone();
|
||||
let realm = realm.clone();
|
||||
unblock( move || {
|
||||
configure_realm(&manager, &realm, &var.0, &var.1);
|
||||
}).await;
|
||||
for var in &vars {
|
||||
configure_realm(&self.manager, &realm, &var.0, &var.1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,18 +279,13 @@ impl RealmsManagerServer {
|
||||
Realm::is_valid_name(name) && self.manager.realm_by_name(name).is_some()
|
||||
}
|
||||
|
||||
async fn create_realm(&self, name: &str) -> bool {
|
||||
|
||||
let manager = self.manager.clone();
|
||||
let name = name.to_string();
|
||||
unblock(move || {
|
||||
if let Err(err) = manager.new_realm(&name) {
|
||||
warn!("Error creating realm ({}): {}", name, err);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}).await
|
||||
fn create_realm(&self, name: &str) -> bool {
|
||||
if let Err(err) = self.manager.new_realm(name) {
|
||||
warn!("Error creating realm ({}): {}", name, err);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn list_realm_f_s(&self) -> Vec<String> {
|
||||
@ -316,23 +299,23 @@ impl RealmsManagerServer {
|
||||
|
||||
}
|
||||
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_started(ctx: &SignalContext<'_>, realm: &str, pid_ns: u64, status: u8) -> zbus::Result<()>;
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_started(&self, realm: &str, pid_ns: u64, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_stopped(ctx: &SignalContext<'_>, realm: &str, status: u8) -> zbus::Result<()>;
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_stopped(&self, realm: &str, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_new(ctx: &SignalContext<'_>, realm: &str, description: &str, status: u8) -> zbus::Result<()>;
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_new(&self, realm: &str, description: &str, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_removed(ctx: &SignalContext<'_>, realm: &str) -> zbus::Result<()>;
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_removed(&self, realm: &str) -> zbus::Result<()> { Ok(()) }
|
||||
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_current(ctx: &SignalContext<'_>, realm: &str, status: u8) -> zbus::Result<()>;
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_current(&self, realm: &str, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||
|
||||
#[zbus(signal)]
|
||||
pub async fn service_started(ctx: &SignalContext<'_>) -> zbus::Result<()>;
|
||||
#[dbus_interface(signal)]
|
||||
pub fn service_started(&self) -> zbus::Result<()> { Ok(()) }
|
||||
|
||||
}
|
||||
|
||||
|
21
update-generator/Cargo.toml
Normal file
21
update-generator/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "update_generator"
|
||||
description = "Utility to create updates for citadel's components"
|
||||
version = "0.1.0"
|
||||
authors = ["isa <isa@subgraph.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
ed25519-dalek = {version = "2.1", features = ["rand_core", "pem"]}
|
||||
serde_cbor = "0.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_derive = "1.0"
|
||||
rand = "0.8"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
rpassword = "7.3"
|
||||
semver = "1.0"
|
||||
zeroize = "1.8"
|
||||
sodiumoxide = "0.2"
|
||||
anyhow = "1.0"
|
||||
glob = "0.3"
|
63
update-generator/README.md
Normal file
63
update-generator/README.md
Normal file
@ -0,0 +1,63 @@
|
||||
# Introduction
|
||||
|
||||
This code is used to produce the files needed to update citadel.
|
||||
The citadel-fetch command is installed in citadel and may be run manually to check for updates.
|
||||
|
||||
N.B.
|
||||
This update framework is at the moment not production ready. It defends against arbitrary installation and rollback attacks. It partially defends against endless data attacks.
|
||||
It does not defend against fast-forward or indefinite freeze attacks. This system is vulnerable to key compromise and does not provide a method to update the root key through the update mechanism.
|
||||
Further work needs to be done to prevent these issues. Consider using libraries from the Update Framework (TUF).
|
||||
|
||||
# Usage
|
||||
|
||||
Within the citadel-tools directory, run `cargo run --bin update_generator generate-keypair` to generate a keypair. Replace the existing public key file by moving update_server_key.pub to citadel/meta-citadel/recipes-citadel/citadel-config/files/citadel-fetch/update_server_key.pub .
|
||||
The private key is available in the same directory as keypair.priv.
|
||||
|
||||
Run `cargo run --bin update_generator create-signed-file -r 1.0.0 -k 6.5.3 -e 1.0.0` replacing the version numbers with yours and a version.cbor file will be generated.
|
||||
Alternatively, the `create-signed-file` can be run with no parameters in which case it will read the version number of the available images in build/images and generate a version.cbor automagically.
|
||||
|
||||
Finally, the files generated (version.cbor and images) must be uploaded/ Run `cargo run --bin update_generator upload-to-server` to automatically upload all the files to the server.
|
||||
To perform the upload manually, put this version.cbor file to the server at: {UPDATE_SERVER_NAME}/{CLIENT}/{CHANNEL}/. For example: https://update.subgraph.com/public/dev/ .
|
||||
Upload the image files to {UPDATE_SERVER_NAME}/{CLIENT}/{CHANNEL}/. For example: https://update.subgraph.com/public/dev/rootfs_1.0.0.img
|
||||
|
||||
More information is available in the `update_generator` help menu.
|
||||
|
||||
For debugging, testing and internal dev use, the config.toml file provides a way to change the path on the server where the files are saved by changing the CLIENT and CHANNEL.
|
||||
|
||||
# Basic structure
|
||||
|
||||
We host the files used by this program as a file server. The current hostname is update.subgraph.com.
|
||||
|
||||
An installed citadel server will automatically check for updates by reading the file at https://update.subgraph.com/{CLIENT}/{CHANNEL}/version.cbor
|
||||
|
||||
{CLIENT} will default to "public" but is read from the currently running system.
|
||||
|
||||
{CHANNEL} will default to "prod" but is read from the currently running system.
|
||||
|
||||
The version.cbor file is signed with Subgraph-controlled keys or equivalent client-controlled keys. The corresponding public key will be embedded in the rootfs image during build. The public key is called update_server_key.pub and is stored in /etc/citadel/ .
|
||||
|
||||
# Update File Structure
|
||||
|
||||
## version.cbor file
|
||||
|
||||
The version.cbor file is the only file read during update and contains all information required for the user's system to decide to update or not as well as where the update files are located on the server.
|
||||
This file is merely a container which provides a cryptographic guarantee that the serialized_citadel_version struct (which contains the actual information we need to disseminate) is authentic.
|
||||
|
||||
1. serialized_citadel_version (CitadelVersionStruct): the serialized data containing the actionable information we need to read to make decisions on the update
|
||||
2. signature: the ed25519 signature of the above serialized data
|
||||
3. signatory: the name of the org or person who produced the version.cbor file
|
||||
|
||||
|
||||
## CitadelVersionStruct
|
||||
|
||||
1. client: the Subgraph client making the request
|
||||
2. channel: whether this is a production release or other
|
||||
3. component_version (Vec<ComponentVersion>): a vector containing structures called ComponentVersion which contain the information on each component's version number and the location of the download file of the component
|
||||
4. publisher: the name of the org or person who released this update
|
||||
|
||||
|
||||
## ComponentVersion
|
||||
|
||||
1. component: the name of the image we may update. Either the rootfs, the kernel or the extra image
|
||||
2. version: a string which contains the semver describing the version of the component
|
||||
3. file_path: where on the update server can the file be downloaded from. This is relative to the domain name we are currently fetching from
|
530
update-generator/src/main.rs
Normal file
530
update-generator/src/main.rs
Normal file
@ -0,0 +1,530 @@
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use ed25519_dalek::pkcs8::DecodePublicKey;
|
||||
use ed25519_dalek::pkcs8::EncodePublicKey;
|
||||
use ed25519_dalek::Signature;
|
||||
use ed25519_dalek::Signer;
|
||||
use ed25519_dalek::SigningKey;
|
||||
use ed25519_dalek::VerifyingKey;
|
||||
use ed25519_dalek::KEYPAIR_LENGTH;
|
||||
use glob::glob;
|
||||
use libcitadel::updates::Component;
|
||||
use libcitadel::{updates, ImageHeader};
|
||||
use rand::rngs::OsRng;
|
||||
use sodiumoxide::crypto::pwhash;
|
||||
use sodiumoxide::crypto::secretbox;
|
||||
use sodiumoxide::crypto::stream;
|
||||
use std::env;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
const SALSA_NONCE: &[u8] = &[
|
||||
116, 138, 142, 103, 234, 105, 192, 48, 117, 29, 150, 214, 106, 116, 195, 64, 120, 251, 94, 20,
|
||||
212, 118, 125, 189,
|
||||
];
|
||||
|
||||
const PASSWORD_SALT: &[u8] = &[
|
||||
18, 191, 168, 237, 156, 199, 54, 43, 122, 165, 35, 9, 89, 106, 36, 209, 145, 161, 90, 2, 121,
|
||||
51, 242, 182, 14, 245, 47, 253, 237, 153, 251, 219,
|
||||
];
|
||||
|
||||
const LAST_RESORT_CLIENT: &str = "public";
|
||||
const LAST_RESORT_CHANNEL: &str = "prod";
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Generate the file we use to inform clients of updates")]
|
||||
#[command(author, about, long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[command(arg_required_else_help = true)]
|
||||
enum Commands {
|
||||
/// Generate a keypair to be used to sign version file. You will be asked to provide a mandatory password.
|
||||
GenerateKeypair {
|
||||
/// keypair filepath to save to
|
||||
#[arg(short, long)]
|
||||
keypair_filepath: Option<String>,
|
||||
},
|
||||
|
||||
/// Generate the complete cbor file. If no components are passed, generate by reading image of each component
|
||||
CreateSignedFile {
|
||||
/// rootfs image semver version
|
||||
#[arg(short, long)]
|
||||
rootfs_image_version: Option<String>,
|
||||
/// kernel image semver version
|
||||
#[arg(short, long)]
|
||||
kernel_image_version: Option<String>,
|
||||
#[arg(short, long)]
|
||||
extra_image_version: Option<String>,
|
||||
/// keypair filepath
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
path_keypair: Option<String>,
|
||||
/// command output complete filepath
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
versionfile_filepath: Option<String>,
|
||||
},
|
||||
|
||||
/// Verify that the version file is correctly signed
|
||||
VerifySignature {
|
||||
/// public key filepath
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
publickey_filepath: Option<String>,
|
||||
/// command output complete filepath
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
versionfile_filepath: Option<String>,
|
||||
},
|
||||
|
||||
UploadToServer {
|
||||
#[arg(long)]
|
||||
component: Option<updates::Component>,
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match &cli.command {
|
||||
Some(Commands::GenerateKeypair { keypair_filepath }) => {
|
||||
generate_keypair(keypair_filepath).context("Failed to generate keypair")?
|
||||
}
|
||||
Some(Commands::CreateSignedFile {
|
||||
path_keypair,
|
||||
rootfs_image_version,
|
||||
kernel_image_version,
|
||||
extra_image_version,
|
||||
versionfile_filepath,
|
||||
}) => create_signed_version_file(
|
||||
path_keypair,
|
||||
rootfs_image_version,
|
||||
kernel_image_version,
|
||||
extra_image_version,
|
||||
versionfile_filepath,
|
||||
)
|
||||
.context("Failed to create signed file")?,
|
||||
Some(Commands::VerifySignature {
|
||||
publickey_filepath,
|
||||
versionfile_filepath,
|
||||
}) => verify_version_signature(publickey_filepath, versionfile_filepath)
|
||||
.context("Failed to verify signature")?,
|
||||
Some(Commands::UploadToServer { component, path }) => {
|
||||
upload_components_to_server(component, path)
|
||||
.context("Failed to upload to the server")?
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_keypair(keypair_filepath: &Option<String>) -> Result<()> {
|
||||
// if the user did not pass a path, we save key files to current directory
|
||||
let keypair_filepath = &keypair_filepath.clone().unwrap_or_else(|| ".".to_string());
|
||||
let path = std::path::Path::new(keypair_filepath);
|
||||
|
||||
let mut password;
|
||||
|
||||
loop {
|
||||
// get passphrase used to encrypt key from user
|
||||
password = rpassword::prompt_password(
|
||||
"Please enter the passphrase we will use to encrypt the private key with: ",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let password_confirm = rpassword::prompt_password("Retype same passphrase: ").unwrap();
|
||||
|
||||
if password != password_confirm {
|
||||
println!("\nPassphrases did not match. Please try again")
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// generate keypair
|
||||
let mut csprng = OsRng;
|
||||
let signing_key: SigningKey = SigningKey::generate(&mut csprng);
|
||||
|
||||
let keypair_fp: PathBuf;
|
||||
let publickey_fp: PathBuf;
|
||||
|
||||
if path.is_dir() {
|
||||
keypair_fp = path.join("keypair.priv");
|
||||
publickey_fp = path.join("update_server_key.pub");
|
||||
} else {
|
||||
keypair_fp = path.to_path_buf();
|
||||
publickey_fp = path.parent().unwrap().join("update_server_key.pub");
|
||||
}
|
||||
|
||||
let mut keyfile = std::fs::File::create(&keypair_fp)?;
|
||||
let mut public_key = std::fs::File::create(&publickey_fp)?;
|
||||
|
||||
// encrypt private key
|
||||
let mut k = secretbox::Key([0; secretbox::KEYBYTES]);
|
||||
|
||||
let secretbox::Key(ref mut kb) = k;
|
||||
let password_hash = pwhash::derive_key(
|
||||
kb,
|
||||
password.as_bytes(),
|
||||
&sodiumoxide::crypto::pwhash::scryptsalsa208sha256::Salt::from_slice(PASSWORD_SALT)
|
||||
.unwrap(),
|
||||
pwhash::OPSLIMIT_INTERACTIVE,
|
||||
pwhash::MEMLIMIT_INTERACTIVE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let plaintext = signing_key.to_keypair_bytes();
|
||||
let key = sodiumoxide::crypto::stream::xsalsa20::Key::from_slice(password_hash)
|
||||
.expect("failed to unwrap key");
|
||||
let nonce = sodiumoxide::crypto::stream::xsalsa20::Nonce::from_slice(SALSA_NONCE)
|
||||
.expect("failed to unwrap nonce");
|
||||
|
||||
// encrypt the plaintext
|
||||
let ciphertext = stream::stream_xor(&plaintext, &nonce, &key);
|
||||
|
||||
keyfile.write_all(&ciphertext)?;
|
||||
public_key.write_all(
|
||||
&signing_key
|
||||
.verifying_key()
|
||||
.to_public_key_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
password.zeroize();
|
||||
|
||||
println!(
|
||||
"Generated the keypair and wrote to {}. The public key is here: {}",
|
||||
keypair_fp.display(),
|
||||
publickey_fp.display()
|
||||
);
|
||||
println!(
|
||||
"You may now move the public key from {} to the citadel build path at citadel/meta-citadel/recipes-citadel/citadel-config/files/citadel-fetch/update_server_key.pub",
|
||||
publickey_fp.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_signed_version_file(
|
||||
signing_key_path: &Option<String>,
|
||||
citadel_rootfs_version: &Option<String>,
|
||||
citadel_kernel_version: &Option<String>,
|
||||
citadel_extra_version: &Option<String>,
|
||||
versionfile_filepath: &Option<String>,
|
||||
) -> Result<()> {
|
||||
let rootfs_version = match citadel_rootfs_version {
|
||||
Some(v) => semver::Version::parse(v)
|
||||
.expect("Error: Failed to parse rootfs semver version")
|
||||
.to_string(),
|
||||
None => get_imageheader_version(&Component::Rootfs).unwrap_or(String::from("0.0.0")),
|
||||
};
|
||||
|
||||
let kernel_version = match citadel_kernel_version {
|
||||
Some(v) => semver::Version::parse(v)
|
||||
.expect("Error: Failed to parse kernel semver version")
|
||||
.to_string(),
|
||||
None => get_imageheader_version(&Component::Kernel).unwrap_or(String::from("0.0.0")),
|
||||
};
|
||||
|
||||
let extra_version = match citadel_extra_version {
|
||||
Some(v) => semver::Version::parse(v)
|
||||
.expect("Error: Failed to parse extra semver version")
|
||||
.to_string(),
|
||||
None => get_imageheader_version(&Component::Extra).unwrap_or(String::from("0.0.0")),
|
||||
};
|
||||
|
||||
let rootfs_path = get_component_path(&Component::Rootfs);
|
||||
let channel;
|
||||
|
||||
if rootfs_path.is_ok() {
|
||||
channel = match ImageHeader::from_file(rootfs_path?) {
|
||||
Ok(image_header) => image_header.metainfo().channel().to_string(),
|
||||
Err(_) => env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
|
||||
};
|
||||
} else {
|
||||
channel = env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string());
|
||||
}
|
||||
|
||||
let component_version_vector = vec![
|
||||
updates::AvailableComponentVersion {
|
||||
component: updates::Component::Rootfs,
|
||||
version: rootfs_version.clone(),
|
||||
file_path: format!(
|
||||
"{}/{}/{}_{}.img",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
channel,
|
||||
"rootfs",
|
||||
rootfs_version
|
||||
)
|
||||
.to_string(),
|
||||
},
|
||||
updates::AvailableComponentVersion {
|
||||
component: updates::Component::Kernel,
|
||||
version: kernel_version.clone(),
|
||||
file_path: format!(
|
||||
"{}/{}/{}_{}.img",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
channel,
|
||||
"kernel",
|
||||
kernel_version
|
||||
)
|
||||
.to_string(),
|
||||
},
|
||||
updates::AvailableComponentVersion {
|
||||
component: updates::Component::Extra,
|
||||
version: extra_version.clone(),
|
||||
file_path: format!(
|
||||
"{}/{}/{}_{}.img",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
channel,
|
||||
"extra",
|
||||
extra_version
|
||||
)
|
||||
.to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
// generate version file
|
||||
let citadel_version = updates::CitadelVersionStruct {
|
||||
client: env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
channel: channel.to_string(),
|
||||
component_version: component_version_vector,
|
||||
publisher: "Subgraph".to_string(),
|
||||
};
|
||||
|
||||
let fp = match signing_key_path {
|
||||
Some(fp) => Path::new(fp),
|
||||
None => Path::new("keypair.priv"),
|
||||
};
|
||||
|
||||
// serialized to cbor
|
||||
let serialized_citadel_version = serde_cbor::to_vec(&citadel_version)?;
|
||||
|
||||
// get signing_key_bytes from the file passed
|
||||
let mut keyfile = std::fs::File::open(&fp)?;
|
||||
let mut buf = [0; KEYPAIR_LENGTH];
|
||||
keyfile.read_exact(&mut buf)?;
|
||||
|
||||
// prompt user for keypair decryption password
|
||||
let mut password =
|
||||
rpassword::prompt_password("Please enter the passphrase used to decrypt the private key: ")
|
||||
.unwrap();
|
||||
|
||||
// decrypt private key
|
||||
let mut k = secretbox::Key([0; secretbox::KEYBYTES]);
|
||||
|
||||
let secretbox::Key(ref mut kb) = k;
|
||||
let password_hash = pwhash::derive_key(
|
||||
kb,
|
||||
password.as_bytes(),
|
||||
&sodiumoxide::crypto::pwhash::scryptsalsa208sha256::Salt::from_slice(PASSWORD_SALT)
|
||||
.unwrap(),
|
||||
pwhash::OPSLIMIT_INTERACTIVE,
|
||||
pwhash::MEMLIMIT_INTERACTIVE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let key = sodiumoxide::crypto::stream::xsalsa20::Key::from_slice(password_hash)
|
||||
.expect("failed to unwrap key");
|
||||
let nonce = sodiumoxide::crypto::stream::xsalsa20::Nonce::from_slice(SALSA_NONCE)
|
||||
.expect("failed to unwrap nonce");
|
||||
|
||||
// decrypt the ciphertext
|
||||
let plaintext = stream::stream_xor(&buf, &nonce, &key);
|
||||
|
||||
let signing_key = SigningKey::from_keypair_bytes(plaintext[0..64].try_into()?)?;
|
||||
|
||||
// sign serialized_citadel_version for inclusion in version_file
|
||||
let signature: Signature = signing_key.sign(&serialized_citadel_version);
|
||||
|
||||
// generate signature of citadel_version cbor format (signed)
|
||||
let version_file = updates::CryptoContainerFile {
|
||||
serialized_citadel_version,
|
||||
signature: signature.to_string(),
|
||||
signatory: "Subgraph".to_string(),
|
||||
};
|
||||
|
||||
let vf_fp = match versionfile_filepath {
|
||||
Some(vf_fp) => {
|
||||
if Path::new(vf_fp).is_dir() {
|
||||
Path::new(vf_fp).join("version.cbor")
|
||||
} else {
|
||||
Path::new(vf_fp).to_path_buf()
|
||||
}
|
||||
}
|
||||
None => Path::new("version.cbor").to_path_buf(),
|
||||
};
|
||||
|
||||
let outfile = std::fs::File::create(&vf_fp)?;
|
||||
|
||||
serde_cbor::to_writer(outfile, &version_file)?;
|
||||
|
||||
password.zeroize();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate that the completed version file correctly verifies given the self-embedded signature and public key
|
||||
fn verify_version_signature(
|
||||
pubkey_filepath: &Option<String>,
|
||||
versionfile_filepath: &Option<String>,
|
||||
) -> Result<()> {
|
||||
let pub_fp = match pubkey_filepath {
|
||||
Some(pub_fp) => Path::new(pub_fp),
|
||||
None => Path::new("update_server_key.pub"),
|
||||
};
|
||||
|
||||
let version_fp = match versionfile_filepath {
|
||||
Some(version_fp) => Path::new(version_fp),
|
||||
None => Path::new("version.cbor"),
|
||||
};
|
||||
|
||||
let mut pubkey_file = std::fs::File::open(&pub_fp)?;
|
||||
|
||||
let mut buf = String::new();
|
||||
pubkey_file.read_to_string(&mut buf)?;
|
||||
|
||||
let verifying_key = VerifyingKey::from_public_key_pem(&buf)?;
|
||||
|
||||
let version_file = &std::fs::File::open(version_fp)?;
|
||||
let crypto_container_struct: updates::CryptoContainerFile =
|
||||
serde_cbor::from_reader(version_file)?;
|
||||
|
||||
let citadel_version_struct: updates::CitadelVersionStruct =
|
||||
serde_cbor::from_slice(&crypto_container_struct.serialized_citadel_version)?;
|
||||
|
||||
let signature = ed25519_dalek::Signature::from_str(&crypto_container_struct.signature)?;
|
||||
|
||||
match verifying_key.verify_strict(
|
||||
&crypto_container_struct.serialized_citadel_version,
|
||||
&signature,
|
||||
) {
|
||||
Ok(_) => println!("Everythin ok. Signature verified correctly"),
|
||||
Err(e) => panic!(
|
||||
"Error: Signature was not able to be verified! Threw error: {}",
|
||||
e
|
||||
),
|
||||
}
|
||||
|
||||
println!(
|
||||
"The destructured version file contains the following information:\n{}",
|
||||
citadel_version_struct
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Make sure to add your ssh key to the "updates" user on the server
|
||||
fn send_with_scp(from: &PathBuf, to: &PathBuf) -> Result<()> {
|
||||
Command::new("scp")
|
||||
.arg("-P 22514")
|
||||
.arg(from)
|
||||
.arg(format!(
|
||||
"updates@{}:/updates/files/{}",
|
||||
updates::UPDATE_SERVER_HOSTNAME,
|
||||
to.to_string_lossy()
|
||||
))
|
||||
.spawn()
|
||||
.context("scp command failed to run")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_imageheader_version(component: &Component) -> Result<String> {
|
||||
let version = match ImageHeader::from_file(get_component_path(component)?) {
|
||||
Ok(image_header) => image_header.metainfo().version().to_string(),
|
||||
Err(_) => String::from("0.0.0"),
|
||||
};
|
||||
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
fn get_component_path(component: &updates::Component) -> Result<PathBuf> {
|
||||
let mut gl = match component {
|
||||
Component::Rootfs => glob("../build/images/citadel-rootfs*.img")?,
|
||||
Component::Kernel => glob("../build/images/citadel-kernel*.img")?,
|
||||
Component::Extra => glob("../build/images/citadel-extra*.img")?,
|
||||
};
|
||||
|
||||
let first = gl.nth(0).context(format!(
|
||||
"Failed to find citadel-{}*.img in ../build/images/",
|
||||
component
|
||||
))?;
|
||||
|
||||
Ok(PathBuf::from(first?))
|
||||
}
|
||||
|
||||
fn upload_all_components() -> Result<()> {
|
||||
for component in updates::Component::iterator() {
|
||||
upload_component(component, &None)?
|
||||
}
|
||||
upload_cbor()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn upload_component(component: &updates::Component, path: &Option<PathBuf>) -> Result<()> {
|
||||
let final_path;
|
||||
|
||||
// uggliest if statement i've ever written
|
||||
// if path was passed to this function
|
||||
if let Some(p) = path {
|
||||
final_path = p.to_path_buf();
|
||||
} else {
|
||||
// if the path wasn't passed to this function, search for the component path
|
||||
if let Ok(p) = get_component_path(&component) {
|
||||
final_path = p;
|
||||
} else {
|
||||
// if path was not passed and we failed to locate the component's path
|
||||
println!(
|
||||
"We failed to find the {} image we were looking for... Skipping...",
|
||||
component
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let image_header = libcitadel::ImageHeader::from_file(&final_path)?;
|
||||
|
||||
let to = PathBuf::from(format!(
|
||||
"{}/{}/{}_{}.img",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
|
||||
component,
|
||||
image_header.metainfo().version()
|
||||
));
|
||||
send_with_scp(&final_path, &to)
|
||||
}
|
||||
|
||||
// If no parameters are passed to this command, upload all components regardless of version on the server
|
||||
// If a path is passed, upload that image to the server regardless version.
|
||||
fn upload_components_to_server(
|
||||
component: &Option<updates::Component>,
|
||||
path: &Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
// check if a component is passed to this function:
|
||||
match component {
|
||||
Some(comp) => upload_component(comp, path)?, // This function handles the option of not passing a path
|
||||
None => upload_all_components()?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn upload_cbor() -> Result<()> {
|
||||
send_with_scp(
|
||||
&PathBuf::from("version.cbor"),
|
||||
&PathBuf::from(format!(
|
||||
"{}/{}/version.cbor",
|
||||
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||
env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
|
||||
)),
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user