Skip to content

Commit 75b8806

Browse files
jmarrerocgwalters
authored andcommitted
cli: add support for soft-reboots
Co-authored-by: Colin Walters <[email protected]> Signed-off-by: Joseph Marrero Corchado <[email protected]> Signed-off-by: Colin Walters <[email protected]>
1 parent 44481fd commit 75b8806

File tree

7 files changed

+143
-10
lines changed

7 files changed

+143
-10
lines changed

crates/lib/src/cli.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ pub(crate) enum Opt {
554554
Note on Rollbacks and the `/etc` Directory:
555555
556556
When you perform a rollback (e.g., with `bootc rollback`), any
557-
changes made to files in the `/etc` directory wont carry over
557+
changes made to files in the `/etc` directory won't carry over
558558
to the rolled-back deployment. The `/etc` files will revert
559559
to their state from that previous deployment instead.
560560
@@ -733,6 +733,40 @@ pub(crate) fn require_root(is_container: bool) -> Result<()> {
733733
Ok(())
734734
}
735735

736+
/// Check if a deployment can perform a soft reboot
737+
fn can_perform_soft_reboot(deployment: Option<&crate::spec::BootEntry>) -> bool {
738+
deployment.map(|d| d.soft_reboot_capable).unwrap_or(false)
739+
}
740+
741+
/// Prepare and execute a soft reboot for the given deployment
742+
#[context("Preparing soft reboot")]
743+
fn prepare_soft_reboot(
744+
sysroot: &crate::store::Storage,
745+
deployment: &ostree::Deployment,
746+
) -> Result<()> {
747+
let cancellable = ostree::gio::Cancellable::NONE;
748+
sysroot
749+
.sysroot
750+
.deployment_set_soft_reboot(deployment, false, cancellable)
751+
.context("Failed to prepare soft-reboot")?;
752+
Ok(())
753+
}
754+
755+
/// Perform a soft reboot for a staged deployment
756+
#[context("Soft reboot staged deployment")]
757+
fn soft_reboot_staged(sysroot: &crate::store::Storage) -> Result<()> {
758+
println!("Staged deployment is soft-reboot capable, performing soft-reboot...");
759+
760+
let deployments_list = sysroot.deployments();
761+
let staged_deployment = deployments_list
762+
.iter()
763+
.find(|d| d.is_staged())
764+
.ok_or_else(|| anyhow::anyhow!("Failed to find staged deployment"))?;
765+
766+
prepare_soft_reboot(sysroot, staged_deployment)?;
767+
Ok(())
768+
}
769+
736770
/// A few process changes that need to be made for writing.
737771
/// IMPORTANT: This may end up re-executing the current process,
738772
/// so anything that happens before this should be idempotent.
@@ -853,6 +887,9 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
853887
println!("Staged update present, not changed.");
854888

855889
if opts.apply {
890+
if can_perform_soft_reboot(host.status.staged.as_ref()) {
891+
soft_reboot_staged(sysroot)?;
892+
}
856893
crate::reboot::reboot()?;
857894
}
858895
} else if booted_unchanged {
@@ -949,6 +986,12 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
949986
sysroot.update_mtime()?;
950987

951988
if opts.apply {
989+
// Get updated status to check for soft-reboot capability
990+
let updated_host = crate::status::get_status(sysroot, Some(&booted_deployment))?.1;
991+
992+
if can_perform_soft_reboot(updated_host.status.staged.as_ref()) {
993+
soft_reboot_staged(sysroot)?;
994+
}
952995
crate::reboot::reboot()?;
953996
}
954997

@@ -962,6 +1005,20 @@ async fn rollback(opts: RollbackOpts) -> Result<()> {
9621005
crate::deploy::rollback(sysroot).await?;
9631006

9641007
if opts.apply {
1008+
// Get status before rollback to check soft-reboot capability
1009+
let host = crate::status::get_status_require_booted(sysroot)?.2;
1010+
1011+
if can_perform_soft_reboot(host.status.rollback.as_ref()) {
1012+
println!("Rollback deployment is soft-reboot capable, performing soft-reboot...");
1013+
1014+
let deployments_list = sysroot.deployments();
1015+
let target_deployment = deployments_list
1016+
.first()
1017+
.ok_or_else(|| anyhow::anyhow!("No deployments found after rollback"))?;
1018+
1019+
prepare_soft_reboot(sysroot, target_deployment)?;
1020+
}
1021+
9651022
crate::reboot::reboot()?;
9661023
}
9671024

crates/lib/src/deploy.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -577,10 +577,6 @@ async fn deploy(
577577
&opts,
578578
Some(cancellable),
579579
)?;
580-
tracing::debug!(
581-
"Soft reboot capable: {:?}",
582-
sysroot.deployment_can_soft_reboot(&d)
583-
);
584580
Ok(d.index())
585581
}),
586582
)

crates/lib/src/spec.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ pub struct BootEntry {
176176
pub incompatible: bool,
177177
/// Whether this entry will be subject to garbage collection
178178
pub pinned: bool,
179+
/// This is true if (relative to the booted system) this is a possible target for a soft reboot
180+
#[serde(default)]
181+
pub soft_reboot_capable: bool,
179182
/// The container storage backend
180183
#[serde(default)]
181184
pub store: Option<Store>,
@@ -517,6 +520,7 @@ mod tests {
517520
image: None,
518521
cached_update: None,
519522
incompatible: false,
523+
soft_reboot_capable: false,
520524
pinned: false,
521525
store: None,
522526
ostree: None,

crates/lib/src/status.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,14 @@ fn boot_entry_from_deployment(
144144
(None, CachedImageStatus::default(), false)
145145
};
146146

147+
let soft_reboot_capable =
148+
ostree_ext::systemd_has_soft_reboot() && sysroot.deployment_can_soft_reboot(deployment);
149+
147150
let r = BootEntry {
148151
image,
149152
cached_update,
150153
incompatible,
154+
soft_reboot_capable,
151155
store,
152156
pinned: deployment.is_pinned(),
153157
ostree: Some(crate::spec::BootEntryOstree {
@@ -228,17 +232,17 @@ pub(crate) fn get_status(
228232
other,
229233
};
230234

231-
let staged = deployments
232-
.staged
235+
let booted = booted_deployment
233236
.as_ref()
234237
.map(|d| boot_entry_from_deployment(sysroot, d))
235238
.transpose()
236-
.context("Staged deployment")?;
237-
let booted = booted_deployment
239+
.context("Booted deployment")?;
240+
let staged = deployments
241+
.staged
238242
.as_ref()
239243
.map(|d| boot_entry_from_deployment(sysroot, d))
240244
.transpose()
241-
.context("Booted deployment")?;
245+
.context("Staged deployment")?;
242246
let rollback = deployments
243247
.rollback
244248
.as_ref()

crates/ostree-ext/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,12 @@ pub mod prelude {
7878
pub mod fixture;
7979
#[cfg(feature = "internal-testing-api")]
8080
pub mod integrationtest;
81+
82+
/// Check if the system has the soft reboot target, which signals
83+
/// systemd support for soft reboots.
84+
pub fn systemd_has_soft_reboot() -> bool {
85+
const UNIT: &str = "/usr/lib/systemd/system/soft-reboot.target";
86+
use std::sync::OnceLock;
87+
static EXISTS: OnceLock<bool> = OnceLock::new();
88+
*EXISTS.get_or_init(|| std::path::Path::new(UNIT).exists())
89+
}

tmt/plans/integration.fmf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ execute:
3737
- /tmt/tests/bootc-install-provision
3838
- /tmt/tests/test-21-logically-bound-switch
3939

40+
/test-21-soft-reboot:
41+
summary: Soft reboot support
42+
discover:
43+
how: fmf
44+
test:
45+
- /tmt/tests/bootc-install-provision
46+
- /tmt/tests/test-21-soft-reboot
47+
4048
/test-22-logically-bound-install:
4149
summary: Execute logically bound images tests for switching images
4250
environment+:

tmt/tests/booted/test-soft-reboot.nu

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Verify that soft reboot works (on by default)
2+
use std assert
3+
use tap.nu
4+
5+
let soft_reboot_capable = "/usr/lib/systemd/system/soft-reboot.target" | path exists
6+
if not $soft_reboot_capable {
7+
echo "Skipping, system is not soft reboot capable"
8+
return
9+
}
10+
11+
# This code runs on *each* boot.
12+
# Here we just capture information.
13+
bootc status
14+
let st = bootc status --json | from json
15+
let booted = $st.status.booted.image
16+
17+
# Run on the first boot
18+
def initial_build [] {
19+
tap begin "local image push + pull + upgrade"
20+
21+
let td = mktemp -d
22+
cd $td
23+
24+
bootc image copy-to-storage
25+
26+
# A simple derived container that adds a file, but also injects some kargs
27+
"FROM localhost/bootc
28+
RUN echo test content > /usr/share/testfile-for-soft-reboot.txt
29+
" | save Dockerfile
30+
# Build it
31+
podman build -t localhost/bootc-derived .
32+
33+
bootc switch --transport containers-storage localhost/bootc-derived
34+
let st = bootc status --json | from json
35+
assert ($st.status.staged.soft_reboot_capable) == true
36+
37+
# And reboot into it
38+
tmt-reboot
39+
}
40+
41+
# The second boot; verify we're in the derived image
42+
def second_boot [] {
43+
assert ("/usr/share/testfile-for-soft-reboot.txt" | path exists)
44+
45+
assert equal (systemctl show -P SoftRebootsCount) "1"
46+
}
47+
48+
def main [] {
49+
# See https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test
50+
match $env.TMT_REBOOT_COUNT? {
51+
null | "0" => initial_build,
52+
"1" => second_boot,
53+
$o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } },
54+
}
55+
}

0 commit comments

Comments
 (0)