@xen-orchestra/backups 0.44.1 → 0.44.3

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.
@@ -4,7 +4,14 @@ import { formatFilenameDate } from './_filenameDate.mjs'
4
4
  import { importIncrementalVm } from './_incrementalVm.mjs'
5
5
  import { Task } from './Task.mjs'
6
6
  import { watchStreamSize } from './_watchStreamSize.mjs'
7
+ import { VhdNegative, VhdSynthetic } from 'vhd-lib'
8
+ import { decorateClass } from '@vates/decorate-with'
9
+ import { createLogger } from '@xen-orchestra/log'
10
+ import { dirname, join } from 'node:path'
11
+ import pickBy from 'lodash/pickBy.js'
12
+ import { defer } from 'golike-defer'
7
13
 
14
+ const { debug, info, warn } = createLogger('xo:backups:importVmBackup')
8
15
  async function resolveUuid(xapi, cache, uuid, type) {
9
16
  if (uuid == null) {
10
17
  return uuid
@@ -21,17 +28,181 @@ export class ImportVmBackup {
21
28
  metadata,
22
29
  srUuid,
23
30
  xapi,
24
- settings: { additionnalVmTag, newMacAddresses, mapVdisSrs = {} } = {},
31
+ settings: { additionnalVmTag, newMacAddresses, mapVdisSrs = {}, useDifferentialRestore = false } = {},
25
32
  }) {
26
33
  this._adapter = adapter
27
- this._importIncrementalVmSettings = { additionnalVmTag, newMacAddresses, mapVdisSrs }
34
+ this._importIncrementalVmSettings = { additionnalVmTag, newMacAddresses, mapVdisSrs, useDifferentialRestore }
28
35
  this._metadata = metadata
29
36
  this._srUuid = srUuid
30
37
  this._xapi = xapi
31
38
  }
32
39
 
33
- async #decorateIncrementalVmMetadata(backup) {
34
- const { additionnalVmTag, mapVdisSrs } = this._importIncrementalVmSettings
40
+ async #getPathOfVdiSnapshot(snapshotUuid) {
41
+ const metadata = this._metadata
42
+ if (this._pathToVdis === undefined) {
43
+ const backups = await this._adapter.listVmBackups(
44
+ this._metadata.vm.uuid,
45
+ ({ mode, timestamp }) => mode === 'delta' && timestamp >= metadata.timestamp
46
+ )
47
+ const map = new Map()
48
+ for (const backup of backups) {
49
+ for (const [vdiRef, vdi] of Object.entries(backup.vdis)) {
50
+ map.set(vdi.uuid, backup.vhds[vdiRef])
51
+ }
52
+ }
53
+ this._pathToVdis = map
54
+ }
55
+ return this._pathToVdis.get(snapshotUuid)
56
+ }
57
+
58
+ async _reuseNearestSnapshot($defer, ignoredVdis) {
59
+ const metadata = this._metadata
60
+ const { mapVdisSrs } = this._importIncrementalVmSettings
61
+ const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
62
+ const streams = {}
63
+ const metdataDir = dirname(metadata._filename)
64
+ const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
65
+
66
+ for (const [vdiRef, vdi] of Object.entries(vdis)) {
67
+ const vhdPath = join(metdataDir, vhds[vdiRef])
68
+
69
+ let xapiDisk
70
+ try {
71
+ xapiDisk = await this._xapi.getRecordByUuid('VDI', vdi.$snapshot_of$uuid)
72
+ } catch (err) {
73
+ // if this disk is not present anymore, fall back to default restore
74
+ warn(err)
75
+ }
76
+
77
+ let snapshotCandidate, backupCandidate
78
+ if (xapiDisk !== undefined) {
79
+ debug('found disks, wlll search its snapshots', { snapshots: xapiDisk.snapshots })
80
+ for (const snapshotRef of xapiDisk.snapshots) {
81
+ const snapshot = await this._xapi.getRecord('VDI', snapshotRef)
82
+ debug('handling snapshot', { snapshot })
83
+
84
+ // take only the first snapshot
85
+ if (snapshotCandidate && snapshotCandidate.snapshot_time < snapshot.snapshot_time) {
86
+ debug('already got a better candidate')
87
+ continue
88
+ }
89
+
90
+ // have a corresponding backup more recent than metadata ?
91
+ const pathToSnapshotData = await this.#getPathOfVdiSnapshot(snapshot.uuid)
92
+ if (pathToSnapshotData === undefined) {
93
+ debug('no backup linked to this snaphot')
94
+ continue
95
+ }
96
+ if (snapshot.$SR.uuid !== (mapVdisSrs[vdi.$snapshot_of$uuid] ?? this._srUuid)) {
97
+ debug('not restored on the same SR', { snapshotSr: snapshot.$SR.uuid, mapVdisSrs, srUuid: this._srUuid })
98
+ continue
99
+ }
100
+
101
+ debug('got a candidate', pathToSnapshotData)
102
+
103
+ snapshotCandidate = snapshot
104
+ backupCandidate = pathToSnapshotData
105
+ }
106
+ }
107
+
108
+ let stream
109
+ const backupWithSnapshotPath = join(metdataDir, backupCandidate ?? '')
110
+ if (vhdPath === backupWithSnapshotPath) {
111
+ // all the data are already on the host
112
+ debug('direct reuse of a snapshot')
113
+ stream = null
114
+ vdis[vdiRef].baseVdi = snapshotCandidate
115
+ // go next disk , we won't use this stream
116
+ continue
117
+ }
118
+
119
+ let disposableDescendants
120
+
121
+ const disposableSynthetic = await VhdSynthetic.fromVhdChain(this._adapter._handler, vhdPath)
122
+
123
+ // this will also clean if another disk of this VM backup fails
124
+ // if user really only need to restore non failing disks he can retry with ignoredVdis
125
+ let disposed = false
126
+ const disposeOnce = async () => {
127
+ if (!disposed) {
128
+ disposed = true
129
+ try {
130
+ await disposableDescendants?.dispose()
131
+ await disposableSynthetic?.dispose()
132
+ } catch (error) {
133
+ warn('openVhd: failed to dispose VHDs', { error })
134
+ }
135
+ }
136
+ }
137
+ $defer.onFailure(() => disposeOnce())
138
+
139
+ const parentVhd = disposableSynthetic.value
140
+ await parentVhd.readBlockAllocationTable()
141
+ debug('got vhd synthetic of parents', parentVhd.length)
142
+
143
+ if (snapshotCandidate !== undefined) {
144
+ try {
145
+ debug('will try to use differential restore', {
146
+ backupWithSnapshotPath,
147
+ vhdPath,
148
+ vdiRef,
149
+ })
150
+
151
+ disposableDescendants = await VhdSynthetic.fromVhdChain(this._adapter._handler, backupWithSnapshotPath, {
152
+ until: vhdPath,
153
+ })
154
+ const descendantsVhd = disposableDescendants.value
155
+ await descendantsVhd.readBlockAllocationTable()
156
+ debug('got vhd synthetic of descendants')
157
+ const negativeVhd = new VhdNegative(parentVhd, descendantsVhd)
158
+ debug('got vhd negative')
159
+
160
+ // update the stream with the negative vhd stream
161
+ stream = await negativeVhd.stream()
162
+ vdis[vdiRef].baseVdi = snapshotCandidate
163
+ } catch (err) {
164
+ // can be a broken VHD chain, a vhd chain with a key backup, ....
165
+ // not an irrecuperable error, don't dispose parentVhd, and fallback to full restore
166
+ warn(`can't use differential restore`, err)
167
+ disposableDescendants?.dispose()
168
+ }
169
+ }
170
+ // didn't make a negative stream : fallback to classic stream
171
+ if (stream === undefined) {
172
+ debug('use legacy restore')
173
+ stream = await parentVhd.stream()
174
+ }
175
+
176
+ stream.on('end', disposeOnce)
177
+ stream.on('close', disposeOnce)
178
+ stream.on('error', disposeOnce)
179
+ info('everything is ready, will transfer', stream.length)
180
+ streams[`${vdiRef}.vhd`] = stream
181
+ }
182
+ return {
183
+ streams,
184
+ vbds,
185
+ vdis,
186
+ version: '1.0.0',
187
+ vifs,
188
+ vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
189
+ }
190
+ }
191
+
192
+ async #decorateIncrementalVmMetadata() {
193
+ const { additionnalVmTag, mapVdisSrs, useDifferentialRestore } = this._importIncrementalVmSettings
194
+
195
+ const ignoredVdis = new Set(
196
+ Object.entries(mapVdisSrs)
197
+ .filter(([_, srUuid]) => srUuid === null)
198
+ .map(([vdiUuid]) => vdiUuid)
199
+ )
200
+ let backup
201
+ if (useDifferentialRestore) {
202
+ backup = await this._reuseNearestSnapshot(ignoredVdis)
203
+ } else {
204
+ backup = await this._adapter.readIncrementalVmBackup(this._metadata, ignoredVdis)
205
+ }
35
206
  const xapi = this._xapi
36
207
 
37
208
  const cache = new Map()
@@ -55,7 +226,7 @@ export class ImportVmBackup {
55
226
  const isFull = metadata.mode === 'full'
56
227
 
57
228
  const sizeContainer = { size: 0 }
58
- const { mapVdisSrs, newMacAddresses } = this._importIncrementalVmSettings
229
+ const { newMacAddresses } = this._importIncrementalVmSettings
59
230
  let backup
60
231
  if (isFull) {
61
232
  backup = await adapter.readFullVmBackup(metadata)
@@ -63,12 +234,7 @@ export class ImportVmBackup {
63
234
  } else {
64
235
  assert.strictEqual(metadata.mode, 'delta')
65
236
 
66
- const ignoredVdis = new Set(
67
- Object.entries(mapVdisSrs)
68
- .filter(([_, srUuid]) => srUuid === null)
69
- .map(([vdiUuid]) => vdiUuid)
70
- )
71
- backup = await this.#decorateIncrementalVmMetadata(await adapter.readIncrementalVmBackup(metadata, ignoredVdis))
237
+ backup = await this.#decorateIncrementalVmMetadata()
72
238
  Object.values(backup.streams).forEach(stream => watchStreamSize(stream, sizeContainer))
73
239
  }
74
240
 
@@ -110,3 +276,5 @@ export class ImportVmBackup {
110
276
  )
111
277
  }
112
278
  }
279
+
280
+ decorateClass(ImportVmBackup, { _reuseNearestSnapshot: defer })
package/_cleanVm.mjs CHANGED
@@ -36,34 +36,32 @@ const computeVhdsSize = (handler, vhdPaths) =>
36
36
  )
37
37
 
38
38
  // chain is [ ancestor, child_1, ..., child_n ]
39
- async function _mergeVhdChain(handler, chain, { logInfo, remove, merge, mergeBlockConcurrency }) {
40
- if (merge) {
41
- logInfo(`merging VHD chain`, { chain })
42
-
43
- let done, total
44
- const handle = setInterval(() => {
45
- if (done !== undefined) {
46
- logInfo('merge in progress', {
47
- done,
48
- parent: chain[0],
49
- progress: Math.round((100 * done) / total),
50
- total,
51
- })
52
- }
53
- }, 10e3)
54
- try {
55
- return await mergeVhdChain(handler, chain, {
56
- logInfo,
57
- mergeBlockConcurrency,
58
- onProgress({ done: d, total: t }) {
59
- done = d
60
- total = t
61
- },
62
- removeUnused: remove,
39
+ async function _mergeVhdChain(handler, chain, { logInfo, remove, mergeBlockConcurrency }) {
40
+ logInfo(`merging VHD chain`, { chain })
41
+
42
+ let done, total
43
+ const handle = setInterval(() => {
44
+ if (done !== undefined) {
45
+ logInfo('merge in progress', {
46
+ done,
47
+ parent: chain[0],
48
+ progress: Math.round((100 * done) / total),
49
+ total,
63
50
  })
64
- } finally {
65
- clearInterval(handle)
66
51
  }
52
+ }, 10e3)
53
+ try {
54
+ return await mergeVhdChain(handler, chain, {
55
+ logInfo,
56
+ mergeBlockConcurrency,
57
+ onProgress({ done: d, total: t }) {
58
+ done = d
59
+ total = t
60
+ },
61
+ removeUnused: remove,
62
+ })
63
+ } finally {
64
+ clearInterval(handle)
67
65
  }
68
66
  }
69
67
 
@@ -471,23 +469,20 @@ export async function cleanVm(
471
469
  const metadataWithMergedVhd = {}
472
470
  const doMerge = async () => {
473
471
  await asyncMap(toMerge, async chain => {
474
- const merged = await limitedMergeVhdChain(handler, chain, {
472
+ const { finalVhdSize } = await limitedMergeVhdChain(handler, chain, {
475
473
  logInfo,
476
474
  logWarn,
477
475
  remove,
478
- merge,
479
476
  mergeBlockConcurrency,
480
477
  })
481
- if (merged !== undefined) {
482
- const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metada file
483
- metadataWithMergedVhd[metadataPath] = true
484
- }
478
+ const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metada file
479
+ metadataWithMergedVhd[metadataPath] = (metadataWithMergedVhd[metadataPath] ?? 0) + finalVhdSize
485
480
  })
486
481
  }
487
482
 
488
483
  await Promise.all([
489
484
  ...unusedVhdsDeletion,
490
- toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : doMerge()),
485
+ toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : () => Promise.resolve()),
491
486
  asyncMap(unusedXvas, path => {
492
487
  logWarn('unused XVA', { path })
493
488
  if (remove) {
@@ -509,12 +504,11 @@ export async function cleanVm(
509
504
 
510
505
  // update size for delta metadata with merged VHD
511
506
  // check for the other that the size is the same as the real file size
512
-
513
507
  await asyncMap(jsons, async metadataPath => {
514
508
  const metadata = backups.get(metadataPath)
515
509
 
516
510
  let fileSystemSize
517
- const merged = metadataWithMergedVhd[metadataPath] !== undefined
511
+ const mergedSize = metadataWithMergedVhd[metadataPath]
518
512
 
519
513
  const { mode, size, vhds, xva } = metadata
520
514
 
@@ -524,26 +518,29 @@ export async function cleanVm(
524
518
  const linkedXva = resolve('/', vmDir, xva)
525
519
  try {
526
520
  fileSystemSize = await handler.getSize(linkedXva)
521
+ if (fileSystemSize !== size && fileSystemSize !== undefined) {
522
+ logWarn('cleanVm: incorrect backup size in metadata', {
523
+ path: metadataPath,
524
+ actual: size ?? 'none',
525
+ expected: fileSystemSize,
526
+ })
527
+ }
527
528
  } catch (error) {
528
529
  // can fail with encrypted remote
529
530
  }
530
531
  } else if (mode === 'delta') {
531
- const linkedVhds = Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
532
- fileSystemSize = await computeVhdsSize(handler, linkedVhds)
533
-
534
- // the size is not computed in some cases (e.g. VhdDirectory)
535
- if (fileSystemSize === undefined) {
536
- return
537
- }
538
-
539
532
  // don't warn if the size has changed after a merge
540
- if (!merged && fileSystemSize !== size) {
541
- // FIXME: figure out why it occurs so often and, once fixed, log the real problems with `logWarn`
542
- console.warn('cleanVm: incorrect backup size in metadata', {
543
- path: metadataPath,
544
- actual: size ?? 'none',
545
- expected: fileSystemSize,
546
- })
533
+ if (mergedSize === undefined) {
534
+ const linkedVhds = Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
535
+ fileSystemSize = await computeVhdsSize(handler, linkedVhds)
536
+ // the size is not computed in some cases (e.g. VhdDirectory)
537
+ if (fileSystemSize !== undefined && fileSystemSize !== size) {
538
+ logWarn('cleanVm: incorrect backup size in metadata', {
539
+ path: metadataPath,
540
+ actual: size ?? 'none',
541
+ expected: fileSystemSize,
542
+ })
543
+ }
547
544
  }
548
545
  }
549
546
  } catch (error) {
@@ -551,9 +548,19 @@ export async function cleanVm(
551
548
  return
552
549
  }
553
550
 
554
- // systematically update size after a merge
555
- if ((merged || fixMetadata) && size !== fileSystemSize) {
556
- metadata.size = fileSystemSize
551
+ // systematically update size and differentials after a merge
552
+
553
+ // @todo : after 2024-04-01 remove the fixmetadata options since the size computation is fixed
554
+ if (mergedSize || (fixMetadata && fileSystemSize !== size)) {
555
+ metadata.size = mergedSize ?? fileSystemSize ?? size
556
+
557
+ if (mergedSize) {
558
+ // all disks are now key disk
559
+ metadata.isVhdDifferencing = {}
560
+ for (const id of Object.values(metadata.vdis ?? {})) {
561
+ metadata.isVhdDifferencing[`${id}.vhd`] = false
562
+ }
563
+ }
557
564
  mustRegenerateCache = true
558
565
  try {
559
566
  await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
@@ -34,6 +34,7 @@ export async function exportIncrementalVm(
34
34
  fullVdisRequired = new Set(),
35
35
 
36
36
  disableBaseTags = false,
37
+ nbdConcurrency = 1,
37
38
  preferNbd,
38
39
  } = {}
39
40
  ) {
@@ -82,6 +83,7 @@ export async function exportIncrementalVm(
82
83
  baseRef: baseVdi?.$ref,
83
84
  cancelToken,
84
85
  format: 'vhd',
86
+ nbdConcurrency,
85
87
  preferNbd,
86
88
  })
87
89
  })
@@ -250,6 +252,10 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
250
252
  // Import VDI contents.
251
253
  cancelableMap(cancelToken, Object.entries(newVdis), async (cancelToken, [id, vdi]) => {
252
254
  for (let stream of ensureArray(streams[`${id}.vhd`])) {
255
+ if (stream === null) {
256
+ // we restore a backup and reuse completly a local snapshot
257
+ continue
258
+ }
253
259
  if (typeof stream === 'function') {
254
260
  stream = await stream()
255
261
  }
@@ -32,10 +32,10 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
32
32
  useChain: false,
33
33
  })
34
34
 
35
- const differentialVhds = {}
35
+ const isVhdDifferencing = {}
36
36
 
37
37
  await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
38
- differentialVhds[key] = await isVhdDifferencingDisk(stream)
38
+ isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
39
39
  })
40
40
 
41
41
  incrementalExport.streams = mapValues(incrementalExport.streams, this._throttleStream)
@@ -43,7 +43,7 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
43
43
  writer =>
44
44
  writer.transfer({
45
45
  deltaExport: forkDeltaExport(incrementalExport),
46
- differentialVhds,
46
+ isVhdDifferencing,
47
47
  timestamp: metadata.timestamp,
48
48
  vm: metadata.vm,
49
49
  vmSnapshot: metadata.vmSnapshot,
@@ -41,6 +41,7 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
41
41
 
42
42
  const deltaExport = await exportIncrementalVm(exportedVm, baseVm, {
43
43
  fullVdisRequired,
44
+ nbdConcurrency: this._settings.nbdConcurrency,
44
45
  preferNbd: this._settings.preferNbd,
45
46
  })
46
47
  // since NBD is network based, if one disk use nbd , all the disk use them
@@ -49,11 +50,11 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
49
50
  Task.info('Transfer data using NBD')
50
51
  }
51
52
 
52
- const differentialVhds = {}
53
+ const isVhdDifferencing = {}
53
54
  // since isVhdDifferencingDisk is reading and unshifting data in stream
54
55
  // it should be done BEFORE any other stream transform
55
56
  await asyncEach(Object.entries(deltaExport.streams), async ([key, stream]) => {
56
- differentialVhds[key] = await isVhdDifferencingDisk(stream)
57
+ isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
57
58
  })
58
59
  const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
59
60
 
@@ -68,7 +69,7 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
68
69
  writer =>
69
70
  writer.transfer({
70
71
  deltaExport: forkDeltaExport(deltaExport),
71
- differentialVhds,
72
+ isVhdDifferencing,
72
73
  sizeContainers,
73
74
  timestamp,
74
75
  vm,
@@ -133,7 +133,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
133
133
  }
134
134
  }
135
135
 
136
- async _transfer($defer, { differentialVhds, timestamp, deltaExport, vm, vmSnapshot }) {
136
+ async _transfer($defer, { isVhdDifferencing, timestamp, deltaExport, vm, vmSnapshot }) {
137
137
  const adapter = this._adapter
138
138
  const job = this._job
139
139
  const scheduleId = this._scheduleId
@@ -161,6 +161,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
161
161
  )
162
162
 
163
163
  metadataContent = {
164
+ isVhdDifferencing,
164
165
  jobId,
165
166
  mode: job.mode,
166
167
  scheduleId,
@@ -180,9 +181,9 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
180
181
  async ([id, vdi]) => {
181
182
  const path = `${this._vmBackupDir}/${vhds[id]}`
182
183
 
183
- const isDelta = differentialVhds[`${id}.vhd`]
184
+ const isDifferencing = isVhdDifferencing[`${id}.vhd`]
184
185
  let parentPath
185
- if (isDelta) {
186
+ if (isDifferencing) {
186
187
  const vdiDir = dirname(path)
187
188
  parentPath = (
188
189
  await handler.list(vdiDir, {
@@ -204,16 +205,20 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
204
205
  // TODO remove when this has been done before the export
205
206
  await checkVhd(handler, parentPath)
206
207
  }
207
-
208
- transferSize += await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
208
+
209
+ // don't write it as transferSize += await async function
210
+ // since i += await asyncFun lead to race condition
211
+ // as explained : https://eslint.org/docs/latest/rules/require-atomic-updates
212
+ const transferSizeOneDisk = await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
209
213
  // no checksum for VHDs, because they will be invalidated by
210
214
  // merges and chainings
211
215
  checksum: false,
212
216
  validator: tmpPath => checkVhd(handler, tmpPath),
213
217
  writeBlockConcurrency: this._config.writeBlockConcurrency,
214
218
  })
219
+ transferSize += transferSizeOneDisk
215
220
 
216
- if (isDelta) {
221
+ if (isDifferencing) {
217
222
  await chainVhd(handler, parentPath, handler, path)
218
223
  }
219
224
 
@@ -2,6 +2,8 @@ import mapValues from 'lodash/mapValues.js'
2
2
  import { dirname } from 'node:path'
3
3
 
4
4
  function formatVmBackup(backup) {
5
+ const { isVhdDifferencing } = backup
6
+
5
7
  return {
6
8
  disks:
7
9
  backup.vhds === undefined
@@ -25,6 +27,10 @@ function formatVmBackup(backup) {
25
27
  name_description: backup.vm.name_description,
26
28
  name_label: backup.vm.name_label,
27
29
  },
30
+
31
+ // isVhdDifferencing is either undefined or an object
32
+ differencingVhds: isVhdDifferencing && Object.values(isVhdDifferencing).filter(t => t).length,
33
+ dynamicVhds: isVhdDifferencing && Object.values(isVhdDifferencing).filter(t => !t).length,
28
34
  }
29
35
  }
30
36
 
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.44.1",
11
+ "version": "0.44.3",
12
12
  "engines": {
13
13
  "node": ">=14.18"
14
14
  },
@@ -25,7 +25,7 @@
25
25
  "@vates/decorate-with": "^2.0.0",
26
26
  "@vates/disposable": "^0.1.5",
27
27
  "@vates/fuse-vhd": "^2.0.0",
28
- "@vates/nbd-client": "^2.0.1",
28
+ "@vates/nbd-client": "^3.0.0",
29
29
  "@vates/parse-duration": "^0.1.1",
30
30
  "@xen-orchestra/async-map": "^0.1.2",
31
31
  "@xen-orchestra/fs": "^4.1.3",
@@ -44,7 +44,7 @@
44
44
  "proper-lockfile": "^4.1.2",
45
45
  "tar": "^6.1.15",
46
46
  "uuid": "^9.0.0",
47
- "vhd-lib": "^4.6.1",
47
+ "vhd-lib": "^4.8.0",
48
48
  "xen-api": "^2.0.0",
49
49
  "yazl": "^2.5.1"
50
50
  },
@@ -56,7 +56,7 @@
56
56
  "tmp": "^0.2.1"
57
57
  },
58
58
  "peerDependencies": {
59
- "@xen-orchestra/xapi": "^4.0.0"
59
+ "@xen-orchestra/xapi": "^4.1.0"
60
60
  },
61
61
  "license": "AGPL-3.0-or-later",
62
62
  "author": {