@xen-orchestra/backups 0.49.0 → 0.50.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/_cleanVm.mjs +7 -2
- package/_incrementalVm.mjs +9 -41
- package/_otherConfig.mjs +165 -0
- package/_runners/_forkStreamUnpipe.mjs +4 -0
- package/_runners/_vmRunners/IncrementalXapi.mjs +58 -45
- package/_runners/_vmRunners/_AbstractXapi.mjs +120 -36
- package/_runners/_writers/FullXapiWriter.mjs +7 -9
- package/_runners/_writers/IncrementalXapiWriter.mjs +44 -59
- package/_runners/_writers/_MixinRemoteWriter.mjs +1 -0
- package/_runners/_writers/_listReplicatedVms.mjs +7 -5
- package/package.json +4 -4
package/_cleanVm.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import sum from 'lodash/sum.js'
|
|
|
3
3
|
import { asyncMap } from '@xen-orchestra/async-map'
|
|
4
4
|
import { Constants, openVhd, VhdAbstract, VhdFile } from 'vhd-lib'
|
|
5
5
|
import { isVhdAlias, resolveVhdAlias } from 'vhd-lib/aliases.js'
|
|
6
|
-
import { dirname, resolve } from 'node:path'
|
|
6
|
+
import { basename, dirname, resolve } from 'node:path'
|
|
7
7
|
import { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } from './_backupType.mjs'
|
|
8
8
|
import { limitConcurrency } from 'limit-concurrency-decorator'
|
|
9
9
|
import { mergeVhdChain } from 'vhd-lib/merge.js'
|
|
@@ -179,7 +179,8 @@ export async function cleanVm(
|
|
|
179
179
|
vmDir,
|
|
180
180
|
{
|
|
181
181
|
fixMetadata,
|
|
182
|
-
remove,
|
|
182
|
+
remove = false,
|
|
183
|
+
removeTmp = remove,
|
|
183
184
|
merge,
|
|
184
185
|
mergeBlockConcurrency,
|
|
185
186
|
mergeLimiter = defaultMergeLimiter,
|
|
@@ -200,6 +201,10 @@ export async function cleanVm(
|
|
|
200
201
|
|
|
201
202
|
// remove broken VHDs
|
|
202
203
|
await asyncMap(vhds, async path => {
|
|
204
|
+
if(removeTmp && basename(path)[0] === '.'){
|
|
205
|
+
logInfo('deleting temporary VHD', { path })
|
|
206
|
+
return VhdAbstract.unlink(handler, path)
|
|
207
|
+
}
|
|
203
208
|
try {
|
|
204
209
|
await Disposable.use(openVhd(handler, path, { checkSecondFooter: !interruptedVhds.has(path) }), vhd => {
|
|
205
210
|
if (vhd.footer.diskType === DISK_TYPES.DIFFERENCING) {
|
package/_incrementalVm.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import groupBy from 'lodash/groupBy.js'
|
|
2
2
|
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
|
3
|
-
import omit from 'lodash/omit.js'
|
|
4
3
|
import { asyncMap } from '@xen-orchestra/async-map'
|
|
5
4
|
import { CancelToken } from 'promise-toolbox'
|
|
6
5
|
import { compareVersions } from 'compare-versions'
|
|
@@ -10,43 +9,16 @@ import { defer } from 'golike-defer'
|
|
|
10
9
|
import { cancelableMap } from './_cancelableMap.mjs'
|
|
11
10
|
import { Task } from './Task.mjs'
|
|
12
11
|
import pick from 'lodash/pick.js'
|
|
13
|
-
|
|
14
|
-
// in `other_config` of an incrementally replicated VM, contains the UUID of the source VM
|
|
15
|
-
export const TAG_BASE_DELTA = 'xo:base_delta'
|
|
16
|
-
|
|
17
|
-
// in `other_config` of an incrementally replicated VM, contains the UUID of the target SR used for replication
|
|
18
|
-
//
|
|
19
|
-
// added after the complete replication
|
|
20
|
-
export const TAG_BACKUP_SR = 'xo:backup:sr'
|
|
21
|
-
|
|
22
|
-
// in other_config of VDIs of an incrementally replicated VM, contains the UUID of the source VDI
|
|
23
|
-
export const TAG_COPY_SRC = 'xo:copy_of'
|
|
12
|
+
import { BASE_DELTA_VDI, COPY_OF, VM_UUID } from './_otherConfig.mjs'
|
|
24
13
|
|
|
25
14
|
const ensureArray = value => (value === undefined ? [] : Array.isArray(value) ? value : [value])
|
|
26
15
|
|
|
27
16
|
export async function exportIncrementalVm(
|
|
28
17
|
vm,
|
|
29
|
-
|
|
30
|
-
{
|
|
31
|
-
cancelToken = CancelToken.none,
|
|
32
|
-
|
|
33
|
-
// Sets of UUIDs of VDIs that must be exported as full.
|
|
34
|
-
fullVdisRequired = new Set(),
|
|
35
|
-
|
|
36
|
-
disableBaseTags = false,
|
|
37
|
-
nbdConcurrency = 1,
|
|
38
|
-
preferNbd,
|
|
39
|
-
} = {}
|
|
18
|
+
baseVdis = {},
|
|
19
|
+
{ cancelToken = CancelToken.none, nbdConcurrency = 1, preferNbd } = {}
|
|
40
20
|
) {
|
|
41
21
|
// refs of VM's VDIs → base's VDIs.
|
|
42
|
-
const baseVdis = {}
|
|
43
|
-
baseVm &&
|
|
44
|
-
baseVm.$VBDs.forEach(vbd => {
|
|
45
|
-
let vdi, snapshotOf
|
|
46
|
-
if ((vdi = vbd.$VDI) && (snapshotOf = vdi.$snapshot_of) && !fullVdisRequired.has(snapshotOf.uuid)) {
|
|
47
|
-
baseVdis[vdi.snapshot_of] = vdi
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
22
|
|
|
51
23
|
const streams = {}
|
|
52
24
|
const vdis = {}
|
|
@@ -67,13 +39,16 @@ export async function exportIncrementalVm(
|
|
|
67
39
|
}
|
|
68
40
|
|
|
69
41
|
// Look for a snapshot of this vdi in the base VM.
|
|
70
|
-
const baseVdi = baseVdis[vdi.
|
|
42
|
+
const baseVdi = baseVdis[vdi.$snapshot_of.uuid]
|
|
71
43
|
|
|
72
44
|
vdis[vdiRef] = {
|
|
73
45
|
...vdi,
|
|
74
46
|
other_config: {
|
|
75
47
|
...vdi.other_config,
|
|
76
|
-
[
|
|
48
|
+
[BASE_DELTA_VDI]: baseVdi?.uuid,
|
|
49
|
+
[VM_UUID]:
|
|
50
|
+
vm.$snapshot_of?.uuid ?? // vm is a snapshot
|
|
51
|
+
vm.uuid, // vm is a not snapshot
|
|
77
52
|
},
|
|
78
53
|
$snapshot_of$uuid: vdi.$snapshot_of?.uuid,
|
|
79
54
|
$SR$uuid: vdi.$SR.uuid,
|
|
@@ -120,13 +95,6 @@ export async function exportIncrementalVm(
|
|
|
120
95
|
vifs,
|
|
121
96
|
vm: {
|
|
122
97
|
...vm,
|
|
123
|
-
other_config:
|
|
124
|
-
baseVm && !disableBaseTags
|
|
125
|
-
? {
|
|
126
|
-
...vm.other_config,
|
|
127
|
-
[TAG_BASE_DELTA]: baseVm.uuid,
|
|
128
|
-
}
|
|
129
|
-
: omit(vm.other_config, TAG_BASE_DELTA),
|
|
130
98
|
},
|
|
131
99
|
},
|
|
132
100
|
'streams',
|
|
@@ -205,7 +173,7 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
|
|
|
205
173
|
newVdi = await xapi.getRecord('VDI', await vdi.baseVdi.$clone())
|
|
206
174
|
$defer.onFailure(() => newVdi.$destroy())
|
|
207
175
|
|
|
208
|
-
await newVdi.update_other_config(
|
|
176
|
+
await newVdi.update_other_config(COPY_OF, vdi.uuid)
|
|
209
177
|
if (vdi.virtual_size > newVdi.virtual_size) {
|
|
210
178
|
await newVdi.$callAsync('resize', vdi.virtual_size)
|
|
211
179
|
}
|
package/_otherConfig.mjs
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { formatDateTime } from '@xen-orchestra/xapi'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
// in `other_config` of an incrementally replicated VM or VDI
|
|
4
|
+
// contains the UUID of the object used as a base for an incremental export
|
|
5
|
+
// used to search for the replica of the base before applying a incremental replication
|
|
6
|
+
export const BASE_DELTA_VDI = 'xo:base_delta_vdi'
|
|
7
|
+
|
|
8
|
+
// in `other_config` of an incrementally replicated VM, contains the UUID of the target SR used for replication
|
|
9
|
+
//
|
|
10
|
+
// added after the complete replication
|
|
11
|
+
export const REPLICATED_TO_SR_UUID = 'xo:backup:sr'
|
|
12
|
+
|
|
13
|
+
// in other_config of VDIs of an incrementally replicated VM
|
|
14
|
+
// contains the UUID of the source exported object (snapshot or VM)
|
|
15
|
+
|
|
16
|
+
export const COPY_OF = 'xo:copy_of'
|
|
17
|
+
|
|
18
|
+
export const DATETIME = 'xo:backup:datetime'
|
|
19
|
+
|
|
20
|
+
export const JOB_ID = 'xo:backup:job'
|
|
21
|
+
export const SCHEDULE_ID = 'xo:backup:schedule'
|
|
22
|
+
|
|
23
|
+
// contains the number of delta in a chain, stored as a string
|
|
24
|
+
export const DELTA_CHAIN_LENGTH = 'xo:backup:deltaChainLength'
|
|
25
|
+
// contains the string true if this vdi has been exported successfully
|
|
26
|
+
export const EXPORTED_SUCCESSFULLY = 'xo:backup:exported'
|
|
27
|
+
|
|
28
|
+
// the VM ( not the snapshot) uuid
|
|
29
|
+
export const VM_UUID = 'xo:backup:vm'
|
|
30
|
+
|
|
31
|
+
async function listVdiRefs(xapi, vmRef) {
|
|
32
|
+
return xapi.VM_getDisks(vmRef)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function applyToVmAndVdis(xapi, vmRef, fn) {
|
|
36
|
+
const vdiRefs = await listVdiRefs(xapi, vmRef)
|
|
37
|
+
return Promise.all([fn('VM', vmRef), ...vdiRefs.map(vdiRef => fn('VDI', vdiRef))])
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function getDeltaChainLength(xapi, type, ref) {
|
|
41
|
+
const otherConfig = await xapi.getField(type, ref, 'other_config')
|
|
42
|
+
return Number(otherConfig[DELTA_CHAIN_LENGTH] ?? 0)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* set the delta chain lenght ( number of delta since last base backup) to a VM and its associated VDIs
|
|
47
|
+
*
|
|
48
|
+
* @param {Xapi} xapi
|
|
49
|
+
* @param {String} vmRef
|
|
50
|
+
* @param {Number} length
|
|
51
|
+
* @returns {Promise}
|
|
52
|
+
*/
|
|
53
|
+
export async function setVmDeltaChainLength(xapi, vmRef, length) {
|
|
54
|
+
return applyToVmAndVdis(xapi, vmRef, async (type, ref) => {
|
|
55
|
+
await xapi.setFieldEntry(type, ref, 'other_config', DELTA_CHAIN_LENGTH, String(length))
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Compute the delta chain length of a VM and its associated VDIs
|
|
61
|
+
* if there is a discrependcy, use, the highest value
|
|
62
|
+
* @param {Xapi} xapi
|
|
63
|
+
* @param {String} vmRef
|
|
64
|
+
* @returns {Promise}
|
|
65
|
+
*/
|
|
66
|
+
export async function getVmDeltaChainLength(xapi, vmRef) {
|
|
67
|
+
const lengths = await applyToVmAndVdis(xapi, vmRef, async (type, ref) => getDeltaChainLength(xapi, type, ref))
|
|
68
|
+
return Math.max(...lengths)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
*
|
|
73
|
+
* Reset the other_config field of a VM and its VDIs
|
|
74
|
+
*
|
|
75
|
+
* @param {Xapi} xapi
|
|
76
|
+
* @param {String} vmRef
|
|
77
|
+
* @returns {Promise}
|
|
78
|
+
*/
|
|
79
|
+
export function resetVmOtherConfig(xapi, vmRef) {
|
|
80
|
+
return applyToVmAndVdis(xapi, vmRef, (type, ref) => {
|
|
81
|
+
return xapi.setFieldEntries(type, ref, 'other_config', {
|
|
82
|
+
[DATETIME]: null,
|
|
83
|
+
[DELTA_CHAIN_LENGTH]: null,
|
|
84
|
+
[EXPORTED_SUCCESSFULLY]: null,
|
|
85
|
+
[JOB_ID]: null,
|
|
86
|
+
[SCHEDULE_ID]: null,
|
|
87
|
+
[VM_UUID]: null,
|
|
88
|
+
// REPLICATED_TO_SR_UUID is not reset since we can replicate a replication
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
*
|
|
95
|
+
* used to ensure compatibiliy with the previous snapshots that were having the config stored only into VM
|
|
96
|
+
*
|
|
97
|
+
* @param {Xapi} xapi
|
|
98
|
+
* @param {String} vmRef
|
|
99
|
+
* @returns {Promise}
|
|
100
|
+
*/
|
|
101
|
+
export async function populateVdisOtherConfig(xapi, vmRef) {
|
|
102
|
+
const otherConfig = await xapi.getField('VM', vmRef, 'other_config')
|
|
103
|
+
const {
|
|
104
|
+
[DATETIME]: datetime,
|
|
105
|
+
[DELTA_CHAIN_LENGTH]: chainLength,
|
|
106
|
+
[EXPORTED_SUCCESSFULLY]: successfully,
|
|
107
|
+
[JOB_ID]: jobId,
|
|
108
|
+
[REPLICATED_TO_SR_UUID]: replicatedTo,
|
|
109
|
+
[SCHEDULE_ID]: scheduleId,
|
|
110
|
+
[VM_UUID]: vmUuid,
|
|
111
|
+
} = otherConfig
|
|
112
|
+
|
|
113
|
+
return applyToVmAndVdis(xapi, vmRef, (type, ref) =>
|
|
114
|
+
xapi.setFieldEntries(type, ref, 'other_config', {
|
|
115
|
+
[DATETIME]: datetime,
|
|
116
|
+
[DELTA_CHAIN_LENGTH]: chainLength,
|
|
117
|
+
[EXPORTED_SUCCESSFULLY]: successfully,
|
|
118
|
+
[JOB_ID]: jobId,
|
|
119
|
+
[REPLICATED_TO_SR_UUID]: replicatedTo,
|
|
120
|
+
[SCHEDULE_ID]: scheduleId,
|
|
121
|
+
[VM_UUID]: vmUuid,
|
|
122
|
+
})
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
*
|
|
128
|
+
* set the other_config key related to a backup of a VM and its associated VDIs
|
|
129
|
+
*
|
|
130
|
+
* @param {Xapi} xapi
|
|
131
|
+
* @param {String} vmRef
|
|
132
|
+
* @param {*} settings
|
|
133
|
+
* @returns {PRomise}
|
|
134
|
+
*/
|
|
135
|
+
export async function setVmOtherConfig(xapi, vmRef, { timestamp, jobId, scheduleId, vmUuid, srUuid = null, ...other }) {
|
|
136
|
+
assert.notEqual(timestamp, undefined)
|
|
137
|
+
assert.notEqual(jobId, undefined)
|
|
138
|
+
assert.notEqual(scheduleId, undefined)
|
|
139
|
+
assert.notEqual(vmUuid, undefined)
|
|
140
|
+
// srUuid is nullish for backup
|
|
141
|
+
assert.equal(Object.keys(other).length, 0)
|
|
142
|
+
|
|
143
|
+
return applyToVmAndVdis(xapi, vmRef, (type, ref) =>
|
|
144
|
+
xapi.setFieldEntries(type, ref, 'other_config', {
|
|
145
|
+
[REPLICATED_TO_SR_UUID]: srUuid,
|
|
146
|
+
[DATETIME]: formatDateTime(timestamp),
|
|
147
|
+
[JOB_ID]: jobId,
|
|
148
|
+
[SCHEDULE_ID]: scheduleId,
|
|
149
|
+
[VM_UUID]: vmUuid,
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
*
|
|
155
|
+
* mark the export of he VM and its VDIs as successfull
|
|
156
|
+
*
|
|
157
|
+
* @param {Xapi} xapi
|
|
158
|
+
* @param {String} vmRef
|
|
159
|
+
* @returns {Promise}
|
|
160
|
+
*/
|
|
161
|
+
export async function markExportSuccessfull(xapi, vmRef) {
|
|
162
|
+
return applyToVmAndVdis(xapi, vmRef, (type, ref) =>
|
|
163
|
+
xapi.setFieldEntry(type, ref, 'other_config', EXPORTED_SUCCESSFULLY, 'true')
|
|
164
|
+
)
|
|
165
|
+
}
|
|
@@ -29,6 +29,10 @@ export function forkStreamUnpipe(source) {
|
|
|
29
29
|
if (source.forks === 0) {
|
|
30
30
|
debug('no more forks, destroying original stream')
|
|
31
31
|
source.destroy(new Error('no more consumers for this stream'))
|
|
32
|
+
} else {
|
|
33
|
+
// a combination of stream.unpipe, onReadable and onData may stall stream here
|
|
34
|
+
// force it to flow again since we're piping it
|
|
35
|
+
source.resume()
|
|
32
36
|
}
|
|
33
37
|
})
|
|
34
38
|
return fork
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { asyncEach } from '@vates/async-each'
|
|
2
|
-
import { asyncMap } from '@xen-orchestra/async-map'
|
|
3
2
|
import { createLogger } from '@xen-orchestra/log'
|
|
4
3
|
import { pipeline } from 'node:stream'
|
|
5
|
-
import findLast from 'lodash/findLast.js'
|
|
6
4
|
import isVhdDifferencingDisk from 'vhd-lib/isVhdDifferencingDisk.js'
|
|
7
5
|
import keyBy from 'lodash/keyBy.js'
|
|
8
6
|
import mapValues from 'lodash/mapValues.js'
|
|
@@ -15,9 +13,15 @@ import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs
|
|
|
15
13
|
import { IncrementalXapiWriter } from '../_writers/IncrementalXapiWriter.mjs'
|
|
16
14
|
import { Task } from '../../Task.mjs'
|
|
17
15
|
import { watchStreamSize } from '../../_watchStreamSize.mjs'
|
|
16
|
+
import {
|
|
17
|
+
DATETIME,
|
|
18
|
+
DELTA_CHAIN_LENGTH,
|
|
19
|
+
EXPORTED_SUCCESSFULLY,
|
|
20
|
+
setVmDeltaChainLength,
|
|
21
|
+
markExportSuccessfull,
|
|
22
|
+
} from '../../_otherConfig.mjs'
|
|
18
23
|
|
|
19
24
|
const { debug } = createLogger('xo:backups:IncrementalXapiVmBackup')
|
|
20
|
-
|
|
21
25
|
const noop = Function.prototype
|
|
22
26
|
|
|
23
27
|
export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXapi {
|
|
@@ -30,17 +34,14 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
async _copy() {
|
|
33
|
-
const
|
|
37
|
+
const baseVdis = this._baseVdis
|
|
34
38
|
const vm = this._vm
|
|
35
39
|
const exportedVm = this._exportedVm
|
|
36
|
-
const fullVdisRequired = this._fullVdisRequired
|
|
37
|
-
|
|
38
|
-
const isFull = fullVdisRequired === undefined || fullVdisRequired.size !== 0
|
|
39
40
|
|
|
41
|
+
const isFull = Object.values(baseVdis).length === 0 || Object.values(baseVdis).some(_ => !_)
|
|
40
42
|
await this._callWriters(writer => writer.prepare({ isFull }), 'writer.prepare()')
|
|
41
43
|
|
|
42
|
-
const deltaExport = await exportIncrementalVm(exportedVm,
|
|
43
|
-
fullVdisRequired,
|
|
44
|
+
const deltaExport = await exportIncrementalVm(exportedVm, baseVdis, {
|
|
44
45
|
nbdConcurrency: this._settings.nbdConcurrency,
|
|
45
46
|
preferNbd: this._settings.preferNbd,
|
|
46
47
|
})
|
|
@@ -90,18 +91,13 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
|
|
90
91
|
'writer.updateUuidAndChain()'
|
|
91
92
|
)
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
await exportedVm.update_other_config(
|
|
97
|
-
'xo:backup:deltaChainLength',
|
|
98
|
-
String(+(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1)
|
|
99
|
-
)
|
|
100
|
-
}
|
|
94
|
+
if (!isFull) {
|
|
95
|
+
await setVmDeltaChainLength(this._xapi, exportedVm.$ref, (this._deltaChainLength ?? 0) + 1)
|
|
96
|
+
} // on a full the delta chain will be null
|
|
101
97
|
|
|
102
98
|
// not the case if offlineBackup
|
|
103
99
|
if (exportedVm.is_a_snapshot) {
|
|
104
|
-
await
|
|
100
|
+
await markExportSuccessfull(this._xapi, exportedVm.$ref)
|
|
105
101
|
}
|
|
106
102
|
|
|
107
103
|
const size = Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0)
|
|
@@ -119,43 +115,62 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
|
|
119
115
|
async _selectBaseVm() {
|
|
120
116
|
const xapi = this._xapi
|
|
121
117
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
118
|
+
// filter _jobSnapshotVdis to have only the last successfully exported vdi
|
|
119
|
+
// compute delta chain length
|
|
120
|
+
// fill baseUuidToSrcVdi with this data
|
|
121
|
+
|
|
122
|
+
const exportedVdis = this._jobSnapshotVdis.filter(
|
|
123
|
+
_ => EXPORTED_SUCCESSFULLY in _.other_config && DATETIME in _.other_config
|
|
124
|
+
)
|
|
125
|
+
let lastSuccessfullBackup
|
|
126
|
+
let lastExportedVdis = []
|
|
127
|
+
for (const exportedVdi of exportedVdis) {
|
|
128
|
+
if (lastSuccessfullBackup === undefined || lastSuccessfullBackup < exportedVdi.other_config[DATETIME]) {
|
|
129
|
+
lastExportedVdis = []
|
|
130
|
+
lastSuccessfullBackup = exportedVdi.other_config[DATETIME]
|
|
131
|
+
}
|
|
132
|
+
if (lastSuccessfullBackup === exportedVdi.other_config[DATETIME]) {
|
|
133
|
+
lastExportedVdis.push(exportedVdi)
|
|
134
|
+
}
|
|
126
135
|
}
|
|
127
136
|
|
|
137
|
+
this._baseVdis = {}
|
|
138
|
+
if (lastExportedVdis.length === 0) {
|
|
139
|
+
debug('no base VDIS found', {
|
|
140
|
+
jobLength: this._jobSnapshotVdis.length,
|
|
141
|
+
lastSuccessfullBackup,
|
|
142
|
+
exportedLength: exportedVdis.length,
|
|
143
|
+
})
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
const deltaChainLength = Math.max(
|
|
147
|
+
...lastExportedVdis.map(({ other_config }) => Number(other_config[DELTA_CHAIN_LENGTH] ?? 0))
|
|
148
|
+
)
|
|
128
149
|
const fullInterval = this._settings.fullInterval
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
debug('not using base VM becaust fullInterval reached')
|
|
150
|
+
if ( fullInterval !== 0 && fullInterval < deltaChainLength + 1) {
|
|
151
|
+
debug('not using base VM because fullInterval reached', { fullInterval, deltaChainLength, eq: fullInterval < deltaChainLength + 1, dc1: deltaChainLength + 1 })
|
|
132
152
|
return
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
const srcVdis = keyBy(await xapi.getRecords('VDI', await this._vm.$getDisks()), '$ref')
|
|
136
156
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
await asyncMap(await baseVm.$getDisks(), async baseRef => {
|
|
142
|
-
const [baseUuid, snapshotOf] = await Promise.all([
|
|
143
|
-
xapi.getField('VDI', baseRef, 'uuid'),
|
|
144
|
-
xapi.getField('VDI', baseRef, 'snapshot_of'),
|
|
145
|
-
])
|
|
157
|
+
const baseUuidToSrcVdiUuid = new Map()
|
|
158
|
+
for (const lastExportedVdi of lastExportedVdis) {
|
|
159
|
+
const baseUuid = lastExportedVdi.uuid
|
|
160
|
+
const snapshotOf = lastExportedVdi.snapshot_of
|
|
146
161
|
const srcVdi = srcVdis[snapshotOf]
|
|
147
162
|
if (srcVdi !== undefined) {
|
|
148
|
-
|
|
163
|
+
baseUuidToSrcVdiUuid.set(baseUuid, srcVdi.uuid)
|
|
149
164
|
} else {
|
|
150
165
|
debug('ignore snapshot VDI because no longer present on VM', {
|
|
151
166
|
vdi: baseUuid,
|
|
152
167
|
})
|
|
153
168
|
}
|
|
154
|
-
}
|
|
169
|
+
}
|
|
155
170
|
|
|
156
|
-
const presentBaseVdis = new Map(
|
|
171
|
+
const presentBaseVdis = new Map(baseUuidToSrcVdiUuid)
|
|
157
172
|
await this._callWriters(
|
|
158
|
-
writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis
|
|
173
|
+
writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis),
|
|
159
174
|
'writer.checkBaseVdis()',
|
|
160
175
|
false
|
|
161
176
|
)
|
|
@@ -164,24 +179,22 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
|
|
164
179
|
debug('no base VM found')
|
|
165
180
|
return
|
|
166
181
|
}
|
|
167
|
-
|
|
168
|
-
const fullVdisRequired = new Set()
|
|
169
|
-
baseUuidToSrcVdi.forEach((srcVdiUuid, baseUuid) => {
|
|
182
|
+
baseUuidToSrcVdiUuid.forEach((srcVdiUuid, baseUuid) => {
|
|
170
183
|
if (presentBaseVdis.has(baseUuid)) {
|
|
171
184
|
debug('found base VDI', {
|
|
172
185
|
base: baseUuid,
|
|
173
186
|
vdi: srcVdiUuid,
|
|
174
187
|
})
|
|
188
|
+
this._baseVdis[srcVdiUuid] = lastExportedVdis.find(vdi => vdi.uuid === baseUuid)
|
|
175
189
|
} else {
|
|
176
190
|
debug('missing base VDI', {
|
|
177
191
|
base: baseUuid,
|
|
178
192
|
vdi: srcVdiUuid,
|
|
179
193
|
})
|
|
180
|
-
|
|
194
|
+
// this will force a full for this vdi
|
|
195
|
+
this._baseVdis[srcVdiUuid] = undefined
|
|
181
196
|
}
|
|
182
197
|
})
|
|
183
|
-
|
|
184
|
-
this._baseVm = baseVm
|
|
185
|
-
this._fullVdisRequired = fullVdisRequired
|
|
198
|
+
this._deltaChainLength = deltaChainLength
|
|
186
199
|
}
|
|
187
200
|
}
|
|
@@ -4,11 +4,18 @@ import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
|
|
4
4
|
import { asyncMap } from '@xen-orchestra/async-map'
|
|
5
5
|
import { decorateMethodsWith } from '@vates/decorate-with'
|
|
6
6
|
import { defer } from 'golike-defer'
|
|
7
|
-
import { formatDateTime } from '@xen-orchestra/xapi'
|
|
8
7
|
|
|
9
8
|
import { getOldEntries } from '../../_getOldEntries.mjs'
|
|
10
9
|
import { Task } from '../../Task.mjs'
|
|
11
10
|
import { Abstract } from './_Abstract.mjs'
|
|
11
|
+
import {
|
|
12
|
+
DATETIME,
|
|
13
|
+
JOB_ID,
|
|
14
|
+
SCHEDULE_ID,
|
|
15
|
+
populateVdisOtherConfig,
|
|
16
|
+
resetVmOtherConfig,
|
|
17
|
+
setVmOtherConfig,
|
|
18
|
+
} from '../../_otherConfig.mjs'
|
|
12
19
|
|
|
13
20
|
export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
14
21
|
constructor({
|
|
@@ -25,7 +32,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
25
32
|
vm,
|
|
26
33
|
}) {
|
|
27
34
|
super()
|
|
28
|
-
if (vm.other_config[
|
|
35
|
+
if (vm.other_config[JOB_ID] === job.id && 'start' in vm.blocked_operations) {
|
|
29
36
|
// don't match replicated VMs created by this very job otherwise they
|
|
30
37
|
// will be replicated again and again
|
|
31
38
|
throw new Error('cannot backup a VM created by this very job')
|
|
@@ -49,17 +56,17 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
49
56
|
this._exportedVm = undefined
|
|
50
57
|
this._vm = vm
|
|
51
58
|
|
|
52
|
-
this.
|
|
59
|
+
this._baseVdis = undefined
|
|
53
60
|
this._getSnapshotNameLabel = getSnapshotNameLabel
|
|
54
61
|
this._isIncremental = job.mode === 'delta'
|
|
55
62
|
this._healthCheckSr = healthCheckSr
|
|
56
63
|
this._jobId = job.id
|
|
57
|
-
this.
|
|
64
|
+
this._jobSnapshotVdis = undefined
|
|
58
65
|
this._throttleStream = throttleStream
|
|
59
66
|
this._xapi = vm.$xapi
|
|
60
67
|
|
|
61
68
|
// Base VM for the export
|
|
62
|
-
this.
|
|
69
|
+
this._baseVdis = undefined
|
|
63
70
|
|
|
64
71
|
// Settings for this specific run (job, schedule, VM)
|
|
65
72
|
if (tags.includes('xo-memory-backup')) {
|
|
@@ -123,15 +130,8 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
123
130
|
// copied on manual snapshots and interfere with the backup jobs
|
|
124
131
|
async _cleanMetadata() {
|
|
125
132
|
const vm = this._vm
|
|
126
|
-
if (
|
|
127
|
-
await vm
|
|
128
|
-
'xo:backup:datetime': null,
|
|
129
|
-
'xo:backup:deltaChainLength': null,
|
|
130
|
-
'xo:backup:exported': null,
|
|
131
|
-
'xo:backup:job': null,
|
|
132
|
-
'xo:backup:schedule': null,
|
|
133
|
-
'xo:backup:vm': null,
|
|
134
|
-
})
|
|
133
|
+
if (JOB_ID in vm.other_config) {
|
|
134
|
+
await resetVmOtherConfig(this._xapi, vm.$ref)
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -146,6 +146,15 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
146
146
|
if (!settings.bypassVdiChainsCheck) {
|
|
147
147
|
await vm.$assertHealthyVdiChains()
|
|
148
148
|
}
|
|
149
|
+
if (settings.preferNbd) {
|
|
150
|
+
try {
|
|
151
|
+
// enable CBT on all disks if possible
|
|
152
|
+
const diskRefs = await xapi.VM_getDisks(vm.$ref)
|
|
153
|
+
await Promise.all(diskRefs.map(diskRef => xapi.call('VDI.enable_cbt', diskRef)))
|
|
154
|
+
} catch (error) {
|
|
155
|
+
Task.info(`couldn't enable CBT`, error)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
149
158
|
|
|
150
159
|
const snapshotRef = await vm[settings.checkpointSnapshot ? '$checkpoint' : '$snapshot']({
|
|
151
160
|
ignoreNobakVdis: true,
|
|
@@ -153,16 +162,13 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
153
162
|
unplugVusbs: true,
|
|
154
163
|
})
|
|
155
164
|
this.timestamp = Date.now()
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
'xo:backup:vm': vm.uuid,
|
|
165
|
+
await setVmOtherConfig(xapi, snapshotRef, {
|
|
166
|
+
timestamp: this.timestamp,
|
|
167
|
+
jobId: this._jobId,
|
|
168
|
+
scheduleId: this.scheduleId,
|
|
169
|
+
vmUuid: vm.uuid,
|
|
162
170
|
})
|
|
163
|
-
|
|
164
171
|
this._exportedVm = await xapi.getRecord('VM', snapshotRef)
|
|
165
|
-
|
|
166
172
|
return this._exportedVm.uuid
|
|
167
173
|
})
|
|
168
174
|
} else {
|
|
@@ -176,38 +182,116 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
176
182
|
const vmRef = this._vm.$ref
|
|
177
183
|
const xapi = this._xapi
|
|
178
184
|
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
// to ensure compatibility with snapshots older than CBT implementation
|
|
186
|
+
// update vdi data to ensure the vdi are correctly fetched in _jobSnapshotVdis
|
|
187
|
+
// remove by then end of 2024
|
|
188
|
+
const vmSnapshotsRef = await xapi.getField('VM', vmRef, 'snapshots')
|
|
189
|
+
const vmSnapshotsOtherConfig = await asyncMap(vmSnapshotsRef, ref => xapi.getField('VM', ref, 'other_config'))
|
|
181
190
|
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
if (other_config[
|
|
185
|
-
|
|
191
|
+
const vmSnapshots = []
|
|
192
|
+
vmSnapshotsOtherConfig.forEach((other_config, i) => {
|
|
193
|
+
if (other_config[JOB_ID] === jobId) {
|
|
194
|
+
vmSnapshots.push({ other_config, $ref: vmSnapshotsRef[i] })
|
|
186
195
|
}
|
|
187
196
|
})
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
await Promise.all(vmSnapshots.map(snapshot => populateVdisOtherConfig(xapi, snapshot.$ref)))
|
|
198
|
+
// end of compatibiliy handling
|
|
199
|
+
|
|
200
|
+
// handle snapshot by VDI
|
|
201
|
+
this._jobSnapshotVdis = []
|
|
202
|
+
const srcVdis = await xapi.getRecords('VDI', await this._vm.$getDisks())
|
|
203
|
+
for (const srcVdi of srcVdis) {
|
|
204
|
+
const snapshots = await xapi.getRecords('VDI', srcVdi.snapshots)
|
|
205
|
+
for (const snapshot of snapshots) {
|
|
206
|
+
if (snapshot.other_config[JOB_ID] === jobId) {
|
|
207
|
+
this._jobSnapshotVdis.push(snapshot)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
190
211
|
}
|
|
191
212
|
|
|
192
213
|
async _removeUnusedSnapshots() {
|
|
193
214
|
const allSettings = this.job.settings
|
|
194
215
|
const baseSettings = this._baseSettings
|
|
195
|
-
const baseVmRef = this._baseVm?.$ref
|
|
196
216
|
|
|
197
|
-
const snapshotsPerSchedule = groupBy(this.
|
|
217
|
+
const snapshotsPerSchedule = groupBy(this._jobSnapshotVdis, _ => _.other_config[SCHEDULE_ID])
|
|
198
218
|
const xapi = this._xapi
|
|
199
|
-
await asyncMap(Object.entries(snapshotsPerSchedule), ([scheduleId, snapshots]) => {
|
|
219
|
+
await asyncMap(Object.entries(snapshotsPerSchedule), async ([scheduleId, snapshots]) => {
|
|
220
|
+
const snapshotPerDatetime = groupBy(snapshots, _ => _.other_config[DATETIME])
|
|
221
|
+
|
|
222
|
+
const datetimes = Object.keys(snapshotPerDatetime)
|
|
223
|
+
datetimes.sort()
|
|
224
|
+
|
|
200
225
|
const settings = {
|
|
201
226
|
...baseSettings,
|
|
202
227
|
...allSettings[scheduleId],
|
|
203
228
|
...allSettings[this._vm.uuid],
|
|
204
229
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
230
|
+
// ensure we never delete the last one
|
|
231
|
+
const retention = Math.max(settings.snapshotRetention ?? 0, 1)
|
|
232
|
+
|
|
233
|
+
await asyncMap(getOldEntries(retention, datetimes), async datetime => {
|
|
234
|
+
const vdis = snapshotPerDatetime[datetime]
|
|
235
|
+
|
|
236
|
+
let vmRef
|
|
237
|
+
// if there is an attached VM => destroy the VM (Non CBT backups)
|
|
238
|
+
for (const vdi of vdis) {
|
|
239
|
+
if (vdi.$VBDs.length > 0) {
|
|
240
|
+
const vbds = vdi.$VBDs
|
|
241
|
+
// only one VM linked to this vdi
|
|
242
|
+
// this will throw error for VDI still attached to control domain
|
|
243
|
+
assert.strictEqual(vbds.length, 1, 'VDI must be free or attached to exactly one VM')
|
|
244
|
+
const vm = vbds[0].$VM
|
|
245
|
+
assert.strictEqual(vm.is_control_domain, false, `Disk is still attached to DOM0 VM`) // don't delete a VM (especially a control domain)
|
|
246
|
+
assert.strictEqual(vm.is_a_snapshot, true, `VM must be a snapshot`) // don't delete a VM (especially a control domain)
|
|
247
|
+
|
|
248
|
+
const vmRefVdi = vm.$ref
|
|
249
|
+
// same vm than other vdi of the same batch
|
|
250
|
+
assert.ok(
|
|
251
|
+
vmRef === undefined || vmRef === vmRefVdi,
|
|
252
|
+
'_removeUnusedSnapshots don t handle vdi related to multiple VMs '
|
|
253
|
+
)
|
|
254
|
+
vmRef = vmRefVdi
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (vmRef !== undefined) {
|
|
258
|
+
return xapi.VM_destroy(vmRef)
|
|
259
|
+
} else {
|
|
260
|
+
return asyncMap(
|
|
261
|
+
vdis.map(async ({ $ref }) => {
|
|
262
|
+
await xapi.VDI_destroy($ref)
|
|
263
|
+
})
|
|
264
|
+
)
|
|
208
265
|
}
|
|
209
266
|
})
|
|
210
267
|
})
|
|
268
|
+
|
|
269
|
+
// now that we use CBT, we can destroy the data of the snapshot used for this backup
|
|
270
|
+
// going back to a previous version of XO not supporting CBT will create a full backup
|
|
271
|
+
// this will only do something after snapshot and transfer
|
|
272
|
+
if (
|
|
273
|
+
// don't modify the VM
|
|
274
|
+
this._exportedVm?.is_a_snapshot &&
|
|
275
|
+
// user don't want to keep the snapshot data
|
|
276
|
+
this._settings.snapshotRetention === 0 &&
|
|
277
|
+
// preferNbd is not a guarantee that the backup used NBD, depending on the network configuration
|
|
278
|
+
this._settings.preferNbd &&
|
|
279
|
+
// only delete snapshost data if the config allows it
|
|
280
|
+
this.config.purgeSnapshotData
|
|
281
|
+
) {
|
|
282
|
+
Task.info('will delete snapshot data')
|
|
283
|
+
const vdiRefs = await this._xapi.VM_getDisks(this._exportedVm?.$ref)
|
|
284
|
+
await xapi.call('VM.destroy', this._exportedVm.$ref)
|
|
285
|
+
for (const vdiRef of vdiRefs) {
|
|
286
|
+
try {
|
|
287
|
+
// data_destroy will fail with a VDI_NO_CBT_METADATA error if CBT is not enabled on this VDI
|
|
288
|
+
await xapi.VDI_dataDestroy(vdiRef)
|
|
289
|
+
Task.info(`Snapshot data has been deleted`, { vdiRef })
|
|
290
|
+
} catch (error) {
|
|
291
|
+
Task.warning(`Couldn't deleted snapshot data`, { error, vdiRef })
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
211
295
|
}
|
|
212
296
|
|
|
213
297
|
async copy() {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
|
2
2
|
import { asyncMap, asyncMapSettled } from '@xen-orchestra/async-map'
|
|
3
|
-
import { formatDateTime } from '@xen-orchestra/xapi'
|
|
4
3
|
|
|
5
4
|
import { formatFilenameDate } from '../../_filenameDate.mjs'
|
|
6
5
|
import { getOldEntries } from '../../_getOldEntries.mjs'
|
|
@@ -9,6 +8,7 @@ import { Task } from '../../Task.mjs'
|
|
|
9
8
|
import { AbstractFullWriter } from './_AbstractFullWriter.mjs'
|
|
10
9
|
import { MixinXapiWriter } from './_MixinXapiWriter.mjs'
|
|
11
10
|
import { listReplicatedVms } from './_listReplicatedVms.mjs'
|
|
11
|
+
import { setVmOtherConfig } from '../../_otherConfig.mjs'
|
|
12
12
|
|
|
13
13
|
export class FullXapiWriter extends MixinXapiWriter(AbstractFullWriter) {
|
|
14
14
|
constructor(props) {
|
|
@@ -76,14 +76,12 @@ export class FullXapiWriter extends MixinXapiWriter(AbstractFullWriter) {
|
|
|
76
76
|
'Start operation for this vm is blocked, clone it if you want to use it.'
|
|
77
77
|
)
|
|
78
78
|
),
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
'xo:backup:schedule': scheduleId,
|
|
86
|
-
'xo:backup:vm': vm.uuid,
|
|
79
|
+
setVmOtherConfig(xapi, targetVmRef, {
|
|
80
|
+
timestamp,
|
|
81
|
+
jobId: job.id,
|
|
82
|
+
scheduleId,
|
|
83
|
+
srUuid,
|
|
84
|
+
vmUuid: vm.uuid,
|
|
87
85
|
}),
|
|
88
86
|
])
|
|
89
87
|
|
|
@@ -1,39 +1,34 @@
|
|
|
1
|
-
import assert from 'node:assert'
|
|
2
1
|
import { asyncMap, asyncMapSettled } from '@xen-orchestra/async-map'
|
|
3
2
|
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
|
4
|
-
import { formatDateTime } from '@xen-orchestra/xapi'
|
|
5
3
|
|
|
6
4
|
import { formatFilenameDate } from '../../_filenameDate.mjs'
|
|
7
5
|
import { getOldEntries } from '../../_getOldEntries.mjs'
|
|
8
|
-
import { importIncrementalVm
|
|
6
|
+
import { importIncrementalVm } from '../../_incrementalVm.mjs'
|
|
9
7
|
import { Task } from '../../Task.mjs'
|
|
10
8
|
|
|
11
9
|
import { AbstractIncrementalWriter } from './_AbstractIncrementalWriter.mjs'
|
|
12
10
|
import { MixinXapiWriter } from './_MixinXapiWriter.mjs'
|
|
13
11
|
import { listReplicatedVms } from './_listReplicatedVms.mjs'
|
|
14
|
-
import
|
|
12
|
+
import { COPY_OF, setVmOtherConfig, BASE_DELTA_VDI } from '../../_otherConfig.mjs'
|
|
15
13
|
|
|
14
|
+
import assert from 'node:assert'
|
|
16
15
|
export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
|
|
17
|
-
async checkBaseVdis(baseUuidToSrcVdi
|
|
18
|
-
assert.notStrictEqual(baseVm, undefined)
|
|
16
|
+
async checkBaseVdis(baseUuidToSrcVdi) {
|
|
19
17
|
const sr = this._sr
|
|
20
|
-
const replicatedVm = listReplicatedVms(sr.$xapi, this._job.id, sr.uuid, this._vmUuid).find(
|
|
21
|
-
vm => vm.other_config[TAG_COPY_SRC] === baseVm.uuid
|
|
22
|
-
)
|
|
23
|
-
if (replicatedVm === undefined) {
|
|
24
|
-
return baseUuidToSrcVdi.clear()
|
|
25
|
-
}
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
// @todo use an index if possible
|
|
20
|
+
// @todo : this seems similare to decorateVmMetadata
|
|
21
|
+
|
|
22
|
+
const replicatedVdis = sr.$VDIs
|
|
23
|
+
.filter(({ other_config }) => {
|
|
24
|
+
// REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
|
|
25
|
+
return baseUuidToSrcVdi.has(other_config?.[COPY_OF])
|
|
32
26
|
})
|
|
33
|
-
|
|
27
|
+
.map(({ other_config }) => other_config?.[COPY_OF])
|
|
28
|
+
.filter(_ => !!_)
|
|
34
29
|
|
|
35
30
|
for (const uuid of baseUuidToSrcVdi.keys()) {
|
|
36
|
-
if (!replicatedVdis.
|
|
31
|
+
if (!replicatedVdis.includes(uuid)) {
|
|
37
32
|
baseUuidToSrcVdi.delete(uuid)
|
|
38
33
|
}
|
|
39
34
|
}
|
|
@@ -89,46 +84,38 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
89
84
|
#decorateVmMetadata(backup) {
|
|
90
85
|
const { _warmMigration } = this._settings
|
|
91
86
|
const sr = this._sr
|
|
92
|
-
const xapi = sr.$xapi
|
|
93
87
|
const vm = backup.vm
|
|
94
|
-
vm.other_config[TAG_COPY_SRC] = vm.uuid
|
|
95
|
-
const remoteBaseVmUuid = vm.other_config[TAG_BASE_DELTA]
|
|
96
|
-
let baseVm
|
|
97
|
-
if (remoteBaseVmUuid) {
|
|
98
|
-
baseVm = find(
|
|
99
|
-
xapi.objects.all,
|
|
100
|
-
obj => (obj = obj.other_config) && obj[TAG_COPY_SRC] === remoteBaseVmUuid && obj[TAG_BACKUP_SR] === sr.$id
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
if (!baseVm) {
|
|
104
|
-
throw new Error(`could not find the base VM (copy of ${remoteBaseVmUuid})`)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
const baseVdis = {}
|
|
108
|
-
baseVm?.$VBDs.forEach(vbd => {
|
|
109
|
-
const vdi = vbd.$VDI
|
|
110
|
-
if (vdi !== undefined) {
|
|
111
|
-
baseVdis[vbd.VDI] = vbd.$VDI
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
88
|
|
|
115
|
-
vm.other_config[
|
|
89
|
+
vm.other_config[COPY_OF] = vm.uuid
|
|
116
90
|
if (!_warmMigration) {
|
|
117
91
|
vm.tags.push('Continuous Replication')
|
|
118
92
|
}
|
|
93
|
+
// extracting the uuid of each delta vdi on the source
|
|
94
|
+
// get all in one pass, since there is a lot of objects
|
|
95
|
+
const sourceVdiUuids = Object.values(backup.vdis)
|
|
96
|
+
.map(({ other_config }) => other_config[BASE_DELTA_VDI])
|
|
97
|
+
// full vdi don't have a base
|
|
98
|
+
.filter(_ => !!_)
|
|
99
|
+
// @todo use index ?
|
|
100
|
+
|
|
101
|
+
const replicatedVdis = sr.$VDIs.filter(({ other_config }) => {
|
|
102
|
+
// REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
|
|
103
|
+
return sourceVdiUuids.includes(other_config?.[COPY_OF])
|
|
104
|
+
})
|
|
119
105
|
|
|
120
106
|
Object.values(backup.vdis).forEach(vdi => {
|
|
121
|
-
vdi.other_config[
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
vdi.baseVdi = baseVdi
|
|
107
|
+
vdi.other_config[COPY_OF] = vdi.uuid
|
|
108
|
+
if (sourceVdiUuids.length > 0) {
|
|
109
|
+
const baseReplicatedTo = replicatedVdis.find(
|
|
110
|
+
replicatedVdi => replicatedVdi.other_config[COPY_OF] === vdi.other_config[BASE_DELTA_VDI]
|
|
111
|
+
)
|
|
112
|
+
assert.notStrictEqual(baseReplicatedTo, undefined)
|
|
113
|
+
vdi.baseVdi = baseReplicatedTo
|
|
114
|
+
} else {
|
|
115
|
+
vdi.baseVdi = undefined
|
|
131
116
|
}
|
|
117
|
+
// ensure the VDI are created on the target SR
|
|
118
|
+
vdi.SR = sr.$ref
|
|
132
119
|
})
|
|
133
120
|
|
|
134
121
|
return backup
|
|
@@ -164,14 +151,12 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
164
151
|
'Start operation for this vm is blocked, clone it if you want to use it.'
|
|
165
152
|
)
|
|
166
153
|
),
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
'xo:backup:schedule': scheduleId,
|
|
174
|
-
[TAG_BASE_DELTA]: vm.uuid,
|
|
154
|
+
setVmOtherConfig(xapi, targetVmRef, {
|
|
155
|
+
timestamp,
|
|
156
|
+
jobId: job.id,
|
|
157
|
+
scheduleId,
|
|
158
|
+
vmUuid: vm.uuid,
|
|
159
|
+
srUuid,
|
|
175
160
|
}),
|
|
176
161
|
])
|
|
177
162
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { DATETIME, JOB_ID, REPLICATED_TO_SR_UUID, SCHEDULE_ID, VM_UUID } from '../../_otherConfig.mjs'
|
|
2
|
+
|
|
1
3
|
const getReplicatedVmDatetime = vm => {
|
|
2
|
-
const {
|
|
4
|
+
const { [DATETIME]: datetime = vm.name_label.slice(-17, -1) } = vm.other_config
|
|
3
5
|
return datetime
|
|
4
6
|
}
|
|
5
7
|
|
|
@@ -16,11 +18,11 @@ export function listReplicatedVms(xapi, scheduleOrJobId, srUuid, vmUuid) {
|
|
|
16
18
|
!object.is_a_snapshot &&
|
|
17
19
|
!object.is_a_template &&
|
|
18
20
|
'start' in object.blocked_operations &&
|
|
19
|
-
(oc[
|
|
20
|
-
oc[
|
|
21
|
-
(oc[
|
|
21
|
+
(oc[JOB_ID] === scheduleOrJobId || oc[SCHEDULE_ID] === scheduleOrJobId) &&
|
|
22
|
+
oc[REPLICATED_TO_SR_UUID] === srUuid &&
|
|
23
|
+
(oc[VM_UUID] === vmUuid ||
|
|
22
24
|
// 2018-03-28, JFT: to catch VMs replicated before this fix
|
|
23
|
-
oc[
|
|
25
|
+
oc[VM_UUID] === undefined)
|
|
24
26
|
) {
|
|
25
27
|
vms[object.$id] = object
|
|
26
28
|
}
|
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.50.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@vates/compose": "^2.1.0",
|
|
25
25
|
"@vates/decorate-with": "^2.1.0",
|
|
26
26
|
"@vates/disposable": "^0.1.5",
|
|
27
|
-
"@vates/fuse-vhd": "^2.1.
|
|
27
|
+
"@vates/fuse-vhd": "^2.1.1",
|
|
28
28
|
"@vates/nbd-client": "^3.0.2",
|
|
29
29
|
"@vates/parse-duration": "^0.1.1",
|
|
30
30
|
"@xen-orchestra/async-map": "^0.1.2",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"proper-lockfile": "^4.1.2",
|
|
47
47
|
"tar": "^6.1.15",
|
|
48
48
|
"uuid": "^9.0.0",
|
|
49
|
-
"vhd-lib": "^4.
|
|
49
|
+
"vhd-lib": "^4.10.0",
|
|
50
50
|
"xen-api": "^4.0.0",
|
|
51
51
|
"yazl": "^2.5.1"
|
|
52
52
|
},
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"tmp": "^0.2.1"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@xen-orchestra/xapi": "^6.
|
|
61
|
+
"@xen-orchestra/xapi": "^6.1.0"
|
|
62
62
|
},
|
|
63
63
|
"license": "AGPL-3.0-or-later",
|
|
64
64
|
"author": {
|