From ac2e0c416223552d9a604bab7e33e514e1d518bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Thu, 27 Mar 2025 08:59:42 +0100 Subject: [PATCH 01/21] tacd: dbus: rauc: re-introspect services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RAUC is in the process of adding native update polling support. Re-introspect the dbus service to add the new APIs. Signed-off-by: Leonard Göhrs --- src/dbus/rauc.rs | 3 +++ src/dbus/rauc/installer.rs | 5 +++++ src/dbus/rauc/poller.rs | 24 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 src/dbus/rauc/poller.rs diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs index 415f7710..6d690843 100644 --- a/src/dbus/rauc.rs +++ b/src/dbus/rauc.rs @@ -43,6 +43,9 @@ mod installer; #[cfg(not(feature = "demo_mode"))] use installer::InstallerProxy; +#[cfg(not(feature = "demo_mode"))] +mod poller; + #[cfg(feature = "demo_mode")] mod imports { use std::collections::HashMap; diff --git a/src/dbus/rauc/installer.rs b/src/dbus/rauc/installer.rs index d8c18361..f0301612 100644 --- a/src/dbus/rauc/installer.rs +++ b/src/dbus/rauc/installer.rs @@ -10,6 +10,11 @@ use zbus::proxy; default_path = "/" )] trait Installer { + /// GetArtifactStatus method + fn get_artifact_status( + &self, + ) -> zbus::Result>>; + /// GetPrimary method fn get_primary(&self) -> zbus::Result; diff --git a/src/dbus/rauc/poller.rs b/src/dbus/rauc/poller.rs new file mode 100644 index 00000000..70fdb3ba --- /dev/null +++ b/src/dbus/rauc/poller.rs @@ -0,0 +1,24 @@ +//! This code was generated by `zbus-xmlgen` `4.1.0` from DBus introspection data. +//! +//! By running `zbus-xmlgen system de.pengutronix.rauc /` on the LXA TAC. + +use zbus::proxy; + +#[proxy( + interface = "de.pengutronix.rauc.Poller", + default_service = "de.pengutronix.rauc", + default_path = "/" +)] +trait Poller { + /// Poll method + fn poll(&self) -> zbus::Result<()>; + + /// NextPoll property + #[zbus(property)] + fn next_poll(&self) -> zbus::Result; + + /// Status property + #[zbus(property)] + fn status(&self) + -> zbus::Result>; +} From 77aafa2af22bb812df0effec2d3a37f808af21ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 14:45:10 +0200 Subject: [PATCH 02/21] dbus: systemd: also monitor rauc.service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This exposes the status of the RAUC daemon to users inside and outside of the tacd. As of now neither of them exist, but in the next few commits support for re-starting the RAUC deamon to reload its configuration will be added. Signed-off-by: Leonard Göhrs --- openapi.yaml | 2 ++ src/dbus/systemd.rs | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/openapi.yaml b/openapi.yaml index 7451efcb..46589cc1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -697,6 +697,7 @@ paths: - network-manager - labgrid-exporter - lxa-iobus + - rauc put: summary: Perform an action on a systemd service tags: [System] @@ -722,6 +723,7 @@ paths: - network-manager - labgrid-exporter - lxa-iobus + - rauc get: summary: Get the status of a systemd service tags: [System] diff --git a/src/dbus/systemd.rs b/src/dbus/systemd.rs index e06f2dd8..ff78eb3d 100644 --- a/src/dbus/systemd.rs +++ b/src/dbus/systemd.rs @@ -66,6 +66,8 @@ pub struct Systemd { pub labgrid: Service, #[allow(dead_code)] pub iobus: Service, + #[allow(dead_code)] + pub rauc: Service, } impl ServiceStatus { @@ -238,6 +240,7 @@ impl Systemd { let networkmanager = Service::new(bb, "network-manager"); let labgrid = Service::new(bb, "labgrid-exporter"); let iobus = Service::new(bb, "lxa-iobus"); + let rauc = Service::new(bb, "rauc"); networkmanager .connect(wtb, conn.clone(), "NetworkManager.service") @@ -248,12 +251,14 @@ impl Systemd { iobus .connect(wtb, conn.clone(), "lxa-iobus.service") .await?; + rauc.connect(wtb, conn.clone(), "rauc.service").await?; Ok(Self { reboot, networkmanager, labgrid, iobus, + rauc, }) } } From a164327eb4853ecedce2aa9b11032ae687b3f11e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 09:59:19 +0200 Subject: [PATCH 03/21] dbus: rauc: add Channels type to use instead of Vec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows us to add methods like "get the primary channel", that operates on all update channels at once, to this `Channels` type later on. Signed-off-by: Leonard Göhrs --- src/dbus/rauc.rs | 12 ++--- src/dbus/rauc/update_channels.rs | 81 ++++++++++++++++++------------ src/motd.rs | 1 + src/ui/screens/diagnostics.rs | 2 +- src/ui/screens/update_available.rs | 7 +-- 5 files changed, 61 insertions(+), 42 deletions(-) diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs index 6d690843..db57da18 100644 --- a/src/dbus/rauc.rs +++ b/src/dbus/rauc.rs @@ -32,7 +32,7 @@ use crate::broker::{BrokerBuilder, Topic}; use crate::watched_tasks::WatchedTasksBuilder; mod update_channels; -pub use update_channels::Channel; +pub use update_channels::{Channel, Channels}; #[cfg(feature = "demo_mode")] mod demo_mode; @@ -124,7 +124,7 @@ pub struct Rauc { pub primary: Arc>, pub last_error: Arc>, pub install: Arc>, - pub channels: Arc>>, + pub channels: Arc>, pub reload: Arc>, pub should_reboot: Arc>, pub enable_polling: Arc>, @@ -203,7 +203,7 @@ fn would_reboot_into_other_slot(slot_status: &SlotStatus, primary: Option, enable_polling: Arc>, - channels: Arc>>, + channels: Arc>, slot_status: Arc>>, name: String, ) { @@ -213,7 +213,7 @@ async fn channel_polling_task( while let Some(mut channel) = channels .try_get() - .and_then(|chs| chs.into_iter().find(|ch| ch.name == name)) + .and_then(|chs| chs.into_vec().into_iter().find(|ch| ch.name == name)) { // Make sure update polling is enabled before doing anything, // as contacting the update server requires user consent. @@ -271,7 +271,7 @@ async fn channel_list_update_task( conn: Arc, mut reload_stream: Receiver, enable_polling: Arc>, - channels: Arc>>, + channels: Arc>, slot_status: Arc>>, ) -> Result<()> { let mut previous: Option = None; @@ -292,7 +292,7 @@ async fn channel_list_update_task( } // Read the list of available update channels - let new_channels = match Channel::from_directory(CHANNELS_DIR) { + let new_channels = match Channels::from_directory(CHANNELS_DIR) { Ok(chs) => chs, Err(e) => { warn!("Failed to get list of update channels: {e}"); diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs index 1a5fedd2..bb40d51a 100644 --- a/src/dbus/rauc/update_channels.rs +++ b/src/dbus/rauc/update_channels.rs @@ -54,6 +54,9 @@ pub struct Channel { pub bundle: Option, } +#[derive(Serialize, Deserialize, Clone, PartialEq)] +pub struct Channels(Vec); + #[derive(Deserialize)] pub struct ChannelFile { pub name: String, @@ -140,38 +143,6 @@ impl Channel { Ok(ch) } - pub(super) fn from_directory(dir: &str) -> Result> { - // Find all .yaml files in CHANNELS_DIR - let mut dir_entries: Vec = read_dir(dir)? - .filter_map(|dir_entry| dir_entry.ok()) - .filter(|dir_entry| { - dir_entry - .file_name() - .as_os_str() - .as_bytes() - .ends_with(b".yaml") - }) - .collect(); - - // And sort them alphabetically, so that 01_stable.yaml takes precedence over - // 05_testing.yaml. - dir_entries.sort_by_key(|dir_entry| dir_entry.file_name()); - - let mut channels: Vec = Vec::new(); - - for dir_entry in dir_entries { - let channel = Self::from_file(&dir_entry.path())?; - - if channels.iter().any(|ch| ch.name == channel.name) { - bail!("Encountered duplicate channel name \"{}\"", channel.name); - } - - channels.push(channel); - } - - Ok(channels) - } - fn update_enabled(&mut self) { // Which channels are enabled is decided based on which RAUC certificates are enabled. let cert_file = self.name.clone() + ".cert.pem"; @@ -206,6 +177,52 @@ impl Channel { } } +impl Channels { + pub(super) fn from_directory(dir: &str) -> Result { + // Find all .yaml files in CHANNELS_DIR + let mut dir_entries: Vec = read_dir(dir)? + .filter_map(|dir_entry| dir_entry.ok()) + .filter(|dir_entry| { + dir_entry + .file_name() + .as_os_str() + .as_bytes() + .ends_with(b".yaml") + }) + .collect(); + + // And sort them alphabetically, so that 01_stable.yaml takes precedence over + // 05_testing.yaml. + dir_entries.sort_by_key(|dir_entry| dir_entry.file_name()); + + let mut channels: Vec = Vec::new(); + + for dir_entry in dir_entries { + let channel = Channel::from_file(&dir_entry.path())?; + + if channels.iter().any(|ch| ch.name == channel.name) { + bail!("Encountered duplicate channel name \"{}\"", channel.name); + } + + channels.push(channel); + } + + Ok(Self(channels)) + } + + pub fn into_vec(self) -> Vec { + self.0 + } + + pub fn iter(&self) -> std::slice::Iter { + self.0.iter() + } + + pub fn iter_mut(&mut self) -> std::slice::IterMut { + self.0.iter_mut() + } +} + impl UpstreamBundle { fn new(compatible: String, version: String, slot_status: Option<&SlotStatus>) -> Self { let mut ub = Self { diff --git a/src/motd.rs b/src/motd.rs index c2990d3b..72202298 100644 --- a/src/motd.rs +++ b/src/motd.rs @@ -207,6 +207,7 @@ pub fn run( }, update = channels_events.recv().fuse() => { motd.rauc_update_urls = update? + .into_vec() .into_iter() .filter_map(|ch| { ch.bundle diff --git a/src/ui/screens/diagnostics.rs b/src/ui/screens/diagnostics.rs index 3fd71604..2ab8f639 100644 --- a/src/ui/screens/diagnostics.rs +++ b/src/ui/screens/diagnostics.rs @@ -130,7 +130,7 @@ fn diagnostic_text(ui: &Ui) -> Result { if let Some(channels) = ui.res.rauc.channels.try_get() { write!(&mut text, "chs: ")?; - for ch in channels { + for ch in channels.into_vec() { let en = if ch.enabled { "[x]" } else { "[ ]" }; let name = ch.name; diff --git a/src/ui/screens/update_available.rs b/src/ui/screens/update_available.rs index 4d5e50f6..887a55c1 100644 --- a/src/ui/screens/update_available.rs +++ b/src/ui/screens/update_available.rs @@ -30,7 +30,7 @@ use super::{ InputEvent, Screen, Ui, }; use crate::broker::Topic; -use crate::dbus::rauc::Channel; +use crate::dbus::rauc::{Channel, Channels}; use crate::watched_tasks::WatchedTasksBuilder; const SCREEN_TYPE: AlertScreen = AlertScreen::UpdateAvailable; @@ -73,8 +73,9 @@ impl Selection { !self.channels.is_empty() } - fn update_channels(&self, channels: Vec) -> Option { + fn update_channels(&self, channels: Channels) -> Option { let channels: Vec = channels + .into_vec() .into_iter() .filter(|ch| { ch.bundle @@ -143,7 +144,7 @@ impl UpdateAvailableScreen { pub fn new( wtb: &mut WatchedTasksBuilder, alerts: &Arc>, - channels: &Arc>>, + channels: &Arc>, ) -> Result { let (mut channels_events, _) = channels.clone().subscribe_unbounded(); let alerts = alerts.clone(); From 0833495804d26c8d6a80ae6ebaecd2f828e28249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 14:14:30 +0200 Subject: [PATCH 04/21] dbus: rauc: use a UpdateRequest object instead of a simple URL for install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right now the UpdateRequest object also only contains the URL, just like before, but in the future we will at least add a `manifest_hash` field that ensures that the user gets exactly the update bundle they agreed to install. The change is backwards-compatible. Incoming requests to the `/v1/tac/update/install` endpoint are first parsed as JSON object (the new UpdateRequest object) and if that fails as simple string (the old URL string) and then transformed transparently. Signed-off-by: Leonard Göhrs --- openapi.yaml | 12 +++++++--- src/dbus/rauc.rs | 35 +++++++++++++++++++++++++++--- src/ui/screens/update_available.rs | 14 ++++++++---- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 46589cc1..97c5cf29 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -827,12 +827,12 @@ paths: content: application/json: schema: - type: string + $ref: '#/components/schemas/UpdateRequest' responses: '204': - description: The value was parsed as string and will be tried + description: The value was parsed successfully and will be tried '400': - description: The value could not be parsed as string + description: The value could not be parsed /v1/tac/update/channels: get: @@ -1102,6 +1102,12 @@ components: nesting_depth: type: number + UpdateRequest: + type: object + properties: + url: + type: string + UpdateChannels: type: array items: diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs index db57da18..ebe5dfda 100644 --- a/src/dbus/rauc.rs +++ b/src/dbus/rauc.rs @@ -114,6 +114,30 @@ impl From<(i32, String, i32)> for Progress { } } +#[derive(Serialize, Deserialize, Clone)] +#[serde(from = "UpdateRequestDe")] +pub struct UpdateRequest { + pub url: Option, +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum UpdateRequestDe { + UrlObject { url: Option }, + UrlOnly(String), +} + +impl From for UpdateRequest { + fn from(de: UpdateRequestDe) -> Self { + // Provide API backward compatibility by allowing either just a String + // as argument or a map with url and manifest hash inside. + match de { + UpdateRequestDe::UrlObject { url } => Self { url }, + UpdateRequestDe::UrlOnly(url) => Self { url: Some(url) }, + } + } +} + type SlotStatus = HashMap>; pub struct Rauc { @@ -123,7 +147,7 @@ pub struct Rauc { #[cfg_attr(feature = "demo_mode", allow(dead_code))] pub primary: Arc>, pub last_error: Arc>, - pub install: Arc>, + pub install: Arc>, pub channels: Arc>, pub reload: Arc>, pub should_reboot: Arc>, @@ -336,7 +360,7 @@ impl Rauc { slot_status: bb.topic_ro("/v1/tac/update/slots", None), primary: bb.topic_ro("/v1/tac/update/primary", None), last_error: bb.topic_ro("/v1/tac/update/last_error", None), - install: bb.topic_wo("/v1/tac/update/install", Some("".to_string())), + install: bb.topic_wo("/v1/tac/update/install", None), channels: bb.topic_ro("/v1/tac/update/channels", None), reload: bb.topic_wo("/v1/tac/update/channels/reload", Some(true)), should_reboot: bb.topic_ro("/v1/tac/update/should_reboot", Some(false)), @@ -546,7 +570,12 @@ impl Rauc { wtb.spawn_task("rauc-forward-install", async move { let proxy = InstallerProxy::new(&conn_task).await.unwrap(); - while let Some(url) = install_stream.next().await { + while let Some(update_request) = install_stream.next().await { + let url = match update_request.url { + Some(url) => url, + None => continue, + }; + // Poor-mans validation. It feels wrong to let someone point to any // file on the TAC from the web interface. if url.starts_with("http://") || url.starts_with("https://") { diff --git a/src/ui/screens/update_available.rs b/src/ui/screens/update_available.rs index 887a55c1..6f125d0d 100644 --- a/src/ui/screens/update_available.rs +++ b/src/ui/screens/update_available.rs @@ -30,7 +30,7 @@ use super::{ InputEvent, Screen, Ui, }; use crate::broker::Topic; -use crate::dbus::rauc::{Channel, Channels}; +use crate::dbus::rauc::{Channel, Channels, UpdateRequest}; use crate::watched_tasks::WatchedTasksBuilder; const SCREEN_TYPE: AlertScreen = AlertScreen::UpdateAvailable; @@ -121,9 +121,15 @@ impl Selection { } } - fn perform(&self, alerts: &Arc>, install: &Arc>) { + fn perform(&self, alerts: &Arc>, install: &Arc>) { match self.highlight { - Highlight::Channel(ch) => install.set(self.channels[ch].url.clone()), + Highlight::Channel(ch) => { + let req = UpdateRequest { + url: Some(self.channels[ch].url.clone()), + }; + + install.set(req); + } Highlight::Dismiss => alerts.deassert(SCREEN_TYPE), } } @@ -136,7 +142,7 @@ pub struct UpdateAvailableScreen { struct Active { widgets: WidgetContainer, alerts: Arc>, - install: Arc>, + install: Arc>, selection: Arc>, } From b1d8baa0f5a6a7e9660480709a04e5f681781f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 15:22:32 +0200 Subject: [PATCH 05/21] dbus: rauc: allow restricting installation to a specific manifest_hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes sure that an user gets exact the bundle they intended to install, e.g. the bundle was not replaced by a newer one on the server or otherwise tampered with. Signed-off-by: Leonard Göhrs --- openapi.yaml | 2 ++ src/dbus/rauc.rs | 22 ++++++++++++++++++---- src/ui/screens/update_available.rs | 1 + 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 97c5cf29..d7b6b0d9 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1107,6 +1107,8 @@ components: properties: url: type: string + manifest_hash: + type: string UpdateChannels: type: array diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs index ebe5dfda..3f552912 100644 --- a/src/dbus/rauc.rs +++ b/src/dbus/rauc.rs @@ -117,13 +117,17 @@ impl From<(i32, String, i32)> for Progress { #[derive(Serialize, Deserialize, Clone)] #[serde(from = "UpdateRequestDe")] pub struct UpdateRequest { + pub manifest_hash: Option, pub url: Option, } #[derive(Deserialize)] #[serde(untagged)] enum UpdateRequestDe { - UrlObject { url: Option }, + UrlAndHash { + manifest_hash: Option, + url: Option, + }, UrlOnly(String), } @@ -132,8 +136,11 @@ impl From for UpdateRequest { // Provide API backward compatibility by allowing either just a String // as argument or a map with url and manifest hash inside. match de { - UpdateRequestDe::UrlObject { url } => Self { url }, - UpdateRequestDe::UrlOnly(url) => Self { url: Some(url) }, + UpdateRequestDe::UrlAndHash { manifest_hash, url } => Self { manifest_hash, url }, + UpdateRequestDe::UrlOnly(url) => Self { + manifest_hash: None, + url: Some(url), + }, } } } @@ -579,7 +586,14 @@ impl Rauc { // Poor-mans validation. It feels wrong to let someone point to any // file on the TAC from the web interface. if url.starts_with("http://") || url.starts_with("https://") { - let args = HashMap::new(); + let manifest_hash: Option = + update_request.manifest_hash.map(|mh| mh.into()); + + let mut args = HashMap::new(); + + if let Some(manifest_hash) = &manifest_hash { + args.insert("require-manifest-hash", manifest_hash); + } if let Err(e) = proxy.install_bundle(&url, args).await { error!("Failed to install bundle: {}", e); diff --git a/src/ui/screens/update_available.rs b/src/ui/screens/update_available.rs index 6f125d0d..637da48c 100644 --- a/src/ui/screens/update_available.rs +++ b/src/ui/screens/update_available.rs @@ -126,6 +126,7 @@ impl Selection { Highlight::Channel(ch) => { let req = UpdateRequest { url: Some(self.channels[ch].url.clone()), + manifest_hash: None, }; install.set(req); From 46cb8753c96c4cd21165562ea16eb1dc87db1691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 16:26:15 +0200 Subject: [PATCH 06/21] dbus: rauc: remove tacd-based update polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RAUC is in the process of adding native polling support, which we want to integrate into the tacd. To do the switch in a reviewable way first remove the tacd-based polling and then add the native support in separate commits. Signed-off-by: Leonard Göhrs --- src/broker/topic.rs | 12 -- src/dbus/rauc.rs | 189 +------------------------------ src/dbus/rauc/update_channels.rs | 92 --------------- 3 files changed, 3 insertions(+), 290 deletions(-) diff --git a/src/broker/topic.rs b/src/broker/topic.rs index 3a0c2c96..ef32e98a 100644 --- a/src/broker/topic.rs +++ b/src/broker/topic.rs @@ -336,18 +336,6 @@ impl Topic { self.modify(|prev| if prev != msg { msg } else { None }); } - - /// Wait until the topic is set to the specified value - pub async fn wait_for(self: &Arc, val: E) { - let (mut stream, sub) = self.clone().subscribe_unbounded(); - - // Unwrap here to keep the interface simple. The stream could only yield - // None if the sender side is dropped, which will not happen as we hold - // an Arc to self which contains the senders vec. - while stream.next().await.unwrap() != val {} - - sub.unsubscribe() - } } impl> Topic { diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs index 3f552912..3e09281d 100644 --- a/src/dbus/rauc.rs +++ b/src/dbus/rauc.rs @@ -15,15 +15,12 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -use std::cmp::Ordering; use std::collections::HashMap; -use std::time::{Duration, Instant}; use anyhow::Result; use async_std::channel::Receiver; use async_std::stream::StreamExt; use async_std::sync::Arc; -use async_std::task::{sleep, spawn, JoinHandle}; use log::warn; use serde::{Deserialize, Serialize}; @@ -48,38 +45,6 @@ mod poller; #[cfg(feature = "demo_mode")] mod imports { - use std::collections::HashMap; - - pub(super) struct InstallerProxy<'a> { - _dummy: &'a (), - } - - impl<'a> InstallerProxy<'a> { - pub async fn new(_conn: C) -> Option> { - Some(Self { _dummy: &() }) - } - - pub async fn inspect_bundle( - &self, - _source: &str, - _args: HashMap<&str, zbus::zvariant::Value<'_>>, - ) -> zbus::Result> { - let update: HashMap = [ - ( - "compatible".into(), - "Linux Automation GmbH - LXA TAC".into(), - ), - ("version".into(), "24.04-20240415070800".into()), - ] - .into(); - - let info: HashMap = - [("update".into(), update.into())].into(); - - Ok(info) - } - } - pub(super) const CHANNELS_DIR: &str = "demo_files/usr/share/tacd/update_channels"; } @@ -91,10 +56,6 @@ mod imports { pub(super) const CHANNELS_DIR: &str = "/usr/share/tacd/update_channels"; } -const RELOAD_RATE_LIMIT: Duration = Duration::from_secs(10 * 60); -const RETRY_INTERVAL_MIN: Duration = Duration::from_secs(60); -const RETRY_INTERVAL_MAX: Duration = Duration::from_secs(60 * 60); - use imports::*; #[derive(Serialize, Deserialize, Clone)] @@ -158,20 +119,10 @@ pub struct Rauc { pub channels: Arc>, pub reload: Arc>, pub should_reboot: Arc>, + #[allow(dead_code)] pub enable_polling: Arc>, } -fn compare_versions(v1: &str, v2: &str) -> Option { - // Version strings look something like this: "4.0-0-20230428214619" - // Use string sorting on the date part to determine which bundle is newer. - let date_1 = v1.rsplit_once('-').map(|(_, d)| d); - let date_2 = v2.rsplit_once('-').map(|(_, d)| d); - - // Return Sone if either version could not be split or a Some with the - // ordering between the dates. - date_1.zip(date_2).map(|(d1, d2)| d1.cmp(d2)) -} - #[cfg(not(feature = "demo_mode"))] fn would_reboot_into_other_slot(slot_status: &SlotStatus, primary: Option) -> Result { let rootfs_0 = slot_status.get("rootfs_0"); @@ -231,97 +182,15 @@ fn would_reboot_into_other_slot(slot_status: &SlotStatus, primary: Option, - enable_polling: Arc>, - channels: Arc>, - slot_status: Arc>>, - name: String, -) { - let proxy = InstallerProxy::new(&conn).await.unwrap(); - - let mut retry_interval = RETRY_INTERVAL_MIN; - - while let Some(mut channel) = channels - .try_get() - .and_then(|chs| chs.into_vec().into_iter().find(|ch| ch.name == name)) - { - // Make sure update polling is enabled before doing anything, - // as contacting the update server requires user consent. - enable_polling.wait_for(true).await; - - let polling_interval = channel.polling_interval; - let slot_status = slot_status.try_get(); - - if let Err(e) = channel.poll(&proxy, slot_status.as_deref()).await { - warn!( - "Failed to fetch update for update channel \"{}\": {}. Retrying in {}s.", - channel.name, - e, - retry_interval.as_secs() - ); - - if retry_interval < RETRY_INTERVAL_MAX { - sleep(retry_interval).await; - - // Perform a (limited) exponential backoff on the retry interval to recover - // fast from short-term issues while also preventing the update server from - // being DDOSed by excessive retries. - retry_interval *= 2; - - continue; - } - } - - retry_interval = RETRY_INTERVAL_MIN; - - channels.modify(|chs| { - let mut chs = chs?; - let channel_prev = chs.iter_mut().find(|ch| ch.name == name)?; - - // Check if the bundle we polled is the same as before and we don't need - // to send a message to the subscribers. - if *channel_prev == channel { - return None; - } - - // Update the channel description with the newly polled bundle info - *channel_prev = channel; - - Some(chs) - }); - - match polling_interval { - Some(pi) => sleep(pi).await, - None => break, - } - } -} - async fn channel_list_update_task( - conn: Arc, mut reload_stream: Receiver, - enable_polling: Arc>, channels: Arc>, - slot_status: Arc>>, ) -> Result<()> { - let mut previous: Option = None; - let mut polling_tasks: Vec> = Vec::new(); - while let Some(reload) = reload_stream.next().await { if !reload { continue; } - // Polling for updates is a somewhat expensive operation. - // Make sure it can not be abused to DOS the tacd. - if previous - .map(|p| p.elapsed() < RELOAD_RATE_LIMIT) - .unwrap_or(false) - { - continue; - } - // Read the list of available update channels let new_channels = match Channels::from_directory(CHANNELS_DIR) { Ok(chs) => chs, @@ -331,29 +200,7 @@ async fn channel_list_update_task( } }; - // Stop the currently running polling tasks - for task in polling_tasks.drain(..) { - task.cancel().await; - } - - let names: Vec = new_channels.iter().map(|c| c.name.clone()).collect(); - channels.set(new_channels); - - // Spawn new polling tasks. They will poll once immediately. - for name in names.into_iter() { - let polling_task = spawn(channel_polling_task( - conn.clone(), - enable_polling.clone(), - channels.clone(), - slot_status.clone(), - name, - )); - - polling_tasks.push(polling_task); - } - - previous = Some(Instant::now()); } Ok(()) @@ -398,13 +245,7 @@ impl Rauc { let (reload_stream, _) = inst.reload.clone().subscribe_unbounded(); wtb.spawn_task( "rauc-channel-list-update", - channel_list_update_task( - Arc::new(Connection), - reload_stream, - inst.enable_polling.clone(), - inst.channels.clone(), - inst.slot_status.clone(), - ), + channel_list_update_task(reload_stream, inst.channels.clone()), )?; Ok(inst) @@ -422,7 +263,6 @@ impl Rauc { let operation = inst.operation.clone(); let slot_status = inst.slot_status.clone(); let primary = inst.primary.clone(); - let channels = inst.channels.clone(); let should_reboot = inst.should_reboot.clone(); wtb.spawn_task("rauc-slot-status-update", async move { @@ -485,23 +325,6 @@ impl Rauc { }) .collect(); - // Update the `newer_than_installed` field for the upstream bundles inside - // of the update channels. - channels.modify(|prev| { - let prev = prev?; - - let mut new = prev.clone(); - - for ch in new.iter_mut() { - if let Some(bundle) = ch.bundle.as_mut() { - bundle.update_install(&slots); - } - } - - // Only send out messages if anything changed - (new != prev).then_some(new) - }); - // Provide a simple yes/no "should reboot into other slot?" information // based on the bundle versions in the booted slot and the other slot. match would_reboot_into_other_slot(&slots, new_primary) { @@ -608,13 +431,7 @@ impl Rauc { let (reload_stream, _) = inst.reload.clone().subscribe_unbounded(); wtb.spawn_task( "rauc-channel-list-update", - channel_list_update_task( - conn.clone(), - reload_stream, - inst.enable_polling.clone(), - inst.channels.clone(), - inst.slot_status.clone(), - ), + channel_list_update_task(reload_stream, inst.channels.clone()), )?; Ok(inst) diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs index bb40d51a..fcdb6937 100644 --- a/src/dbus/rauc/update_channels.rs +++ b/src/dbus/rauc/update_channels.rs @@ -15,7 +15,6 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -use std::collections::HashMap; use std::fs::{read_dir, read_to_string, DirEntry}; use std::os::unix::ffi::OsStrExt; use std::path::Path; @@ -24,8 +23,6 @@ use std::time::Duration; use anyhow::{anyhow, bail, Result}; use serde::{Deserialize, Serialize}; -use super::{compare_versions, InstallerProxy, SlotStatus}; - #[cfg(feature = "demo_mode")] const ENABLE_DIR: &str = "demo_files/etc/rauc/certificates-enabled"; @@ -66,28 +63,6 @@ pub struct ChannelFile { pub polling_interval: Option, } -fn zvariant_walk_nested_dicts(map: &zvariant::Dict, path: &[&str]) -> Result { - let (&key, rem) = path - .split_first() - .ok_or_else(|| anyhow!("Got an empty path to walk"))?; - - let value: &zvariant::Value = map - .get(&key)? - .ok_or_else(|| anyhow!("Could not find key \"{key}\" in dict"))?; - - if rem.is_empty() { - value.downcast_ref().map_err(|e| { - anyhow!("Failed to convert value in dictionary for key \"{key}\" to a string: {e}") - }) - } else { - let value = value.downcast_ref().map_err(|e| { - anyhow!("Failed to convert value in dictionary for key \"{key}\" to a dict: {e}") - })?; - - zvariant_walk_nested_dicts(value, rem) - } -} - impl Channel { fn from_file(path: &Path) -> Result { let file_name = || { @@ -150,31 +125,6 @@ impl Channel { self.enabled = cert_path.exists(); } - - /// Ask RAUC to determine the version of the bundle on the server - pub(super) async fn poll( - &mut self, - proxy: &InstallerProxy<'_>, - slot_status: Option<&SlotStatus>, - ) -> Result<()> { - self.update_enabled(); - - self.bundle = None; - - if self.enabled { - let args = HashMap::new(); - let bundle = proxy.inspect_bundle(&self.url, args).await?; - let bundle: zvariant::Dict = bundle.into(); - - let compatible = - zvariant_walk_nested_dicts(&bundle, &["update", "compatible"])?.to_owned(); - let version = zvariant_walk_nested_dicts(&bundle, &["update", "version"])?.to_owned(); - - self.bundle = Some(UpstreamBundle::new(compatible, version, slot_status)); - } - - Ok(()) - } } impl Channels { @@ -213,46 +163,4 @@ impl Channels { pub fn into_vec(self) -> Vec { self.0 } - - pub fn iter(&self) -> std::slice::Iter { - self.0.iter() - } - - pub fn iter_mut(&mut self) -> std::slice::IterMut { - self.0.iter_mut() - } -} - -impl UpstreamBundle { - fn new(compatible: String, version: String, slot_status: Option<&SlotStatus>) -> Self { - let mut ub = Self { - compatible, - version, - newer_than_installed: false, - }; - - if let Some(slot_status) = slot_status { - ub.update_install(slot_status); - } - - ub - } - - pub(super) fn update_install(&mut self, slot_status: &SlotStatus) { - let slot_0_is_older = slot_status - .get("rootfs_0") - .filter(|r| r.get("boot_status").is_some_and(|b| b == "good")) - .and_then(|r| r.get("bundle_version")) - .and_then(|v| compare_versions(&self.version, v).map(|c| c.is_gt())) - .unwrap_or(true); - - let slot_1_is_older = slot_status - .get("rootfs_1") - .filter(|r| r.get("boot_status").is_some_and(|b| b == "good")) - .and_then(|r| r.get("bundle_version")) - .and_then(|v| compare_versions(&self.version, v).map(|c| c.is_gt())) - .unwrap_or(true); - - self.newer_than_installed = slot_0_is_older && slot_1_is_older; - } } From 6a433336d8d13441f794b8349bd9f76d6559a532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 14:31:58 +0200 Subject: [PATCH 07/21] dbus: rauc: update_channels: add a concept of a single primary channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RAUC native update polling only supports a single update channel, while our native update polling did support multiple (all channels which RAUC would have accepted updates from, based on the enabled signing certificates, were polled for updates and the user was asked if they wanted to install updates from them). Prepare for the change by adding a concept of a single primary update channel. The primary channel is the first enabled one. Based on the channel definition file name. E.g. on production TACs these channel files are available: root@lxatac-00011:~# ls /usr/share/tacd/update_channels/ 01_stable.yaml 05_testing.yaml They are sorted by name when they are read from disk, so if both `stable.cert.pem` and `testing.cert.pem` are found in `/etc/rauc/certificates-enabled/`, then the stable channel will be the primary channel, but bundles from the testing channel may still be installed via the command line interface (e.g. to facilitate a channel switch). Signed-off-by: Leonard Göhrs --- openapi.yaml | 2 ++ src/dbus/rauc/update_channels.rs | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/openapi.yaml b/openapi.yaml index d7b6b0d9..eab9af01 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1132,6 +1132,8 @@ components: type: integer enabled: type: boolean + primary: + type: boolean bundle: type: object properties: diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs index fcdb6937..65cebba0 100644 --- a/src/dbus/rauc/update_channels.rs +++ b/src/dbus/rauc/update_channels.rs @@ -48,6 +48,7 @@ pub struct Channel { pub url: String, pub polling_interval: Option, pub enabled: bool, + pub primary: bool, pub bundle: Option, } @@ -110,6 +111,7 @@ impl Channel { url: channel_file.url.trim().to_string(), polling_interval, enabled: false, + primary: false, bundle: None, }; @@ -147,13 +149,21 @@ impl Channels { let mut channels: Vec = Vec::new(); + let mut have_primary = false; + for dir_entry in dir_entries { - let channel = Channel::from_file(&dir_entry.path())?; + let mut channel = Channel::from_file(&dir_entry.path())?; if channels.iter().any(|ch| ch.name == channel.name) { bail!("Encountered duplicate channel name \"{}\"", channel.name); } + // There can only be one primary channel. + // If multiple channels are enabled the primary one is the one with + // the highest precedence. + channel.primary = channel.enabled && !have_primary; + have_primary |= channel.primary; + channels.push(channel); } From 42b959fb71b165058fd69a40ca31aaea7dadae4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 10:36:00 +0200 Subject: [PATCH 08/21] dbus: rauc: only install bundles from the primary channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This restricts the sources that the `/v1/tac/update/install` will accept update requests from to only the primary channel. The web interface has not exposed the feature to install arbitrary URLs for some time now and users that want to do so are better served by using the command line interface instead. Signed-off-by: Leonard Göhrs --- src/dbus/rauc.rs | 47 ++++++++++++++++++++++---------- src/dbus/rauc/update_channels.rs | 5 ++++ 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs index 3e09281d..f3c281c7 100644 --- a/src/dbus/rauc.rs +++ b/src/dbus/rauc.rs @@ -394,6 +394,7 @@ impl Rauc { })?; let conn_task = conn.clone(); + let channels = inst.channels.clone(); let (mut install_stream, _) = inst.install.clone().subscribe_unbounded(); // Forward the "install" topic from the broker framework to RAUC @@ -401,26 +402,42 @@ impl Rauc { let proxy = InstallerProxy::new(&conn_task).await.unwrap(); while let Some(update_request) = install_stream.next().await { - let url = match update_request.url { - Some(url) => url, - None => continue, + let channels = match channels.try_get() { + Some(chs) => chs, + None => { + warn!("Got install request with no channels available yet"); + continue; + } }; - // Poor-mans validation. It feels wrong to let someone point to any - // file on the TAC from the web interface. - if url.starts_with("http://") || url.starts_with("https://") { - let manifest_hash: Option = - update_request.manifest_hash.map(|mh| mh.into()); - - let mut args = HashMap::new(); - - if let Some(manifest_hash) = &manifest_hash { - args.insert("require-manifest-hash", manifest_hash); + let primary = match channels.primary() { + Some(primary) => primary, + None => { + warn!("Got install request with no primary channel configured"); + continue; } + }; - if let Err(e) = proxy.install_bundle(&url, args).await { - error!("Failed to install bundle: {}", e); + let url = match &update_request.url { + None => &primary.url, + Some(url) if url == &primary.url => &primary.url, + Some(_) => { + warn!("Got install request with URL not matching primary channel URL"); + continue; } + }; + + let manifest_hash: Option = + update_request.manifest_hash.map(|mh| mh.into()); + + let mut args = HashMap::new(); + + if let Some(manifest_hash) = &manifest_hash { + args.insert("require-manifest-hash", manifest_hash); + } + + if let Err(e) = proxy.install_bundle(url, args).await { + error!("Failed to install bundle: {}", e); } } diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs index 65cebba0..6b4c961e 100644 --- a/src/dbus/rauc/update_channels.rs +++ b/src/dbus/rauc/update_channels.rs @@ -173,4 +173,9 @@ impl Channels { pub fn into_vec(self) -> Vec { self.0 } + + #[cfg(not(feature = "demo_mode"))] + pub(super) fn primary(&self) -> Option<&Channel> { + self.0.iter().find(|ch| ch.primary) + } } From 2b84818540ee844643e764f7be96061e6be33001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 10:34:36 +0200 Subject: [PATCH 09/21] dbus: rauc: system_conf: write runtime RAUC config with poll section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This configures RAUC to poll for updates on our behalf. We do not use the information yet or enable automatic installation but those are next steps. We also need to trigger RAUC to re-read the file for this to be useful. All of these features are added in follow-up commits. Signed-off-by: Leonard Göhrs --- src/dbus/rauc.rs | 49 +++++++++---- src/dbus/rauc/system_conf.rs | 120 +++++++++++++++++++++++++++++++ src/dbus/rauc/update_channels.rs | 1 - 3 files changed, 155 insertions(+), 15 deletions(-) create mode 100644 src/dbus/rauc/system_conf.rs diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs index f3c281c7..f32a34c5 100644 --- a/src/dbus/rauc.rs +++ b/src/dbus/rauc.rs @@ -18,9 +18,9 @@ use std::collections::HashMap; use anyhow::Result; -use async_std::channel::Receiver; use async_std::stream::StreamExt; use async_std::sync::Arc; +use futures_util::FutureExt; use log::warn; use serde::{Deserialize, Serialize}; @@ -31,6 +31,9 @@ use crate::watched_tasks::WatchedTasksBuilder; mod update_channels; pub use update_channels::{Channel, Channels}; +mod system_conf; +use system_conf::update_system_conf; + #[cfg(feature = "demo_mode")] mod demo_mode; @@ -119,7 +122,6 @@ pub struct Rauc { pub channels: Arc>, pub reload: Arc>, pub should_reboot: Arc>, - #[allow(dead_code)] pub enable_polling: Arc>, } @@ -183,13 +185,26 @@ fn would_reboot_into_other_slot(slot_status: &SlotStatus, primary: Option, + reload: Arc>, + enable_polling: Arc>, channels: Arc>, ) -> Result<()> { - while let Some(reload) = reload_stream.next().await { - if !reload { - continue; - } + let (reload_stream, _) = reload.subscribe_unbounded(); + let (mut enable_polling_stream, _) = enable_polling.subscribe_unbounded(); + + let mut enable_polling = enable_polling_stream.next().await.unwrap_or(false); + + 'reload_loop: loop { + futures::select! { + reload = reload_stream.recv().fuse() => { + if !(reload?) { + continue 'reload_loop + } + } + enable_polling_new = enable_polling_stream.recv().fuse() => { + enable_polling = enable_polling_new?; + } + }; // Read the list of available update channels let new_channels = match Channels::from_directory(CHANNELS_DIR) { @@ -200,10 +215,10 @@ async fn channel_list_update_task( } }; + update_system_conf(new_channels.primary(), enable_polling)?; + channels.set(new_channels); } - - Ok(()) } impl Rauc { @@ -242,10 +257,13 @@ impl Rauc { inst.last_error.set("".to_string()); // Reload the channel list on request - let (reload_stream, _) = inst.reload.clone().subscribe_unbounded(); wtb.spawn_task( "rauc-channel-list-update", - channel_list_update_task(reload_stream, inst.channels.clone()), + channel_list_update_task( + inst.reload.clone(), + inst.enable_polling.clone(), + inst.channels.clone(), + ), )?; Ok(inst) @@ -444,11 +462,14 @@ impl Rauc { Ok(()) })?; - // Reload the channel list on request - let (reload_stream, _) = inst.reload.clone().subscribe_unbounded(); + // Reload the channel list when required wtb.spawn_task( "rauc-channel-list-update", - channel_list_update_task(reload_stream, inst.channels.clone()), + channel_list_update_task( + inst.reload.clone(), + inst.enable_polling.clone(), + inst.channels.clone(), + ), )?; Ok(inst) diff --git a/src/dbus/rauc/system_conf.rs b/src/dbus/rauc/system_conf.rs new file mode 100644 index 00000000..627df255 --- /dev/null +++ b/src/dbus/rauc/system_conf.rs @@ -0,0 +1,120 @@ +use std::fmt::Write; +use std::fs::{create_dir_all, read_to_string, remove_file, rename, write}; +use std::io::{Error, ErrorKind}; +use std::path::Path; + +use super::Channel; + +use log::info; + +#[cfg(feature = "demo_mode")] +mod imports { + pub(super) const STATIC_CONF_PATH: &str = "demo_files/usr/lib/rauc/system.conf"; + pub(super) const DYNAMIC_CONF_PATH: &str = "demo_files/run/rauc/system.conf"; +} + +#[cfg(not(feature = "demo_mode"))] +mod imports { + pub(super) const STATIC_CONF_PATH: &str = "/usr/lib/rauc/system.conf"; + pub(super) const DYNAMIC_CONF_PATH: &str = "/run/rauc/system.conf"; +} + +use imports::*; + +const MAGIC_LINE: &str = "\n# \n"; + +fn poll_section( + primary_channel: Option<&Channel>, + polling: bool, +) -> Result, std::fmt::Error> { + // If no primary channel is configured or if polling is not enabled, + // then we do not need a `[poll]` section at all. + let primary_channel = match (primary_channel, polling) { + (Some(pc), true) => pc, + _ => return Ok(None), + }; + + let mut section = String::new(); + + writeln!(&mut section)?; + writeln!(&mut section, "[poll]")?; + writeln!(&mut section, "source={}", primary_channel.url)?; + + if let Some(interval) = primary_channel.polling_interval { + writeln!(&mut section, "interval-sec={}", interval.as_secs())?; + } + + writeln!(&mut section, "candidate-criteria=different-version")?; + + Ok(Some(section)) +} + +pub fn update_system_conf( + primary_channel: Option<&Channel>, + enable_polling: bool, +) -> std::io::Result { + let dynamic_conf = { + match poll_section(primary_channel, enable_polling) { + Ok(Some(ps)) => { + // We use the config in /etc as a template ... + let static_conf = read_to_string(STATIC_CONF_PATH)?; + + // ... and replace the line `# ` with our + // generated `[poll]` section. + let dc = static_conf.replacen(MAGIC_LINE, &ps, 1); + + // The user may have decided not to include a `# ` + // line. In that case we do not need a dynamic config at all. + if dc == static_conf { + info!( + "Rauc config {} did not contain magic line '{}'. Will not generate poll section.", + STATIC_CONF_PATH, MAGIC_LINE + ); + + None + } else { + Some(dc) + } + } + _ => None, + } + }; + + /* Do we need a dynamic config in /run/rauc? + * + * If so, then is it actually different from what we already have? + * If not, then there is no need to restart the daemon. + * If it is, we write the new one and signal the need for a daemon + * restart. + * + * If we do not need dynamic config, then try to delete the previous one. + * If there was none, then the daemon does not have to be restarted. + * If there was a dynamic config before, then we need to restart the + * daemon. + */ + match dynamic_conf { + Some(new) => match read_to_string(DYNAMIC_CONF_PATH) { + Ok(old) if old == new => Ok(false), + Err(err) if err.kind() != ErrorKind::NotFound => Err(err), + Ok(_) | Err(_) => { + let dynamic_conf_dir = Path::new(DYNAMIC_CONF_PATH) + .parent() + .ok_or_else(|| Error::other("Invalid dynamic config path"))?; + + let tmp_path = dynamic_conf_dir.join("system.conf.tacd-tmp"); + + create_dir_all(dynamic_conf_dir)?; + + write(&tmp_path, &new)?; + rename(&tmp_path, DYNAMIC_CONF_PATH)?; + + Ok(true) + } + }, + None => match remove_file(DYNAMIC_CONF_PATH) { + Ok(_) => Ok(true), + Err(err) if err.kind() == ErrorKind::NotFound => Ok(false), + Err(err) => Err(err), + }, + } +} diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs index 6b4c961e..e5da5c1c 100644 --- a/src/dbus/rauc/update_channels.rs +++ b/src/dbus/rauc/update_channels.rs @@ -174,7 +174,6 @@ impl Channels { self.0 } - #[cfg(not(feature = "demo_mode"))] pub(super) fn primary(&self) -> Option<&Channel> { self.0.iter().find(|ch| ch.primary) } From 008b8839b5d7b6afd2b359f5c5d7186da8ce5e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 09:42:54 +0200 Subject: [PATCH 10/21] dbus: rauc: reload rauc daemon when required MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This uses the existing systemd DBus API integration to trigger a daemon restart (there is no support to just reload the config as of now) and keep track of the following state changes. Signed-off-by: Leonard Göhrs --- src/dbus.rs | 6 ++++-- src/dbus/rauc.rs | 44 ++++++++++++++++++++++++++++++++++++++++++-- src/dbus/systemd.rs | 1 - 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/dbus.rs b/src/dbus.rs index aa8e333c..326c02a0 100644 --- a/src/dbus.rs +++ b/src/dbus.rs @@ -89,11 +89,13 @@ impl DbusSession { let conn = Arc::new(tacd.serve(conn_builder).build().await?); + let systemd = Systemd::new(bb, wtb, &conn).await?; + Ok(Self { hostname: Hostname::new(bb, wtb, &conn)?, network: Network::new(bb, wtb, &conn, led_dut, led_uplink)?, - rauc: Rauc::new(bb, wtb, &conn)?, - systemd: Systemd::new(bb, wtb, &conn).await?, + rauc: Rauc::new(bb, wtb, &conn, systemd.rauc.clone())?, + systemd, }) } } diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs index f32a34c5..28e9b260 100644 --- a/src/dbus/rauc.rs +++ b/src/dbus/rauc.rs @@ -21,9 +21,10 @@ use anyhow::Result; use async_std::stream::StreamExt; use async_std::sync::Arc; use futures_util::FutureExt; -use log::warn; +use log::{info, warn}; use serde::{Deserialize, Serialize}; +use super::systemd::{Service, ServiceAction}; use super::Connection; use crate::broker::{BrokerBuilder, Topic}; use crate::watched_tasks::WatchedTasksBuilder; @@ -188,6 +189,7 @@ async fn channel_list_update_task( reload: Arc>, enable_polling: Arc>, channels: Arc>, + rauc_service: Service, ) -> Result<()> { let (reload_stream, _) = reload.subscribe_unbounded(); let (mut enable_polling_stream, _) = enable_polling.subscribe_unbounded(); @@ -215,9 +217,43 @@ async fn channel_list_update_task( } }; - update_system_conf(new_channels.primary(), enable_polling)?; + let should_reload = update_system_conf(new_channels.primary(), enable_polling)?; channels.set(new_channels); + + if should_reload { + info!("New RAUC config written. Triggering daemon restart."); + + let (mut status, status_subscription) = + rauc_service.status.clone().subscribe_unbounded(); + rauc_service.action.set(ServiceAction::Restart); + + info!("Waiting for daemon to go down"); + + while let Some(ev) = status.next().await { + info!("Current status: {} ({})", ev.active_state, ev.sub_state); + + if ev.active_state != "active" { + break; + } + } + + info!("Waiting for daemon to come up again"); + + while let Some(ev) = status.next().await { + info!("Current status: {} ({})", ev.active_state, ev.sub_state); + + if ev.active_state == "active" { + break; + } + } + + info!("Done"); + + status_subscription.unsubscribe(); + } else { + info!("Config is up to date. Will not reload."); + } } } @@ -249,6 +285,7 @@ impl Rauc { bb: &mut BrokerBuilder, wtb: &mut WatchedTasksBuilder, _conn: &Arc, + rauc_service: Service, ) -> Result { let inst = Self::setup_topics(bb); @@ -263,6 +300,7 @@ impl Rauc { inst.reload.clone(), inst.enable_polling.clone(), inst.channels.clone(), + rauc_service, ), )?; @@ -274,6 +312,7 @@ impl Rauc { bb: &mut BrokerBuilder, wtb: &mut WatchedTasksBuilder, conn: &Arc, + rauc_service: Service, ) -> Result { let inst = Self::setup_topics(bb); @@ -469,6 +508,7 @@ impl Rauc { inst.reload.clone(), inst.enable_polling.clone(), inst.channels.clone(), + rauc_service, ), )?; diff --git a/src/dbus/systemd.rs b/src/dbus/systemd.rs index ff78eb3d..2f5f72ea 100644 --- a/src/dbus/systemd.rs +++ b/src/dbus/systemd.rs @@ -66,7 +66,6 @@ pub struct Systemd { pub labgrid: Service, #[allow(dead_code)] pub iobus: Service, - #[allow(dead_code)] pub rauc: Service, } From 632685f71cc76c4fba4a5acf9fbf0ac25476212d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Thu, 3 Apr 2025 08:59:22 +0200 Subject: [PATCH 11/21] dbus: rauc: trigger a single poll for updates after reloading the daemon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Leonard Göhrs --- src/dbus/rauc.rs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs index 28e9b260..080a532f 100644 --- a/src/dbus/rauc.rs +++ b/src/dbus/rauc.rs @@ -21,7 +21,7 @@ use anyhow::Result; use async_std::stream::StreamExt; use async_std::sync::Arc; use futures_util::FutureExt; -use log::{info, warn}; +use log::{error, info, warn}; use serde::{Deserialize, Serialize}; use super::systemd::{Service, ServiceAction}; @@ -47,15 +47,31 @@ use installer::InstallerProxy; #[cfg(not(feature = "demo_mode"))] mod poller; +#[cfg(not(feature = "demo_mode"))] +use poller::PollerProxy; + #[cfg(feature = "demo_mode")] mod imports { pub(super) const CHANNELS_DIR: &str = "demo_files/usr/share/tacd/update_channels"; + + pub(super) struct PollerProxy<'a> { + _dummy: &'a (), + } + + impl PollerProxy<'_> { + pub async fn new(_conn: C) -> Option { + Some(Self { _dummy: &() }) + } + + pub async fn poll(&self) -> zbus::Result<()> { + Ok(()) + } + } } #[cfg(not(feature = "demo_mode"))] mod imports { pub(super) use anyhow::bail; - pub(super) use log::error; pub(super) const CHANNELS_DIR: &str = "/usr/share/tacd/update_channels"; } @@ -186,11 +202,14 @@ fn would_reboot_into_other_slot(slot_status: &SlotStatus, primary: Option, reload: Arc>, enable_polling: Arc>, channels: Arc>, rauc_service: Service, ) -> Result<()> { + let poller = PollerProxy::new(&conn).await.unwrap(); + let (reload_stream, _) = reload.subscribe_unbounded(); let (mut enable_polling_stream, _) = enable_polling.subscribe_unbounded(); @@ -254,6 +273,14 @@ async fn channel_list_update_task( } else { info!("Config is up to date. Will not reload."); } + + if enable_polling { + info!("Trigger a poll"); + + if let Err(err) = poller.poll().await { + error!("Failed to poll for updates: {err}"); + } + } } } @@ -297,6 +324,7 @@ impl Rauc { wtb.spawn_task( "rauc-channel-list-update", channel_list_update_task( + Arc::new(Connection), inst.reload.clone(), inst.enable_polling.clone(), inst.channels.clone(), @@ -505,6 +533,7 @@ impl Rauc { wtb.spawn_task( "rauc-channel-list-update", channel_list_update_task( + conn.clone(), inst.reload.clone(), inst.enable_polling.clone(), inst.channels.clone(), From 0ab21a3c075fb0f1a9c2e97ebf78ea3c281d2866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 10:32:28 +0200 Subject: [PATCH 12/21] dbus: rauc: forward poller status to broker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RAUC native polling provides us with information about the recent poll attempts. This includes information about the bundle version and wether it is an update over what is currently running on the device. In other words: it gives us everything we need to show update notifications again. Forward this information to the same places we used with the tacd-based update polling. Signed-off-by: Leonard Göhrs --- src/dbus/rauc.rs | 50 ++++++++++++++++++++++++ src/dbus/rauc/update_channels.rs | 66 ++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs index 080a532f..8c66c3fe 100644 --- a/src/dbus/rauc.rs +++ b/src/dbus/rauc.rs @@ -478,6 +478,56 @@ impl Rauc { Ok(()) })?; + let conn_task = conn.clone(); + let channels = inst.channels.clone(); + + // Forward the "Poller::status" property to the broker framework + wtb.spawn_task("rauc-forward-poller-status", async move { + let proxy = PollerProxy::new(&conn_task).await.unwrap(); + + let mut stream = proxy.receive_status_changed().await; + + if let Ok(status) = proxy.status().await { + channels.modify(|chs| { + let mut chs = chs?; + + match chs.update_from_poll_status(status.into()) { + Ok(true) => Some(chs), + Ok(false) => None, + Err(e) => { + warn!("Could not update channel list from poll status: {e}"); + None + } + } + }); + } + + while let Some(status) = stream.next().await { + let status = match status.get().await { + Ok(status) => status, + Err(e) => { + warn!("Could not get poll status: {e}"); + continue; + } + }; + + channels.modify(|chs| { + let mut chs = chs?; + + match chs.update_from_poll_status(status.into()) { + Ok(true) => Some(chs), + Ok(false) => None, + Err(e) => { + warn!("Could not update channel list from poll status: {e}"); + None + } + } + }); + } + + Ok(()) + })?; + let conn_task = conn.clone(); let channels = inst.channels.clone(); let (mut install_stream, _) = inst.install.clone().subscribe_unbounded(); diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs index e5da5c1c..460fc363 100644 --- a/src/dbus/rauc/update_channels.rs +++ b/src/dbus/rauc/update_channels.rs @@ -15,6 +15,8 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +#[cfg(not(feature = "demo_mode"))] +use std::convert::TryFrom; use std::fs::{read_dir, read_to_string, DirEntry}; use std::os::unix::ffi::OsStrExt; use std::path::Path; @@ -64,6 +66,34 @@ pub struct ChannelFile { pub polling_interval: Option, } +#[cfg(not(feature = "demo_mode"))] +fn zvariant_walk_nested_dicts<'a, T>(map: &'a zvariant::Dict, path: &'a [&'a str]) -> Result<&'a T> +where + &'a T: TryFrom<&'a zvariant::Value<'a>>, + <&'a T as TryFrom<&'a zvariant::Value<'a>>>::Error: Into, +{ + let (key, rem) = path + .split_first() + .ok_or_else(|| anyhow!("Got an empty path to walk"))?; + + let value: &zvariant::Value = map + .get(key)? + .ok_or_else(|| anyhow!("Could not find key \"{key}\" in dict"))?; + + if rem.is_empty() { + value.downcast_ref().map_err(|e| { + let type_name = std::any::type_name::(); + anyhow!("Failed to convert value in dictionary for key \"{key}\" to {type_name}: {e}") + }) + } else { + let value = value.downcast_ref().map_err(|e| { + anyhow!("Failed to convert value in dictionary for key \"{key}\" to a dict: {e}") + })?; + + zvariant_walk_nested_dicts(value, rem) + } +} + impl Channel { fn from_file(path: &Path) -> Result { let file_name = || { @@ -177,4 +207,40 @@ impl Channels { pub(super) fn primary(&self) -> Option<&Channel> { self.0.iter().find(|ch| ch.primary) } + + #[cfg(not(feature = "demo_mode"))] + fn primary_mut(&mut self) -> Option<&mut Channel> { + self.0.iter_mut().find(|ch| ch.primary) + } + + #[cfg(not(feature = "demo_mode"))] + pub(super) fn update_from_poll_status(&mut self, poll_status: zvariant::Dict) -> Result { + let compatible: &zvariant::Str = + zvariant_walk_nested_dicts(&poll_status, &["manifest", "update", "compatible"])?; + let version: &zvariant::Str = + zvariant_walk_nested_dicts(&poll_status, &["manifest", "update", "version"])?; + let newer_than_installed: &bool = + zvariant_walk_nested_dicts(&poll_status, &["update-available"])?; + + if let Some(pb) = self.0.iter().find_map(|ch| ch.bundle.as_ref()) { + if compatible == pb.compatible.as_str() + && version == pb.version.as_str() + && *newer_than_installed == pb.newer_than_installed + { + return Ok(false); + } + } + + self.0.iter_mut().for_each(|ch| ch.bundle = None); + + if let Some(primary) = self.primary_mut() { + primary.bundle = Some(UpstreamBundle { + compatible: compatible.as_str().into(), + version: version.as_str().into(), + newer_than_installed: *newer_than_installed, + }); + } + + Ok(true) + } } From 911e4895dff86b2889dded82f039aaca55eb038c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 14:35:33 +0200 Subject: [PATCH 13/21] dbus: rauc: add manifest_hash und effective_url to UpstreamBundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The RAUC native polling interface provides more information than just the basic `compatible` and `version` fields. Among these extra informations are the following: - `manifest_hash` By using the `manifest_hash` in the `InstallBundle` call we can (cryptographically) ensure that the exact bundle (content) that the user agreed to install is actually being installed and that no switch has happened in between. - `effective_url` This is the bundle URL after all HTTP redirects have been followed. This is e.g. relevant when a "clever" update server is used that redirects poll requests to specific bundles to e.g. implement staged rollouts or prevents updates from incompatible bundle versions. By using this one can ensure that the bundle URL used matches the `manifest_hash` provided and that the redirects did not change (e.g. because the next step in a staged update was reached) between the last poll and the user accepting an update. The update dialog on the LCD is updated to use this mechanism now, while the web interface will be updated later. Signed-off-by: Leonard Göhrs --- openapi.yaml | 4 ++++ src/dbus/rauc.rs | 13 +++++++++++-- src/dbus/rauc/update_channels.rs | 10 ++++++++++ src/ui/screens/update_available.rs | 15 ++++++++++----- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index eab9af01..da1e47b7 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1141,6 +1141,10 @@ components: type: string version: type: string, + manifest_hash: + type: string, + effective_url: + type: string, newer_than_installed: type: boolean diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs index 8c66c3fe..92190431 100644 --- a/src/dbus/rauc.rs +++ b/src/dbus/rauc.rs @@ -553,11 +553,20 @@ impl Rauc { } }; + let upstream_bundle = match primary.bundle.as_ref() { + Some(us) => us, + None => { + warn!("Got install request with no upstream bundle info available yet"); + continue + } + }; + let url = match &update_request.url { - None => &primary.url, + None => &upstream_bundle.effective_url, + Some(url) if url == &upstream_bundle.effective_url => &upstream_bundle.effective_url, Some(url) if url == &primary.url => &primary.url, Some(_) => { - warn!("Got install request with URL not matching primary channel URL"); + warn!("Got install request with URL matching neither channel URL nor effective bundle URL"); continue; } }; diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs index 460fc363..9790a3c8 100644 --- a/src/dbus/rauc/update_channels.rs +++ b/src/dbus/rauc/update_channels.rs @@ -39,6 +39,8 @@ const ONE_DAY: Duration = Duration::from_secs(24 * 60 * 60); pub struct UpstreamBundle { pub compatible: String, pub version: String, + pub manifest_hash: String, + pub effective_url: String, pub newer_than_installed: bool, } @@ -219,12 +221,18 @@ impl Channels { zvariant_walk_nested_dicts(&poll_status, &["manifest", "update", "compatible"])?; let version: &zvariant::Str = zvariant_walk_nested_dicts(&poll_status, &["manifest", "update", "version"])?; + let manifest_hash: &zvariant::Str = + zvariant_walk_nested_dicts(&poll_status, &["manifest", "manifest-hash"])?; + let effective_url: &zvariant::Str = + zvariant_walk_nested_dicts(&poll_status, &["bundle", "effective-url"])?; let newer_than_installed: &bool = zvariant_walk_nested_dicts(&poll_status, &["update-available"])?; if let Some(pb) = self.0.iter().find_map(|ch| ch.bundle.as_ref()) { if compatible == pb.compatible.as_str() && version == pb.version.as_str() + && manifest_hash == pb.manifest_hash.as_str() + && effective_url == pb.effective_url.as_str() && *newer_than_installed == pb.newer_than_installed { return Ok(false); @@ -237,6 +245,8 @@ impl Channels { primary.bundle = Some(UpstreamBundle { compatible: compatible.as_str().into(), version: version.as_str().into(), + manifest_hash: manifest_hash.as_str().into(), + effective_url: effective_url.as_str().into(), newer_than_installed: *newer_than_installed, }); } diff --git a/src/ui/screens/update_available.rs b/src/ui/screens/update_available.rs index 637da48c..dddd34ea 100644 --- a/src/ui/screens/update_available.rs +++ b/src/ui/screens/update_available.rs @@ -22,6 +22,7 @@ use async_trait::async_trait; use embedded_graphics::{ mono_font::MonoTextStyle, pixelcolor::BinaryColor, prelude::*, text::Text, }; +use log::error; use serde::{Deserialize, Serialize}; use super::widgets::*; @@ -124,12 +125,16 @@ impl Selection { fn perform(&self, alerts: &Arc>, install: &Arc>) { match self.highlight { Highlight::Channel(ch) => { - let req = UpdateRequest { - url: Some(self.channels[ch].url.clone()), - manifest_hash: None, - }; + if let Some(bundle) = &self.channels[ch].bundle { + let req = UpdateRequest { + url: Some(bundle.effective_url.clone()), + manifest_hash: Some(bundle.manifest_hash.clone()), + }; - install.set(req); + install.set(req); + } else { + error!("Update channel is missing upstream bundle information."); + }; } Highlight::Dismiss => alerts.deassert(SCREEN_TYPE), } From f6d6b462acb2b9df2a54bc22d2cc159567e6a052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 09:52:55 +0200 Subject: [PATCH 14/21] dbus: rauc: add support for enabling the auto install feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automatic installation and boot of updates can be useful when managing a fleet of devices. This is however a feature that requires strict user consent, hence why it is off by default. Add backend-support for enabling this feature. Frontent support in the web interface will be added later. We always enable auto-reboot together with auto-install, since the migration scripts only run once at the end of the installation. A system that is updated, but not rebooted, would thus accumulate changes that are not migrated to the other slot. Signed-off-by: Leonard Göhrs --- openapi.yaml | 15 +++++++++++++++ src/dbus/rauc.rs | 20 +++++++++++++++++++- src/dbus/rauc/system_conf.rs | 13 ++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index da1e47b7..d8f247ec 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -775,6 +775,21 @@ paths: '400': description: The value could not be parsed as boolean + /v1/tac/update/enable_auto_install: + put: + summary: Enable automatic installation of operating system updates + tags: [Updating] + requestBody: + content: + application/json: + schema: + type: boolean + responses: + '204': + description: Automatic installation of updates was enabled/disabled + '400': + description: The value could not be parsed as boolean + /v1/tac/update/operation: get: summary: Get the currently running system update operation diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs index 92190431..c8ca2ab2 100644 --- a/src/dbus/rauc.rs +++ b/src/dbus/rauc.rs @@ -140,6 +140,7 @@ pub struct Rauc { pub reload: Arc>, pub should_reboot: Arc>, pub enable_polling: Arc>, + pub enable_auto_install: Arc>, } #[cfg(not(feature = "demo_mode"))] @@ -205,6 +206,7 @@ async fn channel_list_update_task( conn: Arc, reload: Arc>, enable_polling: Arc>, + enable_auto_install: Arc>, channels: Arc>, rauc_service: Service, ) -> Result<()> { @@ -212,8 +214,10 @@ async fn channel_list_update_task( let (reload_stream, _) = reload.subscribe_unbounded(); let (mut enable_polling_stream, _) = enable_polling.subscribe_unbounded(); + let (mut enable_auto_install_stream, _) = enable_auto_install.subscribe_unbounded(); let mut enable_polling = enable_polling_stream.next().await.unwrap_or(false); + let mut enable_auto_install = enable_auto_install_stream.next().await.unwrap_or(false); 'reload_loop: loop { futures::select! { @@ -225,6 +229,9 @@ async fn channel_list_update_task( enable_polling_new = enable_polling_stream.recv().fuse() => { enable_polling = enable_polling_new?; } + enable_auto_install_new = enable_auto_install_stream.recv().fuse() => { + enable_auto_install = enable_auto_install_new?; + } }; // Read the list of available update channels @@ -236,7 +243,8 @@ async fn channel_list_update_task( } }; - let should_reload = update_system_conf(new_channels.primary(), enable_polling)?; + let should_reload = + update_system_conf(new_channels.primary(), enable_polling, enable_auto_install)?; channels.set(new_channels); @@ -304,6 +312,14 @@ impl Rauc { Some(false), 1, ), + enable_auto_install: bb.topic( + "/v1/tac/update/enable_auto_install", + true, + true, + true, + Some(false), + 1, + ), } } @@ -327,6 +343,7 @@ impl Rauc { Arc::new(Connection), inst.reload.clone(), inst.enable_polling.clone(), + inst.enable_auto_install.clone(), inst.channels.clone(), rauc_service, ), @@ -595,6 +612,7 @@ impl Rauc { conn.clone(), inst.reload.clone(), inst.enable_polling.clone(), + inst.enable_auto_install.clone(), inst.channels.clone(), rauc_service, ), diff --git a/src/dbus/rauc/system_conf.rs b/src/dbus/rauc/system_conf.rs index 627df255..91f62acb 100644 --- a/src/dbus/rauc/system_conf.rs +++ b/src/dbus/rauc/system_conf.rs @@ -26,6 +26,7 @@ const MAGIC_LINE: &str = "\n# \n"; fn poll_section( primary_channel: Option<&Channel>, polling: bool, + auto_install: bool, ) -> Result, std::fmt::Error> { // If no primary channel is configured or if polling is not enabled, // then we do not need a `[poll]` section at all. @@ -46,15 +47,25 @@ fn poll_section( writeln!(&mut section, "candidate-criteria=different-version")?; + if auto_install { + writeln!(&mut section, "install-criteria=different-version")?; + writeln!( + &mut section, + "reboot-criteria=updated-slots;updated-artifacts" + )?; + writeln!(&mut section, "reboot-cmd=systemctl reboot")?; + } + Ok(Some(section)) } pub fn update_system_conf( primary_channel: Option<&Channel>, enable_polling: bool, + enable_auto_install: bool, ) -> std::io::Result { let dynamic_conf = { - match poll_section(primary_channel, enable_polling) { + match poll_section(primary_channel, enable_polling, enable_auto_install) { Ok(Some(ps)) => { // We use the config in /etc as a template ... let static_conf = read_to_string(STATIC_CONF_PATH)?; From 79f970f23e792eb57ec2bd0d29336e86b74dff7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Mon, 31 Mar 2025 15:17:59 +0200 Subject: [PATCH 15/21] dbus: rauc: implement forced polling via update channel files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users managing a fleet of devices with custom-built update bundles and update channel may want to automatically enable update polling and automatic installation of updates without having to do so explicitly via the web ui. (At least we at Pengutronix do). Enable this usecase by adding optional `force_polling` and `force_auto_install` config options to the update channel definition files. Signed-off-by: Leonard Göhrs --- openapi.yaml | 4 ++++ src/dbus/rauc/system_conf.rs | 15 ++++++++++++++- src/dbus/rauc/update_channels.rs | 6 ++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/openapi.yaml b/openapi.yaml index d8f247ec..c5efb2b3 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1162,6 +1162,10 @@ components: type: string, newer_than_installed: type: boolean + force_polling: + type: boolean + force_auto_install: + type: boolean ServiceStatus: type: object diff --git a/src/dbus/rauc/system_conf.rs b/src/dbus/rauc/system_conf.rs index 91f62acb..e387cb7e 100644 --- a/src/dbus/rauc/system_conf.rs +++ b/src/dbus/rauc/system_conf.rs @@ -65,7 +65,20 @@ pub fn update_system_conf( enable_auto_install: bool, ) -> std::io::Result { let dynamic_conf = { - match poll_section(primary_channel, enable_polling, enable_auto_install) { + // Allow force-enabling update polling and automatic installations + // via the update channel config file to implement company wide + // auto-update policies. + let force_polling = primary_channel + .and_then(|pc| pc.force_polling) + .unwrap_or(false); + let force_auto_install = primary_channel + .and_then(|pc| pc.force_auto_install) + .unwrap_or(false); + + let polling = enable_polling || force_polling; + let auto_install = enable_auto_install || force_auto_install; + + match poll_section(primary_channel, polling, auto_install) { Ok(Some(ps)) => { // We use the config in /etc as a template ... let static_conf = read_to_string(STATIC_CONF_PATH)?; diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs index 9790a3c8..c067d2b7 100644 --- a/src/dbus/rauc/update_channels.rs +++ b/src/dbus/rauc/update_channels.rs @@ -54,6 +54,8 @@ pub struct Channel { pub enabled: bool, pub primary: bool, pub bundle: Option, + pub force_polling: Option, + pub force_auto_install: Option, } #[derive(Serialize, Deserialize, Clone, PartialEq)] @@ -66,6 +68,8 @@ pub struct ChannelFile { pub description: String, pub url: String, pub polling_interval: Option, + pub force_polling: Option, + pub force_auto_install: Option, } #[cfg(not(feature = "demo_mode"))] @@ -145,6 +149,8 @@ impl Channel { enabled: false, primary: false, bundle: None, + force_polling: channel_file.force_polling, + force_auto_install: channel_file.force_auto_install, }; ch.update_enabled(); From e5c3a3cc1c3e012e080475f6a6e5c953f632ce19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Mon, 31 Mar 2025 15:45:50 +0200 Subject: [PATCH 16/21] dbus: rauc: allow configuring the *_criteria in channel files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The native RAUC polling feature allows configuring when a new update bundle is even considered as an update candidate (`candidate_criteria`), when it is considered for automatic installation (`install_criteria`) and under which conditions to auto-boot into another slot after installation (`reboot_criteria`). The defaults we have chosen in previous commits generally make sense, but allow users with custom update channels to customize them if they deem it necessary. Signed-off-by: Leonard Göhrs --- openapi.yaml | 6 ++++++ src/dbus/rauc/system_conf.rs | 23 +++++++++++++++++------ src/dbus/rauc/update_channels.rs | 9 +++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index c5efb2b3..1d6fae61 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1166,6 +1166,12 @@ components: type: boolean force_auto_install: type: boolean + candidate_criteria: + type: string + install_criteria: + type: string + reboot_criteria: + type: string ServiceStatus: type: object diff --git a/src/dbus/rauc/system_conf.rs b/src/dbus/rauc/system_conf.rs index e387cb7e..32dabcbd 100644 --- a/src/dbus/rauc/system_conf.rs +++ b/src/dbus/rauc/system_conf.rs @@ -45,14 +45,25 @@ fn poll_section( writeln!(&mut section, "interval-sec={}", interval.as_secs())?; } - writeln!(&mut section, "candidate-criteria=different-version")?; + let candidate_criteria = primary_channel + .candidate_criteria + .as_deref() + .unwrap_or("different-version"); + + writeln!(&mut section, "candidate-criteria={candidate_criteria}")?; if auto_install { - writeln!(&mut section, "install-criteria=different-version")?; - writeln!( - &mut section, - "reboot-criteria=updated-slots;updated-artifacts" - )?; + let install_criteria = primary_channel + .install_criteria + .as_deref() + .unwrap_or("different-version"); + let reboot_criteria = primary_channel + .reboot_criteria + .as_deref() + .unwrap_or("updated-slots;updated-artifacts"); + + writeln!(&mut section, "install-criteria={install_criteria}")?; + writeln!(&mut section, "reboot-criteria={reboot_criteria}")?; writeln!(&mut section, "reboot-cmd=systemctl reboot")?; } diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs index c067d2b7..d147f6c7 100644 --- a/src/dbus/rauc/update_channels.rs +++ b/src/dbus/rauc/update_channels.rs @@ -56,6 +56,9 @@ pub struct Channel { pub bundle: Option, pub force_polling: Option, pub force_auto_install: Option, + pub candidate_criteria: Option, + pub install_criteria: Option, + pub reboot_criteria: Option, } #[derive(Serialize, Deserialize, Clone, PartialEq)] @@ -70,6 +73,9 @@ pub struct ChannelFile { pub polling_interval: Option, pub force_polling: Option, pub force_auto_install: Option, + pub candidate_criteria: Option, + pub install_criteria: Option, + pub reboot_criteria: Option, } #[cfg(not(feature = "demo_mode"))] @@ -151,6 +157,9 @@ impl Channel { bundle: None, force_polling: channel_file.force_polling, force_auto_install: channel_file.force_auto_install, + candidate_criteria: channel_file.candidate_criteria, + install_criteria: channel_file.install_criteria, + reboot_criteria: channel_file.reboot_criteria, }; ch.update_enabled(); From 31dd18980a67d838adb1c86507ea14fcd8a39c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 10:56:32 +0200 Subject: [PATCH 17/21] dbus: rauc: prevent auto updates while in setup mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is to prevent a situation where a user enables auto-install in the setup mode and an update starts immediately, does a migration of whatever the user has configured so far and triggers a reboot while the user is still configuring things in the web interface. This should make the user experience a bit better: - Unbox TAC. - Enter setup mode. - Enable update polling and auto install. (A poll for updates will trigger immediately, but no auto install) - Configure the rest of the system. - Leave the setup mode. - Another poll for updates may be triggered and if available an installation will start. - The user is greeted by the normal web interface showing a notification about the ongoing installation. Signed-off-by: Leonard Göhrs --- src/dbus.rs | 3 ++- src/dbus/rauc.rs | 18 ++++++++++++++++-- src/dbus/rauc/system_conf.rs | 8 +++++++- src/main.rs | 25 ++++++++++++++++--------- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/dbus.rs b/src/dbus.rs index 326c02a0..db9c4b55 100644 --- a/src/dbus.rs +++ b/src/dbus.rs @@ -82,6 +82,7 @@ impl DbusSession { wtb: &mut WatchedTasksBuilder, led_dut: Arc>, led_uplink: Arc>, + setup_mode: Arc>, ) -> anyhow::Result { let tacd = Tacd::new(); @@ -94,7 +95,7 @@ impl DbusSession { Ok(Self { hostname: Hostname::new(bb, wtb, &conn)?, network: Network::new(bb, wtb, &conn, led_dut, led_uplink)?, - rauc: Rauc::new(bb, wtb, &conn, systemd.rauc.clone())?, + rauc: Rauc::new(bb, wtb, &conn, systemd.rauc.clone(), setup_mode)?, systemd, }) } diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs index c8ca2ab2..11bf60fb 100644 --- a/src/dbus/rauc.rs +++ b/src/dbus/rauc.rs @@ -207,6 +207,7 @@ async fn channel_list_update_task( reload: Arc>, enable_polling: Arc>, enable_auto_install: Arc>, + setup_mode: Arc>, channels: Arc>, rauc_service: Service, ) -> Result<()> { @@ -215,9 +216,11 @@ async fn channel_list_update_task( let (reload_stream, _) = reload.subscribe_unbounded(); let (mut enable_polling_stream, _) = enable_polling.subscribe_unbounded(); let (mut enable_auto_install_stream, _) = enable_auto_install.subscribe_unbounded(); + let (mut setup_mode_stream, _) = setup_mode.subscribe_unbounded(); let mut enable_polling = enable_polling_stream.next().await.unwrap_or(false); let mut enable_auto_install = enable_auto_install_stream.next().await.unwrap_or(false); + let mut setup_mode = setup_mode_stream.next().await.unwrap_or(true); 'reload_loop: loop { futures::select! { @@ -232,6 +235,9 @@ async fn channel_list_update_task( enable_auto_install_new = enable_auto_install_stream.recv().fuse() => { enable_auto_install = enable_auto_install_new?; } + setup_mode_new = setup_mode_stream.recv().fuse() => { + setup_mode = setup_mode_new?; + } }; // Read the list of available update channels @@ -243,8 +249,12 @@ async fn channel_list_update_task( } }; - let should_reload = - update_system_conf(new_channels.primary(), enable_polling, enable_auto_install)?; + let should_reload = update_system_conf( + new_channels.primary(), + enable_polling, + enable_auto_install, + setup_mode, + )?; channels.set(new_channels); @@ -329,6 +339,7 @@ impl Rauc { wtb: &mut WatchedTasksBuilder, _conn: &Arc, rauc_service: Service, + setup_mode: Arc>, ) -> Result { let inst = Self::setup_topics(bb); @@ -344,6 +355,7 @@ impl Rauc { inst.reload.clone(), inst.enable_polling.clone(), inst.enable_auto_install.clone(), + setup_mode, inst.channels.clone(), rauc_service, ), @@ -358,6 +370,7 @@ impl Rauc { wtb: &mut WatchedTasksBuilder, conn: &Arc, rauc_service: Service, + setup_mode: Arc>, ) -> Result { let inst = Self::setup_topics(bb); @@ -613,6 +626,7 @@ impl Rauc { inst.reload.clone(), inst.enable_polling.clone(), inst.enable_auto_install.clone(), + setup_mode, inst.channels.clone(), rauc_service, ), diff --git a/src/dbus/rauc/system_conf.rs b/src/dbus/rauc/system_conf.rs index 32dabcbd..fce32277 100644 --- a/src/dbus/rauc/system_conf.rs +++ b/src/dbus/rauc/system_conf.rs @@ -74,6 +74,7 @@ pub fn update_system_conf( primary_channel: Option<&Channel>, enable_polling: bool, enable_auto_install: bool, + setup_mode: bool, ) -> std::io::Result { let dynamic_conf = { // Allow force-enabling update polling and automatic installations @@ -86,8 +87,13 @@ pub fn update_system_conf( .and_then(|pc| pc.force_auto_install) .unwrap_or(false); + // Allow polling for updates, but not automatically installing them + // while the user is still in setup mode. + // Otherwise they may unbox a TAC, click through the setup process, + // activate auto installation, and then an installation starts in the + // background without them even noticing. let polling = enable_polling || force_polling; - let auto_install = enable_auto_install || force_auto_install; + let auto_install = (enable_auto_install || force_auto_install) && !setup_mode; match poll_section(primary_channel, polling, auto_install) { Ok(Some(ps)) => { diff --git a/src/main.rs b/src/main.rs index 203b74b0..29ab5574 100644 --- a/src/main.rs +++ b/src/main.rs @@ -107,9 +107,23 @@ async fn init(screenshooter: ScreenShooter) -> Result<(Ui, WatchedTasksBuilder)> adc.iobus_curr.fast.clone(), adc.iobus_volt.fast.clone(), )?; + + // Set up a http server and provide some static files like the web + // interface and config files that may be edited inside the web ui. + let mut http_server = HttpServer::new(); + + // Allow editing some aspects of the TAC configuration when in "setup mode". + let setup_mode = SetupMode::new(&mut bb, &mut wtb, &mut http_server.server)?; + let (hostname, network, rauc, systemd) = { - let dbus = - DbusSession::new(&mut bb, &mut wtb, led.eth_dut.clone(), led.eth_lab.clone()).await?; + let dbus = DbusSession::new( + &mut bb, + &mut wtb, + led.eth_dut.clone(), + led.eth_lab.clone(), + setup_mode.setup_mode.clone(), + ) + .await?; (dbus.hostname, dbus.network, dbus.rauc, dbus.systemd) }; @@ -123,13 +137,6 @@ async fn init(screenshooter: ScreenShooter) -> Result<(Ui, WatchedTasksBuilder)> // (if requested on start). let watchdog = Watchdog::new(dut_pwr.tick()); - // Set up a http server and provide some static files like the web - // interface and config files that may be edited inside the web ui. - let mut http_server = HttpServer::new(); - - // Allow editing some aspects of the TAC configuration when in "setup mode". - let setup_mode = SetupMode::new(&mut bb, &mut wtb, &mut http_server.server)?; - // Expose a live log of the TAC's systemd journal so it can be viewed // in the web interface. journal::serve(&mut http_server.server); From cb89591d94e03f056b89d4eda6d83095560c40ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 18:32:50 +0200 Subject: [PATCH 18/21] web: setup: inform the user about additional headers that are sent now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We include the `boot-id` and `uptime` options in the RAUC `send-headers` config in the hopes of detecting boot-loops during update roll-out. This was not anticipated when writing the setup page, so we add it now. Signed-off-by: Leonard Göhrs --- web/src/Setup.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/Setup.tsx b/web/src/Setup.tsx index 816acc52..3ff0c827 100644 --- a/web/src/Setup.tsx +++ b/web/src/Setup.tsx @@ -158,8 +158,9 @@ export default function Setup() { When polling for updates the LXA TAC will transmit the following information to our server: The IP address the - request is coming from, the serial number of the device - and information on the currently installed software. + request is coming from, the serial number of the device, + the device uptime and boot id and information on the + currently installed software. From 9f5d2562ac4037112dfe1bc1e213b1e30bfd2400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 18:35:36 +0200 Subject: [PATCH 19/21] web: indicate to the user when a channel is enabled but not primary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The channel list in the web interface now contains a "Upgrade" column with one of the following: - "Not enabled" for channels which are not enabled, which means bundles from it can not be installed for it. - "Not primary" (this one is new) for channels which are enabled, but are not the primary one and are thus not polled by the native RAUC polling feature. - "Polling disabled" if the polling feature is not enabled. - A spinner if we do not know the status yet. - "Up to date" if the TAC is in sync with this update channel. - "Upgrade" (a button) if an update is available. Signed-off-by: Leonard Göhrs --- web/src/TacComponents.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/src/TacComponents.tsx b/web/src/TacComponents.tsx index e1c99677..8ef5056d 100644 --- a/web/src/TacComponents.tsx +++ b/web/src/TacComponents.tsx @@ -125,6 +125,7 @@ type Channel = { url: string; polling_interval?: Duration; enabled: boolean; + primary: boolean; bundle?: UpstreamBundle; }; @@ -379,6 +380,10 @@ export function UpdateChannels(props: UpdateChannelsProps) { return "Not enabled"; } + if (!e.primary) { + return "Not primary"; + } + if (!e.bundle) { if (enable_polling) { return ; From 84c6b9ecd41dfb0b79f12584c873b11f2351dc7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 18:33:39 +0200 Subject: [PATCH 20/21] web: add toggle switches to enable automatic installation of updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Leonard Göhrs --- web/src/Setup.tsx | 3 +++ web/src/TacComponents.tsx | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/web/src/Setup.tsx b/web/src/Setup.tsx index 3ff0c827..979b30b9 100644 --- a/web/src/Setup.tsx +++ b/web/src/Setup.tsx @@ -166,6 +166,9 @@ export default function Setup() { Periodically check for updates + + Automatically install and boot updates + ), diff --git a/web/src/TacComponents.tsx b/web/src/TacComponents.tsx index 8ef5056d..57249f76 100644 --- a/web/src/TacComponents.tsx +++ b/web/src/TacComponents.tsx @@ -254,6 +254,12 @@ export function UpdateConfig() { Periodically check for updates + + Auto Install + + Automatically install and boot updates + + ); From 4e711b3da94e16eb94b97b4b3c7b0708d2b53b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 2 Apr 2025 18:35:01 +0200 Subject: [PATCH 21/21] web: use manifest_hash and effective_url when triggering an install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This ensures that the exact bundle (content) that the user agreed to install is actually installed. Signed-off-by: Leonard Göhrs --- web/src/TacComponents.tsx | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/web/src/TacComponents.tsx b/web/src/TacComponents.tsx index 57249f76..9f18d5fd 100644 --- a/web/src/TacComponents.tsx +++ b/web/src/TacComponents.tsx @@ -115,6 +115,8 @@ type Duration = { type UpstreamBundle = { compatible: string; version: string; + manifest_hash: string; + effective_url: string; newer_than_installed: boolean; }; @@ -129,6 +131,11 @@ type Channel = { bundle?: UpstreamBundle; }; +type UpdateRequest = { + manifest_hash: string; + url: string; +}; + interface SlotStatusProps { setCmdHint: (hint: React.ReactNode | null) => void; } @@ -402,11 +409,16 @@ export function UpdateChannels(props: UpdateChannelsProps) { return "Up to date"; } + const request: UpdateRequest = { + manifest_hash: e.bundle.manifest_hash, + url: e.bundle.effective_url, + }; + return ( Upgrade @@ -539,7 +551,16 @@ export function UpdateNotification() { if (channels !== undefined) { for (let ch of channels) { if (ch.enabled && ch.bundle && ch.bundle.newer_than_installed) { - updates.push(ch); + const request: UpdateRequest = { + manifest_hash: ch.bundle.manifest_hash, + url: ch.bundle.effective_url, + }; + + updates.push({ + name: ch.name, + display_name: ch.display_name, + request: request, + }); } } } @@ -549,7 +570,7 @@ export function UpdateNotification() { key={u.name} iconName="download" topic="/v1/tac/update/install" - send={u.url} + send={u.request} > Install new {u.display_name} bundle