diff --git a/Cargo.lock b/Cargo.lock index eae664b33..5981ef40b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,11 +312,13 @@ dependencies = [ "anyhow", "bootc-internal-utils", "camino", + "cap-std-ext", "fn-error-context", "indoc", "libc", "rustix 1.0.8", "serde", + "tempfile", "tracing", ] diff --git a/crates/etc-merge/src/lib.rs b/crates/etc-merge/src/lib.rs index b7a09247c..86cea1c8e 100644 --- a/crates/etc-merge/src/lib.rs +++ b/crates/etc-merge/src/lib.rs @@ -20,7 +20,9 @@ use cap_std_ext::dirext::CapStdExtDirExt; use composefs::fsverity::{FsVerityHashValue, Sha256HashValue, Sha512HashValue}; use composefs::generic_tree::{Directory, Inode, Leaf, LeafContent, Stat}; use composefs::tree::ImageError; -use rustix::fs::{AtFlags, Gid, Uid, XattrFlags, lgetxattr, llistxattr, lsetxattr, readlinkat}; +use rustix::fs::{ + AtFlags, Gid, Uid, XattrFlags, lgetxattr, llistxattr, lsetxattr, readlinkat, symlinkat, +}; /// Metadata associated with a file, directory, or symlink entry. #[derive(Debug)] @@ -627,9 +629,8 @@ fn merge_leaf( .context(format!("Deleting {file:?}"))?; if let Some(target) = symlink { - new_etc_fd - .symlink(target.as_ref(), &file) - .context(format!("Creating symlink {file:?}"))?; + // Using rustix's symlinkat here as we might have absolute symlinks which clash with ambient_authority + symlinkat(&**target, new_etc_fd, file).context(format!("Creating symlink {file:?}"))?; } else { current_etc_fd .copy(&file, new_etc_fd, &file) diff --git a/crates/initramfs/src/lib.rs b/crates/initramfs/src/lib.rs index 005917f4b..6867ea664 100644 --- a/crates/initramfs/src/lib.rs +++ b/crates/initramfs/src/lib.rs @@ -112,8 +112,9 @@ pub fn mount_at_wrapper( .with_context(|| format!("Mounting at path {path:?}")) } +/// Wrapper around [`rustix::openat`] #[context("Opening dir {name:?}")] -fn open_dir(dirfd: impl AsFd, name: impl AsRef + Debug) -> Result { +pub fn open_dir(dirfd: impl AsFd, name: impl AsRef + Debug) -> Result { let res = openat( dirfd, name.as_ref(), diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 01d6b3f99..dc1136e9b 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -125,6 +125,19 @@ pub fn get_esp_partition(device: &str) -> Result<(String, Option)> { Ok((esp.node, esp.uuid)) } +pub fn get_sysroot_parent_dev() -> Result { + let sysroot = Utf8PathBuf::from("/sysroot"); + + let fsinfo = inspect_filesystem(&sysroot)?; + let parent_devices = find_parent_devices(&fsinfo.source)?; + + let Some(parent) = parent_devices.into_iter().next() else { + anyhow::bail!("Could not find parent device for mountpoint /sysroot"); + }; + + return Ok(parent); +} + /// Compute SHA256Sum of VMlinuz + Initrd /// /// # Arguments @@ -310,20 +323,12 @@ pub(crate) fn setup_composefs_bls_boot( } BootSetupType::Upgrade((fs, host)) => { - let sysroot = Utf8PathBuf::from("/sysroot"); - - let fsinfo = inspect_filesystem(&sysroot)?; - let parent_devices = find_parent_devices(&fsinfo.source)?; - - let Some(parent) = parent_devices.into_iter().next() else { - anyhow::bail!("Could not find parent device for mountpoint /sysroot"); - }; - + let sysroot_parent = get_sysroot_parent_dev()?; let bootloader = host.require_composefs_booted()?.bootloader.clone(); ( Utf8PathBuf::from("/sysroot"), - get_esp_partition(&parent)?.0, + get_esp_partition(&sysroot_parent)?.0, [ format!("root=UUID={DPS_UUID}"), RW_KARG.to_string(), @@ -554,15 +559,9 @@ pub(crate) fn setup_composefs_uki_boot( BootSetupType::Upgrade(..) => { let sysroot = Utf8PathBuf::from("/sysroot"); + let sysroot_parent = get_sysroot_parent_dev()?; - let fsinfo = inspect_filesystem(&sysroot)?; - let parent_devices = find_parent_devices(&fsinfo.source)?; - - let Some(parent) = parent_devices.into_iter().next() else { - anyhow::bail!("Could not find parent device for mountpoint /sysroot"); - }; - - (sysroot, get_esp_partition(&parent)?.0, None) + (sysroot, get_esp_partition(&sysroot_parent)?.0, None) } }; diff --git a/crates/lib/src/bootc_composefs/finalize.rs b/crates/lib/src/bootc_composefs/finalize.rs new file mode 100644 index 000000000..22ee0ea96 --- /dev/null +++ b/crates/lib/src/bootc_composefs/finalize.rs @@ -0,0 +1,129 @@ +use std::path::Path; + +use crate::bootc_composefs::boot::{get_esp_partition, get_sysroot_parent_dev, BootType}; +use crate::bootc_composefs::rollback::{rename_exchange_bls_entries, rename_exchange_user_cfg}; +use crate::spec::Bootloader; +use crate::{ + bootc_composefs::status::composefs_deployment_status, composefs_consts::STATE_DIR_ABS, +}; +use anyhow::{Context, Result}; +use bootc_initramfs_setup::{mount_composefs_image, open_dir}; +use bootc_mount::tempmount::TempMount; +use cap_std_ext::cap_std::{ambient_authority, fs::Dir}; +use cap_std_ext::dirext::CapStdExtDirExt; +use etc_merge::{compute_diff, merge, traverse_etc}; +use rustix::fs::{fsync, renameat, CWD}; +use rustix::path::Arg; + +use fn_error_context::context; + +pub(crate) async fn composefs_native_finalize() -> Result<()> { + let host = composefs_deployment_status().await?; + + let booted_composefs = host.require_composefs_booted()?; + + let Some(staged_depl) = host.status.staged.as_ref() else { + tracing::debug!("No staged deployment found"); + return Ok(()); + }; + + let staged_composefs = staged_depl.composefs.as_ref().ok_or(anyhow::anyhow!( + "Staged deployment is not a composefs deployment" + ))?; + + // Mount the booted EROFS image to get pristine etc + let sysroot = open_dir(CWD, "/sysroot")?; + let composefs_fd = mount_composefs_image(&sysroot, &booted_composefs.verity, false)?; + + let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?; + + // Perform the /etc merge + let pristine_etc = + Dir::open_ambient_dir(erofs_tmp_mnt.dir.path().join("etc"), ambient_authority())?; + let current_etc = Dir::open_ambient_dir("/etc", ambient_authority())?; + + let new_etc_path = Path::new(STATE_DIR_ABS) + .join(&staged_composefs.verity) + .join("etc"); + + let new_etc = Dir::open_ambient_dir(new_etc_path, ambient_authority())?; + + let (pristine_files, current_files, new_files) = + traverse_etc(&pristine_etc, ¤t_etc, &new_etc)?; + + let diff = compute_diff(&pristine_files, ¤t_files)?; + merge(¤t_etc, ¤t_files, &new_etc, &new_files, diff)?; + + // Unmount EROFS + drop(erofs_tmp_mnt); + + let sysroot_parent = get_sysroot_parent_dev()?; + // NOTE: Assumption here that ESP will always be present + let (esp_part, ..) = get_esp_partition(&sysroot_parent)?; + + let esp_mount = TempMount::mount_dev(&esp_part)?; + let boot_dir = Dir::open_ambient_dir("/sysroot/boot", ambient_authority()) + .context("Opening sysroot/boot")?; + + // NOTE: Assuming here we won't have two bootloaders at the same time + match booted_composefs.bootloader { + Bootloader::Grub => match staged_composefs.boot_type { + BootType::Bls => { + 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)?, + }, + + 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 => rename_staged_uki_entries(&esp_mount.fd)?, + }, + }; + + Ok(()) +} + +#[context("Grub: Finalizing staged UKI")] +fn finalize_staged_grub_uki(esp_mount: &Dir, boot_fd: &Dir) -> Result<()> { + rename_staged_uki_entries(esp_mount)?; + + let entries_dir = boot_fd.open_dir("grub2")?; + rename_exchange_user_cfg(&entries_dir)?; + + let entries_dir = entries_dir.reopen_as_ownedfd()?; + fsync(entries_dir).context("fsync")?; + + Ok(()) +} + +#[context("Renaming staged UKI entries")] +fn rename_staged_uki_entries(esp_mount: &Dir) -> Result<()> { + for entry in esp_mount.entries()? { + let entry = entry?; + + let filename = entry.file_name(); + let filename = filename.as_str()?; + + if !filename.ends_with(".staged") { + continue; + } + + renameat( + &esp_mount, + filename, + &esp_mount, + // SAFETY: We won't reach here if not for the above condition + filename.strip_suffix(".staged").unwrap(), + ) + .context("Renaming {filename}")?; + } + + let esp_mount = esp_mount.reopen_as_ownedfd()?; + fsync(esp_mount).context("fsync")?; + + Ok(()) +} diff --git a/crates/lib/src/bootc_composefs/mod.rs b/crates/lib/src/bootc_composefs/mod.rs index fee03cee9..c19dbfb77 100644 --- a/crates/lib/src/bootc_composefs/mod.rs +++ b/crates/lib/src/bootc_composefs/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod boot; +pub(crate) mod finalize; pub(crate) mod repo; pub(crate) mod rollback; pub(crate) mod state; diff --git a/crates/lib/src/bootc_composefs/rollback.rs b/crates/lib/src/bootc_composefs/rollback.rs index aa948e835..0ea23b7cd 100644 --- a/crates/lib/src/bootc_composefs/rollback.rs +++ b/crates/lib/src/bootc_composefs/rollback.rs @@ -12,11 +12,60 @@ use crate::bootc_composefs::status::{composefs_deployment_status, get_sorted_bls use crate::{ bootc_composefs::{boot::get_efi_uuid_source, status::get_sorted_uki_boot_entries}, composefs_consts::{ - BOOT_LOADER_ENTRIES, ROLLBACK_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_ROLLBACK, + BOOT_LOADER_ENTRIES, STAGED_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_STAGED, }, spec::BootOrder, }; +pub(crate) fn rename_exchange_user_cfg(entries_dir: &Dir) -> Result<()> { + tracing::debug!("Atomically exchanging {USER_CFG_STAGED} and {USER_CFG}"); + renameat_with( + &entries_dir, + USER_CFG_STAGED, + &entries_dir, + USER_CFG, + RenameFlags::EXCHANGE, + ) + .context("renameat")?; + + tracing::debug!("Removing {USER_CFG_STAGED}"); + rustix::fs::unlinkat(&entries_dir, USER_CFG_STAGED, AtFlags::empty()).context("unlinkat")?; + + tracing::debug!("Syncing to disk"); + let entries_dir = entries_dir + .reopen_as_ownedfd() + .context(format!("Reopening entries dir as owned fd"))?; + + fsync(entries_dir).context(format!("fsync entries dir"))?; + + Ok(()) +} + +pub(crate) fn rename_exchange_bls_entries(entries_dir: &Dir) -> Result<()> { + tracing::debug!("Atomically exchanging {STAGED_BOOT_LOADER_ENTRIES} and {BOOT_LOADER_ENTRIES}"); + renameat_with( + &entries_dir, + STAGED_BOOT_LOADER_ENTRIES, + &entries_dir, + BOOT_LOADER_ENTRIES, + RenameFlags::EXCHANGE, + ) + .context("renameat")?; + + tracing::debug!("Removing {STAGED_BOOT_LOADER_ENTRIES}"); + rustix::fs::unlinkat(&entries_dir, STAGED_BOOT_LOADER_ENTRIES, AtFlags::REMOVEDIR) + .context("unlinkat")?; + + tracing::debug!("Syncing to disk"); + let entries_dir = entries_dir + .reopen_as_ownedfd() + .with_context(|| format!("Reopening /sysroot/boot/loader as owned fd"))?; + + fsync(entries_dir).context("fsync")?; + + Ok(()) +} + #[context("Rolling back UKI")] pub(crate) fn rollback_composefs_uki() -> Result<()> { let user_cfg_path = PathBuf::from("/sysroot/boot/grub2"); @@ -45,31 +94,10 @@ pub(crate) fn rollback_composefs_uki() -> Result<()> { .with_context(|| format!("Opening {user_cfg_path:?}"))?; entries_dir - .atomic_write(USER_CFG_ROLLBACK, buffer) - .with_context(|| format!("Writing to {USER_CFG_ROLLBACK}"))?; - - tracing::debug!("Atomically exchanging for {USER_CFG_ROLLBACK} and {USER_CFG}"); - renameat_with( - &entries_dir, - USER_CFG_ROLLBACK, - &entries_dir, - USER_CFG, - RenameFlags::EXCHANGE, - ) - .context("renameat")?; - - tracing::debug!("Removing {USER_CFG_ROLLBACK}"); - rustix::fs::unlinkat(&entries_dir, USER_CFG_ROLLBACK, AtFlags::empty()).context("unlinkat")?; - - tracing::debug!("Syncing to disk"); - fsync( - entries_dir - .reopen_as_ownedfd() - .with_context(|| format!("Reopening {user_cfg_path:?} as owned fd"))?, - ) - .with_context(|| format!("fsync {user_cfg_path:?}"))?; + .atomic_write(USER_CFG_STAGED, buffer) + .with_context(|| format!("Writing to {USER_CFG_STAGED}"))?; - Ok(()) + rename_exchange_user_cfg(&entries_dir) } #[context("Rolling back BLS")] @@ -93,9 +121,7 @@ pub(crate) fn rollback_composefs_bls() -> Result<()> { assert!(all_configs.len() == 2); // Write these - let dir_path = PathBuf::from(format!( - "/sysroot/boot/loader/{ROLLBACK_BOOT_LOADER_ENTRIES}", - )); + let dir_path = PathBuf::from(format!("/sysroot/boot/loader/{STAGED_BOOT_LOADER_ENTRIES}",)); create_dir_all(&dir_path).with_context(|| format!("Failed to create dir: {dir_path:?}"))?; let rollback_entries_dir = @@ -124,30 +150,7 @@ pub(crate) fn rollback_composefs_bls() -> Result<()> { let dir = Dir::open_ambient_dir("/sysroot/boot/loader", cap_std::ambient_authority()) .context("Opening loader dir")?; - tracing::debug!( - "Atomically exchanging for {ROLLBACK_BOOT_LOADER_ENTRIES} and {BOOT_LOADER_ENTRIES}" - ); - renameat_with( - &dir, - ROLLBACK_BOOT_LOADER_ENTRIES, - &dir, - BOOT_LOADER_ENTRIES, - RenameFlags::EXCHANGE, - ) - .context("renameat")?; - - tracing::debug!("Removing {ROLLBACK_BOOT_LOADER_ENTRIES}"); - rustix::fs::unlinkat(&dir, ROLLBACK_BOOT_LOADER_ENTRIES, AtFlags::empty()) - .context("unlinkat")?; - - tracing::debug!("Syncing to disk"); - fsync( - dir.reopen_as_ownedfd() - .with_context(|| format!("Reopening /sysroot/boot/loader as owned fd"))?, - ) - .context("fsync")?; - - Ok(()) + rename_exchange_bls_entries(&dir) } #[context("Rolling back composefs")] diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index b1bb8db15..ae02d242f 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -3,6 +3,7 @@ use std::{fs::create_dir_all, process::Command}; use anyhow::{Context, Result}; use bootc_kernel_cmdline::Cmdline; +use bootc_mount::tempmount::TempMount; use bootc_utils::CommandRunExt; use camino::Utf8PathBuf; use cap_std_ext::{cap_std, dirext::CapStdExtDirExt}; @@ -11,8 +12,7 @@ use fn_error_context::context; use ostree_ext::container::deploy::ORIGIN_CONTAINER; use rustix::{ - fs::{open, Mode, OFlags, CWD}, - mount::{unmount, UnmountFlags}, + fs::{open, Mode, OFlags}, path::Arg, }; @@ -71,22 +71,17 @@ pub(crate) fn copy_etc_to_state( let composefs_fd = bootc_initramfs_setup::mount_composefs_image(&sysroot_fd, &erofs_id, false)?; - let tempdir = tempfile::tempdir().context("Creating tempdir")?; - - bootc_initramfs_setup::mount_at_wrapper(composefs_fd, CWD, tempdir.path())?; + let tempdir = TempMount::mount_fd(composefs_fd)?; // TODO: Replace this with a function to cap_std_ext let cp_ret = Command::new("cp") .args([ "-a", - &format!("{}/etc/.", tempdir.path().as_str()?), + &format!("{}/etc/.", tempdir.dir.path().as_str()?), &format!("{state_path}/etc/."), ]) .run_capture_stderr(); - // Unmount regardless of copy succeeding - unmount(tempdir.path(), UnmountFlags::DETACH).context("Unmounting composefs")?; - cp_ret } diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index c955be7c4..12ea4a345 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -31,8 +31,8 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "composefs-backend")] use crate::bootc_composefs::{ - rollback::composefs_rollback, status::composefs_booted, switch::switch_composefs, - update::upgrade_composefs, + finalize::composefs_native_finalize, rollback::composefs_rollback, status::composefs_booted, + switch::switch_composefs, update::upgrade_composefs, }; use crate::deploy::RequiredHostSpec; use crate::lints; @@ -712,6 +712,8 @@ pub(crate) enum Opt { #[clap(hide(true))] #[cfg(feature = "docgen")] Man(ManOpts), + #[cfg(feature = "composefs-backend")] + ComposefsFinalizeStaged, } /// Ensure we've entered a mount namespace, so that we can remount @@ -1575,6 +1577,9 @@ async fn run_from_opt(opt: Opt) -> Result<()> { Ok(()) } }, + + #[cfg(feature = "composefs-backend")] + Opt::ComposefsFinalizeStaged => composefs_native_finalize().await, } } diff --git a/crates/lib/src/composefs_consts.rs b/crates/lib/src/composefs_consts.rs index 238d9854f..3c18287a1 100644 --- a/crates/lib/src/composefs_consts.rs +++ b/crates/lib/src/composefs_consts.rs @@ -26,12 +26,8 @@ pub(crate) const ORIGIN_KEY_BOOT_DIGEST: &str = "digest"; pub(crate) const BOOT_LOADER_ENTRIES: &str = "entries"; /// Filename for staged boot loader entries pub(crate) const STAGED_BOOT_LOADER_ENTRIES: &str = "entries.staged"; -/// Filename for rollback boot loader entries -pub(crate) const ROLLBACK_BOOT_LOADER_ENTRIES: &str = STAGED_BOOT_LOADER_ENTRIES; /// Filename for grub user config pub(crate) const USER_CFG: &str = "user.cfg"; /// Filename for staged grub user config pub(crate) const USER_CFG_STAGED: &str = "user.cfg.staged"; -/// Filename for rollback grub user config -pub(crate) const USER_CFG_ROLLBACK: &str = USER_CFG_STAGED; diff --git a/crates/mount/Cargo.toml b/crates/mount/Cargo.toml index 8e29bb5f2..a8a7475a3 100644 --- a/crates/mount/Cargo.toml +++ b/crates/mount/Cargo.toml @@ -20,6 +20,8 @@ libc = { workspace = true } rustix = { workspace = true } serde = { workspace = true, features = ["derive"] } tracing = { workspace = true } +tempfile = { workspace = true } +cap-std-ext = { workspace = true } [dev-dependencies] indoc = { workspace = true } diff --git a/crates/mount/src/mount.rs b/crates/mount/src/mount.rs index 71133d9ed..1e5d56e47 100644 --- a/crates/mount/src/mount.rs +++ b/crates/mount/src/mount.rs @@ -22,6 +22,8 @@ use rustix::{ }; use serde::Deserialize; +pub mod tempmount; + /// Well known identifier for pid 1 pub const PID1: Pid = const { match Pid::from_raw(1) { diff --git a/crates/mount/src/tempmount.rs b/crates/mount/src/tempmount.rs new file mode 100644 index 000000000..56a3a6493 --- /dev/null +++ b/crates/mount/src/tempmount.rs @@ -0,0 +1,76 @@ +use std::os::fd::AsFd; + +use anyhow::{Context, Result}; + +use camino::Utf8Path; +use cap_std_ext::cap_std::{ambient_authority, fs::Dir}; +use fn_error_context::context; +use rustix::mount::{move_mount, unmount, MoveMountFlags, UnmountFlags}; + +pub struct TempMount { + pub dir: tempfile::TempDir, + pub fd: Dir, +} + +impl TempMount { + /// Mount device/partition on a tempdir which will be automatically unmounted on drop + #[context("Mounting {dev}")] + pub fn mount_dev(dev: &str) -> Result { + let tempdir = tempfile::TempDir::new()?; + + let utf8path = Utf8Path::from_path(tempdir.path()) + .ok_or(anyhow::anyhow!("Failed to convert path to UTF-8 Path"))?; + + crate::mount(dev, utf8path)?; + + let fd = Dir::open_ambient_dir(tempdir.path(), ambient_authority()) + .with_context(|| format!("Opening {:?}", tempdir.path())); + + let fd = match fd { + Ok(fd) => fd, + Err(e) => { + unmount(tempdir.path(), UnmountFlags::DETACH)?; + Err(e)? + } + }; + + Ok(Self { dir: tempdir, fd }) + } + + /// Mount and fd acquired with `open_tree` like syscall + #[context("Mounting fd")] + pub fn mount_fd(mnt_fd: impl AsFd) -> Result { + let tempdir = tempfile::TempDir::new()?; + + move_mount( + mnt_fd.as_fd(), + "", + rustix::fs::CWD, + tempdir.path(), + MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH, + ) + .context("move_mount")?; + + let fd = Dir::open_ambient_dir(tempdir.path(), ambient_authority()) + .with_context(|| format!("Opening {:?}", tempdir.path())); + + let fd = match fd { + Ok(fd) => fd, + Err(e) => { + unmount(tempdir.path(), UnmountFlags::DETACH)?; + Err(e)? + } + }; + + Ok(Self { dir: tempdir, fd }) + } +} + +impl Drop for TempMount { + fn drop(&mut self) { + match unmount(self.dir.path(), UnmountFlags::DETACH) { + Ok(_) => {} + Err(e) => tracing::warn!("Failed to unmount tempdir: {e:?}"), + } + } +} diff --git a/systemd/composefs-finalize-staged.service b/systemd/composefs-finalize-staged.service new file mode 100644 index 000000000..60e3f0683 --- /dev/null +++ b/systemd/composefs-finalize-staged.service @@ -0,0 +1,46 @@ +# Copyright (C) 2018 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +# For some implementation discussion, see: +# https://lists.freedesktop.org/archives/systemd-devel/2018-March/040557.html +[Unit] +Description=Composefs Finalize Staged Deployment +Documentation=man:bootc(1) +DefaultDependencies=no + +RequiresMountsFor=/sysroot +After=local-fs.target +Before=basic.target final.target +# We want to make sure the transaction logs are persisted to disk: +# https://bugzilla.redhat.com/show_bug.cgi?id=1751272 +After=systemd-journal-flush.service +Conflicts=final.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStop=/usr/bin/bootc composefs-finalize-staged +# This is a quite long timeout intentionally; the failure mode +# here is that people don't get an upgrade. We need to handle +# cases with slow rotational media, etc. +TimeoutStopSec=5m +# Bootc should never touch /var at all...except, we need to remove +# the /var/.updated flag, so we can't just `InaccessiblePaths=/var` right now. +# For now, let's at least use ProtectHome just so we have some sandboxing +# of that. +ProtectHome=yes +# And we shouldn't affect the current deployment's /etc. +ReadOnlyPaths=/etc +# We write to /sysroot and /boot of course.