From e804dab176860436653570d0081d60079bb09d88 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 7 Dec 2023 17:34:13 -0500 Subject: [PATCH] composepost: Support rootfs.transient=yes This pairs with https://github.com/ostreedev/ostree/pull/3114 Basically we want to detect the case where the OS has opted-in to this new mode and *not* symlink things. I originally thought we could implement this by just moving all the toplevel directories, but then I hit on the fact that because the `filesystem` package is creating all the toplevel directories in lua script which we ignore...that doesn't work. So we need to keep making them by hand. --- rust/src/composepost.rs | 115 +++++++++++++++++++++++++-------- rust/src/lib.rs | 1 + rust/src/ostree_prepareroot.rs | 50 ++++++++++++++ 3 files changed, 138 insertions(+), 28 deletions(-) create mode 100644 rust/src/ostree_prepareroot.rs diff --git a/rust/src/composepost.rs b/rust/src/composepost.rs index cd8e6dab01..0bf95b9d28 100644 --- a/rust/src/composepost.rs +++ b/rust/src/composepost.rs @@ -43,6 +43,8 @@ use std::process::Stdio; /// location to `/usr/lib/`. pub(crate) static COMPAT_VARLIB_SYMLINKS: &[&str] = &["alternatives", "vagrant"]; +const DEFAULT_DIRMODE: u32 = 0o755; + /* See rpmostree-core.h */ const RPMOSTREE_BASE_RPMDB: &str = "usr/lib/sysimage/rpm-ostree-base-db"; pub(crate) const RPMOSTREE_RPMDB_LOCATION: &str = "usr/share/rpm"; @@ -59,26 +61,15 @@ fn dir_move_if_exists(src: &cap_std::fs::Dir, dest: &cap_std::fs::Dir, name: &st /// Initialize an ostree-oriented root filesystem. /// -/// This is hardcoded; in the future we may make more things configurable, -/// but the goal is for all state to be in `/etc` and `/var`. -#[context("Initializing rootfs")] -fn compose_init_rootfs(rootfs_dfd: &cap_std::fs::Dir, tmp_is_dir: bool) -> Result<()> { - println!("Initializing rootfs"); - - let default_dirmode: u32 = 0o755; - let default_dirbuilder = &dirbuilder_from_mode(default_dirmode); - let default_dirmode = cap_std::fs::Permissions::from_mode(default_dirmode); - +/// Now unfortunately today, we're not generating toplevel filesystem entries +/// because the `filesystem` package does it from Lua code, which we don't run. +/// (See rpmostree-core.cxx) +#[context("Initializing rootfs (base)")] +fn compose_init_rootfs_base(rootfs_dfd: &cap_std::fs::Dir, tmp_is_dir: bool) -> Result<()> { const TOPLEVEL_DIRS: &[&str] = &["dev", "proc", "run", "sys", "var", "sysroot"]; - const TOPLEVEL_SYMLINKS: &[(&str, &str)] = &[ - ("var/opt", "opt"), - ("var/srv", "srv"), - ("var/mnt", "mnt"), - ("var/roothome", "root"), - ("var/home", "home"), - ("run/media", "media"), - ("sysroot/ostree", "ostree"), - ]; + + let default_dirbuilder = &dirbuilder_from_mode(DEFAULT_DIRMODE); + let default_dirmode = cap_std::fs::Permissions::from_mode(DEFAULT_DIRMODE); rootfs_dfd .set_permissions(".", default_dirmode) @@ -90,11 +81,6 @@ fn compose_init_rootfs(rootfs_dfd: &cap_std::fs::Dir, tmp_is_dir: bool) -> Resul .with_context(|| format!("Creating {d}")) .map(|_: bool| ()) })?; - TOPLEVEL_SYMLINKS.par_iter().try_for_each(|&(dest, src)| { - rootfs_dfd - .symlink(dest, src) - .with_context(|| format!("Creating {src}")) - })?; if tmp_is_dir { let tmp_mode = 0o1777; @@ -108,15 +94,71 @@ fn compose_init_rootfs(rootfs_dfd: &cap_std::fs::Dir, tmp_is_dir: bool) -> Resul rootfs_dfd.symlink("sysroot/tmp", "tmp")?; } + rootfs_dfd + .symlink("sysroot/ostree", "ostree") + .context("Symlinking ostree -> sysroot/ostree")?; + + Ok(()) +} + +/// Add extra toplevel directories. +#[context("Initializing rootfs (base)")] +fn compose_add_rootfs_extra(rootfs_dfd: &cap_std::fs::Dir) -> Result<()> { + const EXTRA_TOPLEVEL_DIRS: &[&str] = &["opt", "media", "mnt"]; + + let default_dirbuilder = &dirbuilder_from_mode(DEFAULT_DIRMODE); + EXTRA_TOPLEVEL_DIRS.par_iter().try_for_each(|&d| { + rootfs_dfd + .ensure_dir_with(d, default_dirbuilder) + .with_context(|| format!("Creating {d}")) + .map(|_: bool| ()) + })?; + + Ok(()) +} + +/// Initialize an ostree-oriented root filesystem. +/// +/// This is hardcoded; in the future we may make more things configurable, +/// but the goal is for all state to be in `/etc` and `/var`. +#[context("Initializing rootfs")] +fn compose_init_rootfs_ostree_strict( + rootfs_dfd: &cap_std::fs::Dir, + tmp_is_dir: bool, +) -> Result<()> { + println!("Initializing rootfs"); + + compose_init_rootfs_base(rootfs_dfd, tmp_is_dir)?; + + // This is used in the case where we don't have a transient rootfs; redirect + // these toplevel directories underneath /var. + const OSTREE_STRICT_MODE_SYMLINKS: &[(&str, &str)] = &[ + ("var/opt", "opt"), + ("var/srv", "srv"), + ("var/mnt", "mnt"), + ("var/roothome", "root"), + ("var/home", "home"), + ("run/media", "media"), + ]; + OSTREE_STRICT_MODE_SYMLINKS + .par_iter() + .try_for_each(|&(dest, src)| { + rootfs_dfd + .symlink(dest, src) + .with_context(|| format!("Creating {src}")) + })?; + Ok(()) } /// Prepare rootfs for commit. /// -/// Initialize a basic root filesystem in @target_root_dfd, then walk over the +/// In the default mode, we initialize a basic root filesystem in @target_root_dfd, then walk over the /// root filesystem in @src_rootfs_fd and take the basic content (/usr, /boot, /var) /// and cherry pick only specific bits of the rest of the toplevel like compatibility /// symlinks (e.g. /lib64 -> /usr/lib64) if they exist. +/// +/// However, if the rootfs is setup as transient, then we just copy everything. #[context("Preparing rootfs for commit")] pub fn compose_prepare_rootfs( src_rootfs_dfd: i32, @@ -127,7 +169,24 @@ pub fn compose_prepare_rootfs( let target_rootfs_dfd = unsafe { &ffi_dirfd(target_rootfs_dfd)? }; let tmp_is_dir = treefile.parsed.base.tmp_is_dir.unwrap_or_default(); - compose_init_rootfs(target_rootfs_dfd, tmp_is_dir)?; + + if crate::ostree_prepareroot::transient_root_enabled(src_rootfs_dfd)? { + println!("Target has transient root enabled"); + for entry in src_rootfs_dfd.entries()? { + let entry = entry?; + let name = entry.file_name(); + src_rootfs_dfd + .rename(&name, target_rootfs_dfd, &name) + .with_context(|| format!("Moving {name:?}"))?; + } + // Now unfortunately because we're not executing the `filesystem` packages' + // lua script, we need to make base directories. + compose_init_rootfs_base(target_rootfs_dfd, tmp_is_dir)?; + compose_add_rootfs_extra(target_rootfs_dfd)?; + return Ok(()); + } + + compose_init_rootfs_ostree_strict(target_rootfs_dfd, tmp_is_dir)?; println!("Moving /usr to target"); src_rootfs_dfd.rename("usr", target_rootfs_dfd, "usr")?; @@ -1312,13 +1371,13 @@ OSTREE_VERSION='33.4' fn test_init_rootfs() -> Result<()> { { let rootfs = cap_tempfile::tempdir(cap_tempfile::ambient_authority())?; - compose_init_rootfs(&rootfs, false)?; + compose_init_rootfs_ostree_strict(&rootfs, false)?; let target = rootfs.read_link("tmp").unwrap(); assert_eq!(target, Path::new("sysroot/tmp")); } { let rootfs = cap_tempfile::tempdir(cap_tempfile::ambient_authority())?; - compose_init_rootfs(&rootfs, true)?; + compose_init_rootfs_ostree_strict(&rootfs, true)?; let tmpdir_meta = rootfs.metadata("tmp").unwrap(); assert!(tmpdir_meta.is_dir()); assert_eq!(tmpdir_meta.permissions().mode() & 0o7777, 0o1777); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index b18cd89122..c11ed0831c 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -970,6 +970,7 @@ pub(crate) use self::modularity::*; mod nameservice; mod normalization; mod origin; +mod ostree_prepareroot; pub(crate) use self::origin::*; mod passwd; use passwd::*; diff --git a/rust/src/ostree_prepareroot.rs b/rust/src/ostree_prepareroot.rs new file mode 100644 index 0000000000..a6ecef18cd --- /dev/null +++ b/rust/src/ostree_prepareroot.rs @@ -0,0 +1,50 @@ +//! Logic related to parsing ostree-prepare-root.conf. +//! + +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::io::BufReader; +use std::io::Read; + +use anyhow::{Context, Result}; +use camino::Utf8Path; +use cap_std::fs::Dir; +use cap_std_ext::dirext::CapStdExtDirExt; +use ostree_ext::glib; +use ostree_ext::keyfileext::KeyFileExt; + +pub(crate) const CONF_PATH: &str = "ostree/prepare-root.conf"; + +pub(crate) fn load_config(rootfs: &Dir) -> Result> { + let kf = glib::KeyFile::new(); + for path in ["etc", "usr/lib"].into_iter().map(Utf8Path::new) { + let path = &path.join(CONF_PATH); + if let Some(fd) = rootfs + .open_optional(path) + .with_context(|| format!("Opening {path}"))? + { + let mut fd = BufReader::new(fd); + let mut buf = String::new(); + fd.read_to_string(&mut buf) + .with_context(|| format!("Reading {path}"))?; + kf.load_from_data(&buf, glib::KeyFileFlags::NONE) + .with_context(|| format!("Parsing {path}"))?; + tracing::debug!("Loaded {path}"); + return Ok(Some(kf)); + } + } + tracing::debug!("No {CONF_PATH} found"); + Ok(None) +} + +/// Query whether the target root has the `root.transient` key +/// which sets up a transient overlayfs. +pub(crate) fn transient_root_enabled(rootfs: &Dir) -> Result { + if let Some(config) = load_config(rootfs)? { + Ok(config + .optional_bool("root", "transient")? + .unwrap_or_default()) + } else { + Ok(false) + } +}