Skip to content

Commit 145cfdf

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 21d5883 commit 145cfdf

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
@@ -29,7 +29,7 @@ use ostree_ext::sysroot::SysrootLock;
2929
use schemars::schema_for;
3030
use serde::{Deserialize, Serialize};
3131

32-
use crate::deploy::RequiredHostSpec;
32+
use crate::deploy::{MergeState, RequiredHostSpec};
3333
use crate::lints;
3434
use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
3535
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 {
@@ -997,8 +1003,9 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
9971003
} else if booted_unchanged {
9981004
println!("No update available.")
9991005
} else {
1000-
let osname = booted_deployment.osname();
1001-
crate::deploy::stage(sysroot, &osname, &fetched, &spec, prog.clone()).await?;
1006+
let stateroot = booted_deployment.osname();
1007+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
1008+
crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone()).await?;
10021009
changed = true;
10031010
if let Some(prev) = booted_image.as_ref() {
10041011
if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
@@ -1110,7 +1117,8 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
11101117
}
11111118

11121119
let stateroot = booted_deployment.osname();
1113-
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
1120+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
1121+
crate::deploy::stage(sysroot, from, &fetched, &new_spec, prog.clone()).await?;
11141122

11151123
sysroot.update_mtime()?;
11161124

@@ -1193,7 +1201,8 @@ async fn edit(opts: EditOpts) -> Result<()> {
11931201
// TODO gc old layers here
11941202

11951203
let stateroot = booted_deployment.osname();
1196-
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
1204+
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
1205+
crate::deploy::stage(sysroot, from, &fetched, &new_spec, prog.clone()).await?;
11971206

11981207
sysroot.update_mtime()?;
11991208

@@ -1379,6 +1388,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
13791388
InstallOpts::ToExistingRoot(opts) => {
13801389
crate::install::install_to_existing_root(opts).await
13811390
}
1391+
InstallOpts::Reset(opts) => crate::install::install_reset(opts).await,
13821392
InstallOpts::PrintConfiguration => crate::install::print_configuration(),
13831393
InstallOpts::EnsureCompletion {} => {
13841394
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;
@@ -55,7 +55,10 @@ use serde::{Deserialize, Serialize};
5555
use self::baseline::InstallBlockDeviceOpts;
5656
use crate::boundimage::{BoundImage, ResolvedBoundImage};
5757
use crate::containerenv::ContainerExecutionInfo;
58-
use crate::deploy::{prepare_for_pull, pull_from_prepared, PreparedImportMeta, PreparedPullResult};
58+
use crate::deploy::{
59+
prepare_for_pull, pull_from_prepared, MergeState, PreparedImportMeta, PreparedPullResult,
60+
};
61+
use crate::kernel_cmdline::Cmdline;
5962
use crate::lsm;
6063
use crate::progress_jsonl::ProgressWriter;
6164
use crate::spec::ImageReference;
@@ -352,6 +355,50 @@ pub(crate) struct InstallToExistingRootOpts {
352355
pub(crate) root_path: Utf8PathBuf,
353356
}
354357

358+
#[derive(Debug, clap::Parser, PartialEq, Eq)]
359+
pub(crate) struct InstallResetOpts {
360+
/// Acknowledge that this command is experimental.
361+
#[clap(long)]
362+
pub(crate) experimental: bool,
363+
364+
#[clap(flatten)]
365+
pub(crate) source_opts: InstallSourceOpts,
366+
367+
#[clap(flatten)]
368+
pub(crate) target_opts: InstallTargetOpts,
369+
370+
/// Name of the target stateroot. If not provided, one will be automatically
371+
/// generated of the form s<year>-<serial> where <serial> starts at zero and
372+
/// increments automatically.
373+
#[clap(long)]
374+
pub(crate) stateroot: Option<String>,
375+
376+
/// Don't display progress
377+
#[clap(long)]
378+
pub(crate) quiet: bool,
379+
380+
#[clap(flatten)]
381+
pub(crate) progress: crate::cli::ProgressOptions,
382+
383+
/// Restart or reboot into the new target image.
384+
///
385+
/// Currently, this option always reboots. In the future this command
386+
/// will detect the case where no kernel changes are queued, and perform
387+
/// a userspace-only restart.
388+
#[clap(long)]
389+
pub(crate) apply: bool,
390+
391+
/// Skip inheriting any automatically discovered root file system kernel arguments.
392+
#[clap(long)]
393+
no_root_kargs: bool,
394+
395+
/// Add a kernel argument. This option can be provided multiple times.
396+
///
397+
/// Example: --karg=nosmt --karg=console=ttyS0,114800n8
398+
#[clap(long)]
399+
karg: Option<Vec<String>>,
400+
}
401+
355402
/// Global state captured from the container.
356403
#[derive(Debug, Clone)]
357404
pub(crate) struct SourceInfo {
@@ -385,6 +432,24 @@ pub(crate) struct State {
385432
pub(crate) tempdir: TempDir,
386433
}
387434

435+
impl InstallTargetOpts {
436+
pub(crate) fn imageref(&self) -> Result<Option<ostree_container::OstreeImageReference>> {
437+
let Some(target_imgname) = self.target_imgref.as_deref() else {
438+
return Ok(None);
439+
};
440+
let target_transport =
441+
ostree_container::Transport::try_from(self.target_transport.as_str())?;
442+
let target_imgref = ostree_container::OstreeImageReference {
443+
sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
444+
imgref: ostree_container::ImageReference {
445+
transport: target_transport,
446+
name: target_imgname.to_string(),
447+
},
448+
};
449+
Ok(Some(target_imgref))
450+
}
451+
}
452+
388453
impl State {
389454
#[context("Loading SELinux policy")]
390455
pub(crate) fn load_policy(&self) -> Result<Option<ostree::SePolicy>> {
@@ -2019,6 +2084,94 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
20192084
install_to_filesystem(opts, true, cleanup).await
20202085
}
20212086

2087+
pub(crate) async fn install_reset(opts: InstallResetOpts) -> Result<()> {
2088+
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2089+
if !opts.experimental {
2090+
anyhow::bail!("This command requires --experimental");
2091+
}
2092+
2093+
let prog: ProgressWriter = opts.progress.try_into()?;
2094+
2095+
let sysroot = &crate::cli::get_storage().await?;
2096+
let repo = &sysroot.repo();
2097+
let (booted_deployment, _deployments, host) =
2098+
crate::status::get_status_require_booted(sysroot)?;
2099+
2100+
let stateroots = list_stateroots(sysroot)?;
2101+
dbg!(&stateroots);
2102+
let target_stateroot = if let Some(s) = opts.stateroot {
2103+
s
2104+
} else {
2105+
let now = chrono::Utc::now();
2106+
let r = allocate_new_stateroot(&sysroot, &stateroots, now)?;
2107+
r.name
2108+
};
2109+
2110+
let booted_stateroot = booted_deployment.osname();
2111+
assert!(booted_stateroot.as_str() != target_stateroot);
2112+
let (fetched, spec) = if let Some(target) = opts.target_opts.imageref()? {
2113+
let mut new_spec = host.spec;
2114+
new_spec.image = Some(target.into());
2115+
let fetched = crate::deploy::pull(
2116+
repo,
2117+
&new_spec.image.as_ref().unwrap(),
2118+
None,
2119+
opts.quiet,
2120+
prog.clone(),
2121+
)
2122+
.await?;
2123+
(fetched, new_spec)
2124+
} else {
2125+
let imgstate = host
2126+
.status
2127+
.booted
2128+
.map(|b| b.query_image(repo))
2129+
.transpose()?
2130+
.flatten()
2131+
.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
2132+
(Box::new((*imgstate).into()), host.spec)
2133+
};
2134+
let spec = crate::deploy::RequiredHostSpec::from_spec(&spec)?;
2135+
2136+
// Compute the kernel arguments to inherit. By default, that's only those involved
2137+
// in the root filesystem.
2138+
let root_kargs = if opts.no_root_kargs {
2139+
Vec::new()
2140+
} else {
2141+
let bootcfg = booted_deployment
2142+
.bootconfig()
2143+
.ok_or_else(|| anyhow!("Missing bootcfg for booted deployment"))?;
2144+
if let Some(options) = bootcfg.get("options") {
2145+
let options = options.split_ascii_whitespace().collect::<Vec<_>>();
2146+
crate::kernel::root_args_from_cmdline(&options)
2147+
.into_iter()
2148+
.map(ToOwned::to_owned)
2149+
.collect::<Vec<_>>()
2150+
} else {
2151+
Vec::new()
2152+
}
2153+
};
2154+
2155+
let kargs = crate::kargs::get_kargs_in_root(rootfs, std::env::consts::ARCH)?
2156+
.into_iter()
2157+
.chain(root_kargs.into_iter())
2158+
.chain(opts.karg.unwrap_or_default())
2159+
.collect::<Vec<_>>();
2160+
2161+
let from = MergeState::Reset {
2162+
stateroot: target_stateroot,
2163+
kargs,
2164+
};
2165+
crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone()).await?;
2166+
2167+
sysroot.update_mtime()?;
2168+
2169+
if opts.apply {
2170+
crate::reboot::reboot()?;
2171+
}
2172+
Ok(())
2173+
}
2174+
20222175
/// Implementation of `bootc install finalize`.
20232176
pub(crate) async fn install_finalize(target: &Utf8Path) -> Result<()> {
20242177
// 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)