Skip to content

Commit

Permalink
WIP: takeover installs
Browse files Browse the repository at this point in the history
This adds `bootc install --takeover` which moves the running
container into RAM and invokes `systemctl switch-root` to it, then
proceeds with an installation to the previously-used block device.

A key use case here is to "takeover" a running cloud instance, e.g.
provision the system via cloud-init or so which invokes
`podman run --privileged ... bootc install --takeover`.

At the current time, this is only scoped to "builtin" installation
types.  We could support `install-to-filesystem` type flows too
by allowing externally-configured block storage setups to be
run as part of the current container (or in the fully general
case, a distinct container, though that adds a lot of complexity).
  • Loading branch information
cgwalters committed Oct 13, 2023
1 parent 6e0b3b9 commit ab2a45e
Show file tree
Hide file tree
Showing 6 changed files with 466 additions and 33 deletions.
12 changes: 12 additions & 0 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ pub(crate) enum TestingOpts {
RunPrivilegedIntegration {},
/// Execute integration tests that target a not-privileged ostree container
RunContainerIntegration {},
/// Copy the container as ostree commit to target root
CopySelfTo { target: Utf8PathBuf },
/// Block device setup for testing
PrepTestInstallFilesystem { blockdev: Utf8PathBuf },
/// e2e test of install-to-filesystem
Expand Down Expand Up @@ -461,6 +463,16 @@ where
I: IntoIterator,
I::Item: Into<OsString> + Clone,
{
let args = args
.into_iter()
.map(|v| Into::<OsString>::into(v))
.collect::<Vec<_>>();
if matches!(
args.get(0).and_then(|v| v.to_str()),
Some(crate::systemtakeover::BIN_NAME)
) {
return crate::systemtakeover::run().await;
}
run_from_opt(Opt::parse_from(args)).await
}

Expand Down
122 changes: 89 additions & 33 deletions lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// This sub-module is the "basic" installer that handles creating basic block device
// and filesystem setup.
mod baseline;
pub(crate) mod baseline;

use std::io::BufWriter;
use std::io::Write;
Expand Down Expand Up @@ -37,6 +37,9 @@ use crate::containerenv::ContainerExecutionInfo;
use crate::task::Task;
use crate::utils::run_in_host_mountns;

/// The path we use to access files on the host
pub(crate) const HOST_RUNDIR: &str = "/run/host";

/// The default "stateroot" or "osname"; see https://github.com/ostreedev/ostree/issues/2794
const STATEROOT_DEFAULT: &str = "default";
/// The toplevel boot directory
Expand Down Expand Up @@ -196,6 +199,8 @@ pub(crate) struct SourceInfo {
pub(crate) commit: String,
/// Whether or not SELinux appears to be enabled in the source commit
pub(crate) selinux: bool,
/// If we should find the image in sysroot/repo, not in containers/storage
pub(crate) from_ostree_repo: bool,
}

// Shared read-only global state
Expand Down Expand Up @@ -345,11 +350,13 @@ impl SourceInfo {
let root = root.downcast_ref::<ostree::RepoFile>().unwrap();
let xattrs = root.xattrs(cancellable)?;
let selinux = crate::lsm::xattrs_have_selinux(&xattrs);
let from_ostree_repo = false;
Ok(Self {
imageref,
digest,
commit,
selinux,
from_ostree_repo,
})
}
}
Expand Down Expand Up @@ -424,6 +431,14 @@ pub(crate) mod config {
}
}

pub(crate) fn import_config_from_host() -> ostree_container::store::ImageProxyConfig {
let skopeo_cmd = run_in_host_mountns("skopeo");
ostree_container::store::ImageProxyConfig {
skopeo_cmd: Some(skopeo_cmd),
..Default::default()
}
}

#[context("Creating ostree deployment")]
async fn initialize_ostree_root_from_self(
state: &State,
Expand Down Expand Up @@ -492,36 +507,10 @@ async fn initialize_ostree_root_from_self(

let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
sysroot.load(cancellable)?;
let dest_repo = &sysroot.repo();

// We need to fetch the container image from the root mount namespace
let skopeo_cmd = run_in_host_mountns("skopeo");
let proxy_cfg = ostree_container::store::ImageProxyConfig {
skopeo_cmd: Some(skopeo_cmd),
..Default::default()
};

let mut temporary_dir = None;
let src_imageref = if skopeo_supports_containers_storage()? {
// We always use exactly the digest of the running image to ensure predictability.
let spec =
crate::utils::digested_pullspec(&state.source.imageref.name, &state.source.digest);
ostree_container::ImageReference {
transport: ostree_container::Transport::ContainerStorage,
name: spec,
}
} else {
let td = tempfile::tempdir_in("/var/tmp")?;
let path: &Utf8Path = td.path().try_into().unwrap();
let r = copy_to_oci(&state.source.imageref, path)?;
temporary_dir = Some(td);
r
};
let src_imageref = ostree_container::OstreeImageReference {
// There are no signatures to verify since we're fetching the already
// pulled container.
sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
imgref: src_imageref,
};
let proxy_cfg = import_config_from_host();

let kargs = root_setup
.kargs
Expand All @@ -532,6 +521,56 @@ async fn initialize_ostree_root_from_self(
options.kargs = Some(kargs.as_slice());
options.target_imgref = Some(&target_imgref);
options.proxy_cfg = Some(proxy_cfg);

// Default image reference pulls from the running container image.
let mut src_imageref = ostree_container::OstreeImageReference {
// There are no signatures to verify since we're fetching the already
// pulled container.
sigverify: SignatureSource::ContainerPolicyAllowInsecure,
imgref: state.source.imageref.clone(),
};

let mut temporary_dir = None;
if state.source.from_ostree_repo {
let root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
let host_repo = {
let repodir = root
.open_dir("sysroot/repo")
.context("Opening sysroot/repo")?;
ostree::Repo::open_at_dir(repodir.as_fd(), ".")?
};
ostree_container::store::copy_as(
&host_repo,
&state.source.imageref,
&dest_repo,
&target_imgref.imgref,
)
.await
.context("Copying image from host repo")?;
// We already copied the image, so src == target
src_imageref = target_imgref.clone();
options.target_imgref = None;
} else {
if skopeo_supports_containers_storage()? {
// We always use exactly the digest of the running image to ensure predictability.
let spec =
crate::utils::digested_pullspec(&state.source.imageref.name, &state.source.digest);
ostree_container::ImageReference {
transport: ostree_container::Transport::ContainerStorage,
name: spec,
}
} else {
let td = tempfile::tempdir_in("/var/tmp")?;
let path: &Utf8Path = td.path().try_into().unwrap();
let r = copy_to_oci(&state.source.imageref, path)?;
temporary_dir = Some(td);
r
};
// In this case the deploy code is pulling the container, so set it up to
// generate a target image reference.
options.target_imgref = Some(&target_imgref);
}

println!("Creating initial deployment");
let state =
ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)).await?;
Expand Down Expand Up @@ -884,11 +923,16 @@ fn installation_complete() {
println!("Installation complete!");
}

/// Implementation of the `bootc install` CLI command.
pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
let block_opts = opts.block_opts;
let state = prepare_install(opts.config_opts, opts.target_opts).await?;
pub(crate) async fn install_takeover(
opts: InstallBlockDeviceOpts,
state: Arc<State>,
) -> Result<()> {
// The takeover code should have unset this
assert!(!opts.takeover);
block_install_impl(opts, state).await
}

async fn block_install_impl(block_opts: InstallBlockDeviceOpts, state: Arc<State>) -> Result<()> {
// This is all blocking stuff
let mut rootfs = {
let state = state.clone();
Expand All @@ -914,6 +958,18 @@ pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
Ok(())
}

/// Implementation of the `bootc install` CLI command.
pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
let block_opts = opts.block_opts;
let state = prepare_install(opts.config_opts, opts.target_opts).await?;
if block_opts.takeover {
tracing::debug!("Performing takeover installation from host");
return crate::systemtakeover::run_from_host(block_opts, state).await;
}

block_install_impl(block_opts, state).await
}

#[context("Verifying empty rootfs")]
fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
for e in rootfs_fd.entries()? {
Expand Down
7 changes: 7 additions & 0 deletions lib/src/install/baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ pub(crate) struct InstallBlockDeviceOpts {
#[serde(default)]
pub(crate) wipe: bool,

/// Write to the block device containing the running root filesystem.
/// This is implemented by moving the container into memory and switching
/// root (terminating all other processes).
#[clap(long)]
#[serde(default)]
pub(crate) takeover: bool,

/// Target root block device setup.
///
/// direct: Filesystem written directly to block device
Expand Down
2 changes: 2 additions & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub(crate) mod mount;
mod podman;
pub mod spec;
#[cfg(feature = "install")]
pub(crate) mod systemtakeover;
#[cfg(feature = "install")]
mod task;

#[cfg(feature = "docgen")]
Expand Down
14 changes: 14 additions & 0 deletions lib/src/privtests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use std::process::Command;

use anyhow::Result;
use camino::{Utf8Path, Utf8PathBuf};
use cap_std::fs::Dir;
use cap_std_ext::cap_std;
use fn_error_context::context;
use rustix::fd::AsFd;
use xshell::{cmd, Shell};
Expand Down Expand Up @@ -169,6 +171,18 @@ pub(crate) async fn run(opts: TestingOpts) -> Result<()> {
TestingOpts::RunContainerIntegration {} => {
tokio::task::spawn_blocking(impl_run_container).await?
}
TestingOpts::CopySelfTo { target } => {
let src_root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
let target = &Dir::open_ambient_dir(target, cap_std::ambient_authority())?;
let container_info = crate::containerenv::get_container_execution_info(src_root)?;
let srcdata = crate::install::SourceInfo::from_container(&container_info)?;
let (did_override, _guard) =
crate::install::reexecute_self_for_selinux_if_needed(&srcdata, false)?;
// Right now we don't expose an override flow
assert!(!did_override);
crate::systemtakeover::copy_self_to(&srcdata, target).await?;
Ok(())
}
TestingOpts::PrepTestInstallFilesystem { blockdev } => {
tokio::task::spawn_blocking(move || prep_test_install_filesystem(&blockdev).map(|_| ()))
.await?
Expand Down
Loading

0 comments on commit ab2a45e

Please sign in to comment.