@xen-orchestra/backups 0.18.2 → 0.19.1

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 (42) hide show
  1. package/Backup.js +4 -2
  2. package/DurablePartition.js +2 -0
  3. package/ImportVmBackup.js +2 -0
  4. package/RemoteAdapter.js +52 -25
  5. package/RestoreMetadataBackup.js +2 -0
  6. package/Task.js +6 -1
  7. package/_PoolMetadataBackup.js +2 -0
  8. package/_VmBackup.js +23 -6
  9. package/_XoMetadataBackup.js +2 -0
  10. package/_backupType.js +2 -0
  11. package/_backupWorker.js +29 -12
  12. package/_cancelableMap.js +4 -2
  13. package/_cleanVm.js +93 -18
  14. package/_deltaVm.js +3 -1
  15. package/_extractIdsFromSimplePattern.js +2 -0
  16. package/_filenameDate.js +2 -0
  17. package/_forkStreamUnpipe.js +2 -0
  18. package/_getOldEntries.js +2 -0
  19. package/_getTmpDir.js +3 -1
  20. package/_getVmBackupDir.js +2 -0
  21. package/_isValidXva.js +21 -6
  22. package/_listPartitions.js +3 -1
  23. package/_lvm.js +3 -1
  24. package/_watchStreamSize.js +2 -0
  25. package/formatVmBackups.js +2 -0
  26. package/merge-worker/cli.js +23 -2
  27. package/merge-worker/index.js +2 -0
  28. package/package.json +6 -6
  29. package/parseMetadataBackupId.js +2 -0
  30. package/runBackupWorker.js +2 -0
  31. package/writers/DeltaBackupWriter.js +11 -2
  32. package/writers/DeltaReplicationWriter.js +3 -1
  33. package/writers/FullBackupWriter.js +2 -0
  34. package/writers/FullReplicationWriter.js +3 -1
  35. package/writers/_AbstractDeltaWriter.js +2 -0
  36. package/writers/_AbstractFullWriter.js +2 -0
  37. package/writers/_AbstractWriter.js +2 -0
  38. package/writers/_MixinBackupWriter.js +8 -5
  39. package/writers/_MixinReplicationWriter.js +2 -0
  40. package/writers/_checkVhd.js +2 -0
  41. package/writers/_listReplicatedVms.js +2 -0
  42. package/writers/_packUuid.js +2 -0
package/Backup.js CHANGED
@@ -1,6 +1,8 @@
1
+ 'use strict'
2
+
1
3
  const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
2
- const Disposable = require('promise-toolbox/Disposable.js')
3
- const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
4
+ const Disposable = require('promise-toolbox/Disposable')
5
+ const ignoreErrors = require('promise-toolbox/ignoreErrors')
4
6
  const { compileTemplate } = require('@xen-orchestra/template')
5
7
  const { limitConcurrency } = require('limit-concurrency-decorator')
6
8
 
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const { asyncMap } = require('@xen-orchestra/async-map')
2
4
 
3
5
  exports.DurablePartition = class DurablePartition {
package/ImportVmBackup.js CHANGED
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const assert = require('assert')
2
4
 
3
5
  const { formatFilenameDate } = require('./_filenameDate.js')
package/RemoteAdapter.js CHANGED
@@ -1,13 +1,17 @@
1
+ 'use strict'
2
+
1
3
  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')
4
+ const Disposable = require('promise-toolbox/Disposable')
5
+ const fromCallback = require('promise-toolbox/fromCallback')
6
+ const fromEvent = require('promise-toolbox/fromEvent')
7
+ const pDefer = require('promise-toolbox/defer')
6
8
  const groupBy = require('lodash/groupBy.js')
7
9
  const { dirname, join, normalize, resolve } = require('path')
8
10
  const { createLogger } = require('@xen-orchestra/log')
9
11
  const { Constants, createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } = require('vhd-lib')
10
12
  const { deduped } = require('@vates/disposable/deduped.js')
13
+ const { decorateMethodsWith } = require('@vates/decorate-with')
14
+ const { compose } = require('@vates/compose')
11
15
  const { execFile } = require('child_process')
12
16
  const { readdir, stat } = require('fs-extra')
13
17
  const { v4: uuidv4 } = require('uuid')
@@ -88,9 +92,6 @@ class RemoteAdapter {
88
92
  return partition
89
93
  }
90
94
 
91
- _getLvmLogicalVolumes = Disposable.factory(this._getLvmLogicalVolumes)
92
- _getLvmLogicalVolumes = deduped(this._getLvmLogicalVolumes, (devicePath, pvId, vgName) => [devicePath, pvId, vgName])
93
- _getLvmLogicalVolumes = debounceResourceFactory(this._getLvmLogicalVolumes)
94
95
  async *_getLvmLogicalVolumes(devicePath, pvId, vgName) {
95
96
  yield this._getLvmPhysicalVolume(devicePath, pvId && (await this._findPartition(devicePath, pvId)))
96
97
 
@@ -102,9 +103,6 @@ class RemoteAdapter {
102
103
  }
103
104
  }
104
105
 
105
- _getLvmPhysicalVolume = Disposable.factory(this._getLvmPhysicalVolume)
106
- _getLvmPhysicalVolume = deduped(this._getLvmPhysicalVolume, (devicePath, partition) => [devicePath, partition?.id])
107
- _getLvmPhysicalVolume = debounceResourceFactory(this._getLvmPhysicalVolume)
108
106
  async *_getLvmPhysicalVolume(devicePath, partition) {
109
107
  const args = []
110
108
  if (partition !== undefined) {
@@ -125,9 +123,6 @@ class RemoteAdapter {
125
123
  }
126
124
  }
127
125
 
128
- _getPartition = Disposable.factory(this._getPartition)
129
- _getPartition = deduped(this._getPartition, (devicePath, partition) => [devicePath, partition?.id])
130
- _getPartition = debounceResourceFactory(this._getPartition)
131
126
  async *_getPartition(devicePath, partition) {
132
127
  const options = ['loop', 'ro']
133
128
 
@@ -180,7 +175,6 @@ class RemoteAdapter {
180
175
  })
181
176
  }
182
177
 
183
- _usePartitionFiles = Disposable.factory(this._usePartitionFiles)
184
178
  async *_usePartitionFiles(diskId, partitionId, paths) {
185
179
  const path = yield this.getPartition(diskId, partitionId)
186
180
 
@@ -230,8 +224,8 @@ class RemoteAdapter {
230
224
  async deleteDeltaVmBackups(backups) {
231
225
  const handler = this._handler
232
226
 
233
- // unused VHDs will be detected by `cleanVm`
234
- await asyncMapSettled(backups, ({ _filename }) => VhdAbstract.unlink(handler, _filename))
227
+ // this will delete the json, unused VHDs will be detected by `cleanVm`
228
+ await asyncMapSettled(backups, ({ _filename }) => handler.unlink(_filename))
235
229
  }
236
230
 
237
231
  async deleteMetadataBackup(backupId) {
@@ -277,6 +271,12 @@ class RemoteAdapter {
277
271
  delta !== undefined && this.deleteDeltaVmBackups(delta),
278
272
  full !== undefined && this.deleteFullVmBackups(full),
279
273
  ])
274
+
275
+ const dirs = new Set(files.map(file => dirname(file)))
276
+ for (const dir of dirs) {
277
+ // don't merge in main process, unused VHDs will be merged in the next backup run
278
+ await this.cleanVm(dir, { remove: true, onLog: warn })
279
+ }
280
280
  }
281
281
 
282
282
  #getCompressionType() {
@@ -291,9 +291,6 @@ class RemoteAdapter {
291
291
  return this.#useVhdDirectory()
292
292
  }
293
293
 
294
- getDisk = Disposable.factory(this.getDisk)
295
- getDisk = deduped(this.getDisk, diskId => [diskId])
296
- getDisk = debounceResourceFactory(this.getDisk)
297
294
  async *getDisk(diskId) {
298
295
  const handler = this._handler
299
296
 
@@ -330,7 +327,6 @@ class RemoteAdapter {
330
327
  // - `<partitionId>`: partitioned disk
331
328
  // - `<pvId>/<vgName>/<lvName>`: LVM on a partitioned disk
332
329
  // - `/<vgName>/lvName>`: LVM on a raw disk
333
- getPartition = Disposable.factory(this.getPartition)
334
330
  async *getPartition(diskId, partitionId) {
335
331
  const devicePath = yield this.getDisk(diskId)
336
332
  if (partitionId === undefined) {
@@ -359,9 +355,14 @@ class RemoteAdapter {
359
355
  const handler = this._handler
360
356
 
361
357
  const backups = { __proto__: null }
362
- await asyncMap(await handler.list(BACKUP_DIR), async vmUuid => {
363
- const vmBackups = await this.listVmBackups(vmUuid)
364
- backups[vmUuid] = vmBackups
358
+ await asyncMap(await handler.list(BACKUP_DIR), async entry => {
359
+ // ignore hidden and lock files
360
+ if (entry[0] !== '.' && !entry.endsWith('.lock')) {
361
+ const vmBackups = await this.listVmBackups(entry)
362
+ if (vmBackups.length !== 0) {
363
+ backups[entry] = vmBackups
364
+ }
365
+ }
365
366
  })
366
367
 
367
368
  return backups
@@ -534,8 +535,8 @@ class RemoteAdapter {
534
535
 
535
536
  // if it's a path : open all hierarchy of parent
536
537
  if (typeof paths === 'string') {
537
- let vhd,
538
- vhdPath = paths
538
+ let vhd
539
+ let vhdPath = paths
539
540
  do {
540
541
  const disposable = await openVhd(handler, vhdPath)
541
542
  vhd = disposable.value
@@ -615,4 +616,30 @@ Object.assign(RemoteAdapter.prototype, {
615
616
  isValidXva,
616
617
  })
617
618
 
619
+ decorateMethodsWith(RemoteAdapter, {
620
+ _getLvmLogicalVolumes: compose([
621
+ Disposable.factory,
622
+ [deduped, (devicePath, pvId, vgName) => [devicePath, pvId, vgName]],
623
+ debounceResourceFactory,
624
+ ]),
625
+
626
+ _getLvmPhysicalVolume: compose([
627
+ Disposable.factory,
628
+ [deduped, (devicePath, partition) => [devicePath, partition?.id]],
629
+ debounceResourceFactory,
630
+ ]),
631
+
632
+ _getPartition: compose([
633
+ Disposable.factory,
634
+ [deduped, (devicePath, partition) => [devicePath, partition?.id]],
635
+ debounceResourceFactory,
636
+ ]),
637
+
638
+ _usePartitionFiles: Disposable.factory,
639
+
640
+ getDisk: compose([Disposable.factory, [deduped, diskId => [diskId]], debounceResourceFactory]),
641
+
642
+ getPartition: Disposable.factory,
643
+ })
644
+
618
645
  exports.RemoteAdapter = RemoteAdapter
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const { DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter.js')
2
4
  const { PATH_DB_DUMP } = require('./_PoolMetadataBackup.js')
3
5
 
package/Task.js CHANGED
@@ -1,4 +1,6 @@
1
- const CancelToken = require('promise-toolbox/CancelToken.js')
1
+ 'use strict'
2
+
3
+ const CancelToken = require('promise-toolbox/CancelToken')
2
4
  const Zone = require('node-zone')
3
5
 
4
6
  const logAfterEnd = () => {
@@ -7,6 +9,8 @@ const logAfterEnd = () => {
7
9
 
8
10
  const noop = Function.prototype
9
11
 
12
+ const serializeErrors = errors => (Array.isArray(errors) ? errors.map(serializeError) : errors)
13
+
10
14
  // Create a serializable object from an error.
11
15
  //
12
16
  // Otherwise some fields might be non-enumerable and missing from logs.
@@ -15,6 +19,7 @@ const serializeError = error =>
15
19
  ? {
16
20
  ...error, // Copy enumerable properties.
17
21
  code: error.code,
22
+ errors: serializeErrors(error.errors), // supports AggregateError
18
23
  message: error.message,
19
24
  name: error.name,
20
25
  stack: error.stack,
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const { asyncMap } = require('@xen-orchestra/async-map')
2
4
 
3
5
  const { DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter.js')
package/_VmBackup.js CHANGED
@@ -1,11 +1,14 @@
1
+ 'use strict'
2
+
1
3
  const assert = require('assert')
2
4
  const findLast = require('lodash/findLast.js')
3
5
  const groupBy = require('lodash/groupBy.js')
4
- const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
6
+ const ignoreErrors = require('promise-toolbox/ignoreErrors')
5
7
  const keyBy = require('lodash/keyBy.js')
6
8
  const mapValues = require('lodash/mapValues.js')
7
9
  const { asyncMap } = require('@xen-orchestra/async-map')
8
10
  const { createLogger } = require('@xen-orchestra/log')
11
+ const { decorateMethodsWith } = require('@vates/decorate-with')
9
12
  const { defer } = require('golike-defer')
10
13
  const { formatDateTime } = require('@xen-orchestra/xapi')
11
14
 
@@ -21,6 +24,13 @@ const { watchStreamSize } = require('./_watchStreamSize.js')
21
24
 
22
25
  const { debug, warn } = createLogger('xo:backups:VmBackup')
23
26
 
27
+ class AggregateError extends Error {
28
+ constructor(errors, message) {
29
+ super(message)
30
+ this.errors = errors
31
+ }
32
+ }
33
+
24
34
  const asyncEach = async (iterable, fn, thisArg = iterable) => {
25
35
  for (const item of iterable) {
26
36
  await fn.call(thisArg, item)
@@ -34,10 +44,11 @@ const forkDeltaExport = deltaExport =>
34
44
  },
35
45
  })
36
46
 
37
- exports.VmBackup = class VmBackup {
47
+ class VmBackup {
38
48
  constructor({ config, getSnapshotNameLabel, job, remoteAdapters, remotes, schedule, settings, srs, vm }) {
39
- if (vm.other_config['xo:backup:job'] === job.id) {
40
- // otherwise replicated VMs would be matched and replicated again and again
49
+ if (vm.other_config['xo:backup:job'] === job.id && 'start' in vm.blocked_operations) {
50
+ // don't match replicated VMs created by this very job otherwise they
51
+ // will be replicated again and again
41
52
  throw new Error('cannot backup a VM created by this very job')
42
53
  }
43
54
 
@@ -124,16 +135,18 @@ exports.VmBackup = class VmBackup {
124
135
  return
125
136
  }
126
137
 
138
+ const errors = []
127
139
  await (parallel ? asyncMap : asyncEach)(writers, async function (writer) {
128
140
  try {
129
141
  await fn(writer)
130
142
  } catch (error) {
143
+ errors.push(error)
131
144
  this.delete(writer)
132
145
  warn(warnMessage, { error, writer: writer.constructor.name })
133
146
  }
134
147
  })
135
148
  if (writers.size === 0) {
136
- throw new Error('all targets have failed, step: ' + warnMessage)
149
+ throw new AggregateError(errors, 'all targets have failed, step: ' + warnMessage)
137
150
  }
138
151
  }
139
152
 
@@ -384,7 +397,6 @@ exports.VmBackup = class VmBackup {
384
397
  this._fullVdisRequired = fullVdisRequired
385
398
  }
386
399
 
387
- run = defer(this.run)
388
400
  async run($defer) {
389
401
  const settings = this._settings
390
402
  assert(
@@ -432,3 +444,8 @@ exports.VmBackup = class VmBackup {
432
444
  }
433
445
  }
434
446
  }
447
+ exports.VmBackup = VmBackup
448
+
449
+ decorateMethodsWith(VmBackup, {
450
+ run: defer,
451
+ })
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const { asyncMap } = require('@xen-orchestra/async-map')
2
4
 
3
5
  const { DIR_XO_CONFIG_BACKUPS } = require('./RemoteAdapter.js')
package/_backupType.js CHANGED
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  exports.isMetadataFile = filename => filename.endsWith('.json')
2
4
  exports.isVhdFile = filename => filename.endsWith('.vhd')
3
5
  exports.isXvaFile = filename => filename.endsWith('.xva')
package/_backupWorker.js CHANGED
@@ -1,11 +1,14 @@
1
+ 'use strict'
2
+
1
3
  require('@xen-orchestra/log/configure.js').catchGlobalErrors(
2
4
  require('@xen-orchestra/log').createLogger('xo:backups:worker')
3
5
  )
4
6
 
5
- const Disposable = require('promise-toolbox/Disposable.js')
6
- const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
7
+ const Disposable = require('promise-toolbox/Disposable')
8
+ const ignoreErrors = require('promise-toolbox/ignoreErrors')
7
9
  const { compose } = require('@vates/compose')
8
10
  const { createDebounceResource } = require('@vates/disposable/debounceResource.js')
11
+ const { decorateMethodsWith } = require('@vates/decorate-with')
9
12
  const { deduped } = require('@vates/disposable/deduped.js')
10
13
  const { getHandler } = require('@xen-orchestra/fs')
11
14
  const { parseDuration } = require('@vates/parse-duration')
@@ -58,11 +61,6 @@ class BackupWorker {
58
61
  }).run()
59
62
  }
60
63
 
61
- getAdapter = Disposable.factory(this.getAdapter)
62
- getAdapter = deduped(this.getAdapter, remote => [remote.url])
63
- getAdapter = compose(this.getAdapter, function (resource) {
64
- return this.debounceResource(resource)
65
- })
66
64
  async *getAdapter(remote) {
67
65
  const handler = getHandler(remote, this.#remoteOptions)
68
66
  await handler.sync()
@@ -77,11 +75,6 @@ class BackupWorker {
77
75
  }
78
76
  }
79
77
 
80
- getXapi = Disposable.factory(this.getXapi)
81
- getXapi = deduped(this.getXapi, ({ url }) => [url])
82
- getXapi = compose(this.getXapi, function (resource) {
83
- return this.debounceResource(resource)
84
- })
85
78
  async *getXapi({ credentials: { username: user, password }, ...opts }) {
86
79
  const xapi = new Xapi({
87
80
  ...this.#xapiOptions,
@@ -103,6 +96,30 @@ class BackupWorker {
103
96
  }
104
97
  }
105
98
 
99
+ decorateMethodsWith(BackupWorker, {
100
+ getAdapter: compose([
101
+ Disposable.factory,
102
+ [deduped, remote => [remote.url]],
103
+ [
104
+ compose,
105
+ function (resource) {
106
+ return this.debounceResource(resource)
107
+ },
108
+ ],
109
+ ]),
110
+
111
+ getXapi: compose([
112
+ Disposable.factory,
113
+ [deduped, xapi => [xapi.url]],
114
+ [
115
+ compose,
116
+ function (resource) {
117
+ return this.debounceResource(resource)
118
+ },
119
+ ],
120
+ ]),
121
+ })
122
+
106
123
  // Received message:
107
124
  //
108
125
  // Message {
package/_cancelableMap.js CHANGED
@@ -1,5 +1,7 @@
1
- const cancelable = require('promise-toolbox/cancelable.js')
2
- const CancelToken = require('promise-toolbox/CancelToken.js')
1
+ 'use strict'
2
+
3
+ const cancelable = require('promise-toolbox/cancelable')
4
+ const CancelToken = require('promise-toolbox/CancelToken')
3
5
 
4
6
  // Similar to `Promise.all` + `map` but pass a cancel token to the callback
5
7
  //
package/_cleanVm.js CHANGED
@@ -1,7 +1,10 @@
1
+ 'use strict'
2
+
1
3
  const assert = require('assert')
2
4
  const sum = require('lodash/sum')
3
5
  const { asyncMap } = require('@xen-orchestra/async-map')
4
6
  const { Constants, mergeVhd, openVhd, VhdAbstract, VhdFile } = require('vhd-lib')
7
+ const { isVhdAlias, resolveVhdAlias } = require('vhd-lib/aliases')
5
8
  const { dirname, resolve } = require('path')
6
9
  const { DISK_TYPES } = Constants
7
10
  const { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } = require('./_backupType.js')
@@ -82,7 +85,6 @@ async function mergeVhdChain(chain, { handler, onLog, remove, merge }) {
82
85
  )
83
86
 
84
87
  clearInterval(handle)
85
-
86
88
  await Promise.all([
87
89
  VhdAbstract.rename(handler, parent, child),
88
90
  asyncMap(children.slice(0, -1), child => {
@@ -100,10 +102,11 @@ async function mergeVhdChain(chain, { handler, onLog, remove, merge }) {
100
102
 
101
103
  const noop = Function.prototype
102
104
 
103
- const INTERRUPTED_VHDS_REG = /^(?:(.+)\/)?\.(.+)\.merge.json$/
105
+ const INTERRUPTED_VHDS_REG = /^\.(.+)\.merge.json$/
104
106
  const listVhds = async (handler, vmDir) => {
105
- const vhds = []
106
- const interruptedVhds = new Set()
107
+ const vhds = new Set()
108
+ const aliases = {}
109
+ const interruptedVhds = new Map()
107
110
 
108
111
  await asyncMap(
109
112
  await handler.list(`${vmDir}/vdis`, {
@@ -118,25 +121,77 @@ const listVhds = async (handler, vmDir) => {
118
121
  async vdiDir => {
119
122
  const list = await handler.list(vdiDir, {
120
123
  filter: file => isVhdFile(file) || INTERRUPTED_VHDS_REG.test(file),
121
- prependDir: true,
122
124
  })
123
-
125
+ aliases[vdiDir] = list.filter(vhd => isVhdAlias(vhd)).map(file => `${vdiDir}/${file}`)
124
126
  list.forEach(file => {
125
127
  const res = INTERRUPTED_VHDS_REG.exec(file)
126
128
  if (res === null) {
127
- vhds.push(file)
129
+ vhds.add(`${vdiDir}/${file}`)
128
130
  } else {
129
- const [, dir, file] = res
130
- interruptedVhds.add(`${dir}/${file}`)
131
+ interruptedVhds.set(`${vdiDir}/${res[1]}`, `${vdiDir}/${file}`)
131
132
  }
132
133
  })
133
134
  }
134
135
  )
135
136
  )
136
137
 
137
- return { vhds, interruptedVhds }
138
+ return { vhds, interruptedVhds, aliases }
138
139
  }
139
140
 
141
+ async function checkAliases(aliasPaths, targetDataRepository, { handler, onLog = noop, remove = false }) {
142
+ const aliasFound = []
143
+ for (const path of aliasPaths) {
144
+ const target = await resolveVhdAlias(handler, path)
145
+
146
+ if (!isVhdFile(target)) {
147
+ onLog(`Alias ${path} references a non vhd target: ${target}`)
148
+ if (remove) {
149
+ await handler.unlink(target)
150
+ await handler.unlink(path)
151
+ }
152
+ continue
153
+ }
154
+
155
+ try {
156
+ const { dispose } = await openVhd(handler, target)
157
+ try {
158
+ await dispose()
159
+ } catch (e) {
160
+ // error during dispose should not trigger a deletion
161
+ }
162
+ } catch (error) {
163
+ onLog(`target ${target} of alias ${path} is missing or broken`, { error })
164
+ if (remove) {
165
+ try {
166
+ await VhdAbstract.unlink(handler, path)
167
+ } catch (e) {
168
+ if (e.code !== 'ENOENT') {
169
+ onLog(`Error while deleting target ${target} of alias ${path}`, { error: e })
170
+ }
171
+ }
172
+ }
173
+ continue
174
+ }
175
+
176
+ aliasFound.push(resolve('/', target))
177
+ }
178
+
179
+ const entries = await handler.list(targetDataRepository, {
180
+ ignoreMissing: true,
181
+ prependDir: true,
182
+ })
183
+
184
+ entries.forEach(async entry => {
185
+ if (!aliasFound.includes(entry)) {
186
+ onLog(`the Vhd ${entry} is not referenced by a an alias`)
187
+ if (remove) {
188
+ await VhdAbstract.unlink(handler, entry)
189
+ }
190
+ }
191
+ })
192
+ }
193
+ exports.checkAliases = checkAliases
194
+
140
195
  const defaultMergeLimiter = limitConcurrency(1)
141
196
 
142
197
  exports.cleanVm = async function cleanVm(
@@ -147,18 +202,16 @@ exports.cleanVm = async function cleanVm(
147
202
 
148
203
  const handler = this._handler
149
204
 
150
- const vhds = new Set()
151
205
  const vhdsToJSons = new Set()
152
206
  const vhdParents = { __proto__: null }
153
207
  const vhdChildren = { __proto__: null }
154
208
 
155
- const vhdsList = await listVhds(handler, vmDir)
209
+ const { vhds, interruptedVhds, aliases } = await listVhds(handler, vmDir)
156
210
 
157
211
  // remove broken VHDs
158
- await asyncMap(vhdsList.vhds, async path => {
212
+ await asyncMap(vhds, async path => {
159
213
  try {
160
- await Disposable.use(openVhd(handler, path, { checkSecondFooter: !vhdsList.interruptedVhds.has(path) }), vhd => {
161
- vhds.add(path)
214
+ await Disposable.use(openVhd(handler, path, { checkSecondFooter: !interruptedVhds.has(path) }), vhd => {
162
215
  if (vhd.footer.diskType === DISK_TYPES.DIFFERENCING) {
163
216
  const parent = resolve('/', dirname(path), vhd.header.parentUnicodeName)
164
217
  vhdParents[path] = parent
@@ -173,6 +226,7 @@ exports.cleanVm = async function cleanVm(
173
226
  }
174
227
  })
175
228
  } catch (error) {
229
+ vhds.delete(path)
176
230
  onLog(`error while checking the VHD with path ${path}`, { error })
177
231
  if (error?.code === 'ERR_ASSERTION' && remove) {
178
232
  onLog(`deleting broken ${path}`)
@@ -181,7 +235,28 @@ exports.cleanVm = async function cleanVm(
181
235
  }
182
236
  })
183
237
 
184
- // @todo : add check for data folder of alias not referenced in a valid alias
238
+ // remove interrupted merge states for missing VHDs
239
+ for (const interruptedVhd of interruptedVhds.keys()) {
240
+ if (!vhds.has(interruptedVhd)) {
241
+ const statePath = interruptedVhds.get(interruptedVhd)
242
+ interruptedVhds.delete(interruptedVhd)
243
+
244
+ onLog('orphan merge state', {
245
+ mergeStatePath: statePath,
246
+ missingVhdPath: interruptedVhd,
247
+ })
248
+ if (remove) {
249
+ onLog(`deleting orphan merge state ${statePath}`)
250
+ await handler.unlink(statePath)
251
+ }
252
+ }
253
+ }
254
+
255
+ // check if alias are correct
256
+ // check if all vhd in data subfolder have a corresponding alias
257
+ await asyncMap(Object.keys(aliases), async dir => {
258
+ await checkAliases(aliases[dir], `${dir}/data`, { handler, onLog, remove })
259
+ })
185
260
 
186
261
  // remove VHDs with missing ancestors
187
262
  {
@@ -344,9 +419,9 @@ exports.cleanVm = async function cleanVm(
344
419
  })
345
420
 
346
421
  // merge interrupted VHDs
347
- vhdsList.interruptedVhds.forEach(parent => {
422
+ for (const parent of interruptedVhds.keys()) {
348
423
  vhdChainsToMerge[parent] = [vhdChildren[parent], parent]
349
- })
424
+ }
350
425
 
351
426
  Object.values(vhdChainsToMerge).forEach(chain => {
352
427
  if (chain !== undefined) {
package/_deltaVm.js CHANGED
@@ -1,7 +1,9 @@
1
+ 'use strict'
2
+
1
3
  const compareVersions = require('compare-versions')
2
4
  const find = require('lodash/find.js')
3
5
  const groupBy = require('lodash/groupBy.js')
4
- const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
6
+ const ignoreErrors = require('promise-toolbox/ignoreErrors')
5
7
  const omit = require('lodash/omit.js')
6
8
  const { asyncMap } = require('@xen-orchestra/async-map')
7
9
  const { CancelToken } = require('promise-toolbox')
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  exports.extractIdsFromSimplePattern = function extractIdsFromSimplePattern(pattern) {
2
4
  if (pattern === undefined) {
3
5
  return []
package/_filenameDate.js CHANGED
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const { utcFormat, utcParse } = require('d3-time-format')
2
4
 
3
5
  // Format a date in ISO 8601 in a safe way to be used in filenames
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const eos = require('end-of-stream')
2
4
  const { PassThrough } = require('stream')
3
5
 
package/_getOldEntries.js CHANGED
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  // returns all entries but the last retention-th
2
4
  exports.getOldEntries = function getOldEntries(retention, entries) {
3
5
  return entries === undefined ? [] : retention > 0 ? entries.slice(0, -retention) : entries
package/_getTmpDir.js CHANGED
@@ -1,4 +1,6 @@
1
- const Disposable = require('promise-toolbox/Disposable.js')
1
+ 'use strict'
2
+
3
+ const Disposable = require('promise-toolbox/Disposable')
2
4
  const { join } = require('path')
3
5
  const { mkdir, rmdir } = require('fs-extra')
4
6
  const { tmpdir } = require('os')
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const BACKUP_DIR = 'xo-vm-backups'
2
4
  exports.BACKUP_DIR = BACKUP_DIR
3
5
 
package/_isValidXva.js CHANGED
@@ -1,11 +1,26 @@
1
+ 'use strict'
2
+
1
3
  const assert = require('assert')
2
4
 
3
- const isGzipFile = async (handler, fd) => {
5
+ const COMPRESSED_MAGIC_NUMBERS = [
4
6
  // https://tools.ietf.org/html/rfc1952.html#page-5
5
- const magicNumber = Buffer.allocUnsafe(2)
7
+ Buffer.from('1F8B', 'hex'),
8
+
9
+ // https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#zstandard-frames
10
+ Buffer.from('28B52FFD', 'hex'),
11
+ ]
12
+ const MAGIC_NUMBER_MAX_LENGTH = Math.max(...COMPRESSED_MAGIC_NUMBERS.map(_ => _.length))
13
+
14
+ const isCompressedFile = async (handler, fd) => {
15
+ const header = Buffer.allocUnsafe(MAGIC_NUMBER_MAX_LENGTH)
16
+ assert.strictEqual((await handler.read(fd, header, 0)).bytesRead, header.length)
6
17
 
7
- assert.strictEqual((await handler.read(fd, magicNumber, 0)).bytesRead, magicNumber.length)
8
- return magicNumber[0] === 31 && magicNumber[1] === 139
18
+ for (const magicNumber of COMPRESSED_MAGIC_NUMBERS) {
19
+ if (magicNumber.compare(header, 0, magicNumber.length) === 0) {
20
+ return true
21
+ }
22
+ }
23
+ return false
9
24
  }
10
25
 
11
26
  // TODO: better check?
@@ -43,8 +58,8 @@ async function isValidXva(path) {
43
58
  return false
44
59
  }
45
60
 
46
- return (await isGzipFile(handler, fd))
47
- ? true // gzip files cannot be validated at this time
61
+ return (await isCompressedFile(handler, fd))
62
+ ? true // compressed files cannot be validated at this time
48
63
  : await isValidTar(handler, size, fd)
49
64
  } finally {
50
65
  handler.closeFile(fd).catch(noop)
@@ -1,4 +1,6 @@
1
- const fromCallback = require('promise-toolbox/fromCallback.js')
1
+ 'use strict'
2
+
3
+ const fromCallback = require('promise-toolbox/fromCallback')
2
4
  const { createLogger } = require('@xen-orchestra/log')
3
5
  const { createParser } = require('parse-pairs')
4
6
  const { execFile } = require('child_process')
package/_lvm.js CHANGED
@@ -1,4 +1,6 @@
1
- const fromCallback = require('promise-toolbox/fromCallback.js')
1
+ 'use strict'
2
+
3
+ const fromCallback = require('promise-toolbox/fromCallback')
2
4
  const { createParser } = require('parse-pairs')
3
5
  const { execFile } = require('child_process')
4
6
 
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  exports.watchStreamSize = function watchStreamSize(stream, container = { size: 0 }) {
2
4
  stream.on('data', data => {
3
5
  container.size += data.length
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const mapValues = require('lodash/mapValues.js')
2
4
  const { dirname } = require('path')
3
5
 
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ 'use strict'
4
+
3
5
  const { catchGlobalErrors } = require('@xen-orchestra/log/configure.js')
4
6
  const { createLogger } = require('@xen-orchestra/log')
5
7
  const { getSyncedHandler } = require('@xen-orchestra/fs')
@@ -41,13 +43,32 @@ const main = Disposable.wrap(async function* main(args) {
41
43
  let taskFiles
42
44
  while ((taskFiles = await listRetry()) !== undefined) {
43
45
  const taskFileBasename = min(taskFiles)
46
+ const previousTaskFile = join(CLEAN_VM_QUEUE, taskFileBasename)
44
47
  const taskFile = join(CLEAN_VM_QUEUE, '_' + taskFileBasename)
45
48
 
46
49
  // move this task to the end
47
- await handler.rename(join(CLEAN_VM_QUEUE, taskFileBasename), taskFile)
50
+ try {
51
+ await handler.rename(previousTaskFile, taskFile)
52
+ } catch (error) {
53
+ // this error occurs if the task failed too many times (i.e. too many `_` prefixes)
54
+ // there is nothing more that can be done
55
+ if (error.code === 'ENAMETOOLONG') {
56
+ await handler.unlink(previousTaskFile)
57
+ }
58
+
59
+ throw error
60
+ }
61
+
48
62
  try {
49
63
  const vmDir = getVmBackupDir(String(await handler.readFile(taskFile)))
50
- await adapter.cleanVm(vmDir, { merge: true, onLog: info, remove: true })
64
+ try {
65
+ await adapter.cleanVm(vmDir, { merge: true, onLog: info, remove: true })
66
+ } catch (error) {
67
+ // consider the clean successful if the VM dir is missing
68
+ if (error.code !== 'ENOENT') {
69
+ throw error
70
+ }
71
+ }
51
72
 
52
73
  handler.unlink(taskFile).catch(error => warn('deleting task failure', { error }))
53
74
  } catch (error) {
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const { join, resolve } = require('path')
2
4
  const { spawn } = require('child_process')
3
5
  const { check } = require('proper-lockfile')
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.18.2",
11
+ "version": "0.19.1",
12
12
  "engines": {
13
13
  "node": ">=14.6"
14
14
  },
@@ -17,10 +17,11 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@vates/compose": "^2.1.0",
20
+ "@vates/decorate-with": "^1.0.0",
20
21
  "@vates/disposable": "^0.1.1",
21
22
  "@vates/parse-duration": "^0.1.1",
22
23
  "@xen-orchestra/async-map": "^0.1.2",
23
- "@xen-orchestra/fs": "^0.19.3",
24
+ "@xen-orchestra/fs": "^0.20.0",
24
25
  "@xen-orchestra/log": "^0.3.0",
25
26
  "@xen-orchestra/template": "^0.1.0",
26
27
  "compare-versions": "^4.0.1",
@@ -32,15 +33,14 @@
32
33
  "lodash": "^4.17.20",
33
34
  "node-zone": "^0.4.0",
34
35
  "parse-pairs": "^1.1.0",
35
- "promise-toolbox": "^0.20.0",
36
+ "promise-toolbox": "^0.21.0",
36
37
  "proper-lockfile": "^4.1.2",
37
- "pump": "^3.0.0",
38
38
  "uuid": "^8.3.2",
39
- "vhd-lib": "^2.1.0",
39
+ "vhd-lib": "^3.1.0",
40
40
  "yazl": "^2.5.1"
41
41
  },
42
42
  "peerDependencies": {
43
- "@xen-orchestra/xapi": "^0.8.5"
43
+ "@xen-orchestra/xapi": "^0.9.0"
44
44
  },
45
45
  "license": "AGPL-3.0-or-later",
46
46
  "author": {
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const { DIR_XO_CONFIG_BACKUPS, DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter.js')
2
4
 
3
5
  exports.parseMetadataBackupId = function parseMetadataBackupId(backupId) {
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const path = require('path')
2
4
  const { createLogger } = require('@xen-orchestra/log')
3
5
  const { fork } = require('child_process')
@@ -1,7 +1,9 @@
1
+ 'use strict'
2
+
1
3
  const assert = require('assert')
2
4
  const map = require('lodash/map.js')
3
5
  const mapValues = require('lodash/mapValues.js')
4
- const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
6
+ const ignoreErrors = require('promise-toolbox/ignoreErrors')
5
7
  const { asyncMap } = require('@xen-orchestra/async-map')
6
8
  const { chainVhd, checkVhdChain, openVhd, VhdAbstract } = require('vhd-lib')
7
9
  const { createLogger } = require('@xen-orchestra/log')
@@ -40,7 +42,14 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
40
42
  await asyncMap(vhds, async path => {
41
43
  try {
42
44
  await checkVhdChain(handler, path)
43
- found = found || (await adapter.isMergeableParent(packedBaseUuid, path))
45
+ // Warning, this should not be written as found = found || await adapter.isMergeableParent(packedBaseUuid, path)
46
+ //
47
+ // since all the checks of a path are done in parallel, found would be containing
48
+ // only the last answer of isMergeableParent which is probably not the right one
49
+ // this led to the support tickets https://help.vates.fr/#ticket/zoom/4751 , 4729, 4665 and 4300
50
+
51
+ const isMergeable = await adapter.isMergeableParent(packedBaseUuid, path)
52
+ found = found || isMergeable
44
53
  } catch (error) {
45
54
  warn('checkBaseVdis', { error })
46
55
  await ignoreErrors.call(VhdAbstract.unlink(handler, path))
@@ -1,5 +1,7 @@
1
+ 'use strict'
2
+
1
3
  const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
2
- const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
4
+ const ignoreErrors = require('promise-toolbox/ignoreErrors')
3
5
  const { formatDateTime } = require('@xen-orchestra/xapi')
4
6
 
5
7
  const { formatFilenameDate } = require('../_filenameDate.js')
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const { formatFilenameDate } = require('../_filenameDate.js')
2
4
  const { getOldEntries } = require('../_getOldEntries.js')
3
5
  const { getVmBackupDir } = require('../_getVmBackupDir.js')
@@ -1,4 +1,6 @@
1
- const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
1
+ 'use strict'
2
+
3
+ const ignoreErrors = require('promise-toolbox/ignoreErrors')
2
4
  const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
3
5
  const { formatDateTime } = require('@xen-orchestra/xapi')
4
6
 
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const { AbstractWriter } = require('./_AbstractWriter.js')
2
4
 
3
5
  exports.AbstractDeltaWriter = class AbstractDeltaWriter extends AbstractWriter {
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const { AbstractWriter } = require('./_AbstractWriter.js')
2
4
 
3
5
  exports.AbstractFullWriter = class AbstractFullWriter extends AbstractWriter {
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  exports.AbstractWriter = class AbstractWriter {
2
4
  constructor({ backup, settings }) {
3
5
  this._backup = backup
@@ -1,7 +1,9 @@
1
+ 'use strict'
2
+
1
3
  const { createLogger } = require('@xen-orchestra/log')
2
4
  const { join } = require('path')
3
5
 
4
- const { BACKUP_DIR, getVmBackupDir } = require('../_getVmBackupDir.js')
6
+ const { getVmBackupDir } = require('../_getVmBackupDir.js')
5
7
  const MergeWorker = require('../merge-worker/index.js')
6
8
  const { formatFilenameDate } = require('../_filenameDate.js')
7
9
 
@@ -44,13 +46,14 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
44
46
 
45
47
  async afterBackup() {
46
48
  const { disableMergeWorker } = this._backup.config
49
+ // merge worker only compatible with local remotes
50
+ const { handler } = this._adapter
51
+ const willMergeInWorker = !disableMergeWorker && typeof handler._getRealPath === 'function'
47
52
 
48
- const { merge } = await this._cleanVm({ remove: true, merge: disableMergeWorker })
53
+ const { merge } = await this._cleanVm({ remove: true, merge: !willMergeInWorker })
49
54
  await this.#lock.dispose()
50
55
 
51
- // merge worker only compatible with local remotes
52
- const { handler } = this._adapter
53
- if (merge && !disableMergeWorker && typeof handler._getRealPath === 'function') {
56
+ if (merge && willMergeInWorker) {
54
57
  const taskFile =
55
58
  join(MergeWorker.CLEAN_VM_QUEUE, formatFilenameDate(new Date())) +
56
59
  '-' +
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  exports.MixinReplicationWriter = (BaseClass = Object) =>
2
4
  class MixinReplicationWriter extends BaseClass {
3
5
  constructor({ sr, ...rest }) {
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const openVhd = require('vhd-lib').openVhd
2
4
  const Disposable = require('promise-toolbox/Disposable')
3
5
 
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const getReplicatedVmDatetime = vm => {
2
4
  const { 'xo:backup:datetime': datetime = vm.name_label.slice(-17, -1) } = vm.other_config
3
5
  return datetime
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const PARSE_UUID_RE = /-/g
2
4
 
3
5
  exports.packUuid = function packUuid(uuid) {