@xen-orchestra/backups 0.27.4 → 0.28.1
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/RemoteAdapter.js +141 -53
- package/_cleanVm.js +26 -3
- package/_deltaVm.js +1 -1
- package/package.json +10 -8
- package/writers/DeltaBackupWriter.js +3 -35
- package/writers/FullBackupWriter.js +1 -4
- package/writers/_MixinBackupWriter.js +40 -3
package/RemoteAdapter.js
CHANGED
|
@@ -22,11 +22,15 @@ const zlib = require('zlib')
|
|
|
22
22
|
|
|
23
23
|
const { BACKUP_DIR } = require('./_getVmBackupDir.js')
|
|
24
24
|
const { cleanVm } = require('./_cleanVm.js')
|
|
25
|
+
const { formatFilenameDate } = require('./_filenameDate.js')
|
|
25
26
|
const { getTmpDir } = require('./_getTmpDir.js')
|
|
26
27
|
const { isMetadataFile } = require('./_backupType.js')
|
|
27
28
|
const { isValidXva } = require('./_isValidXva.js')
|
|
28
29
|
const { listPartitions, LVM_PARTITION_TYPE } = require('./_listPartitions.js')
|
|
29
30
|
const { lvs, pvs } = require('./_lvm.js')
|
|
31
|
+
// @todo : this import is marked extraneous , sould be fixed when lib is published
|
|
32
|
+
const { mount } = require('@vates/fuse-vhd')
|
|
33
|
+
const { asyncEach } = require('@vates/async-each')
|
|
30
34
|
|
|
31
35
|
const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
|
|
32
36
|
exports.DIR_XO_CONFIG_BACKUPS = DIR_XO_CONFIG_BACKUPS
|
|
@@ -34,7 +38,7 @@ exports.DIR_XO_CONFIG_BACKUPS = DIR_XO_CONFIG_BACKUPS
|
|
|
34
38
|
const DIR_XO_POOL_METADATA_BACKUPS = 'xo-pool-metadata-backups'
|
|
35
39
|
exports.DIR_XO_POOL_METADATA_BACKUPS = DIR_XO_POOL_METADATA_BACKUPS
|
|
36
40
|
|
|
37
|
-
const { warn } = createLogger('xo:backups:RemoteAdapter')
|
|
41
|
+
const { debug, warn } = createLogger('xo:backups:RemoteAdapter')
|
|
38
42
|
|
|
39
43
|
const compareTimestamp = (a, b) => a.timestamp - b.timestamp
|
|
40
44
|
|
|
@@ -44,8 +48,6 @@ const resolveRelativeFromFile = (file, path) => resolve('/', dirname(file), path
|
|
|
44
48
|
|
|
45
49
|
const resolveSubpath = (root, path) => resolve(root, `.${resolve('/', path)}`)
|
|
46
50
|
|
|
47
|
-
const RE_VHDI = /^vhdi(\d+)$/
|
|
48
|
-
|
|
49
51
|
async function addDirectory(files, realPath, metadataPath) {
|
|
50
52
|
const stats = await lstat(realPath)
|
|
51
53
|
if (stats.isDirectory()) {
|
|
@@ -74,12 +76,16 @@ const debounceResourceFactory = factory =>
|
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
class RemoteAdapter {
|
|
77
|
-
constructor(
|
|
79
|
+
constructor(
|
|
80
|
+
handler,
|
|
81
|
+
{ debounceResource = res => res, dirMode, vhdDirectoryCompression, useGetDiskLegacy = false } = {}
|
|
82
|
+
) {
|
|
78
83
|
this._debounceResource = debounceResource
|
|
79
84
|
this._dirMode = dirMode
|
|
80
85
|
this._handler = handler
|
|
81
86
|
this._vhdDirectoryCompression = vhdDirectoryCompression
|
|
82
87
|
this._readCacheListVmBackups = synchronized.withKey()(this._readCacheListVmBackups)
|
|
88
|
+
this._useGetDiskLegacy = useGetDiskLegacy
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
get handler() {
|
|
@@ -127,7 +133,9 @@ class RemoteAdapter {
|
|
|
127
133
|
}
|
|
128
134
|
|
|
129
135
|
async *_getPartition(devicePath, partition) {
|
|
130
|
-
|
|
136
|
+
// the norecovery option is necessary because if the partition is dirty,
|
|
137
|
+
// mount will try to fix it which is impossible if because the device is read-only
|
|
138
|
+
const options = ['loop', 'ro', 'norecovery']
|
|
131
139
|
|
|
132
140
|
if (partition !== undefined) {
|
|
133
141
|
const { size, start } = partition
|
|
@@ -224,11 +232,30 @@ class RemoteAdapter {
|
|
|
224
232
|
return promise
|
|
225
233
|
}
|
|
226
234
|
|
|
235
|
+
#removeVmBackupsFromCache(backups) {
|
|
236
|
+
for (const [dir, filenames] of Object.entries(
|
|
237
|
+
groupBy(
|
|
238
|
+
backups.map(_ => _._filename),
|
|
239
|
+
dirname
|
|
240
|
+
)
|
|
241
|
+
)) {
|
|
242
|
+
// detached async action, will not reject
|
|
243
|
+
this._updateCache(dir + '/cache.json.gz', backups => {
|
|
244
|
+
for (const filename of filenames) {
|
|
245
|
+
debug('removing cache entry', { entry: filename })
|
|
246
|
+
delete backups[filename]
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
227
252
|
async deleteDeltaVmBackups(backups) {
|
|
228
253
|
const handler = this._handler
|
|
229
254
|
|
|
230
255
|
// this will delete the json, unused VHDs will be detected by `cleanVm`
|
|
231
256
|
await asyncMapSettled(backups, ({ _filename }) => handler.unlink(_filename))
|
|
257
|
+
|
|
258
|
+
this.#removeVmBackupsFromCache(backups)
|
|
232
259
|
}
|
|
233
260
|
|
|
234
261
|
async deleteMetadataBackup(backupId) {
|
|
@@ -256,6 +283,8 @@ class RemoteAdapter {
|
|
|
256
283
|
await asyncMapSettled(backups, ({ _filename, xva }) =>
|
|
257
284
|
Promise.all([handler.unlink(_filename), handler.unlink(resolveRelativeFromFile(_filename, xva))])
|
|
258
285
|
)
|
|
286
|
+
|
|
287
|
+
this.#removeVmBackupsFromCache(backups)
|
|
259
288
|
}
|
|
260
289
|
|
|
261
290
|
deleteVmBackup(file) {
|
|
@@ -276,14 +305,13 @@ class RemoteAdapter {
|
|
|
276
305
|
full !== undefined && this.deleteFullVmBackups(full),
|
|
277
306
|
])
|
|
278
307
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
// don't
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
await asyncMap(dedupedVmUuid, vmUuid => this.invalidateVmBackupListCache(vmUuid))
|
|
308
|
+
await asyncMap(new Set(files.map(file => dirname(file))), dir =>
|
|
309
|
+
// - don't merge in main process, unused VHDs will be merged in the next backup run
|
|
310
|
+
// - don't error in case this fails:
|
|
311
|
+
// - if lock is already being held, a backup is running and cleanVm will be ran at the end
|
|
312
|
+
// - otherwise, there is nothing more we can do, orphan file will be cleaned in the future
|
|
313
|
+
this.cleanVm(dir, { remove: true, logWarn: warn }).catch(noop)
|
|
314
|
+
)
|
|
287
315
|
}
|
|
288
316
|
|
|
289
317
|
#getCompressionType() {
|
|
@@ -298,7 +326,8 @@ class RemoteAdapter {
|
|
|
298
326
|
return this.#useVhdDirectory()
|
|
299
327
|
}
|
|
300
328
|
|
|
301
|
-
async
|
|
329
|
+
async *#getDiskLegacy(diskId) {
|
|
330
|
+
const RE_VHDI = /^vhdi(\d+)$/
|
|
302
331
|
const handler = this._handler
|
|
303
332
|
|
|
304
333
|
const diskPath = handler._getFilePath('/' + diskId)
|
|
@@ -328,6 +357,20 @@ class RemoteAdapter {
|
|
|
328
357
|
}
|
|
329
358
|
}
|
|
330
359
|
|
|
360
|
+
async *getDisk(diskId) {
|
|
361
|
+
if (this._useGetDiskLegacy) {
|
|
362
|
+
yield* this.#getDiskLegacy(diskId)
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
const handler = this._handler
|
|
366
|
+
// this is a disposable
|
|
367
|
+
const mountDir = yield getTmpDir()
|
|
368
|
+
// this is also a disposable
|
|
369
|
+
yield mount(handler, diskId, mountDir)
|
|
370
|
+
// this will yield disk path to caller
|
|
371
|
+
yield `${mountDir}/vhd0`
|
|
372
|
+
}
|
|
373
|
+
|
|
331
374
|
// partitionId values:
|
|
332
375
|
//
|
|
333
376
|
// - undefined: raw disk
|
|
@@ -378,22 +421,25 @@ class RemoteAdapter {
|
|
|
378
421
|
listPartitionFiles(diskId, partitionId, path) {
|
|
379
422
|
return Disposable.use(this.getPartition(diskId, partitionId), async rootPath => {
|
|
380
423
|
path = resolveSubpath(rootPath, path)
|
|
381
|
-
|
|
382
424
|
const entriesMap = {}
|
|
383
|
-
await
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
425
|
+
await asyncEach(
|
|
426
|
+
await readdir(path),
|
|
427
|
+
async name => {
|
|
428
|
+
try {
|
|
429
|
+
const stats = await lstat(`${path}/${name}`)
|
|
430
|
+
if (stats.isDirectory()) {
|
|
431
|
+
entriesMap[name + '/'] = {}
|
|
432
|
+
} else if (stats.isFile()) {
|
|
433
|
+
entriesMap[name] = {}
|
|
434
|
+
}
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (error == null || error.code !== 'ENOENT') {
|
|
437
|
+
throw error
|
|
438
|
+
}
|
|
394
439
|
}
|
|
395
|
-
}
|
|
396
|
-
|
|
440
|
+
},
|
|
441
|
+
{ concurrency: 1 }
|
|
442
|
+
)
|
|
397
443
|
|
|
398
444
|
return entriesMap
|
|
399
445
|
})
|
|
@@ -458,11 +504,46 @@ class RemoteAdapter {
|
|
|
458
504
|
return backupsByPool
|
|
459
505
|
}
|
|
460
506
|
|
|
507
|
+
#getVmBackupsCache(vmUuid) {
|
|
508
|
+
return `${BACKUP_DIR}/${vmUuid}/cache.json.gz`
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async #readCache(path) {
|
|
512
|
+
try {
|
|
513
|
+
return JSON.parse(await fromCallback(zlib.gunzip, await this.handler.readFile(path)))
|
|
514
|
+
} catch (error) {
|
|
515
|
+
if (error.code !== 'ENOENT') {
|
|
516
|
+
warn('#readCache', { error, path })
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
_updateCache = synchronized.withKey()(this._updateCache)
|
|
522
|
+
// eslint-disable-next-line no-dupe-class-members
|
|
523
|
+
async _updateCache(path, fn) {
|
|
524
|
+
const cache = await this.#readCache(path)
|
|
525
|
+
if (cache !== undefined) {
|
|
526
|
+
fn(cache)
|
|
527
|
+
|
|
528
|
+
await this.#writeCache(path, cache)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async #writeCache(path, data) {
|
|
533
|
+
try {
|
|
534
|
+
await this.handler.writeFile(path, await fromCallback(zlib.gzip, JSON.stringify(data)), { flags: 'w' })
|
|
535
|
+
} catch (error) {
|
|
536
|
+
warn('#writeCache', { error, path })
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
461
540
|
async invalidateVmBackupListCache(vmUuid) {
|
|
462
|
-
await this.handler.unlink(
|
|
541
|
+
await this.handler.unlink(this.#getVmBackupsCache(vmUuid))
|
|
463
542
|
}
|
|
464
543
|
|
|
465
544
|
async #getCachabledDataListVmBackups(dir) {
|
|
545
|
+
debug('generating cache', { path: dir })
|
|
546
|
+
|
|
466
547
|
const handler = this._handler
|
|
467
548
|
const backups = {}
|
|
468
549
|
|
|
@@ -498,41 +579,26 @@ class RemoteAdapter {
|
|
|
498
579
|
// if cache is missing or broken => regenerate it and return
|
|
499
580
|
|
|
500
581
|
async _readCacheListVmBackups(vmUuid) {
|
|
501
|
-
const
|
|
502
|
-
const path = `${dir}/cache.json.gz`
|
|
582
|
+
const path = this.#getVmBackupsCache(vmUuid)
|
|
503
583
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
return
|
|
508
|
-
} catch (error) {
|
|
509
|
-
if (error.code !== 'ENOENT') {
|
|
510
|
-
warn('Cache file was unreadable', { vmUuid, error })
|
|
511
|
-
}
|
|
584
|
+
const cache = await this.#readCache(path)
|
|
585
|
+
if (cache !== undefined) {
|
|
586
|
+
debug('found VM backups cache, using it', { path })
|
|
587
|
+
return cache
|
|
512
588
|
}
|
|
513
589
|
|
|
514
590
|
// nothing cached, or cache unreadable => regenerate it
|
|
515
|
-
const backups = await this.#getCachabledDataListVmBackups(
|
|
591
|
+
const backups = await this.#getCachabledDataListVmBackups(`${BACKUP_DIR}/${vmUuid}`)
|
|
516
592
|
if (backups === undefined) {
|
|
517
593
|
return
|
|
518
594
|
}
|
|
519
595
|
|
|
520
596
|
// detached async action, will not reject
|
|
521
|
-
this.#
|
|
597
|
+
this.#writeCache(path, backups)
|
|
522
598
|
|
|
523
599
|
return backups
|
|
524
600
|
}
|
|
525
601
|
|
|
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
602
|
async listVmBackups(vmUuid, predicate) {
|
|
537
603
|
const backups = []
|
|
538
604
|
const cached = await this._readCacheListVmBackups(vmUuid)
|
|
@@ -571,13 +637,35 @@ class RemoteAdapter {
|
|
|
571
637
|
return backups.sort(compareTimestamp)
|
|
572
638
|
}
|
|
573
639
|
|
|
574
|
-
async
|
|
640
|
+
async writeVmBackupMetadata(vmUuid, metadata) {
|
|
641
|
+
const path = `/${BACKUP_DIR}/${vmUuid}/${formatFilenameDate(metadata.timestamp)}.json`
|
|
642
|
+
|
|
643
|
+
await this.handler.outputFile(path, JSON.stringify(metadata), {
|
|
644
|
+
dirMode: this._dirMode,
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
// will not throw
|
|
648
|
+
this._updateCache(this.#getVmBackupsCache(vmUuid), backups => {
|
|
649
|
+
debug('adding cache entry', { entry: path })
|
|
650
|
+
backups[path] = {
|
|
651
|
+
...metadata,
|
|
652
|
+
|
|
653
|
+
// these values are required in the cache
|
|
654
|
+
_filename: path,
|
|
655
|
+
id: path,
|
|
656
|
+
}
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
return path
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async writeVhd(path, input, { checksum = true, validator = noop, writeBlockConcurrency } = {}) {
|
|
575
663
|
const handler = this._handler
|
|
576
664
|
|
|
577
665
|
if (this.#useVhdDirectory()) {
|
|
578
666
|
const dataPath = `${dirname(path)}/data/${uuidv4()}.vhd`
|
|
579
667
|
await createVhdDirectoryFromStream(handler, dataPath, input, {
|
|
580
|
-
concurrency:
|
|
668
|
+
concurrency: writeBlockConcurrency,
|
|
581
669
|
compression: this.#getCompressionType(),
|
|
582
670
|
async validator() {
|
|
583
671
|
await input.task
|
package/_cleanVm.js
CHANGED
|
@@ -37,7 +37,7 @@ const computeVhdsSize = (handler, vhdPaths) =>
|
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
// chain is [ ancestor, child_1, ..., child_n ]
|
|
40
|
-
async function _mergeVhdChain(handler, chain, { logInfo, remove, merge }) {
|
|
40
|
+
async function _mergeVhdChain(handler, chain, { logInfo, remove, merge, mergeBlockConcurrency }) {
|
|
41
41
|
if (merge) {
|
|
42
42
|
logInfo(`merging VHD chain`, { chain })
|
|
43
43
|
|
|
@@ -55,6 +55,7 @@ async function _mergeVhdChain(handler, chain, { logInfo, remove, merge }) {
|
|
|
55
55
|
try {
|
|
56
56
|
return await mergeVhdChain(handler, chain, {
|
|
57
57
|
logInfo,
|
|
58
|
+
mergeBlockConcurrency,
|
|
58
59
|
onProgress({ done: d, total: t }) {
|
|
59
60
|
done = d
|
|
60
61
|
total = t
|
|
@@ -181,7 +182,15 @@ const defaultMergeLimiter = limitConcurrency(1)
|
|
|
181
182
|
|
|
182
183
|
exports.cleanVm = async function cleanVm(
|
|
183
184
|
vmDir,
|
|
184
|
-
{
|
|
185
|
+
{
|
|
186
|
+
fixMetadata,
|
|
187
|
+
remove,
|
|
188
|
+
merge,
|
|
189
|
+
mergeBlockConcurrency,
|
|
190
|
+
mergeLimiter = defaultMergeLimiter,
|
|
191
|
+
logInfo = noop,
|
|
192
|
+
logWarn = console.warn,
|
|
193
|
+
}
|
|
185
194
|
) {
|
|
186
195
|
const limitedMergeVhdChain = mergeLimiter(_mergeVhdChain)
|
|
187
196
|
|
|
@@ -302,6 +311,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
302
311
|
}
|
|
303
312
|
|
|
304
313
|
const jsons = new Set()
|
|
314
|
+
let mustInvalidateCache = false
|
|
305
315
|
const xvas = new Set()
|
|
306
316
|
const xvaSums = []
|
|
307
317
|
const entries = await handler.list(vmDir, {
|
|
@@ -350,6 +360,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
350
360
|
if (remove) {
|
|
351
361
|
logInfo('deleting incomplete backup', { path: json })
|
|
352
362
|
jsons.delete(json)
|
|
363
|
+
mustInvalidateCache = true
|
|
353
364
|
await handler.unlink(json)
|
|
354
365
|
}
|
|
355
366
|
}
|
|
@@ -372,6 +383,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
372
383
|
logWarn('some VHDs linked to the backup are missing', { backup: json, missingVhds })
|
|
373
384
|
if (remove) {
|
|
374
385
|
logInfo('deleting incomplete backup', { path: json })
|
|
386
|
+
mustInvalidateCache = true
|
|
375
387
|
jsons.delete(json)
|
|
376
388
|
await handler.unlink(json)
|
|
377
389
|
}
|
|
@@ -444,7 +456,13 @@ exports.cleanVm = async function cleanVm(
|
|
|
444
456
|
const metadataWithMergedVhd = {}
|
|
445
457
|
const doMerge = async () => {
|
|
446
458
|
await asyncMap(toMerge, async chain => {
|
|
447
|
-
const merged = await limitedMergeVhdChain(handler, chain, {
|
|
459
|
+
const merged = await limitedMergeVhdChain(handler, chain, {
|
|
460
|
+
logInfo,
|
|
461
|
+
logWarn,
|
|
462
|
+
remove,
|
|
463
|
+
merge,
|
|
464
|
+
mergeBlockConcurrency,
|
|
465
|
+
})
|
|
448
466
|
if (merged !== undefined) {
|
|
449
467
|
const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metada file
|
|
450
468
|
metadataWithMergedVhd[metadataPath] = true
|
|
@@ -528,6 +546,11 @@ exports.cleanVm = async function cleanVm(
|
|
|
528
546
|
}
|
|
529
547
|
})
|
|
530
548
|
|
|
549
|
+
// purge cache if a metadata file has been deleted
|
|
550
|
+
if (mustInvalidateCache) {
|
|
551
|
+
await handler.unlink(vmDir + '/cache.json.gz')
|
|
552
|
+
}
|
|
553
|
+
|
|
531
554
|
return {
|
|
532
555
|
// boolean whether some VHDs were merged (or should be merged)
|
|
533
556
|
merge: toMerge.length !== 0,
|
package/_deltaVm.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const compareVersions = require('compare-versions')
|
|
4
3
|
const find = require('lodash/find.js')
|
|
5
4
|
const groupBy = require('lodash/groupBy.js')
|
|
6
5
|
const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
|
7
6
|
const omit = require('lodash/omit.js')
|
|
8
7
|
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
9
8
|
const { CancelToken } = require('promise-toolbox')
|
|
9
|
+
const { compareVersions } = require('compare-versions')
|
|
10
10
|
const { createVhdStreamWithLength } = require('vhd-lib')
|
|
11
11
|
const { defer } = require('golike-defer')
|
|
12
12
|
|
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.28.1",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.6"
|
|
14
14
|
},
|
|
@@ -16,16 +16,18 @@
|
|
|
16
16
|
"postversion": "npm publish --access public"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
+
"@vates/async-each": "^1.0.0",
|
|
19
20
|
"@vates/cached-dns.lookup": "^1.0.0",
|
|
20
21
|
"@vates/compose": "^2.1.0",
|
|
21
22
|
"@vates/decorate-with": "^2.0.0",
|
|
22
|
-
"@vates/disposable": "^0.1.
|
|
23
|
+
"@vates/disposable": "^0.1.2",
|
|
24
|
+
"@vates/fuse-vhd": "^1.0.0",
|
|
23
25
|
"@vates/parse-duration": "^0.1.1",
|
|
24
26
|
"@xen-orchestra/async-map": "^0.1.2",
|
|
25
|
-
"@xen-orchestra/fs": "^3.
|
|
26
|
-
"@xen-orchestra/log": "^0.
|
|
27
|
+
"@xen-orchestra/fs": "^3.1.0",
|
|
28
|
+
"@xen-orchestra/log": "^0.4.0",
|
|
27
29
|
"@xen-orchestra/template": "^0.1.0",
|
|
28
|
-
"compare-versions": "^
|
|
30
|
+
"compare-versions": "^5.0.1",
|
|
29
31
|
"d3-time-format": "^3.0.0",
|
|
30
32
|
"decorator-synchronized": "^0.6.0",
|
|
31
33
|
"end-of-stream": "^1.4.4",
|
|
@@ -37,8 +39,8 @@
|
|
|
37
39
|
"parse-pairs": "^1.1.0",
|
|
38
40
|
"promise-toolbox": "^0.21.0",
|
|
39
41
|
"proper-lockfile": "^4.1.2",
|
|
40
|
-
"uuid": "^
|
|
41
|
-
"vhd-lib": "^4.
|
|
42
|
+
"uuid": "^9.0.0",
|
|
43
|
+
"vhd-lib": "^4.1.0",
|
|
42
44
|
"yazl": "^2.5.1"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
@@ -46,7 +48,7 @@
|
|
|
46
48
|
"tmp": "^0.2.1"
|
|
47
49
|
},
|
|
48
50
|
"peerDependencies": {
|
|
49
|
-
"@xen-orchestra/xapi": "^1.
|
|
51
|
+
"@xen-orchestra/xapi": "^1.5.0"
|
|
50
52
|
},
|
|
51
53
|
"license": "AGPL-3.0-or-later",
|
|
52
54
|
"author": {
|
|
@@ -19,8 +19,6 @@ 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')
|
|
24
22
|
|
|
25
23
|
const { warn } = createLogger('xo:backups:DeltaBackupWriter')
|
|
26
24
|
|
|
@@ -38,6 +36,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
38
36
|
try {
|
|
39
37
|
const vhds = await handler.list(`${vdisDir}/${srcVdi.uuid}`, {
|
|
40
38
|
filter: _ => _[0] !== '.' && _.endsWith('.vhd'),
|
|
39
|
+
ignoreMissing: true,
|
|
41
40
|
prependDir: true,
|
|
42
41
|
})
|
|
43
42
|
const packedBaseUuid = packUuid(baseUuid)
|
|
@@ -71,35 +70,6 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
71
70
|
return this._cleanVm({ merge: true })
|
|
72
71
|
}
|
|
73
72
|
|
|
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
|
-
|
|
103
73
|
prepare({ isFull }) {
|
|
104
74
|
// create the task related to this export and ensure all methods are called in this context
|
|
105
75
|
const task = new Task({
|
|
@@ -189,7 +159,6 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
189
159
|
}/${adapter.getVhdFileName(basename)}`
|
|
190
160
|
)
|
|
191
161
|
|
|
192
|
-
const metadataFilename = (this._metadataFileName = `${backupDir}/${basename}.json`)
|
|
193
162
|
const metadataContent = {
|
|
194
163
|
jobId,
|
|
195
164
|
mode: job.mode,
|
|
@@ -235,6 +204,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
235
204
|
// merges and chainings
|
|
236
205
|
checksum: false,
|
|
237
206
|
validator: tmpPath => checkVhd(handler, tmpPath),
|
|
207
|
+
writeBlockConcurrency: this._backup.config.writeBlockConcurrency,
|
|
238
208
|
})
|
|
239
209
|
|
|
240
210
|
if (isDelta) {
|
|
@@ -254,9 +224,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
254
224
|
}
|
|
255
225
|
})
|
|
256
226
|
metadataContent.size = size
|
|
257
|
-
await
|
|
258
|
-
dirMode: backup.config.dirMode,
|
|
259
|
-
})
|
|
227
|
+
this._metadataFileName = await adapter.writeVmBackupMetadata(vm.uuid, metadataContent)
|
|
260
228
|
|
|
261
229
|
// TODO: run cleanup?
|
|
262
230
|
}
|
|
@@ -34,7 +34,6 @@ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(Abst
|
|
|
34
34
|
const { job, scheduleId, vm } = backup
|
|
35
35
|
|
|
36
36
|
const adapter = this._adapter
|
|
37
|
-
const handler = adapter.handler
|
|
38
37
|
const backupDir = getVmBackupDir(vm.uuid)
|
|
39
38
|
|
|
40
39
|
// TODO: clean VM backup directory
|
|
@@ -74,9 +73,7 @@ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(Abst
|
|
|
74
73
|
return { size: sizeContainer.size }
|
|
75
74
|
})
|
|
76
75
|
metadata.size = sizeContainer.size
|
|
77
|
-
await
|
|
78
|
-
dirMode: backup.config.dirMode,
|
|
79
|
-
})
|
|
76
|
+
this._metadataFileName = await adapter.writeVmBackupMetadata(vm.uuid, metadata)
|
|
80
77
|
|
|
81
78
|
if (!deleteFirst) {
|
|
82
79
|
await deleteOldBackups()
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
const { createLogger } = require('@xen-orchestra/log')
|
|
4
4
|
const { join } = require('path')
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const MergeWorker = require('../merge-worker/index.js')
|
|
6
|
+
const assert = require('assert')
|
|
8
7
|
const { formatFilenameDate } = require('../_filenameDate.js')
|
|
8
|
+
const { getVmBackupDir } = require('../_getVmBackupDir.js')
|
|
9
|
+
const { HealthCheckVmBackup } = require('../HealthCheckVmBackup.js')
|
|
10
|
+
const { ImportVmBackup } = require('../ImportVmBackup.js')
|
|
9
11
|
const { Task } = require('../Task.js')
|
|
12
|
+
const MergeWorker = require('../merge-worker/index.js')
|
|
10
13
|
|
|
11
14
|
const { info, warn } = createLogger('xo:backups:MixinBackupWriter')
|
|
12
15
|
|
|
@@ -36,6 +39,7 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
|
|
|
36
39
|
Task.warning(message, data)
|
|
37
40
|
},
|
|
38
41
|
lock: false,
|
|
42
|
+
mergeBlockConcurrency: this._backup.config.mergeBlockConcurrency,
|
|
39
43
|
})
|
|
40
44
|
})
|
|
41
45
|
} catch (error) {
|
|
@@ -71,6 +75,39 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
|
|
|
71
75
|
const remotePath = handler._getRealPath()
|
|
72
76
|
await MergeWorker.run(remotePath)
|
|
73
77
|
}
|
|
74
|
-
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
healthCheck(sr) {
|
|
81
|
+
assert.notStrictEqual(
|
|
82
|
+
this._metadataFileName,
|
|
83
|
+
undefined,
|
|
84
|
+
'Metadata file name should be defined before making a healthcheck'
|
|
85
|
+
)
|
|
86
|
+
return Task.run(
|
|
87
|
+
{
|
|
88
|
+
name: 'health check',
|
|
89
|
+
},
|
|
90
|
+
async () => {
|
|
91
|
+
const xapi = sr.$xapi
|
|
92
|
+
const srUuid = sr.uuid
|
|
93
|
+
const adapter = this._adapter
|
|
94
|
+
const metadata = await adapter.readVmBackupMetadata(this._metadataFileName)
|
|
95
|
+
const { id: restoredId } = await new ImportVmBackup({
|
|
96
|
+
adapter,
|
|
97
|
+
metadata,
|
|
98
|
+
srUuid,
|
|
99
|
+
xapi,
|
|
100
|
+
}).run()
|
|
101
|
+
const restoredVm = xapi.getObject(restoredId)
|
|
102
|
+
try {
|
|
103
|
+
await new HealthCheckVmBackup({
|
|
104
|
+
restoredVm,
|
|
105
|
+
xapi,
|
|
106
|
+
}).run()
|
|
107
|
+
} finally {
|
|
108
|
+
await xapi.VM_destroy(restoredVm.$ref)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
)
|
|
75
112
|
}
|
|
76
113
|
}
|