@xen-orchestra/backups 0.64.3 → 0.65.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.
@@ -9,6 +9,7 @@ import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
9
9
  import { FullRemote } from './_vmRunners/FullRemote.mjs'
10
10
  import { IncrementalRemote } from './_vmRunners/IncrementalRemote.mjs'
11
11
  import { Throttle } from '@vates/generator-toolbox'
12
+ import createStreamThrottle from './_createStreamThrottle.mjs'
12
13
 
13
14
  const noop = Function.prototype
14
15
 
@@ -41,7 +42,8 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
41
42
  const schedule = this._schedule
42
43
  const settings = this._settings
43
44
 
44
- const throttleGenerator = new Throttle()
45
+ const throttleGenerator = new Throttle(settings.maxExportRate)
46
+ const throttleStream = createStreamThrottle(settings.maxExportRate)
45
47
 
46
48
  const config = this._config
47
49
 
@@ -89,7 +91,8 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
89
91
  schedule,
90
92
  settings: vmSettings,
91
93
  sourceRemoteAdapter,
92
- throttleGenerator,
94
+ throttleGenerator, // for incrementals
95
+ throttleStream, // for full
93
96
  vmUuid,
94
97
  }
95
98
  let vmBackup
@@ -10,6 +10,7 @@ import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
10
10
  import { IncrementalXapi } from './_vmRunners/IncrementalXapi.mjs'
11
11
  import { FullXapi } from './_vmRunners/FullXapi.mjs'
12
12
  import { Throttle } from '@vates/generator-toolbox'
13
+ import createStreamThrottle from './_createStreamThrottle.mjs'
13
14
 
14
15
  const noop = Function.prototype
15
16
 
@@ -55,7 +56,8 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
55
56
  const schedule = this._schedule
56
57
  const settings = this._settings
57
58
 
58
- const throttleGenerator = new Throttle()
59
+ const throttleGenerator = new Throttle(settings.maxExportRate)
60
+ const throttleStream = createStreamThrottle(settings.maxExportRate)
59
61
 
60
62
  const config = this._config
61
63
 
@@ -148,6 +150,7 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
148
150
  settings: vmSettings,
149
151
  srs,
150
152
  throttleGenerator,
153
+ throttleStream,
151
154
  vm,
152
155
  }
153
156
 
@@ -16,7 +16,7 @@ export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote
16
16
  const transferList = await this._computeTransferList(({ mode }) => mode === 'full')
17
17
 
18
18
  for (const metadata of transferList) {
19
- const stream = await this._sourceRemoteAdapter.readFullVmBackup(metadata)
19
+ const stream = this._throttleStream(await this._sourceRemoteAdapter.readFullVmBackup(metadata))
20
20
  const sizeContainer = watchStreamSize(stream)
21
21
 
22
22
  // @todo shouldn't transfer backup if it will be deleted by retention policy (higher retention on source than destination)
@@ -49,16 +49,14 @@ export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
49
49
  const { compression } = this.job
50
50
  const vm = this._vm
51
51
  const exportedVm = this._exportedVm
52
- // @todo put back throttle for full backup/Replication
53
- const stream =
54
- /* this._throttleStream( */
52
+ const stream = this._throttleStream(
55
53
  (
56
54
  await this._xapi.VM_export(exportedVm.$ref, {
57
55
  compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
58
56
  useSnapshot: false,
59
57
  })
60
58
  ).body
61
- /* ) */
59
+ )
62
60
 
63
61
  const vdis = await exportedVm.$getDisks()
64
62
  let maxStreamLength = 1024 * 1024 // Ovf file and tar headers are a few KB, let's stay safe
@@ -10,7 +10,7 @@ import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs
10
10
  import { Disposable } from 'promise-toolbox'
11
11
  import { openVhd } from 'vhd-lib'
12
12
  import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
13
- import { SynchronizedDisk } from '@xen-orchestra/disk-transform'
13
+ import { SynchronizedDisk, ThrottledDisk } from '@xen-orchestra/disk-transform'
14
14
 
15
15
  const { warn } = createLogger('xo:backups:Incrementalremote')
16
16
  class IncrementalRemoteVmBackupRunner extends AbstractRemote {
@@ -71,8 +71,9 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
71
71
  const isVhdDifferencing = {}
72
72
 
73
73
  for (const key in incrementalExport.disks) {
74
- const disk = incrementalExport.disks[key]
74
+ let disk = incrementalExport.disks[key]
75
75
  isVhdDifferencing[key] = disk.isDifferencing()
76
+ disk = new ThrottledDisk(disk, this._throttleGenerator)
76
77
  incrementalExport.disks[key] = new SynchronizedDisk(disk)
77
78
  }
78
79
 
@@ -14,7 +14,7 @@ import {
14
14
  setVmDeltaChainLength,
15
15
  markExportSuccessfull,
16
16
  } from '../../_otherConfig.mjs'
17
- import { SynchronizedDisk } from '@xen-orchestra/disk-transform'
17
+ import { ThrottledDisk, SynchronizedDisk } from '@xen-orchestra/disk-transform'
18
18
 
19
19
  const { debug } = createLogger('xo:backups:IncrementalXapiVmBackup')
20
20
 
@@ -43,13 +43,14 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
43
43
  const isVhdDifferencing = {}
44
44
  let useNbd = false
45
45
  for (const key in deltaExport.disks) {
46
- const disk = deltaExport.disks[key]
46
+ let disk = deltaExport.disks[key]
47
47
  isVhdDifferencing[key] = disk.isDifferencing()
48
48
  if (!isFull && !isVhdDifferencing[key] && key !== exportedVm.$suspend_VDI?.$ref) {
49
49
  Task.warning('Backup fell back to a full')
50
50
  }
51
- deltaExport.disks[key] = new SynchronizedDisk(disk)
52
51
  useNbd = useNbd || disk.useNbd()
52
+ disk = new ThrottledDisk(disk, this._throttleGenerator)
53
+ deltaExport.disks[key] = new SynchronizedDisk(disk)
53
54
  }
54
55
  if (useNbd) {
55
56
  Task.info('Transfer data using NBD')
@@ -24,6 +24,7 @@ export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstrac
24
24
  settings,
25
25
  sourceRemoteAdapter,
26
26
  throttleGenerator,
27
+ throttleStream,
27
28
  vmUuid,
28
29
  }) {
29
30
  super()
@@ -37,6 +38,7 @@ export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstrac
37
38
  this._healthCheckSr = healthCheckSr
38
39
  this._sourceRemoteAdapter = sourceRemoteAdapter
39
40
  this._throttleGenerator = throttleGenerator
41
+ this._throttleStream = throttleStream
40
42
  this._vmUuid = vmUuid
41
43
 
42
44
  const allSettings = job.settings
@@ -29,6 +29,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
29
29
  settings,
30
30
  srs,
31
31
  throttleGenerator,
32
+ throttleStream,
32
33
  vm,
33
34
  }) {
34
35
  super()
@@ -63,6 +64,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
63
64
  this._jobId = job.id
64
65
  this._jobSnapshotVdis = undefined
65
66
  this._throttleGenerator = throttleGenerator
67
+ this._throttleStream = throttleStream
66
68
  this._xapi = vm.$xapi
67
69
 
68
70
  // Base VM for the export
@@ -10,6 +10,7 @@ import { AbstractIncrementalWriter } from './_AbstractIncrementalWriter.mjs'
10
10
  import { MixinXapiWriter } from './_MixinXapiWriter.mjs'
11
11
  import { listReplicatedVms } from './_listReplicatedVms.mjs'
12
12
  import { COPY_OF, setVmOtherConfig, BASE_DELTA_VDI } from '../../_otherConfig.mjs'
13
+ import assert from 'node:assert'
13
14
 
14
15
  export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
15
16
  async checkBaseVdis(baseUuidToSrcVdi) {
@@ -112,17 +113,21 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
112
113
 
113
114
  const replicatedVdis = sr.$VDIs.filter(vdi => {
114
115
  // REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
115
- return sourceVdiUuids.includes(vdi?.other_config[COPY_OF])
116
+ return vdi?.managed && !vdi?.is_a_snapshot && sourceVdiUuids.includes(vdi?.other_config[COPY_OF])
116
117
  })
117
118
 
118
119
  Object.values(backup.vdis).forEach(vdi => {
119
120
  vdi.other_config[COPY_OF] = vdi.uuid
120
121
  if (sourceVdiUuids.length > 0) {
121
- const baseReplicatedTo = replicatedVdis.find(
122
+ const baseReplicatedTo = replicatedVdis.filter(
122
123
  replicatedVdi => replicatedVdi.other_config[COPY_OF] === vdi.other_config[BASE_DELTA_VDI]
123
124
  )
125
+ assert.ok(
126
+ baseReplicatedTo.length <= 1,
127
+ `Target of a replication must be unique, got ${baseReplicatedTo.length} candidates`
128
+ )
124
129
  // baseReplicatedTo can be undefined if a new disk is added and other are already replicated
125
- vdi.baseVdi = baseReplicatedTo
130
+ vdi.baseVdi = baseReplicatedTo[0]
126
131
  } else {
127
132
  // first replication of this disk
128
133
  vdi.baseVdi = undefined
@@ -18,6 +18,8 @@ function formatVmBackup(backup) {
18
18
  }
19
19
  }
20
20
  return {
21
+ type: 'xo-vm-backup',
22
+ backupRepository: backup.backupRepositoryId,
21
23
  disks:
22
24
  backup.vhds === undefined
23
25
  ? []
@@ -38,6 +40,7 @@ function formatVmBackup(backup) {
38
40
  size: backup.size,
39
41
  timestamp: backup.timestamp,
40
42
  vm: {
43
+ uuid: backup.vm.uuid,
41
44
  name_description: backup.vm.name_description,
42
45
  name_label: backup.vm.name_label,
43
46
  },
@@ -48,7 +51,12 @@ function formatVmBackup(backup) {
48
51
  }
49
52
  }
50
53
 
51
- // format all backups as returned by RemoteAdapter#listAllVmBackups()
52
- export function formatVmBackups(backupsByVM) {
53
- return mapValues(backupsByVM, backups => backups.map(formatVmBackup))
54
+ /**
55
+ * format all backups as returned by RemoteAdapter#listAllVmBackups()
56
+ * @param {Record<string, object[]>} backupsByVM
57
+ * @param {string} backupRepositoryId
58
+ * @returns {Record<string, object[]>}
59
+ */
60
+ export function formatVmBackups(backupsByVM, backupRepositoryId) {
61
+ return mapValues(backupsByVM, backups => backups.map(backup => formatVmBackup({ ...backup, backupRepositoryId })))
54
62
  }
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.64.3",
11
+ "version": "0.65.0",
12
12
  "engines": {
13
13
  "node": ">=14.18"
14
14
  },
@@ -19,18 +19,18 @@
19
19
  "dependencies": {
20
20
  "@iarna/toml": "^2.2.5",
21
21
  "@kldzj/stream-throttle": "^1.1.1",
22
- "@vates/async-each": "^1.0.0",
22
+ "@vates/async-each": "^1.0.1",
23
23
  "@vates/cached-dns.lookup": "^1.0.0",
24
24
  "@vates/compose": "^2.1.0",
25
25
  "@vates/decorate-with": "^2.1.0",
26
26
  "@vates/disposable": "^0.1.6",
27
27
  "@vates/fuse-vhd": "^2.1.2",
28
- "@vates/generator-toolbox": "^1.0.4",
28
+ "@vates/generator-toolbox": "^1.1.0",
29
29
  "@vates/nbd-client": "^3.2.1",
30
30
  "@vates/parse-duration": "^0.1.1",
31
31
  "@xen-orchestra/async-map": "^0.1.2",
32
- "@xen-orchestra/disk-transform": "^1.1.0",
33
- "@xen-orchestra/fs": "^4.6.3",
32
+ "@xen-orchestra/disk-transform": "^1.2.0",
33
+ "@xen-orchestra/fs": "^4.6.4",
34
34
  "@xen-orchestra/log": "^0.7.1",
35
35
  "@xen-orchestra/qcow2": "^1.0.0",
36
36
  "@xen-orchestra/template": "^0.1.0",
@@ -51,18 +51,18 @@
51
51
  "tar": "^6.1.15",
52
52
  "uuid": "^9.0.0",
53
53
  "value-matcher": "^0.2.0",
54
- "vhd-lib": "^4.14.1",
54
+ "vhd-lib": "^4.14.2",
55
55
  "xen-api": "^4.7.4",
56
56
  "yazl": "^2.5.1"
57
57
  },
58
58
  "devDependencies": {
59
59
  "fs-extra": "^11.1.0",
60
- "rimraf": "^5.0.1",
60
+ "rimraf": "^6.0.1",
61
61
  "sinon": "^18.0.0",
62
62
  "tmp": "^0.2.1"
63
63
  },
64
64
  "peerDependencies": {
65
- "@xen-orchestra/xapi": "^8.4.1"
65
+ "@xen-orchestra/xapi": "^8.4.2"
66
66
  },
67
67
  "license": "AGPL-3.0-or-later",
68
68
  "author": {