@xen-orchestra/backups 0.49.1 → 0.51.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/_vmRunners/IncrementalXapi.mjs +58 -45
- package/_runners/_vmRunners/_AbstractXapi.mjs +84 -35
- package/_runners/_writers/FullXapiWriter.mjs +7 -9
- package/_runners/_writers/IncrementalXapiWriter.mjs +54 -64
- 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
|
+
}
|
|
@@ -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
|
|
|
@@ -148,19 +148,17 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
const snapshotRef = await vm[settings.checkpointSnapshot ? '$checkpoint' : '$snapshot']({
|
|
151
|
-
|
|
151
|
+
ignoredVdisTag: '[NOBAK]',
|
|
152
152
|
name_label: this._getSnapshotNameLabel(vm),
|
|
153
153
|
unplugVusbs: true,
|
|
154
154
|
})
|
|
155
155
|
this.timestamp = Date.now()
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
'xo:backup:vm': vm.uuid,
|
|
156
|
+
await setVmOtherConfig(xapi, snapshotRef, {
|
|
157
|
+
timestamp: this.timestamp,
|
|
158
|
+
jobId: this._jobId,
|
|
159
|
+
scheduleId: this.scheduleId,
|
|
160
|
+
vmUuid: vm.uuid,
|
|
162
161
|
})
|
|
163
|
-
|
|
164
162
|
this._exportedVm = await xapi.getRecord('VM', snapshotRef)
|
|
165
163
|
|
|
166
164
|
return this._exportedVm.uuid
|
|
@@ -176,35 +174,86 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
176
174
|
const vmRef = this._vm.$ref
|
|
177
175
|
const xapi = this._xapi
|
|
178
176
|
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
// to ensure compatibility with snapshots older than CBT implementation
|
|
178
|
+
// update vdi data to ensure the vdi are correctly fetched in _jobSnapshotVdis
|
|
179
|
+
// remove by then end of 2024
|
|
180
|
+
const vmSnapshotsRef = await xapi.getField('VM', vmRef, 'snapshots')
|
|
181
|
+
const vmSnapshotsOtherConfig = await asyncMap(vmSnapshotsRef, ref => xapi.getField('VM', ref, 'other_config'))
|
|
181
182
|
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
if (other_config[
|
|
185
|
-
|
|
183
|
+
const vmSnapshots = []
|
|
184
|
+
vmSnapshotsOtherConfig.forEach((other_config, i) => {
|
|
185
|
+
if (other_config[JOB_ID] === jobId) {
|
|
186
|
+
vmSnapshots.push({ other_config, $ref: vmSnapshotsRef[i] })
|
|
186
187
|
}
|
|
187
188
|
})
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
await Promise.all(vmSnapshots.map(snapshot => populateVdisOtherConfig(xapi, snapshot.$ref)))
|
|
190
|
+
// end of compatibiliy handling
|
|
191
|
+
|
|
192
|
+
// handle snapshot by VDI
|
|
193
|
+
this._jobSnapshotVdis = []
|
|
194
|
+
const srcVdis = await xapi.getRecords('VDI', await this._vm.$getDisks())
|
|
195
|
+
for (const srcVdi of srcVdis) {
|
|
196
|
+
const snapshots = await xapi.getRecords('VDI', srcVdi.snapshots)
|
|
197
|
+
for (const snapshot of snapshots) {
|
|
198
|
+
if (snapshot.other_config[JOB_ID] === jobId) {
|
|
199
|
+
this._jobSnapshotVdis.push(snapshot)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
190
203
|
}
|
|
191
204
|
|
|
192
205
|
async _removeUnusedSnapshots() {
|
|
193
206
|
const allSettings = this.job.settings
|
|
194
207
|
const baseSettings = this._baseSettings
|
|
195
|
-
const baseVmRef = this._baseVm?.$ref
|
|
196
208
|
|
|
197
|
-
const snapshotsPerSchedule = groupBy(this.
|
|
209
|
+
const snapshotsPerSchedule = groupBy(this._jobSnapshotVdis, _ => _.other_config[SCHEDULE_ID])
|
|
198
210
|
const xapi = this._xapi
|
|
199
211
|
await asyncMap(Object.entries(snapshotsPerSchedule), ([scheduleId, snapshots]) => {
|
|
212
|
+
const snapshotPerDatetime = groupBy(snapshots, _ => _.other_config[DATETIME])
|
|
213
|
+
|
|
214
|
+
const datetimes = Object.keys(snapshotPerDatetime)
|
|
215
|
+
datetimes.sort()
|
|
216
|
+
|
|
200
217
|
const settings = {
|
|
201
218
|
...baseSettings,
|
|
202
219
|
...allSettings[scheduleId],
|
|
203
220
|
...allSettings[this._vm.uuid],
|
|
204
221
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
222
|
+
// ensure we never delete the last one
|
|
223
|
+
const retention = Math.max(settings.snapshotRetention ?? 0, 1)
|
|
224
|
+
|
|
225
|
+
return asyncMap(getOldEntries(retention, datetimes), async datetime => {
|
|
226
|
+
const vdis = snapshotPerDatetime[datetime]
|
|
227
|
+
|
|
228
|
+
let vmRef
|
|
229
|
+
// if there is an attached VM => destroy the VM (Non CBT backups)
|
|
230
|
+
for (const vdi of vdis) {
|
|
231
|
+
if (vdi.$VBDs.length > 0) {
|
|
232
|
+
const vbds = vdi.$VBDs
|
|
233
|
+
// only one VM linked to this vdi
|
|
234
|
+
// this will throw error for VDI still attached to control domain
|
|
235
|
+
assert.strictEqual(vbds.length, 1, 'VDI must be free or attached to exactly one VM')
|
|
236
|
+
const vm = vbds[0].$VM
|
|
237
|
+
assert.strictEqual(vm.is_a_snapshot, true, `VM must be a snapshot`) // don't delete a VM (especially a control domain)
|
|
238
|
+
assert.strictEqual(vm.is_control_domain, false, `VM can't be a DOM0 VM`) // don't delete a VM (especially a control domain)
|
|
239
|
+
|
|
240
|
+
const vmRefVdi = vm.$ref
|
|
241
|
+
// same vm than other vdi of the same batch
|
|
242
|
+
assert.ok(
|
|
243
|
+
vmRef === undefined || vmRef === vmRefVdi,
|
|
244
|
+
'_removeUnusedSnapshots don t handle vdi related to multiple VMs '
|
|
245
|
+
)
|
|
246
|
+
vmRef = vmRefVdi
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (vmRef !== undefined) {
|
|
250
|
+
return xapi.VM_destroy(vmRef)
|
|
251
|
+
} else {
|
|
252
|
+
return asyncMap(
|
|
253
|
+
vdis.map(async ({ $ref }) => {
|
|
254
|
+
await xapi.VDI_destroy($ref)
|
|
255
|
+
})
|
|
256
|
+
)
|
|
208
257
|
}
|
|
209
258
|
})
|
|
210
259
|
})
|
|
@@ -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
|
}
|
|
@@ -57,10 +52,10 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
57
52
|
this.cleanup = task.wrapFn(this.cleanup, !hasHealthCheckSr)
|
|
58
53
|
this.healthCheck = task.wrapFn(this.healthCheck, hasHealthCheckSr)
|
|
59
54
|
|
|
60
|
-
return task.run(() => this._prepare())
|
|
55
|
+
return task.run(() => this._prepare(isFull))
|
|
61
56
|
}
|
|
62
57
|
|
|
63
|
-
async _prepare() {
|
|
58
|
+
async _prepare(isFull) {
|
|
64
59
|
const settings = this._settings
|
|
65
60
|
const { uuid: srUuid, $xapi: xapi } = this._sr
|
|
66
61
|
const vmUuid = this._vmUuid
|
|
@@ -72,14 +67,19 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
72
67
|
this._oldEntries = getOldEntries(settings.copyRetention - 1, listReplicatedVms(xapi, scheduleId, srUuid, vmUuid))
|
|
73
68
|
|
|
74
69
|
if (settings.deleteFirst) {
|
|
70
|
+
// we want to keep the baseVM when copying a delta
|
|
71
|
+
// even if we want to keep only one after
|
|
72
|
+
let mostRecentEntry
|
|
73
|
+
if (this._oldEntries.length > 1 && settings.copyRetention === 1 && !isFull) {
|
|
74
|
+
mostRecentEntry = this._oldEntries.pop()
|
|
75
|
+
}
|
|
75
76
|
await this._deleteOldEntries()
|
|
77
|
+
this._oldEntries = mostRecentEntry !== undefined ? [mostRecentEntry] : []
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
async cleanup() {
|
|
80
|
-
|
|
81
|
-
await this._deleteOldEntries()
|
|
82
|
-
}
|
|
82
|
+
await this._deleteOldEntries()
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
async _deleteOldEntries() {
|
|
@@ -89,46 +89,38 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
89
89
|
#decorateVmMetadata(backup) {
|
|
90
90
|
const { _warmMigration } = this._settings
|
|
91
91
|
const sr = this._sr
|
|
92
|
-
const xapi = sr.$xapi
|
|
93
92
|
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
93
|
|
|
115
|
-
vm.other_config[
|
|
94
|
+
vm.other_config[COPY_OF] = vm.uuid
|
|
116
95
|
if (!_warmMigration) {
|
|
117
96
|
vm.tags.push('Continuous Replication')
|
|
118
97
|
}
|
|
98
|
+
// extracting the uuid of each delta vdi on the source
|
|
99
|
+
// get all in one pass, since there is a lot of objects
|
|
100
|
+
const sourceVdiUuids = Object.values(backup.vdis)
|
|
101
|
+
.map(({ other_config }) => other_config[BASE_DELTA_VDI])
|
|
102
|
+
// full vdi don't have a base
|
|
103
|
+
.filter(_ => !!_)
|
|
104
|
+
// @todo use index ?
|
|
105
|
+
|
|
106
|
+
const replicatedVdis = sr.$VDIs.filter(({ other_config }) => {
|
|
107
|
+
// REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
|
|
108
|
+
return sourceVdiUuids.includes(other_config?.[COPY_OF])
|
|
109
|
+
})
|
|
119
110
|
|
|
120
111
|
Object.values(backup.vdis).forEach(vdi => {
|
|
121
|
-
vdi.other_config[
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
vdi.baseVdi = baseVdi
|
|
112
|
+
vdi.other_config[COPY_OF] = vdi.uuid
|
|
113
|
+
if (sourceVdiUuids.length > 0) {
|
|
114
|
+
const baseReplicatedTo = replicatedVdis.find(
|
|
115
|
+
replicatedVdi => replicatedVdi.other_config[COPY_OF] === vdi.other_config[BASE_DELTA_VDI]
|
|
116
|
+
)
|
|
117
|
+
assert.notStrictEqual(baseReplicatedTo, undefined)
|
|
118
|
+
vdi.baseVdi = baseReplicatedTo
|
|
119
|
+
} else {
|
|
120
|
+
vdi.baseVdi = undefined
|
|
131
121
|
}
|
|
122
|
+
// ensure the VDI are created on the target SR
|
|
123
|
+
vdi.SR = sr.$ref
|
|
132
124
|
})
|
|
133
125
|
|
|
134
126
|
return backup
|
|
@@ -164,14 +156,12 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
164
156
|
'Start operation for this vm is blocked, clone it if you want to use it.'
|
|
165
157
|
)
|
|
166
158
|
),
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
'xo:backup:schedule': scheduleId,
|
|
174
|
-
[TAG_BASE_DELTA]: vm.uuid,
|
|
159
|
+
setVmOtherConfig(xapi, targetVmRef, {
|
|
160
|
+
timestamp,
|
|
161
|
+
jobId: job.id,
|
|
162
|
+
scheduleId,
|
|
163
|
+
vmUuid: vm.uuid,
|
|
164
|
+
srUuid,
|
|
175
165
|
}),
|
|
176
166
|
])
|
|
177
167
|
}
|
|
@@ -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.51.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": "^
|
|
61
|
+
"@xen-orchestra/xapi": "^7.0.0"
|
|
62
62
|
},
|
|
63
63
|
"license": "AGPL-3.0-or-later",
|
|
64
64
|
"author": {
|