Skip to content

Commit

Permalink
install: Add hidden ensure-completion verb
Browse files Browse the repository at this point in the history
This will be runnable via
```
%post --erroronfail
bootc install ensure-completion
%end
```

in Anaconda to work around the fact that it's not today
using `bootc install to-filesystem`.

Signed-off-by: Colin Walters <[email protected]>
  • Loading branch information
cgwalters committed Nov 22, 2024
1 parent 3cc8dfd commit 21467e0
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 2 deletions.
13 changes: 13 additions & 0 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ pub(crate) enum InstallOpts {
/// will be wiped, but the content of the existing root will otherwise be retained, and will
/// need to be cleaned up if desired when rebooted into the new root.
ToExistingRoot(crate::install::InstallToExistingRootOpts),
/// Intended for use in environments that are performing an ostree-based installation, not bootc.
///
/// In this scenario the installation may be missing bootc specific features such as
/// kernel arguments, logically bound images and more. This command can be used to attempt
/// to reconcile. At the current time, the only tested environment is Anaconda using `ostreecontainer`
/// and it is recommended to avoid usage outside of that environment. Instead, ensure your
/// code is using `bootc install to-filesystem` from the start.
#[clap(hide = true)]
EnsureCompletion,
/// Output JSON to stdout that contains the merged installation configuration
/// as it may be relevant to calling processes using `install to-filesystem`
/// that in particular want to discover the desired root filesystem type from the container image.
Expand Down Expand Up @@ -989,6 +998,10 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
crate::install::install_to_existing_root(opts).await
}
InstallOpts::PrintConfiguration => crate::install::print_configuration(),
InstallOpts::EnsureCompletion => {
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
crate::install::completion::run(rootfs).await
}
},
#[cfg(feature = "install")]
Opt::ExecInHostMountNamespace { args } => {
Expand Down
2 changes: 2 additions & 0 deletions lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// This sub-module is the "basic" installer that handles creating basic block device
// and filesystem setup.
pub(crate) mod baseline;
pub(crate) mod completion;
pub(crate) mod config;
mod osbuild;
pub(crate) mod osconfig;
Expand Down Expand Up @@ -788,6 +789,7 @@ async fn install_container(
)?;
let kargsd = kargsd.iter().map(|s| s.as_str());

// Keep this in sync with install/completion.rs for the Anaconda fixups
let install_config_kargs = state
.install_config
.as_ref()
Expand Down
201 changes: 201 additions & 0 deletions lib/src/install/completion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
//! This module handles finishing/completion after an ostree-based
//! install from e.g. Anaconda.

use std::{io::BufReader, process::Command};

use anyhow::{Context, Result};
use bootc_utils::CommandRunExt;
use camino::Utf8Path;
use cap_std_ext::{cap_std::fs::Dir, cmdext::CapStdExtCommandExt, dirext::CapStdExtDirExt};
use fn_error_context::context;
use ostree_ext::{gio, ostree};

use super::{config, BOOTC_ALEPH_PATH};
use crate::{status::get_image_origin, utils::medium_visibility_warning};

/// An environment variable set by anaconda that hints
/// we are running as part of that environment.
const ANACONDA_ENV_HINT: &str = "ANA_INSTALL_PATH";
const OSTREE_BOOTED: &str = "run/ostree-booted";
const RESOLVCONF: &str = "etc/resolv.conf";
const RUNHOST: &str = "run/host";
const CGROUPFS: &str = "sys/fs/cgroup";
const RUN_OSTREE_AUTH: &str = "run/ostree/auth.json";

fn reconcile_kargs(
rootfs: &Dir,
sysroot: &ostree::Sysroot,
deployment: &ostree::Deployment,
) -> Result<()> {
let cancellable = gio::Cancellable::NONE;

let current_kargs = deployment
.bootconfig()
.expect("bootconfig for deployment")
.get("options");
let current_kargs = current_kargs
.as_ref()
.map(|s| s.as_str())
.unwrap_or_default();
tracing::debug!("current_kargs={current_kargs}");
let current_kargs = ostree::KernelArgs::from_string(&current_kargs);

// Keep this in sync with install_container
let install_config = config::load_config()?;
let install_config_kargs = install_config
.as_ref()
.and_then(|c| c.kargs.as_ref())
.into_iter()
.flatten()
.map(|s| s.as_str())
.collect::<Vec<_>>();
let kargsd = crate::kargs::get_kargs_in_root(rootfs, std::env::consts::ARCH)?;
let kargsd = kargsd.iter().map(|s| s.as_str()).collect::<Vec<_>>();

current_kargs.append_argv(&install_config_kargs);
current_kargs.append_argv(&kargsd);
let new_kargs = current_kargs.to_string();
tracing::debug!("new_kargs={new_kargs}");

sysroot.deployment_set_kargs_in_place(deployment, Some(&new_kargs), cancellable)?;
Ok(())
}

/// Work around https://github.com/containers/buildah/issues/4242#issuecomment-2492480586
/// among other things. We unconditionally replace the contents of `/etc/resolv.conf`
/// in the target root with whatever the host uses (in Fedora 41+, that's systemd-resolved for Anaconda).
#[context("Copying host resolv.conf")]
fn ensure_resolvconf(rootfs: &Dir) -> Result<()> {
rootfs.remove_file_optional(RESOLVCONF)?;
let host_resolvconf = format!("/{RUNHOST}/{RESOLVCONF}");
Command::new("cp")
.args(["-a", host_resolvconf.as_str(), RESOLVCONF])
.cwd_dir(rootfs.try_clone()?)
.run()?;
Ok(())
}

/// Bind a mount point from the host namespace into our root
fn bind_from_host(
rootfs: &Dir,
src: impl AsRef<Utf8Path>,
target: impl AsRef<Utf8Path>,
) -> Result<()> {
fn bind_from_host_impl(rootfs: &Dir, src: &Utf8Path, target: &Utf8Path) -> Result<()> {
rootfs.create_dir_all(target)?;
if rootfs.is_mountpoint(target)?.unwrap_or_default() {
return Ok(());
}
let target = format!("/mnt/sysroot/{target}");
tracing::debug!("Binding {src} to {target}");
Command::new("nsenter")
.args(["-m", "-t", "1", "--", "mount", "--bind"])
.arg(src)
.arg(&target)
.run()?;
Ok(())
}

bind_from_host_impl(rootfs, src.as_ref(), target.as_ref())
}

/// Anaconda doesn't mount /sys/fs/cgroup, do it
fn ensure_cgroupfs(rootfs: &Dir) -> Result<()> {
bind_from_host(rootfs, CGROUPFS, CGROUPFS)
}

/// If we have /etc/ostree/auth.json in the Anaconda environment then propagate
/// it into /run/ostree/auth.json
fn ensure_ostree_auth(rootfs: &Dir) -> Result<()> {
let runhost = &rootfs.open_dir(RUNHOST)?;
let Some((authpath, authfd)) = ostree_ext::globals::get_global_authfile(runhost)? else {
tracing::debug!("No auth found in {RUNHOST}");
return Ok(());
};
tracing::debug!("Discovered auth in host: {authpath}");
let mut authfd = BufReader::new(authfd);
let run_ostree_auth = Utf8Path::new(RUN_OSTREE_AUTH);
rootfs.create_dir_all(run_ostree_auth.parent().unwrap())?;
rootfs.atomic_replace_with(run_ostree_auth, |w| std::io::copy(&mut authfd, w))?;
Ok(())
}

/// Core entrypoint.
pub(crate) async fn run(rootfs: &Dir) -> Result<()> {
if std::env::var_os(ANACONDA_ENV_HINT).is_none() {
// Be loud if a user is invoking this outside of the expected setup.
medium_visibility_warning(&format!("Missing environment variable {ANACONDA_ENV_HINT}"));
} else {
// In the way Anaconda sets up the bind mounts today, this doesn't exist. Later
// code expects it to exist, so do so.
if !rootfs.try_exists(OSTREE_BOOTED)? {
tracing::debug!("Writing {OSTREE_BOOTED}");
rootfs.atomic_write(OSTREE_BOOTED, b"")?;
}
}

// If we've already deployed via bootc, then we're done - we want
// to be i
let aleph_path = &format!("sysroot/{BOOTC_ALEPH_PATH}");
if rootfs.try_exists(aleph_path)? {
println!("Detected existing aleph {BOOTC_ALEPH_PATH}");
return Ok(());
}

// Get access to the Anaconda real rootfs at /run/host. This is ideally
// something anaconda itself would set up. This targets `/run` in the
// target root which will always be cleaned up.
bind_from_host(rootfs, "/", RUNHOST)?;

// Now unshare our mount namespace, so any *further* mounts aren't leaked.
// Note that because this does a re-exec, anything *before* this point
// should be idempotent.
crate::cli::ensure_self_unshared_mount_namespace()?;

ensure_cgroupfs(rootfs)?;
// Sometimes Anaconda may not initialize networking in the target root?
ensure_resolvconf(rootfs)?;
// Propagate an injected authfile for pulling logically bound images
ensure_ostree_auth(rootfs)?;

// Initialize our storage.
let sysroot = crate::cli::get_storage().await?;

let deployments = sysroot.deployments();
let deployment = match deployments.as_slice() {
[d] => d,
o => anyhow::bail!("Expected exactly 1 deployment, not {}", o.len()),
};
let origin = deployment.origin();
let Some(origin) = origin.as_ref() else {
anyhow::bail!("Missing origin for deployment")
};
let Some(src_imgref) = get_image_origin(origin)? else {
anyhow::bail!("Not a bootc deployment");
};
let csum = deployment.csum();
let imgstate = ostree_ext::container::store::query_image_commit(&sysroot.repo(), &csum)?;

// ostree-ext doesn't do kargs, so handle that now
reconcile_kargs(rootfs, &sysroot, deployment)?;

// ostree-ext doesn't do logically bound images
let bound_images = crate::boundimage::query_bound_images(&rootfs)?;
crate::boundimage::pull_images(&sysroot, bound_images)
.await
.context("pulling bound images")?;

// TODO: This isn't fully accurate, but oh well
let selinux_state = super::SELinuxFinalState::Enabled(None);
// Synthesize the alpeh data
let aleph = super::InstallAleph::new(&src_imgref, &imgstate, &selinux_state)?;
let aleph_path = format!("sysroot/{BOOTC_ALEPH_PATH}");
rootfs
.atomic_replace_with(&aleph_path, |f| {
serde_json::to_writer(f, &aleph)?;
anyhow::Ok(())
})
.context("Writing aleph version")?;

Ok(())
}
4 changes: 2 additions & 2 deletions lib/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ impl From<ImageReference> for OstreeImageReference {

/// Parse an ostree origin file (a keyfile) and extract the targeted
/// container image reference.
fn get_image_origin(origin: &glib::KeyFile) -> Result<Option<OstreeImageReference>> {
pub(crate) fn get_image_origin(origin: &glib::KeyFile) -> Result<Option<OstreeImageReference>> {
origin
.optional_string("origin", ostree_container::deploy::ORIGIN_CONTAINER)
.context("Failed to load container image from origin")?
Expand Down Expand Up @@ -121,7 +121,7 @@ pub(crate) fn labels_of_config(

/// Given an OSTree deployment, parse out metadata into our spec.
#[context("Reading deployment metadata")]
fn boot_entry_from_deployment(
pub(crate) fn boot_entry_from_deployment(
sysroot: &Storage,
deployment: &ostree::Deployment,
) -> Result<BootEntry> {
Expand Down

0 comments on commit 21467e0

Please sign in to comment.