diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 5cc475b75..63bded04b 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -5,7 +5,7 @@ use std::path::Path; use anyhow::{anyhow, Context, Result}; use bootc_blockdev::find_parent_devices; -use bootc_kernel_cmdline::utf8::Cmdline; +use bootc_kernel_cmdline::utf8::{Cmdline, Parameter}; use bootc_mount::inspect_filesystem_of_dir; use bootc_mount::tempmount::TempMount; use camino::{Utf8Path, Utf8PathBuf}; @@ -33,6 +33,7 @@ use rustix::{mount::MountFlags, path::Arg}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::bootc_kargs::kargs_from_composefs_filesystem; use crate::composefs_consts::{TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED}; use crate::parsers::bls_config::{BLSConfig, BLSConfigType}; use crate::parsers::grub_menuconfig::MenuEntry; @@ -51,7 +52,6 @@ use crate::{ BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST, STAGED_BOOT_LOADER_ENTRIES, STATE_DIR_ABS, USER_CFG, USER_CFG_STAGED, }, - install::RW_KARG, spec::{Bootloader, Host}, }; @@ -384,7 +384,7 @@ pub(crate) fn setup_composefs_bls_boot( ) -> Result { let id_hex = id.to_hex(); - let (root_path, esp_device, cmdline_refs, fs, bootloader) = match setup_type { + let (root_path, esp_device, mut cmdline_refs, fs, bootloader) = match setup_type { BootSetupType::Setup((root_setup, state, postfetch, fs)) => { // root_setup.kargs has [root=UUID=, "rw"] let mut cmdline_options = Cmdline::new(); @@ -415,10 +415,30 @@ pub(crate) fn setup_composefs_bls_boot( let sysroot_parent = get_sysroot_parent_dev(&storage.physical_root)?; let bootloader = host.require_composefs_booted()?.bootloader.clone(); + let boot_dir = storage.require_boot_dir()?; + let current_cfg = get_booted_bls(&boot_dir)?; + + let mut cmdline = match current_cfg.cfg_type { + BLSConfigType::NonEFI { options, .. } => { + let options = options + .ok_or_else(|| anyhow::anyhow!("No 'options' found in BLS Config"))?; + + Cmdline::from(options) + } + + _ => anyhow::bail!("Found NonEFI config"), + }; + + // Copy all cmdline args, replacing only `composefs=` + let param = format!("{COMPOSEFS_CMDLINE}={id_hex}"); + let param = + Parameter::parse(¶m).context("Failed to create 'composefs=' parameter")?; + cmdline.add_or_modify(¶m); + ( Utf8PathBuf::from("/sysroot"), get_esp_partition(&sysroot_parent)?.0, - Cmdline::from(format!("{RW_KARG} {COMPOSEFS_CMDLINE}={id_hex}")), + cmdline, fs, bootloader, ) @@ -427,6 +447,8 @@ pub(crate) fn setup_composefs_bls_boot( let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..)); + kargs_from_composefs_filesystem(fs, &repo, &mut cmdline_refs, !is_upgrade)?; + let (entry_paths, _tmpdir_guard) = match bootloader { Bootloader::Grub => { let root = Dir::open_ambient_dir(&root_path, ambient_authority()) diff --git a/crates/lib/src/bootc_composefs/delete.rs b/crates/lib/src/bootc_composefs/delete.rs index b16930e83..f1a31101c 100644 --- a/crates/lib/src/bootc_composefs/delete.rs +++ b/crates/lib/src/bootc_composefs/delete.rs @@ -7,10 +7,7 @@ use composefs_boot::bootloader::{EFI_ADDON_DIR_EXT, EFI_EXT}; use crate::{ bootc_composefs::{ - boot::{ - find_vmlinuz_initrd_duplicates, get_efi_uuid_source, get_esp_partition, - get_sysroot_parent_dev, mount_esp, BootType, SYSTEMD_UKI_DIR, - }, + boot::{find_vmlinuz_initrd_duplicates, get_efi_uuid_source, BootType, SYSTEMD_UKI_DIR}, gc::composefs_gc, repo::open_composefs_repo, rollback::{composefs_rollback, rename_exchange_user_cfg}, @@ -215,40 +212,34 @@ fn remove_grub_menucfg_entry(id: &str, boot_dir: &Dir, deleting_staged: bool) -> #[fn_error_context::context("Deleting boot entries for deployment {}", deployment.deployment.verity)] fn delete_depl_boot_entries( deployment: &DeploymentEntry, - physical_root: &Dir, + storage: &Storage, deleting_staged: bool, ) -> Result<()> { - match deployment.deployment.bootloader { - Bootloader::Grub => { - let boot_dir = physical_root.open_dir("boot").context("Opening boot dir")?; + let boot_dir = storage.require_boot_dir()?; - match deployment.deployment.boot_type { - BootType::Bls => delete_type1_entry(deployment, &boot_dir, deleting_staged), + match deployment.deployment.bootloader { + Bootloader::Grub => match deployment.deployment.boot_type { + BootType::Bls => delete_type1_entry(deployment, boot_dir, deleting_staged), - BootType::Uki => { - let device = get_sysroot_parent_dev(physical_root)?; - let (esp_part, ..) = get_esp_partition(&device)?; - let esp_mount = mount_esp(&esp_part)?; + BootType::Uki => { + let esp = storage + .esp + .as_ref() + .ok_or_else(|| anyhow::anyhow!("ESP not found"))?; - remove_grub_menucfg_entry( - &deployment.deployment.verity, - &boot_dir, - deleting_staged, - )?; + remove_grub_menucfg_entry( + &deployment.deployment.verity, + boot_dir, + deleting_staged, + )?; - delete_uki(&deployment.deployment.verity, &esp_mount.fd) - } + delete_uki(&deployment.deployment.verity, &esp.fd) } - } + }, Bootloader::Systemd => { - let device = get_sysroot_parent_dev(physical_root)?; - let (esp_part, ..) = get_esp_partition(&device)?; - - let esp_mount = mount_esp(&esp_part)?; - // For Systemd UKI as well, we use .conf files - delete_type1_entry(deployment, &esp_mount.fd, deleting_staged) + delete_type1_entry(deployment, boot_dir, deleting_staged) } } } @@ -362,7 +353,7 @@ pub(crate) async fn delete_composefs_deployment( tracing::info!("Deleting {kind}deployment '{deployment_id}'"); - delete_depl_boot_entries(&depl_to_del, &storage.physical_root, deleting_staged)?; + delete_depl_boot_entries(&depl_to_del, &storage, deleting_staged)?; composefs_gc(storage, booted_cfs).await?; diff --git a/crates/lib/src/bootc_composefs/finalize.rs b/crates/lib/src/bootc_composefs/finalize.rs index d397c9f5c..027ffb5ee 100644 --- a/crates/lib/src/bootc_composefs/finalize.rs +++ b/crates/lib/src/bootc_composefs/finalize.rs @@ -1,8 +1,6 @@ use std::path::Path; -use crate::bootc_composefs::boot::{ - get_esp_partition, get_sysroot_parent_dev, mount_esp, BootType, -}; +use crate::bootc_composefs::boot::BootType; use crate::bootc_composefs::rollback::{rename_exchange_bls_entries, rename_exchange_user_cfg}; use crate::bootc_composefs::status::get_composefs_status; use crate::composefs_consts::STATE_DIR_ABS; @@ -86,15 +84,12 @@ pub(crate) async fn composefs_backend_finalize( // Unmount EROFS drop(erofs_tmp_mnt); - let sysroot_parent = get_sysroot_parent_dev(&storage.physical_root)?; - // NOTE: Assumption here that ESP will always be present - let (esp_part, ..) = get_esp_partition(&sysroot_parent)?; + let boot_dir = storage.require_boot_dir()?; - let esp_mount = mount_esp(&esp_part)?; - let boot_dir = storage - .physical_root - .open_dir("boot") - .context("Opening boot")?; + let esp_mount = storage + .esp + .as_ref() + .ok_or_else(|| anyhow::anyhow!("ESP not found"))?; // NOTE: Assuming here we won't have two bootloaders at the same time match booted_composefs.bootloader { @@ -103,21 +98,17 @@ pub(crate) async fn composefs_backend_finalize( let entries_dir = boot_dir.open_dir("loader")?; rename_exchange_bls_entries(&entries_dir)?; } - BootType::Uki => finalize_staged_grub_uki(&esp_mount.fd, &boot_dir)?, + BootType::Uki => finalize_staged_grub_uki(&esp_mount.fd, boot_dir)?, }, - Bootloader::Systemd => match staged_composefs.boot_type { - BootType::Bls => { - let entries_dir = esp_mount.fd.open_dir("loader")?; - rename_exchange_bls_entries(&entries_dir)?; - } - BootType::Uki => { + Bootloader::Systemd => { + if matches!(staged_composefs.boot_type, BootType::Uki) { rename_staged_uki_entries(&esp_mount.fd)?; - - let entries_dir = esp_mount.fd.open_dir("loader")?; - rename_exchange_bls_entries(&entries_dir)?; } - }, + + let entries_dir = boot_dir.open_dir("loader")?; + rename_exchange_bls_entries(&entries_dir)?; + } }; Ok(()) diff --git a/crates/lib/src/bootc_composefs/gc.rs b/crates/lib/src/bootc_composefs/gc.rs index 8195cade2..7926d250c 100644 --- a/crates/lib/src/bootc_composefs/gc.rs +++ b/crates/lib/src/bootc_composefs/gc.rs @@ -10,7 +10,6 @@ use composefs::fsverity::{FsVerityHashValue, Sha512HashValue}; use crate::{ bootc_composefs::{ - boot::{get_esp_partition, get_sysroot_parent_dev, mount_esp}, delete::{delete_image, delete_staged, delete_state_dir, get_image_objects}, status::{ get_bootloader, get_composefs_status, get_sorted_grub_uki_boot_entries, @@ -44,20 +43,19 @@ fn list_erofs_images(sysroot: &Dir) -> Result> { /// # Returns /// The fsverity of EROFS images corresponding to boot entries #[fn_error_context::context("Listing bootloader entries")] -fn list_bootloader_entries(physical_root: &Dir) -> Result> { +fn list_bootloader_entries(storage: &Storage) -> Result> { let bootloader = get_bootloader()?; + let boot_dir = storage.require_boot_dir()?; let entries = match bootloader { Bootloader::Grub => { - let boot_dir = physical_root.open_dir("boot").context("Opening boot dir")?; - // Grub entries are always in boot let grub_dir = boot_dir.open_dir("grub2").context("Opening grub dir")?; if grub_dir.exists(USER_CFG) { // Grub UKI let mut s = String::new(); - let boot_entries = get_sorted_grub_uki_boot_entries(&boot_dir, &mut s)?; + let boot_entries = get_sorted_grub_uki_boot_entries(boot_dir, &mut s)?; boot_entries .into_iter() @@ -65,7 +63,7 @@ fn list_bootloader_entries(physical_root: &Dir) -> Result> { .collect::, _>>()? } else { // Type1 Entry - let boot_entries = get_sorted_type1_boot_entries(&boot_dir, true)?; + let boot_entries = get_sorted_type1_boot_entries(boot_dir, true)?; boot_entries .into_iter() @@ -75,11 +73,7 @@ fn list_bootloader_entries(physical_root: &Dir) -> Result> { } Bootloader::Systemd => { - let device = get_sysroot_parent_dev(physical_root)?; - let (esp_part, ..) = get_esp_partition(&device)?; - let esp_mount = mount_esp(&esp_part)?; - - let boot_entries = get_sorted_type1_boot_entries(&esp_mount.fd, true)?; + let boot_entries = get_sorted_type1_boot_entries(boot_dir, true)?; boot_entries .into_iter() @@ -175,7 +169,7 @@ pub(crate) async fn composefs_gc(storage: &Storage, booted_cfs: &BootedComposefs let sysroot = &storage.physical_root; - let bootloader_entries = list_bootloader_entries(&storage.physical_root)?; + let bootloader_entries = list_bootloader_entries(&storage)?; let images = list_erofs_images(&sysroot)?; // Collect the deployments that have an image but no bootloader entry diff --git a/crates/lib/src/bootc_composefs/rollback.rs b/crates/lib/src/bootc_composefs/rollback.rs index 6338bf9b5..5cfd60c39 100644 --- a/crates/lib/src/bootc_composefs/rollback.rs +++ b/crates/lib/src/bootc_composefs/rollback.rs @@ -6,9 +6,7 @@ use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags}; -use crate::bootc_composefs::boot::{ - get_esp_partition, get_sysroot_parent_dev, mount_esp, type1_entry_conf_file_name, BootType, -}; +use crate::bootc_composefs::boot::{type1_entry_conf_file_name, BootType}; use crate::bootc_composefs::status::{get_composefs_status, get_sorted_type1_boot_entries}; use crate::composefs_consts::TYPE1_ENT_PATH_STAGED; use crate::spec::Bootloader; @@ -196,31 +194,21 @@ pub(crate) async fn composefs_rollback( anyhow::bail!("Rollback deployment not a composefs deployment") }; + let boot_dir = storage.require_boot_dir()?; + match &rollback_entry.bootloader { - Bootloader::Grub => { - let boot_dir = storage - .physical_root - .open_dir("boot") - .context("Opening boot dir")?; - - match rollback_entry.boot_type { - BootType::Bls => { - rollback_composefs_entries(&boot_dir, rollback_entry.bootloader.clone())?; - } - - BootType::Uki => { - rollback_grub_uki_entries(&boot_dir)?; - } + Bootloader::Grub => match rollback_entry.boot_type { + BootType::Bls => { + rollback_composefs_entries(boot_dir, rollback_entry.bootloader.clone())?; } - } + BootType::Uki => { + rollback_grub_uki_entries(boot_dir)?; + } + }, Bootloader::Systemd => { - let parent = get_sysroot_parent_dev(&storage.physical_root)?; - let (esp_part, ..) = get_esp_partition(&parent)?; - let esp_mount = mount_esp(&esp_part)?; - // We use BLS entries for systemd UKI as well - rollback_composefs_entries(&esp_mount.fd, rollback_entry.bootloader.clone())?; + rollback_composefs_entries(boot_dir, rollback_entry.bootloader.clone())?; } } diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index 723d6ed19..196e83284 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -1,4 +1,7 @@ +use std::io::Write; +use std::ops::Deref; use std::os::unix::fs::symlink; +use std::path::Path; use std::{fs::create_dir_all, process::Command}; use anyhow::{Context, Result}; @@ -8,7 +11,7 @@ use bootc_mount::tempmount::TempMount; use bootc_utils::CommandRunExt; use camino::Utf8PathBuf; use cap_std_ext::cap_std::ambient_authority; -use cap_std_ext::cap_std::fs::Dir; +use cap_std_ext::cap_std::fs::{Dir, Permissions, PermissionsExt}; use cap_std_ext::dirext::CapStdExtDirExt; use composefs::fsverity::{FsVerityHashValue, Sha512HashValue}; use fn_error_context::context; @@ -23,6 +26,7 @@ use crate::bootc_composefs::boot::BootType; use crate::bootc_composefs::repo::get_imgref; use crate::bootc_composefs::status::get_sorted_type1_boot_entries; use crate::parsers::bls_config::BLSConfigType; +use crate::store::{BootedComposefs, Storage}; use crate::{ composefs_consts::{ COMPOSEFS_CMDLINE, COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, @@ -104,6 +108,49 @@ pub(crate) fn copy_etc_to_state( cp_ret } +/// Updates the currently booted image's target imgref +pub(crate) fn update_target_imgref_in_origin( + storage: &Storage, + booted_cfs: &BootedComposefs, + imgref: &ImageReference, +) -> Result<()> { + let path = Path::new(STATE_DIR_RELATIVE).join(booted_cfs.cmdline.digest.deref()); + + let state_dir = storage + .physical_root + .open_dir(path) + .context("Opening state dir")?; + + let origin_filename = format!("{}.origin", booted_cfs.cmdline.digest.deref()); + + let origin_file = state_dir + .read_to_string(&origin_filename) + .context("Reading origin file")?; + + let mut ini = + tini::Ini::from_string(&origin_file).context("Failed to parse file origin file as ini")?; + + // Replace the origin + ini = ini.section("origin").item( + ORIGIN_CONTAINER, + format!("ostree-unverified-image:{imgref}"), + ); + + state_dir + .atomic_replace_with(origin_filename, move |f| -> std::io::Result<_> { + f.write_all(ini.to_string().as_bytes())?; + f.flush()?; + + let perms = Permissions::from_mode(0o644); + f.get_mut().as_file_mut().set_permissions(perms)?; + + Ok(()) + }) + .context("Writing to origin file")?; + + Ok(()) +} + /// Creates and populates /sysroot/state/deploy/image_id #[context("Writing composefs state")] pub(crate) fn write_composefs_state( diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 29ae212ba..40ec1b757 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -5,7 +5,7 @@ use bootc_kernel_cmdline::utf8::Cmdline; use fn_error_context::context; use crate::{ - bootc_composefs::boot::{get_esp_partition, get_sysroot_parent_dev, mount_esp, BootType}, + bootc_composefs::boot::BootType, composefs_consts::{COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, TYPE1_ENT_PATH, USER_CFG}, install::EFI_LOADER_INFO, parsers::{ @@ -13,6 +13,7 @@ use crate::{ grub_menuconfig::{parse_grub_menuentry_file, MenuEntry}, }, spec::{BootEntry, BootOrder, Host, HostSpec, ImageReference, ImageStatus}, + store::Storage, utils::{read_uefi_var, EfiError}, }; @@ -254,17 +255,20 @@ pub(crate) async fn get_composefs_status( storage: &crate::store::Storage, booted_cfs: &crate::store::BootedComposefs, ) -> Result { - composefs_deployment_status_from(&storage.physical_root, booted_cfs.cmdline).await + composefs_deployment_status_from(&storage, booted_cfs.cmdline).await } #[context("Getting composefs deployment status")] pub(crate) async fn composefs_deployment_status_from( - sysroot: &Dir, + storage: &Storage, cmdline: &ComposefsCmdline, ) -> Result { let composefs_digest = &cmdline.digest; - let deployments = sysroot + let boot_dir = storage.require_boot_dir()?; + + let deployments = storage + .physical_root .read_dir(STATE_DIR_RELATIVE) .with_context(|| format!("Reading sysroot {STATE_DIR_RELATIVE}"))?; @@ -348,30 +352,10 @@ pub(crate) async fn composefs_deployment_status_from( let booted = host.require_composefs_booted()?; - let (boot_dir, _temp_guard) = match booted.bootloader { - Bootloader::Grub => (sysroot.open_dir("boot").context("Opening boot dir")?, None), - - // TODO: This is redundant as we should already have ESP mounted at `/efi/` accoding to - // spec; currently we do not - // - // See: https://uapi-group.org/specifications/specs/boot_loader_specification/#mount-points - Bootloader::Systemd => { - let parent = get_sysroot_parent_dev(sysroot)?; - let (esp_part, ..) = get_esp_partition(&parent)?; - - let esp_mount = mount_esp(&esp_part)?; - - let dir = esp_mount.fd.try_clone().context("Cloning fd")?; - let guard = Some(esp_mount); - - (dir, guard) - } - }; - let is_rollback_queued = match booted.bootloader { Bootloader::Grub => match boot_type { BootType::Bls => { - let bls_config = get_sorted_type1_boot_entries(&boot_dir, false)?; + let bls_config = get_sorted_type1_boot_entries(boot_dir, false)?; let bls_config = bls_config .first() .ok_or(anyhow::anyhow!("First boot entry not found"))?; @@ -392,7 +376,7 @@ pub(crate) async fn composefs_deployment_status_from( BootType::Uki => { let mut s = String::new(); - !get_sorted_grub_uki_boot_entries(&boot_dir, &mut s)? + !get_sorted_grub_uki_boot_entries(boot_dir, &mut s)? .first() .ok_or(anyhow::anyhow!("First boot entry not found"))? .body @@ -403,7 +387,7 @@ pub(crate) async fn composefs_deployment_status_from( // We will have BLS stuff and the UKI stuff in the same DIR Bootloader::Systemd => { - let bls_config = get_sorted_type1_boot_entries(&boot_dir, false)?; + let bls_config = get_sorted_type1_boot_entries(boot_dir, false)?; let bls_config = bls_config .first() .ok_or(anyhow::anyhow!("First boot entry not found"))?; diff --git a/crates/lib/src/bootc_composefs/switch.rs b/crates/lib/src/bootc_composefs/switch.rs index 5cb1e2c1f..8b8a232ec 100644 --- a/crates/lib/src/bootc_composefs/switch.rs +++ b/crates/lib/src/bootc_composefs/switch.rs @@ -1,14 +1,11 @@ use anyhow::{Context, Result}; -use camino::Utf8PathBuf; use fn_error_context::context; use crate::{ bootc_composefs::{ - boot::{setup_composefs_bls_boot, setup_composefs_uki_boot, BootSetupType, BootType}, - repo::pull_composefs_repo, - service::start_finalize_stated_svc, - state::write_composefs_state, + state::update_target_imgref_in_origin, status::get_composefs_status, + update::{do_upgrade, is_image_pulled, validate_update, UpdateAction}, }, cli::{imgref_for_switch, SwitchOpts}, store::{BootedComposefs, Storage}, @@ -42,44 +39,39 @@ pub(crate) async fn switch_composefs( anyhow::bail!("Target image is undefined") }; - start_finalize_stated_svc()?; + let repo = &*booted_cfs.repo; + let (image, manifest, _) = is_image_pulled(repo, &target_imgref).await?; - let (repo, entries, id, fs) = - pull_composefs_repo(&target_imgref.transport, &target_imgref.image).await?; + if let Some(cfg_verity) = image { + let action = validate_update( + storage, + booted_cfs, + &host, + manifest.config().digest().digest(), + &cfg_verity, + true, + )?; - let Some(entry) = entries.iter().next() else { - anyhow::bail!("No boot entries!"); - }; + match action { + UpdateAction::Skip => { + println!("No changes in image: {target_imgref:#}"); + return Ok(()); + } - let boot_type = BootType::from(entry); - let mut boot_digest = None; + UpdateAction::Proceed => { + return do_upgrade(storage, &host, &target_imgref).await; + } - match boot_type { - BootType::Bls => { - boot_digest = Some(setup_composefs_bls_boot( - BootSetupType::Upgrade((storage, &fs, &host)), - repo, - &id, - entry, - )?) + UpdateAction::UpdateOrigin => { + // The staged image will never be the current image's verity digest + println!("Image already in compoesfs repository"); + println!("Updating target image reference"); + return update_target_imgref_in_origin(storage, booted_cfs, &target_imgref); + } } - BootType::Uki => setup_composefs_uki_boot( - BootSetupType::Upgrade((storage, &fs, &host)), - repo, - &id, - entries, - )?, - }; + } - // TODO: Remove this hardcoded path when write_composefs_state accepts a Dir - write_composefs_state( - &Utf8PathBuf::from("/sysroot"), - id, - &target_imgref, - true, - boot_type, - boot_digest, - )?; + do_upgrade(storage, &host, &target_imgref).await?; Ok(()) } diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index eebfd3faa..f9a221e1e 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -1,6 +1,12 @@ use anyhow::{Context, Result}; use camino::Utf8PathBuf; -use composefs::util::{parse_sha256, Sha256Digest}; +use cap_std_ext::cap_std::fs::Dir; +use composefs::{ + fsverity::{FsVerityHashValue, Sha512HashValue}, + util::{parse_sha256, Sha256Digest}, +}; +use composefs_boot::BootOps; +use composefs_oci::image::create_filesystem; use fn_error_context::context; use ostree_ext::oci_spec::image::{ImageConfiguration, ImageManifest}; @@ -10,10 +16,11 @@ use crate::{ repo::{get_imgref, pull_composefs_repo}, service::start_finalize_stated_svc, state::write_composefs_state, - status::{get_composefs_status, get_container_manifest_and_config}, + status::{get_bootloader, get_composefs_status, get_container_manifest_and_config}, }, cli::UpgradeOpts, - spec::ImageReference, + composefs_consts::{STATE_DIR_RELATIVE, TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED}, + spec::{Bootloader, Host, ImageReference}, store::{BootedComposefs, ComposefsRepository, Storage}, }; @@ -38,14 +45,14 @@ pub fn str_to_sha256digest(id: &str) -> Result { /// # Returns /// /// Returns a tuple containing: -/// * `true` if the image is pulled/available locally, `false` otherwise +/// * `Some` if the image is pulled/available locally, `None` otherwise /// * The container image manifest /// * The container image configuration #[context("Checking if image {} is pulled", imgref.image)] -async fn is_image_pulled( +pub(crate) async fn is_image_pulled( repo: &ComposefsRepository, imgref: &ImageReference, -) -> Result<(bool, ImageManifest, ImageConfiguration)> { +) -> Result<(Option, ImageManifest, ImageConfiguration)> { let imgref_repr = get_imgref(&imgref.transport, &imgref.image); let (manifest, config) = get_container_manifest_and_config(&imgref_repr).await?; @@ -55,7 +62,194 @@ async fn is_image_pulled( // check_stream is expensive to run, but probably a good idea let container_pulled = repo.check_stream(&img_sha256).context("Checking stream")?; - Ok((container_pulled.is_some(), manifest, config)) + Ok((container_pulled, manifest, config)) +} + +fn rm_staged_type1_ent(boot_dir: &Dir) -> Result<()> { + if boot_dir.exists(TYPE1_ENT_PATH_STAGED) { + boot_dir + .remove_dir_all(TYPE1_ENT_PATH_STAGED) + .context("Removing staged bootloader entry")?; + } + + Ok(()) +} + +#[derive(Debug)] +pub(crate) enum UpdateAction { + /// Skip the update. We probably have the update in our deployments + Skip, + /// Proceed with the update + Proceed, + /// Only update the target imgref in the .origin file + /// Will only be returned if the Operation is update and not switch + UpdateOrigin, +} + +/// Determines what action should be taken for the update +/// +/// Cases: +/// +/// - The verity is the same as that of the currently booted deployment +/// +/// Nothing to do here as we're currently booted +/// +/// - The verity is the same as that of the staged deployment +/// +/// Nothing to do, as we only get a "staged" deployment if we have +/// /run/composefs/staged-deployment which is the last thing we create while upgrading +/// +/// - The verity is the same as that of the rollback deployment +/// +/// Nothing to do since this is a rollback deployment which means this was unstaged at some +/// point +/// +/// - The verity is not found +/// +/// The update/switch might've been canceled before /run/composefs/staged-deployment +/// was created, or at any other point in time, or it's a new one. +/// Any which way, we can overwrite everything +/// +/// # Arguments +/// +/// * `storage` - The global storage object +/// * `booted_cfs` - Reference to the booted composefs deployment +/// * `host` - Object returned by `get_composefs_status` +/// * `img_digest` - The SHA256 sum of the target image +/// * `config_verity` - The verity of the Image config splitstream +/// * `is_switch` - Whether this is an update operation or a switch operation +/// +/// # Returns +/// * UpdateAction::Skip - Skip the update/switch as we have it as a deployment +/// * UpdateAction::UpdateOrigin - Just update the target imgref in the origin file +/// * UpdateAction::Proceed - Proceed with the update +pub(crate) fn validate_update( + storage: &Storage, + booted_cfs: &BootedComposefs, + host: &Host, + img_digest: &str, + config_verity: &Sha512HashValue, + is_switch: bool, +) -> Result { + let repo = &*booted_cfs.repo; + + let mut fs = create_filesystem(repo, img_digest, Some(config_verity))?; + fs.transform_for_boot(&repo)?; + + let image_id = fs.compute_image_id(); + + // Case1 + // + // "update" image has the same verity as the one currently booted + // This could be someone trying to `bootc switch ` where + // remote_image is the exact same image as the one currently booted, but + // they are wanting to change the target + // + // We could simply update the image origin file here + if image_id.to_hex() == *booted_cfs.cmdline.digest { + let ret = if is_switch { + UpdateAction::UpdateOrigin + } else { + UpdateAction::Skip + }; + + return Ok(ret); + } + + let all_deployments = host.all_composefs_deployments()?; + + let found_depl = all_deployments + .iter() + .find(|d| d.deployment.verity == image_id.to_hex()); + + // We have this in our deployments somewhere, i.e. Case 2 or 3 + if found_depl.is_some() { + return Ok(UpdateAction::Skip); + } + + let booted = host.require_composefs_booted()?; + let boot_dir = storage.require_boot_dir()?; + + // Remove staged bootloader entries, if any + // GC should take care of the UKI PEs and other binaries + match get_bootloader()? { + Bootloader::Grub => match booted.boot_type { + BootType::Bls => rm_staged_type1_ent(boot_dir)?, + + BootType::Uki => { + let grub = boot_dir.open_dir("grub2").context("Opening grub dir")?; + + if grub.exists(USER_CFG_STAGED) { + grub.remove_file(USER_CFG_STAGED) + .context("Removing staged grub user config")?; + } + } + }, + + Bootloader::Systemd => rm_staged_type1_ent(boot_dir)?, + } + + // Remove state directory + let state_dir = storage + .physical_root + .open_dir(STATE_DIR_RELATIVE) + .context("Opening state dir")?; + + if state_dir.exists(image_id.to_hex()) { + state_dir + .remove_dir_all(image_id.to_hex()) + .context("Removing state")?; + } + + Ok(UpdateAction::Proceed) +} + +/// Performs the Update or Switch operation +#[context("Performing Upgrade Operation")] +pub(crate) async fn do_upgrade( + storage: &Storage, + host: &Host, + imgref: &ImageReference, +) -> Result<()> { + start_finalize_stated_svc()?; + + let (repo, entries, id, fs) = pull_composefs_repo(&imgref.transport, &imgref.image).await?; + + let Some(entry) = entries.iter().next() else { + anyhow::bail!("No boot entries!"); + }; + + let boot_type = BootType::from(entry); + let mut boot_digest = None; + + match boot_type { + BootType::Bls => { + boot_digest = Some(setup_composefs_bls_boot( + BootSetupType::Upgrade((storage, &fs, &host)), + repo, + &id, + entry, + )?) + } + + BootType::Uki => setup_composefs_uki_boot( + BootSetupType::Upgrade((storage, &fs, &host)), + repo, + &id, + entries, + )?, + }; + + write_composefs_state( + &Utf8PathBuf::from("/sysroot"), + id, + imgref, + true, + boot_type, + boot_digest, + )?; + + Ok(()) } #[context("Upgrading composefs")] @@ -68,7 +262,7 @@ pub(crate) async fn upgrade_composefs( .await .context("Getting composefs deployment status")?; - let mut imgref = host + let mut booted_imgref = host .spec .image .as_ref() @@ -76,17 +270,11 @@ pub(crate) async fn upgrade_composefs( let repo = &*composefs.repo; - let (img_pulled, mut manifest, mut config) = is_image_pulled(&repo, imgref).await?; - let booted_img_digest = manifest.config().digest().digest(); - - // We already have this container config. No update available - if img_pulled { - println!("No changes in: {imgref:#}"); - // TODO(Johan-Liebert1): What if we have the config but we failed the previous update in the middle? - return Ok(()); - } + let (img_pulled, mut manifest, mut config) = is_image_pulled(&repo, booted_imgref).await?; + let booted_img_digest = manifest.config().digest().digest().to_owned(); // Check if we already have this update staged + // Or if we have another staged deployment with a different image let staged_image = host.status.staged.as_ref().and_then(|i| i.image.as_ref()); if let Some(staged_image) = staged_image { @@ -105,16 +293,64 @@ pub(crate) async fn upgrade_composefs( // We have a staged image but it's not the update image. // Maybe it's something we got by `bootc switch` // Switch takes precedence over update, so we change the imgref - imgref = &staged_image.image; + booted_imgref = &staged_image.image; - let (img_pulled, staged_manifest, staged_cfg) = is_image_pulled(&repo, imgref).await?; + let (img_pulled, staged_manifest, staged_cfg) = + is_image_pulled(&repo, booted_imgref).await?; manifest = staged_manifest; config = staged_cfg; - // We already have this container config. No update available - if img_pulled { - println!("No changes in staged image: {imgref:#}"); - return Ok(()); + if let Some(cfg_verity) = img_pulled { + let action = validate_update( + storage, + composefs, + &host, + manifest.config().digest().digest(), + &cfg_verity, + false, + )?; + + match action { + UpdateAction::Skip => { + println!("No changes in staged image: {booted_imgref:#}"); + return Ok(()); + } + + UpdateAction::Proceed => { + return do_upgrade(storage, &host, booted_imgref).await; + } + + UpdateAction::UpdateOrigin => { + anyhow::bail!("Updating origin not supported for update operation") + } + } + } + } + + // We already have this container config + if let Some(cfg_verity) = img_pulled { + let action = validate_update( + storage, + composefs, + &host, + &booted_img_digest, + &cfg_verity, + false, + )?; + + match action { + UpdateAction::Skip => { + println!("No changes in: {booted_imgref:#}"); + return Ok(()); + } + + UpdateAction::Proceed => { + return do_upgrade(storage, &host, booted_imgref).await; + } + + UpdateAction::UpdateOrigin => { + anyhow::bail!("Updating origin not supported for update operation") + } } } @@ -146,43 +382,7 @@ pub(crate) async fn upgrade_composefs( return Ok(()); } - start_finalize_stated_svc()?; - - let (repo, entries, id, fs) = pull_composefs_repo(&imgref.transport, &imgref.image).await?; - - let Some(entry) = entries.iter().next() else { - anyhow::bail!("No boot entries!"); - }; - - let boot_type = BootType::from(entry); - let mut boot_digest = None; - - match boot_type { - BootType::Bls => { - boot_digest = Some(setup_composefs_bls_boot( - BootSetupType::Upgrade((storage, &fs, &host)), - repo, - &id, - entry, - )?) - } - - BootType::Uki => setup_composefs_uki_boot( - BootSetupType::Upgrade((storage, &fs, &host)), - repo, - &id, - entries, - )?, - }; - - write_composefs_state( - &Utf8PathBuf::from("/sysroot"), - id, - imgref, - true, - boot_type, - boot_digest, - )?; + do_upgrade(storage, &host, booted_imgref).await?; if opts.apply { return crate::reboot::reboot(); diff --git a/crates/lib/src/bootc_kargs.rs b/crates/lib/src/bootc_kargs.rs index af709f1af..688b3baf7 100644 --- a/crates/lib/src/bootc_kargs.rs +++ b/crates/lib/src/bootc_kargs.rs @@ -2,10 +2,13 @@ use anyhow::{Context, Result}; use bootc_kernel_cmdline::utf8::{Cmdline, CmdlineOwned}; use camino::Utf8Path; +use cap_std_ext::cap_std::ambient_authority; use cap_std_ext::cap_std::fs::Dir; use cap_std_ext::cap_std::fs_utf8::Dir as DirUtf8; use cap_std_ext::dirext::CapStdExtDirExt; use cap_std_ext::dirext::CapStdExtDirExtUtf8; +use composefs::fs::read_file; +use composefs::generic_tree::Inode; use ostree::gio; use ostree_ext::ostree; use ostree_ext::ostree::Deployment; @@ -13,9 +16,11 @@ use ostree_ext::prelude::Cast; use ostree_ext::prelude::FileEnumeratorExt; use ostree_ext::prelude::FileExt; use serde::Deserialize; +use std::ffi::OsStr; use crate::deploy::ImageState; use crate::store::Storage; +use crate::store::{ComposefsFilesystem, ComposefsRepository}; /// The relative path to the kernel arguments which may be embedded in an image. const KARGS_PATH: &str = "usr/lib/bootc/kargs.d"; @@ -45,6 +50,96 @@ impl Config { } } +/// Compute the diff between existing and remote kargs +/// Apply the diff to the new kargs +fn compute_apply_kargs_diff( + existing_kargs: &Cmdline, + remote_kargs: &Cmdline, + new_kargs: &mut Cmdline, +) { + // Calculate the diff between the existing and remote kargs + let added_kargs: Vec<_> = remote_kargs + .iter() + .filter(|item| !existing_kargs.iter().any(|existing| *item == existing)) + .collect(); + let removed_kargs: Vec<_> = existing_kargs + .iter() + .filter(|item| !remote_kargs.iter().any(|remote| *item == remote)) + .collect(); + + tracing::debug!("kargs: added={:?} removed={:?}", added_kargs, removed_kargs); + + // Apply the diff to the system kargs + for arg in &removed_kargs { + new_kargs.remove_exact(arg); + } + for arg in &added_kargs { + new_kargs.add(arg); + } +} + +/// Looks for files in usr/lib/bootc/kargs.d and parses cmdline agruments +pub(crate) fn kargs_from_composefs_filesystem( + fs: &ComposefsFilesystem, + repo: &ComposefsRepository, + new_kargs: &mut Cmdline, + fresh_install: bool, +) -> Result<()> { + let existing_kargs = match fresh_install { + // If we are freshly installing, we won't have any existing kargs + true => Cmdline::new(), + + false => { + let root = Dir::open_ambient_dir("/", ambient_authority()).context("Opening root")?; + let existing_kargs = get_kargs_in_root(&root, std::env::consts::ARCH)?; + existing_kargs + } + }; + + let kargs_d = fs + .root + .get_directory_opt(OsStr::new(KARGS_PATH)) + .with_context(|| format!("Getting {KARGS_PATH}"))?; + + let Some(kargs_d) = kargs_d else { + return Ok(()); + }; + + let mut remote_kargs = Cmdline::new(); + + for (fname, inode) in kargs_d.sorted_entries() { + let Inode::Leaf(..) = inode else { continue }; + + let Some(fname_str) = fname.to_str() else { + tracing::warn!( + "Failed to convert filename to UTF-8, will skip parsing: {:?}", + fname + ); + continue; + }; + + if !Config::filename_matches(fname_str) { + continue; + } + + let file = kargs_d + .get_file(fname) + .with_context(|| format!("Getting file {fname_str}"))?; + + let contents = read_file(&file, &repo)?; + let contents = std::str::from_utf8(&contents) + .with_context(|| format!("File {fname_str} is not a valid UTF-8"))?; + + if let Some(kargs) = parse_kargs_toml(contents, std::env::consts::ARCH)? { + remote_kargs.extend(&kargs); + }; + } + + compute_apply_kargs_diff(&existing_kargs, &remote_kargs, new_kargs); + + Ok(()) +} + /// Load and parse all bootc kargs.d files in the specified root, returning /// a combined list. pub(crate) fn get_kargs_in_root(d: &Dir, sys_arch: &str) -> Result { @@ -175,29 +270,7 @@ pub(crate) fn get_kargs( // Fetch the kernel arguments from the new root let remote_kargs = get_kargs_from_ostree(repo, &fetched_tree, sys_arch)?; - // Calculate the diff between the existing and remote kargs - let added_kargs: Vec<_> = remote_kargs - .iter() - .filter(|item| !existing_kargs.iter().any(|existing| *item == existing)) - .collect(); - let removed_kargs: Vec<_> = existing_kargs - .iter() - .filter(|item| !remote_kargs.iter().any(|remote| *item == remote)) - .collect(); - - tracing::debug!( - "kargs: added={:?} removed={:?}", - &added_kargs, - removed_kargs - ); - - // Apply the diff to the system kargs - for arg in &removed_kargs { - kargs.remove_exact(arg); - } - for arg in &added_kargs { - kargs.add(arg); - } + compute_apply_kargs_diff(&existing_kargs, &remote_kargs, &mut kargs); Ok(kargs) } diff --git a/crates/lib/src/store/mod.rs b/crates/lib/src/store/mod.rs index eb65e6eb9..4779093c5 100644 --- a/crates/lib/src/store/mod.rs +++ b/crates/lib/src/store/mod.rs @@ -21,6 +21,7 @@ use std::ops::Deref; use std::sync::Arc; use anyhow::{Context, Result}; +use bootc_mount::tempmount::TempMount; use cap_std_ext::cap_std; use cap_std_ext::cap_std::fs::{Dir, DirBuilder, DirBuilderExt as _}; use cap_std_ext::dirext::CapStdExtDirExt; @@ -31,10 +32,11 @@ use ostree_ext::sysroot::SysrootLock; use ostree_ext::{gio, ostree}; use rustix::fs::Mode; -use crate::bootc_composefs::status::{composefs_booted, ComposefsCmdline}; +use crate::bootc_composefs::boot::{get_esp_partition, get_sysroot_parent_dev, mount_esp}; +use crate::bootc_composefs::status::{composefs_booted, get_bootloader, ComposefsCmdline}; use crate::lsm; use crate::podstorage::CStorage; -use crate::spec::ImageStatus; +use crate::spec::{Bootloader, ImageStatus}; use crate::utils::{deployment_fd, open_dir_remount_rw}; /// See https://github.com/containers/composefs-rs/issues/159 @@ -169,9 +171,23 @@ impl BootedStorage { } let composefs = Arc::new(composefs); + // NOTE: This is assuming that we'll only have composefs in a UEFI system + // We do have this assumptions in a lot of other places + let parent = get_sysroot_parent_dev(&physical_root)?; + let (esp_part, ..) = get_esp_partition(&parent)?; + let esp_mount = mount_esp(&esp_part)?; + + let boot_dir = match get_bootloader()? { + Bootloader::Grub => physical_root.open_dir("boot").context("Opening boot")?, + // NOTE: Handle XBOOTLDR partitions here if and when we use it + Bootloader::Systemd => esp_mount.fd.try_clone().context("Cloning fd")?, + }; + let storage = Storage { physical_root, run, + boot_dir: Some(boot_dir), + esp: Some(esp_mount), ostree: Default::default(), composefs: OnceCell::from(composefs), imgstore: Default::default(), @@ -194,6 +210,8 @@ impl BootedStorage { let storage = Storage { physical_root, run, + boot_dir: None, + esp: None, ostree: OnceCell::from(sysroot), composefs: Default::default(), imgstore: Default::default(), @@ -235,6 +253,15 @@ impl BootedStorage { pub(crate) struct Storage { /// Directory holding the physical root pub physical_root: Dir, + + /// The 'boot' directory, useful and `Some` only for composefs systems + /// For grub booted systems, this points to `/sysroot/boot` + /// For systemd booted systems, this points to the ESP + pub boot_dir: Option, + + /// The ESP mounted at a tmp location + pub esp: Option, + /// Our runtime state run: Dir, @@ -286,12 +313,21 @@ impl Storage { Ok(Self { physical_root, run, + boot_dir: None, + esp: None, ostree: ostree_cell, composefs: Default::default(), imgstore: Default::default(), }) } + /// Returns `boot_dir` if it exists + pub(crate) fn require_boot_dir(&self) -> Result<&Dir> { + self.boot_dir + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Boot dir not found")) + } + /// Access the underlying ostree repository pub(crate) fn get_ostree(&self) -> Result<&SysrootLock> { self.ostree