Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion crates/lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,22 @@ impl InternalsOpts {
const GENERATOR_BIN: &'static str = "bootc-systemd-generator";
}

/// The text of `bootc --version`: the version, then the optional features of
/// this binary that tools driving bootc can rely on being present.
fn long_version() -> &'static str {
static LONG_VERSION: std::sync::OnceLock<String> = std::sync::OnceLock::new();
LONG_VERSION
.get_or_init(|| format_long_version(clap::crate_version!(), crate::install::FEATURES))
}

fn format_long_version(version: &str, features: &[&str]) -> String {
let features = features
.iter()
.map(|f| format!("\n - {f}"))
.collect::<String>();
format!("{version}\nFeatures:{features}")
}

/// Deploy and transactionally in-place with bootable container images.
///
/// The `bootc` project currently uses ostree-containers as a backend
Expand All @@ -986,7 +1002,7 @@ impl InternalsOpts {
#[derive(Debug, Parser, PartialEq, Eq)]
#[clap(name = "bootc")]
#[clap(rename_all = "kebab-case")]
#[clap(version,long_version=clap::crate_version!())]
#[clap(version, long_version = long_version())]
#[allow(clippy::large_enum_variant)]
pub(crate) enum Opt {
/// Download and queue an updated container image to apply.
Expand Down Expand Up @@ -3163,4 +3179,17 @@ mod tests {
}
}
}

#[test]
fn test_long_version() {
assert_eq!(
format_long_version("1.2.3", &["a", "b"]),
"1.2.3\nFeatures:\n - a\n - b"
);
// Every feature we have shows up in `bootc --version`
let long = Opt::command().render_long_version();
for f in crate::install::FEATURES {
assert!(long.contains(&format!("\n - {f}")), "{long}");
}
}
}
111 changes: 109 additions & 2 deletions crates/lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ pub(crate) mod completion;
pub(crate) mod config;
mod osbuild;
pub(crate) mod osconfig;
mod var_mounts;

use std::collections::HashMap;
use std::io::Write;
Expand Down Expand Up @@ -224,6 +225,8 @@ const ALONGSIDE_ROOT_MOUNT: &str = "/target";
pub(crate) const DESTRUCTIVE_CLEANUP: &str = "etc/bootc-destructive-cleanup";
/// This is an ext4 special directory we need to ignore.
const LOST_AND_FOUND: &str = "lost+found";
/// Optional features of this binary, listed by `bootc --version`.
pub(crate) const FEATURES: &[&str] = &[var_mounts::FEATURE];
/// The mount path for selinux
const SELINUXFS: &str = "/sys/fs/selinux";
/// The mount path for uefi
Expand Down Expand Up @@ -554,6 +557,14 @@ pub(crate) struct InstallTargetFilesystemOpts {
#[clap(long)]
pub(crate) boot_mount_spec: Option<String>,

/// Initialize empty filesystems mounted at or below /var in the target
/// from the image's /var, including nested mounts.
///
/// Without this, filesystems that the caller mounted there are left alone,
/// and hide the image's /var content at boot.
#[clap(long)]
pub(crate) initialize_var_mounts: bool,

/// Initialize the system in-place; at the moment, only one mode for this is implemented.
/// In the future, it may also be supported to set up an explicit "dual boot" system.
#[clap(long)]
Expand Down Expand Up @@ -2177,6 +2188,7 @@ async fn install_to_filesystem_impl(
state: &State,
rootfs: &mut RootSetup,
cleanup: Cleanup,
var_mounts: &[Utf8PathBuf],
) -> Result<()> {
if matches!(state.selinux_state, SELinuxFinalState::ForceTargetDisabled) {
rootfs.kargs.extend(&Cmdline::from("selinux=0"));
Expand Down Expand Up @@ -2291,6 +2303,20 @@ async fn install_to_filesystem_impl(
}
}

// The deployment backend seeds its own state directory, not the caller's
// mounted /var tree. Populate fresh external filesystems while all nested
// mounts are still visible, before labeling and finalizing the installation.
let prepared_var_mounts = if !var_mounts.is_empty() {
let source = if state.composefs_options.composefs_backend {
Utf8PathBuf::from(crate::composefs_consts::SHARED_VAR_PATH)
} else {
Utf8PathBuf::from(format!("ostree/deploy/{}/var", state.stateroot()))
};
var_mounts::populate(&rootfs.physical_root, &source, var_mounts)?
} else {
Vec::new()
};

// As the very last step before filesystem finalization, do a full SELinux
// relabel of the physical root filesystem. Any files that are already
// labeled (e.g. ostree deployment contents, composefs objects) are skipped.
Expand All @@ -2299,10 +2325,32 @@ async fn install_to_filesystem_impl(
let mut path = Utf8PathBuf::from("");
crate::lsm::ensure_dir_labeled_recurse(&rootfs.physical_root, &mut path, &policy, None)
.context("Final SELinux relabeling of physical root")?;
// The physical-root walk skips mountpoints. Label each newly
// prepared filesystem separately, using its deployed /var path.
for mount in &prepared_var_mounts {
let mut path = var_mounts::target_path(mount);
// These are fresh filesystems; label their roots unconditionally.
// Without selinuxfs (e.g. in an osbuild buildroot), an inode without
// a label still reports the kernel's unlabeled context, which the
// walk below would take as labeled.
for p in [path.clone(), path.join(LOST_AND_FOUND)] {
if let Some(meta) = rootfs.physical_root.symlink_metadata_optional(&p)? {
crate::lsm::relabel(&rootfs.physical_root, &meta, &p, None, &policy)
.with_context(|| format!("SELinux labeling of /{p}"))?;
}
}
crate::lsm::ensure_dir_labeled_recurse(&rootfs.physical_root, &mut path, &policy, None)
.with_context(|| format!("SELinux labeling of /{path}"))?;
}
} else {
tracing::debug!("Skipping final SELinux relabel (SELinux is disabled)");
}

// Flush the /var filesystems we wrote, surfacing any writeback errors.
// Unlike root and boot they are not remounted read-only or frozen: they
// may be bind mounts or lack freeze support, and callers may add content.
var_mounts::sync(&rootfs.physical_root, &prepared_var_mounts)?;

// Finalize mounted filesystems
if !rootfs.skip_finalize {
let bootfs = rootfs.boot.as_ref().map(|_| ("boot", BOOT));
Expand Down Expand Up @@ -2392,7 +2440,7 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
(rootfs, loopback_dev)
};

install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip).await?;
install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip, &[]).await?;

// Drop all data about the root except the bits we need to ensure any file descriptors etc. are closed.
let (root_path, luksdev) = rootfs.into_storage();
Expand Down Expand Up @@ -2663,6 +2711,25 @@ pub enum Cleanup {
}

/// Implementation of the `bootc install to-filesystem` CLI command.
/// Reinstalling over or alongside an existing OS must not initialize its
/// live state. Checked before anything on the target is changed.
fn check_initialize_var_mounts(
initialize_var_mounts: bool,
targeting_host_root: bool,
is_already_ostree: bool,
replace: Option<ReplaceMode>,
) -> Result<()> {
anyhow::ensure!(
!initialize_var_mounts
|| !(targeting_host_root
|| is_already_ostree
|| replace == Some(ReplaceMode::Alongside)),
"--initialize-var-mounts is not supported over an existing ostree system \
or with --replace=alongside"
);
Ok(())
}

#[context("Installing to filesystem")]
pub(crate) async fn install_to_filesystem(
opts: InstallToFilesystemOpts,
Expand Down Expand Up @@ -2744,6 +2811,12 @@ pub(crate) async fn install_to_filesystem(
);
fsopts.root_path = possible_physical_root;
};
check_initialize_var_mounts(
fsopts.initialize_var_mounts,
targeting_host_root,
is_already_ostree,
fsopts.replace,
)?;

// Get a file descriptor for the root path
// It will be /target/sysroot on ostree OS, or will be /target
Expand Down Expand Up @@ -2939,7 +3012,12 @@ pub(crate) async fn install_to_filesystem(
skip_finalize,
};

install_to_filesystem_impl(&state, &mut rootfs, cleanup).await?;
let var_mounts = if fsopts.initialize_var_mounts {
var_mounts::discover(&rootfs.physical_root_path)?
} else {
Vec::new()
};
install_to_filesystem_impl(&state, &mut rootfs, cleanup, &var_mounts).await?;

// Drop all data about the root except the path to ensure any file descriptors etc. are closed.
drop(rootfs);
Expand Down Expand Up @@ -2984,6 +3062,7 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
root_path: opts.root_path,
root_mount_spec: None,
boot_mount_spec: None,
initialize_var_mounts: false,
replace: opts.replace,
skip_finalize: true,
acknowledge_destructive: opts.acknowledge_destructive,
Expand Down Expand Up @@ -3301,6 +3380,34 @@ mod tests {
}
}

#[test]
fn test_check_initialize_var_mounts() {
use ReplaceMode::*;
// (initialize, host root, already ostree, replace, allowed)
let cases = [
(false, true, true, Some(Alongside), true),
(true, false, false, None, true),
(true, false, false, Some(Wipe), true),
(true, false, false, Some(Alongside), false),
(true, false, true, None, false),
(true, true, false, None, false),
];
for (init, host_root, ostree, replace, allowed) in cases {
let r = check_initialize_var_mounts(init, host_root, ostree, replace);
match r {
Ok(()) => assert!(allowed, "{init} {host_root} {ostree} {replace:?}"),
Err(e) => {
assert!(!allowed, "{init} {host_root} {ostree} {replace:?}: {e}");
assert_eq!(
e.to_string(),
"--initialize-var-mounts is not supported over an existing ostree \
system or with --replace=alongside"
);
}
}
}
}

#[test]
fn test_mountspec() {
let mut ms = MountSpec::new("/dev/vda4", "/boot");
Expand Down
Loading
Loading