Skip to content

Commit b8d843d

Browse files
committed
cli: support the stateroot option also on switch
# Background <hash> added the `stateroot` option to the `install` subcommand # Issue The `stateroot` option is not available on the `switch` subcommand # Solution Add the `stateroot` option to the `switch` subcommand # Implementation * If the stateroot is different than the current, we should allow using the same image as the currently booted one * Stateroot has to be explicitly created (`init_osname` binding) if it doesn't exist. If it does, we still call `init_osname` and simply ignore the error (TODO: only ignore non-already-exists errors) * Copy `/var` from the old stateroot to the new one. I'm doing `--reflink` but it's still very slow * Must use the old stateroot to find the `merge_deployment` because otherwise we boot without the required kargs (it manifested as a missing `root=UUID=...` which caused the dracut rootfs-generator to silently fail to create `sysroot.mount` and so `ostree-prepare-root` failed due to empty `/sysroot`) Signed-off-by: Omer Tuchfeld <[email protected]>
1 parent 1fd6c69 commit b8d843d

File tree

2 files changed

+49
-10
lines changed

2 files changed

+49
-10
lines changed

lib/src/cli.rs

+46-7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use crate::deploy::RequiredHostSpec;
2525
use crate::lints;
2626
use crate::spec::Host;
2727
use crate::spec::ImageReference;
28+
use crate::task::Task;
2829
use crate::utils::sigpolicy_from_opts;
2930

3031
include!(concat!(env!("OUT_DIR"), "/version.rs"));
@@ -99,6 +100,11 @@ pub(crate) struct SwitchOpts {
99100

100101
/// Target image to use for the next boot.
101102
pub(crate) target: String,
103+
104+
/// Make the switch into a custom stateroot. If the stateroot doesn't exist, it will be created
105+
/// and `/var` of the current stateroot will be copied (copy-on-write) into it.
106+
#[clap(long)]
107+
pub(crate) stateroot: Option<String>,
102108
}
103109

104110
/// Options controlling rollback
@@ -628,7 +634,7 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
628634
println!("No update available.")
629635
} else {
630636
let osname = booted_deployment.osname();
631-
crate::deploy::stage(sysroot, &osname, &fetched, &spec).await?;
637+
crate::deploy::stage(sysroot, &osname, &osname, &fetched, &spec).await?;
632638
changed = true;
633639
if let Some(prev) = booted_image.as_ref() {
634640
if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
@@ -664,13 +670,13 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
664670
);
665671
let target = ostree_container::OstreeImageReference { sigverify, imgref };
666672
let target = ImageReference::from(target);
673+
let root = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
667674

668675
// If we're doing an in-place mutation, we shortcut most of the rest of the work here
669676
if opts.mutate_in_place {
670677
let deployid = {
671678
// Clone to pass into helper thread
672679
let target = target.clone();
673-
let root = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
674680
tokio::task::spawn_blocking(move || {
675681
crate::deploy::switch_origin_inplace(&root, &target)
676682
})
@@ -687,18 +693,52 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
687693
let (booted_deployment, _deployments, host) =
688694
crate::status::get_status_require_booted(sysroot)?;
689695

696+
let (old_stateroot, stateroot) = {
697+
let booted_osname = booted_deployment.osname();
698+
let stateroot = opts
699+
.stateroot
700+
.as_deref()
701+
.unwrap_or_else(|| booted_osname.as_str());
702+
703+
(booted_osname.to_owned(), stateroot.to_owned())
704+
};
705+
690706
let new_spec = {
691707
let mut new_spec = host.spec.clone();
692708
new_spec.image = Some(target.clone());
693709
new_spec
694710
};
695711

696-
if new_spec == host.spec {
697-
println!("Image specification is unchanged.");
712+
if new_spec == host.spec && old_stateroot == stateroot {
713+
// TODO: Should we really be confusing users with terms like "stateroot"?
714+
println!(
715+
"The currently running deployment in stateroot {stateroot} is already using this image"
716+
);
698717
return Ok(());
699718
}
700719
let new_spec = RequiredHostSpec::from_spec(&new_spec)?;
701720

721+
if old_stateroot != stateroot {
722+
let init_result = sysroot.init_osname(&stateroot, cancellable);
723+
match init_result {
724+
Ok(_) => {
725+
Task::new("Copying /var to new stateroot", "cp")
726+
.args([
727+
"--recursive",
728+
"--reflink=auto",
729+
"--archive",
730+
format!("/sysroot/ostree/deploy/{old_stateroot}/var").as_str(),
731+
format!("/sysroot/ostree/deploy/{stateroot}/").as_str(),
732+
])
733+
.run()?;
734+
}
735+
Err(err) => {
736+
// TODO: Only ignore non already-exists errors
737+
println!("Ignoring error creating new stateroot: {err}");
738+
}
739+
}
740+
}
741+
702742
let fetched = crate::deploy::pull(repo, &target, None, opts.quiet).await?;
703743

704744
if !opts.retain {
@@ -712,8 +752,7 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
712752
}
713753
}
714754

715-
let stateroot = booted_deployment.osname();
716-
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec).await?;
755+
crate::deploy::stage(sysroot, &old_stateroot, &stateroot, &fetched, &new_spec).await?;
717756

718757
if opts.apply {
719758
crate::reboot::reboot()?;
@@ -766,7 +805,7 @@ async fn edit(opts: EditOpts) -> Result<()> {
766805
// TODO gc old layers here
767806

768807
let stateroot = booted_deployment.osname();
769-
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec).await?;
808+
crate::deploy::stage(sysroot, &stateroot, &stateroot, &fetched, &new_spec).await?;
770809

771810
Ok(())
772811
}

lib/src/deploy.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,6 @@ async fn deploy(
373373
image: &ImageState,
374374
origin: &glib::KeyFile,
375375
) -> Result<Deployment> {
376-
let stateroot = Some(stateroot);
377376
let mut opts = ostree::SysrootDeployTreeOpts::default();
378377
// Compute the kernel argument overrides. In practice today this API is always expecting
379378
// a merge deployment. The kargs code also always looks at the booted root (which
@@ -396,7 +395,7 @@ async fn deploy(
396395
let cancellable = gio::Cancellable::NONE;
397396
return sysroot
398397
.stage_tree_with_options(
399-
stateroot,
398+
Some(stateroot),
400399
image.ostree_commit.as_str(),
401400
Some(origin),
402401
merge_deployment,
@@ -422,11 +421,12 @@ fn origin_from_imageref(imgref: &ImageReference) -> Result<glib::KeyFile> {
422421
#[context("Staging")]
423422
pub(crate) async fn stage(
424423
sysroot: &Storage,
424+
merge_stateroot: &str,
425425
stateroot: &str,
426426
image: &ImageState,
427427
spec: &RequiredHostSpec<'_>,
428428
) -> Result<()> {
429-
let merge_deployment = sysroot.merge_deployment(Some(stateroot));
429+
let merge_deployment = sysroot.merge_deployment(Some(merge_stateroot));
430430
let origin = origin_from_imageref(spec.image)?;
431431
let deployment = crate::deploy::deploy(
432432
sysroot,

0 commit comments

Comments
 (0)