@xen-orchestra/backups 0.64.2 → 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.
@@ -94,11 +94,13 @@ export async function exportIncrementalVm(
94
94
  }
95
95
  })
96
96
 
97
+ // Get a fresh list of VM's VTPM to avoid `vm.VTPMs: [undefined]`
98
+ const vmVtpms = await vm.$xapi.getField('VM', vm.$ref, 'VTPMs')
97
99
  const vtpms = await Promise.all(
98
- vm.$VTPMs.map(async vtpm => {
100
+ vmVtpms.map(async vtpmRef => {
99
101
  let content
100
102
  try {
101
- content = await vm.$xapi.call('VTPM.get_contents', vtpm.$ref)
103
+ content = await vm.$xapi.call('VTPM.get_contents', vtpmRef)
102
104
  } catch (err) {
103
105
  console.error(err)
104
106
  }
@@ -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) {
@@ -25,7 +26,11 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
25
26
  const replicatedVdis = sr.$VDIs
26
27
  .filter(vdi => {
27
28
  // REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
28
- return baseUuidToSrcVdi.has(vdi?.other_config[COPY_OF])
29
+ return (
30
+ vdi?.managed &&
31
+ !vdi?.is_a_snapshot /* only look for real vdi */ &&
32
+ baseUuidToSrcVdi.has(vdi?.other_config[COPY_OF])
33
+ )
29
34
  })
30
35
  .map(({ other_config }) => other_config?.[COPY_OF])
31
36
  .filter(_ => !!_)
@@ -108,17 +113,21 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
108
113
 
109
114
  const replicatedVdis = sr.$VDIs.filter(vdi => {
110
115
  // REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
111
- return sourceVdiUuids.includes(vdi?.other_config[COPY_OF])
116
+ return vdi?.managed && !vdi?.is_a_snapshot && sourceVdiUuids.includes(vdi?.other_config[COPY_OF])
112
117
  })
113
118
 
114
119
  Object.values(backup.vdis).forEach(vdi => {
115
120
  vdi.other_config[COPY_OF] = vdi.uuid
116
121
  if (sourceVdiUuids.length > 0) {
117
- const baseReplicatedTo = replicatedVdis.find(
122
+ const baseReplicatedTo = replicatedVdis.filter(
118
123
  replicatedVdi => replicatedVdi.other_config[COPY_OF] === vdi.other_config[BASE_DELTA_VDI]
119
124
  )
125
+ assert.ok(
126
+ baseReplicatedTo.length <= 1,
127
+ `Target of a replication must be unique, got ${baseReplicatedTo.length} candidates`
128
+ )
120
129
  // baseReplicatedTo can be undefined if a new disk is added and other are already replicated
121
- vdi.baseVdi = baseReplicatedTo
130
+ vdi.baseVdi = baseReplicatedTo[0]
122
131
  } else {
123
132
  // first replication of this disk
124
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.2",
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": {