@xen-orchestra/backups 0.31.0 → 0.33.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 +36 -8
- package/_VmBackup.js +9 -4
- package/_cleanVm.js +2 -1
- package/_createStreamThrottle.js +17 -0
- package/package.json +4 -3
- package/writers/DeltaBackupWriter.js +1 -1
- 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) {
|
|
@@ -660,7 +668,7 @@ class RemoteAdapter {
|
|
|
660
668
|
|
|
661
669
|
async writeVhd(path, input, { checksum = true, validator = noop, writeBlockConcurrency, nbdClient } = {}) {
|
|
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,
|
|
@@ -674,17 +682,37 @@ class RemoteAdapter {
|
|
|
674
682
|
await VhdAbstract.createAlias(handler, path, dataPath)
|
|
675
683
|
return size
|
|
676
684
|
} else {
|
|
677
|
-
|
|
685
|
+
const inputWithSize = await createVhdStreamWithLength(input)
|
|
686
|
+
return this.outputStream(path, inputWithSize, { checksum, validator, expectedSize: inputWithSize.length })
|
|
678
687
|
}
|
|
679
688
|
}
|
|
680
689
|
|
|
681
|
-
async outputStream(path, input, { checksum = true, validator = noop } = {}) {
|
|
690
|
+
async outputStream(path, input, { checksum = true, validator = noop, expectedSize } = {}) {
|
|
682
691
|
const container = watchStreamSize(input)
|
|
692
|
+
|
|
683
693
|
await this._handler.outputStream(path, input, {
|
|
684
694
|
checksum,
|
|
685
695
|
dirMode: this._dirMode,
|
|
686
696
|
async validator() {
|
|
687
697
|
await input.task
|
|
698
|
+
if (expectedSize !== undefined) {
|
|
699
|
+
// check that we read all the stream
|
|
700
|
+
strictEqual(
|
|
701
|
+
container.size,
|
|
702
|
+
expectedSize,
|
|
703
|
+
`transferred size ${container.size}, expected file size : ${expectedSize}`
|
|
704
|
+
)
|
|
705
|
+
}
|
|
706
|
+
let size
|
|
707
|
+
try {
|
|
708
|
+
size = await this._handler.getSize(path)
|
|
709
|
+
} catch (err) {
|
|
710
|
+
// can fail is the remote is encrypted
|
|
711
|
+
}
|
|
712
|
+
if (size !== undefined) {
|
|
713
|
+
// check that everything is written to disk
|
|
714
|
+
strictEqual(size, container.size, `written size ${size}, transfered size : ${container.size}`)
|
|
715
|
+
}
|
|
688
716
|
return validator.apply(this, arguments)
|
|
689
717
|
},
|
|
690
718
|
})
|
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
|
|
@@ -244,6 +246,7 @@ class VmBackup {
|
|
|
244
246
|
fullVdisRequired,
|
|
245
247
|
})
|
|
246
248
|
const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
|
|
249
|
+
deltaExport.streams = mapValues(deltaExport.streams, this._throttleStream)
|
|
247
250
|
|
|
248
251
|
const timestamp = Date.now()
|
|
249
252
|
|
|
@@ -285,10 +288,12 @@ class VmBackup {
|
|
|
285
288
|
|
|
286
289
|
async _copyFull() {
|
|
287
290
|
const { compression } = this.job
|
|
288
|
-
const stream =
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
291
|
+
const stream = this._throttleStream(
|
|
292
|
+
await this._xapi.VM_export(this.exportedVm.$ref, {
|
|
293
|
+
compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
|
|
294
|
+
useSnapshot: false,
|
|
295
|
+
})
|
|
296
|
+
)
|
|
292
297
|
const sizeContainer = watchStreamSize(stream)
|
|
293
298
|
|
|
294
299
|
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.33.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.6"
|
|
14
14
|
},
|
|
@@ -17,6 +17,7 @@
|
|
|
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",
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"@vates/nbd-client": "^1.0.1",
|
|
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",
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
"tmp": "^0.2.1"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
54
|
-
"@xen-orchestra/xapi": "^
|
|
55
|
+
"@xen-orchestra/xapi": "^2.0.0"
|
|
55
56
|
},
|
|
56
57
|
"license": "AGPL-3.0-or-later",
|
|
57
58
|
"author": {
|
|
@@ -203,7 +203,7 @@ class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
|
|
|
203
203
|
const vdiRef = vm.$xapi.getObject(vdi.uuid).$ref
|
|
204
204
|
|
|
205
205
|
let nbdClient
|
|
206
|
-
if (this._backup.config.useNbd) {
|
|
206
|
+
if (this._backup.config.useNbd && adapter.useVhdDirectory()) {
|
|
207
207
|
debug('useNbd is enabled', { vdi: id, path })
|
|
208
208
|
// get nbd if possible
|
|
209
209
|
try {
|