Skip to content

Commit 7b96fa1

Browse files
committed
install: Add reset
This is a nondestructive variant of `to-existing-root`. Signed-off-by: Colin Walters <[email protected]>
1 parent cf6c4a4 commit 7b96fa1

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 {
@@ -963,8 +969,9 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
963969
} else if booted_unchanged {
964970
println!("No update available.")
965971
} else {
966-
let osname = booted_deployment.osname();
967-
crate::deploy::stage(sysroot, &osname, &fetched, &spec, prog.clone()).await?;
972+
let stateroot = booted_deployment.osname();
973+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
974+
crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone()).await?;
968975
changed = true;
969976
if let Some(prev) = booted_image.as_ref() {
970977
if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
@@ -1083,7 +1090,8 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
10831090
}
10841091

10851092
let stateroot = booted_deployment.osname();
1086-
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
1093+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
1094+
crate::deploy::stage(sysroot, from, &fetched, &new_spec, prog.clone()).await?;
10871095

10881096
sysroot.update_mtime()?;
10891097

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

11641172
let stateroot = booted_deployment.osname();
1165-
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
1173+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
1174+
crate::deploy::stage(sysroot, from, &fetched, &new_spec, prog.clone()).await?;
11661175

11671176
sysroot.update_mtime()?;
11681177

@@ -1426,6 +1435,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
14261435
InstallOpts::ToExistingRoot(opts) => {
14271436
crate::install::install_to_existing_root(opts).await
14281437
}
1438+
InstallOpts::Reset(opts) => crate::install::install_reset(opts).await,
14291439
InstallOpts::PrintConfiguration => crate::install::print_configuration(),
14301440
InstallOpts::EnsureCompletion {} => {
14311441
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;
@@ -56,7 +56,10 @@ use self::baseline::InstallBlockDeviceOpts;
5656
use crate::bootc_composefs::{boot::setup_composefs_boot, repo::initialize_composefs_repository};
5757
use crate::boundimage::{BoundImage, ResolvedBoundImage};
5858
use crate::containerenv::ContainerExecutionInfo;
59-
use crate::deploy::{prepare_for_pull, pull_from_prepared, PreparedImportMeta, PreparedPullResult};
59+
use crate::deploy::{
60+
prepare_for_pull, pull_from_prepared, MergeState, PreparedImportMeta, PreparedPullResult,
61+
};
62+
use crate::kernel_cmdline::Cmdline;
6063
use crate::lsm;
6164
use crate::progress_jsonl::ProgressWriter;
6265
use crate::spec::{Bootloader, ImageReference};
@@ -390,6 +393,50 @@ pub(crate) struct InstallToExistingRootOpts {
390393
pub(crate) composefs_opts: InstallComposefsOpts,
391394
}
392395

396+
#[derive(Debug, clap::Parser, PartialEq, Eq)]
397+
pub(crate) struct InstallResetOpts {
398+
/// Acknowledge that this command is experimental.
399+
#[clap(long)]
400+
pub(crate) experimental: bool,
401+
402+
#[clap(flatten)]
403+
pub(crate) source_opts: InstallSourceOpts,
404+
405+
#[clap(flatten)]
406+
pub(crate) target_opts: InstallTargetOpts,
407+
408+
/// Name of the target stateroot. If not provided, one will be automatically
409+
/// generated of the form s<year>-<serial> where <serial> starts at zero and
410+
/// increments automatically.
411+
#[clap(long)]
412+
pub(crate) stateroot: Option<String>,
413+
414+
/// Don't display progress
415+
#[clap(long)]
416+
pub(crate) quiet: bool,
417+
418+
#[clap(flatten)]
419+
pub(crate) progress: crate::cli::ProgressOptions,
420+
421+
/// Restart or reboot into the new target image.
422+
///
423+
/// Currently, this option always reboots. In the future this command
424+
/// will detect the case where no kernel changes are queued, and perform
425+
/// a userspace-only restart.
426+
#[clap(long)]
427+
pub(crate) apply: bool,
428+
429+
/// Skip inheriting any automatically discovered root file system kernel arguments.
430+
#[clap(long)]
431+
no_root_kargs: bool,
432+
433+
/// Add a kernel argument. This option can be provided multiple times.
434+
///
435+
/// Example: --karg=nosmt --karg=console=ttyS0,114800n8
436+
#[clap(long)]
437+
karg: Option<Vec<String>>,
438+
}
439+
393440
/// Global state captured from the container.
394441
#[derive(Debug, Clone)]
395442
pub(crate) struct SourceInfo {
@@ -434,6 +481,24 @@ pub(crate) struct State {
434481
pub(crate) detected_bootloader: crate::spec::Bootloader,
435482
}
436483

484+
impl InstallTargetOpts {
485+
pub(crate) fn imageref(&self) -> Result<Option<ostree_container::OstreeImageReference>> {
486+
let Some(target_imgname) = self.target_imgref.as_deref() else {
487+
return Ok(None);
488+
};
489+
let target_transport =
490+
ostree_container::Transport::try_from(self.target_transport.as_str())?;
491+
let target_imgref = ostree_container::OstreeImageReference {
492+
sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
493+
imgref: ostree_container::ImageReference {
494+
transport: target_transport,
495+
name: target_imgname.to_string(),
496+
},
497+
};
498+
Ok(Some(target_imgref))
499+
}
500+
}
501+
437502
impl State {
438503
#[context("Loading SELinux policy")]
439504
pub(crate) fn load_policy(&self) -> Result<Option<ostree::SePolicy>> {
@@ -2160,6 +2225,94 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
21602225
install_to_filesystem(opts, true, cleanup).await
21612226
}
21622227

2228+
pub(crate) async fn install_reset(opts: InstallResetOpts) -> Result<()> {
2229+
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2230+
if !opts.experimental {
2231+
anyhow::bail!("This command requires --experimental");
2232+
}
2233+
2234+
let prog: ProgressWriter = opts.progress.try_into()?;
2235+
2236+
let sysroot = &crate::cli::get_storage().await?;
2237+
let repo = &sysroot.repo();
2238+
let (booted_deployment, _deployments, host) =
2239+
crate::status::get_status_require_booted(sysroot)?;
2240+
2241+
let stateroots = list_stateroots(sysroot)?;
2242+
dbg!(&stateroots);
2243+
let target_stateroot = if let Some(s) = opts.stateroot {
2244+
s
2245+
} else {
2246+
let now = chrono::Utc::now();
2247+
let r = allocate_new_stateroot(&sysroot, &stateroots, now)?;
2248+
r.name
2249+
};
2250+
2251+
let booted_stateroot = booted_deployment.osname();
2252+
assert!(booted_stateroot.as_str() != target_stateroot);
2253+
let (fetched, spec) = if let Some(target) = opts.target_opts.imageref()? {
2254+
let mut new_spec = host.spec;
2255+
new_spec.image = Some(target.into());
2256+
let fetched = crate::deploy::pull(
2257+
repo,
2258+
&new_spec.image.as_ref().unwrap(),
2259+
None,
2260+
opts.quiet,
2261+
prog.clone(),
2262+
)
2263+
.await?;
2264+
(fetched, new_spec)
2265+
} else {
2266+
let imgstate = host
2267+
.status
2268+
.booted
2269+
.map(|b| b.query_image(repo))
2270+
.transpose()?
2271+
.flatten()
2272+
.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
2273+
(Box::new((*imgstate).into()), host.spec)
2274+
};
2275+
let spec = crate::deploy::RequiredHostSpec::from_spec(&spec)?;
2276+
2277+
// Compute the kernel arguments to inherit. By default, that's only those involved
2278+
// in the root filesystem.
2279+
let root_kargs = if opts.no_root_kargs {
2280+
Vec::new()
2281+
} else {
2282+
let bootcfg = booted_deployment
2283+
.bootconfig()
2284+
.ok_or_else(|| anyhow!("Missing bootcfg for booted deployment"))?;
2285+
if let Some(options) = bootcfg.get("options") {
2286+
let options = options.split_ascii_whitespace().collect::<Vec<_>>();
2287+
crate::kernel::root_args_from_cmdline(&options)
2288+
.into_iter()
2289+
.map(ToOwned::to_owned)
2290+
.collect::<Vec<_>>()
2291+
} else {
2292+
Vec::new()
2293+
}
2294+
};
2295+
2296+
let kargs = crate::kargs::get_kargs_in_root(rootfs, std::env::consts::ARCH)?
2297+
.into_iter()
2298+
.chain(root_kargs.into_iter())
2299+
.chain(opts.karg.unwrap_or_default())
2300+
.collect::<Vec<_>>();
2301+
2302+
let from = MergeState::Reset {
2303+
stateroot: target_stateroot,
2304+
kargs,
2305+
};
2306+
crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone()).await?;
2307+
2308+
sysroot.update_mtime()?;
2309+
2310+
if opts.apply {
2311+
crate::reboot::reboot()?;
2312+
}
2313+
Ok(())
2314+
}
2315+
21632316
/// Implementation of `bootc install finalize`.
21642317
pub(crate) async fn install_finalize(target: &Utf8Path) -> Result<()> {
21652318
// 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)