@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/RemoteAdapter.js
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
|
|
2
|
+
const Disposable = require('promise-toolbox/Disposable.js')
|
|
3
|
+
const fromCallback = require('promise-toolbox/fromCallback.js')
|
|
4
|
+
const fromEvent = require('promise-toolbox/fromEvent.js')
|
|
5
|
+
const pDefer = require('promise-toolbox/defer.js')
|
|
6
|
+
const pump = require('pump')
|
|
7
|
+
const { basename, dirname, join, normalize, resolve } = require('path')
|
|
8
|
+
const { createLogger } = require('@xen-orchestra/log')
|
|
9
|
+
const { createSyntheticStream, mergeVhd, default: Vhd } = require('vhd-lib')
|
|
10
|
+
const { deduped } = require('@vates/disposable/deduped.js')
|
|
11
|
+
const { execFile } = require('child_process')
|
|
12
|
+
const { readdir, stat } = require('fs-extra')
|
|
13
|
+
const { ZipFile } = require('yazl')
|
|
14
|
+
|
|
15
|
+
const { BACKUP_DIR } = require('./_getVmBackupDir.js')
|
|
16
|
+
const { cleanVm } = require('./_cleanVm.js')
|
|
17
|
+
const { getTmpDir } = require('./_getTmpDir.js')
|
|
18
|
+
const { isMetadataFile, isVhdFile } = require('./_backupType.js')
|
|
19
|
+
const { isValidXva } = require('./_isValidXva.js')
|
|
20
|
+
const { listPartitions, LVM_PARTITION_TYPE } = require('./_listPartitions.js')
|
|
21
|
+
const { lvs, pvs } = require('./_lvm.js')
|
|
22
|
+
|
|
23
|
+
const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
|
|
24
|
+
exports.DIR_XO_CONFIG_BACKUPS = DIR_XO_CONFIG_BACKUPS
|
|
25
|
+
|
|
26
|
+
const DIR_XO_POOL_METADATA_BACKUPS = 'xo-pool-metadata-backups'
|
|
27
|
+
exports.DIR_XO_POOL_METADATA_BACKUPS = DIR_XO_POOL_METADATA_BACKUPS
|
|
28
|
+
|
|
29
|
+
const { warn } = createLogger('xo:backups:RemoteAdapter')
|
|
30
|
+
|
|
31
|
+
const compareTimestamp = (a, b) => a.timestamp - b.timestamp
|
|
32
|
+
|
|
33
|
+
const noop = Function.prototype
|
|
34
|
+
|
|
35
|
+
const resolveRelativeFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)
|
|
36
|
+
|
|
37
|
+
const resolveSubpath = (root, path) => resolve(root, `.${resolve('/', path)}`)
|
|
38
|
+
|
|
39
|
+
const RE_VHDI = /^vhdi(\d+)$/
|
|
40
|
+
|
|
41
|
+
async function addDirectory(files, realPath, metadataPath) {
|
|
42
|
+
try {
|
|
43
|
+
const subFiles = await readdir(realPath)
|
|
44
|
+
await asyncMap(subFiles, file => addDirectory(files, realPath + '/' + file, metadataPath + '/' + file))
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error == null || error.code !== 'ENOTDIR') {
|
|
47
|
+
throw error
|
|
48
|
+
}
|
|
49
|
+
files.push({
|
|
50
|
+
realPath,
|
|
51
|
+
metadataPath,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const createSafeReaddir = (handler, methodName) => (path, options) =>
|
|
57
|
+
handler.list(path, options).catch(error => {
|
|
58
|
+
if (error?.code !== 'ENOENT') {
|
|
59
|
+
warn(`${methodName} ${path}`, { error })
|
|
60
|
+
}
|
|
61
|
+
return []
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const debounceResourceFactory = factory =>
|
|
65
|
+
function () {
|
|
66
|
+
return this._debounceResource(factory.apply(this, arguments))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class RemoteAdapter {
|
|
70
|
+
constructor(handler, { debounceResource = res => res, dirMode } = {}) {
|
|
71
|
+
this._debounceResource = debounceResource
|
|
72
|
+
this._dirMode = dirMode
|
|
73
|
+
this._handler = handler
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get handler() {
|
|
77
|
+
return this._handler
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async _deleteVhd(path) {
|
|
81
|
+
const handler = this._handler
|
|
82
|
+
const vhds = await asyncMapSettled(
|
|
83
|
+
await handler.list(dirname(path), {
|
|
84
|
+
filter: isVhdFile,
|
|
85
|
+
prependDir: true,
|
|
86
|
+
}),
|
|
87
|
+
async path => {
|
|
88
|
+
try {
|
|
89
|
+
const vhd = new Vhd(handler, path)
|
|
90
|
+
await vhd.readHeaderAndFooter()
|
|
91
|
+
return {
|
|
92
|
+
footer: vhd.footer,
|
|
93
|
+
header: vhd.header,
|
|
94
|
+
path,
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Do not fail on corrupted VHDs (usually uncleaned temporary files),
|
|
98
|
+
// they are probably inconsequent to the backup process and should not
|
|
99
|
+
// fail it.
|
|
100
|
+
warn(`BackupNg#_deleteVhd ${path}`, { error })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
const base = basename(path)
|
|
105
|
+
const child = vhds.find(_ => _ !== undefined && _.header.parentUnicodeName === base)
|
|
106
|
+
if (child === undefined) {
|
|
107
|
+
await handler.unlink(path)
|
|
108
|
+
return 0
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const childPath = child.path
|
|
113
|
+
const mergedDataSize = await mergeVhd(handler, path, handler, childPath)
|
|
114
|
+
await handler.rename(path, childPath)
|
|
115
|
+
return mergedDataSize
|
|
116
|
+
} catch (error) {
|
|
117
|
+
handler.unlink(path).catch(warn)
|
|
118
|
+
throw error
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async _findPartition(devicePath, partitionId) {
|
|
123
|
+
const partitions = await listPartitions(devicePath)
|
|
124
|
+
const partition = partitions.find(_ => _.id === partitionId)
|
|
125
|
+
if (partition === undefined) {
|
|
126
|
+
throw new Error(`partition ${partitionId} not found`)
|
|
127
|
+
}
|
|
128
|
+
return partition
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_getLvmLogicalVolumes = Disposable.factory(this._getLvmLogicalVolumes)
|
|
132
|
+
_getLvmLogicalVolumes = deduped(this._getLvmLogicalVolumes, (devicePath, pvId, vgName) => [devicePath, pvId, vgName])
|
|
133
|
+
_getLvmLogicalVolumes = debounceResourceFactory(this._getLvmLogicalVolumes)
|
|
134
|
+
async *_getLvmLogicalVolumes(devicePath, pvId, vgName) {
|
|
135
|
+
yield this._getLvmPhysicalVolume(devicePath, pvId && (await this._findPartition(devicePath, pvId)))
|
|
136
|
+
|
|
137
|
+
await fromCallback(execFile, 'vgchange', ['-ay', vgName])
|
|
138
|
+
try {
|
|
139
|
+
yield lvs(['lv_name', 'lv_path'], vgName)
|
|
140
|
+
} finally {
|
|
141
|
+
await fromCallback(execFile, 'vgchange', ['-an', vgName])
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_getLvmPhysicalVolume = Disposable.factory(this._getLvmPhysicalVolume)
|
|
146
|
+
_getLvmPhysicalVolume = deduped(this._getLvmPhysicalVolume, (devicePath, partition) => [devicePath, partition?.id])
|
|
147
|
+
_getLvmPhysicalVolume = debounceResourceFactory(this._getLvmPhysicalVolume)
|
|
148
|
+
async *_getLvmPhysicalVolume(devicePath, partition) {
|
|
149
|
+
const args = []
|
|
150
|
+
if (partition !== undefined) {
|
|
151
|
+
args.push('-o', partition.start * 512, '--sizelimit', partition.size)
|
|
152
|
+
}
|
|
153
|
+
args.push('--show', '-f', devicePath)
|
|
154
|
+
const path = (await fromCallback(execFile, 'losetup', args)).trim()
|
|
155
|
+
try {
|
|
156
|
+
await fromCallback(execFile, 'pvscan', ['--cache', path])
|
|
157
|
+
yield path
|
|
158
|
+
} finally {
|
|
159
|
+
try {
|
|
160
|
+
const vgNames = await pvs('vg_name', path)
|
|
161
|
+
await fromCallback(execFile, 'vgchange', ['-an', ...vgNames])
|
|
162
|
+
} finally {
|
|
163
|
+
await fromCallback(execFile, 'losetup', ['-d', path])
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_getPartition = Disposable.factory(this._getPartition)
|
|
169
|
+
_getPartition = deduped(this._getPartition, (devicePath, partition) => [devicePath, partition?.id])
|
|
170
|
+
_getPartition = debounceResourceFactory(this._getPartition)
|
|
171
|
+
async *_getPartition(devicePath, partition) {
|
|
172
|
+
const options = ['loop', 'ro']
|
|
173
|
+
|
|
174
|
+
if (partition !== undefined) {
|
|
175
|
+
const { size, start } = partition
|
|
176
|
+
options.push(`sizelimit=${size}`)
|
|
177
|
+
if (start !== undefined) {
|
|
178
|
+
options.push(`offset=${start * 512}`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const path = yield getTmpDir()
|
|
183
|
+
const mount = options => {
|
|
184
|
+
return fromCallback(execFile, 'mount', [
|
|
185
|
+
`--options=${options.join(',')}`,
|
|
186
|
+
`--source=${devicePath}`,
|
|
187
|
+
`--target=${path}`,
|
|
188
|
+
])
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// `norecovery` option is used for ext3/ext4/xfs, if it fails it might be
|
|
192
|
+
// another fs, try without
|
|
193
|
+
try {
|
|
194
|
+
await mount([...options, 'norecovery'])
|
|
195
|
+
} catch (error) {
|
|
196
|
+
await mount(options)
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
yield path
|
|
200
|
+
} finally {
|
|
201
|
+
await fromCallback(execFile, 'umount', ['--lazy', path])
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_listLvmLogicalVolumes(devicePath, partition, results = []) {
|
|
206
|
+
return Disposable.use(this._getLvmPhysicalVolume(devicePath, partition), async path => {
|
|
207
|
+
const lvs = await pvs(['lv_name', 'lv_path', 'lv_size', 'vg_name'], path)
|
|
208
|
+
const partitionId = partition !== undefined ? partition.id : ''
|
|
209
|
+
lvs.forEach((lv, i) => {
|
|
210
|
+
const name = lv.lv_name
|
|
211
|
+
if (name !== '') {
|
|
212
|
+
results.push({
|
|
213
|
+
id: `${partitionId}/${lv.vg_name}/${name}`,
|
|
214
|
+
name,
|
|
215
|
+
size: lv.lv_size,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
return results
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_usePartitionFiles = Disposable.factory(this._usePartitionFiles)
|
|
224
|
+
async *_usePartitionFiles(diskId, partitionId, paths) {
|
|
225
|
+
const path = yield this.getPartition(diskId, partitionId)
|
|
226
|
+
|
|
227
|
+
const files = []
|
|
228
|
+
await asyncMap(paths, file =>
|
|
229
|
+
addDirectory(files, resolveSubpath(path, file), normalize('./' + file).replace(/\/+$/, ''))
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return files
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fetchPartitionFiles(diskId, partitionId, paths) {
|
|
236
|
+
const { promise, reject, resolve } = pDefer()
|
|
237
|
+
Disposable.use(
|
|
238
|
+
async function* () {
|
|
239
|
+
const files = yield this._usePartitionFiles(diskId, partitionId, paths)
|
|
240
|
+
const zip = new ZipFile()
|
|
241
|
+
files.forEach(({ realPath, metadataPath }) => zip.addFile(realPath, metadataPath))
|
|
242
|
+
zip.end()
|
|
243
|
+
const { outputStream } = zip
|
|
244
|
+
resolve(outputStream)
|
|
245
|
+
await fromEvent(outputStream, 'end')
|
|
246
|
+
}.bind(this)
|
|
247
|
+
).catch(error => {
|
|
248
|
+
warn(error)
|
|
249
|
+
reject(error)
|
|
250
|
+
})
|
|
251
|
+
return promise
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async deleteDeltaVmBackups(backups) {
|
|
255
|
+
const handler = this._handler
|
|
256
|
+
|
|
257
|
+
// unused VHDs will be detected by `cleanVm`
|
|
258
|
+
await asyncMapSettled(backups, ({ _filename }) => handler.unlink(_filename))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async deleteMetadataBackup(backupId) {
|
|
262
|
+
const uuidReg = '\\w{8}(-\\w{4}){3}-\\w{12}'
|
|
263
|
+
const metadataDirReg = 'xo-(config|pool-metadata)-backups'
|
|
264
|
+
const timestampReg = '\\d{8}T\\d{6}Z'
|
|
265
|
+
const regexp = new RegExp(`^${metadataDirReg}/${uuidReg}(/${uuidReg})?/${timestampReg}`)
|
|
266
|
+
if (!regexp.test(backupId)) {
|
|
267
|
+
throw new Error(`The id (${backupId}) not correspond to a metadata folder`)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
await this._handler.rmtree(backupId)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async deleteOldMetadataBackups(dir, retention) {
|
|
274
|
+
const handler = this.handler
|
|
275
|
+
let list = await handler.list(dir)
|
|
276
|
+
list.sort()
|
|
277
|
+
list = list.filter(timestamp => /^\d{8}T\d{6}Z$/.test(timestamp)).slice(0, -retention)
|
|
278
|
+
await asyncMapSettled(list, timestamp => handler.rmtree(`${dir}/${timestamp}`))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async deleteFullVmBackups(backups) {
|
|
282
|
+
const handler = this._handler
|
|
283
|
+
await asyncMapSettled(backups, ({ _filename, xva }) =>
|
|
284
|
+
Promise.all([handler.unlink(_filename), handler.unlink(resolveRelativeFromFile(_filename, xva))])
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async deleteVmBackup(filename) {
|
|
289
|
+
const metadata = JSON.parse(String(await this._handler.readFile(filename)))
|
|
290
|
+
metadata._filename = filename
|
|
291
|
+
|
|
292
|
+
if (metadata.mode === 'delta') {
|
|
293
|
+
await this.deleteDeltaVmBackups([metadata])
|
|
294
|
+
} else if (metadata.mode === 'full') {
|
|
295
|
+
await this.deleteFullVmBackups([metadata])
|
|
296
|
+
} else {
|
|
297
|
+
throw new Error(`no deleter for backup mode ${metadata.mode}`)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
getDisk = Disposable.factory(this.getDisk)
|
|
302
|
+
getDisk = deduped(this.getDisk, diskId => [diskId])
|
|
303
|
+
getDisk = debounceResourceFactory(this.getDisk)
|
|
304
|
+
async *getDisk(diskId) {
|
|
305
|
+
const handler = this._handler
|
|
306
|
+
|
|
307
|
+
const diskPath = handler._getFilePath('/' + diskId)
|
|
308
|
+
const mountDir = yield getTmpDir()
|
|
309
|
+
await fromCallback(execFile, 'vhdimount', [diskPath, mountDir])
|
|
310
|
+
try {
|
|
311
|
+
let max = 0
|
|
312
|
+
let maxEntry
|
|
313
|
+
const entries = await readdir(mountDir)
|
|
314
|
+
entries.forEach(entry => {
|
|
315
|
+
const matches = RE_VHDI.exec(entry)
|
|
316
|
+
if (matches !== null) {
|
|
317
|
+
const value = +matches[1]
|
|
318
|
+
if (value > max) {
|
|
319
|
+
max = value
|
|
320
|
+
maxEntry = entry
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
if (max === 0) {
|
|
325
|
+
throw new Error('no disks found')
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
yield `${mountDir}/${maxEntry}`
|
|
329
|
+
} finally {
|
|
330
|
+
await fromCallback(execFile, 'fusermount', ['-uz', mountDir])
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// partitionId values:
|
|
335
|
+
//
|
|
336
|
+
// - undefined: raw disk
|
|
337
|
+
// - `<partitionId>`: partitioned disk
|
|
338
|
+
// - `<pvId>/<vgName>/<lvName>`: LVM on a partitioned disk
|
|
339
|
+
// - `/<vgName>/lvName>`: LVM on a raw disk
|
|
340
|
+
getPartition = Disposable.factory(this.getPartition)
|
|
341
|
+
async *getPartition(diskId, partitionId) {
|
|
342
|
+
const devicePath = yield this.getDisk(diskId)
|
|
343
|
+
if (partitionId === undefined) {
|
|
344
|
+
return yield this._getPartition(devicePath)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const isLvmPartition = partitionId.includes('/')
|
|
348
|
+
if (isLvmPartition) {
|
|
349
|
+
const [pvId, vgName, lvName] = partitionId.split('/')
|
|
350
|
+
const lvs = yield this._getLvmLogicalVolumes(devicePath, pvId !== '' ? pvId : undefined, vgName)
|
|
351
|
+
return yield this._getPartition(lvs.find(_ => _.lv_name === lvName).lv_path)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return yield this._getPartition(devicePath, await this._findPartition(devicePath, partitionId))
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async listAllVmBackups() {
|
|
358
|
+
const handler = this._handler
|
|
359
|
+
|
|
360
|
+
const backups = { __proto__: null }
|
|
361
|
+
await asyncMap(await handler.list(BACKUP_DIR), async vmUuid => {
|
|
362
|
+
const vmBackups = await this.listVmBackups(vmUuid)
|
|
363
|
+
backups[vmUuid] = vmBackups
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
return backups
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
listPartitionFiles(diskId, partitionId, path) {
|
|
370
|
+
return Disposable.use(this.getPartition(diskId, partitionId), async rootPath => {
|
|
371
|
+
path = resolveSubpath(rootPath, path)
|
|
372
|
+
|
|
373
|
+
const entriesMap = {}
|
|
374
|
+
await asyncMap(await readdir(path), async name => {
|
|
375
|
+
try {
|
|
376
|
+
const stats = await stat(`${path}/${name}`)
|
|
377
|
+
entriesMap[stats.isDirectory() ? `${name}/` : name] = {}
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (error == null || error.code !== 'ENOENT') {
|
|
380
|
+
throw error
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
return entriesMap
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
listPartitions(diskId) {
|
|
390
|
+
return Disposable.use(this.getDisk(diskId), async devicePath => {
|
|
391
|
+
const partitions = await listPartitions(devicePath)
|
|
392
|
+
|
|
393
|
+
if (partitions.length === 0) {
|
|
394
|
+
try {
|
|
395
|
+
// handle potential raw LVM physical volume
|
|
396
|
+
return await this._listLvmLogicalVolumes(devicePath, undefined, partitions)
|
|
397
|
+
} catch (error) {
|
|
398
|
+
return []
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const results = []
|
|
403
|
+
await asyncMapSettled(partitions, partition =>
|
|
404
|
+
partition.type === LVM_PARTITION_TYPE
|
|
405
|
+
? this._listLvmLogicalVolumes(devicePath, partition, results)
|
|
406
|
+
: results.push(partition)
|
|
407
|
+
)
|
|
408
|
+
return results
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async listPoolMetadataBackups() {
|
|
413
|
+
const handler = this._handler
|
|
414
|
+
const safeReaddir = createSafeReaddir(handler, 'listPoolMetadataBackups')
|
|
415
|
+
|
|
416
|
+
const backupsByPool = {}
|
|
417
|
+
await asyncMap(await safeReaddir(DIR_XO_POOL_METADATA_BACKUPS, { prependDir: true }), async scheduleDir =>
|
|
418
|
+
asyncMap(await safeReaddir(scheduleDir), async poolId => {
|
|
419
|
+
const backups = backupsByPool[poolId] ?? (backupsByPool[poolId] = [])
|
|
420
|
+
return asyncMap(await safeReaddir(`${scheduleDir}/${poolId}`, { prependDir: true }), async backupDir => {
|
|
421
|
+
try {
|
|
422
|
+
backups.push({
|
|
423
|
+
id: backupDir,
|
|
424
|
+
...JSON.parse(String(await handler.readFile(`${backupDir}/metadata.json`))),
|
|
425
|
+
})
|
|
426
|
+
} catch (error) {
|
|
427
|
+
warn(`listPoolMetadataBackups ${backupDir}`, {
|
|
428
|
+
error,
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
// delete empty entries and sort backups
|
|
436
|
+
Object.keys(backupsByPool).forEach(poolId => {
|
|
437
|
+
const backups = backupsByPool[poolId]
|
|
438
|
+
if (backups.length === 0) {
|
|
439
|
+
delete backupsByPool[poolId]
|
|
440
|
+
} else {
|
|
441
|
+
backups.sort(compareTimestamp)
|
|
442
|
+
}
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
return backupsByPool
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async listVmBackups(vmUuid, predicate) {
|
|
449
|
+
const handler = this._handler
|
|
450
|
+
const backups = []
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
const files = await handler.list(`${BACKUP_DIR}/${vmUuid}`, {
|
|
454
|
+
filter: isMetadataFile,
|
|
455
|
+
prependDir: true,
|
|
456
|
+
})
|
|
457
|
+
await asyncMap(files, async file => {
|
|
458
|
+
try {
|
|
459
|
+
const metadata = await this.readVmBackupMetadata(file)
|
|
460
|
+
if (predicate === undefined || predicate(metadata)) {
|
|
461
|
+
// inject an id usable by importVmBackupNg()
|
|
462
|
+
metadata.id = metadata._filename
|
|
463
|
+
|
|
464
|
+
backups.push(metadata)
|
|
465
|
+
}
|
|
466
|
+
} catch (error) {
|
|
467
|
+
warn(`listVmBackups ${file}`, { error })
|
|
468
|
+
}
|
|
469
|
+
})
|
|
470
|
+
} catch (error) {
|
|
471
|
+
let code
|
|
472
|
+
if (error == null || ((code = error.code) !== 'ENOENT' && code !== 'ENOTDIR')) {
|
|
473
|
+
throw error
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return backups.sort(compareTimestamp)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async listXoMetadataBackups() {
|
|
481
|
+
const handler = this._handler
|
|
482
|
+
const safeReaddir = createSafeReaddir(handler, 'listXoMetadataBackups')
|
|
483
|
+
|
|
484
|
+
const backups = []
|
|
485
|
+
await asyncMap(await safeReaddir(DIR_XO_CONFIG_BACKUPS, { prependDir: true }), async scheduleDir =>
|
|
486
|
+
asyncMap(await safeReaddir(scheduleDir, { prependDir: true }), async backupDir => {
|
|
487
|
+
try {
|
|
488
|
+
backups.push({
|
|
489
|
+
id: backupDir,
|
|
490
|
+
...JSON.parse(String(await handler.readFile(`${backupDir}/metadata.json`))),
|
|
491
|
+
})
|
|
492
|
+
} catch (error) {
|
|
493
|
+
warn(`listXoMetadataBackups ${backupDir}`, { error })
|
|
494
|
+
}
|
|
495
|
+
})
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
return backups.sort(compareTimestamp)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async outputStream(path, input, { checksum = true, validator = noop } = {}) {
|
|
502
|
+
await this._handler.outputStream(path, input, {
|
|
503
|
+
checksum,
|
|
504
|
+
dirMode: this._dirMode,
|
|
505
|
+
async validator() {
|
|
506
|
+
await input.task
|
|
507
|
+
return validator.apply(this, arguments)
|
|
508
|
+
},
|
|
509
|
+
})
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async readDeltaVmBackup(metadata) {
|
|
513
|
+
const handler = this._handler
|
|
514
|
+
const { vbds, vdis, vhds, vifs, vm } = metadata
|
|
515
|
+
const dir = dirname(metadata._filename)
|
|
516
|
+
|
|
517
|
+
const streams = {}
|
|
518
|
+
await asyncMapSettled(Object.keys(vdis), async id => {
|
|
519
|
+
streams[`${id}.vhd`] = await createSyntheticStream(handler, join(dir, vhds[id]))
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
streams,
|
|
524
|
+
vbds,
|
|
525
|
+
vdis,
|
|
526
|
+
version: '1.0.0',
|
|
527
|
+
vifs,
|
|
528
|
+
vm,
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
readFullVmBackup(metadata) {
|
|
533
|
+
return this._handler.createReadStream(resolve('/', dirname(metadata._filename), metadata.xva))
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async readVmBackupMetadata(path) {
|
|
537
|
+
return Object.defineProperty(JSON.parse(await this._handler.readFile(path)), '_filename', { value: path })
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
Object.assign(RemoteAdapter.prototype, {
|
|
542
|
+
cleanVm(vmDir, { lock = true } = {}) {
|
|
543
|
+
if (lock) {
|
|
544
|
+
return Disposable.use(this._handler.lock(vmDir), () => cleanVm.apply(this, arguments))
|
|
545
|
+
} else {
|
|
546
|
+
return cleanVm.apply(this, arguments)
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
isValidXva,
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
exports.RemoteAdapter = RemoteAdapter
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const { DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter.js')
|
|
2
|
+
const { PATH_DB_DUMP } = require('./_PoolMetadataBackup.js')
|
|
3
|
+
|
|
4
|
+
exports.RestoreMetadataBackup = class RestoreMetadataBackup {
|
|
5
|
+
constructor({ backupId, handler, xapi }) {
|
|
6
|
+
this._backupId = backupId
|
|
7
|
+
this._handler = handler
|
|
8
|
+
this._xapi = xapi
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async run() {
|
|
12
|
+
const backupId = this._backupId
|
|
13
|
+
const handler = this._handler
|
|
14
|
+
const xapi = this._xapi
|
|
15
|
+
|
|
16
|
+
if (backupId.split('/')[0] === DIR_XO_POOL_METADATA_BACKUPS) {
|
|
17
|
+
return xapi.putResource(await handler.createReadStream(`${backupId}/data`), PATH_DB_DUMP, {
|
|
18
|
+
task: xapi.task_create('Import pool metadata'),
|
|
19
|
+
})
|
|
20
|
+
} else {
|
|
21
|
+
return String(await handler.readFile(`${backupId}/data.json`))
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|