@xen-orchestra/backups 0.36.1 → 0.38.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 +14 -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/VmsRemote.js +98 -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/FullRemote.js +53 -0
- package/_runners/_vmRunners/FullXapi.js +65 -0
- package/_runners/_vmRunners/IncrementalRemote.js +67 -0
- package/_runners/_vmRunners/IncrementalXapi.js +175 -0
- package/_runners/_vmRunners/_Abstract.js +95 -0
- package/_runners/_vmRunners/_AbstractRemote.js +86 -0
- package/_runners/_vmRunners/_AbstractXapi.js +257 -0
- package/_runners/_vmRunners/_forkDeltaExport.js +12 -0
- package/{writers/FullBackupWriter.js → _runners/_writers/FullRemoteWriter.js} +15 -13
- package/{writers/FullReplicationWriter.js → _runners/_writers/FullXapiWriter.js} +8 -7
- package/{writers/DeltaBackupWriter.js → _runners/_writers/IncrementalRemoteWriter.js} +28 -22
- package/{writers/DeltaReplicationWriter.js → _runners/_writers/IncrementalXapiWriter.js} +19 -16
- package/{writers → _runners/_writers}/_AbstractFullWriter.js +2 -2
- package/{writers/_AbstractDeltaWriter.js → _runners/_writers/_AbstractIncrementalWriter.js} +3 -3
- package/_runners/_writers/_AbstractWriter.js +31 -0
- package/{writers/_MixinBackupWriter.js → _runners/_writers/_MixinRemoteWriter.js} +30 -16
- package/{writers/_MixinReplicationWriter.js → _runners/_writers/_MixinXapiWriter.js} +9 -13
- package/package.json +5 -5
- package/_VmBackup.js +0 -515
- package/writers/_AbstractWriter.js +0 -14
- /package/{_createStreamThrottle.js → _runners/_createStreamThrottle.js} +0 -0
- /package/{_forkStreamUnpipe.js → _runners/_forkStreamUnpipe.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,175 @@
|
|
|
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
|
+
const isVhdDifferencingDisk = require('vhd-lib/isVhdDifferencingDisk')
|
|
19
|
+
const { asyncEach } = require('@vates/async-each')
|
|
20
|
+
|
|
21
|
+
const { debug } = createLogger('xo:backups:IncrementalXapiVmBackup')
|
|
22
|
+
|
|
23
|
+
const noop = Function.prototype
|
|
24
|
+
|
|
25
|
+
exports.IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXapi {
|
|
26
|
+
_getWriters() {
|
|
27
|
+
return [IncrementalRemoteWriter, IncrementalXapiWriter]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_mustDoSnapshot() {
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async _copy() {
|
|
35
|
+
const baseVm = this._baseVm
|
|
36
|
+
const vm = this._vm
|
|
37
|
+
const exportedVm = this._exportedVm
|
|
38
|
+
const fullVdisRequired = this._fullVdisRequired
|
|
39
|
+
|
|
40
|
+
const isFull = fullVdisRequired === undefined || fullVdisRequired.size !== 0
|
|
41
|
+
|
|
42
|
+
await this._callWriters(writer => writer.prepare({ isFull }), 'writer.prepare()')
|
|
43
|
+
|
|
44
|
+
const deltaExport = await exportIncrementalVm(exportedVm, baseVm, {
|
|
45
|
+
fullVdisRequired,
|
|
46
|
+
})
|
|
47
|
+
// since NBD is network based, if one disk use nbd , all the disk use them
|
|
48
|
+
// except the suspended VDI
|
|
49
|
+
if (Object.values(deltaExport.streams).some(({ _nbd }) => _nbd)) {
|
|
50
|
+
Task.info('Transfer data using NBD')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const differentialVhds = {}
|
|
54
|
+
// since isVhdDifferencingDisk is reading and unshifting data in stream
|
|
55
|
+
// it should be done BEFORE any other stream transform
|
|
56
|
+
await asyncEach(Object.entries(deltaExport.streams), async ([key, stream]) => {
|
|
57
|
+
differentialVhds[key] = await isVhdDifferencingDisk(stream)
|
|
58
|
+
})
|
|
59
|
+
const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
|
|
60
|
+
|
|
61
|
+
if (this._settings.validateVhdStreams) {
|
|
62
|
+
deltaExport.streams = mapValues(deltaExport.streams, stream => pipeline(stream, vhdStreamValidator, noop))
|
|
63
|
+
}
|
|
64
|
+
deltaExport.streams = mapValues(deltaExport.streams, this._throttleStream)
|
|
65
|
+
|
|
66
|
+
const timestamp = Date.now()
|
|
67
|
+
|
|
68
|
+
await this._callWriters(
|
|
69
|
+
writer =>
|
|
70
|
+
writer.transfer({
|
|
71
|
+
deltaExport: forkDeltaExport(deltaExport),
|
|
72
|
+
differentialVhds,
|
|
73
|
+
sizeContainers,
|
|
74
|
+
timestamp,
|
|
75
|
+
vm,
|
|
76
|
+
vmSnapshot: exportedVm,
|
|
77
|
+
}),
|
|
78
|
+
'writer.transfer()'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
this._baseVm = exportedVm
|
|
82
|
+
|
|
83
|
+
if (baseVm !== undefined) {
|
|
84
|
+
await exportedVm.update_other_config(
|
|
85
|
+
'xo:backup:deltaChainLength',
|
|
86
|
+
String(+(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1)
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// not the case if offlineBackup
|
|
91
|
+
if (exportedVm.is_a_snapshot) {
|
|
92
|
+
await exportedVm.update_other_config('xo:backup:exported', 'true')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const size = Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0)
|
|
96
|
+
const end = Date.now()
|
|
97
|
+
const duration = end - timestamp
|
|
98
|
+
debug('transfer complete', {
|
|
99
|
+
duration,
|
|
100
|
+
speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
|
|
101
|
+
size,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async _selectBaseVm() {
|
|
108
|
+
const xapi = this._xapi
|
|
109
|
+
|
|
110
|
+
let baseVm = findLast(this._jobSnapshots, _ => 'xo:backup:exported' in _.other_config)
|
|
111
|
+
if (baseVm === undefined) {
|
|
112
|
+
debug('no base VM found')
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const fullInterval = this._settings.fullInterval
|
|
117
|
+
const deltaChainLength = +(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1
|
|
118
|
+
if (!(fullInterval === 0 || fullInterval > deltaChainLength)) {
|
|
119
|
+
debug('not using base VM becaust fullInterval reached')
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const srcVdis = keyBy(await xapi.getRecords('VDI', await this._vm.$getDisks()), '$ref')
|
|
124
|
+
|
|
125
|
+
// resolve full record
|
|
126
|
+
baseVm = await xapi.getRecord('VM', baseVm.$ref)
|
|
127
|
+
|
|
128
|
+
const baseUuidToSrcVdi = new Map()
|
|
129
|
+
await asyncMap(await baseVm.$getDisks(), async baseRef => {
|
|
130
|
+
const [baseUuid, snapshotOf] = await Promise.all([
|
|
131
|
+
xapi.getField('VDI', baseRef, 'uuid'),
|
|
132
|
+
xapi.getField('VDI', baseRef, 'snapshot_of'),
|
|
133
|
+
])
|
|
134
|
+
const srcVdi = srcVdis[snapshotOf]
|
|
135
|
+
if (srcVdi !== undefined) {
|
|
136
|
+
baseUuidToSrcVdi.set(baseUuid, srcVdi)
|
|
137
|
+
} else {
|
|
138
|
+
debug('ignore snapshot VDI because no longer present on VM', {
|
|
139
|
+
vdi: baseUuid,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const presentBaseVdis = new Map(baseUuidToSrcVdi)
|
|
145
|
+
await this._callWriters(
|
|
146
|
+
writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis, baseVm),
|
|
147
|
+
'writer.checkBaseVdis()',
|
|
148
|
+
false
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if (presentBaseVdis.size === 0) {
|
|
152
|
+
debug('no base VM found')
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const fullVdisRequired = new Set()
|
|
157
|
+
baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
|
|
158
|
+
if (presentBaseVdis.has(baseUuid)) {
|
|
159
|
+
debug('found base VDI', {
|
|
160
|
+
base: baseUuid,
|
|
161
|
+
vdi: srcVdi.uuid,
|
|
162
|
+
})
|
|
163
|
+
} else {
|
|
164
|
+
debug('missing base VDI', {
|
|
165
|
+
base: baseUuid,
|
|
166
|
+
vdi: srcVdi.uuid,
|
|
167
|
+
})
|
|
168
|
+
fullVdisRequired.add(srcVdi.uuid)
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
this._baseVm = baseVm
|
|
173
|
+
this._fullVdisRequired = fullVdisRequired
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
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._tags
|
|
79
|
+
const intersect = settings.healthCheckVmsWithTags.some(t => tags.includes(t))
|
|
80
|
+
|
|
81
|
+
if (settings.healthCheckVmsWithTags.length !== 0 && !intersect) {
|
|
82
|
+
// create a task to have an info in the logs and reports
|
|
83
|
+
return Task.run(
|
|
84
|
+
{
|
|
85
|
+
name: 'health check',
|
|
86
|
+
},
|
|
87
|
+
() => {
|
|
88
|
+
Task.info(`This VM doesn't match the health check's tags for this schedule`)
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await this._callWriters(writer => writer.healthCheck(), 'writer.healthCheck()')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const { Abstract } = require('./_Abstract')
|
|
3
|
+
|
|
4
|
+
const { getVmBackupDir } = require('../../_getVmBackupDir')
|
|
5
|
+
const { asyncEach } = require('@vates/async-each')
|
|
6
|
+
const { Disposable } = require('promise-toolbox')
|
|
7
|
+
|
|
8
|
+
exports.AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstract {
|
|
9
|
+
constructor({
|
|
10
|
+
config,
|
|
11
|
+
job,
|
|
12
|
+
healthCheckSr,
|
|
13
|
+
remoteAdapters,
|
|
14
|
+
schedule,
|
|
15
|
+
settings,
|
|
16
|
+
sourceRemoteAdapter,
|
|
17
|
+
throttleStream,
|
|
18
|
+
vmUuid,
|
|
19
|
+
}) {
|
|
20
|
+
super()
|
|
21
|
+
this.config = config
|
|
22
|
+
this.job = job
|
|
23
|
+
this.remoteAdapters = remoteAdapters
|
|
24
|
+
this.scheduleId = schedule.id
|
|
25
|
+
this.timestamp = undefined
|
|
26
|
+
|
|
27
|
+
this._healthCheckSr = healthCheckSr
|
|
28
|
+
this._sourceRemoteAdapter = sourceRemoteAdapter
|
|
29
|
+
this._throttleStream = throttleStream
|
|
30
|
+
this._vmUuid = vmUuid
|
|
31
|
+
|
|
32
|
+
const allSettings = job.settings
|
|
33
|
+
const writers = new Set()
|
|
34
|
+
this._writers = writers
|
|
35
|
+
|
|
36
|
+
const RemoteWriter = this._getRemoteWriter()
|
|
37
|
+
Object.entries(remoteAdapters).forEach(([remoteId, adapter]) => {
|
|
38
|
+
const targetSettings = {
|
|
39
|
+
...settings,
|
|
40
|
+
...allSettings[remoteId],
|
|
41
|
+
}
|
|
42
|
+
writers.add(new RemoteWriter({ adapter, config, healthCheckSr, job, vmUuid, remoteId, settings: targetSettings }))
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async _computeTransferList(predicate) {
|
|
47
|
+
const vmBackups = await this._sourceRemoteAdapter.listVmBackups(this._vmUuid, predicate)
|
|
48
|
+
const localMetada = new Map()
|
|
49
|
+
Object.values(vmBackups).forEach(metadata => {
|
|
50
|
+
const timestamp = metadata.timestamp
|
|
51
|
+
localMetada.set(timestamp, metadata)
|
|
52
|
+
})
|
|
53
|
+
const nbRemotes = Object.keys(this.remoteAdapters).length
|
|
54
|
+
const remoteMetadatas = {}
|
|
55
|
+
await asyncEach(Object.values(this.remoteAdapters), async remoteAdapter => {
|
|
56
|
+
const remoteMetadata = await remoteAdapter.listVmBackups(this._vmUuid, predicate)
|
|
57
|
+
remoteMetadata.forEach(metadata => {
|
|
58
|
+
const timestamp = metadata.timestamp
|
|
59
|
+
remoteMetadatas[timestamp] = (remoteMetadatas[timestamp] ?? 0) + 1
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
let chain = []
|
|
64
|
+
const timestamps = [...localMetada.keys()]
|
|
65
|
+
timestamps.sort()
|
|
66
|
+
for (const timestamp of timestamps) {
|
|
67
|
+
if (remoteMetadatas[timestamp] !== nbRemotes) {
|
|
68
|
+
// this backup is not present in all the remote
|
|
69
|
+
// should be retransfered if not found later
|
|
70
|
+
chain.push(localMetada.get(timestamp))
|
|
71
|
+
} else {
|
|
72
|
+
// backup is present in local and remote : the chain has already been transferred
|
|
73
|
+
chain = []
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return chain
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async run() {
|
|
80
|
+
const handler = this._sourceRemoteAdapter._handler
|
|
81
|
+
await Disposable.use(await handler.lock(getVmBackupDir(this._vmUuid)), async () => {
|
|
82
|
+
await this._run()
|
|
83
|
+
await this._healthCheck()
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('assert')
|
|
4
|
+
const groupBy = require('lodash/groupBy.js')
|
|
5
|
+
const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
|
6
|
+
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
7
|
+
const { decorateMethodsWith } = require('@vates/decorate-with')
|
|
8
|
+
const { defer } = require('golike-defer')
|
|
9
|
+
const { formatDateTime } = require('@xen-orchestra/xapi')
|
|
10
|
+
|
|
11
|
+
const { getOldEntries } = require('../../_getOldEntries.js')
|
|
12
|
+
const { Task } = require('../../Task.js')
|
|
13
|
+
const { Abstract } = require('./_Abstract.js')
|
|
14
|
+
|
|
15
|
+
class AbstractXapiVmBackupRunner extends Abstract {
|
|
16
|
+
constructor({
|
|
17
|
+
config,
|
|
18
|
+
getSnapshotNameLabel,
|
|
19
|
+
healthCheckSr,
|
|
20
|
+
job,
|
|
21
|
+
remoteAdapters,
|
|
22
|
+
remotes,
|
|
23
|
+
schedule,
|
|
24
|
+
settings,
|
|
25
|
+
srs,
|
|
26
|
+
throttleStream,
|
|
27
|
+
vm,
|
|
28
|
+
}) {
|
|
29
|
+
super()
|
|
30
|
+
if (vm.other_config['xo:backup:job'] === job.id && 'start' in vm.blocked_operations) {
|
|
31
|
+
// don't match replicated VMs created by this very job otherwise they
|
|
32
|
+
// will be replicated again and again
|
|
33
|
+
throw new Error('cannot backup a VM created by this very job')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.config = config
|
|
37
|
+
this.job = job
|
|
38
|
+
this.remoteAdapters = remoteAdapters
|
|
39
|
+
this.scheduleId = schedule.id
|
|
40
|
+
this.timestamp = undefined
|
|
41
|
+
|
|
42
|
+
// VM currently backed up
|
|
43
|
+
const tags = (this._tags = vm.tags)
|
|
44
|
+
|
|
45
|
+
// VM (snapshot) that is really exported
|
|
46
|
+
this._exportedVm = undefined
|
|
47
|
+
this._vm = vm
|
|
48
|
+
|
|
49
|
+
this._fullVdisRequired = undefined
|
|
50
|
+
this._getSnapshotNameLabel = getSnapshotNameLabel
|
|
51
|
+
this._isIncremental = job.mode === 'delta'
|
|
52
|
+
this._healthCheckSr = healthCheckSr
|
|
53
|
+
this._jobId = job.id
|
|
54
|
+
this._jobSnapshots = undefined
|
|
55
|
+
this._throttleStream = throttleStream
|
|
56
|
+
this._xapi = vm.$xapi
|
|
57
|
+
|
|
58
|
+
// Base VM for the export
|
|
59
|
+
this._baseVm = undefined
|
|
60
|
+
|
|
61
|
+
// Settings for this specific run (job, schedule, VM)
|
|
62
|
+
if (tags.includes('xo-memory-backup')) {
|
|
63
|
+
settings.checkpointSnapshot = true
|
|
64
|
+
}
|
|
65
|
+
if (tags.includes('xo-offline-backup')) {
|
|
66
|
+
settings.offlineSnapshot = true
|
|
67
|
+
}
|
|
68
|
+
this._settings = settings
|
|
69
|
+
// Create writers
|
|
70
|
+
{
|
|
71
|
+
const writers = new Set()
|
|
72
|
+
this._writers = writers
|
|
73
|
+
|
|
74
|
+
const [BackupWriter, ReplicationWriter] = this._getWriters()
|
|
75
|
+
|
|
76
|
+
const allSettings = job.settings
|
|
77
|
+
Object.entries(remoteAdapters).forEach(([remoteId, adapter]) => {
|
|
78
|
+
const targetSettings = {
|
|
79
|
+
...settings,
|
|
80
|
+
...allSettings[remoteId],
|
|
81
|
+
}
|
|
82
|
+
if (targetSettings.exportRetention !== 0) {
|
|
83
|
+
writers.add(new BackupWriter({ adapter, config, healthCheckSr, job, vmUuid: vm.uuid, remoteId, settings: targetSettings }))
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
srs.forEach(sr => {
|
|
87
|
+
const targetSettings = {
|
|
88
|
+
...settings,
|
|
89
|
+
...allSettings[sr.uuid],
|
|
90
|
+
}
|
|
91
|
+
if (targetSettings.copyRetention !== 0) {
|
|
92
|
+
writers.add(new ReplicationWriter({ config, healthCheckSr, job, vmUuid: vm.uuid, sr, settings: targetSettings}))
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ensure the VM itself does not have any backup metadata which would be
|
|
99
|
+
// copied on manual snapshots and interfere with the backup jobs
|
|
100
|
+
async _cleanMetadata() {
|
|
101
|
+
const vm = this._vm
|
|
102
|
+
if ('xo:backup:job' in vm.other_config) {
|
|
103
|
+
await vm.update_other_config({
|
|
104
|
+
'xo:backup:datetime': null,
|
|
105
|
+
'xo:backup:deltaChainLength': null,
|
|
106
|
+
'xo:backup:exported': null,
|
|
107
|
+
'xo:backup:job': null,
|
|
108
|
+
'xo:backup:schedule': null,
|
|
109
|
+
'xo:backup:vm': null,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async _snapshot() {
|
|
115
|
+
const vm = this._vm
|
|
116
|
+
const xapi = this._xapi
|
|
117
|
+
|
|
118
|
+
const settings = this._settings
|
|
119
|
+
|
|
120
|
+
if (this._mustDoSnapshot()) {
|
|
121
|
+
await Task.run({ name: 'snapshot' }, async () => {
|
|
122
|
+
if (!settings.bypassVdiChainsCheck) {
|
|
123
|
+
await vm.$assertHealthyVdiChains()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const snapshotRef = await vm[settings.checkpointSnapshot ? '$checkpoint' : '$snapshot']({
|
|
127
|
+
ignoreNobakVdis: true,
|
|
128
|
+
name_label: this._getSnapshotNameLabel(vm),
|
|
129
|
+
unplugVusbs: true,
|
|
130
|
+
})
|
|
131
|
+
this.timestamp = Date.now()
|
|
132
|
+
|
|
133
|
+
await xapi.setFieldEntries('VM', snapshotRef, 'other_config', {
|
|
134
|
+
'xo:backup:datetime': formatDateTime(this.timestamp),
|
|
135
|
+
'xo:backup:job': this._jobId,
|
|
136
|
+
'xo:backup:schedule': this.scheduleId,
|
|
137
|
+
'xo:backup:vm': vm.uuid,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
this._exportedVm = await xapi.getRecord('VM', snapshotRef)
|
|
141
|
+
|
|
142
|
+
return this._exportedVm.uuid
|
|
143
|
+
})
|
|
144
|
+
} else {
|
|
145
|
+
this._exportedVm = vm
|
|
146
|
+
this.timestamp = Date.now()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async _fetchJobSnapshots() {
|
|
151
|
+
const jobId = this._jobId
|
|
152
|
+
const vmRef = this._vm.$ref
|
|
153
|
+
const xapi = this._xapi
|
|
154
|
+
|
|
155
|
+
const snapshotsRef = await xapi.getField('VM', vmRef, 'snapshots')
|
|
156
|
+
const snapshotsOtherConfig = await asyncMap(snapshotsRef, ref => xapi.getField('VM', ref, 'other_config'))
|
|
157
|
+
|
|
158
|
+
const snapshots = []
|
|
159
|
+
snapshotsOtherConfig.forEach((other_config, i) => {
|
|
160
|
+
if (other_config['xo:backup:job'] === jobId) {
|
|
161
|
+
snapshots.push({ other_config, $ref: snapshotsRef[i] })
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
snapshots.sort((a, b) => (a.other_config['xo:backup:datetime'] < b.other_config['xo:backup:datetime'] ? -1 : 1))
|
|
165
|
+
this._jobSnapshots = snapshots
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async _removeUnusedSnapshots() {
|
|
169
|
+
const allSettings = this.job.settings
|
|
170
|
+
const baseSettings = this._baseSettings
|
|
171
|
+
const baseVmRef = this._baseVm?.$ref
|
|
172
|
+
|
|
173
|
+
const snapshotsPerSchedule = groupBy(this._jobSnapshots, _ => _.other_config['xo:backup:schedule'])
|
|
174
|
+
const xapi = this._xapi
|
|
175
|
+
await asyncMap(Object.entries(snapshotsPerSchedule), ([scheduleId, snapshots]) => {
|
|
176
|
+
const settings = {
|
|
177
|
+
...baseSettings,
|
|
178
|
+
...allSettings[scheduleId],
|
|
179
|
+
...allSettings[this._vm.uuid],
|
|
180
|
+
}
|
|
181
|
+
return asyncMap(getOldEntries(settings.snapshotRetention, snapshots), ({ $ref }) => {
|
|
182
|
+
if ($ref !== baseVmRef) {
|
|
183
|
+
return xapi.VM_destroy($ref)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async copy() {
|
|
190
|
+
throw new Error('Not implemented')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
_getWriters() {
|
|
194
|
+
throw new Error('Not implemented')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
_mustDoSnapshot() {
|
|
198
|
+
throw new Error('Not implemented')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async _selectBaseVm() {
|
|
202
|
+
throw new Error('Not implemented')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async run($defer) {
|
|
206
|
+
const settings = this._settings
|
|
207
|
+
assert(
|
|
208
|
+
!settings.offlineBackup || settings.snapshotRetention === 0,
|
|
209
|
+
'offlineBackup is not compatible with snapshotRetention'
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
await this._callWriters(async writer => {
|
|
213
|
+
await writer.beforeBackup()
|
|
214
|
+
$defer(async () => {
|
|
215
|
+
await writer.afterBackup()
|
|
216
|
+
})
|
|
217
|
+
}, 'writer.beforeBackup()')
|
|
218
|
+
|
|
219
|
+
await this._fetchJobSnapshots()
|
|
220
|
+
|
|
221
|
+
await this._selectBaseVm()
|
|
222
|
+
|
|
223
|
+
await this._cleanMetadata()
|
|
224
|
+
await this._removeUnusedSnapshots()
|
|
225
|
+
|
|
226
|
+
const vm = this._vm
|
|
227
|
+
const isRunning = vm.power_state === 'Running'
|
|
228
|
+
const startAfter = isRunning && (settings.offlineBackup ? 'backup' : settings.offlineSnapshot && 'snapshot')
|
|
229
|
+
if (startAfter) {
|
|
230
|
+
await vm.$callAsync('clean_shutdown')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
await this._snapshot()
|
|
235
|
+
if (startAfter === 'snapshot') {
|
|
236
|
+
ignoreErrors.call(vm.$callAsync('start', false, false))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (this._writers.size !== 0) {
|
|
240
|
+
await this._copy()
|
|
241
|
+
}
|
|
242
|
+
} finally {
|
|
243
|
+
if (startAfter) {
|
|
244
|
+
ignoreErrors.call(vm.$callAsync('start', false, false))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await this._fetchJobSnapshots()
|
|
248
|
+
await this._removeUnusedSnapshots()
|
|
249
|
+
}
|
|
250
|
+
await this._healthCheck()
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
exports.AbstractXapi = AbstractXapiVmBackupRunner
|
|
254
|
+
|
|
255
|
+
decorateMethodsWith(AbstractXapiVmBackupRunner, {
|
|
256
|
+
run: defer,
|
|
257
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { mapValues } = require('lodash')
|
|
4
|
+
const { forkStreamUnpipe } = require('../_forkStreamUnpipe')
|
|
5
|
+
|
|
6
|
+
exports.forkDeltaExport = function forkDeltaExport(deltaExport) {
|
|
7
|
+
return Object.create(deltaExport, {
|
|
8
|
+
streams: {
|
|
9
|
+
value: mapValues(deltaExport.streams, forkStreamUnpipe),
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { formatFilenameDate } = require('
|
|
4
|
-
const { getOldEntries } = require('
|
|
5
|
-
const { Task } = require('
|
|
3
|
+
const { formatFilenameDate } = require('../../_filenameDate.js')
|
|
4
|
+
const { getOldEntries } = require('../../_getOldEntries.js')
|
|
5
|
+
const { Task } = require('../../Task.js')
|
|
6
6
|
|
|
7
|
-
const {
|
|
7
|
+
const { MixinRemoteWriter } = require('./_MixinRemoteWriter.js')
|
|
8
8
|
const { AbstractFullWriter } = require('./_AbstractFullWriter.js')
|
|
9
9
|
|
|
10
|
-
exports.
|
|
10
|
+
exports.FullRemoteWriter = class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
|
|
11
11
|
constructor(props) {
|
|
12
12
|
super(props)
|
|
13
13
|
|
|
@@ -26,15 +26,17 @@ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(Abst
|
|
|
26
26
|
)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
async _run({ timestamp, sizeContainer, stream }) {
|
|
30
|
-
const backup = this._backup
|
|
29
|
+
async _run({ timestamp, sizeContainer, stream, vm, vmSnapshot }) {
|
|
31
30
|
const settings = this._settings
|
|
32
|
-
|
|
33
|
-
const
|
|
31
|
+
const job = this._job
|
|
32
|
+
const scheduleId = this._scheduleId
|
|
34
33
|
|
|
35
34
|
const adapter = this._adapter
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
let metadata = await this._isAlreadyTransferred(timestamp)
|
|
36
|
+
if (metadata !== undefined) {
|
|
37
|
+
// @todo : should skip backup while being vigilant to not stuck the forked stream
|
|
38
|
+
Task.info('This backup has already been transfered')
|
|
39
|
+
}
|
|
38
40
|
|
|
39
41
|
const oldBackups = getOldEntries(
|
|
40
42
|
settings.exportRetention - 1,
|
|
@@ -47,14 +49,14 @@ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(Abst
|
|
|
47
49
|
const dataBasename = basename + '.xva'
|
|
48
50
|
const dataFilename = this._vmBackupDir + '/' + dataBasename
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
metadata = {
|
|
51
53
|
jobId: job.id,
|
|
52
54
|
mode: job.mode,
|
|
53
55
|
scheduleId,
|
|
54
56
|
timestamp,
|
|
55
57
|
version: '2.0.0',
|
|
56
58
|
vm,
|
|
57
|
-
vmSnapshot
|
|
59
|
+
vmSnapshot,
|
|
58
60
|
xva: './' + dataBasename,
|
|
59
61
|
}
|
|
60
62
|
|