@xen-orchestra/backups 0.17.1 → 0.18.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/RemoteAdapter.js CHANGED
@@ -6,7 +6,7 @@ const pDefer = require('promise-toolbox/defer.js')
6
6
  const groupBy = require('lodash/groupBy.js')
7
7
  const { dirname, join, normalize, resolve } = require('path')
8
8
  const { createLogger } = require('@xen-orchestra/log')
9
- const { Constants, createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdSynthetic } = require('vhd-lib')
9
+ const { Constants, createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } = require('vhd-lib')
10
10
  const { deduped } = require('@vates/disposable/deduped.js')
11
11
  const { execFile } = require('child_process')
12
12
  const { readdir, stat } = require('fs-extra')
@@ -68,10 +68,11 @@ const debounceResourceFactory = factory =>
68
68
  }
69
69
 
70
70
  class RemoteAdapter {
71
- constructor(handler, { debounceResource = res => res, dirMode } = {}) {
71
+ constructor(handler, { debounceResource = res => res, dirMode, vhdDirectoryCompression = 'brotli' } = {}) {
72
72
  this._debounceResource = debounceResource
73
73
  this._dirMode = dirMode
74
74
  this._handler = handler
75
+ this._vhdDirectoryCompression = vhdDirectoryCompression
75
76
  }
76
77
 
77
78
  get handler() {
@@ -191,6 +192,22 @@ class RemoteAdapter {
191
192
  return files
192
193
  }
193
194
 
195
+ // check if we will be allowed to merge a a vhd created in this adapter
196
+ // with the vhd at path `path`
197
+ async isMergeableParent(packedParentUid, path) {
198
+ return await Disposable.use(openVhd(this.handler, path), vhd => {
199
+ // this baseUuid is not linked with this vhd
200
+ if (!vhd.footer.uuid.equals(packedParentUid)) {
201
+ return false
202
+ }
203
+
204
+ const isVhdDirectory = vhd instanceof VhdDirectory
205
+ return isVhdDirectory
206
+ ? this.#useVhdDirectory && this.#getCompressionType() === vhd.compressionType
207
+ : !this.#useVhdDirectory
208
+ })
209
+ }
210
+
194
211
  fetchPartitionFiles(diskId, partitionId, paths) {
195
212
  const { promise, reject, resolve } = pDefer()
196
213
  Disposable.use(
@@ -262,6 +279,18 @@ class RemoteAdapter {
262
279
  ])
263
280
  }
264
281
 
282
+ #getCompressionType() {
283
+ return this._vhdDirectoryCompression
284
+ }
285
+
286
+ #useVhdDirectory() {
287
+ return this.handler.type === 's3'
288
+ }
289
+
290
+ #useAlias() {
291
+ return this.#useVhdDirectory()
292
+ }
293
+
265
294
  getDisk = Disposable.factory(this.getDisk)
266
295
  getDisk = deduped(this.getDisk, diskId => [diskId])
267
296
  getDisk = debounceResourceFactory(this.getDisk)
@@ -318,13 +347,10 @@ class RemoteAdapter {
318
347
  return yield this._getPartition(devicePath, await this._findPartition(devicePath, partitionId))
319
348
  }
320
349
 
321
- // this function will be the one where we plug the logic of the storage format by fs type/user settings
322
-
323
- // if the file is named .vhd => vhd
324
- // if the file is named alias.vhd => alias to a vhd
350
+ // if we use alias on this remote, we have to name the file alias.vhd
325
351
  getVhdFileName(baseName) {
326
- if (this._handler.type === 's3') {
327
- return `${baseName}.alias.vhd` // we want an alias to a vhddirectory
352
+ if (this.#useAlias()) {
353
+ return `${baseName}.alias.vhd`
328
354
  }
329
355
  return `${baseName}.vhd`
330
356
  }
@@ -476,10 +502,11 @@ class RemoteAdapter {
476
502
  async writeVhd(path, input, { checksum = true, validator = noop } = {}) {
477
503
  const handler = this._handler
478
504
 
479
- if (path.endsWith('.alias.vhd')) {
505
+ if (this.#useVhdDirectory()) {
480
506
  const dataPath = `${dirname(path)}/data/${uuidv4()}.vhd`
481
507
  await createVhdDirectoryFromStream(handler, dataPath, input, {
482
508
  concurrency: 16,
509
+ compression: this.#getCompressionType(),
483
510
  async validator() {
484
511
  await input.task
485
512
  return validator.apply(this, arguments)
package/_backupWorker.js CHANGED
@@ -70,6 +70,7 @@ class BackupWorker {
70
70
  yield new RemoteAdapter(handler, {
71
71
  debounceResource: this.debounceResource,
72
72
  dirMode: this.#config.dirMode,
73
+ vhdDirectoryCompression: this.#config.vhdDirectoryCompression,
73
74
  })
74
75
  } finally {
75
76
  await handler.forget()
package/_cleanVm.js CHANGED
@@ -10,6 +10,24 @@ const { limitConcurrency } = require('limit-concurrency-decorator')
10
10
  const { Task } = require('./Task.js')
11
11
  const { Disposable } = require('promise-toolbox')
12
12
 
13
+ // checking the size of a vhd directory is costly
14
+ // 1 Http Query per 1000 blocks
15
+ // we only check size of all the vhd are VhdFiles
16
+ function shouldComputeVhdsSize(vhds) {
17
+ return vhds.every(vhd => vhd instanceof VhdFile)
18
+ }
19
+
20
+ const computeVhdsSize = (handler, vhdPaths) =>
21
+ Disposable.use(
22
+ vhdPaths.map(vhdPath => openVhd(handler, vhdPath)),
23
+ async vhds => {
24
+ if (shouldComputeVhdsSize(vhds)) {
25
+ const sizes = await asyncMap(vhds, vhd => vhd.getSize())
26
+ return sum(sizes)
27
+ }
28
+ }
29
+ )
30
+
13
31
  // chain is an array of VHDs from child to parent
14
32
  //
15
33
  // the whole chain will be merged into parent, parent will be renamed to child
@@ -130,6 +148,7 @@ exports.cleanVm = async function cleanVm(
130
148
  const handler = this._handler
131
149
 
132
150
  const vhds = new Set()
151
+ const vhdsToJSons = new Set()
133
152
  const vhdParents = { __proto__: null }
134
153
  const vhdChildren = { __proto__: null }
135
154
 
@@ -242,16 +261,10 @@ exports.cleanVm = async function cleanVm(
242
261
  }
243
262
 
244
263
  const { mode } = metadata
245
- let size
246
264
  if (mode === 'full') {
247
265
  const linkedXva = resolve('/', vmDir, metadata.xva)
248
-
249
266
  if (xvas.has(linkedXva)) {
250
267
  unusedXvas.delete(linkedXva)
251
-
252
- size = await handler.getSize(linkedXva).catch(error => {
253
- onLog(`failed to get size of ${json}`, { error })
254
- })
255
268
  } else {
256
269
  onLog(`the XVA linked to the metadata ${json} is missing`)
257
270
  if (remove) {
@@ -272,22 +285,9 @@ exports.cleanVm = async function cleanVm(
272
285
  // possible (existing disks) even if one disk is missing
273
286
  if (missingVhds.length === 0) {
274
287
  linkedVhds.forEach(_ => unusedVhds.delete(_))
275
-
276
- // checking the size of a vhd directory is costly
277
- // 1 Http Query per 1000 blocks
278
- // we only check size of all the vhd are VhdFiles
279
-
280
- const shouldComputeSize = linkedVhds.every(vhd => vhd instanceof VhdFile)
281
- if (shouldComputeSize) {
282
- try {
283
- await Disposable.use(Disposable.all(linkedVhds.map(vhdPath => openVhd(handler, vhdPath))), async vhds => {
284
- const sizes = await asyncMap(vhds, vhd => vhd.getSize())
285
- size = sum(sizes)
286
- })
287
- } catch (error) {
288
- onLog(`failed to get size of ${json}`, { error })
289
- }
290
- }
288
+ linkedVhds.forEach(path => {
289
+ vhdsToJSons[path] = json
290
+ })
291
291
  } else {
292
292
  onLog(`Some VHDs linked to the metadata ${json} are missing`, { missingVhds })
293
293
  if (remove) {
@@ -297,22 +297,6 @@ exports.cleanVm = async function cleanVm(
297
297
  }
298
298
  }
299
299
  }
300
-
301
- const metadataSize = metadata.size
302
- if (size !== undefined && metadataSize !== size) {
303
- onLog(`incorrect size in metadata: ${metadataSize ?? 'none'} instead of ${size}`)
304
-
305
- // don't update if the the stored size is greater than found files,
306
- // it can indicates a problem
307
- if (fixMetadata && (metadataSize === undefined || metadataSize < size)) {
308
- try {
309
- metadata.size = size
310
- await handler.writeFile(json, JSON.stringify(metadata), { flags: 'w' })
311
- } catch (error) {
312
- onLog(`failed to update size in backup metadata ${json}`, { error })
313
- }
314
- }
315
- }
316
300
  })
317
301
 
318
302
  // TODO: parallelize by vm/job/vdi
@@ -371,9 +355,15 @@ exports.cleanVm = async function cleanVm(
371
355
  })
372
356
  }
373
357
 
374
- const doMerge = () => {
375
- const promise = asyncMap(toMerge, async chain => limitedMergeVhdChain(chain, { handler, onLog, remove, merge }))
376
- return merge ? promise.then(sizes => ({ size: sum(sizes) })) : promise
358
+ const metadataWithMergedVhd = {}
359
+ const doMerge = async () => {
360
+ await asyncMap(toMerge, async chain => {
361
+ const merged = await limitedMergeVhdChain(chain, { handler, onLog, remove, merge })
362
+ if (merged !== undefined) {
363
+ const metadataPath = vhdsToJSons[chain[0]] // all the chain should have the same metada file
364
+ metadataWithMergedVhd[metadataPath] = true
365
+ }
366
+ })
377
367
  }
378
368
 
379
369
  await Promise.all([
@@ -398,6 +388,47 @@ exports.cleanVm = async function cleanVm(
398
388
  }),
399
389
  ])
400
390
 
391
+ // update size for delta metadata with merged VHD
392
+ // check for the other that the size is the same as the real file size
393
+
394
+ await asyncMap(jsons, async metadataPath => {
395
+ const metadata = JSON.parse(await handler.readFile(metadataPath))
396
+
397
+ let fileSystemSize
398
+ const merged = metadataWithMergedVhd[metadataPath] !== undefined
399
+
400
+ const { mode, size, vhds, xva } = metadata
401
+
402
+ try {
403
+ if (mode === 'full') {
404
+ // a full backup : check size
405
+ const linkedXva = resolve('/', vmDir, xva)
406
+ fileSystemSize = await handler.getSize(linkedXva)
407
+ } else if (mode === 'delta') {
408
+ const linkedVhds = Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
409
+ fileSystemSize = await computeVhdsSize(handler, linkedVhds)
410
+
411
+ // don't warn if the size has changed after a merge
412
+ if (!merged && fileSystemSize !== size) {
413
+ onLog(`incorrect size in metadata: ${size ?? 'none'} instead of ${fileSystemSize}`)
414
+ }
415
+ }
416
+ } catch (error) {
417
+ onLog(`failed to get size of ${metadataPath}`, { error })
418
+ return
419
+ }
420
+
421
+ // systematically update size after a merge
422
+ if ((merged || fixMetadata) && size !== fileSystemSize) {
423
+ metadata.size = fileSystemSize
424
+ try {
425
+ await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
426
+ } catch (error) {
427
+ onLog(`failed to update size in backup metadata ${metadataPath} after merge`, { error })
428
+ }
429
+ }
430
+ })
431
+
401
432
  return {
402
433
  // boolean whether some VHDs were merged (or should be merged)
403
434
  merge: toMerge.length !== 0,
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/vatesfr/xen-orchestra.git"
10
10
  },
11
- "version": "0.17.1",
11
+ "version": "0.18.0",
12
12
  "engines": {
13
13
  "node": ">=14.6"
14
14
  },
@@ -36,11 +36,11 @@
36
36
  "proper-lockfile": "^4.1.2",
37
37
  "pump": "^3.0.0",
38
38
  "uuid": "^8.3.2",
39
- "vhd-lib": "^2.0.4",
39
+ "vhd-lib": "^2.1.0",
40
40
  "yazl": "^2.5.1"
41
41
  },
42
42
  "peerDependencies": {
43
- "@xen-orchestra/xapi": "^0.8.4"
43
+ "@xen-orchestra/xapi": "^0.8.5"
44
44
  },
45
45
  "license": "AGPL-3.0-or-later",
46
46
  "author": {
@@ -24,6 +24,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
24
24
  async checkBaseVdis(baseUuidToSrcVdi) {
25
25
  const { handler } = this._adapter
26
26
  const backup = this._backup
27
+ const adapter = this._adapter
27
28
 
28
29
  const backupDir = getVmBackupDir(backup.vm.uuid)
29
30
  const vdisDir = `${backupDir}/vdis/${backup.job.id}`
@@ -35,13 +36,11 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
35
36
  filter: _ => _[0] !== '.' && _.endsWith('.vhd'),
36
37
  prependDir: true,
37
38
  })
39
+ const packedBaseUuid = packUuid(baseUuid)
38
40
  await asyncMap(vhds, async path => {
39
41
  try {
40
42
  await checkVhdChain(handler, path)
41
- await Disposable.use(
42
- openVhd(handler, path),
43
- vhd => (found = found || vhd.footer.uuid.equals(packUuid(baseUuid)))
44
- )
43
+ found = found || (await adapter.isMergeableParent(packedBaseUuid, path))
45
44
  } catch (error) {
46
45
  warn('checkBaseVdis', { error })
47
46
  await ignoreErrors.call(VhdAbstract.unlink(handler, path))