@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.
@@ -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({ adapter, metadata, srUuid, xapi, settings: { newMacAddresses, mapVdisSrs = {} } = {} }) {
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 #decorateIncrementalVmMetadata(backup) {
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 sr = await resolveUuid(xapi, cache, this._srUuid, 'SR')
216
+ const srRef = await resolveUuid(xapi, cache, this._srUuid, 'SR')
37
217
  Object.values(backup.vdis).forEach(vdi => {
38
- vdi.SR = mapVdisSrRefs[vdi.uuid] ?? sr.$ref
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 { mapVdisSrs, newMacAddresses } = this._importIncrementalVmSettings
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
- const ignoredVdis = new Set(
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 })
@@ -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
  }
@@ -96,6 +96,9 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
96
96
  metadata,
97
97
  srUuid,
98
98
  xapi,
99
+ settings: {
100
+ additionnalVmTag: 'xo:no-bak=Health Check',
101
+ },
99
102
  }).run()
100
103
  const restoredVm = xapi.getObject(restoredId)
101
104
  try {
@@ -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.0",
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.6.1",
47
+ "vhd-lib": "^4.7.0",
48
48
  "xen-api": "^2.0.0",
49
49
  "yazl": "^2.5.1"
50
50
  },