Skip to content

Commit ddb01c0

Browse files
cgwaltersckyrouac
authored andcommitted
install: Add reset
This is a nondestructive variant of `to-existing-root`. Signed-off-by: Colin Walters <[email protected]>
1 parent c7dae53 commit ddb01c0

File tree

5 files changed

+281
-31
lines changed

5 files changed

+281
-31
lines changed

crates/lib/src/cli.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ use crate::bootc_composefs::{
4242
switch::switch_composefs,
4343
update::upgrade_composefs,
4444
};
45-
use crate::deploy::RequiredHostSpec;
45+
use crate::deploy::{MergeState, RequiredHostSpec, RequiredHostSpec};
4646
use crate::lints;
4747
use crate::podstorage::set_additional_image_store;
4848
use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
@@ -262,6 +262,12 @@ pub(crate) enum InstallOpts {
262262
/// will be wiped, but the content of the existing root will otherwise be retained, and will
263263
/// need to be cleaned up if desired when rebooted into the new root.
264264
ToExistingRoot(crate::install::InstallToExistingRootOpts),
265+
/// Nondestructively create a fresh installation state inside an existing bootc system.
266+
///
267+
/// This is a nondestructive variant of `install to-existing-root` that works only inside
268+
/// an existing bootc system.
269+
#[clap(hide = true)]
270+
Reset(crate::install::InstallResetOpts),
265271
/// Execute this as the penultimate step of an installation using `install to-filesystem`.
266272
///
267273
Finalize {
@@ -962,8 +968,9 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
962968
} else if booted_unchanged {
963969
println!("No update available.")
964970
} else {
965-
let osname = booted_deployment.osname();
966-
crate::deploy::stage(sysroot, &osname, &fetched, &spec, prog.clone()).await?;
971+
let stateroot = booted_deployment.osname();
972+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
973+
crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone()).await?;
967974
changed = true;
968975
if let Some(prev) = booted_image.as_ref() {
969976
if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
@@ -1082,7 +1089,8 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
10821089
}
10831090

10841091
let stateroot = booted_deployment.osname();
1085-
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
1092+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
1093+
crate::deploy::stage(sysroot, from, &fetched, &new_spec, prog.clone()).await?;
10861094

10871095
sysroot.update_mtime()?;
10881096

@@ -1161,7 +1169,8 @@ async fn edit(opts: EditOpts) -> Result<()> {
11611169
// TODO gc old layers here
11621170

11631171
let stateroot = booted_deployment.osname();
1164-
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
1172+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
1173+
crate::deploy::stage(sysroot, from, &fetched, &new_spec, prog.clone()).await?;
11651174

11661175
sysroot.update_mtime()?;
11671176

@@ -1441,6 +1450,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
14411450
InstallOpts::ToExistingRoot(opts) => {
14421451
crate::install::install_to_existing_root(opts).await
14431452
}
1453+
InstallOpts::Reset(opts) => crate::install::install_reset(opts).await,
14441454
InstallOpts::PrintConfiguration => crate::install::print_configuration(),
14451455
InstallOpts::EnsureCompletion {} => {
14461456
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;

crates/lib/src/deploy.rs

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -577,25 +577,26 @@ pub(crate) fn get_base_commit(repo: &ostree::Repo, commit: &str) -> Result<Optio
577577
#[context("Writing deployment")]
578578
async fn deploy(
579579
sysroot: &Storage,
580-
merge_deployment: Option<&Deployment>,
581-
stateroot: &str,
580+
from: MergeState,
582581
image: &ImageState,
583582
origin: &glib::KeyFile,
584583
) -> Result<Deployment> {
585584
// Compute the kernel argument overrides. In practice today this API is always expecting
586585
// a merge deployment. The kargs code also always looks at the booted root (which
587586
// is a distinct minor issue, but not super important as right now the install path
588587
// doesn't use this API).
589-
let override_kargs = if let Some(deployment) = merge_deployment {
590-
Some(crate::bootc_kargs::get_kargs(sysroot, &deployment, image)?)
591-
} else {
592-
None
588+
let (stateroot, override_kargs) = match &from {
589+
MergeState::MergeDeployment(deployment) => {
590+
let kargs = crate::kargs::get_kargs(sysroot, &deployment, image)?;
591+
(deployment.stateroot().into(), kargs)
592+
}
593+
MergeState::Reset { stateroot, kargs } => (stateroot.clone(), kargs.clone()),
593594
};
594595
// Clone all the things to move to worker thread
595596
let ostree = sysroot.get_ostree_cloned()?;
596597
// ostree::Deployment is incorrectly !Send 😢 so convert it to an integer
598+
let merge_deployment = from.as_merge_deployment();
597599
let merge_deployment = merge_deployment.map(|d| d.index() as usize);
598-
let stateroot = stateroot.to_string();
599600
let ostree_commit = image.ostree_commit.to_string();
600601
// GKeyFile also isn't Send! So we serialize that as a string...
601602
let origin_data = origin.to_data();
@@ -609,12 +610,11 @@ async fn deploy(
609610
// Because the C API expects a Vec<&str>, we need to generate a new Vec<>
610611
// that borrows.
611612
let override_kargs = override_kargs
612-
.as_deref()
613-
.map(|v| v.iter().map(|s| s.as_str()).collect::<Vec<_>>());
614-
if let Some(kargs) = override_kargs.as_deref() {
615-
opts.override_kernel_argv = Some(&kargs);
616-
}
617-
let deployments = ostree.deployments();
613+
.iter()
614+
.map(|s| s.as_str())
615+
.collect::<Vec<_>>();
616+
opts.override_kernel_argv = Some(&override_kargs);
617+
let deployments = sysroot.deployments();
618618
let merge_deployment = merge_deployment.map(|m| &deployments[m]);
619619
let origin = glib::KeyFile::new();
620620
origin.load_from_data(&origin_data, glib::KeyFileFlags::NONE)?;
@@ -649,11 +649,41 @@ fn origin_from_imageref(imgref: &ImageReference) -> Result<glib::KeyFile> {
649649
Ok(origin)
650650
}
651651

652+
/// The source of data for staging a new deployment
653+
#[derive(Debug)]
654+
pub(crate) enum MergeState {
655+
/// Use the provided merge deployment
656+
MergeDeployment(Deployment),
657+
/// Don't use a merge deployment, but only this
658+
/// provided initial state.
659+
Reset {
660+
stateroot: String,
661+
kargs: Vec<String>,
662+
},
663+
}
664+
impl MergeState {
665+
/// Initialize using the default merge deployment for the given stateroot.
666+
pub(crate) fn from_stateroot(sysroot: &Storage, stateroot: &str) -> Result<Self> {
667+
let merge_deployment = sysroot.merge_deployment(Some(stateroot)).ok_or_else(|| {
668+
anyhow::anyhow!("No merge deployment found for stateroot {stateroot}")
669+
})?;
670+
Ok(Self::MergeDeployment(merge_deployment))
671+
}
672+
673+
/// Cast this to a merge deployment case.
674+
pub(crate) fn as_merge_deployment(&self) -> Option<&Deployment> {
675+
match self {
676+
Self::MergeDeployment(d) => Some(d),
677+
Self::Reset { .. } => None,
678+
}
679+
}
680+
}
681+
652682
/// Stage (queue deployment of) a fetched container image.
653683
#[context("Staging")]
654684
pub(crate) async fn stage(
655685
sysroot: &Storage,
656-
stateroot: &str,
686+
from: MergeState,
657687
image: &ImageState,
658688
spec: &RequiredHostSpec<'_>,
659689
prog: ProgressWriter,
@@ -694,7 +724,6 @@ pub(crate) async fn stage(
694724
.collect(),
695725
})
696726
.await;
697-
let merge_deployment = ostree.merge_deployment(Some(stateroot));
698727

699728
subtask.completed = true;
700729
subtasks.push(subtask.clone());
@@ -717,14 +746,7 @@ pub(crate) async fn stage(
717746
})
718747
.await;
719748
let origin = origin_from_imageref(spec.image)?;
720-
let deployment = crate::deploy::deploy(
721-
sysroot,
722-
merge_deployment.as_ref(),
723-
stateroot,
724-
image,
725-
&origin,
726-
)
727-
.await?;
749+
let deployment = crate::deploy::deploy(sysroot, from, image, &origin).await?;
728750

729751
subtask.completed = true;
730752
subtasks.push(subtask.clone());

crates/lib/src/install.rs

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use ostree::gio;
4444
use ostree_ext::ostree;
4545
use ostree_ext::ostree_prepareroot::{ComposefsState, Tristate};
4646
use ostree_ext::prelude::Cast;
47-
use ostree_ext::sysroot::SysrootLock;
47+
use ostree_ext::sysroot::{allocate_new_stateroot, list_stateroots, SysrootLock};
4848
use ostree_ext::{container as ostree_container, ostree_prepareroot};
4949
#[cfg(feature = "install-to-disk")]
5050
use rustix::fs::FileTypeExt;
@@ -57,7 +57,10 @@ use self::baseline::InstallBlockDeviceOpts;
5757
use crate::bootc_composefs::{boot::setup_composefs_boot, repo::initialize_composefs_repository};
5858
use crate::boundimage::{BoundImage, ResolvedBoundImage};
5959
use crate::containerenv::ContainerExecutionInfo;
60-
use crate::deploy::{prepare_for_pull, pull_from_prepared, PreparedImportMeta, PreparedPullResult};
60+
use crate::deploy::{
61+
prepare_for_pull, pull_from_prepared, MergeState, PreparedImportMeta, PreparedPullResult,
62+
};
63+
use crate::kernel_cmdline::Cmdline;
6164
use crate::lsm;
6265
use crate::progress_jsonl::ProgressWriter;
6366
use crate::spec::{Bootloader, ImageReference};
@@ -413,6 +416,50 @@ pub(crate) struct InstallToExistingRootOpts {
413416
pub(crate) composefs_opts: InstallComposefsOpts,
414417
}
415418

419+
#[derive(Debug, clap::Parser, PartialEq, Eq)]
420+
pub(crate) struct InstallResetOpts {
421+
/// Acknowledge that this command is experimental.
422+
#[clap(long)]
423+
pub(crate) experimental: bool,
424+
425+
#[clap(flatten)]
426+
pub(crate) source_opts: InstallSourceOpts,
427+
428+
#[clap(flatten)]
429+
pub(crate) target_opts: InstallTargetOpts,
430+
431+
/// Name of the target stateroot. If not provided, one will be automatically
432+
/// generated of the form s<year>-<serial> where <serial> starts at zero and
433+
/// increments automatically.
434+
#[clap(long)]
435+
pub(crate) stateroot: Option<String>,
436+
437+
/// Don't display progress
438+
#[clap(long)]
439+
pub(crate) quiet: bool,
440+
441+
#[clap(flatten)]
442+
pub(crate) progress: crate::cli::ProgressOptions,
443+
444+
/// Restart or reboot into the new target image.
445+
///
446+
/// Currently, this option always reboots. In the future this command
447+
/// will detect the case where no kernel changes are queued, and perform
448+
/// a userspace-only restart.
449+
#[clap(long)]
450+
pub(crate) apply: bool,
451+
452+
/// Skip inheriting any automatically discovered root file system kernel arguments.
453+
#[clap(long)]
454+
no_root_kargs: bool,
455+
456+
/// Add a kernel argument. This option can be provided multiple times.
457+
///
458+
/// Example: --karg=nosmt --karg=console=ttyS0,114800n8
459+
#[clap(long)]
460+
karg: Option<Vec<String>>,
461+
}
462+
416463
/// Global state captured from the container.
417464
#[derive(Debug, Clone)]
418465
pub(crate) struct SourceInfo {
@@ -458,6 +505,24 @@ pub(crate) struct State {
458505
pub(crate) detected_bootloader: crate::spec::Bootloader,
459506
}
460507

508+
impl InstallTargetOpts {
509+
pub(crate) fn imageref(&self) -> Result<Option<ostree_container::OstreeImageReference>> {
510+
let Some(target_imgname) = self.target_imgref.as_deref() else {
511+
return Ok(None);
512+
};
513+
let target_transport =
514+
ostree_container::Transport::try_from(self.target_transport.as_str())?;
515+
let target_imgref = ostree_container::OstreeImageReference {
516+
sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
517+
imgref: ostree_container::ImageReference {
518+
transport: target_transport,
519+
name: target_imgname.to_string(),
520+
},
521+
};
522+
Ok(Some(target_imgref))
523+
}
524+
}
525+
461526
impl State {
462527
#[context("Loading SELinux policy")]
463528
pub(crate) fn load_policy(&self) -> Result<Option<ostree::SePolicy>> {
@@ -2201,6 +2266,94 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
22012266
install_to_filesystem(opts, true, cleanup).await
22022267
}
22032268

2269+
pub(crate) async fn install_reset(opts: InstallResetOpts) -> Result<()> {
2270+
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2271+
if !opts.experimental {
2272+
anyhow::bail!("This command requires --experimental");
2273+
}
2274+
2275+
let prog: ProgressWriter = opts.progress.try_into()?;
2276+
2277+
let sysroot = &crate::cli::get_storage().await?;
2278+
let repo = &sysroot.repo();
2279+
let (booted_deployment, _deployments, host) =
2280+
crate::status::get_status_require_booted(sysroot)?;
2281+
2282+
let stateroots = list_stateroots(sysroot)?;
2283+
dbg!(&stateroots);
2284+
let target_stateroot = if let Some(s) = opts.stateroot {
2285+
s
2286+
} else {
2287+
let now = chrono::Utc::now();
2288+
let r = allocate_new_stateroot(&sysroot, &stateroots, now)?;
2289+
r.name
2290+
};
2291+
2292+
let booted_stateroot = booted_deployment.osname();
2293+
assert!(booted_stateroot.as_str() != target_stateroot);
2294+
let (fetched, spec) = if let Some(target) = opts.target_opts.imageref()? {
2295+
let mut new_spec = host.spec;
2296+
new_spec.image = Some(target.into());
2297+
let fetched = crate::deploy::pull(
2298+
repo,
2299+
&new_spec.image.as_ref().unwrap(),
2300+
None,
2301+
opts.quiet,
2302+
prog.clone(),
2303+
)
2304+
.await?;
2305+
(fetched, new_spec)
2306+
} else {
2307+
let imgstate = host
2308+
.status
2309+
.booted
2310+
.map(|b| b.query_image(repo))
2311+
.transpose()?
2312+
.flatten()
2313+
.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
2314+
(Box::new((*imgstate).into()), host.spec)
2315+
};
2316+
let spec = crate::deploy::RequiredHostSpec::from_spec(&spec)?;
2317+
2318+
// Compute the kernel arguments to inherit. By default, that's only those involved
2319+
// in the root filesystem.
2320+
let root_kargs = if opts.no_root_kargs {
2321+
Vec::new()
2322+
} else {
2323+
let bootcfg = booted_deployment
2324+
.bootconfig()
2325+
.ok_or_else(|| anyhow!("Missing bootcfg for booted deployment"))?;
2326+
if let Some(options) = bootcfg.get("options") {
2327+
let options = options.split_ascii_whitespace().collect::<Vec<_>>();
2328+
crate::kernel::root_args_from_cmdline(&options)
2329+
.into_iter()
2330+
.map(ToOwned::to_owned)
2331+
.collect::<Vec<_>>()
2332+
} else {
2333+
Vec::new()
2334+
}
2335+
};
2336+
2337+
let kargs = crate::kargs::get_kargs_in_root(rootfs, std::env::consts::ARCH)?
2338+
.into_iter()
2339+
.chain(root_kargs.into_iter())
2340+
.chain(opts.karg.unwrap_or_default())
2341+
.collect::<Vec<_>>();
2342+
2343+
let from = MergeState::Reset {
2344+
stateroot: target_stateroot,
2345+
kargs,
2346+
};
2347+
crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone()).await?;
2348+
2349+
sysroot.update_mtime()?;
2350+
2351+
if opts.apply {
2352+
crate::reboot::reboot()?;
2353+
}
2354+
Ok(())
2355+
}
2356+
22042357
/// Implementation of `bootc install finalize`.
22052358
pub(crate) async fn install_finalize(target: &Utf8Path) -> Result<()> {
22062359
// Log the installation finalization operation to systemd journal

crates/ostree-ext/src/sysroot.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ impl Deref for SysrootLock {
3838
}
3939
}
4040

41-
4241
/// Access the file descriptor for a sysroot
4342
#[allow(unsafe_code)]
4443
pub fn sysroot_fd(sysroot: &ostree::Sysroot) -> BorrowedFd {

0 commit comments

Comments
 (0)