Skip to content

Commit d38a5d5

Browse files
committed
WIP: takeover installs
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).
1 parent 730c07e commit d38a5d5

File tree

6 files changed

+466
-35
lines changed

6 files changed

+466
-35
lines changed

lib/src/cli.rs

+12
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ pub(crate) enum TestingOpts {
8484
RunPrivilegedIntegration {},
8585
/// Execute integration tests that target a not-privileged ostree container
8686
RunContainerIntegration {},
87+
/// Copy the container as ostree commit to target root
88+
CopySelfTo { target: Utf8PathBuf },
8789
/// Block device setup for testing
8890
PrepTestInstallFilesystem { blockdev: Utf8PathBuf },
8991
/// e2e test of install-to-filesystem
@@ -368,6 +370,16 @@ where
368370
I: IntoIterator,
369371
I::Item: Into<OsString> + Clone,
370372
{
373+
let args = args
374+
.into_iter()
375+
.map(|v| Into::<OsString>::into(v))
376+
.collect::<Vec<_>>();
377+
if matches!(
378+
args.get(0).and_then(|v| v.to_str()),
379+
Some(crate::systemtakeover::BIN_NAME)
380+
) {
381+
return crate::systemtakeover::run().await;
382+
}
371383
let opt = Opt::parse_from(args);
372384
match opt {
373385
Opt::Upgrade(opts) => upgrade(opts).await,

lib/src/install.rs

+90-35
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
// This sub-module is the "basic" installer that handles creating basic block device
88
// and filesystem setup.
9-
mod baseline;
9+
pub(crate) mod baseline;
1010

1111
use std::io::BufWriter;
1212
use std::io::Write;
@@ -35,6 +35,9 @@ use crate::containerenv::ContainerExecutionInfo;
3535
use crate::task::Task;
3636
use crate::utils::run_in_host_mountns;
3737

38+
/// The path we use to access files on the host
39+
pub(crate) const HOST_RUNDIR: &str = "/run/host";
40+
3841
/// The default "stateroot" or "osname"; see https://github.com/ostreedev/ostree/issues/2794
3942
const STATEROOT_DEFAULT: &str = "default";
4043
/// The toplevel boot directory
@@ -170,6 +173,8 @@ pub(crate) struct SourceInfo {
170173
pub(crate) commit: String,
171174
/// Whether or not SELinux appears to be enabled in the source commit
172175
pub(crate) selinux: bool,
176+
/// If we should find the image in sysroot/repo, not in containers/storage
177+
pub(crate) from_ostree_repo: bool,
173178
}
174179

175180
// Shared read-only global state
@@ -319,11 +324,13 @@ impl SourceInfo {
319324
let root = root.downcast_ref::<ostree::RepoFile>().unwrap();
320325
let xattrs = root.xattrs(cancellable)?;
321326
let selinux = crate::lsm::xattrs_have_selinux(&xattrs);
327+
let from_ostree_repo = false;
322328
Ok(Self {
323329
imageref,
324330
digest,
325331
commit,
326332
selinux,
333+
from_ostree_repo,
327334
})
328335
}
329336
}
@@ -398,6 +405,14 @@ pub(crate) mod config {
398405
}
399406
}
400407

408+
pub(crate) fn import_config_from_host() -> ostree_container::store::ImageProxyConfig {
409+
let skopeo_cmd = run_in_host_mountns("skopeo");
410+
ostree_container::store::ImageProxyConfig {
411+
skopeo_cmd: Some(skopeo_cmd),
412+
..Default::default()
413+
}
414+
}
415+
401416
#[context("Creating ostree deployment")]
402417
async fn initialize_ostree_root_from_self(
403418
state: &State,
@@ -456,49 +471,72 @@ async fn initialize_ostree_root_from_self(
456471

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

460476
// We need to fetch the container image from the root mount namespace
461-
let skopeo_cmd = run_in_host_mountns("skopeo");
462-
let proxy_cfg = ostree_container::store::ImageProxyConfig {
463-
skopeo_cmd: Some(skopeo_cmd),
464-
..Default::default()
465-
};
466-
467-
let mut temporary_dir = None;
468-
let src_imageref = if skopeo_supports_containers_storage()? {
469-
// We always use exactly the digest of the running image to ensure predictability.
470-
let spec =
471-
crate::utils::digested_pullspec(&state.source.imageref.name, &state.source.digest);
472-
ostree_container::ImageReference {
473-
transport: ostree_container::Transport::ContainerStorage,
474-
name: spec,
475-
}
476-
} else {
477-
let td = tempfile::tempdir_in("/var/tmp")?;
478-
let path: &Utf8Path = td.path().try_into().unwrap();
479-
let r = copy_to_oci(&state.source.imageref, path)?;
480-
temporary_dir = Some(td);
481-
r
482-
};
483-
let src_imageref = ostree_container::OstreeImageReference {
484-
// There are no signatures to verify since we're fetching the already
485-
// pulled container.
486-
sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
487-
imgref: src_imageref,
488-
};
477+
let proxy_cfg = import_config_from_host();
489478

490479
let kargs = root_setup
491480
.kargs
492481
.iter()
493482
.map(|v| v.as_str())
494483
.collect::<Vec<_>>();
484+
485+
// Default image reference pulls from the running container image.
486+
let mut src_imageref = ostree_container::OstreeImageReference {
487+
// There are no signatures to verify since we're fetching the already
488+
// pulled container.
489+
sigverify: SignatureSource::ContainerPolicyAllowInsecure,
490+
imgref: state.source.imageref.clone(),
491+
};
495492
#[allow(clippy::needless_update)]
496-
let options = ostree_container::deploy::DeployOpts {
493+
let mut options = ostree_container::deploy::DeployOpts {
497494
kargs: Some(kargs.as_slice()),
498-
target_imgref: Some(&target_imgref),
499495
proxy_cfg: Some(proxy_cfg),
500496
..Default::default()
501497
};
498+
499+
let mut temporary_dir = None;
500+
if state.source.from_ostree_repo {
501+
let root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
502+
let host_repo = {
503+
let repodir = root
504+
.open_dir("sysroot/repo")
505+
.context("Opening sysroot/repo")?;
506+
ostree::Repo::open_at_dir(&repodir, ".")?
507+
};
508+
ostree_container::store::copy_as(
509+
&host_repo,
510+
&state.source.imageref,
511+
&dest_repo,
512+
&target_imgref.imgref,
513+
)
514+
.await
515+
.context("Copying image from host repo")?;
516+
// We already copied the image, so src == target
517+
src_imageref = target_imgref.clone();
518+
options.target_imgref = None;
519+
} else {
520+
if skopeo_supports_containers_storage()? {
521+
// We always use exactly the digest of the running image to ensure predictability.
522+
let spec =
523+
crate::utils::digested_pullspec(&state.source.imageref.name, &state.source.digest);
524+
ostree_container::ImageReference {
525+
transport: ostree_container::Transport::ContainerStorage,
526+
name: spec,
527+
}
528+
} else {
529+
let td = tempfile::tempdir_in("/var/tmp")?;
530+
let path: &Utf8Path = td.path().try_into().unwrap();
531+
let r = copy_to_oci(&state.source.imageref, path)?;
532+
temporary_dir = Some(td);
533+
r
534+
};
535+
// In this case the deploy code is pulling the container, so set it up to
536+
// generate a target image reference.
537+
options.target_imgref = Some(&target_imgref);
538+
}
539+
502540
println!("Creating initial deployment");
503541
let state =
504542
ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)).await?;
@@ -839,11 +877,16 @@ fn installation_complete() {
839877
println!("Installation complete!");
840878
}
841879

842-
/// Implementation of the `bootc install` CLI command.
843-
pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
844-
let block_opts = opts.block_opts;
845-
let state = prepare_install(opts.config_opts, opts.target_opts).await?;
880+
pub(crate) async fn install_takeover(
881+
opts: InstallBlockDeviceOpts,
882+
state: Arc<State>,
883+
) -> Result<()> {
884+
// The takeover code should have unset this
885+
assert!(!opts.takeover);
886+
block_install_impl(opts, state).await
887+
}
846888

889+
async fn block_install_impl(block_opts: InstallBlockDeviceOpts, state: Arc<State>) -> Result<()> {
847890
// This is all blocking stuff
848891
let mut rootfs = {
849892
let state = state.clone();
@@ -869,6 +912,18 @@ pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
869912
Ok(())
870913
}
871914

915+
/// Implementation of the `bootc install` CLI command.
916+
pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
917+
let block_opts = opts.block_opts;
918+
let state = prepare_install(opts.config_opts, opts.target_opts).await?;
919+
if block_opts.takeover {
920+
tracing::debug!("Performing takeover installation from host");
921+
return crate::systemtakeover::run_from_host(block_opts, state).await;
922+
}
923+
924+
block_install_impl(block_opts, state).await
925+
}
926+
872927
#[context("Verifying empty rootfs")]
873928
fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
874929
for e in rootfs_fd.entries()? {

lib/src/install/baseline.rs

+7
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ pub(crate) struct InstallBlockDeviceOpts {
8383
#[serde(default)]
8484
pub(crate) wipe: bool,
8585

86+
/// Write to the block device containing the running root filesystem.
87+
/// This is implemented by moving the container into memory and switching
88+
/// root (terminating all other processes).
89+
#[clap(long)]
90+
#[serde(default)]
91+
pub(crate) takeover: bool,
92+
8693
/// Target root block device setup.
8794
///
8895
/// direct: Filesystem written directly to block device

lib/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub(crate) mod mount;
3737
#[cfg(feature = "install")]
3838
mod podman;
3939
#[cfg(feature = "install")]
40+
pub(crate) mod systemtakeover;
41+
#[cfg(feature = "install")]
4042
mod task;
4143

4244
#[cfg(feature = "docgen")]

lib/src/privtests.rs

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::process::Command;
22

33
use anyhow::Result;
44
use camino::{Utf8Path, Utf8PathBuf};
5+
use cap_std::fs::Dir;
6+
use cap_std_ext::cap_std;
57
use fn_error_context::context;
68
use rustix::fd::AsFd;
79
use xshell::{cmd, Shell};
@@ -170,6 +172,17 @@ pub(crate) async fn run(opts: TestingOpts) -> Result<()> {
170172
TestingOpts::RunContainerIntegration {} => {
171173
tokio::task::spawn_blocking(impl_run_container).await?
172174
}
175+
TestingOpts::CopySelfTo { target } => {
176+
let target = Dir::open_ambient_dir(target, cap_std::ambient_authority())?;
177+
let container_info = crate::containerenv::get_container_execution_info()?;
178+
let srcdata = crate::install::SourceInfo::from_container(&container_info)?;
179+
let (did_override, _guard) =
180+
crate::install::reexecute_self_for_selinux_if_needed(&srcdata, false)?;
181+
// Right now we don't expose an override flow
182+
assert!(!did_override);
183+
crate::systemtakeover::copy_self_to(&srcdata, &target).await?;
184+
Ok(())
185+
}
173186
TestingOpts::PrepTestInstallFilesystem { blockdev } => {
174187
tokio::task::spawn_blocking(move || prep_test_install_filesystem(&blockdev).map(|_| ()))
175188
.await?

0 commit comments

Comments
 (0)