Skip to content

lib: add support for soft-reboots #1392

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 157 additions & 2 deletions crates/lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ pub(crate) struct UpgradeOpts {
#[clap(long, conflicts_with = "check")]
pub(crate) apply: bool,

/// Configure soft reboot behavior.
///
/// 'required' will fail if soft reboot is not available.
/// 'auto' will use soft reboot if available, otherwise fall back to regular reboot.
#[clap(long = "soft-reboot", conflicts_with = "check")]
pub(crate) soft_reboot: Option<SoftRebootMode>,
Comment on lines +83 to +88
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The soft_reboot option, including its documentation, is duplicated across UpgradeOpts, SwitchOpts, and RollbackOpts. This could become a maintainability issue.

Consider factoring out this common option to avoid repetition. A #[clap(flatten)] struct could be an option if the conflicts_with attribute can be handled. Another approach could be a macro to generate the field.


#[clap(flatten)]
pub(crate) progress: ProgressOptions,
}
Expand All @@ -99,6 +106,13 @@ pub(crate) struct SwitchOpts {
#[clap(long)]
pub(crate) apply: bool,

/// Configure soft reboot behavior.
///
/// 'required' will fail if soft reboot is not available.
/// 'auto' will use soft reboot if available, otherwise fall back to regular reboot.
#[clap(long = "soft-reboot")]
pub(crate) soft_reboot: Option<SoftRebootMode>,

/// The transport; e.g. oci, oci-archive, containers-storage. Defaults to `registry`.
#[clap(long, default_value = "registry")]
pub(crate) transport: String,
Expand Down Expand Up @@ -142,6 +156,13 @@ pub(crate) struct RollbackOpts {
/// a userspace-only restart.
#[clap(long)]
pub(crate) apply: bool,

/// Configure soft reboot behavior.
///
/// 'required' will fail if soft reboot is not available.
/// 'auto' will use soft reboot if available, otherwise fall back to regular reboot.
#[clap(long = "soft-reboot")]
pub(crate) soft_reboot: Option<SoftRebootMode>,
}

/// Perform an edit operation
Expand All @@ -167,6 +188,15 @@ pub(crate) enum OutputFormat {
Json,
}

#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
#[clap(rename_all = "lowercase")]
pub(crate) enum SoftRebootMode {
/// Require a soft reboot; fail if not possible
Required,
/// Automatically use soft reboot if possible, otherwise use regular reboot
Auto,
}

/// Perform an status operation
#[derive(Debug, Parser, PartialEq, Eq)]
pub(crate) struct StatusOpts {
Expand Down Expand Up @@ -562,7 +592,7 @@ pub(crate) enum Opt {
Note on Rollbacks and the `/etc` Directory:
When you perform a rollback (e.g., with `bootc rollback`), any
changes made to files in the `/etc` directory wont carry over
changes made to files in the `/etc` directory won't carry over
to the rolled-back deployment. The `/etc` files will revert
to their state from that previous deployment instead.
Expand Down Expand Up @@ -741,6 +771,104 @@ pub(crate) fn require_root(is_container: bool) -> Result<()> {
Ok(())
}

/// Check if a deployment has soft reboot capability
fn has_soft_reboot_capability(deployment: Option<&crate::spec::BootEntry>) -> bool {
deployment.map(|d| d.soft_reboot_capable).unwrap_or(false)
}

/// Prepare a soft reboot for the given deployment
#[context("Preparing soft reboot")]
fn prepare_soft_reboot(
sysroot: &crate::store::Storage,
deployment: &ostree::Deployment,
) -> Result<()> {
let cancellable = ostree::gio::Cancellable::NONE;
sysroot
.sysroot
.deployment_set_soft_reboot(deployment, false, cancellable)
.context("Failed to prepare soft-reboot")?;
Ok(())
}

/// Handle soft reboot based on the configured mode
#[context("Handling soft reboot")]
fn handle_soft_reboot<F>(
soft_reboot_mode: Option<SoftRebootMode>,
entry: Option<&crate::spec::BootEntry>,
deployment_type: &str,
execute_soft_reboot: F,
) -> Result<()>
where
F: FnOnce() -> Result<()>,
{
let Some(mode) = soft_reboot_mode else {
return Ok(());
};

let can_soft_reboot = has_soft_reboot_capability(entry);
match mode {
SoftRebootMode::Required => {
if can_soft_reboot {
execute_soft_reboot()?;
} else {
anyhow::bail!(
"Soft reboot was required but {} deployment is not soft-reboot capable",
deployment_type
);
}
}
SoftRebootMode::Auto => {
if can_soft_reboot {
execute_soft_reboot()?;
}
}
}
Comment on lines +808 to +825
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic in this match statement can be simplified to be more concise and arguably easier to read by handling the can_soft_reboot check first.

    let can_soft_reboot = has_soft_reboot_capability(entry);

    if can_soft_reboot {
        execute_soft_reboot()?;
    } else if matches!(mode, SoftRebootMode::Required) {
        anyhow::bail!(
            "Soft reboot was required but {} deployment is not soft-reboot capable",
            deployment_type
        );
    }

Ok(())
}

/// Handle soft reboot for staged deployments (used by upgrade and switch)
#[context("Handling staged soft reboot")]
fn handle_staged_soft_reboot(
sysroot: &crate::store::Storage,
soft_reboot_mode: Option<SoftRebootMode>,
host: &crate::spec::Host,
) -> Result<()> {
handle_soft_reboot(
soft_reboot_mode,
host.status.staged.as_ref(),
"staged",
|| soft_reboot_staged(sysroot),
)
}

/// Perform a soft reboot for a staged deployment
#[context("Soft reboot staged deployment")]
fn soft_reboot_staged(sysroot: &crate::store::Storage) -> Result<()> {
println!("Staged deployment is soft-reboot capable, preparing for soft-reboot...");

let deployments_list = sysroot.deployments();
let staged_deployment = deployments_list
.iter()
.find(|d| d.is_staged())
.ok_or_else(|| anyhow::anyhow!("Failed to find staged deployment"))?;

prepare_soft_reboot(sysroot, staged_deployment)?;
Ok(())
}

/// Perform a soft reboot for a rollback deployment
#[context("Soft reboot rollback deployment")]
fn soft_reboot_rollback(sysroot: &crate::store::Storage) -> Result<()> {
println!("Rollback deployment is soft-reboot capable, preparing for soft-reboot...");

let deployments_list = sysroot.deployments();
let target_deployment = deployments_list
.first()
.ok_or_else(|| anyhow::anyhow!("No rollback deployment found!"))?;

prepare_soft_reboot(sysroot, target_deployment)
}

/// A few process changes that need to be made for writing.
/// IMPORTANT: This may end up re-executing the current process,
/// so anything that happens before this should be idempotent.
Expand Down Expand Up @@ -813,6 +941,7 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
let booted_image = host
.status
.booted
.as_ref()
.map(|b| b.query_image(repo))
.transpose()?
.flatten();
Expand Down Expand Up @@ -859,7 +988,7 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
.unwrap_or_default();
if staged_unchanged {
println!("Staged update present, not changed.");

handle_staged_soft_reboot(sysroot, opts.soft_reboot, &host)?;
if opts.apply {
crate::reboot::reboot()?;
}
Expand All @@ -881,6 +1010,13 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
if changed {
sysroot.update_mtime()?;

if opts.soft_reboot.is_some() {
// At this point we have new staged deployment and the host definition has changed.
// We need the updated host status before we check if we can prepare the soft-reboot.
let updated_host = crate::status::get_status(sysroot, Some(&booted_deployment))?.1;
handle_staged_soft_reboot(sysroot, opts.soft_reboot, &updated_host)?;
}

if opts.apply {
crate::reboot::reboot()?;
}
Expand Down Expand Up @@ -956,6 +1092,13 @@ async fn switch(opts: SwitchOpts) -> Result<()> {

sysroot.update_mtime()?;

if opts.soft_reboot.is_some() {
// At this point we have staged the deployment and the host definition has changed.
// We need the updated host status before we check if we can prepare the soft-reboot.
let updated_host = crate::status::get_status(sysroot, Some(&booted_deployment))?.1;
handle_staged_soft_reboot(sysroot, opts.soft_reboot, &updated_host)?;
}

if opts.apply {
crate::reboot::reboot()?;
}
Expand All @@ -969,6 +1112,18 @@ async fn rollback(opts: RollbackOpts) -> Result<()> {
let sysroot = &get_storage().await?;
crate::deploy::rollback(sysroot).await?;

if opts.soft_reboot.is_some() {
// Get status of rollback deployment to check soft-reboot capability
let host = crate::status::get_status_require_booted(sysroot)?.2;

handle_soft_reboot(
opts.soft_reboot,
host.status.rollback.as_ref(),
"rollback",
|| soft_reboot_rollback(sysroot),
)?;
}

if opts.apply {
crate::reboot::reboot()?;
}
Expand Down
4 changes: 4 additions & 0 deletions crates/lib/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ pub struct BootEntry {
pub incompatible: bool,
/// Whether this entry will be subject to garbage collection
pub pinned: bool,
/// This is true if (relative to the booted system) this is a possible target for a soft reboot
#[serde(default)]
pub soft_reboot_capable: bool,
/// The container storage backend
#[serde(default)]
pub store: Option<Store>,
Expand Down Expand Up @@ -517,6 +520,7 @@ mod tests {
image: None,
cached_update: None,
incompatible: false,
soft_reboot_capable: false,
pinned: false,
store: None,
ostree: None,
Expand Down
35 changes: 35 additions & 0 deletions crates/lib/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ impl From<ImageReference> for OstreeImageReference {
}
}

/// Check if a deployment has soft reboot capability
fn has_soft_reboot_capability(sysroot: &Storage, deployment: &ostree::Deployment) -> bool {
ostree_ext::systemd_has_soft_reboot() && sysroot.deployment_can_soft_reboot(deployment)
}

/// Parse an ostree origin file (a keyfile) and extract the targeted
/// container image reference.
fn get_image_origin(origin: &glib::KeyFile) -> Result<Option<OstreeImageReference>> {
Expand Down Expand Up @@ -187,11 +192,13 @@ fn boot_entry_from_deployment(
(CachedImageStatus::default(), false)
};

let soft_reboot_capable = has_soft_reboot_capability(sysroot, deployment);
let store = Some(crate::spec::Store::OstreeContainer);
let r = BootEntry {
image,
cached_update,
incompatible,
soft_reboot_capable,
store,
pinned: deployment.is_pinned(),
ostree: Some(crate::spec::BootEntryOstree {
Expand Down Expand Up @@ -425,6 +432,27 @@ fn render_verbose_ostree_info(
Ok(())
}

/// Helper function to render if soft-reboot capable
fn write_soft_reboot(
mut out: impl Write,
entry: &crate::spec::BootEntry,
prefix_len: usize,
) -> Result<()> {
// Show soft-reboot capability
write_row_name(&mut out, "Soft-reboot", prefix_len)?;
writeln!(
out,
"{}",
if entry.soft_reboot_capable {
"yes"
} else {
"no"
}
)?;

Ok(())
}

/// Write the data for a container image based status.
fn human_render_slot(
mut out: impl Write,
Expand Down Expand Up @@ -507,6 +535,9 @@ fn human_render_slot(
}
}
}

// Show soft-reboot capability
write_soft_reboot(&mut out, entry, prefix_len)?;
}

tracing::debug!("pinned={}", entry.pinned);
Expand Down Expand Up @@ -544,6 +575,9 @@ fn human_render_slot_ostree(
if let Some(ostree) = &entry.ostree {
render_verbose_ostree_info(&mut out, ostree, slot, prefix_len)?;
}

// Show soft-reboot capability
write_soft_reboot(&mut out, entry, prefix_len)?;
}

tracing::debug!("pinned={}", entry.pinned);
Expand Down Expand Up @@ -765,5 +799,6 @@ mod tests {
assert!(w.contains("Deploy serial:"));
assert!(w.contains("Staged:"));
assert!(w.contains("Commit:"));
assert!(w.contains("Soft-reboot:"));
}
}
2 changes: 1 addition & 1 deletion crates/ostree-ext/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ io-lifetimes = "3"
libsystemd = "0.7.0"
ocidir = "0.4.0"
# We re-export this library too.
ostree = { features = ["v2025_2"], version = "0.20" }
ostree = { features = ["v2025_3"], version = "0.20.3" }
pin-project = "1.0"
tar = "0.4.43"
tokio-stream = { features = ["sync"], version = "0.1.8" }
Expand Down
9 changes: 9 additions & 0 deletions crates/ostree-ext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,12 @@ pub mod prelude {
pub mod fixture;
#[cfg(feature = "internal-testing-api")]
pub mod integrationtest;

/// Check if the system has the soft reboot target, which signals
/// systemd support for soft reboots.
pub fn systemd_has_soft_reboot() -> bool {
const UNIT: &str = "/usr/lib/systemd/system/soft-reboot.target";
use std::sync::OnceLock;
static EXISTS: OnceLock<bool> = OnceLock::new();
*EXISTS.get_or_init(|| std::path::Path::new(UNIT).exists())
}
8 changes: 8 additions & 0 deletions tmt/plans/integration.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,11 @@ execute:
test:
- /tmt/tests/bootc-install-provision
- /tmt/tests/test-24-local-upgrade-reboot

/test-25-soft-reboot:
summary: Soft reboot support
discover:
how: fmf
test:
- /tmt/tests/bootc-install-provision
- /tmt/tests/test-25-soft-reboot
Loading
Loading