Skip to content

Commit c7f4488

Browse files
authored
Merge pull request #37 from hnez/rauc-slot-consistency
rauc: only consider good slots for update and reboot notifications
2 parents 909ac07 + 0c2830a commit c7f4488

File tree

2 files changed

+149
-33
lines changed

2 files changed

+149
-33
lines changed

src/dbus/rauc.rs

Lines changed: 147 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ mod imports {
6565

6666
#[cfg(not(feature = "demo_mode"))]
6767
mod imports {
68-
pub(super) use anyhow::{anyhow, bail, Result};
68+
pub(super) use anyhow::{bail, Result};
6969
pub(super) use log::error;
7070

7171
pub(super) const CHANNELS_DIR: &str = "/usr/share/tacd/update_channels";
@@ -98,6 +98,7 @@ pub struct Rauc {
9898
pub operation: Arc<Topic<String>>,
9999
pub progress: Arc<Topic<Progress>>,
100100
pub slot_status: Arc<Topic<Arc<SlotStatus>>>,
101+
pub primary: Arc<Topic<String>>,
101102
pub last_error: Arc<Topic<String>>,
102103
pub install: Arc<Topic<String>>,
103104
pub channels: Arc<Topic<Vec<Channel>>>,
@@ -117,43 +118,61 @@ fn compare_versions(v1: &str, v2: &str) -> Option<Ordering> {
117118
}
118119

119120
#[cfg(not(feature = "demo_mode"))]
120-
fn booted_older_than_other(slot_status: &SlotStatus) -> Result<bool> {
121+
fn would_reboot_into_other_slot(slot_status: &SlotStatus, primary: Option<String>) -> Result<bool> {
121122
let rootfs_0 = slot_status.get("rootfs_0");
122123
let rootfs_1 = slot_status.get("rootfs_1");
123124

125+
let (rootfs_0_is_primary, rootfs_1_is_primary) = primary
126+
.map(|p| (p == "rootfs_0", p == "rootfs_1"))
127+
.unwrap_or((false, false));
128+
124129
let rootfs_0_booted = rootfs_0.and_then(|r| r.get("state")).map(|s| s == "booted");
125130
let rootfs_1_booted = rootfs_1.and_then(|r| r.get("state")).map(|s| s == "booted");
126131

127-
let (booted, other) = match (rootfs_0_booted, rootfs_1_booted) {
128-
(Some(true), Some(true)) => {
129-
bail!("Two booted RAUC slots at the same time");
130-
}
131-
(Some(true), _) => (rootfs_0, rootfs_1),
132-
(_, Some(true)) => (rootfs_1, rootfs_0),
133-
_ => {
134-
bail!("No booted RAUC slot");
135-
}
136-
};
137-
138-
// Not having version information for the booted slot is an error.
139-
let booted_version = booted
140-
.and_then(|r| r.get("bundle_version"))
141-
.ok_or(anyhow!("No bundle version information for booted slot"))?;
142-
143-
// Not having version information for the other slot just means that
144-
// it is not newer.
145-
if let Some(other_version) = other.and_then(|r| r.get("bundle_version")) {
146-
if let Some(rel) = compare_versions(other_version, booted_version) {
147-
Ok(rel.is_gt())
148-
} else {
149-
Err(anyhow!(
150-
"Failed to compare date for bundle versions \"{}\" and \"{}\"",
151-
other_version,
152-
booted_version
153-
))
154-
}
155-
} else {
156-
Ok(false)
132+
let ((booted_slot, booted_is_primary), (other_slot, other_is_primary)) =
133+
match (rootfs_0_booted, rootfs_1_booted) {
134+
(Some(true), Some(true)) => {
135+
bail!("Two booted RAUC slots at the same time");
136+
}
137+
(Some(true), _) => (
138+
(rootfs_0, rootfs_0_is_primary),
139+
(rootfs_1, rootfs_1_is_primary),
140+
),
141+
(_, Some(true)) => (
142+
(rootfs_1, rootfs_1_is_primary),
143+
(rootfs_0, rootfs_0_is_primary),
144+
),
145+
_ => {
146+
bail!("No booted RAUC slot");
147+
}
148+
};
149+
150+
let booted_good = booted_slot
151+
.and_then(|r| r.get("boot_status"))
152+
.map(|s| s == "good");
153+
let other_good = other_slot
154+
.and_then(|r| r.get("boot_status"))
155+
.map(|s| s == "good");
156+
157+
let booted_ok = booted_slot.and_then(|r| r.get("status")).map(|s| s == "ok");
158+
let other_ok = other_slot.and_then(|r| r.get("status")).map(|s| s == "ok");
159+
160+
let booted_viable = booted_good.unwrap_or(false) && booted_ok.unwrap_or(false);
161+
let other_viable = other_good.unwrap_or(false) && other_ok.unwrap_or(false);
162+
163+
match (
164+
booted_viable,
165+
other_viable,
166+
booted_is_primary,
167+
other_is_primary,
168+
) {
169+
(true, false, _, _) => Ok(false),
170+
(false, true, _, _) => Ok(true),
171+
(true, true, true, false) => Ok(false),
172+
(true, true, false, true) => Ok(true),
173+
(false, false, _, _) => bail!("No bootable slot present"),
174+
(_, _, false, false) => bail!("No primary slot present"),
175+
(true, true, true, true) => bail!("Two primary slots present"),
157176
}
158177
}
159178

@@ -265,6 +284,7 @@ impl Rauc {
265284
operation: bb.topic_ro("/v1/tac/update/operation", None),
266285
progress: bb.topic_ro("/v1/tac/update/progress", None),
267286
slot_status: bb.topic_ro("/v1/tac/update/slots", None),
287+
primary: bb.topic_ro("/v1/tac/update/primary", None),
268288
last_error: bb.topic_ro("/v1/tac/update/last_error", None),
269289
install: bb.topic_wo("/v1/tac/update/install", Some("".to_string())),
270290
channels: bb.topic_ro("/v1/tac/update/channels", None),
@@ -300,6 +320,7 @@ impl Rauc {
300320
let conn_task = conn.clone();
301321
let operation = inst.operation.clone();
302322
let slot_status = inst.slot_status.clone();
323+
let primary = inst.primary.clone();
303324
let channels = inst.channels.clone();
304325
let should_reboot = inst.should_reboot.clone();
305326

@@ -313,6 +334,15 @@ impl Rauc {
313334
}
314335

315336
loop {
337+
// Update which slot is considered the primary whenever the current
338+
// operation changes.
339+
// (The one that should be booted next _if it is bootable_)
340+
let new_primary = proxy.get_primary().await.ok().map(|p| p.replace('.', "_"));
341+
342+
if let Some(p) = new_primary.clone() {
343+
primary.set_if_changed(p);
344+
}
345+
316346
// Referesh the slot status whenever the current operation changes
317347
// This is mostly relevant for "installing" -> "idle" transitions
318348
// but it can't hurt to do it on any transition.
@@ -373,7 +403,7 @@ impl Rauc {
373403

374404
// Provide a simple yes/no "should reboot into other slot?" information
375405
// based on the bundle versions in the booted slot and the other slot.
376-
match booted_older_than_other(&slots) {
406+
match would_reboot_into_other_slot(&slots, new_primary) {
377407
Ok(b) => should_reboot.set_if_changed(b),
378408
Err(e) => warn!("Could not determine if TAC should be rebooted: {e}"),
379409
}
@@ -465,3 +495,87 @@ impl Rauc {
465495
inst
466496
}
467497
}
498+
499+
#[cfg(test)]
500+
mod tests {
501+
use std::collections::HashMap;
502+
503+
use super::{would_reboot_into_other_slot, SlotStatus};
504+
505+
#[test]
506+
fn reboot_notifications() {
507+
let bootable = HashMap::from([
508+
("boot_status".to_string(), "good".to_string()),
509+
("status".to_string(), "ok".to_string()),
510+
]);
511+
512+
let not_bootable = HashMap::from([
513+
("boot_status".to_string(), "bad".to_string()),
514+
("status".to_string(), "ok".to_string()),
515+
]);
516+
517+
let cases = [
518+
(bootable.clone(), bootable.clone(), 0, 1, Ok(true)),
519+
(bootable.clone(), bootable.clone(), 1, 0, Ok(true)),
520+
(bootable.clone(), bootable.clone(), 0, 0, Ok(false)),
521+
(bootable.clone(), bootable.clone(), 1, 1, Ok(false)),
522+
(not_bootable.clone(), bootable.clone(), 1, 0, Ok(false)),
523+
(bootable.clone(), not_bootable.clone(), 0, 1, Ok(false)),
524+
(not_bootable.clone(), bootable.clone(), 0, 0, Ok(true)),
525+
(bootable.clone(), not_bootable.clone(), 1, 1, Ok(true)),
526+
(not_bootable.clone(), not_bootable.clone(), 0, 1, Err(())),
527+
(bootable.clone(), bootable.clone(), 2, 0, Err(())),
528+
(bootable.clone(), bootable.clone(), 0, 2, Err(())),
529+
];
530+
531+
for (mut rootfs_0, mut rootfs_1, booted, primary, expected) in cases {
532+
let slots = {
533+
rootfs_0.insert(
534+
"state".to_string(),
535+
if booted == 0 {
536+
"booted".to_string()
537+
} else {
538+
"inactive".to_string()
539+
},
540+
);
541+
542+
rootfs_1.insert(
543+
"state".to_string(),
544+
if booted == 1 {
545+
"booted".to_string()
546+
} else {
547+
"inactive".to_string()
548+
},
549+
);
550+
551+
SlotStatus::from([
552+
("rootfs_0".to_string(), rootfs_0),
553+
("rootfs_1".to_string(), rootfs_1),
554+
])
555+
};
556+
557+
let primary = Some(format!("rootfs_{primary}"));
558+
559+
let res = would_reboot_into_other_slot(&slots, primary.clone());
560+
561+
match (res, expected) {
562+
(Ok(true), Ok(true)) | (Ok(false), Ok(false)) | (Err(_), Err(_)) => {}
563+
(Ok(r), Ok(e)) => {
564+
eprintln!(
565+
"Slot status {slots:?} with primary {primary:?} yielded wrong result"
566+
);
567+
assert_eq!(r, e);
568+
}
569+
(Err(e), Ok(_)) => {
570+
eprintln!(
571+
"Slot status {slots:?} with primary {primary:?} returned unexpected error"
572+
);
573+
panic!("{:?}", e);
574+
}
575+
(Ok(res), Err(_)) => {
576+
panic!("Slot status {:?} with primary {:?} returned Ok({})) but should have errored", slots, primary, res);
577+
}
578+
}
579+
}
580+
}
581+
}

src/dbus/rauc/update_channels.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,14 @@ impl UpstreamBundle {
194194
pub(super) fn update_install(&mut self, slot_status: &SlotStatus) {
195195
let slot_0_is_older = slot_status
196196
.get("rootfs_0")
197+
.filter(|r| r.get("boot_status").map_or(false, |b| b == "good"))
197198
.and_then(|r| r.get("bundle_version"))
198199
.and_then(|v| compare_versions(&self.version, v).map(|c| c.is_gt()))
199200
.unwrap_or(true);
200201

201202
let slot_1_is_older = slot_status
202203
.get("rootfs_1")
204+
.filter(|r| r.get("boot_status").map_or(false, |b| b == "good"))
203205
.and_then(|r| r.get("bundle_version"))
204206
.and_then(|v| compare_versions(&self.version, v).map(|c| c.is_gt()))
205207
.unwrap_or(true);

0 commit comments

Comments
 (0)