@xen-orchestra/backups 0.32.0 → 0.34.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/Backup.js +33 -17
- package/RemoteAdapter.js +37 -10
- package/_VmBackup.js +14 -4
- package/_cleanVm.js +2 -1
- package/_createStreamThrottle.js +17 -0
- package/package.json +6 -5
- package/writers/DeltaBackupWriter.js +1 -31
- package/writers/DeltaReplicationWriter.js +1 -0
- package/writers/FullReplicationWriter.js +1 -0
package/Backup.js
CHANGED
|
@@ -12,6 +12,7 @@ const { PoolMetadataBackup } = require('./_PoolMetadataBackup.js')
|
|
|
12
12
|
const { Task } = require('./Task.js')
|
|
13
13
|
const { VmBackup } = require('./_VmBackup.js')
|
|
14
14
|
const { XoMetadataBackup } = require('./_XoMetadataBackup.js')
|
|
15
|
+
const createStreamThrottle = require('./_createStreamThrottle.js')
|
|
15
16
|
|
|
16
17
|
const noop = Function.prototype
|
|
17
18
|
|
|
@@ -40,6 +41,7 @@ const DEFAULT_VM_SETTINGS = {
|
|
|
40
41
|
fullInterval: 0,
|
|
41
42
|
healthCheckSr: undefined,
|
|
42
43
|
healthCheckVmsWithTags: [],
|
|
44
|
+
maxExportRate: 0,
|
|
43
45
|
maxMergedDeltasPerRun: Infinity,
|
|
44
46
|
offlineBackup: false,
|
|
45
47
|
offlineSnapshot: false,
|
|
@@ -226,9 +228,11 @@ exports.Backup = class Backup {
|
|
|
226
228
|
// FIXME: proper SimpleIdPattern handling
|
|
227
229
|
const getSnapshotNameLabel = this._getSnapshotNameLabel
|
|
228
230
|
const schedule = this._schedule
|
|
231
|
+
const settings = this._settings
|
|
232
|
+
|
|
233
|
+
const throttleStream = createStreamThrottle(settings.maxExportRate)
|
|
229
234
|
|
|
230
235
|
const config = this._config
|
|
231
|
-
const settings = this._settings
|
|
232
236
|
await Disposable.use(
|
|
233
237
|
Disposable.all(
|
|
234
238
|
extractIdsFromSimplePattern(job.srs).map(id =>
|
|
@@ -265,23 +269,35 @@ exports.Backup = class Backup {
|
|
|
265
269
|
const allSettings = this._job.settings
|
|
266
270
|
const baseSettings = this._baseSettings
|
|
267
271
|
|
|
268
|
-
const handleVm = vmUuid =>
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
272
|
+
const handleVm = vmUuid => {
|
|
273
|
+
const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
|
|
274
|
+
|
|
275
|
+
return this._getRecord('VM', vmUuid).then(
|
|
276
|
+
disposableVm =>
|
|
277
|
+
Disposable.use(disposableVm, vm => {
|
|
278
|
+
taskStart.data.name_label = vm.name_label
|
|
279
|
+
return runTask(taskStart, () =>
|
|
280
|
+
new VmBackup({
|
|
281
|
+
baseSettings,
|
|
282
|
+
config,
|
|
283
|
+
getSnapshotNameLabel,
|
|
284
|
+
healthCheckSr,
|
|
285
|
+
job,
|
|
286
|
+
remoteAdapters,
|
|
287
|
+
schedule,
|
|
288
|
+
settings: { ...settings, ...allSettings[vm.uuid] },
|
|
289
|
+
srs,
|
|
290
|
+
throttleStream,
|
|
291
|
+
vm,
|
|
292
|
+
}).run()
|
|
293
|
+
)
|
|
294
|
+
}),
|
|
295
|
+
error =>
|
|
296
|
+
runTask(taskStart, () => {
|
|
297
|
+
throw error
|
|
298
|
+
})
|
|
284
299
|
)
|
|
300
|
+
}
|
|
285
301
|
const { concurrency } = settings
|
|
286
302
|
await asyncMapSettled(vmIds, concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm))
|
|
287
303
|
}
|
package/RemoteAdapter.js
CHANGED
|
@@ -10,7 +10,14 @@ const groupBy = require('lodash/groupBy.js')
|
|
|
10
10
|
const pickBy = require('lodash/pickBy.js')
|
|
11
11
|
const { dirname, join, normalize, resolve } = require('path')
|
|
12
12
|
const { createLogger } = require('@xen-orchestra/log')
|
|
13
|
-
const {
|
|
13
|
+
const {
|
|
14
|
+
createVhdDirectoryFromStream,
|
|
15
|
+
createVhdStreamWithLength,
|
|
16
|
+
openVhd,
|
|
17
|
+
VhdAbstract,
|
|
18
|
+
VhdDirectory,
|
|
19
|
+
VhdSynthetic,
|
|
20
|
+
} = require('vhd-lib')
|
|
14
21
|
const { deduped } = require('@vates/disposable/deduped.js')
|
|
15
22
|
const { decorateMethodsWith } = require('@vates/decorate-with')
|
|
16
23
|
const { compose } = require('@vates/compose')
|
|
@@ -32,6 +39,7 @@ const { watchStreamSize } = require('./_watchStreamSize')
|
|
|
32
39
|
// @todo : this import is marked extraneous , sould be fixed when lib is published
|
|
33
40
|
const { mount } = require('@vates/fuse-vhd')
|
|
34
41
|
const { asyncEach } = require('@vates/async-each')
|
|
42
|
+
const { strictEqual } = require('assert')
|
|
35
43
|
|
|
36
44
|
const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
|
|
37
45
|
exports.DIR_XO_CONFIG_BACKUPS = DIR_XO_CONFIG_BACKUPS
|
|
@@ -209,8 +217,8 @@ class RemoteAdapter {
|
|
|
209
217
|
|
|
210
218
|
const isVhdDirectory = vhd instanceof VhdDirectory
|
|
211
219
|
return isVhdDirectory
|
|
212
|
-
? this
|
|
213
|
-
: !this
|
|
220
|
+
? this.useVhdDirectory() && this.#getCompressionType() === vhd.compressionType
|
|
221
|
+
: !this.useVhdDirectory()
|
|
214
222
|
})
|
|
215
223
|
}
|
|
216
224
|
|
|
@@ -321,12 +329,12 @@ class RemoteAdapter {
|
|
|
321
329
|
return this._vhdDirectoryCompression
|
|
322
330
|
}
|
|
323
331
|
|
|
324
|
-
|
|
332
|
+
useVhdDirectory() {
|
|
325
333
|
return this.handler.useVhdDirectory()
|
|
326
334
|
}
|
|
327
335
|
|
|
328
336
|
#useAlias() {
|
|
329
|
-
return this
|
|
337
|
+
return this.useVhdDirectory()
|
|
330
338
|
}
|
|
331
339
|
|
|
332
340
|
async *#getDiskLegacy(diskId) {
|
|
@@ -658,9 +666,9 @@ class RemoteAdapter {
|
|
|
658
666
|
return path
|
|
659
667
|
}
|
|
660
668
|
|
|
661
|
-
async writeVhd(path, input, { checksum = true, validator = noop, writeBlockConcurrency
|
|
669
|
+
async writeVhd(path, input, { checksum = true, validator = noop, writeBlockConcurrency } = {}) {
|
|
662
670
|
const handler = this._handler
|
|
663
|
-
if (this
|
|
671
|
+
if (this.useVhdDirectory()) {
|
|
664
672
|
const dataPath = `${dirname(path)}/data/${uuidv4()}.vhd`
|
|
665
673
|
const size = await createVhdDirectoryFromStream(handler, dataPath, input, {
|
|
666
674
|
concurrency: writeBlockConcurrency,
|
|
@@ -669,22 +677,41 @@ class RemoteAdapter {
|
|
|
669
677
|
await input.task
|
|
670
678
|
return validator.apply(this, arguments)
|
|
671
679
|
},
|
|
672
|
-
nbdClient,
|
|
673
680
|
})
|
|
674
681
|
await VhdAbstract.createAlias(handler, path, dataPath)
|
|
675
682
|
return size
|
|
676
683
|
} else {
|
|
677
|
-
|
|
684
|
+
const inputWithSize = await createVhdStreamWithLength(input)
|
|
685
|
+
return this.outputStream(path, inputWithSize, { checksum, validator, expectedSize: inputWithSize.length })
|
|
678
686
|
}
|
|
679
687
|
}
|
|
680
688
|
|
|
681
|
-
async outputStream(path, input, { checksum = true, validator = noop } = {}) {
|
|
689
|
+
async outputStream(path, input, { checksum = true, validator = noop, expectedSize } = {}) {
|
|
682
690
|
const container = watchStreamSize(input)
|
|
691
|
+
|
|
683
692
|
await this._handler.outputStream(path, input, {
|
|
684
693
|
checksum,
|
|
685
694
|
dirMode: this._dirMode,
|
|
686
695
|
async validator() {
|
|
687
696
|
await input.task
|
|
697
|
+
if (expectedSize !== undefined) {
|
|
698
|
+
// check that we read all the stream
|
|
699
|
+
strictEqual(
|
|
700
|
+
container.size,
|
|
701
|
+
expectedSize,
|
|
702
|
+
`transferred size ${container.size}, expected file size : ${expectedSize}`
|
|
703
|
+
)
|
|
704
|
+
}
|
|
705
|
+
let size
|
|
706
|
+
try {
|
|
707
|
+
size = await this._handler.getSize(path)
|
|
708
|
+
} catch (err) {
|
|
709
|
+
// can fail is the remote is encrypted
|
|
710
|
+
}
|
|
711
|
+
if (size !== undefined) {
|
|
712
|
+
// check that everything is written to disk
|
|
713
|
+
strictEqual(size, container.size, `written size ${size}, transfered size : ${container.size}`)
|
|
714
|
+
}
|
|
688
715
|
return validator.apply(this, arguments)
|
|
689
716
|
},
|
|
690
717
|
})
|
package/_VmBackup.js
CHANGED
|
@@ -55,6 +55,7 @@ class VmBackup {
|
|
|
55
55
|
schedule,
|
|
56
56
|
settings,
|
|
57
57
|
srs,
|
|
58
|
+
throttleStream,
|
|
58
59
|
vm,
|
|
59
60
|
}) {
|
|
60
61
|
if (vm.other_config['xo:backup:job'] === job.id && 'start' in vm.blocked_operations) {
|
|
@@ -82,6 +83,7 @@ class VmBackup {
|
|
|
82
83
|
this._healthCheckSr = healthCheckSr
|
|
83
84
|
this._jobId = job.id
|
|
84
85
|
this._jobSnapshots = undefined
|
|
86
|
+
this._throttleStream = throttleStream
|
|
85
87
|
this._xapi = vm.$xapi
|
|
86
88
|
|
|
87
89
|
// Base VM for the export
|
|
@@ -243,7 +245,13 @@ class VmBackup {
|
|
|
243
245
|
const deltaExport = await exportDeltaVm(exportedVm, baseVm, {
|
|
244
246
|
fullVdisRequired,
|
|
245
247
|
})
|
|
248
|
+
// since NBD is network based, if one disk use nbd , all the disk use them
|
|
249
|
+
// except the suspended VDI
|
|
250
|
+
if (Object.values(deltaExport.streams).some(({ _nbd }) => _nbd)) {
|
|
251
|
+
Task.info('Transfer data using NBD')
|
|
252
|
+
}
|
|
246
253
|
const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
|
|
254
|
+
deltaExport.streams = mapValues(deltaExport.streams, this._throttleStream)
|
|
247
255
|
|
|
248
256
|
const timestamp = Date.now()
|
|
249
257
|
|
|
@@ -285,10 +293,12 @@ class VmBackup {
|
|
|
285
293
|
|
|
286
294
|
async _copyFull() {
|
|
287
295
|
const { compression } = this.job
|
|
288
|
-
const stream =
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
296
|
+
const stream = this._throttleStream(
|
|
297
|
+
await this._xapi.VM_export(this.exportedVm.$ref, {
|
|
298
|
+
compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
|
|
299
|
+
useSnapshot: false,
|
|
300
|
+
})
|
|
301
|
+
)
|
|
292
302
|
const sizeContainer = watchStreamSize(stream)
|
|
293
303
|
|
|
294
304
|
const timestamp = Date.now()
|
package/_cleanVm.js
CHANGED
|
@@ -541,7 +541,8 @@ exports.cleanVm = async function cleanVm(
|
|
|
541
541
|
|
|
542
542
|
// don't warn if the size has changed after a merge
|
|
543
543
|
if (!merged && fileSystemSize !== size) {
|
|
544
|
-
|
|
544
|
+
// FIXME: figure out why it occurs so often and, once fixed, log the real problems with `logWarn`
|
|
545
|
+
console.warn('cleanVm: incorrect backup size in metadata', {
|
|
545
546
|
path: metadataPath,
|
|
546
547
|
actual: size ?? 'none',
|
|
547
548
|
expected: fileSystemSize,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { pipeline } = require('node:stream')
|
|
4
|
+
const { ThrottleGroup } = require('@kldzj/stream-throttle')
|
|
5
|
+
const identity = require('lodash/identity.js')
|
|
6
|
+
|
|
7
|
+
const noop = Function.prototype
|
|
8
|
+
|
|
9
|
+
module.exports = function createStreamThrottle(rate) {
|
|
10
|
+
if (rate === 0) {
|
|
11
|
+
return identity
|
|
12
|
+
}
|
|
13
|
+
const group = new ThrottleGroup({ rate })
|
|
14
|
+
return function throttleStream(stream) {
|
|
15
|
+
return pipeline(stream, group.createThrottle(), noop)
|
|
16
|
+
}
|
|
17
|
+
}
|
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.34.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.6"
|
|
14
14
|
},
|
|
@@ -17,16 +17,17 @@
|
|
|
17
17
|
"test": "node--test"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"@kldzj/stream-throttle": "^1.1.1",
|
|
20
21
|
"@vates/async-each": "^1.0.0",
|
|
21
22
|
"@vates/cached-dns.lookup": "^1.0.0",
|
|
22
23
|
"@vates/compose": "^2.1.0",
|
|
23
24
|
"@vates/decorate-with": "^2.0.0",
|
|
24
25
|
"@vates/disposable": "^0.1.4",
|
|
25
26
|
"@vates/fuse-vhd": "^1.0.0",
|
|
26
|
-
"@vates/nbd-client": "^1.0
|
|
27
|
+
"@vates/nbd-client": "^1.1.0",
|
|
27
28
|
"@vates/parse-duration": "^0.1.1",
|
|
28
29
|
"@xen-orchestra/async-map": "^0.1.2",
|
|
29
|
-
"@xen-orchestra/fs": "^3.3.
|
|
30
|
+
"@xen-orchestra/fs": "^3.3.4",
|
|
30
31
|
"@xen-orchestra/log": "^0.6.0",
|
|
31
32
|
"@xen-orchestra/template": "^0.1.0",
|
|
32
33
|
"compare-versions": "^5.0.1",
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"promise-toolbox": "^0.21.0",
|
|
42
43
|
"proper-lockfile": "^4.1.2",
|
|
43
44
|
"uuid": "^9.0.0",
|
|
44
|
-
"vhd-lib": "^4.
|
|
45
|
+
"vhd-lib": "^4.3.0",
|
|
45
46
|
"yazl": "^2.5.1"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
"tmp": "^0.2.1"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
54
|
-
"@xen-orchestra/xapi": "^2.
|
|
55
|
+
"@xen-orchestra/xapi": "^2.1.0"
|
|
55
56
|
},
|
|
56
57
|
"license": "AGPL-3.0-or-later",
|
|
57
58
|
"author": {
|
|
@@ -20,9 +20,8 @@ const { AbstractDeltaWriter } = require('./_AbstractDeltaWriter.js')
|
|
|
20
20
|
const { checkVhd } = require('./_checkVhd.js')
|
|
21
21
|
const { packUuid } = require('./_packUuid.js')
|
|
22
22
|
const { Disposable } = require('promise-toolbox')
|
|
23
|
-
const NbdClient = require('@vates/nbd-client')
|
|
24
23
|
|
|
25
|
-
const {
|
|
24
|
+
const { warn } = createLogger('xo:backups:DeltaBackupWriter')
|
|
26
25
|
|
|
27
26
|
class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
|
|
28
27
|
async checkBaseVdis(baseUuidToSrcVdi) {
|
|
@@ -200,41 +199,12 @@ class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
|
|
|
200
199
|
await checkVhd(handler, parentPath)
|
|
201
200
|
}
|
|
202
201
|
|
|
203
|
-
const vdiRef = vm.$xapi.getObject(vdi.uuid).$ref
|
|
204
|
-
|
|
205
|
-
let nbdClient
|
|
206
|
-
if (this._backup.config.useNbd) {
|
|
207
|
-
debug('useNbd is enabled', { vdi: id, path })
|
|
208
|
-
// get nbd if possible
|
|
209
|
-
try {
|
|
210
|
-
// this will always take the first host in the list
|
|
211
|
-
const [nbdInfo] = await vm.$xapi.call('VDI.get_nbd_info', vdiRef)
|
|
212
|
-
debug('got NBD info', { nbdInfo, vdi: id, path })
|
|
213
|
-
nbdClient = new NbdClient(nbdInfo)
|
|
214
|
-
await nbdClient.connect()
|
|
215
|
-
|
|
216
|
-
// this will inform the xapi that we don't need this anymore
|
|
217
|
-
// and will detach the vdi from dom0
|
|
218
|
-
$defer(() => nbdClient.disconnect())
|
|
219
|
-
|
|
220
|
-
info('NBD client ready', { vdi: id, path })
|
|
221
|
-
Task.info('NBD used')
|
|
222
|
-
} catch (error) {
|
|
223
|
-
Task.warning('NBD configured but unusable', { error })
|
|
224
|
-
nbdClient = undefined
|
|
225
|
-
warn('error connecting to NBD server', { error, vdi: id, path })
|
|
226
|
-
}
|
|
227
|
-
} else {
|
|
228
|
-
debug('useNbd is disabled', { vdi: id, path })
|
|
229
|
-
}
|
|
230
|
-
|
|
231
202
|
transferSize += await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
|
|
232
203
|
// no checksum for VHDs, because they will be invalidated by
|
|
233
204
|
// merges and chainings
|
|
234
205
|
checksum: false,
|
|
235
206
|
validator: tmpPath => checkVhd(handler, tmpPath),
|
|
236
207
|
writeBlockConcurrency: this._backup.config.writeBlockConcurrency,
|
|
237
|
-
nbdClient,
|
|
238
208
|
})
|
|
239
209
|
|
|
240
210
|
if (isDelta) {
|