mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 04:01:58 +00:00
Feature/remove postgres (#2570)
* wip: move postgres data to patchdb * wip * wip * wip * complete notifications and clean up warnings * fill in user agent * move os tor bindings to single call
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::panic::UnwindSafe;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -19,11 +18,11 @@ use crate::auth::check_password_against_db;
|
||||
use crate::backup::os::OsBackup;
|
||||
use crate::backup::{BackupReport, ServerBackupReport};
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::model::BackupProgress;
|
||||
use crate::db::model::{BackupProgress, DatabaseModel};
|
||||
use crate::disk::mount::backup::BackupMountGuard;
|
||||
use crate::disk::mount::filesystem::ReadWrite;
|
||||
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
|
||||
use crate::notifications::NotificationLevel;
|
||||
use crate::notifications::{notify, NotificationLevel};
|
||||
use crate::prelude::*;
|
||||
use crate::util::io::dir_copy;
|
||||
use crate::util::serde::IoFormat;
|
||||
@@ -41,6 +40,111 @@ pub struct BackupParams {
|
||||
password: crate::auth::PasswordType,
|
||||
}
|
||||
|
||||
struct BackupStatusGuard(Option<PatchDb>);
|
||||
impl BackupStatusGuard {
|
||||
fn new(db: PatchDb) -> Self {
|
||||
Self(Some(db))
|
||||
}
|
||||
async fn handle_result(
|
||||
mut self,
|
||||
result: Result<BTreeMap<PackageId, PackageBackupReport>, Error>,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(db) = self.0.as_ref() {
|
||||
db.mutate(|v| {
|
||||
v.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_status_info_mut()
|
||||
.as_backup_progress_mut()
|
||||
.ser(&None)
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
if let Some(db) = self.0.take() {
|
||||
match result {
|
||||
Ok(report) if report.iter().all(|(_, rep)| rep.error.is_none()) => {
|
||||
db.mutate(|db| {
|
||||
notify(
|
||||
db,
|
||||
None,
|
||||
NotificationLevel::Success,
|
||||
"Backup Complete".to_owned(),
|
||||
"Your backup has completed".to_owned(),
|
||||
BackupReport {
|
||||
server: ServerBackupReport {
|
||||
attempted: true,
|
||||
error: None,
|
||||
},
|
||||
packages: report,
|
||||
},
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
Ok(report) => {
|
||||
db.mutate(|db| {
|
||||
notify(
|
||||
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,
|
||||
},
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Backup Failed: {}", e);
|
||||
tracing::debug!("{:?}", e);
|
||||
let err_string = e.to_string();
|
||||
db.mutate(|db| {
|
||||
notify(
|
||||
db,
|
||||
None,
|
||||
NotificationLevel::Error,
|
||||
"Backup Failed".to_owned(),
|
||||
"Your backup failed to complete.".to_owned(),
|
||||
BackupReport {
|
||||
server: ServerBackupReport {
|
||||
attempted: true,
|
||||
error: Some(err_string),
|
||||
},
|
||||
packages: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Drop for BackupStatusGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(db) = self.0.take() {
|
||||
tokio::spawn(async move {
|
||||
db.mutate(|v| {
|
||||
v.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_status_info_mut()
|
||||
.as_backup_progress_mut()
|
||||
.ser(&None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, old_password, password))]
|
||||
pub async fn backup_all(
|
||||
ctx: RpcContext,
|
||||
@@ -57,139 +161,81 @@ pub async fn backup_all(
|
||||
.clone()
|
||||
.decrypt(&ctx)?;
|
||||
let password = password.decrypt(&ctx)?;
|
||||
check_password_against_db(ctx.secret_store.acquire().await?.as_mut(), &password).await?;
|
||||
let fs = target_id
|
||||
.load(ctx.secret_store.acquire().await?.as_mut())
|
||||
.await?;
|
||||
|
||||
let ((fs, package_ids), status_guard) = (
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
check_password_against_db(db, &password)?;
|
||||
let fs = target_id.load(db)?;
|
||||
let package_ids = if let Some(ids) = package_ids {
|
||||
ids.into_iter().collect()
|
||||
} else {
|
||||
db.as_public()
|
||||
.as_package_data()
|
||||
.as_entries()?
|
||||
.into_iter()
|
||||
.filter(|(_, m)| m.expect_as_installed().is_ok())
|
||||
.map(|(id, _)| id)
|
||||
.collect()
|
||||
};
|
||||
assure_backing_up(db, &package_ids)?;
|
||||
Ok((fs, package_ids))
|
||||
})
|
||||
.await?,
|
||||
BackupStatusGuard::new(ctx.db.clone()),
|
||||
);
|
||||
|
||||
let mut backup_guard = BackupMountGuard::mount(
|
||||
TmpMountGuard::mount(&fs, ReadWrite).await?,
|
||||
&old_password_decrypted,
|
||||
)
|
||||
.await?;
|
||||
let package_ids = if let Some(ids) = package_ids {
|
||||
ids.into_iter().collect()
|
||||
} else {
|
||||
todo!("all installed packages");
|
||||
};
|
||||
if old_password.is_some() {
|
||||
backup_guard.change_password(&password)?;
|
||||
}
|
||||
assure_backing_up(&ctx.db, &package_ids).await?;
|
||||
tokio::task::spawn(async move {
|
||||
let backup_res = perform_backup(&ctx, backup_guard, &package_ids).await;
|
||||
match backup_res {
|
||||
Ok(report) if report.iter().all(|(_, rep)| rep.error.is_none()) => ctx
|
||||
.notification_manager
|
||||
.notify(
|
||||
ctx.db.clone(),
|
||||
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(
|
||||
ctx.db.clone(),
|
||||
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(
|
||||
ctx.db.clone(),
|
||||
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");
|
||||
}
|
||||
}
|
||||
ctx.db
|
||||
.mutate(|v| {
|
||||
v.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_status_info_mut()
|
||||
.as_backup_progress_mut()
|
||||
.ser(&None)
|
||||
})
|
||||
.await?;
|
||||
Ok::<(), Error>(())
|
||||
status_guard
|
||||
.handle_result(perform_backup(&ctx, backup_guard, &package_ids).await)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(db, packages))]
|
||||
async fn assure_backing_up(
|
||||
db: &PatchDb,
|
||||
packages: impl IntoIterator<Item = &PackageId> + UnwindSafe + Send,
|
||||
fn assure_backing_up<'a>(
|
||||
db: &mut DatabaseModel,
|
||||
packages: impl IntoIterator<Item = &'a PackageId>,
|
||||
) -> Result<(), Error> {
|
||||
db.mutate(|v| {
|
||||
let backing_up = v
|
||||
.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_status_info_mut()
|
||||
.as_backup_progress_mut();
|
||||
if backing_up
|
||||
.clone()
|
||||
.de()?
|
||||
.iter()
|
||||
.flat_map(|x| x.values())
|
||||
.fold(false, |acc, x| {
|
||||
if !x.complete {
|
||||
return true;
|
||||
}
|
||||
acc
|
||||
})
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!("Server is already backing up!"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
backing_up.ser(&Some(
|
||||
packages
|
||||
.into_iter()
|
||||
.map(|x| (x.clone(), BackupProgress { complete: false }))
|
||||
.collect(),
|
||||
))?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
let backing_up = db
|
||||
.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_status_info_mut()
|
||||
.as_backup_progress_mut();
|
||||
if backing_up
|
||||
.clone()
|
||||
.de()?
|
||||
.iter()
|
||||
.flat_map(|x| x.values())
|
||||
.fold(false, |acc, x| {
|
||||
if !x.complete {
|
||||
return true;
|
||||
}
|
||||
acc
|
||||
})
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!("Server is already backing up!"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
backing_up.ser(&Some(
|
||||
packages
|
||||
.into_iter()
|
||||
.map(|x| (x.clone(), BackupProgress { complete: false }))
|
||||
.collect(),
|
||||
))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, backup_guard))]
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use openssl::pkey::PKey;
|
||||
use openssl::pkey::{PKey, Private};
|
||||
use openssl::x509::X509;
|
||||
use patch_db::Value;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssh_key::private::Ed25519Keypair;
|
||||
use torut::onion::TorSecretKeyV3;
|
||||
|
||||
use crate::account::AccountInfo;
|
||||
use crate::hostname::{generate_hostname, generate_id, Hostname};
|
||||
use crate::net::keys::Key;
|
||||
use crate::prelude::*;
|
||||
use crate::util::serde::Base64;
|
||||
use crate::util::crypto::ed25519_expand_key;
|
||||
use crate::util::serde::{Base32, Base64, Pem};
|
||||
|
||||
pub struct OsBackup {
|
||||
pub account: AccountInfo,
|
||||
@@ -19,19 +21,23 @@ impl<'de> Deserialize<'de> for OsBackup {
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let tagged = OsBackupSerDe::deserialize(deserializer)?;
|
||||
match tagged.version {
|
||||
Ok(match tagged.version {
|
||||
0 => patch_db::value::from_value::<OsBackupV0>(tagged.rest)
|
||||
.map_err(serde::de::Error::custom)?
|
||||
.project()
|
||||
.map_err(serde::de::Error::custom),
|
||||
.map_err(serde::de::Error::custom)?,
|
||||
1 => patch_db::value::from_value::<OsBackupV1>(tagged.rest)
|
||||
.map_err(serde::de::Error::custom)?
|
||||
.project()
|
||||
.map_err(serde::de::Error::custom),
|
||||
v => Err(serde::de::Error::custom(&format!(
|
||||
"Unknown backup version {v}"
|
||||
))),
|
||||
}
|
||||
.project(),
|
||||
2 => patch_db::value::from_value::<OsBackupV2>(tagged.rest)
|
||||
.map_err(serde::de::Error::custom)?
|
||||
.project(),
|
||||
v => {
|
||||
return Err(serde::de::Error::custom(&format!(
|
||||
"Unknown backup version {v}"
|
||||
)))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Serialize for OsBackup {
|
||||
@@ -40,11 +46,9 @@ impl Serialize for OsBackup {
|
||||
S: serde::Serializer,
|
||||
{
|
||||
OsBackupSerDe {
|
||||
version: 1,
|
||||
rest: patch_db::value::to_value(
|
||||
&OsBackupV1::unproject(self).map_err(serde::ser::Error::custom)?,
|
||||
)
|
||||
.map_err(serde::ser::Error::custom)?,
|
||||
version: 2,
|
||||
rest: patch_db::value::to_value(&OsBackupV2::unproject(self))
|
||||
.map_err(serde::ser::Error::custom)?,
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
@@ -62,10 +66,10 @@ struct OsBackupSerDe {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename = "kebab-case")]
|
||||
struct OsBackupV0 {
|
||||
// tor_key: Base32<[u8; 64]>,
|
||||
root_ca_key: String, // PEM Encoded OpenSSL Key
|
||||
root_ca_cert: String, // PEM Encoded OpenSSL X509 Certificate
|
||||
ui: Value, // JSON Value
|
||||
tor_key: Base32<[u8; 64]>, // Base32 Encoded Ed25519 Expanded Secret Key
|
||||
root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key
|
||||
root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate
|
||||
ui: Value, // JSON Value
|
||||
}
|
||||
impl OsBackupV0 {
|
||||
fn project(self) -> Result<OsBackup, Error> {
|
||||
@@ -74,9 +78,13 @@ impl OsBackupV0 {
|
||||
server_id: generate_id(),
|
||||
hostname: generate_hostname(),
|
||||
password: Default::default(),
|
||||
key: Key::new(None),
|
||||
root_ca_key: PKey::private_key_from_pem(self.root_ca_key.as_bytes())?,
|
||||
root_ca_cert: X509::from_pem(self.root_ca_cert.as_bytes())?,
|
||||
root_ca_key: self.root_ca_key.0,
|
||||
root_ca_cert: self.root_ca_cert.0,
|
||||
ssh_key: ssh_key::PrivateKey::random(
|
||||
&mut rand::thread_rng(),
|
||||
ssh_key::Algorithm::Ed25519,
|
||||
)?,
|
||||
tor_key: TorSecretKeyV3::from(self.tor_key.0),
|
||||
},
|
||||
ui: self.ui,
|
||||
})
|
||||
@@ -87,36 +95,67 @@ impl OsBackupV0 {
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename = "kebab-case")]
|
||||
struct OsBackupV1 {
|
||||
server_id: String, // uuidv4
|
||||
hostname: String, // embassy-<adjective>-<noun>
|
||||
net_key: Base64<[u8; 32]>, // Ed25519 Secret Key
|
||||
root_ca_key: String, // PEM Encoded OpenSSL Key
|
||||
root_ca_cert: String, // PEM Encoded OpenSSL X509 Certificate
|
||||
ui: Value, // JSON Value
|
||||
// TODO add more
|
||||
server_id: String, // uuidv4
|
||||
hostname: String, // embassy-<adjective>-<noun>
|
||||
net_key: Base64<[u8; 32]>, // Ed25519 Secret Key
|
||||
root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key
|
||||
root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate
|
||||
ui: Value, // JSON Value
|
||||
}
|
||||
impl OsBackupV1 {
|
||||
fn project(self) -> Result<OsBackup, Error> {
|
||||
Ok(OsBackup {
|
||||
fn project(self) -> OsBackup {
|
||||
OsBackup {
|
||||
account: AccountInfo {
|
||||
server_id: self.server_id,
|
||||
hostname: Hostname(self.hostname),
|
||||
password: Default::default(),
|
||||
key: Key::from_bytes(None, self.net_key.0),
|
||||
root_ca_key: PKey::private_key_from_pem(self.root_ca_key.as_bytes())?,
|
||||
root_ca_cert: X509::from_pem(self.root_ca_cert.as_bytes())?,
|
||||
root_ca_key: self.root_ca_key.0,
|
||||
root_ca_cert: self.root_ca_cert.0,
|
||||
ssh_key: ssh_key::PrivateKey::from(Ed25519Keypair::from_seed(&self.net_key.0)),
|
||||
tor_key: TorSecretKeyV3::from(ed25519_expand_key(&self.net_key.0)),
|
||||
},
|
||||
ui: self.ui,
|
||||
})
|
||||
}
|
||||
fn unproject(backup: &OsBackup) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
server_id: backup.account.server_id.clone(),
|
||||
hostname: backup.account.hostname.0.clone(),
|
||||
net_key: Base64(backup.account.key.as_bytes()),
|
||||
root_ca_key: String::from_utf8(backup.account.root_ca_key.private_key_to_pem_pkcs8()?)?,
|
||||
root_ca_cert: String::from_utf8(backup.account.root_ca_cert.to_pem()?)?,
|
||||
ui: backup.ui.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// V2
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename = "kebab-case")]
|
||||
|
||||
struct OsBackupV2 {
|
||||
server_id: String, // uuidv4
|
||||
hostname: String, // <adjective>-<noun>
|
||||
root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key
|
||||
root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate
|
||||
ssh_key: Pem<ssh_key::PrivateKey>, // PEM Encoded OpenSSH Key
|
||||
tor_key: TorSecretKeyV3, // Base64 Encoded Ed25519 Expanded Secret Key
|
||||
ui: Value, // JSON Value
|
||||
}
|
||||
impl OsBackupV2 {
|
||||
fn project(self) -> OsBackup {
|
||||
OsBackup {
|
||||
account: AccountInfo {
|
||||
server_id: self.server_id,
|
||||
hostname: Hostname(self.hostname),
|
||||
password: Default::default(),
|
||||
root_ca_key: self.root_ca_key.0,
|
||||
root_ca_cert: self.root_ca_cert.0,
|
||||
ssh_key: self.ssh_key.0,
|
||||
tor_key: self.tor_key,
|
||||
},
|
||||
ui: self.ui,
|
||||
}
|
||||
}
|
||||
fn unproject(backup: &OsBackup) -> Self {
|
||||
Self {
|
||||
server_id: backup.account.server_id.clone(),
|
||||
hostname: backup.account.hostname.0.clone(),
|
||||
root_ca_key: Pem(backup.account.root_ca_key.clone()),
|
||||
root_ca_cert: Pem(backup.account.root_ca_cert.clone()),
|
||||
ssh_key: Pem(backup.account.ssh_key.clone()),
|
||||
tor_key: backup.account.tor_key.clone(),
|
||||
ui: backup.ui.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use clap::Parser;
|
||||
use futures::{stream, StreamExt};
|
||||
use models::PackageId;
|
||||
use openssl::x509::X509;
|
||||
use patch_db::json_ptr::ROOT;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use torut::onion::OnionAddressV3;
|
||||
use tracing::instrument;
|
||||
@@ -12,6 +13,7 @@ use tracing::instrument;
|
||||
use super::target::BackupTargetId;
|
||||
use crate::backup::os::OsBackup;
|
||||
use crate::context::{RpcContext, SetupContext};
|
||||
use crate::db::model::Database;
|
||||
use crate::disk::mount::backup::BackupMountGuard;
|
||||
use crate::disk::mount::filesystem::ReadWrite;
|
||||
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
|
||||
@@ -42,9 +44,7 @@ pub async fn restore_packages_rpc(
|
||||
password,
|
||||
}: RestorePackageParams,
|
||||
) -> Result<(), Error> {
|
||||
let fs = target_id
|
||||
.load(ctx.secret_store.acquire().await?.as_mut())
|
||||
.await?;
|
||||
let fs = target_id.load(&ctx.db.peek().await)?;
|
||||
let backup_guard =
|
||||
BackupMountGuard::mount(TmpMountGuard::mount(&fs, ReadWrite).await?, &password).await?;
|
||||
|
||||
@@ -95,11 +95,8 @@ pub async fn recover_full_embassy(
|
||||
)
|
||||
.with_kind(ErrorKind::PasswordHashGeneration)?;
|
||||
|
||||
let secret_store = ctx.secret_store().await?;
|
||||
|
||||
os_backup.account.save(&secret_store).await?;
|
||||
|
||||
secret_store.close().await;
|
||||
let db = ctx.db().await?;
|
||||
db.put(&ROOT, &Database::init(&os_backup.account)?).await?;
|
||||
|
||||
init(&ctx.config).await?;
|
||||
|
||||
@@ -129,7 +126,7 @@ pub async fn recover_full_embassy(
|
||||
Ok((
|
||||
disk_guid,
|
||||
os_backup.account.hostname,
|
||||
os_backup.account.key.tor_address(),
|
||||
os_backup.account.tor_key.public().get_onion_address(),
|
||||
os_backup.account.root_ca_cert,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::TryStreamExt;
|
||||
use imbl_value::InternedString;
|
||||
use rpc_toolkit::{command, from_fn_async, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Executor, Postgres};
|
||||
|
||||
use super::{BackupTarget, BackupTargetId};
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::DatabaseModel;
|
||||
use crate::disk::mount::filesystem::cifs::Cifs;
|
||||
use crate::disk::mount::filesystem::ReadOnly;
|
||||
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
|
||||
@@ -16,6 +17,24 @@ use crate::disk::util::{recovery_info, EmbassyOsRecoveryInfo};
|
||||
use crate::prelude::*;
|
||||
use crate::util::serde::KeyVal;
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
pub struct CifsTargets(pub BTreeMap<u32, Cifs>);
|
||||
impl CifsTargets {
|
||||
pub fn new() -> Self {
|
||||
Self(BTreeMap::new())
|
||||
}
|
||||
}
|
||||
impl Map for CifsTargets {
|
||||
type Key = u32;
|
||||
type Value = Cifs;
|
||||
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
|
||||
Self::key_string(key)
|
||||
}
|
||||
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
|
||||
Ok(InternedString::from_display(key))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct CifsBackupTarget {
|
||||
@@ -69,23 +88,27 @@ pub async fn add(
|
||||
) -> Result<KeyVal<BackupTargetId, BackupTarget>, Error> {
|
||||
let cifs = Cifs {
|
||||
hostname,
|
||||
path,
|
||||
path: Path::new("/").join(path),
|
||||
username,
|
||||
password,
|
||||
};
|
||||
let guard = TmpMountGuard::mount(&cifs, ReadOnly).await?;
|
||||
let embassy_os = recovery_info(guard.path()).await?;
|
||||
guard.unmount().await?;
|
||||
let path_string = Path::new("/").join(&cifs.path).display().to_string();
|
||||
let id: i32 = sqlx::query!(
|
||||
"INSERT INTO cifs_shares (hostname, path, username, password) VALUES ($1, $2, $3, $4) RETURNING id",
|
||||
cifs.hostname,
|
||||
path_string,
|
||||
cifs.username,
|
||||
cifs.password,
|
||||
)
|
||||
.fetch_one(&ctx.secret_store)
|
||||
.await?.id;
|
||||
let id = ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let id = db
|
||||
.as_private()
|
||||
.as_cifs()
|
||||
.keys()?
|
||||
.into_iter()
|
||||
.max()
|
||||
.map_or(0, |a| a + 1);
|
||||
db.as_private_mut().as_cifs_mut().insert(&id, &cifs)?;
|
||||
Ok(id)
|
||||
})
|
||||
.await?;
|
||||
Ok(KeyVal {
|
||||
key: BackupTargetId::Cifs { id },
|
||||
value: BackupTarget::Cifs(CifsBackupTarget {
|
||||
@@ -129,32 +152,27 @@ pub async fn update(
|
||||
};
|
||||
let cifs = Cifs {
|
||||
hostname,
|
||||
path,
|
||||
path: Path::new("/").join(path),
|
||||
username,
|
||||
password,
|
||||
};
|
||||
let guard = TmpMountGuard::mount(&cifs, ReadOnly).await?;
|
||||
let embassy_os = recovery_info(guard.path()).await?;
|
||||
guard.unmount().await?;
|
||||
let path_string = Path::new("/").join(&cifs.path).display().to_string();
|
||||
if sqlx::query!(
|
||||
"UPDATE cifs_shares SET hostname = $1, path = $2, username = $3, password = $4 WHERE id = $5",
|
||||
cifs.hostname,
|
||||
path_string,
|
||||
cifs.username,
|
||||
cifs.password,
|
||||
id,
|
||||
)
|
||||
.execute(&ctx.secret_store)
|
||||
.await?
|
||||
.rows_affected()
|
||||
== 0
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!("Backup Target ID {} Not Found", BackupTargetId::Cifs { id }),
|
||||
ErrorKind::NotFound,
|
||||
));
|
||||
};
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_private_mut()
|
||||
.as_cifs_mut()
|
||||
.as_idx_mut(&id)
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("Backup Target ID {} Not Found", BackupTargetId::Cifs { id }),
|
||||
ErrorKind::NotFound,
|
||||
)
|
||||
})?
|
||||
.ser(&cifs)
|
||||
})
|
||||
.await?;
|
||||
Ok(KeyVal {
|
||||
key: BackupTargetId::Cifs { id },
|
||||
value: BackupTarget::Cifs(CifsBackupTarget {
|
||||
@@ -183,74 +201,46 @@ pub async fn remove(ctx: RpcContext, RemoveParams { id }: RemoveParams) -> Resul
|
||||
ErrorKind::NotFound,
|
||||
));
|
||||
};
|
||||
if sqlx::query!("DELETE FROM cifs_shares WHERE id = $1", id)
|
||||
.execute(&ctx.secret_store)
|
||||
.await?
|
||||
.rows_affected()
|
||||
== 0
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!("Backup Target ID {} Not Found", BackupTargetId::Cifs { id }),
|
||||
ErrorKind::NotFound,
|
||||
));
|
||||
};
|
||||
ctx.db
|
||||
.mutate(|db| db.as_private_mut().as_cifs_mut().remove(&id))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load<Ex>(secrets: &mut Ex, id: i32) -> Result<Cifs, Error>
|
||||
where
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
let record = sqlx::query!(
|
||||
"SELECT hostname, path, username, password FROM cifs_shares WHERE id = $1",
|
||||
id
|
||||
)
|
||||
.fetch_one(secrets)
|
||||
.await?;
|
||||
|
||||
Ok(Cifs {
|
||||
hostname: record.hostname,
|
||||
path: PathBuf::from(record.path),
|
||||
username: record.username,
|
||||
password: record.password,
|
||||
})
|
||||
pub fn load(db: &DatabaseModel, id: u32) -> Result<Cifs, Error> {
|
||||
db.as_private()
|
||||
.as_cifs()
|
||||
.as_idx(&id)
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("Backup Target ID {} Not Found", id),
|
||||
ErrorKind::NotFound,
|
||||
)
|
||||
})?
|
||||
.de()
|
||||
}
|
||||
|
||||
pub async fn list<Ex>(secrets: &mut Ex) -> Result<Vec<(i32, CifsBackupTarget)>, Error>
|
||||
where
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
let mut records =
|
||||
sqlx::query!("SELECT id, hostname, path, username, password FROM cifs_shares")
|
||||
.fetch_many(secrets);
|
||||
|
||||
pub async fn list(db: &DatabaseModel) -> Result<Vec<(u32, CifsBackupTarget)>, Error> {
|
||||
let mut cifs = Vec::new();
|
||||
while let Some(query_result) = records.try_next().await? {
|
||||
if let Some(record) = query_result.right() {
|
||||
let mount_info = Cifs {
|
||||
hostname: record.hostname,
|
||||
path: PathBuf::from(record.path),
|
||||
username: record.username,
|
||||
password: record.password,
|
||||
};
|
||||
let embassy_os = async {
|
||||
let guard = TmpMountGuard::mount(&mount_info, ReadOnly).await?;
|
||||
let embassy_os = recovery_info(guard.path()).await?;
|
||||
guard.unmount().await?;
|
||||
Ok::<_, Error>(embassy_os)
|
||||
}
|
||||
.await;
|
||||
cifs.push((
|
||||
record.id,
|
||||
CifsBackupTarget {
|
||||
hostname: mount_info.hostname,
|
||||
path: mount_info.path,
|
||||
username: mount_info.username,
|
||||
mountable: embassy_os.is_ok(),
|
||||
embassy_os: embassy_os.ok().and_then(|a| a),
|
||||
},
|
||||
));
|
||||
for (id, model) in db.as_private().as_cifs().as_entries()? {
|
||||
let mount_info = model.de()?;
|
||||
let embassy_os = async {
|
||||
let guard = TmpMountGuard::mount(&mount_info, ReadOnly).await?;
|
||||
let embassy_os = recovery_info(guard.path()).await?;
|
||||
guard.unmount().await?;
|
||||
Ok::<_, Error>(embassy_os)
|
||||
}
|
||||
.await;
|
||||
cifs.push((
|
||||
id,
|
||||
CifsBackupTarget {
|
||||
hostname: mount_info.hostname,
|
||||
path: mount_info.path,
|
||||
username: mount_info.username,
|
||||
mountable: embassy_os.is_ok(),
|
||||
embassy_os: embassy_os.ok().and_then(|a| a),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
Ok(cifs)
|
||||
|
||||
@@ -11,12 +11,12 @@ use models::PackageId;
|
||||
use rpc_toolkit::{command, from_fn_async, AnyContext, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use sqlx::{Executor, Postgres};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::instrument;
|
||||
|
||||
use self::cifs::CifsBackupTarget;
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::DatabaseModel;
|
||||
use crate::disk::mount::backup::BackupMountGuard;
|
||||
use crate::disk::mount::filesystem::block_dev::BlockDev;
|
||||
use crate::disk::mount::filesystem::cifs::Cifs;
|
||||
@@ -49,18 +49,15 @@ pub enum BackupTarget {
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
pub enum BackupTargetId {
|
||||
Disk { logicalname: PathBuf },
|
||||
Cifs { id: i32 },
|
||||
Cifs { id: u32 },
|
||||
}
|
||||
impl BackupTargetId {
|
||||
pub async fn load<Ex>(self, secrets: &mut Ex) -> Result<BackupTargetFS, Error>
|
||||
where
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
pub fn load(self, db: &DatabaseModel) -> Result<BackupTargetFS, Error> {
|
||||
Ok(match self {
|
||||
BackupTargetId::Disk { logicalname } => {
|
||||
BackupTargetFS::Disk(BlockDev::new(logicalname))
|
||||
}
|
||||
BackupTargetId::Cifs { id } => BackupTargetFS::Cifs(cifs::load(secrets, id).await?),
|
||||
BackupTargetId::Cifs { id } => BackupTargetFS::Cifs(cifs::load(db, id)?),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -161,10 +158,10 @@ pub fn target() -> ParentHandler {
|
||||
|
||||
// #[command(display(display_serializable))]
|
||||
pub async fn list(ctx: RpcContext) -> Result<BTreeMap<BackupTargetId, BackupTarget>, Error> {
|
||||
let mut sql_handle = ctx.secret_store.acquire().await?;
|
||||
let peek = ctx.db.peek().await;
|
||||
let (disks_res, cifs) = tokio::try_join!(
|
||||
crate::disk::util::list(&ctx.os_partitions),
|
||||
cifs::list(sql_handle.as_mut()),
|
||||
cifs::list(&peek),
|
||||
)?;
|
||||
Ok(disks_res
|
||||
.into_iter()
|
||||
@@ -262,13 +259,7 @@ pub async fn info(
|
||||
}: InfoParams,
|
||||
) -> Result<BackupInfo, Error> {
|
||||
let guard = BackupMountGuard::mount(
|
||||
TmpMountGuard::mount(
|
||||
&target_id
|
||||
.load(ctx.secret_store.acquire().await?.as_mut())
|
||||
.await?,
|
||||
ReadWrite,
|
||||
)
|
||||
.await?,
|
||||
TmpMountGuard::mount(&target_id.load(&ctx.db.peek().await)?, ReadWrite).await?,
|
||||
&password,
|
||||
)
|
||||
.await?;
|
||||
@@ -308,14 +299,7 @@ pub async fn mount(
|
||||
}
|
||||
|
||||
let guard = BackupMountGuard::mount(
|
||||
TmpMountGuard::mount(
|
||||
&target_id
|
||||
.clone()
|
||||
.load(ctx.secret_store.acquire().await?.as_mut())
|
||||
.await?,
|
||||
ReadWrite,
|
||||
)
|
||||
.await?,
|
||||
TmpMountGuard::mount(&target_id.clone().load(&ctx.db.peek().await)?, ReadWrite).await?,
|
||||
&password,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Reference in New Issue
Block a user