Skip to content

Commit ab2a45e

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 6e0b3b9 commit ab2a45e

File tree

6 files changed

+466
-33
lines changed

6 files changed

+466
-33
lines changed

lib/src/cli.rs

+12
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ pub(crate) enum TestingOpts {
102102
RunPrivilegedIntegration {},
103103
/// Execute integration tests that target a not-privileged ostree container
104104
RunContainerIntegration {},
105+
/// Copy the container as ostree commit to target root
106+
CopySelfTo { target: Utf8PathBuf },
105107
/// Block device setup for testing
106108
PrepTestInstallFilesystem { blockdev: Utf8PathBuf },
107109
/// e2e test of install-to-filesystem
@@ -461,6 +463,16 @@ where
461463
I: IntoIterator,
462464
I::Item: Into<OsString> + Clone,
463465
{
466+
let args = args
467+
.into_iter()
468+
.map(|v| Into::<OsString>::into(v))
469+
.collect::<Vec<_>>();
470+
if matches!(
471+
args.get(0).and_then(|v| v.to_str()),
472+
Some(crate::systemtakeover::BIN_NAME)
473+
) {
474+
return crate::systemtakeover::run().await;
475+
}
464476
run_from_opt(Opt::parse_from(args)).await
465477
}
466478

lib/src/install.rs

+89-33
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;
@@ -37,6 +37,9 @@ use crate::containerenv::ContainerExecutionInfo;
3737
use crate::task::Task;
3838
use crate::utils::run_in_host_mountns;
3939

40+
/// The path we use to access files on the host
41+
pub(crate) const HOST_RUNDIR: &str = "/run/host";
42+
4043
/// The default "stateroot" or "osname"; see https://github.com/ostreedev/ostree/issues/2794
4144
const STATEROOT_DEFAULT: &str = "default";
4245
/// The toplevel boot directory
@@ -196,6 +199,8 @@ pub(crate) struct SourceInfo {
196199
pub(crate) commit: String,
197200
/// Whether or not SELinux appears to be enabled in the source commit
198201
pub(crate) selinux: bool,
202+
/// If we should find the image in sysroot/repo, not in containers/storage
203+
pub(crate) from_ostree_repo: bool,
199204
}
200205

201206
// Shared read-only global state
@@ -345,11 +350,13 @@ impl SourceInfo {
345350
let root = root.downcast_ref::<ostree::RepoFile>().unwrap();
346351
let xattrs = root.xattrs(cancellable)?;
347352
let selinux = crate::lsm::xattrs_have_selinux(&xattrs);
353+
let from_ostree_repo = false;
348354
Ok(Self {
349355
imageref,
350356
digest,
351357
commit,
352358
selinux,
359+
from_ostree_repo,
353360
})
354361
}
355362
}
@@ -424,6 +431,14 @@ pub(crate) mod config {
424431
}
425432
}
426433

434+
pub(crate) fn import_config_from_host() -> ostree_container::store::ImageProxyConfig {
435+
let skopeo_cmd = run_in_host_mountns("skopeo");
436+
ostree_container::store::ImageProxyConfig {
437+
skopeo_cmd: Some(skopeo_cmd),
438+
..Default::default()
439+
}
440+
}
441+
427442
#[context("Creating ostree deployment")]
428443
async fn initialize_ostree_root_from_self(
429444
state: &State,
@@ -492,36 +507,10 @@ async fn initialize_ostree_root_from_self(
492507

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

496512
// We need to fetch the container image from the root mount namespace
497-
let skopeo_cmd = run_in_host_mountns("skopeo");
498-
let proxy_cfg = ostree_container::store::ImageProxyConfig {
499-
skopeo_cmd: Some(skopeo_cmd),
500-
..Default::default()
501-
};
502-
503-
let mut temporary_dir = None;
504-
let src_imageref = if skopeo_supports_containers_storage()? {
505-
// We always use exactly the digest of the running image to ensure predictability.
506-
let spec =
507-
crate::utils::digested_pullspec(&state.source.imageref.name, &state.source.digest);
508-
ostree_container::ImageReference {
509-
transport: ostree_container::Transport::ContainerStorage,
510-
name: spec,
511-
}
512-
} else {
513-
let td = tempfile::tempdir_in("/var/tmp")?;
514-
let path: &Utf8Path = td.path().try_into().unwrap();
515-
let r = copy_to_oci(&state.source.imageref, path)?;
516-
temporary_dir = Some(td);
517-
r
518-
};
519-
let src_imageref = ostree_container::OstreeImageReference {
520-
// There are no signatures to verify since we're fetching the already
521-
// pulled container.
522-
sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
523-
imgref: src_imageref,
524-
};
513+
let proxy_cfg = import_config_from_host();
525514

526515
let kargs = root_setup
527516
.kargs
@@ -532,6 +521,56 @@ async fn initialize_ostree_root_from_self(
532521
options.kargs = Some(kargs.as_slice());
533522
options.target_imgref = Some(&target_imgref);
534523
options.proxy_cfg = Some(proxy_cfg);
524+
525+
// Default image reference pulls from the running container image.
526+
let mut src_imageref = ostree_container::OstreeImageReference {
527+
// There are no signatures to verify since we're fetching the already
528+
// pulled container.
529+
sigverify: SignatureSource::ContainerPolicyAllowInsecure,
530+
imgref: state.source.imageref.clone(),
531+
};
532+
533+
let mut temporary_dir = None;
534+
if state.source.from_ostree_repo {
535+
let root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
536+
let host_repo = {
537+
let repodir = root
538+
.open_dir("sysroot/repo")
539+
.context("Opening sysroot/repo")?;
540+
ostree::Repo::open_at_dir(repodir.as_fd(), ".")?
541+
};
542+
ostree_container::store::copy_as(
543+
&host_repo,
544+
&state.source.imageref,
545+
&dest_repo,
546+
&target_imgref.imgref,
547+
)
548+
.await
549+
.context("Copying image from host repo")?;
550+
// We already copied the image, so src == target
551+
src_imageref = target_imgref.clone();
552+
options.target_imgref = None;
553+
} else {
554+
if skopeo_supports_containers_storage()? {
555+
// We always use exactly the digest of the running image to ensure predictability.
556+
let spec =
557+
crate::utils::digested_pullspec(&state.source.imageref.name, &state.source.digest);
558+
ostree_container::ImageReference {
559+
transport: ostree_container::Transport::ContainerStorage,
560+
name: spec,
561+
}
562+
} else {
563+
let td = tempfile::tempdir_in("/var/tmp")?;
564+
let path: &Utf8Path = td.path().try_into().unwrap();
565+
let r = copy_to_oci(&state.source.imageref, path)?;
566+
temporary_dir = Some(td);
567+
r
568+
};
569+
// In this case the deploy code is pulling the container, so set it up to
570+
// generate a target image reference.
571+
options.target_imgref = Some(&target_imgref);
572+
}
573+
535574
println!("Creating initial deployment");
536575
let state =
537576
ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)).await?;
@@ -884,11 +923,16 @@ fn installation_complete() {
884923
println!("Installation complete!");
885924
}
886925

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?;
926+
pub(crate) async fn install_takeover(
927+
opts: InstallBlockDeviceOpts,
928+
state: Arc<State>,
929+
) -> Result<()> {
930+
// The takeover code should have unset this
931+
assert!(!opts.takeover);
932+
block_install_impl(opts, state).await
933+
}
891934

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

961+
/// Implementation of the `bootc install` CLI command.
962+
pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
963+
let block_opts = opts.block_opts;
964+
let state = prepare_install(opts.config_opts, opts.target_opts).await?;
965+
if block_opts.takeover {
966+
tracing::debug!("Performing takeover installation from host");
967+
return crate::systemtakeover::run_from_host(block_opts, state).await;
968+
}
969+
970+
block_install_impl(block_opts, state).await
971+
}
972+
917973
#[context("Verifying empty rootfs")]
918974
fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
919975
for e in rootfs_fd.entries()? {

lib/src/install/baseline.rs

+7
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ pub(crate) struct InstallBlockDeviceOpts {
7575
#[serde(default)]
7676
pub(crate) wipe: bool,
7777

78+
/// Write to the block device containing the running root filesystem.
79+
/// This is implemented by moving the container into memory and switching
80+
/// root (terminating all other processes).
81+
#[clap(long)]
82+
#[serde(default)]
83+
pub(crate) takeover: bool,
84+
7885
/// Target root block device setup.
7986
///
8087
/// direct: Filesystem written directly to block device

lib/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pub(crate) mod mount;
3939
mod podman;
4040
pub mod spec;
4141
#[cfg(feature = "install")]
42+
pub(crate) mod systemtakeover;
43+
#[cfg(feature = "install")]
4244
mod task;
4345

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

lib/src/privtests.rs

+14
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};
@@ -169,6 +171,18 @@ 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 src_root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
176+
let target = &Dir::open_ambient_dir(target, cap_std::ambient_authority())?;
177+
let container_info = crate::containerenv::get_container_execution_info(src_root)?;
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+
}
172186
TestingOpts::PrepTestInstallFilesystem { blockdev } => {
173187
tokio::task::spawn_blocking(move || prep_test_install_filesystem(&blockdev).map(|_| ()))
174188
.await?

0 commit comments

Comments
 (0)