1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
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
|