From 86f60381990fa2803f10eb840b2db927d02ab2a1 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Sat, 30 Sep 2023 15:39:08 -0400 Subject: [PATCH] WIP: Add support for `--replace-mode=alongside` for ostree target Ironically our support for `--replace-mode=alongside` breaks when we're targeting an already extant ostree host, because when we first blow away the `/boot` directory, this means the ostree stack loses its knowledge that we're in a booted deployment, and will attempt to GC it... https://github.com/ostreedev/ostree-rs-ext/pull/550/commits/8fa019bfa821303cfb7a7f069ae2320f4c3800fa is a key part of the fix for that. However, a notable improvement we can do here is to grow this whole thing into a real "factory reset" mode, and this will be a compelling answer to https://github.com/coreos/fedora-coreos-tracker/issues/399 To implement this though we need to support configuring the stateroot and not just hardcode `default`. Signed-off-by: Colin Walters --- Cargo.toml | 3 +++ lib/src/install.rs | 47 +++++++++++++++++++++++++++++++++++----------- lib/src/utils.rs | 42 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0c3ab591b..aefd4f424 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +33,6 @@ exclude-crate-paths = [ { name = "libz-sys", exclude = "src/zlib" }, { name = "k8s-openapi", exclude = "src/v1_25" }, { name = "k8s-openapi", exclude = "src/v1_27" }, ] + +[patch.crates-io] +ostree-ext = { path = "../../ostreedev/ostree-rs-ext/lib" } diff --git a/lib/src/install.rs b/lib/src/install.rs index 25363702d..31898a314 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -459,11 +459,17 @@ async fn initialize_ostree_root_from_self( // TODO: make configurable? let stateroot = STATEROOT_DEFAULT; - Task::new_and_run( - "Initializing ostree layout", - "ostree", - ["admin", "init-fs", "--modern", rootfs.as_str()], - )?; + let has_ostree = rootfs_dir.try_exists("ostree/repo")?; + if !has_ostree { + Task::new_and_run( + "Initializing ostree layout", + "ostree", + ["admin", "init-fs", "--modern", rootfs.as_str()], + )?; + } else { + println!("Reusing extant ostree layout"); + let _ = crate::utils::open_dir_remount_rw(rootfs_dir, "sysroot".into())?; + } // Default to avoiding grub2-mkconfig etc., but we need to use zipl on s390x. // TODO: Lower this logic into ostree proper. @@ -482,10 +488,15 @@ async fn initialize_ostree_root_from_self( .quiet() .run()?; } - Task::new("Initializing sysroot", "ostree") - .args(["admin", "os-init", stateroot, "--sysroot", "."]) - .cwd(rootfs_dir)? - .run()?; + let stateroot_exists = rootfs_dir.try_exists(format!("ostree/deploy/{stateroot}"))?; + if stateroot_exists { + anyhow::bail!("Cannot redeploy over extant stateroot {stateroot}"); + } else { + Task::new("Initializing sysroot", "ostree") + .args(["admin", "os-init", stateroot, "--sysroot", "."]) + .cwd(rootfs_dir)? + .run()?; + } // Ensure everything in the ostree repo is labeled state.lsm_label(&rootfs.join("ostree"), "/usr".into(), true)?; @@ -532,6 +543,7 @@ async fn initialize_ostree_root_from_self( options.kargs = Some(kargs.as_slice()); options.target_imgref = Some(&target_imgref); options.proxy_cfg = Some(proxy_cfg); + options.no_clean = has_ostree; println!("Creating initial deployment"); let state = ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)).await?; @@ -845,6 +857,18 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re } let boot_uuid = rootfs.get_boot_uuid()?; + // If we're doing an alongside install, then the /dev bootupd sees needs to be the host's. + // What we probably really want to do here is tunnel in the host's /dev properly, but for now + // just copy /dev/disk + if rootfs.skip_finalize { + if !Utf8Path::new("/dev/disk").try_exists()? { + Task::new_and_run( + "Copying host /dev/disk", + "cp", + ["-a", "/proc/1/root/dev/disk", "/dev/disk"], + )?; + } + } crate::bootloader::install_via_bootupd(&rootfs.device, &rootfs.rootfs, boot_uuid)?; tracing::debug!("Installed bootloader"); @@ -852,7 +876,7 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re if let Some(ignition_file) = state.config_opts.ignition_file.as_deref() { let src = std::fs::File::open(ignition_file) .with_context(|| format!("Opening {ignition_file}"))?; - let bootfs = rootfs.rootfs.join("boot"); + let bootfs = rootfs.rootfs.join(gi"boot"); crate::ignition::write_ignition(&bootfs, &state.config_opts.ignition_hash, &src)?; crate::ignition::enable_firstboot(&bootfs)?; println!("Installed Ignition config from {ignition_file}"); @@ -958,7 +982,8 @@ fn remove_all_in_dir_no_xdev(d: &Dir) -> Result<()> { #[context("Removing boot directory content")] fn clean_boot_directories(rootfs: &Dir) -> Result<()> { - let bootdir = rootfs.open_dir(BOOT).context("Opening /boot")?; + let bootdir = + crate::utils::open_dir_remount_rw(rootfs, BOOT.into()).context("Opening /boot")?; // This should not remove /boot/efi note. remove_all_in_dir_no_xdev(&bootdir)?; if ARCH_USES_EFI { diff --git a/lib/src/utils.rs b/lib/src/utils.rs index 51a73c414..358ada3a3 100644 --- a/lib/src/utils.rs +++ b/lib/src/utils.rs @@ -2,8 +2,12 @@ use std::os::unix::prelude::OsStringExt; use std::process::Command; use anyhow::{Context, Result}; +use camino::Utf8Path; +use cap_std_ext::{cap_std::fs::Dir, prelude::CapStdExtCommandExt}; +use fn_error_context::context; use ostree::glib; use ostree_ext::ostree; +use std::os::fd::AsFd; /// Try to look for keys injected by e.g. rpm-ostree requesting machine-local /// changes; if any are present, return `true`. @@ -32,6 +36,44 @@ pub(crate) fn find_mount_option<'a>( .next() } +/// Try to (heuristically) determine if the provided path is a mount root. +pub(crate) fn is_mountpoint(root: &Dir, path: &Utf8Path) -> Result> { + // https://github.com/systemd/systemd/blob/8fbf0a214e2fe474655b17a4b663122943b55db0/src/basic/mountpoint-util.c#L176 + use rustix::fs::{AtFlags, StatxFlags}; + + // SAFETY(unwrap): We can infallibly convert an i32 into a u64. + let mountroot_flag: u64 = libc::STATX_ATTR_MOUNT_ROOT.try_into().unwrap(); + match rustix::fs::statx( + root.as_fd(), + path.as_std_path(), + AtFlags::NO_AUTOMOUNT | AtFlags::SYMLINK_NOFOLLOW, + StatxFlags::empty(), + ) { + Ok(r) => { + let present = (r.stx_attributes_mask & mountroot_flag) > 0; + Ok(present.then(|| r.stx_attributes & mountroot_flag > 0)) + } + Err(e) if e == rustix::io::Errno::NOSYS => Ok(None), + Err(e) => Err(e.into()), + } +} + +/// Given a target directory, if it's a read-only mount, then remount it writable +#[context("Opening {target} with writable mount")] +pub(crate) fn open_dir_remount_rw(root: &Dir, target: &Utf8Path) -> Result { + if is_mountpoint(root, target)?.unwrap_or_default() { + tracing::debug!("Target {target} is a mountpoint, remounting rw"); + let st = Command::new("mount") + .args(["-o", "remount,rw", target.as_str()]) + .cwd_dir(root.try_clone()?) + .status()?; + if !st.success() { + anyhow::bail!("Failed to remount: {st:?}"); + } + } + root.open_dir(target).map_err(anyhow::Error::new) +} + /// Run a command in the host mount namespace #[allow(dead_code)] pub(crate) fn run_in_host_mountns(cmd: &str) -> Command {