@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/_deltaVm.js ADDED
@@ -0,0 +1,347 @@
1
+ const compareVersions = require('compare-versions')
2
+ const find = require('lodash/find.js')
3
+ const groupBy = require('lodash/groupBy.js')
4
+ const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
5
+ const omit = require('lodash/omit.js')
6
+ const { asyncMap } = require('@xen-orchestra/async-map')
7
+ const { CancelToken } = require('promise-toolbox')
8
+ const { createVhdStreamWithLength } = require('vhd-lib')
9
+ const { defer } = require('golike-defer')
10
+
11
+ const { cancelableMap } = require('./_cancelableMap.js')
12
+
13
+ const TAG_BASE_DELTA = 'xo:base_delta'
14
+ exports.TAG_BASE_DELTA = TAG_BASE_DELTA
15
+
16
+ const TAG_COPY_SRC = 'xo:copy_of'
17
+ exports.TAG_COPY_SRC = TAG_COPY_SRC
18
+
19
+ const ensureArray = value => (value === undefined ? [] : Array.isArray(value) ? value : [value])
20
+
21
+ exports.exportDeltaVm = async function exportDeltaVm(
22
+ vm,
23
+ baseVm,
24
+ {
25
+ cancelToken = CancelToken.none,
26
+
27
+ // Sets of UUIDs of VDIs that must be exported as full.
28
+ fullVdisRequired = new Set(),
29
+
30
+ disableBaseTags = false,
31
+ } = {}
32
+ ) {
33
+ // refs of VM's VDIs → base's VDIs.
34
+ const baseVdis = {}
35
+ baseVm &&
36
+ baseVm.$VBDs.forEach(vbd => {
37
+ let vdi, snapshotOf
38
+ if ((vdi = vbd.$VDI) && (snapshotOf = vdi.$snapshot_of) && !fullVdisRequired.has(snapshotOf.uuid)) {
39
+ baseVdis[vdi.snapshot_of] = vdi
40
+ }
41
+ })
42
+
43
+ const streams = {}
44
+ const vdis = {}
45
+ const vbds = {}
46
+ await cancelableMap(cancelToken, vm.$VBDs, async (cancelToken, vbd) => {
47
+ let vdi
48
+ if (vbd.type !== 'Disk' || !(vdi = vbd.$VDI)) {
49
+ // Ignore this VBD.
50
+ return
51
+ }
52
+
53
+ // If the VDI name start with `[NOBAK]`, do not export it.
54
+ if (vdi.name_label.startsWith('[NOBAK]')) {
55
+ // FIXME: find a way to not create the VDI snapshot in the
56
+ // first time.
57
+ //
58
+ // The snapshot must not exist otherwise it could break the
59
+ // next export.
60
+ ignoreErrors.call(vdi.$destroy())
61
+ return
62
+ }
63
+
64
+ vbds[vbd.$ref] = vbd
65
+
66
+ const vdiRef = vdi.$ref
67
+ if (vdiRef in vdis) {
68
+ // This VDI has already been managed.
69
+ return
70
+ }
71
+
72
+ // Look for a snapshot of this vdi in the base VM.
73
+ const baseVdi = baseVdis[vdi.snapshot_of]
74
+
75
+ vdis[vdiRef] = {
76
+ ...vdi,
77
+ other_config: {
78
+ ...vdi.other_config,
79
+ [TAG_BASE_DELTA]: baseVdi && !disableBaseTags ? baseVdi.uuid : undefined,
80
+ },
81
+ $snapshot_of$uuid: vdi.$snapshot_of?.uuid,
82
+ $SR$uuid: vdi.$SR.uuid,
83
+ }
84
+
85
+ streams[`${vdiRef}.vhd`] = await vdi.$exportContent({
86
+ baseRef: baseVdi?.$ref,
87
+ cancelToken,
88
+ format: 'vhd',
89
+ })
90
+ })
91
+
92
+ const suspendVdi = vm.$suspend_VDI
93
+ if (suspendVdi !== undefined) {
94
+ const vdiRef = suspendVdi.$ref
95
+ vdis[vdiRef] = {
96
+ ...suspendVdi,
97
+ $SR$uuid: suspendVdi.$SR.uuid,
98
+ }
99
+ streams[`${vdiRef}.vhd`] = await suspendVdi.$exportContent({
100
+ cancelToken,
101
+ format: 'vhd',
102
+ })
103
+ }
104
+
105
+ const vifs = {}
106
+ vm.$VIFs.forEach(vif => {
107
+ const network = vif.$network
108
+ vifs[vif.$ref] = {
109
+ ...vif,
110
+ $network$uuid: network.uuid,
111
+ $network$name_label: network.name_label,
112
+ $network$VLAN: network.$PIFs[0]?.VLAN,
113
+ }
114
+ })
115
+
116
+ return Object.defineProperty(
117
+ {
118
+ version: '1.1.0',
119
+ vbds,
120
+ vdis,
121
+ vifs,
122
+ vm: {
123
+ ...vm,
124
+ other_config:
125
+ baseVm && !disableBaseTags
126
+ ? {
127
+ ...vm.other_config,
128
+ [TAG_BASE_DELTA]: baseVm.uuid,
129
+ }
130
+ : omit(vm.other_config, TAG_BASE_DELTA),
131
+ },
132
+ },
133
+ 'streams',
134
+ {
135
+ configurable: true,
136
+ value: streams,
137
+ writable: true,
138
+ }
139
+ )
140
+ }
141
+
142
+ exports.importDeltaVm = defer(async function importDeltaVm(
143
+ $defer,
144
+ deltaVm,
145
+ sr,
146
+ { cancelToken = CancelToken.none, detectBase = true, mapVdisSrs = {}, newMacAddresses = false } = {}
147
+ ) {
148
+ const { version } = deltaVm
149
+ if (compareVersions(version, '1.0.0') < 0) {
150
+ throw new Error(`Unsupported delta backup version: ${version}`)
151
+ }
152
+
153
+ const vmRecord = deltaVm.vm
154
+ const xapi = sr.$xapi
155
+
156
+ let baseVm
157
+ if (detectBase) {
158
+ const remoteBaseVmUuid = vmRecord.other_config[TAG_BASE_DELTA]
159
+ if (remoteBaseVmUuid) {
160
+ baseVm = find(xapi.objects.all, obj => (obj = obj.other_config) && obj[TAG_COPY_SRC] === remoteBaseVmUuid)
161
+
162
+ if (!baseVm) {
163
+ throw new Error(`could not find the base VM (copy of ${remoteBaseVmUuid})`)
164
+ }
165
+ }
166
+ }
167
+
168
+ const baseVdis = {}
169
+ baseVm &&
170
+ baseVm.$VBDs.forEach(vbd => {
171
+ const vdi = vbd.$VDI
172
+ if (vdi !== undefined) {
173
+ baseVdis[vbd.VDI] = vbd.$VDI
174
+ }
175
+ })
176
+ const vdiRecords = deltaVm.vdis
177
+
178
+ // 0. Create suspend_VDI
179
+ let suspendVdi
180
+ if (vmRecord.power_state === 'Suspended') {
181
+ const vdi = vdiRecords[vmRecord.suspend_VDI]
182
+ suspendVdi = await xapi.getRecord(
183
+ 'VDI',
184
+ await xapi.VDI_create({
185
+ ...vdi,
186
+ other_config: {
187
+ ...vdi.other_config,
188
+ [TAG_BASE_DELTA]: undefined,
189
+ [TAG_COPY_SRC]: vdi.uuid,
190
+ },
191
+ sr: mapVdisSrs[vdi.uuid] ?? sr.$ref,
192
+ })
193
+ )
194
+ $defer.onFailure(() => suspendVdi.$destroy())
195
+ }
196
+
197
+ // 1. Create the VM.
198
+ const vmRef = await xapi.VM_create(
199
+ {
200
+ ...vmRecord,
201
+ affinity: undefined,
202
+ blocked_operations: {
203
+ ...vmRecord.blocked_operations,
204
+ start: 'Importing…',
205
+ start_on: 'Importing…',
206
+ },
207
+ ha_always_run: false,
208
+ is_a_template: false,
209
+ name_label: '[Importing…] ' + vmRecord.name_label,
210
+ other_config: {
211
+ ...vmRecord.other_config,
212
+ [TAG_COPY_SRC]: vmRecord.uuid,
213
+ },
214
+ },
215
+ {
216
+ bios_strings: vmRecord.bios_strings,
217
+ generateMacSeed: newMacAddresses,
218
+ suspend_VDI: suspendVdi?.$ref,
219
+ }
220
+ )
221
+ $defer.onFailure.call(xapi, 'VM_destroy', vmRef)
222
+
223
+ // 2. Delete all VBDs which may have been created by the import.
224
+ await asyncMap(await xapi.getField('VM', vmRef, 'VBDs'), ref => ignoreErrors.call(xapi.call('VBD.destroy', ref)))
225
+
226
+ // 3. Create VDIs & VBDs.
227
+ const vbdRecords = deltaVm.vbds
228
+ const vbds = groupBy(vbdRecords, 'VDI')
229
+ const newVdis = {}
230
+ await asyncMap(Object.keys(vdiRecords), async vdiRef => {
231
+ const vdi = vdiRecords[vdiRef]
232
+ let newVdi
233
+
234
+ const remoteBaseVdiUuid = detectBase && vdi.other_config[TAG_BASE_DELTA]
235
+ if (remoteBaseVdiUuid) {
236
+ const baseVdi = find(baseVdis, vdi => vdi.other_config[TAG_COPY_SRC] === remoteBaseVdiUuid)
237
+ if (!baseVdi) {
238
+ throw new Error(`missing base VDI (copy of ${remoteBaseVdiUuid})`)
239
+ }
240
+
241
+ newVdi = await xapi.getRecord('VDI', await baseVdi.$clone())
242
+ $defer.onFailure(() => newVdi.$destroy())
243
+
244
+ await newVdi.update_other_config(TAG_COPY_SRC, vdi.uuid)
245
+ } else if (vdiRef === vmRecord.suspend_VDI) {
246
+ // suspendVDI has already created
247
+ newVdi = suspendVdi
248
+ } else {
249
+ newVdi = await xapi.getRecord(
250
+ 'VDI',
251
+ await xapi.VDI_create({
252
+ ...vdi,
253
+ other_config: {
254
+ ...vdi.other_config,
255
+ [TAG_BASE_DELTA]: undefined,
256
+ [TAG_COPY_SRC]: vdi.uuid,
257
+ },
258
+ SR: mapVdisSrs[vdi.uuid] ?? sr.$ref,
259
+ })
260
+ )
261
+ $defer.onFailure(() => newVdi.$destroy())
262
+ }
263
+
264
+ const vdiVbds = vbds[vdiRef]
265
+ if (vdiVbds !== undefined) {
266
+ await asyncMap(Object.values(vdiVbds), vbd =>
267
+ xapi.VBD_create({
268
+ ...vbd,
269
+ VDI: newVdi.$ref,
270
+ VM: vmRef,
271
+ })
272
+ )
273
+ }
274
+
275
+ newVdis[vdiRef] = newVdi
276
+ })
277
+
278
+ const networksByNameLabelByVlan = {}
279
+ let defaultNetwork
280
+ Object.values(xapi.objects.all).forEach(object => {
281
+ if (object.$type === 'network') {
282
+ const pif = object.$PIFs[0]
283
+ if (pif === undefined) {
284
+ // ignore network
285
+ return
286
+ }
287
+ const vlan = pif.VLAN
288
+ const networksByNameLabel = networksByNameLabelByVlan[vlan] || (networksByNameLabelByVlan[vlan] = {})
289
+ defaultNetwork = networksByNameLabel[object.name_label] = object
290
+ }
291
+ })
292
+
293
+ const { streams } = deltaVm
294
+
295
+ await Promise.all([
296
+ // Import VDI contents.
297
+ cancelableMap(cancelToken, Object.entries(newVdis), async (cancelToken, [id, vdi]) => {
298
+ for (let stream of ensureArray(streams[`${id}.vhd`])) {
299
+ if (typeof stream === 'function') {
300
+ stream = await stream()
301
+ }
302
+ if (stream.length === undefined) {
303
+ stream = await createVhdStreamWithLength(stream)
304
+ }
305
+ await vdi.$importContent(stream, { cancelToken, format: 'vhd' })
306
+ }
307
+ }),
308
+
309
+ // Create VIFs.
310
+ asyncMap(Object.values(deltaVm.vifs), vif => {
311
+ let network = vif.$network$uuid && xapi.getObjectByUuid(vif.$network$uuid, undefined)
312
+
313
+ if (network === undefined) {
314
+ const { $network$VLAN: vlan = -1 } = vif
315
+ const networksByNameLabel = networksByNameLabelByVlan[vlan]
316
+ if (networksByNameLabel !== undefined) {
317
+ network = networksByNameLabel[vif.$network$name_label]
318
+ if (network === undefined) {
319
+ network = networksByNameLabel[Object.keys(networksByNameLabel)[0]]
320
+ }
321
+ } else {
322
+ network = defaultNetwork
323
+ }
324
+ }
325
+
326
+ if (network) {
327
+ return xapi.VIF_create(
328
+ {
329
+ ...vif,
330
+ network: network.$ref,
331
+ VM: vmRef,
332
+ },
333
+ {
334
+ MAC: newMacAddresses ? undefined : vif.MAC,
335
+ }
336
+ )
337
+ }
338
+ }),
339
+ ])
340
+
341
+ await Promise.all([
342
+ deltaVm.vm.ha_always_run && xapi.setField('VM', vmRef, 'ha_always_run', true),
343
+ xapi.setField('VM', vmRef, 'name_label', deltaVm.vm.name_label),
344
+ ])
345
+
346
+ return vmRef
347
+ })
@@ -0,0 +1,29 @@
1
+ exports.extractIdsFromSimplePattern = function extractIdsFromSimplePattern(pattern) {
2
+ if (pattern === undefined) {
3
+ return []
4
+ }
5
+
6
+ if (pattern !== null && typeof pattern === 'object') {
7
+ let keys = Object.keys(pattern)
8
+
9
+ if (keys.length === 1 && keys[0] === 'id') {
10
+ pattern = pattern.id
11
+ if (typeof pattern === 'string') {
12
+ return [pattern]
13
+ }
14
+ if (pattern !== null && typeof pattern === 'object') {
15
+ keys = Object.keys(pattern)
16
+ if (
17
+ keys.length === 1 &&
18
+ keys[0] === '__or' &&
19
+ Array.isArray((pattern = pattern.__or)) &&
20
+ pattern.every(_ => typeof _ === 'string')
21
+ ) {
22
+ return pattern
23
+ }
24
+ }
25
+ }
26
+ }
27
+
28
+ throw new Error('invalid pattern')
29
+ }
@@ -0,0 +1,6 @@
1
+ const { utcFormat, utcParse } = require('d3-time-format')
2
+
3
+ // Format a date in ISO 8601 in a safe way to be used in filenames
4
+ // (even on Windows).
5
+ exports.formatFilenameDate = utcFormat('%Y%m%dT%H%M%SZ')
6
+ exports.parseFilenameDate = utcParse('%Y%m%dT%H%M%SZ')
@@ -0,0 +1,28 @@
1
+ const eos = require('end-of-stream')
2
+ const { PassThrough } = require('stream')
3
+
4
+ // create a new readable stream from an existing one which may be piped later
5
+ //
6
+ // in case of error in the new readable stream, it will simply be unpiped
7
+ // from the original one
8
+ exports.forkStreamUnpipe = function forkStreamUnpipe(stream) {
9
+ const { forks = 0 } = stream
10
+ stream.forks = forks + 1
11
+
12
+ const proxy = new PassThrough()
13
+ stream.pipe(proxy)
14
+ eos(stream, error => {
15
+ if (error !== undefined) {
16
+ proxy.destroy(error)
17
+ }
18
+ })
19
+ eos(proxy, _ => {
20
+ stream.forks--
21
+ stream.unpipe(proxy)
22
+
23
+ if (stream.forks === 0) {
24
+ stream.destroy(new Error('no more consumers for this stream'))
25
+ }
26
+ })
27
+ return proxy
28
+ }
@@ -0,0 +1,4 @@
1
+ // returns all entries but the last retention-th
2
+ exports.getOldEntries = function getOldEntries(retention, entries) {
3
+ return entries === undefined ? [] : retention > 0 ? entries.slice(0, -retention) : entries
4
+ }
package/_getTmpDir.js ADDED
@@ -0,0 +1,20 @@
1
+ const Disposable = require('promise-toolbox/Disposable.js')
2
+ const { join } = require('path')
3
+ const { mkdir, rmdir } = require('fs-extra')
4
+ const { tmpdir } = require('os')
5
+
6
+ const MAX_ATTEMPTS = 3
7
+
8
+ exports.getTmpDir = async function getTmpDir() {
9
+ for (let i = 0; true; ++i) {
10
+ const path = join(tmpdir(), Math.random().toString(36).slice(2))
11
+ try {
12
+ await mkdir(path)
13
+ return new Disposable(() => rmdir(path), path)
14
+ } catch (error) {
15
+ if (i === MAX_ATTEMPTS) {
16
+ throw error
17
+ }
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,6 @@
1
+ const BACKUP_DIR = 'xo-vm-backups'
2
+ exports.BACKUP_DIR = BACKUP_DIR
3
+
4
+ exports.getVmBackupDir = function getVmBackupDir(uuid) {
5
+ return `${BACKUP_DIR}/${uuid}`
6
+ }
package/_isValidXva.js ADDED
@@ -0,0 +1,60 @@
1
+ const assert = require('assert')
2
+
3
+ const isGzipFile = async (handler, fd) => {
4
+ // https://tools.ietf.org/html/rfc1952.html#page-5
5
+ const magicNumber = Buffer.allocUnsafe(2)
6
+
7
+ assert.strictEqual((await handler.read(fd, magicNumber, 0)).bytesRead, magicNumber.length)
8
+ return magicNumber[0] === 31 && magicNumber[1] === 139
9
+ }
10
+
11
+ // TODO: better check?
12
+ //
13
+ // our heuristic is not good enough, there has been some false positives
14
+ // (detected as invalid by us but valid by `tar` and imported with success),
15
+ // either THOUGH THEY MAY HAVE BEEN COMPRESSED FILES:
16
+ // - these files were normal but the check is incorrect
17
+ // - these files were invalid but without data loss
18
+ // - these files were invalid but with silent data loss
19
+ //
20
+ // maybe reading the end of the file looking for a file named
21
+ // /^Ref:\d+/\d+\.checksum$/ and then validating the tar structure from it
22
+ //
23
+ // https://github.com/npm/node-tar/issues/234#issuecomment-538190295
24
+ const isValidTar = async (handler, size, fd) => {
25
+ if (size <= 1024 || size % 512 !== 0) {
26
+ return false
27
+ }
28
+
29
+ const buf = Buffer.allocUnsafe(1024)
30
+ assert.strictEqual((await handler.read(fd, buf, size - buf.length)).bytesRead, buf.length)
31
+ return buf.every(_ => _ === 0)
32
+ }
33
+
34
+ // TODO: find an heuristic for compressed files
35
+ async function isValidXva(path) {
36
+ const handler = this._handler
37
+ try {
38
+ const fd = await handler.openFile(path, 'r')
39
+ try {
40
+ const size = await handler.getSize(fd)
41
+ if (size < 20) {
42
+ // neither a valid gzip not tar
43
+ return false
44
+ }
45
+
46
+ return (await isGzipFile(handler, fd))
47
+ ? true // gzip files cannot be validated at this time
48
+ : await isValidTar(handler, size, fd)
49
+ } finally {
50
+ handler.closeFile(fd).catch(noop)
51
+ }
52
+ } catch (error) {
53
+ // never throw, log and report as valid to avoid side effects
54
+ console.error('isValidXva', path, error)
55
+ return true
56
+ }
57
+ }
58
+ exports.isValidXva = isValidXva
59
+
60
+ const noop = Function.prototype
@@ -0,0 +1,52 @@
1
+ const fromCallback = require('promise-toolbox/fromCallback.js')
2
+ const { createLogger } = require('@xen-orchestra/log')
3
+ const { createParser } = require('parse-pairs')
4
+ const { execFile } = require('child_process')
5
+
6
+ const { debug } = createLogger('xo:backups:listPartitions')
7
+
8
+ const IGNORED_PARTITION_TYPES = {
9
+ // https://github.com/jhermsmeier/node-mbr/blob/master/lib/partition.js#L38
10
+ 0x05: true,
11
+ 0x0f: true,
12
+ 0x15: true,
13
+ 0x5e: true,
14
+ 0x5f: true,
15
+ 0x85: true,
16
+ 0x91: true,
17
+ 0x9b: true,
18
+ 0xc5: true,
19
+ 0xcf: true,
20
+ 0xd5: true,
21
+
22
+ 0x82: true, // swap
23
+ }
24
+
25
+ const LVM_PARTITION_TYPE = 0x8e
26
+ exports.LVM_PARTITION_TYPE = LVM_PARTITION_TYPE
27
+
28
+ const parsePartxLine = createParser({
29
+ keyTransform: key => (key === 'UUID' ? 'id' : key.toLowerCase()),
30
+ valueTransform: (value, key) => (key === 'start' || key === 'size' || key === 'type' ? +value : value),
31
+ })
32
+
33
+ // returns an empty array in case of a non-partitioned disk
34
+ exports.listPartitions = async function listPartitions(devicePath) {
35
+ const parts = await fromCallback(execFile, 'partx', [
36
+ '--bytes',
37
+ '--output=NR,START,SIZE,NAME,UUID,TYPE',
38
+ '--pairs',
39
+ devicePath,
40
+ ]).catch(error => {
41
+ // partx returns 1 since v2.33 when failing to read partitions.
42
+ //
43
+ // Prior versions are correctly handled by the nominal case.
44
+ debug('listPartitions', { error })
45
+ return ''
46
+ })
47
+
48
+ return parts
49
+ .split(/\r?\n/)
50
+ .map(parsePartxLine)
51
+ .filter(({ type }) => type != null && !(type in IGNORED_PARTITION_TYPES))
52
+ }
package/_lvm.js ADDED
@@ -0,0 +1,31 @@
1
+ const fromCallback = require('promise-toolbox/fromCallback.js')
2
+ const { createParser } = require('parse-pairs')
3
+ const { execFile } = require('child_process')
4
+
5
+ // ===================================================================
6
+
7
+ const parse = createParser({
8
+ keyTransform: key => key.slice(5).toLowerCase(),
9
+ })
10
+ const makeFunction =
11
+ command =>
12
+ async (fields, ...args) => {
13
+ const info = await fromCallback(execFile, command, [
14
+ '--noheading',
15
+ '--nosuffix',
16
+ '--nameprefixes',
17
+ '--unbuffered',
18
+ '--units',
19
+ 'b',
20
+ '-o',
21
+ String(fields),
22
+ ...args,
23
+ ])
24
+ return info
25
+ .trim()
26
+ .split(/\r?\n/)
27
+ .map(Array.isArray(fields) ? parse : line => parse(line)[fields])
28
+ }
29
+
30
+ exports.lvs = makeFunction('lvs')
31
+ exports.pvs = makeFunction('pvs')
@@ -0,0 +1,7 @@
1
+ exports.watchStreamSize = function watchStreamSize(stream, container = { size: 0 }) {
2
+ stream.on('data', data => {
3
+ container.size += data.length
4
+ })
5
+ stream.pause()
6
+ return container
7
+ }
@@ -0,0 +1,34 @@
1
+ const mapValues = require('lodash/mapValues.js')
2
+ const { dirname } = require('path')
3
+
4
+ function formatVmBackup(backup) {
5
+ return {
6
+ disks:
7
+ backup.vhds === undefined
8
+ ? []
9
+ : Object.keys(backup.vhds).map(vdiId => {
10
+ const vdi = backup.vdis[vdiId]
11
+ return {
12
+ id: `${dirname(backup._filename)}/${backup.vhds[vdiId]}`,
13
+ name: vdi.name_label,
14
+ uuid: vdi.uuid,
15
+ }
16
+ }),
17
+
18
+ id: backup.id,
19
+ jobId: backup.jobId,
20
+ mode: backup.mode,
21
+ scheduleId: backup.scheduleId,
22
+ size: backup.size,
23
+ timestamp: backup.timestamp,
24
+ vm: {
25
+ name_description: backup.vm.name_description,
26
+ name_label: backup.vm.name_label,
27
+ },
28
+ }
29
+ }
30
+
31
+ // format all backups as returned by RemoteAdapter#listAllVmBackups()
32
+ exports.formatVmBackups = function formatVmBackups(backupsByVM) {
33
+ return mapValues(backupsByVM, backups => backups.map(formatVmBackup))
34
+ }