Skip to content

Commit 644469f

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 5236268 commit 644469f

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
@@ -34,7 +34,7 @@ use crate::bootc_composefs::{
3434
finalize::composefs_native_finalize, rollback::composefs_rollback, status::composefs_booted,
3535
switch::switch_composefs, update::upgrade_composefs,
3636
};
37-
use crate::deploy::RequiredHostSpec;
37+
use crate::deploy::{MergeState, RequiredHostSpec, RequiredHostSpec};
3838
use crate::lints;
3939
use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
4040
use crate::spec::Host;
@@ -252,6 +252,12 @@ pub(crate) enum InstallOpts {
252252
/// will be wiped, but the content of the existing root will otherwise be retained, and will
253253
/// need to be cleaned up if desired when rebooted into the new root.
254254
ToExistingRoot(crate::install::InstallToExistingRootOpts),
255+
/// Nondestructively create a fresh installation state inside an existing bootc system.
256+
///
257+
/// This is a nondestructive variant of `install to-existing-root` that works only inside
258+
/// an existing bootc system.
259+
#[clap(hide = true)]
260+
Reset(crate::install::InstallResetOpts),
255261
/// Execute this as the penultimate step of an installation using `install to-filesystem`.
256262
///
257263
Finalize {
@@ -943,8 +949,9 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
943949
} else if booted_unchanged {
944950
println!("No update available.")
945951
} else {
946-
let osname = booted_deployment.osname();
947-
crate::deploy::stage(sysroot, &osname, &fetched, &spec, prog.clone()).await?;
952+
let stateroot = booted_deployment.osname();
953+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
954+
crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone()).await?;
948955
changed = true;
949956
if let Some(prev) = booted_image.as_ref() {
950957
if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
@@ -1063,7 +1070,8 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
10631070
}
10641071

10651072
let stateroot = booted_deployment.osname();
1066-
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
1073+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
1074+
crate::deploy::stage(sysroot, from, &fetched, &new_spec, prog.clone()).await?;
10671075

10681076
sysroot.update_mtime()?;
10691077

@@ -1142,7 +1150,8 @@ async fn edit(opts: EditOpts) -> Result<()> {
11421150
// TODO gc old layers here
11431151

11441152
let stateroot = booted_deployment.osname();
1145-
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
1153+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
1154+
crate::deploy::stage(sysroot, from, &fetched, &new_spec, prog.clone()).await?;
11461155

11471156
sysroot.update_mtime()?;
11481157

@@ -1363,6 +1372,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
13631372
InstallOpts::ToExistingRoot(opts) => {
13641373
crate::install::install_to_existing_root(opts).await
13651374
}
1375+
InstallOpts::Reset(opts) => crate::install::install_reset(opts).await,
13661376
InstallOpts::PrintConfiguration => crate::install::print_configuration(),
13671377
InstallOpts::EnsureCompletion {} => {
13681378
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};
@@ -399,6 +402,50 @@ pub(crate) struct InstallToExistingRootOpts {
399402
pub(crate) root_path: Utf8PathBuf,
400403
}
401404

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

487+
impl InstallTargetOpts {
488+
pub(crate) fn imageref(&self) -> Result<Option<ostree_container::OstreeImageReference>> {
489+
let Some(target_imgname) = self.target_imgref.as_deref() else {
490+
return Ok(None);
491+
};
492+
let target_transport =
493+
ostree_container::Transport::try_from(self.target_transport.as_str())?;
494+
let target_imgref = ostree_container::OstreeImageReference {
495+
sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
496+
imgref: ostree_container::ImageReference {
497+
transport: target_transport,
498+
name: target_imgname.to_string(),
499+
},
500+
};
501+
Ok(Some(target_imgref))
502+
}
503+
}
504+
440505
impl State {
441506
#[context("Loading SELinux policy")]
442507
pub(crate) fn load_policy(&self) -> Result<Option<ostree::SePolicy>> {
@@ -2128,6 +2193,94 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
21282193
install_to_filesystem(opts, true, cleanup).await
21292194
}
21302195

2196+
pub(crate) async fn install_reset(opts: InstallResetOpts) -> Result<()> {
2197+
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2198+
if !opts.experimental {
2199+
anyhow::bail!("This command requires --experimental");
2200+
}
2201+
2202+
let prog: ProgressWriter = opts.progress.try_into()?;
2203+
2204+
let sysroot = &crate::cli::get_storage().await?;
2205+
let repo = &sysroot.repo();
2206+
let (booted_deployment, _deployments, host) =
2207+
crate::status::get_status_require_booted(sysroot)?;
2208+
2209+
let stateroots = list_stateroots(sysroot)?;
2210+
dbg!(&stateroots);
2211+
let target_stateroot = if let Some(s) = opts.stateroot {
2212+
s
2213+
} else {
2214+
let now = chrono::Utc::now();
2215+
let r = allocate_new_stateroot(&sysroot, &stateroots, now)?;
2216+
r.name
2217+
};
2218+
2219+
let booted_stateroot = booted_deployment.osname();
2220+
assert!(booted_stateroot.as_str() != target_stateroot);
2221+
let (fetched, spec) = if let Some(target) = opts.target_opts.imageref()? {
2222+
let mut new_spec = host.spec;
2223+
new_spec.image = Some(target.into());
2224+
let fetched = crate::deploy::pull(
2225+
repo,
2226+
&new_spec.image.as_ref().unwrap(),
2227+
None,
2228+
opts.quiet,
2229+
prog.clone(),
2230+
)
2231+
.await?;
2232+
(fetched, new_spec)
2233+
} else {
2234+
let imgstate = host
2235+
.status
2236+
.booted
2237+
.map(|b| b.query_image(repo))
2238+
.transpose()?
2239+
.flatten()
2240+
.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
2241+
(Box::new((*imgstate).into()), host.spec)
2242+
};
2243+
let spec = crate::deploy::RequiredHostSpec::from_spec(&spec)?;
2244+
2245+
// Compute the kernel arguments to inherit. By default, that's only those involved
2246+
// in the root filesystem.
2247+
let root_kargs = if opts.no_root_kargs {
2248+
Vec::new()
2249+
} else {
2250+
let bootcfg = booted_deployment
2251+
.bootconfig()
2252+
.ok_or_else(|| anyhow!("Missing bootcfg for booted deployment"))?;
2253+
if let Some(options) = bootcfg.get("options") {
2254+
let options = options.split_ascii_whitespace().collect::<Vec<_>>();
2255+
crate::kernel::root_args_from_cmdline(&options)
2256+
.into_iter()
2257+
.map(ToOwned::to_owned)
2258+
.collect::<Vec<_>>()
2259+
} else {
2260+
Vec::new()
2261+
}
2262+
};
2263+
2264+
let kargs = crate::kargs::get_kargs_in_root(rootfs, std::env::consts::ARCH)?
2265+
.into_iter()
2266+
.chain(root_kargs.into_iter())
2267+
.chain(opts.karg.unwrap_or_default())
2268+
.collect::<Vec<_>>();
2269+
2270+
let from = MergeState::Reset {
2271+
stateroot: target_stateroot,
2272+
kargs,
2273+
};
2274+
crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone()).await?;
2275+
2276+
sysroot.update_mtime()?;
2277+
2278+
if opts.apply {
2279+
crate::reboot::reboot()?;
2280+
}
2281+
Ok(())
2282+
}
2283+
21312284
/// Implementation of `bootc install finalize`.
21322285
pub(crate) async fn install_finalize(target: &Utf8Path) -> Result<()> {
21332286
// 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)