@xen-orchestra/backups 0.69.4 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ImportVmBackup.mjs +9 -1
- package/_incrementalVm.mjs +114 -34
- package/_otherConfig.mjs +27 -0
- package/_runners/_vmRunners/IncrementalXapi.mjs +14 -10
- package/_runners/_vmRunners/_AbstractXapi.mjs +25 -12
- package/_runners/_writers/FullRemoteWriter.mjs +1 -1
- package/_runners/_writers/FullXapiWriter.mjs +1 -1
- package/_runners/_writers/IncrementalRemoteWriter.mjs +2 -2
- package/_runners/_writers/IncrementalXapiWriter.mjs +135 -57
- package/_runners/_writers/_AbstractWriter.mjs +2 -2
- package/_runners/_writers/_MixinRemoteWriter.mjs +1 -1
- package/_runners/_writers/_listReplicatedVms.mjs +4 -3
- package/disks/MergeRemoteDisk.mjs +8 -7
- package/disks/RemoteVhdDisk.mjs +5 -1
- package/disks/index.mjs +49 -0
- package/package.json +13 -9
package/ImportVmBackup.mjs
CHANGED
|
@@ -255,6 +255,14 @@ export class ImportVmBackup {
|
|
|
255
255
|
: await importIncrementalVm(backup, await xapi.getRecord('SR', srRef), {
|
|
256
256
|
newMacAddresses,
|
|
257
257
|
})
|
|
258
|
+
let size = 0
|
|
259
|
+
if (isFull) {
|
|
260
|
+
size = sizeContainer.size
|
|
261
|
+
} else {
|
|
262
|
+
for (const disk of Object.values(backup.disks)) {
|
|
263
|
+
size += disk.getNbGeneratedBlock() * disk.getBlockSize()
|
|
264
|
+
}
|
|
265
|
+
}
|
|
258
266
|
const remoteName = adapter._handler._remote.name
|
|
259
267
|
let desc = `Restored on ${formatFilenameDate(+new Date())}`
|
|
260
268
|
if (remoteName !== undefined) {
|
|
@@ -275,7 +283,7 @@ export class ImportVmBackup {
|
|
|
275
283
|
])
|
|
276
284
|
|
|
277
285
|
return {
|
|
278
|
-
size
|
|
286
|
+
size,
|
|
279
287
|
id: await xapi.getField('VM', vmRef, 'uuid'),
|
|
280
288
|
}
|
|
281
289
|
}
|
package/_incrementalVm.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { defer } from 'golike-defer'
|
|
|
8
8
|
import { cancelableMap } from './_cancelableMap.mjs'
|
|
9
9
|
import { Task } from './Task.mjs'
|
|
10
10
|
import pick from 'lodash/pick.js'
|
|
11
|
-
import { BASE_DELTA_VDI, COPY_OF, VM_UUID } from './_otherConfig.mjs'
|
|
11
|
+
import { BASE_DELTA_VDI, CONTENT_KEY, COPY_OF, VM_UUID } from './_otherConfig.mjs'
|
|
12
12
|
|
|
13
13
|
import { VHD_MAX_SIZE, XapiDiskSource } from '@xen-orchestra/xapi'
|
|
14
14
|
import { toVhdStream } from 'vhd-lib/disk-consumer/index.mjs'
|
|
@@ -124,7 +124,7 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
|
|
|
124
124
|
$defer,
|
|
125
125
|
incrementalVm,
|
|
126
126
|
sr,
|
|
127
|
-
{ cancelToken = CancelToken.none, newMacAddresses = false } = {}
|
|
127
|
+
{ cancelToken = CancelToken.none, newMacAddresses = false, targetRef = undefined } = {}
|
|
128
128
|
) {
|
|
129
129
|
const { version } = incrementalVm
|
|
130
130
|
if (compareVersions(version, '1.0.0') < 0) {
|
|
@@ -136,44 +136,106 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
|
|
|
136
136
|
|
|
137
137
|
const vdiRecords = incrementalVm.vdis
|
|
138
138
|
|
|
139
|
-
//
|
|
139
|
+
// When targetRef is provided, update the existing VM instead of creating a new one.
|
|
140
|
+
const targetVm = targetRef !== undefined ? xapi.getObjectByRef(targetRef, undefined) : undefined
|
|
141
|
+
const isUpdate = targetVm !== undefined && !targetVm.is_a_snapshot && !targetVm.is_a_template
|
|
142
|
+
|
|
143
|
+
// 0. Create suspend_VDI (only when creating a new VM).
|
|
140
144
|
let suspendVdi
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
if (!isUpdate) {
|
|
146
|
+
if (vmRecord.suspend_VDI !== undefined && vmRecord.suspend_VDI !== 'OpaqueRef:NULL') {
|
|
147
|
+
const vdi = vdiRecords[vmRecord.suspend_VDI]
|
|
148
|
+
if (vdi === undefined) {
|
|
149
|
+
Task.warning('Suspend VDI not available for this suspended VM', {
|
|
150
|
+
vm: pick(vmRecord, 'uuid', 'name_label', 'suspend_VDI'),
|
|
151
|
+
})
|
|
152
|
+
} else {
|
|
153
|
+
suspendVdi = await xapi.getRecord('VDI', await xapi.VDI_create(vdi))
|
|
154
|
+
$defer.onFailure(() => suspendVdi.$destroy())
|
|
155
|
+
}
|
|
150
156
|
}
|
|
151
157
|
}
|
|
152
158
|
|
|
153
|
-
// 1. Create the VM.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
// 1. Create the VM or update the existing one.
|
|
160
|
+
let vmRef
|
|
161
|
+
if (isUpdate) {
|
|
162
|
+
vmRef = targetRef
|
|
163
|
+
await Promise.all([
|
|
164
|
+
xapi.setFields('VM', vmRef, {
|
|
165
|
+
actions_after_crash: vmRecord.actions_after_crash,
|
|
166
|
+
actions_after_reboot: vmRecord.actions_after_reboot,
|
|
167
|
+
actions_after_shutdown: vmRecord.actions_after_shutdown,
|
|
168
|
+
domain_type: vmRecord.domain_type,
|
|
169
|
+
ha_restart_priority: vmRecord.ha_restart_priority,
|
|
170
|
+
has_vendor_device: vmRecord.has_vendor_device,
|
|
171
|
+
HVM_boot_params: vmRecord.HVM_boot_params,
|
|
172
|
+
HVM_boot_policy: vmRecord.HVM_boot_policy,
|
|
173
|
+
HVM_shadow_multiplier: vmRecord.HVM_shadow_multiplier,
|
|
174
|
+
memory_dynamic_max: vmRecord.memory_dynamic_max,
|
|
175
|
+
memory_dynamic_min: vmRecord.memory_dynamic_min,
|
|
176
|
+
memory_static_max: vmRecord.memory_static_max,
|
|
177
|
+
memory_static_min: vmRecord.memory_static_min,
|
|
178
|
+
name_label: vmRecord.name_label,
|
|
179
|
+
name_description: vmRecord.name_description,
|
|
180
|
+
order: vmRecord.order,
|
|
181
|
+
other_config: vmRecord.other_config,
|
|
182
|
+
platform: vmRecord.platform,
|
|
183
|
+
PV_args: vmRecord.PV_args,
|
|
184
|
+
PV_bootloader: vmRecord.PV_bootloader,
|
|
185
|
+
PV_bootloader_args: vmRecord.PV_bootloader_args,
|
|
186
|
+
PV_kernel: vmRecord.PV_kernel,
|
|
187
|
+
PV_legacy_args: vmRecord.PV_legacy_args,
|
|
188
|
+
PV_ramdisk: vmRecord.PV_ramdisk,
|
|
189
|
+
recommendations: vmRecord.recommendations,
|
|
190
|
+
shutdown_delay: vmRecord.shutdown_delay,
|
|
191
|
+
start_delay: vmRecord.start_delay,
|
|
192
|
+
suspend_SR: vmRecord.suspend_SR,
|
|
193
|
+
tags: vmRecord.tags,
|
|
194
|
+
user_version: vmRecord.user_version,
|
|
195
|
+
VCPUs_at_startup: vmRecord.VCPUs_at_startup,
|
|
196
|
+
VCPUs_max: vmRecord.VCPUs_max,
|
|
197
|
+
VCPUs_params: vmRecord.VCPUs_params,
|
|
198
|
+
xenstore_data: vmRecord.xenstore_data,
|
|
199
|
+
}),
|
|
200
|
+
targetVm.update_blocked_operations({
|
|
160
201
|
start: 'Importing…',
|
|
161
202
|
start_on: 'Importing…',
|
|
203
|
+
}),
|
|
204
|
+
])
|
|
205
|
+
} else {
|
|
206
|
+
vmRef = await xapi.VM_create(
|
|
207
|
+
{
|
|
208
|
+
...vmRecord,
|
|
209
|
+
affinity: undefined,
|
|
210
|
+
blocked_operations: {
|
|
211
|
+
...vmRecord.blocked_operations,
|
|
212
|
+
start: 'Importing…',
|
|
213
|
+
start_on: 'Importing…',
|
|
214
|
+
},
|
|
215
|
+
ha_always_run: false,
|
|
216
|
+
is_a_template: false,
|
|
217
|
+
name_label: '[Importing…] ' + vmRecord.name_label,
|
|
162
218
|
},
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
)
|
|
173
|
-
$defer.onFailure.call(xapi, 'VM_destroy', vmRef)
|
|
219
|
+
{
|
|
220
|
+
bios_strings: vmRecord.bios_strings,
|
|
221
|
+
generateMacSeed: newMacAddresses,
|
|
222
|
+
suspend_VDI: suspendVdi?.$ref,
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
$defer.onFailure.call(xapi, 'VM_destroy', vmRef)
|
|
226
|
+
}
|
|
174
227
|
|
|
175
228
|
// 2. Delete all VBDs which may have been created by the import.
|
|
176
|
-
|
|
229
|
+
// In update mode, also collect the VDI refs so orphaned VDIs can be destroyed after new ones are created.
|
|
230
|
+
const existingVbdRefs = await xapi.getField('VM', vmRef, 'VBDs')
|
|
231
|
+
const oldVdiRefs = new Set(
|
|
232
|
+
isUpdate
|
|
233
|
+
? (await asyncMap(existingVbdRefs, ref => xapi.getField('VBD', ref, 'VDI'))).filter(
|
|
234
|
+
ref => ref !== undefined && ref !== 'OpaqueRef:NULL'
|
|
235
|
+
)
|
|
236
|
+
: []
|
|
237
|
+
)
|
|
238
|
+
await asyncMap(existingVbdRefs, ref => ignoreErrors.call(xapi.call('VBD.destroy', ref)))
|
|
177
239
|
|
|
178
240
|
// 3. Create VDIs & VBDs.
|
|
179
241
|
const vbdRecords = incrementalVm.vbds
|
|
@@ -184,10 +246,18 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
|
|
|
184
246
|
let newVdi
|
|
185
247
|
|
|
186
248
|
if (vdi.baseVdi?.$ref !== undefined) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
249
|
+
if (isUpdate) {
|
|
250
|
+
// In update mode, reuse the existing target VDI directly — no clone needed.
|
|
251
|
+
newVdi = vdi.baseVdi
|
|
252
|
+
oldVdiRefs.delete(newVdi.$ref)
|
|
253
|
+
} else {
|
|
254
|
+
newVdi = await xapi.getRecord('VDI', await xapi.VDI_clone(vdi.baseVdi.$ref))
|
|
255
|
+
$defer.onFailure(() => newVdi.$destroy())
|
|
256
|
+
}
|
|
190
257
|
await newVdi.update_other_config(COPY_OF, vdi.uuid)
|
|
258
|
+
if (vdi.other_config[CONTENT_KEY] !== undefined) {
|
|
259
|
+
await newVdi.update_other_config(CONTENT_KEY, vdi.other_config[CONTENT_KEY])
|
|
260
|
+
}
|
|
191
261
|
if (vdi.virtual_size > newVdi.virtual_size) {
|
|
192
262
|
await newVdi.$callAsync('resize', vdi.virtual_size)
|
|
193
263
|
}
|
|
@@ -213,6 +283,16 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
|
|
|
213
283
|
newVdis[vdiRef] = newVdi
|
|
214
284
|
})
|
|
215
285
|
|
|
286
|
+
// 3.5. Destroy old VDIs that are no longer attached to the VM.
|
|
287
|
+
// Uses ignoreErrors because some storage backends refuse to destroy a VDI that still has snapshot children;
|
|
288
|
+
// those VDIs will become truly orphaned once the old snapshot VMs are cleaned up by _deleteOldEntries.
|
|
289
|
+
await asyncMap([...oldVdiRefs], ref => ignoreErrors.call(xapi.call('VDI.destroy', ref)))
|
|
290
|
+
|
|
291
|
+
// 4. For updates, destroy existing VIFs before recreating them.
|
|
292
|
+
if (isUpdate) {
|
|
293
|
+
await asyncMap(await xapi.getField('VM', vmRef, 'VIFs'), ref => ignoreErrors.call(xapi.call('VIF.destroy', ref)))
|
|
294
|
+
}
|
|
295
|
+
|
|
216
296
|
const networksByNameLabelByVlan = {}
|
|
217
297
|
let defaultNetwork
|
|
218
298
|
Object.values(xapi.objects.all).forEach(object => {
|
package/_otherConfig.mjs
CHANGED
|
@@ -28,6 +28,11 @@ export const EXPORTED_SUCCESSFULLY = 'xo:backup:exported'
|
|
|
28
28
|
// the VM ( not the snapshot) uuid
|
|
29
29
|
export const VM_UUID = 'xo:backup:vm'
|
|
30
30
|
|
|
31
|
+
// in `other_config` of source snapshot VDIs and replicated target VDIs
|
|
32
|
+
// contains the UUID of the source snapshot VDI whose content this VDI represents
|
|
33
|
+
// allows direction-agnostic discovery of common base VDIs (e.g. for failback/reverse replication)
|
|
34
|
+
export const CONTENT_KEY = 'xo:backup:contentKey'
|
|
35
|
+
|
|
31
36
|
async function listVdiRefs(xapi, vmRef) {
|
|
32
37
|
return xapi.VM_getDisks(vmRef)
|
|
33
38
|
}
|
|
@@ -80,6 +85,7 @@ export async function getVmDeltaChainLength(xapi, vmRef) {
|
|
|
80
85
|
export function resetVmOtherConfig(xapi, vmRef) {
|
|
81
86
|
return applyToVmAndVdis(xapi, vmRef, (type, ref) => {
|
|
82
87
|
return xapi.setFieldEntries(type, ref, 'other_config', {
|
|
88
|
+
[COPY_OF]: null,
|
|
83
89
|
[DATETIME]: null,
|
|
84
90
|
[DELTA_CHAIN_LENGTH]: null,
|
|
85
91
|
[EXPORTED_SUCCESSFULLY]: null,
|
|
@@ -131,3 +137,24 @@ export async function markExportSuccessfull(xapi, vmRef) {
|
|
|
131
137
|
xapi.setFieldEntry(type, ref, 'other_config', EXPORTED_SUCCESSFULLY, 'true')
|
|
132
138
|
)
|
|
133
139
|
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Set CONTENT_KEY on each VDI of a VM snapshot to its own UUID.
|
|
143
|
+
*
|
|
144
|
+
* This marks each snapshot VDI with a unique content identifier that can be
|
|
145
|
+
* propagated to replicated copies for direction-agnostic base VDI discovery
|
|
146
|
+
* (e.g. for failback / reverse replication).
|
|
147
|
+
*
|
|
148
|
+
* @param {Xapi} xapi
|
|
149
|
+
* @param {String} snapshotRef
|
|
150
|
+
* @returns {Promise}
|
|
151
|
+
*/
|
|
152
|
+
export async function setVmSnapshotContentKeys(xapi, snapshotRef) {
|
|
153
|
+
return applyToVmAndVdis(xapi, snapshotRef, async (type, ref) => {
|
|
154
|
+
if (type !== 'VDI') {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
const uuid = await xapi.getField(type, ref, 'uuid')
|
|
158
|
+
return xapi.setFieldEntry(type, ref, 'other_config', CONTENT_KEY, uuid)
|
|
159
|
+
})
|
|
160
|
+
}
|
|
@@ -135,16 +135,6 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
|
|
135
135
|
const deltaChainLength = Math.max(
|
|
136
136
|
...lastExportedVdis.map(({ other_config }) => Number(other_config[DELTA_CHAIN_LENGTH] ?? 0))
|
|
137
137
|
)
|
|
138
|
-
const fullInterval = this._settings.fullInterval
|
|
139
|
-
if (fullInterval !== 0 && fullInterval <= deltaChainLength + 1) {
|
|
140
|
-
debug('not using base VM because fullInterval reached', {
|
|
141
|
-
fullInterval,
|
|
142
|
-
deltaChainLength,
|
|
143
|
-
eq: fullInterval < deltaChainLength + 1,
|
|
144
|
-
dc1: deltaChainLength + 1,
|
|
145
|
-
})
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
138
|
|
|
149
139
|
const srcVdis = keyBy(await xapi.getRecords('VDI', await this._vm.$getDisks()), '$ref')
|
|
150
140
|
|
|
@@ -173,6 +163,20 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
|
|
173
163
|
debug('no base VM found')
|
|
174
164
|
return
|
|
175
165
|
}
|
|
166
|
+
|
|
167
|
+
// we do tafter checkbasevdis because we want the writer to know the target VM
|
|
168
|
+
// especially on replication when we alwayr update the same VM
|
|
169
|
+
const fullInterval = this._settings.fullInterval
|
|
170
|
+
if (fullInterval !== 0 && fullInterval <= deltaChainLength + 1) {
|
|
171
|
+
debug('not using base VM because fullInterval reached', {
|
|
172
|
+
fullInterval,
|
|
173
|
+
deltaChainLength,
|
|
174
|
+
eq: fullInterval < deltaChainLength + 1,
|
|
175
|
+
dc1: deltaChainLength + 1,
|
|
176
|
+
})
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
176
180
|
baseUuidToSrcVdiUuid.forEach((srcVdiUuid, baseUuid) => {
|
|
177
181
|
if (presentBaseVdis.has(baseUuid)) {
|
|
178
182
|
debug('found base VDI', {
|
|
@@ -10,7 +10,16 @@ import { defer } from 'golike-defer'
|
|
|
10
10
|
import { getOldEntries } from '../../_getOldEntries.mjs'
|
|
11
11
|
import { Task } from '../../Task.mjs'
|
|
12
12
|
import { Abstract } from './_Abstract.mjs'
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
COPY_OF,
|
|
15
|
+
DATETIME,
|
|
16
|
+
JOB_ID,
|
|
17
|
+
SCHEDULE_ID,
|
|
18
|
+
VM_UUID,
|
|
19
|
+
resetVmOtherConfig,
|
|
20
|
+
setVmOtherConfig,
|
|
21
|
+
setVmSnapshotContentKeys,
|
|
22
|
+
} from '../../_otherConfig.mjs'
|
|
14
23
|
|
|
15
24
|
const { warn, info } = createLogger('xo:backups:AbstractXapi')
|
|
16
25
|
|
|
@@ -96,7 +105,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
96
105
|
config,
|
|
97
106
|
healthCheckSr,
|
|
98
107
|
job,
|
|
99
|
-
|
|
108
|
+
schedule,
|
|
100
109
|
vmUuid: vm.uuid,
|
|
101
110
|
settings,
|
|
102
111
|
})
|
|
@@ -114,7 +123,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
114
123
|
config,
|
|
115
124
|
healthCheckSr,
|
|
116
125
|
job,
|
|
117
|
-
|
|
126
|
+
schedule,
|
|
118
127
|
vmUuid: vm.uuid,
|
|
119
128
|
remoteId,
|
|
120
129
|
settings: targetSettings,
|
|
@@ -132,7 +141,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
132
141
|
healthCheckSr,
|
|
133
142
|
job,
|
|
134
143
|
ReplicationWriter,
|
|
135
|
-
|
|
144
|
+
schedule,
|
|
136
145
|
vmUuid: vm.uuid,
|
|
137
146
|
srs,
|
|
138
147
|
settings,
|
|
@@ -150,7 +159,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
150
159
|
config,
|
|
151
160
|
healthCheckSr,
|
|
152
161
|
job,
|
|
153
|
-
|
|
162
|
+
schedule,
|
|
154
163
|
vmUuid: vm.uuid,
|
|
155
164
|
sr,
|
|
156
165
|
settings: targetSettings,
|
|
@@ -165,11 +174,9 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
165
174
|
|
|
166
175
|
// ensure the VM itself does not have any backup metadata which would be
|
|
167
176
|
// copied on manual snapshots and interfere with the backup jobs
|
|
177
|
+
|
|
168
178
|
async _cleanMetadata() {
|
|
169
|
-
|
|
170
|
-
if (JOB_ID in vm.other_config) {
|
|
171
|
-
await resetVmOtherConfig(this._xapi, vm.$ref)
|
|
172
|
-
}
|
|
179
|
+
await resetVmOtherConfig(this._xapi, this._vm.$ref)
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
async _snapshot() {
|
|
@@ -205,6 +212,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
205
212
|
scheduleId: this.scheduleId,
|
|
206
213
|
vmUuid: vm.uuid,
|
|
207
214
|
})
|
|
215
|
+
await setVmSnapshotContentKeys(xapi, snapshotRef)
|
|
208
216
|
const snapshot = await xapi.getRecord('VM', snapshotRef)
|
|
209
217
|
await snapshot.set_name_label(this._getSnapshotNameLabel(vm))
|
|
210
218
|
// reload data to ensure it is up to date with the new name label
|
|
@@ -233,11 +241,16 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
233
241
|
const xapi = this._xapi
|
|
234
242
|
|
|
235
243
|
const vdiCandidates = {}
|
|
236
|
-
|
|
244
|
+
const vdiUuids = this._vm.$VBDs.map(({ VDI }) => VDI)
|
|
237
245
|
Object.values(xapi.objects.indexes.type.VDI)
|
|
238
246
|
.filter(_ => !!_) // filter nullish
|
|
239
|
-
.filter(({ other_config,
|
|
240
|
-
return
|
|
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
|
+
)
|
|
241
254
|
})
|
|
242
255
|
.forEach(vdi => {
|
|
243
256
|
vdiCandidates[vdi.uuid] = vdi
|
|
@@ -27,7 +27,7 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
|
|
|
27
27
|
async _run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot }) {
|
|
28
28
|
const settings = this._settings
|
|
29
29
|
const job = this._job
|
|
30
|
-
const scheduleId = this.
|
|
30
|
+
const scheduleId = this._schedule.id
|
|
31
31
|
|
|
32
32
|
const adapter = this._adapter
|
|
33
33
|
let metadata = await this._isAlreadyTransferred(timestamp)
|
|
@@ -34,7 +34,7 @@ export class FullXapiWriter extends MixinXapiWriter(AbstractFullWriter) {
|
|
|
34
34
|
const sr = this._sr
|
|
35
35
|
const settings = this._settings
|
|
36
36
|
const job = this._job
|
|
37
|
-
const scheduleId = this.
|
|
37
|
+
const scheduleId = this._schedule.id
|
|
38
38
|
|
|
39
39
|
const { uuid: srUuid, $xapi: xapi } = sr
|
|
40
40
|
|
|
@@ -90,7 +90,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
90
90
|
async _prepare() {
|
|
91
91
|
const adapter = this._adapter
|
|
92
92
|
const settings = this._settings
|
|
93
|
-
const scheduleId = this.
|
|
93
|
+
const scheduleId = this._schedule.id
|
|
94
94
|
const vmUuid = this._vmUuid
|
|
95
95
|
|
|
96
96
|
const oldEntries = getOldEntries(
|
|
@@ -180,7 +180,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
180
180
|
async _transfer($defer, { isVhdDifferencing, timestamp, deltaExport, vm, vmSnapshot }) {
|
|
181
181
|
const adapter = this._adapter
|
|
182
182
|
const job = this._job
|
|
183
|
-
const scheduleId = this.
|
|
183
|
+
const scheduleId = this._schedule.id
|
|
184
184
|
const settings = this._settings
|
|
185
185
|
const jobId = job.id
|
|
186
186
|
const handler = adapter.handler
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import humanFormat from 'human-format'
|
|
2
|
+
|
|
1
3
|
import { asyncMapSettled } from '@xen-orchestra/async-map'
|
|
2
4
|
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
|
3
5
|
|
|
@@ -7,7 +9,7 @@ import { Task } from '../../Task.mjs'
|
|
|
7
9
|
|
|
8
10
|
import { AbstractIncrementalWriter } from './_AbstractIncrementalWriter.mjs'
|
|
9
11
|
import { MixinXapiWriter } from './_MixinXapiWriter.mjs'
|
|
10
|
-
import { listReplicatedVms } from './_listReplicatedVms.mjs'
|
|
12
|
+
import { compareReplicatedVmDatetime, listReplicatedVms } from './_listReplicatedVms.mjs'
|
|
11
13
|
import {
|
|
12
14
|
COPY_OF,
|
|
13
15
|
setVmOtherConfig,
|
|
@@ -18,34 +20,102 @@ import {
|
|
|
18
20
|
DATETIME,
|
|
19
21
|
VM_UUID,
|
|
20
22
|
} from '../../_otherConfig.mjs'
|
|
21
|
-
import assert from 'node:assert'
|
|
22
23
|
import { formatFilenameDate } from '../../_filenameDate.mjs'
|
|
24
|
+
import { XapiDiskSource } from '@xen-orchestra/xapi'
|
|
25
|
+
import { asyncEach } from '@vates/async-each'
|
|
26
|
+
import { createLogger } from '@xen-orchestra/log'
|
|
27
|
+
|
|
28
|
+
const { debug } = createLogger('xo:backups:IncrementalXapiWriter')
|
|
23
29
|
|
|
24
30
|
export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
|
|
31
|
+
// Map of source VDI UUID (COPY_OF) → validated active VDI on the target SR.
|
|
32
|
+
// Built by checkBaseVdis, consumed by #decorateVmMetadata to set baseVdi.
|
|
33
|
+
#baseVdisBySourceUuid = new Map()
|
|
34
|
+
|
|
25
35
|
async checkBaseVdis(baseUuidToSrcVdi) {
|
|
26
36
|
const sr = this._sr
|
|
37
|
+
this.#baseVdisBySourceUuid = new Map()
|
|
38
|
+
|
|
27
39
|
if (baseUuidToSrcVdi.size === 0) {
|
|
28
40
|
// searching for the vdis is expensive
|
|
29
41
|
// don't do it if there is nothing to find
|
|
30
42
|
return
|
|
31
43
|
}
|
|
32
44
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
// look for the same snapshot
|
|
46
|
+
// ensure there are no data between the snapshot and the active disk
|
|
47
|
+
|
|
48
|
+
const snapshotCandidates = sr.$VDIs.filter(vdi => {
|
|
49
|
+
return (
|
|
50
|
+
vdi?.managed &&
|
|
51
|
+
vdi?.is_a_snapshot &&
|
|
52
|
+
vdi.other_config[JOB_ID] === this._job.id &&
|
|
53
|
+
vdi.other_config[VM_UUID] === this._vmUuid &&
|
|
54
|
+
baseUuidToSrcVdi.has(vdi?.other_config[COPY_OF])
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
debug('checkBaseVdis, got snapshot candidates,', snapshotCandidates.length)
|
|
58
|
+
|
|
59
|
+
// ensure no data have been written since this snapshot
|
|
60
|
+
// but there may be have some other snapshot for another job
|
|
61
|
+
let targetVmRef
|
|
62
|
+
let canChainToTargetVm = true
|
|
63
|
+
await asyncEach(
|
|
64
|
+
snapshotCandidates,
|
|
65
|
+
async snapshot => {
|
|
66
|
+
let diffDisk
|
|
67
|
+
try {
|
|
68
|
+
const activeVdi = sr.$xapi.getObject(snapshot.$snapshot_of)
|
|
69
|
+
const userVbds = activeVdi.$VBDs?.filter(vbd => vbd.$VM && !vbd.$VM.is_control_domain) ?? []
|
|
70
|
+
if (userVbds.length !== 1) {
|
|
71
|
+
debug('checkBaseVdis, share vbd ', { ref: snapshot.$ref, userVbds })
|
|
72
|
+
// shared vdi ignore
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
const vm = userVbds[0].$VM
|
|
76
|
+
if (!('start' in vm.blocked_operations)) {
|
|
77
|
+
debug('checkBaseVdis, vm not blocked', { vmRef: vm.$ref })
|
|
78
|
+
// vm start unlocked
|
|
79
|
+
// not really an issue since we have check the delta
|
|
80
|
+
// but it indicates the users played with the blocked operations
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
diffDisk = new XapiDiskSource({ xapi: sr.$xapi, vdiRef: activeVdi.$ref, baseRef: snapshot.$ref })
|
|
84
|
+
await diffDisk.init()
|
|
85
|
+
if (diffDisk.getBlockIndexes().length === 0) {
|
|
86
|
+
const sourceUuid = snapshot.other_config?.[COPY_OF]
|
|
87
|
+
if (sourceUuid) {
|
|
88
|
+
this.#baseVdisBySourceUuid.set(sourceUuid, activeVdi)
|
|
89
|
+
}
|
|
90
|
+
// Track the target VM (the replicated VM to update on the next transfer).
|
|
91
|
+
targetVmRef = vm.$ref
|
|
92
|
+
} else {
|
|
93
|
+
// not empty, we will create a new VM
|
|
94
|
+
canChainToTargetVm = false
|
|
95
|
+
debug('checkBaseVdis, data between snapshot and active disk', {
|
|
96
|
+
vdiRef: snapshot.$ref,
|
|
97
|
+
nbBlocks: diffDisk.getBlockIndexes().length,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
debug('checkBaseVdis, skipping snapshot', { ref: snapshot.$ref, error })
|
|
102
|
+
return
|
|
103
|
+
} finally {
|
|
104
|
+
await diffDisk?.close().catch(error => debug('checkBaseVdis, error closing', error))
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
concurrency: 4,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if (canChainToTargetVm && targetVmRef !== undefined) {
|
|
113
|
+
debug('checkBaseVdis,got a valid vm target', targetVmRef)
|
|
114
|
+
this._targetVmRef = targetVmRef
|
|
115
|
+
}
|
|
46
116
|
|
|
47
117
|
for (const uuid of baseUuidToSrcVdi.keys()) {
|
|
48
|
-
if (!
|
|
118
|
+
if (!this.#baseVdisBySourceUuid.has(uuid)) {
|
|
49
119
|
baseUuidToSrcVdi.delete(uuid)
|
|
50
120
|
}
|
|
51
121
|
}
|
|
@@ -76,12 +146,21 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
76
146
|
const settings = this._settings
|
|
77
147
|
const { uuid: srUuid, $xapi: xapi } = this._sr
|
|
78
148
|
const vmUuid = this._vmUuid
|
|
79
|
-
const scheduleId = this.
|
|
149
|
+
const scheduleId = this._schedule.id
|
|
80
150
|
|
|
81
151
|
// delete previous interrupted copies
|
|
82
152
|
ignoreErrors.call(asyncMapSettled(listReplicatedVms(xapi, scheduleId, undefined, vmUuid), vm => vm.$destroy))
|
|
83
153
|
|
|
84
|
-
|
|
154
|
+
const allEntries = listReplicatedVms(xapi, scheduleId, srUuid, vmUuid)
|
|
155
|
+
|
|
156
|
+
// In the snapshot-based flow a non-snapshot VM (the live target) coexists with its
|
|
157
|
+
// snapshots (one per transfer). That VM must not be subject to retention — only its
|
|
158
|
+
// snapshots are. Build the set of VM refs that already have snapshots in the list so
|
|
159
|
+
// we can exclude them, while keeping old-style non-snapshot VMs (no snapshots).
|
|
160
|
+
const vmRefsWithSnapshots = new Set(allEntries.filter(e => e.is_a_snapshot).map(e => e.snapshot_of))
|
|
161
|
+
const retentionEntries = allEntries.filter(e => e.is_a_snapshot || !vmRefsWithSnapshots.has(e.$ref))
|
|
162
|
+
retentionEntries.sort(compareReplicatedVmDatetime)
|
|
163
|
+
this._oldEntries = getOldEntries(settings.copyRetention - 1, retentionEntries)
|
|
85
164
|
|
|
86
165
|
if (settings.deleteFirst && settings.skipDeleteOldEntries) {
|
|
87
166
|
// we want to keep the baseVM when copying a delta
|
|
@@ -110,9 +189,8 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
110
189
|
const sr = this._sr
|
|
111
190
|
const vm = backup.vm
|
|
112
191
|
const job = this._job
|
|
113
|
-
const scheduleId = this.
|
|
192
|
+
const scheduleId = this._schedule.id
|
|
114
193
|
|
|
115
|
-
vm.name_label = `${vm.name_label} - ${job.name} - (${formatFilenameDate(timestamp)})`
|
|
116
194
|
// update other_config data as soon as possible to ensure the next job
|
|
117
195
|
// will be able to detect any partial transfer and lean them
|
|
118
196
|
vm.other_config[COPY_OF] = vm.uuid
|
|
@@ -130,18 +208,6 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
130
208
|
if (!_warmMigration) {
|
|
131
209
|
vm.tags.push('Continuous Replication')
|
|
132
210
|
}
|
|
133
|
-
// extracting the uuid of each delta vdi on the source
|
|
134
|
-
// get all in one pass, since there is a lot of objects
|
|
135
|
-
const sourceVdiUuids = Object.values(backup.vdis)
|
|
136
|
-
.map(({ other_config }) => other_config[BASE_DELTA_VDI])
|
|
137
|
-
// full vdi don't have a base
|
|
138
|
-
.filter(_ => !!_)
|
|
139
|
-
// @todo use index ?
|
|
140
|
-
|
|
141
|
-
const replicatedVdis = sr.$VDIs.filter(vdi => {
|
|
142
|
-
// REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
|
|
143
|
-
return vdi?.managed && !vdi?.is_a_snapshot && sourceVdiUuids.includes(vdi?.other_config[COPY_OF])
|
|
144
|
-
})
|
|
145
211
|
|
|
146
212
|
Object.values(backup.vdis).forEach(vdi => {
|
|
147
213
|
vdi.other_config[COPY_OF] = vdi.uuid
|
|
@@ -150,18 +216,12 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
150
216
|
vdi.other_config[REPLICATED_TO_SR_UUID] = sr.uuid
|
|
151
217
|
vdi.other_config[VM_UUID] = vm.uuid
|
|
152
218
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
)
|
|
157
|
-
assert.ok(
|
|
158
|
-
baseReplicatedTo.length <= 1,
|
|
159
|
-
`Target of a replication must be unique, got ${baseReplicatedTo.length} candidates`
|
|
160
|
-
)
|
|
161
|
-
// baseReplicatedTo can be undefined if a new disk is added and other are already replicated
|
|
162
|
-
vdi.baseVdi = baseReplicatedTo[0]
|
|
219
|
+
const baseDeltaVdiUuid = vdi.other_config[BASE_DELTA_VDI]
|
|
220
|
+
if (baseDeltaVdiUuid !== undefined) {
|
|
221
|
+
// reuse the validated mapping built by checkBaseVdis
|
|
222
|
+
vdi.baseVdi = this.#baseVdisBySourceUuid.get(baseDeltaVdiUuid)
|
|
163
223
|
} else {
|
|
164
|
-
// first replication of this disk
|
|
224
|
+
// first replication of this disk (full, no base)
|
|
165
225
|
vdi.baseVdi = undefined
|
|
166
226
|
}
|
|
167
227
|
// ensure the VDI are created on the target SR
|
|
@@ -175,18 +235,43 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
175
235
|
const { _warmMigration } = this._settings
|
|
176
236
|
const sr = this._sr
|
|
177
237
|
const job = this._job
|
|
178
|
-
const
|
|
238
|
+
const schedule = this._schedule
|
|
239
|
+
const scheduleId = schedule.id
|
|
179
240
|
const { uuid: srUuid, $xapi: xapi } = sr
|
|
180
241
|
|
|
181
242
|
let targetVmRef
|
|
182
243
|
await Task.run({ name: 'transfer' }, async () => {
|
|
183
|
-
targetVmRef = await importIncrementalVm(this.#decorateVmMetadata(deltaExport, timestamp), sr
|
|
184
|
-
|
|
244
|
+
targetVmRef = await importIncrementalVm(this.#decorateVmMetadata(deltaExport, timestamp), sr, {
|
|
245
|
+
targetRef: this._targetVmRef,
|
|
246
|
+
})
|
|
247
|
+
// this also ensure the data are up to date on the snapshot
|
|
248
|
+
await setVmOtherConfig(xapi, targetVmRef, {
|
|
249
|
+
timestamp, // updated at the end to mark the transfer as complete
|
|
250
|
+
jobId: job.id,
|
|
251
|
+
scheduleId,
|
|
252
|
+
vmUuid: vm.uuid,
|
|
253
|
+
srUuid,
|
|
254
|
+
})
|
|
255
|
+
const size = Object.values(deltaExport.disks).reduce(
|
|
256
|
+
(sum, disk) => sum + disk.getNbGeneratedBlock() * disk.getBlockSize(),
|
|
257
|
+
0
|
|
258
|
+
)
|
|
259
|
+
await xapi.setField(
|
|
260
|
+
'VM',
|
|
261
|
+
targetVmRef,
|
|
262
|
+
'name_description',
|
|
263
|
+
deltaExport.vm.name_description +
|
|
264
|
+
` -- last replication: ${formatFilenameDate(timestamp)} ${humanFormat.bytes(size)} read`
|
|
265
|
+
)
|
|
266
|
+
// take a snapshot to ensure these data are not modified until next snapshot
|
|
267
|
+
await Task.run({ name: 'target snapshot' }, async () => {
|
|
268
|
+
await xapi.VM_snapshot(targetVmRef, {
|
|
269
|
+
name_label: `${vm.name_label} - ${job.name} / ${schedule.name} ${formatFilenameDate(timestamp)}`,
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
185
273
|
return {
|
|
186
|
-
size
|
|
187
|
-
(sum, disk) => sum + disk.getNbGeneratedBlock() * disk.getBlockSize(),
|
|
188
|
-
0
|
|
189
|
-
),
|
|
274
|
+
size,
|
|
190
275
|
}
|
|
191
276
|
})
|
|
192
277
|
this._targetVmRef = targetVmRef
|
|
@@ -197,13 +282,6 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
197
282
|
!_warmMigration &&
|
|
198
283
|
targetVm.ha_restart_priority !== '' &&
|
|
199
284
|
Promise.all([targetVm.set_ha_restart_priority(''), targetVm.add_tags('HA disabled')]),
|
|
200
|
-
setVmOtherConfig(xapi, targetVmRef, {
|
|
201
|
-
timestamp, // updated at the end to mark the transfer as complete
|
|
202
|
-
jobId: job.id,
|
|
203
|
-
scheduleId,
|
|
204
|
-
vmUuid: vm.uuid,
|
|
205
|
-
srUuid,
|
|
206
|
-
}),
|
|
207
285
|
])
|
|
208
286
|
}
|
|
209
287
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export class AbstractWriter {
|
|
2
|
-
constructor({ config, healthCheckSr, job, vmUuid,
|
|
2
|
+
constructor({ config, healthCheckSr, job, vmUuid, schedule, settings }) {
|
|
3
3
|
this._config = config
|
|
4
4
|
this._healthCheckSr = healthCheckSr
|
|
5
5
|
this._job = job
|
|
6
|
-
this.
|
|
6
|
+
this._schedule = schedule
|
|
7
7
|
this._settings = settings
|
|
8
8
|
this._vmUuid = vmUuid
|
|
9
9
|
}
|
|
@@ -50,7 +50,7 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
|
|
|
50
50
|
|
|
51
51
|
async getLongTermRetentionTags(currentEntry) {
|
|
52
52
|
const settings = this._settings
|
|
53
|
-
const scheduleId = this.
|
|
53
|
+
const scheduleId = this._schedule.id
|
|
54
54
|
const vmUuid = this._vmUuid
|
|
55
55
|
const adapter = this._adapter
|
|
56
56
|
|
|
@@ -23,12 +23,13 @@ export function listReplicatedVms(xapi, scheduleOrJobId, srUuid, vmUuid) {
|
|
|
23
23
|
const oc = object.other_config
|
|
24
24
|
if (
|
|
25
25
|
object.$type === 'VM' &&
|
|
26
|
-
!object.is_a_snapshot &&
|
|
27
26
|
!object.is_a_template &&
|
|
28
|
-
'start' in object.blocked_operations &&
|
|
29
27
|
(oc[JOB_ID] === scheduleOrJobId || oc[SCHEDULE_ID] === scheduleOrJobId) &&
|
|
30
28
|
oc[REPLICATED_TO_SR_UUID] === srUuid &&
|
|
31
|
-
oc[VM_UUID] === vmUuid
|
|
29
|
+
oc[VM_UUID] === vmUuid &&
|
|
30
|
+
// Old-style replication: one VM per transfer (non-snapshot, start blocked)
|
|
31
|
+
// New-style replication: snapshots of the target VM represent each transfer
|
|
32
|
+
(!object.is_a_snapshot ? 'start' in object.blocked_operations : true)
|
|
32
33
|
) {
|
|
33
34
|
vms[object.$id] = object
|
|
34
35
|
}
|
|
@@ -22,6 +22,7 @@ const { warn } = createLogger('remote-disk:merge')
|
|
|
22
22
|
* @property {number} mergedDataSize
|
|
23
23
|
* @property {'mergeBlocks' | 'cleanup'} step
|
|
24
24
|
* @property {number} diskSize
|
|
25
|
+
* @typedef {(message: string, data?: Record<string, unknown>) => void} Logger
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
export class MergeRemoteDisk {
|
|
@@ -85,11 +86,11 @@ export class MergeRemoteDisk {
|
|
|
85
86
|
/**
|
|
86
87
|
* @param {FileAccessor} handler
|
|
87
88
|
* @param {Object} params
|
|
88
|
-
* @param {Function} params.onProgress
|
|
89
|
-
* @param {Logger | Function} params.logInfo
|
|
90
|
-
* @param {boolean} params.removeUnused
|
|
91
|
-
* @param {number} params.mergeBlockConcurrency
|
|
92
|
-
* @param {number} params.writeStateDelay
|
|
89
|
+
* @param {Function} [params.onProgress]
|
|
90
|
+
* @param {Logger | Function} [params.logInfo]
|
|
91
|
+
* @param {boolean} [params.removeUnused]
|
|
92
|
+
* @param {number} [params.mergeBlockConcurrency]
|
|
93
|
+
* @param {number} [params.writeStateDelay]
|
|
93
94
|
*/
|
|
94
95
|
constructor(
|
|
95
96
|
handler,
|
|
@@ -202,8 +203,8 @@ export class MergeRemoteDisk {
|
|
|
202
203
|
|
|
203
204
|
parentDisk.setAllocatedBlocks(alreadyMergedBlocks)
|
|
204
205
|
} else {
|
|
205
|
-
this.#state.child = { uuid: childDisk.getUuid() ??
|
|
206
|
-
this.#state.parent = { uuid: parentDisk.getUuid() ??
|
|
206
|
+
this.#state.child = { uuid: childDisk.getUuid() ?? undefined }
|
|
207
|
+
this.#state.parent = { uuid: parentDisk.getUuid() ?? undefined }
|
|
207
208
|
|
|
208
209
|
// Finds first allocated block for the 2 following loops
|
|
209
210
|
while (this.#state.currentBlock < getMaxBlockCount && !childDisk.hasBlock(this.#state.currentBlock)) {
|
package/disks/RemoteVhdDisk.mjs
CHANGED
|
@@ -95,10 +95,14 @@ export class RemoteVhdDisk extends RemoteDisk {
|
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Closes the VHD.
|
|
98
|
+
* We replace the dispose function call so the disk can only be closed once.
|
|
99
|
+
*
|
|
98
100
|
* @returns {Promise<void>}
|
|
99
101
|
*/
|
|
100
102
|
async close() {
|
|
101
|
-
|
|
103
|
+
const dispose = this.#dispose
|
|
104
|
+
this.#dispose = () => Promise.resolve()
|
|
105
|
+
await dispose()
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
/**
|
package/disks/index.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { RemoteVhdDisk } from './RemoteVhdDisk.mjs'
|
|
2
|
+
|
|
3
|
+
export { RemoteDisk } from './RemoteDisk.mjs'
|
|
4
|
+
export { openDiskChain } from './openDiskChain.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {import('../../disk-transform/src/FileAccessor.mjs').FileAccessor} FileAccessor
|
|
8
|
+
* @typedef {import('./RemoteDisk.mjs').RemoteDisk} RemoteDisk
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {Object} params
|
|
13
|
+
* @param {FileAccessor} params.handler
|
|
14
|
+
* @param {string} params.path
|
|
15
|
+
* @returns {Promise<RemoteDisk>}
|
|
16
|
+
*/
|
|
17
|
+
export async function openDisk({ handler, path }) {
|
|
18
|
+
const disk = new RemoteVhdDisk({ handler, path })
|
|
19
|
+
await disk.init()
|
|
20
|
+
return disk
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} params
|
|
25
|
+
* @param {FileAccessor} params.handler
|
|
26
|
+
* @param {string} params.path
|
|
27
|
+
* @returns {Promise<Disposable<RemoteDisk>>}
|
|
28
|
+
*/
|
|
29
|
+
export async function openDisposableDisk({ handler, path }) {
|
|
30
|
+
const disk = new RemoteVhdDisk({ handler, path })
|
|
31
|
+
await disk.init()
|
|
32
|
+
return {
|
|
33
|
+
value: disk,
|
|
34
|
+
dispose: () => disk.close(),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DISK_EXTENSIONS = ['.vhd']
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns true if the path points to a supported disk format.
|
|
42
|
+
*
|
|
43
|
+
* @param {FileAccessor} _handler - Remote file handler (reserved for future use)
|
|
44
|
+
* @param {string} path - Path to check
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
export function isDisk(_handler, path) {
|
|
48
|
+
return DISK_EXTENSIONS.some(ext => path.endsWith(ext))
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
|
10
10
|
},
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "0.71.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -19,20 +19,20 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@iarna/toml": "^2.2.5",
|
|
21
21
|
"@kldzj/stream-throttle": "^1.1.1",
|
|
22
|
-
"@vates/async-each": "^1.0.
|
|
22
|
+
"@vates/async-each": "^1.0.2",
|
|
23
23
|
"@vates/cached-dns.lookup": "^1.0.0",
|
|
24
24
|
"@vates/compose": "^2.1.0",
|
|
25
25
|
"@vates/decorate-with": "^2.1.0",
|
|
26
26
|
"@vates/disposable": "^0.1.6",
|
|
27
27
|
"@vates/fuse-vhd": "^2.1.2",
|
|
28
|
-
"@vates/generator-toolbox": "^1.1.
|
|
29
|
-
"@vates/nbd-client": "^3.
|
|
28
|
+
"@vates/generator-toolbox": "^1.1.1",
|
|
29
|
+
"@vates/nbd-client": "^3.3.0",
|
|
30
30
|
"@vates/parse-duration": "^0.1.1",
|
|
31
31
|
"@xen-orchestra/async-map": "^0.1.2",
|
|
32
|
-
"@xen-orchestra/disk-transform": "^1.2.
|
|
33
|
-
"@xen-orchestra/fs": "^4.
|
|
32
|
+
"@xen-orchestra/disk-transform": "^1.2.2",
|
|
33
|
+
"@xen-orchestra/fs": "^4.7.0",
|
|
34
34
|
"@xen-orchestra/log": "^0.7.1",
|
|
35
|
-
"@xen-orchestra/qcow2": "^1.
|
|
35
|
+
"@xen-orchestra/qcow2": "^1.2.0",
|
|
36
36
|
"@xen-orchestra/template": "^0.1.0",
|
|
37
37
|
"app-conf": "^3.0.0",
|
|
38
38
|
"compare-versions": "^6.0.0",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"tar": "^7.5.3",
|
|
52
52
|
"uuid": "^9.0.0",
|
|
53
53
|
"value-matcher": "^0.2.0",
|
|
54
|
-
"vhd-lib": "^4.
|
|
54
|
+
"vhd-lib": "^4.15.0",
|
|
55
55
|
"xen-api": "^4.7.6",
|
|
56
56
|
"yazl": "^2.5.1"
|
|
57
57
|
},
|
|
@@ -62,11 +62,15 @@
|
|
|
62
62
|
"tmp": "^0.2.1"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@xen-orchestra/xapi": "^8.
|
|
65
|
+
"@xen-orchestra/xapi": "^8.7.0"
|
|
66
66
|
},
|
|
67
67
|
"license": "AGPL-3.0-or-later",
|
|
68
68
|
"author": {
|
|
69
69
|
"name": "Vates SAS",
|
|
70
70
|
"url": "https://vates.fr"
|
|
71
|
+
},
|
|
72
|
+
"exports": {
|
|
73
|
+
"./disks": "./disks/index.mjs",
|
|
74
|
+
"./*": "./*"
|
|
71
75
|
}
|
|
72
76
|
}
|