@xen-orchestra/backups 0.44.2 → 0.44.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/ImportVmBackup.mjs +1 -1
- package/RestoreMetadataBackup.mjs +1 -1
- package/_cleanVm.mjs +60 -53
- package/_incrementalVm.mjs +2 -0
- package/_runners/_vmRunners/IncrementalRemote.mjs +3 -3
- package/_runners/_vmRunners/IncrementalXapi.mjs +4 -3
- package/_runners/_writers/IncrementalRemoteWriter.mjs +10 -5
- package/_runners/_writers/_MixinXapiWriter.mjs +1 -1
- package/formatVmBackups.mjs +18 -0
- package/package.json +7 -7
package/ImportVmBackup.mjs
CHANGED
|
@@ -191,7 +191,7 @@ export class ImportVmBackup {
|
|
|
191
191
|
|
|
192
192
|
async #decorateIncrementalVmMetadata() {
|
|
193
193
|
const { additionnalVmTag, mapVdisSrs, useDifferentialRestore } = this._importIncrementalVmSettings
|
|
194
|
-
|
|
194
|
+
|
|
195
195
|
const ignoredVdis = new Set(
|
|
196
196
|
Object.entries(mapVdisSrs)
|
|
197
197
|
.filter(([_, srUuid]) => srUuid === null)
|
|
@@ -21,7 +21,7 @@ export class RestoreMetadataBackup {
|
|
|
21
21
|
})
|
|
22
22
|
} else {
|
|
23
23
|
const metadata = JSON.parse(await handler.readFile(join(backupId, 'metadata.json')))
|
|
24
|
-
const dataFileName = resolve(backupId, metadata.data ?? 'data.json')
|
|
24
|
+
const dataFileName = resolve('/', backupId, metadata.data ?? 'data.json').slice(1)
|
|
25
25
|
const data = await handler.readFile(dataFileName)
|
|
26
26
|
|
|
27
27
|
// if data is JSON, sent it as a plain string, otherwise, consider the data as binary and encode it
|
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
|
})
|
|
@@ -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, {
|
|
@@ -205,15 +206,19 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
205
206
|
await checkVhd(handler, parentPath)
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
transferSize += await
|
|
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
|
|
|
@@ -58,7 +58,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
|
|
|
58
58
|
)
|
|
59
59
|
}
|
|
60
60
|
const healthCheckVm = xapi.getObject(healthCheckVmRef) ?? (await xapi.waitObject(healthCheckVmRef))
|
|
61
|
-
await healthCheckVm.
|
|
61
|
+
await healthCheckVm.add_tags('xo:no-bak=Health Check')
|
|
62
62
|
await new HealthCheckVmBackup({
|
|
63
63
|
restoredVm: healthCheckVm,
|
|
64
64
|
xapi,
|
package/formatVmBackups.mjs
CHANGED
|
@@ -2,6 +2,20 @@ import mapValues from 'lodash/mapValues.js'
|
|
|
2
2
|
import { dirname } from 'node:path'
|
|
3
3
|
|
|
4
4
|
function formatVmBackup(backup) {
|
|
5
|
+
const { isVhdDifferencing, vmSnapshot } = backup
|
|
6
|
+
|
|
7
|
+
let differencingVhds
|
|
8
|
+
let dynamicVhds
|
|
9
|
+
const withMemory = vmSnapshot.suspend_VDI !== 'OpaqueRef:NULL'
|
|
10
|
+
// isVhdDifferencing is either undefined or an object
|
|
11
|
+
if (isVhdDifferencing !== undefined) {
|
|
12
|
+
differencingVhds = Object.values(isVhdDifferencing).filter(t => t).length
|
|
13
|
+
dynamicVhds = Object.values(isVhdDifferencing).filter(t => !t).length
|
|
14
|
+
if (withMemory) {
|
|
15
|
+
// the suspend VDI (memory) is always a dynamic
|
|
16
|
+
dynamicVhds -= 1
|
|
17
|
+
}
|
|
18
|
+
}
|
|
5
19
|
return {
|
|
6
20
|
disks:
|
|
7
21
|
backup.vhds === undefined
|
|
@@ -25,6 +39,10 @@ function formatVmBackup(backup) {
|
|
|
25
39
|
name_description: backup.vm.name_description,
|
|
26
40
|
name_label: backup.vm.name_label,
|
|
27
41
|
},
|
|
42
|
+
|
|
43
|
+
differencingVhds,
|
|
44
|
+
dynamicVhds,
|
|
45
|
+
withMemory,
|
|
28
46
|
}
|
|
29
47
|
}
|
|
30
48
|
|
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.4",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"@vates/async-each": "^1.0.0",
|
|
23
23
|
"@vates/cached-dns.lookup": "^1.0.0",
|
|
24
24
|
"@vates/compose": "^2.1.0",
|
|
25
|
-
"@vates/decorate-with": "^2.
|
|
25
|
+
"@vates/decorate-with": "^2.1.0",
|
|
26
26
|
"@vates/disposable": "^0.1.5",
|
|
27
|
-
"@vates/fuse-vhd": "^2.
|
|
28
|
-
"@vates/nbd-client": "^
|
|
27
|
+
"@vates/fuse-vhd": "^2.1.0",
|
|
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,8 +44,8 @@
|
|
|
44
44
|
"proper-lockfile": "^4.1.2",
|
|
45
45
|
"tar": "^6.1.15",
|
|
46
46
|
"uuid": "^9.0.0",
|
|
47
|
-
"vhd-lib": "^4.
|
|
48
|
-
"xen-api": "^2.0.
|
|
47
|
+
"vhd-lib": "^4.9.0",
|
|
48
|
+
"xen-api": "^2.0.1",
|
|
49
49
|
"yazl": "^2.5.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
@@ -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.2.0"
|
|
60
60
|
},
|
|
61
61
|
"license": "AGPL-3.0-or-later",
|
|
62
62
|
"author": {
|