diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index e910ea660..ff443ccb6 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -33,7 +33,6 @@ use crate::bootc_composefs::status::get_sorted_uki_boot_entries; use crate::composefs_consts::{TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED}; use crate::parsers::bls_config::{BLSConfig, BLSConfigType}; use crate::parsers::grub_menuconfig::MenuEntry; -use crate::spec::ImageReference; use crate::task::Task; use crate::{bootc_composefs::repo::open_composefs_repo, store::ComposefsFilesystem}; use crate::{ @@ -369,14 +368,11 @@ pub(crate) fn setup_composefs_bls_boot( // root_setup.kargs has [root=UUID=, "rw"] let mut cmdline_options = String::from(root_setup.kargs.join(" ")); - match &state.composefs_options { - Some(opt) if opt.insecure => { - cmdline_options.push_str(&format!(" {COMPOSEFS_CMDLINE}=?{id_hex}")); - } - None | Some(..) => { - cmdline_options.push_str(&format!(" {COMPOSEFS_CMDLINE}={id_hex}")); - } - }; + if state.composefs_options.insecure { + cmdline_options.push_str(&format!(" {COMPOSEFS_CMDLINE}=?{id_hex}")); + } else { + cmdline_options.push_str(&format!(" {COMPOSEFS_CMDLINE}={id_hex}")); + } // Locate ESP partition device let esp_part = esp_in(&root_setup.device_info)?; @@ -835,18 +831,14 @@ pub(crate) fn setup_composefs_uki_boot( } } - let Some(cfs_opts) = &state.composefs_options else { - anyhow::bail!("ComposeFS options not found"); - }; - let esp_part = esp_in(&root_setup.device_info)?; ( root_setup.physical_root_path.clone(), esp_part.node.clone(), state.detected_bootloader.clone(), - cfs_opts.insecure, - cfs_opts.uki_addon.as_ref(), + state.composefs_options.insecure, + state.composefs_options.uki_addon.as_ref(), ) } @@ -1001,11 +993,7 @@ pub(crate) fn setup_composefs_boot( write_composefs_state( &root_setup.physical_root_path, id, - &ImageReference { - image: state.source.imageref.name.clone(), - transport: state.source.imageref.transport.to_string(), - signature: None, - }, + &crate::spec::ImageReference::from(state.target_imgref.clone()), false, boot_type, boot_digest, diff --git a/crates/lib/src/bootc_composefs/finalize.rs b/crates/lib/src/bootc_composefs/finalize.rs index 22971009d..e09e21350 100644 --- a/crates/lib/src/bootc_composefs/finalize.rs +++ b/crates/lib/src/bootc_composefs/finalize.rs @@ -39,7 +39,7 @@ pub(crate) async fn get_etc_diff() -> Result<()> { Ok(()) } -pub(crate) async fn composefs_native_finalize() -> Result<()> { +pub(crate) async fn composefs_backend_finalize() -> Result<()> { let host = composefs_deployment_status().await?; let booted_composefs = host.require_composefs_booted()?; diff --git a/crates/lib/src/bootc_composefs/mod.rs b/crates/lib/src/bootc_composefs/mod.rs index c19dbfb77..ee8a742ed 100644 --- a/crates/lib/src/bootc_composefs/mod.rs +++ b/crates/lib/src/bootc_composefs/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod boot; pub(crate) mod finalize; pub(crate) mod repo; pub(crate) mod rollback; +pub(crate) mod service; pub(crate) mod state; pub(crate) mod status; pub(crate) mod switch; diff --git a/crates/lib/src/bootc_composefs/repo.rs b/crates/lib/src/bootc_composefs/repo.rs index 8cd5506c3..34ffb9417 100644 --- a/crates/lib/src/bootc_composefs/repo.rs +++ b/crates/lib/src/bootc_composefs/repo.rs @@ -81,7 +81,7 @@ pub(crate) async fn pull_composefs_repo( )> { let rootfs_dir = Dir::open_ambient_dir("/sysroot", ambient_authority())?; - let repo = open_composefs_repo(&rootfs_dir).context("Opening compoesfs repo")?; + let repo = open_composefs_repo(&rootfs_dir).context("Opening composefs repo")?; let final_imgref = get_imgref(transport, image); @@ -91,7 +91,7 @@ pub(crate) async fn pull_composefs_repo( .await .context("Pulling composefs repo")?; - tracing::info!("id: {}, verity: {}", hex::encode(id), verity.to_hex()); + tracing::info!("ID: {}, Verity: {}", hex::encode(id), verity.to_hex()); let repo = open_composefs_repo(&rootfs_dir)?; let mut fs: crate::store::ComposefsFilesystem = diff --git a/crates/lib/src/bootc_composefs/service.rs b/crates/lib/src/bootc_composefs/service.rs new file mode 100644 index 000000000..fdf4136a0 --- /dev/null +++ b/crates/lib/src/bootc_composefs/service.rs @@ -0,0 +1,22 @@ +use anyhow::{Context, Result}; +use fn_error_context::context; +use std::process::Command; + +use crate::composefs_consts::BOOTC_FINALIZE_STAGED_SERVICE; + +/// Starts the finaize staged service which will "unstage" the deployment +/// This is called before an upgrade or switch operation, as these create a staged +/// deployment +#[context("Starting finalize staged service")] +pub(crate) fn start_finalize_stated_svc() -> Result<()> { + let cmd_status = Command::new("systemctl") + .args(["start", "--quiet", BOOTC_FINALIZE_STAGED_SERVICE]) + .status() + .context("Starting finalize service")?; + + if !cmd_status.success() { + anyhow::bail!("systemctl exited with status {cmd_status}") + } + + Ok(()) +} diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index e7b8ce70b..98fc60007 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -113,7 +113,9 @@ pub(crate) fn write_composefs_state( boot_type: BootType, boot_digest: Option, ) -> Result<()> { - let state_path = root_path.join(format!("{STATE_DIR_RELATIVE}/{}", deployment_id.to_hex())); + let state_path = root_path + .join(STATE_DIR_RELATIVE) + .join(deployment_id.to_hex()); create_dir_all(state_path.join("etc"))?; diff --git a/crates/lib/src/bootc_composefs/switch.rs b/crates/lib/src/bootc_composefs/switch.rs index bebb95399..485fc59db 100644 --- a/crates/lib/src/bootc_composefs/switch.rs +++ b/crates/lib/src/bootc_composefs/switch.rs @@ -6,6 +6,7 @@ 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, status::composefs_deployment_status, }, @@ -36,6 +37,8 @@ pub(crate) async fn switch_composefs(opts: SwitchOpts) -> Result<()> { anyhow::bail!("Target image is undefined") }; + start_finalize_stated_svc()?; + let (repo, entries, id, fs) = pull_composefs_repo(&target_imgref.transport, &target_imgref.image).await?; diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 823a50bed..018d8f3ed 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -6,6 +6,7 @@ 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, status::composefs_deployment_status, }, @@ -14,12 +15,12 @@ use crate::{ #[context("Upgrading composefs")] pub(crate) async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> { - // TODO: IMPORTANT Have all the checks here that `bootc upgrade` has for an ostree booted system - let host = composefs_deployment_status() .await .context("Getting composefs deployment status")?; + start_finalize_stated_svc()?; + // TODO: IMPORTANT We need to check if any deployment is staged and get the image from that let imgref = host .spec diff --git a/crates/lib/src/bootloader.rs b/crates/lib/src/bootloader.rs index 3e65b29a6..459289db9 100644 --- a/crates/lib/src/bootloader.rs +++ b/crates/lib/src/bootloader.rs @@ -7,6 +7,8 @@ use fn_error_context::context; use bootc_blockdev::{Partition, PartitionTable}; use bootc_mount as mount; + +#[cfg(any(feature = "composefs-backend", feature = "install-to-disk"))] use bootc_mount::tempmount::TempMount; use crate::utils; diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index f3737b37a..6677342cc 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -35,7 +35,7 @@ use tempfile::tempdir_in; #[cfg(feature = "composefs-backend")] use crate::bootc_composefs::{ - finalize::{composefs_native_finalize, get_etc_diff}, + finalize::{composefs_backend_finalize, get_etc_diff}, rollback::composefs_rollback, state::composefs_usr_overlay, status::composefs_booted, @@ -1605,7 +1605,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> { }, #[cfg(feature = "composefs-backend")] - Opt::ComposefsFinalizeStaged => composefs_native_finalize().await, + Opt::ComposefsFinalizeStaged => composefs_backend_finalize().await, #[cfg(feature = "composefs-backend")] Opt::ConfigDiff => get_etc_diff().await, diff --git a/crates/lib/src/composefs_consts.rs b/crates/lib/src/composefs_consts.rs index 828504a86..c9e6f7512 100644 --- a/crates/lib/src/composefs_consts.rs +++ b/crates/lib/src/composefs_consts.rs @@ -8,9 +8,9 @@ pub(crate) const COMPOSEFS_TRANSIENT_STATE_DIR: &str = "/run/composefs"; /// File created in /run/composefs to record a staged-deployment pub(crate) const COMPOSEFS_STAGED_DEPLOYMENT_FNAME: &str = "staged-deployment"; -/// Absolute path to composefs-native state directory +/// Absolute path to composefs-backend state directory pub(crate) const STATE_DIR_ABS: &str = "/sysroot/state/deploy"; -/// Relative path to composefs-native state directory. Relative to /sysroot +/// Relative path to composefs-backend state directory. Relative to /sysroot pub(crate) const STATE_DIR_RELATIVE: &str = "state/deploy"; /// Relative path to the shared 'var' directory. Relative to /sysroot pub(crate) const SHARED_VAR_PATH: &str = "state/os/default/var"; @@ -36,3 +36,5 @@ pub(crate) const USER_CFG_STAGED: &str = "user.cfg.staged"; /// This is relative to the boot/efi directory pub(crate) const TYPE1_ENT_PATH: &str = "loader/entries"; pub(crate) const TYPE1_ENT_PATH_STAGED: &str = "loader/entries.staged"; + +pub(crate) const BOOTC_FINALIZE_STAGED_SERVICE: &str = "bootc-finalize-staged.service"; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 5d759fe9d..9d3e0eb3e 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -247,10 +247,17 @@ pub(crate) struct InstallConfigOpts { #[derive(Debug, Default, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct InstallComposefsOpts { + /// If true, composefs backend is used, else ostree backend is used + #[clap(long, default_value_t)] + #[serde(default)] + pub(crate) composefs_backend: bool, + + /// Make fs-verity validation optional in case the filesystem doesn't support it #[clap(long, default_value_t)] #[serde(default)] pub(crate) insecure: bool, + /// The bootloader to use. #[clap(long)] #[serde(default)] pub(crate) bootloader: Option, @@ -286,11 +293,6 @@ pub(crate) struct InstallToDiskOpts { #[serde(default)] pub(crate) via_loopback: bool, - #[clap(long)] - #[serde(default)] - #[cfg(feature = "composefs-backend")] - pub(crate) composefs_native: bool, - #[clap(flatten)] #[serde(flatten)] #[cfg(feature = "composefs-backend")] @@ -370,6 +372,10 @@ pub(crate) struct InstallToFilesystemOpts { #[clap(flatten)] pub(crate) config_opts: InstallConfigOpts, + + #[cfg(feature = "composefs-backend")] + #[clap(flatten)] + pub(crate) composefs_opts: InstallComposefsOpts, } #[derive(Debug, Clone, clap::Parser, PartialEq, Eq)] @@ -401,6 +407,10 @@ pub(crate) struct InstallToExistingRootOpts { /// via e.g. `-v /:/target`. #[clap(default_value = ALONGSIDE_ROOT_MOUNT)] pub(crate) root_path: Utf8PathBuf, + + #[cfg(feature = "composefs-backend")] + #[clap(flatten)] + pub(crate) composefs_opts: InstallComposefsOpts, } /// Global state captured from the container. @@ -442,7 +452,7 @@ pub(crate) struct State { // If Some, then --composefs_native is passed #[cfg(feature = "composefs-backend")] - pub(crate) composefs_options: Option, + pub(crate) composefs_options: InstallComposefsOpts, /// Detected bootloader type for the target system pub(crate) detected_bootloader: crate::spec::Bootloader, @@ -575,10 +585,10 @@ impl FromStr for MountSpec { #[cfg(all(feature = "install-to-disk", feature = "composefs-backend"))] impl InstallToDiskOpts { pub(crate) fn validate(&self) -> Result<()> { - if !self.composefs_native { - // Reject using --insecure without --composefs + if !self.composefs_opts.composefs_backend { + // Reject using --insecure without --composefs-backend if self.composefs_opts.insecure != false { - anyhow::bail!("--insecure must not be provided without --composefs"); + anyhow::bail!("--insecure must not be provided without --composefs-backend"); } } @@ -1240,7 +1250,7 @@ async fn prepare_install( config_opts: InstallConfigOpts, source_opts: InstallSourceOpts, target_opts: InstallTargetOpts, - composefs_options: Option, + #[cfg(feature = "composefs-backend")] mut composefs_options: InstallComposefsOpts, ) -> Result> { tracing::trace!("Preparing install"); let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority()) @@ -1312,9 +1322,13 @@ async fn prepare_install( } else { false }; + tracing::debug!("Composefs required: {composefs_required}"); - let composefs_options = - composefs_options.or_else(|| composefs_required.then_some(InstallComposefsOpts::default())); + + #[cfg(feature = "composefs-backend")] + if composefs_required { + composefs_options.composefs_backend = true; + } // We need to access devices that are set up by the host udev bootc_mount::ensure_mirrored_host_mount("/dev")?; @@ -1386,10 +1400,7 @@ async fn prepare_install( // Priority: user-specified > bootupd availability > systemd-boot fallback #[cfg(feature = "composefs-backend")] let detected_bootloader = { - if let Some(bootloader) = composefs_options - .as_ref() - .and_then(|opts| opts.bootloader.clone()) - { + if let Some(bootloader) = composefs_options.bootloader.clone() { bootloader } else { if crate::bootloader::supports_bootupd(None)? { @@ -1418,9 +1429,9 @@ async fn prepare_install( tempdir, host_is_container, composefs_required, + detected_bootloader, #[cfg(feature = "composefs-backend")] composefs_options, - detected_bootloader, }); Ok(state) @@ -1592,7 +1603,7 @@ async fn install_to_filesystem_impl( } #[cfg(feature = "composefs-backend")] - if state.composefs_options.is_some() { + if state.composefs_options.composefs_backend { // Load a fd for the mounted target physical root let (id, verity) = initialize_composefs_repository(state, rootfs).await?; @@ -1669,21 +1680,12 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> { anyhow::bail!("Not a block device: {}", block_opts.device); } - #[cfg(feature = "composefs-backend")] - let composefs_arg = if opts.composefs_native { - Some(opts.composefs_opts) - } else { - None - }; - - #[cfg(not(feature = "composefs-backend"))] - let composefs_arg = None; - let state = prepare_install( opts.config_opts, opts.source_opts, opts.target_opts, - composefs_arg, + #[cfg(feature = "composefs-backend")] + opts.composefs_opts, ) .await?; @@ -1916,7 +1918,15 @@ pub(crate) async fn install_to_filesystem( // IMPORTANT: and hence anything that is done before MUST BE IDEMPOTENT. // IMPORTANT: In practice, we should only be gathering information before this point, // IMPORTANT: and not performing any mutations at all. - let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts, None).await?; + let state = prepare_install( + opts.config_opts, + opts.source_opts, + opts.target_opts, + #[cfg(feature = "composefs-backend")] + opts.composefs_opts, + ) + .await?; + // And the last bit of state here is the fsopts, which we also destructure now. let mut fsopts = opts.filesystem_opts; @@ -2184,6 +2194,8 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) -> source_opts: opts.source_opts, target_opts: opts.target_opts, config_opts: opts.config_opts, + #[cfg(feature = "composefs-backend")] + composefs_opts: opts.composefs_opts, }; install_to_filesystem(opts, true, cleanup).await diff --git a/crates/lib/src/spec.rs b/crates/lib/src/spec.rs index b59877e43..e7d964ead 100644 --- a/crates/lib/src/spec.rs +++ b/crates/lib/src/spec.rs @@ -164,12 +164,14 @@ pub struct BootEntryOstree { } /// Bootloader type to determine whether system was booted via Grub or Systemd -#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive( + clap::ValueEnum, Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, +)] pub enum Bootloader { - /// Booted via Grub + /// Use Grub as the booloader #[default] Grub, - /// Booted via Systemd + /// Use SystemdBoot as the bootloader Systemd, } diff --git a/systemd/composefs-finalize-staged.service b/systemd/bootc-finalize-staged.service similarity index 100% rename from systemd/composefs-finalize-staged.service rename to systemd/bootc-finalize-staged.service diff --git a/tmt/tests/examples/bootc-uki/install-grub.sh b/tmt/tests/examples/bootc-uki/install-grub.sh index 885826046..6a9b0bd60 100755 --- a/tmt/tests/examples/bootc-uki/install-grub.sh +++ b/tmt/tests/examples/bootc-uki/install-grub.sh @@ -19,7 +19,7 @@ podman run \ --security-opt label=type:unconfined_t \ "${IMAGE}" \ bootc install to-disk \ - --composefs-native \ + --composefs-backend \ --boot=uki \ --source-imgref="containers-storage:${IMAGE}" \ --target-imgref="${IMAGE}" \ diff --git a/tmt/tests/examples/bootc-uki/install-systemd-boot.sh b/tmt/tests/examples/bootc-uki/install-systemd-boot.sh index 08e92107b..9eca959a8 100755 --- a/tmt/tests/examples/bootc-uki/install-systemd-boot.sh +++ b/tmt/tests/examples/bootc-uki/install-systemd-boot.sh @@ -24,7 +24,7 @@ podman run \ --security-opt label=type:unconfined_t \ "${IMAGE}" \ bootc install to-disk \ - --composefs-native \ + --composefs-backend \ --boot=uki \ --source-imgref="containers-storage:${IMAGE}" \ --target-imgref="${IMAGE}" \