Skip to content

Commit cf12d52

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 c3e9d72 commit cf12d52

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
@@ -38,7 +38,7 @@ use crate::bootc_composefs::{
3838
switch::switch_composefs,
3939
update::upgrade_composefs,
4040
};
41-
use crate::deploy::RequiredHostSpec;
41+
use crate::deploy::{MergeState, RequiredHostSpec, RequiredHostSpec};
4242
use crate::lints;
4343
use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
4444
use crate::spec::Host;
@@ -256,6 +256,12 @@ pub(crate) enum InstallOpts {
256256
/// will be wiped, but the content of the existing root will otherwise be retained, and will
257257
/// need to be cleaned up if desired when rebooted into the new root.
258258
ToExistingRoot(crate::install::InstallToExistingRootOpts),
259+
/// Nondestructively create a fresh installation state inside an existing bootc system.
260+
///
261+
/// This is a nondestructive variant of `install to-existing-root` that works only inside
262+
/// an existing bootc system.
263+
#[clap(hide = true)]
264+
Reset(crate::install::InstallResetOpts),
259265
/// Execute this as the penultimate step of an installation using `install to-filesystem`.
260266
///
261267
Finalize {
@@ -950,8 +956,9 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
950956
} else if booted_unchanged {
951957
println!("No update available.")
952958
} else {
953-
let osname = booted_deployment.osname();
954-
crate::deploy::stage(sysroot, &osname, &fetched, &spec, prog.clone()).await?;
959+
let stateroot = booted_deployment.osname();
960+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
961+
crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone()).await?;
955962
changed = true;
956963
if let Some(prev) = booted_image.as_ref() {
957964
if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
@@ -1070,7 +1077,8 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
10701077
}
10711078

10721079
let stateroot = booted_deployment.osname();
1073-
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
1080+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
1081+
crate::deploy::stage(sysroot, from, &fetched, &new_spec, prog.clone()).await?;
10741082

10751083
sysroot.update_mtime()?;
10761084

@@ -1149,7 +1157,8 @@ async fn edit(opts: EditOpts) -> Result<()> {
11491157
// TODO gc old layers here
11501158

11511159
let stateroot = booted_deployment.osname();
1152-
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
1160+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
1161+
crate::deploy::stage(sysroot, from, &fetched, &new_spec, prog.clone()).await?;
11531162

11541163
sysroot.update_mtime()?;
11551164

@@ -1380,6 +1389,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
13801389
InstallOpts::ToExistingRoot(opts) => {
13811390
crate::install::install_to_existing_root(opts).await
13821391
}
1392+
InstallOpts::Reset(opts) => crate::install::install_reset(opts).await,
13831393
InstallOpts::PrintConfiguration => crate::install::print_configuration(),
13841394
InstallOpts::EnsureCompletion {} => {
13851395
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};
@@ -405,6 +408,50 @@ pub(crate) struct InstallToExistingRootOpts {
405408
pub(crate) root_path: Utf8PathBuf,
406409
}
407410

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

493+
impl InstallTargetOpts {
494+
pub(crate) fn imageref(&self) -> Result<Option<ostree_container::OstreeImageReference>> {
495+
let Some(target_imgname) = self.target_imgref.as_deref() else {
496+
return Ok(None);
497+
};
498+
let target_transport =
499+
ostree_container::Transport::try_from(self.target_transport.as_str())?;
500+
let target_imgref = ostree_container::OstreeImageReference {
501+
sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
502+
imgref: ostree_container::ImageReference {
503+
transport: target_transport,
504+
name: target_imgname.to_string(),
505+
},
506+
};
507+
Ok(Some(target_imgref))
508+
}
509+
}
510+
446511
impl State {
447512
#[context("Loading SELinux policy")]
448513
pub(crate) fn load_policy(&self) -> Result<Option<ostree::SePolicy>> {
@@ -2134,6 +2199,94 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
21342199
install_to_filesystem(opts, true, cleanup).await
21352200
}
21362201

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