mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
* 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
332 lines
9.6 KiB
Rust
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 < ¤t_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(())
|
|
}
|