Skip to content

Commit

Permalink
composepost: Support rootfs.transient=yes
Browse files Browse the repository at this point in the history
This pairs with ostreedev/ostree#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.
  • Loading branch information
cgwalters committed Dec 11, 2023
1 parent 69deaec commit 7857feb
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 40 deletions.
199 changes: 159 additions & 40 deletions rust/src/composepost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ use std::process::Stdio;
/// location to `/usr/lib/<entry>`.
pub(crate) static COMPAT_VARLIB_SYMLINKS: &[&str] = &["alternatives", "vagrant"];

const DEFAULT_DIRMODE: u32 = 0o755;

/// Symlinks to ensure home directories persist by default.
const OSTREE_HOME_SYMLINKS: &[(&str, &str)] = &[("var/roothome", "root"), ("var/home", "home")];

/* 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";
Expand All @@ -59,26 +64,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)
Expand All @@ -90,11 +84,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;
Expand All @@ -108,15 +97,86 @@ fn compose_init_rootfs(rootfs_dfd: &cap_std::fs::Dir, tmp_is_dir: bool) -> Resul
rootfs_dfd.symlink("sysroot/tmp", "tmp")?;
}

OSTREE_HOME_SYMLINKS
.par_iter()
.try_for_each(|&(dest, src)| {
rootfs_dfd
.symlink(dest, src)
.with_context(|| format!("Creating {src}"))
})?;

rootfs_dfd
.symlink("sysroot/ostree", "ostree")
.context("Symlinking ostree -> sysroot/ostree")?;

Ok(())
}

/// Initialize a root filesystem set up for use with ostree's `root.transient` mode.
#[context("Initializing rootfs (base)")]
fn compose_init_rootfs_transient(rootfs_dfd: &cap_std::fs::Dir) -> Result<()> {
// Enforce tmp-is-dir in this, because there's really no reason not to.
compose_init_rootfs_base(rootfs_dfd, true)?;
// Again we need to make these directories here because we don't run
// the `filesystem` package's lua script.
const EXTRA_TOPLEVEL_DIRS: &[&str] = &["opt", "media", "mnt", "usr/local"];

let mut db = dirbuilder_from_mode(DEFAULT_DIRMODE);
db.recursive(true);
EXTRA_TOPLEVEL_DIRS.par_iter().try_for_each(|&d| {
// We need to handle the case where these may have been created as a symlink
// by tmpfiles.d snippets for example.
if let Some(meta) = rootfs_dfd.symlink_metadata_optional(d)? {
if !meta.is_dir() {
rootfs_dfd.remove_file(d)?;
}
}
rootfs_dfd
.ensure_dir_with(d, &db)
.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_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"),
("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,
Expand All @@ -127,7 +187,32 @@ 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");
// While sadly tmp-is-dir: false by default, we want to encourage
// people to switch, so just error out if they're somehow configured
// things for the newer transient root model but forgot to set `tmp-is-dir`.
if !tmp_is_dir {
return Err("Transient root conflicts with tmp-is-dir: false"
.to_string()
.into());
}
// We grab all the content from the source root by default on general principle,
// but note this won't be very much right now because
// we're not executing the `filesystem` package's lua script.
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:?}"))?;
}
compose_init_rootfs_transient(target_rootfs_dfd)?;
return Ok(());
}

compose_init_rootfs_strict(target_rootfs_dfd, tmp_is_dir)?;

println!("Moving /usr to target");
src_rootfs_dfd.rename("usr", target_rootfs_dfd, "usr")?;
Expand Down Expand Up @@ -883,12 +968,14 @@ pub fn rootfs_prepare_links(rootfs_dfd: i32) -> CxxResult<()> {
let mut db = dirbuilder_from_mode(0o755);
db.recursive(true);

// Unconditionally drop /usr/local and replace it with a symlink.
rootfs
.remove_all_optional("usr/local")
.context("Removing /usr/local")?;
ensure_symlink(rootfs, "../var/usrlocal", "usr/local")
.context("Creating /usr/local symlink")?;
if !crate::ostree_prepareroot::transient_root_enabled(rootfs)? {
// Unconditionally drop /usr/local and replace it with a symlink.
rootfs
.remove_all_optional("usr/local")
.context("Removing /usr/local")?;
ensure_symlink(rootfs, "../var/usrlocal", "usr/local")
.context("Creating /usr/local symlink")?;
}

// Move existing content to /usr/lib, then put a symlink in its
// place under /var/lib.
Expand Down Expand Up @@ -1308,20 +1395,52 @@ OSTREE_VERSION='33.4'
assert_eq!(replaced.as_str(), expected);
}

fn verify_base(rootfs: &Dir) -> Result<()> {
// Not exhaustive, just a sanity check
for d in ["proc", "sys"] {
assert!(rootfs.symlink_metadata(d)?.is_dir());
}
let homelink = rootfs.read_link("home")?;
assert_eq!(homelink.to_str().unwrap(), "var/home");
Ok(())
}

#[test]
fn test_init_rootfs() -> Result<()> {
fn test_init_rootfs_strict() -> Result<()> {
// Test the legacy tmp_is_dir path
{
let rootfs = cap_tempfile::tempdir(cap_tempfile::ambient_authority())?;
compose_init_rootfs(&rootfs, false)?;
compose_init_rootfs_base(&rootfs, false)?;
let target = rootfs.read_link("tmp").unwrap();
assert_eq!(target, Path::new("sysroot/tmp"));
verify_base(&rootfs)?;
}
{
let rootfs = cap_tempfile::tempdir(cap_tempfile::ambient_authority())?;
compose_init_rootfs(&rootfs, true)?;
let tmpdir_meta = rootfs.metadata("tmp").unwrap();
assert!(tmpdir_meta.is_dir());
assert_eq!(tmpdir_meta.permissions().mode() & 0o7777, 0o1777);
// Default expected strict mode
let rootfs = cap_tempfile::tempdir(cap_tempfile::ambient_authority())?;
compose_init_rootfs_base(&rootfs, true)?;
let tmpdir_meta = rootfs.metadata("tmp").unwrap();
assert!(tmpdir_meta.is_dir());
assert_eq!(tmpdir_meta.permissions().mode() & 0o7777, 0o1777);
verify_base(&rootfs)?;
Ok(())
}

#[test]
fn test_init_rootfs_transient() -> Result<()> {
let rootfs = cap_tempfile::tempdir(cap_tempfile::ambient_authority())?;
compose_init_rootfs_transient(&rootfs)?;
let tmpdir_meta = rootfs.metadata("tmp").unwrap();
assert!(tmpdir_meta.is_dir());
assert_eq!(tmpdir_meta.permissions().mode() & 0o7777, 0o1777);
verify_base(&rootfs)?;
for d in ["opt", "usr/local"] {
assert!(
rootfs
.symlink_metadata(d)
.with_context(|| format!("Verifying {d} is dir"))?
.is_dir(),
"Verifying {d} is dir"
);
}
Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down
50 changes: 50 additions & 0 deletions rust/src/ostree_prepareroot.rs
Original file line number Diff line number Diff line change
@@ -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<Option<glib::KeyFile>> {
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<bool> {
if let Some(config) = load_config(rootfs)? {
Ok(config
.optional_bool("root", "transient")?
.unwrap_or_default())
} else {
Ok(false)
}
}
25 changes: 25 additions & 0 deletions tests/compose/test-rootfs-transient.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/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"]'
build_rpm prepare-root-config \
files "/usr/lib/ostree/prepare-root.conf" \
install "mkdir -p %{buildroot}/usr/lib/ostree && echo -e '[root]\ntransient=true' > %{buildroot}/usr/lib/ostree/prepare-root.conf"

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" '["prepare-root-config"]'

# Do the compose
runcompose
echo "ok compose"

ostree --repo=${repo} ls ${treeref} /opt > ls.txt

Check warning

Code scanning / shellcheck

repo is referenced but not assigned. Warning test

repo is referenced but not assigned.

Check warning

Code scanning / shellcheck

treeref is referenced but not assigned. Warning test

treeref is referenced but not assigned.
assert_file_has_content ls.txt 'd00755 *0 *0 *0 */opt'
echo "ok opt is directory with transient rootfs"

0 comments on commit 7857feb

Please sign in to comment.