Skip to content

Commit 98fa95e

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 9cc9fee commit 98fa95e

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
@@ -85,6 +85,8 @@ pub(crate) enum TestingOpts {
8585
RunPrivilegedIntegration {},
8686
/// Execute integration tests that target a not-privileged ostree container
8787
RunContainerIntegration {},
88+
/// Copy the container as ostree commit to target root
89+
CopySelfTo { target: Utf8PathBuf },
8890
/// Block device setup for testing
8991
PrepTestInstallFilesystem { blockdev: Utf8PathBuf },
9092
/// e2e test of install-to-filesystem
@@ -369,6 +371,16 @@ where
369371
I: IntoIterator,
370372
I::Item: Into<OsString> + Clone,
371373
{
374+
let args = args
375+
.into_iter()
376+
.map(|v| Into::<OsString>::into(v))
377+
.collect::<Vec<_>>();
378+
if matches!(
379+
args.get(0).and_then(|v| v.to_str()),
380+
Some(crate::systemtakeover::BIN_NAME)
381+
) {
382+
return crate::systemtakeover::run().await;
383+
}
372384
let opt = Opt::parse_from(args);
373385
match opt {
374386
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;
@@ -36,6 +36,9 @@ use crate::lsm::lsm_label;
3636
use crate::task::Task;
3737
use crate::utils::run_in_host_mountns;
3838

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

176181
// Shared read-only global state
@@ -305,11 +310,13 @@ impl SourceInfo {
305310
let root = root.downcast_ref::<ostree::RepoFile>().unwrap();
306311
let xattrs = root.xattrs(cancellable)?;
307312
let selinux = crate::lsm::xattrs_have_selinux(&xattrs);
313+
let from_ostree_repo = false;
308314
Ok(Self {
309315
imageref,
310316
digest,
311317
commit,
312318
selinux,
319+
from_ostree_repo,
313320
})
314321
}
315322
}
@@ -384,6 +391,14 @@ pub(crate) mod config {
384391
}
385392
}
386393

394+
pub(crate) fn import_config_from_host() -> ostree_container::store::ImageProxyConfig {
395+
let skopeo_cmd = run_in_host_mountns("skopeo");
396+
ostree_container::store::ImageProxyConfig {
397+
skopeo_cmd: Some(skopeo_cmd),
398+
..Default::default()
399+
}
400+
}
401+
387402
#[context("Creating ostree deployment")]
388403
async fn initialize_ostree_root_from_self(
389404
state: &State,
@@ -442,49 +457,72 @@ async fn initialize_ostree_root_from_self(
442457

443458
let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
444459
sysroot.load(cancellable)?;
460+
let dest_repo = &sysroot.repo().unwrap();
445461

446462
// We need to fetch the container image from the root mount namespace
447-
let skopeo_cmd = run_in_host_mountns("skopeo");
448-
let proxy_cfg = ostree_container::store::ImageProxyConfig {
449-
skopeo_cmd: Some(skopeo_cmd),
450-
..Default::default()
451-
};
452-
453-
let mut temporary_dir = None;
454-
let src_imageref = if skopeo_supports_containers_storage()? {
455-
// We always use exactly the digest of the running image to ensure predictability.
456-
let spec =
457-
crate::utils::digested_pullspec(&state.source.imageref.name, &state.source.digest);
458-
ostree_container::ImageReference {
459-
transport: ostree_container::Transport::ContainerStorage,
460-
name: spec,
461-
}
462-
} else {
463-
let td = tempfile::tempdir_in("/var/tmp")?;
464-
let path: &Utf8Path = td.path().try_into().unwrap();
465-
let r = copy_to_oci(&state.source.imageref, path)?;
466-
temporary_dir = Some(td);
467-
r
468-
};
469-
let src_imageref = ostree_container::OstreeImageReference {
470-
// There are no signatures to verify since we're fetching the already
471-
// pulled container.
472-
sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
473-
imgref: src_imageref,
474-
};
463+
let proxy_cfg = import_config_from_host();
475464

476465
let kargs = root_setup
477466
.kargs
478467
.iter()
479468
.map(|v| v.as_str())
480469
.collect::<Vec<_>>();
470+
471+
// Default image reference pulls from the running container image.
472+
let mut src_imageref = ostree_container::OstreeImageReference {
473+
// There are no signatures to verify since we're fetching the already
474+
// pulled container.
475+
sigverify: SignatureSource::ContainerPolicyAllowInsecure,
476+
imgref: state.source.imageref.clone(),
477+
};
481478
#[allow(clippy::needless_update)]
482-
let options = ostree_container::deploy::DeployOpts {
479+
let mut options = ostree_container::deploy::DeployOpts {
483480
kargs: Some(kargs.as_slice()),
484-
target_imgref: Some(&target_imgref),
485481
proxy_cfg: Some(proxy_cfg),
486482
..Default::default()
487483
};
484+
485+
let mut temporary_dir = None;
486+
if state.source.from_ostree_repo {
487+
let root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
488+
let host_repo = {
489+
let repodir = root
490+
.open_dir("sysroot/repo")
491+
.context("Opening sysroot/repo")?;
492+
ostree::Repo::open_at_dir(&repodir, ".")?
493+
};
494+
ostree_container::store::copy_as(
495+
&host_repo,
496+
&state.source.imageref,
497+
&dest_repo,
498+
&target_imgref.imgref,
499+
)
500+
.await
501+
.context("Copying image from host repo")?;
502+
// We already copied the image, so src == target
503+
src_imageref = target_imgref.clone();
504+
options.target_imgref = None;
505+
} else {
506+
if skopeo_supports_containers_storage()? {
507+
// We always use exactly the digest of the running image to ensure predictability.
508+
let spec =
509+
crate::utils::digested_pullspec(&state.source.imageref.name, &state.source.digest);
510+
ostree_container::ImageReference {
511+
transport: ostree_container::Transport::ContainerStorage,
512+
name: spec,
513+
}
514+
} else {
515+
let td = tempfile::tempdir_in("/var/tmp")?;
516+
let path: &Utf8Path = td.path().try_into().unwrap();
517+
let r = copy_to_oci(&state.source.imageref, path)?;
518+
temporary_dir = Some(td);
519+
r
520+
};
521+
// In this case the deploy code is pulling the container, so set it up to
522+
// generate a target image reference.
523+
options.target_imgref = Some(&target_imgref);
524+
}
525+
488526
println!("Creating initial deployment");
489527
let state =
490528
ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)).await?;
@@ -825,11 +863,16 @@ fn installation_complete() {
825863
println!("Installation complete!");
826864
}
827865

828-
/// Implementation of the `bootc install` CLI command.
829-
pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
830-
let block_opts = opts.block_opts;
831-
let state = prepare_install(opts.config_opts, opts.target_opts).await?;
866+
pub(crate) async fn install_takeover(
867+
opts: InstallBlockDeviceOpts,
868+
state: Arc<State>,
869+
) -> Result<()> {
870+
// The takeover code should have unset this
871+
assert!(!opts.takeover);
872+
block_install_impl(opts, state).await
873+
}
832874

875+
async fn block_install_impl(block_opts: InstallBlockDeviceOpts, state: Arc<State>) -> Result<()> {
833876
// This is all blocking stuff
834877
let mut rootfs = {
835878
let state = state.clone();
@@ -855,6 +898,18 @@ pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
855898
Ok(())
856899
}
857900

901+
/// Implementation of the `bootc install` CLI command.
902+
pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
903+
let block_opts = opts.block_opts;
904+
let state = prepare_install(opts.config_opts, opts.target_opts).await?;
905+
if block_opts.takeover {
906+
tracing::debug!("Performing takeover installation from host");
907+
return crate::systemtakeover::run_from_host(block_opts, state).await;
908+
}
909+
910+
block_install_impl(block_opts, state).await
911+
}
912+
858913
#[context("Verifying empty rootfs")]
859914
fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
860915
for e in rootfs_fd.entries()? {

lib/src/install/baseline.rs

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

87+
/// Write to the block device containing the running root filesystem.
88+
/// This is implemented by moving the container into memory and switching
89+
/// root (terminating all other processes).
90+
#[clap(long)]
91+
#[serde(default)]
92+
pub(crate) takeover: bool,
93+
8794
/// Target root block device setup.
8895
///
8996
/// 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 cap_std_ext::rustix;
68
use fn_error_context::context;
79
use rustix::fd::AsFd;
@@ -171,6 +173,17 @@ pub(crate) async fn run(opts: TestingOpts) -> Result<()> {
171173
TestingOpts::RunContainerIntegration {} => {
172174
tokio::task::spawn_blocking(impl_run_container).await?
173175
}
176+
TestingOpts::CopySelfTo { target } => {
177+
let target = Dir::open_ambient_dir(target, cap_std::ambient_authority())?;
178+
let container_info = crate::containerenv::get_container_execution_info()?;
179+
let srcdata = crate::install::SourceInfo::from_container(&container_info)?;
180+
let (did_override, _guard) =
181+
crate::install::reexecute_self_for_selinux_if_needed(&srcdata, false)?;
182+
// Right now we don't expose an override flow
183+
assert!(!did_override);
184+
crate::systemtakeover::copy_self_to(&srcdata, &target).await?;
185+
Ok(())
186+
}
174187
TestingOpts::PrepTestInstallFilesystem { blockdev } => {
175188
tokio::task::spawn_blocking(move || prep_test_install_filesystem(&blockdev).map(|_| ()))
176189
.await?

0 commit comments

Comments
 (0)