@xen-orchestra/backups 0.27.1 → 0.27.4

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
@@ -245,7 +245,7 @@ exports.Backup = class Backup {
245
245
  })
246
246
  )
247
247
  ),
248
- () => settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined,
248
+ () => (settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined),
249
249
  async (srs, remoteAdapters, healthCheckSr) => {
250
250
  // remove adapters that failed (already handled)
251
251
  remoteAdapters = remoteAdapters.filter(_ => _ !== undefined)
package/RemoteAdapter.js CHANGED
@@ -279,7 +279,7 @@ class RemoteAdapter {
279
279
  const dirs = new Set(files.map(file => dirname(file)))
280
280
  for (const dir of dirs) {
281
281
  // don't merge in main process, unused VHDs will be merged in the next backup run
282
- await this.cleanVm(dir, { remove: true, onLog: warn })
282
+ await this.cleanVm(dir, { remove: true, logWarn: warn })
283
283
  }
284
284
 
285
285
  const dedupedVmUuid = new Set(metadatas.map(_ => _.vm.uuid))
package/Task.js CHANGED
@@ -3,8 +3,10 @@
3
3
  const CancelToken = require('promise-toolbox/CancelToken')
4
4
  const Zone = require('node-zone')
5
5
 
6
- const logAfterEnd = () => {
7
- throw new Error('task has already ended')
6
+ const logAfterEnd = log => {
7
+ const error = new Error('task has already ended')
8
+ error.log = log
9
+ throw error
8
10
  }
9
11
 
10
12
  const noop = Function.prototype
package/_VmBackup.js CHANGED
@@ -128,42 +128,49 @@ class VmBackup {
128
128
  }
129
129
 
130
130
  // calls fn for each function, warns of any errors, and throws only if there are no writers left
131
- async _callWriters(fn, warnMessage, parallel = true) {
131
+ async _callWriters(fn, step, parallel = true) {
132
132
  const writers = this._writers
133
133
  const n = writers.size
134
134
  if (n === 0) {
135
135
  return
136
136
  }
137
- if (n === 1) {
138
- const [writer] = writers
137
+
138
+ async function callWriter(writer) {
139
+ const { name } = writer.constructor
139
140
  try {
141
+ debug('writer step starting', { step, writer: name })
140
142
  await fn(writer)
143
+ debug('writer step succeeded', { duration: step, writer: name })
141
144
  } catch (error) {
142
145
  writers.delete(writer)
146
+
147
+ warn('writer step failed', { error, step, writer: name })
148
+
149
+ // these two steps are the only one that are not already in their own sub tasks
150
+ if (step === 'writer.checkBaseVdis()' || step === 'writer.beforeBackup()') {
151
+ Task.warning(
152
+ `the writer ${name} has failed the step ${step} with error ${error.message}. It won't be used anymore in this job execution.`
153
+ )
154
+ }
155
+
143
156
  throw error
144
157
  }
145
- return
158
+ }
159
+ if (n === 1) {
160
+ const [writer] = writers
161
+ return callWriter(writer)
146
162
  }
147
163
 
148
164
  const errors = []
149
165
  await (parallel ? asyncMap : asyncEach)(writers, async function (writer) {
150
166
  try {
151
- await fn(writer)
167
+ await callWriter(writer)
152
168
  } catch (error) {
153
169
  errors.push(error)
154
- this.delete(writer)
155
- warn(warnMessage, { error, writer: writer.constructor.name })
156
-
157
- // these two steps are the only one that are not already in their own sub tasks
158
- if (warnMessage === 'writer.checkBaseVdis()' || warnMessage === 'writer.beforeBackup()') {
159
- Task.warning(
160
- `the writer ${writer.constructor.name} has failed the step ${warnMessage} with error ${error.message}. It won't be used anymore in this job execution.`
161
- )
162
- }
163
170
  }
164
171
  })
165
172
  if (writers.size === 0) {
166
- throw new AggregateError(errors, 'all targets have failed, step: ' + warnMessage)
173
+ throw new AggregateError(errors, 'all targets have failed, step: ' + step)
167
174
  }
168
175
  }
169
176
 
package/_cleanVm.js CHANGED
@@ -1,23 +1,27 @@
1
1
  'use strict'
2
2
 
3
- const assert = require('assert')
4
3
  const sum = require('lodash/sum')
5
4
  const UUID = require('uuid')
6
5
  const { asyncMap } = require('@xen-orchestra/async-map')
7
- const { Constants, mergeVhd, openVhd, VhdAbstract, VhdFile } = require('vhd-lib')
6
+ const { Constants, openVhd, VhdAbstract, VhdFile } = require('vhd-lib')
8
7
  const { isVhdAlias, resolveVhdAlias } = require('vhd-lib/aliases')
9
8
  const { dirname, resolve } = require('path')
10
9
  const { DISK_TYPES } = Constants
11
10
  const { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } = require('./_backupType.js')
12
11
  const { limitConcurrency } = require('limit-concurrency-decorator')
12
+ const { mergeVhdChain } = require('vhd-lib/merge')
13
13
 
14
14
  const { Task } = require('./Task.js')
15
15
  const { Disposable } = require('promise-toolbox')
16
+ const handlerPath = require('@xen-orchestra/fs/path')
16
17
 
17
18
  // checking the size of a vhd directory is costly
18
19
  // 1 Http Query per 1000 blocks
19
20
  // we only check size of all the vhd are VhdFiles
20
- function shouldComputeVhdsSize(vhds) {
21
+ function shouldComputeVhdsSize(handler, vhds) {
22
+ if (handler.isEncrypted) {
23
+ return false
24
+ }
21
25
  return vhds.every(vhd => vhd instanceof VhdFile)
22
26
  }
23
27
 
@@ -25,63 +29,48 @@ const computeVhdsSize = (handler, vhdPaths) =>
25
29
  Disposable.use(
26
30
  vhdPaths.map(vhdPath => openVhd(handler, vhdPath)),
27
31
  async vhds => {
28
- if (shouldComputeVhdsSize(vhds)) {
32
+ if (shouldComputeVhdsSize(handler, vhds)) {
29
33
  const sizes = await asyncMap(vhds, vhd => vhd.getSize())
30
34
  return sum(sizes)
31
35
  }
32
36
  }
33
37
  )
34
38
 
35
- // chain is [ ancestor, child1, ..., childn]
36
- // 1. Create a VhdSynthetic from all children
37
- // 2. Merge the VhdSynthetic into the ancestor
38
- // 3. Delete all (now) unused VHDs
39
- // 4. Rename the ancestor with the merged data to the latest child
40
- //
41
- // VhdSynthetic
42
- // |
43
- // /‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\
44
- // [ ancestor, child1, ...,child n-1, childn ]
45
- // | \___________________/ ^
46
- // | | |
47
- // | unused VHDs |
48
- // | |
49
- // \___________rename_____________/
50
-
51
- async function mergeVhdChain(chain, { handler, logInfo, remove, merge }) {
52
- assert(chain.length >= 2)
53
- const chainCopy = [...chain]
54
- const parent = chainCopy.shift()
55
- const children = chainCopy
56
-
39
+ // chain is [ ancestor, child_1, ..., child_n ]
40
+ async function _mergeVhdChain(handler, chain, { logInfo, remove, merge }) {
57
41
  if (merge) {
58
- logInfo(`merging children into parent`, { childrenCount: children.length, parent })
42
+ logInfo(`merging VHD chain`, { chain })
59
43
 
60
44
  let done, total
61
45
  const handle = setInterval(() => {
62
46
  if (done !== undefined) {
63
- logInfo(`merging children in progress`, { children, parent, doneCount: done, totalCount: total })
47
+ logInfo('merge in progress', {
48
+ done,
49
+ parent: chain[0],
50
+ progress: Math.round((100 * done) / total),
51
+ total,
52
+ })
64
53
  }
65
54
  }, 10e3)
66
-
67
- const mergedSize = await mergeVhd(handler, parent, handler, children, {
68
- logInfo,
69
- onProgress({ done: d, total: t }) {
70
- done = d
71
- total = t
72
- },
73
- remove,
74
- })
75
-
76
- clearInterval(handle)
77
- return mergedSize
55
+ try {
56
+ return await mergeVhdChain(handler, chain, {
57
+ logInfo,
58
+ onProgress({ done: d, total: t }) {
59
+ done = d
60
+ total = t
61
+ },
62
+ removeUnused: remove,
63
+ })
64
+ } finally {
65
+ clearInterval(handle)
66
+ }
78
67
  }
79
68
  }
80
69
 
81
70
  const noop = Function.prototype
82
71
 
83
72
  const INTERRUPTED_VHDS_REG = /^\.(.+)\.merge.json$/
84
- const listVhds = async (handler, vmDir) => {
73
+ const listVhds = async (handler, vmDir, logWarn) => {
85
74
  const vhds = new Set()
86
75
  const aliases = {}
87
76
  const interruptedVhds = new Map()
@@ -101,12 +90,23 @@ const listVhds = async (handler, vmDir) => {
101
90
  filter: file => isVhdFile(file) || INTERRUPTED_VHDS_REG.test(file),
102
91
  })
103
92
  aliases[vdiDir] = list.filter(vhd => isVhdAlias(vhd)).map(file => `${vdiDir}/${file}`)
104
- list.forEach(file => {
93
+
94
+ await asyncMap(list, async file => {
105
95
  const res = INTERRUPTED_VHDS_REG.exec(file)
106
96
  if (res === null) {
107
97
  vhds.add(`${vdiDir}/${file}`)
108
98
  } else {
109
- interruptedVhds.set(`${vdiDir}/${res[1]}`, `${vdiDir}/${file}`)
99
+ try {
100
+ const mergeState = JSON.parse(await handler.readFile(`${vdiDir}/${file}`))
101
+ interruptedVhds.set(`${vdiDir}/${res[1]}`, {
102
+ statePath: `${vdiDir}/${file}`,
103
+ chain: mergeState.chain,
104
+ })
105
+ } catch (error) {
106
+ // fall back to a non resuming merge
107
+ vhds.add(`${vdiDir}/${file}`)
108
+ logWarn('failed to read existing merge state', { path: file, error })
109
+ }
110
110
  }
111
111
  })
112
112
  }
@@ -122,15 +122,15 @@ async function checkAliases(
122
122
  { handler, logInfo = noop, logWarn = console.warn, remove = false }
123
123
  ) {
124
124
  const aliasFound = []
125
- for (const path of aliasPaths) {
126
- const target = await resolveVhdAlias(handler, path)
125
+ for (const alias of aliasPaths) {
126
+ const target = await resolveVhdAlias(handler, alias)
127
127
 
128
128
  if (!isVhdFile(target)) {
129
- logWarn('alias references non VHD target', { path, target })
129
+ logWarn('alias references non VHD target', { alias, target })
130
130
  if (remove) {
131
- logInfo('removing alias and non VHD target', { path, target })
131
+ logInfo('removing alias and non VHD target', { alias, target })
132
132
  await handler.unlink(target)
133
- await handler.unlink(path)
133
+ await handler.unlink(alias)
134
134
  }
135
135
  continue
136
136
  }
@@ -143,13 +143,13 @@ async function checkAliases(
143
143
  // error during dispose should not trigger a deletion
144
144
  }
145
145
  } catch (error) {
146
- logWarn('missing or broken alias target', { target, path, error })
146
+ logWarn('missing or broken alias target', { alias, target, error })
147
147
  if (remove) {
148
148
  try {
149
- await VhdAbstract.unlink(handler, path)
149
+ await VhdAbstract.unlink(handler, alias)
150
150
  } catch (error) {
151
151
  if (error.code !== 'ENOENT') {
152
- logWarn('error deleting alias target', { target, path, error })
152
+ logWarn('error deleting alias target', { alias, target, error })
153
153
  }
154
154
  }
155
155
  }
@@ -159,17 +159,17 @@ async function checkAliases(
159
159
  aliasFound.push(resolve('/', target))
160
160
  }
161
161
 
162
- const entries = await handler.list(targetDataRepository, {
162
+ const vhds = await handler.list(targetDataRepository, {
163
163
  ignoreMissing: true,
164
164
  prependDir: true,
165
165
  })
166
166
 
167
- entries.forEach(async entry => {
168
- if (!aliasFound.includes(entry)) {
169
- logWarn('no alias references VHD', { entry })
167
+ await asyncMap(vhds, async path => {
168
+ if (!aliasFound.includes(path)) {
169
+ logWarn('no alias references VHD', { path })
170
170
  if (remove) {
171
- logInfo('deleting unaliased VHD')
172
- await VhdAbstract.unlink(handler, entry)
171
+ logInfo('deleting unused VHD', { path })
172
+ await VhdAbstract.unlink(handler, path)
173
173
  }
174
174
  }
175
175
  })
@@ -183,7 +183,7 @@ exports.cleanVm = async function cleanVm(
183
183
  vmDir,
184
184
  { fixMetadata, remove, merge, mergeLimiter = defaultMergeLimiter, logInfo = noop, logWarn = console.warn }
185
185
  ) {
186
- const limitedMergeVhdChain = mergeLimiter(mergeVhdChain)
186
+ const limitedMergeVhdChain = mergeLimiter(_mergeVhdChain)
187
187
 
188
188
  const handler = this._handler
189
189
 
@@ -192,7 +192,7 @@ exports.cleanVm = async function cleanVm(
192
192
  const vhdParents = { __proto__: null }
193
193
  const vhdChildren = { __proto__: null }
194
194
 
195
- const { vhds, interruptedVhds, aliases } = await listVhds(handler, vmDir)
195
+ const { vhds, interruptedVhds, aliases } = await listVhds(handler, vmDir, logWarn)
196
196
 
197
197
  // remove broken VHDs
198
198
  await asyncMap(vhds, async path => {
@@ -225,10 +225,8 @@ exports.cleanVm = async function cleanVm(
225
225
  logWarn(`should delete ${duplicate._path}`)
226
226
  vhds.delete(duplicate._path)
227
227
  } else {
228
- logWarn(`same ids but different content`)
228
+ logWarn('same ids but different content')
229
229
  }
230
- } else {
231
- logInfo('not duplicate', UUID.stringify(vhd.footer.uuid), path)
232
230
  }
233
231
  vhdById.set(UUID.stringify(vhdKept.footer.uuid), vhdKept)
234
232
  })
@@ -236,7 +234,7 @@ exports.cleanVm = async function cleanVm(
236
234
  vhds.delete(path)
237
235
  logWarn('VHD check error', { path, error })
238
236
  if (error?.code === 'ERR_ASSERTION' && remove) {
239
- logInfo('deleting broken path', { path })
237
+ logInfo('deleting broken VHD', { path })
240
238
  return VhdAbstract.unlink(handler, path)
241
239
  }
242
240
  }
@@ -245,7 +243,7 @@ exports.cleanVm = async function cleanVm(
245
243
  // remove interrupted merge states for missing VHDs
246
244
  for (const interruptedVhd of interruptedVhds.keys()) {
247
245
  if (!vhds.has(interruptedVhd)) {
248
- const statePath = interruptedVhds.get(interruptedVhd)
246
+ const { statePath } = interruptedVhds.get(interruptedVhd)
249
247
  interruptedVhds.delete(interruptedVhd)
250
248
 
251
249
  logWarn('orphan merge state', {
@@ -284,9 +282,9 @@ exports.cleanVm = async function cleanVm(
284
282
  if (!vhds.has(parent)) {
285
283
  vhds.delete(vhdPath)
286
284
 
287
- logWarn('parent VHD is missing', { parent, vhdPath })
285
+ logWarn('parent VHD is missing', { parent, child: vhdPath })
288
286
  if (remove) {
289
- logInfo('deleting orphan VHD', { vhdPath })
287
+ logInfo('deleting orphan VHD', { path: vhdPath })
290
288
  deletions.push(VhdAbstract.unlink(handler, vhdPath))
291
289
  }
292
290
  }
@@ -337,7 +335,7 @@ exports.cleanVm = async function cleanVm(
337
335
  try {
338
336
  metadata = JSON.parse(await handler.readFile(json))
339
337
  } catch (error) {
340
- logWarn('failed to read metadata file', { json, error })
338
+ logWarn('failed to read backup metadata', { path: json, error })
341
339
  jsons.delete(json)
342
340
  return
343
341
  }
@@ -348,9 +346,9 @@ exports.cleanVm = async function cleanVm(
348
346
  if (xvas.has(linkedXva)) {
349
347
  unusedXvas.delete(linkedXva)
350
348
  } else {
351
- logWarn('metadata XVA is missing', { json })
349
+ logWarn('the XVA linked to the backup is missing', { backup: json, xva: linkedXva })
352
350
  if (remove) {
353
- logInfo('deleting incomplete backup', { json })
351
+ logInfo('deleting incomplete backup', { path: json })
354
352
  jsons.delete(json)
355
353
  await handler.unlink(json)
356
354
  }
@@ -371,9 +369,9 @@ exports.cleanVm = async function cleanVm(
371
369
  vhdsToJSons[path] = json
372
370
  })
373
371
  } else {
374
- logWarn('some metadata VHDs are missing', { json, missingVhds })
372
+ logWarn('some VHDs linked to the backup are missing', { backup: json, missingVhds })
375
373
  if (remove) {
376
- logInfo('deleting incomplete backup', { json })
374
+ logInfo('deleting incomplete backup', { path: json })
377
375
  jsons.delete(json)
378
376
  await handler.unlink(json)
379
377
  }
@@ -414,9 +412,9 @@ exports.cleanVm = async function cleanVm(
414
412
  }
415
413
  }
416
414
 
417
- logWarn('unused VHD', { vhd })
415
+ logWarn('unused VHD', { path: vhd })
418
416
  if (remove) {
419
- logInfo('deleting unused VHD', { vhd })
417
+ logInfo('deleting unused VHD', { path: vhd })
420
418
  unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
421
419
  }
422
420
  }
@@ -427,7 +425,13 @@ exports.cleanVm = async function cleanVm(
427
425
 
428
426
  // merge interrupted VHDs
429
427
  for (const parent of interruptedVhds.keys()) {
430
- vhdChainsToMerge[parent] = [vhdChildren[parent], parent]
428
+ // before #6349 the chain wasn't in the mergeState
429
+ const { chain, statePath } = interruptedVhds.get(parent)
430
+ if (chain === undefined) {
431
+ vhdChainsToMerge[parent] = [parent, vhdChildren[parent]]
432
+ } else {
433
+ vhdChainsToMerge[parent] = chain.map(vhdPath => handlerPath.resolveFromFile(statePath, vhdPath))
434
+ }
431
435
  }
432
436
 
433
437
  Object.values(vhdChainsToMerge).forEach(chain => {
@@ -440,9 +444,9 @@ exports.cleanVm = async function cleanVm(
440
444
  const metadataWithMergedVhd = {}
441
445
  const doMerge = async () => {
442
446
  await asyncMap(toMerge, async chain => {
443
- const merged = await limitedMergeVhdChain(chain, { handler, logInfo, logWarn, remove, merge })
447
+ const merged = await limitedMergeVhdChain(handler, chain, { logInfo, logWarn, remove, merge })
444
448
  if (merged !== undefined) {
445
- const metadataPath = vhdsToJSons[chain[0]] // all the chain should have the same metada file
449
+ const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metada file
446
450
  metadataWithMergedVhd[metadataPath] = true
447
451
  }
448
452
  })
@@ -485,7 +489,11 @@ exports.cleanVm = async function cleanVm(
485
489
  if (mode === 'full') {
486
490
  // a full backup : check size
487
491
  const linkedXva = resolve('/', vmDir, xva)
488
- fileSystemSize = await handler.getSize(linkedXva)
492
+ try {
493
+ fileSystemSize = await handler.getSize(linkedXva)
494
+ } catch (error) {
495
+ // can fail with encrypted remote
496
+ }
489
497
  } else if (mode === 'delta') {
490
498
  const linkedVhds = Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
491
499
  fileSystemSize = await computeVhdsSize(handler, linkedVhds)
@@ -497,11 +505,15 @@ exports.cleanVm = async function cleanVm(
497
505
 
498
506
  // don't warn if the size has changed after a merge
499
507
  if (!merged && fileSystemSize !== size) {
500
- logWarn('incorrect size in metadata', { size: size ?? 'none', fileSystemSize })
508
+ logWarn('incorrect backup size in metadata', {
509
+ path: metadataPath,
510
+ actual: size ?? 'none',
511
+ expected: fileSystemSize,
512
+ })
501
513
  }
502
514
  }
503
515
  } catch (error) {
504
- logWarn('failed to get metadata size', { metadataPath, error })
516
+ logWarn('failed to get backup size', { backup: metadataPath, error })
505
517
  return
506
518
  }
507
519
 
@@ -511,7 +523,7 @@ exports.cleanVm = async function cleanVm(
511
523
  try {
512
524
  await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
513
525
  } catch (error) {
514
- logWarn('metadata size update failed', { metadataPath, error })
526
+ logWarn('failed to update backup size in metadata', { path: metadataPath, error })
515
527
  }
516
528
  }
517
529
  })
@@ -3,6 +3,8 @@
3
3
  const eos = require('end-of-stream')
4
4
  const { PassThrough } = require('stream')
5
5
 
6
+ const { debug } = require('@xen-orchestra/log').createLogger('xo:backups:forkStreamUnpipe')
7
+
6
8
  // create a new readable stream from an existing one which may be piped later
7
9
  //
8
10
  // in case of error in the new readable stream, it will simply be unpiped
@@ -11,18 +13,23 @@ exports.forkStreamUnpipe = function forkStreamUnpipe(stream) {
11
13
  const { forks = 0 } = stream
12
14
  stream.forks = forks + 1
13
15
 
16
+ debug('forking', { forks: stream.forks })
17
+
14
18
  const proxy = new PassThrough()
15
19
  stream.pipe(proxy)
16
20
  eos(stream, error => {
17
21
  if (error !== undefined) {
22
+ debug('error on original stream, destroying fork', { error })
18
23
  proxy.destroy(error)
19
24
  }
20
25
  })
21
- eos(proxy, _ => {
22
- stream.forks--
26
+ eos(proxy, error => {
27
+ debug('end of stream, unpiping', { error, forks: --stream.forks })
28
+
23
29
  stream.unpipe(proxy)
24
30
 
25
31
  if (stream.forks === 0) {
32
+ debug('no more forks, destroying original stream')
26
33
  stream.destroy(new Error('no more consumers for this stream'))
27
34
  }
28
35
  })
package/_isValidXva.js CHANGED
@@ -49,6 +49,11 @@ const isValidTar = async (handler, size, fd) => {
49
49
  // TODO: find an heuristic for compressed files
50
50
  async function isValidXva(path) {
51
51
  const handler = this._handler
52
+
53
+ // size is longer when encrypted + reading part of an encrypted file is not implemented
54
+ if (handler.isEncrypted) {
55
+ return true
56
+ }
52
57
  try {
53
58
  const fd = await handler.openFile(path, 'r')
54
59
  try {
@@ -66,7 +71,6 @@ async function isValidXva(path) {
66
71
  }
67
72
  } catch (error) {
68
73
  // never throw, log and report as valid to avoid side effects
69
- console.error('isValidXva', path, error)
70
74
  return true
71
75
  }
72
76
  }
@@ -64,7 +64,7 @@ const main = Disposable.wrap(async function* main(args) {
64
64
  try {
65
65
  const vmDir = getVmBackupDir(String(await handler.readFile(taskFile)))
66
66
  try {
67
- await adapter.cleanVm(vmDir, { merge: true, onLog: info, remove: true })
67
+ await adapter.cleanVm(vmDir, { merge: true, logInfo: info, logWarn: warn, remove: true })
68
68
  } catch (error) {
69
69
  // consider the clean successful if the VM dir is missing
70
70
  if (error.code !== 'ENOENT') {
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.27.1",
11
+ "version": "0.27.4",
12
12
  "engines": {
13
13
  "node": ">=14.6"
14
14
  },
@@ -22,7 +22,7 @@
22
22
  "@vates/disposable": "^0.1.1",
23
23
  "@vates/parse-duration": "^0.1.1",
24
24
  "@xen-orchestra/async-map": "^0.1.2",
25
- "@xen-orchestra/fs": "^2.0.0",
25
+ "@xen-orchestra/fs": "^3.0.0",
26
26
  "@xen-orchestra/log": "^0.3.0",
27
27
  "@xen-orchestra/template": "^0.1.0",
28
28
  "compare-versions": "^4.0.1",
@@ -38,7 +38,7 @@
38
38
  "promise-toolbox": "^0.21.0",
39
39
  "proper-lockfile": "^4.1.2",
40
40
  "uuid": "^8.3.2",
41
- "vhd-lib": "^3.3.2",
41
+ "vhd-lib": "^4.0.0",
42
42
  "yazl": "^2.5.1"
43
43
  },
44
44
  "devDependencies": {
@@ -46,7 +46,7 @@
46
46
  "tmp": "^0.2.1"
47
47
  },
48
48
  "peerDependencies": {
49
- "@xen-orchestra/xapi": "^1.4.0"
49
+ "@xen-orchestra/xapi": "^1.4.1"
50
50
  },
51
51
  "license": "AGPL-3.0-or-later",
52
52
  "author": {