Files
start-os/backend/src/update/mod.rs
Aiden McClelland 068b861edc overhaul OS build (#2244)
* create init resize for pi

* wip

* defer to OS_ARCH env var

* enable password auth in live image

* use correct live image path

* reorder dependencies

* add grub-common as dependency

* add more depends

* reorder grub

* include systemd-resolved

* misc fixes

* remove grub from dependencies

* imports

* ssh and raspi builds

* fix resolvectl

* generate snake-oil on install

* update raspi build process

* script fixes

* fix resize and config

* add psmisc

* new workflows

* include img

* pass through OS_ARCH env var

* require OS_ARCH

* allow dispatching production builds

* configurable environment

* pass through OS_ARCH on compat build

* fix syntax error

* crossbuild dependencies

* include libavahi-client for cross builds

* reorder add-arch

* add ports

* switch existing repos to amd64

* explicitly install libc6

* add more bullshit

* fix some errors

* use ignored shlibs

* remove ubuntu ports

* platform deb

* Update depends

* Update startos-iso.yaml

* Update startos-iso.yaml

* require pi-beep

* add bios boot, fix environment

* Update startos-iso.yaml

* inline deb

* Update startos-iso.yaml

* allow ssh password auth in live build

* sync hostname on livecd

* require curl
2023-05-05 00:54:09 -06:00

332 lines
9.6 KiB
Rust

use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use clap::ArgMatches;
use color_eyre::eyre::{eyre, Result};
use emver::Version;
use helpers::{Rsync, RsyncOptions};
use lazy_static::lazy_static;
use patch_db::{DbHandle, LockType, Revision};
use reqwest::Url;
use rpc_toolkit::command;
use tokio::process::Command;
use tokio_stream::StreamExt;
use tracing::instrument;
use crate::context::RpcContext;
use crate::db::model::UpdateProgress;
use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::MountGuard;
use crate::notifications::NotificationLevel;
use crate::sound::{
CIRCLE_OF_5THS_SHORT, UPDATE_FAILED_1, UPDATE_FAILED_2, UPDATE_FAILED_3, UPDATE_FAILED_4,
};
use crate::update::latest_information::LatestInformation;
use crate::util::Invoke;
use crate::version::{Current, VersionT};
use crate::{Error, ErrorKind, ResultExt, OS_ARCH};
mod latest_information;
lazy_static! {
static ref UPDATED: AtomicBool = AtomicBool::new(false);
}
/// 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(
rename = "update",
display(display_update_result),
metadata(sync_db = true)
)]
#[instrument(skip_all)]
pub async fn update_system(
#[context] ctx: RpcContext,
#[arg(rename = "marketplace-url")] marketplace_url: Url,
) -> Result<UpdateResult, Error> {
if UPDATED.load(Ordering::SeqCst) {
return Ok(UpdateResult::NoUpdates);
}
Ok(if maybe_do_update(ctx, marketplace_url).await?.is_some() {
UpdateResult::Updating
} else {
UpdateResult::NoUpdates
})
}
/// What is the status of the updates?
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub enum UpdateResult {
NoUpdates,
Updating,
}
fn display_update_result(status: UpdateResult, _: &ArgMatches) {
match status {
UpdateResult::Updating => {
println!("Updating...");
}
UpdateResult::NoUpdates => {
println!("No updates available");
}
}
}
#[instrument(skip_all)]
async fn maybe_do_update(
ctx: RpcContext,
marketplace_url: Url,
) -> Result<Option<Arc<Revision>>, Error> {
let mut db = ctx.db.handle();
let latest_version: Version = reqwest::get(format!(
"{}/eos/v0/latest?eos-version={}&arch={}",
marketplace_url,
Current::new().semver(),
OS_ARCH,
))
.await
.with_kind(ErrorKind::Network)?
.json::<LatestInformation>()
.await
.with_kind(ErrorKind::Network)?
.version;
crate::db::DatabaseModel::new()
.server_info()
.lock(&mut db, LockType::Write)
.await?;
let current_version = crate::db::DatabaseModel::new()
.server_info()
.version()
.get_mut(&mut db)
.await?;
if &latest_version < &current_version {
return Ok(None);
}
let mut tx = db.begin().await?;
let mut status = crate::db::DatabaseModel::new()
.server_info()
.status_info()
.get_mut(&mut tx)
.await?;
if status.update_progress.is_some() {
return Err(Error::new(
eyre!("Server is already updating!"),
crate::ErrorKind::InvalidRequest,
));
}
if status.updated {
return Ok(None);
}
let eos_url = EosUrl {
base: marketplace_url,
version: latest_version,
};
status.update_progress = Some(UpdateProgress {
size: None,
downloaded: 0,
});
status.save(&mut tx).await?;
let rev = tx.commit().await?;
tokio::spawn(async move {
let res = do_update(ctx.clone(), eos_url).await;
let mut db = ctx.db.handle();
let mut status = crate::db::DatabaseModel::new()
.server_info()
.status_info()
.get_mut(&mut db)
.await
.expect("could not access status");
status.update_progress = None;
match res {
Ok(()) => {
status.updated = true;
status.save(&mut db).await.expect("could not save status");
CIRCLE_OF_5THS_SHORT
.play()
.await
.expect("could not play sound");
}
Err(e) => {
status.save(&mut db).await.expect("could not save status");
ctx.notification_manager
.notify(
&mut db,
None,
NotificationLevel::Error,
"embassyOS Update Failed".to_owned(),
format!("Update was not successful because of {}", e),
(),
None,
)
.await
.expect("");
// TODO: refactor sound lib to make compound tempos easier to deal with
UPDATE_FAILED_1
.play()
.await
.expect("could not play song: update failed 1");
UPDATE_FAILED_2
.play()
.await
.expect("could not play song: update failed 2");
UPDATE_FAILED_3
.play()
.await
.expect("could not play song: update failed 3");
UPDATE_FAILED_4
.play()
.await
.expect("could not play song: update failed 4");
}
}
});
Ok(rev)
}
#[instrument(skip_all)]
async fn do_update(ctx: RpcContext, eos_url: EosUrl) -> Result<(), Error> {
let mut rsync = Rsync::new(
eos_url.rsync_path()?,
"/media/embassy/next/",
Default::default(),
)
.await?;
while let Some(progress) = rsync.progress.next().await {
crate::db::DatabaseModel::new()
.server_info()
.status_info()
.update_progress()
.put(
&mut ctx.db.handle(),
&UpdateProgress {
size: Some(100),
downloaded: (100.0 * progress) as u64,
},
)
.await?;
}
rsync.wait().await?;
copy_fstab().await?;
copy_machine_id().await?;
copy_ssh_host_keys().await?;
sync_boot().await?;
swap_boot_label().await?;
Ok(())
}
#[derive(Debug)]
struct EosUrl {
base: Url,
version: Version,
}
impl EosUrl {
#[instrument()]
pub fn rsync_path(&self) -> Result<PathBuf, Error> {
let host = self
.base
.host_str()
.ok_or_else(|| Error::new(eyre!("Could not get host of base"), ErrorKind::ParseUrl))?;
let version: &Version = &self.version;
Ok(format!("{host}::{version}/{OS_ARCH}/")
.parse()
.map_err(|_| Error::new(eyre!("Could not parse path"), ErrorKind::ParseUrl))?)
}
}
async fn copy_fstab() -> Result<(), Error> {
tokio::fs::copy("/etc/fstab", "/media/embassy/next/etc/fstab").await?;
Ok(())
}
async fn copy_machine_id() -> Result<(), Error> {
tokio::fs::copy("/etc/machine-id", "/media/embassy/next/etc/machine-id").await?;
Ok(())
}
async fn copy_ssh_host_keys() -> Result<(), Error> {
tokio::fs::copy(
"/etc/ssh/ssh_host_rsa_key",
"/media/embassy/next/etc/ssh/ssh_host_rsa_key",
)
.await?;
tokio::fs::copy(
"/etc/ssh/ssh_host_rsa_key.pub",
"/media/embassy/next/etc/ssh/ssh_host_rsa_key.pub",
)
.await?;
tokio::fs::copy(
"/etc/ssh/ssh_host_ecdsa_key",
"/media/embassy/next/etc/ssh/ssh_host_ecdsa_key",
)
.await?;
tokio::fs::copy(
"/etc/ssh/ssh_host_ecdsa_key.pub",
"/media/embassy/next/etc/ssh/ssh_host_ecdsa_key.pub",
)
.await?;
tokio::fs::copy(
"/etc/ssh/ssh_host_ed25519_key",
"/media/embassy/next/etc/ssh/ssh_host_ed25519_key",
)
.await?;
tokio::fs::copy(
"/etc/ssh/ssh_host_ed25519_key.pub",
"/media/embassy/next/etc/ssh/ssh_host_ed25519_key.pub",
)
.await?;
Ok(())
}
async fn sync_boot() -> Result<(), Error> {
Rsync::new(
"/media/embassy/next/boot/",
"/boot/",
RsyncOptions {
delete: false,
force: false,
ignore_existing: false,
exclude: Vec::new(),
no_permissions: false,
no_owner: false,
},
)
.await?
.wait()
.await?;
if OS_ARCH != "raspberrypi" {
let dev_mnt =
MountGuard::mount(&Bind::new("/dev"), "/media/embassy/next/dev", ReadWrite).await?;
let sys_mnt =
MountGuard::mount(&Bind::new("/sys"), "/media/embassy/next/sys", ReadWrite).await?;
let proc_mnt =
MountGuard::mount(&Bind::new("/proc"), "/media/embassy/next/proc", ReadWrite).await?;
let boot_mnt =
MountGuard::mount(&Bind::new("/boot"), "/media/embassy/next/boot", ReadWrite).await?;
Command::new("chroot")
.arg("/media/embassy/next")
.arg("update-grub2")
.invoke(ErrorKind::MigrationFailed)
.await?;
boot_mnt.unmount(false).await?;
proc_mnt.unmount(false).await?;
sys_mnt.unmount(false).await?;
dev_mnt.unmount(false).await?;
}
Ok(())
}
#[instrument(skip_all)]
async fn swap_boot_label() -> Result<(), Error> {
tokio::fs::write("/media/embassy/config/upgrade", b"").await?;
Ok(())
}