@xen-orchestra/backups 0.22.0 → 0.25.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 +90 -39
- package/_VmBackup.js +51 -14
- package/_cleanVm.js +70 -76
- 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/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,7 +10,7 @@ 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')
|
|
@@ -17,6 +18,7 @@ const { execFile } = require('child_process')
|
|
|
17
18
|
const { readdir, stat } = 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')
|
|
@@ -78,6 +80,7 @@ class RemoteAdapter {
|
|
|
78
80
|
this._dirMode = dirMode
|
|
79
81
|
this._handler = handler
|
|
80
82
|
this._vhdDirectoryCompression = vhdDirectoryCompression
|
|
83
|
+
this._readCacheListVmBackups = synchronized.withKey()(this._readCacheListVmBackups)
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
get handler() {
|
|
@@ -261,7 +264,8 @@ class RemoteAdapter {
|
|
|
261
264
|
}
|
|
262
265
|
|
|
263
266
|
async deleteVmBackups(files) {
|
|
264
|
-
const
|
|
267
|
+
const metadatas = await asyncMap(files, file => this.readVmBackupMetadata(file))
|
|
268
|
+
const { delta, full, ...others } = groupBy(metadatas, 'mode')
|
|
265
269
|
|
|
266
270
|
const unsupportedModes = Object.keys(others)
|
|
267
271
|
if (unsupportedModes.length !== 0) {
|
|
@@ -278,6 +282,9 @@ class RemoteAdapter {
|
|
|
278
282
|
// don't merge in main process, unused VHDs will be merged in the next backup run
|
|
279
283
|
await this.cleanVm(dir, { remove: true, onLog: warn })
|
|
280
284
|
}
|
|
285
|
+
|
|
286
|
+
const dedupedVmUuid = new Set(metadatas.map(_ => _.vm.uuid))
|
|
287
|
+
await asyncMap(dedupedVmUuid, vmUuid => this.invalidateVmBackupListCache(vmUuid))
|
|
281
288
|
}
|
|
282
289
|
|
|
283
290
|
#getCompressionType() {
|
|
@@ -448,34 +455,94 @@ class RemoteAdapter {
|
|
|
448
455
|
return backupsByPool
|
|
449
456
|
}
|
|
450
457
|
|
|
451
|
-
async
|
|
458
|
+
async invalidateVmBackupListCache(vmUuid) {
|
|
459
|
+
await this.handler.unlink(`${BACKUP_DIR}/${vmUuid}/cache.json.gz`)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async #getCachabledDataListVmBackups(dir) {
|
|
452
463
|
const handler = this._handler
|
|
453
|
-
const backups =
|
|
464
|
+
const backups = {}
|
|
454
465
|
|
|
455
466
|
try {
|
|
456
|
-
const files = await handler.list(
|
|
467
|
+
const files = await handler.list(dir, {
|
|
457
468
|
filter: isMetadataFile,
|
|
458
469
|
prependDir: true,
|
|
459
470
|
})
|
|
460
471
|
await asyncMap(files, async file => {
|
|
461
472
|
try {
|
|
462
473
|
const metadata = await this.readVmBackupMetadata(file)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
backups.push(metadata)
|
|
468
|
-
}
|
|
474
|
+
// inject an id usable by importVmBackupNg()
|
|
475
|
+
metadata.id = metadata._filename
|
|
476
|
+
backups[file] = metadata
|
|
469
477
|
} catch (error) {
|
|
470
|
-
warn(`
|
|
478
|
+
warn(`can't read vm backup metadata`, { error, file, dir })
|
|
471
479
|
}
|
|
472
480
|
})
|
|
481
|
+
return backups
|
|
473
482
|
} catch (error) {
|
|
474
483
|
let code
|
|
475
484
|
if (error == null || ((code = error.code) !== 'ENOENT' && code !== 'ENOTDIR')) {
|
|
476
485
|
throw error
|
|
477
486
|
}
|
|
478
487
|
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// use _ to mark this method as private by convention
|
|
491
|
+
// since we decorate it with synchronized.withKey in the constructor
|
|
492
|
+
// and # function are not writeable.
|
|
493
|
+
//
|
|
494
|
+
// read the list of backup of a Vm from cache
|
|
495
|
+
// if cache is missing or broken => regenerate it and return
|
|
496
|
+
|
|
497
|
+
async _readCacheListVmBackups(vmUuid) {
|
|
498
|
+
const dir = `${BACKUP_DIR}/${vmUuid}`
|
|
499
|
+
const path = `${dir}/cache.json.gz`
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const gzipped = await this.handler.readFile(path)
|
|
503
|
+
const text = await fromCallback(zlib.gunzip, gzipped)
|
|
504
|
+
return JSON.parse(text)
|
|
505
|
+
} catch (error) {
|
|
506
|
+
if (error.code !== 'ENOENT') {
|
|
507
|
+
warn('Cache file was unreadable', { vmUuid, error })
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// nothing cached, or cache unreadable => regenerate it
|
|
512
|
+
const backups = await this.#getCachabledDataListVmBackups(dir)
|
|
513
|
+
if (backups === undefined) {
|
|
514
|
+
return
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// detached async action, will not reject
|
|
518
|
+
this.#writeVmBackupsCache(path, backups)
|
|
519
|
+
|
|
520
|
+
return backups
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async #writeVmBackupsCache(cacheFile, backups) {
|
|
524
|
+
try {
|
|
525
|
+
const text = JSON.stringify(backups)
|
|
526
|
+
const zipped = await fromCallback(zlib.gzip, text)
|
|
527
|
+
await this.handler.writeFile(cacheFile, zipped, { flags: 'w' })
|
|
528
|
+
} catch (error) {
|
|
529
|
+
warn('writeVmBackupsCache', { cacheFile, error })
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async listVmBackups(vmUuid, predicate) {
|
|
534
|
+
const backups = []
|
|
535
|
+
const cached = await this._readCacheListVmBackups(vmUuid)
|
|
536
|
+
|
|
537
|
+
if (cached === undefined) {
|
|
538
|
+
return []
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
Object.values(cached).forEach(metadata => {
|
|
542
|
+
if (predicate === undefined || predicate(metadata)) {
|
|
543
|
+
backups.push(metadata)
|
|
544
|
+
}
|
|
545
|
+
})
|
|
479
546
|
|
|
480
547
|
return backups.sort(compareTimestamp)
|
|
481
548
|
}
|
|
@@ -531,46 +598,27 @@ class RemoteAdapter {
|
|
|
531
598
|
})
|
|
532
599
|
}
|
|
533
600
|
|
|
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
|
-
|
|
601
|
+
// open the hierarchy of ancestors until we find a full one
|
|
602
|
+
async _createSyntheticStream(handler, path) {
|
|
603
|
+
const disposableSynthetic = await VhdSynthetic.fromVhdChain(handler, path)
|
|
552
604
|
// I don't want the vhds to be disposed on return
|
|
553
605
|
// but only when the stream is done ( or failed )
|
|
554
|
-
const disposables = await Disposable.all(disposableVhds)
|
|
555
|
-
const vhds = disposables.value
|
|
556
606
|
|
|
557
607
|
let disposed = false
|
|
558
608
|
const disposeOnce = async () => {
|
|
559
609
|
if (!disposed) {
|
|
560
610
|
disposed = true
|
|
561
|
-
|
|
562
611
|
try {
|
|
563
|
-
await
|
|
612
|
+
await disposableSynthetic.dispose()
|
|
564
613
|
} catch (error) {
|
|
565
|
-
warn('
|
|
614
|
+
warn('openVhd: failed to dispose VHDs', { error })
|
|
566
615
|
}
|
|
567
616
|
}
|
|
568
617
|
}
|
|
569
|
-
|
|
570
|
-
const synthetic = new VhdSynthetic(vhds)
|
|
571
|
-
await synthetic.readHeaderAndFooter()
|
|
618
|
+
const synthetic = disposableSynthetic.value
|
|
572
619
|
await synthetic.readBlockAllocationTable()
|
|
573
620
|
const stream = await synthetic.stream()
|
|
621
|
+
|
|
574
622
|
stream.on('end', disposeOnce)
|
|
575
623
|
stream.on('close', disposeOnce)
|
|
576
624
|
stream.on('error', disposeOnce)
|
|
@@ -603,7 +651,10 @@ class RemoteAdapter {
|
|
|
603
651
|
}
|
|
604
652
|
|
|
605
653
|
async readVmBackupMetadata(path) {
|
|
606
|
-
|
|
654
|
+
// _filename is a private field used to compute the backup id
|
|
655
|
+
//
|
|
656
|
+
// it's enumerable to make it cacheable
|
|
657
|
+
return { ...JSON.parse(await this._handler.readFile(path)), _filename: path }
|
|
607
658
|
}
|
|
608
659
|
}
|
|
609
660
|
|
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
|
package/_cleanVm.js
CHANGED
|
@@ -31,66 +31,53 @@ const computeVhdsSize = (handler, vhdPaths) =>
|
|
|
31
31
|
}
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
// chain is
|
|
34
|
+
// chain is [ ancestor, child1, ..., childn]
|
|
35
|
+
// 1. Create a VhdSynthetic from all children
|
|
36
|
+
// 2. Merge the VhdSynthetic into the ancestor
|
|
37
|
+
// 3. Delete all (now) unused VHDs
|
|
38
|
+
// 4. Rename the ancestor with the merged data to the latest child
|
|
35
39
|
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
40
|
+
// VhdSynthetic
|
|
41
|
+
// |
|
|
42
|
+
// /‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\
|
|
43
|
+
// [ ancestor, child1, ...,child n-1, childn ]
|
|
44
|
+
// | \___________________/ ^
|
|
45
|
+
// | | |
|
|
46
|
+
// | unused VHDs |
|
|
47
|
+
// | |
|
|
48
|
+
// \___________rename_____________/
|
|
49
|
+
|
|
50
|
+
async function mergeVhdChain(chain, { handler, logInfo, remove, merge }) {
|
|
39
51
|
assert(chain.length >= 2)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const children = chain.slice(0, -1).reverse()
|
|
44
|
-
|
|
45
|
-
chain
|
|
46
|
-
.slice(1)
|
|
47
|
-
.reverse()
|
|
48
|
-
.forEach(parent => {
|
|
49
|
-
onLog(`the parent ${parent} of the child ${child} is unused`)
|
|
50
|
-
})
|
|
52
|
+
const chainCopy = [...chain]
|
|
53
|
+
const parent = chainCopy.pop()
|
|
54
|
+
const children = chainCopy
|
|
51
55
|
|
|
52
56
|
if (merge) {
|
|
53
|
-
|
|
54
|
-
// - make it accept a stream
|
|
55
|
-
// - or create synthetic VHD which is not a stream
|
|
56
|
-
if (children.length !== 1) {
|
|
57
|
-
// TODO: implement merging multiple children
|
|
58
|
-
children.length = 1
|
|
59
|
-
child = children[0]
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
onLog(`merging ${child} into ${parent}`)
|
|
57
|
+
logInfo(`merging children into parent`, { childrenCount: children.length, parent })
|
|
63
58
|
|
|
64
59
|
let done, total
|
|
65
60
|
const handle = setInterval(() => {
|
|
66
61
|
if (done !== undefined) {
|
|
67
|
-
|
|
62
|
+
logInfo(`merging children in progress`, { children, parent, doneCount: done, totalCount: total})
|
|
68
63
|
}
|
|
69
64
|
}, 10e3)
|
|
70
65
|
|
|
71
|
-
const mergedSize = await mergeVhd(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// ? child
|
|
78
|
-
// : await createSyntheticStream(handler, children),
|
|
79
|
-
{
|
|
80
|
-
onProgress({ done: d, total: t }) {
|
|
81
|
-
done = d
|
|
82
|
-
total = t
|
|
83
|
-
},
|
|
84
|
-
}
|
|
85
|
-
)
|
|
66
|
+
const mergedSize = await mergeVhd(handler, parent, handler, children, {
|
|
67
|
+
onProgress({ done: d, total: t }) {
|
|
68
|
+
done = d
|
|
69
|
+
total = t
|
|
70
|
+
},
|
|
71
|
+
})
|
|
86
72
|
|
|
87
73
|
clearInterval(handle)
|
|
74
|
+
const mergeTargetChild = children.shift()
|
|
88
75
|
await Promise.all([
|
|
89
|
-
VhdAbstract.rename(handler, parent,
|
|
90
|
-
asyncMap(children
|
|
91
|
-
|
|
76
|
+
VhdAbstract.rename(handler, parent, mergeTargetChild),
|
|
77
|
+
asyncMap(children, child => {
|
|
78
|
+
logInfo(`the VHD child is already merged`, { child })
|
|
92
79
|
if (remove) {
|
|
93
|
-
|
|
80
|
+
logInfo(`deleting merged VHD child`, { child })
|
|
94
81
|
return VhdAbstract.unlink(handler, child)
|
|
95
82
|
}
|
|
96
83
|
}),
|
|
@@ -138,14 +125,19 @@ const listVhds = async (handler, vmDir) => {
|
|
|
138
125
|
return { vhds, interruptedVhds, aliases }
|
|
139
126
|
}
|
|
140
127
|
|
|
141
|
-
async function checkAliases(
|
|
128
|
+
async function checkAliases(
|
|
129
|
+
aliasPaths,
|
|
130
|
+
targetDataRepository,
|
|
131
|
+
{ handler, logInfo = noop, logWarn = console.warn, remove = false }
|
|
132
|
+
) {
|
|
142
133
|
const aliasFound = []
|
|
143
134
|
for (const path of aliasPaths) {
|
|
144
135
|
const target = await resolveVhdAlias(handler, path)
|
|
145
136
|
|
|
146
137
|
if (!isVhdFile(target)) {
|
|
147
|
-
|
|
138
|
+
logWarn('alias references non VHD target', { path, target })
|
|
148
139
|
if (remove) {
|
|
140
|
+
logInfo('removing alias and non VHD target', { path, target })
|
|
149
141
|
await handler.unlink(target)
|
|
150
142
|
await handler.unlink(path)
|
|
151
143
|
}
|
|
@@ -160,13 +152,13 @@ async function checkAliases(aliasPaths, targetDataRepository, { handler, onLog =
|
|
|
160
152
|
// error during dispose should not trigger a deletion
|
|
161
153
|
}
|
|
162
154
|
} catch (error) {
|
|
163
|
-
|
|
155
|
+
logWarn('missing or broken alias target', { target, path, error })
|
|
164
156
|
if (remove) {
|
|
165
157
|
try {
|
|
166
158
|
await VhdAbstract.unlink(handler, path)
|
|
167
|
-
} catch (
|
|
168
|
-
if (
|
|
169
|
-
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error.code !== 'ENOENT') {
|
|
161
|
+
logWarn('error deleting alias target', { target, path, error })
|
|
170
162
|
}
|
|
171
163
|
}
|
|
172
164
|
}
|
|
@@ -183,20 +175,22 @@ async function checkAliases(aliasPaths, targetDataRepository, { handler, onLog =
|
|
|
183
175
|
|
|
184
176
|
entries.forEach(async entry => {
|
|
185
177
|
if (!aliasFound.includes(entry)) {
|
|
186
|
-
|
|
178
|
+
logWarn('no alias references VHD', { entry })
|
|
187
179
|
if (remove) {
|
|
180
|
+
logInfo('deleting unaliased VHD')
|
|
188
181
|
await VhdAbstract.unlink(handler, entry)
|
|
189
182
|
}
|
|
190
183
|
}
|
|
191
184
|
})
|
|
192
185
|
}
|
|
186
|
+
|
|
193
187
|
exports.checkAliases = checkAliases
|
|
194
188
|
|
|
195
189
|
const defaultMergeLimiter = limitConcurrency(1)
|
|
196
190
|
|
|
197
191
|
exports.cleanVm = async function cleanVm(
|
|
198
192
|
vmDir,
|
|
199
|
-
{ fixMetadata, remove, merge, mergeLimiter = defaultMergeLimiter,
|
|
193
|
+
{ fixMetadata, remove, merge, mergeLimiter = defaultMergeLimiter, logInfo = noop, logWarn = console.warn }
|
|
200
194
|
) {
|
|
201
195
|
const limitedMergeVhdChain = mergeLimiter(mergeVhdChain)
|
|
202
196
|
|
|
@@ -227,9 +221,9 @@ exports.cleanVm = async function cleanVm(
|
|
|
227
221
|
})
|
|
228
222
|
} catch (error) {
|
|
229
223
|
vhds.delete(path)
|
|
230
|
-
|
|
224
|
+
logWarn('VHD check error', { path, error })
|
|
231
225
|
if (error?.code === 'ERR_ASSERTION' && remove) {
|
|
232
|
-
|
|
226
|
+
logInfo('deleting broken path', { path })
|
|
233
227
|
return VhdAbstract.unlink(handler, path)
|
|
234
228
|
}
|
|
235
229
|
}
|
|
@@ -241,12 +235,12 @@ exports.cleanVm = async function cleanVm(
|
|
|
241
235
|
const statePath = interruptedVhds.get(interruptedVhd)
|
|
242
236
|
interruptedVhds.delete(interruptedVhd)
|
|
243
237
|
|
|
244
|
-
|
|
238
|
+
logWarn('orphan merge state', {
|
|
245
239
|
mergeStatePath: statePath,
|
|
246
240
|
missingVhdPath: interruptedVhd,
|
|
247
241
|
})
|
|
248
242
|
if (remove) {
|
|
249
|
-
|
|
243
|
+
logInfo('deleting orphan merge state', { statePath })
|
|
250
244
|
await handler.unlink(statePath)
|
|
251
245
|
}
|
|
252
246
|
}
|
|
@@ -255,7 +249,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
255
249
|
// check if alias are correct
|
|
256
250
|
// check if all vhd in data subfolder have a corresponding alias
|
|
257
251
|
await asyncMap(Object.keys(aliases), async dir => {
|
|
258
|
-
await checkAliases(aliases[dir], `${dir}/data`, { handler,
|
|
252
|
+
await checkAliases(aliases[dir], `${dir}/data`, { handler, logInfo, logWarn, remove })
|
|
259
253
|
})
|
|
260
254
|
|
|
261
255
|
// remove VHDs with missing ancestors
|
|
@@ -277,9 +271,9 @@ exports.cleanVm = async function cleanVm(
|
|
|
277
271
|
if (!vhds.has(parent)) {
|
|
278
272
|
vhds.delete(vhdPath)
|
|
279
273
|
|
|
280
|
-
|
|
274
|
+
logWarn('parent VHD is missing', { parent, vhdPath })
|
|
281
275
|
if (remove) {
|
|
282
|
-
|
|
276
|
+
logInfo('deleting orphan VHD', { vhdPath })
|
|
283
277
|
deletions.push(VhdAbstract.unlink(handler, vhdPath))
|
|
284
278
|
}
|
|
285
279
|
}
|
|
@@ -316,7 +310,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
316
310
|
// check is not good enough to delete the file, the best we can do is report
|
|
317
311
|
// it
|
|
318
312
|
if (!(await this.isValidXva(path))) {
|
|
319
|
-
|
|
313
|
+
logWarn('XVA might be broken', { path })
|
|
320
314
|
}
|
|
321
315
|
})
|
|
322
316
|
|
|
@@ -330,7 +324,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
330
324
|
try {
|
|
331
325
|
metadata = JSON.parse(await handler.readFile(json))
|
|
332
326
|
} catch (error) {
|
|
333
|
-
|
|
327
|
+
logWarn('failed to read metadata file', { json, error })
|
|
334
328
|
jsons.delete(json)
|
|
335
329
|
return
|
|
336
330
|
}
|
|
@@ -341,9 +335,9 @@ exports.cleanVm = async function cleanVm(
|
|
|
341
335
|
if (xvas.has(linkedXva)) {
|
|
342
336
|
unusedXvas.delete(linkedXva)
|
|
343
337
|
} else {
|
|
344
|
-
|
|
338
|
+
logWarn('metadata XVA is missing', { json })
|
|
345
339
|
if (remove) {
|
|
346
|
-
|
|
340
|
+
logInfo('deleting incomplete backup', { json })
|
|
347
341
|
jsons.delete(json)
|
|
348
342
|
await handler.unlink(json)
|
|
349
343
|
}
|
|
@@ -364,9 +358,9 @@ exports.cleanVm = async function cleanVm(
|
|
|
364
358
|
vhdsToJSons[path] = json
|
|
365
359
|
})
|
|
366
360
|
} else {
|
|
367
|
-
|
|
361
|
+
logWarn('some metadata VHDs are missing', { json, missingVhds })
|
|
368
362
|
if (remove) {
|
|
369
|
-
|
|
363
|
+
logInfo('deleting incomplete backup', { json })
|
|
370
364
|
jsons.delete(json)
|
|
371
365
|
await handler.unlink(json)
|
|
372
366
|
}
|
|
@@ -407,9 +401,9 @@ exports.cleanVm = async function cleanVm(
|
|
|
407
401
|
}
|
|
408
402
|
}
|
|
409
403
|
|
|
410
|
-
|
|
404
|
+
logWarn('unused VHD', { vhd })
|
|
411
405
|
if (remove) {
|
|
412
|
-
|
|
406
|
+
logInfo('deleting unused VHD', { vhd })
|
|
413
407
|
unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
|
|
414
408
|
}
|
|
415
409
|
}
|
|
@@ -433,7 +427,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
433
427
|
const metadataWithMergedVhd = {}
|
|
434
428
|
const doMerge = async () => {
|
|
435
429
|
await asyncMap(toMerge, async chain => {
|
|
436
|
-
const merged = await limitedMergeVhdChain(chain, { handler,
|
|
430
|
+
const merged = await limitedMergeVhdChain(chain, { handler, logInfo, logWarn, remove, merge })
|
|
437
431
|
if (merged !== undefined) {
|
|
438
432
|
const metadataPath = vhdsToJSons[chain[0]] // all the chain should have the same metada file
|
|
439
433
|
metadataWithMergedVhd[metadataPath] = true
|
|
@@ -445,18 +439,18 @@ exports.cleanVm = async function cleanVm(
|
|
|
445
439
|
...unusedVhdsDeletion,
|
|
446
440
|
toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : doMerge()),
|
|
447
441
|
asyncMap(unusedXvas, path => {
|
|
448
|
-
|
|
442
|
+
logWarn('unused XVA', { path })
|
|
449
443
|
if (remove) {
|
|
450
|
-
|
|
444
|
+
logInfo('deleting unused XVA', { path })
|
|
451
445
|
return handler.unlink(path)
|
|
452
446
|
}
|
|
453
447
|
}),
|
|
454
448
|
asyncMap(xvaSums, path => {
|
|
455
449
|
// no need to handle checksums for XVAs deleted by the script, they will be handled by `unlink()`
|
|
456
450
|
if (!xvas.has(path.slice(0, -'.checksum'.length))) {
|
|
457
|
-
|
|
451
|
+
logInfo('unused XVA checksum', { path })
|
|
458
452
|
if (remove) {
|
|
459
|
-
|
|
453
|
+
logInfo('deleting unused XVA checksum', { path })
|
|
460
454
|
return handler.unlink(path)
|
|
461
455
|
}
|
|
462
456
|
}
|
|
@@ -490,11 +484,11 @@ exports.cleanVm = async function cleanVm(
|
|
|
490
484
|
|
|
491
485
|
// don't warn if the size has changed after a merge
|
|
492
486
|
if (!merged && fileSystemSize !== size) {
|
|
493
|
-
|
|
487
|
+
logWarn('incorrect size in metadata', { size: size ?? 'none', fileSystemSize })
|
|
494
488
|
}
|
|
495
489
|
}
|
|
496
490
|
} catch (error) {
|
|
497
|
-
|
|
491
|
+
logWarn('failed to get metadata size', { metadataPath, error })
|
|
498
492
|
return
|
|
499
493
|
}
|
|
500
494
|
|
|
@@ -504,7 +498,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
504
498
|
try {
|
|
505
499
|
await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
|
|
506
500
|
} catch (error) {
|
|
507
|
-
|
|
501
|
+
logWarn('metadata size update failed', { metadataPath, error })
|
|
508
502
|
}
|
|
509
503
|
}
|
|
510
504
|
})
|
|
File without changes
|
package/merge-worker/cli.js
CHANGED
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
|
10
10
|
},
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "0.25.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.6"
|
|
14
14
|
},
|
|
@@ -22,11 +22,12 @@
|
|
|
22
22
|
"@vates/disposable": "^0.1.1",
|
|
23
23
|
"@vates/parse-duration": "^0.1.1",
|
|
24
24
|
"@xen-orchestra/async-map": "^0.1.2",
|
|
25
|
-
"@xen-orchestra/fs": "^1.0.
|
|
25
|
+
"@xen-orchestra/fs": "^1.0.3",
|
|
26
26
|
"@xen-orchestra/log": "^0.3.0",
|
|
27
27
|
"@xen-orchestra/template": "^0.1.0",
|
|
28
28
|
"compare-versions": "^4.0.1",
|
|
29
29
|
"d3-time-format": "^3.0.0",
|
|
30
|
+
"decorator-synchronized": "^0.6.0",
|
|
30
31
|
"end-of-stream": "^1.4.4",
|
|
31
32
|
"fs-extra": "^10.0.0",
|
|
32
33
|
"golike-defer": "^0.5.1",
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"promise-toolbox": "^0.21.0",
|
|
38
39
|
"proper-lockfile": "^4.1.2",
|
|
39
40
|
"uuid": "^8.3.2",
|
|
40
|
-
"vhd-lib": "^3.
|
|
41
|
+
"vhd-lib": "^3.2.0",
|
|
41
42
|
"yazl": "^2.5.1"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"tmp": "^0.2.1"
|
|
46
47
|
},
|
|
47
48
|
"peerDependencies": {
|
|
48
|
-
"@xen-orchestra/xapi": "^
|
|
49
|
+
"@xen-orchestra/xapi": "^1.1.0"
|
|
49
50
|
},
|
|
50
51
|
"license": "AGPL-3.0-or-later",
|
|
51
52
|
"author": {
|
|
@@ -19,6 +19,8 @@ const { AbstractDeltaWriter } = require('./_AbstractDeltaWriter.js')
|
|
|
19
19
|
const { checkVhd } = require('./_checkVhd.js')
|
|
20
20
|
const { packUuid } = require('./_packUuid.js')
|
|
21
21
|
const { Disposable } = require('promise-toolbox')
|
|
22
|
+
const { HealthCheckVmBackup } = require('../HealthCheckVmBackup.js')
|
|
23
|
+
const { ImportVmBackup } = require('../ImportVmBackup.js')
|
|
22
24
|
|
|
23
25
|
const { warn } = createLogger('xo:backups:DeltaBackupWriter')
|
|
24
26
|
|
|
@@ -69,6 +71,35 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
69
71
|
return this._cleanVm({ merge: true })
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
healthCheck(sr) {
|
|
75
|
+
return Task.run(
|
|
76
|
+
{
|
|
77
|
+
name: 'health check',
|
|
78
|
+
},
|
|
79
|
+
async () => {
|
|
80
|
+
const xapi = sr.$xapi
|
|
81
|
+
const srUuid = sr.uuid
|
|
82
|
+
const adapter = this._adapter
|
|
83
|
+
const metadata = await adapter.readVmBackupMetadata(this._metadataFileName)
|
|
84
|
+
const { id: restoredId } = await new ImportVmBackup({
|
|
85
|
+
adapter,
|
|
86
|
+
metadata,
|
|
87
|
+
srUuid,
|
|
88
|
+
xapi,
|
|
89
|
+
}).run()
|
|
90
|
+
const restoredVm = xapi.getObject(restoredId)
|
|
91
|
+
try {
|
|
92
|
+
await new HealthCheckVmBackup({
|
|
93
|
+
restoredVm,
|
|
94
|
+
xapi,
|
|
95
|
+
}).run()
|
|
96
|
+
} finally {
|
|
97
|
+
await xapi.VM_destroy(restoredVm.$ref)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
72
103
|
prepare({ isFull }) {
|
|
73
104
|
// create the task related to this export and ensure all methods are called in this context
|
|
74
105
|
const task = new Task({
|
|
@@ -80,7 +111,9 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
80
111
|
},
|
|
81
112
|
})
|
|
82
113
|
this.transfer = task.wrapFn(this.transfer)
|
|
83
|
-
this.
|
|
114
|
+
this.healthCheck = task.wrapFn(this.healthCheck)
|
|
115
|
+
this.cleanup = task.wrapFn(this.cleanup)
|
|
116
|
+
this.afterBackup = task.wrapFn(this.afterBackup, true)
|
|
84
117
|
|
|
85
118
|
return task.run(() => this._prepare())
|
|
86
119
|
}
|
|
@@ -156,7 +189,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
156
189
|
}/${adapter.getVhdFileName(basename)}`
|
|
157
190
|
)
|
|
158
191
|
|
|
159
|
-
const metadataFilename = `${backupDir}/${basename}.json`
|
|
192
|
+
const metadataFilename = (this._metadataFileName = `${backupDir}/${basename}.json`)
|
|
160
193
|
const metadataContent = {
|
|
161
194
|
jobId,
|
|
162
195
|
mode: job.mode,
|
|
@@ -6,8 +6,9 @@ const { join } = require('path')
|
|
|
6
6
|
const { getVmBackupDir } = require('../_getVmBackupDir.js')
|
|
7
7
|
const MergeWorker = require('../merge-worker/index.js')
|
|
8
8
|
const { formatFilenameDate } = require('../_filenameDate.js')
|
|
9
|
+
const { Task } = require('../Task.js')
|
|
9
10
|
|
|
10
|
-
const { warn } = createLogger('xo:backups:MixinBackupWriter')
|
|
11
|
+
const { info, warn } = createLogger('xo:backups:MixinBackupWriter')
|
|
11
12
|
|
|
12
13
|
exports.MixinBackupWriter = (BaseClass = Object) =>
|
|
13
14
|
class MixinBackupWriter extends BaseClass {
|
|
@@ -25,11 +26,17 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
|
|
|
25
26
|
|
|
26
27
|
async _cleanVm(options) {
|
|
27
28
|
try {
|
|
28
|
-
return await
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
return await Task.run({ name: 'clean-vm' }, () => {
|
|
30
|
+
return this._adapter.cleanVm(this.#vmBackupDir, {
|
|
31
|
+
...options,
|
|
32
|
+
fixMetadata: true,
|
|
33
|
+
logInfo: info,
|
|
34
|
+
logWarn: (message, data) => {
|
|
35
|
+
warn(message, data)
|
|
36
|
+
Task.warning(message, data)
|
|
37
|
+
},
|
|
38
|
+
lock: false,
|
|
39
|
+
})
|
|
33
40
|
})
|
|
34
41
|
} catch (error) {
|
|
35
42
|
warn(error)
|
|
@@ -64,5 +71,6 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
|
|
|
64
71
|
const remotePath = handler._getRealPath()
|
|
65
72
|
await MergeWorker.run(remotePath)
|
|
66
73
|
}
|
|
74
|
+
await this._adapter.invalidateVmBackupListCache(this._backup.vm.uuid)
|
|
67
75
|
}
|
|
68
76
|
}
|