@xen-orchestra/backups 0.17.1 → 0.18.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/RemoteAdapter.js +36 -9
- package/_backupWorker.js +1 -0
- package/_cleanVm.js +77 -41
- package/package.json +3 -3
- package/writers/DeltaBackupWriter.js +10 -4
package/RemoteAdapter.js
CHANGED
|
@@ -6,7 +6,7 @@ const pDefer = require('promise-toolbox/defer.js')
|
|
|
6
6
|
const groupBy = require('lodash/groupBy.js')
|
|
7
7
|
const { dirname, join, normalize, resolve } = require('path')
|
|
8
8
|
const { createLogger } = require('@xen-orchestra/log')
|
|
9
|
-
const { Constants, createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdSynthetic } = require('vhd-lib')
|
|
9
|
+
const { Constants, createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } = require('vhd-lib')
|
|
10
10
|
const { deduped } = require('@vates/disposable/deduped.js')
|
|
11
11
|
const { execFile } = require('child_process')
|
|
12
12
|
const { readdir, stat } = require('fs-extra')
|
|
@@ -68,10 +68,11 @@ const debounceResourceFactory = factory =>
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
class RemoteAdapter {
|
|
71
|
-
constructor(handler, { debounceResource = res => res, dirMode } = {}) {
|
|
71
|
+
constructor(handler, { debounceResource = res => res, dirMode, vhdDirectoryCompression } = {}) {
|
|
72
72
|
this._debounceResource = debounceResource
|
|
73
73
|
this._dirMode = dirMode
|
|
74
74
|
this._handler = handler
|
|
75
|
+
this._vhdDirectoryCompression = vhdDirectoryCompression
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
get handler() {
|
|
@@ -191,6 +192,22 @@ class RemoteAdapter {
|
|
|
191
192
|
return files
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
// check if we will be allowed to merge a a vhd created in this adapter
|
|
196
|
+
// with the vhd at path `path`
|
|
197
|
+
async isMergeableParent(packedParentUid, path) {
|
|
198
|
+
return await Disposable.use(openVhd(this.handler, path), vhd => {
|
|
199
|
+
// this baseUuid is not linked with this vhd
|
|
200
|
+
if (!vhd.footer.uuid.equals(packedParentUid)) {
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const isVhdDirectory = vhd instanceof VhdDirectory
|
|
205
|
+
return isVhdDirectory
|
|
206
|
+
? this.#useVhdDirectory() && this.#getCompressionType() === vhd.compressionType
|
|
207
|
+
: !this.#useVhdDirectory()
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
194
211
|
fetchPartitionFiles(diskId, partitionId, paths) {
|
|
195
212
|
const { promise, reject, resolve } = pDefer()
|
|
196
213
|
Disposable.use(
|
|
@@ -262,6 +279,18 @@ class RemoteAdapter {
|
|
|
262
279
|
])
|
|
263
280
|
}
|
|
264
281
|
|
|
282
|
+
#getCompressionType() {
|
|
283
|
+
return this._vhdDirectoryCompression
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#useVhdDirectory() {
|
|
287
|
+
return this.handler.type === 's3'
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#useAlias() {
|
|
291
|
+
return this.#useVhdDirectory()
|
|
292
|
+
}
|
|
293
|
+
|
|
265
294
|
getDisk = Disposable.factory(this.getDisk)
|
|
266
295
|
getDisk = deduped(this.getDisk, diskId => [diskId])
|
|
267
296
|
getDisk = debounceResourceFactory(this.getDisk)
|
|
@@ -318,13 +347,10 @@ class RemoteAdapter {
|
|
|
318
347
|
return yield this._getPartition(devicePath, await this._findPartition(devicePath, partitionId))
|
|
319
348
|
}
|
|
320
349
|
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
// if the file is named .vhd => vhd
|
|
324
|
-
// if the file is named alias.vhd => alias to a vhd
|
|
350
|
+
// if we use alias on this remote, we have to name the file alias.vhd
|
|
325
351
|
getVhdFileName(baseName) {
|
|
326
|
-
if (this
|
|
327
|
-
return `${baseName}.alias.vhd`
|
|
352
|
+
if (this.#useAlias()) {
|
|
353
|
+
return `${baseName}.alias.vhd`
|
|
328
354
|
}
|
|
329
355
|
return `${baseName}.vhd`
|
|
330
356
|
}
|
|
@@ -476,10 +502,11 @@ class RemoteAdapter {
|
|
|
476
502
|
async writeVhd(path, input, { checksum = true, validator = noop } = {}) {
|
|
477
503
|
const handler = this._handler
|
|
478
504
|
|
|
479
|
-
if (
|
|
505
|
+
if (this.#useVhdDirectory()) {
|
|
480
506
|
const dataPath = `${dirname(path)}/data/${uuidv4()}.vhd`
|
|
481
507
|
await createVhdDirectoryFromStream(handler, dataPath, input, {
|
|
482
508
|
concurrency: 16,
|
|
509
|
+
compression: this.#getCompressionType(),
|
|
483
510
|
async validator() {
|
|
484
511
|
await input.task
|
|
485
512
|
return validator.apply(this, arguments)
|
package/_backupWorker.js
CHANGED
package/_cleanVm.js
CHANGED
|
@@ -10,6 +10,24 @@ const { limitConcurrency } = require('limit-concurrency-decorator')
|
|
|
10
10
|
const { Task } = require('./Task.js')
|
|
11
11
|
const { Disposable } = require('promise-toolbox')
|
|
12
12
|
|
|
13
|
+
// checking the size of a vhd directory is costly
|
|
14
|
+
// 1 Http Query per 1000 blocks
|
|
15
|
+
// we only check size of all the vhd are VhdFiles
|
|
16
|
+
function shouldComputeVhdsSize(vhds) {
|
|
17
|
+
return vhds.every(vhd => vhd instanceof VhdFile)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const computeVhdsSize = (handler, vhdPaths) =>
|
|
21
|
+
Disposable.use(
|
|
22
|
+
vhdPaths.map(vhdPath => openVhd(handler, vhdPath)),
|
|
23
|
+
async vhds => {
|
|
24
|
+
if (shouldComputeVhdsSize(vhds)) {
|
|
25
|
+
const sizes = await asyncMap(vhds, vhd => vhd.getSize())
|
|
26
|
+
return sum(sizes)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
13
31
|
// chain is an array of VHDs from child to parent
|
|
14
32
|
//
|
|
15
33
|
// the whole chain will be merged into parent, parent will be renamed to child
|
|
@@ -130,6 +148,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
130
148
|
const handler = this._handler
|
|
131
149
|
|
|
132
150
|
const vhds = new Set()
|
|
151
|
+
const vhdsToJSons = new Set()
|
|
133
152
|
const vhdParents = { __proto__: null }
|
|
134
153
|
const vhdChildren = { __proto__: null }
|
|
135
154
|
|
|
@@ -242,16 +261,10 @@ exports.cleanVm = async function cleanVm(
|
|
|
242
261
|
}
|
|
243
262
|
|
|
244
263
|
const { mode } = metadata
|
|
245
|
-
let size
|
|
246
264
|
if (mode === 'full') {
|
|
247
265
|
const linkedXva = resolve('/', vmDir, metadata.xva)
|
|
248
|
-
|
|
249
266
|
if (xvas.has(linkedXva)) {
|
|
250
267
|
unusedXvas.delete(linkedXva)
|
|
251
|
-
|
|
252
|
-
size = await handler.getSize(linkedXva).catch(error => {
|
|
253
|
-
onLog(`failed to get size of ${json}`, { error })
|
|
254
|
-
})
|
|
255
268
|
} else {
|
|
256
269
|
onLog(`the XVA linked to the metadata ${json} is missing`)
|
|
257
270
|
if (remove) {
|
|
@@ -272,22 +285,9 @@ exports.cleanVm = async function cleanVm(
|
|
|
272
285
|
// possible (existing disks) even if one disk is missing
|
|
273
286
|
if (missingVhds.length === 0) {
|
|
274
287
|
linkedVhds.forEach(_ => unusedVhds.delete(_))
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// we only check size of all the vhd are VhdFiles
|
|
279
|
-
|
|
280
|
-
const shouldComputeSize = linkedVhds.every(vhd => vhd instanceof VhdFile)
|
|
281
|
-
if (shouldComputeSize) {
|
|
282
|
-
try {
|
|
283
|
-
await Disposable.use(Disposable.all(linkedVhds.map(vhdPath => openVhd(handler, vhdPath))), async vhds => {
|
|
284
|
-
const sizes = await asyncMap(vhds, vhd => vhd.getSize())
|
|
285
|
-
size = sum(sizes)
|
|
286
|
-
})
|
|
287
|
-
} catch (error) {
|
|
288
|
-
onLog(`failed to get size of ${json}`, { error })
|
|
289
|
-
}
|
|
290
|
-
}
|
|
288
|
+
linkedVhds.forEach(path => {
|
|
289
|
+
vhdsToJSons[path] = json
|
|
290
|
+
})
|
|
291
291
|
} else {
|
|
292
292
|
onLog(`Some VHDs linked to the metadata ${json} are missing`, { missingVhds })
|
|
293
293
|
if (remove) {
|
|
@@ -297,22 +297,6 @@ exports.cleanVm = async function cleanVm(
|
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
|
-
|
|
301
|
-
const metadataSize = metadata.size
|
|
302
|
-
if (size !== undefined && metadataSize !== size) {
|
|
303
|
-
onLog(`incorrect size in metadata: ${metadataSize ?? 'none'} instead of ${size}`)
|
|
304
|
-
|
|
305
|
-
// don't update if the the stored size is greater than found files,
|
|
306
|
-
// it can indicates a problem
|
|
307
|
-
if (fixMetadata && (metadataSize === undefined || metadataSize < size)) {
|
|
308
|
-
try {
|
|
309
|
-
metadata.size = size
|
|
310
|
-
await handler.writeFile(json, JSON.stringify(metadata), { flags: 'w' })
|
|
311
|
-
} catch (error) {
|
|
312
|
-
onLog(`failed to update size in backup metadata ${json}`, { error })
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
300
|
})
|
|
317
301
|
|
|
318
302
|
// TODO: parallelize by vm/job/vdi
|
|
@@ -371,9 +355,15 @@ exports.cleanVm = async function cleanVm(
|
|
|
371
355
|
})
|
|
372
356
|
}
|
|
373
357
|
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
358
|
+
const metadataWithMergedVhd = {}
|
|
359
|
+
const doMerge = async () => {
|
|
360
|
+
await asyncMap(toMerge, async chain => {
|
|
361
|
+
const merged = await limitedMergeVhdChain(chain, { handler, onLog, remove, merge })
|
|
362
|
+
if (merged !== undefined) {
|
|
363
|
+
const metadataPath = vhdsToJSons[chain[0]] // all the chain should have the same metada file
|
|
364
|
+
metadataWithMergedVhd[metadataPath] = true
|
|
365
|
+
}
|
|
366
|
+
})
|
|
377
367
|
}
|
|
378
368
|
|
|
379
369
|
await Promise.all([
|
|
@@ -398,6 +388,52 @@ exports.cleanVm = async function cleanVm(
|
|
|
398
388
|
}),
|
|
399
389
|
])
|
|
400
390
|
|
|
391
|
+
// update size for delta metadata with merged VHD
|
|
392
|
+
// check for the other that the size is the same as the real file size
|
|
393
|
+
|
|
394
|
+
await asyncMap(jsons, async metadataPath => {
|
|
395
|
+
const metadata = JSON.parse(await handler.readFile(metadataPath))
|
|
396
|
+
|
|
397
|
+
let fileSystemSize
|
|
398
|
+
const merged = metadataWithMergedVhd[metadataPath] !== undefined
|
|
399
|
+
|
|
400
|
+
const { mode, size, vhds, xva } = metadata
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
if (mode === 'full') {
|
|
404
|
+
// a full backup : check size
|
|
405
|
+
const linkedXva = resolve('/', vmDir, xva)
|
|
406
|
+
fileSystemSize = await handler.getSize(linkedXva)
|
|
407
|
+
} else if (mode === 'delta') {
|
|
408
|
+
const linkedVhds = Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
|
|
409
|
+
fileSystemSize = await computeVhdsSize(handler, linkedVhds)
|
|
410
|
+
|
|
411
|
+
// the size is not computed in some cases (e.g. VhdDirectory)
|
|
412
|
+
if (fileSystemSize === undefined) {
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// don't warn if the size has changed after a merge
|
|
417
|
+
if (!merged && fileSystemSize !== size) {
|
|
418
|
+
onLog(`incorrect size in metadata: ${size ?? 'none'} instead of ${fileSystemSize}`)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} catch (error) {
|
|
422
|
+
onLog(`failed to get size of ${metadataPath}`, { error })
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// systematically update size after a merge
|
|
427
|
+
if ((merged || fixMetadata) && size !== fileSystemSize) {
|
|
428
|
+
metadata.size = fileSystemSize
|
|
429
|
+
try {
|
|
430
|
+
await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
|
|
431
|
+
} catch (error) {
|
|
432
|
+
onLog(`failed to update size in backup metadata ${metadataPath} after merge`, { error })
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
|
|
401
437
|
return {
|
|
402
438
|
// boolean whether some VHDs were merged (or should be merged)
|
|
403
439
|
merge: toMerge.length !== 0,
|
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.18.3",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.6"
|
|
14
14
|
},
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"proper-lockfile": "^4.1.2",
|
|
37
37
|
"pump": "^3.0.0",
|
|
38
38
|
"uuid": "^8.3.2",
|
|
39
|
-
"vhd-lib": "^
|
|
39
|
+
"vhd-lib": "^3.0.0",
|
|
40
40
|
"yazl": "^2.5.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@xen-orchestra/xapi": "^0.8.
|
|
43
|
+
"@xen-orchestra/xapi": "^0.8.5"
|
|
44
44
|
},
|
|
45
45
|
"license": "AGPL-3.0-or-later",
|
|
46
46
|
"author": {
|
|
@@ -24,6 +24,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
24
24
|
async checkBaseVdis(baseUuidToSrcVdi) {
|
|
25
25
|
const { handler } = this._adapter
|
|
26
26
|
const backup = this._backup
|
|
27
|
+
const adapter = this._adapter
|
|
27
28
|
|
|
28
29
|
const backupDir = getVmBackupDir(backup.vm.uuid)
|
|
29
30
|
const vdisDir = `${backupDir}/vdis/${backup.job.id}`
|
|
@@ -35,13 +36,18 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
35
36
|
filter: _ => _[0] !== '.' && _.endsWith('.vhd'),
|
|
36
37
|
prependDir: true,
|
|
37
38
|
})
|
|
39
|
+
const packedBaseUuid = packUuid(baseUuid)
|
|
38
40
|
await asyncMap(vhds, async path => {
|
|
39
41
|
try {
|
|
40
42
|
await checkVhdChain(handler, path)
|
|
41
|
-
await
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// Warning, this should not be written as found = found || await adapter.isMergeableParent(packedBaseUuid, path)
|
|
44
|
+
//
|
|
45
|
+
// since all the checks of a path are done in parallel, found would be containing
|
|
46
|
+
// only the last answer of isMergeableParent which is probably not the right one
|
|
47
|
+
// this led to the support tickets https://help.vates.fr/#ticket/zoom/4751 , 4729, 4665 and 4300
|
|
48
|
+
|
|
49
|
+
const isMergeable = await adapter.isMergeableParent(packedBaseUuid, path)
|
|
50
|
+
found = found || isMergeable
|
|
45
51
|
} catch (error) {
|
|
46
52
|
warn('checkBaseVdis', { error })
|
|
47
53
|
await ignoreErrors.call(VhdAbstract.unlink(handler, path))
|