@xen-orchestra/backups 0.16.0 → 0.17.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/RemoteAdapter.js +54 -17
- package/_VmBackup.js +17 -4
- package/_cleanVm.js +18 -5
- package/package.json +5 -5
- package/writers/_MixinBackupWriter.js +19 -5
package/RemoteAdapter.js
CHANGED
|
@@ -3,9 +3,10 @@ const Disposable = require('promise-toolbox/Disposable.js')
|
|
|
3
3
|
const fromCallback = require('promise-toolbox/fromCallback.js')
|
|
4
4
|
const fromEvent = require('promise-toolbox/fromEvent.js')
|
|
5
5
|
const pDefer = require('promise-toolbox/defer.js')
|
|
6
|
+
const groupBy = require('lodash/groupBy.js')
|
|
6
7
|
const { dirname, join, normalize, resolve } = require('path')
|
|
7
8
|
const { createLogger } = require('@xen-orchestra/log')
|
|
8
|
-
const {
|
|
9
|
+
const { Constants, createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdSynthetic } = require('vhd-lib')
|
|
9
10
|
const { deduped } = require('@vates/disposable/deduped.js')
|
|
10
11
|
const { execFile } = require('child_process')
|
|
11
12
|
const { readdir, stat } = require('fs-extra')
|
|
@@ -243,17 +244,22 @@ class RemoteAdapter {
|
|
|
243
244
|
)
|
|
244
245
|
}
|
|
245
246
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
deleteVmBackup(file) {
|
|
248
|
+
return this.deleteVmBackups([file])
|
|
249
|
+
}
|
|
249
250
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
throw new Error(
|
|
251
|
+
async deleteVmBackups(files) {
|
|
252
|
+
const { delta, full, ...others } = groupBy(await asyncMap(files, file => this.readVmBackupMetadata(file)), 'mode')
|
|
253
|
+
|
|
254
|
+
const unsupportedModes = Object.keys(others)
|
|
255
|
+
if (unsupportedModes.length !== 0) {
|
|
256
|
+
throw new Error('no deleter for backup modes: ' + unsupportedModes.join(', '))
|
|
256
257
|
}
|
|
258
|
+
|
|
259
|
+
await Promise.all([
|
|
260
|
+
delta !== undefined && this.deleteDeltaVmBackups(delta),
|
|
261
|
+
full !== undefined && this.deleteFullVmBackups(full),
|
|
262
|
+
])
|
|
257
263
|
}
|
|
258
264
|
|
|
259
265
|
getDisk = Disposable.factory(this.getDisk)
|
|
@@ -469,10 +475,10 @@ class RemoteAdapter {
|
|
|
469
475
|
|
|
470
476
|
async writeVhd(path, input, { checksum = true, validator = noop } = {}) {
|
|
471
477
|
const handler = this._handler
|
|
472
|
-
let dataPath = path
|
|
473
478
|
|
|
474
479
|
if (path.endsWith('.alias.vhd')) {
|
|
475
|
-
|
|
480
|
+
const dataPath = `${dirname(path)}/data/${uuidv4()}.vhd`
|
|
481
|
+
await createVhdDirectoryFromStream(handler, dataPath, input, {
|
|
476
482
|
concurrency: 16,
|
|
477
483
|
async validator() {
|
|
478
484
|
await input.task
|
|
@@ -481,7 +487,7 @@ class RemoteAdapter {
|
|
|
481
487
|
})
|
|
482
488
|
await VhdAbstract.createAlias(handler, path, dataPath)
|
|
483
489
|
} else {
|
|
484
|
-
await this.outputStream(
|
|
490
|
+
await this.outputStream(path, input, { checksum, validator })
|
|
485
491
|
}
|
|
486
492
|
}
|
|
487
493
|
|
|
@@ -497,17 +503,48 @@ class RemoteAdapter {
|
|
|
497
503
|
}
|
|
498
504
|
|
|
499
505
|
async _createSyntheticStream(handler, paths) {
|
|
506
|
+
let disposableVhds = []
|
|
507
|
+
|
|
508
|
+
// if it's a path : open all hierarchy of parent
|
|
509
|
+
if (typeof paths === 'string') {
|
|
510
|
+
let vhd,
|
|
511
|
+
vhdPath = paths
|
|
512
|
+
do {
|
|
513
|
+
const disposable = await openVhd(handler, vhdPath)
|
|
514
|
+
vhd = disposable.value
|
|
515
|
+
disposableVhds.push(disposable)
|
|
516
|
+
vhdPath = resolveRelativeFromFile(vhdPath, vhd.header.parentUnicodeName)
|
|
517
|
+
} while (vhd.footer.diskType !== Constants.DISK_TYPES.DYNAMIC)
|
|
518
|
+
} else {
|
|
519
|
+
// only open the list of path given
|
|
520
|
+
disposableVhds = paths.map(path => openVhd(handler, path))
|
|
521
|
+
}
|
|
522
|
+
|
|
500
523
|
// I don't want the vhds to be disposed on return
|
|
501
524
|
// but only when the stream is done ( or failed )
|
|
502
|
-
const disposables = await Disposable.all(
|
|
525
|
+
const disposables = await Disposable.all(disposableVhds)
|
|
503
526
|
const vhds = disposables.value
|
|
527
|
+
|
|
528
|
+
let disposed = false
|
|
529
|
+
const disposeOnce = async () => {
|
|
530
|
+
if (!disposed) {
|
|
531
|
+
disposed = true
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
await disposables.dispose()
|
|
535
|
+
} catch (error) {
|
|
536
|
+
warn('_createSyntheticStream: failed to dispose VHDs', { error })
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
504
541
|
const synthetic = new VhdSynthetic(vhds)
|
|
505
542
|
await synthetic.readHeaderAndFooter()
|
|
506
543
|
await synthetic.readBlockAllocationTable()
|
|
507
544
|
const stream = await synthetic.stream()
|
|
508
|
-
stream.on('end',
|
|
509
|
-
stream.on('close',
|
|
510
|
-
stream.on('error',
|
|
545
|
+
stream.on('end', disposeOnce)
|
|
546
|
+
stream.on('close', disposeOnce)
|
|
547
|
+
stream.on('error', disposeOnce)
|
|
511
548
|
return stream
|
|
512
549
|
}
|
|
513
550
|
|
package/_VmBackup.js
CHANGED
|
@@ -36,6 +36,11 @@ const forkDeltaExport = deltaExport =>
|
|
|
36
36
|
|
|
37
37
|
exports.VmBackup = class VmBackup {
|
|
38
38
|
constructor({ config, getSnapshotNameLabel, job, remoteAdapters, remotes, schedule, settings, srs, vm }) {
|
|
39
|
+
if (vm.other_config['xo:backup:job'] === job.id) {
|
|
40
|
+
// otherwise replicated VMs would be matched and replicated again and again
|
|
41
|
+
throw new Error('cannot backup a VM created by this very job')
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
this.config = config
|
|
40
45
|
this.job = job
|
|
41
46
|
this.remoteAdapters = remoteAdapters
|
|
@@ -333,13 +338,16 @@ exports.VmBackup = class VmBackup {
|
|
|
333
338
|
|
|
334
339
|
const baseUuidToSrcVdi = new Map()
|
|
335
340
|
await asyncMap(await baseVm.$getDisks(), async baseRef => {
|
|
336
|
-
const snapshotOf = await
|
|
341
|
+
const [baseUuid, snapshotOf] = await Promise.all([
|
|
342
|
+
xapi.getField('VDI', baseRef, 'uuid'),
|
|
343
|
+
xapi.getField('VDI', baseRef, 'snapshot_of'),
|
|
344
|
+
])
|
|
337
345
|
const srcVdi = srcVdis[snapshotOf]
|
|
338
346
|
if (srcVdi !== undefined) {
|
|
339
|
-
baseUuidToSrcVdi.set(
|
|
347
|
+
baseUuidToSrcVdi.set(baseUuid, srcVdi)
|
|
340
348
|
} else {
|
|
341
|
-
debug('
|
|
342
|
-
vdi:
|
|
349
|
+
debug('ignore snapshot VDI because no longer present on VM', {
|
|
350
|
+
vdi: baseUuid,
|
|
343
351
|
})
|
|
344
352
|
}
|
|
345
353
|
})
|
|
@@ -351,6 +359,11 @@ exports.VmBackup = class VmBackup {
|
|
|
351
359
|
false
|
|
352
360
|
)
|
|
353
361
|
|
|
362
|
+
if (presentBaseVdis.size === 0) {
|
|
363
|
+
debug('no base VM found')
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
354
367
|
const fullVdisRequired = new Set()
|
|
355
368
|
baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
|
|
356
369
|
if (presentBaseVdis.has(baseUuid)) {
|
package/_cleanVm.js
CHANGED
|
@@ -202,7 +202,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
202
202
|
await Promise.all(deletions)
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
const jsons =
|
|
205
|
+
const jsons = new Set()
|
|
206
206
|
const xvas = new Set()
|
|
207
207
|
const xvaSums = []
|
|
208
208
|
const entries = await handler.list(vmDir, {
|
|
@@ -210,7 +210,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
210
210
|
})
|
|
211
211
|
entries.forEach(path => {
|
|
212
212
|
if (isMetadataFile(path)) {
|
|
213
|
-
jsons.
|
|
213
|
+
jsons.add(path)
|
|
214
214
|
} else if (isXvaFile(path)) {
|
|
215
215
|
xvas.add(path)
|
|
216
216
|
} else if (isXvaSumFile(path)) {
|
|
@@ -232,7 +232,15 @@ exports.cleanVm = async function cleanVm(
|
|
|
232
232
|
// compile the list of unused XVAs and VHDs, and remove backup metadata which
|
|
233
233
|
// reference a missing XVA/VHD
|
|
234
234
|
await asyncMap(jsons, async json => {
|
|
235
|
-
|
|
235
|
+
let metadata
|
|
236
|
+
try {
|
|
237
|
+
metadata = JSON.parse(await handler.readFile(json))
|
|
238
|
+
} catch (error) {
|
|
239
|
+
onLog(`failed to read metadata file ${json}`, { error })
|
|
240
|
+
jsons.delete(json)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
236
244
|
const { mode } = metadata
|
|
237
245
|
let size
|
|
238
246
|
if (mode === 'full') {
|
|
@@ -248,6 +256,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
248
256
|
onLog(`the XVA linked to the metadata ${json} is missing`)
|
|
249
257
|
if (remove) {
|
|
250
258
|
onLog(`deleting incomplete backup ${json}`)
|
|
259
|
+
jsons.delete(json)
|
|
251
260
|
await handler.unlink(json)
|
|
252
261
|
}
|
|
253
262
|
}
|
|
@@ -256,9 +265,12 @@ exports.cleanVm = async function cleanVm(
|
|
|
256
265
|
const { vhds } = metadata
|
|
257
266
|
return Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
|
|
258
267
|
})()
|
|
268
|
+
|
|
269
|
+
const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
|
|
270
|
+
|
|
259
271
|
// FIXME: find better approach by keeping as much of the backup as
|
|
260
272
|
// possible (existing disks) even if one disk is missing
|
|
261
|
-
if (
|
|
273
|
+
if (missingVhds.length === 0) {
|
|
262
274
|
linkedVhds.forEach(_ => unusedVhds.delete(_))
|
|
263
275
|
|
|
264
276
|
// checking the size of a vhd directory is costly
|
|
@@ -277,9 +289,10 @@ exports.cleanVm = async function cleanVm(
|
|
|
277
289
|
}
|
|
278
290
|
}
|
|
279
291
|
} else {
|
|
280
|
-
onLog(`Some VHDs linked to the metadata ${json} are missing
|
|
292
|
+
onLog(`Some VHDs linked to the metadata ${json} are missing`, { missingVhds })
|
|
281
293
|
if (remove) {
|
|
282
294
|
onLog(`deleting incomplete backup ${json}`)
|
|
295
|
+
jsons.delete(json)
|
|
283
296
|
await handler.unlink(json)
|
|
284
297
|
}
|
|
285
298
|
}
|
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.17.1",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.6"
|
|
14
14
|
},
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
"postversion": "npm publish --access public"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@vates/compose": "^2.
|
|
19
|
+
"@vates/compose": "^2.1.0",
|
|
20
20
|
"@vates/disposable": "^0.1.1",
|
|
21
21
|
"@vates/parse-duration": "^0.1.1",
|
|
22
22
|
"@xen-orchestra/async-map": "^0.1.2",
|
|
23
|
-
"@xen-orchestra/fs": "^0.19.
|
|
23
|
+
"@xen-orchestra/fs": "^0.19.3",
|
|
24
24
|
"@xen-orchestra/log": "^0.3.0",
|
|
25
25
|
"@xen-orchestra/template": "^0.1.0",
|
|
26
26
|
"compare-versions": "^4.0.1",
|
|
@@ -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": "^2.0.
|
|
39
|
+
"vhd-lib": "^2.0.4",
|
|
40
40
|
"yazl": "^2.5.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@xen-orchestra/xapi": "^0.8.
|
|
43
|
+
"@xen-orchestra/xapi": "^0.8.4"
|
|
44
44
|
},
|
|
45
45
|
"license": "AGPL-3.0-or-later",
|
|
46
46
|
"author": {
|
|
@@ -21,10 +21,18 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
|
|
|
21
21
|
this.#vmBackupDir = getVmBackupDir(this._backup.vm.uuid)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
_cleanVm(options) {
|
|
25
|
-
|
|
26
|
-
.cleanVm(this.#vmBackupDir, {
|
|
27
|
-
|
|
24
|
+
async _cleanVm(options) {
|
|
25
|
+
try {
|
|
26
|
+
return await this._adapter.cleanVm(this.#vmBackupDir, {
|
|
27
|
+
...options,
|
|
28
|
+
fixMetadata: true,
|
|
29
|
+
onLog: warn,
|
|
30
|
+
lock: false,
|
|
31
|
+
})
|
|
32
|
+
} catch (error) {
|
|
33
|
+
warn(error)
|
|
34
|
+
return {}
|
|
35
|
+
}
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
async beforeBackup() {
|
|
@@ -43,7 +51,13 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
|
|
|
43
51
|
// merge worker only compatible with local remotes
|
|
44
52
|
const { handler } = this._adapter
|
|
45
53
|
if (merge && !disableMergeWorker && typeof handler._getRealPath === 'function') {
|
|
46
|
-
|
|
54
|
+
const taskFile =
|
|
55
|
+
join(MergeWorker.CLEAN_VM_QUEUE, formatFilenameDate(new Date())) +
|
|
56
|
+
'-' +
|
|
57
|
+
// add a random suffix to avoid collision in case multiple tasks are created at the same second
|
|
58
|
+
Math.random().toString(36).slice(2)
|
|
59
|
+
|
|
60
|
+
await handler.outputFile(taskFile, this._backup.vm.uuid)
|
|
47
61
|
const remotePath = handler._getRealPath()
|
|
48
62
|
await MergeWorker.run(remotePath)
|
|
49
63
|
}
|