diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md
index ae5beafcd..e29a084ec 100644
--- a/CHANGELOG.unreleased.md
+++ b/CHANGELOG.unreleased.md
@@ -8,6 +8,7 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [VM export] Feat export to `ova` format (PR [#6006](https://github.com/vatesfr/xen-orchestra/pull/6006))
+- [Backup] Add *Restore Health Check*: ensure a backup is viable by doing an automatic test restore (requires guest tools in the VM) [#6148](https://github.com/vatesfr/xen-orchestra/pull/6148)
### Bug fixes
diff --git a/packages/xo-server/src/api/backup-ng.mjs b/packages/xo-server/src/api/backup-ng.mjs
index bd27619ae..d7c25ce2c 100644
--- a/packages/xo-server/src/api/backup-ng.mjs
+++ b/packages/xo-server/src/api/backup-ng.mjs
@@ -268,6 +268,24 @@ importVmBackup.params = {
},
}
+export function checkBackup({ id, settings, sr }) {
+ return this.checkVmBackupNg(id, sr, settings)
+}
+
+checkBackup.permission = 'admin'
+
+checkBackup.params = {
+ id: {
+ type: 'string',
+ },
+ settings: {
+ type: 'object',
+ },
+ sr: {
+ type: 'string',
+ },
+}
+
// -----------------------------------------------------------------------------
export function listPartitions({ remote, disk }) {
diff --git a/packages/xo-server/src/xo-mixins/backups-ng/index.mjs b/packages/xo-server/src/xo-mixins/backups-ng/index.mjs
index e29626a00..cd05357fc 100644
--- a/packages/xo-server/src/xo-mixins/backups-ng/index.mjs
+++ b/packages/xo-server/src/xo-mixins/backups-ng/index.mjs
@@ -534,4 +534,46 @@ export default class BackupNg {
return backupsByVmByRemote
}
+
+ async checkVmBackupNg(id, srId, settings) {
+ let restoredVm, xapi
+ try {
+ const restoredId = await this.importVmBackupNg(id, srId, settings)
+ const app = this._app
+ xapi = app.getXapi(srId)
+ restoredVm = xapi.getObject(restoredId)
+
+ // remove vifs
+ await Promise.all(restoredVm.$VIFs.map(vif => xapi._deleteVif(vif)))
+
+ const start = new Date()
+ // start Vm
+ await xapi.startVm(restoredId)
+ const timeout = 10 * 60 * 1000
+ const startDuration = new Date() - start
+
+ if (startDuration >= timeout) {
+ throw new Error(`VM ${restoredId} not started after ${timeout / 1000} second`)
+ }
+
+ const remainingTimeout = timeout - startDuration
+
+ await new Promise((resolve, reject) => {
+ const stopWatch = xapi.watchObject(restoredVm.$ref, vm => {
+ if (vm.$guest_metrics) {
+ stopWatch()
+ timeoutId !== undefined && clearTimeout(timeoutId)
+ resolve()
+ }
+ })
+
+ const timeoutId = setTimeout(() => {
+ stopWatch()
+ reject(new Error(`Guest tools of VM ${restoredId} not started after ${timeout / 1000} second`))
+ }, remainingTimeout)
+ })
+ } finally {
+ restoredVm !== undefined && xapi !== undefined && (await xapi.VM_destroy(restoredVm.$ref))
+ }
+ }
}
diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js
index ddd5fbfa4..d80cae89a 100644
--- a/packages/xo-web/src/common/intl/messages.js
+++ b/packages/xo-web/src/common/intl/messages.js
@@ -481,6 +481,7 @@ const messages = {
'If your country participates in DST, it is advised that you avoid scheduling jobs at the time of change. e.g. 2AM to 3AM for US.',
// ------ New backup -----
+ checkBackup: 'Restore health check',
newBackupAdvancedSettings: 'Advanced settings',
newBackupSettings: 'Settings',
reportWhenAlways: 'Always',
@@ -1631,6 +1632,7 @@ const messages = {
refreshBackupList: 'Refresh backup list',
restoreVmBackups: 'Restore',
restoreVmBackupsTitle: 'Restore {vm}',
+ checkVmBackupsTitle: 'Restore health check {vm}',
restoreVmBackupsBulkTitle: 'Restore {nVms, number} VM{nVms, plural, one {} other {s}}',
restoreVmBackupsBulkMessage:
'Restore {nVms, number} VM{nVms, plural, one {} other {s}} from {nVms, plural, one {its} other {their}} {oldestOrLatest} backup.',
diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js
index f53d255ba..acdac9070 100644
--- a/packages/xo-web/src/common/xo/index.js
+++ b/packages/xo-web/src/common/xo/index.js
@@ -2237,6 +2237,14 @@ export const restoreBackup = (
return promise
}
+export const checkBackup = (backup, sr, { mapVdisSrs = {} } = {}) => {
+ return _call('backupNg.checkBackup', {
+ id: resolveId(backup),
+ settings: { mapVdisSrs: resolveIds(mapVdisSrs) },
+ sr: resolveId(sr),
+ })
+}
+
export const deleteBackup = backup => _call('backupNg.deleteVmBackup', { id: resolveId(backup) })
export const deleteBackups = async backups =>
diff --git a/packages/xo-web/src/icons.scss b/packages/xo-web/src/icons.scss
index b0c2940d0..d702e8ba3 100644
--- a/packages/xo-web/src/icons.scss
+++ b/packages/xo-web/src/icons.scss
@@ -345,6 +345,10 @@
@extend .fa;
@extend .fa-download;
}
+ &-check {
+ @extend .fa;
+ @extend .fa-check;
+ }
&-restore {
@extend .fa;
@extend .fa-upload;
diff --git a/packages/xo-web/src/xo-app/backup/restore/index.js b/packages/xo-web/src/xo-app/backup/restore/index.js
index d8b8cb601..69c7f514e 100644
--- a/packages/xo-web/src/xo-app/backup/restore/index.js
+++ b/packages/xo-web/src/xo-app/backup/restore/index.js
@@ -10,7 +10,7 @@ import { confirm } from 'modal'
import { error } from 'notification'
import { FormattedDate } from 'react-intl'
import { cloneDeep, filter, find, flatMap, forEach, map, reduce, orderBy } from 'lodash'
-import { deleteBackups, listVmBackups, restoreBackup, subscribeBackupNgJobs, subscribeRemotes } from 'xo'
+import { checkBackup, deleteBackups, listVmBackups, restoreBackup, subscribeBackupNgJobs, subscribeRemotes } from 'xo'
import RestoreBackupsModalBody, { RestoreBackupsBulkModalBody } from './restore-backups-modal-body'
import DeleteBackupsModalBody from './delete-backups-modal-body'
@@ -94,7 +94,7 @@ export default class Restore extends Component {
backupDataByVm: {},
}
- componentWillReceiveProps(props) {
+ UNSAFE_componentWillReceiveProps(props) {
if (props.remotes !== this.props.remotes || props.jobs !== this.props.jobs) {
this._refreshBackupList(props.remotes, props.jobs)
}
@@ -198,6 +198,24 @@ export default class Restore extends Component {
}, noop)
.then(() => this._refreshBackupList())
+ _restoreHealthCheck = data =>
+ confirm({
+ title: _('checkVmBackupsTitle', { vm: data.last.vm.name_label }),
+ body: