@xen-orchestra/backups 0.29.6 → 0.31.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
@@ -3,6 +3,7 @@
3
3
  const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
4
4
  const Disposable = require('promise-toolbox/Disposable')
5
5
  const ignoreErrors = require('promise-toolbox/ignoreErrors')
6
+ const pTimeout = require('promise-toolbox/timeout')
6
7
  const { compileTemplate } = require('@xen-orchestra/template')
7
8
  const { limitConcurrency } = require('limit-concurrency-decorator')
8
9
 
@@ -25,6 +26,7 @@ const getAdaptersByRemote = adapters => {
25
26
  const runTask = (...args) => Task.run(...args).catch(noop) // errors are handled by logs
26
27
 
27
28
  const DEFAULT_SETTINGS = {
29
+ getRemoteTimeout: 300e3,
28
30
  reportWhen: 'failure',
29
31
  }
30
32
 
@@ -53,6 +55,13 @@ const DEFAULT_METADATA_SETTINGS = {
53
55
  retentionXoMetadata: 0,
54
56
  }
55
57
 
58
+ class RemoteTimeoutError extends Error {
59
+ constructor(remoteId) {
60
+ super('timeout while getting the remote ' + remoteId)
61
+ this.remoteId = remoteId
62
+ }
63
+ }
64
+
56
65
  exports.Backup = class Backup {
57
66
  constructor({ config, getAdapter, getConnectedRecord, job, schedule }) {
58
67
  this._config = config
@@ -60,13 +69,6 @@ exports.Backup = class Backup {
60
69
  this._job = job
61
70
  this._schedule = schedule
62
71
 
63
- this._getAdapter = Disposable.factory(function* (remoteId) {
64
- return {
65
- adapter: yield getAdapter(remoteId),
66
- remoteId,
67
- }
68
- })
69
-
70
72
  this._getSnapshotNameLabel = compileTemplate(config.snapshotNameLabelTpl, {
71
73
  '{job.name}': job.name,
72
74
  '{vm.name_label}': vm => vm.name_label,
@@ -87,6 +89,27 @@ exports.Backup = class Backup {
87
89
 
88
90
  this._baseSettings = baseSettings
89
91
  this._settings = { ...baseSettings, ...job.settings[schedule.id] }
92
+
93
+ const { getRemoteTimeout } = this._settings
94
+ this._getAdapter = async function (remoteId) {
95
+ try {
96
+ const disposable = await pTimeout.call(getAdapter(remoteId), getRemoteTimeout, new RemoteTimeoutError(remoteId))
97
+
98
+ return new Disposable(() => disposable.dispose(), {
99
+ adapter: disposable.value,
100
+ remoteId,
101
+ })
102
+ } catch (error) {
103
+ // See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
104
+ runTask(
105
+ {
106
+ name: 'get remote adapter',
107
+ data: { type: 'remote', id: remoteId },
108
+ },
109
+ () => Promise.reject(error)
110
+ )
111
+ }
112
+ }
90
113
  }
91
114
 
92
115
  async _runMetadataBackup() {
@@ -132,20 +155,7 @@ exports.Backup = class Backup {
132
155
  })
133
156
  )
134
157
  ),
135
- Disposable.all(
136
- remoteIds.map(id =>
137
- this._getAdapter(id).catch(error => {
138
- // See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
139
- runTask(
140
- {
141
- name: 'get remote adapter',
142
- data: { type: 'remote', id },
143
- },
144
- () => Promise.reject(error)
145
- )
146
- })
147
- )
148
- ),
158
+ Disposable.all(remoteIds.map(id => this._getAdapter(id))),
149
159
  async (pools, remoteAdapters) => {
150
160
  // remove adapters that failed (already handled)
151
161
  remoteAdapters = remoteAdapters.filter(_ => _ !== undefined)
@@ -233,19 +243,7 @@ exports.Backup = class Backup {
233
243
  })
234
244
  )
235
245
  ),
236
- Disposable.all(
237
- extractIdsFromSimplePattern(job.remotes).map(id =>
238
- this._getAdapter(id).catch(error => {
239
- runTask(
240
- {
241
- name: 'get remote adapter',
242
- data: { type: 'remote', id },
243
- },
244
- () => Promise.reject(error)
245
- )
246
- })
247
- )
248
- ),
246
+ Disposable.all(extractIdsFromSimplePattern(job.remotes).map(id => this._getAdapter(id))),
249
247
  () => (settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined),
250
248
  async (srs, remoteAdapters, healthCheckSr) => {
251
249
  // remove adapters that failed (already handled)
package/_backupWorker.js CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict'
2
2
 
3
- require('@xen-orchestra/log/configure').catchGlobalErrors(
4
- require('@xen-orchestra/log').createLogger('xo:backups:worker')
5
- )
3
+ const logger = require('@xen-orchestra/log').createLogger('xo:backups:worker')
4
+
5
+ require('@xen-orchestra/log/configure').catchGlobalErrors(logger)
6
6
 
7
7
  require('@vates/cached-dns.lookup').createCachedLookup().patchGlobal()
8
8
 
@@ -20,6 +20,8 @@ const { Backup } = require('./Backup.js')
20
20
  const { RemoteAdapter } = require('./RemoteAdapter.js')
21
21
  const { Task } = require('./Task.js')
22
22
 
23
+ const { debug } = logger
24
+
23
25
  class BackupWorker {
24
26
  #config
25
27
  #job
@@ -122,6 +124,11 @@ decorateMethodsWith(BackupWorker, {
122
124
  ]),
123
125
  })
124
126
 
127
+ const emitMessage = message => {
128
+ debug('message emitted', { message })
129
+ process.send(message)
130
+ }
131
+
125
132
  // Received message:
126
133
  //
127
134
  // Message {
@@ -139,6 +146,8 @@ decorateMethodsWith(BackupWorker, {
139
146
  // result?: any
140
147
  // }
141
148
  process.on('message', async message => {
149
+ debug('message received', { message })
150
+
142
151
  if (message.action === 'run') {
143
152
  const backupWorker = new BackupWorker(message.data)
144
153
  try {
@@ -147,7 +156,7 @@ process.on('message', async message => {
147
156
  {
148
157
  name: 'backup run',
149
158
  onLog: data =>
150
- process.send({
159
+ emitMessage({
151
160
  data,
152
161
  type: 'log',
153
162
  }),
@@ -156,13 +165,13 @@ process.on('message', async message => {
156
165
  )
157
166
  : await backupWorker.run()
158
167
 
159
- process.send({
168
+ emitMessage({
160
169
  type: 'result',
161
170
  result,
162
171
  status: 'success',
163
172
  })
164
173
  } catch (error) {
165
- process.send({
174
+ emitMessage({
166
175
  type: 'result',
167
176
  result: error,
168
177
  status: 'failure',
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
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.6",
11
+ "version": "0.31.0",
12
12
  "engines": {
13
13
  "node": ">=14.6"
14
14
  },
@@ -26,7 +26,7 @@
26
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",
@@ -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 => {
@@ -218,7 +218,9 @@ class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
218
218
  $defer(() => nbdClient.disconnect())
219
219
 
220
220
  info('NBD client ready', { vdi: id, path })
221
+ Task.info('NBD used')
221
222
  } catch (error) {
223
+ Task.warning('NBD configured but unusable', { error })
222
224
  nbdClient = undefined
223
225
  warn('error connecting to NBD server', { error, vdi: id, path })
224
226
  }
@@ -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
  }