Skip to content

Commit 0a3fa5d

Browse files
committed
cli: add support for soft-reboots
This commit adds --soft-reboot=required|auto 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 7ae83d1 commit 0a3fa5d

File tree

8 files changed

+301
-6
lines changed

8 files changed

+301
-6
lines changed

crates/lib/src/cli.rs

Lines changed: 157 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+
/// Configure soft reboot behavior.
84+
///
85+
/// 'required' will fail if soft reboot is not available.
86+
/// 'auto' will use soft reboot if available, otherwise fall back to regular reboot.
87+
#[clap(long = "soft-reboot", conflicts_with = "check")]
88+
pub(crate) soft_reboot: Option<SoftRebootMode>,
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+
/// Configure soft reboot behavior.
110+
///
111+
/// 'required' will fail if soft reboot is not available.
112+
/// 'auto' will use soft reboot if available, otherwise fall back to regular reboot.
113+
#[clap(long = "soft-reboot")]
114+
pub(crate) soft_reboot: Option<SoftRebootMode>,
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+
/// Configure soft reboot behavior.
161+
///
162+
/// 'required' will fail if soft reboot is not available.
163+
/// 'auto' will use soft reboot if available, otherwise fall back to regular reboot.
164+
#[clap(long = "soft-reboot")]
165+
pub(crate) soft_reboot: Option<SoftRebootMode>,
145166
}
146167

147168
/// Perform an edit operation
@@ -167,6 +188,15 @@ pub(crate) enum OutputFormat {
167188
Json,
168189
}
169190

191+
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
192+
#[clap(rename_all = "lowercase")]
193+
pub(crate) enum SoftRebootMode {
194+
/// Require a soft reboot; fail if not possible
195+
Required,
196+
/// Automatically use soft reboot if possible, otherwise use regular reboot
197+
Auto,
198+
}
199+
170200
/// Perform an status operation
171201
#[derive(Debug, Parser, PartialEq, Eq)]
172202
pub(crate) struct StatusOpts {
@@ -562,7 +592,7 @@ pub(crate) enum Opt {
562592
Note on Rollbacks and the `/etc` Directory:
563593
564594
When you perform a rollback (e.g., with `bootc rollback`), any
565-
changes made to files in the `/etc` directory wont carry over
595+
changes made to files in the `/etc` directory won't carry over
566596
to the rolled-back deployment. The `/etc` files will revert
567597
to their state from that previous deployment instead.
568598
@@ -741,6 +771,104 @@ pub(crate) fn require_root(is_container: bool) -> Result<()> {
741771
Ok(())
742772
}
743773

774+
/// Check if a deployment has soft reboot capability
775+
fn has_soft_reboot_capability(deployment: Option<&crate::spec::BootEntry>) -> bool {
776+
deployment.map(|d| d.soft_reboot_capable).unwrap_or(false)
777+
}
778+
779+
/// Prepare a soft reboot for the given deployment
780+
#[context("Preparing soft reboot")]
781+
fn prepare_soft_reboot(
782+
sysroot: &crate::store::Storage,
783+
deployment: &ostree::Deployment,
784+
) -> Result<()> {
785+
let cancellable = ostree::gio::Cancellable::NONE;
786+
sysroot
787+
.sysroot
788+
.deployment_set_soft_reboot(deployment, false, cancellable)
789+
.context("Failed to prepare soft-reboot")?;
790+
Ok(())
791+
}
792+
793+
/// Handle soft reboot based on the configured mode
794+
#[context("Handling soft reboot")]
795+
fn handle_soft_reboot<F>(
796+
soft_reboot_mode: Option<SoftRebootMode>,
797+
entry: Option<&crate::spec::BootEntry>,
798+
deployment_type: &str,
799+
execute_soft_reboot: F,
800+
) -> Result<()>
801+
where
802+
F: FnOnce() -> Result<()>,
803+
{
804+
let Some(mode) = soft_reboot_mode else {
805+
return Ok(());
806+
};
807+
808+
let can_soft_reboot = has_soft_reboot_capability(entry);
809+
match mode {
810+
SoftRebootMode::Required => {
811+
if can_soft_reboot {
812+
execute_soft_reboot()?;
813+
} else {
814+
anyhow::bail!(
815+
"Soft reboot was required but {} deployment is not soft-reboot capable",
816+
deployment_type
817+
);
818+
}
819+
}
820+
SoftRebootMode::Auto => {
821+
if can_soft_reboot {
822+
execute_soft_reboot()?;
823+
}
824+
}
825+
}
826+
Ok(())
827+
}
828+
829+
/// Handle soft reboot for staged deployments (used by upgrade and switch)
830+
#[context("Handling staged soft reboot")]
831+
fn handle_staged_soft_reboot(
832+
sysroot: &crate::store::Storage,
833+
soft_reboot_mode: Option<SoftRebootMode>,
834+
host: &crate::spec::Host,
835+
) -> Result<()> {
836+
handle_soft_reboot(
837+
soft_reboot_mode,
838+
host.status.staged.as_ref(),
839+
"staged",
840+
|| soft_reboot_staged(sysroot),
841+
)
842+
}
843+
844+
/// Perform a soft reboot for a staged deployment
845+
#[context("Soft reboot staged deployment")]
846+
fn soft_reboot_staged(sysroot: &crate::store::Storage) -> Result<()> {
847+
println!("Staged deployment is soft-reboot capable, preparing for soft-reboot...");
848+
849+
let deployments_list = sysroot.deployments();
850+
let staged_deployment = deployments_list
851+
.iter()
852+
.find(|d| d.is_staged())
853+
.ok_or_else(|| anyhow::anyhow!("Failed to find staged deployment"))?;
854+
855+
prepare_soft_reboot(sysroot, staged_deployment)?;
856+
Ok(())
857+
}
858+
859+
/// Perform a soft reboot for a rollback deployment
860+
#[context("Soft reboot rollback deployment")]
861+
fn soft_reboot_rollback(sysroot: &crate::store::Storage) -> Result<()> {
862+
println!("Rollback deployment is soft-reboot capable, preparing for soft-reboot...");
863+
864+
let deployments_list = sysroot.deployments();
865+
let target_deployment = deployments_list
866+
.first()
867+
.ok_or_else(|| anyhow::anyhow!("No rollback deployment found!"))?;
868+
869+
prepare_soft_reboot(sysroot, target_deployment)
870+
}
871+
744872
/// A few process changes that need to be made for writing.
745873
/// IMPORTANT: This may end up re-executing the current process,
746874
/// so anything that happens before this should be idempotent.
@@ -813,6 +941,7 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
813941
let booted_image = host
814942
.status
815943
.booted
944+
.as_ref()
816945
.map(|b| b.query_image(repo))
817946
.transpose()?
818947
.flatten();
@@ -859,7 +988,7 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
859988
.unwrap_or_default();
860989
if staged_unchanged {
861990
println!("Staged update present, not changed.");
862-
991+
handle_staged_soft_reboot(sysroot, opts.soft_reboot, &host)?;
863992
if opts.apply {
864993
crate::reboot::reboot()?;
865994
}
@@ -881,6 +1010,13 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
8811010
if changed {
8821011
sysroot.update_mtime()?;
8831012

1013+
if opts.soft_reboot.is_some() {
1014+
// At this point we have new staged deployment and the host definition has changed.
1015+
// We need the updated host status before we check if we can prepare the soft-reboot.
1016+
let updated_host = crate::status::get_status(sysroot, Some(&booted_deployment))?.1;
1017+
handle_staged_soft_reboot(sysroot, opts.soft_reboot, &updated_host)?;
1018+
}
1019+
8841020
if opts.apply {
8851021
crate::reboot::reboot()?;
8861022
}
@@ -956,6 +1092,13 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
9561092

9571093
sysroot.update_mtime()?;
9581094

1095+
if opts.soft_reboot.is_some() {
1096+
// At this point we have staged the deployment and the host definition has changed.
1097+
// We need the updated host status before we check if we can prepare the soft-reboot.
1098+
let updated_host = crate::status::get_status(sysroot, Some(&booted_deployment))?.1;
1099+
handle_staged_soft_reboot(sysroot, opts.soft_reboot, &updated_host)?;
1100+
}
1101+
9591102
if opts.apply {
9601103
crate::reboot::reboot()?;
9611104
}
@@ -969,6 +1112,18 @@ async fn rollback(opts: RollbackOpts) -> Result<()> {
9691112
let sysroot = &get_storage().await?;
9701113
crate::deploy::rollback(sysroot).await?;
9711114

1115+
if opts.soft_reboot.is_some() {
1116+
// Get status of rollback deployment to check soft-reboot capability
1117+
let host = crate::status::get_status_require_booted(sysroot)?.2;
1118+
1119+
handle_soft_reboot(
1120+
opts.soft_reboot,
1121+
host.status.rollback.as_ref(),
1122+
"rollback",
1123+
|| soft_reboot_rollback(sysroot),
1124+
)?;
1125+
}
1126+
9721127
if opts.apply {
9731128
crate::reboot::reboot()?;
9741129
}

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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ impl From<ImageReference> for OstreeImageReference {
9090
}
9191
}
9292

93+
/// Check if a deployment has soft reboot capability
94+
fn has_soft_reboot_capability(sysroot: &Storage, deployment: &ostree::Deployment) -> bool {
95+
ostree_ext::systemd_has_soft_reboot() && sysroot.deployment_can_soft_reboot(deployment)
96+
}
97+
9398
/// Parse an ostree origin file (a keyfile) and extract the targeted
9499
/// container image reference.
95100
fn get_image_origin(origin: &glib::KeyFile) -> Result<Option<OstreeImageReference>> {
@@ -187,11 +192,13 @@ fn boot_entry_from_deployment(
187192
(CachedImageStatus::default(), false)
188193
};
189194

195+
let soft_reboot_capable = has_soft_reboot_capability(sysroot, deployment);
190196
let store = Some(crate::spec::Store::OstreeContainer);
191197
let r = BootEntry {
192198
image,
193199
cached_update,
194200
incompatible,
201+
soft_reboot_capable,
195202
store,
196203
pinned: deployment.is_pinned(),
197204
ostree: Some(crate::spec::BootEntryOstree {
@@ -425,6 +432,27 @@ fn render_verbose_ostree_info(
425432
Ok(())
426433
}
427434

435+
/// Helper function to render if soft-reboot capable
436+
fn write_soft_reboot(
437+
mut out: impl Write,
438+
entry: &crate::spec::BootEntry,
439+
prefix_len: usize,
440+
) -> Result<()> {
441+
// Show soft-reboot capability
442+
write_row_name(&mut out, "Soft-reboot", prefix_len)?;
443+
writeln!(
444+
out,
445+
"{}",
446+
if entry.soft_reboot_capable {
447+
"yes"
448+
} else {
449+
"no"
450+
}
451+
)?;
452+
453+
Ok(())
454+
}
455+
428456
/// Write the data for a container image based status.
429457
fn human_render_slot(
430458
mut out: impl Write,
@@ -507,6 +535,9 @@ fn human_render_slot(
507535
}
508536
}
509537
}
538+
539+
// Show soft-reboot capability
540+
write_soft_reboot(&mut out, entry, prefix_len)?;
510541
}
511542

512543
tracing::debug!("pinned={}", entry.pinned);
@@ -544,6 +575,9 @@ fn human_render_slot_ostree(
544575
if let Some(ostree) = &entry.ostree {
545576
render_verbose_ostree_info(&mut out, ostree, slot, prefix_len)?;
546577
}
578+
579+
// Show soft-reboot capability
580+
write_soft_reboot(&mut out, entry, prefix_len)?;
547581
}
548582

549583
tracing::debug!("pinned={}", entry.pinned);
@@ -765,5 +799,6 @@ mod tests {
765799
assert!(w.contains("Deploy serial:"));
766800
assert!(w.contains("Staged:"));
767801
assert!(w.contains("Commit:"));
802+
assert!(w.contains("Soft-reboot:"));
768803
}
769804
}

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

0 commit comments

Comments
 (0)