@xen-orchestra/backups 0.53.1 → 0.54.1
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/HealthCheckVmBackup.mjs +7 -17
- package/RemoteAdapter.mjs +24 -0
- package/_cleanVm.mjs +15 -3
- package/_incrementalVm.mjs +26 -12
- package/_runners/_vmRunners/IncrementalRemote.mjs +19 -8
- package/_runners/_vmRunners/_AbstractXapi.mjs +22 -5
- package/_runners/_writers/IncrementalRemoteWriter.mjs +4 -1
- package/_runners/_writers/IncrementalXapiWriter.mjs +10 -7
- package/package.json +9 -9
package/HealthCheckVmBackup.mjs
CHANGED
|
@@ -49,26 +49,25 @@ export class HealthCheckVmBackup {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// wait for the 'Running' event to be really stored in local xapi object cache
|
|
52
|
+
|
|
52
53
|
restoredVm = await xapi.waitObjectState(restoredVm.$ref, vm => vm.power_state === 'Running', {
|
|
53
54
|
timeout: remainingTimeout,
|
|
55
|
+
timeoutMessage: refOrUuid =>
|
|
56
|
+
`local xapi did not get Running state for VM ${refOrUuid} after ${timeout / 1000} second`,
|
|
54
57
|
})
|
|
55
58
|
|
|
56
59
|
const running = new Date()
|
|
57
60
|
remainingTimeout -= running - started
|
|
58
61
|
|
|
59
|
-
if (remainingTimeout < 0) {
|
|
60
|
-
throw new Error(`local xapi did not get Running state for VM ${restoredId} after ${timeout / 1000} second`)
|
|
61
|
-
}
|
|
62
62
|
// wait for the guest tool version to be defined
|
|
63
63
|
await xapi.waitObjectState(restoredVm.guest_metrics, gm => gm?.PV_drivers_version?.major !== undefined, {
|
|
64
64
|
timeout: remainingTimeout,
|
|
65
|
+
timeoutMessage: refOrUuid =>
|
|
66
|
+
`timeout reached while waiting for ${refOrUuid} to report the driver version through the Xen tools. Please check or update the Xen tools.`,
|
|
65
67
|
})
|
|
66
68
|
|
|
67
69
|
const guestToolsReady = new Date()
|
|
68
70
|
remainingTimeout -= guestToolsReady - running
|
|
69
|
-
if (remainingTimeout < 0) {
|
|
70
|
-
throw new Error(`local xapi did not get he guest tools check ${restoredId} after ${timeout / 1000} second`)
|
|
71
|
-
}
|
|
72
71
|
|
|
73
72
|
if (waitForScript) {
|
|
74
73
|
const startedRestoredVm = await xapi.waitObjectState(
|
|
@@ -79,19 +78,10 @@ export class HealthCheckVmBackup {
|
|
|
79
78
|
vm.xenstore_data['vm-data/xo-backup-health-check'] === 'failure'),
|
|
80
79
|
{
|
|
81
80
|
timeout: remainingTimeout,
|
|
81
|
+
timeoutMessage: refOrUuid =>
|
|
82
|
+
`timeout reached while waiting for ${refOrUuid} to report the startup script execution.`,
|
|
82
83
|
}
|
|
83
84
|
)
|
|
84
|
-
const scriptOk = new Date()
|
|
85
|
-
remainingTimeout -= scriptOk - guestToolsReady
|
|
86
|
-
if (remainingTimeout < 0) {
|
|
87
|
-
throw new Error(
|
|
88
|
-
`Backup health check script did not update vm-data/xo-backup-health-check of ${restoredId} after ${
|
|
89
|
-
timeout / 1000
|
|
90
|
-
} second, got ${
|
|
91
|
-
startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check']
|
|
92
|
-
} instead of 'success' or 'failure'`
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
85
|
|
|
96
86
|
if (startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check'] !== 'success') {
|
|
97
87
|
const message = startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check-error']
|
package/RemoteAdapter.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import fromEvent from 'promise-toolbox/fromEvent'
|
|
|
18
18
|
import groupBy from 'lodash/groupBy.js'
|
|
19
19
|
import pDefer from 'promise-toolbox/defer'
|
|
20
20
|
import pickBy from 'lodash/pickBy.js'
|
|
21
|
+
import reduce from 'lodash/reduce.js'
|
|
21
22
|
import tar from 'tar'
|
|
22
23
|
import zlib from 'zlib'
|
|
23
24
|
|
|
@@ -826,6 +827,29 @@ export class RemoteAdapter {
|
|
|
826
827
|
}
|
|
827
828
|
return metadata
|
|
828
829
|
}
|
|
830
|
+
|
|
831
|
+
#computeTotalBackupSizeRecursively(backups) {
|
|
832
|
+
return reduce(
|
|
833
|
+
backups,
|
|
834
|
+
(prev, backup) => {
|
|
835
|
+
const _backup = Array.isArray(backup) ? this.#computeTotalBackupSizeRecursively(backup) : backup
|
|
836
|
+
return {
|
|
837
|
+
onDisk: prev.onDisk + (_backup.onDisk ?? _backup.size),
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
{ onDisk: 0 }
|
|
841
|
+
)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async getTotalVmBackupSize() {
|
|
845
|
+
return this.#computeTotalBackupSizeRecursively(await this.listAllVmBackups())
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async getTotalBackupSize() {
|
|
849
|
+
const vmBackupSize = await this.getTotalVmBackupSize()
|
|
850
|
+
// @TODO: add `getTotalXoBackupSize` and `getTotalPoolBackupSize` once `size` is implemented by fs
|
|
851
|
+
return vmBackupSize
|
|
852
|
+
}
|
|
829
853
|
}
|
|
830
854
|
|
|
831
855
|
Object.assign(RemoteAdapter.prototype, {
|
package/_cleanVm.mjs
CHANGED
|
@@ -121,7 +121,19 @@ export async function checkAliases(
|
|
|
121
121
|
) {
|
|
122
122
|
const aliasFound = []
|
|
123
123
|
for (const alias of aliasPaths) {
|
|
124
|
-
|
|
124
|
+
let target
|
|
125
|
+
try {
|
|
126
|
+
target = await resolveVhdAlias(handler, alias)
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (err.code === 'ENOENT') {
|
|
129
|
+
logWarn('missing target of alias', { alias })
|
|
130
|
+
if (remove) {
|
|
131
|
+
logInfo('removing alias and non VHD target', { alias, target })
|
|
132
|
+
await handler.unlink(target)
|
|
133
|
+
await handler.unlink(alias)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
125
137
|
|
|
126
138
|
if (!isVhdFile(target)) {
|
|
127
139
|
logWarn('alias references non VHD target', { alias, target })
|
|
@@ -201,9 +213,9 @@ export async function cleanVm(
|
|
|
201
213
|
|
|
202
214
|
// remove broken VHDs
|
|
203
215
|
await asyncMap(vhds, async path => {
|
|
204
|
-
if(removeTmp && basename(path)[0] === '.'){
|
|
216
|
+
if (removeTmp && basename(path)[0] === '.') {
|
|
205
217
|
logInfo('deleting temporary VHD', { path })
|
|
206
|
-
|
|
218
|
+
return VhdAbstract.unlink(handler, path)
|
|
207
219
|
}
|
|
208
220
|
try {
|
|
209
221
|
await Disposable.use(openVhd(handler, path, { checkSecondFooter: !interruptedVhds.has(path) }), vhd => {
|
package/_incrementalVm.mjs
CHANGED
|
@@ -3,7 +3,6 @@ import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
|
|
3
3
|
import { asyncMap } from '@xen-orchestra/async-map'
|
|
4
4
|
import { CancelToken } from 'promise-toolbox'
|
|
5
5
|
import { compareVersions } from 'compare-versions'
|
|
6
|
-
import { createVhdStreamWithLength } from 'vhd-lib'
|
|
7
6
|
import { defer } from 'golike-defer'
|
|
8
7
|
|
|
9
8
|
import { cancelableMap } from './_cancelableMap.mjs'
|
|
@@ -53,14 +52,32 @@ export async function exportIncrementalVm(
|
|
|
53
52
|
$snapshot_of$uuid: vdi.$snapshot_of?.uuid,
|
|
54
53
|
$SR$uuid: vdi.$SR.uuid,
|
|
55
54
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
try {
|
|
56
|
+
streams[`${vdiRef}.vhd`] = await vdi.$exportContent({
|
|
57
|
+
baseRef: baseVdi?.$ref,
|
|
58
|
+
cancelToken,
|
|
59
|
+
format: 'vhd',
|
|
60
|
+
nbdConcurrency,
|
|
61
|
+
preferNbd,
|
|
62
|
+
})
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err.code === 'VDI_CANT_DO_DELTA') {
|
|
65
|
+
// fall back to a base
|
|
66
|
+
Task.info(`Can't do delta, will try to get a full stream`, { vdi })
|
|
67
|
+
streams[`${vdiRef}.vhd`] = await vdi.$exportContent({
|
|
68
|
+
cancelToken,
|
|
69
|
+
format: 'vhd',
|
|
70
|
+
nbdConcurrency,
|
|
71
|
+
preferNbd,
|
|
72
|
+
})
|
|
73
|
+
// only warn if the fall back succeed
|
|
74
|
+
Task.warning(`Can't do delta with this vdi, transfer will be a full`, {
|
|
75
|
+
vdi,
|
|
76
|
+
})
|
|
77
|
+
} else {
|
|
78
|
+
throw err
|
|
79
|
+
}
|
|
80
|
+
}
|
|
64
81
|
})
|
|
65
82
|
|
|
66
83
|
const suspendVdi = vm.$suspend_VDI
|
|
@@ -227,9 +244,6 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
|
|
|
227
244
|
if (typeof stream === 'function') {
|
|
228
245
|
stream = await stream()
|
|
229
246
|
}
|
|
230
|
-
if (stream.length === undefined) {
|
|
231
|
-
stream = await createVhdStreamWithLength(stream)
|
|
232
|
-
}
|
|
233
247
|
await xapi.setField('VDI', vdi.$ref, 'name_label', `[Importing] ${vdiRecords[id].name_label}`)
|
|
234
248
|
await vdi.$importContent(stream, { cancelToken, format: 'vhd' })
|
|
235
249
|
await xapi.setField('VDI', vdi.$ref, 'name_label', vdiRecords[id].name_label)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createLogger } from '@xen-orchestra/log'
|
|
2
|
+
|
|
1
3
|
import { asyncEach } from '@vates/async-each'
|
|
2
4
|
import assert from 'node:assert'
|
|
3
5
|
import * as UUID from 'uuid'
|
|
@@ -11,6 +13,7 @@ import { Disposable } from 'promise-toolbox'
|
|
|
11
13
|
import { openVhd } from 'vhd-lib'
|
|
12
14
|
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
|
|
13
15
|
|
|
16
|
+
const { warn } = createLogger('xo:backups:Incrementalremote')
|
|
14
17
|
class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
15
18
|
_getRemoteWriter() {
|
|
16
19
|
return IncrementalRemoteWriter
|
|
@@ -46,11 +49,7 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
|
46
49
|
})
|
|
47
50
|
|
|
48
51
|
const presentBaseVdis = new Map(baseUuidToSrcVdi)
|
|
49
|
-
await this._callWriters(
|
|
50
|
-
writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis),
|
|
51
|
-
'writer.checkBaseVdis()',
|
|
52
|
-
false
|
|
53
|
-
)
|
|
52
|
+
await this._callWriters(writer => writer.checkBaseVdis(presentBaseVdis), 'writer.checkBaseVdis()', false)
|
|
54
53
|
// check if the parent vdi are present in all the remotes
|
|
55
54
|
baseUuidToSrcVdi.forEach((srcVdiUuid, baseUuid) => {
|
|
56
55
|
if (!presentBaseVdis.has(baseUuid)) {
|
|
@@ -64,17 +63,29 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
|
64
63
|
|
|
65
64
|
for (const metadata of transferList) {
|
|
66
65
|
assert.strictEqual(metadata.mode, 'delta')
|
|
67
|
-
await this._selectBaseVm(metadata)
|
|
68
|
-
await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
|
|
69
66
|
const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, {
|
|
70
67
|
useChain: false,
|
|
71
68
|
})
|
|
72
|
-
|
|
69
|
+
// don't trust metadata too much
|
|
70
|
+
// recompute if it's a base backup
|
|
71
|
+
// recompute if disks are differencing or not
|
|
73
72
|
const isVhdDifferencing = {}
|
|
74
73
|
|
|
75
74
|
await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
|
|
76
75
|
isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
|
|
77
76
|
})
|
|
77
|
+
const hasDifferencingDisk = Object.values(isVhdDifferencing).includes(true)
|
|
78
|
+
if (metadata.isBase === hasDifferencingDisk) {
|
|
79
|
+
warn(`Metadata isBase and real disk value are different`, {
|
|
80
|
+
metadataIsBase: metadata.isBase,
|
|
81
|
+
diskIsBase: !hasDifferencingDisk,
|
|
82
|
+
isVhdDifferencing,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
metadata.isBase = !hasDifferencingDisk
|
|
86
|
+
metadata.isVhdDifferencing = isVhdDifferencing
|
|
87
|
+
await this._selectBaseVm(metadata)
|
|
88
|
+
await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
|
|
78
89
|
|
|
79
90
|
incrementalExport.streams = mapValues(incrementalExport.streams, this._throttleStream)
|
|
80
91
|
await this._callWriters(
|
|
@@ -203,7 +203,13 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
203
203
|
for (const srcVdi of srcVdis) {
|
|
204
204
|
const snapshots = await xapi.getRecords('VDI', srcVdi.snapshots)
|
|
205
205
|
for (const snapshot of snapshots) {
|
|
206
|
-
|
|
206
|
+
// only keep the snapshot related to this backup job
|
|
207
|
+
// and only if the job is still using purge snapshot data or if the disk
|
|
208
|
+
// is not a cbt metadata disk ( expect a type: user for normal disks)
|
|
209
|
+
if (
|
|
210
|
+
snapshot.other_config[JOB_ID] === jobId &&
|
|
211
|
+
(this._settings.cbtDestroySnapshotData || snapshot.type !== 'cbt_metadata')
|
|
212
|
+
) {
|
|
207
213
|
this._jobSnapshotVdis.push(snapshot)
|
|
208
214
|
}
|
|
209
215
|
}
|
|
@@ -219,6 +225,14 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
219
225
|
await xapi.barrier()
|
|
220
226
|
// ensure cached object are up to date
|
|
221
227
|
this._jobSnapshotVdis = this._jobSnapshotVdis.map(vdi => xapi.getObject(vdi.$ref))
|
|
228
|
+
|
|
229
|
+
// get the datetime of the most recent snapshot
|
|
230
|
+
const lastSnapshotDateTime = this._jobSnapshotVdis
|
|
231
|
+
.map(({ other_config }) => other_config[DATETIME])
|
|
232
|
+
.sort()
|
|
233
|
+
.pop()
|
|
234
|
+
|
|
235
|
+
// remove older snapshot schedule per schedule
|
|
222
236
|
const snapshotsPerSchedule = groupBy(this._jobSnapshotVdis, _ => _.other_config[SCHEDULE_ID])
|
|
223
237
|
await asyncMap(Object.entries(snapshotsPerSchedule), async ([scheduleId, snapshots]) => {
|
|
224
238
|
const snapshotPerDatetime = groupBy(snapshots, _ => _.other_config[DATETIME])
|
|
@@ -231,10 +245,13 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
231
245
|
...allSettings[scheduleId],
|
|
232
246
|
...allSettings[this._vm.uuid],
|
|
233
247
|
}
|
|
234
|
-
|
|
235
|
-
const minRetention = this.job.mode === 'delta' ? 1 : 0
|
|
236
|
-
const retention = Math.max(settings.snapshotRetention ?? 0, minRetention)
|
|
248
|
+
const retention = settings.snapshotRetention ?? 0
|
|
237
249
|
await asyncMap(getOldEntries(retention, datetimes), async datetime => {
|
|
250
|
+
// keep the last snapshot across all schedules for delta
|
|
251
|
+
// since we'll need it to compute delta for next backup
|
|
252
|
+
if (this.job.mode === 'delta' && datetime === lastSnapshotDateTime) {
|
|
253
|
+
return
|
|
254
|
+
}
|
|
238
255
|
const vdis = snapshotPerDatetime[datetime]
|
|
239
256
|
let vmRef
|
|
240
257
|
// if there is an attached VM => destroy the VM (Non CBT backups)
|
|
@@ -293,7 +310,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
293
310
|
await this._xapi.VDI_dataDestroy(vdiRef)
|
|
294
311
|
Task.info(`Snapshot data has been deleted`, { vdiRef })
|
|
295
312
|
} catch (error) {
|
|
296
|
-
Task.warning(`Couldn't
|
|
313
|
+
Task.warning(`Couldn't delete snapshot data`, { error, vdiRef })
|
|
297
314
|
}
|
|
298
315
|
}
|
|
299
316
|
}
|
|
@@ -66,7 +66,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
66
66
|
|
|
67
67
|
async beforeBackup() {
|
|
68
68
|
await super.beforeBackup()
|
|
69
|
-
return this._cleanVm({ merge: true })
|
|
69
|
+
return this._cleanVm({ merge: true, remove: true })
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
prepare({ isFull }) {
|
|
@@ -149,6 +149,9 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
149
149
|
assert.notStrictEqual(parentPath, undefined, 'A differential VHD must have a parent')
|
|
150
150
|
// forbid any kind of loop
|
|
151
151
|
assert.ok(basename(parentPath) < basename(path), `vhd must be sorted to be chained`)
|
|
152
|
+
// re-chainVhd is mandatory
|
|
153
|
+
// since the parent may be a alias or not
|
|
154
|
+
// and the child may be the other
|
|
152
155
|
await chainVhd(handler, parentPath, handler, path)
|
|
153
156
|
}
|
|
154
157
|
|
|
@@ -15,11 +15,15 @@ import assert from 'node:assert'
|
|
|
15
15
|
export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
|
|
16
16
|
async checkBaseVdis(baseUuidToSrcVdi) {
|
|
17
17
|
const sr = this._sr
|
|
18
|
+
if (baseUuidToSrcVdi.size === 0) {
|
|
19
|
+
// searching for the vdis is expensive
|
|
20
|
+
// don't do it if there is nothing to find
|
|
21
|
+
return
|
|
22
|
+
}
|
|
18
23
|
|
|
19
24
|
// @todo use an index if possible
|
|
20
25
|
// @todo : this seems similare to decorateVmMetadata
|
|
21
|
-
|
|
22
|
-
const replicatedVdis = sr.$VDIs
|
|
26
|
+
const replicatedVdis = sr.$VDIs
|
|
23
27
|
.filter(vdi => {
|
|
24
28
|
// REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
|
|
25
29
|
return baseUuidToSrcVdi.has(vdi?.other_config[COPY_OF])
|
|
@@ -103,11 +107,10 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
103
107
|
.filter(_ => !!_)
|
|
104
108
|
// @todo use index ?
|
|
105
109
|
|
|
106
|
-
const replicatedVdis = sr.$VDIs
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
})
|
|
110
|
+
const replicatedVdis = sr.$VDIs.filter(vdi => {
|
|
111
|
+
// REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
|
|
112
|
+
return sourceVdiUuids.includes(vdi?.other_config[COPY_OF])
|
|
113
|
+
})
|
|
111
114
|
|
|
112
115
|
Object.values(backup.vdis).forEach(vdi => {
|
|
113
116
|
vdi.other_config[COPY_OF] = vdi.uuid
|
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.
|
|
11
|
+
"version": "0.54.1",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -23,13 +23,13 @@
|
|
|
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
|
-
"@vates/disposable": "^0.1.
|
|
27
|
-
"@vates/fuse-vhd": "^2.1.
|
|
28
|
-
"@vates/nbd-client": "^3.1.
|
|
26
|
+
"@vates/disposable": "^0.1.6",
|
|
27
|
+
"@vates/fuse-vhd": "^2.1.2",
|
|
28
|
+
"@vates/nbd-client": "^3.1.1",
|
|
29
29
|
"@vates/parse-duration": "^0.1.1",
|
|
30
30
|
"@xen-orchestra/async-map": "^0.1.2",
|
|
31
|
-
"@xen-orchestra/fs": "^4.
|
|
32
|
-
"@xen-orchestra/log": "^0.
|
|
31
|
+
"@xen-orchestra/fs": "^4.2.0",
|
|
32
|
+
"@xen-orchestra/log": "^0.7.0",
|
|
33
33
|
"@xen-orchestra/template": "^0.1.0",
|
|
34
34
|
"app-conf": "^3.0.0",
|
|
35
35
|
"compare-versions": "^6.0.0",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"tar": "^6.1.15",
|
|
48
48
|
"uuid": "^9.0.0",
|
|
49
49
|
"value-matcher": "^0.2.0",
|
|
50
|
-
"vhd-lib": "^4.11.
|
|
51
|
-
"xen-api": "^4.
|
|
50
|
+
"vhd-lib": "^4.11.1",
|
|
51
|
+
"xen-api": "^4.4.0",
|
|
52
52
|
"yazl": "^2.5.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"tmp": "^0.2.1"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@xen-orchestra/xapi": "^7.
|
|
62
|
+
"@xen-orchestra/xapi": "^7.6.1"
|
|
63
63
|
},
|
|
64
64
|
"license": "AGPL-3.0-or-later",
|
|
65
65
|
"author": {
|