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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
9 changes: 5 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# use e.g. --build-arg=base=quay.io/fedora/fedora-bootc:41 to target
# Fedora instead.

ARG base=quay.io/centos-bootc/centos-bootc:stream9
ARG base=quay.io/centos-bootc/centos-bootc:stream10

FROM scratch as src
COPY . /src
Expand All @@ -23,6 +23,8 @@ case $ID in
centos|rhel) dnf config-manager --set-enabled crb;;
fedora) dnf -y install dnf-utils 'dnf5-command(builddep)';;
esac
# Hot patch
dnf -y install https://kojihub.stream.centos.org/kojifiles/packages/ostree/2025.3/1.el10/$(arch)/ostree-{libs-,devel-,}2025.3-1.el10.$(arch).rpm
dnf -y builddep /tmp/bootc.spec
# Extra dependencies
dnf -y install git-core
Expand Down
4 changes: 3 additions & 1 deletion crates/lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ bootc-mount = { path = "../mount" }
bootc-tmpfiles = { path = "../tmpfiles" }
bootc-sysusers = { path = "../sysusers" }
camino = { workspace = true, features = ["serde1"] }
cfg-if = "1.0"
ostree-ext = { path = "../ostree-ext", features = ["bootc"] }
chrono = { workspace = true, features = ["serde"] }
clap = { workspace = true, features = ["derive","cargo"] }
Expand Down Expand Up @@ -62,7 +63,7 @@ similar-asserts = { workspace = true }
static_assertions = { workspace = true }

[features]
default = ["install-to-disk"]
default = ["install-to-disk", "ostree-2025-3"]
# This feature enables `bootc install to-disk`, which is considered just a "demo"
# or reference installer; we expect most nontrivial use cases to be using
# `bootc install to-filesystem`.
Expand All @@ -72,6 +73,7 @@ install-to-disk = []
rhsm = []
# Implementation detail of man page generation.
docgen = ["clap_mangen"]
ostree-2025-3 = ["ostree-ext/ostree-2025-3"]

[lints]
workspace = true
81 changes: 79 additions & 2 deletions crates/lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use anyhow::{ensure, Context, Result};
use camino::Utf8PathBuf;
use cap_std_ext::cap_std;
use cap_std_ext::cap_std::fs::Dir;
use cfg_if::cfg_if;
use clap::Parser;
use clap::ValueEnum;
use fn_error_context::context;
Expand Down Expand Up @@ -544,7 +545,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 @@ -723,6 +724,43 @@ pub(crate) fn require_root(is_container: bool) -> Result<()> {
Ok(())
}

/// Check if a deployment can perform a soft reboot
#[cfg(feature = "ostree-2025-3")]
fn can_perform_soft_reboot(deployment: Option<&crate::spec::BootEntry>) -> bool {
deployment.map(|d| d.soft_reboot_capable).unwrap_or(false)
}

/// Prepare and execute a soft reboot for the given deployment
#[context("Preparing soft reboot")]
#[cfg(feature = "ostree-2025-3")]
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(())
}

/// Perform a soft reboot for a staged deployment
#[context("Soft reboot staged deployment")]
#[cfg(feature = "ostree-2025-3")]
fn soft_reboot_staged(sysroot: &crate::store::Storage) -> Result<()> {
println!("Staged deployment is soft-reboot capable, performing 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(())
}

/// 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 @@ -843,6 +881,10 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
println!("Staged update present, not changed.");

if opts.apply {
#[cfg(feature = "ostree-2025-3")]
if can_perform_soft_reboot(host.status.staged.as_ref()) {
soft_reboot_staged(sysroot)?;
}
crate::reboot::reboot()?;
}
} else if booted_unchanged {
Expand Down Expand Up @@ -939,6 +981,16 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
sysroot.update_mtime()?;

if opts.apply {
#[cfg(feature = "ostree-2025-3")]
{
// Get updated status to check for soft-reboot capability
let (_updated_deployments, updated_host) =
crate::status::get_status(sysroot, Some(&booted_deployment))?;

if can_perform_soft_reboot(updated_host.status.staged.as_ref()) {
soft_reboot_staged(sysroot)?;
}
}
crate::reboot::reboot()?;
}

Expand All @@ -949,10 +1001,35 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
#[context("Rollback")]
async fn rollback(opts: RollbackOpts) -> Result<()> {
let sysroot = &get_storage().await?;
crate::deploy::rollback(sysroot).await?;

if opts.apply {
// Get status before rollback to check soft-reboot capability
let (_booted_deployment, _deployments, host) =
crate::status::get_status_require_booted(sysroot)?;

// Perform the rollback
crate::deploy::rollback(sysroot).await?;

cfg_if! {
if #[cfg(feature = "ostree-2025-3")] {
if can_perform_soft_reboot(host.status.rollback.as_ref()) {
println!("Rollback deployment is soft-reboot capable, performing soft-reboot...");

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

prepare_soft_reboot(sysroot, target_deployment)?;
}
} else {
let _host = host;
}
}

crate::reboot::reboot()?;
} else {
crate::deploy::rollback(sysroot).await?;
}

Ok(())
Expand Down
4 changes: 4 additions & 0 deletions crates/lib/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,10 @@ async fn deploy(
&opts,
Some(cancellable),
)?;
tracing::debug!(
"Soft reboot capable: {:?}",
ostree_ext::deployment_can_soft_reboot(&sysroot, &d)
);
Ok(d.index())
}),
)
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
14 changes: 9 additions & 5 deletions crates/lib/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,14 @@ fn boot_entry_from_deployment(
(None, CachedImageStatus::default(), false)
};

let soft_reboot_capable =
ostree_ext::deployment_can_soft_reboot(&sysroot, deployment).unwrap_or_default();

let r = BootEntry {
image,
cached_update,
incompatible,
soft_reboot_capable,
store,
pinned: deployment.is_pinned(),
ostree: Some(crate::spec::BootEntryOstree {
Expand Down Expand Up @@ -228,17 +232,17 @@ pub(crate) fn get_status(
other,
};

let staged = deployments
.staged
let booted = booted_deployment
.as_ref()
.map(|d| boot_entry_from_deployment(sysroot, d))
.transpose()
.context("Staged deployment")?;
let booted = booted_deployment
.context("Booted deployment")?;
let staged = deployments
.staged
.as_ref()
.map(|d| boot_entry_from_deployment(sysroot, d))
.transpose()
.context("Booted deployment")?;
.context("Staged deployment")?;
let rollback = deployments
.rollback
.as_ref()
Expand Down
4 changes: 3 additions & 1 deletion crates/ostree-ext/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ version = "0.15.3"
[dependencies]
containers-image-proxy = "0.8.0"
# We re-export this library too.
ostree = { features = ["v2025_2"], version = "0.20" }
# Default to v2025_2, upgrade to v2025_3 when feature is enabled
ostree = { version = "0.20", features = ["v2025_2"] }

# Private dependencies
anyhow = { workspace = true }
Expand Down Expand Up @@ -65,6 +66,7 @@ features = ["dox"]
docgen = ["clap_mangen"]
dox = ["ostree/dox"]
internal-testing-api = ["xshell", "indoc", "similar-asserts"]
ostree-2025-3 = ["ostree/v2025_3"]
# Enable calling back into bootc
bootc = []

Expand Down
31 changes: 31 additions & 0 deletions crates/ostree-ext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,34 @@ pub mod prelude {
pub mod fixture;
#[cfg(feature = "internal-testing-api")]
pub mod integrationtest;

#[cfg(feature = "ostree-2025-3")]
/// 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())
}

/// Dynamic detection wrapper for soft reboots, if the installed ostree is too old
/// then we return `None`.
pub fn deployment_can_soft_reboot(
sysroot: &ostree::Sysroot,
deployment: &ostree::Deployment,
) -> Option<bool> {
#[cfg(feature = "ostree-2025-3")]
{
// Even if ostree is new enough, it
if !systemd_has_soft_reboot() {
return None;
}
Some(sysroot.deployment_can_soft_reboot(deployment))
}
#[cfg(not(feature = "ostree-2025-3"))]
{
let _ = (sysroot, deployment);
None
}
}
Loading