feat(backup NG): new option to shutdown VMs before snapshot (#3060)
Fixes #3058
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
- [Delta Backup NG logs] Display wether the export is a full or a delta [#2711](https://github.com/vatesfr/xen-orchestra/issues/2711)
|
||||
- Copy VDIs' UUID from SR/disks view [#3051](https://github.com/vatesfr/xen-orchestra/issues/3051)
|
||||
- [Backup NG] New option to shutdown VMs before snapshotting them [#3058](https://github.com/vatesfr/xen-orchestra/issues/3058#event-1673756438)
|
||||
|
||||
### Bugs
|
||||
|
||||
|
||||
@@ -1017,13 +1017,12 @@ export async function stop ({ vm, force }) {
|
||||
|
||||
// Hard shutdown
|
||||
if (force) {
|
||||
await xapi.call('VM.hard_shutdown', vm._xapiRef)
|
||||
return
|
||||
return xapi.shutdownVm(vm._xapiRef, { hard: true })
|
||||
}
|
||||
|
||||
// Clean shutdown
|
||||
try {
|
||||
await xapi.call('VM.clean_shutdown', vm._xapiRef)
|
||||
await xapi.shutdownVm(vm._xapiRef)
|
||||
} catch (error) {
|
||||
const { code } = error
|
||||
if (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import deferrable from 'golike-defer'
|
||||
import { catchPlus as pCatch, ignoreErrors } from 'promise-toolbox'
|
||||
import { find, gte, includes, isEmpty, lte } from 'lodash'
|
||||
import { find, gte, includes, isEmpty, lte, noop } from 'lodash'
|
||||
|
||||
import { forEach, mapToArray, parseSize } from '../../utils'
|
||||
|
||||
@@ -429,4 +429,11 @@ export default {
|
||||
// the force parameter is always true
|
||||
return this.call('VM.resume', this.getObject(vmId).$ref, false, true)
|
||||
},
|
||||
|
||||
shutdownVm (vmId, { hard = false } = {}) {
|
||||
return this.call(
|
||||
`VM.${hard ? 'hard' : 'clean'}_shutdown`,
|
||||
this.getObject(vmId).$ref
|
||||
).then(noop)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -8,7 +8,11 @@ import { type Pattern, createPredicate } from 'value-matcher'
|
||||
import { type Readable, PassThrough } from 'stream'
|
||||
import { basename, dirname } from 'path'
|
||||
import { isEmpty, last, mapValues, noop, some, sum, values } from 'lodash'
|
||||
import { fromEvent as pFromEvent, timeout as pTimeout } from 'promise-toolbox'
|
||||
import {
|
||||
fromEvent as pFromEvent,
|
||||
ignoreErrors,
|
||||
timeout as pTimeout,
|
||||
} from 'promise-toolbox'
|
||||
import Vhd, {
|
||||
chainVhd,
|
||||
createSyntheticStream as createVhdReadStream,
|
||||
@@ -40,6 +44,7 @@ type Settings = {|
|
||||
concurrency?: number,
|
||||
deleteFirst?: boolean,
|
||||
exportRetention?: number,
|
||||
offlineSnapshot?: boolean,
|
||||
reportWhen?: ReportWhen,
|
||||
snapshotRetention?: number,
|
||||
vmTimeout?: number,
|
||||
@@ -104,6 +109,7 @@ const getOldEntries = <T>(retention: number, entries?: T[]): T[] =>
|
||||
const defaultSettings: Settings = {
|
||||
deleteFirst: false,
|
||||
exportRetention: 0,
|
||||
offlineSnapshot: false,
|
||||
reportWhen: 'failure',
|
||||
snapshotRetention: 0,
|
||||
vmTimeout: 0,
|
||||
@@ -745,6 +751,17 @@ export default class BackupNg {
|
||||
|
||||
await xapi._assertHealthyVdiChains(vm)
|
||||
|
||||
const offlineSnapshot: boolean = getSetting(
|
||||
settings,
|
||||
'offlineSnapshot',
|
||||
vmUuid,
|
||||
''
|
||||
)
|
||||
const startAfterSnapshot = offlineSnapshot && vm.power_state === 'Running'
|
||||
if (startAfterSnapshot) {
|
||||
await xapi.shutdownVm(vm)
|
||||
}
|
||||
|
||||
let snapshot: Vm = (await wrapTask(
|
||||
{
|
||||
parentId: taskId,
|
||||
@@ -758,6 +775,11 @@ export default class BackupNg {
|
||||
`[XO Backup ${job.name}] ${vm.name_label}`
|
||||
)
|
||||
): any)
|
||||
|
||||
if (startAfterSnapshot) {
|
||||
ignoreErrors.call(xapi.startVm(vm))
|
||||
}
|
||||
|
||||
await xapi._updateObjectMapProperty(snapshot, 'other_config', {
|
||||
'xo:backup:job': jobId,
|
||||
'xo:backup:schedule': scheduleId,
|
||||
|
||||
@@ -364,6 +364,7 @@ const messages = {
|
||||
backupName: 'Name',
|
||||
useDelta: 'Use delta',
|
||||
useCompression: 'Use compression',
|
||||
offlineSnapshot: 'Offline snapshot',
|
||||
dbAndDrRequireEntreprisePlan: 'Delta Backup and DR require Entreprise plan',
|
||||
crRequiresPremiumPlan: 'CR requires Premium plan',
|
||||
smartBackupModeTitle: 'Smart mode',
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
find,
|
||||
findKey,
|
||||
flatten,
|
||||
get,
|
||||
includes,
|
||||
isEmpty,
|
||||
keyBy,
|
||||
@@ -120,6 +119,7 @@ const getInitialState = () => ({
|
||||
formId: getRandomId(),
|
||||
name: '',
|
||||
newSchedules: {},
|
||||
offlineSnapshot: false,
|
||||
paramsUpdated: false,
|
||||
powerState: 'All',
|
||||
remotes: [],
|
||||
@@ -160,6 +160,7 @@ export default [
|
||||
'': {
|
||||
reportWhen: state.reportWhen,
|
||||
concurrency: state.concurrency || undefined,
|
||||
offlineSnapshot: state.offlineSnapshot,
|
||||
},
|
||||
},
|
||||
remotes:
|
||||
@@ -230,6 +231,7 @@ export default [
|
||||
if (id === '') {
|
||||
oldSetting.reportWhen = state.reportWhen
|
||||
oldSetting.concurrency = state.concurrency || undefined
|
||||
oldSetting.offlineSnapshot = state.offlineSnapshot
|
||||
} else if (!(id in settings)) {
|
||||
delete oldSettings[id]
|
||||
} else if (
|
||||
@@ -269,9 +271,9 @@ export default [
|
||||
...state,
|
||||
[mode]: !state[mode],
|
||||
}),
|
||||
setCompression: (_, { target: { checked } }) => state => ({
|
||||
setCheckboxValue: (_, { target: { checked, name } }) => state => ({
|
||||
...state,
|
||||
compression: checked,
|
||||
[name]: checked,
|
||||
}),
|
||||
toggleSmartMode: (_, smartMode) => state => ({
|
||||
...state,
|
||||
@@ -312,7 +314,8 @@ export default [
|
||||
const remotes =
|
||||
job.remotes !== undefined ? destructPattern(job.remotes) : []
|
||||
const srs = job.srs !== undefined ? destructPattern(job.srs) : []
|
||||
const globalSettings = job.settings['']
|
||||
const { concurrency, reportWhen, offlineSnapshot } =
|
||||
job.settings[''] || {}
|
||||
const settings = { ...job.settings }
|
||||
delete settings['']
|
||||
|
||||
@@ -332,8 +335,9 @@ export default [
|
||||
crMode: job.mode === 'delta' && !isEmpty(srs),
|
||||
remotes,
|
||||
srs,
|
||||
reportWhen: get(globalSettings, 'reportWhen') || 'failure',
|
||||
concurrency: get(globalSettings, 'concurrency') || 0,
|
||||
reportWhen: reportWhen || 'failure',
|
||||
concurrency: concurrency || 0,
|
||||
offlineSnapshot,
|
||||
settings,
|
||||
schedules,
|
||||
...destructVmsPattern(job.vms),
|
||||
@@ -586,9 +590,10 @@ export default [
|
||||
{state.showCompression && (
|
||||
<label>
|
||||
<input
|
||||
type='checkbox'
|
||||
onChange={effects.setCompression}
|
||||
checked={state.compression}
|
||||
name='compression'
|
||||
onChange={effects.setCheckboxValue}
|
||||
type='checkbox'
|
||||
/>{' '}
|
||||
<strong>{_('useCompression')}</strong>
|
||||
</label>
|
||||
@@ -768,6 +773,17 @@ export default [
|
||||
value={state.concurrency}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label>
|
||||
<strong>{_('offlineSnapshot')}</strong>{' '}
|
||||
<input
|
||||
checked={state.offlineSnapshot}
|
||||
name='offlineSnapshot'
|
||||
onChange={effects.setCheckboxValue}
|
||||
type='checkbox'
|
||||
/>
|
||||
</label>
|
||||
</FormGroup>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
Reference in New Issue
Block a user