From 6c24c84a0421d5a8b7cd8e84dbb8aa60b3a16f03 Mon Sep 17 00:00:00 2001 From: Jonathan Lebon Date: Thu, 14 Dec 2023 16:49:06 -0500 Subject: [PATCH] Support RPMs installing in `/opt` and `/usr/local` This solves the `/opt` problem by using the new state overlay concept in OSTree: an overlay filesystem is mounted on top of `/usr/lib/opt` and the upper dir is automatically "rebased" whenever new content comes in. Concretely, this means that app state is carried forward, all while allowing the (OSTree-managed) package contents to be updated. We also solve the `/usr/local` problem the same way. The app state issue isn't really present there, but `/usr/local` has traditionally been system state. We want to keep supporting dropping files there all while also supporting shipping OSTree-owned content. See also: https://github.com/ostreedev/ostree/issues/3113 Fixes: https://github.com/coreos/rpm-ostree/issues/233 --- docs/treefile.md | 6 ++++ rust/src/composepost.rs | 80 +++++++++++++++++++++++++++++++++++++---- rust/src/treefile.rs | 3 ++ 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/docs/treefile.md b/docs/treefile.md index 8a8979410b..c03819bc79 100644 --- a/docs/treefile.md +++ b/docs/treefile.md @@ -479,3 +479,9 @@ version of `rpm-ostree`. names to use when substituting variables in yum repo files. The `releasever` variable name is invalid. Use the `releasever` key instead. The `basearch` name is invalid; it is filled in automatically. + * `opt-usrlocal-overlays`: boolean, optional: Defaults to `false`. By + default, `/opt` and `/usr/local` are symlinks to subdirectories in `/ + var`. This prevents the ability to compose with packages that install in + those directories. If enabled, RPMs with `/opt` and `/usr/local` content + are allowed; client-side, both paths are writable overlay directories on. + Requires libostree v2023.9+. diff --git a/rust/src/composepost.rs b/rust/src/composepost.rs index 4d3871be32..a72dc19e3a 100644 --- a/rust/src/composepost.rs +++ b/rust/src/composepost.rs @@ -145,20 +145,32 @@ fn compose_init_rootfs_transient(rootfs_dfd: &cap_std::fs::Dir) -> Result<()> { /// 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_strict(rootfs_dfd: &cap_std::fs::Dir, tmp_is_dir: bool) -> Result<()> { +fn compose_init_rootfs_strict( + rootfs_dfd: &cap_std::fs::Dir, + tmp_is_dir: bool, + opt_state_overlay: bool, +) -> Result<()> { println!("Initializing rootfs"); compose_init_rootfs_base(rootfs_dfd, tmp_is_dir)?; + const OPT_SYMLINK_LEGACY: &str = "var/opt"; + const OPT_SYMLINK_STATEOVERLAY: &str = "usr/lib/opt"; + let opt_symlink = if opt_state_overlay { + OPT_SYMLINK_STATEOVERLAY + } else { + OPT_SYMLINK_LEGACY + }; + // 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"), + let ostree_strict_mode_symlinks: &[(&str, &str)] = &[ + (opt_symlink, "opt"), ("var/srv", "srv"), ("var/mnt", "mnt"), ("run/media", "media"), ]; - OSTREE_STRICT_MODE_SYMLINKS + ostree_strict_mode_symlinks .par_iter() .try_for_each(|&(dest, src)| { rootfs_dfd @@ -212,7 +224,15 @@ pub fn compose_prepare_rootfs( return Ok(()); } - compose_init_rootfs_strict(target_rootfs_dfd, tmp_is_dir)?; + compose_init_rootfs_strict( + target_rootfs_dfd, + tmp_is_dir, + treefile + .parsed + .base + .opt_usrlocal_overlays + .unwrap_or_default(), + )?; println!("Moving /usr to target"); src_rootfs_dfd.rename("usr", target_rootfs_dfd, "usr")?; @@ -606,6 +626,30 @@ fn compose_postprocess_rpmdb(rootfs_dfd: &Dir) -> Result<()> { Ok(()) } +/// Enables ostree-state-overlay@.service for /usr/lib/opt and /usr/local. These symlinks are also used later in the compose process as a way to check that +fn compose_postprocess_state_overlays(rootfs_dfd: &Dir) -> Result<()> { + let mut db = cap_std::fs::DirBuilder::new(); + db.recursive(true); + db.mode(0o755); + let localfs_requires = Path::new("usr/lib/systemd/system/local-fs.target.requires"); + rootfs_dfd.ensure_dir_with(localfs_requires, &db)?; + + const UNITS: &[&str] = &[ + "ostree-state-overlay@usr-lib-opt.service", + "ostree-state-overlay@usr-local.service", + ]; + + UNITS.par_iter().try_for_each(|&unit| { + let target = Path::new("..").join(unit); + let linkpath = localfs_requires.join(unit); + rootfs_dfd + .symlink(target, linkpath) + .with_context(|| format!("Enabling {unit}")) + })?; + + Ok(()) +} + /// Rust portion of rpmostree_treefile_postprocessing() pub fn compose_postprocess( rootfs_dfd: i32, @@ -627,6 +671,15 @@ pub fn compose_postprocess( compose_postprocess_default_target(rootfs, t)?; } + if treefile + .parsed + .base + .opt_usrlocal_overlays + .unwrap_or_default() + { + compose_postprocess_state_overlays(rootfs)?; + } + treefile.write_compose_json(rootfs)?; let etc_guard = crate::core::prepare_tempetc_guard(rootfs_dfd.as_raw_fd())?; @@ -955,6 +1008,17 @@ fn convert_path_to_tmpfiles_d_recurse( Ok(()) } +fn state_overlay_enabled(rootfs_dfd: &cap_std::fs::Dir, state_overlay: &str) -> Result { + let linkname = format!( + "usr/lib/systemd/system/local-fs.target.requires/ostree-state-overlay@{state_overlay}.service" + ); + match rootfs_dfd.symlink_metadata_optional(&linkname)? { + Some(meta) if meta.is_symlink() => Ok(true), + Some(_) => Err(anyhow!("{linkname} is not a symlink")), + None => Ok(false), + } +} + /// Walk over the root filesystem and perform some core conversions /// from RPM conventions to OSTree conventions. /// @@ -969,7 +1033,11 @@ pub fn rootfs_prepare_links(rootfs_dfd: i32, skip_usrlocal: bool) -> CxxResult<( db.recursive(true); if !skip_usrlocal { - if !crate::ostree_prepareroot::transient_root_enabled(rootfs)? { + if state_overlay_enabled(rootfs, "usr-local")? { + // because of the filesystem lua issue (see + // compose_init_rootfs_base()) we need to create this manually + rootfs.ensure_dir_with("usr/local", &db)?; + } else if !crate::ostree_prepareroot::transient_root_enabled(rootfs)? { // Unconditionally drop /usr/local and replace it with a symlink. rootfs .remove_all_optional("usr/local") diff --git a/rust/src/treefile.rs b/rust/src/treefile.rs index 7528b9b0d3..86c3dfaa23 100644 --- a/rust/src/treefile.rs +++ b/rust/src/treefile.rs @@ -427,6 +427,7 @@ fn treefile_merge(dest: &mut TreeComposeConfig, src: &mut TreeComposeConfig) { documentation, boot_location, tmp_is_dir, + opt_usrlocal_overlays, default_target, machineid_compat, releasever, @@ -2531,6 +2532,8 @@ pub(crate) struct BaseComposeConfigFields { pub(crate) boot_location: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) tmp_is_dir: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) opt_usrlocal_overlays: Option, // systemd #[serde(skip_serializing_if = "Option::is_none")]