@xen-orchestra/backups 0.72.0 → 0.72.1
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/RemoteAdapter.mjs
CHANGED
|
@@ -702,7 +702,7 @@ export class RemoteAdapter {
|
|
|
702
702
|
const handler = this._handler
|
|
703
703
|
|
|
704
704
|
if (this.useVhdDirectory()) {
|
|
705
|
-
await writeToVhdDirectory({
|
|
705
|
+
return await writeToVhdDirectory({
|
|
706
706
|
disk,
|
|
707
707
|
target: {
|
|
708
708
|
handler,
|
|
@@ -714,8 +714,9 @@ export class RemoteAdapter {
|
|
|
714
714
|
})
|
|
715
715
|
} else {
|
|
716
716
|
const stream = await toVhdStream(disk)
|
|
717
|
-
await this.outputStream(path, stream, { validator, checksum: false })
|
|
717
|
+
const size = await this.outputStream(path, stream, { validator, checksum: false })
|
|
718
718
|
await validator(path)
|
|
719
|
+
return size
|
|
719
720
|
}
|
|
720
721
|
}
|
|
721
722
|
|
package/_cleanVm.mjs
CHANGED
|
@@ -586,18 +586,13 @@ export async function cleanVm(
|
|
|
586
586
|
return
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
metadata.
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
// all disks are now key disk
|
|
597
|
-
metadata.isVhdDifferencing = {}
|
|
598
|
-
for (const id of Object.keys(metadata.vdis ?? {})) {
|
|
599
|
-
metadata.isVhdDifferencing[id] = false
|
|
600
|
-
}
|
|
589
|
+
// Rewrite metadata when a merge changed the backup layout.
|
|
590
|
+
if (mergedSize) {
|
|
591
|
+
metadata.size = mergedSize
|
|
592
|
+
// all disks are now key disk
|
|
593
|
+
metadata.isVhdDifferencing = {}
|
|
594
|
+
for (const id of Object.keys(metadata.vdis ?? {})) {
|
|
595
|
+
metadata.isVhdDifferencing[id] = false
|
|
601
596
|
}
|
|
602
597
|
mustRegenerateCache = true
|
|
603
598
|
try {
|
|
@@ -338,6 +338,20 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
this._jobSnapshotVdis = Object.values(vdiCandidates)
|
|
341
|
+
|
|
342
|
+
// For VMs with no disks, retention must be tracked directly on VM snapshots
|
|
343
|
+
// since there are no VDIs to anchor the discovery.
|
|
344
|
+
if (vdiUuids.length === 0) {
|
|
345
|
+
this._disklessJobSnapshotVms = this._vm.$snapshots.filter(Boolean).filter(
|
|
346
|
+
({ other_config, $snapshot_of }) =>
|
|
347
|
+
$snapshot_of !== undefined &&
|
|
348
|
+
other_config[JOB_ID] === jobId &&
|
|
349
|
+
other_config[VM_UUID] === this._vm.uuid &&
|
|
350
|
+
other_config[COPY_OF] === undefined
|
|
351
|
+
)
|
|
352
|
+
} else {
|
|
353
|
+
this._disklessJobSnapshotVms = []
|
|
354
|
+
}
|
|
341
355
|
}
|
|
342
356
|
|
|
343
357
|
async _removeUnusedSnapshots() {
|
|
@@ -349,9 +363,10 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
349
363
|
await xapi.barrier()
|
|
350
364
|
// ensure cached object are up to date
|
|
351
365
|
this._jobSnapshotVdis = this._jobSnapshotVdis.map(vdi => xapi.getObject(vdi.$ref))
|
|
366
|
+
const disklessVmSnapshots = this._disklessJobSnapshotVms.map(vm => xapi.getObject(vm.$ref))
|
|
352
367
|
|
|
353
|
-
// get the datetime of the most recent snapshot
|
|
354
|
-
const lastSnapshotDateTime = this._jobSnapshotVdis
|
|
368
|
+
// get the datetime of the most recent snapshot across both VDI and diskless VM snapshots
|
|
369
|
+
const lastSnapshotDateTime = [...this._jobSnapshotVdis, ...disklessVmSnapshots]
|
|
355
370
|
.map(({ other_config }) => other_config[DATETIME])
|
|
356
371
|
.sort()
|
|
357
372
|
.pop()
|
|
@@ -428,6 +443,28 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
428
443
|
})
|
|
429
444
|
})
|
|
430
445
|
|
|
446
|
+
// Retention for VMs with no disks: VM snapshots are not reachable via VDIs
|
|
447
|
+
if (disklessVmSnapshots.length > 0) {
|
|
448
|
+
const snapshotsPerSchedule = groupBy(disklessVmSnapshots, _ => _.other_config[SCHEDULE_ID])
|
|
449
|
+
await asyncEach(Object.entries(snapshotsPerSchedule), async ([scheduleId, snapshots]) => {
|
|
450
|
+
// we only have one snapshot per date time since it's at the VM level
|
|
451
|
+
const snapshotPerDatetime = Object.fromEntries(snapshots.map(s => [s.other_config[DATETIME], s.$ref]))
|
|
452
|
+
const datetimes = Object.keys(snapshotPerDatetime).sort()
|
|
453
|
+
const settings = {
|
|
454
|
+
...baseSettings,
|
|
455
|
+
...allSettings[scheduleId],
|
|
456
|
+
...allSettings[this._vm.uuid],
|
|
457
|
+
}
|
|
458
|
+
const retention = settings.snapshotRetention ?? 0
|
|
459
|
+
await asyncEach(getOldEntries(retention, datetimes), async datetime => {
|
|
460
|
+
if (this.job.mode === 'delta' && datetime === lastSnapshotDateTime) {
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
await xapi.VM_destroy(snapshotPerDatetime[datetime])
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
431
468
|
// list and remove the snapshot were the jobs failed between
|
|
432
469
|
// makesnapshot and update_other_config
|
|
433
470
|
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,
|
|
@@ -57,71 +57,14 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
57
57
|
debug('checkBaseVdis, got snapshot candidates,', snapshotCandidates.length)
|
|
58
58
|
|
|
59
59
|
if (snapshotCandidates.length > 0) {
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
let activeVdi
|
|
69
|
-
try {
|
|
70
|
-
activeVdi = sr.$xapi.getObject(snapshot.$snapshot_of)
|
|
71
|
-
const userVbds = activeVdi.$VBDs?.filter(vbd => vbd.$VM && !vbd.$VM.is_control_domain) ?? []
|
|
72
|
-
if (userVbds.length !== 1) {
|
|
73
|
-
debug('checkBaseVdis, share vbd ', { ref: snapshot.$ref, userVbds })
|
|
74
|
-
// shared vdi ignore
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
const vm = userVbds[0].$VM
|
|
78
|
-
if (!('start' in vm.blocked_operations)) {
|
|
79
|
-
debug('checkBaseVdis, vm not blocked', { vmRef: vm.$ref })
|
|
80
|
-
// vm start unlocked
|
|
81
|
-
// not really an issue since we have check the delta
|
|
82
|
-
// but it indicates the users played with the blocked operations
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
diffDisk = new XapiDiskSource({
|
|
86
|
-
xapi: sr.$xapi,
|
|
87
|
-
vdiRef: activeVdi.$ref,
|
|
88
|
-
baseRef: snapshot.$ref,
|
|
89
|
-
onlyListChangedBlocks: true,
|
|
90
|
-
})
|
|
91
|
-
await diffDisk.init()
|
|
92
|
-
if (diffDisk.getBlockIndexes().length === 0) {
|
|
93
|
-
const sourceUuid = snapshot.other_config?.[COPY_OF]
|
|
94
|
-
if (sourceUuid) {
|
|
95
|
-
this.#baseVdisBySourceUuid.set(sourceUuid, activeVdi)
|
|
96
|
-
}
|
|
97
|
-
// Track the target VM (the replicated VM to update on the next transfer).
|
|
98
|
-
targetVmRef = vm.$ref
|
|
99
|
-
} else {
|
|
100
|
-
// not empty, we will create a new VM
|
|
101
|
-
canChainToTargetVm = false
|
|
102
|
-
debug('checkBaseVdis, data between snapshot and active disk', {
|
|
103
|
-
vdiRef: snapshot.$ref,
|
|
104
|
-
nbBlocks: diffDisk.getBlockIndexes().length,
|
|
105
|
-
})
|
|
106
|
-
}
|
|
107
|
-
} catch (error) {
|
|
108
|
-
debug('checkBaseVdis, skipping snapshot', { ref: snapshot.$ref, error })
|
|
109
|
-
return
|
|
110
|
-
} finally {
|
|
111
|
-
await diffDisk?.close().catch(error => debug('checkBaseVdis, error closing', error))
|
|
112
|
-
await sr.$xapi.VDI_disconnectFromControlDomain(snapshot.$ref)
|
|
113
|
-
if (activeVdi !== undefined) {
|
|
114
|
-
await sr.$xapi.VDI_disconnectFromControlDomain(activeVdi.$ref)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
concurrency: 4,
|
|
120
|
-
}
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
if (canChainToTargetVm && targetVmRef !== undefined) {
|
|
124
|
-
debug('checkBaseVdis,got a valid vm target', targetVmRef)
|
|
60
|
+
// reset before searching for candidates
|
|
61
|
+
this.#baseVdisBySourceUuid = new Map()
|
|
62
|
+
this._targetVmRef = undefined
|
|
63
|
+
const { baseVdisBySourceUuid, targetVmRef } = await this.#validateSnapshotCandidates(snapshotCandidates)
|
|
64
|
+
for (const [sourceUuid, vdi] of baseVdisBySourceUuid) {
|
|
65
|
+
this.#baseVdisBySourceUuid.set(sourceUuid, vdi)
|
|
66
|
+
}
|
|
67
|
+
if (targetVmRef !== undefined) {
|
|
125
68
|
this._targetVmRef = targetVmRef
|
|
126
69
|
}
|
|
127
70
|
} else {
|
|
@@ -145,6 +88,93 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
145
88
|
}
|
|
146
89
|
}
|
|
147
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* 6.3+ snapshot-based validation: for each snapshot candidate, check whether
|
|
93
|
+
* the active VDI has diverged from the snapshot. Returns a baseVdisBySourceUuid
|
|
94
|
+
* map and, when all disks are clean, the targetVmRef to reuse.
|
|
95
|
+
*/
|
|
96
|
+
async #validateSnapshotCandidates(snapshotCandidates) {
|
|
97
|
+
const sr = this._sr
|
|
98
|
+
const baseVdisBySourceUuid = new Map()
|
|
99
|
+
let targetVmRef
|
|
100
|
+
let canChainToTargetVm = true
|
|
101
|
+
|
|
102
|
+
await asyncEach(
|
|
103
|
+
snapshotCandidates,
|
|
104
|
+
async snapshot => {
|
|
105
|
+
let diffDisk
|
|
106
|
+
let activeVdi
|
|
107
|
+
try {
|
|
108
|
+
activeVdi = sr.$xapi.getObject(snapshot.$snapshot_of)
|
|
109
|
+
const userVbds = activeVdi.$VBDs?.filter(vbd => vbd.$VM && !vbd.$VM.is_control_domain) ?? []
|
|
110
|
+
if (userVbds.length !== 1) {
|
|
111
|
+
debug('checkBaseVdis, shared vbd ', { ref: snapshot.$ref, userVbds })
|
|
112
|
+
// shared vdi ignore
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const vm = userVbds[0].$VM
|
|
116
|
+
if (!('start' in vm.blocked_operations)) {
|
|
117
|
+
debug('checkBaseVdis, vm not blocked', { vmRef: vm.$ref })
|
|
118
|
+
// vm start unlocked
|
|
119
|
+
// not really an issue since we have check the delta
|
|
120
|
+
// but it indicates the users played with the blocked operations
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
diffDisk = new XapiDiskSource({
|
|
124
|
+
xapi: sr.$xapi,
|
|
125
|
+
vdiRef: activeVdi.$ref,
|
|
126
|
+
baseRef: snapshot.$ref,
|
|
127
|
+
onlyListChangedBlocks: true,
|
|
128
|
+
})
|
|
129
|
+
await diffDisk.init()
|
|
130
|
+
const sourceUuid = snapshot.other_config?.[COPY_OF]
|
|
131
|
+
if (diffDisk.getBlockIndexes().length === 0) {
|
|
132
|
+
// no block modification since the common snapshot, we can chain VM and disk
|
|
133
|
+
if (sourceUuid) {
|
|
134
|
+
baseVdisBySourceUuid.set(sourceUuid, activeVdi)
|
|
135
|
+
}
|
|
136
|
+
// Track the target VM (the replicated VM to update on the next transfer).
|
|
137
|
+
targetVmRef = vm.$ref
|
|
138
|
+
} else {
|
|
139
|
+
if (sourceUuid) {
|
|
140
|
+
baseVdisBySourceUuid.set(sourceUuid, snapshot)
|
|
141
|
+
}
|
|
142
|
+
// there are changed block since the snapshot
|
|
143
|
+
// we can reuse it to transfer a delta, but we will
|
|
144
|
+
// create a new VM
|
|
145
|
+
canChainToTargetVm = false
|
|
146
|
+
debug('checkBaseVdis, data between snapshot and active disk', {
|
|
147
|
+
vdiRef: snapshot.$ref,
|
|
148
|
+
nbBlocks: diffDisk.getBlockIndexes().length,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
debug('checkBaseVdis, skipping snapshot', { ref: snapshot.$ref, error })
|
|
153
|
+
return
|
|
154
|
+
} finally {
|
|
155
|
+
await diffDisk?.close().catch(error => debug('checkBaseVdis, error closing', error))
|
|
156
|
+
await sr.$xapi.VDI_disconnectFromControlDomain(snapshot.$ref)
|
|
157
|
+
if (activeVdi !== undefined) {
|
|
158
|
+
await sr.$xapi.VDI_disconnectFromControlDomain(activeVdi.$ref)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
concurrency: 4,
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if (!canChainToTargetVm) {
|
|
168
|
+
// if at least one disk has new data, create a new VM
|
|
169
|
+
// instead of updating it
|
|
170
|
+
targetVmRef = undefined
|
|
171
|
+
} else if (targetVmRef !== undefined) {
|
|
172
|
+
debug('checkBaseVdis,got a valid vm target', targetVmRef)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { baseVdisBySourceUuid, targetVmRef }
|
|
176
|
+
}
|
|
177
|
+
|
|
148
178
|
updateUuidAndChain() {
|
|
149
179
|
// nothing to do, the chaining is not modified in this case
|
|
150
180
|
}
|
|
@@ -222,6 +222,7 @@ export class MergeRemoteDisk {
|
|
|
222
222
|
await this.#mergeBlocks(parentDisk, childDisk)
|
|
223
223
|
await parentDisk.flushMetadata(childDisk)
|
|
224
224
|
await parentDisk.mergeMetadata(childDisk)
|
|
225
|
+
this.#state.diskSize = parentDisk.getSizeOnDisk()
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
/**
|
|
@@ -266,8 +267,6 @@ export class MergeRemoteDisk {
|
|
|
266
267
|
|
|
267
268
|
await this.#writeState()
|
|
268
269
|
|
|
269
|
-
this.#state.diskSize = childDisk.getSizeOnDisk()
|
|
270
|
-
|
|
271
270
|
this.#onProgress({ total: nBlocks, done: nBlocks })
|
|
272
271
|
}
|
|
273
272
|
|