@xen-orchestra/backups 0.31.0 → 0.33.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) {
@@ -660,7 +668,7 @@ class RemoteAdapter {
660
668
 
661
669
  async writeVhd(path, input, { checksum = true, validator = noop, writeBlockConcurrency, nbdClient } = {}) {
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,
@@ -674,17 +682,37 @@ class RemoteAdapter {
674
682
  await VhdAbstract.createAlias(handler, path, dataPath)
675
683
  return size
676
684
  } else {
677
- return this.outputStream(path, input, { checksum, validator })
685
+ const inputWithSize = await createVhdStreamWithLength(input)
686
+ return this.outputStream(path, inputWithSize, { checksum, validator, expectedSize: inputWithSize.length })
678
687
  }
679
688
  }
680
689
 
681
- async outputStream(path, input, { checksum = true, validator = noop } = {}) {
690
+ async outputStream(path, input, { checksum = true, validator = noop, expectedSize } = {}) {
682
691
  const container = watchStreamSize(input)
692
+
683
693
  await this._handler.outputStream(path, input, {
684
694
  checksum,
685
695
  dirMode: this._dirMode,
686
696
  async validator() {
687
697
  await input.task
698
+ if (expectedSize !== undefined) {
699
+ // check that we read all the stream
700
+ strictEqual(
701
+ container.size,
702
+ expectedSize,
703
+ `transferred size ${container.size}, expected file size : ${expectedSize}`
704
+ )
705
+ }
706
+ let size
707
+ try {
708
+ size = await this._handler.getSize(path)
709
+ } catch (err) {
710
+ // can fail is the remote is encrypted
711
+ }
712
+ if (size !== undefined) {
713
+ // check that everything is written to disk
714
+ strictEqual(size, container.size, `written size ${size}, transfered size : ${container.size}`)
715
+ }
688
716
  return validator.apply(this, arguments)
689
717
  },
690
718
  })
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
@@ -244,6 +246,7 @@ class VmBackup {
244
246
  fullVdisRequired,
245
247
  })
246
248
  const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
249
+ deltaExport.streams = mapValues(deltaExport.streams, this._throttleStream)
247
250
 
248
251
  const timestamp = Date.now()
249
252
 
@@ -285,10 +288,12 @@ class VmBackup {
285
288
 
286
289
  async _copyFull() {
287
290
  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
- })
291
+ const stream = this._throttleStream(
292
+ await this._xapi.VM_export(this.exportedVm.$ref, {
293
+ compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
294
+ useSnapshot: false,
295
+ })
296
+ )
292
297
  const sizeContainer = watchStreamSize(stream)
293
298
 
294
299
  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.31.0",
11
+ "version": "0.33.0",
12
12
  "engines": {
13
13
  "node": ">=14.6"
14
14
  },
@@ -17,6 +17,7 @@
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",
@@ -26,7 +27,7 @@
26
27
  "@vates/nbd-client": "^1.0.1",
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",
@@ -51,7 +52,7 @@
51
52
  "tmp": "^0.2.1"
52
53
  },
53
54
  "peerDependencies": {
54
- "@xen-orchestra/xapi": "^1.6.1"
55
+ "@xen-orchestra/xapi": "^2.0.0"
55
56
  },
56
57
  "license": "AGPL-3.0-or-later",
57
58
  "author": {
@@ -203,7 +203,7 @@ class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
203
203
  const vdiRef = vm.$xapi.getObject(vdi.uuid).$ref
204
204
 
205
205
  let nbdClient
206
- if (this._backup.config.useNbd) {
206
+ if (this._backup.config.useNbd && adapter.useVhdDirectory()) {
207
207
  debug('useNbd is enabled', { vdi: id, path })
208
208
  // get nbd if possible
209
209
  try {
@@ -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?