feat(xo-server,xo-web/backups): restore health check (#6148)
This commit is contained in:
committed by
GitHub
parent
c024346475
commit
7d6e832226
@@ -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
|
||||
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -345,6 +345,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-download;
|
||||
}
|
||||
&-check {
|
||||
@extend .fa;
|
||||
@extend .fa-check;
|
||||
}
|
||||
&-restore {
|
||||
@extend .fa;
|
||||
@extend .fa-upload;
|
||||
|
||||
@@ -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: <RestoreBackupsModalBody data={data} showGenerateNewMacAddress={false} showStartAfterBackup={false} />,
|
||||
icon: 'restore',
|
||||
})
|
||||
.then(({ backup, targetSrs: { mainSr, mapVdisSrs } }) => {
|
||||
if (backup == null || mainSr == null) {
|
||||
error(_('backupRestoreErrorTitle'), _('backupRestoreErrorMessage'))
|
||||
return
|
||||
}
|
||||
|
||||
return checkBackup(backup, mainSr, {
|
||||
mapVdisSrs,
|
||||
})
|
||||
}, noop)
|
||||
.then(() => this._refreshBackupList())
|
||||
|
||||
_delete = data =>
|
||||
confirm({
|
||||
title: _('deleteVmBackupsTitle', { vm: data.last.vm.name_label }),
|
||||
@@ -251,6 +269,12 @@ export default class Restore extends Component {
|
||||
label: _('restoreVmBackups'),
|
||||
level: 'primary',
|
||||
},
|
||||
{
|
||||
icon: 'check',
|
||||
individualHandler: this._restoreHealthCheck,
|
||||
label: _('checkBackup'),
|
||||
level: 'secondary',
|
||||
},
|
||||
{
|
||||
handler: this._bulkDelete,
|
||||
icon: 'delete',
|
||||
|
||||
@@ -53,23 +53,31 @@ export default class RestoreBackupsModalBody extends Component {
|
||||
vdis={this._getDisks()}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Toggle iconSize={1} onChange={this.linkState('start')} /> {_('restoreVmBackupsStart', { nVms: 1 })}
|
||||
</div>
|
||||
<div>
|
||||
<Toggle
|
||||
iconSize={1}
|
||||
value={this.state.generateNewMacAddresses}
|
||||
onChange={this.toggleState('generateNewMacAddresses')}
|
||||
/>{' '}
|
||||
{_('generateNewMacAddress')}
|
||||
</div>
|
||||
{this.props.showStartAfterBackup && (
|
||||
<div>
|
||||
<Toggle iconSize={1} onChange={this.linkState('start')} /> {_('restoreVmBackupsStart', { nVms: 1 })}
|
||||
</div>
|
||||
)}
|
||||
{this.props.showGenerateNewMacAddress && (
|
||||
<div>
|
||||
<Toggle
|
||||
iconSize={1}
|
||||
value={this.state.generateNewMacAddresses}
|
||||
onChange={this.toggleState('generateNewMacAddresses')}
|
||||
/>{' '}
|
||||
{_('generateNewMacAddress')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
RestoreBackupsModalBody.defaultProps = {
|
||||
showGenerateNewMacAddress: true,
|
||||
showStartAfterBackup: true,
|
||||
}
|
||||
|
||||
export class RestoreBackupsBulkModalBody extends Component {
|
||||
state = { generateNewMacAddresses: false, latest: true }
|
||||
|
||||
Reference in New Issue
Block a user