Skip to content

Commit 773bdfa

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 bfb2c00 commit 773bdfa

File tree

6 files changed

+467
-37
lines changed

6 files changed

+467
-37
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
@@ -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

+92-37
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
@@ -303,11 +308,13 @@ impl SourceInfo {
303308
let root = root.downcast_ref::<ostree::RepoFile>().unwrap();
304309
let xattrs = root.xattrs(cancellable)?;
305310
let selinux = crate::lsm::xattrs_have_selinux(&xattrs);
311+
let from_ostree_repo = false;
306312
Ok(Self {
307313
imageref,
308314
digest,
309315
commit,
310316
selinux,
317+
from_ostree_repo,
311318
})
312319
}
313320
}
@@ -382,6 +389,14 @@ pub(crate) mod config {
382389
}
383390
}
384391

392+
pub(crate) fn import_config_from_host() -> ostree_container::store::ImageProxyConfig {
393+
let skopeo_cmd = run_in_host_mountns("skopeo");
394+
ostree_container::store::ImageProxyConfig {
395+
skopeo_cmd: Some(skopeo_cmd),
396+
..Default::default()
397+
}
398+
}
399+
385400
#[context("Creating ostree deployment")]
386401
async fn initialize_ostree_root_from_self(
387402
state: &State,
@@ -407,12 +422,12 @@ async fn initialize_ostree_root_from_self(
407422
name: imgref.to_string(),
408423
};
409424
ostree_container::OstreeImageReference {
410-
sigverify: target_sigverify,
425+
sigverify: target_sigverify.clone(),
411426
imgref,
412427
}
413428
} else {
414429
ostree_container::OstreeImageReference {
415-
sigverify: target_sigverify,
430+
sigverify: target_sigverify.clone(),
416431
imgref: state.source.imageref.clone(),
417432
}
418433
};
@@ -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?;
@@ -811,11 +849,16 @@ fn installation_complete() {
811849
println!("Installation complete!");
812850
}
813851

814-
/// Implementation of the `bootc install` CLI command.
815-
pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
816-
let block_opts = opts.block_opts;
817-
let state = prepare_install(opts.config_opts, opts.target_opts).await?;
852+
pub(crate) async fn install_takeover(
853+
opts: InstallBlockDeviceOpts,
854+
state: Arc<State>,
855+
) -> Result<()> {
856+
// The takeover code should have unset this
857+
assert!(!opts.takeover);
858+
block_install_impl(opts, state).await
859+
}
818860

861+
async fn block_install_impl(block_opts: InstallBlockDeviceOpts, state: Arc<State>) -> Result<()> {
819862
// This is all blocking stuff
820863
let mut rootfs = {
821864
let state = state.clone();
@@ -841,6 +884,18 @@ pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
841884
Ok(())
842885
}
843886

887+
/// Implementation of the `bootc install` CLI command.
888+
pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
889+
let block_opts = opts.block_opts;
890+
let state = prepare_install(opts.config_opts, opts.target_opts).await?;
891+
if block_opts.takeover {
892+
tracing::debug!("Performing takeover installation from host");
893+
return crate::systemtakeover::run_from_host(block_opts, state).await;
894+
}
895+
896+
block_install_impl(block_opts, state).await
897+
}
898+
844899
#[context("Verifying empty rootfs")]
845900
fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
846901
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;
@@ -169,6 +171,17 @@ pub(crate) async fn run(opts: TestingOpts) -> Result<()> {
169171
TestingOpts::RunContainerIntegration {} => {
170172
tokio::task::spawn_blocking(impl_run_container).await?
171173
}
174+
TestingOpts::CopySelfTo { target } => {
175+
let target = Dir::open_ambient_dir(target, cap_std::ambient_authority())?;
176+
let container_info = crate::containerenv::get_container_execution_info()?;
177+
let srcdata = crate::install::SourceInfo::from_container(&container_info)?;
178+
let did_override =
179+
crate::install::reexecute_self_for_selinux_if_needed(&srcdata, false)?;
180+
// Right now we don't expose an override flow
181+
assert!(!did_override);
182+
crate::systemtakeover::copy_self_to(&srcdata, &target).await?;
183+
Ok(())
184+
}
172185
TestingOpts::PrepTestInstallFilesystem { blockdev } => {
173186
tokio::task::spawn_blocking(move || prep_test_install_filesystem(&blockdev).map(|_| ()))
174187
.await?

0 commit comments

Comments
 (0)