@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.
Files changed (43) hide show
  1. package/Backup.js +263 -0
  2. package/DurablePartition.js +40 -0
  3. package/ImportVmBackup.js +66 -0
  4. package/README.md +28 -0
  5. package/RemoteAdapter.js +552 -0
  6. package/RestoreMetadataBackup.js +24 -0
  7. package/Task.js +151 -0
  8. package/_PoolMetadataBackup.js +75 -0
  9. package/_VmBackup.js +409 -0
  10. package/_XoMetadataBackup.js +62 -0
  11. package/_backupType.js +4 -0
  12. package/_backupWorker.js +155 -0
  13. package/_cancelableMap.js +20 -0
  14. package/_cleanVm.js +378 -0
  15. package/_deltaVm.js +347 -0
  16. package/_extractIdsFromSimplePattern.js +29 -0
  17. package/_filenameDate.js +6 -0
  18. package/_forkStreamUnpipe.js +28 -0
  19. package/_getOldEntries.js +4 -0
  20. package/_getTmpDir.js +20 -0
  21. package/_getVmBackupDir.js +6 -0
  22. package/_isValidXva.js +60 -0
  23. package/_listPartitions.js +52 -0
  24. package/_lvm.js +31 -0
  25. package/_watchStreamSize.js +7 -0
  26. package/formatVmBackups.js +34 -0
  27. package/merge-worker/cli.js +69 -0
  28. package/merge-worker/index.js +25 -0
  29. package/package.json +49 -0
  30. package/parseMetadataBackupId.js +23 -0
  31. package/runBackupWorker.js +38 -0
  32. package/writers/DeltaBackupWriter.js +221 -0
  33. package/writers/DeltaReplicationWriter.js +126 -0
  34. package/writers/FullBackupWriter.js +85 -0
  35. package/writers/FullReplicationWriter.js +88 -0
  36. package/writers/_AbstractDeltaWriter.js +26 -0
  37. package/writers/_AbstractFullWriter.js +12 -0
  38. package/writers/_AbstractWriter.js +10 -0
  39. package/writers/_MixinBackupWriter.js +51 -0
  40. package/writers/_MixinReplicationWriter.js +8 -0
  41. package/writers/_checkVhd.js +5 -0
  42. package/writers/_listReplicatedVms.js +30 -0
  43. 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
+ [![Package Version](https://badgen.net/npm/v/@xen-orchestra/backups)](https://npmjs.org/package/@xen-orchestra/backups) ![License](https://badgen.net/npm/license/@xen-orchestra/backups) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@xen-orchestra/backups)](https://bundlephobia.com/result?p=@xen-orchestra/backups) [![Node compatibility](https://badgen.net/npm/node/@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)