@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 +32 -34
- package/_backupWorker.js +15 -6
- package/_deltaVm.js +1 -1
- package/package.json +2 -2
- package/runBackupWorker.js +1 -1
- package/writers/DeltaBackupWriter.js +2 -0
- package/writers/DeltaReplicationWriter.js +6 -3
- package/writers/FullReplicationWriter.js +7 -3
- package/writers/_MixinReplicationWriter.js +40 -0
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
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
+
emitMessage({
|
|
160
169
|
type: 'result',
|
|
161
170
|
result,
|
|
162
171
|
status: 'success',
|
|
163
172
|
})
|
|
164
173
|
} catch (error) {
|
|
165
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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",
|
package/runBackupWorker.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|