mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
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:
@@ -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,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const matchManifest = object(
|
|||||||
matchProcedure,
|
matchProcedure,
|
||||||
object({
|
object({
|
||||||
name: string,
|
name: string,
|
||||||
|
["success-message"]: string,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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| {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
74
core/startos/src/service/transition/restore.rs
Normal file
74
core/startos/src/service/transition/restore.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" }
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user