diff options
Diffstat (limited to '0001-Add-a-rollback-verb-and-rollbackQueued-status.patch')
| -rw-r--r-- | 0001-Add-a-rollback-verb-and-rollbackQueued-status.patch | 321 | 
1 files changed, 321 insertions, 0 deletions
| diff --git a/0001-Add-a-rollback-verb-and-rollbackQueued-status.patch b/0001-Add-a-rollback-verb-and-rollbackQueued-status.patch new file mode 100644 index 0000000..b96ac6c --- /dev/null +++ b/0001-Add-a-rollback-verb-and-rollbackQueued-status.patch @@ -0,0 +1,321 @@ +From 04baad3080246b1ce26df7a783874b2b1b3ddaa4 Mon Sep 17 00:00:00 2001 +From: Colin Walters <walters@verbum.org> +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 <walters@verbum.org> +--- + lib/src/cli.rs                            | 33 ++++++++++++ + lib/src/deploy.rs                         | 61 ++++++++++++++++++++++- + lib/src/spec.rs                           | 40 +++++++++++++++ + lib/src/status.rs                         | 15 +++++- + tests/integration/playbooks/rollback.yaml |  4 +- + 5 files changed, 148 insertions(+), 5 deletions(-) + +diff --git a/lib/src/cli.rs b/lib/src/cli.rs +index 42bfbcc..623796a 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<()> { +@@ -522,7 +546,15 @@ async fn edit(opts: EditOpts) -> Result<()> { +         println!("Edit cancelled, no changes made."); +         return Ok(()); +     } ++    host.spec.verify_transition(&new_host.spec)?; +     let new_spec = RequiredHostSpec::from_spec(&new_host.spec)?; ++ ++    // We only support two state transitions right now; switching the image, ++    // or flipping the bootloader ordering. ++    if host.spec.boot_order != new_host.spec.boot_order { ++        return crate::deploy::rollback(sysroot).await; ++    } ++ +     let fetched = crate::deploy::pull(sysroot, new_spec.image, opts.quiet).await?; +  +     // TODO gc old layers here +@@ -586,6 +618,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 444ad8e..3eb31a8 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; +@@ -19,8 +19,8 @@ use ostree_ext::ostree; + use ostree_ext::ostree::Deployment; + use ostree_ext::sysroot::SysrootLock; +  +-use crate::spec::HostSpec; + use crate::spec::ImageReference; ++use crate::spec::{BootOrder, HostSpec}; + use crate::status::labels_of_config; +  + // TODO use https://github.com/ostreedev/ostree-rs-ext/pull/493/commits/afc1837ff383681b947de30c0cefc70080a4f87a +@@ -276,6 +276,63 @@ 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 new_spec = { ++        let mut new_spec = host.spec.clone(); ++        new_spec.boot_order = new_spec.boot_order.swap(); ++        new_spec ++    }; ++ ++    // Just to be sure ++    host.spec.verify_transition(&new_spec)?; ++ ++    let reverting = new_spec.boot_order == BootOrder::Default; ++    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::<Vec<_>>(); ++    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<String> { +     let mut dirs = Vec::new(); +     for ent in deploysdir.entries()? { +diff --git a/lib/src/spec.rs b/lib/src/spec.rs +index 6de9639..5f6df93 100644 +--- a/lib/src/spec.rs ++++ b/lib/src/spec.rs +@@ -28,12 +28,27 @@ pub struct Host { +     pub status: HostStatus, + } +  ++/// Configuration for system boot ordering. ++ ++#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] ++#[serde(rename_all = "camelCase")] ++pub enum BootOrder { ++    /// The staged or booted deployment will be booted next ++    #[default] ++    Default, ++    /// The rollback deployment will be booted next ++    Rollback, ++} ++ + #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] + #[serde(rename_all = "camelCase")] + /// The host specification + pub struct HostSpec { +     /// The host image +     pub image: Option<ImageReference>, ++    /// If set, and there is a rollback deployment, it will be set for the next boot. ++    #[serde(default)] ++    pub boot_order: BootOrder, + } +  + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +@@ -121,6 +136,9 @@ pub struct HostStatus { +     pub booted: Option<BootEntry>, +     /// The previously booted image +     pub rollback: Option<BootEntry>, ++    /// Set to true if the rollback entry is queued for the next boot. ++    #[serde(default)] ++    pub rollback_queued: bool, +  +     /// The detected type of system +     #[serde(rename = "type")] +@@ -152,6 +170,28 @@ impl Default for Host { +     } + } +  ++impl HostSpec { ++    /// Validate a spec state transition; some changes cannot be made simultaneously, ++    /// such as fetching a new image and doing a rollback. ++    pub(crate) fn verify_transition(&self, new: &Self) -> anyhow::Result<()> { ++        let rollback = self.boot_order != new.boot_order; ++        let image_change = self.image != new.image; ++        if rollback && image_change { ++            anyhow::bail!("Invalid state transition: rollback and image change"); ++        } ++        Ok(()) ++    } ++} ++ ++impl BootOrder { ++    pub(crate) fn swap(&self) -> Self { ++        match self { ++            BootOrder::Default => BootOrder::Rollback, ++            BootOrder::Rollback => BootOrder::Default, ++        } ++    } ++} ++ + impl Display for ImageReference { +     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +         // For the default of fetching from a remote registry, just output the image name +diff --git a/lib/src/status.rs b/lib/src/status.rs +index dba4889..e8f1fa5 100644 +--- a/lib/src/status.rs ++++ b/lib/src/status.rs +@@ -1,6 +1,6 @@ + use std::collections::VecDeque; +  +-use crate::spec::{BootEntry, Host, HostSpec, HostStatus, HostType, ImageStatus}; ++use crate::spec::{BootEntry, BootOrder, Host, HostSpec, HostStatus, HostType, ImageStatus}; + use crate::spec::{ImageReference, ImageSignature}; + use anyhow::{Context, Result}; + use camino::Utf8Path; +@@ -224,11 +224,22 @@ 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, ++    }; ++    let boot_order = if rollback_queued { ++        BootOrder::Rollback ++    } else { ++        BootOrder::Default ++    }; ++    tracing::debug!("Rollback queued={rollback_queued:?}"); +     let other = { +         related_deployments.extend(other_deployments); +         related_deployments +@@ -262,6 +273,7 @@ pub(crate) fn get_status( +         .and_then(|entry| entry.image.as_ref()) +         .map(|img| HostSpec { +             image: Some(img.image.clone()), ++            boot_order, +         }) +         .unwrap_or_default(); +  +@@ -281,6 +293,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 a801656..e193ff5 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 +--  +2.43.0 + | 
