use std::collections::BTreeMap; use std::sync::Arc; use chrono::Utc; use color_eyre::eyre::eyre; use openssl::pkey::{PKey, Private}; use openssl::x509::X509; use patch_db::{DbHandle, LockType, PatchDbHandle, Revision}; use rpc_toolkit::command; use serde::{Deserialize, Serialize}; use serde_json::Value; use tokio::io::AsyncWriteExt; use tokio::sync::oneshot::Sender; use torut::onion::TorSecretKeyV3; use tracing::instrument; use super::target::BackupTargetId; use super::PackageBackupReport; use crate::auth::check_password_against_db; use crate::backup::{BackupReport, ServerBackupReport}; use crate::context::RpcContext; use crate::db::util::WithRevision; use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::guard::TmpMountGuard; use crate::notifications::NotificationLevel; use crate::s9pk::manifest::PackageId; use crate::status::MainStatus; use crate::util::serde::IoFormat; use crate::util::{display_none, AtomicFile}; use crate::version::VersionT; use crate::Error; #[derive(Debug)] pub struct OsBackup { pub tor_key: TorSecretKeyV3, pub root_ca_key: PKey, pub root_ca_cert: X509, pub ui: Value, } impl<'de> Deserialize<'de> for OsBackup { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { #[derive(Deserialize)] #[serde(rename = "kebab-case")] struct OsBackupDe { tor_key: String, root_ca_key: String, root_ca_cert: String, ui: Value, } let int = OsBackupDe::deserialize(deserializer)?; let key_vec = base32::decode(base32::Alphabet::RFC4648 { padding: true }, &int.tor_key) .ok_or_else(|| { serde::de::Error::invalid_value( serde::de::Unexpected::Str(&int.tor_key), &"an RFC4648 encoded string", ) })?; if key_vec.len() != 64 { return Err(serde::de::Error::invalid_value( serde::de::Unexpected::Str(&int.tor_key), &"a 64 byte value encoded as an RFC4648 string", )); } let mut key_slice = [0; 64]; key_slice.clone_from_slice(&key_vec); Ok(OsBackup { tor_key: TorSecretKeyV3::from(key_slice), root_ca_key: PKey::::private_key_from_pem(int.root_ca_key.as_bytes()) .map_err(serde::de::Error::custom)?, root_ca_cert: X509::from_pem(int.root_ca_cert.as_bytes()) .map_err(serde::de::Error::custom)?, ui: int.ui, }) } } impl Serialize for OsBackup { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { #[derive(Serialize)] #[serde(rename = "kebab-case")] struct OsBackupSer<'a> { tor_key: String, root_ca_key: String, root_ca_cert: String, ui: &'a Value, } OsBackupSer { tor_key: base32::encode( base32::Alphabet::RFC4648 { padding: true }, &self.tor_key.as_bytes(), ), root_ca_key: String::from_utf8( self.root_ca_key .private_key_to_pem_pkcs8() .map_err(serde::ser::Error::custom)?, ) .map_err(serde::ser::Error::custom)?, root_ca_cert: String::from_utf8( self.root_ca_cert .to_pem() .map_err(serde::ser::Error::custom)?, ) .map_err(serde::ser::Error::custom)?, ui: &self.ui, } .serialize(serializer) } } #[command(rename = "create", display(display_none))] #[instrument(skip(ctx, old_password, password))] pub async fn backup_all( #[context] ctx: RpcContext, #[arg(rename = "target-id")] target_id: BackupTargetId, #[arg(rename = "old-password", long = "old-password")] old_password: Option, #[arg] password: String, ) -> Result, Error> { let mut db = ctx.db.handle(); check_password_against_db(&mut ctx.secret_store.acquire().await?, &password).await?; let fs = target_id .load(&mut ctx.secret_store.acquire().await?) .await?; let mut backup_guard = BackupMountGuard::mount( TmpMountGuard::mount(&fs).await?, old_password.as_ref().unwrap_or(&password), ) .await?; if old_password.is_some() { backup_guard.change_password(&password)?; } let revision = assure_backing_up(&mut db).await?; tokio::task::spawn(async move { let backup_res = perform_backup(&ctx, &mut db, backup_guard).await; let status_model = crate::db::DatabaseModel::new() .server_info() .status_info() .backing_up(); status_model .clone() .lock(&mut db, LockType::Write) .await .expect("failed to lock server status"); match backup_res { Ok(report) if report.iter().all(|(_, rep)| rep.error.is_none()) => ctx .notification_manager .notify( &mut db, None, NotificationLevel::Success, "Backup Complete".to_owned(), "Your backup has completed".to_owned(), BackupReport { server: ServerBackupReport { attempted: true, error: None, }, packages: report, }, None, ) .await .expect("failed to send notification"), Ok(report) => ctx .notification_manager .notify( &mut db, None, NotificationLevel::Warning, "Backup Complete".to_owned(), "Your backup has completed, but some package(s) failed to backup".to_owned(), BackupReport { server: ServerBackupReport { attempted: true, error: None, }, packages: report, }, None, ) .await .expect("failed to send notification"), Err(e) => { tracing::error!("Backup Failed: {}", e); tracing::debug!("{:?}", e); ctx.notification_manager .notify( &mut db, None, NotificationLevel::Error, "Backup Failed".to_owned(), "Your backup failed to complete.".to_owned(), BackupReport { server: ServerBackupReport { attempted: true, error: Some(e.to_string()), }, packages: BTreeMap::new(), }, None, ) .await .expect("failed to send notification"); } } status_model .put(&mut db, &false) .await .expect("failed to change server status"); }); Ok(WithRevision { response: (), revision, }) } #[instrument(skip(db))] async fn assure_backing_up(db: &mut PatchDbHandle) -> Result>, Error> { let mut tx = db.begin().await?; let mut backing_up = crate::db::DatabaseModel::new() .server_info() .status_info() .backing_up() .get_mut(&mut tx) .await?; if *backing_up { return Err(Error::new( eyre!("Server is already backing up!"), crate::ErrorKind::InvalidRequest, )); } *backing_up = true; backing_up.save(&mut tx).await?; Ok(tx.commit(None).await?) } #[instrument(skip(ctx, db, backup_guard))] async fn perform_backup( ctx: &RpcContext, mut db: Db, mut backup_guard: BackupMountGuard, ) -> Result, Error> { let mut backup_report = BTreeMap::new(); for package_id in crate::db::DatabaseModel::new() .package_data() .keys(&mut db, false) .await? { let mut tx = db.begin().await?; // for lock scope let installed_model = if let Some(installed_model) = crate::db::DatabaseModel::new() .package_data() .idx_model(&package_id) .and_then(|m| m.installed()) .check(&mut tx) .await? { installed_model } else { continue; }; let main_status_model = installed_model.clone().status().main(); main_status_model.lock(&mut tx, LockType::Write).await?; let (started, health) = match main_status_model.get(&mut tx, true).await?.into_owned() { MainStatus::Starting => (Some(Utc::now()), Default::default()), MainStatus::Running { started, health } => (Some(started), health.clone()), MainStatus::Stopped | MainStatus::Stopping => (None, Default::default()), MainStatus::BackingUp { .. } => { backup_report.insert( package_id, PackageBackupReport { error: Some( "Can't do backup because service is in a backing up state".to_owned(), ), }, ); continue; } }; main_status_model .put( &mut tx, &MainStatus::BackingUp { started, health: health.clone(), }, ) .await?; tx.save().await?; // drop locks let manifest = installed_model .clone() .manifest() .get(&mut db, false) .await?; ctx.managers .get(&(manifest.id.clone(), manifest.version.clone())) .await .ok_or_else(|| { Error::new(eyre!("Manager not found"), crate::ErrorKind::InvalidRequest) })? .synchronize() .await; let mut tx = db.begin().await?; installed_model.lock(&mut tx, LockType::Write).await?; let guard = backup_guard.mount_package_backup(&package_id).await?; let res = manifest .backup .create( ctx, &package_id, &manifest.title, &manifest.version, &manifest.interfaces, &manifest.volumes, ) .await; guard.unmount().await?; backup_report.insert( package_id.clone(), PackageBackupReport { error: res.as_ref().err().map(|e| e.to_string()), }, ); if let Ok(pkg_meta) = res { installed_model .last_backup() .put(&mut tx, &Some(pkg_meta.timestamp)) .await?; backup_guard .metadata .package_backups .insert(package_id, pkg_meta); } main_status_model .put( &mut tx, &match started { Some(started) => MainStatus::Running { started, health }, None => MainStatus::Stopped, }, ) .await?; tx.save().await?; } crate::db::DatabaseModel::new() .lock(&mut db, LockType::Write) .await?; let (root_ca_key, root_ca_cert) = ctx.net_controller.ssl.export_root_ca().await?; let mut os_backup_file = AtomicFile::new(backup_guard.as_ref().join("os-backup.cbor")).await?; os_backup_file .write_all( &IoFormat::Cbor.to_vec(&OsBackup { tor_key: ctx.net_controller.tor.embassyd_tor_key().await, root_ca_key, root_ca_cert, ui: crate::db::DatabaseModel::new() .ui() .get(&mut db, true) .await? .into_owned(), })?, ) .await?; os_backup_file.save().await?; let timestamp = Some(Utc::now()); backup_guard.unencrypted_metadata.version = crate::version::Current::new().semver().into(); backup_guard.unencrypted_metadata.full = true; backup_guard.metadata.version = crate::version::Current::new().semver().into(); backup_guard.metadata.timestamp = timestamp; backup_guard.save_and_unmount().await?; crate::db::DatabaseModel::new() .server_info() .last_backup() .put(&mut db, ×tamp) .await?; Ok(backup_report) }