Skip to content

Commit

Permalink
Support RPMs installing in /opt and /usr/local
Browse files Browse the repository at this point in the history
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: ostreedev/ostree#3113
Fixes: #233
  • Loading branch information
jlebon committed Jan 9, 2024
1 parent bf01690 commit 9b7b8ec
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 6 deletions.
6 changes: 6 additions & 0 deletions docs/treefile.md
Original file line number Diff line number Diff line change
Expand Up @@ -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+.
80 changes: 74 additions & 6 deletions rust/src/composepost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")?;
Expand Down Expand Up @@ -606,6 +626,30 @@ fn compose_postprocess_rpmdb(rootfs_dfd: &Dir) -> Result<()> {
Ok(())
}

/// Enables [email protected] 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] = &[
"[email protected]",
"[email protected]",
];

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,
Expand All @@ -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())?;
Expand Down Expand Up @@ -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<bool> {
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.
///
Expand All @@ -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")
Expand Down
3 changes: 3 additions & 0 deletions rust/src/treefile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -2531,6 +2532,8 @@ pub(crate) struct BaseComposeConfigFields {
pub(crate) boot_location: Option<BootLocation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) tmp_is_dir: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) opt_usrlocal_overlays: Option<bool>,

// systemd
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down

0 comments on commit 9b7b8ec

Please sign in to comment.