@xen-orchestra/backups 0.15.1 → 0.17.0
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 +96 -57
- package/_VmBackup.js +17 -4
- package/_cleanVm.js +49 -32
- package/package.json +7 -6
- package/writers/DeltaBackupWriter.js +14 -13
- package/writers/_MixinBackupWriter.js +19 -5
- package/writers/_checkVhd.js +3 -2
package/RemoteAdapter.js
CHANGED
|
@@ -3,19 +3,20 @@ 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
|
|
7
|
-
const {
|
|
6
|
+
const groupBy = require('lodash/groupBy.js')
|
|
7
|
+
const { dirname, join, normalize, resolve } = require('path')
|
|
8
8
|
const { createLogger } = require('@xen-orchestra/log')
|
|
9
|
-
const {
|
|
9
|
+
const { Constants, createVhdDirectoryFromStream, openVhd, VhdAbstract, 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')
|
|
13
|
+
const { v4: uuidv4 } = require('uuid')
|
|
13
14
|
const { ZipFile } = require('yazl')
|
|
14
15
|
|
|
15
16
|
const { BACKUP_DIR } = require('./_getVmBackupDir.js')
|
|
16
17
|
const { cleanVm } = require('./_cleanVm.js')
|
|
17
18
|
const { getTmpDir } = require('./_getTmpDir.js')
|
|
18
|
-
const { isMetadataFile
|
|
19
|
+
const { isMetadataFile } = require('./_backupType.js')
|
|
19
20
|
const { isValidXva } = require('./_isValidXva.js')
|
|
20
21
|
const { listPartitions, LVM_PARTITION_TYPE } = require('./_listPartitions.js')
|
|
21
22
|
const { lvs, pvs } = require('./_lvm.js')
|
|
@@ -77,48 +78,6 @@ class RemoteAdapter {
|
|
|
77
78
|
return this._handler
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
async _deleteVhd(path) {
|
|
81
|
-
const handler = this._handler
|
|
82
|
-
const vhds = await asyncMapSettled(
|
|
83
|
-
await handler.list(dirname(path), {
|
|
84
|
-
filter: isVhdFile,
|
|
85
|
-
prependDir: true,
|
|
86
|
-
}),
|
|
87
|
-
async path => {
|
|
88
|
-
try {
|
|
89
|
-
const vhd = new VhdFile(handler, path)
|
|
90
|
-
await vhd.readHeaderAndFooter()
|
|
91
|
-
return {
|
|
92
|
-
footer: vhd.footer,
|
|
93
|
-
header: vhd.header,
|
|
94
|
-
path,
|
|
95
|
-
}
|
|
96
|
-
} catch (error) {
|
|
97
|
-
// Do not fail on corrupted VHDs (usually uncleaned temporary files),
|
|
98
|
-
// they are probably inconsequent to the backup process and should not
|
|
99
|
-
// fail it.
|
|
100
|
-
warn(`BackupNg#_deleteVhd ${path}`, { error })
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
)
|
|
104
|
-
const base = basename(path)
|
|
105
|
-
const child = vhds.find(_ => _ !== undefined && _.header.parentUnicodeName === base)
|
|
106
|
-
if (child === undefined) {
|
|
107
|
-
await handler.unlink(path)
|
|
108
|
-
return 0
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
const childPath = child.path
|
|
113
|
-
const mergedDataSize = await mergeVhd(handler, path, handler, childPath)
|
|
114
|
-
await handler.rename(path, childPath)
|
|
115
|
-
return mergedDataSize
|
|
116
|
-
} catch (error) {
|
|
117
|
-
handler.unlink(path).catch(warn)
|
|
118
|
-
throw error
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
81
|
async _findPartition(devicePath, partitionId) {
|
|
123
82
|
const partitions = await listPartitions(devicePath)
|
|
124
83
|
const partition = partitions.find(_ => _.id === partitionId)
|
|
@@ -255,7 +214,7 @@ class RemoteAdapter {
|
|
|
255
214
|
const handler = this._handler
|
|
256
215
|
|
|
257
216
|
// unused VHDs will be detected by `cleanVm`
|
|
258
|
-
await asyncMapSettled(backups, ({ _filename }) =>
|
|
217
|
+
await asyncMapSettled(backups, ({ _filename }) => VhdAbstract.unlink(handler, _filename))
|
|
259
218
|
}
|
|
260
219
|
|
|
261
220
|
async deleteMetadataBackup(backupId) {
|
|
@@ -285,17 +244,22 @@ class RemoteAdapter {
|
|
|
285
244
|
)
|
|
286
245
|
}
|
|
287
246
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
247
|
+
deleteVmBackup(file) {
|
|
248
|
+
return this.deleteVmBackups([file])
|
|
249
|
+
}
|
|
291
250
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
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(', '))
|
|
298
257
|
}
|
|
258
|
+
|
|
259
|
+
await Promise.all([
|
|
260
|
+
delta !== undefined && this.deleteDeltaVmBackups(delta),
|
|
261
|
+
full !== undefined && this.deleteFullVmBackups(full),
|
|
262
|
+
])
|
|
299
263
|
}
|
|
300
264
|
|
|
301
265
|
getDisk = Disposable.factory(this.getDisk)
|
|
@@ -354,6 +318,17 @@ class RemoteAdapter {
|
|
|
354
318
|
return yield this._getPartition(devicePath, await this._findPartition(devicePath, partitionId))
|
|
355
319
|
}
|
|
356
320
|
|
|
321
|
+
// this function will be the one where we plug the logic of the storage format by fs type/user settings
|
|
322
|
+
|
|
323
|
+
// if the file is named .vhd => vhd
|
|
324
|
+
// if the file is named alias.vhd => alias to a vhd
|
|
325
|
+
getVhdFileName(baseName) {
|
|
326
|
+
if (this._handler.type === 's3') {
|
|
327
|
+
return `${baseName}.alias.vhd` // we want an alias to a vhddirectory
|
|
328
|
+
}
|
|
329
|
+
return `${baseName}.vhd`
|
|
330
|
+
}
|
|
331
|
+
|
|
357
332
|
async listAllVmBackups() {
|
|
358
333
|
const handler = this._handler
|
|
359
334
|
|
|
@@ -498,6 +473,24 @@ class RemoteAdapter {
|
|
|
498
473
|
return backups.sort(compareTimestamp)
|
|
499
474
|
}
|
|
500
475
|
|
|
476
|
+
async writeVhd(path, input, { checksum = true, validator = noop } = {}) {
|
|
477
|
+
const handler = this._handler
|
|
478
|
+
|
|
479
|
+
if (path.endsWith('.alias.vhd')) {
|
|
480
|
+
const dataPath = `${dirname(path)}/data/${uuidv4()}.vhd`
|
|
481
|
+
await createVhdDirectoryFromStream(handler, dataPath, input, {
|
|
482
|
+
concurrency: 16,
|
|
483
|
+
async validator() {
|
|
484
|
+
await input.task
|
|
485
|
+
return validator.apply(this, arguments)
|
|
486
|
+
},
|
|
487
|
+
})
|
|
488
|
+
await VhdAbstract.createAlias(handler, path, dataPath)
|
|
489
|
+
} else {
|
|
490
|
+
await this.outputStream(path, input, { checksum, validator })
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
501
494
|
async outputStream(path, input, { checksum = true, validator = noop } = {}) {
|
|
502
495
|
await this._handler.outputStream(path, input, {
|
|
503
496
|
checksum,
|
|
@@ -509,6 +502,52 @@ class RemoteAdapter {
|
|
|
509
502
|
})
|
|
510
503
|
}
|
|
511
504
|
|
|
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
|
+
|
|
523
|
+
// I don't want the vhds to be disposed on return
|
|
524
|
+
// but only when the stream is done ( or failed )
|
|
525
|
+
const disposables = await Disposable.all(disposableVhds)
|
|
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
|
+
|
|
541
|
+
const synthetic = new VhdSynthetic(vhds)
|
|
542
|
+
await synthetic.readHeaderAndFooter()
|
|
543
|
+
await synthetic.readBlockAllocationTable()
|
|
544
|
+
const stream = await synthetic.stream()
|
|
545
|
+
stream.on('end', disposeOnce)
|
|
546
|
+
stream.on('close', disposeOnce)
|
|
547
|
+
stream.on('error', disposeOnce)
|
|
548
|
+
return stream
|
|
549
|
+
}
|
|
550
|
+
|
|
512
551
|
async readDeltaVmBackup(metadata) {
|
|
513
552
|
const handler = this._handler
|
|
514
553
|
const { vbds, vdis, vhds, vifs, vm } = metadata
|
|
@@ -516,7 +555,7 @@ class RemoteAdapter {
|
|
|
516
555
|
|
|
517
556
|
const streams = {}
|
|
518
557
|
await asyncMapSettled(Object.keys(vdis), async id => {
|
|
519
|
-
streams[`${id}.vhd`] = await
|
|
558
|
+
streams[`${id}.vhd`] = await this._createSyntheticStream(handler, join(dir, vhds[id]))
|
|
520
559
|
})
|
|
521
560
|
|
|
522
561
|
return {
|
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
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
const assert = require('assert')
|
|
2
2
|
const sum = require('lodash/sum')
|
|
3
3
|
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
4
|
-
const {
|
|
4
|
+
const { Constants, mergeVhd, openVhd, VhdAbstract, VhdFile } = require('vhd-lib')
|
|
5
5
|
const { dirname, resolve } = require('path')
|
|
6
|
-
const {
|
|
6
|
+
const { DISK_TYPES } = Constants
|
|
7
7
|
const { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } = require('./_backupType.js')
|
|
8
8
|
const { limitConcurrency } = require('limit-concurrency-decorator')
|
|
9
9
|
|
|
10
10
|
const { Task } = require('./Task.js')
|
|
11
|
+
const { Disposable } = require('promise-toolbox')
|
|
11
12
|
|
|
12
13
|
// chain is an array of VHDs from child to parent
|
|
13
14
|
//
|
|
@@ -65,12 +66,12 @@ async function mergeVhdChain(chain, { handler, onLog, remove, merge }) {
|
|
|
65
66
|
clearInterval(handle)
|
|
66
67
|
|
|
67
68
|
await Promise.all([
|
|
68
|
-
|
|
69
|
+
VhdAbstract.rename(handler, parent, child),
|
|
69
70
|
asyncMap(children.slice(0, -1), child => {
|
|
70
71
|
onLog(`the VHD ${child} is unused`)
|
|
71
72
|
if (remove) {
|
|
72
73
|
onLog(`deleting unused VHD ${child}`)
|
|
73
|
-
return
|
|
74
|
+
return VhdAbstract.unlink(handler, child)
|
|
74
75
|
}
|
|
75
76
|
}),
|
|
76
77
|
])
|
|
@@ -137,53 +138,55 @@ exports.cleanVm = async function cleanVm(
|
|
|
137
138
|
// remove broken VHDs
|
|
138
139
|
await asyncMap(vhdsList.vhds, async path => {
|
|
139
140
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
141
|
+
await Disposable.use(openVhd(handler, path, { checkSecondFooter: !vhdsList.interruptedVhds.has(path) }), vhd => {
|
|
142
|
+
vhds.add(path)
|
|
143
|
+
if (vhd.footer.diskType === DISK_TYPES.DIFFERENCING) {
|
|
144
|
+
const parent = resolve('/', dirname(path), vhd.header.parentUnicodeName)
|
|
145
|
+
vhdParents[path] = parent
|
|
146
|
+
if (parent in vhdChildren) {
|
|
147
|
+
const error = new Error('this script does not support multiple VHD children')
|
|
148
|
+
error.parent = parent
|
|
149
|
+
error.child1 = vhdChildren[parent]
|
|
150
|
+
error.child2 = path
|
|
151
|
+
throw error // should we throw?
|
|
152
|
+
}
|
|
153
|
+
vhdChildren[parent] = path
|
|
152
154
|
}
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
+
})
|
|
155
156
|
} catch (error) {
|
|
156
157
|
onLog(`error while checking the VHD with path ${path}`, { error })
|
|
157
158
|
if (error?.code === 'ERR_ASSERTION' && remove) {
|
|
158
159
|
onLog(`deleting broken ${path}`)
|
|
159
|
-
|
|
160
|
+
return VhdAbstract.unlink(handler, path)
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
})
|
|
163
164
|
|
|
165
|
+
// @todo : add check for data folder of alias not referenced in a valid alias
|
|
166
|
+
|
|
164
167
|
// remove VHDs with missing ancestors
|
|
165
168
|
{
|
|
166
169
|
const deletions = []
|
|
167
170
|
|
|
168
171
|
// return true if the VHD has been deleted or is missing
|
|
169
|
-
const deleteIfOrphan =
|
|
170
|
-
const parent = vhdParents[
|
|
172
|
+
const deleteIfOrphan = vhdPath => {
|
|
173
|
+
const parent = vhdParents[vhdPath]
|
|
171
174
|
if (parent === undefined) {
|
|
172
175
|
return
|
|
173
176
|
}
|
|
174
177
|
|
|
175
178
|
// no longer needs to be checked
|
|
176
|
-
delete vhdParents[
|
|
179
|
+
delete vhdParents[vhdPath]
|
|
177
180
|
|
|
178
181
|
deleteIfOrphan(parent)
|
|
179
182
|
|
|
180
183
|
if (!vhds.has(parent)) {
|
|
181
|
-
vhds.delete(
|
|
184
|
+
vhds.delete(vhdPath)
|
|
182
185
|
|
|
183
|
-
onLog(`the parent ${parent} of the VHD ${
|
|
186
|
+
onLog(`the parent ${parent} of the VHD ${vhdPath} is missing`)
|
|
184
187
|
if (remove) {
|
|
185
|
-
onLog(`deleting orphan VHD ${
|
|
186
|
-
deletions.push(
|
|
188
|
+
onLog(`deleting orphan VHD ${vhdPath}`)
|
|
189
|
+
deletions.push(VhdAbstract.unlink(handler, vhdPath))
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
192
|
}
|
|
@@ -254,16 +257,30 @@ exports.cleanVm = async function cleanVm(
|
|
|
254
257
|
return Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
|
|
255
258
|
})()
|
|
256
259
|
|
|
260
|
+
const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
|
|
261
|
+
|
|
257
262
|
// FIXME: find better approach by keeping as much of the backup as
|
|
258
263
|
// possible (existing disks) even if one disk is missing
|
|
259
|
-
if (
|
|
264
|
+
if (missingVhds.length === 0) {
|
|
260
265
|
linkedVhds.forEach(_ => unusedVhds.delete(_))
|
|
261
266
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
267
|
+
// checking the size of a vhd directory is costly
|
|
268
|
+
// 1 Http Query per 1000 blocks
|
|
269
|
+
// we only check size of all the vhd are VhdFiles
|
|
270
|
+
|
|
271
|
+
const shouldComputeSize = linkedVhds.every(vhd => vhd instanceof VhdFile)
|
|
272
|
+
if (shouldComputeSize) {
|
|
273
|
+
try {
|
|
274
|
+
await Disposable.use(Disposable.all(linkedVhds.map(vhdPath => openVhd(handler, vhdPath))), async vhds => {
|
|
275
|
+
const sizes = await asyncMap(vhds, vhd => vhd.getSize())
|
|
276
|
+
size = sum(sizes)
|
|
277
|
+
})
|
|
278
|
+
} catch (error) {
|
|
279
|
+
onLog(`failed to get size of ${json}`, { error })
|
|
280
|
+
}
|
|
281
|
+
}
|
|
265
282
|
} else {
|
|
266
|
-
onLog(`Some VHDs linked to the metadata ${json} are missing
|
|
283
|
+
onLog(`Some VHDs linked to the metadata ${json} are missing`, { missingVhds })
|
|
267
284
|
if (remove) {
|
|
268
285
|
onLog(`deleting incomplete backup ${json}`)
|
|
269
286
|
await handler.unlink(json)
|
|
@@ -324,7 +341,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
324
341
|
onLog(`the VHD ${vhd} is unused`)
|
|
325
342
|
if (remove) {
|
|
326
343
|
onLog(`deleting unused VHD ${vhd}`)
|
|
327
|
-
unusedVhdsDeletion.push(
|
|
344
|
+
unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
|
|
328
345
|
}
|
|
329
346
|
}
|
|
330
347
|
|
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.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.6"
|
|
14
14
|
},
|
|
@@ -16,14 +16,14 @@
|
|
|
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.
|
|
23
|
+
"@xen-orchestra/fs": "^0.19.2",
|
|
24
24
|
"@xen-orchestra/log": "^0.3.0",
|
|
25
25
|
"@xen-orchestra/template": "^0.1.0",
|
|
26
|
-
"compare-versions": "^
|
|
26
|
+
"compare-versions": "^4.0.1",
|
|
27
27
|
"d3-time-format": "^3.0.0",
|
|
28
28
|
"end-of-stream": "^1.4.4",
|
|
29
29
|
"fs-extra": "^10.0.0",
|
|
@@ -35,11 +35,12 @@
|
|
|
35
35
|
"promise-toolbox": "^0.20.0",
|
|
36
36
|
"proper-lockfile": "^4.1.2",
|
|
37
37
|
"pump": "^3.0.0",
|
|
38
|
-
"
|
|
38
|
+
"uuid": "^8.3.2",
|
|
39
|
+
"vhd-lib": "^2.0.4",
|
|
39
40
|
"yazl": "^2.5.1"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
|
-
"@xen-orchestra/xapi": "^0.8.
|
|
43
|
+
"@xen-orchestra/xapi": "^0.8.4"
|
|
43
44
|
},
|
|
44
45
|
"license": "AGPL-3.0-or-later",
|
|
45
46
|
"author": {
|
|
@@ -3,7 +3,7 @@ const map = require('lodash/map.js')
|
|
|
3
3
|
const mapValues = require('lodash/mapValues.js')
|
|
4
4
|
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
|
|
5
5
|
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
6
|
-
const { chainVhd, checkVhdChain,
|
|
6
|
+
const { chainVhd, checkVhdChain, openVhd, VhdAbstract } = require('vhd-lib')
|
|
7
7
|
const { createLogger } = require('@xen-orchestra/log')
|
|
8
8
|
const { dirname } = require('path')
|
|
9
9
|
|
|
@@ -16,6 +16,7 @@ const { MixinBackupWriter } = require('./_MixinBackupWriter.js')
|
|
|
16
16
|
const { AbstractDeltaWriter } = require('./_AbstractDeltaWriter.js')
|
|
17
17
|
const { checkVhd } = require('./_checkVhd.js')
|
|
18
18
|
const { packUuid } = require('./_packUuid.js')
|
|
19
|
+
const { Disposable } = require('promise-toolbox')
|
|
19
20
|
|
|
20
21
|
const { warn } = createLogger('xo:backups:DeltaBackupWriter')
|
|
21
22
|
|
|
@@ -37,13 +38,13 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
37
38
|
await asyncMap(vhds, async path => {
|
|
38
39
|
try {
|
|
39
40
|
await checkVhdChain(handler, path)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
await Disposable.use(
|
|
42
|
+
openVhd(handler, path),
|
|
43
|
+
vhd => (found = found || vhd.footer.uuid.equals(packUuid(baseUuid)))
|
|
44
|
+
)
|
|
44
45
|
} catch (error) {
|
|
45
46
|
warn('checkBaseVdis', { error })
|
|
46
|
-
await ignoreErrors.call(
|
|
47
|
+
await ignoreErrors.call(VhdAbstract.unlink(handler, path))
|
|
47
48
|
}
|
|
48
49
|
})
|
|
49
50
|
} catch (error) {
|
|
@@ -144,7 +145,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
144
145
|
// don't do delta for it
|
|
145
146
|
vdi.uuid
|
|
146
147
|
: vdi.$snapshot_of$uuid
|
|
147
|
-
}/${basename}
|
|
148
|
+
}/${adapter.getVhdFileName(basename)}`
|
|
148
149
|
)
|
|
149
150
|
|
|
150
151
|
const metadataFilename = `${backupDir}/${basename}.json`
|
|
@@ -188,7 +189,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
188
189
|
await checkVhd(handler, parentPath)
|
|
189
190
|
}
|
|
190
191
|
|
|
191
|
-
await adapter.
|
|
192
|
+
await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
|
|
192
193
|
// no checksum for VHDs, because they will be invalidated by
|
|
193
194
|
// merges and chainings
|
|
194
195
|
checksum: false,
|
|
@@ -200,11 +201,11 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
// set the correct UUID in the VHD
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
await Disposable.use(openVhd(handler, path), async vhd => {
|
|
205
|
+
vhd.footer.uuid = packUuid(vdi.uuid)
|
|
206
|
+
await vhd.readBlockAllocationTable() // required by writeFooter()
|
|
207
|
+
await vhd.writeFooter()
|
|
208
|
+
})
|
|
208
209
|
})
|
|
209
210
|
)
|
|
210
211
|
return {
|
|
@@ -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
|
}
|
package/writers/_checkVhd.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const
|
|
1
|
+
const openVhd = require('vhd-lib').openVhd
|
|
2
|
+
const Disposable = require('promise-toolbox/Disposable')
|
|
2
3
|
|
|
3
4
|
exports.checkVhd = async function checkVhd(handler, path) {
|
|
4
|
-
await
|
|
5
|
+
await Disposable.use(openVhd(handler, path), () => {})
|
|
5
6
|
}
|