forked from brl/citadel-tools
Compare commits
2 Commits
updates
...
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"
|
||||
|
2151
Cargo.lock
generated
2151
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
[workspace]
|
||||
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "launch-gnome-software", "update-realmfs" ]
|
||||
resolver = "2"
|
||||
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
@@ -21,8 +21,6 @@ use cursive::vec::Vec2;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::fs::File;
|
||||
use std::io::{self,BufWriter,Write};
|
||||
use std::os::fd::AsFd;
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
@@ -51,10 +49,19 @@ pub struct Backend {
|
||||
input_receiver: Receiver<TEvent>,
|
||||
resize_receiver: Receiver<()>,
|
||||
|
||||
tty_fd: OwnedFd,
|
||||
tty_fd: RawFd,
|
||||
input_thread: JoinHandle<()>,
|
||||
}
|
||||
|
||||
fn close_fd(fd: RawFd) -> io::Result<()> {
|
||||
unsafe {
|
||||
if libc::close(fd) != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pthread_kill(tid: libc::pthread_t, sig: libc::c_int) -> io::Result<()> {
|
||||
unsafe {
|
||||
if libc::pthread_kill(tid, sig) != 0 {
|
||||
@@ -91,7 +98,7 @@ impl Backend {
|
||||
// Read input from a separate thread
|
||||
|
||||
let input = std::fs::File::open("/dev/tty").unwrap();
|
||||
let tty_fd = input.as_fd().try_clone_to_owned().unwrap();
|
||||
let tty_fd = input.as_raw_fd();
|
||||
let input_thread = thread::spawn(move || {
|
||||
let mut events = input.events();
|
||||
|
||||
@@ -120,6 +127,12 @@ impl Backend {
|
||||
Ok(Box::new(c))
|
||||
}
|
||||
|
||||
fn close_tty(&self) {
|
||||
if let Err(e) = close_fd(self.tty_fd) {
|
||||
warn!("error closing tty fd: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn kill_thread(&self) {
|
||||
if let Err(e) = pthread_kill(self.input_thread.as_pthread_t(), libc::SIGWINCH) {
|
||||
warn!("error sending signal to input thread: {}", e);
|
||||
@@ -232,6 +245,7 @@ impl backend::Backend for Backend {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.close_tty();
|
||||
self.kill_thread();
|
||||
}
|
||||
|
||||
|
@@ -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"
|
@@ -138,6 +138,7 @@ fn compare_boot_partitions(a: Option<Partition>, b: Partition) -> Option<Partiti
|
||||
// Compare versions and channels
|
||||
let bind_a = a.metainfo();
|
||||
let bind_b = b.metainfo();
|
||||
|
||||
let a_v = bind_a.version();
|
||||
let b_v = bind_b.version();
|
||||
|
||||
|
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);
|
||||
}
|
||||
}
|
@@ -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,7 +38,7 @@ impl UpdateBuilder {
|
||||
}
|
||||
|
||||
fn target_filename(&self) -> String {
|
||||
format!("citadel-{}-{}-{}.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 {
|
||||
|
@@ -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());
|
||||
}
|
||||
|
5
data/com.subgraph.RealmConfig.desktop
Normal file
5
data/com.subgraph.RealmConfig.desktop
Normal file
@@ -0,0 +1,5 @@
|
||||
[Desktop Entry]
|
||||
Name=RealmConfig
|
||||
Type=Application
|
||||
Icon=org.gnome.Settings
|
||||
NoDisplay=true
|
@@ -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"
|
||||
|
@@ -21,6 +21,8 @@ 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"
|
||||
|
@@ -78,9 +78,3 @@ impl Display for Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<fmt::Error> for crate::Error {
|
||||
fn from(e: fmt::Error) -> Self {
|
||||
format_err!("Error formatting string: {}", e).into()
|
||||
}
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ pub mod symlink;
|
||||
mod realm;
|
||||
pub mod terminal;
|
||||
mod system;
|
||||
pub mod updates;
|
||||
|
||||
pub mod flatpak;
|
||||
|
||||
@@ -34,12 +35,10 @@ pub use crate::realmfs::{RealmFS,Mountpoint};
|
||||
pub use crate::keyring::{KeyRing,KernelKey};
|
||||
pub use crate::exec::{Exec,FileRange};
|
||||
pub use crate::realmfs::resizer::ResizeSize;
|
||||
pub use crate::realmfs::update::RealmFSUpdate;
|
||||
pub use crate::realm::overlay::RealmOverlay;
|
||||
pub use crate::realm::realm::Realm;
|
||||
pub use crate::realm::pidmapper::PidLookupResult;
|
||||
pub use crate::realm::config::{RealmConfig,OverlayType,GLOBAL_CONFIG};
|
||||
pub use crate::realm::liveconfig::LiveConfig;
|
||||
pub use crate::realm::events::RealmEvent;
|
||||
pub use crate::realm::realms::Realms;
|
||||
pub use crate::realm::manager::RealmManager;
|
||||
|
@@ -266,28 +266,12 @@ impl RealmConfig {
|
||||
self.bool_value(|c| c.use_kvm)
|
||||
}
|
||||
|
||||
pub fn set_kvm(&mut self, value: bool) -> bool {
|
||||
if self.kvm() == value {
|
||||
return false;
|
||||
}
|
||||
self.use_kvm = Some(value);
|
||||
true
|
||||
}
|
||||
|
||||
/// If `true` device /dev/fuse will be added to realm
|
||||
///
|
||||
pub fn fuse(&self) -> bool {
|
||||
self.bool_value(|c| c.use_fuse)
|
||||
}
|
||||
|
||||
pub fn set_fuse(&mut self, value: bool) -> bool {
|
||||
if self.fuse() == value {
|
||||
return false;
|
||||
}
|
||||
self.use_fuse = Some(value);
|
||||
true
|
||||
}
|
||||
|
||||
/// If `true` flatpak directory will be mounted into realm
|
||||
/// and a desktop file will be created to launch gnome-software
|
||||
///
|
||||
@@ -295,14 +279,6 @@ impl RealmConfig {
|
||||
self.bool_value(|c| c.use_flatpak)
|
||||
}
|
||||
|
||||
pub fn set_flatpak(&mut self, value: bool) -> bool {
|
||||
if self.flatpak() == value {
|
||||
return false;
|
||||
}
|
||||
self.use_flatpak = Some(value);
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
/// If `true` render node device /dev/dri/renderD128 will be added to realm.
|
||||
///
|
||||
@@ -311,28 +287,12 @@ impl RealmConfig {
|
||||
self.bool_value(|c| c.use_gpu)
|
||||
}
|
||||
|
||||
pub fn set_gpu(&mut self, value: bool) -> bool {
|
||||
if self.gpu() == value {
|
||||
return false;
|
||||
}
|
||||
self.use_gpu = Some(value);
|
||||
true
|
||||
}
|
||||
|
||||
/// If `true` and `self.gpu()` is also true, privileged device /dev/dri/card0 will be
|
||||
/// added to realm.
|
||||
pub fn gpu_card0(&self) -> bool {
|
||||
self.bool_value(|c| c.use_gpu_card0)
|
||||
}
|
||||
|
||||
pub fn set_gpu_card0(&mut self, value: bool) -> bool {
|
||||
if self.gpu_card0() == value {
|
||||
return false;
|
||||
}
|
||||
self.use_gpu_card0 = Some(value);
|
||||
true
|
||||
}
|
||||
|
||||
/// If `true` the /Shared directory will be mounted in home directory of realm.
|
||||
///
|
||||
/// This directory is shared between all running realms and is an easy way to move files
|
||||
@@ -341,28 +301,12 @@ impl RealmConfig {
|
||||
self.bool_value(|c| c.use_shared_dir)
|
||||
}
|
||||
|
||||
pub fn set_shared_dir(&mut self, value: bool) -> bool {
|
||||
if self.shared_dir() == value {
|
||||
return false;
|
||||
}
|
||||
self.use_shared_dir = Some(value);
|
||||
true
|
||||
}
|
||||
|
||||
/// If `true` the mount directory for external storage devices will be bind mounted as /Media
|
||||
///
|
||||
pub fn media_dir(&self) -> bool {
|
||||
self.bool_value(|c| c.use_media_dir)
|
||||
}
|
||||
|
||||
pub fn set_media_dir(&mut self, value: bool) -> bool {
|
||||
if self.media_dir() == value {
|
||||
return false;
|
||||
}
|
||||
self.use_media_dir = Some(value);
|
||||
true
|
||||
}
|
||||
|
||||
/// If `true` the home directory of this realm will be set up in ephemeral mode.
|
||||
///
|
||||
/// The ephemeral home directory is set up with the following steps:
|
||||
@@ -377,14 +321,6 @@ impl RealmConfig {
|
||||
self.bool_value(|c| c.use_ephemeral_home)
|
||||
}
|
||||
|
||||
pub fn set_ephemeral_home(&mut self, value: bool) -> bool {
|
||||
if self.ephemeral_home() == value {
|
||||
return false;
|
||||
}
|
||||
self.use_ephemeral_home = Some(value);
|
||||
true
|
||||
}
|
||||
|
||||
/// A list of subdirectories of /realms/realm-${name}/home to bind mount into realm
|
||||
/// home directory when ephemeral-home is enabled.
|
||||
pub fn ephemeral_persistent_dirs(&self) -> Vec<String> {
|
||||
@@ -407,14 +343,6 @@ impl RealmConfig {
|
||||
self.bool_value(|c| c.use_sound)
|
||||
}
|
||||
|
||||
pub fn set_sound(&mut self, value: bool) -> bool {
|
||||
if self.sound() == value {
|
||||
return false;
|
||||
}
|
||||
self.use_sound = Some(value);
|
||||
true
|
||||
}
|
||||
|
||||
/// If `true` access to the X11 server will be added to realm by bind mounting
|
||||
/// directory /tmp/.X11-unix
|
||||
pub fn x11(&self) -> bool {
|
||||
@@ -423,28 +351,12 @@ impl RealmConfig {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_x11(&mut self, value: bool) -> bool {
|
||||
if self.x11() == value {
|
||||
return false;
|
||||
}
|
||||
self.use_x11 = Some(value);
|
||||
true
|
||||
}
|
||||
|
||||
/// If `true` access to Wayland display will be permitted in realm by adding
|
||||
/// wayland socket /run/user/1000/wayland-0
|
||||
pub fn wayland(&self) -> bool {
|
||||
self.bool_value(|c| c.use_wayland)
|
||||
}
|
||||
|
||||
pub fn set_wayland(&mut self, value: bool) -> bool {
|
||||
if self.wayland() == value {
|
||||
return false;
|
||||
}
|
||||
self.use_wayland = Some(value);
|
||||
true
|
||||
}
|
||||
|
||||
/// The name of the wayland socket to use if `self.wayland()` is `true`
|
||||
/// defaults to wayland-0, will appear in the realm as wayland-0 regardless of value
|
||||
pub fn wayland_socket(&self) -> &str {
|
||||
@@ -457,19 +369,12 @@ impl RealmConfig {
|
||||
self.bool_value(|c| c.use_network)
|
||||
}
|
||||
|
||||
pub fn set_network(&mut self, value: bool) -> bool {
|
||||
if self.network() == value {
|
||||
return false;
|
||||
}
|
||||
self.use_network = Some(value);
|
||||
true
|
||||
}
|
||||
|
||||
/// The name of the network zone this realm will use if `self.network()` is `true`.
|
||||
pub fn network_zone(&self) -> &str {
|
||||
self.str_value(|c| c.network_zone.as_ref()).unwrap_or(DEFAULT_ZONE)
|
||||
}
|
||||
|
||||
|
||||
/// If configured, this realm uses a fixed IP address on the zone subnet. The last
|
||||
/// octet of the network address for this realm will be set to the provided value.
|
||||
pub fn reserved_ip(&self) -> Option<u8> {
|
||||
@@ -530,6 +435,7 @@ impl RealmConfig {
|
||||
self.overlay = overlay.to_str_value().map(String::from)
|
||||
}
|
||||
|
||||
|
||||
pub fn netns(&self) -> Option<&str> {
|
||||
self.str_value(|c| c.netns.as_ref())
|
||||
}
|
||||
|
@@ -12,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()),
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use std::fmt::Write;
|
||||
use std::fmt::{self,Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{Realm, Result, util, realm::network::NetworkConfig};
|
||||
@@ -18,23 +18,6 @@ $EXTRA_FILE_OPTIONS
|
||||
|
||||
";
|
||||
|
||||
// SYSTEMD_NSPAWN_SHARE_NS_IPC is a secret flag that allows sharing IPC namespace between
|
||||
// nspawn container and host. This is needed so that the X11 MIT-SHM extension will work
|
||||
// correctly. Sharing the IPC namespace is not ideal, but also not obviously harmful.
|
||||
//
|
||||
// If this patch was applied to Mutter, then the MIT-SHM extension could be disabled for
|
||||
// XWayland which would break some applications and degrade performance of others.
|
||||
//
|
||||
// https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/2136
|
||||
//
|
||||
// Setting QT_X11_NO_MITSHM=1 should at least prevent QT applications from crashing if
|
||||
// extension is not available.
|
||||
//
|
||||
// Another approach would be to use LD_PRELOAD to disable visibility of the extension for
|
||||
// applications inside of container:
|
||||
//
|
||||
// https://github.com/jessfraz/dockerfiles/issues/359#issuecomment-828714848
|
||||
//
|
||||
const REALM_SERVICE_TEMPLATE: &str = "\
|
||||
[Unit]
|
||||
Description=Application Image $REALM_NAME instance
|
||||
@@ -252,4 +235,9 @@ impl <'a> RealmLauncher <'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<fmt::Error> for crate::Error {
|
||||
fn from(e: fmt::Error) -> Self {
|
||||
format_err!("Error formatting string: {}", e).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,200 +0,0 @@
|
||||
use std::process::Command;
|
||||
use crate::{Realm,Result};
|
||||
|
||||
pub struct LiveConfig<'a> {
|
||||
realm: &'a Realm,
|
||||
}
|
||||
|
||||
const LIVE_VARS: &[&str] = &[
|
||||
"use-gpu", "use-gpu-card0", "use-wayland", "use-x11",
|
||||
"use-sound", "use-shared-dir", "use-kvm", "use-media-dir",
|
||||
"use-fuse", "use-flatpak"
|
||||
];
|
||||
|
||||
const SYSTEMCTL_PATH: &str = "/usr/bin/systemctl";
|
||||
const SYSTEMD_RUN_PATH: &str = "/usr/bin/systemd-run";
|
||||
const MACHINECTL_PATH: &str = "/usr/bin/machinectl";
|
||||
|
||||
impl <'a> LiveConfig<'a> {
|
||||
pub fn is_live_configurable(varname: &str) -> bool {
|
||||
LIVE_VARS.contains(&varname)
|
||||
}
|
||||
|
||||
pub fn new(realm: &'a Realm) -> Self {
|
||||
LiveConfig {
|
||||
realm
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure(&self, varname: &str, enabled: bool) -> Result<()> {
|
||||
match varname {
|
||||
"use-gpu" => self.configure_gpu(enabled),
|
||||
"use-gpu-card0" => self.configure_gpu_card0(enabled),
|
||||
"use-wayland" => self.configure_wayland(enabled),
|
||||
"use-x11" => self.configure_x11(enabled),
|
||||
"use-sound" => self.configure_sound(enabled),
|
||||
"use-shared-dir" => self.configure_shared(enabled),
|
||||
"use-kvm" => self.configure_kvm(enabled),
|
||||
"use-media-dir" => self.configure_media(enabled),
|
||||
"use-fuse" => self.configure_fuse(enabled),
|
||||
"use-flatpak" => self.configure_flatpak(enabled),
|
||||
_ => bail!("Unknown live configuration variable '{}'", varname),
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_gpu(&self, enabled: bool) -> Result<()> {
|
||||
self.enable_device("/dev/dri/renderD128", enabled)
|
||||
}
|
||||
|
||||
fn configure_gpu_card0(&self, enabled: bool) -> Result<()> {
|
||||
self.enable_device("/dev/dri/card0", enabled)
|
||||
}
|
||||
|
||||
fn configure_kvm(&self, enabled: bool) -> Result<()> {
|
||||
self.enable_device("/dev/kvm", enabled)
|
||||
}
|
||||
|
||||
fn configure_fuse(&self, enabled: bool) -> Result<()> {
|
||||
self.enable_device("/dev/fuse", enabled)
|
||||
}
|
||||
|
||||
fn configure_shared(&self, enabled: bool) -> Result<()> {
|
||||
self.enable_bind_mount(
|
||||
"/realms/Shared",
|
||||
Some("/home/user/Shared"),
|
||||
false,
|
||||
enabled)
|
||||
}
|
||||
|
||||
fn configure_media(&self, enabled: bool) -> Result<()> {
|
||||
self.enable_bind_mount(
|
||||
"/run/media/citadel",
|
||||
Some("/home/user/Media"),
|
||||
false,
|
||||
enabled)
|
||||
}
|
||||
|
||||
fn configure_sound(&self, enabled: bool) -> Result<()> {
|
||||
self.enable_bind_mount(
|
||||
"/run/user/1000/pulse",
|
||||
Some("/run/user/host/pulse"),
|
||||
true,
|
||||
enabled)
|
||||
}
|
||||
|
||||
fn configure_wayland(&self, enabled: bool) -> Result<()> {
|
||||
self.enable_bind_mount(
|
||||
"/run/user/1000/wayland-0",
|
||||
Some("/run/user/host/wayland-0"),
|
||||
true,
|
||||
enabled)
|
||||
}
|
||||
|
||||
fn configure_x11(&self, enabled: bool) -> Result<()> {
|
||||
self.enable_bind_mount(
|
||||
"/tmp/.X11-unix",
|
||||
None,
|
||||
true,
|
||||
enabled)
|
||||
}
|
||||
|
||||
fn configure_flatpak(&self, enabled: bool) -> Result<()> {
|
||||
let path = self.realm.base_path_file("flatpak")
|
||||
.display()
|
||||
.to_string();
|
||||
|
||||
self.enable_bind_mount(
|
||||
&path,
|
||||
Some("/var/lib/flatpak"),
|
||||
true,
|
||||
enabled)
|
||||
}
|
||||
|
||||
fn enable_bind_mount(&self, path: &str, target: Option<&str>, readonly: bool, enabled: bool) -> Result<()> {
|
||||
if enabled {
|
||||
self.machinectl_bind(path, target, readonly)
|
||||
} else if let Some(target) = target {
|
||||
self.unmount(target, false)
|
||||
} else {
|
||||
self.unmount(path, false)
|
||||
}
|
||||
}
|
||||
|
||||
fn enable_device(&self, device_path: &str, enabled: bool) -> Result<()> {
|
||||
if enabled {
|
||||
self.machinectl_bind(device_path, None, false)?;
|
||||
self.systemctl_device_allow(device_path)?;
|
||||
} else {
|
||||
self.unmount(device_path, true)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unmount(&self, path: &str, delete: bool) -> Result<()> {
|
||||
// systemd-run --machine={name} umount {path}
|
||||
self.systemd_run(&["umount", path])?;
|
||||
if delete {
|
||||
self.systemd_run(&["rm", path])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn systemctl_device_allow(&self, device_path: &str) -> Result<()> {
|
||||
let status = Command::new(SYSTEMCTL_PATH)
|
||||
.arg("set-property")
|
||||
.arg(format!("realm-{}.service", self.realm.name()))
|
||||
.arg(format!("DeviceAllow={}", device_path))
|
||||
.status()
|
||||
.map_err(context!("failed to execute {}", SYSTEMCTL_PATH))?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("'systemctl set-property realm-{}.service DeviceAllow={}' command did not complete successfully status: {:?}", self.realm.name(), device_path, status.code());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn machinectl_bind(&self, path: &str, target: Option<&str>, readonly: bool) -> Result<()> {
|
||||
let mut cmd = Command::new(MACHINECTL_PATH);
|
||||
cmd.arg("--mkdir");
|
||||
|
||||
if readonly {
|
||||
cmd.arg("--read-only");
|
||||
}
|
||||
cmd.arg("bind")
|
||||
.arg(self.realm.name())
|
||||
.arg(path);
|
||||
|
||||
if let Some(target) = target {
|
||||
cmd.arg(target);
|
||||
}
|
||||
|
||||
let status = cmd.status()
|
||||
.map_err(context!("failed to execute {}", MACHINECTL_PATH))?;
|
||||
|
||||
|
||||
if !status.success() {
|
||||
bail!("machinectl bind {} {} {:?} did not complete successfully status: {:?}", self.realm.name(), path, target, status.code());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
}
|
||||
|
||||
fn systemd_run(&self, args: &[&str]) -> Result<()> {
|
||||
let status = Command::new(SYSTEMD_RUN_PATH)
|
||||
.arg("--quiet")
|
||||
.arg(format!("--machine={}", self.realm.name()))
|
||||
.args(args)
|
||||
.status()
|
||||
.map_err(context!("failed to execute {}", MACHINECTL_PATH))?;
|
||||
|
||||
|
||||
if !status.success() {
|
||||
bail!("systemd-run --machine={} command did not complete successfully args: {:?} status: {:?}", self.realm.name(), args, status.code());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@@ -194,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();
|
||||
|
@@ -1,7 +1,6 @@
|
||||
|
||||
pub(crate) mod overlay;
|
||||
pub(crate) mod config;
|
||||
pub(crate) mod liveconfig;
|
||||
pub(crate) mod realms;
|
||||
pub(crate) mod manager;
|
||||
#[allow(clippy::module_inception)]
|
||||
|
@@ -169,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()
|
||||
}
|
||||
|
@@ -244,7 +244,7 @@ impl Realms {
|
||||
pub fn delete_realm(&mut self, name: &str, save_home: bool) -> Result<()> {
|
||||
let _lock = Self::realmslock()?;
|
||||
|
||||
let realm = match self.by_name(name) {
|
||||
let realm = match self.realms.take(name) {
|
||||
Some(realm) => realm,
|
||||
None => bail!("Cannot remove realm '{}' because it doesn't seem to exist", name),
|
||||
};
|
||||
|
@@ -113,7 +113,7 @@ impl Systemd {
|
||||
.arg(name)
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.map_err(context!("failed to execute {}", SYSTEMCTL_PATH))?;
|
||||
.map_err(context!("failed to execute {}", MACHINECTL_PATH))?;
|
||||
Ok(ok)
|
||||
}
|
||||
|
||||
|
@@ -1,174 +0,0 @@
|
||||
use std::cell::Cell;
|
||||
use std::fmt::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::realm::BridgeAllocator;
|
||||
use crate::{util, Result};
|
||||
|
||||
const NSPAWN_FILE_TEMPLATE: &str = "\
|
||||
[Exec]
|
||||
Boot=true
|
||||
$NETWORK_CONFIG
|
||||
|
||||
[Files]
|
||||
BindReadOnly=/storage/citadel-state/resolv.conf:/etc/resolv.conf
|
||||
|
||||
$BIND_MOUNTS
|
||||
";
|
||||
|
||||
const SERVICE_TEMPLATE: &str = "\
|
||||
[Unit]
|
||||
Description=Update RealmFS $MACHINE_NAME instance
|
||||
|
||||
[Service]
|
||||
|
||||
DevicePolicy=closed
|
||||
|
||||
ExecStart=/usr/bin/systemd-nspawn --quiet --console=passive --notify-ready=yes --keep-unit --machine=$MACHINE_NAME --link-journal=auto --directory=$ROOTFS
|
||||
|
||||
KillMode=mixed
|
||||
Type=notify
|
||||
RestartForceExitStatus=133
|
||||
SuccessExitStatus=133
|
||||
";
|
||||
|
||||
const SYSTEMCTL_PATH: &str = "/usr/bin/systemctl";
|
||||
const SYSTEMD_NSPAWN_PATH: &str = "/run/systemd/nspawn";
|
||||
const SYSTEMD_UNIT_PATH: &str = "/run/systemd/system";
|
||||
|
||||
/// Launcher for RealmFS update containers
|
||||
pub struct RealmFSUpdateLauncher {
|
||||
machine_name: String,
|
||||
shared_directory: bool,
|
||||
running: Cell<bool>,
|
||||
rootfs: PathBuf,
|
||||
service_path: PathBuf,
|
||||
nspawn_path: PathBuf,
|
||||
|
||||
}
|
||||
|
||||
impl RealmFSUpdateLauncher {
|
||||
|
||||
fn new(machine_name: &str, rootfs: &Path, shared_directory: bool) -> Self {
|
||||
let machine_name = machine_name.to_string();
|
||||
let running = Cell::new(false);
|
||||
let rootfs = rootfs.to_owned();
|
||||
let service_path = PathBuf::from(SYSTEMD_UNIT_PATH).join(format!("realmfs-{machine_name}.service"));
|
||||
let nspawn_path= PathBuf::from(SYSTEMD_NSPAWN_PATH).join(format!("{machine_name}.nspawn"));
|
||||
RealmFSUpdateLauncher {
|
||||
machine_name, shared_directory, running, rootfs, service_path, nspawn_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn launch_update_container(machine_name: &str, rootfs: &Path, shared_directory: bool) -> Result<Self> {
|
||||
let launcher = Self::new(machine_name, rootfs, shared_directory);
|
||||
launcher.start_container()?;
|
||||
Ok(launcher)
|
||||
}
|
||||
|
||||
fn systemctl_start(&self) -> Result<bool> {
|
||||
self.run_systemctl("start")
|
||||
}
|
||||
|
||||
fn systemctl_stop(&self) -> Result<bool> {
|
||||
self.run_systemctl("stop")
|
||||
}
|
||||
|
||||
fn run_systemctl(&self, op: &str) -> Result<bool> {
|
||||
let service_name = format!("realmfs-{}", self.machine_name);
|
||||
let ok = Command::new(SYSTEMCTL_PATH)
|
||||
.arg(op)
|
||||
.arg(service_name)
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.map_err(context!("failed to execute {}", SYSTEMCTL_PATH))?;
|
||||
Ok(ok)
|
||||
}
|
||||
|
||||
fn start_container(&self) -> Result<()> {
|
||||
self.write_launch_config_files()?;
|
||||
let ok = self.systemctl_start()?;
|
||||
self.running.set(ok);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop_container(&self) -> Result<()> {
|
||||
if self.running.replace(false) {
|
||||
self.systemctl_stop()?;
|
||||
self.remove_launch_config_files()?;
|
||||
// XXX remove IP address allocation?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_launch_config_files(&self) -> Result<()> {
|
||||
util::remove_file(&self.nspawn_path)?;
|
||||
util::remove_file(&self.service_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_nspawn_file(&self) -> Result<String> {
|
||||
Ok(NSPAWN_FILE_TEMPLATE
|
||||
.replace("$BIND_MOUNTS", &self.generate_bind_mounts()?)
|
||||
.replace("$NETWORK_CONFIG", &self.generate_network_config()?))
|
||||
}
|
||||
|
||||
fn generate_service_file(&self) -> String {
|
||||
let rootfs = self.rootfs.display().to_string();
|
||||
SERVICE_TEMPLATE
|
||||
.replace("$MACHINE_NAME", &self.machine_name)
|
||||
.replace("$ROOTFS", &rootfs)
|
||||
}
|
||||
|
||||
/// Write the string `content` to file `path`. If the directory does
|
||||
/// not already exist, create it.
|
||||
fn write_launch_config_file(&self, path: &Path, content: &str) -> Result<()> {
|
||||
match path.parent() {
|
||||
Some(parent) => util::create_dir(parent)?,
|
||||
None => bail!("config file path {} has no parent?", path.display()),
|
||||
};
|
||||
util::write_file(path, content)
|
||||
}
|
||||
|
||||
fn generate_bind_mounts(&self) -> Result<String> {
|
||||
let mut s = String::new();
|
||||
if self.shared_directory && Path::new("/realms/Shared").exists() {
|
||||
writeln!(s, "Bind=/realms/Shared:/run/Shared")?;
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn generate_network_config(&self) -> Result<String> {
|
||||
let mut s = String::new();
|
||||
|
||||
let mut alloc = BridgeAllocator::default_bridge()?;
|
||||
let addr = alloc.allocate_address_for(&self.machine_name)?;
|
||||
let gw = alloc.gateway();
|
||||
|
||||
writeln!(s, "Environment=IFCONFIG_IP={}", addr)?;
|
||||
writeln!(s, "Environment=IFCONFIG_GW={}", gw)?;
|
||||
writeln!(s, "[Network]")?;
|
||||
writeln!(s, "Zone=clear")?;
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn write_launch_config_files(&self) -> Result<()> {
|
||||
let nspawn_content = self.generate_nspawn_file()?;
|
||||
self.write_launch_config_file(&self.nspawn_path, &nspawn_content)?;
|
||||
|
||||
let service_content = self.generate_service_file();
|
||||
self.write_launch_config_file(&self.service_path, &service_content)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RealmFSUpdateLauncher {
|
||||
fn drop(&mut self) {
|
||||
if let Err(err) = self.stop_container() {
|
||||
warn!("Error stopping RealmFS update container: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,9 @@
|
||||
pub(crate) mod resizer;
|
||||
mod mountpoint;
|
||||
pub(crate) mod update;
|
||||
mod update;
|
||||
pub(crate) mod realmfs_set;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod realmfs;
|
||||
mod launcher;
|
||||
|
||||
pub use self::realmfs::RealmFS;
|
||||
pub use self::mountpoint::Mountpoint;
|
||||
|
@@ -7,7 +7,7 @@ use std::sync::{Arc, Weak, RwLock};
|
||||
|
||||
use crate::{ImageHeader, MetaInfo, Result, KeyRing, KeyPair, util, RealmManager, PublicKey, ResizeSize};
|
||||
use crate::realmfs::resizer::Superblock;
|
||||
use crate::realmfs::update::RealmFSUpdate;
|
||||
use crate::realmfs::update::Update;
|
||||
use super::mountpoint::Mountpoint;
|
||||
|
||||
// Maximum length of a RealmFS name
|
||||
@@ -266,13 +266,8 @@ impl RealmFS {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&self) -> Result<RealmFSUpdate> {
|
||||
let update = RealmFSUpdate::create(self)?;
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
pub fn interactive_update(&self, scheme: Option<&str>) -> Result<()> {
|
||||
let mut update = RealmFSUpdate::create(self)?;
|
||||
let mut update = Update::create(self)?;
|
||||
update.run_interactive_update(scheme)
|
||||
}
|
||||
|
||||
@@ -393,14 +388,14 @@ impl RealmFS {
|
||||
|
||||
pub fn resize_grow_to(&self, size: ResizeSize) -> Result<()> {
|
||||
info!("Resizing to {} blocks", size.nblocks());
|
||||
let mut update = RealmFSUpdate::create(self)?;
|
||||
let mut update = Update::create(self)?;
|
||||
update.grow_to(size);
|
||||
update.resize()
|
||||
}
|
||||
|
||||
pub fn resize_grow_by(&self, size: ResizeSize) -> Result<()> {
|
||||
info!("Resizing to an increase of {} blocks", size.nblocks());
|
||||
let mut update = RealmFSUpdate::create(self)?;
|
||||
let mut update = Update::create(self)?;
|
||||
update.grow_by(size);
|
||||
update.resize()
|
||||
}
|
||||
|
@@ -11,8 +11,6 @@ use crate::util::is_euid_root;
|
||||
use crate::terminal::TerminalRestorer;
|
||||
use crate::verity::Verity;
|
||||
|
||||
use super::launcher::RealmFSUpdateLauncher;
|
||||
|
||||
const BLOCK_SIZE: usize = 4096;
|
||||
|
||||
// The maximum number of backup copies the rotate() method will create
|
||||
@@ -23,41 +21,36 @@ const RESIZE2FS: &str = "resize2fs";
|
||||
|
||||
/// Manages the process of updating or resizing a `RealmFS` image file.
|
||||
///
|
||||
pub struct RealmFSUpdate {
|
||||
realmfs: RealmFS, // RealmFS being updated
|
||||
pub struct Update<'a> {
|
||||
realmfs: &'a RealmFS, // RealmFS being updated
|
||||
name: String, // name for nspawn instance
|
||||
target: PathBuf, // Path to the update copy of realmfs image
|
||||
mountpath: PathBuf, // Path at which update copy is mounted
|
||||
container: Option<RealmFSUpdateLauncher>,
|
||||
_lock: FileLock,
|
||||
resize: Option<ResizeSize>, // If the image needs to be resized, the resize size is stored here
|
||||
network_allocated: bool,
|
||||
}
|
||||
|
||||
impl RealmFSUpdate {
|
||||
fn new(realmfs: RealmFS, lock: FileLock) -> Self {
|
||||
impl <'a> Update<'a> {
|
||||
fn new(realmfs: &'a RealmFS, lock: FileLock) -> Self {
|
||||
|
||||
let metainfo = realmfs.metainfo();
|
||||
let tag = metainfo.verity_tag();
|
||||
let mountpath = Path::new(RealmFS::RUN_DIRECTORY)
|
||||
.join(format!("realmfs-{}-{}.update", realmfs.name(), tag));
|
||||
|
||||
let name = format!("{}-{}-update", realmfs.name(), tag);
|
||||
let resize = ResizeSize::auto_resize_size(&realmfs);
|
||||
let target = realmfs.path().with_extension("update");
|
||||
RealmFSUpdate {
|
||||
Update {
|
||||
realmfs,
|
||||
name,
|
||||
target,
|
||||
name: format!("{}-{}-update", realmfs.name(), tag),
|
||||
target: realmfs.path().with_extension("update"),
|
||||
mountpath,
|
||||
_lock: lock,
|
||||
container: None,
|
||||
resize,
|
||||
resize: ResizeSize::auto_resize_size(realmfs),
|
||||
network_allocated: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(realmfs: &RealmFS) -> Result<Self> {
|
||||
pub fn create(realmfs: &'a RealmFS) -> Result<Self> {
|
||||
let lock = FileLock::nonblocking_acquire(realmfs.path().with_extension("lock"))?
|
||||
.ok_or(format_err!("Unable to obtain file lock to update realmfs image: {}", realmfs.name()))?;
|
||||
|
||||
@@ -65,10 +58,10 @@ impl RealmFSUpdate {
|
||||
bail!("Cannot seal realmfs image, no sealing keys available");
|
||||
}
|
||||
|
||||
Ok(RealmFSUpdate::new(realmfs.clone(), lock))
|
||||
Ok(Update::new(realmfs, lock))
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
@@ -81,9 +74,6 @@ impl RealmFSUpdate {
|
||||
info!("Update file {} already exists, removing it", self.target.display());
|
||||
util::remove_file(&self.target)?;
|
||||
}
|
||||
info!("Creating update copy of realmfs {} -> {}",
|
||||
self.realmfs.path().display(),
|
||||
self.target().display());
|
||||
self.realmfs.copy_image_file(self.target())?;
|
||||
|
||||
Ok(())
|
||||
@@ -112,10 +102,6 @@ impl RealmFSUpdate {
|
||||
}
|
||||
|
||||
fn mount_update_image(&mut self) -> Result<()> {
|
||||
info!("Loop device mounting {} at {}",
|
||||
self.target().display(),
|
||||
self.mountpath.display());
|
||||
|
||||
LoopDevice::with_loop(self.target(), Some(BLOCK_SIZE), false, |loopdev| {
|
||||
if self.resize.is_some() {
|
||||
self.resize_device(loopdev)?;
|
||||
@@ -183,7 +169,6 @@ impl RealmFSUpdate {
|
||||
|
||||
// Remove dm-verity hash tree from update copy of image file.
|
||||
fn truncate_verity(&self) -> Result<()> {
|
||||
info!("Truncating dm-verity hash tree from {}", self.target().display());
|
||||
let file_nblocks = self.realmfs.file_nblocks()?;
|
||||
let metainfo_nblocks = self.metainfo_nblock_size();
|
||||
|
||||
@@ -202,8 +187,6 @@ impl RealmFSUpdate {
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
info!("Resizing target file to {} blocks", nblocks);
|
||||
|
||||
if nblocks < self.metainfo_nblock_size() {
|
||||
bail!("Cannot shrink image")
|
||||
}
|
||||
@@ -214,19 +197,7 @@ impl RealmFSUpdate {
|
||||
self.set_target_len(nblocks)
|
||||
}
|
||||
|
||||
fn shutdown_container(&mut self) -> Result<()> {
|
||||
if let Some(update) = self.container.take() {
|
||||
update.stop_container()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cleanup(&mut self) {
|
||||
// if a container was started, stop it
|
||||
if let Err(err) = self.shutdown_container() {
|
||||
warn!("Error shutting down update container: {}", err);
|
||||
}
|
||||
|
||||
if self.mountpath.exists() {
|
||||
self.unmount_update_image();
|
||||
}
|
||||
@@ -294,21 +265,6 @@ impl RealmFSUpdate {
|
||||
Ok(yes)
|
||||
}
|
||||
|
||||
pub fn prepare_update(&mut self, shared_directory: bool) -> Result<()> {
|
||||
if !is_euid_root() {
|
||||
bail!("RealmFS updates must be prepared as root");
|
||||
}
|
||||
self.setup()?;
|
||||
self.launch_update_container(shared_directory)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn commit_update(&mut self) -> Result<()> {
|
||||
let result = self.apply_update();
|
||||
self.cleanup();
|
||||
result
|
||||
}
|
||||
|
||||
pub fn run_interactive_update(&mut self, scheme: Option<&str>) -> Result<()> {
|
||||
if !is_euid_root() {
|
||||
bail!("RealmFS updates must be run as root");
|
||||
@@ -340,21 +296,6 @@ impl RealmFSUpdate {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn launch_update_container(&mut self, shared_directory: bool) -> Result<()> {
|
||||
|
||||
if self.container.is_some() {
|
||||
bail!("Update container is already running");
|
||||
}
|
||||
|
||||
info!("Launching update container '{}'", self.name());
|
||||
|
||||
let update = RealmFSUpdateLauncher::launch_update_container(self.name(), &self.mountpath, shared_directory)?;
|
||||
|
||||
self.container = Some(update);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_update_shell(&mut self, command: &str) -> Result<()> {
|
||||
|
||||
let mut alloc = BridgeAllocator::default_bridge()?;
|
||||
@@ -404,7 +345,7 @@ impl RealmFSUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RealmFSUpdate {
|
||||
impl <'a> Drop for Update<'a> {
|
||||
fn drop(&mut self) {
|
||||
self.cleanup();
|
||||
}
|
||||
|
@@ -422,6 +422,7 @@ fn compare_images(a: Option<ResourceImage>, b: ResourceImage) -> Result<Resource
|
||||
|
||||
let bind_a = a.metainfo();
|
||||
let bind_b = b.metainfo();
|
||||
|
||||
let ver_a = bind_a.version();
|
||||
let ver_b = bind_b.version();
|
||||
|
||||
|
@@ -5,5 +5,5 @@ mod uname;
|
||||
|
||||
pub use self::uname::UtsName;
|
||||
pub use self::loopdev::LoopDevice;
|
||||
pub use self::mounts::Mounts;
|
||||
pub use self::mounts::{Mounts,MountLine};
|
||||
pub use self::lock::FileLock;
|
||||
|
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"),
|
||||
}
|
||||
}
|
||||
}
|
@@ -344,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");
|
||||
|
15
realm-config-ui/Cargo.toml
Normal file
15
realm-config-ui/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "realm-config-ui"
|
||||
version = "0.1.0"
|
||||
authors = ["Bruce Leidl <bruce@subgraph.com>"]
|
||||
edition = "2018"
|
||||
description = "Realm Configuration Tool"
|
||||
homepage = "https://subgraph.com"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
rand = "0.8"
|
||||
zvariant = "2.7.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
zbus = "=2.0.0-beta.5"
|
||||
gtk = { version = "0.14.0", features = ["v3_24"] }
|
77
realm-config-ui/src/colorscheme/colorscheme-dialog.ui
Normal file
77
realm-config-ui/src/colorscheme/colorscheme-dialog.ui
Normal file
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="ColorSchemeDialog" parent="GtkDialog">
|
||||
<property name="title">Choose Terminal Colors</property>
|
||||
<property name="modal">True</property>
|
||||
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">horizontal</property>
|
||||
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="hscrollbar-policy">never</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="colorscheme-tree">
|
||||
<property name="headers-visible">False</property>
|
||||
<property name="model">treemodel</property>
|
||||
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn">
|
||||
<property name="expand">True</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText"/>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
</object>
|
||||
</child>
|
||||
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<object class="GtkSeparator">
|
||||
<property name="orientation">vertical</property>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<object class="GtkLabel" id="colorscheme-label">
|
||||
<property name="hexpand">True</property>
|
||||
<property name="halign">fill</property>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="use-underline">1</property>
|
||||
<property name="label">Cancel</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="ok_button">
|
||||
<property name="use-underline">1</property>
|
||||
<property name="label">_Choose</property>
|
||||
<property name="can-default">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="cancel">cancel_button</action-widget>
|
||||
<action-widget response="ok" default="true">ok_button</action-widget>
|
||||
</action-widgets>
|
||||
</template>
|
||||
<object class="GtkTreeStore" id="treemodel">
|
||||
<columns>
|
||||
<column type="gchararray" />
|
||||
<column type="gchararray" />
|
||||
</columns>
|
||||
</object>
|
||||
</interface>
|
216
realm-config-ui/src/colorscheme/colorschemes.rs
Normal file
216
realm-config-ui/src/colorscheme/colorschemes.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk::prelude::*;
|
||||
use gtk::glib;
|
||||
use libcitadel::terminal::{Base16Scheme, Color};
|
||||
|
||||
enum RootEntry {
|
||||
Scheme(Base16Scheme),
|
||||
Category(String, Vec<Base16Scheme>),
|
||||
}
|
||||
|
||||
impl RootEntry {
|
||||
fn key(&self) -> &str {
|
||||
match self {
|
||||
RootEntry::Scheme(scheme) => scheme.slug(),
|
||||
RootEntry::Category(name, _) => name.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_to_category(list: &mut Vec<RootEntry>, category: &str, scheme: &Base16Scheme) {
|
||||
let scheme = scheme.clone();
|
||||
for entry in list.iter_mut() {
|
||||
if let RootEntry::Category(name, schemes) = entry {
|
||||
if name == category {
|
||||
schemes.push(scheme);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
list.push(RootEntry::Category(category.to_string(), vec![scheme]))
|
||||
}
|
||||
|
||||
fn build_list() -> Vec<RootEntry> {
|
||||
let mut list = Vec::new();
|
||||
for scheme in Base16Scheme::all_schemes() {
|
||||
if let Some(category) = scheme.category() {
|
||||
Self::add_to_category(&mut list,category, &scheme);
|
||||
} else {
|
||||
list.push(RootEntry::Scheme(scheme));
|
||||
}
|
||||
}
|
||||
list.sort_by(|a, b| a.key().cmp(b.key()));
|
||||
list
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ColorSchemes {
|
||||
entries: Rc<Vec<RootEntry>>,
|
||||
}
|
||||
|
||||
impl ColorSchemes {
|
||||
pub fn new() -> Self {
|
||||
ColorSchemes {
|
||||
entries: Rc::new(RootEntry::build_list()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn populate_tree_model(&self, store: >k::TreeStore) {
|
||||
for entry in self.entries.iter() {
|
||||
match entry {
|
||||
RootEntry::Scheme(scheme) => {
|
||||
let first = scheme.slug().to_string();
|
||||
let second = scheme.name().to_string();
|
||||
store.insert_with_values(None, None, &[(0, &first), (1, &second)]);
|
||||
}
|
||||
RootEntry::Category(name, list) => {
|
||||
let first = String::new();
|
||||
let second = name.to_string();
|
||||
let iter = store.insert_with_values(None, None, &[(0, &first), (1, &second)]);
|
||||
for scheme in list {
|
||||
let first = scheme.slug().to_string();
|
||||
let second = scheme.name().to_string();
|
||||
store.insert_with_values(Some(&iter), None, &[(0, &first), (1, &second)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preview_scheme(&self, id: &str) -> Option<(String, Color)> {
|
||||
let scheme = Base16Scheme::by_name(id)?;
|
||||
let bg = scheme.terminal_background();
|
||||
let text = PreviewRender::new(scheme).render_preview();
|
||||
Some((text, bg))
|
||||
}
|
||||
}
|
||||
|
||||
struct PreviewRender {
|
||||
buffer: String,
|
||||
scheme: Base16Scheme,
|
||||
}
|
||||
|
||||
impl PreviewRender {
|
||||
fn new(scheme: &Base16Scheme) -> Self {
|
||||
let scheme = scheme.clone();
|
||||
PreviewRender {
|
||||
buffer: String::new(),
|
||||
scheme,
|
||||
}
|
||||
}
|
||||
fn print(mut self, color_idx: usize, text: &str) -> Self {
|
||||
let s = glib::markup_escape_text(text);
|
||||
|
||||
let color = self.scheme.terminal_palette_color(color_idx);
|
||||
self.color_span(Some(color), None);
|
||||
self.buffer.push_str(s.as_str());
|
||||
self.end_span();
|
||||
self
|
||||
}
|
||||
|
||||
fn vtype(self, text: &str) -> Self {
|
||||
self.print(3, text)
|
||||
}
|
||||
|
||||
fn konst(self, text: &str) -> Self {
|
||||
self.print(1, text)
|
||||
}
|
||||
|
||||
fn func(self, text: &str) -> Self {
|
||||
self.print(4, text)
|
||||
}
|
||||
|
||||
fn string(self, text: &str) -> Self {
|
||||
self.print(2, text)
|
||||
}
|
||||
|
||||
fn keyword(self, text: &str) -> Self {
|
||||
self.print(5, text)
|
||||
}
|
||||
fn comment(self, text: &str) -> Self {
|
||||
self.print(8, text)
|
||||
}
|
||||
|
||||
fn text(mut self, text: &str) -> Self {
|
||||
let color = self.scheme.terminal_foreground();
|
||||
self.color_span(Some(color), None);
|
||||
self.buffer.push_str(text);
|
||||
self.end_span();
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
fn color_attrib(&mut self, name: &str, color: Color) {
|
||||
let (r,g,b) = color.rgb();
|
||||
self.buffer.push_str(&format!(" {}='#{:02X}{:02X}{:02X}'", name, r, g, b));
|
||||
}
|
||||
|
||||
fn color_span(&mut self, fg: Option<Color>, bg: Option<Color>) {
|
||||
self.buffer.push_str("<span");
|
||||
if let Some(color) = fg {
|
||||
self.color_attrib("foreground", color);
|
||||
}
|
||||
if let Some(color) = bg {
|
||||
self.color_attrib("background", color);
|
||||
}
|
||||
self.buffer.push_str(">");
|
||||
}
|
||||
|
||||
fn end_span(&mut self) {
|
||||
self.buffer.push_str("</span>");
|
||||
}
|
||||
|
||||
fn nl(mut self) -> Self {
|
||||
self.buffer.push_str(" \n ");
|
||||
self
|
||||
}
|
||||
|
||||
fn render_colorbar(&mut self) {
|
||||
self.buffer.push_str("\n ");
|
||||
let color = self.scheme.terminal_foreground();
|
||||
self.color_span(Some(color), None);
|
||||
for i in 0..16 {
|
||||
self.buffer.push_str(&format!(" {:X} ", i));
|
||||
}
|
||||
self.end_span();
|
||||
self.buffer.push_str(" \n ");
|
||||
for i in 0..16 {
|
||||
let c = self.scheme.color(i);
|
||||
self.color_span(None, Some(c));
|
||||
self.buffer.push_str(" ");
|
||||
self.end_span();
|
||||
}
|
||||
self.buffer.push_str(" \n ");
|
||||
for i in 8..16 {
|
||||
let c = self.scheme.terminal_palette_color(i);
|
||||
self.color_span(None, Some(c));
|
||||
self.buffer.push_str(" ");
|
||||
self.end_span();
|
||||
}
|
||||
self.buffer.push_str(" \n ");
|
||||
}
|
||||
|
||||
fn render_preview(mut self) -> String {
|
||||
let name = self.scheme.name().to_string();
|
||||
self.render_colorbar();
|
||||
self.nl()
|
||||
.comment("/**").nl()
|
||||
.comment(" * An example of how this color scheme").nl()
|
||||
.comment(" * might look in a text editor with syntax").nl()
|
||||
.comment(" * highlighting.").nl()
|
||||
.comment(" */").nl()
|
||||
.nl()
|
||||
.func("#include ").string("<stdio.h>").nl()
|
||||
.func("#include ").string("<stdlib.h>").nl()
|
||||
.nl()
|
||||
.vtype("static char").text(" theme[] = ").string(&format!("\"{}\"", name)).text(";").nl()
|
||||
.nl()
|
||||
.vtype("int").text(" main(").vtype("int").text(" argc, ").vtype("char").text(" **argv) {").nl()
|
||||
.text(" printf(").string("\"Hello, ").keyword("%s").text("!").keyword("\\n").string("\"").text(", theme);").nl()
|
||||
.text(" exit(").konst("0").text(");").nl()
|
||||
.text("}")
|
||||
.nl()
|
||||
.nl().buffer
|
||||
}
|
||||
}
|
153
realm-config-ui/src/colorscheme/dialog.rs
Normal file
153
realm-config-ui/src/colorscheme/dialog.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use gtk::glib;
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
use gtk::CompositeTemplate;
|
||||
|
||||
use libcitadel::terminal::{Base16Scheme, Color};
|
||||
|
||||
use crate::colorscheme::colorschemes::ColorSchemes;
|
||||
|
||||
#[derive(CompositeTemplate)]
|
||||
#[template(file = "colorscheme-dialog.ui")]
|
||||
pub struct ColorSchemeDialog {
|
||||
#[template_child(id="colorscheme-tree")]
|
||||
tree: TemplateChild<gtk::TreeView>,
|
||||
|
||||
#[template_child]
|
||||
treemodel: TemplateChild<gtk::TreeStore>,
|
||||
|
||||
#[template_child(id="colorscheme-label")]
|
||||
preview: TemplateChild<gtk::Label>,
|
||||
|
||||
css_provider: gtk::CssProvider,
|
||||
|
||||
colorschemes: ColorSchemes,
|
||||
|
||||
tracker: RefCell<Option<SelectionTracker>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SelectionTracker {
|
||||
model: gtk::TreeStore,
|
||||
selection: gtk::TreeSelection,
|
||||
preview: gtk::Label,
|
||||
colorschemes: ColorSchemes,
|
||||
css_provider: gtk::CssProvider,
|
||||
}
|
||||
|
||||
impl SelectionTracker {
|
||||
fn new(dialog: &ColorSchemeDialog) -> Self {
|
||||
let tracker = SelectionTracker {
|
||||
model: dialog.treemodel.clone(),
|
||||
selection: dialog.tree.selection(),
|
||||
preview: dialog.preview.clone(),
|
||||
colorschemes: dialog.colorschemes.clone(),
|
||||
css_provider: dialog.css_provider.clone(),
|
||||
};
|
||||
tracker.selection.connect_changed(glib::clone!(@strong tracker => move |_| {
|
||||
if let Some(id) = tracker.selected_id() {
|
||||
if let Some((text, background)) = tracker.colorschemes.preview_scheme(&id) {
|
||||
tracker.set_preview_background(background);
|
||||
tracker.preview.set_markup(&text);
|
||||
}
|
||||
}
|
||||
}));
|
||||
tracker
|
||||
}
|
||||
|
||||
fn selected_id(&self) -> Option<String> {
|
||||
self.selection.selected().and_then(|(model,iter)| {
|
||||
model.value(&iter, 0).get::<String>().ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn set_preview_background(&self, color: Color) {
|
||||
const CSS: &str =
|
||||
r##"
|
||||
#colorscheme-label {
|
||||
background-color: $COLOR;
|
||||
font-family: monospace;
|
||||
font-size: 14pt;
|
||||
}
|
||||
"##;
|
||||
let (r, g, b) = color.rgb();
|
||||
let css = CSS.replace("$COLOR", &format!("#{:02x}{:02x}{:02x}", r, g, b));
|
||||
if let Err(e) = self.css_provider.load_from_data(css.as_bytes()) {
|
||||
warn!("Error loading CSS provider data: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_selected_id(&self, id: &str) {
|
||||
self.model.foreach(glib::clone!(@strong self.selection as selection => move |model, _path, iter| {
|
||||
if let Ok(ref s) = model.value(iter, 0).get::<String>() {
|
||||
if s == id {
|
||||
selection.select_iter(iter);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorSchemeDialog {
|
||||
pub fn set_selected_id(&self, colorscheme_id: &str) {
|
||||
let tracker = self.tracker.borrow();
|
||||
if let Some(tracker) = tracker.as_ref() {
|
||||
tracker.set_selected_id(colorscheme_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_selected_scheme (&self) -> Option<Base16Scheme> {
|
||||
let tracker = self.tracker.borrow();
|
||||
tracker.as_ref().and_then(|t| t.selected_id())
|
||||
.and_then(|id| Base16Scheme::by_name(&id))
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ColorSchemeDialog {
|
||||
fn default() -> Self {
|
||||
ColorSchemeDialog {
|
||||
tree: Default::default(),
|
||||
treemodel: Default::default(),
|
||||
preview: Default::default(),
|
||||
css_provider: gtk::CssProvider::new(),
|
||||
colorschemes: ColorSchemes::new(),
|
||||
tracker: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ColorSchemeDialog {
|
||||
const NAME: &'static str = "ColorSchemeDialog";
|
||||
type Type = super::ColorSchemeDialog;
|
||||
type ParentType = gtk::Dialog;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
}
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for ColorSchemeDialog {
|
||||
fn constructed(&self, obj: &Self::Type) {
|
||||
self.parent_constructed(obj);
|
||||
self.preview.set_widget_name("colorscheme-label");
|
||||
self.preview.style_context().add_provider(&self.css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION);
|
||||
self.colorschemes.populate_tree_model(&self.treemodel);
|
||||
let tracker = SelectionTracker::new(self);
|
||||
self.tracker.borrow_mut().replace(tracker);
|
||||
}
|
||||
}
|
||||
|
||||
impl DialogImpl for ColorSchemeDialog {}
|
||||
impl WindowImpl for ColorSchemeDialog {}
|
||||
impl BinImpl for ColorSchemeDialog {}
|
||||
impl ContainerImpl for ColorSchemeDialog {}
|
||||
impl WidgetImpl for ColorSchemeDialog {}
|
31
realm-config-ui/src/colorscheme/mod.rs
Normal file
31
realm-config-ui/src/colorscheme/mod.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use gtk::glib;
|
||||
use glib::subclass::prelude::*;
|
||||
use libcitadel::terminal::Base16Scheme;
|
||||
|
||||
mod dialog;
|
||||
mod colorschemes;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ColorSchemeDialog(ObjectSubclass<dialog::ColorSchemeDialog>)
|
||||
@extends gtk::Dialog, gtk::Window, gtk::Bin, gtk::Container, gtk::Widget,
|
||||
@implements gtk::Buildable;
|
||||
}
|
||||
|
||||
impl ColorSchemeDialog {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new(&[("use-header-bar", &1)])
|
||||
.expect("Failed to create ColorSchemeDialog")
|
||||
}
|
||||
|
||||
fn instance(&self) -> &dialog::ColorSchemeDialog {
|
||||
dialog::ColorSchemeDialog::from_instance(self)
|
||||
}
|
||||
|
||||
pub fn get_selected_scheme(&self) -> Option<Base16Scheme> {
|
||||
self.instance().get_selected_scheme()
|
||||
}
|
||||
|
||||
pub fn set_selected_scheme(&self, id: &str) {
|
||||
self.instance().set_selected_id(id);
|
||||
}
|
||||
}
|
155
realm-config-ui/src/configure_dialog/configure-dialog.ui
Normal file
155
realm-config-ui/src/configure_dialog/configure-dialog.ui
Normal file
@@ -0,0 +1,155 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
|
||||
<template class="ConfigureDialog" parent="GtkDialog">
|
||||
<property name="title">Configure Realm</property>
|
||||
<property name="modal">True</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="margin">20</property>
|
||||
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label">Options</property>
|
||||
<property name="halign">start</property>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="margin-bottom">20</property>
|
||||
<child>
|
||||
<!-- -->
|
||||
<object class="GtkListBox" id="bool-options-box">
|
||||
<property name="margin">10</property>
|
||||
<property name="selection_mode">none</property>
|
||||
<property name="activate_on_single_click">False</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="tooltip-markup"><![CDATA[<b><big>Overlay</big></b>
|
||||
|
||||
Type of rootfs overlay realm is configured to use.
|
||||
|
||||
<b>None</b> Don't use a rootfs overlay
|
||||
<b>TmpFS</b> Use a rootfs overlay stored on tmpfs
|
||||
<b>Storage</b> Use a rootfs overlay stored on disk in storage partition
|
||||
]]></property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label">Overlay</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="halign">start</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="overlay-combo">
|
||||
<property name="active">0</property>
|
||||
<items>
|
||||
<item id="storage">Storage</item>
|
||||
<item id="tmpfs">TmpFS</item>
|
||||
<item id="none">None</item>
|
||||
</items>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="tooltip-markup"><![CDATA[<b><big>RealmFS</big></b>
|
||||
|
||||
Root filesystem image to use for realm.
|
||||
]]></property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label">RealmFS</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="halign">start</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="realmfs-combo">
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="tooltip-markup"><![CDATA[<b><big>Terminal Color Scheme</big></b>
|
||||
|
||||
Choose a color scheme to use in terminals in this realm.
|
||||
]]></property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label">Color Scheme</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="halign">start</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="color-scheme-button">
|
||||
<property name="label">Default Dark</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="tooltip-markup"><![CDATA[<b><big>Window Frame Color</big></b>
|
||||
|
||||
Set a color to be used when frames are drawn around application windows for this realm.
|
||||
]]></property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label">Frame Color</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="halign">start</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkColorButton" id="frame-color-button">
|
||||
<property name="color">#ffff00000000</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="use-underline">1</property>
|
||||
<property name="label">Cancel</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="ok_button">
|
||||
<property name="use-underline">1</property>
|
||||
<property name="label">Apply</property>
|
||||
<property name="can-default">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="cancel">cancel_button</action-widget>
|
||||
<action-widget response="ok" default="true">ok_button</action-widget>
|
||||
</action-widgets>
|
||||
|
||||
</template>
|
||||
<object class="GtkSizeGroup">
|
||||
<widgets>
|
||||
<widget name="overlay-combo" />
|
||||
<widget name="realmfs-combo" />
|
||||
<widget name="color-scheme-button" />
|
||||
<widget name="frame-color-button" />
|
||||
</widgets>
|
||||
</object>
|
||||
</interface>
|
||||
|
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="ConfigureOption" parent="GtkListBoxRow">
|
||||
<property name="width_request">100</property>
|
||||
<property name="activatable">False</property>
|
||||
<property name="selectable">False</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="spacing">30</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="name">
|
||||
<property name="hexpand">True</property>
|
||||
<property name="halign">start</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="switch">
|
||||
<property name="halign">end</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
|
||||
</interface>
|
203
realm-config-ui/src/configure_dialog/dialog.rs
Normal file
203
realm-config-ui/src/configure_dialog/dialog.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
use std::cell::{Ref, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk::glib;
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
use gtk::CompositeTemplate;
|
||||
|
||||
use crate::colorscheme::ColorSchemeDialog;
|
||||
use crate::configure_dialog::ConfigOptions;
|
||||
use crate::configure_dialog::settings::CitadelSettings;
|
||||
use crate::realmsd::RealmConfig;
|
||||
|
||||
#[derive(CompositeTemplate)]
|
||||
#[template(file = "configure-dialog.ui")]
|
||||
pub struct ConfigureDialog {
|
||||
#[template_child(id="bool-options-box")]
|
||||
bool_option_list: TemplateChild<gtk::ListBox>,
|
||||
|
||||
#[template_child(id="overlay-combo")]
|
||||
overlay: TemplateChild<gtk::ComboBoxText>,
|
||||
|
||||
#[template_child(id="realmfs-combo")]
|
||||
realmfs: TemplateChild<gtk::ComboBoxText>,
|
||||
|
||||
#[template_child(id="color-scheme-button")]
|
||||
colorscheme: TemplateChild<gtk::Button>,
|
||||
|
||||
#[template_child(id="frame-color-button")]
|
||||
frame_color: TemplateChild<gtk::ColorButton>,
|
||||
|
||||
options: Rc<RefCell<ConfigOptions>>,
|
||||
|
||||
bool_option_rows: RefCell<Vec<super::ConfigureOption>>,
|
||||
|
||||
colorscheme_dialog: ColorSchemeDialog,
|
||||
|
||||
settings: RefCell<CitadelSettings>,
|
||||
|
||||
}
|
||||
|
||||
impl ConfigureDialog {
|
||||
|
||||
pub fn set_realm_name(&self, name: &str) {
|
||||
let color = self.settings.borrow().get_realm_color(Some(name));
|
||||
self.frame_color.set_rgba(&color);
|
||||
}
|
||||
|
||||
pub fn reset_options(&self) {
|
||||
self.options.borrow_mut().reset();
|
||||
self.update_options();
|
||||
}
|
||||
|
||||
pub fn set_config(&self, config: &RealmConfig) {
|
||||
self.options.borrow_mut().configure(config);
|
||||
self.realmfs.remove_all();
|
||||
|
||||
self.update_options();
|
||||
}
|
||||
|
||||
pub fn changes(&self) -> Vec<(String,String)> {
|
||||
self.options.borrow().changes()
|
||||
}
|
||||
|
||||
pub fn store_settings(&self, realm_name: &str) {
|
||||
let color = self.frame_color.rgba();
|
||||
self.settings.borrow_mut().store_realm_color(realm_name, color);
|
||||
}
|
||||
|
||||
pub fn options(&self) -> Ref<ConfigOptions> {
|
||||
self.options.borrow()
|
||||
}
|
||||
|
||||
fn update_realmfs(&self) {
|
||||
self.realmfs.remove_all();
|
||||
for realmfs in self.options().realmfs_list() {
|
||||
self.realmfs.append(Some(realmfs.as_str()), realmfs.as_str());
|
||||
}
|
||||
let current = self.options().realmfs();
|
||||
self.realmfs.set_active_id(Some(¤t));
|
||||
}
|
||||
|
||||
fn update_options(&self) {
|
||||
let rows = self.bool_option_rows.borrow();
|
||||
for row in rows.iter() {
|
||||
row.update();
|
||||
}
|
||||
let overlay_id = self.options().overlay_id();
|
||||
self.overlay.set_active_id(Some(&overlay_id));
|
||||
|
||||
self.update_realmfs();
|
||||
|
||||
let scheme = self.options().colorscheme();
|
||||
self.colorscheme.set_label(scheme.name());
|
||||
}
|
||||
|
||||
fn create_option_rows(&self) {
|
||||
let mut rows = self.bool_option_rows.borrow_mut();
|
||||
let options = self.options.borrow();
|
||||
for op in options.bool_options() {
|
||||
let w = super::ConfigureOption::new(op);
|
||||
self.bool_option_list.add(&w);
|
||||
rows.push(w);
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_overlay(&self) {
|
||||
let options = self.options.clone();
|
||||
self.overlay.connect_changed(move |combo| {
|
||||
if let Some(text) = combo.active_id() {
|
||||
options.borrow_mut().set_overlay_id(text.as_str());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn setup_realmfs(&self) {
|
||||
let options = self.options.clone();
|
||||
self.realmfs.connect_changed(move |combo| {
|
||||
if let Some(text) = combo.active_text() {
|
||||
options.borrow_mut().set_realmfs(text.as_str());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn setup_colorscheme(&self) {
|
||||
let dialog = self.colorscheme_dialog.clone();
|
||||
let options = self.options.clone();
|
||||
|
||||
self.colorscheme.connect_clicked(move |b| {
|
||||
dialog.show_all();
|
||||
let scheme = options.borrow().colorscheme();
|
||||
dialog.set_selected_scheme(scheme.slug());
|
||||
|
||||
match dialog.run() {
|
||||
gtk::ResponseType::Ok => {
|
||||
if let Some(scheme) = dialog.get_selected_scheme() {
|
||||
options.borrow_mut().set_colorscheme_id(scheme.slug());
|
||||
b.set_label(scheme.name());
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
dialog.hide();
|
||||
});
|
||||
}
|
||||
|
||||
fn setup_frame_color(&self) {
|
||||
let color = self.settings.borrow().get_realm_color(None);
|
||||
self.frame_color.set_rgba(&color);
|
||||
}
|
||||
|
||||
fn setup_widgets(&self) {
|
||||
self.create_option_rows();
|
||||
self.setup_overlay();
|
||||
self.setup_realmfs();
|
||||
self.setup_colorscheme();
|
||||
self.setup_frame_color();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ConfigureDialog {
|
||||
fn default() -> Self {
|
||||
ConfigureDialog {
|
||||
bool_option_list: Default::default(),
|
||||
overlay: Default::default(),
|
||||
realmfs: Default::default(),
|
||||
colorscheme: Default::default(),
|
||||
frame_color: Default::default(),
|
||||
colorscheme_dialog: ColorSchemeDialog::new(),
|
||||
options: Rc::new(RefCell::new(ConfigOptions::new())),
|
||||
settings: RefCell::new(CitadelSettings::new()),
|
||||
bool_option_rows: RefCell::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ConfigureDialog {
|
||||
const NAME: &'static str = "ConfigureDialog";
|
||||
type Type = super::ConfigureDialog;
|
||||
type ParentType = gtk::Dialog;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
}
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for ConfigureDialog {
|
||||
fn constructed(&self, obj: &Self::Type) {
|
||||
self.parent_constructed(obj);
|
||||
self.colorscheme_dialog.set_transient_for(Some(&self.instance()));
|
||||
self.setup_widgets();
|
||||
}
|
||||
}
|
||||
|
||||
impl DialogImpl for ConfigureDialog {}
|
||||
impl WindowImpl for ConfigureDialog {}
|
||||
impl BinImpl for ConfigureDialog {}
|
||||
impl ContainerImpl for ConfigureDialog {}
|
||||
impl WidgetImpl for ConfigureDialog {}
|
78
realm-config-ui/src/configure_dialog/mod.rs
Normal file
78
realm-config-ui/src/configure_dialog/mod.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use gtk::glib;
|
||||
use gtk::prelude::*;
|
||||
use glib::subclass::prelude::*;
|
||||
|
||||
use crate::realmsd::RealmConfig;
|
||||
pub use crate::configure_dialog::options::{ConfigOptions,BoolOption};
|
||||
|
||||
mod dialog;
|
||||
mod option_row;
|
||||
mod options;
|
||||
mod settings;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ConfigureDialog(ObjectSubclass<dialog::ConfigureDialog>)
|
||||
@extends gtk::Dialog, gtk::Window, gtk::Bin, gtk::Container, gtk::Widget,
|
||||
@implements gtk::Buildable;
|
||||
}
|
||||
|
||||
impl ConfigureDialog {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new(&[("use-header-bar", &1)])
|
||||
.expect("Failed to create ConfigureDialog")
|
||||
}
|
||||
|
||||
fn instance(&self) -> &dialog::ConfigureDialog {
|
||||
dialog::ConfigureDialog::from_instance(self)
|
||||
}
|
||||
|
||||
pub fn changes(&self) -> Vec<(String,String)> {
|
||||
self.instance().changes()
|
||||
}
|
||||
|
||||
pub fn store_settings(&self, realm_name: &str) {
|
||||
self.instance().store_settings(realm_name);
|
||||
}
|
||||
|
||||
pub fn reset_options(&self) {
|
||||
self.instance().reset_options();
|
||||
}
|
||||
|
||||
pub fn set_realm_name(&self, name: &str) {
|
||||
self.set_title(&format!("Configure realm-{}", name));
|
||||
self.instance().set_realm_name(name);
|
||||
}
|
||||
|
||||
pub fn set_config(&self, config: &RealmConfig) {
|
||||
self.instance().set_config(config);
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ConfigureOption(ObjectSubclass<option_row::ConfigureOption>)
|
||||
@extends gtk::Widget, gtk::Bin, gtk::Container,
|
||||
@implements gtk::Buildable, gtk::Actionable;
|
||||
}
|
||||
|
||||
impl ConfigureOption {
|
||||
pub fn new(option: &BoolOption) -> Self {
|
||||
let widget :Self = glib::Object::new(&[])
|
||||
.expect("Failed to create ConfigureOption");
|
||||
widget.set_bool_option(option);
|
||||
widget
|
||||
}
|
||||
|
||||
fn instance(&self) -> &option_row::ConfigureOption {
|
||||
option_row::ConfigureOption::from_instance(self)
|
||||
}
|
||||
|
||||
pub fn update(&self) {
|
||||
self.instance().update();
|
||||
}
|
||||
|
||||
fn set_bool_option(&self, option: &BoolOption) {
|
||||
self.set_tooltip_markup(Some(option.tooltip()));
|
||||
self.instance().set_bool_option(option);
|
||||
}
|
||||
}
|
||||
|
68
realm-config-ui/src/configure_dialog/option_row.rs
Normal file
68
realm-config-ui/src/configure_dialog/option_row.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use gtk::glib;
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
use gtk::CompositeTemplate;
|
||||
|
||||
use crate::configure_dialog::BoolOption;
|
||||
|
||||
#[derive(CompositeTemplate)]
|
||||
#[template(file = "configure-option-switch.ui")]
|
||||
pub struct ConfigureOption {
|
||||
#[template_child]
|
||||
pub name: TemplateChild<gtk::Label>,
|
||||
#[template_child]
|
||||
pub switch: TemplateChild<gtk::Switch>,
|
||||
|
||||
pub option: RefCell<Option<BoolOption>>,
|
||||
}
|
||||
|
||||
impl Default for ConfigureOption {
|
||||
fn default() -> Self {
|
||||
ConfigureOption {
|
||||
name: Default::default(),
|
||||
switch: Default::default(),
|
||||
option: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigureOption {
|
||||
pub fn set_bool_option(&self, option: &BoolOption) {
|
||||
self.name.set_text(option.description());
|
||||
self.switch.set_state(option.value());
|
||||
self.switch.connect_state_set(glib::clone!(@strong option => move |_b,v| {
|
||||
option.set_value(v);
|
||||
Inhibit(false)
|
||||
}));
|
||||
self.option.borrow_mut().replace(option.clone());
|
||||
}
|
||||
|
||||
pub fn update(&self) {
|
||||
let option = self.option.borrow();
|
||||
if let Some(option) = option.as_ref() {
|
||||
self.switch.set_state(option.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ConfigureOption {
|
||||
const NAME: &'static str = "ConfigureOption";
|
||||
type Type = super::ConfigureOption;
|
||||
type ParentType = gtk::ListBoxRow;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
}
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for ConfigureOption {}
|
||||
impl WidgetImpl for ConfigureOption {}
|
||||
impl ContainerImpl for ConfigureOption {}
|
||||
impl BinImpl for ConfigureOption {}
|
||||
impl ListBoxRowImpl for ConfigureOption {}
|
384
realm-config-ui/src/configure_dialog/options.rs
Normal file
384
realm-config-ui/src/configure_dialog/options.rs
Normal file
@@ -0,0 +1,384 @@
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use libcitadel::OverlayType;
|
||||
use libcitadel::terminal::Base16Scheme;
|
||||
|
||||
use crate::realmsd::RealmConfig;
|
||||
|
||||
const GPU_TOOLTIP: &str = r#"If enabled the render node device <tt><b>/dev/dri/renderD128</b></tt> will be mounted into the realm container.
|
||||
|
||||
If privileged device <tt><b>/dev/dri/card0</b></tt> is also needed set
|
||||
additional variable in realm configuration file:
|
||||
|
||||
<tt><b>use-gpu-card0 = true</b></tt>
|
||||
|
||||
"#;
|
||||
const WAYLAND_TOOLTIP: &str = "\
|
||||
If enabled access to Wayland display will be permitted in realm by adding wayland socket to realm.
|
||||
|
||||
<tt><b>/run/user/1000/wayland-0</b></tt>
|
||||
|
||||
";
|
||||
|
||||
const X11_TOOLTIP: &str = "\
|
||||
If enabled access to X11 server will be added by mounting directory X11 directory into realm.
|
||||
|
||||
<tt><b>/tmp/.X11-unix</b></tt>
|
||||
";
|
||||
|
||||
const SOUND_TOOLTIP: &str = r#"If enabled allows use of sound inside of realm. The following items will be added:
|
||||
|
||||
<tt><b>/dev/snd</b></tt>
|
||||
<tt><b>/dev/shm</b></tt>
|
||||
<tt><b>/run/user/1000/pulse</b></tt>
|
||||
"#;
|
||||
|
||||
const SHARED_DIR_TOOLTIP: &str = r#"If enabled the shared directory will be mounted as <tt><b>/Shared</b></tt> in home directory of realm.
|
||||
|
||||
This directory is shared between all realms with this option enabled and is an easy way to move files between realms.
|
||||
"#;
|
||||
|
||||
const NETWORK_TOOLTIP: &str = "\
|
||||
If enabled the realm will have access to the network.
|
||||
";
|
||||
|
||||
const KVM_TOOLTIP: &str = r#"If enabled device <tt><b>/dev/kvm</b></tt> will be added to realm.
|
||||
|
||||
This allows use of applications such as Qemu inside of realms.
|
||||
"#;
|
||||
|
||||
const EPHERMERAL_HOME_TOOLTIP: &str = r#"If enabled the home directory of realm will be set up in ephemeral mode.
|
||||
|
||||
The ephemeral home directory is set up with the following steps:
|
||||
|
||||
1. Home directory is mounted as tmpfs filesystem
|
||||
2. Any files in <tt><b>/realms/skel</b></tt> are copied into home directory
|
||||
3. Any files in <tt><b>/realms/realm-$name/skel</b></tt> are copied into home directory.
|
||||
4. Any directories listed in config file variable <tt><b>ephemeral_persistent_dirs</b></tt>
|
||||
are bind mounted from <tt><b>/realms/realm-$name/home</b></tt> into ephemeral
|
||||
home directory.
|
||||
"#;
|
||||
|
||||
const BOOL_OPTIONS: &[(&str, &str, &str)] = &[
|
||||
("use-gpu", "Use GPU in Realm", GPU_TOOLTIP),
|
||||
("use-wayland", "Use Wayland in Realm", WAYLAND_TOOLTIP),
|
||||
("use-x11", "Use X11 in Realm", X11_TOOLTIP),
|
||||
("use-sound", "Use Sound in Realm", SOUND_TOOLTIP),
|
||||
("use-shared-dir", "Mount /Shared directory in Realm", SHARED_DIR_TOOLTIP),
|
||||
("use-network", "Realm has network access", NETWORK_TOOLTIP),
|
||||
("use-kvm", "Use KVM (/dev/kvm) in Realm", KVM_TOOLTIP),
|
||||
("use-ephemeral-home", "Use ephemeral tmpfs mount for home directory", EPHERMERAL_HOME_TOOLTIP),
|
||||
];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BoolOption {
|
||||
id: String,
|
||||
description: String,
|
||||
tooltip: String,
|
||||
original: Rc<Cell<bool>>,
|
||||
value: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl BoolOption {
|
||||
fn create_options() -> Vec<BoolOption> {
|
||||
let mut bools = Vec::new();
|
||||
for (id, description, tooltip) in BOOL_OPTIONS {
|
||||
bools.push(BoolOption::new(id, description, tooltip));
|
||||
}
|
||||
bools
|
||||
}
|
||||
|
||||
fn new(id: &str, description: &str, tooltip: &str) -> Self {
|
||||
let id = id.to_string();
|
||||
let description = description.to_string();
|
||||
let tooltip = format!("<b><big>{}</big></b>\n\n{}", description, tooltip);
|
||||
let value = Rc::new(Cell::new(false));
|
||||
let original = Rc::new(Cell::new(false));
|
||||
BoolOption { id, description, tooltip, original, value }
|
||||
}
|
||||
|
||||
pub fn value(&self) -> bool {
|
||||
self.value.get()
|
||||
}
|
||||
|
||||
fn has_changed(&self) -> bool {
|
||||
self.value() != self.original.get()
|
||||
}
|
||||
|
||||
pub fn set_value(&self, v: bool) {
|
||||
self.value.set(v);
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn description(&self) -> &str {
|
||||
&self.description
|
||||
}
|
||||
|
||||
pub fn tooltip(&self) -> &str {
|
||||
&self.tooltip
|
||||
}
|
||||
|
||||
fn configure(&self, config: &RealmConfig) {
|
||||
let v = config.get_bool(self.id());
|
||||
self.original.set(v);
|
||||
self.value.set(v);
|
||||
}
|
||||
|
||||
fn reset(&self) {
|
||||
self.set_value(self.original.get());
|
||||
}
|
||||
|
||||
fn add_changes(&self, result: &mut Vec<(String, String)>) {
|
||||
if self.has_changed() {
|
||||
let k = self.id.clone();
|
||||
let v = self.value().to_string();
|
||||
result.push((k, v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OverlayOption {
|
||||
original: OverlayType,
|
||||
current: OverlayType,
|
||||
}
|
||||
|
||||
impl OverlayOption {
|
||||
fn new() -> Self {
|
||||
OverlayOption {
|
||||
original: OverlayType::None,
|
||||
current: OverlayType::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay_str_to_enum(str: Option<&str>) -> OverlayType {
|
||||
match str {
|
||||
Some("storage") => OverlayType::Storage,
|
||||
Some("tmpfs") => OverlayType::TmpFS,
|
||||
Some("none") => OverlayType::None,
|
||||
None => OverlayType::None,
|
||||
Some(s) => {
|
||||
warn!("Unexpected overlay type: {}", s);
|
||||
OverlayType::None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn set_overlay(&mut self, overlay: &str) {
|
||||
self.current = Self::overlay_str_to_enum(Some(overlay));
|
||||
}
|
||||
|
||||
fn str_value(&self) -> String {
|
||||
self.current.to_str_value()
|
||||
.unwrap_or("none").to_string()
|
||||
}
|
||||
|
||||
fn configure(&mut self, config: &RealmConfig) {
|
||||
let overlay = Self::overlay_str_to_enum(config.get_string("overlay"));
|
||||
self.original = overlay;
|
||||
self.current = overlay;
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.current = self.original;
|
||||
}
|
||||
|
||||
fn add_changes(&self, result: &mut Vec<(String, String)>) {
|
||||
if self.original != self.current {
|
||||
let k = "overlay".to_string();
|
||||
let v = self.str_value();
|
||||
result.push((k, v));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RealmFsOption {
|
||||
original: String,
|
||||
current: String,
|
||||
realmfs_list: Vec<String>,
|
||||
}
|
||||
|
||||
impl RealmFsOption {
|
||||
|
||||
fn new() -> Self {
|
||||
let base = String::from("base");
|
||||
RealmFsOption {
|
||||
original: base.clone(),
|
||||
current: base.clone(),
|
||||
realmfs_list: vec![base],
|
||||
}
|
||||
}
|
||||
|
||||
fn realmfs_list(&self) -> Vec<String> {
|
||||
self.realmfs_list.clone()
|
||||
}
|
||||
|
||||
fn current(&self) -> String {
|
||||
self.current.clone()
|
||||
}
|
||||
|
||||
fn set_current(&mut self, realmfs: &str) {
|
||||
self.current = realmfs.to_string();
|
||||
}
|
||||
|
||||
fn configure(&mut self, config: &RealmConfig) {
|
||||
if let Some(realmfs) = config.get_string("realmfs") {
|
||||
|
||||
self.realmfs_list.clear();
|
||||
self.realmfs_list.extend(config.realmfs_list().iter().cloned());
|
||||
self.original = realmfs.to_string();
|
||||
self.current = realmfs.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.current = self.original.clone();
|
||||
}
|
||||
|
||||
fn add_changes(&self, result: &mut Vec<(String, String)>) {
|
||||
if self.current.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.current != self.original {
|
||||
result.push(("realmfs".to_string(), self.current.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_SCHEME: &str = "default-dark";
|
||||
|
||||
struct ColorSchemeOption {
|
||||
original: Base16Scheme,
|
||||
current: Base16Scheme,
|
||||
}
|
||||
|
||||
impl ColorSchemeOption {
|
||||
fn new() -> Self {
|
||||
let scheme = Base16Scheme::by_name(DEFAULT_SCHEME)
|
||||
.expect("default Base16Scheme");
|
||||
|
||||
ColorSchemeOption {
|
||||
original: scheme.clone(),
|
||||
current: scheme.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn configure(&mut self, config: &RealmConfig) {
|
||||
if let Some(scheme) = config.get_string("terminal-scheme") {
|
||||
if let Some(scheme) = Base16Scheme::by_name(scheme) {
|
||||
self.original = scheme.clone();
|
||||
self.current = scheme.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.set_current(self.original.clone());
|
||||
}
|
||||
|
||||
fn set_current(&mut self, scheme: Base16Scheme) {
|
||||
self.current = scheme;
|
||||
}
|
||||
|
||||
fn set_current_id(&mut self, id: &str) {
|
||||
if let Some(scheme) = Base16Scheme::by_name(id) {
|
||||
self.set_current(scheme.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn current(&self) -> Base16Scheme {
|
||||
self.current.clone()
|
||||
}
|
||||
|
||||
fn add_changes(&self, result: &mut Vec<(String, String)>) {
|
||||
if self.original.slug() != self.current.slug() {
|
||||
result.push(("terminal-scheme".to_string(), self.current.slug().to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConfigOptions {
|
||||
bool_options: Vec<BoolOption>,
|
||||
overlay: OverlayOption,
|
||||
realmfs: RealmFsOption,
|
||||
colorscheme: ColorSchemeOption,
|
||||
}
|
||||
|
||||
impl ConfigOptions {
|
||||
|
||||
pub fn configure(&mut self, config: &RealmConfig) {
|
||||
for op in &self.bool_options {
|
||||
op.configure(config);
|
||||
}
|
||||
self.overlay.configure(config);
|
||||
self.realmfs.configure(config);
|
||||
self.colorscheme.configure(config);
|
||||
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
for op in &self.bool_options {
|
||||
op.reset();
|
||||
}
|
||||
self.overlay.reset();
|
||||
self.realmfs.reset();
|
||||
self.colorscheme.reset();
|
||||
}
|
||||
|
||||
pub fn changes(&self) -> Vec<(String,String)> {
|
||||
let mut changes = Vec::new();
|
||||
for op in &self.bool_options {
|
||||
op.add_changes(&mut changes);
|
||||
}
|
||||
self.overlay.add_changes(&mut changes);
|
||||
self.realmfs.add_changes(&mut changes);
|
||||
self.colorscheme.add_changes(&mut changes);
|
||||
changes
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
let bool_options = BoolOption::create_options();
|
||||
let overlay = OverlayOption::new();
|
||||
let realmfs = RealmFsOption::new();
|
||||
let colorscheme = ColorSchemeOption::new();
|
||||
ConfigOptions {
|
||||
bool_options, overlay, realmfs, colorscheme,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bool_options(&self) -> &[BoolOption] {
|
||||
&self.bool_options
|
||||
}
|
||||
|
||||
pub fn realmfs_list(&self) -> Vec<String> {
|
||||
self.realmfs.realmfs_list()
|
||||
}
|
||||
|
||||
pub fn overlay_id(&self) -> String {
|
||||
self.overlay.str_value()
|
||||
}
|
||||
|
||||
pub fn set_overlay_id(&mut self, id: &str) {
|
||||
self.overlay.set_overlay(id);
|
||||
}
|
||||
|
||||
pub fn realmfs(&self) -> String {
|
||||
self.realmfs.current()
|
||||
}
|
||||
|
||||
pub fn set_realmfs(&mut self, realmfs: &str) {
|
||||
self.realmfs.set_current(realmfs);
|
||||
}
|
||||
|
||||
pub fn colorscheme(&self) -> Base16Scheme {
|
||||
self.colorscheme.current()
|
||||
}
|
||||
|
||||
pub fn set_colorscheme_id(&mut self, id: &str) {
|
||||
self.colorscheme.set_current_id(id);
|
||||
}
|
||||
}
|
126
realm-config-ui/src/configure_dialog/settings.rs
Normal file
126
realm-config-ui/src/configure_dialog/settings.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use gtk::{gdk,gio};
|
||||
use gtk::gio::prelude::*;
|
||||
use rand::Rng;
|
||||
use libcitadel::Realm;
|
||||
|
||||
pub struct CitadelSettings {
|
||||
settings: gio::Settings,
|
||||
frame_colors: Vec<gdk::RGBA>,
|
||||
realms: Vec<RealmFrameColor>,
|
||||
used_colors: HashSet<gdk::RGBA>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RealmFrameColor(String,gdk::RGBA);
|
||||
|
||||
impl RealmFrameColor {
|
||||
|
||||
fn new(realm: &str, color: &gdk::RGBA) -> Self {
|
||||
RealmFrameColor(realm.to_string(), color.clone())
|
||||
}
|
||||
|
||||
fn realm(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn color(&self) -> &gdk::RGBA {
|
||||
&self.1
|
||||
}
|
||||
|
||||
fn set_color(&mut self, color: gdk::RGBA) {
|
||||
self.1 = color;
|
||||
}
|
||||
}
|
||||
|
||||
impl CitadelSettings {
|
||||
|
||||
fn choose_random_color(&self) -> gdk::RGBA {
|
||||
if !self.frame_colors.is_empty() {
|
||||
let n = rand::thread_rng().gen_range(0..self.frame_colors.len());
|
||||
self.frame_colors[n].clone()
|
||||
} else {
|
||||
gdk::RGBA::blue()
|
||||
}
|
||||
}
|
||||
|
||||
fn allocate_color(&self) -> gdk::RGBA {
|
||||
self.frame_colors.iter()
|
||||
.find(|&c| !self.used_colors.contains(c))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| self.choose_random_color())
|
||||
}
|
||||
|
||||
pub fn get_realm_color(&self, name: Option<&str>) -> gdk::RGBA {
|
||||
name.and_then(|name| self.get_realm_frame_color(name))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| self.allocate_color())
|
||||
}
|
||||
|
||||
pub fn store_realm_color(&mut self, name: &str, color: gdk::RGBA) -> bool {
|
||||
if let Some(realm) = self.realms.iter_mut().find(|r| r.realm() == name) {
|
||||
realm.set_color(color);
|
||||
} else {
|
||||
self.realms.push(RealmFrameColor::new(name, &color));
|
||||
}
|
||||
|
||||
let list = self.realms.iter().map(|r| r.to_string()).collect::<Vec<String>>();
|
||||
let realms = list.iter().map(|s| s.as_str()).collect::<Vec<&str>>();
|
||||
self.settings.set_strv("realm-label-colors", &realms).is_ok()
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
let settings = gio::Settings::new("com.subgraph.citadel");
|
||||
|
||||
let realms = settings.strv("realm-label-colors")
|
||||
.into_iter()
|
||||
.flat_map(|gs| RealmFrameColor::try_from(gs.as_str()).ok())
|
||||
.collect::<Vec<RealmFrameColor>>();
|
||||
|
||||
let frame_colors = settings.strv("label-color-list").into_iter()
|
||||
.flat_map(|gs| gs.as_str().parse().ok())
|
||||
.collect();
|
||||
|
||||
let used_colors = realms.iter()
|
||||
.map(|rfc| rfc.1.clone()).collect();
|
||||
|
||||
CitadelSettings {
|
||||
settings,
|
||||
frame_colors,
|
||||
realms,
|
||||
used_colors,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_realm_frame_color(&self, name: &str) -> Option<&gdk::RGBA> {
|
||||
self.realms.iter()
|
||||
.find(|r| r.realm() == name)
|
||||
.map(|r| r.color())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for RealmFrameColor {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let idx = value.find(':').ok_or(())?;
|
||||
let (realm, color_str) = value.split_at(idx);
|
||||
|
||||
let rgba = &color_str[1..].parse::<gdk::RGBA>()
|
||||
.map_err(|_| ())?;
|
||||
|
||||
if Realm::is_valid_name(realm) {
|
||||
Ok(RealmFrameColor::new(realm, rgba))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for RealmFrameColor {
|
||||
fn to_string(&self) -> String {
|
||||
format!("{}:{}", self.realm(), self.color())
|
||||
}
|
||||
}
|
60
realm-config-ui/src/error.rs
Normal file
60
realm-config-ui/src/error.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use std::result;
|
||||
use std::fmt;
|
||||
use crate::error::Error::Zbus;
|
||||
use std::fmt::Formatter;
|
||||
use gtk::prelude::*;
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Zbus(zbus::Error),
|
||||
ManagerConnect,
|
||||
NoSuchRealm(String),
|
||||
CreateRealmFailed,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
fn create_dialog(&self) -> gtk::MessageDialog {
|
||||
let title = "Error";
|
||||
let message = self.to_string();
|
||||
|
||||
gtk::MessageDialog::builder()
|
||||
.message_type(gtk::MessageType::Error)
|
||||
.title(title)
|
||||
.text(&message)
|
||||
.buttons(gtk::ButtonsType::Close)
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn error_dialog<P: IsA<gtk::Window>>(&self, parent: Option<&P>) {
|
||||
let dialog = self.create_dialog();
|
||||
dialog.set_transient_for(parent);
|
||||
dialog.run();
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
pub fn app_error_dialog(&self, app: >k::Application) {
|
||||
let dialog = self.create_dialog();
|
||||
app.add_window(&dialog);
|
||||
dialog.run();
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Zbus(e) => write!(f, "ZBus error: {}", e),
|
||||
Error::ManagerConnect => write!(f, "Unable to connect to Realms Manager"),
|
||||
Error::NoSuchRealm(name) => write!(f, "Realm '{}' does not exist", name),
|
||||
Error::CreateRealmFailed => write!(f, "Failed to create new realm"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<zbus::Error> for Error {
|
||||
fn from(e: zbus::Error) -> Self {
|
||||
Zbus(e)
|
||||
}
|
||||
}
|
120
realm-config-ui/src/main.rs
Normal file
120
realm-config-ui/src/main.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
#[macro_use] extern crate libcitadel;
|
||||
use std::env;
|
||||
|
||||
use gtk::prelude::*;
|
||||
use gtk::gio;
|
||||
|
||||
use crate::configure_dialog::ConfigureDialog;
|
||||
use crate::new_realm::NewRealmDialog;
|
||||
use crate::error::Result;
|
||||
use crate::realmsd::{RealmConfig, RealmsManagerProxy};
|
||||
|
||||
mod realmsd;
|
||||
mod error;
|
||||
mod colorscheme;
|
||||
mod configure_dialog;
|
||||
mod new_realm;
|
||||
|
||||
|
||||
fn load_realm_names() -> Result<(RealmsManagerProxy<'static>, Vec<String>, RealmConfig)> {
|
||||
let manager = RealmsManagerProxy::connect()?;
|
||||
let names = manager.realm_names()?;
|
||||
let config = manager.default_config()?;
|
||||
Ok((manager, names, config))
|
||||
}
|
||||
|
||||
fn new_realm_ui(app: >k::Application) {
|
||||
let (manager, realms, config) = match load_realm_names() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
err.app_error_dialog(app);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let dialog = NewRealmDialog::new();
|
||||
dialog.set_realm_names(&realms);
|
||||
dialog.set_config(&config);
|
||||
app.add_window(&dialog);
|
||||
dialog.show_all();
|
||||
|
||||
if dialog.run() == gtk::ResponseType::Ok {
|
||||
let realm = dialog.get_realm_name();
|
||||
dialog.store_config_settings();
|
||||
let changes = dialog.config_changes();
|
||||
if let Err(err) = manager.create_new_realm(&realm, changes) {
|
||||
err.error_dialog(Some(&dialog));
|
||||
}
|
||||
}
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
fn load_realm_config(realm_name: &str) -> Result<(RealmsManagerProxy<'static>, RealmConfig)> {
|
||||
let manager = RealmsManagerProxy::connect()?;
|
||||
let config = manager.config(realm_name)?;
|
||||
Ok((manager, config))
|
||||
}
|
||||
|
||||
fn configure_realm_ui(app: >k::Application, name: &str) {
|
||||
let (manager, config) = match load_realm_config(name) {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
err.app_error_dialog(app);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let dialog = ConfigureDialog::new();
|
||||
app.add_window(&dialog);
|
||||
dialog.set_config(&config);
|
||||
dialog.set_realm_name(name);
|
||||
dialog.show_all();
|
||||
|
||||
if dialog.run() == gtk::ResponseType::Ok {
|
||||
dialog.store_settings(name);
|
||||
let changes = dialog.changes();
|
||||
if !changes.is_empty() {
|
||||
if let Err(err) = manager.configure_realm(name, changes) {
|
||||
err.error_dialog(Some(&dialog));
|
||||
}
|
||||
}
|
||||
}
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
fn test_ui(app: >k::Application) {
|
||||
let config = RealmConfig::new_default(vec![String::from("main"), String::from("foo")]);
|
||||
let dialog = ConfigureDialog::new();
|
||||
app.add_window(&dialog);
|
||||
dialog.set_config(&config);
|
||||
dialog.set_title("Configure realm-testing");
|
||||
dialog.show_all();
|
||||
|
||||
if dialog.run() == gtk::ResponseType::Ok {
|
||||
let changes = dialog.changes();
|
||||
println!("Changes: {:?}", changes);
|
||||
}
|
||||
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
let mut args = env::args().collect::<Vec<String>>();
|
||||
|
||||
|
||||
if args.len() > 1 {
|
||||
let first = args.remove(1);
|
||||
let application = gtk::Application::new(Some("com.subgraph.RealmConfig"), gio::ApplicationFlags::empty());
|
||||
if first.as_str() == "--new" {
|
||||
application.connect_activate(new_realm_ui);
|
||||
} else if first.as_str() == "--test" {
|
||||
application.connect_activate(test_ui);
|
||||
} else {
|
||||
application.connect_activate(move |app| {
|
||||
configure_realm_ui(app, &first);
|
||||
});
|
||||
}
|
||||
application.run_with_args(&args);
|
||||
}
|
||||
}
|
134
realm-config-ui/src/new_realm/dialog.rs
Normal file
134
realm-config-ui/src/new_realm/dialog.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk::glib;
|
||||
use gtk::CompositeTemplate;
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
|
||||
use crate::configure_dialog::ConfigureDialog;
|
||||
use crate::new_realm::verifier::RealmNameVerifier;
|
||||
use crate::realmsd::RealmConfig;
|
||||
|
||||
#[derive(CompositeTemplate)]
|
||||
#[template(file = "new-realm-dialog.ui")]
|
||||
pub struct NewRealmDialog {
|
||||
#[template_child]
|
||||
pub infobar: TemplateChild<gtk::InfoBar>,
|
||||
|
||||
#[template_child]
|
||||
pub infolabel: TemplateChild<gtk::Label>,
|
||||
|
||||
#[template_child]
|
||||
pub label: TemplateChild<gtk::Label>,
|
||||
|
||||
#[template_child]
|
||||
entry: TemplateChild<gtk::Entry>,
|
||||
|
||||
#[template_child (id="config-button")]
|
||||
pub config_button: TemplateChild<gtk::Button>,
|
||||
|
||||
pub realm_names: Rc<RefCell<Vec<String>>>,
|
||||
|
||||
configure_dialog: ConfigureDialog,
|
||||
}
|
||||
|
||||
impl Default for NewRealmDialog {
|
||||
fn default() -> Self {
|
||||
NewRealmDialog {
|
||||
infobar: Default::default(),
|
||||
infolabel: Default::default(),
|
||||
label: Default::default(),
|
||||
entry: Default::default(),
|
||||
config_button: Default::default(),
|
||||
realm_names: Default::default(),
|
||||
configure_dialog: ConfigureDialog::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NewRealmDialog {
|
||||
pub fn set_realm_names(&self, names: &[String]) {
|
||||
let mut lock = self.realm_names.borrow_mut();
|
||||
lock.clear();
|
||||
lock.extend_from_slice(&names)
|
||||
}
|
||||
|
||||
pub fn set_config(&self, config: &RealmConfig) {
|
||||
self.configure_dialog.set_config(config);
|
||||
}
|
||||
|
||||
pub fn get_realm_name(&self) -> String {
|
||||
self.entry.text().to_string()
|
||||
}
|
||||
|
||||
pub fn config_changes(&self) -> Vec<(String,String)> {
|
||||
self.configure_dialog.changes()
|
||||
}
|
||||
|
||||
pub fn store_config_settings(&self) {
|
||||
let realm_name = self.get_realm_name();
|
||||
if !realm_name.is_empty() {
|
||||
self.configure_dialog.store_settings(&realm_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for NewRealmDialog {
|
||||
const NAME: &'static str = "NewRealmDialog";
|
||||
type Type = super::NewRealmDialog;
|
||||
type ParentType = gtk::Dialog;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
}
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for NewRealmDialog {
|
||||
fn constructed(&self, obj: &Self::Type) {
|
||||
self.parent_constructed(obj);
|
||||
|
||||
self.configure_dialog.set_transient_for(Some(&self.instance()));
|
||||
let verifier = Rc::new(RealmNameVerifier::new(self));
|
||||
|
||||
self.entry.connect_insert_text(glib::clone!(@strong verifier => move |entry, text, pos|{
|
||||
if !verifier.verify_insert(entry, text, *pos) {
|
||||
entry.stop_signal_emission("insert-text");
|
||||
}
|
||||
}));
|
||||
|
||||
self.entry.connect_delete_text(glib::clone!(@strong verifier => move |entry, start, end| {
|
||||
if !verifier.verify_delete(entry, start, end) {
|
||||
entry.stop_signal_emission("delete-text");
|
||||
}
|
||||
}));
|
||||
|
||||
self.entry.connect_changed(glib::clone!(@strong verifier => move |entry| {
|
||||
verifier.changed(entry);
|
||||
}));
|
||||
|
||||
let config_dialog = self.configure_dialog.clone();
|
||||
let entry = self.entry.clone();
|
||||
self.config_button.connect_clicked(move |_b| {
|
||||
let name = entry.text().to_string();
|
||||
config_dialog.set_title(&format!("Configure realm-{}", name));
|
||||
config_dialog.show_all();
|
||||
match config_dialog.run() {
|
||||
gtk::ResponseType::Ok => {},
|
||||
_ => config_dialog.reset_options(),
|
||||
}
|
||||
config_dialog.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl DialogImpl for NewRealmDialog {}
|
||||
impl WindowImpl for NewRealmDialog {}
|
||||
impl BinImpl for NewRealmDialog {}
|
||||
impl ContainerImpl for NewRealmDialog {}
|
||||
impl WidgetImpl for NewRealmDialog {}
|
44
realm-config-ui/src/new_realm/mod.rs
Normal file
44
realm-config-ui/src/new_realm/mod.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use gtk::glib;
|
||||
use glib::subclass::prelude::*;
|
||||
|
||||
use crate::realmsd::RealmConfig;
|
||||
|
||||
mod dialog;
|
||||
mod verifier;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct NewRealmDialog(ObjectSubclass<dialog::NewRealmDialog>)
|
||||
@extends gtk::Dialog, gtk::Window, gtk::Bin, gtk::Container, gtk::Widget,
|
||||
@implements gtk::Buildable;
|
||||
}
|
||||
|
||||
impl NewRealmDialog {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new(&[("use-header-bar", &1)])
|
||||
.expect("Failed to create NewRealmDialog")
|
||||
}
|
||||
|
||||
fn instance(&self) -> &dialog::NewRealmDialog {
|
||||
dialog::NewRealmDialog::from_instance(self)
|
||||
}
|
||||
|
||||
pub fn set_realm_names(&self, names: &[String]) {
|
||||
self.instance().set_realm_names(names);
|
||||
}
|
||||
|
||||
pub fn set_config(&self, config: &RealmConfig) {
|
||||
self.instance().set_config(config);
|
||||
}
|
||||
|
||||
pub fn get_realm_name(&self) -> String {
|
||||
self.instance().get_realm_name()
|
||||
}
|
||||
|
||||
pub fn config_changes(&self) -> Vec<(String,String)> {
|
||||
self.instance().config_changes()
|
||||
}
|
||||
|
||||
pub fn store_config_settings(&self) {
|
||||
self.instance().store_config_settings();
|
||||
}
|
||||
}
|
89
realm-config-ui/src/new_realm/new-realm-dialog.ui
Normal file
89
realm-config-ui/src/new_realm/new-realm-dialog.ui
Normal file
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="NewRealmDialog" parent="GtkDialog">
|
||||
<property name="title">Create New Realm</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
|
||||
<!-- GtkInfoBar -->
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="infobar">
|
||||
<property name="revealed">False</property>
|
||||
<property name="message-type">warning</property>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<child>
|
||||
<object class="GtkLabel" id="infolabel">
|
||||
<property name="label">Name already exists</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<!-- GtkLabel -->
|
||||
<child>
|
||||
<object class="GtkLabel" id="label">
|
||||
<property name="label">Enter name for new realm:</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-start">20</property>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<!-- GtkEntry-->
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<child>
|
||||
<object class="GtkEntry" id="entry">
|
||||
<property name="hexpand">True</property>
|
||||
<property name="placeholder-text">Enter name of new realm</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">20</property>
|
||||
<property name="margin-start">20</property>
|
||||
<property name="margin-end">5</property>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<!-- GtkButton -->
|
||||
<child>
|
||||
<object class="GtkButton" id="config-button">
|
||||
<property name="sensitive">False</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">20</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">20</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">emblem-system-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="use-underline">1</property>
|
||||
<property name="label">Cancel</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="ok_button">
|
||||
<property name="use-underline">1</property>
|
||||
<property name="label">Create</property>
|
||||
<property name="can-default">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="cancel">cancel_button</action-widget>
|
||||
<action-widget response="ok" default="true">ok_button</action-widget>
|
||||
</action-widgets>
|
||||
</template>
|
||||
</interface>
|
76
realm-config-ui/src/new_realm/verifier.rs
Normal file
76
realm-config-ui/src/new_realm/verifier.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
|
||||
use libcitadel::Realm;
|
||||
|
||||
use crate::new_realm::dialog::NewRealmDialog;
|
||||
|
||||
pub struct RealmNameVerifier {
|
||||
ok: gtk::Widget,
|
||||
infobar: gtk::InfoBar,
|
||||
infolabel: gtk::Label,
|
||||
label: gtk::Label,
|
||||
config: gtk::Button,
|
||||
realms: Rc<RefCell<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl RealmNameVerifier {
|
||||
pub fn new(dialog: &NewRealmDialog) -> Self {
|
||||
let ok = dialog.instance().widget_for_response(gtk::ResponseType::Ok).expect("No Ok Widget found");
|
||||
RealmNameVerifier {
|
||||
ok,
|
||||
infobar: dialog.infobar.clone(),
|
||||
infolabel: dialog.infolabel.clone(),
|
||||
label: dialog.label.clone(),
|
||||
config: dialog.config_button.clone(),
|
||||
realms: dialog.realm_names.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_insert(&self, entry: >k::Entry, text: &str, pos: i32) -> bool {
|
||||
let mut s = entry.text().to_string();
|
||||
s.insert_str(pos as usize, text);
|
||||
Realm::is_valid_name(&s)
|
||||
}
|
||||
|
||||
pub fn verify_delete(&self, entry: >k::Entry, start: i32, end: i32) -> bool {
|
||||
let mut s = entry.text().to_string();
|
||||
let start = start as usize;
|
||||
let end = end as usize;
|
||||
s.replace_range(start..end, "");
|
||||
s.is_empty() || Realm::is_valid_name(&s)
|
||||
}
|
||||
|
||||
fn verify_name (&self, name: &String) -> bool {
|
||||
if self.realms.borrow().contains(name) {
|
||||
self.infolabel.set_markup(&format!("Realm already exists with name <b>realm-{}</b>", name));
|
||||
self.infobar.set_revealed(true);
|
||||
false
|
||||
} else {
|
||||
self.infobar.set_revealed(false);
|
||||
self.infolabel.set_markup("");
|
||||
!name.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn changed(&self, entry: >k::Entry) {
|
||||
let s = entry.text().to_string();
|
||||
|
||||
if self.verify_name(&s) {
|
||||
self.ok.set_sensitive(true);
|
||||
self.config.set_sensitive(true);
|
||||
self.label.set_markup(&format!("<b>realm-{}</b>", s));
|
||||
} else {
|
||||
self.ok.set_sensitive(false);
|
||||
self.config.set_sensitive(false);
|
||||
if s.is_empty() {
|
||||
self.label.set_markup("Enter name for new realm:");
|
||||
} else {
|
||||
self.label.set_markup("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
154
realm-config-ui/src/realmsd.rs
Normal file
154
realm-config-ui/src/realmsd.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use zbus::dbus_proxy;
|
||||
use zvariant::derive::Type;
|
||||
use serde::{Serialize,Deserialize};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
#[derive(Deserialize,Serialize,Type)]
|
||||
pub struct RealmItem {
|
||||
name: String,
|
||||
description: String,
|
||||
realmfs: String,
|
||||
namespace: u64,
|
||||
status: u8,
|
||||
}
|
||||
|
||||
impl RealmItem {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone)]
|
||||
pub struct RealmConfig {
|
||||
options: Rc<HashMap<String,String>>,
|
||||
realmfs_list: Rc<Vec<String>>,
|
||||
}
|
||||
|
||||
impl RealmConfig {
|
||||
pub fn new_default(realmfs_list: Vec<String>) -> Self {
|
||||
let config = libcitadel::RealmConfig::default();
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert("use-gpu".to_string(), config.gpu().to_string());
|
||||
vars.insert("use-wayland".to_string(), config.wayland().to_string());
|
||||
vars.insert("use-x11".to_string(), config.x11().to_string());
|
||||
vars.insert("use-sound".to_string(), config.sound().to_string());
|
||||
vars.insert("use-shared-dir".to_string(), config.shared_dir().to_string());
|
||||
vars.insert("use-network".to_string(), config.network().to_string());
|
||||
vars.insert("use-kvm".to_string(), config.kvm().to_string());
|
||||
vars.insert("use-ephemeral-home".to_string(), config.ephemeral_home().to_string());
|
||||
|
||||
if realmfs_list.contains(&String::from("main")) {
|
||||
vars.insert("realmfs".to_string(), String::from("main"));
|
||||
} else if let Some(first) = realmfs_list.first() {
|
||||
vars.insert("realmfs".to_string(), first.clone());
|
||||
}
|
||||
Self::new(vars, realmfs_list)
|
||||
}
|
||||
|
||||
fn new(options: HashMap<String, String>, realmfs_list: Vec<String>) -> Self {
|
||||
RealmConfig {
|
||||
options: Rc::new(options),
|
||||
realmfs_list: Rc::new(realmfs_list),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_string(&self, id: &str) -> Option<&str> {
|
||||
self.options.get(id).map(|s| s.as_str())
|
||||
}
|
||||
|
||||
fn parse_bool(val: &str) -> bool {
|
||||
match val.parse::<bool>() {
|
||||
Ok(v) => v,
|
||||
_ => {
|
||||
warn!("Failed to parse value '{}' as bool", val);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_bool(&self, id: &str) -> bool {
|
||||
match self.get_string(id) {
|
||||
Some(val) => Self::parse_bool(val),
|
||||
None => {
|
||||
warn!("No value found for option '{}'", id);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn realmfs_list(&self) -> &[String] {
|
||||
&self.realmfs_list
|
||||
}
|
||||
}
|
||||
|
||||
#[dbus_proxy(
|
||||
default_service = "com.subgraph.realms",
|
||||
interface = "com.subgraph.realms.Manager",
|
||||
default_path = "/com/subgraph/realms"
|
||||
)]
|
||||
pub trait RealmsManager {
|
||||
fn get_current(&self) -> zbus::Result<String>;
|
||||
fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) -> zbus::Result<()>;
|
||||
fn list(&self) -> zbus::Result<Vec<RealmItem>>;
|
||||
fn realm_config(&self, name: &str) -> zbus::Result<HashMap<String,String>>;
|
||||
fn realm_exists(&self, name: &str) -> zbus::Result<bool>;
|
||||
fn list_realm_f_s(&self) -> zbus::Result<Vec<String>>;
|
||||
fn create_realm(&self, name: &str) -> zbus::Result<bool>;
|
||||
}
|
||||
|
||||
impl RealmsManagerProxy<'_> {
|
||||
pub fn connect() -> Result<Self> {
|
||||
let connection = zbus::Connection::new_system()?;
|
||||
|
||||
let proxy = RealmsManagerProxy::new(&connection)
|
||||
.map_err(|_| Error::ManagerConnect)?;
|
||||
|
||||
// Test connection
|
||||
proxy.get_current().map_err(|_| Error::ManagerConnect)?;
|
||||
|
||||
Ok(proxy)
|
||||
}
|
||||
|
||||
pub fn realm_names(&self) -> Result<Vec<String>> {
|
||||
let realms = self.list()?;
|
||||
let names = realms.iter()
|
||||
.map(|r| r.name().to_string())
|
||||
.collect();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
pub fn default_config(&self) -> Result<RealmConfig> {
|
||||
let realmfs_list = self.list_realm_f_s()?;
|
||||
Ok(RealmConfig::new_default(realmfs_list))
|
||||
}
|
||||
|
||||
pub fn config(&self, realm: &str) -> Result<RealmConfig> {
|
||||
if !self.realm_exists(realm)? {
|
||||
return Err(Error::NoSuchRealm(realm.to_string()));
|
||||
}
|
||||
|
||||
let options = self.realm_config(realm)?;
|
||||
let realmfs_list = self.list_realm_f_s()?;
|
||||
Ok(RealmConfig::new(options, realmfs_list))
|
||||
}
|
||||
|
||||
pub fn configure_realm(&self, realm: &str, config: Vec<(String, String)>) -> Result<()> {
|
||||
self.realm_set_config(realm, config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_new_realm(&self, realm: &str, config: Vec<(String, String)>) -> Result<()> {
|
||||
if self.create_realm(realm)? {
|
||||
if !config.is_empty() {
|
||||
self.realm_set_config(realm, config)?;
|
||||
}
|
||||
} else {
|
||||
return Err(Error::CreateRealmFailed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@@ -6,10 +6,8 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
async-io = "2.3.2"
|
||||
blocking = "1.6.1"
|
||||
event-listener = "5.3.1"
|
||||
zbus = "5.7.1"
|
||||
zbus = "=2.0.0-beta.5"
|
||||
zvariant = "2.7.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_repr = "0.1.20"
|
||||
serde_repr = "0.1.8"
|
||||
|
||||
|
@@ -1,16 +1,15 @@
|
||||
use async_io::block_on;
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
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_emitter<F>(&self, func: F) -> zbus::Result<()>
|
||||
fn with_server<F>(&self, func: F) -> zbus::Result<()>
|
||||
where
|
||||
F: Fn(&SignalEmitter) -> 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 emitter = iface.signal_emitter();
|
||||
func(emitter)
|
||||
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_emitter(|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_emitter(|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_emitter(|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_emitter(|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_emitter(|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 = Connection::system()
|
||||
let connection = create_system_connection()
|
||||
.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,227 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zbus::fdo;
|
||||
use libcitadel::{LiveConfig, OverlayType, Realm, GLOBAL_CONFIG};
|
||||
use libcitadel::terminal::Base16Scheme;
|
||||
use zbus::zvariant::Type;
|
||||
use crate::next::manager::failed;
|
||||
|
||||
const BOOL_CONFIG_VARS: &[&str] = &[
|
||||
"use-gpu", "use-gpu-card0", "use-wayland", "use-x11", "use-sound",
|
||||
"use-shared-dir", "use-network", "use-kvm", "use-ephemeral-home",
|
||||
"use-media-dir", "use-fuse", "use-flatpak",
|
||||
];
|
||||
|
||||
fn is_bool_config_variable(variable: &str) -> bool {
|
||||
BOOL_CONFIG_VARS.iter().any(|&s| s == variable)
|
||||
}
|
||||
|
||||
fn add_boolean_vars(config: &libcitadel::RealmConfig, vars: &mut RealmConfigVars) {
|
||||
vars.add_bool("use-gpu", config.gpu());
|
||||
vars.add_bool("use-gpu-card0", config.gpu_card0());
|
||||
vars.add_bool("use-wayland", config.wayland());
|
||||
vars.add_bool("use-x11", config.x11());
|
||||
vars.add_bool("use-sound", config.sound());
|
||||
vars.add_bool("use-shared-dir", config.shared_dir());
|
||||
vars.add_bool("use-network", config.network());
|
||||
vars.add_bool("use-kvm", config.kvm());
|
||||
vars.add_bool("use-ephemeral-home", config.ephemeral_home());
|
||||
vars.add_bool("use-media-dir", config.media_dir());
|
||||
vars.add_bool("use-fuse", config.fuse());
|
||||
vars.add_bool("use-flatpak", config.flatpak());
|
||||
}
|
||||
|
||||
fn set_boolean_config_var(config: &mut libcitadel::RealmConfig, var: &str, value: bool) -> bool {
|
||||
match var {
|
||||
"use-gpu" => config.set_gpu(value),
|
||||
"use-gpu-card0" => config.set_gpu_card0(value),
|
||||
"use-wayland" => config.set_wayland(value),
|
||||
"use-x11" => config.set_x11(value),
|
||||
"use-sound" => config.set_sound(value),
|
||||
"use-shared-dir" => config.set_shared_dir(value),
|
||||
"use-network" => config.set_network(value),
|
||||
"use-kvm" => config.set_kvm(value),
|
||||
"use-ephemeral-home" => config.set_ephemeral_home(value),
|
||||
"use-media-dir" => config.set_media_dir(value),
|
||||
"use-fuse" => config.set_fuse(value),
|
||||
"use-flatpak" => config.set_flatpak(value),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize,Serialize,Type)]
|
||||
pub struct RealmConfigVars {
|
||||
items: HashMap<String,String>,
|
||||
}
|
||||
|
||||
impl RealmConfigVars {
|
||||
fn new() -> Self {
|
||||
RealmConfigVars { items: HashMap::new() }
|
||||
}
|
||||
|
||||
pub fn new_global() -> Self {
|
||||
Self::new_from_config(&GLOBAL_CONFIG)
|
||||
}
|
||||
|
||||
fn new_from_realm(realm: &Realm) -> Self {
|
||||
let config = realm.config();
|
||||
Self::new_from_config(&config)
|
||||
}
|
||||
|
||||
fn new_from_config(config: &libcitadel::RealmConfig) -> Self {
|
||||
let mut vars = RealmConfigVars::new();
|
||||
add_boolean_vars(config, &mut vars);
|
||||
|
||||
let overlay = match config.overlay() {
|
||||
OverlayType::None => "none",
|
||||
OverlayType::TmpFS => "tmpfs",
|
||||
OverlayType::Storage => "storage",
|
||||
};
|
||||
vars.add("overlay", overlay);
|
||||
|
||||
let scheme = match config.terminal_scheme() {
|
||||
Some(name) => name.to_string(),
|
||||
None => String::new(),
|
||||
};
|
||||
vars.add("terminal-scheme", scheme);
|
||||
vars.add("realmfs", config.realmfs());
|
||||
vars
|
||||
}
|
||||
|
||||
fn add_bool(&mut self, name: &str, val: bool) {
|
||||
let valstr = if val { "true".to_string() } else { "false".to_string() };
|
||||
self.add(name, valstr);
|
||||
}
|
||||
|
||||
fn add<S,T>(&mut self, k: S, v: T) where S: Into<String>, T: Into<String> {
|
||||
self.items.insert(k.into(), v.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealmConfig {
|
||||
realm: Realm,
|
||||
changed: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl RealmConfig {
|
||||
|
||||
pub fn new(realm: Realm) -> Self {
|
||||
let changed = Arc::new(AtomicBool::new(false));
|
||||
RealmConfig { realm, changed }
|
||||
}
|
||||
|
||||
fn mark_changed(&self) {
|
||||
self.changed.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn is_changed(&self) -> bool {
|
||||
self.changed.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn config_vars(&self) -> RealmConfigVars {
|
||||
RealmConfigVars::new_from_realm(&self.realm)
|
||||
}
|
||||
|
||||
fn set_bool_var(&mut self, var: &str, value: &str) -> fdo::Result<()> {
|
||||
let v = match value {
|
||||
"true" => true,
|
||||
"false" => false,
|
||||
_ => return failed(format!("Invalid boolean value '{}' for realm config variable '{}'", value, var)),
|
||||
};
|
||||
|
||||
let mut has_changed = true;
|
||||
self.realm.with_mut_config(|c| {
|
||||
has_changed = set_boolean_config_var(c, var, v);
|
||||
});
|
||||
if has_changed {
|
||||
self.mark_changed();
|
||||
if self.realm.is_active() && LiveConfig::is_live_configurable(var) {
|
||||
let lc = LiveConfig::new(&self.realm);
|
||||
if let Err(err) = lc.configure(var, v) {
|
||||
warn!("Error live setting {} = {}: {}", var, value, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_overlay(&mut self, value: &str) -> fdo::Result<()> {
|
||||
let val = match value {
|
||||
"tmpfs" => Some("tmpfs".to_string()),
|
||||
"storage" => Some("storage".to_string()),
|
||||
"none" => None,
|
||||
_ => return failed(format!("Invalid value '{}' for overlay config", value)),
|
||||
};
|
||||
if self.realm.config().overlay != val {
|
||||
self.realm.with_mut_config(|c| {
|
||||
c.overlay = Some(value.to_string());
|
||||
});
|
||||
self.mark_changed();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_terminal_scheme(&mut self, value: &str) -> fdo::Result<()> {
|
||||
if Some(value) == self.realm.config().terminal_scheme() {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
let scheme = match Base16Scheme::by_name(value) {
|
||||
Some(scheme) => scheme,
|
||||
None => return failed(format!("Invalid terminal color scheme '{}'", value)),
|
||||
};
|
||||
|
||||
let manager = self.realm.manager();
|
||||
if let Err(err) = scheme.apply_to_realm(&manager, &self.realm) {
|
||||
return failed(format!("Error applying terminal color scheme: {}", err));
|
||||
}
|
||||
|
||||
self.realm.with_mut_config(|c| {
|
||||
c.terminal_scheme = Some(value.to_string());
|
||||
});
|
||||
self.mark_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_realmfs(&mut self, value: &str) -> fdo::Result<()> {
|
||||
let manager = self.realm.manager();
|
||||
if manager.realmfs_by_name(value).is_none() {
|
||||
return failed(format!("Failed to set 'realmfs' config for realm-{}: RealmFS named '{}' does not exist", self.realm.name(), value));
|
||||
}
|
||||
if self.realm.config().realmfs() != value {
|
||||
self.realm.with_mut_config(|c| {
|
||||
c.realmfs = Some(value.to_string())
|
||||
});
|
||||
self.mark_changed();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_config(&self) -> fdo::Result<()> {
|
||||
if self.is_changed() {
|
||||
self.realm.config()
|
||||
.write()
|
||||
.map_err(|err| fdo::Error::Failed(format!("Error writing config file for realm-{}: {}", self.realm.name(), err)))?;
|
||||
|
||||
self.changed.store(false, Ordering::Relaxed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_var(&mut self, var: &str, value: &str) -> fdo::Result<()> {
|
||||
if is_bool_config_variable(var) {
|
||||
self.set_bool_var(var, value)
|
||||
} else if var == "overlay" {
|
||||
self.set_overlay(value)
|
||||
} else if var == "terminal-scheme" {
|
||||
self.set_terminal_scheme(value)
|
||||
} else if var == "realmfs" {
|
||||
self.set_realmfs(value)
|
||||
} else {
|
||||
failed(format!("Unknown realm configuration variable '{}'", var))
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,150 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use blocking::unblock;
|
||||
use event_listener::Event;
|
||||
use serde::Serialize;
|
||||
use serde_repr::Serialize_repr;
|
||||
use zbus::blocking::fdo::DBusProxy;
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::names::BusName;
|
||||
use zbus::zvariant::Type;
|
||||
use zbus::{fdo, interface};
|
||||
use libcitadel::{PidLookupResult, RealmManager};
|
||||
use crate::next::config::RealmConfigVars;
|
||||
use crate::next::realm::RealmItemState;
|
||||
|
||||
use super::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() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn name_owner_changed_loop(&self, connection: &Connection) -> zbus::Result<()> {
|
||||
let dbus = DBusProxy::new(connection)?;
|
||||
|
||||
for sig in dbus.receive_name_owner_changed()? {
|
||||
let args = sig.args()?;
|
||||
match &args.name {
|
||||
BusName::Unique(unique_name) if args.new_owner().is_none() => {
|
||||
self.realmfs_state.client_disconnected(unique_name);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
}
|
||||
|
||||
fn listen_name_owner_changed(&self, connection: &Connection) {
|
||||
let connection = connection.clone();
|
||||
let server = self.clone();
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = server.name_owner_changed_loop(&connection) {
|
||||
warn!("error: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn load(connection: &Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> zbus::Result<Self> {
|
||||
let server = Self::new(connection.clone(), manager.clone(), quit_event);
|
||||
server.realms.load_realms(&manager)?;
|
||||
server.realmfs_state.load(&manager)?;
|
||||
server.realms.populate_realmfs(&server.realmfs_state)?;
|
||||
server.listen_name_owner_changed(connection);
|
||||
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 remove_realm(&self, name: &str, save_home: bool) -> fdo::Result<()> {
|
||||
let manager = self.manager.clone();
|
||||
let realm = match manager.realm_by_name(name) {
|
||||
Some(realm) => realm,
|
||||
None => return Err(fdo::Error::Failed(format!("No realm named {} exists", name))),
|
||||
};
|
||||
|
||||
unblock(move || {
|
||||
manager.delete_realm(&realm, save_home)
|
||||
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
|
||||
Ok(())
|
||||
}).await
|
||||
}
|
||||
|
||||
async fn 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,392 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Command;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
|
||||
use blocking::unblock;
|
||||
use zbus::zvariant::{OwnedObjectPath, Value};
|
||||
use zbus::{interface, fdo};
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::names::{BusName, InterfaceName};
|
||||
use libcitadel::{Realm, RealmEvent, RealmManager, Result};
|
||||
use crate::next::config::{RealmConfig, RealmConfigVars};
|
||||
use crate::next::realmfs::RealmFSState;
|
||||
use crate::next::REALMS2_SERVER_OBJECT_PATH;
|
||||
|
||||
#[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?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn open_terminal(&self) -> fdo::Result<()> {
|
||||
let _res = Command::new("/usr/bin/machinectl")
|
||||
.uid(1000)
|
||||
.gid(1000)
|
||||
.arg("-q")
|
||||
.arg("-E")
|
||||
.arg(format!("REALM_NAME={}", self.realm.name()))
|
||||
.arg("shell")
|
||||
.arg(format!("user@{}", self.realm.name()))
|
||||
.arg("/usr/bin/gnome-terminal")
|
||||
.spawn();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[zbus(property, name = "RunStatus")]
|
||||
fn run_status(&self) -> u32 {
|
||||
self.get_run_status() as u32
|
||||
}
|
||||
|
||||
#[zbus(property, name="IsSystemRealm")]
|
||||
fn is_system_realm(&self) -> bool {
|
||||
self.realm.is_system()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Name")]
|
||||
fn name(&self) -> &str {
|
||||
self.realm.name()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Description")]
|
||||
fn description(&self) -> String {
|
||||
self.realm.notes()
|
||||
.unwrap_or(String::new())
|
||||
}
|
||||
|
||||
#[zbus(property, name = "PidNS")]
|
||||
fn pid_ns(&self) -> u64 {
|
||||
self.realm.pid_ns().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "RealmFS")]
|
||||
fn realmfs(&self) -> u32 {
|
||||
self.realmfs_index.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Timestamp")]
|
||||
fn timestamp(&self) -> u64 {
|
||||
self.realm.timestamp() as u64
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[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,251 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::message::Header;
|
||||
use zbus::names::UniqueName;
|
||||
use zbus::zvariant::{ObjectPath, OwnedObjectPath};
|
||||
use zbus::{fdo, interface};
|
||||
use libcitadel::{RealmFS, RealmManager,RealmFSUpdate};
|
||||
use crate::next::REALMS2_SERVER_OBJECT_PATH;
|
||||
|
||||
struct UpdateState(Option<(UniqueName<'static>, RealmFSUpdate)>);
|
||||
|
||||
impl UpdateState {
|
||||
|
||||
fn new() -> Self {
|
||||
UpdateState(None)
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
self.0.is_some()
|
||||
}
|
||||
|
||||
fn matches(&self, name: &UniqueName) -> bool {
|
||||
match &self.0{
|
||||
Some((sender, _update)) => sender == name,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn activate(&mut self, sender: UniqueName<'static>, update: RealmFSUpdate) {
|
||||
if self.0.is_none() {
|
||||
self.0 = Some((sender, update));
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_update(&mut self) {
|
||||
if let Some((_name, mut update)) = self.0.take() {
|
||||
update.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
fn commit_update(&mut self) {
|
||||
if let Some((_name, mut update)) = self.0.take() {
|
||||
if let Err(err) = update.commit_update() {
|
||||
warn!("Error committing RealmFS update: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BLOCK_SIZE: u64 = 4096;
|
||||
#[derive(Clone)]
|
||||
pub struct RealmFSItem {
|
||||
update_state: Arc<Mutex<UpdateState>>,
|
||||
object_path: OwnedObjectPath,
|
||||
index: u32,
|
||||
realmfs: RealmFS,
|
||||
}
|
||||
|
||||
impl RealmFSItem {
|
||||
|
||||
fn update_state(&self) -> MutexGuard<UpdateState> {
|
||||
self.update_state.lock().unwrap()
|
||||
}
|
||||
|
||||
|
||||
fn client_disconnected(&mut self, name: &UniqueName) {
|
||||
//debug!("disconnect {} {}", self.object_path, name);
|
||||
let mut state = self.update_state();
|
||||
|
||||
if state.matches(name) {
|
||||
state.cleanup_update();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_from_realmfs(index: u32, realmfs: RealmFS) -> RealmFSItem {
|
||||
let object_path = format!("{}/RealmFS{}", REALMS2_SERVER_OBJECT_PATH, index).try_into().unwrap();
|
||||
RealmFSItem {
|
||||
update_state: Arc::new(Mutex::new(UpdateState::new())),
|
||||
object_path,
|
||||
index,
|
||||
realmfs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index(&self) -> u32 {
|
||||
self.index
|
||||
}
|
||||
|
||||
pub fn object_path(&self) -> ObjectPath {
|
||||
self.object_path.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[interface(
|
||||
name = "com.subgraph.realms.RealmFS"
|
||||
)]
|
||||
impl RealmFSItem {
|
||||
|
||||
async fn prepare_update(
|
||||
&mut self,
|
||||
#[zbus(header)]
|
||||
hdr: Header<'_>,
|
||||
shared_directory: bool,
|
||||
) -> fdo::Result<String> {
|
||||
|
||||
let mut state = self.update_state();
|
||||
|
||||
if state.is_active() {
|
||||
return Err(fdo::Error::Failed("An update is already in progress".to_owned()));
|
||||
}
|
||||
|
||||
let sender = match hdr.sender() {
|
||||
Some(sender) => sender,
|
||||
None => todo!(),
|
||||
};
|
||||
|
||||
let mut update = self.realmfs.update()
|
||||
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
|
||||
|
||||
update.prepare_update(shared_directory)
|
||||
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
|
||||
|
||||
let update_container = update.name().to_string();
|
||||
debug!("Update from {}, container: {}", sender, update_container);
|
||||
state.activate(sender.to_owned(), update);
|
||||
Ok(update_container)
|
||||
}
|
||||
|
||||
async fn commit_update(&mut self) -> fdo::Result<()> {
|
||||
self.update_state().commit_update();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn abandon_update(&mut self) -> fdo::Result<()> {
|
||||
self.update_state().cleanup_update();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Name")]
|
||||
fn name(&self) -> &str {
|
||||
self.realmfs.name()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "Activated")]
|
||||
fn activated(&self) -> bool {
|
||||
self.realmfs.is_activated()
|
||||
}
|
||||
|
||||
#[zbus(property, name = "InUse")]
|
||||
fn in_use(&self) -> bool {
|
||||
self.realmfs.is_activated()
|
||||
}
|
||||
#[zbus(property, name = "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)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealmFSState(Arc<Mutex<Inner>>);
|
||||
|
||||
impl RealmFSState {
|
||||
pub fn new(connection: Connection) -> Self {
|
||||
RealmFSState(Arc::new(Mutex::new(Inner::new(connection))))
|
||||
}
|
||||
|
||||
fn inner(&self) -> MutexGuard<Inner> {
|
||||
self.0.lock().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn load(&self, manager: &RealmManager) -> zbus::Result<()> {
|
||||
self.inner().load(manager)
|
||||
}
|
||||
|
||||
pub fn realmfs_by_name(&self, name: &str) -> Option<RealmFSItem> {
|
||||
self.inner().realmfs_by_name(name)
|
||||
}
|
||||
|
||||
pub fn client_disconnected(&self, client_name: &UniqueName) {
|
||||
let mut lock = self.inner();
|
||||
for (_,v) in &mut lock.items {
|
||||
v.client_disconnected(client_name);
|
||||
}
|
||||
println!("client disconnected: {client_name}")
|
||||
}
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
connection: Connection,
|
||||
next_index: u32,
|
||||
items: HashMap<String, RealmFSItem>,
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
fn new(connection: Connection) -> Self {
|
||||
Inner {
|
||||
connection,
|
||||
next_index: 1,
|
||||
items: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(&mut self, manager: &RealmManager) -> zbus::Result<()> {
|
||||
for realmfs in manager.realmfs_list() {
|
||||
self.add_realmfs(realmfs)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_realmfs(&mut self, realmfs: RealmFS) -> zbus::Result<()> {
|
||||
if !self.items.contains_key(realmfs.name()) {
|
||||
let name = realmfs.name().to_string();
|
||||
let item = RealmFSItem::new_from_realmfs(self.next_index, realmfs);
|
||||
self.connection.object_server().at(item.object_path(), item.clone())?;
|
||||
self.items.insert(name, item);
|
||||
self.next_index += 1;
|
||||
} else {
|
||||
warn!("Attempted to add duplicate realmfs '{}'", realmfs.name());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn realmfs_by_name(&self, name: &str) -> Option<RealmFSItem> {
|
||||
let res = self.items.get(name).cloned();
|
||||
if res.is_none() {
|
||||
warn!("Failed to find RealmFS with name '{}'", name);
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
@@ -1,15 +1,11 @@
|
||||
use libcitadel::{RealmManager, Realm, OverlayType, Result, PidLookupResult};
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::Type;
|
||||
use std::sync::Arc;
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::{dbus_interface, ObjectServer,Connection};
|
||||
use zvariant::derive::Type;
|
||||
use std::thread;
|
||||
use std::collections::HashMap;
|
||||
use blocking::unblock;
|
||||
use event_listener::Event;
|
||||
use serde::{Serialize,Deserialize};
|
||||
use serde_repr::Serialize_repr;
|
||||
use zbus::interface;
|
||||
use crate::events::EventHandler;
|
||||
use libcitadel::terminal::Base16Scheme;
|
||||
|
||||
@@ -43,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] = &[
|
||||
@@ -126,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) {
|
||||
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() {
|
||||
fn get_current(&self) -> String {
|
||||
match self.manager.current_realm() {
|
||||
Some(realm) => realm.name().to_string(),
|
||||
None => String::new(),
|
||||
}
|
||||
}).await
|
||||
}
|
||||
|
||||
fn list(&self) -> Vec<RealmItem> {
|
||||
@@ -254,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 {
|
||||
@@ -270,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 => {
|
||||
@@ -279,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,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) {
|
||||
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
|
||||
}
|
||||
}).await
|
||||
}
|
||||
|
||||
fn list_realm_f_s(&self) -> Vec<String> {
|
||||
@@ -317,23 +299,23 @@ impl RealmsManagerServer {
|
||||
|
||||
}
|
||||
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_started(ctx: &SignalEmitter<'_>, 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: &SignalEmitter<'_>, 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: &SignalEmitter<'_>, 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: &SignalEmitter<'_>, 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: &SignalEmitter<'_>, 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: &SignalEmitter<'_>) -> 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()),
|
||||
)),
|
||||
)
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
[package]
|
||||
name = "update-realmfs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
zbus = "5.7.1"
|
||||
anyhow = "1.0"
|
@@ -1,139 +0,0 @@
|
||||
use std::io::Write;
|
||||
use std::{env, io};
|
||||
use std::process::Command;
|
||||
|
||||
use libcitadel::terminal::{AnsiTerminal, Color};
|
||||
use libcitadel::warn;
|
||||
use zbus::blocking::Connection;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use self::realmsd::RealmFS;
|
||||
|
||||
mod realmsd;
|
||||
|
||||
|
||||
fn open_update_shell(machine_name: &str, realmfs_name: &str) -> Result<()> {
|
||||
let mut child = Command::new("/usr/bin/machinectl")
|
||||
.env("PS1", format!("(Update realmfs-{}) # ", realmfs_name))
|
||||
.arg("--quiet")
|
||||
.arg("--setenv=PS1")
|
||||
|
||||
// dumb hack to avoid PS1 being overwritten by /etc/bash.bashrc
|
||||
.arg("--setenv=SUDO_PS1=foo")
|
||||
.arg("--setenv=SUDO_USER=user")
|
||||
|
||||
.arg("shell")
|
||||
.arg(machine_name)
|
||||
.spawn()?;
|
||||
|
||||
let _status = child.wait()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_realmfs(name: &str, shared_dir: bool) -> Result<()> {
|
||||
let connection = Connection::system()?;
|
||||
let realmfs = RealmFS::lookup(&connection, name)?;
|
||||
|
||||
|
||||
print!("\x1B[2J\x1B[1;1H");
|
||||
println!();
|
||||
println!("Starting update container to modify '{name}-realmfs.img'");
|
||||
println!();
|
||||
|
||||
let machine_name = realmfs.prepare_update(shared_dir)
|
||||
.context(format!("Failed to prepare update for RealmFS ({name})"))?;
|
||||
|
||||
|
||||
println!("The root filesystem can be updated from this shell to upgrade packages,");
|
||||
println!("install new software, or make other changes to the filesystem.");
|
||||
println!();
|
||||
if shared_dir {
|
||||
println!("The Shared directory has been mounted at /run/Shared");
|
||||
println!();
|
||||
}
|
||||
println!("Exit update shell with ctrl-d or 'exit'");
|
||||
println!();
|
||||
|
||||
open_update_shell(&machine_name, name)?;
|
||||
|
||||
println!();
|
||||
print!("Save changes (Y/N)? ");
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer)?;
|
||||
|
||||
let response = buffer.trim();
|
||||
|
||||
if response == "y" || response == "Y" {
|
||||
println!("Saving changes");
|
||||
realmfs.commit_update()?;
|
||||
} else {
|
||||
println!("Discarding changes");
|
||||
realmfs.abandon_update()?;
|
||||
}
|
||||
|
||||
println!("Done...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn realmfs_arg(args: &[String]) -> Option<&str> {
|
||||
if args.len() < 2 {
|
||||
None
|
||||
} else {
|
||||
Some(&args[args.len() - 1])
|
||||
}
|
||||
}
|
||||
|
||||
fn has_arg(args: &[String], arg: &str) -> bool {
|
||||
for a in args {
|
||||
if a == arg {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn set_background() -> Result<Color> {
|
||||
let mut ansi = AnsiTerminal::new()?;
|
||||
let saved_bg = ansi.read_palette_bg()?;
|
||||
let bg = Color::new(0xd0, 0xd0, 0xff);
|
||||
ansi.set_palette_bg(bg)?;
|
||||
Ok(saved_bg)
|
||||
}
|
||||
|
||||
fn restore_background(bg: Color) -> Result<()> {
|
||||
let mut ansi = AnsiTerminal::new()?;
|
||||
ansi.set_palette_bg(bg)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
|
||||
let args = env::args().collect::<Vec<_>>();
|
||||
|
||||
let realmfs_name = match realmfs_arg(&args) {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
println!("Need a realmfs name");
|
||||
return Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
let shared_dir = has_arg(&args, "--shared-dir");
|
||||
|
||||
println!("realmfs name is {realmfs_name}");
|
||||
|
||||
let saved_bg = set_background()?;
|
||||
|
||||
if let Err(err) = update_realmfs(&realmfs_name, shared_dir) {
|
||||
warn!("{}", err);
|
||||
}
|
||||
restore_background(saved_bg)?;
|
||||
|
||||
Ok(())
|
||||
}
|
@@ -1,72 +0,0 @@
|
||||
use zbus::blocking::fdo::ObjectManagerProxy;
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::{fdo, proxy};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
#[proxy(
|
||||
interface = "com.subgraph.realms.RealmFS",
|
||||
default_service = "com.subgraph.realms",
|
||||
gen_async = false
|
||||
)]
|
||||
trait RealmFS {
|
||||
|
||||
fn prepare_update(&self, shared_dir: bool) -> zbus::Result<String>;
|
||||
fn commit_update(&self) -> zbus::Result<()>;
|
||||
fn abandon_update(&self) -> zbus::Result<()>;
|
||||
|
||||
#[zbus(property, name = "Name")]
|
||||
fn name(&self) -> fdo::Result<String>;
|
||||
|
||||
#[zbus(property, name = "FreeSpace")]
|
||||
fn free_space(&self) -> fdo::Result<u64> ;
|
||||
|
||||
#[zbus(property, name = "AllocatedSpace")]
|
||||
fn allocated_space(&self) -> fdo::Result<u64>;
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealmFS<'a> {
|
||||
proxy: RealmFSProxy<'a>,
|
||||
}
|
||||
|
||||
impl <'a> RealmFS <'a> {
|
||||
pub fn lookup(connection: &Connection, name: &str) -> Result<Self> {
|
||||
|
||||
let obj_mgr = ObjectManagerProxy::new(
|
||||
connection,
|
||||
"com.subgraph.realms",
|
||||
"/com/subgraph/Realms2")?;
|
||||
|
||||
for (path, map) in obj_mgr.get_managed_objects()? {
|
||||
if map.contains_key("com.subgraph.realms.RealmFS") {
|
||||
let proxy = RealmFSProxy::builder(&connection)
|
||||
.path(path.to_owned())?
|
||||
.build()?;
|
||||
|
||||
let realmfs_name = proxy.name()?;
|
||||
|
||||
if &realmfs_name == name {
|
||||
return Ok(RealmFS{ proxy });
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow::bail!("No RealmFS named '{name}' found.");
|
||||
}
|
||||
|
||||
pub fn prepare_update(&self, shared_dir: bool) -> Result<String> {
|
||||
let name = self.proxy.prepare_update(shared_dir)?;
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
pub fn abandon_update(&self) -> Result<()> {
|
||||
self.proxy.abandon_update()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn commit_update(&self) -> Result<()> {
|
||||
self.proxy.commit_update()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user