Feature/backup+restore (#2613)

* feat: Implementation on the backup for the service.

* wip: Getting the flow of backup/restore

* feat: Recover

* Feature: Commit the full pass on the backup restore.

* use special type for backup instead of special id (#2614)

* fix: Allow compat docker style to run again

* fix: Backup for the js side

* chore: Update some of the callbacks

---------

Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
Jade
2024-05-06 15:46:36 -06:00
committed by GitHub
parent 9b14d714ca
commit 30aabe255b
21 changed files with 415 additions and 102 deletions

View File

@@ -81,7 +81,7 @@ const callbackType = object({
id: idType, id: idType,
method: literal("callback"), method: literal("callback"),
params: object({ params: object({
callback: string, callback: number,
args: array, args: array,
}), }),
}) })

View File

@@ -68,7 +68,7 @@ export class DockerProcedureContainer {
}, },
}) })
} else if (volumeMount.type === "backup") { } else if (volumeMount.type === "backup") {
throw new Error("TODO") await overlay.mount({ type: "backup", subpath: null }, mounts[mount])
} }
} }
} }

View File

@@ -145,6 +145,15 @@ export class MainLoop {
...actionProcedure.args, ...actionProcedure.args,
JSON.stringify(timeChanged), JSON.stringify(timeChanged),
]) ])
if (executed.exitCode === 0) {
await effects.setHealth({
id: healthId,
name: value.name,
result: "success",
message: actionProcedure["success-message"],
})
return
}
if (executed.exitCode === 59) { if (executed.exitCode === 59) {
await effects.setHealth({ await effects.setHealth({
id: healthId, id: healthId,

View File

@@ -399,11 +399,10 @@ export class SystemForEmbassy implements System {
): Promise<void> { ): Promise<void> {
const backup = this.manifest.backup.create const backup = this.manifest.backup.create
if (backup.type === "docker") { if (backup.type === "docker") {
const container = await DockerProcedureContainer.of( const container = await DockerProcedureContainer.of(effects, backup, {
effects, ...this.manifest.volumes,
backup, BACKUP: { type: "backup", readonly: false },
this.manifest.volumes, })
)
await container.execFail([backup.entrypoint, ...backup.args], timeoutMs) await container.execFail([backup.entrypoint, ...backup.args], timeoutMs)
} else { } else {
const moduleCode = await this.moduleCode const moduleCode = await this.moduleCode
@@ -421,7 +420,10 @@ export class SystemForEmbassy implements System {
const container = await DockerProcedureContainer.of( const container = await DockerProcedureContainer.of(
effects, effects,
restoreBackup, restoreBackup,
this.manifest.volumes, {
...this.manifest.volumes,
BACKUP: { type: "backup", readonly: true },
},
) )
await container.execFail( await container.execFail(
[restoreBackup.entrypoint, ...restoreBackup.args], [restoreBackup.entrypoint, ...restoreBackup.args],
@@ -664,46 +666,6 @@ export class SystemForEmbassy implements System {
} }
throw new Error(`Unknown type in the fetch properties: ${setConfigValue}`) throw new Error(`Unknown type in the fetch properties: ${setConfigValue}`)
} }
private async health(
effects: HostSystemStartOs,
healthId: string,
timeSinceStarted: unknown,
timeoutMs: number | null,
): Promise<void> {
const healthProcedure = this.manifest["health-checks"][healthId]
if (!healthProcedure) return
if (healthProcedure.type === "docker") {
const container = await DockerProcedureContainer.of(
effects,
healthProcedure,
this.manifest.volumes,
)
return JSON.parse(
(
await container.execFail(
[
healthProcedure.entrypoint,
...healthProcedure.args,
JSON.stringify(timeSinceStarted),
],
timeoutMs,
)
).stdout.toString(),
)
} else if (healthProcedure.type === "script") {
const moduleCode = await this.moduleCode
const method = moduleCode.health?.[healthId]
if (!method) throw new Error("Expecting that the method health exists")
await method(
new PolyfillEffects(effects, this.manifest),
Number(timeSinceStarted),
).then((x) => {
if ("result" in x) return x.result
if ("error" in x) throw new Error("Error getting config: " + x.error)
throw new Error("Error getting config: " + x["error-code"][1])
})
}
}
private async action( private async action(
effects: HostSystemStartOs, effects: HostSystemStartOs,
actionId: string, actionId: string,

View File

@@ -57,6 +57,7 @@ export const matchManifest = object(
matchProcedure, matchProcedure,
object({ object({
name: string, name: string,
["success-message"]: string,
}), }),
), ),
]), ]),

View File

@@ -267,6 +267,7 @@ export class PolyfillEffects implements oet.Effects {
json: () => fetched.json(), json: () => fetched.json(),
} }
} }
runRsync(rsyncOptions: { runRsync(rsyncOptions: {
srcVolume: string srcVolume: string
dstVolume: string dstVolume: string
@@ -277,6 +278,36 @@ export class PolyfillEffects implements oet.Effects {
id: () => Promise<string> id: () => Promise<string>
wait: () => Promise<null> wait: () => Promise<null>
progress: () => Promise<number> progress: () => Promise<number>
} {
let secondRun: ReturnType<typeof this._runRsync> | undefined
let firstRun = this._runRsync(rsyncOptions)
let waitValue = firstRun.wait().then((x) => {
secondRun = this._runRsync(rsyncOptions)
return secondRun.wait()
})
const id = async () => {
return secondRun?.id?.() ?? firstRun.id()
}
const wait = () => waitValue
const progress = async () => {
const secondProgress = secondRun?.progress?.()
if (secondProgress) {
return (await secondProgress) / 2.0 + 0.5
}
return (await firstRun.progress()) / 2.0
}
return { id, wait, progress }
}
_runRsync(rsyncOptions: {
srcVolume: string
dstVolume: string
srcPath: string
dstPath: string
options: oet.BackupOptions
}): {
id: () => Promise<string>
wait: () => Promise<null>
progress: () => Promise<number>
} { } {
const { srcVolume, dstVolume, srcPath, dstPath, options } = rsyncOptions const { srcVolume, dstVolume, srcPath, dstPath, options } = rsyncOptions
const command = "rsync" const command = "rsync"

View File

@@ -2,16 +2,16 @@ export class CallbackHolder {
constructor() {} constructor() {}
private root = (Math.random() + 1).toString(36).substring(7) private root = (Math.random() + 1).toString(36).substring(7)
private inc = 0 private inc = 0
private callbacks = new Map<string, Function>() private callbacks = new Map<number, Function>()
private newId() { private newId() {
return this.root + (this.inc++).toString(36) return this.inc++
} }
addCallback(callback: Function) { addCallback(callback: Function) {
const id = this.newId() const id = this.newId()
this.callbacks.set(id, callback) this.callbacks.set(id, callback)
return id return id
} }
callCallback(index: string, args: any[]): Promise<unknown> { callCallback(index: number, args: any[]): Promise<unknown> {
const callback = this.callbacks.get(index) const callback = this.callbacks.get(index)
if (!callback) throw new Error(`Callback ${index} does not exist`) if (!callback) throw new Error(`Callback ${index} does not exist`)
this.callbacks.delete(index) this.callbacks.delete(index)

View File

@@ -1,14 +1,17 @@
import * as fs from "node:fs/promises" import * as fs from "node:fs/promises"
export const BACKUP = "backup"
export class Volume { export class Volume {
readonly path: string readonly path: string
constructor( constructor(
readonly volumeId: string, readonly volumeId: string,
_path = "", _path = "",
) { ) {
const path = (this.path = `/media/startos/volumes/${volumeId}${ if (volumeId.toLowerCase() === BACKUP) {
!_path ? "" : `/${_path}` this.path = `/media/startos/backup${!_path ? "" : `/${_path}`}`
}`) } else {
this.path = `/media/startos/volumes/${volumeId}${!_path ? "" : `/${_path}`}`
}
} }
async exists() { async exists() {
return fs.stat(this.path).then( return fs.stat(this.path).then(

View File

@@ -13,7 +13,7 @@ use tokio::io::AsyncWriteExt;
use tracing::instrument; use tracing::instrument;
use ts_rs::TS; use ts_rs::TS;
use super::target::BackupTargetId; use super::target::{BackupTargetId, PackageBackupInfo};
use super::PackageBackupReport; use super::PackageBackupReport;
use crate::auth::check_password_against_db; use crate::auth::check_password_against_db;
use crate::backup::os::OsBackup; use crate::backup::os::OsBackup;
@@ -246,19 +246,43 @@ async fn perform_backup(
backup_guard: BackupMountGuard<TmpMountGuard>, backup_guard: BackupMountGuard<TmpMountGuard>,
package_ids: &OrdSet<PackageId>, package_ids: &OrdSet<PackageId>,
) -> Result<BTreeMap<PackageId, PackageBackupReport>, Error> { ) -> Result<BTreeMap<PackageId, PackageBackupReport>, Error> {
let db = ctx.db.peek().await;
let mut backup_report = BTreeMap::new(); let mut backup_report = BTreeMap::new();
let backup_guard = Arc::new(backup_guard); let backup_guard = Arc::new(backup_guard);
let mut package_backups: BTreeMap<PackageId, PackageBackupInfo> =
backup_guard.metadata.package_backups.clone();
for id in package_ids { for id in package_ids {
if let Some(service) = &*ctx.services.get(id).await { if let Some(service) = &*ctx.services.get(id).await {
let backup_result = service
.backup(backup_guard.package_backup(id))
.await
.err()
.map(|e| e.to_string());
if backup_result.is_none() {
let manifest = db
.as_public()
.as_package_data()
.as_idx(id)
.or_not_found(id)?
.as_state_info()
.expect_installed()?
.as_manifest();
package_backups.insert(
id.clone(),
PackageBackupInfo {
os_version: manifest.as_os_version().de()?,
version: manifest.as_version().de()?,
title: manifest.as_title().de()?,
timestamp: Utc::now(),
},
);
}
backup_report.insert( backup_report.insert(
id.clone(), id.clone(),
PackageBackupReport { PackageBackupReport {
error: service error: backup_result,
.backup(backup_guard.package_backup(id))
.await
.err()
.map(|e| e.to_string()),
}, },
); );
} }
@@ -298,7 +322,7 @@ async fn perform_backup(
} }
let luks_folder = Path::new("/media/startos/config/luks"); let luks_folder = Path::new("/media/startos/config/luks");
if tokio::fs::metadata(&luks_folder).await.is_ok() { if tokio::fs::metadata(&luks_folder).await.is_ok() {
dir_copy(&luks_folder, &luks_folder_bak, None).await?; dir_copy(luks_folder, &luks_folder_bak, None).await?;
} }
let timestamp = Some(Utc::now()); let timestamp = Some(Utc::now());
@@ -307,8 +331,9 @@ async fn perform_backup(
backup_guard.unencrypted_metadata.full = true; backup_guard.unencrypted_metadata.full = true;
backup_guard.metadata.version = crate::version::Current::new().semver().into(); backup_guard.metadata.version = crate::version::Current::new().semver().into();
backup_guard.metadata.timestamp = timestamp; backup_guard.metadata.timestamp = timestamp;
backup_guard.metadata.package_backups = package_backups;
backup_guard.save_and_unmount().await?; backup_guard.save().await?;
ctx.db ctx.db
.mutate(|v| { .mutate(|v| {

View File

@@ -10,7 +10,7 @@ use persistent_container::PersistentContainer;
use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, HandlerArgs, HandlerFor}; use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, HandlerArgs, HandlerFor};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use start_stop::StartStop; use start_stop::StartStop;
use tokio::sync::Notify; use tokio::{fs::File, sync::Notify};
use ts_rs::TS; use ts_rs::TS;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
@@ -296,13 +296,20 @@ impl Service {
} }
pub async fn restore( pub async fn restore(
_ctx: RpcContext, ctx: RpcContext,
_s9pk: S9pk, s9pk: S9pk,
_guard: impl GenericMountGuard, backup_source: impl GenericMountGuard,
_progress: Option<InstallProgressHandles>, progress: Option<InstallProgressHandles>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
// TODO let service = Service::install(ctx.clone(), s9pk, None, progress).await?;
Err(Error::new(eyre!("not yet implemented"), ErrorKind::Unknown))
service
.actor
.send(transition::restore::Restore {
path: backup_source.path().to_path_buf(),
})
.await?;
Ok(service)
} }
pub async fn shutdown(self) -> Result<(), Error> { pub async fn shutdown(self) -> Result<(), Error> {
@@ -348,9 +355,23 @@ impl Service {
Ok(()) Ok(())
} }
pub async fn backup(&self, _guard: impl GenericMountGuard) -> Result<BackupReturn, Error> { #[instrument(skip_all)]
// TODO pub async fn backup(&self, guard: impl GenericMountGuard) -> Result<(), Error> {
Err(Error::new(eyre!("not yet implemented"), ErrorKind::Unknown)) let id = &self.seed.id;
let mut file = File::create(guard.path().join(id).with_extension("s9pk")).await?;
self.seed
.persistent_container
.s9pk
.clone()
.serialize(&mut file, true)
.await?;
drop(file);
self.actor
.send(transition::backup::Backup {
path: guard.path().to_path_buf(),
})
.await?;
Ok(())
} }
pub fn container_id(&self) -> Result<ContainerId, Error> { pub fn container_id(&self) -> Result<ContainerId, Error> {
@@ -425,6 +446,7 @@ impl Actor for ServiceActor {
kinds.running_status, kinds.running_status,
) { ) {
(Some(TransitionKind::Restarting), _, _) => MainStatus::Restarting, (Some(TransitionKind::Restarting), _, _) => MainStatus::Restarting,
(Some(TransitionKind::Restoring), _, _) => MainStatus::Restoring,
(Some(TransitionKind::BackingUp), _, Some(status)) => { (Some(TransitionKind::BackingUp), _, Some(status)) => {
MainStatus::BackingUp { MainStatus::BackingUp {
started: Some(status.started), started: Some(status.started),

View File

@@ -1,5 +1,5 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::Path; use std::path::{Path, PathBuf};
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use std::time::Duration; use std::time::Duration;
@@ -222,6 +222,40 @@ impl PersistentContainer {
}) })
} }
#[instrument(skip_all)]
pub async fn mount_backup(
&self,
backup_path: impl AsRef<Path>,
mount_type: MountType,
) -> Result<MountGuard, Error> {
let backup_path: PathBuf = backup_path.as_ref().to_path_buf();
let mountpoint = self
.lxc_container
.get()
.ok_or_else(|| {
Error::new(
eyre!("PersistentContainer has been destroyed"),
ErrorKind::Incoherent,
)
})?
.rootfs_dir()
.join("media/startos/backup");
tokio::fs::create_dir_all(&mountpoint).await?;
Command::new("chown")
.arg("100000:100000")
.arg(mountpoint.as_os_str())
.invoke(ErrorKind::Filesystem)
.await?;
let bind = Bind::new(&backup_path);
let mount_guard = MountGuard::mount(&bind, &mountpoint, mount_type).await;
Command::new("chown")
.arg("100000:100000")
.arg(backup_path.as_os_str())
.invoke(ErrorKind::Filesystem)
.await?;
mount_guard
}
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn init(&self, seed: Weak<ServiceActorSeed>) -> Result<(), Error> { pub async fn init(&self, seed: Weak<ServiceActorSeed>) -> Result<(), Error> {
let socket_server_context = EffectContext::new(seed); let socket_server_context = EffectContext::new(seed);

View File

@@ -1070,11 +1070,6 @@ pub async fn create_overlayed_image(
.rootfs_dir(); .rootfs_dir();
let mountpoint = rootfs_dir.join("media/startos/overlays").join(&*guid); let mountpoint = rootfs_dir.join("media/startos/overlays").join(&*guid);
tokio::fs::create_dir_all(&mountpoint).await?; tokio::fs::create_dir_all(&mountpoint).await?;
Command::new("chown")
.arg("100000:100000")
.arg(&mountpoint)
.invoke(ErrorKind::Filesystem)
.await?;
let container_mountpoint = Path::new("/").join( let container_mountpoint = Path::new("/").join(
mountpoint mountpoint
.strip_prefix(rootfs_dir) .strip_prefix(rootfs_dir)
@@ -1083,9 +1078,14 @@ pub async fn create_overlayed_image(
tracing::info!("Mounting overlay {guid} for {image_id}"); tracing::info!("Mounting overlay {guid} for {image_id}");
let guard = OverlayGuard::mount( let guard = OverlayGuard::mount(
&IdMapped::new(LoopDev::from(&**image), 0, 100000, 65536), &IdMapped::new(LoopDev::from(&**image), 0, 100000, 65536),
mountpoint, &mountpoint,
) )
.await?; .await?;
Command::new("chown")
.arg("100000:100000")
.arg(&mountpoint)
.invoke(ErrorKind::Filesystem)
.await?;
tracing::info!("Mounted overlay {guid} for {image_id}"); tracing::info!("Mounted overlay {guid} for {image_id}");
ctx.persistent_container ctx.persistent_container
.overlays .overlays

View File

@@ -15,6 +15,7 @@ impl From<MainStatus> for StartStop {
fn from(value: MainStatus) -> Self { fn from(value: MainStatus) -> Self {
match value { match value {
MainStatus::Stopped => StartStop::Stop, MainStatus::Stopped => StartStop::Stop,
MainStatus::Restoring => StartStop::Stop,
MainStatus::Restarting => StartStop::Start, MainStatus::Restarting => StartStop::Start,
MainStatus::Stopping { .. } => StartStop::Stop, MainStatus::Stopping { .. } => StartStop::Stop,
MainStatus::Starting => StartStop::Start, MainStatus::Starting => StartStop::Start,

View File

@@ -1 +1,94 @@
use std::path::PathBuf;
use futures::FutureExt;
use models::ProcedureName;
use super::TempDesiredRestore;
use crate::disk::mount::filesystem::ReadWrite;
use crate::prelude::*;
use crate::service::config::GetConfig;
use crate::service::dependencies::DependencyConfig;
use crate::service::transition::{TransitionKind, TransitionState};
use crate::service::ServiceActor;
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::actor::{ConflictBuilder, Handler};
use crate::util::future::RemoteCancellable;
pub(in crate::service) struct Backup {
pub path: PathBuf,
}
impl Handler<Backup> for ServiceActor {
type Response = Result<(), Error>;
fn conflicts_with(_: &Backup) -> ConflictBuilder<Self> {
ConflictBuilder::everything()
.except::<GetConfig>()
.except::<DependencyConfig>()
}
async fn handle(&mut self, backup: Backup, jobs: &BackgroundJobQueue) -> Self::Response {
// So Need a handle to just a single field in the state
let temp: TempDesiredRestore = TempDesiredRestore::new(&self.0.persistent_container.state);
let mut current = self.0.persistent_container.state.subscribe();
let path = backup.path.clone();
let seed = self.0.clone();
let state = self.0.persistent_container.state.clone();
let transition = RemoteCancellable::new(
async move {
temp.stop();
current
.wait_for(|s| s.running_status.is_none())
.await
.with_kind(ErrorKind::Unknown)?;
let backup_guard = seed
.persistent_container
.mount_backup(path, ReadWrite)
.await?;
seed.persistent_container
.execute(ProcedureName::CreateBackup, Value::Null, None)
.await?;
backup_guard.unmount(true).await?;
if temp.restore().is_start() {
current
.wait_for(|s| s.running_status.is_some())
.await
.with_kind(ErrorKind::Unknown)?;
}
drop(temp);
state.send_modify(|s| {
s.transition_state.take();
});
Ok::<_, Error>(())
}
.map(|x| {
if let Err(err) = dbg!(x) {
tracing::debug!("{:?}", err);
tracing::warn!("{}", err);
}
}),
);
let cancel_handle = transition.cancellation_handle();
let transition = transition.shared();
let job_transition = transition.clone();
jobs.add_job(job_transition.map(|_| ()));
let mut old = None;
self.0.persistent_container.state.send_modify(|s| {
old = std::mem::replace(
&mut s.transition_state,
Some(TransitionState {
kind: TransitionKind::BackingUp,
cancel_handle,
}),
)
});
if let Some(t) = old {
t.abort().await;
}
match transition.await {
None => Err(Error::new(eyre!("Backup canceled"), ErrorKind::Unknown)),
Some(x) => Ok(x),
}
}
}

View File

@@ -10,11 +10,13 @@ use crate::util::future::{CancellationHandle, RemoteCancellable};
pub mod backup; pub mod backup;
pub mod restart; pub mod restart;
pub mod restore;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum TransitionKind { pub enum TransitionKind {
BackingUp, BackingUp,
Restarting, Restarting,
Restoring,
} }
/// Used only in the manager/mod and is used to keep track of the state of the manager during the /// Used only in the manager/mod and is used to keep track of the state of the manager during the
@@ -59,21 +61,23 @@ impl Drop for TransitionState {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TempDesiredState(pub(super) Arc<watch::Sender<ServiceState>>); pub struct TempDesiredRestore(pub(super) Arc<watch::Sender<ServiceState>>, StartStop);
impl TempDesiredState { impl TempDesiredRestore {
pub fn new(state: &Arc<watch::Sender<ServiceState>>) -> Self { pub fn new(state: &Arc<watch::Sender<ServiceState>>) -> Self {
Self(state.clone()) Self(state.clone(), state.borrow().desired_state)
} }
pub fn stop(&self) { pub fn stop(&self) {
self.0 self.0
.send_modify(|s| s.temp_desired_state = Some(StartStop::Stop)); .send_modify(|s| s.temp_desired_state = Some(StartStop::Stop));
} }
pub fn start(&self) { pub fn restore(&self) -> StartStop {
let restore_state = self.1;
self.0 self.0
.send_modify(|s| s.temp_desired_state = Some(StartStop::Start)); .send_modify(|s| s.temp_desired_state = Some(restore_state));
restore_state
} }
} }
impl Drop for TempDesiredState { impl Drop for TempDesiredRestore {
fn drop(&mut self) { fn drop(&mut self) {
self.0.send_modify(|s| s.temp_desired_state = None); self.0.send_modify(|s| s.temp_desired_state = None);
} }

View File

@@ -1,6 +1,6 @@
use futures::FutureExt; use futures::FutureExt;
use super::TempDesiredState; use super::TempDesiredRestore;
use crate::prelude::*; use crate::prelude::*;
use crate::service::config::GetConfig; use crate::service::config::GetConfig;
use crate::service::dependencies::DependencyConfig; use crate::service::dependencies::DependencyConfig;
@@ -20,18 +20,39 @@ impl Handler<Restart> for ServiceActor {
} }
async fn handle(&mut self, _: Restart, jobs: &BackgroundJobQueue) -> Self::Response { async fn handle(&mut self, _: Restart, jobs: &BackgroundJobQueue) -> Self::Response {
// So Need a handle to just a single field in the state // So Need a handle to just a single field in the state
let temp = TempDesiredState::new(&self.0.persistent_container.state); let temp = TempDesiredRestore::new(&self.0.persistent_container.state);
let mut current = self.0.persistent_container.state.subscribe(); let mut current = self.0.persistent_container.state.subscribe();
let transition = RemoteCancellable::new(async move { let state = self.0.persistent_container.state.clone();
temp.stop(); let transition = RemoteCancellable::new(
current.wait_for(|s| s.running_status.is_none()).await; async move {
temp.start(); temp.stop();
current.wait_for(|s| s.running_status.is_some()).await; current
drop(temp); .wait_for(|s| s.running_status.is_none())
}); .await
.with_kind(ErrorKind::Unknown)?;
if temp.restore().is_start() {
current
.wait_for(|s| s.running_status.is_some())
.await
.with_kind(ErrorKind::Unknown)?;
}
drop(temp);
state.send_modify(|s| {
s.transition_state.take();
});
Ok::<_, Error>(())
}
.map(|x| {
if let Err(err) = x {
tracing::debug!("{:?}", err);
tracing::warn!("{}", err);
}
}),
);
let cancel_handle = transition.cancellation_handle(); let cancel_handle = transition.cancellation_handle();
jobs.add_job(transition.map(|_| ())); let transition = transition.shared();
let notified = self.0.synchronized.notified(); let job_transition = transition.clone();
jobs.add_job(job_transition.map(|_| ()));
let mut old = None; let mut old = None;
self.0.persistent_container.state.send_modify(|s| { self.0.persistent_container.state.send_modify(|s| {
@@ -46,10 +67,13 @@ impl Handler<Restart> for ServiceActor {
if let Some(t) = old { if let Some(t) = old {
t.abort().await; t.abort().await;
} }
notified.await if transition.await.is_none() {
tracing::warn!("Service {} has been cancelled", &self.0.id);
}
} }
} }
impl Service { impl Service {
#[instrument(skip_all)]
pub async fn restart(&self) -> Result<(), Error> { pub async fn restart(&self) -> Result<(), Error> {
self.actor.send(Restart).await self.actor.send(Restart).await
} }

View File

@@ -0,0 +1,74 @@
use std::path::PathBuf;
use futures::FutureExt;
use models::ProcedureName;
use crate::disk::mount::filesystem::ReadOnly;
use crate::prelude::*;
use crate::service::transition::{TransitionKind, TransitionState};
use crate::service::ServiceActor;
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::actor::{ConflictBuilder, Handler};
use crate::util::future::RemoteCancellable;
pub(in crate::service) struct Restore {
pub path: PathBuf,
}
impl Handler<Restore> for ServiceActor {
type Response = Result<(), Error>;
fn conflicts_with(_: &Restore) -> ConflictBuilder<Self> {
ConflictBuilder::everything()
}
async fn handle(&mut self, restore: Restore, jobs: &BackgroundJobQueue) -> Self::Response {
// So Need a handle to just a single field in the state
let path = restore.path.clone();
let seed = self.0.clone();
let state = self.0.persistent_container.state.clone();
let transition = RemoteCancellable::new(
async move {
let backup_guard = seed
.persistent_container
.mount_backup(path, ReadOnly)
.await?;
seed.persistent_container
.execute(ProcedureName::RestoreBackup, Value::Null, None)
.await?;
backup_guard.unmount(true).await?;
state.send_modify(|s| {
s.transition_state.take();
});
Ok::<_, Error>(())
}
.map(|x| {
if let Err(err) = dbg!(x) {
tracing::debug!("{:?}", err);
tracing::warn!("{}", err);
}
}),
);
let cancel_handle = transition.cancellation_handle();
let transition = transition.shared();
let job_transition = transition.clone();
jobs.add_job(job_transition.map(|_| ()));
let mut old = None;
self.0.persistent_container.state.send_modify(|s| {
old = std::mem::replace(
&mut s.transition_state,
Some(TransitionState {
kind: TransitionKind::Restoring,
cancel_handle,
}),
)
});
if let Some(t) = old {
t.abort().await;
}
match transition.await {
None => Err(Error::new(eyre!("Restoring canceled"), ErrorKind::Unknown)),
Some(x) => Ok(x),
}
}
}

View File

@@ -25,6 +25,7 @@ pub struct Status {
pub enum MainStatus { pub enum MainStatus {
Stopped, Stopped,
Restarting, Restarting,
Restoring,
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
Stopping { Stopping {
timeout: crate::util::serde::Duration, timeout: crate::util::serde::Duration,
@@ -54,6 +55,7 @@ impl MainStatus {
started: Some(_), .. started: Some(_), ..
} => true, } => true,
MainStatus::Stopped MainStatus::Stopped
| MainStatus::Restoring
| MainStatus::Stopping { .. } | MainStatus::Stopping { .. }
| MainStatus::Restarting | MainStatus::Restarting
| MainStatus::BackingUp { started: None, .. } => false, | MainStatus::BackingUp { started: None, .. } => false,
@@ -75,6 +77,7 @@ impl MainStatus {
MainStatus::Running { started, .. } => Some(*started), MainStatus::Running { started, .. } => Some(*started),
MainStatus::BackingUp { started, .. } => *started, MainStatus::BackingUp { started, .. } => *started,
MainStatus::Stopped => None, MainStatus::Stopped => None,
MainStatus::Restoring => None,
MainStatus::Restarting => None, MainStatus::Restarting => None,
MainStatus::Stopping { .. } => None, MainStatus::Stopping { .. } => None,
MainStatus::Starting { .. } => None, MainStatus::Starting { .. } => None,
@@ -84,9 +87,10 @@ impl MainStatus {
let (started, health) = match self { let (started, health) = match self {
MainStatus::Starting { .. } => (Some(Utc::now()), Default::default()), MainStatus::Starting { .. } => (Some(Utc::now()), Default::default()),
MainStatus::Running { started, health } => (Some(started.clone()), health.clone()), MainStatus::Running { started, health } => (Some(started.clone()), health.clone()),
MainStatus::Stopped | MainStatus::Stopping { .. } | MainStatus::Restarting => { MainStatus::Stopped
(None, Default::default()) | MainStatus::Stopping { .. }
} | MainStatus::Restoring
| MainStatus::Restarting => (None, Default::default()),
MainStatus::BackingUp { .. } => return self.clone(), MainStatus::BackingUp { .. } => return self.clone(),
}; };
MainStatus::BackingUp { started, health } MainStatus::BackingUp { started, health }
@@ -96,7 +100,10 @@ impl MainStatus {
match self { match self {
MainStatus::Running { health, .. } => Some(health), MainStatus::Running { health, .. } => Some(health),
MainStatus::BackingUp { health, .. } => Some(health), MainStatus::BackingUp { health, .. } => Some(health),
MainStatus::Stopped | MainStatus::Stopping { .. } | MainStatus::Restarting => None, MainStatus::Stopped
| MainStatus::Restoring
| MainStatus::Stopping { .. }
| MainStatus::Restarting => None,
MainStatus::Starting { .. } => None, MainStatus::Starting { .. } => None,
} }
} }

View File

@@ -1,9 +1,12 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AddressInfo } from "./AddressInfo" import type { AddressInfo } from "./AddressInfo"
import type { ExportedHostnameInfo } from "./ExportedHostnameInfo"
import type { HostKind } from "./HostKind"
import type { ServiceInterfaceId } from "./ServiceInterfaceId"
import type { ServiceInterfaceType } from "./ServiceInterfaceType" import type { ServiceInterfaceType } from "./ServiceInterfaceType"
export type ExportServiceInterfaceParams = { export type ExportServiceInterfaceParams = {
id: string id: ServiceInterfaceId
name: string name: string
description: string description: string
hasPrimary: boolean hasPrimary: boolean
@@ -11,4 +14,6 @@ export type ExportServiceInterfaceParams = {
masked: boolean masked: boolean
addressInfo: AddressInfo addressInfo: AddressInfo
type: ServiceInterfaceType type: ServiceInterfaceType
hostKind: HostKind
hostnames: Array<ExportedHostnameInfo>
} }

View File

@@ -6,6 +6,7 @@ import type { HealthCheckResult } from "./HealthCheckResult"
export type MainStatus = export type MainStatus =
| { status: "stopped" } | { status: "stopped" }
| { status: "restarting" } | { status: "restarting" }
| { status: "restoring" }
| { status: "stopping"; timeout: Duration } | { status: "stopping"; timeout: Duration }
| { status: "starting" } | { status: "starting" }
| { | {

View File

@@ -55,6 +55,17 @@ export class Overlay {
]) ])
} else if (options.type === "pointer") { } else if (options.type === "pointer") {
await this.effects.mount({ location: path, target: options }) await this.effects.mount({ location: path, target: options })
} else if (options.type === "backup") {
const subpath = options.subpath
? options.subpath.startsWith("/")
? options.subpath
: `/${options.subpath}`
: "/"
await execFile("mount", [
"--bind",
`/media/startos/backup${subpath}`,
path,
])
} else { } else {
throw new Error(`unknown type ${(options as any).type}`) throw new Error(`unknown type ${(options as any).type}`)
} }
@@ -188,6 +199,7 @@ export type MountOptions =
| MountOptionsVolume | MountOptionsVolume
| MountOptionsAssets | MountOptionsAssets
| MountOptionsPointer | MountOptionsPointer
| MountOptionsBackup
export type MountOptionsVolume = { export type MountOptionsVolume = {
type: "volume" type: "volume"
@@ -209,3 +221,8 @@ export type MountOptionsPointer = {
subpath: string | null subpath: string | null
readonly: boolean readonly: boolean
} }
export type MountOptionsBackup = {
type: "backup"
subpath: string | null
}