@xen-orchestra/backups 0.37.0 → 0.38.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/Backup.js +3 -0
- package/_runners/VmsRemote.js +98 -0
- package/_runners/_vmRunners/FullRemote.js +53 -0
- package/_runners/_vmRunners/FullXapi.js +6 -2
- package/_runners/_vmRunners/IncrementalRemote.js +67 -0
- package/_runners/_vmRunners/IncrementalXapi.js +15 -3
- package/_runners/_vmRunners/_Abstract.js +11 -3
- package/_runners/_vmRunners/_AbstractRemote.js +97 -0
- package/_runners/_vmRunners/_AbstractXapi.js +35 -15
- package/_runners/_writers/FullRemoteWriter.js +10 -8
- package/_runners/_writers/FullXapiWriter.js +3 -2
- package/_runners/_writers/IncrementalRemoteWriter.js +21 -15
- package/_runners/_writers/IncrementalXapiWriter.js +11 -8
- package/_runners/_writers/_AbstractFullWriter.js +2 -2
- package/_runners/_writers/_AbstractIncrementalWriter.js +2 -2
- package/_runners/_writers/_AbstractWriter.js +19 -2
- package/_runners/_writers/_MixinRemoteWriter.js +20 -6
- package/_runners/_writers/_MixinXapiWriter.js +3 -1
- package/package.json +2 -2
package/Backup.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { Metadata } = require('./_runners/Metadata.js')
|
|
4
|
+
const { VmsRemote } = require('./_runners/VmsRemote.js')
|
|
4
5
|
const { VmsXapi } = require('./_runners/VmsXapi.js')
|
|
5
6
|
|
|
6
7
|
exports.createRunner = function createRunner(opts) {
|
|
@@ -8,6 +9,8 @@ exports.createRunner = function createRunner(opts) {
|
|
|
8
9
|
switch (type) {
|
|
9
10
|
case 'backup':
|
|
10
11
|
return new VmsXapi(opts)
|
|
12
|
+
case 'mirrorBackup':
|
|
13
|
+
return new VmsRemote(opts)
|
|
11
14
|
case 'metadataBackup':
|
|
12
15
|
return new Metadata(opts)
|
|
13
16
|
default:
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { asyncMapSettled } = require('@xen-orchestra/async-map')
|
|
4
|
+
const Disposable = require('promise-toolbox/Disposable')
|
|
5
|
+
const { limitConcurrency } = require('limit-concurrency-decorator')
|
|
6
|
+
|
|
7
|
+
const { extractIdsFromSimplePattern } = require('../extractIdsFromSimplePattern.js')
|
|
8
|
+
const { Task } = require('../Task.js')
|
|
9
|
+
const createStreamThrottle = require('./_createStreamThrottle.js')
|
|
10
|
+
const { DEFAULT_SETTINGS, Abstract } = require('./_Abstract.js')
|
|
11
|
+
const { runTask } = require('./_runTask.js')
|
|
12
|
+
const { getAdaptersByRemote } = require('./_getAdaptersByRemote.js')
|
|
13
|
+
const { FullRemote } = require('./_vmRunners/FullRemote.js')
|
|
14
|
+
const { IncrementalRemote } = require('./_vmRunners/IncrementalRemote.js')
|
|
15
|
+
|
|
16
|
+
const DEFAULT_REMOTE_VM_SETTINGS = {
|
|
17
|
+
concurrency: 2,
|
|
18
|
+
copyRetention: 0,
|
|
19
|
+
deleteFirst: false,
|
|
20
|
+
exportRetention: 0,
|
|
21
|
+
healthCheckSr: undefined,
|
|
22
|
+
healthCheckVmsWithTags: [],
|
|
23
|
+
maxExportRate: 0,
|
|
24
|
+
maxMergedDeltasPerRun: Infinity,
|
|
25
|
+
timeout: 0,
|
|
26
|
+
validateVhdStreams: false,
|
|
27
|
+
vmTimeout: 0,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
exports.VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
|
31
|
+
_computeBaseSettings(config, job) {
|
|
32
|
+
const baseSettings = { ...DEFAULT_SETTINGS }
|
|
33
|
+
Object.assign(baseSettings, DEFAULT_REMOTE_VM_SETTINGS, config.defaultSettings, config.vm?.defaultSettings)
|
|
34
|
+
Object.assign(baseSettings, job.settings[''])
|
|
35
|
+
return baseSettings
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async run() {
|
|
39
|
+
const job = this._job
|
|
40
|
+
const schedule = this._schedule
|
|
41
|
+
const settings = this._settings
|
|
42
|
+
|
|
43
|
+
const throttleStream = createStreamThrottle(settings.maxExportRate)
|
|
44
|
+
|
|
45
|
+
const config = this._config
|
|
46
|
+
await Disposable.use(
|
|
47
|
+
() => this._getAdapter(job.sourceRemote),
|
|
48
|
+
() => (settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined),
|
|
49
|
+
Disposable.all(
|
|
50
|
+
extractIdsFromSimplePattern(job.remotes).map(id => id !== job.sourceRemote && this._getAdapter(id))
|
|
51
|
+
),
|
|
52
|
+
async ({ adapter: sourceRemoteAdapter }, healthCheckSr, remoteAdapters) => {
|
|
53
|
+
// remove adapters that failed (already handled)
|
|
54
|
+
remoteAdapters = remoteAdapters.filter(_ => !!_)
|
|
55
|
+
if (remoteAdapters.length === 0) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const vmsUuids = await sourceRemoteAdapter.listAllVms()
|
|
60
|
+
|
|
61
|
+
Task.info('vms', { vms: vmsUuids })
|
|
62
|
+
|
|
63
|
+
remoteAdapters = getAdaptersByRemote(remoteAdapters)
|
|
64
|
+
const allSettings = this._job.settings
|
|
65
|
+
const baseSettings = this._baseSettings
|
|
66
|
+
|
|
67
|
+
const handleVm = vmUuid => {
|
|
68
|
+
const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
|
|
69
|
+
|
|
70
|
+
const opts = {
|
|
71
|
+
baseSettings,
|
|
72
|
+
config,
|
|
73
|
+
job,
|
|
74
|
+
healthCheckSr,
|
|
75
|
+
remoteAdapters,
|
|
76
|
+
schedule,
|
|
77
|
+
settings: { ...settings, ...allSettings[vmUuid] },
|
|
78
|
+
sourceRemoteAdapter,
|
|
79
|
+
throttleStream,
|
|
80
|
+
vmUuid,
|
|
81
|
+
}
|
|
82
|
+
let vmBackup
|
|
83
|
+
if (job.mode === 'delta') {
|
|
84
|
+
vmBackup = new IncrementalRemote(opts)
|
|
85
|
+
} else if (job.mode === 'full') {
|
|
86
|
+
vmBackup = new FullRemote(opts)
|
|
87
|
+
} else {
|
|
88
|
+
throw new Error(`Job mode ${job.mode} not implemented for mirror backup`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return runTask(taskStart, () => vmBackup.run())
|
|
92
|
+
}
|
|
93
|
+
const { concurrency } = settings
|
|
94
|
+
await asyncMapSettled(vmsUuids, !concurrency ? handleVm : limitConcurrency(concurrency)(handleVm))
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { decorateMethodsWith } = require('@vates/decorate-with')
|
|
4
|
+
const { defer } = require('golike-defer')
|
|
5
|
+
const { AbstractRemote } = require('./_AbstractRemote')
|
|
6
|
+
const { FullRemoteWriter } = require('../_writers/FullRemoteWriter')
|
|
7
|
+
const { forkStreamUnpipe } = require('../_forkStreamUnpipe')
|
|
8
|
+
const { watchStreamSize } = require('../../_watchStreamSize')
|
|
9
|
+
const { Task } = require('../../Task')
|
|
10
|
+
|
|
11
|
+
class FullRemoteVmBackupRunner extends AbstractRemote {
|
|
12
|
+
_getRemoteWriter() {
|
|
13
|
+
return FullRemoteWriter
|
|
14
|
+
}
|
|
15
|
+
async _run($defer) {
|
|
16
|
+
const transferList = await this._computeTransferList(({ mode }) => mode === 'full')
|
|
17
|
+
|
|
18
|
+
await this._callWriters(async writer => {
|
|
19
|
+
await writer.beforeBackup()
|
|
20
|
+
$defer(async () => {
|
|
21
|
+
await writer.afterBackup()
|
|
22
|
+
})
|
|
23
|
+
}, 'writer.beforeBackup()')
|
|
24
|
+
if (transferList.length > 0) {
|
|
25
|
+
for (const metadata of transferList) {
|
|
26
|
+
const stream = await this._sourceRemoteAdapter.readFullVmBackup(metadata)
|
|
27
|
+
const sizeContainer = watchStreamSize(stream)
|
|
28
|
+
|
|
29
|
+
// @todo shouldn't transfer backup if it will be deleted by retention policy (higher retention on source than destination)
|
|
30
|
+
await this._callWriters(
|
|
31
|
+
writer =>
|
|
32
|
+
writer.run({
|
|
33
|
+
stream: forkStreamUnpipe(stream),
|
|
34
|
+
timestamp: metadata.timestamp,
|
|
35
|
+
vm: metadata.vm,
|
|
36
|
+
vmSnapshot: metadata.vmSnapshot,
|
|
37
|
+
sizeContainer,
|
|
38
|
+
}),
|
|
39
|
+
'writer.run()'
|
|
40
|
+
)
|
|
41
|
+
// for healthcheck
|
|
42
|
+
this._tags = metadata.vm.tags
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
Task.info('No new data to upload for this VM')
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
exports.FullRemote = FullRemoteVmBackupRunner
|
|
51
|
+
decorateMethodsWith(FullRemoteVmBackupRunner, {
|
|
52
|
+
_run: defer,
|
|
53
|
+
})
|
|
@@ -16,7 +16,7 @@ exports.FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
_mustDoSnapshot() {
|
|
19
|
-
const
|
|
19
|
+
const vm = this._vm
|
|
20
20
|
|
|
21
21
|
const settings = this._settings
|
|
22
22
|
return (
|
|
@@ -29,8 +29,10 @@ exports.FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
|
|
|
29
29
|
|
|
30
30
|
async _copy() {
|
|
31
31
|
const { compression } = this.job
|
|
32
|
+
const vm = this._vm
|
|
33
|
+
const exportedVm = this._exportedVm
|
|
32
34
|
const stream = this._throttleStream(
|
|
33
|
-
await this._xapi.VM_export(
|
|
35
|
+
await this._xapi.VM_export(exportedVm.$ref, {
|
|
34
36
|
compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
|
|
35
37
|
useSnapshot: false,
|
|
36
38
|
})
|
|
@@ -45,6 +47,8 @@ exports.FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
|
|
|
45
47
|
sizeContainer,
|
|
46
48
|
stream: forkStreamUnpipe(stream),
|
|
47
49
|
timestamp,
|
|
50
|
+
vm,
|
|
51
|
+
vmSnapshot: exportedVm,
|
|
48
52
|
}),
|
|
49
53
|
'writer.run()'
|
|
50
54
|
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const assert = require('node:assert')
|
|
3
|
+
|
|
4
|
+
const { decorateMethodsWith } = require('@vates/decorate-with')
|
|
5
|
+
const { defer } = require('golike-defer')
|
|
6
|
+
const { mapValues } = require('lodash')
|
|
7
|
+
const { Task } = require('../../Task')
|
|
8
|
+
const { AbstractRemote } = require('./_AbstractRemote')
|
|
9
|
+
const { IncrementalRemoteWriter } = require('../_writers/IncrementalRemoteWriter')
|
|
10
|
+
const { forkDeltaExport } = require('./_forkDeltaExport')
|
|
11
|
+
const isVhdDifferencingDisk = require('vhd-lib/isVhdDifferencingDisk')
|
|
12
|
+
const { asyncEach } = require('@vates/async-each')
|
|
13
|
+
|
|
14
|
+
class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
15
|
+
_getRemoteWriter() {
|
|
16
|
+
return IncrementalRemoteWriter
|
|
17
|
+
}
|
|
18
|
+
async _run($defer) {
|
|
19
|
+
const transferList = await this._computeTransferList(({ mode }) => mode === 'delta')
|
|
20
|
+
await this._callWriters(async writer => {
|
|
21
|
+
await writer.beforeBackup()
|
|
22
|
+
$defer(async () => {
|
|
23
|
+
await writer.afterBackup()
|
|
24
|
+
})
|
|
25
|
+
}, 'writer.beforeBackup()')
|
|
26
|
+
|
|
27
|
+
if (transferList.length > 0) {
|
|
28
|
+
for (const metadata of transferList) {
|
|
29
|
+
assert.strictEqual(metadata.mode, 'delta')
|
|
30
|
+
|
|
31
|
+
await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
|
|
32
|
+
const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, {
|
|
33
|
+
useChain: false,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const differentialVhds = {}
|
|
37
|
+
|
|
38
|
+
await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
|
|
39
|
+
differentialVhds[key] = await isVhdDifferencingDisk(stream)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
incrementalExport.streams = mapValues(incrementalExport.streams, this._throttleStream)
|
|
43
|
+
await this._callWriters(
|
|
44
|
+
writer =>
|
|
45
|
+
writer.transfer({
|
|
46
|
+
deltaExport: forkDeltaExport(incrementalExport),
|
|
47
|
+
differentialVhds,
|
|
48
|
+
timestamp: metadata.timestamp,
|
|
49
|
+
vm: metadata.vm,
|
|
50
|
+
vmSnapshot: metadata.vmSnapshot,
|
|
51
|
+
}),
|
|
52
|
+
'writer.transfer()'
|
|
53
|
+
)
|
|
54
|
+
await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
|
|
55
|
+
// for healthcheck
|
|
56
|
+
this._tags = metadata.vm.tags
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
Task.info('No new data to upload for this VM')
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
exports.IncrementalRemote = IncrementalRemoteVmBackupRunner
|
|
65
|
+
decorateMethodsWith(IncrementalRemoteVmBackupRunner, {
|
|
66
|
+
_run: defer,
|
|
67
|
+
})
|
|
@@ -15,6 +15,8 @@ const { Task } = require('../../Task.js')
|
|
|
15
15
|
const { watchStreamSize } = require('../../_watchStreamSize.js')
|
|
16
16
|
const { AbstractXapi } = require('./_AbstractXapi.js')
|
|
17
17
|
const { forkDeltaExport } = require('./_forkDeltaExport.js')
|
|
18
|
+
const isVhdDifferencingDisk = require('vhd-lib/isVhdDifferencingDisk')
|
|
19
|
+
const { asyncEach } = require('@vates/async-each')
|
|
18
20
|
|
|
19
21
|
const { debug } = createLogger('xo:backups:IncrementalXapiVmBackup')
|
|
20
22
|
|
|
@@ -30,8 +32,9 @@ exports.IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXa
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
async _copy() {
|
|
33
|
-
const { exportedVm } = this
|
|
34
35
|
const baseVm = this._baseVm
|
|
36
|
+
const vm = this._vm
|
|
37
|
+
const exportedVm = this._exportedVm
|
|
35
38
|
const fullVdisRequired = this._fullVdisRequired
|
|
36
39
|
|
|
37
40
|
const isFull = fullVdisRequired === undefined || fullVdisRequired.size !== 0
|
|
@@ -46,12 +49,18 @@ exports.IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXa
|
|
|
46
49
|
if (Object.values(deltaExport.streams).some(({ _nbd }) => _nbd)) {
|
|
47
50
|
Task.info('Transfer data using NBD')
|
|
48
51
|
}
|
|
52
|
+
|
|
53
|
+
const differentialVhds = {}
|
|
54
|
+
// since isVhdDifferencingDisk is reading and unshifting data in stream
|
|
55
|
+
// it should be done BEFORE any other stream transform
|
|
56
|
+
await asyncEach(Object.entries(deltaExport.streams), async ([key, stream]) => {
|
|
57
|
+
differentialVhds[key] = await isVhdDifferencingDisk(stream)
|
|
58
|
+
})
|
|
49
59
|
const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
|
|
50
60
|
|
|
51
61
|
if (this._settings.validateVhdStreams) {
|
|
52
62
|
deltaExport.streams = mapValues(deltaExport.streams, stream => pipeline(stream, vhdStreamValidator, noop))
|
|
53
63
|
}
|
|
54
|
-
|
|
55
64
|
deltaExport.streams = mapValues(deltaExport.streams, this._throttleStream)
|
|
56
65
|
|
|
57
66
|
const timestamp = Date.now()
|
|
@@ -60,8 +69,11 @@ exports.IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXa
|
|
|
60
69
|
writer =>
|
|
61
70
|
writer.transfer({
|
|
62
71
|
deltaExport: forkDeltaExport(deltaExport),
|
|
72
|
+
differentialVhds,
|
|
63
73
|
sizeContainers,
|
|
64
74
|
timestamp,
|
|
75
|
+
vm,
|
|
76
|
+
vmSnapshot: exportedVm,
|
|
65
77
|
}),
|
|
66
78
|
'writer.transfer()'
|
|
67
79
|
)
|
|
@@ -108,7 +120,7 @@ exports.IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXa
|
|
|
108
120
|
return
|
|
109
121
|
}
|
|
110
122
|
|
|
111
|
-
const srcVdis = keyBy(await xapi.getRecords('VDI', await this.
|
|
123
|
+
const srcVdis = keyBy(await xapi.getRecords('VDI', await this._vm.$getDisks()), '$ref')
|
|
112
124
|
|
|
113
125
|
// resolve full record
|
|
114
126
|
baseVm = await xapi.getRecord('VM', baseVm.$ref)
|
|
@@ -75,13 +75,21 @@ exports.Abstract = class AbstractVmBackupRunner {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// check if current VM has tags
|
|
78
|
-
const
|
|
78
|
+
const tags = this._tags
|
|
79
79
|
const intersect = settings.healthCheckVmsWithTags.some(t => tags.includes(t))
|
|
80
80
|
|
|
81
81
|
if (settings.healthCheckVmsWithTags.length !== 0 && !intersect) {
|
|
82
|
-
|
|
82
|
+
// create a task to have an info in the logs and reports
|
|
83
|
+
return Task.run(
|
|
84
|
+
{
|
|
85
|
+
name: 'health check',
|
|
86
|
+
},
|
|
87
|
+
() => {
|
|
88
|
+
Task.info(`This VM doesn't match the health check's tags for this schedule`)
|
|
89
|
+
}
|
|
90
|
+
)
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
await this._callWriters(writer => writer.healthCheck(
|
|
93
|
+
await this._callWriters(writer => writer.healthCheck(), 'writer.healthCheck()')
|
|
86
94
|
}
|
|
87
95
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const { Abstract } = require('./_Abstract')
|
|
3
|
+
|
|
4
|
+
const { getVmBackupDir } = require('../../_getVmBackupDir')
|
|
5
|
+
const { asyncEach } = require('@vates/async-each')
|
|
6
|
+
const { Disposable } = require('promise-toolbox')
|
|
7
|
+
|
|
8
|
+
exports.AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstract {
|
|
9
|
+
constructor({
|
|
10
|
+
config,
|
|
11
|
+
job,
|
|
12
|
+
healthCheckSr,
|
|
13
|
+
remoteAdapters,
|
|
14
|
+
schedule,
|
|
15
|
+
settings,
|
|
16
|
+
sourceRemoteAdapter,
|
|
17
|
+
throttleStream,
|
|
18
|
+
vmUuid,
|
|
19
|
+
}) {
|
|
20
|
+
super()
|
|
21
|
+
this.config = config
|
|
22
|
+
this.job = job
|
|
23
|
+
this.remoteAdapters = remoteAdapters
|
|
24
|
+
this.scheduleId = schedule.id
|
|
25
|
+
this.timestamp = undefined
|
|
26
|
+
|
|
27
|
+
this._healthCheckSr = healthCheckSr
|
|
28
|
+
this._sourceRemoteAdapter = sourceRemoteAdapter
|
|
29
|
+
this._throttleStream = throttleStream
|
|
30
|
+
this._vmUuid = vmUuid
|
|
31
|
+
|
|
32
|
+
const allSettings = job.settings
|
|
33
|
+
const writers = new Set()
|
|
34
|
+
this._writers = writers
|
|
35
|
+
|
|
36
|
+
const RemoteWriter = this._getRemoteWriter()
|
|
37
|
+
Object.entries(remoteAdapters).forEach(([remoteId, adapter]) => {
|
|
38
|
+
const targetSettings = {
|
|
39
|
+
...settings,
|
|
40
|
+
...allSettings[remoteId],
|
|
41
|
+
}
|
|
42
|
+
writers.add(
|
|
43
|
+
new RemoteWriter({
|
|
44
|
+
adapter,
|
|
45
|
+
config,
|
|
46
|
+
healthCheckSr,
|
|
47
|
+
job,
|
|
48
|
+
scheduleId: schedule.id,
|
|
49
|
+
vmUuid,
|
|
50
|
+
remoteId,
|
|
51
|
+
settings: targetSettings,
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async _computeTransferList(predicate) {
|
|
58
|
+
const vmBackups = await this._sourceRemoteAdapter.listVmBackups(this._vmUuid, predicate)
|
|
59
|
+
const localMetada = new Map()
|
|
60
|
+
Object.values(vmBackups).forEach(metadata => {
|
|
61
|
+
const timestamp = metadata.timestamp
|
|
62
|
+
localMetada.set(timestamp, metadata)
|
|
63
|
+
})
|
|
64
|
+
const nbRemotes = Object.keys(this.remoteAdapters).length
|
|
65
|
+
const remoteMetadatas = {}
|
|
66
|
+
await asyncEach(Object.values(this.remoteAdapters), async remoteAdapter => {
|
|
67
|
+
const remoteMetadata = await remoteAdapter.listVmBackups(this._vmUuid, predicate)
|
|
68
|
+
remoteMetadata.forEach(metadata => {
|
|
69
|
+
const timestamp = metadata.timestamp
|
|
70
|
+
remoteMetadatas[timestamp] = (remoteMetadatas[timestamp] ?? 0) + 1
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
let chain = []
|
|
75
|
+
const timestamps = [...localMetada.keys()]
|
|
76
|
+
timestamps.sort()
|
|
77
|
+
for (const timestamp of timestamps) {
|
|
78
|
+
if (remoteMetadatas[timestamp] !== nbRemotes) {
|
|
79
|
+
// this backup is not present in all the remote
|
|
80
|
+
// should be retransfered if not found later
|
|
81
|
+
chain.push(localMetada.get(timestamp))
|
|
82
|
+
} else {
|
|
83
|
+
// backup is present in local and remote : the chain has already been transferred
|
|
84
|
+
chain = []
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return chain
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async run() {
|
|
91
|
+
const handler = this._sourceRemoteAdapter._handler
|
|
92
|
+
await Disposable.use(await handler.lock(getVmBackupDir(this._vmUuid)), async () => {
|
|
93
|
+
await this._run()
|
|
94
|
+
await this._healthCheck()
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -40,11 +40,11 @@ class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
40
40
|
this.timestamp = undefined
|
|
41
41
|
|
|
42
42
|
// VM currently backed up
|
|
43
|
-
this.
|
|
44
|
-
const { tags } = this.vm
|
|
43
|
+
const tags = (this._tags = vm.tags)
|
|
45
44
|
|
|
46
45
|
// VM (snapshot) that is really exported
|
|
47
|
-
this.
|
|
46
|
+
this._exportedVm = undefined
|
|
47
|
+
this._vm = vm
|
|
48
48
|
|
|
49
49
|
this._fullVdisRequired = undefined
|
|
50
50
|
this._getSnapshotNameLabel = getSnapshotNameLabel
|
|
@@ -66,7 +66,6 @@ class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
66
66
|
settings.offlineSnapshot = true
|
|
67
67
|
}
|
|
68
68
|
this._settings = settings
|
|
69
|
-
|
|
70
69
|
// Create writers
|
|
71
70
|
{
|
|
72
71
|
const writers = new Set()
|
|
@@ -75,13 +74,24 @@ class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
75
74
|
const [BackupWriter, ReplicationWriter] = this._getWriters()
|
|
76
75
|
|
|
77
76
|
const allSettings = job.settings
|
|
78
|
-
Object.
|
|
77
|
+
Object.entries(remoteAdapters).forEach(([remoteId, adapter]) => {
|
|
79
78
|
const targetSettings = {
|
|
80
79
|
...settings,
|
|
81
80
|
...allSettings[remoteId],
|
|
82
81
|
}
|
|
83
82
|
if (targetSettings.exportRetention !== 0) {
|
|
84
|
-
writers.add(
|
|
83
|
+
writers.add(
|
|
84
|
+
new BackupWriter({
|
|
85
|
+
adapter,
|
|
86
|
+
config,
|
|
87
|
+
healthCheckSr,
|
|
88
|
+
job,
|
|
89
|
+
scheduleId: schedule.id,
|
|
90
|
+
vmUuid: vm.uuid,
|
|
91
|
+
remoteId,
|
|
92
|
+
settings: targetSettings,
|
|
93
|
+
})
|
|
94
|
+
)
|
|
85
95
|
}
|
|
86
96
|
})
|
|
87
97
|
srs.forEach(sr => {
|
|
@@ -90,7 +100,17 @@ class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
90
100
|
...allSettings[sr.uuid],
|
|
91
101
|
}
|
|
92
102
|
if (targetSettings.copyRetention !== 0) {
|
|
93
|
-
writers.add(
|
|
103
|
+
writers.add(
|
|
104
|
+
new ReplicationWriter({
|
|
105
|
+
config,
|
|
106
|
+
healthCheckSr,
|
|
107
|
+
job,
|
|
108
|
+
scheduleId: schedule.id,
|
|
109
|
+
vmUuid: vm.uuid,
|
|
110
|
+
sr,
|
|
111
|
+
settings: targetSettings,
|
|
112
|
+
})
|
|
113
|
+
)
|
|
94
114
|
}
|
|
95
115
|
})
|
|
96
116
|
}
|
|
@@ -99,7 +119,7 @@ class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
99
119
|
// ensure the VM itself does not have any backup metadata which would be
|
|
100
120
|
// copied on manual snapshots and interfere with the backup jobs
|
|
101
121
|
async _cleanMetadata() {
|
|
102
|
-
const
|
|
122
|
+
const vm = this._vm
|
|
103
123
|
if ('xo:backup:job' in vm.other_config) {
|
|
104
124
|
await vm.update_other_config({
|
|
105
125
|
'xo:backup:datetime': null,
|
|
@@ -113,7 +133,7 @@ class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
113
133
|
}
|
|
114
134
|
|
|
115
135
|
async _snapshot() {
|
|
116
|
-
const
|
|
136
|
+
const vm = this._vm
|
|
117
137
|
const xapi = this._xapi
|
|
118
138
|
|
|
119
139
|
const settings = this._settings
|
|
@@ -138,19 +158,19 @@ class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
138
158
|
'xo:backup:vm': vm.uuid,
|
|
139
159
|
})
|
|
140
160
|
|
|
141
|
-
this.
|
|
161
|
+
this._exportedVm = await xapi.getRecord('VM', snapshotRef)
|
|
142
162
|
|
|
143
|
-
return this.
|
|
163
|
+
return this._exportedVm.uuid
|
|
144
164
|
})
|
|
145
165
|
} else {
|
|
146
|
-
this.
|
|
166
|
+
this._exportedVm = vm
|
|
147
167
|
this.timestamp = Date.now()
|
|
148
168
|
}
|
|
149
169
|
}
|
|
150
170
|
|
|
151
171
|
async _fetchJobSnapshots() {
|
|
152
172
|
const jobId = this._jobId
|
|
153
|
-
const vmRef = this.
|
|
173
|
+
const vmRef = this._vm.$ref
|
|
154
174
|
const xapi = this._xapi
|
|
155
175
|
|
|
156
176
|
const snapshotsRef = await xapi.getField('VM', vmRef, 'snapshots')
|
|
@@ -177,7 +197,7 @@ class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
177
197
|
const settings = {
|
|
178
198
|
...baseSettings,
|
|
179
199
|
...allSettings[scheduleId],
|
|
180
|
-
...allSettings[this.
|
|
200
|
+
...allSettings[this._vm.uuid],
|
|
181
201
|
}
|
|
182
202
|
return asyncMap(getOldEntries(settings.snapshotRetention, snapshots), ({ $ref }) => {
|
|
183
203
|
if ($ref !== baseVmRef) {
|
|
@@ -224,7 +244,7 @@ class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
224
244
|
await this._cleanMetadata()
|
|
225
245
|
await this._removeUnusedSnapshots()
|
|
226
246
|
|
|
227
|
-
const
|
|
247
|
+
const vm = this._vm
|
|
228
248
|
const isRunning = vm.power_state === 'Running'
|
|
229
249
|
const startAfter = isRunning && (settings.offlineBackup ? 'backup' : settings.offlineSnapshot && 'snapshot')
|
|
230
250
|
if (startAfter) {
|
|
@@ -26,15 +26,17 @@ exports.FullRemoteWriter = class FullRemoteWriter extends MixinRemoteWriter(Abst
|
|
|
26
26
|
)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
async _run({ timestamp, sizeContainer, stream }) {
|
|
30
|
-
const backup = this._backup
|
|
29
|
+
async _run({ timestamp, sizeContainer, stream, vm, vmSnapshot }) {
|
|
31
30
|
const settings = this._settings
|
|
32
|
-
|
|
33
|
-
const
|
|
31
|
+
const job = this._job
|
|
32
|
+
const scheduleId = this._scheduleId
|
|
34
33
|
|
|
35
34
|
const adapter = this._adapter
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
let metadata = await this._isAlreadyTransferred(timestamp)
|
|
36
|
+
if (metadata !== undefined) {
|
|
37
|
+
// @todo : should skip backup while being vigilant to not stuck the forked stream
|
|
38
|
+
Task.info('This backup has already been transfered')
|
|
39
|
+
}
|
|
38
40
|
|
|
39
41
|
const oldBackups = getOldEntries(
|
|
40
42
|
settings.exportRetention - 1,
|
|
@@ -47,14 +49,14 @@ exports.FullRemoteWriter = class FullRemoteWriter extends MixinRemoteWriter(Abst
|
|
|
47
49
|
const dataBasename = basename + '.xva'
|
|
48
50
|
const dataFilename = this._vmBackupDir + '/' + dataBasename
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
metadata = {
|
|
51
53
|
jobId: job.id,
|
|
52
54
|
mode: job.mode,
|
|
53
55
|
scheduleId,
|
|
54
56
|
timestamp,
|
|
55
57
|
version: '2.0.0',
|
|
56
58
|
vm,
|
|
57
|
-
vmSnapshot
|
|
59
|
+
vmSnapshot,
|
|
58
60
|
xva: './' + dataBasename,
|
|
59
61
|
}
|
|
60
62
|
|
|
@@ -32,10 +32,11 @@ exports.FullXapiWriter = class FullXapiWriter extends MixinXapiWriter(AbstractFu
|
|
|
32
32
|
)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
async _run({ timestamp, sizeContainer, stream }) {
|
|
35
|
+
async _run({ timestamp, sizeContainer, stream, vm }) {
|
|
36
36
|
const sr = this._sr
|
|
37
37
|
const settings = this._settings
|
|
38
|
-
const
|
|
38
|
+
const job = this._job
|
|
39
|
+
const scheduleId = this.scheduleId
|
|
39
40
|
|
|
40
41
|
const { uuid: srUuid, $xapi: xapi } = sr
|
|
41
42
|
|
|
@@ -26,10 +26,9 @@ const { warn } = createLogger('xo:backups:DeltaBackupWriter')
|
|
|
26
26
|
class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWriter) {
|
|
27
27
|
async checkBaseVdis(baseUuidToSrcVdi) {
|
|
28
28
|
const { handler } = this._adapter
|
|
29
|
-
const backup = this._backup
|
|
30
29
|
const adapter = this._adapter
|
|
31
30
|
|
|
32
|
-
const vdisDir = `${this._vmBackupDir}/vdis/${
|
|
31
|
+
const vdisDir = `${this._vmBackupDir}/vdis/${this._job.id}`
|
|
33
32
|
|
|
34
33
|
await asyncMap(baseUuidToSrcVdi, async ([baseUuid, srcVdi]) => {
|
|
35
34
|
let found = false
|
|
@@ -91,11 +90,12 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
|
|
|
91
90
|
async _prepare() {
|
|
92
91
|
const adapter = this._adapter
|
|
93
92
|
const settings = this._settings
|
|
94
|
-
const
|
|
93
|
+
const scheduleId = this._scheduleId
|
|
94
|
+
const vmUuid = this._vmUuid
|
|
95
95
|
|
|
96
96
|
const oldEntries = getOldEntries(
|
|
97
97
|
settings.exportRetention - 1,
|
|
98
|
-
await adapter.listVmBackups(
|
|
98
|
+
await adapter.listVmBackups(vmUuid, _ => _.mode === 'delta' && _.scheduleId === scheduleId)
|
|
99
99
|
)
|
|
100
100
|
this._oldEntries = oldEntries
|
|
101
101
|
|
|
@@ -134,16 +134,19 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
async _transfer($defer, { timestamp, deltaExport }) {
|
|
137
|
+
async _transfer($defer, { differentialVhds, timestamp, deltaExport, vm, vmSnapshot }) {
|
|
138
138
|
const adapter = this._adapter
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
const { job, scheduleId, vm } = backup
|
|
139
|
+
const job = this._job
|
|
140
|
+
const scheduleId = this._scheduleId
|
|
142
141
|
|
|
143
142
|
const jobId = job.id
|
|
144
143
|
const handler = adapter.handler
|
|
145
144
|
|
|
146
|
-
|
|
145
|
+
let metadataContent = await this._isAlreadyTransferred(timestamp)
|
|
146
|
+
if (metadataContent !== undefined) {
|
|
147
|
+
// @todo : should skip backup while being vigilant to not stuck the forked stream
|
|
148
|
+
Task.info('This backup has already been transfered')
|
|
149
|
+
}
|
|
147
150
|
|
|
148
151
|
const basename = formatFilenameDate(timestamp)
|
|
149
152
|
const vhds = mapValues(
|
|
@@ -158,7 +161,7 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
|
|
|
158
161
|
}/${adapter.getVhdFileName(basename)}`
|
|
159
162
|
)
|
|
160
163
|
|
|
161
|
-
|
|
164
|
+
metadataContent = {
|
|
162
165
|
jobId,
|
|
163
166
|
mode: job.mode,
|
|
164
167
|
scheduleId,
|
|
@@ -169,16 +172,15 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
|
|
|
169
172
|
vifs: deltaExport.vifs,
|
|
170
173
|
vhds,
|
|
171
174
|
vm,
|
|
172
|
-
vmSnapshot
|
|
175
|
+
vmSnapshot,
|
|
173
176
|
}
|
|
174
|
-
|
|
175
177
|
const { size } = await Task.run({ name: 'transfer' }, async () => {
|
|
176
178
|
let transferSize = 0
|
|
177
179
|
await Promise.all(
|
|
178
180
|
map(deltaExport.vdis, async (vdi, id) => {
|
|
179
181
|
const path = `${this._vmBackupDir}/${vhds[id]}`
|
|
180
182
|
|
|
181
|
-
const isDelta =
|
|
183
|
+
const isDelta = differentialVhds[`${id}.vhd`]
|
|
182
184
|
let parentPath
|
|
183
185
|
if (isDelta) {
|
|
184
186
|
const vdiDir = dirname(path)
|
|
@@ -191,7 +193,11 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
|
|
|
191
193
|
.sort()
|
|
192
194
|
.pop()
|
|
193
195
|
|
|
194
|
-
assert.notStrictEqual(
|
|
196
|
+
assert.notStrictEqual(
|
|
197
|
+
parentPath,
|
|
198
|
+
undefined,
|
|
199
|
+
`missing parent of ${id} in ${dirname(path)}, looking for ${vdi.other_config['xo:base_delta']}`
|
|
200
|
+
)
|
|
195
201
|
|
|
196
202
|
parentPath = parentPath.slice(1) // remove leading slash
|
|
197
203
|
|
|
@@ -204,7 +210,7 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
|
|
|
204
210
|
// merges and chainings
|
|
205
211
|
checksum: false,
|
|
206
212
|
validator: tmpPath => checkVhd(handler, tmpPath),
|
|
207
|
-
writeBlockConcurrency: this.
|
|
213
|
+
writeBlockConcurrency: this._config.writeBlockConcurrency,
|
|
208
214
|
})
|
|
209
215
|
|
|
210
216
|
if (isDelta) {
|
|
@@ -16,7 +16,7 @@ const { listReplicatedVms } = require('./_listReplicatedVms.js')
|
|
|
16
16
|
exports.IncrementalXapiWriter = class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
|
|
17
17
|
async checkBaseVdis(baseUuidToSrcVdi, baseVm) {
|
|
18
18
|
const sr = this._sr
|
|
19
|
-
const replicatedVm = listReplicatedVms(sr.$xapi, this.
|
|
19
|
+
const replicatedVm = listReplicatedVms(sr.$xapi, this._job.id, sr.uuid, this._vmUuid).find(
|
|
20
20
|
vm => vm.other_config[TAG_COPY_SRC] === baseVm.uuid
|
|
21
21
|
)
|
|
22
22
|
if (replicatedVm === undefined) {
|
|
@@ -49,9 +49,10 @@ exports.IncrementalXapiWriter = class IncrementalXapiWriter extends MixinXapiWri
|
|
|
49
49
|
type: 'SR',
|
|
50
50
|
},
|
|
51
51
|
})
|
|
52
|
+
const hasHealthCheckSr = this._healthCheckSr !== undefined
|
|
52
53
|
this.transfer = task.wrapFn(this.transfer)
|
|
53
|
-
this.cleanup = task.wrapFn(this.cleanup)
|
|
54
|
-
this.healthCheck = task.wrapFn(this.healthCheck,
|
|
54
|
+
this.cleanup = task.wrapFn(this.cleanup, !hasHealthCheckSr)
|
|
55
|
+
this.healthCheck = task.wrapFn(this.healthCheck, hasHealthCheckSr)
|
|
55
56
|
|
|
56
57
|
return task.run(() => this._prepare())
|
|
57
58
|
}
|
|
@@ -59,12 +60,13 @@ exports.IncrementalXapiWriter = class IncrementalXapiWriter extends MixinXapiWri
|
|
|
59
60
|
async _prepare() {
|
|
60
61
|
const settings = this._settings
|
|
61
62
|
const { uuid: srUuid, $xapi: xapi } = this._sr
|
|
62
|
-
const
|
|
63
|
+
const vmUuid = this._vmUuid
|
|
64
|
+
const scheduleId = this._scheduleId
|
|
63
65
|
|
|
64
66
|
// delete previous interrupted copies
|
|
65
|
-
ignoreErrors.call(asyncMapSettled(listReplicatedVms(xapi, scheduleId, undefined,
|
|
67
|
+
ignoreErrors.call(asyncMapSettled(listReplicatedVms(xapi, scheduleId, undefined, vmUuid), vm => vm.$destroy))
|
|
66
68
|
|
|
67
|
-
this._oldEntries = getOldEntries(settings.copyRetention - 1, listReplicatedVms(xapi, scheduleId, srUuid,
|
|
69
|
+
this._oldEntries = getOldEntries(settings.copyRetention - 1, listReplicatedVms(xapi, scheduleId, srUuid, vmUuid))
|
|
68
70
|
|
|
69
71
|
if (settings.deleteFirst) {
|
|
70
72
|
await this._deleteOldEntries()
|
|
@@ -81,10 +83,11 @@ exports.IncrementalXapiWriter = class IncrementalXapiWriter extends MixinXapiWri
|
|
|
81
83
|
return asyncMapSettled(this._oldEntries, vm => vm.$destroy())
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
async _transfer({ timestamp, deltaExport, sizeContainers }) {
|
|
86
|
+
async _transfer({ timestamp, deltaExport, sizeContainers, vm }) {
|
|
85
87
|
const { _warmMigration } = this._settings
|
|
86
88
|
const sr = this._sr
|
|
87
|
-
const
|
|
89
|
+
const job = this._job
|
|
90
|
+
const scheduleId = this._scheduleId
|
|
88
91
|
|
|
89
92
|
const { uuid: srUuid, $xapi: xapi } = sr
|
|
90
93
|
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
const { AbstractWriter } = require('./_AbstractWriter.js')
|
|
4
4
|
|
|
5
5
|
exports.AbstractFullWriter = class AbstractFullWriter extends AbstractWriter {
|
|
6
|
-
async run({ timestamp, sizeContainer, stream }) {
|
|
6
|
+
async run({ timestamp, sizeContainer, stream, vm, vmSnapshot }) {
|
|
7
7
|
try {
|
|
8
|
-
return await this._run({ timestamp, sizeContainer, stream })
|
|
8
|
+
return await this._run({ timestamp, sizeContainer, stream, vm, vmSnapshot })
|
|
9
9
|
} finally {
|
|
10
10
|
// ensure stream is properly closed
|
|
11
11
|
stream.destroy()
|
|
@@ -15,9 +15,9 @@ exports.AbstractIncrementalWriter = class AbstractIncrementalWriter extends Abst
|
|
|
15
15
|
throw new Error('Not implemented')
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
async transfer({
|
|
18
|
+
async transfer({ deltaExport, ...other }) {
|
|
19
19
|
try {
|
|
20
|
-
return await this._transfer({
|
|
20
|
+
return await this._transfer({ deltaExport, ...other })
|
|
21
21
|
} finally {
|
|
22
22
|
// ensure all streams are properly closed
|
|
23
23
|
for (const stream of Object.values(deltaExport.streams)) {
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { formatFilenameDate } = require('../../_filenameDate')
|
|
4
|
+
const { getVmBackupDir } = require('../../_getVmBackupDir')
|
|
5
|
+
|
|
3
6
|
exports.AbstractWriter = class AbstractWriter {
|
|
4
|
-
constructor({
|
|
5
|
-
this.
|
|
7
|
+
constructor({ config, healthCheckSr, job, vmUuid, scheduleId, settings }) {
|
|
8
|
+
this._config = config
|
|
9
|
+
this._healthCheckSr = healthCheckSr
|
|
10
|
+
this._job = job
|
|
11
|
+
this._scheduleId = scheduleId
|
|
6
12
|
this._settings = settings
|
|
13
|
+
this._vmUuid = vmUuid
|
|
7
14
|
}
|
|
8
15
|
|
|
9
16
|
beforeBackup() {}
|
|
@@ -11,4 +18,14 @@ exports.AbstractWriter = class AbstractWriter {
|
|
|
11
18
|
afterBackup() {}
|
|
12
19
|
|
|
13
20
|
healthCheck(sr) {}
|
|
21
|
+
|
|
22
|
+
_isAlreadyTransferred(timestamp) {
|
|
23
|
+
const vmUuid = this._vmUuid
|
|
24
|
+
const adapter = this._adapter
|
|
25
|
+
const backupDir = getVmBackupDir(vmUuid)
|
|
26
|
+
try {
|
|
27
|
+
const actualMetadata = JSON.parse(adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`))
|
|
28
|
+
return actualMetadata
|
|
29
|
+
} catch (error) {}
|
|
30
|
+
}
|
|
14
31
|
}
|
|
@@ -17,13 +17,13 @@ exports.MixinRemoteWriter = (BaseClass = Object) =>
|
|
|
17
17
|
class MixinRemoteWriter extends BaseClass {
|
|
18
18
|
#lock
|
|
19
19
|
|
|
20
|
-
constructor({ remoteId, ...rest }) {
|
|
20
|
+
constructor({ remoteId, adapter, ...rest }) {
|
|
21
21
|
super(rest)
|
|
22
22
|
|
|
23
|
-
this._adapter =
|
|
23
|
+
this._adapter = adapter
|
|
24
24
|
this._remoteId = remoteId
|
|
25
25
|
|
|
26
|
-
this._vmBackupDir = getVmBackupDir(
|
|
26
|
+
this._vmBackupDir = getVmBackupDir(rest.vmUuid)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
async _cleanVm(options) {
|
|
@@ -38,7 +38,7 @@ exports.MixinRemoteWriter = (BaseClass = Object) =>
|
|
|
38
38
|
Task.warning(message, data)
|
|
39
39
|
},
|
|
40
40
|
lock: false,
|
|
41
|
-
mergeBlockConcurrency: this.
|
|
41
|
+
mergeBlockConcurrency: this._config.mergeBlockConcurrency,
|
|
42
42
|
})
|
|
43
43
|
})
|
|
44
44
|
} catch (error) {
|
|
@@ -55,7 +55,7 @@ exports.MixinRemoteWriter = (BaseClass = Object) =>
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
async afterBackup() {
|
|
58
|
-
const { disableMergeWorker } = this.
|
|
58
|
+
const { disableMergeWorker } = this._config
|
|
59
59
|
// merge worker only compatible with local remotes
|
|
60
60
|
const { handler } = this._adapter
|
|
61
61
|
const willMergeInWorker = !disableMergeWorker && typeof handler.getRealPath === 'function'
|
|
@@ -76,7 +76,9 @@ exports.MixinRemoteWriter = (BaseClass = Object) =>
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
healthCheck(
|
|
79
|
+
healthCheck() {
|
|
80
|
+
const sr = this._healthCheckSr
|
|
81
|
+
assert.notStrictEqual(sr, undefined, 'SR should be defined before making a health check')
|
|
80
82
|
assert.notStrictEqual(
|
|
81
83
|
this._metadataFileName,
|
|
82
84
|
undefined,
|
|
@@ -109,4 +111,16 @@ exports.MixinRemoteWriter = (BaseClass = Object) =>
|
|
|
109
111
|
}
|
|
110
112
|
)
|
|
111
113
|
}
|
|
114
|
+
|
|
115
|
+
_isAlreadyTransferred(timestamp) {
|
|
116
|
+
const vmUuid = this._vmUuid
|
|
117
|
+
const adapter = this._adapter
|
|
118
|
+
const backupDir = getVmBackupDir(vmUuid)
|
|
119
|
+
try {
|
|
120
|
+
const actualMetadata = JSON.parse(
|
|
121
|
+
adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
|
|
122
|
+
)
|
|
123
|
+
return actualMetadata
|
|
124
|
+
} catch (error) {}
|
|
125
|
+
}
|
|
112
126
|
}
|
|
@@ -14,7 +14,9 @@ exports.MixinXapiWriter = (BaseClass = Object) =>
|
|
|
14
14
|
this._sr = sr
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
healthCheck(
|
|
17
|
+
healthCheck() {
|
|
18
|
+
const sr = this._healthCheckSr
|
|
19
|
+
assert.notStrictEqual(sr, undefined, 'SR should be defined before making a health check')
|
|
18
20
|
assert.notEqual(this._targetVmRef, undefined, 'A vm should have been transfered to be health checked')
|
|
19
21
|
// copy VM
|
|
20
22
|
return Task.run(
|
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.38.1",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.6"
|
|
14
14
|
},
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"promise-toolbox": "^0.21.0",
|
|
43
43
|
"proper-lockfile": "^4.1.2",
|
|
44
44
|
"uuid": "^9.0.0",
|
|
45
|
-
"vhd-lib": "^4.
|
|
45
|
+
"vhd-lib": "^4.5.0",
|
|
46
46
|
"yazl": "^2.5.1"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|