@xen-orchestra/backups 0.14.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 +263 -0
- package/DurablePartition.js +40 -0
- package/ImportVmBackup.js +66 -0
- package/README.md +28 -0
- package/RemoteAdapter.js +552 -0
- package/RestoreMetadataBackup.js +24 -0
- package/Task.js +151 -0
- package/_PoolMetadataBackup.js +75 -0
- package/_VmBackup.js +409 -0
- package/_XoMetadataBackup.js +62 -0
- package/_backupType.js +4 -0
- package/_backupWorker.js +155 -0
- package/_cancelableMap.js +20 -0
- package/_cleanVm.js +378 -0
- package/_deltaVm.js +347 -0
- package/_extractIdsFromSimplePattern.js +29 -0
- package/_filenameDate.js +6 -0
- package/_forkStreamUnpipe.js +28 -0
- package/_getOldEntries.js +4 -0
- package/_getTmpDir.js +20 -0
- package/_getVmBackupDir.js +6 -0
- package/_isValidXva.js +60 -0
- package/_listPartitions.js +52 -0
- package/_lvm.js +31 -0
- package/_watchStreamSize.js +7 -0
- package/formatVmBackups.js +34 -0
- package/merge-worker/cli.js +69 -0
- package/merge-worker/index.js +25 -0
- package/package.json +49 -0
- package/parseMetadataBackupId.js +23 -0
- package/runBackupWorker.js +38 -0
- package/writers/DeltaBackupWriter.js +221 -0
- package/writers/DeltaReplicationWriter.js +126 -0
- package/writers/FullBackupWriter.js +85 -0
- package/writers/FullReplicationWriter.js +88 -0
- package/writers/_AbstractDeltaWriter.js +26 -0
- package/writers/_AbstractFullWriter.js +12 -0
- package/writers/_AbstractWriter.js +10 -0
- package/writers/_MixinBackupWriter.js +51 -0
- package/writers/_MixinReplicationWriter.js +8 -0
- package/writers/_checkVhd.js +5 -0
- package/writers/_listReplicatedVms.js +30 -0
- package/writers/_packUuid.js +5 -0
package/Backup.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
|
|
2
|
+
const Disposable = require('promise-toolbox/Disposable.js')
|
|
3
|
+
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
|
|
4
|
+
const { compileTemplate } = require('@xen-orchestra/template')
|
|
5
|
+
const { limitConcurrency } = require('limit-concurrency-decorator')
|
|
6
|
+
|
|
7
|
+
const { extractIdsFromSimplePattern } = require('./_extractIdsFromSimplePattern.js')
|
|
8
|
+
const { PoolMetadataBackup } = require('./_PoolMetadataBackup.js')
|
|
9
|
+
const { Task } = require('./Task.js')
|
|
10
|
+
const { VmBackup } = require('./_VmBackup.js')
|
|
11
|
+
const { XoMetadataBackup } = require('./_XoMetadataBackup.js')
|
|
12
|
+
|
|
13
|
+
const noop = Function.prototype
|
|
14
|
+
|
|
15
|
+
const getAdaptersByRemote = adapters => {
|
|
16
|
+
const adaptersByRemote = {}
|
|
17
|
+
adapters.forEach(({ adapter, remoteId }) => {
|
|
18
|
+
adaptersByRemote[remoteId] = adapter
|
|
19
|
+
})
|
|
20
|
+
return adaptersByRemote
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const runTask = (...args) => Task.run(...args).catch(noop) // errors are handled by logs
|
|
24
|
+
|
|
25
|
+
exports.Backup = class Backup {
|
|
26
|
+
constructor({ config, getAdapter, getConnectedRecord, job, schedule }) {
|
|
27
|
+
this._config = config
|
|
28
|
+
this._getRecord = getConnectedRecord
|
|
29
|
+
this._job = job
|
|
30
|
+
this._schedule = schedule
|
|
31
|
+
|
|
32
|
+
this._getAdapter = Disposable.factory(function* (remoteId) {
|
|
33
|
+
return {
|
|
34
|
+
adapter: yield getAdapter(remoteId),
|
|
35
|
+
remoteId,
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
this._getSnapshotNameLabel = compileTemplate(config.snapshotNameLabelTpl, {
|
|
40
|
+
'{job.name}': job.name,
|
|
41
|
+
'{vm.name_label}': vm => vm.name_label,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
run() {
|
|
46
|
+
const type = this._job.type
|
|
47
|
+
if (type === 'backup') {
|
|
48
|
+
return this._runVmBackup()
|
|
49
|
+
} else if (type === 'metadataBackup') {
|
|
50
|
+
return this._runMetadataBackup()
|
|
51
|
+
} else {
|
|
52
|
+
throw new Error(`No runner for the backup type ${type}`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async _runMetadataBackup() {
|
|
57
|
+
const schedule = this._schedule
|
|
58
|
+
const job = this._job
|
|
59
|
+
const remoteIds = extractIdsFromSimplePattern(job.remotes)
|
|
60
|
+
if (remoteIds.length === 0) {
|
|
61
|
+
throw new Error('metadata backup job cannot run without remotes')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const config = this._config
|
|
65
|
+
const settings = {
|
|
66
|
+
...config.defaultSettings,
|
|
67
|
+
...config.metadata.defaultSettings,
|
|
68
|
+
...job.settings[''],
|
|
69
|
+
...job.settings[schedule.id],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const poolIds = extractIdsFromSimplePattern(job.pools)
|
|
73
|
+
const isEmptyPools = poolIds.length === 0
|
|
74
|
+
const isXoMetadata = job.xoMetadata !== undefined
|
|
75
|
+
if (!isXoMetadata && isEmptyPools) {
|
|
76
|
+
throw new Error('no metadata mode found')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { retentionPoolMetadata, retentionXoMetadata } = settings
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
(retentionPoolMetadata === 0 && retentionXoMetadata === 0) ||
|
|
83
|
+
(!isXoMetadata && retentionPoolMetadata === 0) ||
|
|
84
|
+
(isEmptyPools && retentionXoMetadata === 0)
|
|
85
|
+
) {
|
|
86
|
+
throw new Error('no retentions corresponding to the metadata modes found')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await Disposable.use(
|
|
90
|
+
Disposable.all(
|
|
91
|
+
poolIds.map(id =>
|
|
92
|
+
this._getRecord('pool', id).catch(error => {
|
|
93
|
+
// See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
|
|
94
|
+
runTask(
|
|
95
|
+
{
|
|
96
|
+
name: 'get pool record',
|
|
97
|
+
data: { type: 'pool', id },
|
|
98
|
+
},
|
|
99
|
+
() => Promise.reject(error)
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
)
|
|
103
|
+
),
|
|
104
|
+
Disposable.all(
|
|
105
|
+
remoteIds.map(id =>
|
|
106
|
+
this._getAdapter(id).catch(error => {
|
|
107
|
+
// See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
|
|
108
|
+
runTask(
|
|
109
|
+
{
|
|
110
|
+
name: 'get remote adapter',
|
|
111
|
+
data: { type: 'remote', id },
|
|
112
|
+
},
|
|
113
|
+
() => Promise.reject(error)
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
)
|
|
117
|
+
),
|
|
118
|
+
async (pools, remoteAdapters) => {
|
|
119
|
+
// remove adapters that failed (already handled)
|
|
120
|
+
remoteAdapters = remoteAdapters.filter(_ => _ !== undefined)
|
|
121
|
+
if (remoteAdapters.length === 0) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
remoteAdapters = getAdaptersByRemote(remoteAdapters)
|
|
125
|
+
|
|
126
|
+
// remove pools that failed (already handled)
|
|
127
|
+
pools = pools.filter(_ => _ !== undefined)
|
|
128
|
+
|
|
129
|
+
const promises = []
|
|
130
|
+
if (pools.length !== 0 && settings.retentionPoolMetadata !== 0) {
|
|
131
|
+
promises.push(
|
|
132
|
+
asyncMap(pools, async pool =>
|
|
133
|
+
runTask(
|
|
134
|
+
{
|
|
135
|
+
name: `Starting metadata backup for the pool (${pool.$id}). (${job.id})`,
|
|
136
|
+
data: {
|
|
137
|
+
id: pool.$id,
|
|
138
|
+
pool,
|
|
139
|
+
poolMaster: await ignoreErrors.call(pool.$xapi.getRecord('host', pool.master)),
|
|
140
|
+
type: 'pool',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
() =>
|
|
144
|
+
new PoolMetadataBackup({
|
|
145
|
+
config,
|
|
146
|
+
job,
|
|
147
|
+
pool,
|
|
148
|
+
remoteAdapters,
|
|
149
|
+
schedule,
|
|
150
|
+
settings,
|
|
151
|
+
}).run()
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (job.xoMetadata !== undefined && settings.retentionXoMetadata !== 0) {
|
|
158
|
+
promises.push(
|
|
159
|
+
runTask(
|
|
160
|
+
{
|
|
161
|
+
name: `Starting XO metadata backup. (${job.id})`,
|
|
162
|
+
data: {
|
|
163
|
+
type: 'xo',
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
() =>
|
|
167
|
+
new XoMetadataBackup({
|
|
168
|
+
config,
|
|
169
|
+
job,
|
|
170
|
+
remoteAdapters,
|
|
171
|
+
schedule,
|
|
172
|
+
settings,
|
|
173
|
+
}).run()
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
await Promise.all(promises)
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async _runVmBackup() {
|
|
183
|
+
const job = this._job
|
|
184
|
+
|
|
185
|
+
// FIXME: proper SimpleIdPattern handling
|
|
186
|
+
const getSnapshotNameLabel = this._getSnapshotNameLabel
|
|
187
|
+
const schedule = this._schedule
|
|
188
|
+
|
|
189
|
+
const config = this._config
|
|
190
|
+
const { settings } = job
|
|
191
|
+
const scheduleSettings = {
|
|
192
|
+
...config.defaultSettings,
|
|
193
|
+
...config.vm.defaultSettings,
|
|
194
|
+
...settings[''],
|
|
195
|
+
...settings[schedule.id],
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await Disposable.use(
|
|
199
|
+
Disposable.all(
|
|
200
|
+
extractIdsFromSimplePattern(job.srs).map(id =>
|
|
201
|
+
this._getRecord('SR', id).catch(error => {
|
|
202
|
+
runTask(
|
|
203
|
+
{
|
|
204
|
+
name: 'get SR record',
|
|
205
|
+
data: { type: 'SR', id },
|
|
206
|
+
},
|
|
207
|
+
() => Promise.reject(error)
|
|
208
|
+
)
|
|
209
|
+
})
|
|
210
|
+
)
|
|
211
|
+
),
|
|
212
|
+
Disposable.all(
|
|
213
|
+
extractIdsFromSimplePattern(job.remotes).map(id =>
|
|
214
|
+
this._getAdapter(id).catch(error => {
|
|
215
|
+
runTask(
|
|
216
|
+
{
|
|
217
|
+
name: 'get remote adapter',
|
|
218
|
+
data: { type: 'remote', id },
|
|
219
|
+
},
|
|
220
|
+
() => Promise.reject(error)
|
|
221
|
+
)
|
|
222
|
+
})
|
|
223
|
+
)
|
|
224
|
+
),
|
|
225
|
+
async (srs, remoteAdapters) => {
|
|
226
|
+
// remove adapters that failed (already handled)
|
|
227
|
+
remoteAdapters = remoteAdapters.filter(_ => _ !== undefined)
|
|
228
|
+
|
|
229
|
+
// remove srs that failed (already handled)
|
|
230
|
+
srs = srs.filter(_ => _ !== undefined)
|
|
231
|
+
|
|
232
|
+
if (remoteAdapters.length === 0 && srs.length === 0 && scheduleSettings.snapshotRetention === 0) {
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const vmIds = extractIdsFromSimplePattern(job.vms)
|
|
237
|
+
|
|
238
|
+
Task.info('vms', { vms: vmIds })
|
|
239
|
+
|
|
240
|
+
remoteAdapters = getAdaptersByRemote(remoteAdapters)
|
|
241
|
+
|
|
242
|
+
const handleVm = vmUuid =>
|
|
243
|
+
runTask({ name: 'backup VM', data: { type: 'VM', id: vmUuid } }, () =>
|
|
244
|
+
Disposable.use(this._getRecord('VM', vmUuid), vm =>
|
|
245
|
+
new VmBackup({
|
|
246
|
+
config,
|
|
247
|
+
getSnapshotNameLabel,
|
|
248
|
+
job,
|
|
249
|
+
// remotes,
|
|
250
|
+
remoteAdapters,
|
|
251
|
+
schedule,
|
|
252
|
+
settings: { ...scheduleSettings, ...settings[vmUuid] },
|
|
253
|
+
srs,
|
|
254
|
+
vm,
|
|
255
|
+
}).run()
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
const { concurrency } = scheduleSettings
|
|
259
|
+
await asyncMapSettled(vmIds, concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm))
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
2
|
+
|
|
3
|
+
exports.DurablePartition = class DurablePartition {
|
|
4
|
+
// private resource API is used exceptionally to be able to separate resource creation and release
|
|
5
|
+
#partitionDisposers = {}
|
|
6
|
+
|
|
7
|
+
flushAll() {
|
|
8
|
+
const partitionDisposers = this.#partitionDisposers
|
|
9
|
+
return asyncMap(Object.keys(partitionDisposers), path => {
|
|
10
|
+
const disposers = partitionDisposers[path]
|
|
11
|
+
delete partitionDisposers[path]
|
|
12
|
+
return asyncMap(disposers, d => d(path).catch(noop => {}))
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async mount(adapter, diskId, partitionId) {
|
|
17
|
+
const { value: path, dispose } = await adapter.getPartition(diskId, partitionId)
|
|
18
|
+
|
|
19
|
+
const partitionDisposers = this.#partitionDisposers
|
|
20
|
+
if (partitionDisposers[path] === undefined) {
|
|
21
|
+
partitionDisposers[path] = []
|
|
22
|
+
}
|
|
23
|
+
partitionDisposers[path].push(dispose)
|
|
24
|
+
|
|
25
|
+
return path
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async unmount(path) {
|
|
29
|
+
const partitionDisposers = this.#partitionDisposers
|
|
30
|
+
const disposers = partitionDisposers[path]
|
|
31
|
+
if (disposers === undefined) {
|
|
32
|
+
throw new Error(`No partition corresponding to the path ${path} found`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await disposers.pop()()
|
|
36
|
+
if (disposers.length === 0) {
|
|
37
|
+
delete partitionDisposers[path]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const assert = require('assert')
|
|
2
|
+
|
|
3
|
+
const { formatFilenameDate } = require('./_filenameDate.js')
|
|
4
|
+
const { importDeltaVm } = require('./_deltaVm.js')
|
|
5
|
+
const { Task } = require('./Task.js')
|
|
6
|
+
const { watchStreamSize } = require('./_watchStreamSize.js')
|
|
7
|
+
|
|
8
|
+
exports.ImportVmBackup = class ImportVmBackup {
|
|
9
|
+
constructor({ adapter, metadata, srUuid, xapi, settings: { newMacAddresses } = {} }) {
|
|
10
|
+
this._adapter = adapter
|
|
11
|
+
this._importDeltaVmSettings = { newMacAddresses }
|
|
12
|
+
this._metadata = metadata
|
|
13
|
+
this._srUuid = srUuid
|
|
14
|
+
this._xapi = xapi
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async run() {
|
|
18
|
+
const adapter = this._adapter
|
|
19
|
+
const metadata = this._metadata
|
|
20
|
+
const isFull = metadata.mode === 'full'
|
|
21
|
+
|
|
22
|
+
const sizeContainer = { size: 0 }
|
|
23
|
+
|
|
24
|
+
let backup
|
|
25
|
+
if (isFull) {
|
|
26
|
+
backup = await adapter.readFullVmBackup(metadata)
|
|
27
|
+
watchStreamSize(backup, sizeContainer)
|
|
28
|
+
} else {
|
|
29
|
+
assert.strictEqual(metadata.mode, 'delta')
|
|
30
|
+
|
|
31
|
+
backup = await adapter.readDeltaVmBackup(metadata)
|
|
32
|
+
Object.values(backup.streams).forEach(stream => watchStreamSize(stream, sizeContainer))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Task.run(
|
|
36
|
+
{
|
|
37
|
+
name: 'transfer',
|
|
38
|
+
},
|
|
39
|
+
async () => {
|
|
40
|
+
const xapi = this._xapi
|
|
41
|
+
const srRef = await xapi.call('SR.get_by_uuid', this._srUuid)
|
|
42
|
+
|
|
43
|
+
const vmRef = isFull
|
|
44
|
+
? await xapi.VM_import(backup, srRef)
|
|
45
|
+
: await importDeltaVm(backup, await xapi.getRecord('SR', srRef), {
|
|
46
|
+
...this._importDeltaVmSettings,
|
|
47
|
+
detectBase: false,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
await Promise.all([
|
|
51
|
+
xapi.call('VM.add_tags', vmRef, 'restored from backup'),
|
|
52
|
+
xapi.call(
|
|
53
|
+
'VM.set_name_label',
|
|
54
|
+
vmRef,
|
|
55
|
+
`${metadata.vm.name_label} (${formatFilenameDate(metadata.timestamp)})`
|
|
56
|
+
),
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
size: sizeContainer.size,
|
|
61
|
+
id: await xapi.getField('VM', vmRef, 'uuid'),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
|
2
|
+
|
|
3
|
+
# @xen-orchestra/backups
|
|
4
|
+
|
|
5
|
+
[](https://npmjs.org/package/@xen-orchestra/backups)  [](https://bundlephobia.com/result?p=@xen-orchestra/backups) [](https://npmjs.org/package/@xen-orchestra/backups)
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/backups):
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
> npm install --save @xen-orchestra/backups
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Contributions
|
|
16
|
+
|
|
17
|
+
Contributions are _very_ welcomed, either on the documentation or on
|
|
18
|
+
the code.
|
|
19
|
+
|
|
20
|
+
You may:
|
|
21
|
+
|
|
22
|
+
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
|
23
|
+
you've encountered;
|
|
24
|
+
- fork and create a pull request.
|
|
25
|
+
|
|
26
|
+
## License
|
|
27
|
+
|
|
28
|
+
[AGPL-3.0-or-later](https://spdx.org/licenses/AGPL-3.0-or-later) © [Vates SAS](https://vates.fr)
|