@xen-orchestra/backups 0.36.0 → 0.37.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 (35) hide show
  1. package/Backup.js +11 -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/VmsXapi.js +138 -0
  9. package/_runners/_Abstract.js +51 -0
  10. package/{_PoolMetadataBackup.js → _runners/_PoolMetadataBackup.js} +3 -3
  11. package/_runners/_RemoteTimeoutError.js +8 -0
  12. package/{_XoMetadataBackup.js → _runners/_XoMetadataBackup.js} +3 -3
  13. package/_runners/_getAdaptersByRemote.js +9 -0
  14. package/_runners/_runTask.js +6 -0
  15. package/_runners/_vmRunners/FullXapi.js +61 -0
  16. package/_runners/_vmRunners/IncrementalXapi.js +163 -0
  17. package/_runners/_vmRunners/_Abstract.js +87 -0
  18. package/_runners/_vmRunners/_AbstractXapi.js +258 -0
  19. package/_runners/_vmRunners/_forkDeltaExport.js +12 -0
  20. package/{writers/FullBackupWriter.js → _runners/_writers/FullRemoteWriter.js} +5 -5
  21. package/{writers/FullReplicationWriter.js → _runners/_writers/FullXapiWriter.js} +5 -5
  22. package/{writers/DeltaBackupWriter.js → _runners/_writers/IncrementalRemoteWriter.js} +7 -7
  23. package/{writers/DeltaReplicationWriter.js → _runners/_writers/IncrementalXapiWriter.js} +10 -10
  24. package/{writers/_AbstractDeltaWriter.js → _runners/_writers/_AbstractIncrementalWriter.js} +1 -1
  25. package/{writers/_MixinBackupWriter.js → _runners/_writers/_MixinRemoteWriter.js} +10 -10
  26. package/{writers/_MixinReplicationWriter.js → _runners/_writers/_MixinXapiWriter.js} +6 -12
  27. package/package.json +6 -6
  28. package/_VmBackup.js +0 -515
  29. /package/{_createStreamThrottle.js → _runners/_createStreamThrottle.js} +0 -0
  30. /package/{_forkStreamUnpipe.js → _runners/_forkStreamUnpipe.js} +0 -0
  31. /package/{writers → _runners/_writers}/_AbstractFullWriter.js +0 -0
  32. /package/{writers → _runners/_writers}/_AbstractWriter.js +0 -0
  33. /package/{writers → _runners/_writers}/_checkVhd.js +0 -0
  34. /package/{writers → _runners/_writers}/_listReplicatedVms.js +0 -0
  35. /package/{writers → _runners/_writers}/_packUuid.js +0 -0
package/_VmBackup.js DELETED
@@ -1,515 +0,0 @@
1
- 'use strict'
2
-
3
- const assert = require('assert')
4
- const findLast = require('lodash/findLast.js')
5
- const groupBy = require('lodash/groupBy.js')
6
- const ignoreErrors = require('promise-toolbox/ignoreErrors')
7
- const keyBy = require('lodash/keyBy.js')
8
- const mapValues = require('lodash/mapValues.js')
9
- const vhdStreamValidator = require('vhd-lib/vhdStreamValidator.js')
10
- const { asyncMap } = require('@xen-orchestra/async-map')
11
- const { createLogger } = require('@xen-orchestra/log')
12
- const { decorateMethodsWith } = require('@vates/decorate-with')
13
- const { defer } = require('golike-defer')
14
- const { formatDateTime } = require('@xen-orchestra/xapi')
15
- const { pipeline } = require('node:stream')
16
-
17
- const { DeltaBackupWriter } = require('./writers/DeltaBackupWriter.js')
18
- const { DeltaReplicationWriter } = require('./writers/DeltaReplicationWriter.js')
19
- const { exportDeltaVm } = require('./_deltaVm.js')
20
- const { forkStreamUnpipe } = require('./_forkStreamUnpipe.js')
21
- const { FullBackupWriter } = require('./writers/FullBackupWriter.js')
22
- const { FullReplicationWriter } = require('./writers/FullReplicationWriter.js')
23
- const { getOldEntries } = require('./_getOldEntries.js')
24
- const { Task } = require('./Task.js')
25
- const { watchStreamSize } = require('./_watchStreamSize.js')
26
-
27
- const { debug, warn } = createLogger('xo:backups:VmBackup')
28
-
29
- class AggregateError extends Error {
30
- constructor(errors, message) {
31
- super(message)
32
- this.errors = errors
33
- }
34
- }
35
-
36
- const asyncEach = async (iterable, fn, thisArg = iterable) => {
37
- for (const item of iterable) {
38
- await fn.call(thisArg, item)
39
- }
40
- }
41
-
42
- const forkDeltaExport = deltaExport =>
43
- Object.create(deltaExport, {
44
- streams: {
45
- value: mapValues(deltaExport.streams, forkStreamUnpipe),
46
- },
47
- })
48
-
49
- const noop = Function.prototype
50
-
51
- class VmBackup {
52
- constructor({
53
- config,
54
- getSnapshotNameLabel,
55
- healthCheckSr,
56
- job,
57
- remoteAdapters,
58
- remotes,
59
- schedule,
60
- settings,
61
- srs,
62
- throttleStream,
63
- vm,
64
- }) {
65
- if (vm.other_config['xo:backup:job'] === job.id && 'start' in vm.blocked_operations) {
66
- // don't match replicated VMs created by this very job otherwise they
67
- // will be replicated again and again
68
- throw new Error('cannot backup a VM created by this very job')
69
- }
70
-
71
- this.config = config
72
- this.job = job
73
- this.remoteAdapters = remoteAdapters
74
- this.scheduleId = schedule.id
75
- this.timestamp = undefined
76
-
77
- // VM currently backed up
78
- this.vm = vm
79
- const { tags } = this.vm
80
-
81
- // VM (snapshot) that is really exported
82
- this.exportedVm = undefined
83
-
84
- this._fullVdisRequired = undefined
85
- this._getSnapshotNameLabel = getSnapshotNameLabel
86
- this._isDelta = job.mode === 'delta'
87
- this._healthCheckSr = healthCheckSr
88
- this._jobId = job.id
89
- this._jobSnapshots = undefined
90
- this._throttleStream = throttleStream
91
- this._xapi = vm.$xapi
92
-
93
- // Base VM for the export
94
- this._baseVm = undefined
95
-
96
- // Settings for this specific run (job, schedule, VM)
97
- if (tags.includes('xo-memory-backup')) {
98
- settings.checkpointSnapshot = true
99
- }
100
- if (tags.includes('xo-offline-backup')) {
101
- settings.offlineSnapshot = true
102
- }
103
- this._settings = settings
104
-
105
- // Create writers
106
- {
107
- const writers = new Set()
108
- this._writers = writers
109
-
110
- const [BackupWriter, ReplicationWriter] = this._isDelta
111
- ? [DeltaBackupWriter, DeltaReplicationWriter]
112
- : [FullBackupWriter, FullReplicationWriter]
113
-
114
- const allSettings = job.settings
115
- Object.keys(remoteAdapters).forEach(remoteId => {
116
- const targetSettings = {
117
- ...settings,
118
- ...allSettings[remoteId],
119
- }
120
- if (targetSettings.exportRetention !== 0) {
121
- writers.add(new BackupWriter({ backup: this, remoteId, settings: targetSettings }))
122
- }
123
- })
124
- srs.forEach(sr => {
125
- const targetSettings = {
126
- ...settings,
127
- ...allSettings[sr.uuid],
128
- }
129
- if (targetSettings.copyRetention !== 0) {
130
- writers.add(new ReplicationWriter({ backup: this, sr, settings: targetSettings }))
131
- }
132
- })
133
- }
134
- }
135
-
136
- // calls fn for each function, warns of any errors, and throws only if there are no writers left
137
- async _callWriters(fn, step, parallel = true) {
138
- const writers = this._writers
139
- const n = writers.size
140
- if (n === 0) {
141
- return
142
- }
143
-
144
- async function callWriter(writer) {
145
- const { name } = writer.constructor
146
- try {
147
- debug('writer step starting', { step, writer: name })
148
- await fn(writer)
149
- debug('writer step succeeded', { duration: step, writer: name })
150
- } catch (error) {
151
- writers.delete(writer)
152
-
153
- warn('writer step failed', { error, step, writer: name })
154
-
155
- // these two steps are the only one that are not already in their own sub tasks
156
- if (step === 'writer.checkBaseVdis()' || step === 'writer.beforeBackup()') {
157
- Task.warning(
158
- `the writer ${name} has failed the step ${step} with error ${error.message}. It won't be used anymore in this job execution.`
159
- )
160
- }
161
-
162
- throw error
163
- }
164
- }
165
- if (n === 1) {
166
- const [writer] = writers
167
- return callWriter(writer)
168
- }
169
-
170
- const errors = []
171
- await (parallel ? asyncMap : asyncEach)(writers, async function (writer) {
172
- try {
173
- await callWriter(writer)
174
- } catch (error) {
175
- errors.push(error)
176
- }
177
- })
178
- if (writers.size === 0) {
179
- throw new AggregateError(errors, 'all targets have failed, step: ' + step)
180
- }
181
- }
182
-
183
- // ensure the VM itself does not have any backup metadata which would be
184
- // copied on manual snapshots and interfere with the backup jobs
185
- async _cleanMetadata() {
186
- const { vm } = this
187
- if ('xo:backup:job' in vm.other_config) {
188
- await vm.update_other_config({
189
- 'xo:backup:datetime': null,
190
- 'xo:backup:deltaChainLength': null,
191
- 'xo:backup:exported': null,
192
- 'xo:backup:job': null,
193
- 'xo:backup:schedule': null,
194
- 'xo:backup:vm': null,
195
- })
196
- }
197
- }
198
-
199
- async _snapshot() {
200
- const { vm } = this
201
- const xapi = this._xapi
202
-
203
- const settings = this._settings
204
-
205
- const doSnapshot =
206
- settings.unconditionalSnapshot ||
207
- this._isDelta ||
208
- (!settings.offlineBackup && vm.power_state === 'Running') ||
209
- settings.snapshotRetention !== 0
210
- if (doSnapshot) {
211
- await Task.run({ name: 'snapshot' }, async () => {
212
- if (!settings.bypassVdiChainsCheck) {
213
- await vm.$assertHealthyVdiChains()
214
- }
215
-
216
- const snapshotRef = await vm[settings.checkpointSnapshot ? '$checkpoint' : '$snapshot']({
217
- ignoreNobakVdis: true,
218
- name_label: this._getSnapshotNameLabel(vm),
219
- unplugVusbs: true,
220
- })
221
- this.timestamp = Date.now()
222
-
223
- await xapi.setFieldEntries('VM', snapshotRef, 'other_config', {
224
- 'xo:backup:datetime': formatDateTime(this.timestamp),
225
- 'xo:backup:job': this._jobId,
226
- 'xo:backup:schedule': this.scheduleId,
227
- 'xo:backup:vm': vm.uuid,
228
- })
229
-
230
- this.exportedVm = await xapi.getRecord('VM', snapshotRef)
231
-
232
- return this.exportedVm.uuid
233
- })
234
- } else {
235
- this.exportedVm = vm
236
- this.timestamp = Date.now()
237
- }
238
- }
239
-
240
- async _copyDelta() {
241
- const { exportedVm } = this
242
- const baseVm = this._baseVm
243
- const fullVdisRequired = this._fullVdisRequired
244
-
245
- const isFull = fullVdisRequired === undefined || fullVdisRequired.size !== 0
246
-
247
- await this._callWriters(writer => writer.prepare({ isFull }), 'writer.prepare()')
248
-
249
- const deltaExport = await exportDeltaVm(exportedVm, baseVm, {
250
- fullVdisRequired,
251
- })
252
- // since NBD is network based, if one disk use nbd , all the disk use them
253
- // except the suspended VDI
254
- if (Object.values(deltaExport.streams).some(({ _nbd }) => _nbd)) {
255
- Task.info('Transfer data using NBD')
256
- }
257
- const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
258
-
259
- if (this._settings.validateVhdStreams) {
260
- deltaExport.streams = mapValues(deltaExport.streams, stream => pipeline(stream, vhdStreamValidator, noop))
261
- }
262
-
263
- deltaExport.streams = mapValues(deltaExport.streams, this._throttleStream)
264
-
265
- const timestamp = Date.now()
266
-
267
- await this._callWriters(
268
- writer =>
269
- writer.transfer({
270
- deltaExport: forkDeltaExport(deltaExport),
271
- sizeContainers,
272
- timestamp,
273
- }),
274
- 'writer.transfer()'
275
- )
276
-
277
- this._baseVm = exportedVm
278
-
279
- if (baseVm !== undefined) {
280
- await exportedVm.update_other_config(
281
- 'xo:backup:deltaChainLength',
282
- String(+(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1)
283
- )
284
- }
285
-
286
- // not the case if offlineBackup
287
- if (exportedVm.is_a_snapshot) {
288
- await exportedVm.update_other_config('xo:backup:exported', 'true')
289
- }
290
-
291
- const size = Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0)
292
- const end = Date.now()
293
- const duration = end - timestamp
294
- debug('transfer complete', {
295
- duration,
296
- speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
297
- size,
298
- })
299
-
300
- await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
301
- }
302
-
303
- async _copyFull() {
304
- const { compression } = this.job
305
- const stream = this._throttleStream(
306
- await this._xapi.VM_export(this.exportedVm.$ref, {
307
- compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
308
- useSnapshot: false,
309
- })
310
- )
311
- const sizeContainer = watchStreamSize(stream)
312
-
313
- const timestamp = Date.now()
314
-
315
- await this._callWriters(
316
- writer =>
317
- writer.run({
318
- sizeContainer,
319
- stream: forkStreamUnpipe(stream),
320
- timestamp,
321
- }),
322
- 'writer.run()'
323
- )
324
-
325
- const { size } = sizeContainer
326
- const end = Date.now()
327
- const duration = end - timestamp
328
- debug('transfer complete', {
329
- duration,
330
- speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
331
- size,
332
- })
333
- }
334
-
335
- async _fetchJobSnapshots() {
336
- const jobId = this._jobId
337
- const vmRef = this.vm.$ref
338
- const xapi = this._xapi
339
-
340
- const snapshotsRef = await xapi.getField('VM', vmRef, 'snapshots')
341
- const snapshotsOtherConfig = await asyncMap(snapshotsRef, ref => xapi.getField('VM', ref, 'other_config'))
342
-
343
- const snapshots = []
344
- snapshotsOtherConfig.forEach((other_config, i) => {
345
- if (other_config['xo:backup:job'] === jobId) {
346
- snapshots.push({ other_config, $ref: snapshotsRef[i] })
347
- }
348
- })
349
- snapshots.sort((a, b) => (a.other_config['xo:backup:datetime'] < b.other_config['xo:backup:datetime'] ? -1 : 1))
350
- this._jobSnapshots = snapshots
351
- }
352
-
353
- async _removeUnusedSnapshots() {
354
- const allSettings = this.job.settings
355
- const baseSettings = this._baseSettings
356
- const baseVmRef = this._baseVm?.$ref
357
-
358
- const snapshotsPerSchedule = groupBy(this._jobSnapshots, _ => _.other_config['xo:backup:schedule'])
359
- const xapi = this._xapi
360
- await asyncMap(Object.entries(snapshotsPerSchedule), ([scheduleId, snapshots]) => {
361
- const settings = {
362
- ...baseSettings,
363
- ...allSettings[scheduleId],
364
- ...allSettings[this.vm.uuid],
365
- }
366
- return asyncMap(getOldEntries(settings.snapshotRetention, snapshots), ({ $ref }) => {
367
- if ($ref !== baseVmRef) {
368
- return xapi.VM_destroy($ref)
369
- }
370
- })
371
- })
372
- }
373
-
374
- async _selectBaseVm() {
375
- const xapi = this._xapi
376
-
377
- let baseVm = findLast(this._jobSnapshots, _ => 'xo:backup:exported' in _.other_config)
378
- if (baseVm === undefined) {
379
- debug('no base VM found')
380
- return
381
- }
382
-
383
- const fullInterval = this._settings.fullInterval
384
- const deltaChainLength = +(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1
385
- if (!(fullInterval === 0 || fullInterval > deltaChainLength)) {
386
- debug('not using base VM becaust fullInterval reached')
387
- return
388
- }
389
-
390
- const srcVdis = keyBy(await xapi.getRecords('VDI', await this.vm.$getDisks()), '$ref')
391
-
392
- // resolve full record
393
- baseVm = await xapi.getRecord('VM', baseVm.$ref)
394
-
395
- const baseUuidToSrcVdi = new Map()
396
- await asyncMap(await baseVm.$getDisks(), async baseRef => {
397
- const [baseUuid, snapshotOf] = await Promise.all([
398
- xapi.getField('VDI', baseRef, 'uuid'),
399
- xapi.getField('VDI', baseRef, 'snapshot_of'),
400
- ])
401
- const srcVdi = srcVdis[snapshotOf]
402
- if (srcVdi !== undefined) {
403
- baseUuidToSrcVdi.set(baseUuid, srcVdi)
404
- } else {
405
- debug('ignore snapshot VDI because no longer present on VM', {
406
- vdi: baseUuid,
407
- })
408
- }
409
- })
410
-
411
- const presentBaseVdis = new Map(baseUuidToSrcVdi)
412
- await this._callWriters(
413
- writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis, baseVm),
414
- 'writer.checkBaseVdis()',
415
- false
416
- )
417
-
418
- if (presentBaseVdis.size === 0) {
419
- debug('no base VM found')
420
- return
421
- }
422
-
423
- const fullVdisRequired = new Set()
424
- baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
425
- if (presentBaseVdis.has(baseUuid)) {
426
- debug('found base VDI', {
427
- base: baseUuid,
428
- vdi: srcVdi.uuid,
429
- })
430
- } else {
431
- debug('missing base VDI', {
432
- base: baseUuid,
433
- vdi: srcVdi.uuid,
434
- })
435
- fullVdisRequired.add(srcVdi.uuid)
436
- }
437
- })
438
-
439
- this._baseVm = baseVm
440
- this._fullVdisRequired = fullVdisRequired
441
- }
442
-
443
- async _healthCheck() {
444
- const settings = this._settings
445
-
446
- if (this._healthCheckSr === undefined) {
447
- return
448
- }
449
-
450
- // check if current VM has tags
451
- const { tags } = this.vm
452
- const intersect = settings.healthCheckVmsWithTags.some(t => tags.includes(t))
453
-
454
- if (settings.healthCheckVmsWithTags.length !== 0 && !intersect) {
455
- return
456
- }
457
-
458
- await this._callWriters(writer => writer.healthCheck(this._healthCheckSr), 'writer.healthCheck()')
459
- }
460
-
461
- async run($defer) {
462
- const settings = this._settings
463
- assert(
464
- !settings.offlineBackup || settings.snapshotRetention === 0,
465
- 'offlineBackup is not compatible with snapshotRetention'
466
- )
467
-
468
- await this._callWriters(async writer => {
469
- await writer.beforeBackup()
470
- $defer(async () => {
471
- await writer.afterBackup()
472
- })
473
- }, 'writer.beforeBackup()')
474
-
475
- await this._fetchJobSnapshots()
476
-
477
- if (this._isDelta) {
478
- await this._selectBaseVm()
479
- }
480
-
481
- await this._cleanMetadata()
482
- await this._removeUnusedSnapshots()
483
-
484
- const { vm } = this
485
- const isRunning = vm.power_state === 'Running'
486
- const startAfter = isRunning && (settings.offlineBackup ? 'backup' : settings.offlineSnapshot && 'snapshot')
487
- if (startAfter) {
488
- await vm.$callAsync('clean_shutdown')
489
- }
490
-
491
- try {
492
- await this._snapshot()
493
- if (startAfter === 'snapshot') {
494
- ignoreErrors.call(vm.$callAsync('start', false, false))
495
- }
496
-
497
- if (this._writers.size !== 0) {
498
- await (this._isDelta ? this._copyDelta() : this._copyFull())
499
- }
500
- } finally {
501
- if (startAfter) {
502
- ignoreErrors.call(vm.$callAsync('start', false, false))
503
- }
504
-
505
- await this._fetchJobSnapshots()
506
- await this._removeUnusedSnapshots()
507
- }
508
- await this._healthCheck()
509
- }
510
- }
511
- exports.VmBackup = VmBackup
512
-
513
- decorateMethodsWith(VmBackup, {
514
- run: defer,
515
- })
File without changes
File without changes