From d7e29757c95075abe12dcb69d07a9b6998c843fd Mon Sep 17 00:00:00 2001 From: Jonathan Lebon Date: Thu, 14 Dec 2023 16:49:02 -0500 Subject: [PATCH 1/4] importer: Allow `/usr/local` RPM content Obviously, this alone is not enough to expose that content but it's a start. Currently as is, it'll get nuked when we replace `/usr/local` by a symlink in postprocessing. A future patch will address that part. Part of https://github.com/coreos/rpm-ostree/issues/233. --- rust/src/importer.rs | 4 ++-- tests/vmcheck/test-layering-basic-1.sh | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/rust/src/importer.rs b/rust/src/importer.rs index c5ec4cb896..dea0dabc02 100644 --- a/rust/src/importer.rs +++ b/rust/src/importer.rs @@ -392,7 +392,7 @@ fn path_is_ostree_compliant(path: &str) -> bool { return true; } - if path.starts_with("/usr/") && !path.starts_with("/usr/local") { + if path.starts_with("/usr/") { return true; } @@ -491,7 +491,7 @@ mod tests { assert_eq!(path_is_ostree_compliant(entry), false, "{}", entry); } - let denied_cases = &["/var", "/etc", "/var/run", "/usr/local", "", "./", "usr/"]; + let denied_cases = &["/var", "/etc", "/var/run", "", "./", "usr/"]; for entry in denied_cases { assert_eq!(path_is_ostree_compliant(entry), false, "{}", entry); } diff --git a/tests/vmcheck/test-layering-basic-1.sh b/tests/vmcheck/test-layering-basic-1.sh index d35f7ad289..edeae21bda 100755 --- a/tests/vmcheck/test-layering-basic-1.sh +++ b/tests/vmcheck/test-layering-basic-1.sh @@ -79,7 +79,8 @@ vm_build_rpm test-usrlocal \ if vm_rpmostree install test-usrlocal-1.0 2>err.txt; then assert_not_reached "Was able to install a package in /usr/local/" fi -assert_file_has_content err.txt "Unsupported path; see https://github.com/projectatomic/rpm-ostree/issues/233" +# this error is worse now than it used to be now that we experimentally *do* support /usr/local RPMs +assert_file_has_content err.txt "opendir(local): No such file or directory" echo "ok failed to install in /usr/local" From 4d941ee599173febb3a3d93b56b008336eb5ab01 Mon Sep 17 00:00:00 2001 From: Jonathan Lebon Date: Thu, 14 Dec 2023 16:49:06 -0500 Subject: [PATCH 2/4] 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 | 82 ++++++++++++++++++++++++++-- rust/src/treefile.rs | 3 + tests/compose/test-state-overlays.sh | 45 +++++++++++++++ 4 files changed, 130 insertions(+), 6 deletions(-) create mode 100755 tests/compose/test-state-overlays.sh 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..ddd4a4c088 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,32 @@ 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 (and client-side composes) +/// as a way to check that state overlays are turned on. +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 +673,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 +1010,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 +1035,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")] diff --git a/tests/compose/test-state-overlays.sh b/tests/compose/test-state-overlays.sh new file mode 100755 index 0000000000..a5a6bfa236 --- /dev/null +++ b/tests/compose/test-state-overlays.sh @@ -0,0 +1,45 @@ +#!/bin/bash +set -xeuo pipefail + +dn=$(cd "$(dirname "$0")" && pwd) +# shellcheck source=libcomposetest.sh +. "${dn}/libcomposetest.sh" + +# Add a local rpm-md repo so we can mutate local test packages +treefile_append "repos" '["test-repo"]' + +# An RPM that installs in /opt +build_rpm test-opt \ + install "mkdir -p %{buildroot}/opt/megacorp/bin + install %{name} %{buildroot}/opt/megacorp/bin" \ + files "/opt/megacorp" + +# An RPM that installs in /usr/local +build_rpm test-usr-local \ + install "mkdir -p %{buildroot}/usr/local/bin + install %{name} %{buildroot}/usr/local/bin" \ + files "/usr/local/bin/%{name}" + +echo gpgcheck=0 >> yumrepo.repo +ln "$PWD/yumrepo.repo" config/yumrepo.repo + +# the top-level manifest doesn't have any packages, so just set it +treefile_append "packages" '["test-opt", "test-usr-local"]' + +# enable state overlays +treefile_set "opt-usrlocal-overlays" 'True' + +runcompose + +# shellcheck disable=SC2154 +ostree --repo="${repo}" ls -R "${treeref}" /usr/lib/opt > opt.txt +assert_file_has_content opt.txt "/usr/lib/opt/megacorp/bin/test-opt" + +ostree --repo="${repo}" ls -R "${treeref}" /usr/local > usr-local.txt +assert_file_has_content usr-local.txt "/usr/local/bin/test-usr-local" + +ostree --repo="${repo}" ls -R "${treeref}" /usr/lib/systemd/system/local-fs.target.requires > local-fs.txt +assert_file_has_content local-fs.txt "ostree-state-overlay@usr-lib-opt.service" +assert_file_has_content local-fs.txt "ostree-state-overlay@usr-local.service" + +echo "ok /opt and /usr/local RPMs" From dd6ca8ac0acfe7e3268848f26e8400689d362c0b Mon Sep 17 00:00:00 2001 From: Jonathan Lebon Date: Thu, 14 Dec 2023 16:49:07 -0500 Subject: [PATCH 3/4] app: Make `/var/usrlocal` and `/var/opt` compat symlinks in state overlay model Newly provisioned nodes don't have state to migrate yet, so they can start with the new model right away when provisioning from an image that turned on `opt-usrlocal-overlays`. Upgrading nodes have had the legacy directories created a long time ago and systemd-tmpfiles will not replace them (we're using `L`, not `L+`). --- Makefile-rpm-ostree.am | 2 +- rpmostree-cxxrs.cxx | 10 ++++++++ rpmostree-cxxrs.h | 1 + rust/src/lib.rs | 1 + rust/src/treefile.rs | 4 ++++ ...ree-0-integration-opt-usrlocal-compat.conf | 5 ++++ ...rpm-ostree-0-integration-opt-usrlocal.conf | 3 +++ src/libpriv/rpmostree-postprocess.cxx | 24 ++++++++++++++----- 8 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 src/app/rpm-ostree-0-integration-opt-usrlocal-compat.conf diff --git a/Makefile-rpm-ostree.am b/Makefile-rpm-ostree.am index acc7916f1b..757ee80a05 100644 --- a/Makefile-rpm-ostree.am +++ b/Makefile-rpm-ostree.am @@ -85,7 +85,7 @@ librpmostreeinternals_la_CXXFLAGS = $(AM_CXXFLAGS) $(sanitizer_flags) $(rpmostre librpmostreeinternals_la_LIBADD = $(rpmostree_common_libs) privdatadir=$(pkglibdir) -privdata_DATA = src/app/rpm-ostree-0-integration.conf src/app/rpm-ostree-0-integration-opt-usrlocal.conf +privdata_DATA = src/app/rpm-ostree-0-integration.conf src/app/rpm-ostree-0-integration-opt-usrlocal.conf src/app/rpm-ostree-0-integration-opt-usrlocal-compat.conf # Propagate automake verbose mode cargo_build = $(cargo) build $(if $(subst 0,,$(V)),--verbose,) diff --git a/rpmostree-cxxrs.cxx b/rpmostree-cxxrs.cxx index 3f53d35d09..fb9f17cc4f 100644 --- a/rpmostree-cxxrs.cxx +++ b/rpmostree-cxxrs.cxx @@ -1774,6 +1774,7 @@ struct Treefile final : public ::rust::Opaque ::rpmostreecxx::RepoMetadataTarget get_repo_metadata_target () const noexcept; bool rpmdb_backend_is_target () const noexcept; bool should_normalize_rpmdb () const noexcept; + bool get_opt_usrlocal_overlays () const noexcept; ::rust::Vec< ::rust::String> get_files_remove_regex (::rust::Str package) const noexcept; ::rust::String get_checksum (::rpmostreecxx::OstreeRepo const &repo) const; ::rust::String get_ostree_ref () const noexcept; @@ -2643,6 +2644,9 @@ extern "C" bool rpmostreecxx$cxxbridge1$Treefile$should_normalize_rpmdb ( ::rpmostreecxx::Treefile const &self) noexcept; + bool rpmostreecxx$cxxbridge1$Treefile$get_opt_usrlocal_overlays ( + ::rpmostreecxx::Treefile const &self) noexcept; + void rpmostreecxx$cxxbridge1$Treefile$get_files_remove_regex ( ::rpmostreecxx::Treefile const &self, ::rust::Str package, ::rust::Vec< ::rust::String> *return$) noexcept; @@ -5257,6 +5261,12 @@ Treefile::should_normalize_rpmdb () const noexcept return rpmostreecxx$cxxbridge1$Treefile$should_normalize_rpmdb (*this); } +bool +Treefile::get_opt_usrlocal_overlays () const noexcept +{ + return rpmostreecxx$cxxbridge1$Treefile$get_opt_usrlocal_overlays (*this); +} + ::rust::Vec< ::rust::String> Treefile::get_files_remove_regex (::rust::Str package) const noexcept { diff --git a/rpmostree-cxxrs.h b/rpmostree-cxxrs.h index 3798438655..262fbd2dc6 100644 --- a/rpmostree-cxxrs.h +++ b/rpmostree-cxxrs.h @@ -1556,6 +1556,7 @@ struct Treefile final : public ::rust::Opaque ::rpmostreecxx::RepoMetadataTarget get_repo_metadata_target () const noexcept; bool rpmdb_backend_is_target () const noexcept; bool should_normalize_rpmdb () const noexcept; + bool get_opt_usrlocal_overlays () const noexcept; ::rust::Vec< ::rust::String> get_files_remove_regex (::rust::Str package) const noexcept; ::rust::String get_checksum (::rpmostreecxx::OstreeRepo const &repo) const; ::rust::String get_ostree_ref () const noexcept; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 6ea5d4df48..558a4b6c6d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -625,6 +625,7 @@ pub mod ffi { fn get_repo_metadata_target(&self) -> RepoMetadataTarget; fn rpmdb_backend_is_target(&self) -> bool; fn should_normalize_rpmdb(&self) -> bool; + fn get_opt_usrlocal_overlays(&self) -> bool; fn get_files_remove_regex(&self, package: &str) -> Vec; fn get_checksum(&self, repo: &OstreeRepo) -> Result; fn get_ostree_ref(&self) -> String; diff --git a/rust/src/treefile.rs b/rust/src/treefile.rs index 86c3dfaa23..0a8b904c24 100644 --- a/rust/src/treefile.rs +++ b/rust/src/treefile.rs @@ -1412,6 +1412,10 @@ impl Treefile { self.parsed.base.rpmdb_normalize.unwrap_or(false) } + pub(crate) fn get_opt_usrlocal_overlays(&self) -> bool { + self.parsed.base.opt_usrlocal_overlays.unwrap_or_default() + } + pub(crate) fn get_files_remove_regex(&self, package: &str) -> Vec { let mut files_to_remove: Vec = Vec::new(); if let Some(ref packages) = self.parsed.base.remove_from_packages { diff --git a/src/app/rpm-ostree-0-integration-opt-usrlocal-compat.conf b/src/app/rpm-ostree-0-integration-opt-usrlocal-compat.conf new file mode 100644 index 0000000000..c7e05cd680 --- /dev/null +++ b/src/app/rpm-ostree-0-integration-opt-usrlocal-compat.conf @@ -0,0 +1,5 @@ +# Traditionally, /usr/local has been a link to /var/usrlocal and /opt to /var/opt. +# A new model now is to allow OSTree commit content in those directories. For +# backwards compatibility, we keep the /var paths but flip the symlinks around. +L /var/usrlocal - - - - ../usr/local +L /var/opt - - - - ../usr/lib/opt diff --git a/src/app/rpm-ostree-0-integration-opt-usrlocal.conf b/src/app/rpm-ostree-0-integration-opt-usrlocal.conf index 9051f1f2ea..3c36481b19 100644 --- a/src/app/rpm-ostree-0-integration-opt-usrlocal.conf +++ b/src/app/rpm-ostree-0-integration-opt-usrlocal.conf @@ -1,2 +1,5 @@ +# Traditionally, /usr/local has been a link to /var/usrlocal and /opt to /var/opt. +# A new model now is to allow OSTree commit content in those directories. But +# this dropin implements the old model. d /var/opt 0755 root root - d /var/usrlocal 0755 root root - diff --git a/src/libpriv/rpmostree-postprocess.cxx b/src/libpriv/rpmostree-postprocess.cxx index af3e661a08..b6c2179cd1 100644 --- a/src/libpriv/rpmostree-postprocess.cxx +++ b/src/libpriv/rpmostree-postprocess.cxx @@ -461,12 +461,24 @@ postprocess_final (int rootfs_dfd, rpmostreecxx::Treefile &treefile, gboolean un cancellable, error)) return FALSE; - if (!glnx_file_copy_at (pkglibdir_dfd, "rpm-ostree-0-integration-opt-usrlocal.conf", NULL, - rootfs_dfd, - "usr/lib/tmpfiles.d/rpm-ostree-0-integration-opt-usrlocal.conf", - GLNX_FILE_COPY_NOXATTRS, /* Don't take selinux label */ - cancellable, error)) - return FALSE; + if (treefile.get_opt_usrlocal_overlays ()) + { + if (!glnx_file_copy_at ( + pkglibdir_dfd, "rpm-ostree-0-integration-opt-usrlocal-compat.conf", NULL, rootfs_dfd, + "usr/lib/tmpfiles.d/rpm-ostree-0-integration-opt-usrlocal-compat.conf", + GLNX_FILE_COPY_NOXATTRS, /* Don't take selinux label */ + cancellable, error)) + return FALSE; + } + else + { + if (!glnx_file_copy_at (pkglibdir_dfd, "rpm-ostree-0-integration-opt-usrlocal.conf", NULL, + rootfs_dfd, + "usr/lib/tmpfiles.d/rpm-ostree-0-integration-opt-usrlocal.conf", + GLNX_FILE_COPY_NOXATTRS, /* Don't take selinux label */ + cancellable, error)) + return FALSE; + } /* Handle kernel/initramfs if we're not doing a container */ if (!container) From 7fd72dc2e8820214d2ff5ba4d3ab2cf21eaa9864 Mon Sep 17 00:00:00 2001 From: Jonathan Lebon Date: Thu, 14 Dec 2023 16:49:08 -0500 Subject: [PATCH 4/4] core: Add knob to make it easier to test out state overlay model I want people to be able to test out the state overlay approach without having to switch over the base compose in e.g. FCOS to use it. Hackily add an env var that will forcibly fix the symlinks in the OSTree commit when doing a local assembly. This is enough to allow properly checking out RPMs that install into `/opt` and `/usr/local`. Have it also enable the systemd services for the overlays. (In `/usr`, so that if the knob is removed, the services are no longer enabled.) --- src/libpriv/rpmostree-core.cxx | 49 ++++++++++ tests/kolainst/destructive/state-overlays | 104 ++++++++++++++++++++++ tests/kolainst/kolainst-build.sh | 15 ++++ 3 files changed, 168 insertions(+) create mode 100755 tests/kolainst/destructive/state-overlays diff --git a/src/libpriv/rpmostree-core.cxx b/src/libpriv/rpmostree-core.cxx index 1e6af8edaa..9cc872b27b 100644 --- a/src/libpriv/rpmostree-core.cxx +++ b/src/libpriv/rpmostree-core.cxx @@ -4062,6 +4062,55 @@ rpmostree_context_assemble (RpmOstreeContext *self, GCancellable *cancellable, G if (!build_rootfs_usrlinks (self, error)) return FALSE; + /* This is purely for making it easier for people to test out the + * state-overlay stuff until it's stabilized and part of base composes. */ + if (g_getenv ("RPMOSTREE_EXPERIMENTAL_FORCE_OPT_USRLOCAL_OVERLAY")) + { + rpmostree_output_message ( + "Enabling experimental state overlay support for /opt and /usr/local"); + + struct stat stbuf; + + if (!glnx_fstatat_allow_noent (tmprootfs_dfd, "opt", &stbuf, AT_SYMLINK_NOFOLLOW, error)) + return FALSE; + if (errno == ENOENT || (errno == 0 && S_ISLNK (stbuf.st_mode))) + { + if (errno == 0 && !glnx_unlinkat (tmprootfs_dfd, "opt", 0, error)) + return FALSE; + if (symlinkat ("usr/lib/opt", tmprootfs_dfd, "opt") < 0) + return glnx_throw_errno_prefix (error, "symlinkat(/opt)"); + } + + if (!glnx_fstatat_allow_noent (tmprootfs_dfd, "usr/local", &stbuf, AT_SYMLINK_NOFOLLOW, + error)) + return FALSE; + if (errno == ENOENT || (errno == 0 && S_ISLNK (stbuf.st_mode))) + { + if (errno == 0 && !glnx_unlinkat (tmprootfs_dfd, "usr/local", 0, error)) + return FALSE; + if (mkdirat (tmprootfs_dfd, "usr/local", 0755) < 0) + return glnx_throw_errno_prefix (error, "mkdirat(/usr/local)"); + } + + if (!glnx_shutil_mkdir_p_at (tmprootfs_dfd, "usr/lib/systemd/system/local-fs.target.wants", + 0755, cancellable, error)) + return FALSE; + + if (symlinkat ("ostree-state-overlay@usr-lib-opt.service", tmprootfs_dfd, + "usr/lib/systemd/system/local-fs.target.wants/" + "ostree-state-overlay@usr-lib-opt.service") + < 0 + && errno != EEXIST) + return glnx_throw_errno_prefix (error, "enabling ostree-state-overlay for /usr/lib/opt"); + + if (symlinkat ( + "ostree-state-overlay@usr-local.service", tmprootfs_dfd, + "usr/lib/systemd/system/local-fs.target.wants/ostree-state-overlay@usr-local.service") + < 0 + && errno != EEXIST) + return glnx_throw_errno_prefix (error, "enabling ostree-state-overlay for /usr/local"); + } + /* We need up to date labels; the set of things needing relabeling * will have been calculated in sort_packages() */ diff --git a/tests/kolainst/destructive/state-overlays b/tests/kolainst/destructive/state-overlays new file mode 100755 index 0000000000..c2aac0d1c5 --- /dev/null +++ b/tests/kolainst/destructive/state-overlays @@ -0,0 +1,104 @@ +#!/bin/bash +## kola: +## tags: "needs-internet" + +set -euo pipefail + +. ${KOLA_EXT_DATA}/libtest.sh + +rm -rf /etc/yum.repos.d/* +cat > /etc/yum.repos.d/vmcheck.repo << EOF +[test-repo] +name=test-repo +baseurl=file:///${KOLA_EXT_DATA}/rpm-repos/0 +gpgcheck=0 +enabled=1 +EOF + + +case "${AUTOPKGTEST_REBOOT_MARK:-}" in + "") + # switch over to local ref so upgrades are purely about package changes + booted_commit=$(rpm-ostree status --json | jq -r '.deployments[0].checksum') + ostree refs --create kolatest "${booted_commit}" + systemctl stop rpm-ostreed + unshare -m /bin/bash -c 'mount -o rw,remount /sysroot && sed -i -e "s/refspec=.*/refspec=kolatest/" /ostree/deploy/*/deploy/*.origin' + + # XXX: until ostree v2024.1 hits FCOS + ostree_ver=$(rpm -q ostree --qf '%{version}') + if [ "${ostree_ver}" != "2024.1" ] && \ + [ "$(echo -e "${ostree_ver}\n2024.1" | sort -V | tail -n1)" = "2024.1" ]; then + rpm-ostree override replace https://bodhi.fedoraproject.org/updates/FEDORA-2024-6c7480dd2f + fi + + # FCOS doesn't enable opt-usrlocal-overlays so use the hack instead + mkdir -p /etc/systemd/system/rpm-ostreed.service.d/ + cat > /etc/systemd/system/rpm-ostreed.service.d/state-overlay.conf < /etc/systemd/system/move-usr-local.service < /tmp/out.txt + assert_file_has_content /tmp/out.txt 'test-opt' + assert_file_has_content /opt/megacorp/lib/mylib 'lib1' + + # add some state files + echo 'foobar' > /opt/megacorp/state/mystate + + # change some base files + echo 'badlib' > /opt/megacorp/lib/mylib + /tmp/autopkgtest-reboot 2 + ;; + 2) + # check our state is still there + assert_file_has_content /opt/megacorp/state/mystate 'foobar' + assert_file_has_content /opt/megacorp/lib/mylib 'badlib' + + # upgrade to -2 + sed -i -e 's,rpm-repos/0,rpm-repos/1,' /etc/yum.repos.d/vmcheck.repo + rpm-ostree upgrade + /tmp/autopkgtest-reboot 3 + ;; + 3) + # check our state is still there + assert_file_has_content /opt/megacorp/state/mystate 'foobar' + + # but base content has been reset + assert_file_has_content /opt/megacorp/lib/mylib 'lib2' + ;; + *) echo "unexpected mark: ${AUTOPKGTEST_REBOOT_MARK}"; exit 1;; +esac diff --git a/tests/kolainst/kolainst-build.sh b/tests/kolainst/kolainst-build.sh index 544e3f8d49..8ed020f163 100755 --- a/tests/kolainst/kolainst-build.sh +++ b/tests/kolainst/kolainst-build.sh @@ -96,6 +96,13 @@ build_module_defaults foomodular \ build_rpm zincati version 99.99 release 2 build_rpm zincati version 99.99 release 3 +# An RPM that installs in /opt +build_rpm test-opt \ + install "mkdir -p %{buildroot}/opt/megacorp/{bin,lib,state} + install %{name} %{buildroot}/opt/megacorp/bin + echo lib1 > %{buildroot}/opt/megacorp/lib/mylib" \ + files "/opt/megacorp" + mv ${test_tmpdir}/yumrepo/* ${test_tmpdir}/rpm-repos/${repover} # To test remote override replace update @@ -111,6 +118,14 @@ build_rpm pkgsystemd \ install "mkdir -p %{buildroot}/usr/lib/systemd/system install %{name}.service %{buildroot}/usr/lib/systemd/system" \ files "/usr/lib/systemd/system/%{name}.service" + +# to test updates to RPMs that install in /opt +build_rpm test-opt release 2 \ + install "mkdir -p %{buildroot}/opt/megacorp/{bin,lib,state} + install %{name} %{buildroot}/opt/megacorp/bin + echo lib2 > %{buildroot}/opt/megacorp/lib/mylib" \ + files "/opt/megacorp" + mv ${test_tmpdir}/yumrepo/* ${test_tmpdir}/rpm-repos/${repover} # Create an empty repo when we want to test inability to find a package