@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 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('./_extractIdsFromSimplePattern.js')
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
- run() {
48
- const type = this._job.type
74
+ const { type } = job
75
+ const baseSettings = { ...DEFAULT_SETTINGS }
49
76
  if (type === 'backup') {
50
- return this._runVmBackup()
77
+ Object.assign(baseSettings, DEFAULT_VM_SETTINGS, config.defaultSettings, config.vm?.defaultSettings)
78
+ this.run = this._runVmBackup
51
79
  } else if (type === 'metadataBackup') {
52
- return this._runMetadataBackup()
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 { settings } = job
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
- async (srs, remoteAdapters) => {
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 && scheduleSettings.snapshotRetention === 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: { ...scheduleSettings, ...settings[vmUuid] },
280
+ settings: { ...settings, ...allSettings[vm.uuid] },
255
281
  srs,
256
282
  vm,
257
283
  }).run()
258
284
  )
259
285
  )
260
- const { concurrency } = scheduleSettings
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 { Constants, createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } = require('vhd-lib')
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, stat } = require('fs-extra')
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
- try {
49
- const subFiles = await readdir(realPath)
50
- await asyncMap(subFiles, file => addDirectory(files, realPath + '/' + file, metadataPath + '/' + file))
51
- } catch (error) {
52
- if (error == null || error.code !== 'ENOTDIR') {
53
- throw error
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 { delta, full, ...others } = groupBy(await asyncMap(files, file => this.readVmBackupMetadata(file)), 'mode')
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.type === 's3'
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 stat(`${path}/${name}`)
380
- entriesMap[stats.isDirectory() ? `${name}/` : name] = {}
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 listVmBackups(vmUuid, predicate) {
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(`${BACKUP_DIR}/${vmUuid}`, {
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
- if (predicate === undefined || predicate(metadata)) {
464
- // inject an id usable by importVmBackupNg()
465
- metadata.id = metadata._filename
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(`listVmBackups ${file}`, { error })
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
- async _createSyntheticStream(handler, paths) {
535
- let disposableVhds = []
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 disposables.dispose()
615
+ await disposableSynthetic.dispose()
564
616
  } catch (error) {
565
- warn('_createSyntheticStream: failed to dispose VHDs', { error })
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
- return Object.defineProperty(JSON.parse(await this._handler.readFile(path)), '_filename', { value: path })
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({ config, getSnapshotNameLabel, job, remoteAdapters, remotes, schedule, settings, srs, vm }) {
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
- this._isDelta || (!settings.offlineBackup && vm.power_state === 'Running') || settings.snapshotRetention !== 0
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 jobSettings = this.job.settings
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
- ...jobSettings[scheduleId],
322
- ...jobSettings[this.vm.uuid],
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(() => writer.afterBackup())
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