@xen-orchestra/backups 0.17.0 → 0.18.2

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 } = {}) {
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
 
@@ -202,7 +221,7 @@ exports.cleanVm = async function cleanVm(
202
221
  await Promise.all(deletions)
203
222
  }
204
223
 
205
- const jsons = []
224
+ const jsons = new Set()
206
225
  const xvas = new Set()
207
226
  const xvaSums = []
208
227
  const entries = await handler.list(vmDir, {
@@ -210,7 +229,7 @@ exports.cleanVm = async function cleanVm(
210
229
  })
211
230
  entries.forEach(path => {
212
231
  if (isMetadataFile(path)) {
213
- jsons.push(path)
232
+ jsons.add(path)
214
233
  } else if (isXvaFile(path)) {
215
234
  xvas.add(path)
216
235
  } else if (isXvaSumFile(path)) {
@@ -232,22 +251,25 @@ exports.cleanVm = async function cleanVm(
232
251
  // compile the list of unused XVAs and VHDs, and remove backup metadata which
233
252
  // reference a missing XVA/VHD
234
253
  await asyncMap(jsons, async json => {
235
- const metadata = JSON.parse(await handler.readFile(json))
254
+ let metadata
255
+ try {
256
+ metadata = JSON.parse(await handler.readFile(json))
257
+ } catch (error) {
258
+ onLog(`failed to read metadata file ${json}`, { error })
259
+ jsons.delete(json)
260
+ return
261
+ }
262
+
236
263
  const { mode } = metadata
237
- let size
238
264
  if (mode === 'full') {
239
265
  const linkedXva = resolve('/', vmDir, metadata.xva)
240
-
241
266
  if (xvas.has(linkedXva)) {
242
267
  unusedXvas.delete(linkedXva)
243
-
244
- size = await handler.getSize(linkedXva).catch(error => {
245
- onLog(`failed to get size of ${json}`, { error })
246
- })
247
268
  } else {
248
269
  onLog(`the XVA linked to the metadata ${json} is missing`)
249
270
  if (remove) {
250
271
  onLog(`deleting incomplete backup ${json}`)
272
+ jsons.delete(json)
251
273
  await handler.unlink(json)
252
274
  }
253
275
  }
@@ -263,46 +285,18 @@ exports.cleanVm = async function cleanVm(
263
285
  // possible (existing disks) even if one disk is missing
264
286
  if (missingVhds.length === 0) {
265
287
  linkedVhds.forEach(_ => unusedVhds.delete(_))
266
-
267
- // checking the size of a vhd directory is costly
268
- // 1 Http Query per 1000 blocks
269
- // we only check size of all the vhd are VhdFiles
270
-
271
- const shouldComputeSize = linkedVhds.every(vhd => vhd instanceof VhdFile)
272
- if (shouldComputeSize) {
273
- try {
274
- await Disposable.use(Disposable.all(linkedVhds.map(vhdPath => openVhd(handler, vhdPath))), async vhds => {
275
- const sizes = await asyncMap(vhds, vhd => vhd.getSize())
276
- size = sum(sizes)
277
- })
278
- } catch (error) {
279
- onLog(`failed to get size of ${json}`, { error })
280
- }
281
- }
288
+ linkedVhds.forEach(path => {
289
+ vhdsToJSons[path] = json
290
+ })
282
291
  } else {
283
292
  onLog(`Some VHDs linked to the metadata ${json} are missing`, { missingVhds })
284
293
  if (remove) {
285
294
  onLog(`deleting incomplete backup ${json}`)
295
+ jsons.delete(json)
286
296
  await handler.unlink(json)
287
297
  }
288
298
  }
289
299
  }
290
-
291
- const metadataSize = metadata.size
292
- if (size !== undefined && metadataSize !== size) {
293
- onLog(`incorrect size in metadata: ${metadataSize ?? 'none'} instead of ${size}`)
294
-
295
- // don't update if the the stored size is greater than found files,
296
- // it can indicates a problem
297
- if (fixMetadata && (metadataSize === undefined || metadataSize < size)) {
298
- try {
299
- metadata.size = size
300
- await handler.writeFile(json, JSON.stringify(metadata), { flags: 'w' })
301
- } catch (error) {
302
- onLog(`failed to update size in backup metadata ${json}`, { error })
303
- }
304
- }
305
- }
306
300
  })
307
301
 
308
302
  // TODO: parallelize by vm/job/vdi
@@ -361,9 +355,15 @@ exports.cleanVm = async function cleanVm(
361
355
  })
362
356
  }
363
357
 
364
- const doMerge = () => {
365
- const promise = asyncMap(toMerge, async chain => limitedMergeVhdChain(chain, { handler, onLog, remove, merge }))
366
- 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
+ })
367
367
  }
368
368
 
369
369
  await Promise.all([
@@ -388,6 +388,52 @@ exports.cleanVm = async function cleanVm(
388
388
  }),
389
389
  ])
390
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
+ // the size is not computed in some cases (e.g. VhdDirectory)
412
+ if (fileSystemSize === undefined) {
413
+ return
414
+ }
415
+
416
+ // don't warn if the size has changed after a merge
417
+ if (!merged && fileSystemSize !== size) {
418
+ onLog(`incorrect size in metadata: ${size ?? 'none'} instead of ${fileSystemSize}`)
419
+ }
420
+ }
421
+ } catch (error) {
422
+ onLog(`failed to get size of ${metadataPath}`, { error })
423
+ return
424
+ }
425
+
426
+ // systematically update size after a merge
427
+ if ((merged || fixMetadata) && size !== fileSystemSize) {
428
+ metadata.size = fileSystemSize
429
+ try {
430
+ await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
431
+ } catch (error) {
432
+ onLog(`failed to update size in backup metadata ${metadataPath} after merge`, { error })
433
+ }
434
+ }
435
+ })
436
+
391
437
  return {
392
438
  // boolean whether some VHDs were merged (or should be merged)
393
439
  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.0",
11
+ "version": "0.18.2",
12
12
  "engines": {
13
13
  "node": ">=14.6"
14
14
  },
@@ -20,7 +20,7 @@
20
20
  "@vates/disposable": "^0.1.1",
21
21
  "@vates/parse-duration": "^0.1.1",
22
22
  "@xen-orchestra/async-map": "^0.1.2",
23
- "@xen-orchestra/fs": "^0.19.2",
23
+ "@xen-orchestra/fs": "^0.19.3",
24
24
  "@xen-orchestra/log": "^0.3.0",
25
25
  "@xen-orchestra/template": "^0.1.0",
26
26
  "compare-versions": "^4.0.1",
@@ -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))