@xen-orchestra/backups 0.36.1 → 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.
Files changed (40) hide show
  1. package/Backup.js +14 -302
  2. package/ImportVmBackup.js +6 -6
  3. package/RemoteAdapter.js +20 -13
  4. package/RestoreMetadataBackup.js +1 -1
  5. package/_backupWorker.js +2 -2
  6. package/{_deltaVm.js → _incrementalVm.js} +11 -11
  7. package/_runners/Metadata.js +134 -0
  8. package/_runners/VmsRemote.js +98 -0
  9. package/_runners/VmsXapi.js +138 -0
  10. package/_runners/_Abstract.js +51 -0
  11. package/{_PoolMetadataBackup.js → _runners/_PoolMetadataBackup.js} +3 -3
  12. package/_runners/_RemoteTimeoutError.js +8 -0
  13. package/{_XoMetadataBackup.js → _runners/_XoMetadataBackup.js} +3 -3
  14. package/_runners/_getAdaptersByRemote.js +9 -0
  15. package/_runners/_runTask.js +6 -0
  16. package/_runners/_vmRunners/FullRemote.js +53 -0
  17. package/_runners/_vmRunners/FullXapi.js +65 -0
  18. package/_runners/_vmRunners/IncrementalRemote.js +67 -0
  19. package/_runners/_vmRunners/IncrementalXapi.js +175 -0
  20. package/_runners/_vmRunners/_Abstract.js +95 -0
  21. package/_runners/_vmRunners/_AbstractRemote.js +86 -0
  22. package/_runners/_vmRunners/_AbstractXapi.js +257 -0
  23. package/_runners/_vmRunners/_forkDeltaExport.js +12 -0
  24. package/{writers/FullBackupWriter.js → _runners/_writers/FullRemoteWriter.js} +15 -13
  25. package/{writers/FullReplicationWriter.js → _runners/_writers/FullXapiWriter.js} +8 -7
  26. package/{writers/DeltaBackupWriter.js → _runners/_writers/IncrementalRemoteWriter.js} +28 -22
  27. package/{writers/DeltaReplicationWriter.js → _runners/_writers/IncrementalXapiWriter.js} +19 -16
  28. package/{writers → _runners/_writers}/_AbstractFullWriter.js +2 -2
  29. package/{writers/_AbstractDeltaWriter.js → _runners/_writers/_AbstractIncrementalWriter.js} +3 -3
  30. package/_runners/_writers/_AbstractWriter.js +31 -0
  31. package/{writers/_MixinBackupWriter.js → _runners/_writers/_MixinRemoteWriter.js} +30 -16
  32. package/{writers/_MixinReplicationWriter.js → _runners/_writers/_MixinXapiWriter.js} +9 -13
  33. package/package.json +5 -5
  34. package/_VmBackup.js +0 -515
  35. package/writers/_AbstractWriter.js +0 -14
  36. /package/{_createStreamThrottle.js → _runners/_createStreamThrottle.js} +0 -0
  37. /package/{_forkStreamUnpipe.js → _runners/_forkStreamUnpipe.js} +0 -0
  38. /package/{writers → _runners/_writers}/_checkVhd.js +0 -0
  39. /package/{writers → _runners/_writers}/_listReplicatedVms.js +0 -0
  40. /package/{writers → _runners/_writers}/_packUuid.js +0 -0
@@ -0,0 +1,175 @@
1
+ 'use strict'
2
+
3
+ const findLast = require('lodash/findLast.js')
4
+ const keyBy = require('lodash/keyBy.js')
5
+ const mapValues = require('lodash/mapValues.js')
6
+ const vhdStreamValidator = require('vhd-lib/vhdStreamValidator.js')
7
+ const { asyncMap } = require('@xen-orchestra/async-map')
8
+ const { createLogger } = require('@xen-orchestra/log')
9
+ const { pipeline } = require('node:stream')
10
+
11
+ const { IncrementalRemoteWriter } = require('../_writers/IncrementalRemoteWriter.js')
12
+ const { IncrementalXapiWriter } = require('../_writers/IncrementalXapiWriter.js')
13
+ const { exportIncrementalVm } = require('../../_incrementalVm.js')
14
+ const { Task } = require('../../Task.js')
15
+ const { watchStreamSize } = require('../../_watchStreamSize.js')
16
+ const { AbstractXapi } = require('./_AbstractXapi.js')
17
+ const { forkDeltaExport } = require('./_forkDeltaExport.js')
18
+ const isVhdDifferencingDisk = require('vhd-lib/isVhdDifferencingDisk')
19
+ const { asyncEach } = require('@vates/async-each')
20
+
21
+ const { debug } = createLogger('xo:backups:IncrementalXapiVmBackup')
22
+
23
+ const noop = Function.prototype
24
+
25
+ exports.IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXapi {
26
+ _getWriters() {
27
+ return [IncrementalRemoteWriter, IncrementalXapiWriter]
28
+ }
29
+
30
+ _mustDoSnapshot() {
31
+ return true
32
+ }
33
+
34
+ async _copy() {
35
+ const baseVm = this._baseVm
36
+ const vm = this._vm
37
+ const exportedVm = this._exportedVm
38
+ const fullVdisRequired = this._fullVdisRequired
39
+
40
+ const isFull = fullVdisRequired === undefined || fullVdisRequired.size !== 0
41
+
42
+ await this._callWriters(writer => writer.prepare({ isFull }), 'writer.prepare()')
43
+
44
+ const deltaExport = await exportIncrementalVm(exportedVm, baseVm, {
45
+ fullVdisRequired,
46
+ })
47
+ // since NBD is network based, if one disk use nbd , all the disk use them
48
+ // except the suspended VDI
49
+ if (Object.values(deltaExport.streams).some(({ _nbd }) => _nbd)) {
50
+ Task.info('Transfer data using NBD')
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
+ })
59
+ const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
60
+
61
+ if (this._settings.validateVhdStreams) {
62
+ deltaExport.streams = mapValues(deltaExport.streams, stream => pipeline(stream, vhdStreamValidator, noop))
63
+ }
64
+ deltaExport.streams = mapValues(deltaExport.streams, this._throttleStream)
65
+
66
+ const timestamp = Date.now()
67
+
68
+ await this._callWriters(
69
+ writer =>
70
+ writer.transfer({
71
+ deltaExport: forkDeltaExport(deltaExport),
72
+ differentialVhds,
73
+ sizeContainers,
74
+ timestamp,
75
+ vm,
76
+ vmSnapshot: exportedVm,
77
+ }),
78
+ 'writer.transfer()'
79
+ )
80
+
81
+ this._baseVm = exportedVm
82
+
83
+ if (baseVm !== undefined) {
84
+ await exportedVm.update_other_config(
85
+ 'xo:backup:deltaChainLength',
86
+ String(+(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1)
87
+ )
88
+ }
89
+
90
+ // not the case if offlineBackup
91
+ if (exportedVm.is_a_snapshot) {
92
+ await exportedVm.update_other_config('xo:backup:exported', 'true')
93
+ }
94
+
95
+ const size = Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0)
96
+ const end = Date.now()
97
+ const duration = end - timestamp
98
+ debug('transfer complete', {
99
+ duration,
100
+ speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
101
+ size,
102
+ })
103
+
104
+ await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
105
+ }
106
+
107
+ async _selectBaseVm() {
108
+ const xapi = this._xapi
109
+
110
+ let baseVm = findLast(this._jobSnapshots, _ => 'xo:backup:exported' in _.other_config)
111
+ if (baseVm === undefined) {
112
+ debug('no base VM found')
113
+ return
114
+ }
115
+
116
+ const fullInterval = this._settings.fullInterval
117
+ const deltaChainLength = +(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1
118
+ if (!(fullInterval === 0 || fullInterval > deltaChainLength)) {
119
+ debug('not using base VM becaust fullInterval reached')
120
+ return
121
+ }
122
+
123
+ const srcVdis = keyBy(await xapi.getRecords('VDI', await this._vm.$getDisks()), '$ref')
124
+
125
+ // resolve full record
126
+ baseVm = await xapi.getRecord('VM', baseVm.$ref)
127
+
128
+ const baseUuidToSrcVdi = new Map()
129
+ await asyncMap(await baseVm.$getDisks(), async baseRef => {
130
+ const [baseUuid, snapshotOf] = await Promise.all([
131
+ xapi.getField('VDI', baseRef, 'uuid'),
132
+ xapi.getField('VDI', baseRef, 'snapshot_of'),
133
+ ])
134
+ const srcVdi = srcVdis[snapshotOf]
135
+ if (srcVdi !== undefined) {
136
+ baseUuidToSrcVdi.set(baseUuid, srcVdi)
137
+ } else {
138
+ debug('ignore snapshot VDI because no longer present on VM', {
139
+ vdi: baseUuid,
140
+ })
141
+ }
142
+ })
143
+
144
+ const presentBaseVdis = new Map(baseUuidToSrcVdi)
145
+ await this._callWriters(
146
+ writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis, baseVm),
147
+ 'writer.checkBaseVdis()',
148
+ false
149
+ )
150
+
151
+ if (presentBaseVdis.size === 0) {
152
+ debug('no base VM found')
153
+ return
154
+ }
155
+
156
+ const fullVdisRequired = new Set()
157
+ baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
158
+ if (presentBaseVdis.has(baseUuid)) {
159
+ debug('found base VDI', {
160
+ base: baseUuid,
161
+ vdi: srcVdi.uuid,
162
+ })
163
+ } else {
164
+ debug('missing base VDI', {
165
+ base: baseUuid,
166
+ vdi: srcVdi.uuid,
167
+ })
168
+ fullVdisRequired.add(srcVdi.uuid)
169
+ }
170
+ })
171
+
172
+ this._baseVm = baseVm
173
+ this._fullVdisRequired = fullVdisRequired
174
+ }
175
+ }
@@ -0,0 +1,95 @@
1
+ 'use strict'
2
+
3
+ const { asyncMap } = require('@xen-orchestra/async-map')
4
+ const { createLogger } = require('@xen-orchestra/log')
5
+ const { Task } = require('../../Task.js')
6
+
7
+ const { debug, warn } = createLogger('xo:backups:AbstractVmRunner')
8
+
9
+ class AggregateError extends Error {
10
+ constructor(errors, message) {
11
+ super(message)
12
+ this.errors = errors
13
+ }
14
+ }
15
+
16
+ const asyncEach = async (iterable, fn, thisArg = iterable) => {
17
+ for (const item of iterable) {
18
+ await fn.call(thisArg, item)
19
+ }
20
+ }
21
+
22
+ exports.Abstract = class AbstractVmBackupRunner {
23
+ // calls fn for each function, warns of any errors, and throws only if there are no writers left
24
+ async _callWriters(fn, step, parallel = true) {
25
+ const writers = this._writers
26
+ const n = writers.size
27
+ if (n === 0) {
28
+ return
29
+ }
30
+
31
+ async function callWriter(writer) {
32
+ const { name } = writer.constructor
33
+ try {
34
+ debug('writer step starting', { step, writer: name })
35
+ await fn(writer)
36
+ debug('writer step succeeded', { duration: step, writer: name })
37
+ } catch (error) {
38
+ writers.delete(writer)
39
+
40
+ warn('writer step failed', { error, step, writer: name })
41
+
42
+ // these two steps are the only one that are not already in their own sub tasks
43
+ if (step === 'writer.checkBaseVdis()' || step === 'writer.beforeBackup()') {
44
+ Task.warning(
45
+ `the writer ${name} has failed the step ${step} with error ${error.message}. It won't be used anymore in this job execution.`
46
+ )
47
+ }
48
+
49
+ throw error
50
+ }
51
+ }
52
+ if (n === 1) {
53
+ const [writer] = writers
54
+ return callWriter(writer)
55
+ }
56
+
57
+ const errors = []
58
+ await (parallel ? asyncMap : asyncEach)(writers, async function (writer) {
59
+ try {
60
+ await callWriter(writer)
61
+ } catch (error) {
62
+ errors.push(error)
63
+ }
64
+ })
65
+ if (writers.size === 0) {
66
+ throw new AggregateError(errors, 'all targets have failed, step: ' + step)
67
+ }
68
+ }
69
+
70
+ async _healthCheck() {
71
+ const settings = this._settings
72
+
73
+ if (this._healthCheckSr === undefined) {
74
+ return
75
+ }
76
+
77
+ // check if current VM has tags
78
+ const tags = this._tags
79
+ const intersect = settings.healthCheckVmsWithTags.some(t => tags.includes(t))
80
+
81
+ if (settings.healthCheckVmsWithTags.length !== 0 && !intersect) {
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
+ )
91
+ }
92
+
93
+ await this._callWriters(writer => writer.healthCheck(), 'writer.healthCheck()')
94
+ }
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
+ }
@@ -0,0 +1,257 @@
1
+ 'use strict'
2
+
3
+ const assert = require('assert')
4
+ const groupBy = require('lodash/groupBy.js')
5
+ const ignoreErrors = require('promise-toolbox/ignoreErrors')
6
+ const { asyncMap } = require('@xen-orchestra/async-map')
7
+ const { decorateMethodsWith } = require('@vates/decorate-with')
8
+ const { defer } = require('golike-defer')
9
+ const { formatDateTime } = require('@xen-orchestra/xapi')
10
+
11
+ const { getOldEntries } = require('../../_getOldEntries.js')
12
+ const { Task } = require('../../Task.js')
13
+ const { Abstract } = require('./_Abstract.js')
14
+
15
+ class AbstractXapiVmBackupRunner extends Abstract {
16
+ constructor({
17
+ config,
18
+ getSnapshotNameLabel,
19
+ healthCheckSr,
20
+ job,
21
+ remoteAdapters,
22
+ remotes,
23
+ schedule,
24
+ settings,
25
+ srs,
26
+ throttleStream,
27
+ vm,
28
+ }) {
29
+ super()
30
+ if (vm.other_config['xo:backup:job'] === job.id && 'start' in vm.blocked_operations) {
31
+ // don't match replicated VMs created by this very job otherwise they
32
+ // will be replicated again and again
33
+ throw new Error('cannot backup a VM created by this very job')
34
+ }
35
+
36
+ this.config = config
37
+ this.job = job
38
+ this.remoteAdapters = remoteAdapters
39
+ this.scheduleId = schedule.id
40
+ this.timestamp = undefined
41
+
42
+ // VM currently backed up
43
+ const tags = (this._tags = vm.tags)
44
+
45
+ // VM (snapshot) that is really exported
46
+ this._exportedVm = undefined
47
+ this._vm = vm
48
+
49
+ this._fullVdisRequired = undefined
50
+ this._getSnapshotNameLabel = getSnapshotNameLabel
51
+ this._isIncremental = job.mode === 'delta'
52
+ this._healthCheckSr = healthCheckSr
53
+ this._jobId = job.id
54
+ this._jobSnapshots = undefined
55
+ this._throttleStream = throttleStream
56
+ this._xapi = vm.$xapi
57
+
58
+ // Base VM for the export
59
+ this._baseVm = undefined
60
+
61
+ // Settings for this specific run (job, schedule, VM)
62
+ if (tags.includes('xo-memory-backup')) {
63
+ settings.checkpointSnapshot = true
64
+ }
65
+ if (tags.includes('xo-offline-backup')) {
66
+ settings.offlineSnapshot = true
67
+ }
68
+ this._settings = settings
69
+ // Create writers
70
+ {
71
+ const writers = new Set()
72
+ this._writers = writers
73
+
74
+ const [BackupWriter, ReplicationWriter] = this._getWriters()
75
+
76
+ const allSettings = job.settings
77
+ Object.entries(remoteAdapters).forEach(([remoteId, adapter]) => {
78
+ const targetSettings = {
79
+ ...settings,
80
+ ...allSettings[remoteId],
81
+ }
82
+ if (targetSettings.exportRetention !== 0) {
83
+ writers.add(new BackupWriter({ adapter, config, healthCheckSr, job, vmUuid: vm.uuid, remoteId, settings: targetSettings }))
84
+ }
85
+ })
86
+ srs.forEach(sr => {
87
+ const targetSettings = {
88
+ ...settings,
89
+ ...allSettings[sr.uuid],
90
+ }
91
+ if (targetSettings.copyRetention !== 0) {
92
+ writers.add(new ReplicationWriter({ config, healthCheckSr, job, vmUuid: vm.uuid, sr, settings: targetSettings}))
93
+ }
94
+ })
95
+ }
96
+ }
97
+
98
+ // ensure the VM itself does not have any backup metadata which would be
99
+ // copied on manual snapshots and interfere with the backup jobs
100
+ async _cleanMetadata() {
101
+ const vm = this._vm
102
+ if ('xo:backup:job' in vm.other_config) {
103
+ await vm.update_other_config({
104
+ 'xo:backup:datetime': null,
105
+ 'xo:backup:deltaChainLength': null,
106
+ 'xo:backup:exported': null,
107
+ 'xo:backup:job': null,
108
+ 'xo:backup:schedule': null,
109
+ 'xo:backup:vm': null,
110
+ })
111
+ }
112
+ }
113
+
114
+ async _snapshot() {
115
+ const vm = this._vm
116
+ const xapi = this._xapi
117
+
118
+ const settings = this._settings
119
+
120
+ if (this._mustDoSnapshot()) {
121
+ await Task.run({ name: 'snapshot' }, async () => {
122
+ if (!settings.bypassVdiChainsCheck) {
123
+ await vm.$assertHealthyVdiChains()
124
+ }
125
+
126
+ const snapshotRef = await vm[settings.checkpointSnapshot ? '$checkpoint' : '$snapshot']({
127
+ ignoreNobakVdis: true,
128
+ name_label: this._getSnapshotNameLabel(vm),
129
+ unplugVusbs: true,
130
+ })
131
+ this.timestamp = Date.now()
132
+
133
+ await xapi.setFieldEntries('VM', snapshotRef, 'other_config', {
134
+ 'xo:backup:datetime': formatDateTime(this.timestamp),
135
+ 'xo:backup:job': this._jobId,
136
+ 'xo:backup:schedule': this.scheduleId,
137
+ 'xo:backup:vm': vm.uuid,
138
+ })
139
+
140
+ this._exportedVm = await xapi.getRecord('VM', snapshotRef)
141
+
142
+ return this._exportedVm.uuid
143
+ })
144
+ } else {
145
+ this._exportedVm = vm
146
+ this.timestamp = Date.now()
147
+ }
148
+ }
149
+
150
+ async _fetchJobSnapshots() {
151
+ const jobId = this._jobId
152
+ const vmRef = this._vm.$ref
153
+ const xapi = this._xapi
154
+
155
+ const snapshotsRef = await xapi.getField('VM', vmRef, 'snapshots')
156
+ const snapshotsOtherConfig = await asyncMap(snapshotsRef, ref => xapi.getField('VM', ref, 'other_config'))
157
+
158
+ const snapshots = []
159
+ snapshotsOtherConfig.forEach((other_config, i) => {
160
+ if (other_config['xo:backup:job'] === jobId) {
161
+ snapshots.push({ other_config, $ref: snapshotsRef[i] })
162
+ }
163
+ })
164
+ snapshots.sort((a, b) => (a.other_config['xo:backup:datetime'] < b.other_config['xo:backup:datetime'] ? -1 : 1))
165
+ this._jobSnapshots = snapshots
166
+ }
167
+
168
+ async _removeUnusedSnapshots() {
169
+ const allSettings = this.job.settings
170
+ const baseSettings = this._baseSettings
171
+ const baseVmRef = this._baseVm?.$ref
172
+
173
+ const snapshotsPerSchedule = groupBy(this._jobSnapshots, _ => _.other_config['xo:backup:schedule'])
174
+ const xapi = this._xapi
175
+ await asyncMap(Object.entries(snapshotsPerSchedule), ([scheduleId, snapshots]) => {
176
+ const settings = {
177
+ ...baseSettings,
178
+ ...allSettings[scheduleId],
179
+ ...allSettings[this._vm.uuid],
180
+ }
181
+ return asyncMap(getOldEntries(settings.snapshotRetention, snapshots), ({ $ref }) => {
182
+ if ($ref !== baseVmRef) {
183
+ return xapi.VM_destroy($ref)
184
+ }
185
+ })
186
+ })
187
+ }
188
+
189
+ async copy() {
190
+ throw new Error('Not implemented')
191
+ }
192
+
193
+ _getWriters() {
194
+ throw new Error('Not implemented')
195
+ }
196
+
197
+ _mustDoSnapshot() {
198
+ throw new Error('Not implemented')
199
+ }
200
+
201
+ async _selectBaseVm() {
202
+ throw new Error('Not implemented')
203
+ }
204
+
205
+ async run($defer) {
206
+ const settings = this._settings
207
+ assert(
208
+ !settings.offlineBackup || settings.snapshotRetention === 0,
209
+ 'offlineBackup is not compatible with snapshotRetention'
210
+ )
211
+
212
+ await this._callWriters(async writer => {
213
+ await writer.beforeBackup()
214
+ $defer(async () => {
215
+ await writer.afterBackup()
216
+ })
217
+ }, 'writer.beforeBackup()')
218
+
219
+ await this._fetchJobSnapshots()
220
+
221
+ await this._selectBaseVm()
222
+
223
+ await this._cleanMetadata()
224
+ await this._removeUnusedSnapshots()
225
+
226
+ const vm = this._vm
227
+ const isRunning = vm.power_state === 'Running'
228
+ const startAfter = isRunning && (settings.offlineBackup ? 'backup' : settings.offlineSnapshot && 'snapshot')
229
+ if (startAfter) {
230
+ await vm.$callAsync('clean_shutdown')
231
+ }
232
+
233
+ try {
234
+ await this._snapshot()
235
+ if (startAfter === 'snapshot') {
236
+ ignoreErrors.call(vm.$callAsync('start', false, false))
237
+ }
238
+
239
+ if (this._writers.size !== 0) {
240
+ await this._copy()
241
+ }
242
+ } finally {
243
+ if (startAfter) {
244
+ ignoreErrors.call(vm.$callAsync('start', false, false))
245
+ }
246
+
247
+ await this._fetchJobSnapshots()
248
+ await this._removeUnusedSnapshots()
249
+ }
250
+ await this._healthCheck()
251
+ }
252
+ }
253
+ exports.AbstractXapi = AbstractXapiVmBackupRunner
254
+
255
+ decorateMethodsWith(AbstractXapiVmBackupRunner, {
256
+ run: defer,
257
+ })
@@ -0,0 +1,12 @@
1
+ 'use strict'
2
+
3
+ const { mapValues } = require('lodash')
4
+ const { forkStreamUnpipe } = require('../_forkStreamUnpipe')
5
+
6
+ exports.forkDeltaExport = function forkDeltaExport(deltaExport) {
7
+ return Object.create(deltaExport, {
8
+ streams: {
9
+ value: mapValues(deltaExport.streams, forkStreamUnpipe),
10
+ },
11
+ })
12
+ }
@@ -1,13 +1,13 @@
1
1
  'use strict'
2
2
 
3
- const { formatFilenameDate } = require('../_filenameDate.js')
4
- const { getOldEntries } = require('../_getOldEntries.js')
5
- const { Task } = require('../Task.js')
3
+ const { formatFilenameDate } = require('../../_filenameDate.js')
4
+ const { getOldEntries } = require('../../_getOldEntries.js')
5
+ const { Task } = require('../../Task.js')
6
6
 
7
- const { MixinBackupWriter } = require('./_MixinBackupWriter.js')
7
+ const { MixinRemoteWriter } = require('./_MixinRemoteWriter.js')
8
8
  const { AbstractFullWriter } = require('./_AbstractFullWriter.js')
9
9
 
10
- exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(AbstractFullWriter) {
10
+ exports.FullRemoteWriter = class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
11
11
  constructor(props) {
12
12
  super(props)
13
13
 
@@ -26,15 +26,17 @@ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(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.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(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