@xen-orchestra/backups 0.37.0 → 0.38.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
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { Metadata } = require('./_runners/Metadata.js')
4
+ const { VmsRemote } = require('./_runners/VmsRemote.js')
4
5
  const { VmsXapi } = require('./_runners/VmsXapi.js')
5
6
 
6
7
  exports.createRunner = function createRunner(opts) {
@@ -8,6 +9,8 @@ exports.createRunner = function createRunner(opts) {
8
9
  switch (type) {
9
10
  case 'backup':
10
11
  return new VmsXapi(opts)
12
+ case 'mirrorBackup':
13
+ return new VmsRemote(opts)
11
14
  case 'metadataBackup':
12
15
  return new Metadata(opts)
13
16
  default:
@@ -0,0 +1,98 @@
1
+ 'use strict'
2
+
3
+ const { asyncMapSettled } = require('@xen-orchestra/async-map')
4
+ const Disposable = require('promise-toolbox/Disposable')
5
+ const { limitConcurrency } = require('limit-concurrency-decorator')
6
+
7
+ const { extractIdsFromSimplePattern } = require('../extractIdsFromSimplePattern.js')
8
+ const { Task } = require('../Task.js')
9
+ const createStreamThrottle = require('./_createStreamThrottle.js')
10
+ const { DEFAULT_SETTINGS, Abstract } = require('./_Abstract.js')
11
+ const { runTask } = require('./_runTask.js')
12
+ const { getAdaptersByRemote } = require('./_getAdaptersByRemote.js')
13
+ const { FullRemote } = require('./_vmRunners/FullRemote.js')
14
+ const { IncrementalRemote } = require('./_vmRunners/IncrementalRemote.js')
15
+
16
+ const DEFAULT_REMOTE_VM_SETTINGS = {
17
+ concurrency: 2,
18
+ copyRetention: 0,
19
+ deleteFirst: false,
20
+ exportRetention: 0,
21
+ healthCheckSr: undefined,
22
+ healthCheckVmsWithTags: [],
23
+ maxExportRate: 0,
24
+ maxMergedDeltasPerRun: Infinity,
25
+ timeout: 0,
26
+ validateVhdStreams: false,
27
+ vmTimeout: 0,
28
+ }
29
+
30
+ exports.VmsRemote = class RemoteVmsBackupRunner extends Abstract {
31
+ _computeBaseSettings(config, job) {
32
+ const baseSettings = { ...DEFAULT_SETTINGS }
33
+ Object.assign(baseSettings, DEFAULT_REMOTE_VM_SETTINGS, config.defaultSettings, config.vm?.defaultSettings)
34
+ Object.assign(baseSettings, job.settings[''])
35
+ return baseSettings
36
+ }
37
+
38
+ async run() {
39
+ const job = this._job
40
+ const schedule = this._schedule
41
+ const settings = this._settings
42
+
43
+ const throttleStream = createStreamThrottle(settings.maxExportRate)
44
+
45
+ const config = this._config
46
+ await Disposable.use(
47
+ () => this._getAdapter(job.sourceRemote),
48
+ () => (settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined),
49
+ Disposable.all(
50
+ extractIdsFromSimplePattern(job.remotes).map(id => id !== job.sourceRemote && this._getAdapter(id))
51
+ ),
52
+ async ({ adapter: sourceRemoteAdapter }, healthCheckSr, remoteAdapters) => {
53
+ // remove adapters that failed (already handled)
54
+ remoteAdapters = remoteAdapters.filter(_ => !!_)
55
+ if (remoteAdapters.length === 0) {
56
+ return
57
+ }
58
+
59
+ const vmsUuids = await sourceRemoteAdapter.listAllVms()
60
+
61
+ Task.info('vms', { vms: vmsUuids })
62
+
63
+ remoteAdapters = getAdaptersByRemote(remoteAdapters)
64
+ const allSettings = this._job.settings
65
+ const baseSettings = this._baseSettings
66
+
67
+ const handleVm = vmUuid => {
68
+ const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
69
+
70
+ const opts = {
71
+ baseSettings,
72
+ config,
73
+ job,
74
+ healthCheckSr,
75
+ remoteAdapters,
76
+ schedule,
77
+ settings: { ...settings, ...allSettings[vmUuid] },
78
+ sourceRemoteAdapter,
79
+ throttleStream,
80
+ vmUuid,
81
+ }
82
+ let vmBackup
83
+ if (job.mode === 'delta') {
84
+ vmBackup = new IncrementalRemote(opts)
85
+ } else if (job.mode === 'full') {
86
+ vmBackup = new FullRemote(opts)
87
+ } else {
88
+ throw new Error(`Job mode ${job.mode} not implemented for mirror backup`)
89
+ }
90
+
91
+ return runTask(taskStart, () => vmBackup.run())
92
+ }
93
+ const { concurrency } = settings
94
+ await asyncMapSettled(vmsUuids, !concurrency ? handleVm : limitConcurrency(concurrency)(handleVm))
95
+ }
96
+ )
97
+ }
98
+ }
@@ -0,0 +1,53 @@
1
+ 'use strict'
2
+
3
+ const { decorateMethodsWith } = require('@vates/decorate-with')
4
+ const { defer } = require('golike-defer')
5
+ const { AbstractRemote } = require('./_AbstractRemote')
6
+ const { FullRemoteWriter } = require('../_writers/FullRemoteWriter')
7
+ const { forkStreamUnpipe } = require('../_forkStreamUnpipe')
8
+ const { watchStreamSize } = require('../../_watchStreamSize')
9
+ const { Task } = require('../../Task')
10
+
11
+ class FullRemoteVmBackupRunner extends AbstractRemote {
12
+ _getRemoteWriter() {
13
+ return FullRemoteWriter
14
+ }
15
+ async _run($defer) {
16
+ const transferList = await this._computeTransferList(({ mode }) => mode === 'full')
17
+
18
+ await this._callWriters(async writer => {
19
+ await writer.beforeBackup()
20
+ $defer(async () => {
21
+ await writer.afterBackup()
22
+ })
23
+ }, 'writer.beforeBackup()')
24
+ if (transferList.length > 0) {
25
+ for (const metadata of transferList) {
26
+ const stream = await this._sourceRemoteAdapter.readFullVmBackup(metadata)
27
+ const sizeContainer = watchStreamSize(stream)
28
+
29
+ // @todo shouldn't transfer backup if it will be deleted by retention policy (higher retention on source than destination)
30
+ await this._callWriters(
31
+ writer =>
32
+ writer.run({
33
+ stream: forkStreamUnpipe(stream),
34
+ timestamp: metadata.timestamp,
35
+ vm: metadata.vm,
36
+ vmSnapshot: metadata.vmSnapshot,
37
+ sizeContainer,
38
+ }),
39
+ 'writer.run()'
40
+ )
41
+ // for healthcheck
42
+ this._tags = metadata.vm.tags
43
+ }
44
+ } else {
45
+ Task.info('No new data to upload for this VM')
46
+ }
47
+ }
48
+ }
49
+
50
+ exports.FullRemote = FullRemoteVmBackupRunner
51
+ decorateMethodsWith(FullRemoteVmBackupRunner, {
52
+ _run: defer,
53
+ })
@@ -16,7 +16,7 @@ exports.FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
16
16
  }
17
17
 
18
18
  _mustDoSnapshot() {
19
- const { vm } = this
19
+ const vm = this._vm
20
20
 
21
21
  const settings = this._settings
22
22
  return (
@@ -29,8 +29,10 @@ exports.FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
29
29
 
30
30
  async _copy() {
31
31
  const { compression } = this.job
32
+ const vm = this._vm
33
+ const exportedVm = this._exportedVm
32
34
  const stream = this._throttleStream(
33
- await this._xapi.VM_export(this.exportedVm.$ref, {
35
+ await this._xapi.VM_export(exportedVm.$ref, {
34
36
  compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
35
37
  useSnapshot: false,
36
38
  })
@@ -45,6 +47,8 @@ exports.FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
45
47
  sizeContainer,
46
48
  stream: forkStreamUnpipe(stream),
47
49
  timestamp,
50
+ vm,
51
+ vmSnapshot: exportedVm,
48
52
  }),
49
53
  'writer.run()'
50
54
  )
@@ -0,0 +1,67 @@
1
+ 'use strict'
2
+ const assert = require('node:assert')
3
+
4
+ const { decorateMethodsWith } = require('@vates/decorate-with')
5
+ const { defer } = require('golike-defer')
6
+ const { mapValues } = require('lodash')
7
+ const { Task } = require('../../Task')
8
+ const { AbstractRemote } = require('./_AbstractRemote')
9
+ const { IncrementalRemoteWriter } = require('../_writers/IncrementalRemoteWriter')
10
+ const { forkDeltaExport } = require('./_forkDeltaExport')
11
+ const isVhdDifferencingDisk = require('vhd-lib/isVhdDifferencingDisk')
12
+ const { asyncEach } = require('@vates/async-each')
13
+
14
+ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
15
+ _getRemoteWriter() {
16
+ return IncrementalRemoteWriter
17
+ }
18
+ async _run($defer) {
19
+ const transferList = await this._computeTransferList(({ mode }) => mode === 'delta')
20
+ await this._callWriters(async writer => {
21
+ await writer.beforeBackup()
22
+ $defer(async () => {
23
+ await writer.afterBackup()
24
+ })
25
+ }, 'writer.beforeBackup()')
26
+
27
+ if (transferList.length > 0) {
28
+ for (const metadata of transferList) {
29
+ assert.strictEqual(metadata.mode, 'delta')
30
+
31
+ await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
32
+ const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, {
33
+ useChain: false,
34
+ })
35
+
36
+ const differentialVhds = {}
37
+
38
+ await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
39
+ differentialVhds[key] = await isVhdDifferencingDisk(stream)
40
+ })
41
+
42
+ incrementalExport.streams = mapValues(incrementalExport.streams, this._throttleStream)
43
+ await this._callWriters(
44
+ writer =>
45
+ writer.transfer({
46
+ deltaExport: forkDeltaExport(incrementalExport),
47
+ differentialVhds,
48
+ timestamp: metadata.timestamp,
49
+ vm: metadata.vm,
50
+ vmSnapshot: metadata.vmSnapshot,
51
+ }),
52
+ 'writer.transfer()'
53
+ )
54
+ await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
55
+ // for healthcheck
56
+ this._tags = metadata.vm.tags
57
+ }
58
+ } else {
59
+ Task.info('No new data to upload for this VM')
60
+ }
61
+ }
62
+ }
63
+
64
+ exports.IncrementalRemote = IncrementalRemoteVmBackupRunner
65
+ decorateMethodsWith(IncrementalRemoteVmBackupRunner, {
66
+ _run: defer,
67
+ })
@@ -15,6 +15,8 @@ const { Task } = require('../../Task.js')
15
15
  const { watchStreamSize } = require('../../_watchStreamSize.js')
16
16
  const { AbstractXapi } = require('./_AbstractXapi.js')
17
17
  const { forkDeltaExport } = require('./_forkDeltaExport.js')
18
+ const isVhdDifferencingDisk = require('vhd-lib/isVhdDifferencingDisk')
19
+ const { asyncEach } = require('@vates/async-each')
18
20
 
19
21
  const { debug } = createLogger('xo:backups:IncrementalXapiVmBackup')
20
22
 
@@ -30,8 +32,9 @@ exports.IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXa
30
32
  }
31
33
 
32
34
  async _copy() {
33
- const { exportedVm } = this
34
35
  const baseVm = this._baseVm
36
+ const vm = this._vm
37
+ const exportedVm = this._exportedVm
35
38
  const fullVdisRequired = this._fullVdisRequired
36
39
 
37
40
  const isFull = fullVdisRequired === undefined || fullVdisRequired.size !== 0
@@ -46,12 +49,18 @@ exports.IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXa
46
49
  if (Object.values(deltaExport.streams).some(({ _nbd }) => _nbd)) {
47
50
  Task.info('Transfer data using NBD')
48
51
  }
52
+
53
+ const differentialVhds = {}
54
+ // since isVhdDifferencingDisk is reading and unshifting data in stream
55
+ // it should be done BEFORE any other stream transform
56
+ await asyncEach(Object.entries(deltaExport.streams), async ([key, stream]) => {
57
+ differentialVhds[key] = await isVhdDifferencingDisk(stream)
58
+ })
49
59
  const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
50
60
 
51
61
  if (this._settings.validateVhdStreams) {
52
62
  deltaExport.streams = mapValues(deltaExport.streams, stream => pipeline(stream, vhdStreamValidator, noop))
53
63
  }
54
-
55
64
  deltaExport.streams = mapValues(deltaExport.streams, this._throttleStream)
56
65
 
57
66
  const timestamp = Date.now()
@@ -60,8 +69,11 @@ exports.IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXa
60
69
  writer =>
61
70
  writer.transfer({
62
71
  deltaExport: forkDeltaExport(deltaExport),
72
+ differentialVhds,
63
73
  sizeContainers,
64
74
  timestamp,
75
+ vm,
76
+ vmSnapshot: exportedVm,
65
77
  }),
66
78
  'writer.transfer()'
67
79
  )
@@ -108,7 +120,7 @@ exports.IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXa
108
120
  return
109
121
  }
110
122
 
111
- const srcVdis = keyBy(await xapi.getRecords('VDI', await this.vm.$getDisks()), '$ref')
123
+ const srcVdis = keyBy(await xapi.getRecords('VDI', await this._vm.$getDisks()), '$ref')
112
124
 
113
125
  // resolve full record
114
126
  baseVm = await xapi.getRecord('VM', baseVm.$ref)
@@ -75,13 +75,21 @@ exports.Abstract = class AbstractVmBackupRunner {
75
75
  }
76
76
 
77
77
  // check if current VM has tags
78
- const { tags } = this.vm
78
+ const tags = this._tags
79
79
  const intersect = settings.healthCheckVmsWithTags.some(t => tags.includes(t))
80
80
 
81
81
  if (settings.healthCheckVmsWithTags.length !== 0 && !intersect) {
82
- return
82
+ // create a task to have an info in the logs and reports
83
+ return Task.run(
84
+ {
85
+ name: 'health check',
86
+ },
87
+ () => {
88
+ Task.info(`This VM doesn't match the health check's tags for this schedule`)
89
+ }
90
+ )
83
91
  }
84
92
 
85
- await this._callWriters(writer => writer.healthCheck(this._healthCheckSr), 'writer.healthCheck()')
93
+ await this._callWriters(writer => writer.healthCheck(), 'writer.healthCheck()')
86
94
  }
87
95
  }
@@ -0,0 +1,86 @@
1
+ 'use strict'
2
+ const { Abstract } = require('./_Abstract')
3
+
4
+ const { getVmBackupDir } = require('../../_getVmBackupDir')
5
+ const { asyncEach } = require('@vates/async-each')
6
+ const { Disposable } = require('promise-toolbox')
7
+
8
+ exports.AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstract {
9
+ constructor({
10
+ config,
11
+ job,
12
+ healthCheckSr,
13
+ remoteAdapters,
14
+ schedule,
15
+ settings,
16
+ sourceRemoteAdapter,
17
+ throttleStream,
18
+ vmUuid,
19
+ }) {
20
+ super()
21
+ this.config = config
22
+ this.job = job
23
+ this.remoteAdapters = remoteAdapters
24
+ this.scheduleId = schedule.id
25
+ this.timestamp = undefined
26
+
27
+ this._healthCheckSr = healthCheckSr
28
+ this._sourceRemoteAdapter = sourceRemoteAdapter
29
+ this._throttleStream = throttleStream
30
+ this._vmUuid = vmUuid
31
+
32
+ const allSettings = job.settings
33
+ const writers = new Set()
34
+ this._writers = writers
35
+
36
+ const RemoteWriter = this._getRemoteWriter()
37
+ Object.entries(remoteAdapters).forEach(([remoteId, adapter]) => {
38
+ const targetSettings = {
39
+ ...settings,
40
+ ...allSettings[remoteId],
41
+ }
42
+ writers.add(new RemoteWriter({ adapter, config, healthCheckSr, job, vmUuid, remoteId, settings: targetSettings }))
43
+ })
44
+ }
45
+
46
+ async _computeTransferList(predicate) {
47
+ const vmBackups = await this._sourceRemoteAdapter.listVmBackups(this._vmUuid, predicate)
48
+ const localMetada = new Map()
49
+ Object.values(vmBackups).forEach(metadata => {
50
+ const timestamp = metadata.timestamp
51
+ localMetada.set(timestamp, metadata)
52
+ })
53
+ const nbRemotes = Object.keys(this.remoteAdapters).length
54
+ const remoteMetadatas = {}
55
+ await asyncEach(Object.values(this.remoteAdapters), async remoteAdapter => {
56
+ const remoteMetadata = await remoteAdapter.listVmBackups(this._vmUuid, predicate)
57
+ remoteMetadata.forEach(metadata => {
58
+ const timestamp = metadata.timestamp
59
+ remoteMetadatas[timestamp] = (remoteMetadatas[timestamp] ?? 0) + 1
60
+ })
61
+ })
62
+
63
+ let chain = []
64
+ const timestamps = [...localMetada.keys()]
65
+ timestamps.sort()
66
+ for (const timestamp of timestamps) {
67
+ if (remoteMetadatas[timestamp] !== nbRemotes) {
68
+ // this backup is not present in all the remote
69
+ // should be retransfered if not found later
70
+ chain.push(localMetada.get(timestamp))
71
+ } else {
72
+ // backup is present in local and remote : the chain has already been transferred
73
+ chain = []
74
+ }
75
+ }
76
+ return chain
77
+ }
78
+
79
+ async run() {
80
+ const handler = this._sourceRemoteAdapter._handler
81
+ await Disposable.use(await handler.lock(getVmBackupDir(this._vmUuid)), async () => {
82
+ await this._run()
83
+ await this._healthCheck()
84
+ })
85
+ }
86
+ }
@@ -40,11 +40,11 @@ class AbstractXapiVmBackupRunner extends Abstract {
40
40
  this.timestamp = undefined
41
41
 
42
42
  // VM currently backed up
43
- this.vm = vm
44
- const { tags } = this.vm
43
+ const tags = (this._tags = vm.tags)
45
44
 
46
45
  // VM (snapshot) that is really exported
47
- this.exportedVm = undefined
46
+ this._exportedVm = undefined
47
+ this._vm = vm
48
48
 
49
49
  this._fullVdisRequired = undefined
50
50
  this._getSnapshotNameLabel = getSnapshotNameLabel
@@ -66,7 +66,6 @@ class AbstractXapiVmBackupRunner extends Abstract {
66
66
  settings.offlineSnapshot = true
67
67
  }
68
68
  this._settings = settings
69
-
70
69
  // Create writers
71
70
  {
72
71
  const writers = new Set()
@@ -75,13 +74,13 @@ class AbstractXapiVmBackupRunner extends Abstract {
75
74
  const [BackupWriter, ReplicationWriter] = this._getWriters()
76
75
 
77
76
  const allSettings = job.settings
78
- Object.keys(remoteAdapters).forEach(remoteId => {
77
+ Object.entries(remoteAdapters).forEach(([remoteId, adapter]) => {
79
78
  const targetSettings = {
80
79
  ...settings,
81
80
  ...allSettings[remoteId],
82
81
  }
83
82
  if (targetSettings.exportRetention !== 0) {
84
- writers.add(new BackupWriter({ backup: this, remoteId, settings: targetSettings }))
83
+ writers.add(new BackupWriter({ adapter, config, healthCheckSr, job, vmUuid: vm.uuid, remoteId, settings: targetSettings }))
85
84
  }
86
85
  })
87
86
  srs.forEach(sr => {
@@ -90,7 +89,7 @@ class AbstractXapiVmBackupRunner extends Abstract {
90
89
  ...allSettings[sr.uuid],
91
90
  }
92
91
  if (targetSettings.copyRetention !== 0) {
93
- writers.add(new ReplicationWriter({ backup: this, sr, settings: targetSettings }))
92
+ writers.add(new ReplicationWriter({ config, healthCheckSr, job, vmUuid: vm.uuid, sr, settings: targetSettings}))
94
93
  }
95
94
  })
96
95
  }
@@ -99,7 +98,7 @@ class AbstractXapiVmBackupRunner extends Abstract {
99
98
  // ensure the VM itself does not have any backup metadata which would be
100
99
  // copied on manual snapshots and interfere with the backup jobs
101
100
  async _cleanMetadata() {
102
- const { vm } = this
101
+ const vm = this._vm
103
102
  if ('xo:backup:job' in vm.other_config) {
104
103
  await vm.update_other_config({
105
104
  'xo:backup:datetime': null,
@@ -113,7 +112,7 @@ class AbstractXapiVmBackupRunner extends Abstract {
113
112
  }
114
113
 
115
114
  async _snapshot() {
116
- const { vm } = this
115
+ const vm = this._vm
117
116
  const xapi = this._xapi
118
117
 
119
118
  const settings = this._settings
@@ -138,19 +137,19 @@ class AbstractXapiVmBackupRunner extends Abstract {
138
137
  'xo:backup:vm': vm.uuid,
139
138
  })
140
139
 
141
- this.exportedVm = await xapi.getRecord('VM', snapshotRef)
140
+ this._exportedVm = await xapi.getRecord('VM', snapshotRef)
142
141
 
143
- return this.exportedVm.uuid
142
+ return this._exportedVm.uuid
144
143
  })
145
144
  } else {
146
- this.exportedVm = vm
145
+ this._exportedVm = vm
147
146
  this.timestamp = Date.now()
148
147
  }
149
148
  }
150
149
 
151
150
  async _fetchJobSnapshots() {
152
151
  const jobId = this._jobId
153
- const vmRef = this.vm.$ref
152
+ const vmRef = this._vm.$ref
154
153
  const xapi = this._xapi
155
154
 
156
155
  const snapshotsRef = await xapi.getField('VM', vmRef, 'snapshots')
@@ -177,7 +176,7 @@ class AbstractXapiVmBackupRunner extends Abstract {
177
176
  const settings = {
178
177
  ...baseSettings,
179
178
  ...allSettings[scheduleId],
180
- ...allSettings[this.vm.uuid],
179
+ ...allSettings[this._vm.uuid],
181
180
  }
182
181
  return asyncMap(getOldEntries(settings.snapshotRetention, snapshots), ({ $ref }) => {
183
182
  if ($ref !== baseVmRef) {
@@ -224,7 +223,7 @@ class AbstractXapiVmBackupRunner extends Abstract {
224
223
  await this._cleanMetadata()
225
224
  await this._removeUnusedSnapshots()
226
225
 
227
- const { vm } = this
226
+ const vm = this._vm
228
227
  const isRunning = vm.power_state === 'Running'
229
228
  const startAfter = isRunning && (settings.offlineBackup ? 'backup' : settings.offlineSnapshot && 'snapshot')
230
229
  if (startAfter) {
@@ -26,15 +26,17 @@ exports.FullRemoteWriter = class FullRemoteWriter extends MixinRemoteWriter(Abst
26
26
  )
27
27
  }
28
28
 
29
- async _run({ timestamp, sizeContainer, stream }) {
30
- const backup = this._backup
29
+ async _run({ timestamp, sizeContainer, stream, vm, vmSnapshot }) {
31
30
  const settings = this._settings
32
-
33
- const { job, scheduleId, vm } = backup
31
+ const job = this._job
32
+ const scheduleId = this._scheduleId
34
33
 
35
34
  const adapter = this._adapter
36
-
37
- // TODO: clean VM backup directory
35
+ let metadata = await this._isAlreadyTransferred(timestamp)
36
+ if (metadata !== undefined) {
37
+ // @todo : should skip backup while being vigilant to not stuck the forked stream
38
+ Task.info('This backup has already been transfered')
39
+ }
38
40
 
39
41
  const oldBackups = getOldEntries(
40
42
  settings.exportRetention - 1,
@@ -47,14 +49,14 @@ exports.FullRemoteWriter = class FullRemoteWriter extends MixinRemoteWriter(Abst
47
49
  const dataBasename = basename + '.xva'
48
50
  const dataFilename = this._vmBackupDir + '/' + dataBasename
49
51
 
50
- const metadata = {
52
+ metadata = {
51
53
  jobId: job.id,
52
54
  mode: job.mode,
53
55
  scheduleId,
54
56
  timestamp,
55
57
  version: '2.0.0',
56
58
  vm,
57
- vmSnapshot: this._backup.exportedVm,
59
+ vmSnapshot,
58
60
  xva: './' + dataBasename,
59
61
  }
60
62
 
@@ -32,10 +32,11 @@ exports.FullXapiWriter = class FullXapiWriter extends MixinXapiWriter(AbstractFu
32
32
  )
33
33
  }
34
34
 
35
- async _run({ timestamp, sizeContainer, stream }) {
35
+ async _run({ timestamp, sizeContainer, stream, vm }) {
36
36
  const sr = this._sr
37
37
  const settings = this._settings
38
- const { job, scheduleId, vm } = this._backup
38
+ const job = this._job
39
+ const scheduleId = this.scheduleId
39
40
 
40
41
  const { uuid: srUuid, $xapi: xapi } = sr
41
42
 
@@ -26,10 +26,9 @@ const { warn } = createLogger('xo:backups:DeltaBackupWriter')
26
26
  class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWriter) {
27
27
  async checkBaseVdis(baseUuidToSrcVdi) {
28
28
  const { handler } = this._adapter
29
- const backup = this._backup
30
29
  const adapter = this._adapter
31
30
 
32
- const vdisDir = `${this._vmBackupDir}/vdis/${backup.job.id}`
31
+ const vdisDir = `${this._vmBackupDir}/vdis/${this._job.id}`
33
32
 
34
33
  await asyncMap(baseUuidToSrcVdi, async ([baseUuid, srcVdi]) => {
35
34
  let found = false
@@ -91,11 +90,12 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
91
90
  async _prepare() {
92
91
  const adapter = this._adapter
93
92
  const settings = this._settings
94
- const { scheduleId, vm } = this._backup
93
+ const scheduleId = this._scheduleId
94
+ const vmUuid = this._vmUuid
95
95
 
96
96
  const oldEntries = getOldEntries(
97
97
  settings.exportRetention - 1,
98
- await adapter.listVmBackups(vm.uuid, _ => _.mode === 'delta' && _.scheduleId === scheduleId)
98
+ await adapter.listVmBackups(vmUuid, _ => _.mode === 'delta' && _.scheduleId === scheduleId)
99
99
  )
100
100
  this._oldEntries = oldEntries
101
101
 
@@ -134,16 +134,19 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
134
134
  }
135
135
  }
136
136
 
137
- async _transfer($defer, { timestamp, deltaExport }) {
137
+ async _transfer($defer, { differentialVhds, timestamp, deltaExport, vm, vmSnapshot }) {
138
138
  const adapter = this._adapter
139
- const backup = this._backup
140
-
141
- const { job, scheduleId, vm } = backup
139
+ const job = this._job
140
+ const scheduleId = this._scheduleId
142
141
 
143
142
  const jobId = job.id
144
143
  const handler = adapter.handler
145
144
 
146
- // TODO: clean VM backup directory
145
+ let metadataContent = await this._isAlreadyTransferred(timestamp)
146
+ if (metadataContent !== undefined) {
147
+ // @todo : should skip backup while being vigilant to not stuck the forked stream
148
+ Task.info('This backup has already been transfered')
149
+ }
147
150
 
148
151
  const basename = formatFilenameDate(timestamp)
149
152
  const vhds = mapValues(
@@ -158,7 +161,7 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
158
161
  }/${adapter.getVhdFileName(basename)}`
159
162
  )
160
163
 
161
- const metadataContent = {
164
+ metadataContent = {
162
165
  jobId,
163
166
  mode: job.mode,
164
167
  scheduleId,
@@ -169,16 +172,15 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
169
172
  vifs: deltaExport.vifs,
170
173
  vhds,
171
174
  vm,
172
- vmSnapshot: this._backup.exportedVm,
175
+ vmSnapshot,
173
176
  }
174
-
175
177
  const { size } = await Task.run({ name: 'transfer' }, async () => {
176
178
  let transferSize = 0
177
179
  await Promise.all(
178
180
  map(deltaExport.vdis, async (vdi, id) => {
179
181
  const path = `${this._vmBackupDir}/${vhds[id]}`
180
182
 
181
- const isDelta = vdi.other_config['xo:base_delta'] !== undefined
183
+ const isDelta = differentialVhds[`${id}.vhd`]
182
184
  let parentPath
183
185
  if (isDelta) {
184
186
  const vdiDir = dirname(path)
@@ -191,7 +193,11 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
191
193
  .sort()
192
194
  .pop()
193
195
 
194
- assert.notStrictEqual(parentPath, undefined, `missing parent of ${id}`)
196
+ assert.notStrictEqual(
197
+ parentPath,
198
+ undefined,
199
+ `missing parent of ${id} in ${dirname(path)}, looking for ${vdi.other_config['xo:base_delta']}`
200
+ )
195
201
 
196
202
  parentPath = parentPath.slice(1) // remove leading slash
197
203
 
@@ -204,7 +210,7 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
204
210
  // merges and chainings
205
211
  checksum: false,
206
212
  validator: tmpPath => checkVhd(handler, tmpPath),
207
- writeBlockConcurrency: this._backup.config.writeBlockConcurrency,
213
+ writeBlockConcurrency: this._config.writeBlockConcurrency,
208
214
  })
209
215
 
210
216
  if (isDelta) {
@@ -16,7 +16,7 @@ const { listReplicatedVms } = require('./_listReplicatedVms.js')
16
16
  exports.IncrementalXapiWriter = class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
17
17
  async checkBaseVdis(baseUuidToSrcVdi, baseVm) {
18
18
  const sr = this._sr
19
- const replicatedVm = listReplicatedVms(sr.$xapi, this._backup.job.id, sr.uuid, this._backup.vm.uuid).find(
19
+ const replicatedVm = listReplicatedVms(sr.$xapi, this._job.id, sr.uuid, this._vmUuid).find(
20
20
  vm => vm.other_config[TAG_COPY_SRC] === baseVm.uuid
21
21
  )
22
22
  if (replicatedVm === undefined) {
@@ -49,9 +49,10 @@ exports.IncrementalXapiWriter = class IncrementalXapiWriter extends MixinXapiWri
49
49
  type: 'SR',
50
50
  },
51
51
  })
52
+ const hasHealthCheckSr = this._healthCheckSr !== undefined
52
53
  this.transfer = task.wrapFn(this.transfer)
53
- this.cleanup = task.wrapFn(this.cleanup)
54
- this.healthCheck = task.wrapFn(this.healthCheck, true)
54
+ this.cleanup = task.wrapFn(this.cleanup, !hasHealthCheckSr)
55
+ this.healthCheck = task.wrapFn(this.healthCheck, hasHealthCheckSr)
55
56
 
56
57
  return task.run(() => this._prepare())
57
58
  }
@@ -59,12 +60,13 @@ exports.IncrementalXapiWriter = class IncrementalXapiWriter extends MixinXapiWri
59
60
  async _prepare() {
60
61
  const settings = this._settings
61
62
  const { uuid: srUuid, $xapi: xapi } = this._sr
62
- const { scheduleId, vm } = this._backup
63
+ const vmUuid = this._vmUuid
64
+ const scheduleId = this._scheduleId
63
65
 
64
66
  // delete previous interrupted copies
65
- ignoreErrors.call(asyncMapSettled(listReplicatedVms(xapi, scheduleId, undefined, vm.uuid), vm => vm.$destroy))
67
+ ignoreErrors.call(asyncMapSettled(listReplicatedVms(xapi, scheduleId, undefined, vmUuid), vm => vm.$destroy))
66
68
 
67
- this._oldEntries = getOldEntries(settings.copyRetention - 1, listReplicatedVms(xapi, scheduleId, srUuid, vm.uuid))
69
+ this._oldEntries = getOldEntries(settings.copyRetention - 1, listReplicatedVms(xapi, scheduleId, srUuid, vmUuid))
68
70
 
69
71
  if (settings.deleteFirst) {
70
72
  await this._deleteOldEntries()
@@ -81,10 +83,11 @@ exports.IncrementalXapiWriter = class IncrementalXapiWriter extends MixinXapiWri
81
83
  return asyncMapSettled(this._oldEntries, vm => vm.$destroy())
82
84
  }
83
85
 
84
- async _transfer({ timestamp, deltaExport, sizeContainers }) {
86
+ async _transfer({ timestamp, deltaExport, sizeContainers, vm }) {
85
87
  const { _warmMigration } = this._settings
86
88
  const sr = this._sr
87
- const { job, scheduleId, vm } = this._backup
89
+ const job = this._job
90
+ const scheduleId = this._scheduleId
88
91
 
89
92
  const { uuid: srUuid, $xapi: xapi } = sr
90
93
 
@@ -3,9 +3,9 @@
3
3
  const { AbstractWriter } = require('./_AbstractWriter.js')
4
4
 
5
5
  exports.AbstractFullWriter = class AbstractFullWriter extends AbstractWriter {
6
- async run({ timestamp, sizeContainer, stream }) {
6
+ async run({ timestamp, sizeContainer, stream, vm, vmSnapshot }) {
7
7
  try {
8
- return await this._run({ timestamp, sizeContainer, stream })
8
+ return await this._run({ timestamp, sizeContainer, stream, vm, vmSnapshot })
9
9
  } finally {
10
10
  // ensure stream is properly closed
11
11
  stream.destroy()
@@ -15,9 +15,9 @@ exports.AbstractIncrementalWriter = class AbstractIncrementalWriter extends Abst
15
15
  throw new Error('Not implemented')
16
16
  }
17
17
 
18
- async transfer({ timestamp, deltaExport, sizeContainers }) {
18
+ async transfer({ deltaExport, ...other }) {
19
19
  try {
20
- return await this._transfer({ timestamp, deltaExport, sizeContainers })
20
+ return await this._transfer({ deltaExport, ...other })
21
21
  } finally {
22
22
  // ensure all streams are properly closed
23
23
  for (const stream of Object.values(deltaExport.streams)) {
@@ -1,9 +1,16 @@
1
1
  'use strict'
2
2
 
3
+ const { formatFilenameDate } = require('../../_filenameDate')
4
+ const { getVmBackupDir } = require('../../_getVmBackupDir')
5
+
3
6
  exports.AbstractWriter = class AbstractWriter {
4
- constructor({ backup, settings }) {
5
- this._backup = backup
7
+ constructor({ config, healthCheckSr, job, vmUuid, scheduleId, settings }) {
8
+ this._config = config
9
+ this._healthCheckSr = healthCheckSr
10
+ this._job = job
11
+ this._scheduleId = scheduleId
6
12
  this._settings = settings
13
+ this._vmUuid = vmUuid
7
14
  }
8
15
 
9
16
  beforeBackup() {}
@@ -11,4 +18,14 @@ exports.AbstractWriter = class AbstractWriter {
11
18
  afterBackup() {}
12
19
 
13
20
  healthCheck(sr) {}
21
+
22
+ _isAlreadyTransferred(timestamp) {
23
+ const vmUuid = this._vmUuid
24
+ const adapter = this._adapter
25
+ const backupDir = getVmBackupDir(vmUuid)
26
+ try {
27
+ const actualMetadata = JSON.parse(adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`))
28
+ return actualMetadata
29
+ } catch (error) {}
30
+ }
14
31
  }
@@ -17,13 +17,13 @@ exports.MixinRemoteWriter = (BaseClass = Object) =>
17
17
  class MixinRemoteWriter extends BaseClass {
18
18
  #lock
19
19
 
20
- constructor({ remoteId, ...rest }) {
20
+ constructor({ remoteId, adapter, ...rest }) {
21
21
  super(rest)
22
22
 
23
- this._adapter = rest.backup.remoteAdapters[remoteId]
23
+ this._adapter = adapter
24
24
  this._remoteId = remoteId
25
25
 
26
- this._vmBackupDir = getVmBackupDir(this._backup.vm.uuid)
26
+ this._vmBackupDir = getVmBackupDir(rest.vmUuid)
27
27
  }
28
28
 
29
29
  async _cleanVm(options) {
@@ -38,7 +38,7 @@ exports.MixinRemoteWriter = (BaseClass = Object) =>
38
38
  Task.warning(message, data)
39
39
  },
40
40
  lock: false,
41
- mergeBlockConcurrency: this._backup.config.mergeBlockConcurrency,
41
+ mergeBlockConcurrency: this._config.mergeBlockConcurrency,
42
42
  })
43
43
  })
44
44
  } catch (error) {
@@ -55,7 +55,7 @@ exports.MixinRemoteWriter = (BaseClass = Object) =>
55
55
  }
56
56
 
57
57
  async afterBackup() {
58
- const { disableMergeWorker } = this._backup.config
58
+ const { disableMergeWorker } = this._config
59
59
  // merge worker only compatible with local remotes
60
60
  const { handler } = this._adapter
61
61
  const willMergeInWorker = !disableMergeWorker && typeof handler.getRealPath === 'function'
@@ -76,7 +76,9 @@ exports.MixinRemoteWriter = (BaseClass = Object) =>
76
76
  }
77
77
  }
78
78
 
79
- healthCheck(sr) {
79
+ healthCheck() {
80
+ const sr = this._healthCheckSr
81
+ assert.notStrictEqual(sr, undefined, 'SR should be defined before making a health check')
80
82
  assert.notStrictEqual(
81
83
  this._metadataFileName,
82
84
  undefined,
@@ -109,4 +111,16 @@ exports.MixinRemoteWriter = (BaseClass = Object) =>
109
111
  }
110
112
  )
111
113
  }
114
+
115
+ _isAlreadyTransferred(timestamp) {
116
+ const vmUuid = this._vmUuid
117
+ const adapter = this._adapter
118
+ const backupDir = getVmBackupDir(vmUuid)
119
+ try {
120
+ const actualMetadata = JSON.parse(
121
+ adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
122
+ )
123
+ return actualMetadata
124
+ } catch (error) {}
125
+ }
112
126
  }
@@ -14,7 +14,9 @@ exports.MixinXapiWriter = (BaseClass = Object) =>
14
14
  this._sr = sr
15
15
  }
16
16
 
17
- healthCheck(sr) {
17
+ healthCheck() {
18
+ const sr = this._healthCheckSr
19
+ assert.notStrictEqual(sr, undefined, 'SR should be defined before making a health check')
18
20
  assert.notEqual(this._targetVmRef, undefined, 'A vm should have been transfered to be health checked')
19
21
  // copy VM
20
22
  return Task.run(
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.37.0",
11
+ "version": "0.38.0",
12
12
  "engines": {
13
13
  "node": ">=14.6"
14
14
  },
@@ -42,7 +42,7 @@
42
42
  "promise-toolbox": "^0.21.0",
43
43
  "proper-lockfile": "^4.1.2",
44
44
  "uuid": "^9.0.0",
45
- "vhd-lib": "^4.4.1",
45
+ "vhd-lib": "^4.5.0",
46
46
  "yazl": "^2.5.1"
47
47
  },
48
48
  "devDependencies": {