@xen-orchestra/backups 0.36.1 → 0.38.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 +14 -302
- package/ImportVmBackup.js +6 -6
- package/RemoteAdapter.js +20 -13
- package/RestoreMetadataBackup.js +1 -1
- package/_backupWorker.js +2 -2
- package/{_deltaVm.js → _incrementalVm.js} +11 -11
- package/_runners/Metadata.js +134 -0
- package/_runners/VmsRemote.js +98 -0
- package/_runners/VmsXapi.js +138 -0
- package/_runners/_Abstract.js +51 -0
- package/{_PoolMetadataBackup.js → _runners/_PoolMetadataBackup.js} +3 -3
- package/_runners/_RemoteTimeoutError.js +8 -0
- package/{_XoMetadataBackup.js → _runners/_XoMetadataBackup.js} +3 -3
- package/_runners/_getAdaptersByRemote.js +9 -0
- package/_runners/_runTask.js +6 -0
- package/_runners/_vmRunners/FullRemote.js +53 -0
- package/_runners/_vmRunners/FullXapi.js +65 -0
- package/_runners/_vmRunners/IncrementalRemote.js +67 -0
- package/_runners/_vmRunners/IncrementalXapi.js +175 -0
- package/_runners/_vmRunners/_Abstract.js +95 -0
- package/_runners/_vmRunners/_AbstractRemote.js +86 -0
- package/_runners/_vmRunners/_AbstractXapi.js +257 -0
- package/_runners/_vmRunners/_forkDeltaExport.js +12 -0
- package/{writers/FullBackupWriter.js → _runners/_writers/FullRemoteWriter.js} +15 -13
- package/{writers/FullReplicationWriter.js → _runners/_writers/FullXapiWriter.js} +8 -7
- package/{writers/DeltaBackupWriter.js → _runners/_writers/IncrementalRemoteWriter.js} +28 -22
- package/{writers/DeltaReplicationWriter.js → _runners/_writers/IncrementalXapiWriter.js} +19 -16
- package/{writers → _runners/_writers}/_AbstractFullWriter.js +2 -2
- package/{writers/_AbstractDeltaWriter.js → _runners/_writers/_AbstractIncrementalWriter.js} +3 -3
- package/_runners/_writers/_AbstractWriter.js +31 -0
- package/{writers/_MixinBackupWriter.js → _runners/_writers/_MixinRemoteWriter.js} +30 -16
- package/{writers/_MixinReplicationWriter.js → _runners/_writers/_MixinXapiWriter.js} +9 -13
- package/package.json +5 -5
- package/_VmBackup.js +0 -515
- package/writers/_AbstractWriter.js +0 -14
- /package/{_createStreamThrottle.js → _runners/_createStreamThrottle.js} +0 -0
- /package/{_forkStreamUnpipe.js → _runners/_forkStreamUnpipe.js} +0 -0
- /package/{writers → _runners/_writers}/_checkVhd.js +0 -0
- /package/{writers → _runners/_writers}/_listReplicatedVms.js +0 -0
- /package/{writers → _runners/_writers}/_packUuid.js +0 -0
|
@@ -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,138 @@
|
|
|
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 { IncrementalXapi } = require('./_vmRunners/IncrementalXapi.js')
|
|
14
|
+
const { FullXapi } = require('./_vmRunners/FullXapi.js')
|
|
15
|
+
|
|
16
|
+
const DEFAULT_XAPI_VM_SETTINGS = {
|
|
17
|
+
bypassVdiChainsCheck: false,
|
|
18
|
+
checkpointSnapshot: false,
|
|
19
|
+
concurrency: 2,
|
|
20
|
+
copyRetention: 0,
|
|
21
|
+
deleteFirst: false,
|
|
22
|
+
exportRetention: 0,
|
|
23
|
+
fullInterval: 0,
|
|
24
|
+
healthCheckSr: undefined,
|
|
25
|
+
healthCheckVmsWithTags: [],
|
|
26
|
+
maxExportRate: 0,
|
|
27
|
+
maxMergedDeltasPerRun: Infinity,
|
|
28
|
+
offlineBackup: false,
|
|
29
|
+
offlineSnapshot: false,
|
|
30
|
+
snapshotRetention: 0,
|
|
31
|
+
timeout: 0,
|
|
32
|
+
useNbd: false,
|
|
33
|
+
unconditionalSnapshot: false,
|
|
34
|
+
validateVhdStreams: false,
|
|
35
|
+
vmTimeout: 0,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
exports.VmsXapi = class VmsXapiBackupRunner extends Abstract {
|
|
39
|
+
_computeBaseSettings(config, job) {
|
|
40
|
+
const baseSettings = { ...DEFAULT_SETTINGS }
|
|
41
|
+
Object.assign(baseSettings, DEFAULT_XAPI_VM_SETTINGS, config.defaultSettings, config.vm?.defaultSettings)
|
|
42
|
+
Object.assign(baseSettings, job.settings[''])
|
|
43
|
+
return baseSettings
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async run() {
|
|
47
|
+
const job = this._job
|
|
48
|
+
|
|
49
|
+
// FIXME: proper SimpleIdPattern handling
|
|
50
|
+
const getSnapshotNameLabel = this._getSnapshotNameLabel
|
|
51
|
+
const schedule = this._schedule
|
|
52
|
+
const settings = this._settings
|
|
53
|
+
|
|
54
|
+
const throttleStream = createStreamThrottle(settings.maxExportRate)
|
|
55
|
+
|
|
56
|
+
const config = this._config
|
|
57
|
+
await Disposable.use(
|
|
58
|
+
Disposable.all(
|
|
59
|
+
extractIdsFromSimplePattern(job.srs).map(id =>
|
|
60
|
+
this._getRecord('SR', id).catch(error => {
|
|
61
|
+
runTask(
|
|
62
|
+
{
|
|
63
|
+
name: 'get SR record',
|
|
64
|
+
data: { type: 'SR', id },
|
|
65
|
+
},
|
|
66
|
+
() => Promise.reject(error)
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
),
|
|
71
|
+
Disposable.all(extractIdsFromSimplePattern(job.remotes).map(id => this._getAdapter(id))),
|
|
72
|
+
() => (settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined),
|
|
73
|
+
async (srs, remoteAdapters, healthCheckSr) => {
|
|
74
|
+
// remove adapters that failed (already handled)
|
|
75
|
+
remoteAdapters = remoteAdapters.filter(_ => _ !== undefined)
|
|
76
|
+
|
|
77
|
+
// remove srs that failed (already handled)
|
|
78
|
+
srs = srs.filter(_ => _ !== undefined)
|
|
79
|
+
|
|
80
|
+
if (remoteAdapters.length === 0 && srs.length === 0 && settings.snapshotRetention === 0) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const vmIds = extractIdsFromSimplePattern(job.vms)
|
|
85
|
+
|
|
86
|
+
Task.info('vms', { vms: vmIds })
|
|
87
|
+
|
|
88
|
+
remoteAdapters = getAdaptersByRemote(remoteAdapters)
|
|
89
|
+
|
|
90
|
+
const allSettings = this._job.settings
|
|
91
|
+
const baseSettings = this._baseSettings
|
|
92
|
+
|
|
93
|
+
const handleVm = vmUuid => {
|
|
94
|
+
const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
|
|
95
|
+
|
|
96
|
+
return this._getRecord('VM', vmUuid).then(
|
|
97
|
+
disposableVm =>
|
|
98
|
+
Disposable.use(disposableVm, vm => {
|
|
99
|
+
taskStart.data.name_label = vm.name_label
|
|
100
|
+
return runTask(taskStart, () => {
|
|
101
|
+
const opts = {
|
|
102
|
+
baseSettings,
|
|
103
|
+
config,
|
|
104
|
+
getSnapshotNameLabel,
|
|
105
|
+
healthCheckSr,
|
|
106
|
+
job,
|
|
107
|
+
remoteAdapters,
|
|
108
|
+
schedule,
|
|
109
|
+
settings: { ...settings, ...allSettings[vm.uuid] },
|
|
110
|
+
srs,
|
|
111
|
+
throttleStream,
|
|
112
|
+
vm,
|
|
113
|
+
}
|
|
114
|
+
let vmBackup
|
|
115
|
+
if (job.mode === 'delta') {
|
|
116
|
+
vmBackup = new IncrementalXapi(opts)
|
|
117
|
+
} else {
|
|
118
|
+
if (job.mode === 'full') {
|
|
119
|
+
vmBackup = new FullXapi(opts)
|
|
120
|
+
} else {
|
|
121
|
+
throw new Error(`Job mode ${job.mode} not implemented`)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return vmBackup.run()
|
|
125
|
+
})
|
|
126
|
+
}),
|
|
127
|
+
error =>
|
|
128
|
+
runTask(taskStart, () => {
|
|
129
|
+
throw error
|
|
130
|
+
})
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
const { concurrency } = settings
|
|
134
|
+
await asyncMapSettled(vmIds, concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm))
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Disposable = require('promise-toolbox/Disposable')
|
|
4
|
+
const pTimeout = require('promise-toolbox/timeout')
|
|
5
|
+
const { compileTemplate } = require('@xen-orchestra/template')
|
|
6
|
+
const { runTask } = require('./_runTask.js')
|
|
7
|
+
const { RemoteTimeoutError } = require('./_RemoteTimeoutError.js')
|
|
8
|
+
|
|
9
|
+
exports.DEFAULT_SETTINGS = {
|
|
10
|
+
getRemoteTimeout: 300e3,
|
|
11
|
+
reportWhen: 'failure',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
exports.Abstract = class AbstractRunner {
|
|
15
|
+
constructor({ config, getAdapter, getConnectedRecord, job, schedule }) {
|
|
16
|
+
this._config = config
|
|
17
|
+
this._getRecord = getConnectedRecord
|
|
18
|
+
this._job = job
|
|
19
|
+
this._schedule = schedule
|
|
20
|
+
|
|
21
|
+
this._getSnapshotNameLabel = compileTemplate(config.snapshotNameLabelTpl, {
|
|
22
|
+
'{job.name}': job.name,
|
|
23
|
+
'{vm.name_label}': vm => vm.name_label,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const baseSettings = this._computeBaseSettings(config, job)
|
|
27
|
+
this._baseSettings = baseSettings
|
|
28
|
+
this._settings = { ...baseSettings, ...job.settings[schedule.id] }
|
|
29
|
+
|
|
30
|
+
const { getRemoteTimeout } = this._settings
|
|
31
|
+
this._getAdapter = async function (remoteId) {
|
|
32
|
+
try {
|
|
33
|
+
const disposable = await pTimeout.call(getAdapter(remoteId), getRemoteTimeout, new RemoteTimeoutError(remoteId))
|
|
34
|
+
|
|
35
|
+
return new Disposable(() => disposable.dispose(), {
|
|
36
|
+
adapter: disposable.value,
|
|
37
|
+
remoteId,
|
|
38
|
+
})
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
|
|
41
|
+
runTask(
|
|
42
|
+
{
|
|
43
|
+
name: 'get remote adapter',
|
|
44
|
+
data: { type: 'remote', id: remoteId },
|
|
45
|
+
},
|
|
46
|
+
() => Promise.reject(error)
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
4
4
|
|
|
5
|
-
const { DIR_XO_POOL_METADATA_BACKUPS } = require('
|
|
5
|
+
const { DIR_XO_POOL_METADATA_BACKUPS } = require('../RemoteAdapter.js')
|
|
6
6
|
const { forkStreamUnpipe } = require('./_forkStreamUnpipe.js')
|
|
7
|
-
const { formatFilenameDate } = require('
|
|
8
|
-
const { Task } = require('
|
|
7
|
+
const { formatFilenameDate } = require('../_filenameDate.js')
|
|
8
|
+
const { Task } = require('../Task.js')
|
|
9
9
|
|
|
10
10
|
const PATH_DB_DUMP = '/pool/xmldbdump'
|
|
11
11
|
exports.PATH_DB_DUMP = PATH_DB_DUMP
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
4
4
|
|
|
5
|
-
const { DIR_XO_CONFIG_BACKUPS } = require('
|
|
6
|
-
const { formatFilenameDate } = require('
|
|
7
|
-
const { Task } = require('
|
|
5
|
+
const { DIR_XO_CONFIG_BACKUPS } = require('../RemoteAdapter.js')
|
|
6
|
+
const { formatFilenameDate } = require('../_filenameDate.js')
|
|
7
|
+
const { Task } = require('../Task.js')
|
|
8
8
|
|
|
9
9
|
exports.XoMetadataBackup = class XoMetadataBackup {
|
|
10
10
|
constructor({ config, job, remoteAdapters, schedule, settings }) {
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { createLogger } = require('@xen-orchestra/log')
|
|
4
|
+
|
|
5
|
+
const { forkStreamUnpipe } = require('../_forkStreamUnpipe.js')
|
|
6
|
+
const { FullRemoteWriter } = require('../_writers/FullRemoteWriter.js')
|
|
7
|
+
const { FullXapiWriter } = require('../_writers/FullXapiWriter.js')
|
|
8
|
+
const { watchStreamSize } = require('../../_watchStreamSize.js')
|
|
9
|
+
const { AbstractXapi } = require('./_AbstractXapi.js')
|
|
10
|
+
|
|
11
|
+
const { debug } = createLogger('xo:backups:FullXapiVmBackup')
|
|
12
|
+
|
|
13
|
+
exports.FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
|
|
14
|
+
_getWriters() {
|
|
15
|
+
return [FullRemoteWriter, FullXapiWriter]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_mustDoSnapshot() {
|
|
19
|
+
const vm = this._vm
|
|
20
|
+
|
|
21
|
+
const settings = this._settings
|
|
22
|
+
return (
|
|
23
|
+
settings.unconditionalSnapshot ||
|
|
24
|
+
(!settings.offlineBackup && vm.power_state === 'Running') ||
|
|
25
|
+
settings.snapshotRetention !== 0
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
_selectBaseVm() {}
|
|
29
|
+
|
|
30
|
+
async _copy() {
|
|
31
|
+
const { compression } = this.job
|
|
32
|
+
const vm = this._vm
|
|
33
|
+
const exportedVm = this._exportedVm
|
|
34
|
+
const stream = this._throttleStream(
|
|
35
|
+
await this._xapi.VM_export(exportedVm.$ref, {
|
|
36
|
+
compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
|
|
37
|
+
useSnapshot: false,
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
const sizeContainer = watchStreamSize(stream)
|
|
41
|
+
|
|
42
|
+
const timestamp = Date.now()
|
|
43
|
+
|
|
44
|
+
await this._callWriters(
|
|
45
|
+
writer =>
|
|
46
|
+
writer.run({
|
|
47
|
+
sizeContainer,
|
|
48
|
+
stream: forkStreamUnpipe(stream),
|
|
49
|
+
timestamp,
|
|
50
|
+
vm,
|
|
51
|
+
vmSnapshot: exportedVm,
|
|
52
|
+
}),
|
|
53
|
+
'writer.run()'
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const { size } = sizeContainer
|
|
57
|
+
const end = Date.now()
|
|
58
|
+
const duration = end - timestamp
|
|
59
|
+
debug('transfer complete', {
|
|
60
|
+
duration,
|
|
61
|
+
speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
|
|
62
|
+
size,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -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
|
+
})
|