@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 +2 -2
- package/_deltaVm.js +4 -1
- package/_forkStreamUnpipe.js +15 -16
- package/package.json +4 -5
- package/runBackupWorker.js +1 -1
- package/writers/DeltaBackupWriter.js +14 -2
- package/writers/DeltaReplicationWriter.js +6 -3
- package/writers/FullReplicationWriter.js +7 -3
- package/writers/_MixinReplicationWriter.js +40 -0
package/README.md
CHANGED
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
|
|
@@ -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
|
package/_forkStreamUnpipe.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
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(
|
|
13
|
-
const { forks = 0 } =
|
|
14
|
-
|
|
11
|
+
exports.forkStreamUnpipe = function forkStreamUnpipe(source) {
|
|
12
|
+
const { forks = 0 } = source
|
|
13
|
+
source.forks = forks + 1
|
|
15
14
|
|
|
16
|
-
debug('forking', { forks:
|
|
15
|
+
debug('forking', { forks: source.forks })
|
|
17
16
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
22
|
+
fork.destroy(error)
|
|
24
23
|
}
|
|
25
24
|
})
|
|
26
|
-
|
|
27
|
-
debug('end of stream, unpiping', { error, forks: --
|
|
25
|
+
finished(fork, { readable: false }, error => {
|
|
26
|
+
debug('end of stream, unpiping', { error, forks: --source.forks })
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
source.unpipe(fork)
|
|
30
29
|
|
|
31
|
-
if (
|
|
30
|
+
if (source.forks === 0) {
|
|
32
31
|
debug('no more forks, destroying original stream')
|
|
33
|
-
|
|
32
|
+
source.destroy(new Error('no more consumers for this stream'))
|
|
34
33
|
}
|
|
35
34
|
})
|
|
36
|
-
return
|
|
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.
|
|
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.
|
|
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.
|
|
54
|
+
"@xen-orchestra/xapi": "^1.6.1"
|
|
56
55
|
},
|
|
57
56
|
"license": "AGPL-3.0-or-later",
|
|
58
57
|
"author": {
|
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 => {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|