@xen-orchestra/backups 0.44.2 → 0.44.4

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.
@@ -191,7 +191,7 @@ export class ImportVmBackup {
191
191
 
192
192
  async #decorateIncrementalVmMetadata() {
193
193
  const { additionnalVmTag, mapVdisSrs, useDifferentialRestore } = this._importIncrementalVmSettings
194
-
194
+
195
195
  const ignoredVdis = new Set(
196
196
  Object.entries(mapVdisSrs)
197
197
  .filter(([_, srUuid]) => srUuid === null)
@@ -21,7 +21,7 @@ export class RestoreMetadataBackup {
21
21
  })
22
22
  } else {
23
23
  const metadata = JSON.parse(await handler.readFile(join(backupId, 'metadata.json')))
24
- const dataFileName = resolve(backupId, metadata.data ?? 'data.json')
24
+ const dataFileName = resolve('/', backupId, metadata.data ?? 'data.json').slice(1)
25
25
  const data = await handler.readFile(dataFileName)
26
26
 
27
27
  // if data is JSON, sent it as a plain string, otherwise, consider the data as binary and encode it
package/_cleanVm.mjs CHANGED
@@ -36,34 +36,32 @@ const computeVhdsSize = (handler, vhdPaths) =>
36
36
  )
37
37
 
38
38
  // chain is [ ancestor, child_1, ..., child_n ]
39
- async function _mergeVhdChain(handler, chain, { logInfo, remove, merge, mergeBlockConcurrency }) {
40
- if (merge) {
41
- logInfo(`merging VHD chain`, { chain })
42
-
43
- let done, total
44
- const handle = setInterval(() => {
45
- if (done !== undefined) {
46
- logInfo('merge in progress', {
47
- done,
48
- parent: chain[0],
49
- progress: Math.round((100 * done) / total),
50
- total,
51
- })
52
- }
53
- }, 10e3)
54
- try {
55
- return await mergeVhdChain(handler, chain, {
56
- logInfo,
57
- mergeBlockConcurrency,
58
- onProgress({ done: d, total: t }) {
59
- done = d
60
- total = t
61
- },
62
- removeUnused: remove,
39
+ async function _mergeVhdChain(handler, chain, { logInfo, remove, mergeBlockConcurrency }) {
40
+ logInfo(`merging VHD chain`, { chain })
41
+
42
+ let done, total
43
+ const handle = setInterval(() => {
44
+ if (done !== undefined) {
45
+ logInfo('merge in progress', {
46
+ done,
47
+ parent: chain[0],
48
+ progress: Math.round((100 * done) / total),
49
+ total,
63
50
  })
64
- } finally {
65
- clearInterval(handle)
66
51
  }
52
+ }, 10e3)
53
+ try {
54
+ return await mergeVhdChain(handler, chain, {
55
+ logInfo,
56
+ mergeBlockConcurrency,
57
+ onProgress({ done: d, total: t }) {
58
+ done = d
59
+ total = t
60
+ },
61
+ removeUnused: remove,
62
+ })
63
+ } finally {
64
+ clearInterval(handle)
67
65
  }
68
66
  }
69
67
 
@@ -471,23 +469,20 @@ export async function cleanVm(
471
469
  const metadataWithMergedVhd = {}
472
470
  const doMerge = async () => {
473
471
  await asyncMap(toMerge, async chain => {
474
- const merged = await limitedMergeVhdChain(handler, chain, {
472
+ const { finalVhdSize } = await limitedMergeVhdChain(handler, chain, {
475
473
  logInfo,
476
474
  logWarn,
477
475
  remove,
478
- merge,
479
476
  mergeBlockConcurrency,
480
477
  })
481
- if (merged !== undefined) {
482
- const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metada file
483
- metadataWithMergedVhd[metadataPath] = true
484
- }
478
+ const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metada file
479
+ metadataWithMergedVhd[metadataPath] = (metadataWithMergedVhd[metadataPath] ?? 0) + finalVhdSize
485
480
  })
486
481
  }
487
482
 
488
483
  await Promise.all([
489
484
  ...unusedVhdsDeletion,
490
- toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : doMerge()),
485
+ toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : () => Promise.resolve()),
491
486
  asyncMap(unusedXvas, path => {
492
487
  logWarn('unused XVA', { path })
493
488
  if (remove) {
@@ -509,12 +504,11 @@ export async function cleanVm(
509
504
 
510
505
  // update size for delta metadata with merged VHD
511
506
  // check for the other that the size is the same as the real file size
512
-
513
507
  await asyncMap(jsons, async metadataPath => {
514
508
  const metadata = backups.get(metadataPath)
515
509
 
516
510
  let fileSystemSize
517
- const merged = metadataWithMergedVhd[metadataPath] !== undefined
511
+ const mergedSize = metadataWithMergedVhd[metadataPath]
518
512
 
519
513
  const { mode, size, vhds, xva } = metadata
520
514
 
@@ -524,26 +518,29 @@ export async function cleanVm(
524
518
  const linkedXva = resolve('/', vmDir, xva)
525
519
  try {
526
520
  fileSystemSize = await handler.getSize(linkedXva)
521
+ if (fileSystemSize !== size && fileSystemSize !== undefined) {
522
+ logWarn('cleanVm: incorrect backup size in metadata', {
523
+ path: metadataPath,
524
+ actual: size ?? 'none',
525
+ expected: fileSystemSize,
526
+ })
527
+ }
527
528
  } catch (error) {
528
529
  // can fail with encrypted remote
529
530
  }
530
531
  } else if (mode === 'delta') {
531
- const linkedVhds = Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
532
- fileSystemSize = await computeVhdsSize(handler, linkedVhds)
533
-
534
- // the size is not computed in some cases (e.g. VhdDirectory)
535
- if (fileSystemSize === undefined) {
536
- return
537
- }
538
-
539
532
  // don't warn if the size has changed after a merge
540
- if (!merged && fileSystemSize !== size) {
541
- // FIXME: figure out why it occurs so often and, once fixed, log the real problems with `logWarn`
542
- console.warn('cleanVm: incorrect backup size in metadata', {
543
- path: metadataPath,
544
- actual: size ?? 'none',
545
- expected: fileSystemSize,
546
- })
533
+ if (mergedSize === undefined) {
534
+ const linkedVhds = Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
535
+ fileSystemSize = await computeVhdsSize(handler, linkedVhds)
536
+ // the size is not computed in some cases (e.g. VhdDirectory)
537
+ if (fileSystemSize !== undefined && fileSystemSize !== size) {
538
+ logWarn('cleanVm: incorrect backup size in metadata', {
539
+ path: metadataPath,
540
+ actual: size ?? 'none',
541
+ expected: fileSystemSize,
542
+ })
543
+ }
547
544
  }
548
545
  }
549
546
  } catch (error) {
@@ -551,9 +548,19 @@ export async function cleanVm(
551
548
  return
552
549
  }
553
550
 
554
- // systematically update size after a merge
555
- if ((merged || fixMetadata) && size !== fileSystemSize) {
556
- metadata.size = fileSystemSize
551
+ // systematically update size and differentials after a merge
552
+
553
+ // @todo : after 2024-04-01 remove the fixmetadata options since the size computation is fixed
554
+ if (mergedSize || (fixMetadata && fileSystemSize !== size)) {
555
+ metadata.size = mergedSize ?? fileSystemSize ?? size
556
+
557
+ if (mergedSize) {
558
+ // all disks are now key disk
559
+ metadata.isVhdDifferencing = {}
560
+ for (const id of Object.values(metadata.vdis ?? {})) {
561
+ metadata.isVhdDifferencing[`${id}.vhd`] = false
562
+ }
563
+ }
557
564
  mustRegenerateCache = true
558
565
  try {
559
566
  await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
@@ -34,6 +34,7 @@ export async function exportIncrementalVm(
34
34
  fullVdisRequired = new Set(),
35
35
 
36
36
  disableBaseTags = false,
37
+ nbdConcurrency = 1,
37
38
  preferNbd,
38
39
  } = {}
39
40
  ) {
@@ -82,6 +83,7 @@ export async function exportIncrementalVm(
82
83
  baseRef: baseVdi?.$ref,
83
84
  cancelToken,
84
85
  format: 'vhd',
86
+ nbdConcurrency,
85
87
  preferNbd,
86
88
  })
87
89
  })
@@ -32,10 +32,10 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
32
32
  useChain: false,
33
33
  })
34
34
 
35
- const differentialVhds = {}
35
+ const isVhdDifferencing = {}
36
36
 
37
37
  await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
38
- differentialVhds[key] = await isVhdDifferencingDisk(stream)
38
+ isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
39
39
  })
40
40
 
41
41
  incrementalExport.streams = mapValues(incrementalExport.streams, this._throttleStream)
@@ -43,7 +43,7 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
43
43
  writer =>
44
44
  writer.transfer({
45
45
  deltaExport: forkDeltaExport(incrementalExport),
46
- differentialVhds,
46
+ isVhdDifferencing,
47
47
  timestamp: metadata.timestamp,
48
48
  vm: metadata.vm,
49
49
  vmSnapshot: metadata.vmSnapshot,
@@ -41,6 +41,7 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
41
41
 
42
42
  const deltaExport = await exportIncrementalVm(exportedVm, baseVm, {
43
43
  fullVdisRequired,
44
+ nbdConcurrency: this._settings.nbdConcurrency,
44
45
  preferNbd: this._settings.preferNbd,
45
46
  })
46
47
  // since NBD is network based, if one disk use nbd , all the disk use them
@@ -49,11 +50,11 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
49
50
  Task.info('Transfer data using NBD')
50
51
  }
51
52
 
52
- const differentialVhds = {}
53
+ const isVhdDifferencing = {}
53
54
  // since isVhdDifferencingDisk is reading and unshifting data in stream
54
55
  // it should be done BEFORE any other stream transform
55
56
  await asyncEach(Object.entries(deltaExport.streams), async ([key, stream]) => {
56
- differentialVhds[key] = await isVhdDifferencingDisk(stream)
57
+ isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
57
58
  })
58
59
  const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
59
60
 
@@ -68,7 +69,7 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
68
69
  writer =>
69
70
  writer.transfer({
70
71
  deltaExport: forkDeltaExport(deltaExport),
71
- differentialVhds,
72
+ isVhdDifferencing,
72
73
  sizeContainers,
73
74
  timestamp,
74
75
  vm,
@@ -133,7 +133,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
133
133
  }
134
134
  }
135
135
 
136
- async _transfer($defer, { differentialVhds, timestamp, deltaExport, vm, vmSnapshot }) {
136
+ async _transfer($defer, { isVhdDifferencing, timestamp, deltaExport, vm, vmSnapshot }) {
137
137
  const adapter = this._adapter
138
138
  const job = this._job
139
139
  const scheduleId = this._scheduleId
@@ -161,6 +161,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
161
161
  )
162
162
 
163
163
  metadataContent = {
164
+ isVhdDifferencing,
164
165
  jobId,
165
166
  mode: job.mode,
166
167
  scheduleId,
@@ -180,9 +181,9 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
180
181
  async ([id, vdi]) => {
181
182
  const path = `${this._vmBackupDir}/${vhds[id]}`
182
183
 
183
- const isDelta = differentialVhds[`${id}.vhd`]
184
+ const isDifferencing = isVhdDifferencing[`${id}.vhd`]
184
185
  let parentPath
185
- if (isDelta) {
186
+ if (isDifferencing) {
186
187
  const vdiDir = dirname(path)
187
188
  parentPath = (
188
189
  await handler.list(vdiDir, {
@@ -205,15 +206,19 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
205
206
  await checkVhd(handler, parentPath)
206
207
  }
207
208
 
208
- transferSize += await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
209
+ // don't write it as transferSize += await async function
210
+ // since i += await asyncFun lead to race condition
211
+ // as explained : https://eslint.org/docs/latest/rules/require-atomic-updates
212
+ const transferSizeOneDisk = await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
209
213
  // no checksum for VHDs, because they will be invalidated by
210
214
  // merges and chainings
211
215
  checksum: false,
212
216
  validator: tmpPath => checkVhd(handler, tmpPath),
213
217
  writeBlockConcurrency: this._config.writeBlockConcurrency,
214
218
  })
219
+ transferSize += transferSizeOneDisk
215
220
 
216
- if (isDelta) {
221
+ if (isDifferencing) {
217
222
  await chainVhd(handler, parentPath, handler, path)
218
223
  }
219
224
 
@@ -58,7 +58,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
58
58
  )
59
59
  }
60
60
  const healthCheckVm = xapi.getObject(healthCheckVmRef) ?? (await xapi.waitObject(healthCheckVmRef))
61
- await healthCheckVm.add_tag('xo:no-bak=Health Check')
61
+ await healthCheckVm.add_tags('xo:no-bak=Health Check')
62
62
  await new HealthCheckVmBackup({
63
63
  restoredVm: healthCheckVm,
64
64
  xapi,
@@ -2,6 +2,20 @@ import mapValues from 'lodash/mapValues.js'
2
2
  import { dirname } from 'node:path'
3
3
 
4
4
  function formatVmBackup(backup) {
5
+ const { isVhdDifferencing, vmSnapshot } = backup
6
+
7
+ let differencingVhds
8
+ let dynamicVhds
9
+ const withMemory = vmSnapshot.suspend_VDI !== 'OpaqueRef:NULL'
10
+ // isVhdDifferencing is either undefined or an object
11
+ if (isVhdDifferencing !== undefined) {
12
+ differencingVhds = Object.values(isVhdDifferencing).filter(t => t).length
13
+ dynamicVhds = Object.values(isVhdDifferencing).filter(t => !t).length
14
+ if (withMemory) {
15
+ // the suspend VDI (memory) is always a dynamic
16
+ dynamicVhds -= 1
17
+ }
18
+ }
5
19
  return {
6
20
  disks:
7
21
  backup.vhds === undefined
@@ -25,6 +39,10 @@ function formatVmBackup(backup) {
25
39
  name_description: backup.vm.name_description,
26
40
  name_label: backup.vm.name_label,
27
41
  },
42
+
43
+ differencingVhds,
44
+ dynamicVhds,
45
+ withMemory,
28
46
  }
29
47
  }
30
48
 
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.44.2",
11
+ "version": "0.44.4",
12
12
  "engines": {
13
13
  "node": ">=14.18"
14
14
  },
@@ -22,10 +22,10 @@
22
22
  "@vates/async-each": "^1.0.0",
23
23
  "@vates/cached-dns.lookup": "^1.0.0",
24
24
  "@vates/compose": "^2.1.0",
25
- "@vates/decorate-with": "^2.0.0",
25
+ "@vates/decorate-with": "^2.1.0",
26
26
  "@vates/disposable": "^0.1.5",
27
- "@vates/fuse-vhd": "^2.0.0",
28
- "@vates/nbd-client": "^2.0.1",
27
+ "@vates/fuse-vhd": "^2.1.0",
28
+ "@vates/nbd-client": "^3.0.0",
29
29
  "@vates/parse-duration": "^0.1.1",
30
30
  "@xen-orchestra/async-map": "^0.1.2",
31
31
  "@xen-orchestra/fs": "^4.1.3",
@@ -44,8 +44,8 @@
44
44
  "proper-lockfile": "^4.1.2",
45
45
  "tar": "^6.1.15",
46
46
  "uuid": "^9.0.0",
47
- "vhd-lib": "^4.7.0",
48
- "xen-api": "^2.0.0",
47
+ "vhd-lib": "^4.9.0",
48
+ "xen-api": "^2.0.1",
49
49
  "yazl": "^2.5.1"
50
50
  },
51
51
  "devDependencies": {
@@ -56,7 +56,7 @@
56
56
  "tmp": "^0.2.1"
57
57
  },
58
58
  "peerDependencies": {
59
- "@xen-orchestra/xapi": "^4.0.0"
59
+ "@xen-orchestra/xapi": "^4.2.0"
60
60
  },
61
61
  "license": "AGPL-3.0-or-later",
62
62
  "author": {