-
Notifications
You must be signed in to change notification settings - Fork 84
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
install: Add hidden
ensure-completion
verb
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
Showing
4 changed files
with
218 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(¤t_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(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters