@xen-orchestra/backups 0.36.0 → 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} +10 -10
- 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 +6 -6
- 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
package/Backup.js
CHANGED
|
@@ -1,307 +1,16 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const { XoMetadataBackup } = require('./_XoMetadataBackup.js')
|
|
15
|
-
const createStreamThrottle = require('./_createStreamThrottle.js')
|
|
16
|
-
|
|
17
|
-
const noop = Function.prototype
|
|
18
|
-
|
|
19
|
-
const getAdaptersByRemote = adapters => {
|
|
20
|
-
const adaptersByRemote = {}
|
|
21
|
-
adapters.forEach(({ adapter, remoteId }) => {
|
|
22
|
-
adaptersByRemote[remoteId] = adapter
|
|
23
|
-
})
|
|
24
|
-
return adaptersByRemote
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const runTask = (...args) => Task.run(...args).catch(noop) // errors are handled by logs
|
|
28
|
-
|
|
29
|
-
const DEFAULT_SETTINGS = {
|
|
30
|
-
getRemoteTimeout: 300e3,
|
|
31
|
-
reportWhen: 'failure',
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const DEFAULT_VM_SETTINGS = {
|
|
35
|
-
bypassVdiChainsCheck: false,
|
|
36
|
-
checkpointSnapshot: false,
|
|
37
|
-
concurrency: 2,
|
|
38
|
-
copyRetention: 0,
|
|
39
|
-
deleteFirst: false,
|
|
40
|
-
exportRetention: 0,
|
|
41
|
-
fullInterval: 0,
|
|
42
|
-
healthCheckSr: undefined,
|
|
43
|
-
healthCheckVmsWithTags: [],
|
|
44
|
-
maxExportRate: 0,
|
|
45
|
-
maxMergedDeltasPerRun: Infinity,
|
|
46
|
-
offlineBackup: false,
|
|
47
|
-
offlineSnapshot: false,
|
|
48
|
-
snapshotRetention: 0,
|
|
49
|
-
timeout: 0,
|
|
50
|
-
useNbd: false,
|
|
51
|
-
unconditionalSnapshot: false,
|
|
52
|
-
validateVhdStreams: false,
|
|
53
|
-
vmTimeout: 0,
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const DEFAULT_METADATA_SETTINGS = {
|
|
57
|
-
retentionPoolMetadata: 0,
|
|
58
|
-
retentionXoMetadata: 0,
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
class RemoteTimeoutError extends Error {
|
|
62
|
-
constructor(remoteId) {
|
|
63
|
-
super('timeout while getting the remote ' + remoteId)
|
|
64
|
-
this.remoteId = remoteId
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
exports.Backup = class Backup {
|
|
69
|
-
constructor({ config, getAdapter, getConnectedRecord, job, schedule }) {
|
|
70
|
-
this._config = config
|
|
71
|
-
this._getRecord = getConnectedRecord
|
|
72
|
-
this._job = job
|
|
73
|
-
this._schedule = schedule
|
|
74
|
-
|
|
75
|
-
this._getSnapshotNameLabel = compileTemplate(config.snapshotNameLabelTpl, {
|
|
76
|
-
'{job.name}': job.name,
|
|
77
|
-
'{vm.name_label}': vm => vm.name_label,
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
const { type } = job
|
|
81
|
-
const baseSettings = { ...DEFAULT_SETTINGS }
|
|
82
|
-
if (type === 'backup') {
|
|
83
|
-
Object.assign(baseSettings, DEFAULT_VM_SETTINGS, config.defaultSettings, config.vm?.defaultSettings)
|
|
84
|
-
this.run = this._runVmBackup
|
|
85
|
-
} else if (type === 'metadataBackup') {
|
|
86
|
-
Object.assign(baseSettings, DEFAULT_METADATA_SETTINGS, config.defaultSettings, config.metadata?.defaultSettings)
|
|
87
|
-
this.run = this._runMetadataBackup
|
|
88
|
-
} else {
|
|
3
|
+
const { Metadata } = require('./_runners/Metadata.js')
|
|
4
|
+
const { VmsXapi } = require('./_runners/VmsXapi.js')
|
|
5
|
+
|
|
6
|
+
exports.createRunner = function createRunner(opts) {
|
|
7
|
+
const { type } = opts.job
|
|
8
|
+
switch (type) {
|
|
9
|
+
case 'backup':
|
|
10
|
+
return new VmsXapi(opts)
|
|
11
|
+
case 'metadataBackup':
|
|
12
|
+
return new Metadata(opts)
|
|
13
|
+
default:
|
|
89
14
|
throw new Error(`No runner for the backup type ${type}`)
|
|
90
|
-
}
|
|
91
|
-
Object.assign(baseSettings, job.settings[''])
|
|
92
|
-
|
|
93
|
-
this._baseSettings = baseSettings
|
|
94
|
-
this._settings = { ...baseSettings, ...job.settings[schedule.id] }
|
|
95
|
-
|
|
96
|
-
const { getRemoteTimeout } = this._settings
|
|
97
|
-
this._getAdapter = async function (remoteId) {
|
|
98
|
-
try {
|
|
99
|
-
const disposable = await pTimeout.call(getAdapter(remoteId), getRemoteTimeout, new RemoteTimeoutError(remoteId))
|
|
100
|
-
|
|
101
|
-
return new Disposable(() => disposable.dispose(), {
|
|
102
|
-
adapter: disposable.value,
|
|
103
|
-
remoteId,
|
|
104
|
-
})
|
|
105
|
-
} catch (error) {
|
|
106
|
-
// See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
|
|
107
|
-
runTask(
|
|
108
|
-
{
|
|
109
|
-
name: 'get remote adapter',
|
|
110
|
-
data: { type: 'remote', id: remoteId },
|
|
111
|
-
},
|
|
112
|
-
() => Promise.reject(error)
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async _runMetadataBackup() {
|
|
119
|
-
const schedule = this._schedule
|
|
120
|
-
const job = this._job
|
|
121
|
-
const remoteIds = extractIdsFromSimplePattern(job.remotes)
|
|
122
|
-
if (remoteIds.length === 0) {
|
|
123
|
-
throw new Error('metadata backup job cannot run without remotes')
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const config = this._config
|
|
127
|
-
const poolIds = extractIdsFromSimplePattern(job.pools)
|
|
128
|
-
const isEmptyPools = poolIds.length === 0
|
|
129
|
-
const isXoMetadata = job.xoMetadata !== undefined
|
|
130
|
-
if (!isXoMetadata && isEmptyPools) {
|
|
131
|
-
throw new Error('no metadata mode found')
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const settings = this._settings
|
|
135
|
-
|
|
136
|
-
const { retentionPoolMetadata, retentionXoMetadata } = settings
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
(retentionPoolMetadata === 0 && retentionXoMetadata === 0) ||
|
|
140
|
-
(!isXoMetadata && retentionPoolMetadata === 0) ||
|
|
141
|
-
(isEmptyPools && retentionXoMetadata === 0)
|
|
142
|
-
) {
|
|
143
|
-
throw new Error('no retentions corresponding to the metadata modes found')
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
await Disposable.use(
|
|
147
|
-
Disposable.all(
|
|
148
|
-
poolIds.map(id =>
|
|
149
|
-
this._getRecord('pool', id).catch(error => {
|
|
150
|
-
// See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
|
|
151
|
-
runTask(
|
|
152
|
-
{
|
|
153
|
-
name: 'get pool record',
|
|
154
|
-
data: { type: 'pool', id },
|
|
155
|
-
},
|
|
156
|
-
() => Promise.reject(error)
|
|
157
|
-
)
|
|
158
|
-
})
|
|
159
|
-
)
|
|
160
|
-
),
|
|
161
|
-
Disposable.all(remoteIds.map(id => this._getAdapter(id))),
|
|
162
|
-
async (pools, remoteAdapters) => {
|
|
163
|
-
// remove adapters that failed (already handled)
|
|
164
|
-
remoteAdapters = remoteAdapters.filter(_ => _ !== undefined)
|
|
165
|
-
if (remoteAdapters.length === 0) {
|
|
166
|
-
return
|
|
167
|
-
}
|
|
168
|
-
remoteAdapters = getAdaptersByRemote(remoteAdapters)
|
|
169
|
-
|
|
170
|
-
// remove pools that failed (already handled)
|
|
171
|
-
pools = pools.filter(_ => _ !== undefined)
|
|
172
|
-
|
|
173
|
-
const promises = []
|
|
174
|
-
if (pools.length !== 0 && settings.retentionPoolMetadata !== 0) {
|
|
175
|
-
promises.push(
|
|
176
|
-
asyncMap(pools, async pool =>
|
|
177
|
-
runTask(
|
|
178
|
-
{
|
|
179
|
-
name: `Starting metadata backup for the pool (${pool.$id}). (${job.id})`,
|
|
180
|
-
data: {
|
|
181
|
-
id: pool.$id,
|
|
182
|
-
pool,
|
|
183
|
-
poolMaster: await ignoreErrors.call(pool.$xapi.getRecord('host', pool.master)),
|
|
184
|
-
type: 'pool',
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
() =>
|
|
188
|
-
new PoolMetadataBackup({
|
|
189
|
-
config,
|
|
190
|
-
job,
|
|
191
|
-
pool,
|
|
192
|
-
remoteAdapters,
|
|
193
|
-
schedule,
|
|
194
|
-
settings,
|
|
195
|
-
}).run()
|
|
196
|
-
)
|
|
197
|
-
)
|
|
198
|
-
)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (job.xoMetadata !== undefined && settings.retentionXoMetadata !== 0) {
|
|
202
|
-
promises.push(
|
|
203
|
-
runTask(
|
|
204
|
-
{
|
|
205
|
-
name: `Starting XO metadata backup. (${job.id})`,
|
|
206
|
-
data: {
|
|
207
|
-
type: 'xo',
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
() =>
|
|
211
|
-
new XoMetadataBackup({
|
|
212
|
-
config,
|
|
213
|
-
job,
|
|
214
|
-
remoteAdapters,
|
|
215
|
-
schedule,
|
|
216
|
-
settings,
|
|
217
|
-
}).run()
|
|
218
|
-
)
|
|
219
|
-
)
|
|
220
|
-
}
|
|
221
|
-
await Promise.all(promises)
|
|
222
|
-
}
|
|
223
|
-
)
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async _runVmBackup() {
|
|
227
|
-
const job = this._job
|
|
228
|
-
|
|
229
|
-
// FIXME: proper SimpleIdPattern handling
|
|
230
|
-
const getSnapshotNameLabel = this._getSnapshotNameLabel
|
|
231
|
-
const schedule = this._schedule
|
|
232
|
-
const settings = this._settings
|
|
233
|
-
|
|
234
|
-
const throttleStream = createStreamThrottle(settings.maxExportRate)
|
|
235
|
-
|
|
236
|
-
const config = this._config
|
|
237
|
-
await Disposable.use(
|
|
238
|
-
Disposable.all(
|
|
239
|
-
extractIdsFromSimplePattern(job.srs).map(id =>
|
|
240
|
-
this._getRecord('SR', id).catch(error => {
|
|
241
|
-
runTask(
|
|
242
|
-
{
|
|
243
|
-
name: 'get SR record',
|
|
244
|
-
data: { type: 'SR', id },
|
|
245
|
-
},
|
|
246
|
-
() => Promise.reject(error)
|
|
247
|
-
)
|
|
248
|
-
})
|
|
249
|
-
)
|
|
250
|
-
),
|
|
251
|
-
Disposable.all(extractIdsFromSimplePattern(job.remotes).map(id => this._getAdapter(id))),
|
|
252
|
-
() => (settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined),
|
|
253
|
-
async (srs, remoteAdapters, healthCheckSr) => {
|
|
254
|
-
// remove adapters that failed (already handled)
|
|
255
|
-
remoteAdapters = remoteAdapters.filter(_ => _ !== undefined)
|
|
256
|
-
|
|
257
|
-
// remove srs that failed (already handled)
|
|
258
|
-
srs = srs.filter(_ => _ !== undefined)
|
|
259
|
-
|
|
260
|
-
if (remoteAdapters.length === 0 && srs.length === 0 && settings.snapshotRetention === 0) {
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const vmIds = extractIdsFromSimplePattern(job.vms)
|
|
265
|
-
|
|
266
|
-
Task.info('vms', { vms: vmIds })
|
|
267
|
-
|
|
268
|
-
remoteAdapters = getAdaptersByRemote(remoteAdapters)
|
|
269
|
-
|
|
270
|
-
const allSettings = this._job.settings
|
|
271
|
-
const baseSettings = this._baseSettings
|
|
272
|
-
|
|
273
|
-
const handleVm = vmUuid => {
|
|
274
|
-
const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
|
|
275
|
-
|
|
276
|
-
return this._getRecord('VM', vmUuid).then(
|
|
277
|
-
disposableVm =>
|
|
278
|
-
Disposable.use(disposableVm, vm => {
|
|
279
|
-
taskStart.data.name_label = vm.name_label
|
|
280
|
-
return runTask(taskStart, () =>
|
|
281
|
-
new VmBackup({
|
|
282
|
-
baseSettings,
|
|
283
|
-
config,
|
|
284
|
-
getSnapshotNameLabel,
|
|
285
|
-
healthCheckSr,
|
|
286
|
-
job,
|
|
287
|
-
remoteAdapters,
|
|
288
|
-
schedule,
|
|
289
|
-
settings: { ...settings, ...allSettings[vm.uuid] },
|
|
290
|
-
srs,
|
|
291
|
-
throttleStream,
|
|
292
|
-
vm,
|
|
293
|
-
}).run()
|
|
294
|
-
)
|
|
295
|
-
}),
|
|
296
|
-
error =>
|
|
297
|
-
runTask(taskStart, () => {
|
|
298
|
-
throw error
|
|
299
|
-
})
|
|
300
|
-
)
|
|
301
|
-
}
|
|
302
|
-
const { concurrency } = settings
|
|
303
|
-
await asyncMapSettled(vmIds, concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm))
|
|
304
|
-
}
|
|
305
|
-
)
|
|
306
15
|
}
|
|
307
16
|
}
|
package/ImportVmBackup.js
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
const assert = require('assert')
|
|
4
4
|
|
|
5
5
|
const { formatFilenameDate } = require('./_filenameDate.js')
|
|
6
|
-
const {
|
|
6
|
+
const { importIncrementalVm } = require('./_incrementalVm.js')
|
|
7
7
|
const { Task } = require('./Task.js')
|
|
8
8
|
const { watchStreamSize } = require('./_watchStreamSize.js')
|
|
9
9
|
|
|
10
10
|
exports.ImportVmBackup = class ImportVmBackup {
|
|
11
11
|
constructor({ adapter, metadata, srUuid, xapi, settings: { newMacAddresses, mapVdisSrs = {} } = {} }) {
|
|
12
12
|
this._adapter = adapter
|
|
13
|
-
this.
|
|
13
|
+
this._importIncrementalVmSettings = { newMacAddresses, mapVdisSrs }
|
|
14
14
|
this._metadata = metadata
|
|
15
15
|
this._srUuid = srUuid
|
|
16
16
|
this._xapi = xapi
|
|
@@ -31,11 +31,11 @@ exports.ImportVmBackup = class ImportVmBackup {
|
|
|
31
31
|
assert.strictEqual(metadata.mode, 'delta')
|
|
32
32
|
|
|
33
33
|
const ignoredVdis = new Set(
|
|
34
|
-
Object.entries(this.
|
|
34
|
+
Object.entries(this._importIncrementalVmSettings.mapVdisSrs)
|
|
35
35
|
.filter(([_, srUuid]) => srUuid === null)
|
|
36
36
|
.map(([vdiUuid]) => vdiUuid)
|
|
37
37
|
)
|
|
38
|
-
backup = await adapter.
|
|
38
|
+
backup = await adapter.readIncrementalVmBackup(metadata, ignoredVdis)
|
|
39
39
|
Object.values(backup.streams).forEach(stream => watchStreamSize(stream, sizeContainer))
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -49,8 +49,8 @@ exports.ImportVmBackup = class ImportVmBackup {
|
|
|
49
49
|
|
|
50
50
|
const vmRef = isFull
|
|
51
51
|
? await xapi.VM_import(backup, srRef)
|
|
52
|
-
: await
|
|
53
|
-
...this.
|
|
52
|
+
: await importIncrementalVm(backup, await xapi.getRecord('SR', srRef), {
|
|
53
|
+
...this._importIncrementalVmSettings,
|
|
54
54
|
detectBase: false,
|
|
55
55
|
})
|
|
56
56
|
|
package/RemoteAdapter.js
CHANGED
|
@@ -333,7 +333,7 @@ class RemoteAdapter {
|
|
|
333
333
|
const RE_VHDI = /^vhdi(\d+)$/
|
|
334
334
|
const handler = this._handler
|
|
335
335
|
|
|
336
|
-
const diskPath = handler.
|
|
336
|
+
const diskPath = handler.getFilePath('/' + diskId)
|
|
337
337
|
const mountDir = yield getTmpDir()
|
|
338
338
|
await fromCallback(execFile, 'vhdimount', [diskPath, mountDir])
|
|
339
339
|
try {
|
|
@@ -404,20 +404,27 @@ class RemoteAdapter {
|
|
|
404
404
|
return `${baseName}.vhd`
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
-
async
|
|
407
|
+
async listAllVms() {
|
|
408
408
|
const handler = this._handler
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
await asyncMap(await handler.list(BACKUP_DIR), async entry => {
|
|
409
|
+
const vmsUuids = []
|
|
410
|
+
await asyncEach(await handler.list(BACKUP_DIR), async entry => {
|
|
412
411
|
// ignore hidden and lock files
|
|
413
412
|
if (entry[0] !== '.' && !entry.endsWith('.lock')) {
|
|
414
|
-
|
|
415
|
-
if (vmBackups.length !== 0) {
|
|
416
|
-
backups[entry] = vmBackups
|
|
417
|
-
}
|
|
413
|
+
vmsUuids.push(entry)
|
|
418
414
|
}
|
|
419
415
|
})
|
|
416
|
+
return vmsUuids
|
|
417
|
+
}
|
|
420
418
|
|
|
419
|
+
async listAllVmBackups() {
|
|
420
|
+
const vmsUuids = await this.listAllVms()
|
|
421
|
+
const backups = { __proto__: null }
|
|
422
|
+
await asyncEach(vmsUuids, async vmUuid => {
|
|
423
|
+
const vmBackups = await this.listVmBackups(vmUuid)
|
|
424
|
+
if (vmBackups.length !== 0) {
|
|
425
|
+
backups[vmUuid] = vmBackups
|
|
426
|
+
}
|
|
427
|
+
})
|
|
421
428
|
return backups
|
|
422
429
|
}
|
|
423
430
|
|
|
@@ -691,8 +698,8 @@ class RemoteAdapter {
|
|
|
691
698
|
}
|
|
692
699
|
|
|
693
700
|
// open the hierarchy of ancestors until we find a full one
|
|
694
|
-
async
|
|
695
|
-
const disposableSynthetic = await VhdSynthetic.fromVhdChain(handler, path)
|
|
701
|
+
async _createVhdStream(handler, path, { useChain }) {
|
|
702
|
+
const disposableSynthetic = useChain ? await VhdSynthetic.fromVhdChain(handler, path) : await openVhd(handler, path)
|
|
696
703
|
// I don't want the vhds to be disposed on return
|
|
697
704
|
// but only when the stream is done ( or failed )
|
|
698
705
|
|
|
@@ -717,7 +724,7 @@ class RemoteAdapter {
|
|
|
717
724
|
return stream
|
|
718
725
|
}
|
|
719
726
|
|
|
720
|
-
async
|
|
727
|
+
async readIncrementalVmBackup(metadata, ignoredVdis, { useChain = true } = {}) {
|
|
721
728
|
const handler = this._handler
|
|
722
729
|
const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
|
|
723
730
|
const dir = dirname(metadata._filename)
|
|
@@ -725,7 +732,7 @@ class RemoteAdapter {
|
|
|
725
732
|
|
|
726
733
|
const streams = {}
|
|
727
734
|
await asyncMapSettled(Object.keys(vdis), async ref => {
|
|
728
|
-
streams[`${ref}.vhd`] = await this.
|
|
735
|
+
streams[`${ref}.vhd`] = await this._createVhdStream(handler, join(dir, vhds[ref]), { useChain })
|
|
729
736
|
})
|
|
730
737
|
|
|
731
738
|
return {
|
package/RestoreMetadataBackup.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter.js')
|
|
4
|
-
const { PATH_DB_DUMP } = require('./_PoolMetadataBackup.js')
|
|
4
|
+
const { PATH_DB_DUMP } = require('./_runners/_PoolMetadataBackup.js')
|
|
5
5
|
|
|
6
6
|
exports.RestoreMetadataBackup = class RestoreMetadataBackup {
|
|
7
7
|
constructor({ backupId, handler, xapi }) {
|
package/_backupWorker.js
CHANGED
|
@@ -13,10 +13,10 @@ const { createDebounceResource } = require('@vates/disposable/debounceResource.j
|
|
|
13
13
|
const { decorateMethodsWith } = require('@vates/decorate-with')
|
|
14
14
|
const { deduped } = require('@vates/disposable/deduped.js')
|
|
15
15
|
const { getHandler } = require('@xen-orchestra/fs')
|
|
16
|
+
const { createRunner } = require('./Backup.js')
|
|
16
17
|
const { parseDuration } = require('@vates/parse-duration')
|
|
17
18
|
const { Xapi } = require('@xen-orchestra/xapi')
|
|
18
19
|
|
|
19
|
-
const { Backup } = require('./Backup.js')
|
|
20
20
|
const { RemoteAdapter } = require('./RemoteAdapter.js')
|
|
21
21
|
const { Task } = require('./Task.js')
|
|
22
22
|
|
|
@@ -48,7 +48,7 @@ class BackupWorker {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
run() {
|
|
51
|
-
return
|
|
51
|
+
return createRunner({
|
|
52
52
|
config: this.#config,
|
|
53
53
|
getAdapter: remoteId => this.getAdapter(this.#remotes[remoteId]),
|
|
54
54
|
getConnectedRecord: Disposable.factory(async function* getConnectedRecord(type, uuid) {
|
|
@@ -33,7 +33,7 @@ const resolveUuid = async (xapi, cache, uuid, type) => {
|
|
|
33
33
|
return ref
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
exports.
|
|
36
|
+
exports.exportIncrementalVm = async function exportIncrementalVm(
|
|
37
37
|
vm,
|
|
38
38
|
baseVm,
|
|
39
39
|
{
|
|
@@ -143,18 +143,18 @@ exports.exportDeltaVm = async function exportDeltaVm(
|
|
|
143
143
|
)
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
exports.
|
|
146
|
+
exports.importIncrementalVm = defer(async function importIncrementalVm(
|
|
147
147
|
$defer,
|
|
148
|
-
|
|
148
|
+
incrementalVm,
|
|
149
149
|
sr,
|
|
150
150
|
{ cancelToken = CancelToken.none, detectBase = true, mapVdisSrs = {}, newMacAddresses = false } = {}
|
|
151
151
|
) {
|
|
152
|
-
const { version } =
|
|
152
|
+
const { version } = incrementalVm
|
|
153
153
|
if (compareVersions(version, '1.0.0') < 0) {
|
|
154
154
|
throw new Error(`Unsupported delta backup version: ${version}`)
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
const vmRecord =
|
|
157
|
+
const vmRecord = incrementalVm.vm
|
|
158
158
|
const xapi = sr.$xapi
|
|
159
159
|
|
|
160
160
|
let baseVm
|
|
@@ -183,7 +183,7 @@ exports.importDeltaVm = defer(async function importDeltaVm(
|
|
|
183
183
|
baseVdis[vbd.VDI] = vbd.$VDI
|
|
184
184
|
}
|
|
185
185
|
})
|
|
186
|
-
const vdiRecords =
|
|
186
|
+
const vdiRecords = incrementalVm.vdis
|
|
187
187
|
|
|
188
188
|
// 0. Create suspend_VDI
|
|
189
189
|
let suspendVdi
|
|
@@ -240,7 +240,7 @@ exports.importDeltaVm = defer(async function importDeltaVm(
|
|
|
240
240
|
await asyncMap(await xapi.getField('VM', vmRef, 'VBDs'), ref => ignoreErrors.call(xapi.call('VBD.destroy', ref)))
|
|
241
241
|
|
|
242
242
|
// 3. Create VDIs & VBDs.
|
|
243
|
-
const vbdRecords =
|
|
243
|
+
const vbdRecords = incrementalVm.vbds
|
|
244
244
|
const vbds = groupBy(vbdRecords, 'VDI')
|
|
245
245
|
const newVdis = {}
|
|
246
246
|
await asyncMap(Object.keys(vdiRecords), async vdiRef => {
|
|
@@ -309,7 +309,7 @@ exports.importDeltaVm = defer(async function importDeltaVm(
|
|
|
309
309
|
}
|
|
310
310
|
})
|
|
311
311
|
|
|
312
|
-
const { streams } =
|
|
312
|
+
const { streams } = incrementalVm
|
|
313
313
|
|
|
314
314
|
await Promise.all([
|
|
315
315
|
// Import VDI contents.
|
|
@@ -326,7 +326,7 @@ exports.importDeltaVm = defer(async function importDeltaVm(
|
|
|
326
326
|
}),
|
|
327
327
|
|
|
328
328
|
// Create VIFs.
|
|
329
|
-
asyncMap(Object.values(
|
|
329
|
+
asyncMap(Object.values(incrementalVm.vifs), vif => {
|
|
330
330
|
let network = vif.$network$uuid && xapi.getObjectByUuid(vif.$network$uuid, undefined)
|
|
331
331
|
|
|
332
332
|
if (network === undefined) {
|
|
@@ -358,8 +358,8 @@ exports.importDeltaVm = defer(async function importDeltaVm(
|
|
|
358
358
|
])
|
|
359
359
|
|
|
360
360
|
await Promise.all([
|
|
361
|
-
|
|
362
|
-
xapi.setField('VM', vmRef, 'name_label',
|
|
361
|
+
incrementalVm.vm.ha_always_run && xapi.setField('VM', vmRef, 'ha_always_run', true),
|
|
362
|
+
xapi.setField('VM', vmRef, 'name_label', incrementalVm.vm.name_label),
|
|
363
363
|
])
|
|
364
364
|
|
|
365
365
|
return vmRef
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
4
|
+
const Disposable = require('promise-toolbox/Disposable')
|
|
5
|
+
const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
|
6
|
+
|
|
7
|
+
const { extractIdsFromSimplePattern } = require('../extractIdsFromSimplePattern.js')
|
|
8
|
+
const { PoolMetadataBackup } = require('./_PoolMetadataBackup.js')
|
|
9
|
+
const { XoMetadataBackup } = require('./_XoMetadataBackup.js')
|
|
10
|
+
const { DEFAULT_SETTINGS, Abstract } = require('./_Abstract.js')
|
|
11
|
+
const { runTask } = require('./_runTask.js')
|
|
12
|
+
const { getAdaptersByRemote } = require('./_getAdaptersByRemote.js')
|
|
13
|
+
|
|
14
|
+
const DEFAULT_METADATA_SETTINGS = {
|
|
15
|
+
retentionPoolMetadata: 0,
|
|
16
|
+
retentionXoMetadata: 0,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
exports.Metadata = class MetadataBackupRunner extends Abstract {
|
|
20
|
+
_computeBaseSettings(config, job) {
|
|
21
|
+
const baseSettings = { ...DEFAULT_SETTINGS }
|
|
22
|
+
Object.assign(baseSettings, DEFAULT_METADATA_SETTINGS, config.defaultSettings, config.metadata?.defaultSettings)
|
|
23
|
+
Object.assign(baseSettings, job.settings[''])
|
|
24
|
+
return baseSettings
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async run() {
|
|
28
|
+
const schedule = this._schedule
|
|
29
|
+
const job = this._job
|
|
30
|
+
const remoteIds = extractIdsFromSimplePattern(job.remotes)
|
|
31
|
+
if (remoteIds.length === 0) {
|
|
32
|
+
throw new Error('metadata backup job cannot run without remotes')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const config = this._config
|
|
36
|
+
const poolIds = extractIdsFromSimplePattern(job.pools)
|
|
37
|
+
const isEmptyPools = poolIds.length === 0
|
|
38
|
+
const isXoMetadata = job.xoMetadata !== undefined
|
|
39
|
+
if (!isXoMetadata && isEmptyPools) {
|
|
40
|
+
throw new Error('no metadata mode found')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const settings = this._settings
|
|
44
|
+
|
|
45
|
+
const { retentionPoolMetadata, retentionXoMetadata } = settings
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
(retentionPoolMetadata === 0 && retentionXoMetadata === 0) ||
|
|
49
|
+
(!isXoMetadata && retentionPoolMetadata === 0) ||
|
|
50
|
+
(isEmptyPools && retentionXoMetadata === 0)
|
|
51
|
+
) {
|
|
52
|
+
throw new Error('no retentions corresponding to the metadata modes found')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await Disposable.use(
|
|
56
|
+
Disposable.all(
|
|
57
|
+
poolIds.map(id =>
|
|
58
|
+
this._getRecord('pool', id).catch(error => {
|
|
59
|
+
// See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
|
|
60
|
+
runTask(
|
|
61
|
+
{
|
|
62
|
+
name: 'get pool record',
|
|
63
|
+
data: { type: 'pool', id },
|
|
64
|
+
},
|
|
65
|
+
() => Promise.reject(error)
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
)
|
|
69
|
+
),
|
|
70
|
+
Disposable.all(remoteIds.map(id => this._getAdapter(id))),
|
|
71
|
+
async (pools, remoteAdapters) => {
|
|
72
|
+
// remove adapters that failed (already handled)
|
|
73
|
+
remoteAdapters = remoteAdapters.filter(_ => _ !== undefined)
|
|
74
|
+
if (remoteAdapters.length === 0) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
remoteAdapters = getAdaptersByRemote(remoteAdapters)
|
|
78
|
+
|
|
79
|
+
// remove pools that failed (already handled)
|
|
80
|
+
pools = pools.filter(_ => _ !== undefined)
|
|
81
|
+
|
|
82
|
+
const promises = []
|
|
83
|
+
if (pools.length !== 0 && settings.retentionPoolMetadata !== 0) {
|
|
84
|
+
promises.push(
|
|
85
|
+
asyncMap(pools, async pool =>
|
|
86
|
+
runTask(
|
|
87
|
+
{
|
|
88
|
+
name: `Starting metadata backup for the pool (${pool.$id}). (${job.id})`,
|
|
89
|
+
data: {
|
|
90
|
+
id: pool.$id,
|
|
91
|
+
pool,
|
|
92
|
+
poolMaster: await ignoreErrors.call(pool.$xapi.getRecord('host', pool.master)),
|
|
93
|
+
type: 'pool',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
() =>
|
|
97
|
+
new PoolMetadataBackup({
|
|
98
|
+
config,
|
|
99
|
+
job,
|
|
100
|
+
pool,
|
|
101
|
+
remoteAdapters,
|
|
102
|
+
schedule,
|
|
103
|
+
settings,
|
|
104
|
+
}).run()
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (job.xoMetadata !== undefined && settings.retentionXoMetadata !== 0) {
|
|
111
|
+
promises.push(
|
|
112
|
+
runTask(
|
|
113
|
+
{
|
|
114
|
+
name: `Starting XO metadata backup. (${job.id})`,
|
|
115
|
+
data: {
|
|
116
|
+
type: 'xo',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
() =>
|
|
120
|
+
new XoMetadataBackup({
|
|
121
|
+
config,
|
|
122
|
+
job,
|
|
123
|
+
remoteAdapters,
|
|
124
|
+
schedule,
|
|
125
|
+
settings,
|
|
126
|
+
}).run()
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
await Promise.all(promises)
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|