@xen-orchestra/backups 0.29.5 → 0.30.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/README.md CHANGED
@@ -8,8 +8,8 @@
8
8
 
9
9
  Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/backups):
10
10
 
11
- ```
12
- > npm install --save @xen-orchestra/backups
11
+ ```sh
12
+ npm install --save @xen-orchestra/backups
13
13
  ```
14
14
 
15
15
  ## Contributions
package/_deltaVm.js CHANGED
@@ -12,7 +12,7 @@ const { defer } = require('golike-defer')
12
12
 
13
13
  const { cancelableMap } = require('./_cancelableMap.js')
14
14
  const { Task } = require('./Task.js')
15
- const { pick } = require('lodash')
15
+ const pick = require('lodash/pick.js')
16
16
 
17
17
  const TAG_BASE_DELTA = 'xo:base_delta'
18
18
  exports.TAG_BASE_DELTA = TAG_BASE_DELTA
@@ -258,6 +258,9 @@ exports.importDeltaVm = defer(async function importDeltaVm(
258
258
  $defer.onFailure(() => newVdi.$destroy())
259
259
 
260
260
  await newVdi.update_other_config(TAG_COPY_SRC, vdi.uuid)
261
+ if (vdi.virtual_size > newVdi.virtual_size) {
262
+ await newVdi.$callAsync('resize', vdi.virtual_size)
263
+ }
261
264
  } else if (vdiRef === vmRecord.suspend_VDI) {
262
265
  // suspendVDI has already created
263
266
  newVdi = suspendVdi
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const eos = require('end-of-stream')
4
- const { PassThrough } = require('stream')
3
+ const { finished, PassThrough } = require('node:stream')
5
4
 
6
5
  const { debug } = require('@xen-orchestra/log').createLogger('xo:backups:forkStreamUnpipe')
7
6
 
@@ -9,29 +8,29 @@ const { debug } = require('@xen-orchestra/log').createLogger('xo:backups:forkStr
9
8
  //
10
9
  // in case of error in the new readable stream, it will simply be unpiped
11
10
  // from the original one
12
- exports.forkStreamUnpipe = function forkStreamUnpipe(stream) {
13
- const { forks = 0 } = stream
14
- stream.forks = forks + 1
11
+ exports.forkStreamUnpipe = function forkStreamUnpipe(source) {
12
+ const { forks = 0 } = source
13
+ source.forks = forks + 1
15
14
 
16
- debug('forking', { forks: stream.forks })
15
+ debug('forking', { forks: source.forks })
17
16
 
18
- const proxy = new PassThrough()
19
- stream.pipe(proxy)
20
- eos(stream, error => {
17
+ const fork = new PassThrough()
18
+ source.pipe(fork)
19
+ finished(source, { writable: false }, error => {
21
20
  if (error !== undefined) {
22
21
  debug('error on original stream, destroying fork', { error })
23
- proxy.destroy(error)
22
+ fork.destroy(error)
24
23
  }
25
24
  })
26
- eos(proxy, error => {
27
- debug('end of stream, unpiping', { error, forks: --stream.forks })
25
+ finished(fork, { readable: false }, error => {
26
+ debug('end of stream, unpiping', { error, forks: --source.forks })
28
27
 
29
- stream.unpipe(proxy)
28
+ source.unpipe(fork)
30
29
 
31
- if (stream.forks === 0) {
30
+ if (source.forks === 0) {
32
31
  debug('no more forks, destroying original stream')
33
- stream.destroy(new Error('no more consumers for this stream'))
32
+ source.destroy(new Error('no more consumers for this stream'))
34
33
  }
35
34
  })
36
- return proxy
35
+ return fork
37
36
  }
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.29.5",
11
+ "version": "0.30.0",
12
12
  "engines": {
13
13
  "node": ">=14.6"
14
14
  },
@@ -23,16 +23,15 @@
23
23
  "@vates/decorate-with": "^2.0.0",
24
24
  "@vates/disposable": "^0.1.4",
25
25
  "@vates/fuse-vhd": "^1.0.0",
26
- "@vates/nbd-client": "*",
26
+ "@vates/nbd-client": "^1.0.1",
27
27
  "@vates/parse-duration": "^0.1.1",
28
28
  "@xen-orchestra/async-map": "^0.1.2",
29
- "@xen-orchestra/fs": "^3.3.1",
29
+ "@xen-orchestra/fs": "^3.3.2",
30
30
  "@xen-orchestra/log": "^0.6.0",
31
31
  "@xen-orchestra/template": "^0.1.0",
32
32
  "compare-versions": "^5.0.1",
33
33
  "d3-time-format": "^3.0.0",
34
34
  "decorator-synchronized": "^0.6.0",
35
- "end-of-stream": "^1.4.4",
36
35
  "fs-extra": "^11.1.0",
37
36
  "golike-defer": "^0.5.1",
38
37
  "limit-concurrency-decorator": "^0.5.0",
@@ -52,7 +51,7 @@
52
51
  "tmp": "^0.2.1"
53
52
  },
54
53
  "peerDependencies": {
55
- "@xen-orchestra/xapi": "^1.6.0"
54
+ "@xen-orchestra/xapi": "^1.6.1"
56
55
  },
57
56
  "license": "AGPL-3.0-or-later",
58
57
  "author": {
@@ -12,7 +12,7 @@ exports.runBackupWorker = function runBackupWorker(params, onLog) {
12
12
  return new Promise((resolve, reject) => {
13
13
  const worker = fork(PATH)
14
14
 
15
- worker.on('exit', code => reject(new Error(`worker exited with code ${code}`)))
15
+ worker.on('exit', (code, signal) => reject(new Error(`worker exited with code ${code} and signal ${signal}`)))
16
16
  worker.on('error', reject)
17
17
 
18
18
  worker.on('message', message => {
@@ -7,6 +7,8 @@ const ignoreErrors = require('promise-toolbox/ignoreErrors')
7
7
  const { asyncMap } = require('@xen-orchestra/async-map')
8
8
  const { chainVhd, checkVhdChain, openVhd, VhdAbstract } = require('vhd-lib')
9
9
  const { createLogger } = require('@xen-orchestra/log')
10
+ const { decorateClass } = require('@vates/decorate-with')
11
+ const { defer } = require('golike-defer')
10
12
  const { dirname } = require('path')
11
13
 
12
14
  const { formatFilenameDate } = require('../_filenameDate.js')
@@ -22,7 +24,7 @@ const NbdClient = require('@vates/nbd-client')
22
24
 
23
25
  const { debug, warn, info } = createLogger('xo:backups:DeltaBackupWriter')
24
26
 
25
- exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
27
+ class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
26
28
  async checkBaseVdis(baseUuidToSrcVdi) {
27
29
  const { handler } = this._adapter
28
30
  const backup = this._backup
@@ -133,7 +135,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
133
135
  }
134
136
  }
135
137
 
136
- async _transfer({ timestamp, deltaExport }) {
138
+ async _transfer($defer, { timestamp, deltaExport }) {
137
139
  const adapter = this._adapter
138
140
  const backup = this._backup
139
141
 
@@ -210,8 +212,15 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
210
212
  debug('got NBD info', { nbdInfo, vdi: id, path })
211
213
  nbdClient = new NbdClient(nbdInfo)
212
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
+
213
220
  info('NBD client ready', { vdi: id, path })
221
+ Task.info('NBD used')
214
222
  } catch (error) {
223
+ Task.warning('NBD configured but unusable', { error })
215
224
  nbdClient = undefined
216
225
  warn('error connecting to NBD server', { error, vdi: id, path })
217
226
  }
@@ -248,3 +257,6 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
248
257
  // TODO: run cleanup?
249
258
  }
250
259
  }
260
+ exports.DeltaBackupWriter = decorateClass(DeltaBackupWriter, {
261
+ _transfer: defer,
262
+ })
@@ -80,6 +80,7 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
80
80
  }
81
81
 
82
82
  async _transfer({ timestamp, deltaExport, sizeContainers }) {
83
+ const { _warmMigration } = this._settings
83
84
  const sr = this._sr
84
85
  const { job, scheduleId, vm } = this._backup
85
86
 
@@ -92,7 +93,7 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
92
93
  __proto__: deltaExport,
93
94
  vm: {
94
95
  ...deltaExport.vm,
95
- tags: [...deltaExport.vm.tags, 'Continuous Replication'],
96
+ tags: _warmMigration ? deltaExport.vm.tags : [...deltaExport.vm.tags, 'Continuous Replication'],
96
97
  },
97
98
  },
98
99
  sr
@@ -101,11 +102,13 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
101
102
  size: Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0),
102
103
  }
103
104
  })
104
-
105
+ this._targetVmRef = targetVmRef
105
106
  const targetVm = await xapi.getRecord('VM', targetVmRef)
106
107
 
107
108
  await Promise.all([
108
- targetVm.ha_restart_priority !== '' &&
109
+ // warm migration does not disable HA , since the goal is to start the new VM in production
110
+ !_warmMigration &&
111
+ targetVm.ha_restart_priority !== '' &&
109
112
  Promise.all([targetVm.set_ha_restart_priority(''), targetVm.add_tags('HA disabled')]),
110
113
  targetVm.set_name_label(`${vm.name_label} - ${job.name} - (${formatFilenameDate(timestamp)})`),
111
114
  asyncMap(['start', 'start_on'], op =>
@@ -46,7 +46,7 @@ exports.FullReplicationWriter = class FullReplicationWriter extends MixinReplica
46
46
  const oldVms = getOldEntries(settings.copyRetention - 1, listReplicatedVms(xapi, scheduleId, srUuid, vm.uuid))
47
47
 
48
48
  const deleteOldBackups = () => asyncMapSettled(oldVms, vm => xapi.VM_destroy(vm.$ref))
49
- const { deleteFirst } = settings
49
+ const { deleteFirst, _warmMigration } = settings
50
50
  if (deleteFirst) {
51
51
  await deleteOldBackups()
52
52
  }
@@ -55,14 +55,18 @@ exports.FullReplicationWriter = class FullReplicationWriter extends MixinReplica
55
55
  await Task.run({ name: 'transfer' }, async () => {
56
56
  targetVmRef = await xapi.VM_import(stream, sr.$ref, vm =>
57
57
  Promise.all([
58
- vm.add_tags('Disaster Recovery'),
59
- vm.ha_restart_priority !== '' && Promise.all([vm.set_ha_restart_priority(''), vm.add_tags('HA disabled')]),
58
+ !_warmMigration && vm.add_tags('Disaster Recovery'),
59
+ // warm migration does not disable HA , since the goal is to start the new VM in production
60
+ !_warmMigration &&
61
+ vm.ha_restart_priority !== '' &&
62
+ Promise.all([vm.set_ha_restart_priority(''), vm.add_tags('HA disabled')]),
60
63
  vm.set_name_label(`${vm.name_label} - ${job.name} - (${formatFilenameDate(timestamp)})`),
61
64
  ])
62
65
  )
63
66
  return { size: sizeContainer.size }
64
67
  })
65
68
 
69
+ this._targetVmRef = targetVmRef
66
70
  const targetVm = await xapi.getRecord('VM', targetVmRef)
67
71
 
68
72
  await Promise.all([
@@ -1,5 +1,17 @@
1
1
  'use strict'
2
2
 
3
+ const { Task } = require('../Task')
4
+ const assert = require('node:assert/strict')
5
+ const { HealthCheckVmBackup } = require('../HealthCheckVmBackup')
6
+
7
+ function extractOpaqueRef(str) {
8
+ const OPAQUE_REF_RE = /OpaqueRef:[0-9a-z-]+/
9
+ const matches = OPAQUE_REF_RE.exec(str)
10
+ if (!matches) {
11
+ throw new Error('no opaque ref found')
12
+ }
13
+ return matches[0]
14
+ }
3
15
  exports.MixinReplicationWriter = (BaseClass = Object) =>
4
16
  class MixinReplicationWriter extends BaseClass {
5
17
  constructor({ sr, ...rest }) {
@@ -7,4 +19,32 @@ exports.MixinReplicationWriter = (BaseClass = Object) =>
7
19
 
8
20
  this._sr = sr
9
21
  }
22
+
23
+ healthCheck(sr) {
24
+ assert.notEqual(this._targetVmRef, undefined, 'A vm should have been transfered to be health checked')
25
+ // copy VM
26
+ return Task.run(
27
+ {
28
+ name: 'health check',
29
+ },
30
+ async () => {
31
+ const { $xapi: xapi } = sr
32
+ let clonedVm
33
+ try {
34
+ const baseVm = xapi.getObject(this._targetVmRef) ?? (await xapi.waitObject(this._targetVmRef))
35
+ const clonedRef = await xapi
36
+ .callAsync('VM.clone', this._targetVmRef, `Health Check - ${baseVm.name_label}`)
37
+ .then(extractOpaqueRef)
38
+ clonedVm = xapi.getObject(clonedRef) ?? (await xapi.waitObject(clonedRef))
39
+
40
+ await new HealthCheckVmBackup({
41
+ restoredVm: clonedVm,
42
+ xapi,
43
+ }).run()
44
+ } finally {
45
+ clonedVm && (await xapi.VM_destroy(clonedVm.$ref))
46
+ }
47
+ }
48
+ )
49
+ }
10
50
  }