From 32348a5c1659c00f05084219b098f4076cfc0e0b Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Sat, 9 Mar 2024 15:33:27 -0500 Subject: [PATCH] Add a `rollback` verb and `rollbackQueued` status I'd really hoped to do something more declarative here, and really flesh out the intersections with automated upgrades and automated rollbacks. But, this just exposes the simple primitive, equivalent to `rpm-ostree rollback`. Signed-off-by: Colin Walters --- lib/src/cli.rs | 25 ++++++++++++ lib/src/deploy.rs | 49 ++++++++++++++++++++++- lib/src/spec.rs | 2 + lib/src/status.rs | 7 ++++ tests/integration/playbooks/rollback.yaml | 4 +- 5 files changed, 84 insertions(+), 3 deletions(-) diff --git a/lib/src/cli.rs b/lib/src/cli.rs index 42bfbcca..57a2ac92 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -89,6 +89,10 @@ pub(crate) struct SwitchOpts { pub(crate) target: String, } +/// Options controlling rollback +#[derive(Debug, Parser, PartialEq, Eq)] +pub(crate) struct RollbackOpts {} + /// Perform an edit operation #[derive(Debug, Parser, PartialEq, Eq)] pub(crate) struct EditOpts { @@ -214,6 +218,18 @@ pub(crate) enum Opt { /// This operates in a very similar fashion to `upgrade`, but changes the container image reference /// instead. Switch(SwitchOpts), + /// Change the bootloader entry ordering; the deployment under `rollback` will be queued for the next boot, + /// and the current will become rollback. If there is a `staged` entry (an unapplied, queued upgrade) + /// then it will be discarded. + /// + /// Note that absent any additional control logic, if there is an active agent doing automated upgrades + /// (such as the default `bootc-fetch-apply-updates.timer` and associated `.service`) the + /// change here may be reverted. It's recommended to only use this in concert with an agent that + /// is in active control. + /// + /// A systemd journal message will be logged with `MESSAGE_ID=26f3b1eb24464d12aa5e7b544a6b5468` in + /// order to detect a rollback invocation. + Rollback(RollbackOpts), /// Apply full changes to the host specification. /// /// This command operates very similarly to `kubectl apply`; if invoked interactively, @@ -500,6 +516,14 @@ async fn switch(opts: SwitchOpts) -> Result<()> { Ok(()) } +/// Implementation of the `bootc rollback` CLI command. +#[context("Rollback")] +async fn rollback(_opts: RollbackOpts) -> Result<()> { + prepare_for_write().await?; + let sysroot = &get_locked_sysroot().await?; + crate::deploy::rollback(sysroot).await +} + /// Implementation of the `bootc edit` CLI command. #[context("Editing spec")] async fn edit(opts: EditOpts) -> Result<()> { @@ -586,6 +610,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> { match opt { Opt::Upgrade(opts) => upgrade(opts).await, Opt::Switch(opts) => switch(opts).await, + Opt::Rollback(opts) => rollback(opts).await, Opt::Edit(opts) => edit(opts).await, Opt::UsrOverlay => usroverlay().await, #[cfg(feature = "install")] diff --git a/lib/src/deploy.rs b/lib/src/deploy.rs index 444ad8e0..903d7104 100644 --- a/lib/src/deploy.rs +++ b/lib/src/deploy.rs @@ -5,7 +5,7 @@ use std::io::{BufRead, Write}; use anyhow::Ok; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use cap_std::fs::{Dir, MetadataExt}; use cap_std_ext::cap_std; @@ -276,6 +276,53 @@ pub(crate) async fn stage( Ok(()) } +/// Implementation of rollback functionality +pub(crate) async fn rollback(sysroot: &SysrootLock) -> Result<()> { + const ROLLBACK_JOURNAL_ID: &str = "26f3b1eb24464d12aa5e7b544a6b5468"; + let repo = &sysroot.repo(); + let (booted_deployment, deployments, host) = crate::status::get_status_require_booted(sysroot)?; + let reverting = host.status.rollback_queued; + if reverting { + println!("notice: Reverting queued rollback state"); + } + let rollback_status = host + .status + .rollback + .ok_or_else(|| anyhow!("No rollback available"))?; + let rollback_image = rollback_status + .query_image(repo)? + .ok_or_else(|| anyhow!("Rollback is not container image based"))?; + let msg = format!("Rolling back to image: {}", rollback_image.manifest_digest); + libsystemd::logging::journal_send( + libsystemd::logging::Priority::Info, + &msg, + [ + ("MESSAGE_ID", ROLLBACK_JOURNAL_ID), + ("BOOTC_MANIFEST_DIGEST", &rollback_image.manifest_digest), + ] + .into_iter(), + )?; + // SAFETY: If there's a rollback status, then there's a deployment + let rollback_deployment = deployments.rollback.expect("rollback deployment"); + let new_deployments = if reverting { + [booted_deployment, rollback_deployment] + } else { + [rollback_deployment, booted_deployment] + }; + let new_deployments = new_deployments + .into_iter() + .chain(deployments.other) + .collect::>(); + tracing::debug!("Writing new deployments: {new_deployments:?}"); + sysroot.write_deployments(&new_deployments, gio::Cancellable::NONE)?; + if reverting { + println!("Next boot: current deployment"); + } else { + println!("Next boot: rollback deployment"); + } + Ok(()) +} + fn find_newest_deployment_name(deploysdir: &Dir) -> Result { let mut dirs = Vec::new(); for ent in deploysdir.entries()? { diff --git a/lib/src/spec.rs b/lib/src/spec.rs index 6de96390..f4aeb74e 100644 --- a/lib/src/spec.rs +++ b/lib/src/spec.rs @@ -121,6 +121,8 @@ pub struct HostStatus { pub booted: Option, /// The previously booted image pub rollback: Option, + /// Set to true if the rollback entry is queued for the next boot. + pub rollback_queued: bool, /// The detected type of system #[serde(rename = "type")] diff --git a/lib/src/status.rs b/lib/src/status.rs index dba4889f..b82edc64 100644 --- a/lib/src/status.rs +++ b/lib/src/status.rs @@ -224,11 +224,17 @@ pub(crate) fn get_status( .iter() .position(|d| d.is_staged()) .map(|i| related_deployments.remove(i).unwrap()); + tracing::debug!("Staged: {staged:?}"); // Filter out the booted, the caller already found that if let Some(booted) = booted_deployment.as_ref() { related_deployments.retain(|f| !f.equal(booted)); } let rollback = related_deployments.pop_front(); + let rollback_queued = match (booted_deployment.as_ref(), rollback.as_ref()) { + (Some(booted), Some(rollback)) => rollback.index() < booted.index(), + _ => false, + }; + tracing::debug!("Rollback queued={rollback_queued:?}"); let other = { related_deployments.extend(other_deployments); related_deployments @@ -281,6 +287,7 @@ pub(crate) fn get_status( staged, booted, rollback, + rollback_queued, ty, }; Ok((deployments, host)) diff --git a/tests/integration/playbooks/rollback.yaml b/tests/integration/playbooks/rollback.yaml index a801656d..e193ff50 100644 --- a/tests/integration/playbooks/rollback.yaml +++ b/tests/integration/playbooks/rollback.yaml @@ -6,8 +6,8 @@ failed_counter: "0" tasks: - - name: rpm-ostree rollback - command: rpm-ostree rollback + - name: bootc rollback + command: bootc rollback become: true - name: Reboot to deploy new system