@xen-orchestra/backups 0.14.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 (43) hide show
  1. package/Backup.js +263 -0
  2. package/DurablePartition.js +40 -0
  3. package/ImportVmBackup.js +66 -0
  4. package/README.md +28 -0
  5. package/RemoteAdapter.js +552 -0
  6. package/RestoreMetadataBackup.js +24 -0
  7. package/Task.js +151 -0
  8. package/_PoolMetadataBackup.js +75 -0
  9. package/_VmBackup.js +409 -0
  10. package/_XoMetadataBackup.js +62 -0
  11. package/_backupType.js +4 -0
  12. package/_backupWorker.js +155 -0
  13. package/_cancelableMap.js +20 -0
  14. package/_cleanVm.js +378 -0
  15. package/_deltaVm.js +347 -0
  16. package/_extractIdsFromSimplePattern.js +29 -0
  17. package/_filenameDate.js +6 -0
  18. package/_forkStreamUnpipe.js +28 -0
  19. package/_getOldEntries.js +4 -0
  20. package/_getTmpDir.js +20 -0
  21. package/_getVmBackupDir.js +6 -0
  22. package/_isValidXva.js +60 -0
  23. package/_listPartitions.js +52 -0
  24. package/_lvm.js +31 -0
  25. package/_watchStreamSize.js +7 -0
  26. package/formatVmBackups.js +34 -0
  27. package/merge-worker/cli.js +69 -0
  28. package/merge-worker/index.js +25 -0
  29. package/package.json +49 -0
  30. package/parseMetadataBackupId.js +23 -0
  31. package/runBackupWorker.js +38 -0
  32. package/writers/DeltaBackupWriter.js +221 -0
  33. package/writers/DeltaReplicationWriter.js +126 -0
  34. package/writers/FullBackupWriter.js +85 -0
  35. package/writers/FullReplicationWriter.js +88 -0
  36. package/writers/_AbstractDeltaWriter.js +26 -0
  37. package/writers/_AbstractFullWriter.js +12 -0
  38. package/writers/_AbstractWriter.js +10 -0
  39. package/writers/_MixinBackupWriter.js +51 -0
  40. package/writers/_MixinReplicationWriter.js +8 -0
  41. package/writers/_checkVhd.js +5 -0
  42. package/writers/_listReplicatedVms.js +30 -0
  43. package/writers/_packUuid.js +5 -0
package/Task.js ADDED
@@ -0,0 +1,151 @@
1
+ const CancelToken = require('promise-toolbox/CancelToken.js')
2
+ const Zone = require('node-zone')
3
+
4
+ const logAfterEnd = () => {
5
+ throw new Error('task has already ended')
6
+ }
7
+
8
+ const noop = Function.prototype
9
+
10
+ // Create a serializable object from an error.
11
+ //
12
+ // Otherwise some fields might be non-enumerable and missing from logs.
13
+ const serializeError = error =>
14
+ error instanceof Error
15
+ ? {
16
+ ...error, // Copy enumerable properties.
17
+ code: error.code,
18
+ message: error.message,
19
+ name: error.name,
20
+ stack: error.stack,
21
+ }
22
+ : error
23
+
24
+ const $$task = Symbol('@xen-orchestra/backups/Task')
25
+
26
+ class Task {
27
+ static get cancelToken() {
28
+ const task = Zone.current.data[$$task]
29
+ return task !== undefined ? task.#cancelToken : CancelToken.none
30
+ }
31
+
32
+ static run(opts, fn) {
33
+ return new this(opts).run(fn, true)
34
+ }
35
+
36
+ static wrapFn(opts, fn) {
37
+ // compatibility with @decorateWith
38
+ if (typeof fn !== 'function') {
39
+ ;[fn, opts] = [opts, fn]
40
+ }
41
+
42
+ return function () {
43
+ return Task.run(typeof opts === 'function' ? opts.apply(this, arguments) : opts, () => fn.apply(this, arguments))
44
+ }
45
+ }
46
+
47
+ #cancelToken
48
+ #id = Math.random().toString(36).slice(2)
49
+ #onLog
50
+ #zone
51
+
52
+ constructor({ name, data, onLog }) {
53
+ let parentCancelToken, parentId
54
+ if (onLog === undefined) {
55
+ const parent = Zone.current.data[$$task]
56
+ if (parent === undefined) {
57
+ onLog = noop
58
+ } else {
59
+ onLog = log => parent.#onLog(log)
60
+ parentCancelToken = parent.#cancelToken
61
+ parentId = parent.#id
62
+ }
63
+ }
64
+
65
+ const zone = Zone.current.fork('@xen-orchestra/backups/Task')
66
+ zone.data[$$task] = this
67
+ this.#zone = zone
68
+
69
+ const { cancel, token } = CancelToken.source(parentCancelToken && [parentCancelToken])
70
+ this.#cancelToken = token
71
+ this.cancel = cancel
72
+
73
+ this.#onLog = onLog
74
+
75
+ this.#log('start', {
76
+ data,
77
+ message: name,
78
+ parentId,
79
+ })
80
+ }
81
+
82
+ failure(error) {
83
+ this.#end('failure', serializeError(error))
84
+ }
85
+
86
+ info(message, data) {
87
+ this.#log('info', { data, message })
88
+ }
89
+
90
+ /**
91
+ * Run a function in the context of this task
92
+ *
93
+ * In case of error, the task will be failed.
94
+ *
95
+ * @typedef Result
96
+ * @param {() => Result)} fn
97
+ * @param {boolean} last - Whether the task should succeed if there is no error
98
+ * @returns Result
99
+ */
100
+ run(fn, last = false) {
101
+ return this.#zone.run(() => {
102
+ try {
103
+ const result = fn()
104
+ let then
105
+ if (result != null && typeof (then = result.then) === 'function') {
106
+ then.call(result, last && (value => this.success(value)), error => this.failure(error))
107
+ } else if (last) {
108
+ this.success(result)
109
+ }
110
+ return result
111
+ } catch (error) {
112
+ this.failure(error)
113
+ throw error
114
+ }
115
+ })
116
+ }
117
+
118
+ success(value) {
119
+ this.#end('success', value)
120
+ }
121
+
122
+ warning(message, data) {
123
+ this.#log('warning', { data, message })
124
+ }
125
+
126
+ wrapFn(fn, last) {
127
+ const task = this
128
+ return function () {
129
+ return task.run(() => fn.apply(this, arguments), last)
130
+ }
131
+ }
132
+
133
+ #end(status, result) {
134
+ this.#log('end', { result, status })
135
+ this.#onLog = logAfterEnd
136
+ }
137
+
138
+ #log(event, props) {
139
+ this.#onLog({
140
+ ...props,
141
+ event,
142
+ taskId: this.#id,
143
+ timestamp: Date.now(),
144
+ })
145
+ }
146
+ }
147
+ exports.Task = Task
148
+
149
+ for (const method of ['info', 'warning']) {
150
+ Task[method] = (...args) => Zone.current.data[$$task]?.[method](...args)
151
+ }
@@ -0,0 +1,75 @@
1
+ const { asyncMap } = require('@xen-orchestra/async-map')
2
+
3
+ const { DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter.js')
4
+ const { forkStreamUnpipe } = require('./_forkStreamUnpipe.js')
5
+ const { formatFilenameDate } = require('./_filenameDate.js')
6
+ const { Task } = require('./Task.js')
7
+
8
+ const PATH_DB_DUMP = '/pool/xmldbdump'
9
+ exports.PATH_DB_DUMP = PATH_DB_DUMP
10
+
11
+ exports.PoolMetadataBackup = class PoolMetadataBackup {
12
+ constructor({ config, job, pool, remoteAdapters, schedule, settings }) {
13
+ this._config = config
14
+ this._job = job
15
+ this._pool = pool
16
+ this._remoteAdapters = remoteAdapters
17
+ this._schedule = schedule
18
+ this._settings = settings
19
+ }
20
+
21
+ _exportPoolMetadata() {
22
+ const xapi = this._pool.$xapi
23
+ return xapi.getResource(PATH_DB_DUMP, {
24
+ task: xapi.task_create('Export pool metadata'),
25
+ })
26
+ }
27
+
28
+ async run() {
29
+ const timestamp = Date.now()
30
+
31
+ const { _job: job, _schedule: schedule, _pool: pool } = this
32
+ const poolDir = `${DIR_XO_POOL_METADATA_BACKUPS}/${schedule.id}/${pool.$id}`
33
+ const dir = `${poolDir}/${formatFilenameDate(timestamp)}`
34
+
35
+ const stream = await this._exportPoolMetadata()
36
+ const fileName = `${dir}/data`
37
+
38
+ const metadata = JSON.stringify(
39
+ {
40
+ jobId: job.id,
41
+ jobName: job.name,
42
+ pool,
43
+ poolMaster: pool.$master,
44
+ scheduleId: schedule.id,
45
+ scheduleName: schedule.name,
46
+ timestamp,
47
+ },
48
+ null,
49
+ 2
50
+ )
51
+ const metaDataFileName = `${dir}/metadata.json`
52
+
53
+ await asyncMap(
54
+ Object.entries(this._remoteAdapters),
55
+ ([remoteId, adapter]) =>
56
+ Task.run(
57
+ {
58
+ name: `Starting metadata backup for the pool (${pool.$id}) for the remote (${remoteId}). (${job.id})`,
59
+ data: {
60
+ id: remoteId,
61
+ type: 'remote',
62
+ },
63
+ },
64
+ async () => {
65
+ // forkStreamUnpipe should be used in a sync way, do not wait for a promise before using it
66
+ await adapter.outputStream(fileName, forkStreamUnpipe(stream), { checksum: false })
67
+ await adapter.handler.outputFile(metaDataFileName, metadata, {
68
+ dirMode: this._config.dirMode,
69
+ })
70
+ await adapter.deleteOldMetadataBackups(poolDir, this._settings.retentionPoolMetadata)
71
+ }
72
+ ).catch(() => {}) // errors are handled by logs
73
+ )
74
+ }
75
+ }
package/_VmBackup.js ADDED
@@ -0,0 +1,409 @@
1
+ const assert = require('assert')
2
+ const findLast = require('lodash/findLast.js')
3
+ const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
4
+ const keyBy = require('lodash/keyBy.js')
5
+ const mapValues = require('lodash/mapValues.js')
6
+ const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
7
+ const { createLogger } = require('@xen-orchestra/log')
8
+ const { defer } = require('golike-defer')
9
+ const { formatDateTime } = require('@xen-orchestra/xapi')
10
+
11
+ const { DeltaBackupWriter } = require('./writers/DeltaBackupWriter.js')
12
+ const { DeltaReplicationWriter } = require('./writers/DeltaReplicationWriter.js')
13
+ const { exportDeltaVm } = require('./_deltaVm.js')
14
+ const { forkStreamUnpipe } = require('./_forkStreamUnpipe.js')
15
+ const { FullBackupWriter } = require('./writers/FullBackupWriter.js')
16
+ const { FullReplicationWriter } = require('./writers/FullReplicationWriter.js')
17
+ const { getOldEntries } = require('./_getOldEntries.js')
18
+ const { Task } = require('./Task.js')
19
+ const { watchStreamSize } = require('./_watchStreamSize.js')
20
+
21
+ const { debug, warn } = createLogger('xo:backups:VmBackup')
22
+
23
+ const asyncEach = async (iterable, fn, thisArg = iterable) => {
24
+ for (const item of iterable) {
25
+ await fn.call(thisArg, item)
26
+ }
27
+ }
28
+
29
+ const forkDeltaExport = deltaExport =>
30
+ Object.create(deltaExport, {
31
+ streams: {
32
+ value: mapValues(deltaExport.streams, forkStreamUnpipe),
33
+ },
34
+ })
35
+
36
+ exports.VmBackup = class VmBackup {
37
+ constructor({ config, getSnapshotNameLabel, job, remoteAdapters, remotes, schedule, settings, srs, vm }) {
38
+ this.config = config
39
+ this.job = job
40
+ this.remoteAdapters = remoteAdapters
41
+ this.remotes = remotes
42
+ this.scheduleId = schedule.id
43
+ this.timestamp = undefined
44
+
45
+ // VM currently backed up
46
+ this.vm = vm
47
+ const { tags } = this.vm
48
+
49
+ // VM (snapshot) that is really exported
50
+ this.exportedVm = undefined
51
+
52
+ this._fullVdisRequired = undefined
53
+ this._getSnapshotNameLabel = getSnapshotNameLabel
54
+ this._isDelta = job.mode === 'delta'
55
+ this._jobId = job.id
56
+ this._jobSnapshots = undefined
57
+ this._xapi = vm.$xapi
58
+
59
+ // Base VM for the export
60
+ this._baseVm = undefined
61
+
62
+ // Settings for this specific run (job, schedule, VM)
63
+ if (tags.includes('xo-memory-backup')) {
64
+ settings.checkpointSnapshot = true
65
+ }
66
+ if (tags.includes('xo-offline-backup')) {
67
+ settings.offlineSnapshot = true
68
+ }
69
+ this._settings = settings
70
+
71
+ // Create writers
72
+ {
73
+ const writers = new Set()
74
+ this._writers = writers
75
+
76
+ const [BackupWriter, ReplicationWriter] = this._isDelta
77
+ ? [DeltaBackupWriter, DeltaReplicationWriter]
78
+ : [FullBackupWriter, FullReplicationWriter]
79
+
80
+ const allSettings = job.settings
81
+
82
+ Object.keys(remoteAdapters).forEach(remoteId => {
83
+ const targetSettings = {
84
+ ...settings,
85
+ ...allSettings[remoteId],
86
+ }
87
+ if (targetSettings.exportRetention !== 0) {
88
+ writers.add(new BackupWriter({ backup: this, remoteId, settings: targetSettings }))
89
+ }
90
+ })
91
+ srs.forEach(sr => {
92
+ const targetSettings = {
93
+ ...settings,
94
+ ...allSettings[sr.uuid],
95
+ }
96
+ if (targetSettings.copyRetention !== 0) {
97
+ writers.add(new ReplicationWriter({ backup: this, sr, settings: targetSettings }))
98
+ }
99
+ })
100
+ }
101
+ }
102
+
103
+ // calls fn for each function, warns of any errors, and throws only if there are no writers left
104
+ async _callWriters(fn, warnMessage, parallel = true) {
105
+ const writers = this._writers
106
+ const n = writers.size
107
+ if (n === 0) {
108
+ return
109
+ }
110
+ if (n === 1) {
111
+ const [writer] = writers
112
+ try {
113
+ await fn(writer)
114
+ } catch (error) {
115
+ writers.delete(writer)
116
+ throw error
117
+ }
118
+ return
119
+ }
120
+
121
+ await (parallel ? asyncMap : asyncEach)(writers, async function (writer) {
122
+ try {
123
+ await fn(writer)
124
+ } catch (error) {
125
+ this.delete(writer)
126
+ warn(warnMessage, { error, writer: writer.constructor.name })
127
+ }
128
+ })
129
+ if (writers.size === 0) {
130
+ throw new Error('all targets have failed, step: ' + warnMessage)
131
+ }
132
+ }
133
+
134
+ // ensure the VM itself does not have any backup metadata which would be
135
+ // copied on manual snapshots and interfere with the backup jobs
136
+ async _cleanMetadata() {
137
+ const { vm } = this
138
+ if ('xo:backup:job' in vm.other_config) {
139
+ await vm.update_other_config({
140
+ 'xo:backup:datetime': null,
141
+ 'xo:backup:deltaChainLength': null,
142
+ 'xo:backup:exported': null,
143
+ 'xo:backup:job': null,
144
+ 'xo:backup:schedule': null,
145
+ 'xo:backup:vm': null,
146
+ })
147
+ }
148
+ }
149
+
150
+ async _snapshot() {
151
+ const { vm } = this
152
+ const xapi = this._xapi
153
+
154
+ const settings = this._settings
155
+
156
+ const doSnapshot =
157
+ this._isDelta || (!settings.offlineBackup && vm.power_state === 'Running') || settings.snapshotRetention !== 0
158
+ if (doSnapshot) {
159
+ await Task.run({ name: 'snapshot' }, async () => {
160
+ if (!settings.bypassVdiChainsCheck) {
161
+ await vm.$assertHealthyVdiChains()
162
+ }
163
+
164
+ const snapshotRef = await vm[settings.checkpointSnapshot ? '$checkpoint' : '$snapshot']({
165
+ name_label: this._getSnapshotNameLabel(vm),
166
+ })
167
+ this.timestamp = Date.now()
168
+
169
+ await xapi.setFieldEntries('VM', snapshotRef, 'other_config', {
170
+ 'xo:backup:datetime': formatDateTime(this.timestamp),
171
+ 'xo:backup:job': this._jobId,
172
+ 'xo:backup:schedule': this.scheduleId,
173
+ 'xo:backup:vm': vm.uuid,
174
+ })
175
+
176
+ this.exportedVm = await xapi.getRecord('VM', snapshotRef)
177
+
178
+ return this.exportedVm.uuid
179
+ })
180
+ } else {
181
+ this.exportedVm = vm
182
+ this.timestamp = Date.now()
183
+ }
184
+ }
185
+
186
+ async _copyDelta() {
187
+ const { exportedVm } = this
188
+ const baseVm = this._baseVm
189
+ const fullVdisRequired = this._fullVdisRequired
190
+
191
+ const isFull = fullVdisRequired === undefined || fullVdisRequired.size !== 0
192
+
193
+ await this._callWriters(writer => writer.prepare({ isFull }), 'writer.prepare()')
194
+
195
+ const deltaExport = await exportDeltaVm(exportedVm, baseVm, {
196
+ fullVdisRequired,
197
+ })
198
+ const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
199
+
200
+ const timestamp = Date.now()
201
+
202
+ await this._callWriters(
203
+ writer =>
204
+ writer.transfer({
205
+ deltaExport: forkDeltaExport(deltaExport),
206
+ sizeContainers,
207
+ timestamp,
208
+ }),
209
+ 'writer.transfer()'
210
+ )
211
+
212
+ this._baseVm = exportedVm
213
+
214
+ if (baseVm !== undefined) {
215
+ await exportedVm.update_other_config(
216
+ 'xo:backup:deltaChainLength',
217
+ String(+(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1)
218
+ )
219
+ }
220
+
221
+ // not the case if offlineBackup
222
+ if (exportedVm.is_a_snapshot) {
223
+ await exportedVm.update_other_config('xo:backup:exported', 'true')
224
+ }
225
+
226
+ const size = Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0)
227
+ const end = Date.now()
228
+ const duration = end - timestamp
229
+ debug('transfer complete', {
230
+ duration,
231
+ speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
232
+ size,
233
+ })
234
+
235
+ await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
236
+ }
237
+
238
+ async _copyFull() {
239
+ const { compression } = this.job
240
+ const stream = await this._xapi.VM_export(this.exportedVm.$ref, {
241
+ compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
242
+ useSnapshot: false,
243
+ })
244
+ const sizeContainer = watchStreamSize(stream)
245
+
246
+ const timestamp = Date.now()
247
+
248
+ await this._callWriters(
249
+ writer =>
250
+ writer.run({
251
+ sizeContainer,
252
+ stream: forkStreamUnpipe(stream),
253
+ timestamp,
254
+ }),
255
+ 'writer.run()'
256
+ )
257
+
258
+ const { size } = sizeContainer
259
+ const end = Date.now()
260
+ const duration = end - timestamp
261
+ debug('transfer complete', {
262
+ duration,
263
+ speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
264
+ size,
265
+ })
266
+ }
267
+
268
+ async _fetchJobSnapshots() {
269
+ const jobId = this._jobId
270
+ const vmRef = this.vm.$ref
271
+ const xapi = this._xapi
272
+
273
+ const snapshotsRef = await xapi.getField('VM', vmRef, 'snapshots')
274
+ const snapshotsOtherConfig = await asyncMap(snapshotsRef, ref => xapi.getField('VM', ref, 'other_config'))
275
+
276
+ const snapshots = []
277
+ snapshotsOtherConfig.forEach((other_config, i) => {
278
+ if (other_config['xo:backup:job'] === jobId) {
279
+ snapshots.push({ other_config, $ref: snapshotsRef[i] })
280
+ }
281
+ })
282
+ snapshots.sort((a, b) => (a.other_config['xo:backup:datetime'] < b.other_config['xo:backup:datetime'] ? -1 : 1))
283
+ this._jobSnapshots = snapshots
284
+ }
285
+
286
+ async _removeUnusedSnapshots() {
287
+ // TODO: handle all schedules (no longer existing schedules default to 0 retention)
288
+
289
+ const { scheduleId } = this
290
+ const scheduleSnapshots = this._jobSnapshots.filter(_ => _.other_config['xo:backup:schedule'] === scheduleId)
291
+
292
+ const baseVmRef = this._baseVm?.$ref
293
+ const xapi = this._xapi
294
+ await asyncMap(getOldEntries(this._settings.snapshotRetention, scheduleSnapshots), ({ $ref }) => {
295
+ if ($ref !== baseVmRef) {
296
+ return xapi.VM_destroy($ref)
297
+ }
298
+ })
299
+ }
300
+
301
+ async _selectBaseVm() {
302
+ const xapi = this._xapi
303
+
304
+ let baseVm = findLast(this._jobSnapshots, _ => 'xo:backup:exported' in _.other_config)
305
+ if (baseVm === undefined) {
306
+ debug('no base VM found')
307
+ return
308
+ }
309
+
310
+ const fullInterval = this._settings.fullInterval
311
+ const deltaChainLength = +(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1
312
+ if (!(fullInterval === 0 || fullInterval > deltaChainLength)) {
313
+ debug('not using base VM becaust fullInterval reached')
314
+ return
315
+ }
316
+
317
+ const srcVdis = keyBy(await xapi.getRecords('VDI', await this.vm.$getDisks()), '$ref')
318
+
319
+ // resolve full record
320
+ baseVm = await xapi.getRecord('VM', baseVm.$ref)
321
+
322
+ const baseUuidToSrcVdi = new Map()
323
+ await asyncMap(await baseVm.$getDisks(), async baseRef => {
324
+ const snapshotOf = await xapi.getField('VDI', baseRef, 'snapshot_of')
325
+ const srcVdi = srcVdis[snapshotOf]
326
+ if (srcVdi !== undefined) {
327
+ baseUuidToSrcVdi.set(await xapi.getField('VDI', baseRef, 'uuid'), srcVdi)
328
+ } else {
329
+ debug('no base VDI found', {
330
+ vdi: srcVdi.uuid,
331
+ })
332
+ }
333
+ })
334
+
335
+ const presentBaseVdis = new Map(baseUuidToSrcVdi)
336
+ await this._callWriters(
337
+ writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis, baseVm),
338
+ 'writer.checkBaseVdis()',
339
+ false
340
+ )
341
+
342
+ const fullVdisRequired = new Set()
343
+ baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
344
+ if (presentBaseVdis.has(baseUuid)) {
345
+ debug('found base VDI', {
346
+ base: baseUuid,
347
+ vdi: srcVdi.uuid,
348
+ })
349
+ } else {
350
+ debug('missing base VDI', {
351
+ base: baseUuid,
352
+ vdi: srcVdi.uuid,
353
+ })
354
+ fullVdisRequired.add(srcVdi.uuid)
355
+ }
356
+ })
357
+
358
+ this._baseVm = baseVm
359
+ this._fullVdisRequired = fullVdisRequired
360
+ }
361
+
362
+ run = defer(this.run)
363
+ async run($defer) {
364
+ const settings = this._settings
365
+ assert(
366
+ !settings.offlineBackup || settings.snapshotRetention === 0,
367
+ 'offlineBackup is not compatible with snapshotRetention'
368
+ )
369
+
370
+ await this._callWriters(async writer => {
371
+ await writer.beforeBackup()
372
+ $defer(() => writer.afterBackup())
373
+ }, 'writer.beforeBackup()')
374
+
375
+ await this._fetchJobSnapshots()
376
+
377
+ if (this._isDelta) {
378
+ await this._selectBaseVm()
379
+ }
380
+
381
+ await this._cleanMetadata()
382
+ await this._removeUnusedSnapshots()
383
+
384
+ const { vm } = this
385
+ const isRunning = vm.power_state === 'Running'
386
+ const startAfter = isRunning && (settings.offlineBackup ? 'backup' : settings.offlineSnapshot && 'snapshot')
387
+ if (startAfter) {
388
+ await vm.$callAsync('clean_shutdown')
389
+ }
390
+
391
+ try {
392
+ await this._snapshot()
393
+ if (startAfter === 'snapshot') {
394
+ ignoreErrors.call(vm.$callAsync('start', false, false))
395
+ }
396
+
397
+ if (this._writers.size !== 0) {
398
+ await (this._isDelta ? this._copyDelta() : this._copyFull())
399
+ }
400
+ } finally {
401
+ if (startAfter) {
402
+ ignoreErrors.call(vm.$callAsync('start', false, false))
403
+ }
404
+
405
+ await this._fetchJobSnapshots()
406
+ await this._removeUnusedSnapshots()
407
+ }
408
+ }
409
+ }
@@ -0,0 +1,62 @@
1
+ const { asyncMap } = require('@xen-orchestra/async-map')
2
+
3
+ const { DIR_XO_CONFIG_BACKUPS } = require('./RemoteAdapter.js')
4
+ const { formatFilenameDate } = require('./_filenameDate.js')
5
+ const { Task } = require('./Task.js')
6
+
7
+ exports.XoMetadataBackup = class XoMetadataBackup {
8
+ constructor({ config, job, remoteAdapters, schedule, settings }) {
9
+ this._config = config
10
+ this._job = job
11
+ this._remoteAdapters = remoteAdapters
12
+ this._schedule = schedule
13
+ this._settings = settings
14
+ }
15
+
16
+ async run() {
17
+ const timestamp = Date.now()
18
+
19
+ const { _job: job, _schedule: schedule } = this
20
+ const scheduleDir = `${DIR_XO_CONFIG_BACKUPS}/${schedule.id}`
21
+ const dir = `${scheduleDir}/${formatFilenameDate(timestamp)}`
22
+
23
+ const data = job.xoMetadata
24
+ const fileName = `${dir}/data.json`
25
+
26
+ const metadata = JSON.stringify(
27
+ {
28
+ jobId: job.id,
29
+ jobName: job.name,
30
+ scheduleId: schedule.id,
31
+ scheduleName: schedule.name,
32
+ timestamp,
33
+ },
34
+ null,
35
+ 2
36
+ )
37
+ const metaDataFileName = `${dir}/metadata.json`
38
+
39
+ await asyncMap(
40
+ Object.entries(this._remoteAdapters),
41
+ ([remoteId, adapter]) =>
42
+ Task.run(
43
+ {
44
+ name: `Starting XO metadata backup for the remote (${remoteId}). (${job.id})`,
45
+ data: {
46
+ id: remoteId,
47
+ type: 'remote',
48
+ },
49
+ },
50
+ async () => {
51
+ const handler = adapter.handler
52
+ const dirMode = this._config.dirMode
53
+ await handler.outputFile(fileName, data, { dirMode })
54
+ await handler.outputFile(metaDataFileName, metadata, {
55
+ dirMode,
56
+ })
57
+ await adapter.deleteOldMetadataBackups(scheduleDir, this._settings.retentionXoMetadata)
58
+ }
59
+ ).catch(() => {}) // errors are handled by logs
60
+ )
61
+ }
62
+ }
package/_backupType.js ADDED
@@ -0,0 +1,4 @@
1
+ exports.isMetadataFile = filename => filename.endsWith('.json')
2
+ exports.isVhdFile = filename => filename.endsWith('.vhd')
3
+ exports.isXvaFile = filename => filename.endsWith('.xva')
4
+ exports.isXvaSumFile = filename => filename.endsWith('.xva.cheksum')