@xen-orchestra/backups 0.72.0 → 0.73.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/ImportVmBackup.mjs +1 -1
- package/RemoteAdapter.mjs +11 -8
- package/_otherConfig.mjs +2 -0
- package/_runners/_vmRunners/IncrementalXapi.mjs +115 -41
- package/_runners/_vmRunners/_AbstractXapi.mjs +69 -28
- package/_runners/_writers/IncrementalRemoteWriter.mjs +1 -2
- package/_runners/_writers/IncrementalXapiWriter.mjs +172 -77
- package/_runners/_writers/_MixinRemoteWriter.mjs +0 -2
- package/package.json +8 -6
- package/tests.fixtures.d.mts +47 -0
- package/_cleanVm.mjs +0 -628
- package/disks/MergeRemoteDisk.mjs +0 -325
- package/disks/RemoteDisk.mjs +0 -223
- package/disks/RemoteVhdDisk.mjs +0 -480
- package/disks/RemoteVhdDiskChain.mjs +0 -304
- package/disks/index.mjs +0 -49
- package/disks/openDiskChain.mjs +0 -40
package/ImportVmBackup.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { dirname, join } from 'node:path'
|
|
|
10
10
|
import pickBy from 'lodash/pickBy.js'
|
|
11
11
|
import { defer } from 'golike-defer'
|
|
12
12
|
import { NegativeDisk } from '@xen-orchestra/disk-transform'
|
|
13
|
-
import { openDiskChain } from '
|
|
13
|
+
import { openDiskChain } from '@xen-orchestra/backup-archive/disks'
|
|
14
14
|
import { resetVmOtherConfig } from './_otherConfig.mjs'
|
|
15
15
|
|
|
16
16
|
const { debug, info, warn } = createLogger('xo:backups:importVmBackup')
|
package/RemoteAdapter.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import * as tar from 'tar'
|
|
|
22
22
|
import zlib from 'zlib'
|
|
23
23
|
|
|
24
24
|
import { BACKUP_DIR } from './_getVmBackupDir.mjs'
|
|
25
|
-
import {
|
|
25
|
+
import { VmBackupDirectory } from '@xen-orchestra/backup-archive'
|
|
26
26
|
import { formatFilenameDate } from './_filenameDate.mjs'
|
|
27
27
|
import { getTmpDir } from './_getTmpDir.mjs'
|
|
28
28
|
import { isMetadataFile } from './_backupType.mjs'
|
|
@@ -31,8 +31,7 @@ import { listPartitions, LVM_PARTITION_TYPE_MBR, LVM_PARTITION_TYPE_GPT } from '
|
|
|
31
31
|
import { lvs, pvs } from './_lvm.mjs'
|
|
32
32
|
import { watchStreamSize } from './_watchStreamSize.mjs'
|
|
33
33
|
|
|
34
|
-
import { RemoteVhdDisk } from '
|
|
35
|
-
import { openDiskChain } from './disks/openDiskChain.mjs'
|
|
34
|
+
import { RemoteVhdDisk, openDiskChain } from '@xen-orchestra/backup-archive/disks'
|
|
36
35
|
import { toVhdStream, writeToVhdDirectory } from 'vhd-lib/disk-consumer/index.mjs'
|
|
37
36
|
import { ReadAhead } from '@xen-orchestra/disk-transform'
|
|
38
37
|
|
|
@@ -702,7 +701,7 @@ export class RemoteAdapter {
|
|
|
702
701
|
const handler = this._handler
|
|
703
702
|
|
|
704
703
|
if (this.useVhdDirectory()) {
|
|
705
|
-
await writeToVhdDirectory({
|
|
704
|
+
return await writeToVhdDirectory({
|
|
706
705
|
disk,
|
|
707
706
|
target: {
|
|
708
707
|
handler,
|
|
@@ -714,8 +713,9 @@ export class RemoteAdapter {
|
|
|
714
713
|
})
|
|
715
714
|
} else {
|
|
716
715
|
const stream = await toVhdStream(disk)
|
|
717
|
-
await this.outputStream(path, stream, { validator, checksum: false })
|
|
716
|
+
const size = await this.outputStream(path, stream, { validator, checksum: false })
|
|
718
717
|
await validator(path)
|
|
718
|
+
return size
|
|
719
719
|
}
|
|
720
720
|
}
|
|
721
721
|
|
|
@@ -878,11 +878,14 @@ export class RemoteAdapter {
|
|
|
878
878
|
}
|
|
879
879
|
|
|
880
880
|
Object.assign(RemoteAdapter.prototype, {
|
|
881
|
-
cleanVm(
|
|
881
|
+
cleanVm(vmBackupPath, opts = {}) {
|
|
882
|
+
const { lock = true, ...cleanOpts } = opts
|
|
882
883
|
if (lock) {
|
|
883
|
-
return Disposable.use(this._handler.lock(
|
|
884
|
+
return Disposable.use(this._handler.lock(vmBackupPath), () => {
|
|
885
|
+
return VmBackupDirectory.cleanVm(this._handler, vmBackupPath, cleanOpts)
|
|
886
|
+
})
|
|
884
887
|
} else {
|
|
885
|
-
return cleanVm
|
|
888
|
+
return VmBackupDirectory.cleanVm(this._handler, vmBackupPath, cleanOpts)
|
|
886
889
|
}
|
|
887
890
|
},
|
|
888
891
|
isValidXva,
|
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
|
+
[CONTENT_KEY]: null,
|
|
88
89
|
[COPY_OF]: null,
|
|
89
90
|
[DATETIME]: null,
|
|
90
91
|
[DELTA_CHAIN_LENGTH]: null,
|
|
@@ -92,6 +93,7 @@ export function resetVmOtherConfig(xapi, vmRef) {
|
|
|
92
93
|
[JOB_ID]: null,
|
|
93
94
|
[SCHEDULE_ID]: null,
|
|
94
95
|
[VM_UUID]: null,
|
|
96
|
+
|
|
95
97
|
// REPLICATED_TO_SR_UUID is not reset since we can replicate a replication
|
|
96
98
|
})
|
|
97
99
|
})
|
|
@@ -8,6 +8,7 @@ import { exportIncrementalVm } from '../../_incrementalVm.mjs'
|
|
|
8
8
|
import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
|
|
9
9
|
import { IncrementalXapiWriter } from '../_writers/IncrementalXapiWriter.mjs'
|
|
10
10
|
import {
|
|
11
|
+
CONTENT_KEY,
|
|
11
12
|
DATETIME,
|
|
12
13
|
DELTA_CHAIN_LENGTH,
|
|
13
14
|
EXPORTED_SUCCESSFULLY,
|
|
@@ -101,63 +102,136 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
|
|
101
102
|
await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Groups snapshot VDIs by their DATETIME other_config value and sorts them most-recent-first.
|
|
107
|
+
*
|
|
108
|
+
* @param {import('@vates/types').XenApiVdi[]} snapshotVdis
|
|
109
|
+
* @returns {{ datetime: string, vdis: import('@vates/types').XenApiVdi[] }[]}
|
|
110
|
+
*/
|
|
111
|
+
_groupSnapshotVdisByDatetime(snapshotVdis) {
|
|
112
|
+
const byDatetime = new Map()
|
|
113
|
+
for (const vdi of snapshotVdis) {
|
|
114
|
+
const datetime = vdi.other_config[DATETIME]
|
|
115
|
+
if (!byDatetime.has(datetime)) {
|
|
116
|
+
byDatetime.set(datetime, [])
|
|
117
|
+
}
|
|
118
|
+
byDatetime.get(datetime).push(vdi)
|
|
119
|
+
}
|
|
120
|
+
// sort them by decreasing datetime
|
|
121
|
+
return [...byDatetime.entries()]
|
|
122
|
+
.sort(([a], [b]) => b.localeCompare(a))
|
|
123
|
+
.map(([datetime, vdis]) => ({ datetime, vdis }))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* For each snapshot VDI in `snapshotVdis`, maps it to its live source VDI,
|
|
128
|
+
* attaches its CONTENT_KEY when present, calls checkBaseVdis on all writers,
|
|
129
|
+
* and returns both the full mapping and the subset confirmed present by writers.
|
|
130
|
+
*
|
|
131
|
+
* @param {import('@vates/types').XenApiVdi[]} snapshotVdis - Snapshot VDI records to use as base candidates
|
|
132
|
+
* @param {Record<string, object>} srcVdisByRef - Live VM VDI records keyed by $ref
|
|
133
|
+
* @param {{ useContentKey?: boolean }} [options]
|
|
134
|
+
* @returns {Promise<{ presentBaseVdis: Map<string, string>, baseUuidToSrcVdiUuid: Map<string, string>}>}
|
|
135
|
+
*/
|
|
136
|
+
async _checkBaseVdis(snapshotVdis, srcVdisByRef, { useContentKey = false } = {}) {
|
|
137
|
+
const baseUuidToSrcVdiUuid = new Map()
|
|
138
|
+
const baseUuidToContentKey = new Map()
|
|
139
|
+
for (const snapshotVdi of snapshotVdis) {
|
|
140
|
+
const baseUuid = snapshotVdi.uuid
|
|
141
|
+
const srcVdi = srcVdisByRef[snapshotVdi.snapshot_of]
|
|
142
|
+
if (srcVdi !== undefined) {
|
|
143
|
+
baseUuidToSrcVdiUuid.set(baseUuid, srcVdi.uuid)
|
|
144
|
+
if (useContentKey) {
|
|
145
|
+
const contentKey = snapshotVdi.other_config[CONTENT_KEY]
|
|
146
|
+
if (contentKey !== undefined) {
|
|
147
|
+
baseUuidToContentKey.set(baseUuid, contentKey)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
debug('ignore snapshot VDI because no longer present on VM', { vdi: baseUuid })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const presentBaseVdis = new Map(baseUuidToSrcVdiUuid)
|
|
156
|
+
await this._callWriters(
|
|
157
|
+
writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis, baseUuidToContentKey),
|
|
158
|
+
'writer.checkBaseVdis()',
|
|
159
|
+
false
|
|
160
|
+
)
|
|
161
|
+
return { presentBaseVdis, baseUuidToSrcVdiUuid }
|
|
162
|
+
}
|
|
163
|
+
|
|
104
164
|
async _selectBaseVm() {
|
|
105
165
|
const xapi = this._xapi
|
|
106
166
|
|
|
107
|
-
// filter _jobSnapshotVdis to have only the last successfully exported vdi
|
|
108
|
-
// compute delta chain length
|
|
109
|
-
// fill baseUuidToSrcVdi with this data
|
|
110
|
-
|
|
111
167
|
const exportedVdis = this._jobSnapshotVdis.filter(
|
|
112
168
|
_ => EXPORTED_SUCCESSFULLY in _.other_config && DATETIME in _.other_config
|
|
113
169
|
)
|
|
114
|
-
|
|
115
|
-
let lastExportedVdis = []
|
|
116
|
-
for (const exportedVdi of exportedVdis) {
|
|
117
|
-
if (lastSuccessfullBackup === undefined || lastSuccessfullBackup < exportedVdi.other_config[DATETIME]) {
|
|
118
|
-
lastExportedVdis = []
|
|
119
|
-
lastSuccessfullBackup = exportedVdi.other_config[DATETIME]
|
|
120
|
-
}
|
|
121
|
-
if (lastSuccessfullBackup === exportedVdi.other_config[DATETIME]) {
|
|
122
|
-
lastExportedVdis.push(exportedVdi)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
170
|
+
const grouped = this._groupSnapshotVdisByDatetime(exportedVdis)
|
|
125
171
|
|
|
126
172
|
this._baseVdis = {}
|
|
127
|
-
if (
|
|
128
|
-
debug('no base
|
|
173
|
+
if (grouped.length === 0) {
|
|
174
|
+
debug('no base VDIs found', {
|
|
129
175
|
jobLength: this._jobSnapshotVdis.length,
|
|
130
|
-
lastSuccessfullBackup,
|
|
131
176
|
exportedLength: exportedVdis.length,
|
|
132
177
|
})
|
|
133
|
-
return
|
|
134
178
|
}
|
|
135
|
-
const deltaChainLength = Math.max(
|
|
136
|
-
...lastExportedVdis.map(({ other_config }) => Number(other_config[DELTA_CHAIN_LENGTH] ?? 0))
|
|
137
|
-
)
|
|
138
|
-
|
|
139
179
|
const srcVdis = keyBy(await xapi.getRecords('VDI', await this._vm.$getDisks()), '$ref')
|
|
180
|
+
let lastExportedVdis = []
|
|
181
|
+
let presentBaseVdis = new Map()
|
|
182
|
+
let baseUuidToSrcVdiUuid = new Map()
|
|
183
|
+
let deltaChainLength
|
|
140
184
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
} else {
|
|
149
|
-
debug('ignore snapshot VDI because no longer present on VM', {
|
|
150
|
-
vdi: baseUuid,
|
|
151
|
-
})
|
|
152
|
-
}
|
|
185
|
+
if (grouped.length > 0) {
|
|
186
|
+
lastExportedVdis = grouped[0].vdis
|
|
187
|
+
// only compute it on the _jobSnapshotVdis , not on the reused snapshot from CONTENT_KEYS
|
|
188
|
+
deltaChainLength = Math.max(
|
|
189
|
+
...lastExportedVdis.map(({ other_config }) => Number(other_config[DELTA_CHAIN_LENGTH] ?? 0))
|
|
190
|
+
)
|
|
191
|
+
;({ presentBaseVdis, baseUuidToSrcVdiUuid } = await this._checkBaseVdis(lastExportedVdis, srcVdis))
|
|
153
192
|
}
|
|
154
193
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
194
|
+
// if we don't find any candidates
|
|
195
|
+
// if there is only one writer of type IncrementalXapiWriter
|
|
196
|
+
// allow the reuse of any matching snapshot of this VM even from another job
|
|
197
|
+
if (
|
|
198
|
+
presentBaseVdis.size === 0 &&
|
|
199
|
+
this._writers.size === 1 &&
|
|
200
|
+
[...this._writers][0] instanceof IncrementalXapiWriter
|
|
201
|
+
) {
|
|
202
|
+
debug('fallback to content key')
|
|
203
|
+
const candidates = {}
|
|
204
|
+
for (const srcVdi of Object.values(srcVdis)) {
|
|
205
|
+
const snapshots = srcVdi.$snapshots
|
|
206
|
+
debug(`${srcVdi.uuid} got ${snapshots.length} snapshots`)
|
|
207
|
+
for (const snapshot of snapshots) {
|
|
208
|
+
if (DATETIME in snapshot.other_config) {
|
|
209
|
+
candidates[snapshot.uuid] = snapshot
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
debug('got candidates ', Object.keys(candidates).length)
|
|
215
|
+
this._filterValidSnapshotVdis(candidates)
|
|
216
|
+
debug('got filtered ', Object.keys(candidates).length)
|
|
217
|
+
for (const group of this._groupSnapshotVdisByDatetime(Object.values(candidates))) {
|
|
218
|
+
debug('check by datetime ', [...group.vdis].length)
|
|
219
|
+
debug(group.vdis)
|
|
220
|
+
const result = await this._checkBaseVdis(group.vdis, srcVdis, { useContentKey: true })
|
|
221
|
+
debug('result ', result)
|
|
222
|
+
if (result.presentBaseVdis.size > 0) {
|
|
223
|
+
lastExportedVdis = group.vdis
|
|
224
|
+
|
|
225
|
+
presentBaseVdis = result.presentBaseVdis
|
|
226
|
+
baseUuidToSrcVdiUuid = result.baseUuidToSrcVdiUuid
|
|
227
|
+
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
debug('try next group if any')
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
debug(' no fallback ')
|
|
234
|
+
}
|
|
161
235
|
|
|
162
236
|
if (presentBaseVdis.size === 0) {
|
|
163
237
|
debug('no base VM found')
|
|
@@ -236,28 +236,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
236
236
|
// ensure they are attached to only one vm snapshot
|
|
237
237
|
// ensure any VM-snapshot harvested by this has all its disk harvested (no mix of vdi snapshot from this job and not)
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
const jobId = this._jobId
|
|
241
|
-
const xapi = this._xapi
|
|
242
|
-
|
|
243
|
-
const vdiCandidates = {}
|
|
244
|
-
const vdiUuids = this._vm.$VBDs.map(({ VDI }) => VDI)
|
|
245
|
-
Object.values(xapi.objects.indexes.type.VDI)
|
|
246
|
-
.filter(_ => !!_) // filter nullish
|
|
247
|
-
.filter(({ other_config, snapshot_of }) => {
|
|
248
|
-
return (
|
|
249
|
-
vdiUuids.includes(snapshot_of) &&
|
|
250
|
-
other_config[JOB_ID] === jobId &&
|
|
251
|
-
other_config[VM_UUID] === this._vm.uuid &&
|
|
252
|
-
other_config[COPY_OF] === undefined
|
|
253
|
-
)
|
|
254
|
-
})
|
|
255
|
-
.forEach(vdi => {
|
|
256
|
-
vdiCandidates[vdi.uuid] = vdi
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
// check that user snapshots are clean
|
|
260
|
-
|
|
239
|
+
_filterValidSnapshotVdis(vdiCandidates) {
|
|
261
240
|
for (const vdi of Object.values(vdiCandidates)) {
|
|
262
241
|
// cbt metadata are always considered linked to a backup job
|
|
263
242
|
// if they have the right other_config
|
|
@@ -279,7 +258,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
279
258
|
const userVms = vbds.map(({ $VM }) => $VM)
|
|
280
259
|
if (vbds.length > 1) {
|
|
281
260
|
warn(
|
|
282
|
-
`vdi ${vdi.name_label} (${vdi.uuid}) is linked to multipe vms : ${userVms.map(({ name_label, uuid }) => `${name_label} ${uuid}`).join(', ')}.
|
|
261
|
+
`vdi ${vdi.name_label} (${vdi.uuid}) is linked to multipe vms : ${userVms.map(({ name_label, uuid }) => `${name_label} ${uuid}`).join(', ')}.
|
|
283
262
|
This disk snapshot will be excluded from the backup cleaning`,
|
|
284
263
|
{ vdi, userVms }
|
|
285
264
|
)
|
|
@@ -294,7 +273,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
294
273
|
// => we exclude these from the backup processing
|
|
295
274
|
if (vm.$snapshot_of === undefined) {
|
|
296
275
|
warn(
|
|
297
|
-
`vdi ${vdi.name_label} (${vdi.uuid}) is a snapshot linked to a non snapshot vm ${vm.name_label} ${vm.uuid}.
|
|
276
|
+
`vdi ${vdi.name_label} (${vdi.uuid}) is a snapshot linked to a non snapshot vm ${vm.name_label} ${vm.uuid}.
|
|
298
277
|
This disk snapshot will be excluded from the backup cleaning`,
|
|
299
278
|
{ vdi, vm }
|
|
300
279
|
)
|
|
@@ -311,7 +290,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
311
290
|
vm.other_config[VM_UUID] !== vdi.other_config[VM_UUID]
|
|
312
291
|
) {
|
|
313
292
|
warn(
|
|
314
|
-
`vdi ${vdi.name_label} (${vdi.uuid}) is a snapshot linked to a snapshot vm ${vm.name_label} ${vm.uuid} out of this backup job scope.
|
|
293
|
+
`vdi ${vdi.name_label} (${vdi.uuid}) is a snapshot linked to a snapshot vm ${vm.name_label} ${vm.uuid} out of this backup job scope.
|
|
315
294
|
This disk snapshot will be excluded from the backup cleaning`,
|
|
316
295
|
{ vdi, vm }
|
|
317
296
|
)
|
|
@@ -328,7 +307,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
328
307
|
.forEach(({ $VDI: outOfSnapshotsVdi, ...other }) => {
|
|
329
308
|
warn(
|
|
330
309
|
`vdi ${vdi.name_label} ${vdi.uuid} is recognized as a snapshot of the backup job,
|
|
331
|
-
linked to vm ${vm.name_label} ${vm.uuid} but vdi ${outOfSnapshotsVdi.name_label} ${outOfSnapshotsVdi.uuid}
|
|
310
|
+
linked to vm ${vm.name_label} ${vm.uuid} but vdi ${outOfSnapshotsVdi.name_label} ${outOfSnapshotsVdi.uuid}
|
|
332
311
|
is not linked to the job. This disk snapshot will be excluded from the backup cleaning`,
|
|
333
312
|
{ vdi, vm, vbds, outOfSnapshotsVdi }
|
|
334
313
|
)
|
|
@@ -336,8 +315,47 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
336
315
|
delete vdiCandidates[vdi.uuid]
|
|
337
316
|
})
|
|
338
317
|
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async _fetchJobSnapshots() {
|
|
321
|
+
const jobId = this._jobId
|
|
322
|
+
const xapi = this._xapi
|
|
323
|
+
|
|
324
|
+
const vdiCandidates = {}
|
|
325
|
+
const vdiUuids = this._vm.$VBDs.map(({ VDI }) => VDI)
|
|
326
|
+
Object.values(xapi.objects.indexes.type.VDI)
|
|
327
|
+
.filter(_ => !!_) // filter nullish
|
|
328
|
+
.filter(({ other_config, snapshot_of }) => {
|
|
329
|
+
return (
|
|
330
|
+
vdiUuids.includes(snapshot_of) &&
|
|
331
|
+
other_config[JOB_ID] === jobId &&
|
|
332
|
+
other_config[VM_UUID] === this._vm.uuid &&
|
|
333
|
+
other_config[COPY_OF] === undefined
|
|
334
|
+
)
|
|
335
|
+
})
|
|
336
|
+
.forEach(vdi => {
|
|
337
|
+
vdiCandidates[vdi.uuid] = vdi
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
this._filterValidSnapshotVdis(vdiCandidates)
|
|
339
341
|
|
|
340
342
|
this._jobSnapshotVdis = Object.values(vdiCandidates)
|
|
343
|
+
|
|
344
|
+
// For VMs with no disks, retention must be tracked directly on VM snapshots
|
|
345
|
+
// since there are no VDIs to anchor the discovery.
|
|
346
|
+
if (vdiUuids.length === 0) {
|
|
347
|
+
this._disklessJobSnapshotVms = this._vm.$snapshots
|
|
348
|
+
.filter(Boolean)
|
|
349
|
+
.filter(
|
|
350
|
+
({ other_config, $snapshot_of }) =>
|
|
351
|
+
$snapshot_of !== undefined &&
|
|
352
|
+
other_config[JOB_ID] === jobId &&
|
|
353
|
+
other_config[VM_UUID] === this._vm.uuid &&
|
|
354
|
+
other_config[COPY_OF] === undefined
|
|
355
|
+
)
|
|
356
|
+
} else {
|
|
357
|
+
this._disklessJobSnapshotVms = []
|
|
358
|
+
}
|
|
341
359
|
}
|
|
342
360
|
|
|
343
361
|
async _removeUnusedSnapshots() {
|
|
@@ -349,9 +367,10 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
349
367
|
await xapi.barrier()
|
|
350
368
|
// ensure cached object are up to date
|
|
351
369
|
this._jobSnapshotVdis = this._jobSnapshotVdis.map(vdi => xapi.getObject(vdi.$ref))
|
|
370
|
+
const disklessVmSnapshots = this._disklessJobSnapshotVms.map(vm => xapi.getObject(vm.$ref))
|
|
352
371
|
|
|
353
|
-
// get the datetime of the most recent snapshot
|
|
354
|
-
const lastSnapshotDateTime = this._jobSnapshotVdis
|
|
372
|
+
// get the datetime of the most recent snapshot across both VDI and diskless VM snapshots
|
|
373
|
+
const lastSnapshotDateTime = [...this._jobSnapshotVdis, ...disklessVmSnapshots]
|
|
355
374
|
.map(({ other_config }) => other_config[DATETIME])
|
|
356
375
|
.sort()
|
|
357
376
|
.pop()
|
|
@@ -428,6 +447,28 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
428
447
|
})
|
|
429
448
|
})
|
|
430
449
|
|
|
450
|
+
// Retention for VMs with no disks: VM snapshots are not reachable via VDIs
|
|
451
|
+
if (disklessVmSnapshots.length > 0) {
|
|
452
|
+
const snapshotsPerSchedule = groupBy(disklessVmSnapshots, _ => _.other_config[SCHEDULE_ID])
|
|
453
|
+
await asyncEach(Object.entries(snapshotsPerSchedule), async ([scheduleId, snapshots]) => {
|
|
454
|
+
// we only have one snapshot per date time since it's at the VM level
|
|
455
|
+
const snapshotPerDatetime = Object.fromEntries(snapshots.map(s => [s.other_config[DATETIME], s.$ref]))
|
|
456
|
+
const datetimes = Object.keys(snapshotPerDatetime).sort()
|
|
457
|
+
const settings = {
|
|
458
|
+
...baseSettings,
|
|
459
|
+
...allSettings[scheduleId],
|
|
460
|
+
...allSettings[this._vm.uuid],
|
|
461
|
+
}
|
|
462
|
+
const retention = settings.snapshotRetention ?? 0
|
|
463
|
+
await asyncEach(getOldEntries(retention, datetimes), async datetime => {
|
|
464
|
+
if (this.job.mode === 'delta' && datetime === lastSnapshotDateTime) {
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
await xapi.VM_destroy(snapshotPerDatetime[datetime])
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
|
|
431
472
|
// list and remove the snapshot were the jobs failed between
|
|
432
473
|
// makesnapshot and update_other_config
|
|
433
474
|
const snapshots = this._vm.$snapshots.filter(_ => !!_).filter(({ name_label }) => name_label === TEMP_SNAPSHOT_NAME)
|
|
@@ -229,14 +229,13 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
229
229
|
Object.entries(deltaExport.disks),
|
|
230
230
|
async ([diskRef, disk]) => {
|
|
231
231
|
const path = `${this._vmBackupDir}/${vhds[diskRef]}`
|
|
232
|
-
await adapter.writeVhd(path, disk, {
|
|
232
|
+
size += await adapter.writeVhd(path, disk, {
|
|
233
233
|
// no checksum for VHDs, because they will be invalidated by
|
|
234
234
|
// merges and chains
|
|
235
235
|
checksum: false,
|
|
236
236
|
validator: tmpPath => checkVhd(handler, tmpPath),
|
|
237
237
|
writeBlockConcurrency: this._config.writeBlockConcurrency,
|
|
238
238
|
})
|
|
239
|
-
size = size + disk.getNbGeneratedBlock() * disk.getBlockSize()
|
|
240
239
|
},
|
|
241
240
|
{
|
|
242
241
|
concurrency: settings.diskPerVmConcurrency,
|