@xen-orchestra/backups 0.70.0 → 0.71.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/_otherConfig.mjs +1 -0
- package/_runners/_vmRunners/_AbstractXapi.mjs +6 -8
- package/_runners/_writers/FullRemoteWriter.mjs +1 -1
- package/_runners/_writers/FullXapiWriter.mjs +1 -1
- package/_runners/_writers/IncrementalRemoteWriter.mjs +2 -2
- package/_runners/_writers/IncrementalXapiWriter.mjs +107 -61
- package/_runners/_writers/_AbstractWriter.mjs +2 -2
- package/_runners/_writers/_MixinRemoteWriter.mjs +1 -1
- package/disks/RemoteVhdDisk.mjs +5 -1
- package/package.json +2 -2
package/_otherConfig.mjs
CHANGED
|
@@ -85,6 +85,7 @@ export async function getVmDeltaChainLength(xapi, vmRef) {
|
|
|
85
85
|
export function resetVmOtherConfig(xapi, vmRef) {
|
|
86
86
|
return applyToVmAndVdis(xapi, vmRef, (type, ref) => {
|
|
87
87
|
return xapi.setFieldEntries(type, ref, 'other_config', {
|
|
88
|
+
[COPY_OF]: null,
|
|
88
89
|
[DATETIME]: null,
|
|
89
90
|
[DELTA_CHAIN_LENGTH]: null,
|
|
90
91
|
[EXPORTED_SUCCESSFULLY]: null,
|
|
@@ -105,7 +105,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
105
105
|
config,
|
|
106
106
|
healthCheckSr,
|
|
107
107
|
job,
|
|
108
|
-
|
|
108
|
+
schedule,
|
|
109
109
|
vmUuid: vm.uuid,
|
|
110
110
|
settings,
|
|
111
111
|
})
|
|
@@ -123,7 +123,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
123
123
|
config,
|
|
124
124
|
healthCheckSr,
|
|
125
125
|
job,
|
|
126
|
-
|
|
126
|
+
schedule,
|
|
127
127
|
vmUuid: vm.uuid,
|
|
128
128
|
remoteId,
|
|
129
129
|
settings: targetSettings,
|
|
@@ -141,7 +141,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
141
141
|
healthCheckSr,
|
|
142
142
|
job,
|
|
143
143
|
ReplicationWriter,
|
|
144
|
-
|
|
144
|
+
schedule,
|
|
145
145
|
vmUuid: vm.uuid,
|
|
146
146
|
srs,
|
|
147
147
|
settings,
|
|
@@ -159,7 +159,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
159
159
|
config,
|
|
160
160
|
healthCheckSr,
|
|
161
161
|
job,
|
|
162
|
-
|
|
162
|
+
schedule,
|
|
163
163
|
vmUuid: vm.uuid,
|
|
164
164
|
sr,
|
|
165
165
|
settings: targetSettings,
|
|
@@ -174,11 +174,9 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
174
174
|
|
|
175
175
|
// ensure the VM itself does not have any backup metadata which would be
|
|
176
176
|
// copied on manual snapshots and interfere with the backup jobs
|
|
177
|
+
|
|
177
178
|
async _cleanMetadata() {
|
|
178
|
-
|
|
179
|
-
if (JOB_ID in vm.other_config) {
|
|
180
|
-
await resetVmOtherConfig(this._xapi, vm.$ref)
|
|
181
|
-
}
|
|
179
|
+
await resetVmOtherConfig(this._xapi, this._vm.$ref)
|
|
182
180
|
}
|
|
183
181
|
|
|
184
182
|
async _snapshot() {
|
|
@@ -27,7 +27,7 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
|
|
|
27
27
|
async _run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot }) {
|
|
28
28
|
const settings = this._settings
|
|
29
29
|
const job = this._job
|
|
30
|
-
const scheduleId = this.
|
|
30
|
+
const scheduleId = this._schedule.id
|
|
31
31
|
|
|
32
32
|
const adapter = this._adapter
|
|
33
33
|
let metadata = await this._isAlreadyTransferred(timestamp)
|
|
@@ -34,7 +34,7 @@ export class FullXapiWriter extends MixinXapiWriter(AbstractFullWriter) {
|
|
|
34
34
|
const sr = this._sr
|
|
35
35
|
const settings = this._settings
|
|
36
36
|
const job = this._job
|
|
37
|
-
const scheduleId = this.
|
|
37
|
+
const scheduleId = this._schedule.id
|
|
38
38
|
|
|
39
39
|
const { uuid: srUuid, $xapi: xapi } = sr
|
|
40
40
|
|
|
@@ -90,7 +90,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
90
90
|
async _prepare() {
|
|
91
91
|
const adapter = this._adapter
|
|
92
92
|
const settings = this._settings
|
|
93
|
-
const scheduleId = this.
|
|
93
|
+
const scheduleId = this._schedule.id
|
|
94
94
|
const vmUuid = this._vmUuid
|
|
95
95
|
|
|
96
96
|
const oldEntries = getOldEntries(
|
|
@@ -180,7 +180,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
180
180
|
async _transfer($defer, { isVhdDifferencing, timestamp, deltaExport, vm, vmSnapshot }) {
|
|
181
181
|
const adapter = this._adapter
|
|
182
182
|
const job = this._job
|
|
183
|
-
const scheduleId = this.
|
|
183
|
+
const scheduleId = this._schedule.id
|
|
184
184
|
const settings = this._settings
|
|
185
185
|
const jobId = job.id
|
|
186
186
|
const handler = adapter.handler
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import humanFormat from 'human-format'
|
|
2
|
+
|
|
1
3
|
import { asyncMapSettled } from '@xen-orchestra/async-map'
|
|
2
4
|
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
|
3
5
|
|
|
@@ -18,50 +20,105 @@ import {
|
|
|
18
20
|
DATETIME,
|
|
19
21
|
VM_UUID,
|
|
20
22
|
} from '../../_otherConfig.mjs'
|
|
21
|
-
import assert from 'node:assert'
|
|
22
23
|
import { formatFilenameDate } from '../../_filenameDate.mjs'
|
|
24
|
+
import { XapiDiskSource } from '@xen-orchestra/xapi'
|
|
25
|
+
import { asyncEach } from '@vates/async-each'
|
|
26
|
+
import { createLogger } from '@xen-orchestra/log'
|
|
27
|
+
|
|
28
|
+
const { debug } = createLogger('xo:backups:IncrementalXapiWriter')
|
|
23
29
|
|
|
24
30
|
export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
|
|
31
|
+
// Map of source VDI UUID (COPY_OF) → validated active VDI on the target SR.
|
|
32
|
+
// Built by checkBaseVdis, consumed by #decorateVmMetadata to set baseVdi.
|
|
33
|
+
#baseVdisBySourceUuid = new Map()
|
|
34
|
+
|
|
25
35
|
async checkBaseVdis(baseUuidToSrcVdi) {
|
|
26
36
|
const sr = this._sr
|
|
37
|
+
this.#baseVdisBySourceUuid = new Map()
|
|
38
|
+
|
|
27
39
|
if (baseUuidToSrcVdi.size === 0) {
|
|
28
40
|
// searching for the vdis is expensive
|
|
29
41
|
// don't do it if there is nothing to find
|
|
30
42
|
return
|
|
31
43
|
}
|
|
32
44
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
if (!vdi?.managed || vdi?.is_a_snapshot || !baseUuidToSrcVdi.has(vdi?.other_config[COPY_OF])) {
|
|
38
|
-
return false
|
|
39
|
-
}
|
|
40
|
-
const userVbds = vdi.$VBDs?.filter(vbd => vbd.$VM && !vbd.$VM.is_control_domain) ?? []
|
|
41
|
-
if (userVbds.length !== 1) {
|
|
42
|
-
return false
|
|
43
|
-
}
|
|
44
|
-
const vm = userVbds[0].$VM
|
|
45
|
+
// look for the same snapshot
|
|
46
|
+
// ensure there are no data between the snapshot and the active disk
|
|
47
|
+
|
|
48
|
+
const snapshotCandidates = sr.$VDIs.filter(vdi => {
|
|
45
49
|
return (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
vdi?.managed &&
|
|
51
|
+
vdi?.is_a_snapshot &&
|
|
52
|
+
vdi.other_config[JOB_ID] === this._job.id &&
|
|
53
|
+
vdi.other_config[VM_UUID] === this._vmUuid &&
|
|
54
|
+
baseUuidToSrcVdi.has(vdi?.other_config[COPY_OF])
|
|
49
55
|
)
|
|
50
56
|
})
|
|
57
|
+
debug('checkBaseVdis, got snapshot candidates,', snapshotCandidates.length)
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
// ensure no data have been written since this snapshot
|
|
60
|
+
// but there may be have some other snapshot for another job
|
|
61
|
+
let targetVmRef
|
|
62
|
+
let canChainToTargetVm = true
|
|
63
|
+
await asyncEach(
|
|
64
|
+
snapshotCandidates,
|
|
65
|
+
async snapshot => {
|
|
66
|
+
let diffDisk
|
|
67
|
+
try {
|
|
68
|
+
const activeVdi = sr.$xapi.getObject(snapshot.$snapshot_of)
|
|
69
|
+
const userVbds = activeVdi.$VBDs?.filter(vbd => vbd.$VM && !vbd.$VM.is_control_domain) ?? []
|
|
70
|
+
if (userVbds.length !== 1) {
|
|
71
|
+
debug('checkBaseVdis, share vbd ', { ref: snapshot.$ref, userVbds })
|
|
72
|
+
// shared vdi ignore
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
const vm = userVbds[0].$VM
|
|
76
|
+
if (!('start' in vm.blocked_operations)) {
|
|
77
|
+
debug('checkBaseVdis, vm not blocked', { vmRef: vm.$ref })
|
|
78
|
+
// vm start unlocked
|
|
79
|
+
// not really an issue since we have check the delta
|
|
80
|
+
// but it indicates the users played with the blocked operations
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
diffDisk = new XapiDiskSource({ xapi: sr.$xapi, vdiRef: activeVdi.$ref, baseRef: snapshot.$ref })
|
|
84
|
+
await diffDisk.init()
|
|
85
|
+
if (diffDisk.getBlockIndexes().length === 0) {
|
|
86
|
+
const sourceUuid = snapshot.other_config?.[COPY_OF]
|
|
87
|
+
if (sourceUuid) {
|
|
88
|
+
this.#baseVdisBySourceUuid.set(sourceUuid, activeVdi)
|
|
89
|
+
}
|
|
90
|
+
// Track the target VM (the replicated VM to update on the next transfer).
|
|
91
|
+
targetVmRef = vm.$ref
|
|
92
|
+
} else {
|
|
93
|
+
// not empty, we will create a new VM
|
|
94
|
+
canChainToTargetVm = false
|
|
95
|
+
debug('checkBaseVdis, data between snapshot and active disk', {
|
|
96
|
+
vdiRef: snapshot.$ref,
|
|
97
|
+
nbBlocks: diffDisk.getBlockIndexes().length,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
debug('checkBaseVdis, skipping snapshot', { ref: snapshot.$ref, error })
|
|
102
|
+
return
|
|
103
|
+
} finally {
|
|
104
|
+
await diffDisk?.close().catch(error => debug('checkBaseVdis, error closing', error))
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
concurrency: 4,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if (canChainToTargetVm && targetVmRef !== undefined) {
|
|
113
|
+
debug('checkBaseVdis,got a valid vm target', targetVmRef)
|
|
114
|
+
this._targetVmRef = targetVmRef
|
|
115
|
+
}
|
|
53
116
|
|
|
54
117
|
for (const uuid of baseUuidToSrcVdi.keys()) {
|
|
55
|
-
if (!
|
|
118
|
+
if (!this.#baseVdisBySourceUuid.has(uuid)) {
|
|
56
119
|
baseUuidToSrcVdi.delete(uuid)
|
|
57
120
|
}
|
|
58
121
|
}
|
|
59
|
-
|
|
60
|
-
// Track the target VM (the replicated VM to update on the next transfer).
|
|
61
|
-
if (replicatedVdis.length > 0) {
|
|
62
|
-
const vbd = replicatedVdis[0].$VBDs.find(vbd => vbd.$VM && !vbd.$VM.is_control_domain)
|
|
63
|
-
this._targetVmRef = vbd.$VM.$ref
|
|
64
|
-
}
|
|
65
122
|
}
|
|
66
123
|
updateUuidAndChain() {
|
|
67
124
|
// nothing to do, the chaining is not modified in this case
|
|
@@ -89,7 +146,7 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
89
146
|
const settings = this._settings
|
|
90
147
|
const { uuid: srUuid, $xapi: xapi } = this._sr
|
|
91
148
|
const vmUuid = this._vmUuid
|
|
92
|
-
const scheduleId = this.
|
|
149
|
+
const scheduleId = this._schedule.id
|
|
93
150
|
|
|
94
151
|
// delete previous interrupted copies
|
|
95
152
|
ignoreErrors.call(asyncMapSettled(listReplicatedVms(xapi, scheduleId, undefined, vmUuid), vm => vm.$destroy))
|
|
@@ -132,9 +189,8 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
132
189
|
const sr = this._sr
|
|
133
190
|
const vm = backup.vm
|
|
134
191
|
const job = this._job
|
|
135
|
-
const scheduleId = this.
|
|
192
|
+
const scheduleId = this._schedule.id
|
|
136
193
|
|
|
137
|
-
vm.name_label = `${vm.name_label} - ${job.name}`
|
|
138
194
|
// update other_config data as soon as possible to ensure the next job
|
|
139
195
|
// will be able to detect any partial transfer and lean them
|
|
140
196
|
vm.other_config[COPY_OF] = vm.uuid
|
|
@@ -152,18 +208,6 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
152
208
|
if (!_warmMigration) {
|
|
153
209
|
vm.tags.push('Continuous Replication')
|
|
154
210
|
}
|
|
155
|
-
// extracting the uuid of each delta vdi on the source
|
|
156
|
-
// get all in one pass, since there is a lot of objects
|
|
157
|
-
const sourceVdiUuids = Object.values(backup.vdis)
|
|
158
|
-
.map(({ other_config }) => other_config[BASE_DELTA_VDI])
|
|
159
|
-
// full vdi don't have a base
|
|
160
|
-
.filter(_ => !!_)
|
|
161
|
-
// @todo use index ?
|
|
162
|
-
|
|
163
|
-
const replicatedVdis = sr.$VDIs.filter(vdi => {
|
|
164
|
-
// REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
|
|
165
|
-
return vdi?.managed && !vdi?.is_a_snapshot && sourceVdiUuids.includes(vdi?.other_config[COPY_OF])
|
|
166
|
-
})
|
|
167
211
|
|
|
168
212
|
Object.values(backup.vdis).forEach(vdi => {
|
|
169
213
|
vdi.other_config[COPY_OF] = vdi.uuid
|
|
@@ -172,18 +216,12 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
172
216
|
vdi.other_config[REPLICATED_TO_SR_UUID] = sr.uuid
|
|
173
217
|
vdi.other_config[VM_UUID] = vm.uuid
|
|
174
218
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
)
|
|
179
|
-
assert.ok(
|
|
180
|
-
baseReplicatedTo.length <= 1,
|
|
181
|
-
`Target of a replication must be unique, got ${baseReplicatedTo.length} candidates`
|
|
182
|
-
)
|
|
183
|
-
// baseReplicatedTo can be undefined if a new disk is added and other are already replicated
|
|
184
|
-
vdi.baseVdi = baseReplicatedTo[0]
|
|
219
|
+
const baseDeltaVdiUuid = vdi.other_config[BASE_DELTA_VDI]
|
|
220
|
+
if (baseDeltaVdiUuid !== undefined) {
|
|
221
|
+
// reuse the validated mapping built by checkBaseVdis
|
|
222
|
+
vdi.baseVdi = this.#baseVdisBySourceUuid.get(baseDeltaVdiUuid)
|
|
185
223
|
} else {
|
|
186
|
-
// first replication of this disk
|
|
224
|
+
// first replication of this disk (full, no base)
|
|
187
225
|
vdi.baseVdi = undefined
|
|
188
226
|
}
|
|
189
227
|
// ensure the VDI are created on the target SR
|
|
@@ -197,7 +235,8 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
197
235
|
const { _warmMigration } = this._settings
|
|
198
236
|
const sr = this._sr
|
|
199
237
|
const job = this._job
|
|
200
|
-
const
|
|
238
|
+
const schedule = this._schedule
|
|
239
|
+
const scheduleId = schedule.id
|
|
201
240
|
const { uuid: srUuid, $xapi: xapi } = sr
|
|
202
241
|
|
|
203
242
|
let targetVmRef
|
|
@@ -213,19 +252,26 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
213
252
|
vmUuid: vm.uuid,
|
|
214
253
|
srUuid,
|
|
215
254
|
})
|
|
216
|
-
|
|
255
|
+
const size = Object.values(deltaExport.disks).reduce(
|
|
256
|
+
(sum, disk) => sum + disk.getNbGeneratedBlock() * disk.getBlockSize(),
|
|
257
|
+
0
|
|
258
|
+
)
|
|
259
|
+
await xapi.setField(
|
|
260
|
+
'VM',
|
|
261
|
+
targetVmRef,
|
|
262
|
+
'name_description',
|
|
263
|
+
deltaExport.vm.name_description +
|
|
264
|
+
` -- last replication: ${formatFilenameDate(timestamp)} ${humanFormat.bytes(size)} read`
|
|
265
|
+
)
|
|
217
266
|
// take a snapshot to ensure these data are not modified until next snapshot
|
|
218
|
-
await Task.run({ name: 'target snapshot' }, () =>
|
|
219
|
-
xapi.VM_snapshot(targetVmRef, {
|
|
220
|
-
name_label: `${vm.name_label} - ${job.name}
|
|
267
|
+
await Task.run({ name: 'target snapshot' }, async () => {
|
|
268
|
+
await xapi.VM_snapshot(targetVmRef, {
|
|
269
|
+
name_label: `${vm.name_label} - ${job.name} / ${schedule.name} ${formatFilenameDate(timestamp)}`,
|
|
221
270
|
})
|
|
222
|
-
)
|
|
223
|
-
|
|
271
|
+
})
|
|
272
|
+
|
|
224
273
|
return {
|
|
225
|
-
size
|
|
226
|
-
(sum, disk) => sum + disk.getNbGeneratedBlock() * disk.getBlockSize(),
|
|
227
|
-
0
|
|
228
|
-
),
|
|
274
|
+
size,
|
|
229
275
|
}
|
|
230
276
|
})
|
|
231
277
|
this._targetVmRef = targetVmRef
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export class AbstractWriter {
|
|
2
|
-
constructor({ config, healthCheckSr, job, vmUuid,
|
|
2
|
+
constructor({ config, healthCheckSr, job, vmUuid, schedule, settings }) {
|
|
3
3
|
this._config = config
|
|
4
4
|
this._healthCheckSr = healthCheckSr
|
|
5
5
|
this._job = job
|
|
6
|
-
this.
|
|
6
|
+
this._schedule = schedule
|
|
7
7
|
this._settings = settings
|
|
8
8
|
this._vmUuid = vmUuid
|
|
9
9
|
}
|
|
@@ -50,7 +50,7 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
|
|
|
50
50
|
|
|
51
51
|
async getLongTermRetentionTags(currentEntry) {
|
|
52
52
|
const settings = this._settings
|
|
53
|
-
const scheduleId = this.
|
|
53
|
+
const scheduleId = this._schedule.id
|
|
54
54
|
const vmUuid = this._vmUuid
|
|
55
55
|
const adapter = this._adapter
|
|
56
56
|
|
package/disks/RemoteVhdDisk.mjs
CHANGED
|
@@ -95,10 +95,14 @@ export class RemoteVhdDisk extends RemoteDisk {
|
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Closes the VHD.
|
|
98
|
+
* We replace the dispose function call so the disk can only be closed once.
|
|
99
|
+
*
|
|
98
100
|
* @returns {Promise<void>}
|
|
99
101
|
*/
|
|
100
102
|
async close() {
|
|
101
|
-
|
|
103
|
+
const dispose = this.#dispose
|
|
104
|
+
this.#dispose = () => Promise.resolve()
|
|
105
|
+
await dispose()
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
/**
|
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.71.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"tmp": "^0.2.1"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@xen-orchestra/xapi": "^8.
|
|
65
|
+
"@xen-orchestra/xapi": "^8.7.0"
|
|
66
66
|
},
|
|
67
67
|
"license": "AGPL-3.0-or-later",
|
|
68
68
|
"author": {
|