@xen-orchestra/backups 0.23.0 → 0.26.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 +52 -26
- package/HealthCheckVmBackup.js +64 -0
- package/RemoteAdapter.js +104 -50
- package/_VmBackup.js +51 -14
- package/_cleanVm.js +67 -83
- package/{_extractIdsFromSimplePattern.js → extractIdsFromSimplePattern.js} +0 -0
- package/merge-worker/cli.js +2 -0
- package/package.json +5 -4
- package/writers/DeltaBackupWriter.js +35 -2
- package/writers/_AbstractWriter.js +2 -0
- package/writers/_MixinBackupWriter.js +14 -6
- package/gitignore-cleanVm-debug.js +0 -516
package/Backup.js
CHANGED
|
@@ -6,7 +6,7 @@ const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
|
|
6
6
|
const { compileTemplate } = require('@xen-orchestra/template')
|
|
7
7
|
const { limitConcurrency } = require('limit-concurrency-decorator')
|
|
8
8
|
|
|
9
|
-
const { extractIdsFromSimplePattern } = require('./
|
|
9
|
+
const { extractIdsFromSimplePattern } = require('./extractIdsFromSimplePattern.js')
|
|
10
10
|
const { PoolMetadataBackup } = require('./_PoolMetadataBackup.js')
|
|
11
11
|
const { Task } = require('./Task.js')
|
|
12
12
|
const { VmBackup } = require('./_VmBackup.js')
|
|
@@ -24,6 +24,34 @@ const getAdaptersByRemote = adapters => {
|
|
|
24
24
|
|
|
25
25
|
const runTask = (...args) => Task.run(...args).catch(noop) // errors are handled by logs
|
|
26
26
|
|
|
27
|
+
const DEFAULT_SETTINGS = {
|
|
28
|
+
reportWhen: 'failure',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEFAULT_VM_SETTINGS = {
|
|
32
|
+
bypassVdiChainsCheck: false,
|
|
33
|
+
checkpointSnapshot: false,
|
|
34
|
+
concurrency: 2,
|
|
35
|
+
copyRetention: 0,
|
|
36
|
+
deleteFirst: false,
|
|
37
|
+
exportRetention: 0,
|
|
38
|
+
fullInterval: 0,
|
|
39
|
+
healthCheckSr: undefined,
|
|
40
|
+
healthCheckVmsWithTags: [],
|
|
41
|
+
maxMergedDeltasPerRun: 2,
|
|
42
|
+
offlineBackup: false,
|
|
43
|
+
offlineSnapshot: false,
|
|
44
|
+
snapshotRetention: 0,
|
|
45
|
+
timeout: 0,
|
|
46
|
+
unconditionalSnapshot: false,
|
|
47
|
+
vmTimeout: 0,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const DEFAULT_METADATA_SETTINGS = {
|
|
51
|
+
retentionPoolMetadata: 0,
|
|
52
|
+
retentionXoMetadata: 0,
|
|
53
|
+
}
|
|
54
|
+
|
|
27
55
|
exports.Backup = class Backup {
|
|
28
56
|
constructor({ config, getAdapter, getConnectedRecord, job, schedule }) {
|
|
29
57
|
this._config = config
|
|
@@ -42,17 +70,22 @@ exports.Backup = class Backup {
|
|
|
42
70
|
'{job.name}': job.name,
|
|
43
71
|
'{vm.name_label}': vm => vm.name_label,
|
|
44
72
|
})
|
|
45
|
-
}
|
|
46
73
|
|
|
47
|
-
|
|
48
|
-
const
|
|
74
|
+
const { type } = job
|
|
75
|
+
const baseSettings = { ...DEFAULT_SETTINGS }
|
|
49
76
|
if (type === 'backup') {
|
|
50
|
-
|
|
77
|
+
Object.assign(baseSettings, DEFAULT_VM_SETTINGS, config.defaultSettings, config.vm?.defaultSettings)
|
|
78
|
+
this.run = this._runVmBackup
|
|
51
79
|
} else if (type === 'metadataBackup') {
|
|
52
|
-
|
|
80
|
+
Object.assign(baseSettings, DEFAULT_METADATA_SETTINGS, config.defaultSettings, config.metadata?.defaultSettings)
|
|
81
|
+
this.run = this._runMetadataBackup
|
|
53
82
|
} else {
|
|
54
83
|
throw new Error(`No runner for the backup type ${type}`)
|
|
55
84
|
}
|
|
85
|
+
Object.assign(baseSettings, job.settings[''])
|
|
86
|
+
|
|
87
|
+
this._baseSettings = baseSettings
|
|
88
|
+
this._settings = { ...baseSettings, ...job.settings[schedule.id] }
|
|
56
89
|
}
|
|
57
90
|
|
|
58
91
|
async _runMetadataBackup() {
|
|
@@ -64,13 +97,6 @@ exports.Backup = class Backup {
|
|
|
64
97
|
}
|
|
65
98
|
|
|
66
99
|
const config = this._config
|
|
67
|
-
const settings = {
|
|
68
|
-
...config.defaultSettings,
|
|
69
|
-
...config.metadata.defaultSettings,
|
|
70
|
-
...job.settings[''],
|
|
71
|
-
...job.settings[schedule.id],
|
|
72
|
-
}
|
|
73
|
-
|
|
74
100
|
const poolIds = extractIdsFromSimplePattern(job.pools)
|
|
75
101
|
const isEmptyPools = poolIds.length === 0
|
|
76
102
|
const isXoMetadata = job.xoMetadata !== undefined
|
|
@@ -78,6 +104,8 @@ exports.Backup = class Backup {
|
|
|
78
104
|
throw new Error('no metadata mode found')
|
|
79
105
|
}
|
|
80
106
|
|
|
107
|
+
const settings = this._settings
|
|
108
|
+
|
|
81
109
|
const { retentionPoolMetadata, retentionXoMetadata } = settings
|
|
82
110
|
|
|
83
111
|
if (
|
|
@@ -189,14 +217,7 @@ exports.Backup = class Backup {
|
|
|
189
217
|
const schedule = this._schedule
|
|
190
218
|
|
|
191
219
|
const config = this._config
|
|
192
|
-
const
|
|
193
|
-
const scheduleSettings = {
|
|
194
|
-
...config.defaultSettings,
|
|
195
|
-
...config.vm.defaultSettings,
|
|
196
|
-
...settings[''],
|
|
197
|
-
...settings[schedule.id],
|
|
198
|
-
}
|
|
199
|
-
|
|
220
|
+
const settings = this._settings
|
|
200
221
|
await Disposable.use(
|
|
201
222
|
Disposable.all(
|
|
202
223
|
extractIdsFromSimplePattern(job.srs).map(id =>
|
|
@@ -224,14 +245,15 @@ exports.Backup = class Backup {
|
|
|
224
245
|
})
|
|
225
246
|
)
|
|
226
247
|
),
|
|
227
|
-
|
|
248
|
+
() => settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined,
|
|
249
|
+
async (srs, remoteAdapters, healthCheckSr) => {
|
|
228
250
|
// remove adapters that failed (already handled)
|
|
229
251
|
remoteAdapters = remoteAdapters.filter(_ => _ !== undefined)
|
|
230
252
|
|
|
231
253
|
// remove srs that failed (already handled)
|
|
232
254
|
srs = srs.filter(_ => _ !== undefined)
|
|
233
255
|
|
|
234
|
-
if (remoteAdapters.length === 0 && srs.length === 0 &&
|
|
256
|
+
if (remoteAdapters.length === 0 && srs.length === 0 && settings.snapshotRetention === 0) {
|
|
235
257
|
return
|
|
236
258
|
}
|
|
237
259
|
|
|
@@ -241,23 +263,27 @@ exports.Backup = class Backup {
|
|
|
241
263
|
|
|
242
264
|
remoteAdapters = getAdaptersByRemote(remoteAdapters)
|
|
243
265
|
|
|
266
|
+
const allSettings = this._job.settings
|
|
267
|
+
const baseSettings = this._baseSettings
|
|
268
|
+
|
|
244
269
|
const handleVm = vmUuid =>
|
|
245
270
|
runTask({ name: 'backup VM', data: { type: 'VM', id: vmUuid } }, () =>
|
|
246
271
|
Disposable.use(this._getRecord('VM', vmUuid), vm =>
|
|
247
272
|
new VmBackup({
|
|
273
|
+
baseSettings,
|
|
248
274
|
config,
|
|
249
275
|
getSnapshotNameLabel,
|
|
276
|
+
healthCheckSr,
|
|
250
277
|
job,
|
|
251
|
-
// remotes,
|
|
252
278
|
remoteAdapters,
|
|
253
279
|
schedule,
|
|
254
|
-
settings: { ...
|
|
280
|
+
settings: { ...settings, ...allSettings[vm.uuid] },
|
|
255
281
|
srs,
|
|
256
282
|
vm,
|
|
257
283
|
}).run()
|
|
258
284
|
)
|
|
259
285
|
)
|
|
260
|
-
const { concurrency } =
|
|
286
|
+
const { concurrency } = settings
|
|
261
287
|
await asyncMapSettled(vmIds, concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm))
|
|
262
288
|
}
|
|
263
289
|
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Task } = require('./Task')
|
|
4
|
+
|
|
5
|
+
exports.HealthCheckVmBackup = class HealthCheckVmBackup {
|
|
6
|
+
#xapi
|
|
7
|
+
#restoredVm
|
|
8
|
+
|
|
9
|
+
constructor({ restoredVm, xapi }) {
|
|
10
|
+
this.#restoredVm = restoredVm
|
|
11
|
+
this.#xapi = xapi
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async run() {
|
|
15
|
+
return Task.run(
|
|
16
|
+
{
|
|
17
|
+
name: 'vmstart',
|
|
18
|
+
},
|
|
19
|
+
async () => {
|
|
20
|
+
let restoredVm = this.#restoredVm
|
|
21
|
+
const xapi = this.#xapi
|
|
22
|
+
const restoredId = restoredVm.uuid
|
|
23
|
+
|
|
24
|
+
// remove vifs
|
|
25
|
+
await Promise.all(restoredVm.$VIFs.map(vif => xapi.callAsync('VIF.destroy', vif.$ref)))
|
|
26
|
+
|
|
27
|
+
const start = new Date()
|
|
28
|
+
// start Vm
|
|
29
|
+
|
|
30
|
+
await xapi.callAsync(
|
|
31
|
+
'VM.start',
|
|
32
|
+
restoredVm.$ref,
|
|
33
|
+
false, // Start paused?
|
|
34
|
+
false // Skip pre-boot checks?
|
|
35
|
+
)
|
|
36
|
+
const started = new Date()
|
|
37
|
+
const timeout = 10 * 60 * 1000
|
|
38
|
+
const startDuration = started - start
|
|
39
|
+
|
|
40
|
+
let remainingTimeout = timeout - startDuration
|
|
41
|
+
|
|
42
|
+
if (remainingTimeout < 0) {
|
|
43
|
+
throw new Error(`VM ${restoredId} not started after ${timeout / 1000} second`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// wait for the 'Running' event to be really stored in local xapi object cache
|
|
47
|
+
restoredVm = await xapi.waitObjectState(restoredVm.$ref, vm => vm.power_state === 'Running', {
|
|
48
|
+
timeout: remainingTimeout,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const running = new Date()
|
|
52
|
+
remainingTimeout -= running - started
|
|
53
|
+
|
|
54
|
+
if (remainingTimeout < 0) {
|
|
55
|
+
throw new Error(`local xapi did not get Runnig state for VM ${restoredId} after ${timeout / 1000} second`)
|
|
56
|
+
}
|
|
57
|
+
// wait for the guest tool version to be defined
|
|
58
|
+
await xapi.waitObjectState(restoredVm.guest_metrics, gm => gm?.PV_drivers_version?.major !== undefined, {
|
|
59
|
+
timeout: remainingTimeout,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
package/RemoteAdapter.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
|
|
4
|
+
const { synchronized } = require('decorator-synchronized')
|
|
4
5
|
const Disposable = require('promise-toolbox/Disposable')
|
|
5
6
|
const fromCallback = require('promise-toolbox/fromCallback')
|
|
6
7
|
const fromEvent = require('promise-toolbox/fromEvent')
|
|
@@ -9,14 +10,15 @@ const groupBy = require('lodash/groupBy.js')
|
|
|
9
10
|
const pickBy = require('lodash/pickBy.js')
|
|
10
11
|
const { dirname, join, normalize, resolve } = require('path')
|
|
11
12
|
const { createLogger } = require('@xen-orchestra/log')
|
|
12
|
-
const {
|
|
13
|
+
const { createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } = require('vhd-lib')
|
|
13
14
|
const { deduped } = require('@vates/disposable/deduped.js')
|
|
14
15
|
const { decorateMethodsWith } = require('@vates/decorate-with')
|
|
15
16
|
const { compose } = require('@vates/compose')
|
|
16
17
|
const { execFile } = require('child_process')
|
|
17
|
-
const { readdir,
|
|
18
|
+
const { readdir, lstat } = require('fs-extra')
|
|
18
19
|
const { v4: uuidv4 } = require('uuid')
|
|
19
20
|
const { ZipFile } = require('yazl')
|
|
21
|
+
const zlib = require('zlib')
|
|
20
22
|
|
|
21
23
|
const { BACKUP_DIR } = require('./_getVmBackupDir.js')
|
|
22
24
|
const { cleanVm } = require('./_cleanVm.js')
|
|
@@ -45,13 +47,12 @@ const resolveSubpath = (root, path) => resolve(root, `.${resolve('/', path)}`)
|
|
|
45
47
|
const RE_VHDI = /^vhdi(\d+)$/
|
|
46
48
|
|
|
47
49
|
async function addDirectory(files, realPath, metadataPath) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
await asyncMap(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
50
|
+
const stats = await lstat(realPath)
|
|
51
|
+
if (stats.isDirectory()) {
|
|
52
|
+
await asyncMap(await readdir(realPath), file =>
|
|
53
|
+
addDirectory(files, realPath + '/' + file, metadataPath + '/' + file)
|
|
54
|
+
)
|
|
55
|
+
} else if (stats.isFile()) {
|
|
55
56
|
files.push({
|
|
56
57
|
realPath,
|
|
57
58
|
metadataPath,
|
|
@@ -78,6 +79,7 @@ class RemoteAdapter {
|
|
|
78
79
|
this._dirMode = dirMode
|
|
79
80
|
this._handler = handler
|
|
80
81
|
this._vhdDirectoryCompression = vhdDirectoryCompression
|
|
82
|
+
this._readCacheListVmBackups = synchronized.withKey()(this._readCacheListVmBackups)
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
get handler() {
|
|
@@ -261,7 +263,8 @@ class RemoteAdapter {
|
|
|
261
263
|
}
|
|
262
264
|
|
|
263
265
|
async deleteVmBackups(files) {
|
|
264
|
-
const
|
|
266
|
+
const metadatas = await asyncMap(files, file => this.readVmBackupMetadata(file))
|
|
267
|
+
const { delta, full, ...others } = groupBy(metadatas, 'mode')
|
|
265
268
|
|
|
266
269
|
const unsupportedModes = Object.keys(others)
|
|
267
270
|
if (unsupportedModes.length !== 0) {
|
|
@@ -278,6 +281,9 @@ class RemoteAdapter {
|
|
|
278
281
|
// don't merge in main process, unused VHDs will be merged in the next backup run
|
|
279
282
|
await this.cleanVm(dir, { remove: true, onLog: warn })
|
|
280
283
|
}
|
|
284
|
+
|
|
285
|
+
const dedupedVmUuid = new Set(metadatas.map(_ => _.vm.uuid))
|
|
286
|
+
await asyncMap(dedupedVmUuid, vmUuid => this.invalidateVmBackupListCache(vmUuid))
|
|
281
287
|
}
|
|
282
288
|
|
|
283
289
|
#getCompressionType() {
|
|
@@ -285,7 +291,7 @@ class RemoteAdapter {
|
|
|
285
291
|
}
|
|
286
292
|
|
|
287
293
|
#useVhdDirectory() {
|
|
288
|
-
return this.handler.
|
|
294
|
+
return this.handler.useVhdDirectory()
|
|
289
295
|
}
|
|
290
296
|
|
|
291
297
|
#useAlias() {
|
|
@@ -376,8 +382,12 @@ class RemoteAdapter {
|
|
|
376
382
|
const entriesMap = {}
|
|
377
383
|
await asyncMap(await readdir(path), async name => {
|
|
378
384
|
try {
|
|
379
|
-
const stats = await
|
|
380
|
-
|
|
385
|
+
const stats = await lstat(`${path}/${name}`)
|
|
386
|
+
if (stats.isDirectory()) {
|
|
387
|
+
entriesMap[name + '/'] = {}
|
|
388
|
+
} else if (stats.isFile()) {
|
|
389
|
+
entriesMap[name] = {}
|
|
390
|
+
}
|
|
381
391
|
} catch (error) {
|
|
382
392
|
if (error == null || error.code !== 'ENOENT') {
|
|
383
393
|
throw error
|
|
@@ -448,34 +458,94 @@ class RemoteAdapter {
|
|
|
448
458
|
return backupsByPool
|
|
449
459
|
}
|
|
450
460
|
|
|
451
|
-
async
|
|
461
|
+
async invalidateVmBackupListCache(vmUuid) {
|
|
462
|
+
await this.handler.unlink(`${BACKUP_DIR}/${vmUuid}/cache.json.gz`)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async #getCachabledDataListVmBackups(dir) {
|
|
452
466
|
const handler = this._handler
|
|
453
|
-
const backups =
|
|
467
|
+
const backups = {}
|
|
454
468
|
|
|
455
469
|
try {
|
|
456
|
-
const files = await handler.list(
|
|
470
|
+
const files = await handler.list(dir, {
|
|
457
471
|
filter: isMetadataFile,
|
|
458
472
|
prependDir: true,
|
|
459
473
|
})
|
|
460
474
|
await asyncMap(files, async file => {
|
|
461
475
|
try {
|
|
462
476
|
const metadata = await this.readVmBackupMetadata(file)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
backups.push(metadata)
|
|
468
|
-
}
|
|
477
|
+
// inject an id usable by importVmBackupNg()
|
|
478
|
+
metadata.id = metadata._filename
|
|
479
|
+
backups[file] = metadata
|
|
469
480
|
} catch (error) {
|
|
470
|
-
warn(`
|
|
481
|
+
warn(`can't read vm backup metadata`, { error, file, dir })
|
|
471
482
|
}
|
|
472
483
|
})
|
|
484
|
+
return backups
|
|
473
485
|
} catch (error) {
|
|
474
486
|
let code
|
|
475
487
|
if (error == null || ((code = error.code) !== 'ENOENT' && code !== 'ENOTDIR')) {
|
|
476
488
|
throw error
|
|
477
489
|
}
|
|
478
490
|
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// use _ to mark this method as private by convention
|
|
494
|
+
// since we decorate it with synchronized.withKey in the constructor
|
|
495
|
+
// and # function are not writeable.
|
|
496
|
+
//
|
|
497
|
+
// read the list of backup of a Vm from cache
|
|
498
|
+
// if cache is missing or broken => regenerate it and return
|
|
499
|
+
|
|
500
|
+
async _readCacheListVmBackups(vmUuid) {
|
|
501
|
+
const dir = `${BACKUP_DIR}/${vmUuid}`
|
|
502
|
+
const path = `${dir}/cache.json.gz`
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
const gzipped = await this.handler.readFile(path)
|
|
506
|
+
const text = await fromCallback(zlib.gunzip, gzipped)
|
|
507
|
+
return JSON.parse(text)
|
|
508
|
+
} catch (error) {
|
|
509
|
+
if (error.code !== 'ENOENT') {
|
|
510
|
+
warn('Cache file was unreadable', { vmUuid, error })
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// nothing cached, or cache unreadable => regenerate it
|
|
515
|
+
const backups = await this.#getCachabledDataListVmBackups(dir)
|
|
516
|
+
if (backups === undefined) {
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// detached async action, will not reject
|
|
521
|
+
this.#writeVmBackupsCache(path, backups)
|
|
522
|
+
|
|
523
|
+
return backups
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async #writeVmBackupsCache(cacheFile, backups) {
|
|
527
|
+
try {
|
|
528
|
+
const text = JSON.stringify(backups)
|
|
529
|
+
const zipped = await fromCallback(zlib.gzip, text)
|
|
530
|
+
await this.handler.writeFile(cacheFile, zipped, { flags: 'w' })
|
|
531
|
+
} catch (error) {
|
|
532
|
+
warn('writeVmBackupsCache', { cacheFile, error })
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async listVmBackups(vmUuid, predicate) {
|
|
537
|
+
const backups = []
|
|
538
|
+
const cached = await this._readCacheListVmBackups(vmUuid)
|
|
539
|
+
|
|
540
|
+
if (cached === undefined) {
|
|
541
|
+
return []
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
Object.values(cached).forEach(metadata => {
|
|
545
|
+
if (predicate === undefined || predicate(metadata)) {
|
|
546
|
+
backups.push(metadata)
|
|
547
|
+
}
|
|
548
|
+
})
|
|
479
549
|
|
|
480
550
|
return backups.sort(compareTimestamp)
|
|
481
551
|
}
|
|
@@ -531,46 +601,27 @@ class RemoteAdapter {
|
|
|
531
601
|
})
|
|
532
602
|
}
|
|
533
603
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
// if it's a path : open all hierarchy of parent
|
|
538
|
-
if (typeof paths === 'string') {
|
|
539
|
-
let vhd
|
|
540
|
-
let vhdPath = paths
|
|
541
|
-
do {
|
|
542
|
-
const disposable = await openVhd(handler, vhdPath)
|
|
543
|
-
vhd = disposable.value
|
|
544
|
-
disposableVhds.push(disposable)
|
|
545
|
-
vhdPath = resolveRelativeFromFile(vhdPath, vhd.header.parentUnicodeName)
|
|
546
|
-
} while (vhd.footer.diskType !== Constants.DISK_TYPES.DYNAMIC)
|
|
547
|
-
} else {
|
|
548
|
-
// only open the list of path given
|
|
549
|
-
disposableVhds = paths.map(path => openVhd(handler, path))
|
|
550
|
-
}
|
|
551
|
-
|
|
604
|
+
// open the hierarchy of ancestors until we find a full one
|
|
605
|
+
async _createSyntheticStream(handler, path) {
|
|
606
|
+
const disposableSynthetic = await VhdSynthetic.fromVhdChain(handler, path)
|
|
552
607
|
// I don't want the vhds to be disposed on return
|
|
553
608
|
// but only when the stream is done ( or failed )
|
|
554
|
-
const disposables = await Disposable.all(disposableVhds)
|
|
555
|
-
const vhds = disposables.value
|
|
556
609
|
|
|
557
610
|
let disposed = false
|
|
558
611
|
const disposeOnce = async () => {
|
|
559
612
|
if (!disposed) {
|
|
560
613
|
disposed = true
|
|
561
|
-
|
|
562
614
|
try {
|
|
563
|
-
await
|
|
615
|
+
await disposableSynthetic.dispose()
|
|
564
616
|
} catch (error) {
|
|
565
|
-
warn('
|
|
617
|
+
warn('openVhd: failed to dispose VHDs', { error })
|
|
566
618
|
}
|
|
567
619
|
}
|
|
568
620
|
}
|
|
569
|
-
|
|
570
|
-
const synthetic = new VhdSynthetic(vhds)
|
|
571
|
-
await synthetic.readHeaderAndFooter()
|
|
621
|
+
const synthetic = disposableSynthetic.value
|
|
572
622
|
await synthetic.readBlockAllocationTable()
|
|
573
623
|
const stream = await synthetic.stream()
|
|
624
|
+
|
|
574
625
|
stream.on('end', disposeOnce)
|
|
575
626
|
stream.on('close', disposeOnce)
|
|
576
627
|
stream.on('error', disposeOnce)
|
|
@@ -603,7 +654,10 @@ class RemoteAdapter {
|
|
|
603
654
|
}
|
|
604
655
|
|
|
605
656
|
async readVmBackupMetadata(path) {
|
|
606
|
-
|
|
657
|
+
// _filename is a private field used to compute the backup id
|
|
658
|
+
//
|
|
659
|
+
// it's enumerable to make it cacheable
|
|
660
|
+
return { ...JSON.parse(await this._handler.readFile(path)), _filename: path }
|
|
607
661
|
}
|
|
608
662
|
}
|
|
609
663
|
|
package/_VmBackup.js
CHANGED
|
@@ -45,7 +45,18 @@ const forkDeltaExport = deltaExport =>
|
|
|
45
45
|
})
|
|
46
46
|
|
|
47
47
|
class VmBackup {
|
|
48
|
-
constructor({
|
|
48
|
+
constructor({
|
|
49
|
+
config,
|
|
50
|
+
getSnapshotNameLabel,
|
|
51
|
+
healthCheckSr,
|
|
52
|
+
job,
|
|
53
|
+
remoteAdapters,
|
|
54
|
+
remotes,
|
|
55
|
+
schedule,
|
|
56
|
+
settings,
|
|
57
|
+
srs,
|
|
58
|
+
vm,
|
|
59
|
+
}) {
|
|
49
60
|
if (vm.other_config['xo:backup:job'] === job.id && 'start' in vm.blocked_operations) {
|
|
50
61
|
// don't match replicated VMs created by this very job otherwise they
|
|
51
62
|
// will be replicated again and again
|
|
@@ -55,7 +66,6 @@ class VmBackup {
|
|
|
55
66
|
this.config = config
|
|
56
67
|
this.job = job
|
|
57
68
|
this.remoteAdapters = remoteAdapters
|
|
58
|
-
this.remotes = remotes
|
|
59
69
|
this.scheduleId = schedule.id
|
|
60
70
|
this.timestamp = undefined
|
|
61
71
|
|
|
@@ -69,6 +79,7 @@ class VmBackup {
|
|
|
69
79
|
this._fullVdisRequired = undefined
|
|
70
80
|
this._getSnapshotNameLabel = getSnapshotNameLabel
|
|
71
81
|
this._isDelta = job.mode === 'delta'
|
|
82
|
+
this._healthCheckSr = healthCheckSr
|
|
72
83
|
this._jobId = job.id
|
|
73
84
|
this._jobSnapshots = undefined
|
|
74
85
|
this._xapi = vm.$xapi
|
|
@@ -95,7 +106,6 @@ class VmBackup {
|
|
|
95
106
|
: [FullBackupWriter, FullReplicationWriter]
|
|
96
107
|
|
|
97
108
|
const allSettings = job.settings
|
|
98
|
-
|
|
99
109
|
Object.keys(remoteAdapters).forEach(remoteId => {
|
|
100
110
|
const targetSettings = {
|
|
101
111
|
...settings,
|
|
@@ -143,6 +153,13 @@ class VmBackup {
|
|
|
143
153
|
errors.push(error)
|
|
144
154
|
this.delete(writer)
|
|
145
155
|
warn(warnMessage, { error, writer: writer.constructor.name })
|
|
156
|
+
|
|
157
|
+
// these two steps are the only one that are not already in their own sub tasks
|
|
158
|
+
if (warnMessage === 'writer.checkBaseVdis()' || warnMessage === 'writer.beforeBackup()') {
|
|
159
|
+
Task.warning(
|
|
160
|
+
`the writer ${writer.constructor.name} has failed the step ${warnMessage} with error ${error.message}. It won't be used anymore in this job execution.`
|
|
161
|
+
)
|
|
162
|
+
}
|
|
146
163
|
}
|
|
147
164
|
})
|
|
148
165
|
if (writers.size === 0) {
|
|
@@ -173,7 +190,10 @@ class VmBackup {
|
|
|
173
190
|
const settings = this._settings
|
|
174
191
|
|
|
175
192
|
const doSnapshot =
|
|
176
|
-
|
|
193
|
+
settings.unconditionalSnapshot ||
|
|
194
|
+
this._isDelta ||
|
|
195
|
+
(!settings.offlineBackup && vm.power_state === 'Running') ||
|
|
196
|
+
settings.snapshotRetention !== 0
|
|
177
197
|
if (doSnapshot) {
|
|
178
198
|
await Task.run({ name: 'snapshot' }, async () => {
|
|
179
199
|
if (!settings.bypassVdiChainsCheck) {
|
|
@@ -183,6 +203,7 @@ class VmBackup {
|
|
|
183
203
|
const snapshotRef = await vm[settings.checkpointSnapshot ? '$checkpoint' : '$snapshot']({
|
|
184
204
|
ignoreNobakVdis: true,
|
|
185
205
|
name_label: this._getSnapshotNameLabel(vm),
|
|
206
|
+
unplugVusbs: true,
|
|
186
207
|
})
|
|
187
208
|
this.timestamp = Date.now()
|
|
188
209
|
|
|
@@ -304,22 +325,17 @@ class VmBackup {
|
|
|
304
325
|
}
|
|
305
326
|
|
|
306
327
|
async _removeUnusedSnapshots() {
|
|
307
|
-
const
|
|
328
|
+
const allSettings = this.job.settings
|
|
329
|
+
const baseSettings = this._baseSettings
|
|
308
330
|
const baseVmRef = this._baseVm?.$ref
|
|
309
|
-
const { config } = this
|
|
310
|
-
const baseSettings = {
|
|
311
|
-
...config.defaultSettings,
|
|
312
|
-
...config.metadata.defaultSettings,
|
|
313
|
-
...jobSettings[''],
|
|
314
|
-
}
|
|
315
331
|
|
|
316
332
|
const snapshotsPerSchedule = groupBy(this._jobSnapshots, _ => _.other_config['xo:backup:schedule'])
|
|
317
333
|
const xapi = this._xapi
|
|
318
334
|
await asyncMap(Object.entries(snapshotsPerSchedule), ([scheduleId, snapshots]) => {
|
|
319
335
|
const settings = {
|
|
320
336
|
...baseSettings,
|
|
321
|
-
...
|
|
322
|
-
...
|
|
337
|
+
...allSettings[scheduleId],
|
|
338
|
+
...allSettings[this.vm.uuid],
|
|
323
339
|
}
|
|
324
340
|
return asyncMap(getOldEntries(settings.snapshotRetention, snapshots), ({ $ref }) => {
|
|
325
341
|
if ($ref !== baseVmRef) {
|
|
@@ -398,6 +414,24 @@ class VmBackup {
|
|
|
398
414
|
this._fullVdisRequired = fullVdisRequired
|
|
399
415
|
}
|
|
400
416
|
|
|
417
|
+
async _healthCheck() {
|
|
418
|
+
const settings = this._settings
|
|
419
|
+
|
|
420
|
+
if (this._healthCheckSr === undefined) {
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// check if current VM has tags
|
|
425
|
+
const { tags } = this.vm
|
|
426
|
+
const intersect = settings.healthCheckVmsWithTags.some(t => tags.includes(t))
|
|
427
|
+
|
|
428
|
+
if (settings.healthCheckVmsWithTags.length !== 0 && !intersect) {
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
await this._callWriters(writer => writer.healthCheck(this._healthCheckSr), 'writer.healthCheck()')
|
|
433
|
+
}
|
|
434
|
+
|
|
401
435
|
async run($defer) {
|
|
402
436
|
const settings = this._settings
|
|
403
437
|
assert(
|
|
@@ -407,7 +441,9 @@ class VmBackup {
|
|
|
407
441
|
|
|
408
442
|
await this._callWriters(async writer => {
|
|
409
443
|
await writer.beforeBackup()
|
|
410
|
-
$defer(() =>
|
|
444
|
+
$defer(async () => {
|
|
445
|
+
await writer.afterBackup()
|
|
446
|
+
})
|
|
411
447
|
}, 'writer.beforeBackup()')
|
|
412
448
|
|
|
413
449
|
await this._fetchJobSnapshots()
|
|
@@ -443,6 +479,7 @@ class VmBackup {
|
|
|
443
479
|
await this._fetchJobSnapshots()
|
|
444
480
|
await this._removeUnusedSnapshots()
|
|
445
481
|
}
|
|
482
|
+
await this._healthCheck()
|
|
446
483
|
}
|
|
447
484
|
}
|
|
448
485
|
exports.VmBackup = VmBackup
|