@xen-orchestra/backups 0.13.0 → 0.16.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 +52 -57
- package/_VmBackup.js +37 -10
- package/_cleanVm.js +77 -43
- package/merge-worker/cli.js +69 -0
- package/merge-worker/index.js +25 -0
- package/package.json +9 -7
- package/writers/DeltaBackupWriter.js +21 -26
- package/writers/_MixinBackupWriter.js +24 -7
- package/writers/_checkVhd.js +3 -2
package/RemoteAdapter.js
CHANGED
|
@@ -3,19 +3,19 @@ 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 { basename, dirname, join, normalize, resolve } = require('path')
|
|
6
|
+
const { dirname, join, normalize, resolve } = require('path')
|
|
8
7
|
const { createLogger } = require('@xen-orchestra/log')
|
|
9
|
-
const {
|
|
8
|
+
const { VhdAbstract, createVhdDirectoryFromStream } = require('vhd-lib')
|
|
10
9
|
const { deduped } = require('@vates/disposable/deduped.js')
|
|
11
10
|
const { execFile } = require('child_process')
|
|
12
11
|
const { readdir, stat } = require('fs-extra')
|
|
12
|
+
const { v4: uuidv4 } = require('uuid')
|
|
13
13
|
const { ZipFile } = require('yazl')
|
|
14
14
|
|
|
15
15
|
const { BACKUP_DIR } = require('./_getVmBackupDir.js')
|
|
16
16
|
const { cleanVm } = require('./_cleanVm.js')
|
|
17
17
|
const { getTmpDir } = require('./_getTmpDir.js')
|
|
18
|
-
const { isMetadataFile
|
|
18
|
+
const { isMetadataFile } = require('./_backupType.js')
|
|
19
19
|
const { isValidXva } = require('./_isValidXva.js')
|
|
20
20
|
const { listPartitions, LVM_PARTITION_TYPE } = require('./_listPartitions.js')
|
|
21
21
|
const { lvs, pvs } = require('./_lvm.js')
|
|
@@ -77,48 +77,6 @@ class RemoteAdapter {
|
|
|
77
77
|
return this._handler
|
|
78
78
|
}
|
|
79
79
|
|
|
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 Vhd(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
80
|
async _findPartition(devicePath, partitionId) {
|
|
123
81
|
const partitions = await listPartitions(devicePath)
|
|
124
82
|
const partition = partitions.find(_ => _.id === partitionId)
|
|
@@ -253,16 +211,9 @@ class RemoteAdapter {
|
|
|
253
211
|
|
|
254
212
|
async deleteDeltaVmBackups(backups) {
|
|
255
213
|
const handler = this._handler
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
handler.unlink(_filename),
|
|
260
|
-
asyncMap(Object.values(vhds), async _ => {
|
|
261
|
-
mergedDataSize += await this._deleteVhd(resolveRelativeFromFile(_filename, _))
|
|
262
|
-
}),
|
|
263
|
-
])
|
|
264
|
-
)
|
|
265
|
-
return mergedDataSize
|
|
214
|
+
|
|
215
|
+
// unused VHDs will be detected by `cleanVm`
|
|
216
|
+
await asyncMapSettled(backups, ({ _filename }) => VhdAbstract.unlink(handler, _filename))
|
|
266
217
|
}
|
|
267
218
|
|
|
268
219
|
async deleteMetadataBackup(backupId) {
|
|
@@ -361,6 +312,17 @@ class RemoteAdapter {
|
|
|
361
312
|
return yield this._getPartition(devicePath, await this._findPartition(devicePath, partitionId))
|
|
362
313
|
}
|
|
363
314
|
|
|
315
|
+
// this function will be the one where we plug the logic of the storage format by fs type/user settings
|
|
316
|
+
|
|
317
|
+
// if the file is named .vhd => vhd
|
|
318
|
+
// if the file is named alias.vhd => alias to a vhd
|
|
319
|
+
getVhdFileName(baseName) {
|
|
320
|
+
if (this._handler.type === 's3') {
|
|
321
|
+
return `${baseName}.alias.vhd` // we want an alias to a vhddirectory
|
|
322
|
+
}
|
|
323
|
+
return `${baseName}.vhd`
|
|
324
|
+
}
|
|
325
|
+
|
|
364
326
|
async listAllVmBackups() {
|
|
365
327
|
const handler = this._handler
|
|
366
328
|
|
|
@@ -505,6 +467,24 @@ class RemoteAdapter {
|
|
|
505
467
|
return backups.sort(compareTimestamp)
|
|
506
468
|
}
|
|
507
469
|
|
|
470
|
+
async writeVhd(path, input, { checksum = true, validator = noop } = {}) {
|
|
471
|
+
const handler = this._handler
|
|
472
|
+
let dataPath = path
|
|
473
|
+
|
|
474
|
+
if (path.endsWith('.alias.vhd')) {
|
|
475
|
+
await createVhdDirectoryFromStream(handler, `${dirname(path)}/data/${uuidv4()}.vhd`, input, {
|
|
476
|
+
concurrency: 16,
|
|
477
|
+
async validator() {
|
|
478
|
+
await input.task
|
|
479
|
+
return validator.apply(this, arguments)
|
|
480
|
+
},
|
|
481
|
+
})
|
|
482
|
+
await VhdAbstract.createAlias(handler, path, dataPath)
|
|
483
|
+
} else {
|
|
484
|
+
await this.outputStream(dataPath, input, { checksum, validator })
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
508
488
|
async outputStream(path, input, { checksum = true, validator = noop } = {}) {
|
|
509
489
|
await this._handler.outputStream(path, input, {
|
|
510
490
|
checksum,
|
|
@@ -516,6 +496,21 @@ class RemoteAdapter {
|
|
|
516
496
|
})
|
|
517
497
|
}
|
|
518
498
|
|
|
499
|
+
async _createSyntheticStream(handler, paths) {
|
|
500
|
+
// I don't want the vhds to be disposed on return
|
|
501
|
+
// but only when the stream is done ( or failed )
|
|
502
|
+
const disposables = await Disposable.all(paths.map(path => openVhd(handler, path)))
|
|
503
|
+
const vhds = disposables.value
|
|
504
|
+
const synthetic = new VhdSynthetic(vhds)
|
|
505
|
+
await synthetic.readHeaderAndFooter()
|
|
506
|
+
await synthetic.readBlockAllocationTable()
|
|
507
|
+
const stream = await synthetic.stream()
|
|
508
|
+
stream.on('end', () => disposables.dispose())
|
|
509
|
+
stream.on('close', () => disposables.dispose())
|
|
510
|
+
stream.on('error', () => disposables.dispose())
|
|
511
|
+
return stream
|
|
512
|
+
}
|
|
513
|
+
|
|
519
514
|
async readDeltaVmBackup(metadata) {
|
|
520
515
|
const handler = this._handler
|
|
521
516
|
const { vbds, vdis, vhds, vifs, vm } = metadata
|
|
@@ -523,7 +518,7 @@ class RemoteAdapter {
|
|
|
523
518
|
|
|
524
519
|
const streams = {}
|
|
525
520
|
await asyncMapSettled(Object.keys(vdis), async id => {
|
|
526
|
-
streams[`${id}.vhd`] = await
|
|
521
|
+
streams[`${id}.vhd`] = await this._createSyntheticStream(handler, join(dir, vhds[id]))
|
|
527
522
|
})
|
|
528
523
|
|
|
529
524
|
return {
|
package/_VmBackup.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const assert = require('assert')
|
|
2
2
|
const findLast = require('lodash/findLast.js')
|
|
3
|
+
const groupBy = require('lodash/groupBy.js')
|
|
3
4
|
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
|
|
4
5
|
const keyBy = require('lodash/keyBy.js')
|
|
5
6
|
const mapValues = require('lodash/mapValues.js')
|
|
6
|
-
const { asyncMap
|
|
7
|
+
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
7
8
|
const { createLogger } = require('@xen-orchestra/log')
|
|
8
9
|
const { defer } = require('golike-defer')
|
|
9
10
|
const { formatDateTime } = require('@xen-orchestra/xapi')
|
|
@@ -284,17 +285,28 @@ exports.VmBackup = class VmBackup {
|
|
|
284
285
|
}
|
|
285
286
|
|
|
286
287
|
async _removeUnusedSnapshots() {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const { scheduleId } = this
|
|
290
|
-
const scheduleSnapshots = this._jobSnapshots.filter(_ => _.other_config['xo:backup:schedule'] === scheduleId)
|
|
291
|
-
|
|
288
|
+
const jobSettings = this.job.settings
|
|
292
289
|
const baseVmRef = this._baseVm?.$ref
|
|
290
|
+
const { config } = this
|
|
291
|
+
const baseSettings = {
|
|
292
|
+
...config.defaultSettings,
|
|
293
|
+
...config.metadata.defaultSettings,
|
|
294
|
+
...jobSettings[''],
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const snapshotsPerSchedule = groupBy(this._jobSnapshots, _ => _.other_config['xo:backup:schedule'])
|
|
293
298
|
const xapi = this._xapi
|
|
294
|
-
await asyncMap(
|
|
295
|
-
|
|
296
|
-
|
|
299
|
+
await asyncMap(Object.entries(snapshotsPerSchedule), ([scheduleId, snapshots]) => {
|
|
300
|
+
const settings = {
|
|
301
|
+
...baseSettings,
|
|
302
|
+
...jobSettings[scheduleId],
|
|
303
|
+
...jobSettings[this.vm.uuid],
|
|
297
304
|
}
|
|
305
|
+
return asyncMap(getOldEntries(settings.snapshotRetention, snapshots), ({ $ref }) => {
|
|
306
|
+
if ($ref !== baseVmRef) {
|
|
307
|
+
return xapi.VM_destroy($ref)
|
|
308
|
+
}
|
|
309
|
+
})
|
|
298
310
|
})
|
|
299
311
|
}
|
|
300
312
|
|
|
@@ -303,12 +315,14 @@ exports.VmBackup = class VmBackup {
|
|
|
303
315
|
|
|
304
316
|
let baseVm = findLast(this._jobSnapshots, _ => 'xo:backup:exported' in _.other_config)
|
|
305
317
|
if (baseVm === undefined) {
|
|
318
|
+
debug('no base VM found')
|
|
306
319
|
return
|
|
307
320
|
}
|
|
308
321
|
|
|
309
322
|
const fullInterval = this._settings.fullInterval
|
|
310
323
|
const deltaChainLength = +(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1
|
|
311
324
|
if (!(fullInterval === 0 || fullInterval > deltaChainLength)) {
|
|
325
|
+
debug('not using base VM becaust fullInterval reached')
|
|
312
326
|
return
|
|
313
327
|
}
|
|
314
328
|
|
|
@@ -323,6 +337,10 @@ exports.VmBackup = class VmBackup {
|
|
|
323
337
|
const srcVdi = srcVdis[snapshotOf]
|
|
324
338
|
if (srcVdi !== undefined) {
|
|
325
339
|
baseUuidToSrcVdi.set(await xapi.getField('VDI', baseRef, 'uuid'), srcVdi)
|
|
340
|
+
} else {
|
|
341
|
+
debug('no base VDI found', {
|
|
342
|
+
vdi: srcVdi.uuid,
|
|
343
|
+
})
|
|
326
344
|
}
|
|
327
345
|
})
|
|
328
346
|
|
|
@@ -335,7 +353,16 @@ exports.VmBackup = class VmBackup {
|
|
|
335
353
|
|
|
336
354
|
const fullVdisRequired = new Set()
|
|
337
355
|
baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
|
|
338
|
-
if (
|
|
356
|
+
if (presentBaseVdis.has(baseUuid)) {
|
|
357
|
+
debug('found base VDI', {
|
|
358
|
+
base: baseUuid,
|
|
359
|
+
vdi: srcVdi.uuid,
|
|
360
|
+
})
|
|
361
|
+
} else {
|
|
362
|
+
debug('missing base VDI', {
|
|
363
|
+
base: baseUuid,
|
|
364
|
+
vdi: srcVdi.uuid,
|
|
365
|
+
})
|
|
339
366
|
fullVdisRequired.add(srcVdi.uuid)
|
|
340
367
|
}
|
|
341
368
|
})
|
package/_cleanVm.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
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
|
+
const { Task } = require('./Task.js')
|
|
11
|
+
const { Disposable } = require('promise-toolbox')
|
|
12
|
+
|
|
10
13
|
// chain is an array of VHDs from child to parent
|
|
11
14
|
//
|
|
12
15
|
// the whole chain will be merged into parent, parent will be renamed to child
|
|
13
16
|
// and all the others will deleted
|
|
14
|
-
|
|
17
|
+
async function mergeVhdChain(chain, { handler, onLog, remove, merge }) {
|
|
15
18
|
assert(chain.length >= 2)
|
|
16
19
|
|
|
17
20
|
let child = chain[0]
|
|
@@ -44,7 +47,7 @@ const mergeVhdChain = limitConcurrency(1)(async function mergeVhdChain(chain, {
|
|
|
44
47
|
}
|
|
45
48
|
}, 10e3)
|
|
46
49
|
|
|
47
|
-
await mergeVhd(
|
|
50
|
+
const mergedSize = await mergeVhd(
|
|
48
51
|
handler,
|
|
49
52
|
parent,
|
|
50
53
|
handler,
|
|
@@ -63,17 +66,19 @@ const mergeVhdChain = limitConcurrency(1)(async function mergeVhdChain(chain, {
|
|
|
63
66
|
clearInterval(handle)
|
|
64
67
|
|
|
65
68
|
await Promise.all([
|
|
66
|
-
|
|
69
|
+
VhdAbstract.rename(handler, parent, child),
|
|
67
70
|
asyncMap(children.slice(0, -1), child => {
|
|
68
71
|
onLog(`the VHD ${child} is unused`)
|
|
69
72
|
if (remove) {
|
|
70
73
|
onLog(`deleting unused VHD ${child}`)
|
|
71
|
-
return
|
|
74
|
+
return VhdAbstract.unlink(handler, child)
|
|
72
75
|
}
|
|
73
76
|
}),
|
|
74
77
|
])
|
|
78
|
+
|
|
79
|
+
return mergedSize
|
|
75
80
|
}
|
|
76
|
-
}
|
|
81
|
+
}
|
|
77
82
|
|
|
78
83
|
const noop = Function.prototype
|
|
79
84
|
|
|
@@ -114,7 +119,14 @@ const listVhds = async (handler, vmDir) => {
|
|
|
114
119
|
return { vhds, interruptedVhds }
|
|
115
120
|
}
|
|
116
121
|
|
|
117
|
-
|
|
122
|
+
const defaultMergeLimiter = limitConcurrency(1)
|
|
123
|
+
|
|
124
|
+
exports.cleanVm = async function cleanVm(
|
|
125
|
+
vmDir,
|
|
126
|
+
{ fixMetadata, remove, merge, mergeLimiter = defaultMergeLimiter, onLog = noop }
|
|
127
|
+
) {
|
|
128
|
+
const limitedMergeVhdChain = mergeLimiter(mergeVhdChain)
|
|
129
|
+
|
|
118
130
|
const handler = this._handler
|
|
119
131
|
|
|
120
132
|
const vhds = new Set()
|
|
@@ -126,53 +138,55 @@ exports.cleanVm = async function cleanVm(vmDir, { fixMetadata, remove, merge, on
|
|
|
126
138
|
// remove broken VHDs
|
|
127
139
|
await asyncMap(vhdsList.vhds, async path => {
|
|
128
140
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
141
154
|
}
|
|
142
|
-
|
|
143
|
-
}
|
|
155
|
+
})
|
|
144
156
|
} catch (error) {
|
|
145
157
|
onLog(`error while checking the VHD with path ${path}`, { error })
|
|
146
158
|
if (error?.code === 'ERR_ASSERTION' && remove) {
|
|
147
159
|
onLog(`deleting broken ${path}`)
|
|
148
|
-
|
|
160
|
+
return VhdAbstract.unlink(handler, path)
|
|
149
161
|
}
|
|
150
162
|
}
|
|
151
163
|
})
|
|
152
164
|
|
|
165
|
+
// @todo : add check for data folder of alias not referenced in a valid alias
|
|
166
|
+
|
|
153
167
|
// remove VHDs with missing ancestors
|
|
154
168
|
{
|
|
155
169
|
const deletions = []
|
|
156
170
|
|
|
157
171
|
// return true if the VHD has been deleted or is missing
|
|
158
|
-
const deleteIfOrphan =
|
|
159
|
-
const parent = vhdParents[
|
|
172
|
+
const deleteIfOrphan = vhdPath => {
|
|
173
|
+
const parent = vhdParents[vhdPath]
|
|
160
174
|
if (parent === undefined) {
|
|
161
175
|
return
|
|
162
176
|
}
|
|
163
177
|
|
|
164
178
|
// no longer needs to be checked
|
|
165
|
-
delete vhdParents[
|
|
179
|
+
delete vhdParents[vhdPath]
|
|
166
180
|
|
|
167
181
|
deleteIfOrphan(parent)
|
|
168
182
|
|
|
169
183
|
if (!vhds.has(parent)) {
|
|
170
|
-
vhds.delete(
|
|
184
|
+
vhds.delete(vhdPath)
|
|
171
185
|
|
|
172
|
-
onLog(`the parent ${parent} of the VHD ${
|
|
186
|
+
onLog(`the parent ${parent} of the VHD ${vhdPath} is missing`)
|
|
173
187
|
if (remove) {
|
|
174
|
-
onLog(`deleting orphan VHD ${
|
|
175
|
-
deletions.push(
|
|
188
|
+
onLog(`deleting orphan VHD ${vhdPath}`)
|
|
189
|
+
deletions.push(VhdAbstract.unlink(handler, vhdPath))
|
|
176
190
|
}
|
|
177
191
|
}
|
|
178
192
|
}
|
|
@@ -242,15 +256,26 @@ exports.cleanVm = async function cleanVm(vmDir, { fixMetadata, remove, merge, on
|
|
|
242
256
|
const { vhds } = metadata
|
|
243
257
|
return Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
|
|
244
258
|
})()
|
|
245
|
-
|
|
246
259
|
// FIXME: find better approach by keeping as much of the backup as
|
|
247
260
|
// possible (existing disks) even if one disk is missing
|
|
248
261
|
if (linkedVhds.every(_ => vhds.has(_))) {
|
|
249
262
|
linkedVhds.forEach(_ => unusedVhds.delete(_))
|
|
250
263
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
264
|
+
// checking the size of a vhd directory is costly
|
|
265
|
+
// 1 Http Query per 1000 blocks
|
|
266
|
+
// we only check size of all the vhd are VhdFiles
|
|
267
|
+
|
|
268
|
+
const shouldComputeSize = linkedVhds.every(vhd => vhd instanceof VhdFile)
|
|
269
|
+
if (shouldComputeSize) {
|
|
270
|
+
try {
|
|
271
|
+
await Disposable.use(Disposable.all(linkedVhds.map(vhdPath => openVhd(handler, vhdPath))), async vhds => {
|
|
272
|
+
const sizes = await asyncMap(vhds, vhd => vhd.getSize())
|
|
273
|
+
size = sum(sizes)
|
|
274
|
+
})
|
|
275
|
+
} catch (error) {
|
|
276
|
+
onLog(`failed to get size of ${json}`, { error })
|
|
277
|
+
}
|
|
278
|
+
}
|
|
254
279
|
} else {
|
|
255
280
|
onLog(`Some VHDs linked to the metadata ${json} are missing`)
|
|
256
281
|
if (remove) {
|
|
@@ -279,6 +304,7 @@ exports.cleanVm = async function cleanVm(vmDir, { fixMetadata, remove, merge, on
|
|
|
279
304
|
|
|
280
305
|
// TODO: parallelize by vm/job/vdi
|
|
281
306
|
const unusedVhdsDeletion = []
|
|
307
|
+
const toMerge = []
|
|
282
308
|
{
|
|
283
309
|
// VHD chains (as list from child to ancestor) to merge indexed by last
|
|
284
310
|
// ancestor
|
|
@@ -312,7 +338,7 @@ exports.cleanVm = async function cleanVm(vmDir, { fixMetadata, remove, merge, on
|
|
|
312
338
|
onLog(`the VHD ${vhd} is unused`)
|
|
313
339
|
if (remove) {
|
|
314
340
|
onLog(`deleting unused VHD ${vhd}`)
|
|
315
|
-
unusedVhdsDeletion.push(
|
|
341
|
+
unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
|
|
316
342
|
}
|
|
317
343
|
}
|
|
318
344
|
|
|
@@ -321,22 +347,25 @@ exports.cleanVm = async function cleanVm(vmDir, { fixMetadata, remove, merge, on
|
|
|
321
347
|
})
|
|
322
348
|
|
|
323
349
|
// merge interrupted VHDs
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
})
|
|
328
|
-
}
|
|
350
|
+
vhdsList.interruptedVhds.forEach(parent => {
|
|
351
|
+
vhdChainsToMerge[parent] = [vhdChildren[parent], parent]
|
|
352
|
+
})
|
|
329
353
|
|
|
330
|
-
Object.
|
|
331
|
-
const chain = vhdChainsToMerge[key]
|
|
354
|
+
Object.values(vhdChainsToMerge).forEach(chain => {
|
|
332
355
|
if (chain !== undefined) {
|
|
333
|
-
|
|
356
|
+
toMerge.push(chain)
|
|
334
357
|
}
|
|
335
358
|
})
|
|
336
359
|
}
|
|
337
360
|
|
|
361
|
+
const doMerge = () => {
|
|
362
|
+
const promise = asyncMap(toMerge, async chain => limitedMergeVhdChain(chain, { handler, onLog, remove, merge }))
|
|
363
|
+
return merge ? promise.then(sizes => ({ size: sum(sizes) })) : promise
|
|
364
|
+
}
|
|
365
|
+
|
|
338
366
|
await Promise.all([
|
|
339
367
|
...unusedVhdsDeletion,
|
|
368
|
+
toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : doMerge()),
|
|
340
369
|
asyncMap(unusedXvas, path => {
|
|
341
370
|
onLog(`the XVA ${path} is unused`)
|
|
342
371
|
if (remove) {
|
|
@@ -355,4 +384,9 @@ exports.cleanVm = async function cleanVm(vmDir, { fixMetadata, remove, merge, on
|
|
|
355
384
|
}
|
|
356
385
|
}),
|
|
357
386
|
])
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
// boolean whether some VHDs were merged (or should be merged)
|
|
390
|
+
merge: toMerge.length !== 0,
|
|
391
|
+
}
|
|
358
392
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { catchGlobalErrors } = require('@xen-orchestra/log/configure.js')
|
|
4
|
+
const { createLogger } = require('@xen-orchestra/log')
|
|
5
|
+
const { getSyncedHandler } = require('@xen-orchestra/fs')
|
|
6
|
+
const { join } = require('path')
|
|
7
|
+
const Disposable = require('promise-toolbox/Disposable')
|
|
8
|
+
const min = require('lodash/min')
|
|
9
|
+
|
|
10
|
+
const { getVmBackupDir } = require('../_getVmBackupDir.js')
|
|
11
|
+
const { RemoteAdapter } = require('../RemoteAdapter.js')
|
|
12
|
+
|
|
13
|
+
const { CLEAN_VM_QUEUE } = require('./index.js')
|
|
14
|
+
|
|
15
|
+
// -------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
catchGlobalErrors(createLogger('xo:backups:mergeWorker'))
|
|
18
|
+
|
|
19
|
+
const { fatal, info, warn } = createLogger('xo:backups:mergeWorker')
|
|
20
|
+
|
|
21
|
+
// -------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const main = Disposable.wrap(async function* main(args) {
|
|
24
|
+
const handler = yield getSyncedHandler({ url: 'file://' + process.cwd() })
|
|
25
|
+
|
|
26
|
+
yield handler.lock(CLEAN_VM_QUEUE)
|
|
27
|
+
|
|
28
|
+
const adapter = new RemoteAdapter(handler)
|
|
29
|
+
|
|
30
|
+
const listRetry = async () => {
|
|
31
|
+
const timeoutResolver = resolve => setTimeout(resolve, 10e3)
|
|
32
|
+
for (let i = 0; i < 10; ++i) {
|
|
33
|
+
const entries = await handler.list(CLEAN_VM_QUEUE)
|
|
34
|
+
if (entries.length !== 0) {
|
|
35
|
+
return entries
|
|
36
|
+
}
|
|
37
|
+
await new Promise(timeoutResolver)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let taskFiles
|
|
42
|
+
while ((taskFiles = await listRetry()) !== undefined) {
|
|
43
|
+
const taskFileBasename = min(taskFiles)
|
|
44
|
+
const taskFile = join(CLEAN_VM_QUEUE, '_' + taskFileBasename)
|
|
45
|
+
|
|
46
|
+
// move this task to the end
|
|
47
|
+
await handler.rename(join(CLEAN_VM_QUEUE, taskFileBasename), taskFile)
|
|
48
|
+
try {
|
|
49
|
+
const vmDir = getVmBackupDir(String(await handler.readFile(taskFile)))
|
|
50
|
+
await adapter.cleanVm(vmDir, { merge: true, onLog: info, remove: true })
|
|
51
|
+
|
|
52
|
+
handler.unlink(taskFile).catch(error => warn('deleting task failure', { error }))
|
|
53
|
+
} catch (error) {
|
|
54
|
+
warn('failure handling task', { error })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
info('starting')
|
|
60
|
+
main(process.argv.slice(2)).then(
|
|
61
|
+
() => {
|
|
62
|
+
info('bye :-)')
|
|
63
|
+
},
|
|
64
|
+
error => {
|
|
65
|
+
fatal(error)
|
|
66
|
+
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const { join, resolve } = require('path')
|
|
2
|
+
const { spawn } = require('child_process')
|
|
3
|
+
const { check } = require('proper-lockfile')
|
|
4
|
+
|
|
5
|
+
const CLEAN_VM_QUEUE = (exports.CLEAN_VM_QUEUE = '/xo-vm-backups/.queue/clean-vm/')
|
|
6
|
+
|
|
7
|
+
const CLI_PATH = resolve(__dirname, 'cli.js')
|
|
8
|
+
exports.run = async function runMergeWorker(remotePath) {
|
|
9
|
+
try {
|
|
10
|
+
// TODO: find a way to pass the acquire the lock and then pass it down the worker
|
|
11
|
+
if (await check(join(remotePath, CLEAN_VM_QUEUE))) {
|
|
12
|
+
// already locked, don't start another worker
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
spawn(CLI_PATH, {
|
|
17
|
+
cwd: remotePath,
|
|
18
|
+
detached: true,
|
|
19
|
+
stdio: 'inherit',
|
|
20
|
+
}).unref()
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// we usually don't want to throw if the merge worker failed to start
|
|
23
|
+
return error
|
|
24
|
+
}
|
|
25
|
+
}
|
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.16.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.6"
|
|
14
14
|
},
|
|
@@ -20,10 +20,10 @@
|
|
|
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.
|
|
24
|
-
"@xen-orchestra/log": "^0.
|
|
23
|
+
"@xen-orchestra/fs": "^0.19.1",
|
|
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",
|
|
@@ -32,13 +32,15 @@
|
|
|
32
32
|
"lodash": "^4.17.20",
|
|
33
33
|
"node-zone": "^0.4.0",
|
|
34
34
|
"parse-pairs": "^1.1.0",
|
|
35
|
-
"promise-toolbox": "^0.
|
|
35
|
+
"promise-toolbox": "^0.20.0",
|
|
36
|
+
"proper-lockfile": "^4.1.2",
|
|
36
37
|
"pump": "^3.0.0",
|
|
37
|
-
"
|
|
38
|
+
"uuid": "^8.3.2",
|
|
39
|
+
"vhd-lib": "^2.0.0",
|
|
38
40
|
"yazl": "^2.5.1"
|
|
39
41
|
},
|
|
40
42
|
"peerDependencies": {
|
|
41
|
-
"@xen-orchestra/xapi": "^0.
|
|
43
|
+
"@xen-orchestra/xapi": "^0.8.0"
|
|
42
44
|
},
|
|
43
45
|
"license": "AGPL-3.0-or-later",
|
|
44
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) {
|
|
@@ -113,19 +114,13 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
async _deleteOldEntries() {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
size += await adapter.deleteDeltaVmBackups([oldEntries[i]])
|
|
124
|
-
}
|
|
125
|
-
return {
|
|
126
|
-
size,
|
|
127
|
-
}
|
|
128
|
-
})
|
|
117
|
+
const adapter = this._adapter
|
|
118
|
+
const oldEntries = this._oldEntries
|
|
119
|
+
|
|
120
|
+
// delete sequentially from newest to oldest to avoid unnecessary merges
|
|
121
|
+
for (let i = oldEntries.length; i-- > 0; ) {
|
|
122
|
+
await adapter.deleteDeltaVmBackups([oldEntries[i]])
|
|
123
|
+
}
|
|
129
124
|
}
|
|
130
125
|
|
|
131
126
|
async _transfer({ timestamp, deltaExport, sizeContainers }) {
|
|
@@ -150,7 +145,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
150
145
|
// don't do delta for it
|
|
151
146
|
vdi.uuid
|
|
152
147
|
: vdi.$snapshot_of$uuid
|
|
153
|
-
}/${basename}
|
|
148
|
+
}/${adapter.getVhdFileName(basename)}`
|
|
154
149
|
)
|
|
155
150
|
|
|
156
151
|
const metadataFilename = `${backupDir}/${basename}.json`
|
|
@@ -194,7 +189,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
194
189
|
await checkVhd(handler, parentPath)
|
|
195
190
|
}
|
|
196
191
|
|
|
197
|
-
await adapter.
|
|
192
|
+
await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
|
|
198
193
|
// no checksum for VHDs, because they will be invalidated by
|
|
199
194
|
// merges and chainings
|
|
200
195
|
checksum: false,
|
|
@@ -206,11 +201,11 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
206
201
|
}
|
|
207
202
|
|
|
208
203
|
// set the correct UUID in the VHD
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
})
|
|
214
209
|
})
|
|
215
210
|
)
|
|
216
211
|
return {
|
|
@@ -1,34 +1,51 @@
|
|
|
1
1
|
const { createLogger } = require('@xen-orchestra/log')
|
|
2
|
+
const { join } = require('path')
|
|
2
3
|
|
|
3
|
-
const { getVmBackupDir } = require('../_getVmBackupDir.js')
|
|
4
|
+
const { BACKUP_DIR, getVmBackupDir } = require('../_getVmBackupDir.js')
|
|
5
|
+
const MergeWorker = require('../merge-worker/index.js')
|
|
6
|
+
const { formatFilenameDate } = require('../_filenameDate.js')
|
|
4
7
|
|
|
5
8
|
const { warn } = createLogger('xo:backups:MixinBackupWriter')
|
|
6
9
|
|
|
7
10
|
exports.MixinBackupWriter = (BaseClass = Object) =>
|
|
8
11
|
class MixinBackupWriter extends BaseClass {
|
|
12
|
+
#lock
|
|
13
|
+
#vmBackupDir
|
|
14
|
+
|
|
9
15
|
constructor({ remoteId, ...rest }) {
|
|
10
16
|
super(rest)
|
|
11
17
|
|
|
12
18
|
this._adapter = rest.backup.remoteAdapters[remoteId]
|
|
13
19
|
this._remoteId = remoteId
|
|
14
|
-
|
|
20
|
+
|
|
21
|
+
this.#vmBackupDir = getVmBackupDir(this._backup.vm.uuid)
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
_cleanVm(options) {
|
|
18
25
|
return this._adapter
|
|
19
|
-
.cleanVm(
|
|
26
|
+
.cleanVm(this.#vmBackupDir, { ...options, fixMetadata: true, onLog: warn, lock: false })
|
|
20
27
|
.catch(warn)
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
async beforeBackup() {
|
|
24
31
|
const { handler } = this._adapter
|
|
25
|
-
const vmBackupDir =
|
|
32
|
+
const vmBackupDir = this.#vmBackupDir
|
|
26
33
|
await handler.mktree(vmBackupDir)
|
|
27
|
-
this
|
|
34
|
+
this.#lock = await handler.lock(vmBackupDir)
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
async afterBackup() {
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
const { disableMergeWorker } = this._backup.config
|
|
39
|
+
|
|
40
|
+
const { merge } = await this._cleanVm({ remove: true, merge: disableMergeWorker })
|
|
41
|
+
await this.#lock.dispose()
|
|
42
|
+
|
|
43
|
+
// merge worker only compatible with local remotes
|
|
44
|
+
const { handler } = this._adapter
|
|
45
|
+
if (merge && !disableMergeWorker && typeof handler._getRealPath === 'function') {
|
|
46
|
+
await handler.outputFile(join(MergeWorker.CLEAN_VM_QUEUE, formatFilenameDate(new Date())), this._backup.vm.uuid)
|
|
47
|
+
const remotePath = handler._getRealPath()
|
|
48
|
+
await MergeWorker.run(remotePath)
|
|
49
|
+
}
|
|
33
50
|
}
|
|
34
51
|
}
|
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
|
}
|