diff --git a/appmgr/src/update/mod.rs b/appmgr/src/update/mod.rs index 1eaf89a8e..924716e9b 100644 --- a/appmgr/src/update/mod.rs +++ b/appmgr/src/update/mod.rs @@ -1,7 +1,9 @@ -use anyhow::anyhow; +use anyhow::{anyhow, bail, Result}; use clap::ArgMatches; use digest::Digest; use futures::Stream; +use lazy_static::lazy_static; +use regex::Regex; use rpc_toolkit::command; use serde_json::Value; use sha2::Sha256; @@ -13,22 +15,98 @@ use crate::context::RpcContext; use crate::update::latest_information::LatestInformation; use crate::{Error, ErrorKind, ResultExt}; +/// An user/ daemon would call this to update the system to the latest version and do the updates available, +/// and this will return something if there is an update, and in that case there will need to be a restart. +#[command(display(display_properties))] +pub async fn update_system(#[context] ctx: RpcContext) -> Result { + if let None = maybe_do_update(ctx).await? { + return Ok(UpdateSystem::Updated); + } + Ok(UpdateSystem::NoUpdates) +} + +/// What is the status of the updates? +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub enum UpdateSystem { + NoUpdates, + Updated, +} + +fn display_properties(status: UpdateSystem, _: &ArgMatches<'_>) { + match status { + UpdateSystem::NoUpdates => { + println!("Updates are ready, please reboot"); + } + UpdateSystem::Updated => { + println!("No updates needed"); + } + } +} + const URL: &str = "https://beta-registry-0-3.start9labs.com/eos/latest"; const HEADER_KEY: &str = "CHECKSUM"; mod latest_information; -pub fn display_properties(_: (), _: &ArgMatches<'_>) { - println!("Test"); -} -#[command(display(display_properties))] -pub async fn update_system(#[context] ctx: RpcContext) -> Result<(), Error> { - if let None = maybe_do_update(ctx).await? { - return Ok(()); - } - todo!() +enum WritableDrives { + Green, + Blue, } -pub async fn maybe_do_update(mut ctx: RpcContext) -> Result, Error> { +struct Boot; + +/// We are going to be creating some folders and mounting so +/// we need to know the labels for those types. These labels +/// are the labels that are shipping with the embassy, blue/ green +/// are where the os sits and will do a swap during update. +trait FileType { + fn mount_folder(&self) -> String { + format!("/media/{}", self.label()) + } + fn label(&self) -> String; +} +impl FileType for WritableDrives { + fn label(&self) -> String { + match self { + WritableDrives::Green => "green", + WritableDrives::Blue => "blue", + } + .to_string() + } +} +impl FileType for Boot { + fn label(&self) -> String { + "system-boot".to_string() + } +} + +/// Proven data that this is mounted, should be consumed in an unmount +struct MountedResource(X); +impl MountedResource { + async fn unmount_label(&self) -> Result<()> { + let folder = self.0.mount_folder(); + tokio::process::Command::new("umount") + .arg(&folder) + .output() + .await?; + tokio::process::Command::new("rmdir") + .arg(folder) + .output() + .await?; + Ok(()) + } +} + +/// This will be where we are going to be putting the new update +struct NewLabel<'a>(&'a MountedResource); + +/// This is our current label where the os is running +struct CurrentLabel<'a>(&'a MountedResource); + +lazy_static! { + static ref PARSE_COLOR: Regex = Regex::new("#LABEL=(\\w+) /media/root-ro/").unwrap(); +} + +async fn maybe_do_update(mut ctx: RpcContext) -> Result, Error> { let mut db = ctx.db.handle(); let latest_version = reqwest::get(URL) .await @@ -42,17 +120,72 @@ pub async fn maybe_do_update(mut ctx: RpcContext) -> Result, Error> { .version() .get_mut(&mut db) .await?; - if &latest_version > ¤t_version { - let file_name = "/tmp/test"; - download_file(file_name).await?; - swap(&mut ctx).await?; - Ok(Some(())) - } else { - Ok(None) + if &latest_version <= ¤t_version { + return Ok(None); + } + let file_name = "/tmp/test"; + let mounted_blue = mount_label(WritableDrives::Blue) + .await + .with_kind(ErrorKind::Filesystem)?; + let mounted_green = mount_label(WritableDrives::Green) + .await + .with_kind(ErrorKind::Filesystem)?; + let mounted_boot = mount_label(Boot).await.with_kind(ErrorKind::Filesystem)?; + let potential_error_actions = async { + let (new_label, _current_label) = query_mounted_label(&mounted_blue, &mounted_green) + .await + .with_kind(ErrorKind::Filesystem)?; + download_file(file_name, &new_label).await?; + + swap_boot_label(&mut ctx, &new_label, &mounted_boot).await?; + Ok::<_, Error>(()) + } + .await; + + mounted_blue + .unmount_label() + .await + .with_kind(ErrorKind::Filesystem)?; + mounted_green + .unmount_label() + .await + .with_kind(ErrorKind::Filesystem)?; + mounted_boot + .unmount_label() + .await + .with_kind(ErrorKind::Filesystem)?; + potential_error_actions?; + Ok(Some(())) +} + +async fn query_mounted_label<'a>( + mounted_resource_left: &'a MountedResource, + mounted_resource_right: &'a MountedResource, +) -> Result<(NewLabel<'a>, CurrentLabel<'a>)> { + let output = String::from_utf8( + tokio::process::Command::new("cat") + .arg("/etc/fstab") + .output() + .await? + .stdout, + )?; + match &PARSE_COLOR + .captures(&output) + .ok_or_else(|| anyhow!("Can't find pattern in {}", output))?[1] + { + x if x == &mounted_resource_left.0.label() => Ok(( + NewLabel(mounted_resource_left), + CurrentLabel(mounted_resource_right), + )), + x if x == &mounted_resource_right.0.label() => Ok(( + NewLabel(mounted_resource_right), + CurrentLabel(mounted_resource_left), + )), + e => bail!("Could not find a mounted resource for {}", e), } } -pub async fn download_file(file_name: &str) -> Result<(), Error> { +async fn download_file(file_name: &str, new_label: &NewLabel<'_>) -> Result<(), Error> { let download_request = reqwest::get(URL).await.with_kind(ErrorKind::Network)?; let hash_from_header: String = download_request .headers() @@ -83,7 +216,7 @@ async fn write_stream_to_file( Ok(hasher.finalize().to_vec()) } -pub async fn check_download(hash_from_header: &str, file_digest: Vec) -> Result<(), Error> { +async fn check_download(hash_from_header: &str, file_digest: Vec) -> Result<(), Error> { if hex::decode(hash_from_header).with_kind(ErrorKind::Network)? != file_digest { return Err(Error::new( anyhow!("Hash sum does not match source"), @@ -92,9 +225,54 @@ pub async fn check_download(hash_from_header: &str, file_digest: Vec) -> Res } Ok(()) } - -pub async fn swap(ctx: &mut RpcContext) -> Result { +async fn swap_boot_label( + ctx: &mut RpcContext, + new_label: &NewLabel<'_>, + mounted_boot: &MountedResource, +) -> Result<(), Error> { // disk/util add setLabel - todo!("Do swap"); - todo!("Let system know that we need a reboot or something") + tokio::process::Command::new("sed") + .arg(format!(r#""r/(blue|green)/{}/g""#, new_label.0 .0.label())) + .arg(format!("{}/etc/fstab", mounted_boot.0.mount_folder())) + .output() + .await?; + Ok(()) +} + +async fn mount_label(file_type: F) -> Result> +where + F: FileType, +{ + let label = file_type.label(); + let folder = file_type.mount_folder(); + tokio::process::Command::new("mdkir") + .arg(&folder) + .output() + .await?; + tokio::process::Command::new("mount") + .arg("-L") + .arg(label) + .arg(folder) + .output() + .await?; + Ok(MountedResource(file_type)) +} +/// Captured from doing an fstab with an embassy box and the cat from the /etc/fstab +#[test] +fn test_capture() { + let output = r#" +# +# This fstab is for overlayroot. The real one can be found at +# /media/root-ro/etc/fstab +# The original entry for '/' and other mounts have been updated to be placed +# under /media/root-ro. +# To permanently modify this (or any other file), you should change-root into +# a writable view of the underlying filesystem using: +# sudo overlayroot-chroot +# +#LABEL=blue /media/root-ro/ ext4 ro,discard,errors=remount-ro,noauto 0 1 +/media/root-ro/ / overlay lowerdir=/media/root-ro/,upperdir=/media/root-rw/overlay/,workdir=/media/root-rw/overlay-workdir/_ 0 1 +LABEL=system-boot /boot/firmware vfat defaults 0 1 # overlayroot:fs-unsupported +"#; + assert_eq!(&PARSE_COLOR.captures(&output).unwrap()[1], "blue"); }