@xen-orchestra/backups 0.32.0 → 0.34.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
@@ -12,6 +12,7 @@ const { PoolMetadataBackup } = require('./_PoolMetadataBackup.js')
12
12
  const { Task } = require('./Task.js')
13
13
  const { VmBackup } = require('./_VmBackup.js')
14
14
  const { XoMetadataBackup } = require('./_XoMetadataBackup.js')
15
+ const createStreamThrottle = require('./_createStreamThrottle.js')
15
16
 
16
17
  const noop = Function.prototype
17
18
 
@@ -40,6 +41,7 @@ const DEFAULT_VM_SETTINGS = {
40
41
  fullInterval: 0,
41
42
  healthCheckSr: undefined,
42
43
  healthCheckVmsWithTags: [],
44
+ maxExportRate: 0,
43
45
  maxMergedDeltasPerRun: Infinity,
44
46
  offlineBackup: false,
45
47
  offlineSnapshot: false,
@@ -226,9 +228,11 @@ exports.Backup = class Backup {
226
228
  // FIXME: proper SimpleIdPattern handling
227
229
  const getSnapshotNameLabel = this._getSnapshotNameLabel
228
230
  const schedule = this._schedule
231
+ const settings = this._settings
232
+
233
+ const throttleStream = createStreamThrottle(settings.maxExportRate)
229
234
 
230
235
  const config = this._config
231
- const settings = this._settings
232
236
  await Disposable.use(
233
237
  Disposable.all(
234
238
  extractIdsFromSimplePattern(job.srs).map(id =>
@@ -265,23 +269,35 @@ exports.Backup = class Backup {
265
269
  const allSettings = this._job.settings
266
270
  const baseSettings = this._baseSettings
267
271
 
268
- const handleVm = vmUuid =>
269
- runTask({ name: 'backup VM', data: { type: 'VM', id: vmUuid } }, () =>
270
- Disposable.use(this._getRecord('VM', vmUuid), vm =>
271
- new VmBackup({
272
- baseSettings,
273
- config,
274
- getSnapshotNameLabel,
275
- healthCheckSr,
276
- job,
277
- remoteAdapters,
278
- schedule,
279
- settings: { ...settings, ...allSettings[vm.uuid] },
280
- srs,
281
- vm,
282
- }).run()
283
- )
272
+ const handleVm = vmUuid => {
273
+ const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
274
+
275
+ return this._getRecord('VM', vmUuid).then(
276
+ disposableVm =>
277
+ Disposable.use(disposableVm, vm => {
278
+ taskStart.data.name_label = vm.name_label
279
+ return runTask(taskStart, () =>
280
+ new VmBackup({
281
+ baseSettings,
282
+ config,
283
+ getSnapshotNameLabel,
284
+ healthCheckSr,
285
+ job,
286
+ remoteAdapters,
287
+ schedule,
288
+ settings: { ...settings, ...allSettings[vm.uuid] },
289
+ srs,
290
+ throttleStream,
291
+ vm,
292
+ }).run()
293
+ )
294
+ }),
295
+ error =>
296
+ runTask(taskStart, () => {
297
+ throw error
298
+ })
284
299
  )
300
+ }
285
301
  const { concurrency } = settings
286
302
  await asyncMapSettled(vmIds, concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm))
287
303
  }
package/RemoteAdapter.js CHANGED
@@ -10,7 +10,14 @@ const groupBy = require('lodash/groupBy.js')
10
10
  const pickBy = require('lodash/pickBy.js')
11
11
  const { dirname, join, normalize, resolve } = require('path')
12
12
  const { createLogger } = require('@xen-orchestra/log')
13
- const { createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } = require('vhd-lib')
13
+ const {
14
+ createVhdDirectoryFromStream,
15
+ createVhdStreamWithLength,
16
+ openVhd,
17
+ VhdAbstract,
18
+ VhdDirectory,
19
+ VhdSynthetic,
20
+ } = require('vhd-lib')
14
21
  const { deduped } = require('@vates/disposable/deduped.js')
15
22
  const { decorateMethodsWith } = require('@vates/decorate-with')
16
23
  const { compose } = require('@vates/compose')
@@ -32,6 +39,7 @@ const { watchStreamSize } = require('./_watchStreamSize')
32
39
  // @todo : this import is marked extraneous , sould be fixed when lib is published
33
40
  const { mount } = require('@vates/fuse-vhd')
34
41
  const { asyncEach } = require('@vates/async-each')
42
+ const { strictEqual } = require('assert')
35
43
 
36
44
  const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
37
45
  exports.DIR_XO_CONFIG_BACKUPS = DIR_XO_CONFIG_BACKUPS
@@ -209,8 +217,8 @@ class RemoteAdapter {
209
217
 
210
218
  const isVhdDirectory = vhd instanceof VhdDirectory
211
219
  return isVhdDirectory
212
- ? this.#useVhdDirectory() && this.#getCompressionType() === vhd.compressionType
213
- : !this.#useVhdDirectory()
220
+ ? this.useVhdDirectory() && this.#getCompressionType() === vhd.compressionType
221
+ : !this.useVhdDirectory()
214
222
  })
215
223
  }
216
224
 
@@ -321,12 +329,12 @@ class RemoteAdapter {
321
329
  return this._vhdDirectoryCompression
322
330
  }
323
331
 
324
- #useVhdDirectory() {
332
+ useVhdDirectory() {
325
333
  return this.handler.useVhdDirectory()
326
334
  }
327
335
 
328
336
  #useAlias() {
329
- return this.#useVhdDirectory()
337
+ return this.useVhdDirectory()
330
338
  }
331
339
 
332
340
  async *#getDiskLegacy(diskId) {
@@ -658,9 +666,9 @@ class RemoteAdapter {
658
666
  return path
659
667
  }
660
668
 
661
- async writeVhd(path, input, { checksum = true, validator = noop, writeBlockConcurrency, nbdClient } = {}) {
669
+ async writeVhd(path, input, { checksum = true, validator = noop, writeBlockConcurrency } = {}) {
662
670
  const handler = this._handler
663
- if (this.#useVhdDirectory()) {
671
+ if (this.useVhdDirectory()) {
664
672
  const dataPath = `${dirname(path)}/data/${uuidv4()}.vhd`
665
673
  const size = await createVhdDirectoryFromStream(handler, dataPath, input, {
666
674
  concurrency: writeBlockConcurrency,
@@ -669,22 +677,41 @@ class RemoteAdapter {
669
677
  await input.task
670
678
  return validator.apply(this, arguments)
671
679
  },
672
- nbdClient,
673
680
  })
674
681
  await VhdAbstract.createAlias(handler, path, dataPath)
675
682
  return size
676
683
  } else {
677
- return this.outputStream(path, input, { checksum, validator })
684
+ const inputWithSize = await createVhdStreamWithLength(input)
685
+ return this.outputStream(path, inputWithSize, { checksum, validator, expectedSize: inputWithSize.length })
678
686
  }
679
687
  }
680
688
 
681
- async outputStream(path, input, { checksum = true, validator = noop } = {}) {
689
+ async outputStream(path, input, { checksum = true, validator = noop, expectedSize } = {}) {
682
690
  const container = watchStreamSize(input)
691
+
683
692
  await this._handler.outputStream(path, input, {
684
693
  checksum,
685
694
  dirMode: this._dirMode,
686
695
  async validator() {
687
696
  await input.task
697
+ if (expectedSize !== undefined) {
698
+ // check that we read all the stream
699
+ strictEqual(
700
+ container.size,
701
+ expectedSize,
702
+ `transferred size ${container.size}, expected file size : ${expectedSize}`
703
+ )
704
+ }
705
+ let size
706
+ try {
707
+ size = await this._handler.getSize(path)
708
+ } catch (err) {
709
+ // can fail is the remote is encrypted
710
+ }
711
+ if (size !== undefined) {
712
+ // check that everything is written to disk
713
+ strictEqual(size, container.size, `written size ${size}, transfered size : ${container.size}`)
714
+ }
688
715
  return validator.apply(this, arguments)
689
716
  },
690
717
  })
package/_VmBackup.js CHANGED
@@ -55,6 +55,7 @@ class VmBackup {
55
55
  schedule,
56
56
  settings,
57
57
  srs,
58
+ throttleStream,
58
59
  vm,
59
60
  }) {
60
61
  if (vm.other_config['xo:backup:job'] === job.id && 'start' in vm.blocked_operations) {
@@ -82,6 +83,7 @@ class VmBackup {
82
83
  this._healthCheckSr = healthCheckSr
83
84
  this._jobId = job.id
84
85
  this._jobSnapshots = undefined
86
+ this._throttleStream = throttleStream
85
87
  this._xapi = vm.$xapi
86
88
 
87
89
  // Base VM for the export
@@ -243,7 +245,13 @@ class VmBackup {
243
245
  const deltaExport = await exportDeltaVm(exportedVm, baseVm, {
244
246
  fullVdisRequired,
245
247
  })
248
+ // since NBD is network based, if one disk use nbd , all the disk use them
249
+ // except the suspended VDI
250
+ if (Object.values(deltaExport.streams).some(({ _nbd }) => _nbd)) {
251
+ Task.info('Transfer data using NBD')
252
+ }
246
253
  const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
254
+ deltaExport.streams = mapValues(deltaExport.streams, this._throttleStream)
247
255
 
248
256
  const timestamp = Date.now()
249
257
 
@@ -285,10 +293,12 @@ class VmBackup {
285
293
 
286
294
  async _copyFull() {
287
295
  const { compression } = this.job
288
- const stream = await this._xapi.VM_export(this.exportedVm.$ref, {
289
- compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
290
- useSnapshot: false,
291
- })
296
+ const stream = this._throttleStream(
297
+ await this._xapi.VM_export(this.exportedVm.$ref, {
298
+ compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
299
+ useSnapshot: false,
300
+ })
301
+ )
292
302
  const sizeContainer = watchStreamSize(stream)
293
303
 
294
304
  const timestamp = Date.now()
package/_cleanVm.js CHANGED
@@ -541,7 +541,8 @@ exports.cleanVm = async function cleanVm(
541
541
 
542
542
  // don't warn if the size has changed after a merge
543
543
  if (!merged && fileSystemSize !== size) {
544
- logWarn('incorrect backup size in metadata', {
544
+ // FIXME: figure out why it occurs so often and, once fixed, log the real problems with `logWarn`
545
+ console.warn('cleanVm: incorrect backup size in metadata', {
545
546
  path: metadataPath,
546
547
  actual: size ?? 'none',
547
548
  expected: fileSystemSize,
@@ -0,0 +1,17 @@
1
+ 'use strict'
2
+
3
+ const { pipeline } = require('node:stream')
4
+ const { ThrottleGroup } = require('@kldzj/stream-throttle')
5
+ const identity = require('lodash/identity.js')
6
+
7
+ const noop = Function.prototype
8
+
9
+ module.exports = function createStreamThrottle(rate) {
10
+ if (rate === 0) {
11
+ return identity
12
+ }
13
+ const group = new ThrottleGroup({ rate })
14
+ return function throttleStream(stream) {
15
+ return pipeline(stream, group.createThrottle(), noop)
16
+ }
17
+ }
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.32.0",
11
+ "version": "0.34.0",
12
12
  "engines": {
13
13
  "node": ">=14.6"
14
14
  },
@@ -17,16 +17,17 @@
17
17
  "test": "node--test"
18
18
  },
19
19
  "dependencies": {
20
+ "@kldzj/stream-throttle": "^1.1.1",
20
21
  "@vates/async-each": "^1.0.0",
21
22
  "@vates/cached-dns.lookup": "^1.0.0",
22
23
  "@vates/compose": "^2.1.0",
23
24
  "@vates/decorate-with": "^2.0.0",
24
25
  "@vates/disposable": "^0.1.4",
25
26
  "@vates/fuse-vhd": "^1.0.0",
26
- "@vates/nbd-client": "^1.0.1",
27
+ "@vates/nbd-client": "^1.1.0",
27
28
  "@vates/parse-duration": "^0.1.1",
28
29
  "@xen-orchestra/async-map": "^0.1.2",
29
- "@xen-orchestra/fs": "^3.3.2",
30
+ "@xen-orchestra/fs": "^3.3.4",
30
31
  "@xen-orchestra/log": "^0.6.0",
31
32
  "@xen-orchestra/template": "^0.1.0",
32
33
  "compare-versions": "^5.0.1",
@@ -41,7 +42,7 @@
41
42
  "promise-toolbox": "^0.21.0",
42
43
  "proper-lockfile": "^4.1.2",
43
44
  "uuid": "^9.0.0",
44
- "vhd-lib": "^4.2.1",
45
+ "vhd-lib": "^4.3.0",
45
46
  "yazl": "^2.5.1"
46
47
  },
47
48
  "devDependencies": {
@@ -51,7 +52,7 @@
51
52
  "tmp": "^0.2.1"
52
53
  },
53
54
  "peerDependencies": {
54
- "@xen-orchestra/xapi": "^2.0.0"
55
+ "@xen-orchestra/xapi": "^2.1.0"
55
56
  },
56
57
  "license": "AGPL-3.0-or-later",
57
58
  "author": {
@@ -20,9 +20,8 @@ const { AbstractDeltaWriter } = require('./_AbstractDeltaWriter.js')
20
20
  const { checkVhd } = require('./_checkVhd.js')
21
21
  const { packUuid } = require('./_packUuid.js')
22
22
  const { Disposable } = require('promise-toolbox')
23
- const NbdClient = require('@vates/nbd-client')
24
23
 
25
- const { debug, warn, info } = createLogger('xo:backups:DeltaBackupWriter')
24
+ const { warn } = createLogger('xo:backups:DeltaBackupWriter')
26
25
 
27
26
  class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
28
27
  async checkBaseVdis(baseUuidToSrcVdi) {
@@ -200,41 +199,12 @@ class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
200
199
  await checkVhd(handler, parentPath)
201
200
  }
202
201
 
203
- const vdiRef = vm.$xapi.getObject(vdi.uuid).$ref
204
-
205
- let nbdClient
206
- if (this._backup.config.useNbd) {
207
- debug('useNbd is enabled', { vdi: id, path })
208
- // get nbd if possible
209
- try {
210
- // this will always take the first host in the list
211
- const [nbdInfo] = await vm.$xapi.call('VDI.get_nbd_info', vdiRef)
212
- debug('got NBD info', { nbdInfo, vdi: id, path })
213
- nbdClient = new NbdClient(nbdInfo)
214
- await nbdClient.connect()
215
-
216
- // this will inform the xapi that we don't need this anymore
217
- // and will detach the vdi from dom0
218
- $defer(() => nbdClient.disconnect())
219
-
220
- info('NBD client ready', { vdi: id, path })
221
- Task.info('NBD used')
222
- } catch (error) {
223
- Task.warning('NBD configured but unusable', { error })
224
- nbdClient = undefined
225
- warn('error connecting to NBD server', { error, vdi: id, path })
226
- }
227
- } else {
228
- debug('useNbd is disabled', { vdi: id, path })
229
- }
230
-
231
202
  transferSize += await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
232
203
  // no checksum for VHDs, because they will be invalidated by
233
204
  // merges and chainings
234
205
  checksum: false,
235
206
  validator: tmpPath => checkVhd(handler, tmpPath),
236
207
  writeBlockConcurrency: this._backup.config.writeBlockConcurrency,
237
- nbdClient,
238
208
  })
239
209
 
240
210
  if (isDelta) {
@@ -45,6 +45,7 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
45
45
  data: {
46
46
  id: this._sr.uuid,
47
47
  isFull,
48
+ name_label: this._sr.name_label,
48
49
  type: 'SR',
49
50
  },
50
51
  })
@@ -21,6 +21,7 @@ exports.FullReplicationWriter = class FullReplicationWriter extends MixinReplica
21
21
  name: 'export',
22
22
  data: {
23
23
  id: props.sr.uuid,
24
+ name_label: this._sr.name_label,
24
25
  type: 'SR',
25
26
 
26
27
  // necessary?