Skip to content

Commit d6a843b

Browse files
committed
cli: add support for soft-reboots
This commit adds --queue-soft-reboot to the cli which uses the ostree api's to setup soft-reboots during switch, update and rollback operations. Co-authored-by: Colin Walters <[email protected]> Signed-off-by: Joseph Marrero Corchado <[email protected]> Signed-off-by: Colin Walters <[email protected]>
1 parent aaa8f85 commit d6a843b

File tree

8 files changed

+240
-6
lines changed

8 files changed

+240
-6
lines changed

crates/lib/src/cli.rs

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ pub(crate) struct UpgradeOpts {
8080
#[clap(long, conflicts_with = "check")]
8181
pub(crate) apply: bool,
8282

83+
/// Queue a soft reboot after staging the deployment.
84+
///
85+
/// This will prepare the system for a soft reboot instead of a full reboot,
86+
/// which allows userspace to restart without rebooting the kernel.
87+
#[clap(long, conflicts_with = "check")]
88+
pub(crate) queue_soft_reboot: bool,
89+
8390
#[clap(flatten)]
8491
pub(crate) progress: ProgressOptions,
8592
}
@@ -99,6 +106,13 @@ pub(crate) struct SwitchOpts {
99106
#[clap(long)]
100107
pub(crate) apply: bool,
101108

109+
/// Queue a soft reboot after staging the deployment.
110+
///
111+
/// This will prepare the system for a soft reboot instead of a full reboot,
112+
/// which allows userspace to restart without rebooting the kernel.
113+
#[clap(long)]
114+
pub(crate) queue_soft_reboot: bool,
115+
102116
/// The transport; e.g. oci, oci-archive, containers-storage. Defaults to `registry`.
103117
#[clap(long, default_value = "registry")]
104118
pub(crate) transport: String,
@@ -142,6 +156,13 @@ pub(crate) struct RollbackOpts {
142156
/// a userspace-only restart.
143157
#[clap(long)]
144158
pub(crate) apply: bool,
159+
160+
/// Queue a soft reboot after performing the rollback.
161+
///
162+
/// This will prepare the system for a soft reboot instead of a full reboot,
163+
/// which allows userspace to restart without rebooting the kernel.
164+
#[clap(long)]
165+
pub(crate) queue_soft_reboot: bool,
145166
}
146167

147168
/// Perform an edit operation
@@ -562,7 +583,7 @@ pub(crate) enum Opt {
562583
Note on Rollbacks and the `/etc` Directory:
563584
564585
When you perform a rollback (e.g., with `bootc rollback`), any
565-
changes made to files in the `/etc` directory wont carry over
586+
changes made to files in the `/etc` directory won't carry over
566587
to the rolled-back deployment. The `/etc` files will revert
567588
to their state from that previous deployment instead.
568589
@@ -741,6 +762,40 @@ pub(crate) fn require_root(is_container: bool) -> Result<()> {
741762
Ok(())
742763
}
743764

765+
/// Check if a deployment has soft reboot capability
766+
fn has_soft_reboot_capability(deployment: Option<&crate::spec::BootEntry>) -> bool {
767+
deployment.map(|d| d.soft_reboot_capable).unwrap_or(false)
768+
}
769+
770+
/// Prepare a soft reboot for the given deployment
771+
#[context("Preparing soft reboot")]
772+
fn prepare_soft_reboot(
773+
sysroot: &crate::store::Storage,
774+
deployment: &ostree::Deployment,
775+
) -> Result<()> {
776+
let cancellable = ostree::gio::Cancellable::NONE;
777+
sysroot
778+
.sysroot
779+
.deployment_set_soft_reboot(deployment, false, cancellable)
780+
.context("Failed to prepare soft-reboot")?;
781+
Ok(())
782+
}
783+
784+
/// Perform a soft reboot for a staged deployment
785+
#[context("Soft reboot staged deployment")]
786+
fn soft_reboot_staged(sysroot: &crate::store::Storage) -> Result<()> {
787+
println!("Staged deployment is soft-reboot capable, preparing for soft-reboot...");
788+
789+
let deployments_list = sysroot.deployments();
790+
let staged_deployment = deployments_list
791+
.iter()
792+
.find(|d| d.is_staged())
793+
.ok_or_else(|| anyhow::anyhow!("Failed to find staged deployment"))?;
794+
795+
prepare_soft_reboot(sysroot, staged_deployment)?;
796+
Ok(())
797+
}
798+
744799
/// A few process changes that need to be made for writing.
745800
/// IMPORTANT: This may end up re-executing the current process,
746801
/// so anything that happens before this should be idempotent.
@@ -859,7 +914,11 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
859914
.unwrap_or_default();
860915
if staged_unchanged {
861916
println!("Staged update present, not changed.");
862-
917+
if opts.queue_soft_reboot {
918+
if has_soft_reboot_capability(host.status.staged.as_ref()) {
919+
soft_reboot_staged(sysroot)?;
920+
}
921+
}
863922
if opts.apply {
864923
crate::reboot::reboot()?;
865924
}
@@ -881,6 +940,15 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
881940
if changed {
882941
sysroot.update_mtime()?;
883942

943+
if opts.queue_soft_reboot {
944+
// At this point we have new staged deployment and the host definition has changed.
945+
// We need the updated host status before we check if we can prepare the soft-reboot.
946+
let updated_host = crate::status::get_status(sysroot, Some(&booted_deployment))?.1;
947+
if has_soft_reboot_capability(updated_host.status.staged.as_ref()) {
948+
soft_reboot_staged(sysroot)?;
949+
}
950+
}
951+
884952
if opts.apply {
885953
crate::reboot::reboot()?;
886954
}
@@ -956,6 +1024,15 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
9561024

9571025
sysroot.update_mtime()?;
9581026

1027+
if opts.queue_soft_reboot {
1028+
// At this point we have staged the deployment and the host definition has changed.
1029+
// We need the updated host status before we check if we can prepare the soft-reboot.
1030+
let updated_host = crate::status::get_status(sysroot, Some(&booted_deployment))?.1;
1031+
if has_soft_reboot_capability(updated_host.status.staged.as_ref()) {
1032+
soft_reboot_staged(sysroot)?;
1033+
}
1034+
}
1035+
9591036
if opts.apply {
9601037
crate::reboot::reboot()?;
9611038
}
@@ -969,6 +1046,22 @@ async fn rollback(opts: RollbackOpts) -> Result<()> {
9691046
let sysroot = &get_storage().await?;
9701047
crate::deploy::rollback(sysroot).await?;
9711048

1049+
if opts.queue_soft_reboot {
1050+
// Get status of rollback deployment to check soft-reboot capability
1051+
let host = crate::status::get_status_require_booted(sysroot)?.2;
1052+
1053+
if has_soft_reboot_capability(host.status.rollback.as_ref()) {
1054+
println!("Rollback deployment is soft-reboot capable, preparing for soft-reboot...");
1055+
1056+
let deployments_list = sysroot.deployments();
1057+
let target_deployment = deployments_list
1058+
.first()
1059+
.ok_or_else(|| anyhow::anyhow!("No rollback deployment found!"))?;
1060+
1061+
prepare_soft_reboot(sysroot, target_deployment)?;
1062+
}
1063+
}
1064+
9721065
if opts.apply {
9731066
crate::reboot::reboot()?;
9741067
}

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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ impl From<ImageReference> for OstreeImageReference {
8686
}
8787
}
8888

89+
/// Check if a deployment has soft reboot capability
90+
fn has_soft_reboot_capability(sysroot: &Storage, deployment: &ostree::Deployment) -> bool {
91+
ostree_ext::systemd_has_soft_reboot() && sysroot.deployment_can_soft_reboot(deployment)
92+
}
93+
8994
/// Parse an ostree origin file (a keyfile) and extract the targeted
9095
/// container image reference.
9196
fn get_image_origin(origin: &glib::KeyFile) -> Result<Option<OstreeImageReference>> {
@@ -144,10 +149,13 @@ fn boot_entry_from_deployment(
144149
(None, CachedImageStatus::default(), false)
145150
};
146151

152+
let soft_reboot_capable = has_soft_reboot_capability(sysroot, deployment);
153+
147154
let r = BootEntry {
148155
image,
149156
cached_update,
150157
incompatible,
158+
soft_reboot_capable,
151159
store,
152160
pinned: deployment.is_pinned(),
153161
ostree: Some(crate::spec::BootEntryOstree {
@@ -381,6 +389,27 @@ fn render_verbose_ostree_info(
381389
Ok(())
382390
}
383391

392+
/// Helper function to render if soft-reboot capable
393+
fn write_soft_reboot(
394+
mut out: impl Write,
395+
entry: &crate::spec::BootEntry,
396+
prefix_len: usize,
397+
) -> Result<()> {
398+
// Show soft-reboot capability
399+
write_row_name(&mut out, "Soft-reboot", prefix_len)?;
400+
writeln!(
401+
out,
402+
"{}",
403+
if entry.soft_reboot_capable {
404+
"yes"
405+
} else {
406+
"no"
407+
}
408+
)?;
409+
410+
Ok(())
411+
}
412+
384413
/// Write the data for a container image based status.
385414
fn human_render_slot(
386415
mut out: impl Write,
@@ -463,6 +492,9 @@ fn human_render_slot(
463492
}
464493
}
465494
}
495+
496+
// Show soft-reboot capability
497+
write_soft_reboot(&mut out, entry, prefix_len)?;
466498
}
467499

468500
tracing::debug!("pinned={}", entry.pinned);
@@ -500,6 +532,9 @@ fn human_render_slot_ostree(
500532
if let Some(ostree) = &entry.ostree {
501533
render_verbose_ostree_info(&mut out, ostree, slot, prefix_len)?;
502534
}
535+
536+
// Show soft-reboot capability
537+
write_soft_reboot(&mut out, entry, prefix_len)?;
503538
}
504539

505540
tracing::debug!("pinned={}", entry.pinned);
@@ -721,5 +756,6 @@ mod tests {
721756
assert!(w.contains("Deploy serial:"));
722757
assert!(w.contains("Staged:"));
723758
assert!(w.contains("Commit:"));
759+
assert!(w.contains("Soft-reboot:"));
724760
}
725761
}

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
@@ -62,3 +62,11 @@ execute:
6262
test:
6363
- /tmt/tests/bootc-install-provision
6464
- /tmt/tests/test-24-local-upgrade-reboot
65+
66+
/test-25-soft-reboot:
67+
summary: Soft reboot support
68+
discover:
69+
how: fmf
70+
test:
71+
- /tmt/tests/bootc-install-provision
72+
- /tmt/tests/test-25-soft-reboot

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

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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+
15+
# Run on the first boot
16+
def initial_build [] {
17+
tap begin "local image push + pull + upgrade"
18+
19+
let td = mktemp -d
20+
cd $td
21+
22+
bootc image copy-to-storage
23+
24+
# A simple derived container that adds a file, but also injects some kargs
25+
"FROM localhost/bootc
26+
RUN echo test content > /usr/share/testfile-for-soft-reboot.txt
27+
" | save Dockerfile
28+
# Build it
29+
podman build -t localhost/bootc-derived .
30+
31+
assert (not ("/run/nextroot" | path exists))
32+
33+
bootc switch --queue-soft-reboot --transport containers-storage localhost/bootc-derived
34+
let st = bootc status --json | from json
35+
assert $st.status.staged.softRebootCapable
36+
37+
assert ("/run/nextroot" | path exists)
38+
39+
#Let's reset the soft-reboot as we still can't correctly soft-reboot with tmt
40+
ostree admin prepare-soft-reboot --reset
41+
# https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test
42+
tmt-reboot
43+
}
44+
45+
# The second boot; verify we're in the derived image
46+
def second_boot [] {
47+
assert ("/usr/share/testfile-for-soft-reboot.txt" | path exists)
48+
#tmt-reboot seems not to be using systemd soft-reboot
49+
# and tmt-reboot -c "systemctl soft-reboot" is not connecting back
50+
# let's comment this check.
51+
#assert equal (systemctl show -P SoftRebootsCount) "1"
52+
53+
# A new derived with new kargs which should stop the soft reboot.
54+
"FROM localhost/bootc
55+
RUN echo test content > /usr/share/testfile-for-soft-reboot.txt
56+
RUN echo 'kargs = ["foo1=bar2"]' | tee /usr/lib/bootc/kargs.d/00-foo1bar2.toml > /dev/null
57+
" | save Dockerfile
58+
# Build it
59+
podman build -t localhost/bootc-derived .
60+
61+
bootc update --queue-soft-reboot
62+
let st = bootc status --json | from json
63+
# Should not be soft-reboot capable because of kargs diff
64+
assert (not $st.status.staged.softRebootCapable)
65+
66+
# And reboot into it
67+
tmt-reboot
68+
}
69+
70+
# The third boot; verify we're in the derived image
71+
def third_boot [] {
72+
assert ("/usr/lib/bootc/kargs.d/00-foo1bar2.toml" | path exists)
73+
74+
assert equal (systemctl show -P SoftRebootsCount) "0"
75+
}
76+
77+
def main [] {
78+
# See https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test
79+
match $env.TMT_REBOOT_COUNT? {
80+
null | "0" => initial_build,
81+
"1" => second_boot,
82+
"2" => third_boot,
83+
$o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } },
84+
}
85+
}

tmt/tests/test-25-soft-reboot.fmf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
summary: Execute soft reboot test
2+
test: nu booted/test-soft-reboot.nu
3+
duration: 30m

0 commit comments

Comments
 (0)