1
0
forked from brl/citadel-tools

2 Commits

Author SHA1 Message Date
isa
96f364cfe8 Fix unused function error return value 2024-08-29 15:42:42 -04:00
isa
04457a47ef Improve error handling by using std lib rust code 2024-08-29 14:54:31 -04:00
51 changed files with 1326 additions and 6853 deletions

2364
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
[workspace]
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui", "launch-gnome-software" ]
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui" ]
[profile.release]
lto = true
codegen-units = 1

File diff suppressed because it is too large Load Diff

View File

@@ -8,19 +8,14 @@ homepage = "https://subgraph.com"
[dependencies]
libcitadel = { path = "../libcitadel" }
rpassword = "7.3"
clap = { version = "4.5", features = ["cargo", "derive"] }
rpassword = "4.0"
clap = "2.33"
lazy_static = "1.4"
serde_derive = "1.0"
serde = "1.0"
toml = "0.8"
toml = "0.5"
hex = "0.4"
byteorder = "1"
dbus = "0.8.4"
pwhash = "1.0"
pwhash = "0.3.1"
tempfile = "3"
ed25519-dalek = {version = "2.1", features = ["pem"]}
anyhow = "1.0"
reqwest = {version = "0.12", features = ["blocking"]}
glob = "0.3"
serde_cbor = "0.11"

View File

@@ -4,7 +4,7 @@ use std::fs;
use std::thread::{self,JoinHandle};
use std::time::{self,Instant};
use libcitadel::{Result, UtsName, util};
use libcitadel::{UtsName, util};
use libcitadel::ResourceImage;
use crate::boot::disks;
@@ -13,34 +13,33 @@ use crate::install::installer::Installer;
const IMAGE_DIRECTORY: &str = "/run/citadel/images";
pub fn live_rootfs() -> Result<()> {
pub fn live_rootfs() -> Result<(), Box<dyn std::error::Error>> {
copy_artifacts()?;
let rootfs = find_rootfs_image()?;
setup_rootfs_resource(&rootfs)
}
pub fn live_setup() -> Result<()> {
pub fn live_setup() -> Result<(), Box<dyn std::error::Error>> {
decompress_images(true)?;
info!("Starting live setup");
let live = Installer::new_livesetup();
live.run()
}
fn copy_artifacts() -> Result<()> {
fn copy_artifacts() -> Result<(), Box<dyn std::error::Error>> {
for _ in 0..3 {
if try_copy_artifacts()? {
//decompress_images()?;
return Ok(())
return Ok(());
}
// Try again after waiting for more devices to be discovered
info!("Failed to find partition with images, trying again in 2 seconds");
thread::sleep(time::Duration::from_secs(2));
}
bail!("could not find partition containing resource images")
Result::Err("could not find partition containing resource images".into())
}
fn try_copy_artifacts() -> Result<bool> {
fn try_copy_artifacts() -> Result<bool, Box<dyn std::error::Error>> {
let rootfs_image = Path::new("/boot/images/citadel-rootfs.img");
// Already mounted?
if rootfs_image.exists() {
@@ -60,13 +59,13 @@ fn try_copy_artifacts() -> Result<bool> {
Ok(false)
}
fn kernel_version() -> String {
fn kernel_version() -> Result<String, Box<dyn std::error::Error>> {
let utsname = UtsName::uname();
let v = utsname.release().split('-').collect::<Vec<_>>();
v[0].to_string()
Ok(v[0].to_string())
}
fn deploy_artifacts() -> Result<()> {
fn deploy_artifacts() -> Result<(), Box<dyn std::error::Error>> {
let run_images = Path::new(IMAGE_DIRECTORY);
if !run_images.exists() {
util::create_dir(run_images)?;
@@ -78,7 +77,7 @@ fn deploy_artifacts() -> Result<()> {
util::copy_file(dent.path(), run_images.join(dent.file_name()))
})?;
let kv = kernel_version();
let kv = kernel_version()?;
println!("Copying bzImage-{} to /run/citadel/images", kv);
let from = format!("/boot/bzImage-{}", kv);
let to = format!("/run/citadel/images/bzImage-{}", kv);
@@ -92,7 +91,7 @@ fn deploy_artifacts() -> Result<()> {
Ok(())
}
fn deploy_syslinux_artifacts() -> Result<()> {
fn deploy_syslinux_artifacts() -> Result<(), Box<dyn std::error::Error>> {
let boot_syslinux = Path::new("/boot/syslinux");
if !boot_syslinux.exists() {
@@ -112,10 +111,11 @@ fn deploy_syslinux_artifacts() -> Result<()> {
}
}
Ok(())
})
})?;
Ok(())
}
fn find_rootfs_image() -> Result<ResourceImage> {
fn find_rootfs_image() -> Result<ResourceImage, Box<dyn std::error::Error>> {
let entries = fs::read_dir(IMAGE_DIRECTORY)
.map_err(context!("error reading directory {}", IMAGE_DIRECTORY))?;
for entry in entries {
@@ -123,15 +123,21 @@ fn find_rootfs_image() -> Result<ResourceImage> {
if entry.path().extension() == Some(OsStr::new("img")) {
if let Ok(image) = ResourceImage::from_path(&entry.path()) {
if image.metainfo().image_type() == "rootfs" {
return Ok(image)
return Ok(image);
}
}
}
}
bail!("unable to find rootfs resource image in {}", IMAGE_DIRECTORY)
Result::Err(
format!(
"unable to find rootfs resource image in {}",
IMAGE_DIRECTORY
)
.into(),
)
}
fn decompress_images(sync: bool) -> Result<()> {
fn decompress_images(sync: bool) -> Result<(), Box<dyn std::error::Error>> {
info!("Decompressing images");
let mut threads = Vec::new();
util::read_directory("/run/citadel/images", |dent| {
@@ -140,9 +146,8 @@ fn decompress_images(sync: bool) -> Result<()> {
if image.is_compressed() {
if sync {
if let Err(err) = decompress_one_image_sync(image) {
warn!("Error: {}", err);
warn!("Error decompressing image: {}", err);
}
} else {
threads.push(decompress_one_image(image));
}
@@ -161,9 +166,11 @@ fn decompress_images(sync: bool) -> Result<()> {
Ok(())
}
fn decompress_one_image_sync(image: ResourceImage) -> Result<()> {
let start = Instant::now();
info!("Decompressing {}", image.path().display());
fn decompress_one_image_sync(
image: ResourceImage,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let start = Instant::now();
info!("Decompressing {}", image.path().display());
image.decompress(true)
.map_err(|e| format_err!("Failed to decompress image file {}: {}", image.path().display(), e))?;
cmd!("/usr/bin/du", "-h {}", image.path().display())?;
@@ -173,8 +180,7 @@ fn decompress_one_image_sync(image: ResourceImage) -> Result<()> {
Ok(())
}
fn decompress_one_image(image: ResourceImage) -> JoinHandle<Result<()>> {
thread::spawn(move || {
decompress_one_image_sync(image)
})
fn decompress_one_image(image: ResourceImage,) ->
JoinHandle<Result<(), Box<dyn std::error::Error + Send + Sync>>> {
thread::spawn(move || decompress_one_image_sync(image))
}

View File

@@ -1,53 +1,48 @@
use std::fs;
use std::process::exit;
use libcitadel::{Result, ResourceImage, CommandLine, KeyRing, LogLevel, Logger, util};
use libcitadel::{ResourceImage, CommandLine, KeyRing, LogLevel, Logger, util};
use libcitadel::RealmManager;
use crate::boot::disks::DiskPartition;
use std::path::Path;
mod live;
mod disks;
pub mod rootfs;
mod rootfs;
pub fn main(args: Vec<String>) {
pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
if CommandLine::debug() {
Logger::set_log_level(LogLevel::Debug);
} else if CommandLine::verbose() {
Logger::set_log_level(LogLevel::Info);
}
let result = match args.get(1) {
match args.get(1) {
Some(s) if s == "rootfs" => do_rootfs(),
Some(s) if s == "setup" => do_setup(),
Some(s) if s == "boot-automount" => do_boot_automount(),
Some(s) if s == "start-realms" => do_start_realms(),
_ => Err(format_err!("Bad or missing argument").into()),
};
}?;
if let Err(ref e) = result {
warn!("Failed: {}", e);
exit(1);
}
Ok(())
}
fn do_rootfs() -> Result<()> {
fn do_rootfs() -> Result<(), Box<dyn std::error::Error>> {
if CommandLine::live_mode() || CommandLine::install_mode() {
live::live_rootfs()
} else {
rootfs::setup_rootfs()
Ok(rootfs::setup_rootfs()?)
}
}
fn setup_keyring() -> Result<()> {
fn setup_keyring() -> Result<(), Box<dyn std::error::Error>> {
ResourceImage::ensure_storage_mounted()?;
let keyring = KeyRing::load_with_cryptsetup_passphrase("/sysroot/storage/keyring")?;
keyring.add_keys_to_kernel()?;
Ok(())
}
fn do_setup() -> Result<()> {
fn do_setup() -> Result<(), Box<dyn std::error::Error>> {
if CommandLine::live_mode() || CommandLine::install_mode() {
live::live_setup()?;
} else if let Err(err) = setup_keyring() {
@@ -64,8 +59,7 @@ fn do_setup() -> Result<()> {
Ok(())
}
fn mount_overlay() -> Result<()> {
fn mount_overlay() -> Result<(), Box<dyn std::error::Error>> {
info!("Creating rootfs overlay");
info!("Moving /sysroot mount to /rootfs.ro");
@@ -89,13 +83,13 @@ fn mount_overlay() -> Result<()> {
Ok(())
}
fn do_start_realms() -> Result<()> {
fn do_start_realms() -> Result<(), Box<dyn std::error::Error>> {
let manager = RealmManager::load()?;
manager.start_boot_realms()
Ok(manager.start_boot_realms()?)
}
// Write automount unit for /boot partition
fn do_boot_automount() -> Result<()> {
fn do_boot_automount() -> Result<(), Box<dyn std::error::Error>> {
Logger::set_log_level(LogLevel::Info);
if CommandLine::live_mode() || CommandLine::install_mode() {
@@ -105,10 +99,10 @@ fn do_boot_automount() -> Result<()> {
let boot_partition = find_boot_partition()?;
info!("Creating /boot automount units for boot partition {}", boot_partition);
cmd!("/usr/bin/systemd-mount", "-A --timeout-idle-sec=300 {} /boot", boot_partition)
Ok(cmd!("/usr/bin/systemd-mount", "-A --timeout-idle-sec=300 {} /boot", boot_partition)?)
}
fn find_boot_partition() -> Result<String> {
fn find_boot_partition() -> Result<String, Box<dyn std::error::Error>> {
let loader_dev = read_loader_dev_efi_var()?;
let boot_partitions = DiskPartition::boot_partitions(true)?
.into_iter()
@@ -116,7 +110,7 @@ fn find_boot_partition() -> Result<String> {
.collect::<Vec<_>>();
if boot_partitions.len() != 1 {
return Err(format_err!("Cannot uniquely determine boot partition"));
return Result::Err("Cannot uniquely determine boot partition".into());
}
Ok(boot_partitions[0].path().display().to_string())
@@ -141,7 +135,7 @@ fn matches_loader_dev(partition: &DiskPartition, dev: &Option<String>) -> bool {
const LOADER_EFI_VAR_PATH: &str =
"/sys/firmware/efi/efivars/LoaderDevicePartUUID-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
fn read_loader_dev_efi_var() -> Result<Option<String>> {
fn read_loader_dev_efi_var() -> Result<Option<String>, Box<dyn std::error::Error>> {
let efi_var = Path::new(LOADER_EFI_VAR_PATH);
if efi_var.exists() {
let s = fs::read(efi_var)

View File

@@ -1,10 +1,10 @@
use std::path::Path;
use std::process::{Command,Stdio};
use libcitadel::{BlockDev, ResourceImage, CommandLine, ImageHeader, Partition, Result, LoopDevice};
use libcitadel::{BlockDev, ResourceImage, CommandLine, ImageHeader, Partition, LoopDevice};
use libcitadel::verity::Verity;
pub fn setup_rootfs() -> Result<()> {
pub fn setup_rootfs() -> Result<(), Box<dyn std::error::Error>> {
let mut p = choose_boot_partiton(true, CommandLine::revert_rootfs())?;
if CommandLine::noverity() {
setup_partition_unverified(&p)
@@ -13,7 +13,7 @@ pub fn setup_rootfs() -> Result<()> {
}
}
pub fn setup_rootfs_resource(rootfs: &ResourceImage) -> Result<()> {
pub fn setup_rootfs_resource(rootfs: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
if CommandLine::noverity() {
setup_resource_unverified(&rootfs)
} else {
@@ -21,7 +21,7 @@ pub fn setup_rootfs_resource(rootfs: &ResourceImage) -> Result<()> {
}
}
fn setup_resource_unverified(img: &ResourceImage) -> Result<()> {
fn setup_resource_unverified(img: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
if img.is_compressed() {
img.decompress(false)?;
}
@@ -30,25 +30,31 @@ fn setup_resource_unverified(img: &ResourceImage) -> Result<()> {
setup_linear_mapping(loopdev.device())
}
fn setup_resource_verified(img: &ResourceImage) -> Result<()> {
fn setup_resource_verified(img: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
let _ = img.setup_verity_device()?;
Ok(())
}
fn setup_partition_unverified(p: &Partition) -> Result<()> {
fn setup_partition_unverified(p: &Partition) -> Result<(), Box<dyn std::error::Error>> {
info!("Creating /dev/mapper/rootfs device with linear device mapping of partition (no verity)");
setup_linear_mapping(p.path())
}
fn setup_partition_verified(p: &mut Partition) -> Result<()> {
fn setup_partition_verified(p: &mut Partition) -> Result<(), Box<dyn std::error::Error>> {
info!("Creating /dev/mapper/rootfs dm-verity device");
if !CommandLine::nosignatures() {
if !p.has_public_key() {
bail!("no public key available for channel {}", p.metainfo().channel())
return Result::Err(
format!(
"no public key available for channel {}",
p.metainfo().channel()
)
.into(),
);
}
if !p.is_signature_valid() {
p.write_status(ImageHeader::STATUS_BAD_SIG)?;
bail!("signature verification failed on partition");
return Result::Err("signature verification failed on partition".into());
}
info!("Image signature is valid for channel {}", p.metainfo().channel());
}
@@ -56,7 +62,7 @@ fn setup_partition_verified(p: &mut Partition) -> Result<()> {
Ok(())
}
fn setup_linear_mapping(blockdev: &Path) -> Result<()> {
fn setup_linear_mapping(blockdev: &Path) -> Result<(), Box<dyn std::error::Error>> {
let dev = BlockDev::open_ro(blockdev)?;
let table = format!("0 {} linear {} 0", dev.nsectors()?, blockdev.display());
@@ -70,7 +76,9 @@ fn setup_linear_mapping(blockdev: &Path) -> Result<()> {
.success();
if !ok {
bail!("failed to set up linear identity mapping with /usr/sbin/dmsetup");
return Result::Err(
"failed to set up linear identity mapping with /usr/sbin/dmsetup".into(),
);
}
Ok(())
}
@@ -94,7 +102,8 @@ fn choose_revert_partition(best: Option<Partition>) -> Option<Partition> {
best
}
pub fn choose_boot_partiton(scan: bool, revert_rootfs: bool) -> Result<Partition> {
fn choose_boot_partiton(scan: bool, revert_rootfs: bool,
) -> Result<Partition, Box<dyn std::error::Error>> {
let mut partitions = Partition::rootfs_partitions()?;
if scan {
@@ -136,17 +145,17 @@ 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 meta_a = a.metainfo();
let meta_b = b.metainfo();
let a_v = bind_a.version();
let b_v = bind_b.version();
let ver_a = meta_a.version();
let ver_b = meta_b.version();
// Compare versions only if channels match
if a.metainfo().channel() == b.metainfo().channel() {
if a_v > b_v {
if ver_a > ver_b {
return Some(a);
} else if b_v > a_v {
} else if ver_b > ver_a {
return Some(b);
}
}

View File

@@ -1,293 +0,0 @@
use crate::{update, Path};
use anyhow::{bail, Context, Result};
use clap::ArgMatches;
use ed25519_dalek::{pkcs8::DecodePublicKey, VerifyingKey};
use libcitadel::updates::UPDATE_SERVER_HOSTNAME;
use libcitadel::{updates, updates::CitadelVersionStruct};
use libcitadel::{OsRelease, ResourceImage};
use std::io::prelude::*;
use std::str::FromStr;
const UPDATE_SERVER_KEY_PATH: &str = "/etc/citadel/update_server_key.pub";
pub fn check() -> Result<()> {
let current_version = get_current_os_config()?;
let server_citadel_version = fetch_and_verify_version_cbor(&current_version)?;
let components_to_upgrade =
compare_citadel_versions(&current_version, &server_citadel_version)?;
if components_to_upgrade.len() == 1 {
println!(
"We found the following component to upgrade: {}",
components_to_upgrade[0]
);
} else if components_to_upgrade.len() > 1 {
println!("We found the following components to upgrade:");
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(&current_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(&current_version)?;
// What do we need to upgrade?
let components_to_upgrade =
compare_citadel_versions(&current_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(&current_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)
}
// We need to get the version of the rootfs, kernel and extra images currently installed
fn get_current_os_config() -> Result<updates::CitadelVersionStruct> {
let client = OsRelease::citadel_client().context("Failed to find client of current system")?;
let channel = OsRelease::citadel_channel().context("Failed to find channel of current system")?;
let publisher = OsRelease::citadel_publisher().context("Failed to find publisher of current system")?;
let metainfo;
// choose best partion to boot from as the partition to compare versions with
let rootfs_version = match crate::boot::rootfs::choose_boot_partiton(false, false) {
Ok(part) => {metainfo = part.header().metainfo();
metainfo.version() }
Err(e) => bail!("Rootfs version not found. Error: {e}"),
};
// Get highest values of image versions
let kernel_resource = ResourceImage::find("kernel")?.metainfo();
let kernel_version = kernel_resource.version();
let extra_resource = ResourceImage::find("extra")?.metainfo();
let extra_version = extra_resource.version();
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.to_owned(),
component_version,
publisher: publisher.to_owned(),
};
Ok(current_version_struct)
}
fn fetch_and_verify_version_cbor(
current_citadel_version: &updates::CitadelVersionStruct,
) -> Result<updates::CitadelVersionStruct> {
let url = format!(
"https://{}/{}/{}/version.cbor",
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)
.context("We failed to verify the signature update release file. Please make sure the key at /etc/citade/update_server_key.pub matches the one publicly linked to your update provider.")?;
// 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 and install 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))
}

View File

@@ -1,55 +0,0 @@
use clap::{arg, command, ArgAction, Command};
use std::process::exit;
use libcitadel::util;
mod fetch;
pub fn main() {
if !util::is_euid_root() {
println!("Please run this program as root");
exit(1);
}
let matches = command!()
.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);
}
}

View File

@@ -1,97 +1,88 @@
use std::path::Path;
use std::process::exit;
use clap::{Arg,ArgMatches};
use clap::{command, ArgAction, Command};
use clap::{App,Arg,SubCommand,ArgMatches};
use clap::AppSettings::*;
use libcitadel::{ResourceImage, Logger, LogLevel, Partition, KeyPair, ImageHeader, util};
use hex;
use libcitadel::{Result, ResourceImage, Logger, LogLevel, Partition, KeyPair, ImageHeader, util};
pub fn main() {
let matches = command!()
pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
let app = App::new("citadel-image")
.about("Citadel update image builder")
.arg_required_else_help(true)
.disable_help_subcommand(true)
.subcommand(
Command::new("metainfo")
.about("Display metainfo variables for an image file")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.subcommand(
Command::new("info")
.about("Display metainfo variables for an image file")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.subcommand(
Command::new("generate-verity")
.about("Generate dm-verity hash tree for an image file")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.subcommand(
Command::new("verify")
.about("Verify dm-verity hash tree for an image file")
.arg(
Arg::new("option")
.long("option")
.required(true)
.help("Path to image file"),
),
)
.subcommand(
Command::new("install-rootfs")
.about("Install rootfs image file to a partition")
.arg(
Arg::new("choose")
.long("just-choose")
.action(ArgAction::SetTrue)
.help("Don't install anything, just show which partition would be chosen"),
)
.arg(
Arg::new("skip-sha")
.long("skip-sha")
.action(ArgAction::SetTrue)
.help("Skip verification of header sha256 value"),
)
.arg(
Arg::new("no-prefer")
.long("no-prefer")
.action(ArgAction::SetTrue)
.help("Don't set PREFER_BOOT flag"),
)
.arg(
Arg::new("path")
.required_unless_present("choose")
.help("Path to image file"),
),
)
.subcommand(Command::new("genkeys").about("Generate a pair of keys"))
.subcommand(
Command::new("decompress")
.about("Decompress a compressed image file")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.subcommand(
Command::new("bless")
.about("Mark currently mounted rootfs partition as successfully booted"),
)
.subcommand(
Command::new("verify-shasum")
.about("Verify the sha256 sum of the image")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.get_matches();
.settings(&[ArgRequiredElseHelp,ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder])
.subcommand(SubCommand::with_name("metainfo")
.about("Display metainfo variables for an image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("info")
.about("Display metainfo variables for an image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("generate-verity")
.about("Generate dm-verity hash tree for an image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("verify")
.about("Verify dm-verity hash tree for an image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("install-rootfs")
.about("Install rootfs image file to a partition")
.arg(Arg::with_name("choose")
.long("just-choose")
.help("Don't install anything, just show which partition would be chosen"))
.arg(Arg::with_name("skip-sha")
.long("skip-sha")
.help("Skip verification of header sha256 value"))
.arg(Arg::with_name("no-prefer")
.long("no-prefer")
.help("Don't set PREFER_BOOT flag"))
.arg(Arg::with_name("path")
.required_unless("choose")
.help("Path to image file")))
.subcommand(SubCommand::with_name("genkeys")
.about("Generate a pair of keys"))
.subcommand(SubCommand::with_name("decompress")
.about("Decompress a compressed image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("bless")
.about("Mark currently mounted rootfs partition as successfully booted"))
.subcommand(SubCommand::with_name("verify-shasum")
.about("Verify the sha256 sum of the image")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")));
Logger::set_log_level(LogLevel::Debug);
let matches = app.get_matches_from(args);
let result = match matches.subcommand() {
Some(("metainfo", sub_m)) => metainfo(sub_m),
Some(("info", sub_m)) => info(sub_m),
Some(("generate-verity", sub_m)) => generate_verity(sub_m),
Some(("verify", sub_m)) => verify(sub_m),
Some(("sign-image", sub_m)) => sign_image(sub_m),
Some(("genkeys", _)) => genkeys(),
Some(("decompress", sub_m)) => decompress(sub_m),
Some(("verify-shasum", sub_m)) => verify_shasum(sub_m),
Some(("install-rootfs", sub_m)) => install_rootfs(sub_m),
Some(("install", sub_m)) => install_image(sub_m),
Some(("bless", _)) => bless(),
("metainfo", Some(m)) => metainfo(m),
("info", Some(m)) => info(m),
("generate-verity", Some(m)) => generate_verity(m),
("verify", Some(m)) => verify(m),
("sign-image", Some(m)) => sign_image(m),
("genkeys", Some(_)) => genkeys(),
("decompress", Some(m)) => decompress(m),
("verify-shasum", Some(m)) => verify_shasum(m),
("install-rootfs", Some(m)) => install_rootfs(m),
("install", Some(m)) => install_image(m),
("bless", Some(_)) => bless(),
_ => Ok(()),
};
@@ -99,16 +90,18 @@ pub fn main() {
println!("Error: {}", e);
exit(1);
}
Ok(())
}
fn info(arg_matches: &ArgMatches) -> Result<()> {
fn info(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = load_image(arg_matches)?;
print!("{}",String::from_utf8(img.header().metainfo_bytes())?);
print!("{}", String::from_utf8(img.header().metainfo_bytes())?);
info_signature(&img)?;
Ok(())
}
fn info_signature(img: &ResourceImage) -> Result<()> {
fn info_signature(img: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
if img.header().has_signature() {
println!("Signature: {}", hex::encode(&img.header().signature()));
} else {
@@ -124,15 +117,15 @@ fn info_signature(img: &ResourceImage) -> Result<()> {
},
None => { println!("No public key found for channel '{}'", img.metainfo().channel()) },
}
Ok(())
Ok(())
}
fn metainfo(arg_matches: &ArgMatches) -> Result<()> {
fn metainfo(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = load_image(arg_matches)?;
print!("{}",String::from_utf8(img.header().metainfo_bytes())?);
print!("{}", String::from_utf8(img.header().metainfo_bytes())?);
Ok(())
}
fn generate_verity(arg_matches: &ArgMatches) -> Result<()> {
fn generate_verity(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = load_image(arg_matches)?;
if img.has_verity_hashtree() {
info!("Image already has dm-verity hashtree appended, doing nothing.");
@@ -142,7 +135,7 @@ fn generate_verity(arg_matches: &ArgMatches) -> Result<()> {
Ok(())
}
fn verify(arg_matches: &ArgMatches) -> Result<()> {
fn verify(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = load_image(arg_matches)?;
let ok = img.verify_verity()?;
if ok {
@@ -153,7 +146,7 @@ fn verify(arg_matches: &ArgMatches) -> Result<()> {
Ok(())
}
fn verify_shasum(arg_matches: &ArgMatches) -> Result<()> {
fn verify_shasum(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = load_image(arg_matches)?;
let shasum = img.generate_shasum()?;
if shasum == img.metainfo().shasum() {
@@ -166,39 +159,37 @@ fn verify_shasum(arg_matches: &ArgMatches) -> Result<()> {
Ok(())
}
fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage> {
let path = arg_matches.get_one::<String>("path")
.expect("path argument missing");
fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage, Box<dyn std::error::Error>> {
let path = arg_matches.value_of("path").expect("path argument missing");
if !Path::new(path).exists() {
bail!("Cannot load image {}: File does not exist", path);
panic!("Cannot load image {}: File does not exist", path);
}
let img = ResourceImage::from_path(path)?;
if !img.is_valid_image() {
bail!("File {} is not a valid image file", path);
panic!("File {} is not a valid image file", path);
}
Ok(img)
}
fn install_rootfs(arg_matches: &ArgMatches) -> Result<()> {
if arg_matches.get_flag("choose") {
fn install_rootfs(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
if arg_matches.is_present("choose") {
let _ = choose_install_partition(true)?;
return Ok(())
return Ok(());
}
let img = load_image(arg_matches)?;
if !arg_matches.get_flag("skip-sha") {
if !arg_matches.is_present("skip-sha") {
info!("Verifying sha256 hash of image");
let shasum = img.generate_shasum()?;
if shasum != img.metainfo().shasum() {
bail!("image file does not have expected sha256 value");
panic!("image file does not have expected sha256 value");
}
}
let partition = choose_install_partition(true)?;
if !arg_matches.get_flag("no-prefer") {
if !arg_matches.is_present("no-prefer") {
clear_prefer_boot()?;
img.header().set_flag(ImageHeader::FLAG_PREFER_BOOT);
}
@@ -206,7 +197,7 @@ fn install_rootfs(arg_matches: &ArgMatches) -> Result<()> {
Ok(())
}
fn clear_prefer_boot() -> Result<()> {
fn clear_prefer_boot() -> Result<(), Box<dyn std::error::Error>> {
for mut p in Partition::rootfs_partitions()? {
if p.is_initialized() && p.header().has_flag(ImageHeader::FLAG_PREFER_BOOT) {
p.clear_flag_and_write(ImageHeader::FLAG_PREFER_BOOT)?;
@@ -215,16 +206,14 @@ fn clear_prefer_boot() -> Result<()> {
Ok(())
}
fn sign_image(arg_matches: &ArgMatches) -> Result<()> {
fn sign_image(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let _img = load_image(arg_matches)?;
info!("Not implemented yet");
Ok(())
}
fn install_image(arg_matches: &ArgMatches) -> Result<()> {
let source = arg_matches.get_one::<String>("path")
.expect("path argument missing");
fn install_image(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let source = arg_matches.value_of("path").expect("path argument missing");
let img = load_image(arg_matches)?;
let _hdr = img.header();
let metainfo = img.metainfo();
@@ -232,12 +221,12 @@ fn install_image(arg_matches: &ArgMatches) -> Result<()> {
// XXX verify signature?
if !(metainfo.image_type() == "kernel" || metainfo.image_type() == "extra") {
bail!("Cannot install image type {}", metainfo.image_type());
panic!("Cannot install image type {}", metainfo.image_type());
}
let shasum = img.generate_shasum()?;
if shasum != img.metainfo().shasum() {
bail!("Image shasum does not match metainfo");
panic!("Image shasum does not match metainfo");
}
img.generate_verity_hashtree()?;
@@ -245,44 +234,49 @@ fn install_image(arg_matches: &ArgMatches) -> Result<()> {
let filename = if metainfo.image_type() == "kernel" {
let kernel_version = match metainfo.kernel_version() {
Some(version) => version,
None => bail!("Kernel image does not have a kernel version field in metainfo"),
None => panic!("Kernel image does not have a kernel version field in metainfo"),
};
if kernel_version.chars().any(|c| c == '/') {
bail!("Kernel version field has / char");
panic!("Kernel version field has / char");
}
format!("citadel-kernel-{}-{}.img", kernel_version, metainfo.version())
format!("citadel-kernel-{}-{:03}.img", kernel_version, metainfo.version())
} else {
format!("citadel-extra-{}.img", metainfo.version())
format!("citadel-extra-{:03}.img", metainfo.version())
};
if !metainfo.channel().chars().all(|c| c.is_ascii_lowercase()) {
bail!("Refusing to build path from strange channel name {}", metainfo.channel());
panic!(
"Refusing to build path from strange channel name {}",
metainfo.channel()
);
}
let image_dir = Path::new("/storage/resources").join(metainfo.channel());
let image_dest = image_dir.join(filename);
if image_dest.exists() {
rotate(&image_dest)?;
}
util::rename(source, &image_dest)
util::rename(source, &image_dest)?;
Ok(())
}
fn rotate(path: &Path) -> Result<()> {
fn rotate(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
if !path.exists() || path.file_name().is_none() {
return Ok(());
}
let filename = path.file_name().unwrap();
let dot_zero = path.with_file_name(format!("{}.0", filename.to_string_lossy()));
util::remove_file(&dot_zero)?;
util::rename(path, &dot_zero)
util::rename(path, &dot_zero).unwrap();
Ok(())
}
fn genkeys() -> Result<()> {
fn genkeys() -> Result<(), Box<dyn std::error::Error>> {
let keypair = KeyPair::generate();
println!("keypair = \"{}\"", keypair.to_hex());
Ok(())
}
fn decompress(arg_matches: &ArgMatches) -> Result<()> {
fn decompress(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = load_image(arg_matches)?;
if !img.is_compressed() {
info!("Image is not compressed, not decompressing.");
@@ -292,7 +286,7 @@ fn decompress(arg_matches: &ArgMatches) -> Result<()> {
Ok(())
}
fn bless() -> Result<()> {
fn bless() -> Result<(), Box<dyn std::error::Error>> {
for mut p in Partition::rootfs_partitions()? {
if p.is_initialized() && p.is_mounted() {
p.bless()?;
@@ -311,7 +305,7 @@ fn bool_to_yesno(val: bool) -> &'static str {
}
}
fn choose_install_partition(verbose: bool) -> Result<Partition> {
fn choose_install_partition(verbose: bool) -> Result<Partition, Box<dyn std::error::Error>> {
let partitions = Partition::rootfs_partitions()?;
if verbose {
@@ -326,9 +320,12 @@ fn choose_install_partition(verbose: bool) -> Result<Partition> {
for p in &partitions {
if !p.is_mounted() && !p.is_initialized() {
if verbose {
info!("Choosing {} because it is empty and not mounted", p.path().display());
info!(
"Choosing {} because it is empty and not mounted",
p.path().display()
);
}
return Ok(p.clone())
return Ok(p.clone());
}
}
for p in &partitions {
@@ -336,10 +333,10 @@ fn choose_install_partition(verbose: bool) -> Result<Partition> {
if verbose {
info!("Choosing {} because it is not mounted", p.path().display());
info!("Header metainfo:");
print!("{}",String::from_utf8(p.header().metainfo_bytes())?);
print!("{}", String::from_utf8(p.header().metainfo_bytes())?);
}
return Ok(p.clone())
return Ok(p.clone());
}
}
bail!("No suitable install partition found")
panic!("No suitable install partition found")
}

View File

@@ -1,6 +1,5 @@
use std::io::{self,Write};
use std::path::Path;
use libcitadel::Result;
use super::disk::Disk;
use rpassword;
use crate::install::installer::Installer;
@@ -8,7 +7,7 @@ use crate::install::installer::Installer;
const CITADEL_PASSPHRASE_PROMPT: &str = "Enter a password for the Citadel user (or 'q' to quit)";
const LUKS_PASSPHRASE_PROMPT: &str = "Enter a disk encryption passphrase (or 'q' to quit";
pub fn run_cli_install() -> Result<bool> {
pub fn run_cli_install() -> Result<bool, Box<dyn std::error::Error>> {
let disk = match choose_disk()? {
Some(disk) => disk,
None => return Ok(false),
@@ -33,7 +32,7 @@ pub fn run_cli_install() -> Result<bool> {
Ok(true)
}
pub fn run_cli_install_with<P: AsRef<Path>>(target: P) -> Result<bool> {
pub fn run_cli_install_with<P: AsRef<Path>>(target: P) -> Result<bool, Box<dyn std::error::Error>> {
let disk = find_disk_by_path(target.as_ref())?;
display_disk(&disk);
@@ -55,11 +54,15 @@ pub fn run_cli_install_with<P: AsRef<Path>>(target: P) -> Result<bool> {
Ok(true)
}
fn run_install(disk: Disk, citadel_passphrase: String, passphrase: String) -> Result<()> {
fn run_install(
disk: Disk,
citadel_passphrase: String,
passphrase: String,
) -> Result<(), Box<dyn std::error::Error>> {
let mut install = Installer::new(disk.path(), &citadel_passphrase, &passphrase);
install.set_install_syslinux(true);
install.verify()?;
install.run()
Ok(install.run()?)
}
fn display_disk(disk: &Disk) {
@@ -70,22 +73,22 @@ fn display_disk(disk: &Disk) {
println!();
}
fn find_disk_by_path(path: &Path) -> Result<Disk> {
fn find_disk_by_path(path: &Path) -> Result<Disk, Box<dyn std::error::Error>> {
if !path.exists() {
bail!("Target disk path {} does not exist", path.display());
panic!("Target disk path {} does not exist", path.display());
}
for disk in Disk::probe_all()? {
if disk.path() == path {
return Ok(disk.clone());
}
}
bail!("installation target {} is not a valid disk", path.display())
panic!("installation target {} is not a valid disk", path.display())
}
fn choose_disk() -> Result<Option<Disk>> {
fn choose_disk() -> Result<Option<Disk>, Box<dyn std::error::Error>> {
let disks = Disk::probe_all()?;
if disks.is_empty() {
bail!("no disks found.");
panic!("no disks found.");
}
loop {
@@ -96,7 +99,7 @@ fn choose_disk() -> Result<Option<Disk>> {
}
if let Ok(n) = line.parse::<usize>() {
if n > 0 && n <= disks.len() {
return Ok(Some(disks[n-1].clone()));
return Ok(Some(disks[n - 1].clone()));
}
}
}
@@ -111,7 +114,7 @@ fn prompt_choose_disk(disks: &[Disk]) {
let _ = io::stdout().flush();
}
fn read_line() -> Result<String> {
fn read_line() -> Result<String, Box<dyn std::error::Error>> {
let mut input = String::new();
io::stdin().read_line(&mut input)
.map_err(context!("error reading line from stdin"))?;
@@ -125,7 +128,7 @@ fn read_passphrase(prompt: &str) -> io::Result<Option<String>> {
loop {
println!("{}", prompt);
println!();
let passphrase = rpassword::prompt_password(" Passphrase : ")?;
let passphrase = rpassword::read_password_from_tty(Some(" Passphrase : "))?;
if passphrase.is_empty() {
println!("Passphrase cannot be empty");
continue;
@@ -133,7 +136,7 @@ fn read_passphrase(prompt: &str) -> io::Result<Option<String>> {
if passphrase == "q" || passphrase == "Q" {
return Ok(None);
}
let confirm = rpassword::prompt_password(" Confirm : ")?;
let confirm = rpassword::read_password_from_tty(Some(" Confirm : "))?;
if confirm == "q" || confirm == "Q" {
return Ok(None);
}
@@ -146,7 +149,7 @@ fn read_passphrase(prompt: &str) -> io::Result<Option<String>> {
}
}
fn confirm_install(disk: &Disk) -> Result<bool> {
fn confirm_install(disk: &Disk) -> Result<bool, Box<dyn std::error::Error>> {
println!("Are you sure you want to completely erase this this device?");
println!();
println!(" Device: {}", disk.path().display());
@@ -158,4 +161,3 @@ fn confirm_install(disk: &Disk) -> Result<bool> {
let answer = read_line()?;
Ok(answer == "YES")
}

View File

@@ -9,7 +9,6 @@ use pwhash::sha512_crypt;
use libcitadel::util;
use libcitadel::RealmFS;
use libcitadel::Result;
use libcitadel::OsRelease;
use libcitadel::KeyRing;
use libcitadel::terminal::Base16Scheme;
@@ -183,7 +182,7 @@ impl Installer {
self.install_syslinux = val;
}
pub fn verify(&self) -> Result<()> {
pub fn verify(&self) -> Result<(), Box<dyn std::error::Error>> {
let kernel_img = self.kernel_imagename();
let bzimage = format!("bzImage-{}", self.kernel_version());
let artifacts = vec![
@@ -192,26 +191,29 @@ impl Installer {
];
if !self.target().exists() {
bail!("target device {:?} does not exist", self.target());
panic!("target device {:?} does not exist", self.target());
}
for a in artifacts {
if !self.artifact_path(a).exists() {
bail!("required install artifact {} does not exist in {}", a, self.artifact_directory);
panic!(
"required install artifact {} does not exist in {}",
a, self.artifact_directory
);
}
}
Ok(())
}
pub fn run(&self) -> Result<()> {
pub fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
match self._type {
InstallType::Install => self.run_install(),
InstallType::LiveSetup => self.run_live_setup(),
}
}
pub fn run_install(&self) -> Result<()> {
pub fn run_install(&self) -> Result<(), Box<dyn std::error::Error>> {
let start = Instant::now();
self.partition_disk()?;
self.setup_luks()?;
@@ -224,7 +226,7 @@ impl Installer {
Ok(())
}
pub fn run_live_setup(&self) -> Result<()> {
pub fn run_live_setup(&self) -> Result<(), Box<dyn std::error::Error>> {
self.cmd_list(&[
"/bin/mount -t tmpfs var-tmpfs /sysroot/var",
"/bin/mount -t tmpfs home-tmpfs /sysroot/home",
@@ -242,8 +244,7 @@ impl Installer {
Ok(())
}
fn setup_live_realm(&self) -> Result<()> {
fn setup_live_realm(&self) -> Result<(), Box<dyn std::error::Error>> {
let realmfs_dir = self.storage().join("realms/realmfs-images");
let base_realmfs = realmfs_dir.join("base-realmfs.img");
@@ -261,14 +262,14 @@ impl Installer {
Ok(())
}
pub fn partition_disk(&self) -> Result<()> {
pub fn partition_disk(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Partitioning target disk")?;
self.cmd_list(PARTITION_COMMANDS, &[
("$TARGET", self.target_str())
])
}
pub fn setup_luks(&self) -> Result<()> {
pub fn setup_luks(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Setting up LUKS disk encryption")?;
util::create_dir(INSTALL_MOUNT)?;
util::write_file(LUKS_PASSPHRASE_FILE, self.passphrase().as_bytes())?;
@@ -281,15 +282,16 @@ impl Installer {
("$LUKS_PASSFILE", LUKS_PASSPHRASE_FILE),
])?;
util::remove_file(LUKS_PASSPHRASE_FILE)
util::remove_file(LUKS_PASSPHRASE_FILE)?;
Ok(())
}
pub fn setup_lvm(&self) -> Result<()> {
pub fn setup_lvm(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Setting up LVM volumes")?;
self.cmd_list(LVM_COMMANDS, &[])
}
pub fn setup_boot(&self) -> Result<()> {
pub fn setup_boot(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Setting up /boot partition")?;
let boot_partition = self.target_partition(1);
self.cmd(format!("/sbin/mkfs.vfat -F 32 {}", boot_partition))?;
@@ -323,11 +325,11 @@ impl Installer {
Ok(())
}
fn setup_syslinux(&self) -> Result<()> {
fn setup_syslinux(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Installing syslinux")?;
let syslinux_src = self.artifact_path("syslinux");
if !syslinux_src.exists() {
bail!("no syslinux directory found in artifact directory, cannot install syslinux");
panic!("no syslinux directory found in artifact directory, cannot install syslinux");
}
let dst = Path::new(INSTALL_MOUNT).join("syslinux");
util::create_dir(&dst)?;
@@ -345,17 +347,17 @@ impl Installer {
self.cmd(format!("/sbin/extlinux --install {}", dst.display()))
}
fn setup_syslinux_post_umount(&self) -> Result<()> {
fn setup_syslinux_post_umount(&self) -> Result<(), Box<dyn std::error::Error>> {
let mbrbin = self.artifact_path("syslinux/gptmbr.bin");
if !mbrbin.exists() {
bail!("could not find MBR image: {:?}", mbrbin);
panic!("could not find MBR image: {:?}", mbrbin);
}
self.cmd(format!("/bin/dd bs=440 count=1 conv=notrunc if={} of={}", mbrbin.display(), self.target().display()))?;
self.cmd(format!("/sbin/parted -s {} set 1 legacy_boot on", self.target_str()))
}
pub fn create_storage(&self) -> Result<()> {
pub fn create_storage(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Setting up /storage partition")?;
self.cmd_list(CREATE_STORAGE_COMMANDS,
@@ -365,7 +367,7 @@ impl Installer {
self.cmd(format!("/bin/umount {}", INSTALL_MOUNT))
}
fn setup_storage(&self) -> Result<()> {
fn setup_storage(&self) -> Result<(), Box<dyn std::error::Error>> {
if self._type == InstallType::Install {
self.create_keyring()?;
self.setup_storage_resources()?;
@@ -389,33 +391,33 @@ impl Installer {
Ok(())
}
fn create_keyring(&self) -> Result<()> {
fn create_keyring(&self) -> Result<(), Box<dyn std::error::Error>> {
self.info("Creating initial keyring")?;
let keyring = KeyRing::create_new();
keyring.write(self.storage().join("keyring"), self.passphrase.as_ref().unwrap())
Ok(keyring.write(self.storage().join("keyring"), self.passphrase.as_ref().unwrap())?)
}
fn setup_base_realmfs(&self) -> Result<()> {
fn setup_base_realmfs(&self) -> Result<(), Box<dyn std::error::Error>> {
let realmfs_dir = self.storage().join("realms/realmfs-images");
util::create_dir(&realmfs_dir)?;
self.sparse_copy_artifact("base-realmfs.img", &realmfs_dir)?;
self.cmd(format!("/usr/bin/citadel-image decompress {}/base-realmfs.img", realmfs_dir.display()))
}
fn setup_realm_skel(&self) -> Result<()> {
fn setup_realm_skel(&self) -> Result<(), Box<dyn std::error::Error>> {
let realm_skel = self.storage().join("realms/skel");
util::create_dir(&realm_skel)?;
util::copy_tree_with_chown(&self.skel(), &realm_skel, (1000,1000))
util::copy_tree_with_chown(&self.skel(), &realm_skel, (1000, 1000))?;
Ok(())
}
fn create_realmlock(&self, dir: &Path) -> Result<()> {
fn create_realmlock(&self, dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
fs::File::create(dir.join(".realmlock"))
.map_err(context!("failed to create {:?}/.realmlock file", dir))?;
Ok(())
}
fn setup_main_realm(&self) -> Result<()> {
fn setup_main_realm(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Creating main realm")?;
let realm = self.storage().join("realms/realm-main");
@@ -440,7 +442,7 @@ impl Installer {
self.create_realmlock(&realm)
}
fn setup_apt_cacher_realm(&self) -> Result<()> {
fn setup_apt_cacher_realm(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Creating apt-cacher realm")?;
let realm_base = self.storage().join("realms/realm-apt-cacher");
@@ -460,7 +462,7 @@ impl Installer {
self.create_realmlock(&realm_base)
}
fn setup_storage_resources(&self) -> Result<()> {
fn setup_storage_resources(&self) -> Result<(), Box<dyn std::error::Error>> {
let channel = match OsRelease::citadel_channel() {
Some(channel) => channel,
None => "dev",
@@ -474,7 +476,7 @@ impl Installer {
self.sparse_copy_artifact(&kernel_img, &resources)
}
fn setup_citadel_passphrase(&self) -> Result<()> {
fn setup_citadel_passphrase(&self) -> Result<(), Box<dyn std::error::Error>> {
if self._type == InstallType::LiveSetup {
self.info("Creating temporary citadel passphrase file for live mode")?;
let path = self.storage().join("citadel-state/passwd");
@@ -497,17 +499,15 @@ impl Installer {
Ok(())
}
pub fn install_rootfs_partitions(&self) -> Result<()> {
pub fn install_rootfs_partitions(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Installing rootfs partitions")?;
let rootfs = self.artifact_path("citadel-rootfs.img");
self.cmd(format!("/usr/bin/citadel-image install-rootfs --skip-sha {}", rootfs.display()))?;
self.cmd(format!("/usr/bin/citadel-image install-rootfs --skip-sha --no-prefer {}", rootfs.display()))
}
pub fn finish_install(&self) -> Result<()> {
self.cmd_list(FINISH_COMMANDS, &[
("$TARGET", self.target_str())
])
pub fn finish_install(&self) -> Result<(), Box<dyn std::error::Error>> {
self.cmd_list(FINISH_COMMANDS, &[("$TARGET", self.target_str())])
}
fn global_realm_config(&self) -> &str {
@@ -546,16 +546,33 @@ impl Installer {
Path::new(&self.artifact_directory).join(filename)
}
fn copy_artifact<P: AsRef<Path>>(&self, filename: &str, target: P) -> Result<()> {
fn copy_artifact<P: AsRef<Path>>(
&self,
filename: &str,
target: P,
) -> Result<(), Box<dyn std::error::Error>> {
self._copy_artifact(filename, target, false)
}
fn sparse_copy_artifact<P: AsRef<Path>>(&self, filename: &str, target: P) -> Result<()> {
fn sparse_copy_artifact<P: AsRef<Path>>(
&self,
filename: &str,
target: P,
) -> Result<(), Box<dyn std::error::Error>> {
self._copy_artifact(filename, target, true)
}
fn _copy_artifact<P: AsRef<Path>>(&self, filename: &str, target: P, sparse: bool) -> Result<()> {
self.info(format!("Copying {} to {}", filename, target.as_ref().display()))?;
fn _copy_artifact<P: AsRef<Path>>(
&self,
filename: &str,
target: P,
sparse: bool,
) -> Result<(), Box<dyn std::error::Error>> {
self.info(format!(
"Copying {} to {}",
filename,
target.as_ref().display()
))?;
let src = self.artifact_path(filename);
let target = target.as_ref();
util::create_dir(target)?;
@@ -568,20 +585,19 @@ impl Installer {
Ok(())
}
fn header<S: AsRef<str>>(&self, s: S) -> Result<()> {
fn header<S: AsRef<str>>(&self, s: S) -> Result<(), Box<dyn std::error::Error>> {
self.output(format!("\n[+] {}\n", s.as_ref()))
}
fn info<S: AsRef<str>>(&self, s: S) -> Result<()> {
fn info<S: AsRef<str>>(&self, s: S) -> Result<(), Box<dyn std::error::Error>> {
self.output(format!(" [>] {}", s.as_ref()))
}
fn output<S: AsRef<str>>(&self, s: S) -> Result<()> {
self.write_output(s.as_ref()).map_err(context!("error writing output"))
fn output<S: AsRef<str>>(&self, s: S) -> Result<(), Box<dyn std::error::Error>> {
self.write_output(s.as_ref())
}
fn write_output(&self, s: &str) -> io::Result<()> {
fn write_output(&self, s: &str) -> Result<(), Box<dyn std::error::Error>> {
println!("{}", s);
io::stdout().flush()?;
@@ -592,7 +608,11 @@ impl Installer {
Ok(())
}
fn cmd_list<I: IntoIterator<Item=S>, S: AsRef<str>>(&self, cmd_lines: I, subs: &[(&str,&str)]) -> Result<()> {
fn cmd_list<I: IntoIterator<Item = S>, S: AsRef<str>>(
&self,
cmd_lines: I,
subs: &[(&str, &str)],
) -> Result<(), Box<dyn std::error::Error>> {
for line in cmd_lines {
let line = line.as_ref();
let line = subs.iter().fold(line.to_string(), |acc, (from,to)| acc.replace(from,to));
@@ -602,12 +622,12 @@ impl Installer {
Ok(())
}
fn cmd<S: AsRef<str>>(&self, args: S) -> Result<()> {
fn cmd<S: AsRef<str>>(&self, args: S) -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<&str> = args.as_ref().split_whitespace().collect::<Vec<_>>();
self.run_cmd(args, false)
}
fn run_cmd(&self, args: Vec<&str>, as_user: bool) -> Result<()> {
fn run_cmd(&self, args: Vec<&str>, as_user: bool) -> Result<(), Box<dyn std::error::Error>> {
self.output(format!(" # {}", args.join(" ")))?;
let mut command = Command::new(args[0]);
@@ -632,8 +652,8 @@ impl Installer {
if !result.status.success() {
match result.status.code() {
Some(code) => bail!("command {} failed with exit code: {}", args[0], code),
None => bail!("command {} failed with no exit code", args[0]),
Some(code) => panic!("command {} failed with exit code: {}", args[0], code),
None => panic!("command {} failed with no exit code", args[0]),
}
}
Ok(())

View File

@@ -4,7 +4,7 @@ pub(crate) mod installer;
mod cli;
mod disk;
pub fn main(args: Vec<String>) {
pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
let mut args = args.iter().skip(1);
let result = if let Some(dev) = args.next() {
cli::run_cli_install_with(dev)
@@ -17,10 +17,11 @@ pub fn main(args: Vec<String>) {
Err(ref err) => {
println!("Install failed: {}", err);
exit(1);
},
}
};
if !ok {
println!("Install cancelled...");
}
}
Ok(())
}

View File

@@ -7,7 +7,6 @@ use std::sync::mpsc::{Sender};
use dbus::tree::{self, Factory, MTFn, MethodResult, Tree};
use dbus::{Message};
use dbus::blocking::LocalConnection;
use libcitadel::{Result};
// Use local version of disk.rs since we added some methods
use crate::install_backend::disk::*;
use crate::install::installer::*;
@@ -15,7 +14,6 @@ use std::fmt;
type MethodInfo<'a> = tree::MethodInfo<'a, MTFn<TData>, TData>;
const OBJECT_PATH: &str = "/com/subgraph/installer";
const INTERFACE_NAME: &str = "com.subgraph.installer.Manager";
const BUS_NAME: &str = "com.subgraph.installer";
@@ -37,8 +35,7 @@ pub struct DbusServer {
}
impl DbusServer {
pub fn connect() -> Result<DbusServer> {
pub fn connect() -> Result<DbusServer, Box<dyn std::error::Error>> {
let connection = LocalConnection::new_system()
.map_err(|e| format_err!("Failed to connect to DBUS system bus: {}", e))?;
let connection = Arc::new(connection);
@@ -80,7 +77,7 @@ impl DbusServer {
Ok(vec![m.msg.method_return().append1(list)])
}
fn run_install(path: String, citadel_passphrase: String, luks_passphrase: String, sender: Sender<Msg>) -> Result<()> {
fn run_install(path: String, citadel_passphrase: String, luks_passphrase: String, sender: Sender<Msg>) -> Result<(), Box<dyn std::error::Error>> {
let mut install = Installer::new(path, &citadel_passphrase, &luks_passphrase);
install.set_install_syslinux(true);
install.verify()?;
@@ -174,12 +171,12 @@ impl DbusServer {
Message::signal(&path, &iface, &member).append1(text)
}
pub fn start(&self) -> Result<()> {
let (sender, receiver) = mpsc::channel::<Msg>();
pub fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
let (sender, receiver) = mpsc::channel::<Msg>();
let sender_clone = sender.clone();
let tree = self.build_tree(sender);
if let Err(_err) = self.connection.request_name(BUS_NAME, false, true, false) {
bail!("Failed to request name");
panic!("Failed to request name");
}
tree.start_receive(self.connection.as_ref());
@@ -231,7 +228,6 @@ impl DbusServer {
}
}
}
}
#[derive(Clone)]
@@ -243,7 +239,6 @@ impl TreeData {
TreeData {}
}
fn disks(&self) -> HashMap<String, Vec<String>> {
let disks = Disk::probe_all().unwrap();
@@ -257,7 +252,6 @@ impl TreeData {
}
disk_map
}
}
impl fmt::Debug for TreeData {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

View File

@@ -1,24 +1,20 @@
use libcitadel::Result;
use std::process::exit;
mod disk;
mod dbus;
use libcitadel::CommandLine;
pub fn main() {
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
if CommandLine::install_mode() {
if let Err(e) = run_dbus_server() {
warn!("Error: {}", e);
}
run_dbus_server()
} else {
println!("Citadel installer backend will only run in install or live mode");
exit(1);
}
}
fn run_dbus_server() -> Result<()> {
fn run_dbus_server() -> Result<(), Box<dyn std::error::Error>> {
let server = dbus::DbusServer::connect()?;
server.start()?;
Ok(())
}

View File

@@ -16,73 +16,75 @@ mod mkimage;
mod realmfs;
mod sync;
mod update;
mod fetch;
fn main() {
let exe = match env::current_exe() {
Ok(path) => path,
Err(_e) => {
return;
},
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let exe = env::current_exe()?;
let args = env::args().collect::<Vec<String>>();
if exe == Path::new("/usr/libexec/citadel-boot") {
boot::main(args);
boot::main(args)
} else if exe == Path::new("/usr/libexec/citadel-install") {
install::main(args);
install::main(args)
} else if exe == Path::new("/usr/libexec/citadel-install-backend") {
install_backend::main();
install_backend::main()
} else if exe == Path::new("/usr/bin/citadel-image") {
image::main();
image::main(args)
} else if exe == Path::new("/usr/bin/citadel-realmfs") {
realmfs::main();
realmfs::main(args)
} else if exe == Path::new("/usr/bin/citadel-update") {
update::main(args);
} else if exe == Path::new("/usr/bin/citadel-fetch") {
fetch::main();
update::main(args)
} else if exe == Path::new("/usr/libexec/citadel-desktop-sync") {
sync::main(args);
sync::main(args)
} else if exe == Path::new("/usr/libexec/citadel-run") {
do_citadel_run(args);
do_citadel_run(args)
} else if exe.file_name() == Some(OsStr::new("citadel-mkimage")) {
mkimage::main(args);
mkimage::main(args)
} else if exe.file_name() == Some(OsStr::new("citadel-tool")) {
dispatch_command(args);
dispatch_command(args)
} else {
println!("Error: unknown executable {}", exe.display());
Result::Err(format!("Error: unknown executable {}", exe.display()).into())
}
}
fn dispatch_command(args: Vec<String>) {
fn dispatch_command(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
if let Some(command) = args.get(1) {
match command.as_str() {
"boot" => boot::main(rebuild_args("citadel-boot", args)),
"install" => install::main(rebuild_args("citadel-install", args)),
"image" => image::main(),
"realmfs" => realmfs::main(),
"image" => image::main(rebuild_args("citadel-image", args)),
"realmfs" => realmfs::main(rebuild_args("citadel-realmfs", args)),
"update" => update::main(rebuild_args("citadel-update", args)),
"fetch" => update::main(rebuild_args("citadel-fetch", args)),
"mkimage" => mkimage::main(rebuild_args("citadel-mkimage", args)),
"sync" => sync::main(rebuild_args("citadel-desktop-sync", args)),
"run" => do_citadel_run(rebuild_args("citadel-run", args)),
_ => println!("Error: unknown command {}", command),
_ => throw_err(command),
}
} else {
println!("Must provide an argument");
Result::Err("Must provide an argument".into())
}
}
fn throw_err(command: &String) -> Result<(), Box<dyn std::error::Error>> {
Result::Err(format!("Error: unknown command {}", command).into())
}
fn rebuild_args(command: &str, args: Vec<String>) -> Vec<String> {
iter::once(command.to_string())
.chain(args.into_iter().skip(2))
.collect()
}
fn do_citadel_run(args: Vec<String>) {
fn do_citadel_run(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
if let Err(e) = RealmManager::run_in_current(&args[1..], true) {
println!("RealmManager::run_in_current({:?}) failed: {}", &args[1..], e);
return Result::Err(
format!(
"RealmManager::run_in_current({:?}) failed: {}",
&args[1..],
e
)
.into(),
);
}
Ok(())
}

View File

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

View File

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

View File

@@ -1,13 +1,10 @@
use std::process::exit;
use libcitadel::Result;
mod config;
mod build;
pub fn main(args: Vec<String>) {
pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
let config_path = match args.get(1) {
Some(arg) => arg,
None => {
@@ -21,11 +18,11 @@ pub fn main(args: Vec<String>) {
exit(1);
}
Ok(())
}
fn build_image(config_path: &str) -> Result<()> {
fn build_image(config_path: &str) -> Result<(), Box<dyn std::error::Error>> {
let conf = config::BuildConfig::load(config_path)?;
let mut builder = build::UpdateBuilder::new(conf);
builder.build()
}
Ok(builder.build()?)
}

View File

@@ -1,24 +1,26 @@
use clap::{command, Command};
use clap::{Arg, ArgMatches};
use clap::App;
use clap::ArgMatches;
use libcitadel::{Result,RealmFS,Logger,LogLevel};
use libcitadel::{RealmFS, Logger, LogLevel};
use libcitadel::util::is_euid_root;
use clap::SubCommand;
use clap::AppSettings::*;
use clap::Arg;
use libcitadel::ResizeSize;
use std::process::exit;
pub fn main() {
pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
Logger::set_log_level(LogLevel::Debug);
let matches = command!()
.about("citadel-realmfs")
.arg_required_else_help(true)
.subcommand(Command::new("resize")
let app = App::new("citadel-realmfs")
.about("Citadel realmfs image tool")
.settings(&[ArgRequiredElseHelp,ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder,SubcommandsNegateReqs])
.subcommand(SubCommand::with_name("resize")
.about("Resize an existing RealmFS image. If the image is currently sealed, it will also be unsealed.")
.arg(Arg::new("image")
.arg(Arg::with_name("image")
.help("Path or name of RealmFS image to resize")
.required(true))
.arg(Arg::new("size")
.arg(Arg::with_name("size")
.help("Size to increase RealmFS image to (or by if prefixed with '+')")
.long_help("\
The size can be followed by a 'g' or 'm' character \
@@ -31,66 +33,63 @@ is the final absolute size of the image.")
.required(true)))
.subcommand(Command::new("fork")
.subcommand(SubCommand::with_name("fork")
.about("Create a new RealmFS image as an unsealed copy of an existing image")
.arg(Arg::new("image")
.arg(Arg::with_name("image")
.help("Path or name of RealmFS image to fork")
.required(true))
.arg(Arg::new("forkname")
.arg(Arg::with_name("forkname")
.help("Name of new image to create")
.required(true)))
.subcommand(Command::new("autoresize")
.subcommand(SubCommand::with_name("autoresize")
.about("Increase size of RealmFS image if not enough free space remains")
.arg(Arg::new("image")
.arg(Arg::with_name("image")
.help("Path or name of RealmFS image")
.required(true)))
.subcommand(Command::new("update")
.subcommand(SubCommand::with_name("update")
.about("Open an update shell on the image")
.arg(Arg::new("image")
.arg(Arg::with_name("image")
.help("Path or name of RealmFS image")
.required(true)))
.subcommand(Command::new("activate")
.subcommand(SubCommand::with_name("activate")
.about("Activate a RealmFS by creating a block device for the image and mounting it.")
.arg(Arg::new("image")
.arg(Arg::with_name("image")
.help("Path or name of RealmFS image to activate")
.required(true)))
.subcommand(Command::new("deactivate")
.subcommand(SubCommand::with_name("deactivate")
.about("Deactivate a RealmFS by unmounting it and removing block device created during activation.")
.arg(Arg::new("image")
.arg(Arg::with_name("image")
.help("Path or name of RealmFS image to deactivate")
.required(true)))
.arg(Arg::new("image")
.arg(Arg::with_name("image")
.help("Name of or path to RealmFS image to display information about")
.required(true))
.get_matches();
.required(true));
let matches = app.get_matches_from(args);
let result = match matches.subcommand() {
Some(("resize", m)) => resize(m),
Some(("autoresize", m)) => autoresize(m),
Some(("fork", m)) => fork(m),
Some(("update", m)) => update(m),
Some(("activate", m)) => activate(m),
Some(("deactivate", m)) => deactivate(m),
("resize", Some(m)) => resize(m),
("autoresize", Some(m)) => autoresize(m),
("fork", Some(m)) => fork(m),
("update", Some(m)) => update(m),
("activate", Some(m)) => activate(m),
("deactivate", Some(m)) => deactivate(m),
_ => image_info(&matches),
};
}?;
if let Err(ref e) = result {
eprintln!("Error: {}", e);
exit(1);
}
Ok(())
}
fn realmfs_image(arg_matches: &ArgMatches) -> Result<RealmFS> {
let image = match arg_matches.get_one::<String>("image") {
fn realmfs_image(arg_matches: &ArgMatches) -> Result<RealmFS, Box<dyn std::error::Error>> {
let image = match arg_matches.value_of("image") {
Some(s) => s,
None => bail!("Image argument required."),
None => panic!("Image argument required."),
};
let realmfs = if RealmFS::is_valid_name(image) {
@@ -98,41 +97,45 @@ fn realmfs_image(arg_matches: &ArgMatches) -> Result<RealmFS> {
} else if RealmFS::is_valid_realmfs_image(image) {
RealmFS::load_from_path(image)?
} else {
bail!("Not a valid realmfs name or path to realmfs image file: {}", image);
panic!(
"Not a valid realmfs name or path to realmfs image file: {}",
image
);
};
Ok(realmfs)
}
fn image_info(arg_matches: &ArgMatches) -> Result<()> {
fn image_info(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = realmfs_image(arg_matches)?;
print!("{}", String::from_utf8(img.header().metainfo_bytes())?);
Ok(())
}
fn parse_resize_size(s: &str) -> Result<ResizeSize> {
fn parse_resize_size(s: &str) -> Result<ResizeSize, Box<dyn std::error::Error>> {
let unit = s.chars().last().filter(|c| c.is_alphabetic());
let skip = if s.starts_with('+') { 1 } else { 0 };
let size = s.chars()
let size = s
.chars()
.skip(skip)
.take_while(|c| c.is_numeric())
.collect::<String>()
.parse::<usize>()
.map_err(|_| format_err!("Unable to parse size value '{}'",s))?;
.map_err(|_| format_err!("Unable to parse size value '{}'", s))?;
let sz = match unit {
Some('g') | Some('G') => ResizeSize::gigs(size),
Some('m') | Some('M') => ResizeSize::megs(size),
Some(c) => bail!("Unknown size unit '{}'", c),
Some(c) => panic!("Unknown size unit '{}'", c),
None => ResizeSize::blocks(size),
};
Ok(sz)
}
fn resize(arg_matches: &ArgMatches) -> Result<()> {
fn resize(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = realmfs_image(arg_matches)?;
info!("image is {}", img.path().display());
let size_arg = match arg_matches.get_one::<String>("size") {
let size_arg = match arg_matches.value_of("size") {
Some(size) => size,
None => "No size argument",
};
@@ -141,51 +144,55 @@ fn resize(arg_matches: &ArgMatches) -> Result<()> {
let size = parse_resize_size(size_arg)?;
if mode_add {
img.resize_grow_by(size)
img.resize_grow_by(size)?;
} else {
img.resize_grow_to(size)
img.resize_grow_to(size)?;
}
Ok(())
}
fn autoresize(arg_matches: &ArgMatches) -> Result<()> {
fn autoresize(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = realmfs_image(arg_matches)?;
if let Some(size) = img.auto_resize_size() {
img.resize_grow_to(size)
img.resize_grow_to(size)?;
} else {
info!("RealmFS image {} has sufficient free space, doing nothing", img.path().display());
Ok(())
info!(
"RealmFS image {} has sufficient free space, doing nothing",
img.path().display()
);
}
Ok(())
}
fn fork(arg_matches: &ArgMatches) -> Result<()> {
fn fork(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = realmfs_image(arg_matches)?;
let forkname = match arg_matches.get_one::<String>("forkname") {
let forkname = match arg_matches.value_of("forkname") {
Some(name) => name,
None => bail!("No fork name argument"),
None => panic!("No fork name argument"),
};
if !RealmFS::is_valid_name(forkname) {
bail!("Not a valid RealmFS image name '{}'", forkname);
panic!("Not a valid RealmFS image name '{}'", forkname);
}
if RealmFS::named_image_exists(forkname) {
bail!("A RealmFS image named '{}' already exists", forkname);
panic!("A RealmFS image named '{}' already exists", forkname);
}
img.fork(forkname)?;
Ok(())
}
fn update(arg_matches: &ArgMatches) -> Result<()> {
fn update(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
if !is_euid_root() {
bail!("RealmFS updates must be run as root");
panic!("RealmFS updates must be run as root");
}
let img = realmfs_image(arg_matches)?;
img.interactive_update(Some("icy"))?;
Ok(())
}
fn activate(arg_matches: &ArgMatches) -> Result<()> {
fn activate(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = realmfs_image(arg_matches)?;
let img_arg = arg_matches.get_one::<String>("image").unwrap();
let img_arg = arg_matches.value_of("image").unwrap();
if img.is_activated() {
info!("RealmFS image {} is already activated", img_arg);
@@ -196,9 +203,9 @@ fn activate(arg_matches: &ArgMatches) -> Result<()> {
Ok(())
}
fn deactivate(arg_matches: &ArgMatches) -> Result<()> {
fn deactivate(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = realmfs_image(arg_matches)?;
let img_arg = arg_matches.get_one::<String>("image").unwrap();
let img_arg = arg_matches.value_of("image").unwrap();
if !img.is_activated() {
info!("RealmFS image {} is not activated", img_arg);
} else if img.is_in_use() {

View File

@@ -1,9 +1,9 @@
use std::collections::HashSet;
use std::ffi::OsStr;
use std::path::{Path,PathBuf};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use libcitadel::{Realm, Realms, Result, util};
use libcitadel::{Realm, Realms, util};
use crate::sync::parser::DesktopFileParser;
use std::fs::DirEntry;
use crate::sync::desktop_file::DesktopFile;
@@ -17,14 +17,13 @@ pub struct DesktopFileSync {
icons: Option<IconSync>,
}
#[derive(Eq,PartialEq,Hash)]
#[derive(Eq, PartialEq, Hash)]
struct DesktopItem {
path: PathBuf,
mtime: SystemTime,
}
impl DesktopItem {
fn new(path: PathBuf, mtime: SystemTime) -> Self {
DesktopItem { path, mtime }
}
@@ -46,7 +45,7 @@ impl DesktopItem {
impl DesktopFileSync {
pub const CITADEL_APPLICATIONS: &'static str = "/home/citadel/.local/share/applications";
pub fn sync_active_realms() -> Result<()> {
pub fn sync_active_realms() -> Result<(), Box<dyn std::error::Error>> {
let realms = Realms::load()?;
for realm in realms.active(true) {
let mut sync = DesktopFileSync::new(realm);
@@ -72,7 +71,7 @@ impl DesktopFileSync {
DesktopFileSync { realm, items: HashSet::new(), icons }
}
pub fn run_sync(&mut self, clear: bool) -> Result<()> {
pub fn run_sync(&mut self, clear: bool) -> Result<(), Box<dyn std::error::Error>> {
IconSync::ensure_theme_index_exists()?;
@@ -98,8 +97,8 @@ impl DesktopFileSync {
Ok(())
}
fn collect_source_files(&mut self, directory: impl AsRef<Path>) -> Result<()> {
let mut directory = self.realm.run_path().join(directory.as_ref());
fn collect_source_files(&mut self, directory: impl AsRef<Path>) -> Result<(), Box<dyn std::error::Error>> {
let mut directory = Realms::current_realm_symlink().join(directory.as_ref());
directory.push("share/applications");
if directory.exists() {
util::read_directory(&directory, |dent| {
@@ -119,20 +118,16 @@ impl DesktopFileSync {
}
}
pub fn clear_target_files() -> Result<()> {
util::read_directory(Self::CITADEL_APPLICATIONS, |dent| {
pub fn clear_target_files() -> Result<(), Box<dyn std::error::Error>> {
Ok(util::read_directory(Self::CITADEL_APPLICATIONS, |dent| {
util::remove_file(dent.path())
})
})?)
}
fn remove_missing_target_files(&mut self) -> Result<()> {
let mut sources = self.source_filenames();
// If flatpak is enabled, don't remove the generated GNOME Software desktop file
if self.realm.config().flatpak() {
sources.insert(format!("realm-{}.org.gnome.Software.desktop", self.realm.name()));
}
fn remove_missing_target_files(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let sources = self.source_filenames();
let prefix = format!("realm-{}.", self.realm.name());
util::read_directory(Self::CITADEL_APPLICATIONS, |dent| {
Ok(util::read_directory(Self::CITADEL_APPLICATIONS, |dent| {
if let Some(filename) = dent.file_name().to_str() {
if filename.starts_with(&prefix) && !sources.contains(filename) {
let path = dent.path();
@@ -141,7 +136,7 @@ impl DesktopFileSync {
}
}
Ok(())
})
})?)
}
fn mtime(path: &Path) -> Option<SystemTime> {
@@ -160,7 +155,7 @@ impl DesktopFileSync {
.collect()
}
fn synchronize_items(&self) -> Result<()> {
fn synchronize_items(&self) -> Result<(), Box<dyn std::error::Error>> {
for item in &self.items {
let target = Path::new(Self::CITADEL_APPLICATIONS).join(item.filename());
if item.is_newer_than(&target) {
@@ -184,11 +179,9 @@ impl DesktopFileSync {
}
}
fn sync_item(&self, item: &DesktopItem) -> Result<()> {
fn sync_item(&self, item: &DesktopItem) -> Result<(), Box<dyn std::error::Error>> {
let mut dfp = DesktopFileParser::parse_from_path(&item.path, "/usr/libexec/citadel-run ")?;
// When use-flatpak is enabled a gnome-software desktop file will be generated
let flatpak_gs_hide = dfp.filename() == "org.gnome.Software.desktop" && self.realm.config().flatpak();
if dfp.is_showable() && !flatpak_gs_hide {
if dfp.is_showable() {
self.sync_item_icon(&mut dfp);
dfp.write_to_dir(Self::CITADEL_APPLICATIONS, Some(&self.realm))?;
} else {

View File

@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
use libcitadel::{Result, util, Realm};
use std::cell::{RefCell, Cell};
use std::fs;
use crate::sync::desktop_file::DesktopFile;
use crate::sync::REALM_BASE_PATHS;

View File

@@ -1,4 +1,4 @@
use libcitadel::{Result, Logger, LogLevel};
use libcitadel::{Logger, LogLevel};
mod desktop_file;
mod parser;
@@ -14,13 +14,12 @@ fn has_arg(args: &[String], arg: &str) -> bool {
pub const REALM_BASE_PATHS:&[&str] = &[
"rootfs/usr",
"flatpak/exports",
"rootfs/var/lib/flatpak/exports",
"home/.local",
"home/.local/share/flatpak/exports"
];
pub fn main(args: Vec<String>) {
pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
if has_arg(&args, "-v") {
Logger::set_log_level(LogLevel::Debug);
} else {
@@ -37,12 +36,14 @@ pub fn main(args: Vec<String>) {
println!("Desktop file sync failed: {}", e);
}
}
Ok(())
}
fn sync(clear: bool) -> Result<()> {
fn sync(clear: bool) -> Result<(), Box<dyn std::error::Error>> {
if let Some(mut sync) = DesktopFileSync::new_current() {
sync.run_sync(clear)
sync.run_sync(clear)?
} else {
DesktopFileSync::clear_target_files()
DesktopFileSync::clear_target_files()?
}
Ok(())
}

View File

@@ -1,6 +1,6 @@
use std::path::{Path, PathBuf};
use libcitadel::{Result, Partition, ResourceImage, ImageHeader, LogLevel, Logger, util};
use libcitadel::{Partition, ResourceImage, ImageHeader, LogLevel, Logger, util};
use crate::update::kernel::{KernelInstaller, KernelVersion};
use std::collections::HashSet;
use std::fs::{DirEntry, File};
@@ -16,7 +16,7 @@ const FLAG_QUIET: u32 = 0x04;
const RESOURCES_DIRECTORY: &str = "/storage/resources";
const TEMP_DIRECTORY: &str = "/storage/resources/tmp";
pub fn main(args: Vec<String>) {
pub fn main(args: Vec<String>) -> std::result::Result<(), Box<dyn std::error::Error>> {
let mut args = args.iter().skip(1);
let mut flags = 0;
@@ -34,7 +34,7 @@ pub fn main(args: Vec<String>) {
Logger::set_log_level(LogLevel::Debug);
} else if arg == "--choose-rootfs" {
let _ = choose_install_partition(true);
return;
return Ok(())
} else {
let path = Path::new(arg);
if let Err(e) = install_image(path, flags) {
@@ -42,12 +42,13 @@ pub fn main(args: Vec<String>) {
}
}
}
Ok(())
}
// Search directory containing installed image files for an
// image file that has an identical shasum and abort the installation
// if a duplicate is found.
fn detect_duplicates(header: &ImageHeader) -> Result<()> {
fn detect_duplicates(header: &ImageHeader) -> Result<(), Box<dyn std::error::Error>> {
let metainfo = header.metainfo();
let channel = metainfo.channel();
let shasum = metainfo.shasum();
@@ -61,17 +62,20 @@ fn detect_duplicates(header: &ImageHeader) -> Result<()> {
return Ok(())
}
util::read_directory(&resource_dir, |dent| {
Ok(util::read_directory(&resource_dir, |dent| {
if let Ok(hdr) = ImageHeader::from_file(dent.path()) {
if hdr.metainfo().shasum() == shasum {
bail!("A duplicate image file with the same shasum already exists at {}", dent.path().display());
panic!(
"A duplicate image file with the same shasum already exists at {}",
dent.path().display()
);
}
}
Ok(())
})
})?)
}
fn create_tmp_copy(path: &Path) -> Result<PathBuf> {
fn create_tmp_copy(path: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
if !Path::new(TEMP_DIRECTORY).exists() {
util::create_dir(TEMP_DIRECTORY)?;
}
@@ -93,12 +97,12 @@ fn create_tmp_copy(path: &Path) -> Result<PathBuf> {
Ok(path)
}
pub fn install_image(path: &Path, flags: u32) -> Result<()> {
fn install_image(path: &Path, flags: u32) -> Result<(), Box<dyn std::error::Error>> {
if !path.exists() || path.file_name().is_none() {
bail!("file path {} does not exist", path.display());
panic!("file path {} does not exist", path.display());
}
if !util::is_euid_root() {
bail!("Image updates must be installed by root user");
panic!("Image updates must be installed by root user");
}
let header = ImageHeader::from_file(path)?;
@@ -113,14 +117,14 @@ pub fn install_image(path: &Path, flags: u32) -> Result<()> {
match image.metainfo().image_type() {
"kernel" => install_kernel_image(&mut image),
"extra" => install_extra_image(&image),
"rootfs" => install_rootfs_image(&image, flags),
image_type => bail!("Unknown image type: {}", image_type),
"rootfs" => install_rootfs_image(&image, flags),
image_type => panic!("Unknown image type: {}", image_type),
}
}
// Prepare the image file for installation by decompressing and generating
// dmverity hash tree.
fn prepare_image(image: &ResourceImage, flags: u32) -> Result<()> {
fn prepare_image(image: &ResourceImage, flags: u32) -> Result<(), Box<dyn std::error::Error>> {
if image.is_compressed() {
image.decompress(false)?;
}
@@ -129,7 +133,7 @@ fn prepare_image(image: &ResourceImage, flags: u32) -> Result<()> {
info!("Verifying sha256 hash of image");
let shasum = image.generate_shasum()?;
if shasum != image.metainfo().shasum() {
bail!("image file does not have expected sha256 value");
panic!("image file does not have expected sha256 value");
}
}
@@ -139,24 +143,28 @@ fn prepare_image(image: &ResourceImage, flags: u32) -> Result<()> {
Ok(())
}
fn install_extra_image(image: &ResourceImage) -> Result<()> {
let filename = format!("citadel-extra-{}.img", image.header().metainfo().version());
fn install_extra_image(image: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
let filename = format!("citadel-extra-{:03}.img", image.header().metainfo().version());
install_image_file(image, filename.as_str())?;
remove_old_extra_images(image)?;
Ok(())
}
fn remove_old_extra_images(image: &ResourceImage) -> Result<()> {
fn remove_old_extra_images(image: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
let new_meta = image.header().metainfo();
let shasum = new_meta.shasum();
let target_dir = target_directory(image)?;
util::read_directory(&target_dir, |dent| {
Ok(util::read_directory(&target_dir, |dent| {
let path = dent.path();
maybe_remove_old_extra_image(&path, shasum)
})
maybe_remove_old_extra_image(&path, shasum).unwrap();
Ok(())
})?)
}
fn maybe_remove_old_extra_image(path: &Path, shasum: &str) -> Result<()> {
fn maybe_remove_old_extra_image(
path: &Path,
shasum: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let header = ImageHeader::from_file(&path)?;
if !header.is_magic_valid() {
return Ok(());
@@ -172,21 +180,21 @@ fn maybe_remove_old_extra_image(path: &Path, shasum: &str) -> Result<()> {
Ok(())
}
fn install_kernel_image(image: &mut ResourceImage) -> Result<()> {
fn install_kernel_image(image: &mut ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
if !Path::new("/boot/loader/loader.conf").exists() {
bail!("failed to automount /boot partition. Please manually mount correct partition.");
panic!("failed to automount /boot partition. Please manually mount correct partition.");
}
let metainfo = image.header().metainfo();
let version = metainfo.version();
let kernel_version = match metainfo.kernel_version() {
Some(kv) => kv,
None => bail!("kernel image does not have kernel version field"),
None => panic!("kernel image does not have kernel version field"),
};
info!("kernel version is {}", kernel_version);
install_kernel_file(image, &kernel_version)?;
let filename = format!("citadel-kernel-{}-{}.img", kernel_version, version);
let filename = format!("citadel-kernel-{}-{:03}.img", kernel_version, version);
install_image_file(image, &filename)?;
let all_versions = all_boot_kernel_versions()?;
@@ -194,7 +202,7 @@ fn install_kernel_image(image: &mut ResourceImage) -> Result<()> {
let mut remove_paths = Vec::new();
util::read_directory(&image_dir, |dent| {
let path = dent.path();
if is_unused_kernel_image(&path, &all_versions)? {
if is_unused_kernel_image(&path, &all_versions).unwrap() {
remove_paths.push(path);
}
Ok(())
@@ -206,7 +214,7 @@ fn install_kernel_image(image: &mut ResourceImage) -> Result<()> {
Ok(())
}
fn is_unused_kernel_image(path: &Path, versions: &HashSet<String>) -> Result<bool> {
fn is_unused_kernel_image(path: &Path, versions: &HashSet<String>) -> Result<bool, Box<dyn std::error::Error>> {
let header = ImageHeader::from_file(path)?;
if !header.is_magic_valid() {
return Ok(false);
@@ -226,23 +234,25 @@ fn is_unused_kernel_image(path: &Path, versions: &HashSet<String>) -> Result<boo
Ok(false)
}
fn install_kernel_file(image: &mut ResourceImage, kernel_version: &str) -> Result<()> {
fn install_kernel_file(
image: &mut ResourceImage,
kernel_version: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let mountpoint = Path::new("/run/citadel/images/kernel-install.mountpoint");
info!("Temporarily mounting kernel resource image");
let mut handle = image.mount_at(mountpoint)?;
let kernel_path = mountpoint.join("kernel/bzImage");
if !kernel_path.exists() {
handle.unmount()?;
bail!("kernel not found in kernel resource image at /kernel/bzImage")
panic!("kernel not found in kernel resource image at /kernel/bzImage")
}
let result = KernelInstaller::install_kernel(&kernel_path, kernel_version);
KernelInstaller::install_kernel(&kernel_path, kernel_version)?;
info!("Unmounting kernel resource image");
handle.unmount()?;
result
Ok(handle.unmount()?)
}
fn all_boot_kernel_versions() -> Result<HashSet<String>> {
fn all_boot_kernel_versions() -> Result<HashSet<String>, Box<dyn std::error::Error>> {
let mut result = HashSet::new();
util::read_directory("/boot", |dent| {
if is_kernel_dirent(&dent) {
@@ -264,7 +274,10 @@ fn is_kernel_dirent(dirent: &DirEntry) -> bool {
}
}
fn install_image_file(image: &ResourceImage, filename: &str) -> Result<()> {
fn install_image_file(
image: &ResourceImage,
filename: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let image_dir = target_directory(image)?;
let image_dest = image_dir.join(filename);
if image_dest.exists() {
@@ -275,14 +288,14 @@ fn install_image_file(image: &ResourceImage, filename: &str) -> Result<()> {
Ok(())
}
fn target_directory(image: &ResourceImage) -> Result<PathBuf> {
fn target_directory(image: &ResourceImage) -> Result<PathBuf, Box<dyn std::error::Error>> {
let metainfo = image.header().metainfo();
let channel = metainfo.channel();
validate_channel_name(channel)?;
Ok(Path::new("/storage/resources").join(channel))
}
fn rotate(path: &Path) -> Result<()> {
fn rotate(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
if !path.exists() || path.file_name().is_none() {
return Ok(());
}
@@ -293,14 +306,17 @@ fn rotate(path: &Path) -> Result<()> {
Ok(())
}
fn validate_channel_name(channel: &str) -> Result<()> {
fn validate_channel_name(channel: &str) -> Result<(), Box<dyn std::error::Error>> {
if !channel.chars().all(|c| c.is_ascii_lowercase()) {
bail!("image has invalid channel name '{}'", channel);
panic!("image has invalid channel name '{}'", channel);
}
Ok(())
}
fn install_rootfs_image(image: &ResourceImage, flags: u32) -> Result<()> {
fn install_rootfs_image(
image: &ResourceImage,
flags: u32,
) -> Result<(), Box<dyn std::error::Error>> {
let quiet = flags & FLAG_QUIET != 0;
let partition = choose_install_partition(!quiet)?;
@@ -315,7 +331,7 @@ fn install_rootfs_image(image: &ResourceImage, flags: u32) -> Result<()> {
Ok(())
}
fn clear_prefer_boot() -> Result<()> {
fn clear_prefer_boot() -> Result<(), Box<dyn std::error::Error>> {
for mut p in Partition::rootfs_partitions()? {
if p.is_initialized() && p.header().has_flag(ImageHeader::FLAG_PREFER_BOOT) {
p.clear_flag_and_write(ImageHeader::FLAG_PREFER_BOOT)?;
@@ -332,7 +348,7 @@ fn bool_to_yesno(val: bool) -> &'static str {
}
}
fn choose_install_partition(verbose: bool) -> Result<Partition> {
fn choose_install_partition(verbose: bool) -> Result<Partition, Box<dyn std::error::Error>> {
let partitions = Partition::rootfs_partitions()?;
if verbose {
@@ -362,5 +378,5 @@ fn choose_install_partition(verbose: bool) -> Result<Partition> {
return Ok(p.clone())
}
}
bail!("no suitable install partition found")
panic!("no suitable install partition found")
}

View File

@@ -1,8 +0,0 @@
[package]
name = "launch-gnome-software"
version = "0.1.0"
edition = "2021"
[dependencies]
libcitadel = { path = "../libcitadel" }
anyhow = "1.0"

View File

@@ -1,67 +0,0 @@
use std::env;
use libcitadel::{Logger, LogLevel, Realm, Realms, util};
use libcitadel::flatpak::GnomeSoftwareLauncher;
use anyhow::{bail, Result};
fn realm_arg() -> Option<String> {
let mut args = env::args();
while let Some(arg) = args.next() {
if arg == "--realm" {
return args.next();
}
}
None
}
fn choose_realm() -> Result<Realm> {
let mut realms = Realms::load()?;
if let Some(realm_name) = realm_arg() {
match realms.by_name(&realm_name) {
None => bail!("realm '{}' not found", realm_name),
Some(realm) => return Ok(realm),
}
}
let realm = match realms.current() {
Some(realm) => realm,
None => bail!("no current realm"),
};
Ok(realm)
}
fn has_arg(arg: &str) -> bool {
env::args()
.skip(1)
.any(|s| s == arg)
}
fn launch() -> Result<()> {
let realm = choose_realm()?;
if !util::is_euid_root() {
bail!("Must be run with root euid");
}
let mut launcher = GnomeSoftwareLauncher::new(realm)?;
if has_arg("--quit") {
launcher.quit()?;
} else {
if has_arg("--shell") {
launcher.set_run_shell();
}
launcher.launch()?;
}
Ok(())
}
fn main() {
if has_arg("--verbose") {
Logger::set_log_level(LogLevel::Verbose);
}
if let Err(err) = launch() {
eprintln!("Error: {}", err);
}
}

View File

@@ -10,7 +10,6 @@ nix = "0.17.0"
toml = "0.5"
serde = "1.0"
serde_derive = "1.0"
serde_json = "=1.0.1"
lazy_static = "1.4"
sodiumoxide = "0.2"
hex = "0.4"
@@ -20,8 +19,6 @@ walkdir = "2"
dbus = "0.6"
posix-acl = "1.0.0"
procfs = "0.12.0"
semver = "1.0"
clap = "4.5"
[dependencies.inotify]
version = "0.8"

View File

@@ -84,8 +84,8 @@ impl OsRelease {
OsRelease::get_value("CITADEL_IMAGE_PUBKEY")
}
pub fn citadel_rootfs_version() -> Option<&'static str> {
OsRelease::get_value("CITADEL_ROOTFS_VERSION")
pub fn citadel_rootfs_version() -> Option<usize> {
OsRelease::get_int_value("CITADEL_ROOTFS_VERSION")
}
pub fn citadel_kernel_version() -> Option<&'static str> {
@@ -96,14 +96,6 @@ impl OsRelease {
OsRelease::get_value("CITADEL_KERNEL_ID")
}
pub fn citadel_client() -> Option<&'static str> {
OsRelease::get_value("CITADEL_CLIENT")
}
pub fn citadel_publisher() -> Option<&'static str> {
OsRelease::get_value("CITADEL_PUBLISHER")
}
fn _get_value(&self, key: &str) -> Option<&str> {
self.vars.get(key).map(|v| v.as_str())
}

View File

@@ -1,221 +0,0 @@
use std::ffi::OsStr;
use std::{fs, io};
use std::fs::File;
use std::os::fd::AsRawFd;
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::Command;
use crate::{Logger, LogLevel, Result, verbose};
const BWRAP_PATH: &str = "/usr/libexec/flatpak-bwrap";
pub struct BubbleWrap {
command: Command,
}
impl BubbleWrap {
pub fn new() -> Self {
BubbleWrap {
command: Command::new(BWRAP_PATH),
}
}
fn add_arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
self.command.arg(arg);
self
}
fn add_args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
for arg in args {
self.add_arg(arg.as_ref());
}
self
}
pub fn ro_bind(&mut self, path_list: &[&str]) -> &mut Self {
for &path in path_list {
self.add_args(&["--ro-bind", path, path]);
}
self
}
pub fn ro_bind_to(&mut self, src: &str, dest: &str) -> &mut Self {
self.add_args(&["--ro-bind", src, dest])
}
pub fn bind_to(&mut self, src: &str, dest: &str) -> &mut Self {
self.add_args(&["--bind", src, dest])
}
pub fn create_dirs(&mut self, dir_list: &[&str]) -> &mut Self {
for &dir in dir_list {
self.add_args(&["--dir", dir]);
}
self
}
pub fn create_symlinks(&mut self, links: &[(&str, &str)]) -> &mut Self {
for (src,dest) in links {
self.add_args(&["--symlink", src, dest]);
}
self
}
pub fn mount_dev(&mut self) -> &mut Self {
self.add_args(&["--dev", "/dev"])
}
pub fn mount_proc(&mut self) -> &mut Self {
self.add_args(&["--proc", "/proc"])
}
pub fn dev_bind(&mut self, path: &str) -> &mut Self {
self.add_args(&["--dev-bind", path, path])
}
pub fn clear_env(&mut self) -> &mut Self {
self.add_arg("--clearenv")
}
pub fn set_env_list(&mut self, env_list: &[&str]) -> &mut Self {
for line in env_list {
if let Some((k,v)) = line.split_once("=") {
self.add_args(&["--setenv", k, v]);
} else {
eprintln!("Warning: environment variable '{}' does not have = delimiter. Ignoring", line);
}
}
self
}
pub fn unshare_all(&mut self) -> &mut Self {
self.add_arg("--unshare-all")
}
pub fn share_net(&mut self) -> &mut Self {
self.add_arg("--share-net")
}
pub fn log_command(&self) {
let mut buffer = String::new();
verbose!("{}", BWRAP_PATH);
for arg in self.command.get_args() {
if let Some(s) = arg.to_str() {
if s.starts_with("-") {
if !buffer.is_empty() {
verbose!(" {}", buffer);
buffer.clear();
}
}
if !buffer.is_empty() {
buffer.push(' ');
}
buffer.push_str(s);
}
}
if !buffer.is_empty() {
verbose!(" {}", buffer);
}
}
pub fn status_file(&mut self, status_file: &File) -> &mut Self {
// Rust sets O_CLOEXEC when opening files so we create
// a new descriptor that will remain open across exec()
let dup_fd = unsafe {
libc::dup(status_file.as_raw_fd())
};
if dup_fd == -1 {
warn!("Failed to dup() status file descriptor: {}", io::Error::last_os_error());
warn!("Skipping --json-status-fd argument");
self
} else {
self.add_arg("--json-status-fd")
.add_arg(dup_fd.to_string())
}
}
pub fn launch<S: AsRef<OsStr>>(&mut self, cmd: &[S]) -> Result<()> {
if Logger::is_log_level(LogLevel::Verbose) {
self.log_command();
let s = cmd.iter().map(|s| format!("{} ", s.as_ref().to_str().unwrap())).collect::<String>();
verbose!(" {}", s)
}
self.add_args(cmd);
let err = self.command.exec();
bail!("failed to exec bubblewrap: {}", err);
}
}
#[derive(Deserialize,Clone)]
#[serde(rename_all="kebab-case")]
pub struct BubbleWrapRunningStatus {
pub child_pid: u64,
pub cgroup_namespace: u64,
pub ipc_namespace: u64,
pub mnt_namespace: u64,
pub pid_namespace: u64,
pub uts_namespace: u64,
}
#[derive(Deserialize,Clone)]
#[serde(rename_all="kebab-case")]
pub struct BubbleWrapExitStatus {
pub exit_code: u32,
}
#[derive(Clone)]
pub struct BubbleWrapStatus {
running: BubbleWrapRunningStatus,
exit: Option<BubbleWrapExitStatus>,
}
impl BubbleWrapStatus {
pub fn parse_file(path: impl AsRef<Path>) -> Result<Option<Self>> {
if !path.as_ref().exists() {
return Ok(None)
}
let s = fs::read_to_string(path)
.map_err(context!("error reading status file"))?;
let mut lines = s.lines();
let running = match lines.next() {
None => return Ok(None),
Some(s) => serde_json::from_str::<BubbleWrapRunningStatus>(s)
.map_err(context!("failed to parse status line ({})", s))?
};
let exit = match lines.next() {
None => None,
Some(s) => Some(serde_json::from_str::<BubbleWrapExitStatus>(s)
.map_err(context!("failed to parse exit line ({})", s))?)
};
Ok(Some(BubbleWrapStatus {
running,
exit
}))
}
pub fn is_running(&self) -> bool {
self.exit.is_none()
}
pub fn running_status(&self) -> &BubbleWrapRunningStatus {
&self.running
}
pub fn child_pid(&self) -> u64 {
self.running.child_pid
}
pub fn pid_namespace(&self) -> u64 {
self.running.pid_namespace
}
pub fn exit_status(&self) -> Option<&BubbleWrapExitStatus> {
self.exit.as_ref()
}
}

View File

@@ -1,259 +0,0 @@
use std::{fs, io};
use std::fs::File;
use std::os::unix::fs::FileTypeExt;
use std::os::unix::prelude::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::{Realm, Result, util};
use crate::flatpak::{BubbleWrap, BubbleWrapStatus, SANDBOX_STATUS_FILE_DIRECTORY, SandboxStatus};
use crate::flatpak::netns::NetNS;
const FLATPAK_PATH: &str = "/usr/bin/flatpak";
const ENVIRONMENT: &[&str; 7] = &[
"HOME=/home/citadel",
"USER=citadel",
"XDG_RUNTIME_DIR=/run/user/1000",
"XDG_DATA_DIRS=/home/citadel/.local/share/flatpak/exports/share:/usr/share",
"TERM=xterm-256color",
"GTK_A11Y=none",
"FLATPAK_USER_DIR=/home/citadel/realm-flatpak",
];
const FLATHUB_URL: &str = "https://dl.flathub.org/repo/flathub.flatpakrepo";
pub struct GnomeSoftwareLauncher {
realm: Realm,
status: Option<BubbleWrapStatus>,
netns: NetNS,
run_shell: bool,
}
impl GnomeSoftwareLauncher {
pub fn new(realm: Realm) -> Result<Self> {
let sandbox_status = SandboxStatus::load(SANDBOX_STATUS_FILE_DIRECTORY)?;
let status = sandbox_status.realm_status(&realm);
let netns = NetNS::new(NetNS::GS_NETNS_NAME);
Ok(GnomeSoftwareLauncher {
realm,
status,
netns,
run_shell: false,
})
}
pub fn set_run_shell(&mut self) {
self.run_shell = true;
}
fn ensure_flatpak_dir(&self) -> Result<()> {
let flatpak_user_dir = self.realm.base_path_file("flatpak");
if !flatpak_user_dir.exists() {
if let Err(err) = fs::create_dir(&flatpak_user_dir) {
bail!("failed to create realm flatpak directory ({}): {}", flatpak_user_dir.display(), err);
}
util::chown_user(&flatpak_user_dir)?;
}
Ok(())
}
fn add_flathub(&self) -> Result<()> {
let flatpak_user_dir = self.realm.base_path_file("flatpak");
match Command::new(FLATPAK_PATH)
.env("FLATPAK_USER_DIR", flatpak_user_dir)
.arg("remote-add")
.arg("--user")
.arg("--if-not-exists")
.arg("flathub")
.arg(FLATHUB_URL)
.status() {
Ok(status) => {
if status.success() {
Ok(())
} else {
bail!("failed to add flathub repo")
}
},
Err(err) => bail!("error running flatpak command: {}", err),
}
}
fn scan_tmp_directory(path: &Path) -> io::Result<Option<String>> {
for entry in fs::read_dir(&path)? {
let entry = entry?;
if entry.file_type()?.is_socket() {
if let Some(filename) = entry.path().file_name() {
if let Some(filename) = filename.to_str() {
if filename.starts_with("dbus-") {
return Ok(Some(format!("/tmp/{}", filename)));
}
}
}
}
}
Ok(None)
}
fn find_dbus_socket(&self) -> Result<String> {
let pid = self.running_pid()?;
let tmp_dir = PathBuf::from(format!("/proc/{}/root/tmp", pid));
if !tmp_dir.is_dir() {
bail!("no /tmp directory found for process pid={}", pid);
}
if let Some(s) = Self::scan_tmp_directory(&tmp_dir)
.map_err(context!("error reading directory {}", tmp_dir.display()))? {
Ok(s)
} else {
bail!("no dbus socket found in /tmp directory for process pid={}", pid);
}
}
fn launch_sandbox(&self, status_file: &File) -> Result<()> {
self.ensure_flatpak_dir()?;
if let Err(err) = self.netns.nsenter() {
bail!("Failed to enter 'gnome-software' network namespace: {}", err);
}
verbose!("Entered network namespace ({})", NetNS::GS_NETNS_NAME);
if let Err(err) = util::drop_privileges(1000, 1000) {
bail!("Failed to drop privileges to uid = gid = 1000: {}", err);
}
verbose!("Dropped privileges (uid=1000, gid=1000)");
self.add_flathub()?;
let flatpak_user_dir = self.realm.base_path_file("flatpak");
let flatpak_user_dir = flatpak_user_dir.to_str().unwrap();
let cmd = if self.run_shell { "/usr/bin/bash" } else { "/usr/bin/gnome-software"};
verbose!("Running command in sandbox: {}", cmd);
BubbleWrap::new()
.ro_bind(&[
"/usr/bin",
"/usr/lib",
"/usr/libexec",
"/usr/share/dbus-1",
"/usr/share/icons",
"/usr/share/mime",
"/usr/share/X11",
"/usr/share/glib-2.0",
"/usr/share/xml",
"/usr/share/drirc.d",
"/usr/share/fontconfig",
"/usr/share/fonts",
"/usr/share/zoneinfo",
"/usr/share/swcatalog",
"/etc/passwd",
"/etc/machine-id",
"/etc/nsswitch.conf",
"/etc/fonts",
"/etc/ssl",
"/sys/dev/char", "/sys/devices",
"/run/user/1000/wayland-0",
])
.ro_bind_to("/run/NetworkManager/resolv.conf", "/etc/resolv.conf")
.bind_to(flatpak_user_dir, "/home/citadel/realm-flatpak")
.create_symlinks(&[
("usr/lib", "/lib64"),
("usr/bin", "/bin"),
("/tmp", "/var/tmp"),
])
.create_dirs(&[
"/var/lib/flatpak",
"/home/citadel",
"/tmp",
"/sys/block", "/sys/bus", "/sys/class",
])
.mount_dev()
.dev_bind("/dev/dri")
.mount_proc()
.unshare_all()
.share_net()
.clear_env()
.set_env_list(ENVIRONMENT)
.status_file(status_file)
.launch(&["dbus-run-session", "--", cmd])?;
Ok(())
}
pub fn new_realm_status_file(&self) -> Result<File> {
let path = Path::new(SANDBOX_STATUS_FILE_DIRECTORY).join(self.realm.name());
File::create(&path)
.map_err(context!("failed to open sandbox status file {}", path.display()))
}
pub fn launch(&self) -> Result<()> {
self.netns.ensure_exists()?;
if self.is_running() {
let cmd = if self.run_shell { "/usr/bin/bash" } else { "/usr/bin/gnome-software"};
self.launch_in_running_sandbox(&[cmd])?;
} else {
let status_file = self.new_realm_status_file()?;
self.ensure_flatpak_dir()?;
self.launch_sandbox(&status_file)?;
}
Ok(())
}
pub fn quit(&self) -> Result<()> {
if self.is_running() {
self.launch_in_running_sandbox(&["/usr/bin/gnome-software", "--quit"])?;
} else {
warn!("No running sandbox found for realm {}", self.realm.name());
}
Ok(())
}
pub fn is_running(&self) -> bool {
self.status.as_ref()
.map(|s| s.is_running())
.unwrap_or(false)
}
fn running_pid(&self) -> Result<u64> {
self.status.as_ref()
.map(|s| s.child_pid())
.ok_or(format_err!("no sandbox status available for realm '{}',", self.realm.name()))
}
fn dbus_session_address(&self) -> Result<String> {
let dbus_socket = Self::find_dbus_socket(&self)?;
Ok(format!("unix:path={}", dbus_socket))
}
fn launch_in_running_sandbox(&self, command: &[&str]) -> Result<()> {
let dbus_address = self.dbus_session_address()?;
let pid = self.running_pid()?.to_string();
let mut env = ENVIRONMENT.iter()
.map(|s| s.split_once('=').unwrap())
.collect::<Vec<_>>();
env.push(("DBUS_SESSION_BUS_ADDRESS", dbus_address.as_str()));
let err = Command::new("/usr/bin/nsenter")
.env_clear()
.envs( env )
.args(&[
"--all",
"--target", pid.as_str(),
"--setuid", "1000",
"--setgid", "1000",
])
.args(command)
.exec();
Err(format_err!("failed to execute nsenter: {}", err))
}
}

View File

@@ -1,16 +0,0 @@
pub(crate) mod setup;
pub(crate) mod status;
pub(crate) mod bubblewrap;
pub(crate) mod launcher;
pub(crate) mod netns;
pub use status::SandboxStatus;
pub use bubblewrap::{BubbleWrap,BubbleWrapStatus,BubbleWrapRunningStatus,BubbleWrapExitStatus};
pub use launcher::GnomeSoftwareLauncher;
pub const SANDBOX_STATUS_FILE_DIRECTORY: &str = "/run/citadel/realms/gs-sandbox-status";

View File

@@ -1,132 +0,0 @@
use std::ffi::OsStr;
use std::path::Path;
use std::process::Command;
use crate::{util,Result};
const BRIDGE_NAME: &str = "vz-clear";
const VETH0: &str = "gs-veth0";
const VETH1: &str = "gs-veth1";
const IP_ADDRESS: &str = "172.17.0.222/24";
const GW_ADDRESS: &str = "172.17.0.1";
pub struct NetNS {
name: String,
}
impl NetNS {
pub const GS_NETNS_NAME: &'static str = "gnome-software";
pub fn new(name: &str) -> Self {
NetNS {
name: name.to_string(),
}
}
fn create(&self) -> crate::Result<()> {
Ip::new().link_add_veth(VETH0, VETH1).run()?;
Ip::new().link_set_netns(VETH0, &self.name).run()?;
Ip::new().link_set_master(VETH1, BRIDGE_NAME).run()?;
Ip::new().link_set_dev_up(VETH1).run()?;
Ip::new().ip_netns_exec_ip(&self.name).addr_add(IP_ADDRESS, VETH0).run()?;
Ip::new().ip_netns_exec_ip(&self.name).link_set_dev_up(VETH0).run()?;
Ip::new().ip_netns_exec_ip(&self.name).route_add_default(GW_ADDRESS).run()?;
Ok(())
}
pub fn ensure_exists(&self) -> Result<()> {
if Path::new(&format!("/run/netns/{}", self.name)).exists() {
verbose!("Network namespace ({}) exists", self.name);
return Ok(())
}
verbose!("Setting up network namespace ({})", self.name);
Ip::new().netns_add(&self.name).run()
.map_err(context!("Failed to add network namespace '{}'", self.name))?;
if let Err(err) = self.create() {
Ip::new().netns_delete(&self.name).run()?;
Err(err)
} else {
Ok(())
}
}
pub fn nsenter(&self) -> Result<()> {
util::nsenter_netns(&self.name)
}
}
const IP_PATH: &str = "/usr/sbin/ip";
struct Ip {
command: Command,
}
impl Ip {
fn new() -> Self {
let mut command = Command::new(IP_PATH);
command.env_clear();
Ip { command }
}
fn add_args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
for arg in args {
self.command.arg(arg);
}
self
}
pub fn netns_add(&mut self, name: &str) -> &mut Self {
self.add_args(&["netns", "add", name])
}
pub fn netns_delete(&mut self, name: &str) -> &mut Self {
self.add_args(&["netns", "delete", name])
}
pub fn link_add_veth(&mut self, name: &str, peer_name: &str) -> &mut Self {
self.add_args(&["link", "add", name, "type", "veth", "peer", "name", peer_name])
}
pub fn link_set_netns(&mut self, iface: &str, netns_name: &str) -> &mut Self {
self.add_args(&["link", "set", iface, "netns", netns_name])
}
pub fn link_set_master(&mut self, iface: &str, bridge_name: &str) -> &mut Self {
self.add_args(&["link", "set", iface, "master", bridge_name])
}
pub fn link_set_dev_up(&mut self, iface: &str) -> &mut Self {
self.add_args(&["link", "set", "dev", iface, "up"])
}
pub fn ip_netns_exec_ip(&mut self, netns_name: &str) -> &mut Self {
self.add_args(&["netns", "exec", netns_name, IP_PATH])
}
pub fn addr_add(&mut self, ip_address: &str, dev: &str) -> &mut Self {
self.add_args(&["addr", "add", ip_address, "dev", dev])
}
pub fn route_add_default(&mut self, gateway: &str) -> &mut Self {
self.add_args(&["route", "add", "default", "via", gateway])
}
fn run(&mut self) -> crate::Result<()> {
verbose!("{:?}", self.command);
match self.command.status() {
Ok(status) => {
if status.success() {
Ok(())
} else {
bail!("IP command ({:?}) did not succeeed.", self.command);
}
}
Err(err) => {
bail!("error running ip command ({:?}): {}", self.command, err);
}
}
}
}

View File

@@ -1,58 +0,0 @@
use std::path::Path;
use crate::{Realm, Result, util};
const GNOME_SOFTWARE_DESKTOP_TEMPLATE: &str = "\
[Desktop Entry]
Name=Software
Comment=Add, remove or update software on this computer
Icon=org.gnome.Software
Exec=/usr/libexec/launch-gnome-software --realm $REALM_NAME
Terminal=false
Type=Application
Categories=GNOME;GTK;System;PackageManager;
Keywords=Updates;Upgrade;Sources;Repositories;Preferences;Install;Uninstall;Program;Software;App;Store;
StartupNotify=true
";
const APPLICATION_DIRECTORY: &str = "/home/citadel/.local/share/applications";
pub struct FlatpakSetup<'a> {
realm: &'a Realm,
}
impl <'a> FlatpakSetup<'a> {
pub fn new(realm: &'a Realm) -> Self {
Self { realm }
}
pub fn setup(&self) -> Result<()> {
self.write_desktop_file()?;
self.ensure_flatpak_directory()?;
Ok(())
}
fn write_desktop_file(&self) -> Result<()> {
let appdir = Path::new(APPLICATION_DIRECTORY);
if !appdir.exists() {
util::create_dir(appdir)?;
if let Some(parent) = appdir.parent().and_then(|p| p.parent()) {
util::chown_tree(parent, (1000,1000), true)?;
}
}
let path = appdir.join(format!("realm-{}.org.gnome.Software.desktop", self.realm.name()));
util::write_file(path, GNOME_SOFTWARE_DESKTOP_TEMPLATE.replace("$REALM_NAME", self.realm.name()))?;
Ok(())
}
fn ensure_flatpak_directory(&self) -> Result<()> {
let path = self.realm.base_path_file("flatpak");
if !path.exists() {
util::create_dir(&path)?;
util::chown_user(&path)?;
}
Ok(())
}
}

View File

@@ -1,144 +0,0 @@
use std::collections::HashMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use crate::flatpak::bubblewrap::BubbleWrapStatus;
use crate::{Realm, Result, util};
/// Utility function to read modified time from a path.
fn modified_time(path: &Path) -> Result<SystemTime> {
path.metadata().and_then(|meta| meta.modified())
.map_err(context!("failed to read modified time from '{}'", path.display()))
}
/// Utility function to detect if current modified time of a path
/// matches an earlier recorded modified time.
fn modified_changed(path: &Path, old_modified: SystemTime) -> bool {
if !path.exists() {
// Path existed at some earlier point, so something changed
return true;
}
match modified_time(path) {
Ok(modified) => old_modified != modified,
Err(err) => {
// Print a warning but assume change
warn!("{}", err);
true
},
}
}
/// Records the content of single entry in a sandbox status directory.
///
/// The path to the status file as well as the last modified time are
/// recorded so that changes in status of a sandbox can be detected.
struct StatusEntry {
status: BubbleWrapStatus,
path: PathBuf,
modified: SystemTime,
}
impl StatusEntry {
fn load_timestamp_and_status(path: &Path) -> Result<Option<(SystemTime, BubbleWrapStatus)>> {
if path.exists() {
let modified = modified_time(path)?;
if let Some(status) = BubbleWrapStatus::parse_file(path)? {
return Ok(Some((modified, status)));
}
}
Ok(None)
}
fn load(base_dir: &Path, name: &str) -> Result<Option<Self>> {
let path = base_dir.join(name);
let result = StatusEntry::load_timestamp_and_status(&path)?
.map(|(modified, status)| StatusEntry { status, path, modified });
Ok(result)
}
fn is_modified(&self) -> bool {
modified_changed(&self.path, self.modified)
}
}
/// Holds information about entries in a sandbox status directory.
///
/// Bubblewrap accepts a command line argument that asks for status
/// information to be written as a json structure to a file descriptor.
///
pub struct SandboxStatus {
base_dir: PathBuf,
base_modified: SystemTime,
entries: HashMap<String, StatusEntry>,
}
impl SandboxStatus {
pub fn need_reload(&self) -> bool {
if modified_changed(&self.base_dir, self.base_modified) {
return true;
}
self.entries.values().any(|entry| entry.is_modified())
}
fn process_dir_entry(&mut self, dir_entry: PathBuf) -> Result<()> {
fn realm_name_for_path(path: &Path) -> Option<&str> {
path.file_name()
.and_then(|name| name.to_str())
.filter(|name| Realm::is_valid_name(name))
}
if dir_entry.is_file() {
if let Some(name) = realm_name_for_path(&dir_entry) {
if let Some(entry) = StatusEntry::load(&self.base_dir, name)? {
self.entries.insert(name.to_string(), entry);
}
}
}
Ok(())
}
pub fn reload(&mut self) -> Result<()> {
self.entries.clear();
self.base_modified = modified_time(&self.base_dir)?;
let base_dir = self.base_dir.clone();
util::read_directory(&base_dir, |entry| {
self.process_dir_entry(entry.path())
})
}
fn new(base_dir: &Path) -> Result<Self> {
let base_dir = base_dir.to_owned();
let base_modified = modified_time(&base_dir)?;
Ok(SandboxStatus {
base_dir,
base_modified,
entries: HashMap::new(),
})
}
pub fn load(directory: impl AsRef<Path>) -> Result<SandboxStatus> {
let base_dir = directory.as_ref();
if !base_dir.exists() {
util::create_dir(base_dir)?;
}
let mut status = SandboxStatus::new(base_dir)?;
status.reload()?;
Ok(status)
}
pub fn realm_status(&self, realm: &Realm) -> Option<BubbleWrapStatus> {
self.entries.get(realm.name()).map(|entry| entry.status.clone())
}
pub fn new_realm_status_file(&self, realm: &Realm) -> Result<File> {
let path = self.base_dir.join(realm.name());
File::create(&path)
.map_err(context!("failed to open sandbox status file {}", path.display()))
}
}

View File

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

View File

@@ -20,9 +20,6 @@ pub mod symlink;
mod realm;
pub mod terminal;
mod system;
pub mod updates;
pub mod flatpak;
pub use crate::config::OsRelease;
pub use crate::blockdev::BlockDev;

View File

@@ -62,11 +62,6 @@ impl Logger {
logger.level = level;
}
pub fn is_log_level(level: LogLevel) -> bool {
let logger = LOGGER.lock().unwrap();
logger.level >= level
}
pub fn set_log_output(output: Box<dyn LogOutput>) {
let mut logger = LOGGER.lock().unwrap();
logger.output = output;

View File

@@ -77,9 +77,6 @@ pub struct RealmConfig {
#[serde(rename="use-fuse")]
pub use_fuse: Option<bool>,
#[serde(rename="use-flatpak")]
pub use_flatpak: Option<bool>,
#[serde(rename="use-gpu")]
pub use_gpu: Option<bool>,
@@ -204,7 +201,6 @@ impl RealmConfig {
wayland_socket: Some("wayland-0".to_string()),
use_kvm: Some(false),
use_fuse: Some(false),
use_flatpak: Some(false),
use_gpu: Some(false),
use_gpu_card0: Some(false),
use_network: Some(true),
@@ -237,7 +233,6 @@ impl RealmConfig {
wayland_socket: None,
use_kvm: None,
use_fuse: None,
use_flatpak: None,
use_gpu: None,
use_gpu_card0: None,
use_network: None,
@@ -272,14 +267,6 @@ impl RealmConfig {
self.bool_value(|c| c.use_fuse)
}
/// If `true` flatpak directory will be mounted into realm
/// and a desktop file will be created to launch gnome-software
///
pub fn flatpak(&self) -> bool {
self.bool_value(|c| c.use_flatpak)
}
/// If `true` render node device /dev/dri/renderD128 will be added to realm.
///
/// This enables hardware graphics acceleration in realm.

View File

@@ -60,7 +60,7 @@ impl <'a> RealmLauncher <'a> {
if config.kvm() {
self.add_device("/dev/kvm");
}
if config.fuse() || config.flatpak() {
if config.fuse() {
self.add_device("/dev/fuse");
}
@@ -153,10 +153,6 @@ impl <'a> RealmLauncher <'a> {
writeln!(s, "BindReadOnly=/run/user/1000/{}:/run/user/host/wayland-0", config.wayland_socket())?;
}
if config.flatpak() {
writeln!(s, "BindReadOnly={}:/var/lib/flatpak", self.realm.base_path_file("flatpak").display())?;
}
for bind in config.extra_bindmounts() {
if Self::is_valid_bind_item(bind) {
writeln!(s, "Bind={}", bind)?;

View File

@@ -4,8 +4,6 @@ use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use posix_acl::{ACL_EXECUTE, ACL_READ, PosixACL, Qualifier};
use crate::{Mountpoint, Result, Realms, RealmFS, Realm, util};
use crate::flatpak::GnomeSoftwareLauncher;
use crate::flatpak::setup::FlatpakSetup;
use crate::realm::pidmapper::{PidLookupResult, PidMapper};
use crate::realmfs::realmfs_set::RealmFSSet;
@@ -23,7 +21,6 @@ struct Inner {
events: RealmEventListener,
realms: Realms,
realmfs_set: RealmFSSet,
pid_mapper: PidMapper,
}
impl Inner {
@@ -31,8 +28,7 @@ impl Inner {
let events = RealmEventListener::new();
let realms = Realms::load()?;
let realmfs_set = RealmFSSet::load()?;
let pid_mapper = PidMapper::new()?;
Ok(Inner { events, realms, realmfs_set, pid_mapper })
Ok(Inner { events, realms, realmfs_set })
}
}
@@ -234,10 +230,6 @@ impl RealmManager {
self.ensure_run_media_directory()?;
}
if realm.config().flatpak() {
FlatpakSetup::new(realm).setup()?;
}
self.systemd.start_realm(realm, &rootfs)?;
self.create_realm_namefile(realm)?;
@@ -276,15 +268,6 @@ impl RealmManager {
self.run_in_realm(realm, &["/usr/bin/ln", "-s", "/run/user/host/wayland-0", "/run/user/1000/wayland-0"], false)
}
fn stop_gnome_software_sandbox(&self, realm: &Realm) -> Result<()> {
let launcher = GnomeSoftwareLauncher::new(realm.clone())?;
if launcher.is_running() {
info!("Stopping GNOME Software sandbox for {}", realm.name());
launcher.quit()?;
}
Ok(())
}
pub fn stop_realm(&self, realm: &Realm) -> Result<()> {
if !realm.is_active() {
info!("ignoring stop request on realm '{}' which is not running", realm.name());
@@ -293,12 +276,6 @@ impl RealmManager {
info!("Stopping realm {}", realm.name());
if realm.config().flatpak() {
if let Err(err) = self.stop_gnome_software_sandbox(realm) {
warn!("Error stopping GNOME Software sandbox: {}", err);
}
}
realm.set_active(false);
self.systemd.stop_realm(realm)?;
realm.cleanup_rootfs();
@@ -358,8 +335,8 @@ impl RealmManager {
}
pub fn realm_by_pid(&self, pid: u32) -> PidLookupResult {
let realms = self.realm_list();
self.inner_mut().pid_mapper.lookup_pid(pid as libc::pid_t, realms)
let mapper = PidMapper::new(self.active_realms(false));
mapper.lookup_pid(pid as libc::pid_t)
}
pub fn rescan_realms(&self) -> Result<(Vec<Realm>,Vec<Realm>)> {

View File

@@ -1,7 +1,6 @@
use std::ffi::OsStr;
use procfs::process::Process;
use crate::{Result, Realm};
use crate::flatpak::{SANDBOX_STATUS_FILE_DIRECTORY, SandboxStatus};
use crate::Realm;
pub enum PidLookupResult {
Unknown,
@@ -10,15 +9,14 @@ pub enum PidLookupResult {
}
pub struct PidMapper {
sandbox_status: SandboxStatus,
active_realms: Vec<Realm>,
my_pid_ns_id: Option<u64>,
}
impl PidMapper {
pub fn new() -> Result<Self> {
let sandbox_status = SandboxStatus::load(SANDBOX_STATUS_FILE_DIRECTORY)?;
pub fn new(active_realms: Vec<Realm>) -> Self {
let my_pid_ns_id = Self::self_pid_namespace_id();
Ok(PidMapper { sandbox_status, my_pid_ns_id })
PidMapper { active_realms, my_pid_ns_id }
}
fn read_process(pid: libc::pid_t) -> Option<Process> {
@@ -74,30 +72,7 @@ impl PidMapper {
Self::read_process(ppid)
}
fn refresh_sandbox_status(&mut self) -> Result<()> {
if self.sandbox_status.need_reload() {
self.sandbox_status.reload()?;
}
Ok(())
}
fn search_sandbox_realms(&mut self, pid_ns: u64, realms: &[Realm]) -> Option<Realm> {
if let Err(err) = self.refresh_sandbox_status() {
warn!("error reloading sandbox status directory: {}", err);
return None;
}
for r in realms {
if let Some(status) = self.sandbox_status.realm_status(r) {
if status.pid_namespace() == pid_ns {
return Some(r.clone())
}
}
}
None
}
pub fn lookup_pid(&mut self, pid: libc::pid_t, realms: Vec<Realm>) -> PidLookupResult {
pub fn lookup_pid(&self, pid: libc::pid_t) -> PidLookupResult {
const MAX_PARENT_SEARCH: i32 = 8;
let mut n = 0;
@@ -117,17 +92,13 @@ impl PidMapper {
return PidLookupResult::Citadel;
}
if let Some(realm) = realms.iter()
.find(|r| r.is_active() && r.has_pid_ns(pid_ns_id))
if let Some(realm) = self.active_realms.iter()
.find(|r| r.has_pid_ns(pid_ns_id))
.cloned()
{
return PidLookupResult::Realm(realm)
}
if let Some(r) = self.search_sandbox_realms(pid_ns_id, &realms) {
return PidLookupResult::Realm(r)
}
proc = match Self::parent_process(proc) {
Some(proc) => proc,
None => return PidLookupResult::Unknown,
@@ -137,4 +108,5 @@ impl PidMapper {
}
PidLookupResult::Unknown
}
}

View File

@@ -279,9 +279,6 @@ impl Realm {
symlink::write(&rootfs, self.rootfs_symlink(), false)?;
symlink::write(mountpoint.path(), self.realmfs_mountpoint_symlink(), false)?;
symlink::write(self.base_path().join("home"), self.run_path().join("home"), false)?;
if self.config().flatpak() {
symlink::write(self.base_path().join("flatpak"), self.run_path().join("flatpak"), false)?;
}
Ok(rootfs)
}
@@ -303,9 +300,6 @@ impl Realm {
Self::remove_symlink(self.realmfs_mountpoint_symlink());
Self::remove_symlink(self.rootfs_symlink());
Self::remove_symlink(self.run_path().join("home"));
if self.config().flatpak() {
Self::remove_symlink(self.run_path().join("flatpak"));
}
if let Err(e) = fs::remove_dir(self.run_path()) {
warn!("failed to remove run directory {}: {}", self.run_path().display(), e);

View File

@@ -31,28 +31,6 @@ impl Systemd {
if realm.config().ephemeral_home() {
self.setup_ephemeral_home(realm)?;
}
if realm.config().flatpak() {
self.setup_flatpak_workaround(realm)?;
}
Ok(())
}
// What even is this??
//
// Good question.
//
// https://bugzilla.redhat.com/show_bug.cgi?id=2210335#c10
//
fn setup_flatpak_workaround(&self, realm: &Realm) -> Result<()> {
let commands = &[
vec!["/usr/bin/mount", "-m", "-t","proc", "proc", "/run/flatpak-workaround/proc"],
vec!["/usr/bin/chmod", "700", "/run/flatpak-workaround"],
];
for cmd in commands {
Self::machinectl_shell(realm, cmd, "root", false, true)?;
}
Ok(())
}

View File

@@ -23,8 +23,8 @@ impl ResizeSize {
pub fn gigs(n: usize) -> Self {
ResizeSize(BLOCKS_PER_GIG * n)
}
}
pub fn megs(n: usize) -> Self {
ResizeSize(BLOCKS_PER_MEG * n)
}
@@ -45,8 +45,8 @@ impl ResizeSize {
self.0 / BLOCKS_PER_MEG
}
/// If the RealmFS has less than `AUTO_RESIZE_MINIMUM_FREE` blocks free then choose a new
/// size to resize the filesystem to and return it. Otherwise, return `None`
/// If the RealmFS needs to be resized to a larger size, returns the
/// recommended size.
pub fn auto_resize_size(realmfs: &RealmFS) -> Option<ResizeSize> {
let sb = match Superblock::load(realmfs.path(), 4096) {
Ok(sb) => sb,
@@ -56,37 +56,22 @@ impl ResizeSize {
},
};
sb.free_block_count();
let free_blocks = sb.free_block_count() as usize;
if free_blocks >= AUTO_RESIZE_MINIMUM_FREE.nblocks() {
return None;
}
let metainfo_nblocks = realmfs.metainfo().nblocks();
if metainfo_nblocks >= AUTO_RESIZE_INCREASE_SIZE.nblocks() {
return Some(ResizeSize::blocks(metainfo_nblocks + AUTO_RESIZE_INCREASE_SIZE.nblocks()))
}
// If current size is under 4GB (AUTO_RESIZE_INCREASE_SIZE) and raising size to 4GB will create more than the
// minimum free space (1GB) then just do that.
if free_blocks + (AUTO_RESIZE_INCREASE_SIZE.nblocks() - metainfo_nblocks) >= AUTO_RESIZE_MINIMUM_FREE.nblocks() {
Some(AUTO_RESIZE_INCREASE_SIZE)
if free_blocks < AUTO_RESIZE_MINIMUM_FREE.nblocks() {
let metainfo_nblocks = realmfs.metainfo().nblocks() + 1;
let increase_multiple = metainfo_nblocks / AUTO_RESIZE_INCREASE_SIZE.nblocks();
let grow_size = (increase_multiple + 1) * AUTO_RESIZE_INCREASE_SIZE.nblocks();
let mask = grow_size - 1;
let grow_blocks = (free_blocks + mask) & !mask;
Some(ResizeSize::blocks(grow_blocks))
} else {
// Otherwise for original size under 4GB, since raising to 4GB is not enough,
// raise size to 8GB
Some(ResizeSize::blocks(AUTO_RESIZE_INCREASE_SIZE.nblocks() * 2))
None
}
}
}
const SUPERBLOCK_SIZE: usize = 1024;
/// An EXT4 superblock structure.
///
/// A class for reading the first superblock from an EXT4 filesystem
/// and parsing the Free Block Count field. No other fields are parsed
/// since this is the only information needed for the resize operation.
///
pub struct Superblock([u8; SUPERBLOCK_SIZE]);
impl Superblock {

View File

@@ -76,6 +76,8 @@ impl <'a> Update<'a> {
}
self.realmfs.copy_image_file(self.target())?;
self.truncate_verity()?;
self.resize_image_file()?;
Ok(())
}
@@ -113,8 +115,9 @@ impl <'a> Update<'a> {
}
// Return size of image file in blocks based on metainfo `nblocks` field.
// Include header block in count so add one block
fn metainfo_nblock_size(&self) -> usize {
self.realmfs.metainfo().nblocks()
self.realmfs.metainfo().nblocks() + 1
}
fn unmount_update_image(&mut self) {
@@ -156,8 +159,7 @@ impl <'a> Update<'a> {
}
fn set_target_len(&self, nblocks: usize) -> Result<()> {
// add one block for header block
let len = ((nblocks + 1) * BLOCK_SIZE) as u64;
let len = (nblocks * BLOCK_SIZE) as u64;
let f = fs::OpenOptions::new()
.write(true)
.open(&self.target)
@@ -174,7 +176,7 @@ impl <'a> Update<'a> {
if self.realmfs.header().has_flag(ImageHeader::FLAG_HASH_TREE) {
self.set_target_len(metainfo_nblocks)?;
} else if file_nblocks > (metainfo_nblocks + 1) {
} else if file_nblocks > metainfo_nblocks {
warn!("RealmFS image size was greater than length indicated by metainfo.nblocks but FLAG_HASH_TREE not set");
}
Ok(())
@@ -183,7 +185,7 @@ impl <'a> Update<'a> {
// If resize was requested, adjust size of update copy of image file.
fn resize_image_file(&self) -> Result<()> {
let nblocks = match self.resize {
Some(rs) => rs.nblocks(),
Some(rs) => rs.nblocks() + 1,
None => return Ok(()),
};
@@ -222,7 +224,7 @@ impl <'a> Update<'a> {
fn seal(&mut self) -> Result<()> {
let nblocks = match self.resize {
Some(rs) => rs.nblocks(),
None => self.metainfo_nblock_size(),
None => self.metainfo_nblock_size() - 1,
};
let salt = hex::encode(randombytes(32));
@@ -230,11 +232,20 @@ impl <'a> Update<'a> {
.map_err(context!("failed to create verity context for realmfs update image {:?}", self.target()))?;
let output = verity.generate_image_hashtree_with_salt(&salt, nblocks)
.map_err(context!("failed to generate dm-verity hashtree for realmfs update image {:?}", self.target()))?;
// XXX passes metainfo for nblocks
//let output = Verity::new(&self.target).generate_image_hashtree_with_salt(&self.realmfs.metainfo(), &salt)?;
let root_hash = output.root_hash()
.ok_or_else(|| format_err!("no root hash returned from verity format operation"))?;
info!("root hash is {}", output.root_hash().unwrap());
/*
let nblocks = match self.resize {
Some(rs) => rs.nblocks(),
None => self.metainfo_nblock_size() - 1,
};
*/
info!("Signing new image with user realmfs keys");
let metainfo_bytes = RealmFS::generate_metainfo(self.realmfs.name(), nblocks, salt.as_str(), root_hash);
let keys = self.realmfs.sealing_keys().expect("No sealing keys");

View File

@@ -40,11 +40,6 @@ impl ResourceImage {
pub fn find(image_type: &str) -> Result<Self> {
let channel = Self::rootfs_channel();
// search when citadel is installed
if let Some(image) = search_directory(format!("/storage/resources/{channel}/"), image_type, Some(&channel))? {
return Ok(image);
}
info!("Searching run directory for image {} with channel {}", image_type, channel);
if let Some(image) = search_directory(RUN_DIRECTORY, image_type, Some(&channel))? {
@@ -358,11 +353,6 @@ impl ResourceImage {
if Mounts::is_source_mounted("/dev/mapper/citadel-storage")? {
return Ok(true);
}
if Mounts::is_source_mounted("/storage")? {
return Ok(true);
}
let path = Path::new("/dev/mapper/citadel-storage");
if !path.exists() {
return Ok(false);
@@ -430,11 +420,8 @@ fn compare_images(a: Option<ResourceImage>, b: ResourceImage) -> Result<Resource
None => return Ok(b),
};
let bind_a = a.metainfo();
let bind_b = b.metainfo();
let ver_a = bind_a.version();
let ver_b = bind_b.version();
let ver_a = a.metainfo().version();
let ver_b = b.metainfo().version();
if ver_a > ver_b {
Ok(a)
@@ -534,14 +521,8 @@ fn maybe_add_dir_entry(entry: &DirEntry,
return Ok(())
}
if kernel_id.is_some() {
if image_type == "kernel" && (metainfo.kernel_version() != kernel_version || metainfo.kernel_id() != kernel_id) {
return Ok(());
}
} else { // in live mode, kernel_id is None
if image_type == "kernel" && metainfo.kernel_version() != kernel_version {
return Ok(());
}
if image_type == "kernel" && (metainfo.kernel_version() != kernel_version || metainfo.kernel_id() != kernel_id) {
return Ok(());
}
images.push(ResourceImage::new(&path, header));

View File

@@ -15,12 +15,11 @@ impl Mounts {
pub fn is_source_mounted<P: AsRef<Path>>(path: P) -> Result<bool> {
let path = path.as_ref();
for i in Self::load()?.mounts() {
if i.line.contains(&path.display().to_string()) {
return Ok(true)
}
}
Ok(false)
let mounted = Self::load()?
.mounts()
.any(|m| m.source_path() == path);
Ok(mounted)
}
pub fn is_target_mounted<P: AsRef<Path>>(path: P) -> Result<bool> {

View File

@@ -1,97 +0,0 @@
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 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"),
}
}
}

View File

@@ -7,7 +7,6 @@ use std::env;
use std::fs::{self, File, DirEntry};
use std::ffi::CString;
use std::io::{self, Seek, Read, BufReader, SeekFrom};
use std::os::fd::AsRawFd;
use std::time::{SystemTime, UNIX_EPOCH};
use walkdir::WalkDir;
@@ -48,12 +47,6 @@ fn search_path(filename: &str) -> Result<PathBuf> {
bail!("could not find {} in $PATH", filename)
}
pub fn append_to_path(p: &Path, s: &str) -> PathBuf {
let mut p_osstr = p.as_os_str().to_owned();
p_osstr.push(s);
p_osstr.into()
}
pub fn ensure_command_exists(cmd: &str) -> Result<()> {
let path = Path::new(cmd);
if !path.is_absolute() {
@@ -224,8 +217,7 @@ where
///
pub fn remove_file(path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref();
let is_symlink = fs::symlink_metadata(path).is_ok();
if is_symlink || path.exists() {
if path.exists() {
fs::remove_file(path)
.map_err(context!("failed to remove file {:?}", path))?;
}
@@ -344,6 +336,7 @@ 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");
@@ -375,38 +368,9 @@ pub fn touch_mtime(path: &Path) -> Result<()> {
utimes(path, meta.atime(),mtime)?;
Ok(())
}
pub fn nsenter_netns(netns: &str) -> Result<()> {
let mut path = PathBuf::from("/run/netns");
path.push(netns);
if !path.exists() {
bail!("Network namespace '{}' does not exist", netns);
}
let f = File::open(&path)
.map_err(context!("error opening netns file {}", path.display()))?;
let fd = f.as_raw_fd();
unsafe {
if libc::setns(fd, libc::CLONE_NEWNET) == -1 {
let err = io::Error::last_os_error();
bail!("failed to setns() into network namespace '{}': {}", netns, err);
}
}
Ok(())
}
pub fn drop_privileges(uid: u32, gid: u32) -> Result<()> {
unsafe {
if libc::setgid(gid) == -1 {
let err = io::Error::last_os_error();
bail!("failed to call setgid({}): {}", gid, err);
} else if libc::setuid(uid) == -1 {
let err = io::Error::last_os_error();
bail!("failed to call setuid({}): {}", uid, err);
}
}
Ok(())
}

View File

@@ -4,6 +4,6 @@ StartLimitIntervalSec=0
[Path]
PathChanged=/run/citadel/realms/current/current.realm/rootfs/usr/share/applications
PathChanged=/run/citadel/realms/current/current.realm/flatpak/exports/share/applications
PathChanged=/run/citadel/realms/current/current.realm/rootfs/var/lib/flatpak/exports/share/applications
PathChanged=/run/citadel/realms/current/current.realm/home/.local/share/applications
PathChanged=/run/citadel/realms/current/current.realm/home/.local/share/flatpak/exports/share/applications

View File

@@ -1,529 +0,0 @@
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 = "Perform tasks needed to create an update and publish it")]
#[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(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()),
)),
)
}