@xen-orchestra/backups 0.36.1 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Backup.js +11 -302
- package/ImportVmBackup.js +6 -6
- package/RemoteAdapter.js +20 -13
- package/RestoreMetadataBackup.js +1 -1
- package/_backupWorker.js +2 -2
- package/{_deltaVm.js → _incrementalVm.js} +11 -11
- package/_runners/Metadata.js +134 -0
- package/_runners/VmsXapi.js +138 -0
- package/_runners/_Abstract.js +51 -0
- package/{_PoolMetadataBackup.js → _runners/_PoolMetadataBackup.js} +3 -3
- package/_runners/_RemoteTimeoutError.js +8 -0
- package/{_XoMetadataBackup.js → _runners/_XoMetadataBackup.js} +3 -3
- package/_runners/_getAdaptersByRemote.js +9 -0
- package/_runners/_runTask.js +6 -0
- package/_runners/_vmRunners/FullXapi.js +61 -0
- package/_runners/_vmRunners/IncrementalXapi.js +163 -0
- package/_runners/_vmRunners/_Abstract.js +87 -0
- package/_runners/_vmRunners/_AbstractXapi.js +258 -0
- package/_runners/_vmRunners/_forkDeltaExport.js +12 -0
- package/{writers/FullBackupWriter.js → _runners/_writers/FullRemoteWriter.js} +5 -5
- package/{writers/FullReplicationWriter.js → _runners/_writers/FullXapiWriter.js} +5 -5
- package/{writers/DeltaBackupWriter.js → _runners/_writers/IncrementalRemoteWriter.js} +7 -7
- package/{writers/DeltaReplicationWriter.js → _runners/_writers/IncrementalXapiWriter.js} +8 -8
- package/{writers/_AbstractDeltaWriter.js → _runners/_writers/_AbstractIncrementalWriter.js} +1 -1
- package/{writers/_MixinBackupWriter.js → _runners/_writers/_MixinRemoteWriter.js} +10 -10
- package/{writers/_MixinReplicationWriter.js → _runners/_writers/_MixinXapiWriter.js} +6 -12
- package/package.json +5 -5
- package/_VmBackup.js +0 -515
- /package/{_createStreamThrottle.js → _runners/_createStreamThrottle.js} +0 -0
- /package/{_forkStreamUnpipe.js → _runners/_forkStreamUnpipe.js} +0 -0
- /package/{writers → _runners/_writers}/_AbstractFullWriter.js +0 -0
- /package/{writers → _runners/_writers}/_AbstractWriter.js +0 -0
- /package/{writers → _runners/_writers}/_checkVhd.js +0 -0
- /package/{writers → _runners/_writers}/_listReplicatedVms.js +0 -0
- /package/{writers → _runners/_writers}/_packUuid.js +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { asyncMapSettled } = require('@xen-orchestra/async-map')
|
|
4
|
+
const Disposable = require('promise-toolbox/Disposable')
|
|
5
|
+
const { limitConcurrency } = require('limit-concurrency-decorator')
|
|
6
|
+
|
|
7
|
+
const { extractIdsFromSimplePattern } = require('../extractIdsFromSimplePattern.js')
|
|
8
|
+
const { Task } = require('../Task.js')
|
|
9
|
+
const createStreamThrottle = require('./_createStreamThrottle.js')
|
|
10
|
+
const { DEFAULT_SETTINGS, Abstract } = require('./_Abstract.js')
|
|
11
|
+
const { runTask } = require('./_runTask.js')
|
|
12
|
+
const { getAdaptersByRemote } = require('./_getAdaptersByRemote.js')
|
|
13
|
+
const { IncrementalXapi } = require('./_vmRunners/IncrementalXapi.js')
|
|
14
|
+
const { FullXapi } = require('./_vmRunners/FullXapi.js')
|
|
15
|
+
|
|
16
|
+
const DEFAULT_XAPI_VM_SETTINGS = {
|
|
17
|
+
bypassVdiChainsCheck: false,
|
|
18
|
+
checkpointSnapshot: false,
|
|
19
|
+
concurrency: 2,
|
|
20
|
+
copyRetention: 0,
|
|
21
|
+
deleteFirst: false,
|
|
22
|
+
exportRetention: 0,
|
|
23
|
+
fullInterval: 0,
|
|
24
|
+
healthCheckSr: undefined,
|
|
25
|
+
healthCheckVmsWithTags: [],
|
|
26
|
+
maxExportRate: 0,
|
|
27
|
+
maxMergedDeltasPerRun: Infinity,
|
|
28
|
+
offlineBackup: false,
|
|
29
|
+
offlineSnapshot: false,
|
|
30
|
+
snapshotRetention: 0,
|
|
31
|
+
timeout: 0,
|
|
32
|
+
useNbd: false,
|
|
33
|
+
unconditionalSnapshot: false,
|
|
34
|
+
validateVhdStreams: false,
|
|
35
|
+
vmTimeout: 0,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
exports.VmsXapi = class VmsXapiBackupRunner extends Abstract {
|
|
39
|
+
_computeBaseSettings(config, job) {
|
|
40
|
+
const baseSettings = { ...DEFAULT_SETTINGS }
|
|
41
|
+
Object.assign(baseSettings, DEFAULT_XAPI_VM_SETTINGS, config.defaultSettings, config.vm?.defaultSettings)
|
|
42
|
+
Object.assign(baseSettings, job.settings[''])
|
|
43
|
+
return baseSettings
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async run() {
|
|
47
|
+
const job = this._job
|
|
48
|
+
|
|
49
|
+
// FIXME: proper SimpleIdPattern handling
|
|
50
|
+
const getSnapshotNameLabel = this._getSnapshotNameLabel
|
|
51
|
+
const schedule = this._schedule
|
|
52
|
+
const settings = this._settings
|
|
53
|
+
|
|
54
|
+
const throttleStream = createStreamThrottle(settings.maxExportRate)
|
|
55
|
+
|
|
56
|
+
const config = this._config
|
|
57
|
+
await Disposable.use(
|
|
58
|
+
Disposable.all(
|
|
59
|
+
extractIdsFromSimplePattern(job.srs).map(id =>
|
|
60
|
+
this._getRecord('SR', id).catch(error => {
|
|
61
|
+
runTask(
|
|
62
|
+
{
|
|
63
|
+
name: 'get SR record',
|
|
64
|
+
data: { type: 'SR', id },
|
|
65
|
+
},
|
|
66
|
+
() => Promise.reject(error)
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
),
|
|
71
|
+
Disposable.all(extractIdsFromSimplePattern(job.remotes).map(id => this._getAdapter(id))),
|
|
72
|
+
() => (settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined),
|
|
73
|
+
async (srs, remoteAdapters, healthCheckSr) => {
|
|
74
|
+
// remove adapters that failed (already handled)
|
|
75
|
+
remoteAdapters = remoteAdapters.filter(_ => _ !== undefined)
|
|
76
|
+
|
|
77
|
+
// remove srs that failed (already handled)
|
|
78
|
+
srs = srs.filter(_ => _ !== undefined)
|
|
79
|
+
|
|
80
|
+
if (remoteAdapters.length === 0 && srs.length === 0 && settings.snapshotRetention === 0) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const vmIds = extractIdsFromSimplePattern(job.vms)
|
|
85
|
+
|
|
86
|
+
Task.info('vms', { vms: vmIds })
|
|
87
|
+
|
|
88
|
+
remoteAdapters = getAdaptersByRemote(remoteAdapters)
|
|
89
|
+
|
|
90
|
+
const allSettings = this._job.settings
|
|
91
|
+
const baseSettings = this._baseSettings
|
|
92
|
+
|
|
93
|
+
const handleVm = vmUuid => {
|
|
94
|
+
const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
|
|
95
|
+
|
|
96
|
+
return this._getRecord('VM', vmUuid).then(
|
|
97
|
+
disposableVm =>
|
|
98
|
+
Disposable.use(disposableVm, vm => {
|
|
99
|
+
taskStart.data.name_label = vm.name_label
|
|
100
|
+
return runTask(taskStart, () => {
|
|
101
|
+
const opts = {
|
|
102
|
+
baseSettings,
|
|
103
|
+
config,
|
|
104
|
+
getSnapshotNameLabel,
|
|
105
|
+
healthCheckSr,
|
|
106
|
+
job,
|
|
107
|
+
remoteAdapters,
|
|
108
|
+
schedule,
|
|
109
|
+
settings: { ...settings, ...allSettings[vm.uuid] },
|
|
110
|
+
srs,
|
|
111
|
+
throttleStream,
|
|
112
|
+
vm,
|
|
113
|
+
}
|
|
114
|
+
let vmBackup
|
|
115
|
+
if (job.mode === 'delta') {
|
|
116
|
+
vmBackup = new IncrementalXapi(opts)
|
|
117
|
+
} else {
|
|
118
|
+
if (job.mode === 'full') {
|
|
119
|
+
vmBackup = new FullXapi(opts)
|
|
120
|
+
} else {
|
|
121
|
+
throw new Error(`Job mode ${job.mode} not implemented`)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return vmBackup.run()
|
|
125
|
+
})
|
|
126
|
+
}),
|
|
127
|
+
error =>
|
|
128
|
+
runTask(taskStart, () => {
|
|
129
|
+
throw error
|
|
130
|
+
})
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
const { concurrency } = settings
|
|
134
|
+
await asyncMapSettled(vmIds, concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm))
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Disposable = require('promise-toolbox/Disposable')
|
|
4
|
+
const pTimeout = require('promise-toolbox/timeout')
|
|
5
|
+
const { compileTemplate } = require('@xen-orchestra/template')
|
|
6
|
+
const { runTask } = require('./_runTask.js')
|
|
7
|
+
const { RemoteTimeoutError } = require('./_RemoteTimeoutError.js')
|
|
8
|
+
|
|
9
|
+
exports.DEFAULT_SETTINGS = {
|
|
10
|
+
getRemoteTimeout: 300e3,
|
|
11
|
+
reportWhen: 'failure',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
exports.Abstract = class AbstractRunner {
|
|
15
|
+
constructor({ config, getAdapter, getConnectedRecord, job, schedule }) {
|
|
16
|
+
this._config = config
|
|
17
|
+
this._getRecord = getConnectedRecord
|
|
18
|
+
this._job = job
|
|
19
|
+
this._schedule = schedule
|
|
20
|
+
|
|
21
|
+
this._getSnapshotNameLabel = compileTemplate(config.snapshotNameLabelTpl, {
|
|
22
|
+
'{job.name}': job.name,
|
|
23
|
+
'{vm.name_label}': vm => vm.name_label,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const baseSettings = this._computeBaseSettings(config, job)
|
|
27
|
+
this._baseSettings = baseSettings
|
|
28
|
+
this._settings = { ...baseSettings, ...job.settings[schedule.id] }
|
|
29
|
+
|
|
30
|
+
const { getRemoteTimeout } = this._settings
|
|
31
|
+
this._getAdapter = async function (remoteId) {
|
|
32
|
+
try {
|
|
33
|
+
const disposable = await pTimeout.call(getAdapter(remoteId), getRemoteTimeout, new RemoteTimeoutError(remoteId))
|
|
34
|
+
|
|
35
|
+
return new Disposable(() => disposable.dispose(), {
|
|
36
|
+
adapter: disposable.value,
|
|
37
|
+
remoteId,
|
|
38
|
+
})
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
|
|
41
|
+
runTask(
|
|
42
|
+
{
|
|
43
|
+
name: 'get remote adapter',
|
|
44
|
+
data: { type: 'remote', id: remoteId },
|
|
45
|
+
},
|
|
46
|
+
() => Promise.reject(error)
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
4
4
|
|
|
5
|
-
const { DIR_XO_POOL_METADATA_BACKUPS } = require('
|
|
5
|
+
const { DIR_XO_POOL_METADATA_BACKUPS } = require('../RemoteAdapter.js')
|
|
6
6
|
const { forkStreamUnpipe } = require('./_forkStreamUnpipe.js')
|
|
7
|
-
const { formatFilenameDate } = require('
|
|
8
|
-
const { Task } = require('
|
|
7
|
+
const { formatFilenameDate } = require('../_filenameDate.js')
|
|
8
|
+
const { Task } = require('../Task.js')
|
|
9
9
|
|
|
10
10
|
const PATH_DB_DUMP = '/pool/xmldbdump'
|
|
11
11
|
exports.PATH_DB_DUMP = PATH_DB_DUMP
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
4
4
|
|
|
5
|
-
const { DIR_XO_CONFIG_BACKUPS } = require('
|
|
6
|
-
const { formatFilenameDate } = require('
|
|
7
|
-
const { Task } = require('
|
|
5
|
+
const { DIR_XO_CONFIG_BACKUPS } = require('../RemoteAdapter.js')
|
|
6
|
+
const { formatFilenameDate } = require('../_filenameDate.js')
|
|
7
|
+
const { Task } = require('../Task.js')
|
|
8
8
|
|
|
9
9
|
exports.XoMetadataBackup = class XoMetadataBackup {
|
|
10
10
|
constructor({ config, job, remoteAdapters, schedule, settings }) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { createLogger } = require('@xen-orchestra/log')
|
|
4
|
+
|
|
5
|
+
const { forkStreamUnpipe } = require('../_forkStreamUnpipe.js')
|
|
6
|
+
const { FullRemoteWriter } = require('../_writers/FullRemoteWriter.js')
|
|
7
|
+
const { FullXapiWriter } = require('../_writers/FullXapiWriter.js')
|
|
8
|
+
const { watchStreamSize } = require('../../_watchStreamSize.js')
|
|
9
|
+
const { AbstractXapi } = require('./_AbstractXapi.js')
|
|
10
|
+
|
|
11
|
+
const { debug } = createLogger('xo:backups:FullXapiVmBackup')
|
|
12
|
+
|
|
13
|
+
exports.FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
|
|
14
|
+
_getWriters() {
|
|
15
|
+
return [FullRemoteWriter, FullXapiWriter]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_mustDoSnapshot() {
|
|
19
|
+
const { vm } = this
|
|
20
|
+
|
|
21
|
+
const settings = this._settings
|
|
22
|
+
return (
|
|
23
|
+
settings.unconditionalSnapshot ||
|
|
24
|
+
(!settings.offlineBackup && vm.power_state === 'Running') ||
|
|
25
|
+
settings.snapshotRetention !== 0
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
_selectBaseVm() {}
|
|
29
|
+
|
|
30
|
+
async _copy() {
|
|
31
|
+
const { compression } = this.job
|
|
32
|
+
const stream = this._throttleStream(
|
|
33
|
+
await this._xapi.VM_export(this.exportedVm.$ref, {
|
|
34
|
+
compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
|
|
35
|
+
useSnapshot: false,
|
|
36
|
+
})
|
|
37
|
+
)
|
|
38
|
+
const sizeContainer = watchStreamSize(stream)
|
|
39
|
+
|
|
40
|
+
const timestamp = Date.now()
|
|
41
|
+
|
|
42
|
+
await this._callWriters(
|
|
43
|
+
writer =>
|
|
44
|
+
writer.run({
|
|
45
|
+
sizeContainer,
|
|
46
|
+
stream: forkStreamUnpipe(stream),
|
|
47
|
+
timestamp,
|
|
48
|
+
}),
|
|
49
|
+
'writer.run()'
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const { size } = sizeContainer
|
|
53
|
+
const end = Date.now()
|
|
54
|
+
const duration = end - timestamp
|
|
55
|
+
debug('transfer complete', {
|
|
56
|
+
duration,
|
|
57
|
+
speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
|
|
58
|
+
size,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const findLast = require('lodash/findLast.js')
|
|
4
|
+
const keyBy = require('lodash/keyBy.js')
|
|
5
|
+
const mapValues = require('lodash/mapValues.js')
|
|
6
|
+
const vhdStreamValidator = require('vhd-lib/vhdStreamValidator.js')
|
|
7
|
+
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
8
|
+
const { createLogger } = require('@xen-orchestra/log')
|
|
9
|
+
const { pipeline } = require('node:stream')
|
|
10
|
+
|
|
11
|
+
const { IncrementalRemoteWriter } = require('../_writers/IncrementalRemoteWriter.js')
|
|
12
|
+
const { IncrementalXapiWriter } = require('../_writers/IncrementalXapiWriter.js')
|
|
13
|
+
const { exportIncrementalVm } = require('../../_incrementalVm.js')
|
|
14
|
+
const { Task } = require('../../Task.js')
|
|
15
|
+
const { watchStreamSize } = require('../../_watchStreamSize.js')
|
|
16
|
+
const { AbstractXapi } = require('./_AbstractXapi.js')
|
|
17
|
+
const { forkDeltaExport } = require('./_forkDeltaExport.js')
|
|
18
|
+
|
|
19
|
+
const { debug } = createLogger('xo:backups:IncrementalXapiVmBackup')
|
|
20
|
+
|
|
21
|
+
const noop = Function.prototype
|
|
22
|
+
|
|
23
|
+
exports.IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXapi {
|
|
24
|
+
_getWriters() {
|
|
25
|
+
return [IncrementalRemoteWriter, IncrementalXapiWriter]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_mustDoSnapshot() {
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async _copy() {
|
|
33
|
+
const { exportedVm } = this
|
|
34
|
+
const baseVm = this._baseVm
|
|
35
|
+
const fullVdisRequired = this._fullVdisRequired
|
|
36
|
+
|
|
37
|
+
const isFull = fullVdisRequired === undefined || fullVdisRequired.size !== 0
|
|
38
|
+
|
|
39
|
+
await this._callWriters(writer => writer.prepare({ isFull }), 'writer.prepare()')
|
|
40
|
+
|
|
41
|
+
const deltaExport = await exportIncrementalVm(exportedVm, baseVm, {
|
|
42
|
+
fullVdisRequired,
|
|
43
|
+
})
|
|
44
|
+
// since NBD is network based, if one disk use nbd , all the disk use them
|
|
45
|
+
// except the suspended VDI
|
|
46
|
+
if (Object.values(deltaExport.streams).some(({ _nbd }) => _nbd)) {
|
|
47
|
+
Task.info('Transfer data using NBD')
|
|
48
|
+
}
|
|
49
|
+
const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
|
|
50
|
+
|
|
51
|
+
if (this._settings.validateVhdStreams) {
|
|
52
|
+
deltaExport.streams = mapValues(deltaExport.streams, stream => pipeline(stream, vhdStreamValidator, noop))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
deltaExport.streams = mapValues(deltaExport.streams, this._throttleStream)
|
|
56
|
+
|
|
57
|
+
const timestamp = Date.now()
|
|
58
|
+
|
|
59
|
+
await this._callWriters(
|
|
60
|
+
writer =>
|
|
61
|
+
writer.transfer({
|
|
62
|
+
deltaExport: forkDeltaExport(deltaExport),
|
|
63
|
+
sizeContainers,
|
|
64
|
+
timestamp,
|
|
65
|
+
}),
|
|
66
|
+
'writer.transfer()'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
this._baseVm = exportedVm
|
|
70
|
+
|
|
71
|
+
if (baseVm !== undefined) {
|
|
72
|
+
await exportedVm.update_other_config(
|
|
73
|
+
'xo:backup:deltaChainLength',
|
|
74
|
+
String(+(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1)
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// not the case if offlineBackup
|
|
79
|
+
if (exportedVm.is_a_snapshot) {
|
|
80
|
+
await exportedVm.update_other_config('xo:backup:exported', 'true')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const size = Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0)
|
|
84
|
+
const end = Date.now()
|
|
85
|
+
const duration = end - timestamp
|
|
86
|
+
debug('transfer complete', {
|
|
87
|
+
duration,
|
|
88
|
+
speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
|
|
89
|
+
size,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async _selectBaseVm() {
|
|
96
|
+
const xapi = this._xapi
|
|
97
|
+
|
|
98
|
+
let baseVm = findLast(this._jobSnapshots, _ => 'xo:backup:exported' in _.other_config)
|
|
99
|
+
if (baseVm === undefined) {
|
|
100
|
+
debug('no base VM found')
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const fullInterval = this._settings.fullInterval
|
|
105
|
+
const deltaChainLength = +(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1
|
|
106
|
+
if (!(fullInterval === 0 || fullInterval > deltaChainLength)) {
|
|
107
|
+
debug('not using base VM becaust fullInterval reached')
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const srcVdis = keyBy(await xapi.getRecords('VDI', await this.vm.$getDisks()), '$ref')
|
|
112
|
+
|
|
113
|
+
// resolve full record
|
|
114
|
+
baseVm = await xapi.getRecord('VM', baseVm.$ref)
|
|
115
|
+
|
|
116
|
+
const baseUuidToSrcVdi = new Map()
|
|
117
|
+
await asyncMap(await baseVm.$getDisks(), async baseRef => {
|
|
118
|
+
const [baseUuid, snapshotOf] = await Promise.all([
|
|
119
|
+
xapi.getField('VDI', baseRef, 'uuid'),
|
|
120
|
+
xapi.getField('VDI', baseRef, 'snapshot_of'),
|
|
121
|
+
])
|
|
122
|
+
const srcVdi = srcVdis[snapshotOf]
|
|
123
|
+
if (srcVdi !== undefined) {
|
|
124
|
+
baseUuidToSrcVdi.set(baseUuid, srcVdi)
|
|
125
|
+
} else {
|
|
126
|
+
debug('ignore snapshot VDI because no longer present on VM', {
|
|
127
|
+
vdi: baseUuid,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const presentBaseVdis = new Map(baseUuidToSrcVdi)
|
|
133
|
+
await this._callWriters(
|
|
134
|
+
writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis, baseVm),
|
|
135
|
+
'writer.checkBaseVdis()',
|
|
136
|
+
false
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if (presentBaseVdis.size === 0) {
|
|
140
|
+
debug('no base VM found')
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const fullVdisRequired = new Set()
|
|
145
|
+
baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
|
|
146
|
+
if (presentBaseVdis.has(baseUuid)) {
|
|
147
|
+
debug('found base VDI', {
|
|
148
|
+
base: baseUuid,
|
|
149
|
+
vdi: srcVdi.uuid,
|
|
150
|
+
})
|
|
151
|
+
} else {
|
|
152
|
+
debug('missing base VDI', {
|
|
153
|
+
base: baseUuid,
|
|
154
|
+
vdi: srcVdi.uuid,
|
|
155
|
+
})
|
|
156
|
+
fullVdisRequired.add(srcVdi.uuid)
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
this._baseVm = baseVm
|
|
161
|
+
this._fullVdisRequired = fullVdisRequired
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
4
|
+
const { createLogger } = require('@xen-orchestra/log')
|
|
5
|
+
const { Task } = require('../../Task.js')
|
|
6
|
+
|
|
7
|
+
const { debug, warn } = createLogger('xo:backups:AbstractVmRunner')
|
|
8
|
+
|
|
9
|
+
class AggregateError extends Error {
|
|
10
|
+
constructor(errors, message) {
|
|
11
|
+
super(message)
|
|
12
|
+
this.errors = errors
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const asyncEach = async (iterable, fn, thisArg = iterable) => {
|
|
17
|
+
for (const item of iterable) {
|
|
18
|
+
await fn.call(thisArg, item)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
exports.Abstract = class AbstractVmBackupRunner {
|
|
23
|
+
// calls fn for each function, warns of any errors, and throws only if there are no writers left
|
|
24
|
+
async _callWriters(fn, step, parallel = true) {
|
|
25
|
+
const writers = this._writers
|
|
26
|
+
const n = writers.size
|
|
27
|
+
if (n === 0) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function callWriter(writer) {
|
|
32
|
+
const { name } = writer.constructor
|
|
33
|
+
try {
|
|
34
|
+
debug('writer step starting', { step, writer: name })
|
|
35
|
+
await fn(writer)
|
|
36
|
+
debug('writer step succeeded', { duration: step, writer: name })
|
|
37
|
+
} catch (error) {
|
|
38
|
+
writers.delete(writer)
|
|
39
|
+
|
|
40
|
+
warn('writer step failed', { error, step, writer: name })
|
|
41
|
+
|
|
42
|
+
// these two steps are the only one that are not already in their own sub tasks
|
|
43
|
+
if (step === 'writer.checkBaseVdis()' || step === 'writer.beforeBackup()') {
|
|
44
|
+
Task.warning(
|
|
45
|
+
`the writer ${name} has failed the step ${step} with error ${error.message}. It won't be used anymore in this job execution.`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw error
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (n === 1) {
|
|
53
|
+
const [writer] = writers
|
|
54
|
+
return callWriter(writer)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const errors = []
|
|
58
|
+
await (parallel ? asyncMap : asyncEach)(writers, async function (writer) {
|
|
59
|
+
try {
|
|
60
|
+
await callWriter(writer)
|
|
61
|
+
} catch (error) {
|
|
62
|
+
errors.push(error)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
if (writers.size === 0) {
|
|
66
|
+
throw new AggregateError(errors, 'all targets have failed, step: ' + step)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async _healthCheck() {
|
|
71
|
+
const settings = this._settings
|
|
72
|
+
|
|
73
|
+
if (this._healthCheckSr === undefined) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// check if current VM has tags
|
|
78
|
+
const { tags } = this.vm
|
|
79
|
+
const intersect = settings.healthCheckVmsWithTags.some(t => tags.includes(t))
|
|
80
|
+
|
|
81
|
+
if (settings.healthCheckVmsWithTags.length !== 0 && !intersect) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await this._callWriters(writer => writer.healthCheck(this._healthCheckSr), 'writer.healthCheck()')
|
|
86
|
+
}
|
|
87
|
+
}
|