@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.
- package/ImportVmBackup.mjs +179 -11
- package/_cleanVm.mjs +60 -53
- package/_incrementalVm.mjs +6 -0
- package/_runners/_vmRunners/IncrementalRemote.mjs +3 -3
- package/_runners/_vmRunners/IncrementalXapi.mjs +4 -3
- package/_runners/_writers/IncrementalRemoteWriter.mjs +11 -6
- package/formatVmBackups.mjs +6 -0
- package/package.json +4 -4
package/ImportVmBackup.mjs
CHANGED
|
@@ -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 #
|
|
34
|
-
const
|
|
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 {
|
|
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
|
-
|
|
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,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
472
|
+
const { finalVhdSize } = await limitedMergeVhdChain(handler, chain, {
|
|
475
473
|
logInfo,
|
|
476
474
|
logWarn,
|
|
477
475
|
remove,
|
|
478
|
-
merge,
|
|
479
476
|
mergeBlockConcurrency,
|
|
480
477
|
})
|
|
481
|
-
|
|
482
|
-
|
|
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) :
|
|
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
|
|
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 (
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
556
|
-
|
|
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' })
|
package/_incrementalVm.mjs
CHANGED
|
@@ -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
|
|
35
|
+
const isVhdDifferencing = {}
|
|
36
36
|
|
|
37
37
|
await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
|
184
|
+
const isDifferencing = isVhdDifferencing[`${id}.vhd`]
|
|
184
185
|
let parentPath
|
|
185
|
-
if (
|
|
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
|
|
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 (
|
|
221
|
+
if (isDifferencing) {
|
|
217
222
|
await chainVhd(handler, parentPath, handler, path)
|
|
218
223
|
}
|
|
219
224
|
|
package/formatVmBackups.mjs
CHANGED
|
@@ -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.
|
|
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": "^
|
|
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.
|
|
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.
|
|
59
|
+
"@xen-orchestra/xapi": "^4.1.0"
|
|
60
60
|
},
|
|
61
61
|
"license": "AGPL-3.0-or-later",
|
|
62
62
|
"author": {
|