@xen-orchestra/backups 0.44.0 → 0.44.2
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
CHANGED
|
@@ -4,7 +4,14 @@ import { formatFilenameDate } from './_filenameDate.mjs'
|
|
|
4
4
|
import { importIncrementalVm } from './_incrementalVm.mjs'
|
|
5
5
|
import { Task } from './Task.mjs'
|
|
6
6
|
import { watchStreamSize } from './_watchStreamSize.mjs'
|
|
7
|
+
import { VhdNegative, VhdSynthetic } from 'vhd-lib'
|
|
8
|
+
import { decorateClass } from '@vates/decorate-with'
|
|
9
|
+
import { createLogger } from '@xen-orchestra/log'
|
|
10
|
+
import { dirname, join } from 'node:path'
|
|
11
|
+
import pickBy from 'lodash/pickBy.js'
|
|
12
|
+
import { defer } from 'golike-defer'
|
|
7
13
|
|
|
14
|
+
const { debug, info, warn } = createLogger('xo:backups:importVmBackup')
|
|
8
15
|
async function resolveUuid(xapi, cache, uuid, type) {
|
|
9
16
|
if (uuid == null) {
|
|
10
17
|
return uuid
|
|
@@ -16,26 +23,199 @@ async function resolveUuid(xapi, cache, uuid, type) {
|
|
|
16
23
|
return cache.get(uuid)
|
|
17
24
|
}
|
|
18
25
|
export class ImportVmBackup {
|
|
19
|
-
constructor({
|
|
26
|
+
constructor({
|
|
27
|
+
adapter,
|
|
28
|
+
metadata,
|
|
29
|
+
srUuid,
|
|
30
|
+
xapi,
|
|
31
|
+
settings: { additionnalVmTag, newMacAddresses, mapVdisSrs = {}, useDifferentialRestore = false } = {},
|
|
32
|
+
}) {
|
|
20
33
|
this._adapter = adapter
|
|
21
|
-
this._importIncrementalVmSettings = { newMacAddresses, mapVdisSrs }
|
|
34
|
+
this._importIncrementalVmSettings = { additionnalVmTag, newMacAddresses, mapVdisSrs, useDifferentialRestore }
|
|
22
35
|
this._metadata = metadata
|
|
23
36
|
this._srUuid = srUuid
|
|
24
37
|
this._xapi = xapi
|
|
25
38
|
}
|
|
26
39
|
|
|
27
|
-
async #
|
|
40
|
+
async #getPathOfVdiSnapshot(snapshotUuid) {
|
|
41
|
+
const metadata = this._metadata
|
|
42
|
+
if (this._pathToVdis === undefined) {
|
|
43
|
+
const backups = await this._adapter.listVmBackups(
|
|
44
|
+
this._metadata.vm.uuid,
|
|
45
|
+
({ mode, timestamp }) => mode === 'delta' && timestamp >= metadata.timestamp
|
|
46
|
+
)
|
|
47
|
+
const map = new Map()
|
|
48
|
+
for (const backup of backups) {
|
|
49
|
+
for (const [vdiRef, vdi] of Object.entries(backup.vdis)) {
|
|
50
|
+
map.set(vdi.uuid, backup.vhds[vdiRef])
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
this._pathToVdis = map
|
|
54
|
+
}
|
|
55
|
+
return this._pathToVdis.get(snapshotUuid)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async _reuseNearestSnapshot($defer, ignoredVdis) {
|
|
59
|
+
const metadata = this._metadata
|
|
28
60
|
const { mapVdisSrs } = this._importIncrementalVmSettings
|
|
61
|
+
const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
|
|
62
|
+
const streams = {}
|
|
63
|
+
const metdataDir = dirname(metadata._filename)
|
|
64
|
+
const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
|
|
65
|
+
|
|
66
|
+
for (const [vdiRef, vdi] of Object.entries(vdis)) {
|
|
67
|
+
const vhdPath = join(metdataDir, vhds[vdiRef])
|
|
68
|
+
|
|
69
|
+
let xapiDisk
|
|
70
|
+
try {
|
|
71
|
+
xapiDisk = await this._xapi.getRecordByUuid('VDI', vdi.$snapshot_of$uuid)
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// if this disk is not present anymore, fall back to default restore
|
|
74
|
+
warn(err)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let snapshotCandidate, backupCandidate
|
|
78
|
+
if (xapiDisk !== undefined) {
|
|
79
|
+
debug('found disks, wlll search its snapshots', { snapshots: xapiDisk.snapshots })
|
|
80
|
+
for (const snapshotRef of xapiDisk.snapshots) {
|
|
81
|
+
const snapshot = await this._xapi.getRecord('VDI', snapshotRef)
|
|
82
|
+
debug('handling snapshot', { snapshot })
|
|
83
|
+
|
|
84
|
+
// take only the first snapshot
|
|
85
|
+
if (snapshotCandidate && snapshotCandidate.snapshot_time < snapshot.snapshot_time) {
|
|
86
|
+
debug('already got a better candidate')
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// have a corresponding backup more recent than metadata ?
|
|
91
|
+
const pathToSnapshotData = await this.#getPathOfVdiSnapshot(snapshot.uuid)
|
|
92
|
+
if (pathToSnapshotData === undefined) {
|
|
93
|
+
debug('no backup linked to this snaphot')
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
if (snapshot.$SR.uuid !== (mapVdisSrs[vdi.$snapshot_of$uuid] ?? this._srUuid)) {
|
|
97
|
+
debug('not restored on the same SR', { snapshotSr: snapshot.$SR.uuid, mapVdisSrs, srUuid: this._srUuid })
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
debug('got a candidate', pathToSnapshotData)
|
|
102
|
+
|
|
103
|
+
snapshotCandidate = snapshot
|
|
104
|
+
backupCandidate = pathToSnapshotData
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let stream
|
|
109
|
+
const backupWithSnapshotPath = join(metdataDir, backupCandidate ?? '')
|
|
110
|
+
if (vhdPath === backupWithSnapshotPath) {
|
|
111
|
+
// all the data are already on the host
|
|
112
|
+
debug('direct reuse of a snapshot')
|
|
113
|
+
stream = null
|
|
114
|
+
vdis[vdiRef].baseVdi = snapshotCandidate
|
|
115
|
+
// go next disk , we won't use this stream
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let disposableDescendants
|
|
120
|
+
|
|
121
|
+
const disposableSynthetic = await VhdSynthetic.fromVhdChain(this._adapter._handler, vhdPath)
|
|
122
|
+
|
|
123
|
+
// this will also clean if another disk of this VM backup fails
|
|
124
|
+
// if user really only need to restore non failing disks he can retry with ignoredVdis
|
|
125
|
+
let disposed = false
|
|
126
|
+
const disposeOnce = async () => {
|
|
127
|
+
if (!disposed) {
|
|
128
|
+
disposed = true
|
|
129
|
+
try {
|
|
130
|
+
await disposableDescendants?.dispose()
|
|
131
|
+
await disposableSynthetic?.dispose()
|
|
132
|
+
} catch (error) {
|
|
133
|
+
warn('openVhd: failed to dispose VHDs', { error })
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
$defer.onFailure(() => disposeOnce())
|
|
138
|
+
|
|
139
|
+
const parentVhd = disposableSynthetic.value
|
|
140
|
+
await parentVhd.readBlockAllocationTable()
|
|
141
|
+
debug('got vhd synthetic of parents', parentVhd.length)
|
|
142
|
+
|
|
143
|
+
if (snapshotCandidate !== undefined) {
|
|
144
|
+
try {
|
|
145
|
+
debug('will try to use differential restore', {
|
|
146
|
+
backupWithSnapshotPath,
|
|
147
|
+
vhdPath,
|
|
148
|
+
vdiRef,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
disposableDescendants = await VhdSynthetic.fromVhdChain(this._adapter._handler, backupWithSnapshotPath, {
|
|
152
|
+
until: vhdPath,
|
|
153
|
+
})
|
|
154
|
+
const descendantsVhd = disposableDescendants.value
|
|
155
|
+
await descendantsVhd.readBlockAllocationTable()
|
|
156
|
+
debug('got vhd synthetic of descendants')
|
|
157
|
+
const negativeVhd = new VhdNegative(parentVhd, descendantsVhd)
|
|
158
|
+
debug('got vhd negative')
|
|
159
|
+
|
|
160
|
+
// update the stream with the negative vhd stream
|
|
161
|
+
stream = await negativeVhd.stream()
|
|
162
|
+
vdis[vdiRef].baseVdi = snapshotCandidate
|
|
163
|
+
} catch (err) {
|
|
164
|
+
// can be a broken VHD chain, a vhd chain with a key backup, ....
|
|
165
|
+
// not an irrecuperable error, don't dispose parentVhd, and fallback to full restore
|
|
166
|
+
warn(`can't use differential restore`, err)
|
|
167
|
+
disposableDescendants?.dispose()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// didn't make a negative stream : fallback to classic stream
|
|
171
|
+
if (stream === undefined) {
|
|
172
|
+
debug('use legacy restore')
|
|
173
|
+
stream = await parentVhd.stream()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
stream.on('end', disposeOnce)
|
|
177
|
+
stream.on('close', disposeOnce)
|
|
178
|
+
stream.on('error', disposeOnce)
|
|
179
|
+
info('everything is ready, will transfer', stream.length)
|
|
180
|
+
streams[`${vdiRef}.vhd`] = stream
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
streams,
|
|
184
|
+
vbds,
|
|
185
|
+
vdis,
|
|
186
|
+
version: '1.0.0',
|
|
187
|
+
vifs,
|
|
188
|
+
vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async #decorateIncrementalVmMetadata() {
|
|
193
|
+
const { additionnalVmTag, mapVdisSrs, useDifferentialRestore } = this._importIncrementalVmSettings
|
|
194
|
+
|
|
195
|
+
const ignoredVdis = new Set(
|
|
196
|
+
Object.entries(mapVdisSrs)
|
|
197
|
+
.filter(([_, srUuid]) => srUuid === null)
|
|
198
|
+
.map(([vdiUuid]) => vdiUuid)
|
|
199
|
+
)
|
|
200
|
+
let backup
|
|
201
|
+
if (useDifferentialRestore) {
|
|
202
|
+
backup = await this._reuseNearestSnapshot(ignoredVdis)
|
|
203
|
+
} else {
|
|
204
|
+
backup = await this._adapter.readIncrementalVmBackup(this._metadata, ignoredVdis)
|
|
205
|
+
}
|
|
29
206
|
const xapi = this._xapi
|
|
30
207
|
|
|
31
208
|
const cache = new Map()
|
|
32
209
|
const mapVdisSrRefs = {}
|
|
210
|
+
if (additionnalVmTag !== undefined) {
|
|
211
|
+
backup.vm.tags.push(additionnalVmTag)
|
|
212
|
+
}
|
|
33
213
|
for (const [vdiUuid, srUuid] of Object.entries(mapVdisSrs)) {
|
|
34
214
|
mapVdisSrRefs[vdiUuid] = await resolveUuid(xapi, cache, srUuid, 'SR')
|
|
35
215
|
}
|
|
36
|
-
const
|
|
216
|
+
const srRef = await resolveUuid(xapi, cache, this._srUuid, 'SR')
|
|
37
217
|
Object.values(backup.vdis).forEach(vdi => {
|
|
38
|
-
vdi.SR = mapVdisSrRefs[vdi.uuid] ??
|
|
218
|
+
vdi.SR = mapVdisSrRefs[vdi.uuid] ?? srRef
|
|
39
219
|
})
|
|
40
220
|
return backup
|
|
41
221
|
}
|
|
@@ -46,7 +226,7 @@ export class ImportVmBackup {
|
|
|
46
226
|
const isFull = metadata.mode === 'full'
|
|
47
227
|
|
|
48
228
|
const sizeContainer = { size: 0 }
|
|
49
|
-
const {
|
|
229
|
+
const { newMacAddresses } = this._importIncrementalVmSettings
|
|
50
230
|
let backup
|
|
51
231
|
if (isFull) {
|
|
52
232
|
backup = await adapter.readFullVmBackup(metadata)
|
|
@@ -54,12 +234,7 @@ export class ImportVmBackup {
|
|
|
54
234
|
} else {
|
|
55
235
|
assert.strictEqual(metadata.mode, 'delta')
|
|
56
236
|
|
|
57
|
-
|
|
58
|
-
Object.entries(mapVdisSrs)
|
|
59
|
-
.filter(([_, srUuid]) => srUuid === null)
|
|
60
|
-
.map(([vdiUuid]) => vdiUuid)
|
|
61
|
-
)
|
|
62
|
-
backup = await this.#decorateIncrementalVmMetadata(await adapter.readIncrementalVmBackup(metadata, ignoredVdis))
|
|
237
|
+
backup = await this.#decorateIncrementalVmMetadata()
|
|
63
238
|
Object.values(backup.streams).forEach(stream => watchStreamSize(stream, sizeContainer))
|
|
64
239
|
}
|
|
65
240
|
|
|
@@ -101,3 +276,5 @@ export class ImportVmBackup {
|
|
|
101
276
|
)
|
|
102
277
|
}
|
|
103
278
|
}
|
|
279
|
+
|
|
280
|
+
decorateClass(ImportVmBackup, { _reuseNearestSnapshot: defer })
|
package/_incrementalVm.mjs
CHANGED
|
@@ -250,6 +250,10 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
|
|
|
250
250
|
// Import VDI contents.
|
|
251
251
|
cancelableMap(cancelToken, Object.entries(newVdis), async (cancelToken, [id, vdi]) => {
|
|
252
252
|
for (let stream of ensureArray(streams[`${id}.vhd`])) {
|
|
253
|
+
if (stream === null) {
|
|
254
|
+
// we restore a backup and reuse completly a local snapshot
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
253
257
|
if (typeof stream === 'function') {
|
|
254
258
|
stream = await stream()
|
|
255
259
|
}
|
|
@@ -58,7 +58,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
|
|
|
58
58
|
)
|
|
59
59
|
}
|
|
60
60
|
const healthCheckVm = xapi.getObject(healthCheckVmRef) ?? (await xapi.waitObject(healthCheckVmRef))
|
|
61
|
-
|
|
61
|
+
await healthCheckVm.add_tag('xo:no-bak=Health Check')
|
|
62
62
|
await new HealthCheckVmBackup({
|
|
63
63
|
restoredVm: healthCheckVm,
|
|
64
64
|
xapi,
|
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.44.
|
|
11
|
+
"version": "0.44.2",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"proper-lockfile": "^4.1.2",
|
|
45
45
|
"tar": "^6.1.15",
|
|
46
46
|
"uuid": "^9.0.0",
|
|
47
|
-
"vhd-lib": "^4.
|
|
47
|
+
"vhd-lib": "^4.7.0",
|
|
48
48
|
"xen-api": "^2.0.0",
|
|
49
49
|
"yazl": "^2.5.1"
|
|
50
50
|
},
|