@xen-orchestra/backups 0.27.4 → 0.28.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/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,14 @@ const debounceResourceFactory = factory =>
74
76
  }
75
77
 
76
78
  class RemoteAdapter {
77
- constructor(handler, { debounceResource = res => res, dirMode, vhdDirectoryCompression } = {}) {
79
+ constructor(handler, { debounceResource = res => res, dirMode, vhdDirectoryCompression, useGetDiskLegacy=false } = {}) {
78
80
  this._debounceResource = debounceResource
79
81
  this._dirMode = dirMode
80
82
  this._handler = handler
81
83
  this._vhdDirectoryCompression = vhdDirectoryCompression
82
84
  this._readCacheListVmBackups = synchronized.withKey()(this._readCacheListVmBackups)
85
+ this._useGetDiskLegacy = useGetDiskLegacy
86
+
83
87
  }
84
88
 
85
89
  get handler() {
@@ -127,7 +131,9 @@ class RemoteAdapter {
127
131
  }
128
132
 
129
133
  async *_getPartition(devicePath, partition) {
130
- const options = ['loop', 'ro']
134
+ // the norecovery option is necessary because if the partition is dirty,
135
+ // mount will try to fix it which is impossible if because the device is read-only
136
+ const options = ['loop', 'ro', 'norecovery']
131
137
 
132
138
  if (partition !== undefined) {
133
139
  const { size, start } = partition
@@ -224,11 +230,30 @@ class RemoteAdapter {
224
230
  return promise
225
231
  }
226
232
 
233
+ #removeVmBackupsFromCache(backups) {
234
+ for (const [dir, filenames] of Object.entries(
235
+ groupBy(
236
+ backups.map(_ => _._filename),
237
+ dirname
238
+ )
239
+ )) {
240
+ // detached async action, will not reject
241
+ this._updateCache(dir + '/cache.json.gz', backups => {
242
+ for (const filename of filenames) {
243
+ debug('removing cache entry', { entry: filename })
244
+ delete backups[filename]
245
+ }
246
+ })
247
+ }
248
+ }
249
+
227
250
  async deleteDeltaVmBackups(backups) {
228
251
  const handler = this._handler
229
252
 
230
253
  // this will delete the json, unused VHDs will be detected by `cleanVm`
231
254
  await asyncMapSettled(backups, ({ _filename }) => handler.unlink(_filename))
255
+
256
+ this.#removeVmBackupsFromCache(backups)
232
257
  }
233
258
 
234
259
  async deleteMetadataBackup(backupId) {
@@ -256,6 +281,8 @@ class RemoteAdapter {
256
281
  await asyncMapSettled(backups, ({ _filename, xva }) =>
257
282
  Promise.all([handler.unlink(_filename), handler.unlink(resolveRelativeFromFile(_filename, xva))])
258
283
  )
284
+
285
+ this.#removeVmBackupsFromCache(backups)
259
286
  }
260
287
 
261
288
  deleteVmBackup(file) {
@@ -276,14 +303,13 @@ class RemoteAdapter {
276
303
  full !== undefined && this.deleteFullVmBackups(full),
277
304
  ])
278
305
 
279
- const dirs = new Set(files.map(file => dirname(file)))
280
- for (const dir of dirs) {
281
- // don't merge in main process, unused VHDs will be merged in the next backup run
282
- await this.cleanVm(dir, { remove: true, logWarn: warn })
283
- }
284
-
285
- const dedupedVmUuid = new Set(metadatas.map(_ => _.vm.uuid))
286
- await asyncMap(dedupedVmUuid, vmUuid => this.invalidateVmBackupListCache(vmUuid))
306
+ await asyncMap(new Set(files.map(file => dirname(file))), dir =>
307
+ // - don't merge in main process, unused VHDs will be merged in the next backup run
308
+ // - don't error in case this fails:
309
+ // - if lock is already being held, a backup is running and cleanVm will be ran at the end
310
+ // - otherwise, there is nothing more we can do, orphan file will be cleaned in the future
311
+ this.cleanVm(dir, { remove: true, logWarn: warn }).catch(noop)
312
+ )
287
313
  }
288
314
 
289
315
  #getCompressionType() {
@@ -298,7 +324,10 @@ class RemoteAdapter {
298
324
  return this.#useVhdDirectory()
299
325
  }
300
326
 
301
- async *getDisk(diskId) {
327
+
328
+ async *#getDiskLegacy(diskId) {
329
+
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 asyncMap(await readdir(path), async name => {
384
- try {
385
- const stats = await lstat(`${path}/${name}`)
386
- if (stats.isDirectory()) {
387
- entriesMap[name + '/'] = {}
388
- } else if (stats.isFile()) {
389
- entriesMap[name] = {}
390
- }
391
- } catch (error) {
392
- if (error == null || error.code !== 'ENOENT') {
393
- throw error
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(`${BACKUP_DIR}/${vmUuid}/cache.json.gz`)
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 dir = `${BACKUP_DIR}/${vmUuid}`
502
- const path = `${dir}/cache.json.gz`
582
+ const path = this.#getVmBackupsCache(vmUuid)
503
583
 
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
- }
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(dir)
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.#writeVmBackupsCache(path, backups)
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 writeVhd(path, input, { checksum = true, validator = noop } = {}) {
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: 16,
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
- { fixMetadata, remove, merge, mergeLimiter = defaultMergeLimiter, logInfo = noop, logWarn = console.warn }
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, { logInfo, logWarn, remove, merge })
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.27.4",
11
+ "version": "0.28.0",
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
23
  "@vates/disposable": "^0.1.1",
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.0.0",
27
+ "@xen-orchestra/fs": "^3.1.0",
26
28
  "@xen-orchestra/log": "^0.3.0",
27
29
  "@xen-orchestra/template": "^0.1.0",
28
- "compare-versions": "^4.0.1",
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": "^8.3.2",
41
- "vhd-lib": "^4.0.0",
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.4.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 handler.outputFile(metadataFilename, JSON.stringify(metadataContent), {
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 handler.outputFile(metadataFilename, JSON.stringify(metadata), {
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 { getVmBackupDir } = require('../_getVmBackupDir.js')
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
- await this._adapter.invalidateVmBackupListCache(this._backup.vm.uuid)
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
  }