@xen-orchestra/backups 0.34.0 → 0.36.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
 
@@ -3,12 +3,14 @@
3
3
  const { Task } = require('./Task')
4
4
 
5
5
  exports.HealthCheckVmBackup = class HealthCheckVmBackup {
6
- #xapi
7
6
  #restoredVm
7
+ #timeout
8
+ #xapi
8
9
 
9
- constructor({ restoredVm, xapi }) {
10
+ constructor({ restoredVm, timeout = 10 * 60 * 1000, xapi }) {
10
11
  this.#restoredVm = restoredVm
11
12
  this.#xapi = xapi
13
+ this.#timeout = timeout
12
14
  }
13
15
 
14
16
  async run() {
@@ -23,7 +25,12 @@ exports.HealthCheckVmBackup = class HealthCheckVmBackup {
23
25
 
24
26
  // remove vifs
25
27
  await Promise.all(restoredVm.$VIFs.map(vif => xapi.callAsync('VIF.destroy', vif.$ref)))
26
-
28
+ const waitForScript = restoredVm.tags.includes('xo-backup-health-check-xenstore')
29
+ if (waitForScript) {
30
+ await restoredVm.set_xenstore_data({
31
+ 'vm-data/xo-backup-health-check': 'planned',
32
+ })
33
+ }
27
34
  const start = new Date()
28
35
  // start Vm
29
36
 
@@ -34,7 +41,7 @@ exports.HealthCheckVmBackup = class HealthCheckVmBackup {
34
41
  false // Skip pre-boot checks?
35
42
  )
36
43
  const started = new Date()
37
- const timeout = 10 * 60 * 1000
44
+ const timeout = this.#timeout
38
45
  const startDuration = started - start
39
46
 
40
47
  let remainingTimeout = timeout - startDuration
@@ -52,12 +59,52 @@ exports.HealthCheckVmBackup = class HealthCheckVmBackup {
52
59
  remainingTimeout -= running - started
53
60
 
54
61
  if (remainingTimeout < 0) {
55
- throw new Error(`local xapi did not get Runnig state for VM ${restoredId} after ${timeout / 1000} second`)
62
+ throw new Error(`local xapi did not get Running state for VM ${restoredId} after ${timeout / 1000} second`)
56
63
  }
57
64
  // wait for the guest tool version to be defined
58
65
  await xapi.waitObjectState(restoredVm.guest_metrics, gm => gm?.PV_drivers_version?.major !== undefined, {
59
66
  timeout: remainingTimeout,
60
67
  })
68
+
69
+ const guestToolsReady = new Date()
70
+ remainingTimeout -= guestToolsReady - running
71
+ if (remainingTimeout < 0) {
72
+ throw new Error(`local xapi did not get he guest tools check ${restoredId} after ${timeout / 1000} second`)
73
+ }
74
+
75
+ if (waitForScript) {
76
+ const startedRestoredVm = await xapi.waitObjectState(
77
+ restoredVm.$ref,
78
+ vm =>
79
+ vm?.xenstore_data !== undefined &&
80
+ (vm.xenstore_data['vm-data/xo-backup-health-check'] === 'success' ||
81
+ vm.xenstore_data['vm-data/xo-backup-health-check'] === 'failure'),
82
+ {
83
+ timeout: remainingTimeout,
84
+ }
85
+ )
86
+ const scriptOk = new Date()
87
+ remainingTimeout -= scriptOk - guestToolsReady
88
+ if (remainingTimeout < 0) {
89
+ throw new Error(
90
+ `Backup health check script did not update vm-data/xo-backup-health-check of ${restoredId} after ${
91
+ timeout / 1000
92
+ } second, got ${
93
+ startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check']
94
+ } instead of 'success' or 'failure'`
95
+ )
96
+ }
97
+
98
+ if (startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check'] !== 'success') {
99
+ const message = startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check-error']
100
+ if (message) {
101
+ throw new Error(`Backup health check script failed with message ${message} for VM ${restoredId} `)
102
+ } else {
103
+ throw new Error(`Backup health check script failed for VM ${restoredId} `)
104
+ }
105
+ }
106
+ Task.info('Backup health check script successfully executed')
107
+ }
61
108
  }
62
109
  )
63
110
  }
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
@@ -681,37 +673,17 @@ class RemoteAdapter {
681
673
  await VhdAbstract.createAlias(handler, path, dataPath)
682
674
  return size
683
675
  } else {
684
- const inputWithSize = await createVhdStreamWithLength(input)
685
- return this.outputStream(path, inputWithSize, { checksum, validator, expectedSize: inputWithSize.length })
676
+ return this.outputStream(path, input, { checksum, validator })
686
677
  }
687
678
  }
688
679
 
689
- async outputStream(path, input, { checksum = true, validator = noop, expectedSize } = {}) {
680
+ async outputStream(path, input, { checksum = true, validator = noop } = {}) {
690
681
  const container = watchStreamSize(input)
691
-
692
682
  await this._handler.outputStream(path, input, {
693
683
  checksum,
694
684
  dirMode: this._dirMode,
695
685
  async validator() {
696
686
  await input.task
697
- if (expectedSize !== undefined) {
698
- // check that we read all the stream
699
- strictEqual(
700
- container.size,
701
- expectedSize,
702
- `transferred size ${container.size}, expected file size : ${expectedSize}`
703
- )
704
- }
705
- let size
706
- try {
707
- size = await this._handler.getSize(path)
708
- } catch (err) {
709
- // can fail is the remote is encrypted
710
- }
711
- if (size !== undefined) {
712
- // check that everything is written to disk
713
- strictEqual(size, container.size, `written size ${size}, transfered size : ${container.size}`)
714
- }
715
687
  return validator.apply(this, arguments)
716
688
  },
717
689
  })
@@ -747,7 +719,7 @@ class RemoteAdapter {
747
719
 
748
720
  async readDeltaVmBackup(metadata, ignoredVdis) {
749
721
  const handler = this._handler
750
- const { vbds, vhds, vifs, vm } = metadata
722
+ const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
751
723
  const dir = dirname(metadata._filename)
752
724
  const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
753
725
 
@@ -762,7 +734,7 @@ class RemoteAdapter {
762
734
  vdis,
763
735
  version: '1.0.0',
764
736
  vifs,
765
- vm,
737
+ vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
766
738
  }
767
739
  }
768
740
 
@@ -774,7 +746,49 @@ class RemoteAdapter {
774
746
  // _filename is a private field used to compute the backup id
775
747
  //
776
748
  // it's enumerable to make it cacheable
777
- 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
778
792
  }
779
793
  }
780
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,
@@ -251,6 +255,11 @@ class VmBackup {
251
255
  Task.info('Transfer data using NBD')
252
256
  }
253
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
+
254
263
  deltaExport.streams = mapValues(deltaExport.streams, this._throttleStream)
255
264
 
256
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.34.0",
11
+ "version": "0.36.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.1.0",
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.3.0",
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.1.0"
55
+ "@xen-orchestra/xapi": "^2.2.0"
56
56
  },
57
57
  "license": "AGPL-3.0-or-later",
58
58
  "author": {
@@ -50,6 +50,7 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
50
50
  },
51
51
  })
52
52
  this.transfer = task.wrapFn(this.transfer)
53
+ this.healthCheck = task.wrapFn(this.healthCheck)
53
54
  this.cleanup = task.wrapFn(this.cleanup, true)
54
55
 
55
56
  return task.run(() => this._prepare())
@@ -80,7 +80,7 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
80
80
  assert.notStrictEqual(
81
81
  this._metadataFileName,
82
82
  undefined,
83
- 'Metadata file name should be defined before making a healthcheck'
83
+ 'Metadata file name should be defined before making a health check'
84
84
  )
85
85
  return Task.run(
86
86
  {