@xen-orchestra/backups 0.33.0 → 0.35.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/Backup.js CHANGED
@@ -49,6 +49,7 @@ const DEFAULT_VM_SETTINGS = {
49
49
  timeout: 0,
50
50
  useNbd: false,
51
51
  unconditionalSnapshot: false,
52
+ validateVhdStreams: false,
52
53
  vmTimeout: 0,
53
54
  }
54
55
 
package/RemoteAdapter.js CHANGED
@@ -10,14 +10,7 @@ const groupBy = require('lodash/groupBy.js')
10
10
  const pickBy = require('lodash/pickBy.js')
11
11
  const { dirname, join, normalize, resolve } = require('path')
12
12
  const { createLogger } = require('@xen-orchestra/log')
13
- const {
14
- createVhdDirectoryFromStream,
15
- createVhdStreamWithLength,
16
- openVhd,
17
- VhdAbstract,
18
- VhdDirectory,
19
- VhdSynthetic,
20
- } = require('vhd-lib')
13
+ const { createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } = require('vhd-lib')
21
14
  const { deduped } = require('@vates/disposable/deduped.js')
22
15
  const { decorateMethodsWith } = require('@vates/decorate-with')
23
16
  const { compose } = require('@vates/compose')
@@ -39,7 +32,6 @@ const { watchStreamSize } = require('./_watchStreamSize')
39
32
  // @todo : this import is marked extraneous , sould be fixed when lib is published
40
33
  const { mount } = require('@vates/fuse-vhd')
41
34
  const { asyncEach } = require('@vates/async-each')
42
- const { strictEqual } = require('assert')
43
35
 
44
36
  const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
45
37
  exports.DIR_XO_CONFIG_BACKUPS = DIR_XO_CONFIG_BACKUPS
@@ -666,7 +658,7 @@ class RemoteAdapter {
666
658
  return path
667
659
  }
668
660
 
669
- async writeVhd(path, input, { checksum = true, validator = noop, writeBlockConcurrency, nbdClient } = {}) {
661
+ async writeVhd(path, input, { checksum = true, validator = noop, writeBlockConcurrency } = {}) {
670
662
  const handler = this._handler
671
663
  if (this.useVhdDirectory()) {
672
664
  const dataPath = `${dirname(path)}/data/${uuidv4()}.vhd`
@@ -677,42 +669,21 @@ class RemoteAdapter {
677
669
  await input.task
678
670
  return validator.apply(this, arguments)
679
671
  },
680
- nbdClient,
681
672
  })
682
673
  await VhdAbstract.createAlias(handler, path, dataPath)
683
674
  return size
684
675
  } else {
685
- const inputWithSize = await createVhdStreamWithLength(input)
686
- return this.outputStream(path, inputWithSize, { checksum, validator, expectedSize: inputWithSize.length })
676
+ return this.outputStream(path, input, { checksum, validator })
687
677
  }
688
678
  }
689
679
 
690
- async outputStream(path, input, { checksum = true, validator = noop, expectedSize } = {}) {
680
+ async outputStream(path, input, { checksum = true, validator = noop } = {}) {
691
681
  const container = watchStreamSize(input)
692
-
693
682
  await this._handler.outputStream(path, input, {
694
683
  checksum,
695
684
  dirMode: this._dirMode,
696
685
  async validator() {
697
686
  await input.task
698
- if (expectedSize !== undefined) {
699
- // check that we read all the stream
700
- strictEqual(
701
- container.size,
702
- expectedSize,
703
- `transferred size ${container.size}, expected file size : ${expectedSize}`
704
- )
705
- }
706
- let size
707
- try {
708
- size = await this._handler.getSize(path)
709
- } catch (err) {
710
- // can fail is the remote is encrypted
711
- }
712
- if (size !== undefined) {
713
- // check that everything is written to disk
714
- strictEqual(size, container.size, `written size ${size}, transfered size : ${container.size}`)
715
- }
716
687
  return validator.apply(this, arguments)
717
688
  },
718
689
  })
@@ -748,7 +719,7 @@ class RemoteAdapter {
748
719
 
749
720
  async readDeltaVmBackup(metadata, ignoredVdis) {
750
721
  const handler = this._handler
751
- const { vbds, vhds, vifs, vm } = metadata
722
+ const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
752
723
  const dir = dirname(metadata._filename)
753
724
  const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
754
725
 
@@ -763,7 +734,7 @@ class RemoteAdapter {
763
734
  vdis,
764
735
  version: '1.0.0',
765
736
  vifs,
766
- vm,
737
+ vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
767
738
  }
768
739
  }
769
740
 
@@ -775,7 +746,49 @@ class RemoteAdapter {
775
746
  // _filename is a private field used to compute the backup id
776
747
  //
777
748
  // it's enumerable to make it cacheable
778
- return { ...JSON.parse(await this._handler.readFile(path)), _filename: path }
749
+ const metadata = { ...JSON.parse(await this._handler.readFile(path)), _filename: path }
750
+
751
+ // backups created on XenServer < 7.1 via JSON in XML-RPC transports have boolean values encoded as integers, which make them unusable with more recent XAPIs
752
+ if (typeof metadata.vm.is_a_template === 'number') {
753
+ const properties = {
754
+ vbds: ['bootable', 'unpluggable', 'storage_lock', 'empty', 'currently_attached'],
755
+ vdis: [
756
+ 'sharable',
757
+ 'read_only',
758
+ 'storage_lock',
759
+ 'managed',
760
+ 'missing',
761
+ 'is_a_snapshot',
762
+ 'allow_caching',
763
+ 'metadata_latest',
764
+ ],
765
+ vifs: ['currently_attached', 'MAC_autogenerated'],
766
+ vm: ['is_a_template', 'is_control_domain', 'ha_always_run', 'is_a_snapshot', 'is_snapshot_from_vmpp'],
767
+ vmSnapshot: ['is_a_template', 'is_control_domain', 'ha_always_run', 'is_snapshot_from_vmpp'],
768
+ }
769
+
770
+ function fixBooleans(obj, properties) {
771
+ properties.forEach(property => {
772
+ if (typeof obj[property] === 'number') {
773
+ obj[property] = obj[property] === 1
774
+ }
775
+ })
776
+ }
777
+
778
+ for (const [key, propertiesInKey] of Object.entries(properties)) {
779
+ const value = metadata[key]
780
+ if (value !== undefined) {
781
+ // some properties of the metadata are collections indexed by the opaqueRef
782
+ const isCollection = Object.keys(value).some(subKey => subKey.startsWith('OpaqueRef:'))
783
+ if (isCollection) {
784
+ Object.values(value).forEach(subValue => fixBooleans(subValue, propertiesInKey))
785
+ } else {
786
+ fixBooleans(value, propertiesInKey)
787
+ }
788
+ }
789
+ }
790
+ }
791
+ return metadata
779
792
  }
780
793
  }
781
794
 
package/_VmBackup.js CHANGED
@@ -6,11 +6,13 @@ const groupBy = require('lodash/groupBy.js')
6
6
  const ignoreErrors = require('promise-toolbox/ignoreErrors')
7
7
  const keyBy = require('lodash/keyBy.js')
8
8
  const mapValues = require('lodash/mapValues.js')
9
+ const vhdStreamValidator = require('vhd-lib/vhdStreamValidator.js')
9
10
  const { asyncMap } = require('@xen-orchestra/async-map')
10
11
  const { createLogger } = require('@xen-orchestra/log')
11
12
  const { decorateMethodsWith } = require('@vates/decorate-with')
12
13
  const { defer } = require('golike-defer')
13
14
  const { formatDateTime } = require('@xen-orchestra/xapi')
15
+ const { pipeline } = require('node:stream')
14
16
 
15
17
  const { DeltaBackupWriter } = require('./writers/DeltaBackupWriter.js')
16
18
  const { DeltaReplicationWriter } = require('./writers/DeltaReplicationWriter.js')
@@ -44,6 +46,8 @@ const forkDeltaExport = deltaExport =>
44
46
  },
45
47
  })
46
48
 
49
+ const noop = Function.prototype
50
+
47
51
  class VmBackup {
48
52
  constructor({
49
53
  config,
@@ -245,7 +249,17 @@ class VmBackup {
245
249
  const deltaExport = await exportDeltaVm(exportedVm, baseVm, {
246
250
  fullVdisRequired,
247
251
  })
252
+ // since NBD is network based, if one disk use nbd , all the disk use them
253
+ // except the suspended VDI
254
+ if (Object.values(deltaExport.streams).some(({ _nbd }) => _nbd)) {
255
+ Task.info('Transfer data using NBD')
256
+ }
248
257
  const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
258
+
259
+ if (this._settings.validateVhdStreams) {
260
+ deltaExport.streams = mapValues(deltaExport.streams, stream => pipeline(stream, vhdStreamValidator, noop))
261
+ }
262
+
249
263
  deltaExport.streams = mapValues(deltaExport.streams, this._throttleStream)
250
264
 
251
265
  const timestamp = Date.now()
package/_deltaVm.js CHANGED
@@ -187,11 +187,11 @@ exports.importDeltaVm = defer(async function importDeltaVm(
187
187
 
188
188
  // 0. Create suspend_VDI
189
189
  let suspendVdi
190
- if (vmRecord.power_state === 'Suspended') {
190
+ if (vmRecord.suspend_VDI !== undefined && vmRecord.suspend_VDI !== 'OpaqueRef:NULL') {
191
191
  const vdi = vdiRecords[vmRecord.suspend_VDI]
192
192
  if (vdi === undefined) {
193
193
  Task.warning('Suspend VDI not available for this suspended VM', {
194
- vm: pick(vmRecord, 'uuid', 'name_label'),
194
+ vm: pick(vmRecord, 'uuid', 'name_label', 'suspend_VDI'),
195
195
  })
196
196
  } else {
197
197
  suspendVdi = await xapi.getRecord(
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.33.0",
11
+ "version": "0.35.0",
12
12
  "engines": {
13
13
  "node": ">=14.6"
14
14
  },
@@ -24,7 +24,7 @@
24
24
  "@vates/decorate-with": "^2.0.0",
25
25
  "@vates/disposable": "^0.1.4",
26
26
  "@vates/fuse-vhd": "^1.0.0",
27
- "@vates/nbd-client": "^1.0.1",
27
+ "@vates/nbd-client": "^1.2.0",
28
28
  "@vates/parse-duration": "^0.1.1",
29
29
  "@xen-orchestra/async-map": "^0.1.2",
30
30
  "@xen-orchestra/fs": "^3.3.4",
@@ -42,7 +42,7 @@
42
42
  "promise-toolbox": "^0.21.0",
43
43
  "proper-lockfile": "^4.1.2",
44
44
  "uuid": "^9.0.0",
45
- "vhd-lib": "^4.2.1",
45
+ "vhd-lib": "^4.4.0",
46
46
  "yazl": "^2.5.1"
47
47
  },
48
48
  "devDependencies": {
@@ -52,7 +52,7 @@
52
52
  "tmp": "^0.2.1"
53
53
  },
54
54
  "peerDependencies": {
55
- "@xen-orchestra/xapi": "^2.0.0"
55
+ "@xen-orchestra/xapi": "^2.2.0"
56
56
  },
57
57
  "license": "AGPL-3.0-or-later",
58
58
  "author": {
@@ -20,9 +20,8 @@ const { AbstractDeltaWriter } = require('./_AbstractDeltaWriter.js')
20
20
  const { checkVhd } = require('./_checkVhd.js')
21
21
  const { packUuid } = require('./_packUuid.js')
22
22
  const { Disposable } = require('promise-toolbox')
23
- const NbdClient = require('@vates/nbd-client')
24
23
 
25
- const { debug, warn, info } = createLogger('xo:backups:DeltaBackupWriter')
24
+ const { warn } = createLogger('xo:backups:DeltaBackupWriter')
26
25
 
27
26
  class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
28
27
  async checkBaseVdis(baseUuidToSrcVdi) {
@@ -200,41 +199,12 @@ class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
200
199
  await checkVhd(handler, parentPath)
201
200
  }
202
201
 
203
- const vdiRef = vm.$xapi.getObject(vdi.uuid).$ref
204
-
205
- let nbdClient
206
- if (this._backup.config.useNbd && adapter.useVhdDirectory()) {
207
- debug('useNbd is enabled', { vdi: id, path })
208
- // get nbd if possible
209
- try {
210
- // this will always take the first host in the list
211
- const [nbdInfo] = await vm.$xapi.call('VDI.get_nbd_info', vdiRef)
212
- debug('got NBD info', { nbdInfo, vdi: id, path })
213
- nbdClient = new NbdClient(nbdInfo)
214
- await nbdClient.connect()
215
-
216
- // this will inform the xapi that we don't need this anymore
217
- // and will detach the vdi from dom0
218
- $defer(() => nbdClient.disconnect())
219
-
220
- info('NBD client ready', { vdi: id, path })
221
- Task.info('NBD used')
222
- } catch (error) {
223
- Task.warning('NBD configured but unusable', { error })
224
- nbdClient = undefined
225
- warn('error connecting to NBD server', { error, vdi: id, path })
226
- }
227
- } else {
228
- debug('useNbd is disabled', { vdi: id, path })
229
- }
230
-
231
202
  transferSize += await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
232
203
  // no checksum for VHDs, because they will be invalidated by
233
204
  // merges and chainings
234
205
  checksum: false,
235
206
  validator: tmpPath => checkVhd(handler, tmpPath),
236
207
  writeBlockConcurrency: this._backup.config.writeBlockConcurrency,
237
- nbdClient,
238
208
  })
239
209
 
240
210
  if (isDelta) {