@xen-orchestra/backups 0.43.2 → 0.44.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.
@@ -5,22 +5,57 @@ import { importIncrementalVm } from './_incrementalVm.mjs'
5
5
  import { Task } from './Task.mjs'
6
6
  import { watchStreamSize } from './_watchStreamSize.mjs'
7
7
 
8
+ async function resolveUuid(xapi, cache, uuid, type) {
9
+ if (uuid == null) {
10
+ return uuid
11
+ }
12
+ const ref = cache.get(uuid)
13
+ if (ref === undefined) {
14
+ cache.set(uuid, xapi.call(`${type}.get_by_uuid`, uuid))
15
+ }
16
+ return cache.get(uuid)
17
+ }
8
18
  export class ImportVmBackup {
9
- constructor({ adapter, metadata, srUuid, xapi, settings: { newMacAddresses, mapVdisSrs = {} } = {} }) {
19
+ constructor({
20
+ adapter,
21
+ metadata,
22
+ srUuid,
23
+ xapi,
24
+ settings: { additionnalVmTag, newMacAddresses, mapVdisSrs = {} } = {},
25
+ }) {
10
26
  this._adapter = adapter
11
- this._importIncrementalVmSettings = { newMacAddresses, mapVdisSrs }
27
+ this._importIncrementalVmSettings = { additionnalVmTag, newMacAddresses, mapVdisSrs }
12
28
  this._metadata = metadata
13
29
  this._srUuid = srUuid
14
30
  this._xapi = xapi
15
31
  }
16
32
 
33
+ async #decorateIncrementalVmMetadata(backup) {
34
+ const { additionnalVmTag, mapVdisSrs } = this._importIncrementalVmSettings
35
+ const xapi = this._xapi
36
+
37
+ const cache = new Map()
38
+ const mapVdisSrRefs = {}
39
+ if (additionnalVmTag !== undefined) {
40
+ backup.vm.tags.push(additionnalVmTag)
41
+ }
42
+ for (const [vdiUuid, srUuid] of Object.entries(mapVdisSrs)) {
43
+ mapVdisSrRefs[vdiUuid] = await resolveUuid(xapi, cache, srUuid, 'SR')
44
+ }
45
+ const srRef = await resolveUuid(xapi, cache, this._srUuid, 'SR')
46
+ Object.values(backup.vdis).forEach(vdi => {
47
+ vdi.SR = mapVdisSrRefs[vdi.uuid] ?? srRef
48
+ })
49
+ return backup
50
+ }
51
+
17
52
  async run() {
18
53
  const adapter = this._adapter
19
54
  const metadata = this._metadata
20
55
  const isFull = metadata.mode === 'full'
21
56
 
22
57
  const sizeContainer = { size: 0 }
23
-
58
+ const { mapVdisSrs, newMacAddresses } = this._importIncrementalVmSettings
24
59
  let backup
25
60
  if (isFull) {
26
61
  backup = await adapter.readFullVmBackup(metadata)
@@ -29,11 +64,11 @@ export class ImportVmBackup {
29
64
  assert.strictEqual(metadata.mode, 'delta')
30
65
 
31
66
  const ignoredVdis = new Set(
32
- Object.entries(this._importIncrementalVmSettings.mapVdisSrs)
67
+ Object.entries(mapVdisSrs)
33
68
  .filter(([_, srUuid]) => srUuid === null)
34
69
  .map(([vdiUuid]) => vdiUuid)
35
70
  )
36
- backup = await adapter.readIncrementalVmBackup(metadata, ignoredVdis)
71
+ backup = await this.#decorateIncrementalVmMetadata(await adapter.readIncrementalVmBackup(metadata, ignoredVdis))
37
72
  Object.values(backup.streams).forEach(stream => watchStreamSize(stream, sizeContainer))
38
73
  }
39
74
 
@@ -48,8 +83,7 @@ export class ImportVmBackup {
48
83
  const vmRef = isFull
49
84
  ? await xapi.VM_import(backup, srRef)
50
85
  : await importIncrementalVm(backup, await xapi.getRecord('SR', srRef), {
51
- ...this._importIncrementalVmSettings,
52
- detectBase: false,
86
+ newMacAddresses,
53
87
  })
54
88
 
55
89
  await Promise.all([
@@ -59,6 +93,13 @@ export class ImportVmBackup {
59
93
  vmRef,
60
94
  `${metadata.vm.name_label} (${formatFilenameDate(metadata.timestamp)})`
61
95
  ),
96
+ xapi.call(
97
+ 'VM.set_name_description',
98
+ vmRef,
99
+ `Restored on ${formatFilenameDate(+new Date())} from ${adapter._handler._remote.name} -
100
+ ${metadata.vm.name_description}
101
+ `
102
+ ),
62
103
  ])
63
104
 
64
105
  return {
@@ -1,4 +1,3 @@
1
- import find from 'lodash/find.js'
2
1
  import groupBy from 'lodash/groupBy.js'
3
2
  import ignoreErrors from 'promise-toolbox/ignoreErrors'
4
3
  import omit from 'lodash/omit.js'
@@ -12,24 +11,18 @@ import { cancelableMap } from './_cancelableMap.mjs'
12
11
  import { Task } from './Task.mjs'
13
12
  import pick from 'lodash/pick.js'
14
13
 
14
+ // in `other_config` of an incrementally replicated VM, contains the UUID of the source VM
15
15
  export const TAG_BASE_DELTA = 'xo:base_delta'
16
16
 
17
- export const TAG_COPY_SRC = 'xo:copy_of'
17
+ // in `other_config` of an incrementally replicated VM, contains the UUID of the target SR used for replication
18
+ //
19
+ // added after the complete replication
20
+ export const TAG_BACKUP_SR = 'xo:backup:sr'
18
21
 
19
- const TAG_BACKUP_SR = 'xo:backup:sr'
22
+ // in other_config of VDIs of an incrementally replicated VM, contains the UUID of the source VDI
23
+ export const TAG_COPY_SRC = 'xo:copy_of'
20
24
 
21
25
  const ensureArray = value => (value === undefined ? [] : Array.isArray(value) ? value : [value])
22
- const resolveUuid = async (xapi, cache, uuid, type) => {
23
- if (uuid == null) {
24
- return uuid
25
- }
26
- let ref = cache.get(uuid)
27
- if (ref === undefined) {
28
- ref = await xapi.call(`${type}.get_by_uuid`, uuid)
29
- cache.set(uuid, ref)
30
- }
31
- return ref
32
- }
33
26
 
34
27
  export async function exportIncrementalVm(
35
28
  vm,
@@ -147,7 +140,7 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
147
140
  $defer,
148
141
  incrementalVm,
149
142
  sr,
150
- { cancelToken = CancelToken.none, detectBase = true, mapVdisSrs = {}, newMacAddresses = false } = {}
143
+ { cancelToken = CancelToken.none, newMacAddresses = false } = {}
151
144
  ) {
152
145
  const { version } = incrementalVm
153
146
  if (compareVersions(version, '1.0.0') < 0) {
@@ -157,35 +150,6 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
157
150
  const vmRecord = incrementalVm.vm
158
151
  const xapi = sr.$xapi
159
152
 
160
- let baseVm
161
- if (detectBase) {
162
- const remoteBaseVmUuid = vmRecord.other_config[TAG_BASE_DELTA]
163
- if (remoteBaseVmUuid) {
164
- baseVm = find(
165
- xapi.objects.all,
166
- obj => (obj = obj.other_config) && obj[TAG_COPY_SRC] === remoteBaseVmUuid && obj[TAG_BACKUP_SR] === sr.$id
167
- )
168
-
169
- if (!baseVm) {
170
- throw new Error(`could not find the base VM (copy of ${remoteBaseVmUuid})`)
171
- }
172
- }
173
- }
174
-
175
- const cache = new Map()
176
- const mapVdisSrRefs = {}
177
- for (const [vdiUuid, srUuid] of Object.entries(mapVdisSrs)) {
178
- mapVdisSrRefs[vdiUuid] = await resolveUuid(xapi, cache, srUuid, 'SR')
179
- }
180
-
181
- const baseVdis = {}
182
- baseVm &&
183
- baseVm.$VBDs.forEach(vbd => {
184
- const vdi = vbd.$VDI
185
- if (vdi !== undefined) {
186
- baseVdis[vbd.VDI] = vbd.$VDI
187
- }
188
- })
189
153
  const vdiRecords = incrementalVm.vdis
190
154
 
191
155
  // 0. Create suspend_VDI
@@ -197,18 +161,7 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
197
161
  vm: pick(vmRecord, 'uuid', 'name_label', 'suspend_VDI'),
198
162
  })
199
163
  } else {
200
- suspendVdi = await xapi.getRecord(
201
- 'VDI',
202
- await xapi.VDI_create({
203
- ...vdi,
204
- other_config: {
205
- ...vdi.other_config,
206
- [TAG_BASE_DELTA]: undefined,
207
- [TAG_COPY_SRC]: vdi.uuid,
208
- },
209
- sr: mapVdisSrRefs[vdi.uuid] ?? sr.$ref,
210
- })
211
- )
164
+ suspendVdi = await xapi.getRecord('VDI', await xapi.VDI_create(vdi))
212
165
  $defer.onFailure(() => suspendVdi.$destroy())
213
166
  }
214
167
  }
@@ -226,10 +179,6 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
226
179
  ha_always_run: false,
227
180
  is_a_template: false,
228
181
  name_label: '[Importing…] ' + vmRecord.name_label,
229
- other_config: {
230
- ...vmRecord.other_config,
231
- [TAG_COPY_SRC]: vmRecord.uuid,
232
- },
233
182
  },
234
183
  {
235
184
  bios_strings: vmRecord.bios_strings,
@@ -250,14 +199,8 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
250
199
  const vdi = vdiRecords[vdiRef]
251
200
  let newVdi
252
201
 
253
- const remoteBaseVdiUuid = detectBase && vdi.other_config[TAG_BASE_DELTA]
254
- if (remoteBaseVdiUuid) {
255
- const baseVdi = find(baseVdis, vdi => vdi.other_config[TAG_COPY_SRC] === remoteBaseVdiUuid)
256
- if (!baseVdi) {
257
- throw new Error(`missing base VDI (copy of ${remoteBaseVdiUuid})`)
258
- }
259
-
260
- newVdi = await xapi.getRecord('VDI', await baseVdi.$clone())
202
+ if (vdi.baseVdi !== undefined) {
203
+ newVdi = await xapi.getRecord('VDI', await vdi.baseVdi.$clone())
261
204
  $defer.onFailure(() => newVdi.$destroy())
262
205
 
263
206
  await newVdi.update_other_config(TAG_COPY_SRC, vdi.uuid)
@@ -268,18 +211,7 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
268
211
  // suspendVDI has already created
269
212
  newVdi = suspendVdi
270
213
  } else {
271
- newVdi = await xapi.getRecord(
272
- 'VDI',
273
- await xapi.VDI_create({
274
- ...vdi,
275
- other_config: {
276
- ...vdi.other_config,
277
- [TAG_BASE_DELTA]: undefined,
278
- [TAG_COPY_SRC]: vdi.uuid,
279
- },
280
- SR: mapVdisSrRefs[vdi.uuid] ?? sr.$ref,
281
- })
282
- )
214
+ newVdi = await xapi.getRecord('VDI', await xapi.VDI_create(vdi))
283
215
  $defer.onFailure(() => newVdi.$destroy())
284
216
  }
285
217
 
@@ -324,7 +256,9 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
324
256
  if (stream.length === undefined) {
325
257
  stream = await createVhdStreamWithLength(stream)
326
258
  }
259
+ await xapi.setField('VDI', vdi.$ref, 'name_label', `[Importing] ${vdiRecords[id].name_label}`)
327
260
  await vdi.$importContent(stream, { cancelToken, format: 'vhd' })
261
+ await xapi.setField('VDI', vdi.$ref, 'name_label', vdiRecords[id].name_label)
328
262
  }
329
263
  }),
330
264
 
@@ -1,11 +1,11 @@
1
+ import cloneDeep from 'lodash/cloneDeep.js'
1
2
  import mapValues from 'lodash/mapValues.js'
2
3
 
3
4
  import { forkStreamUnpipe } from '../_forkStreamUnpipe.mjs'
4
5
 
5
6
  export function forkDeltaExport(deltaExport) {
6
- return Object.create(deltaExport, {
7
- streams: {
8
- value: mapValues(deltaExport.streams, forkStreamUnpipe),
9
- },
10
- })
7
+ const { streams, ...rest } = deltaExport
8
+ const newMetadata = cloneDeep(rest)
9
+ newMetadata.streams = mapValues(streams, forkStreamUnpipe)
10
+ return newMetadata
11
11
  }
@@ -11,6 +11,7 @@ import { dirname } from 'node:path'
11
11
 
12
12
  import { formatFilenameDate } from '../../_filenameDate.mjs'
13
13
  import { getOldEntries } from '../../_getOldEntries.mjs'
14
+ import { TAG_BASE_DELTA } from '../../_incrementalVm.mjs'
14
15
  import { Task } from '../../Task.mjs'
15
16
 
16
17
  import { MixinRemoteWriter } from './_MixinRemoteWriter.mjs'
@@ -195,7 +196,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
195
196
  assert.notStrictEqual(
196
197
  parentPath,
197
198
  undefined,
198
- `missing parent of ${id} in ${dirname(path)}, looking for ${vdi.other_config['xo:base_delta']}`
199
+ `missing parent of ${id} in ${dirname(path)}, looking for ${vdi.other_config[TAG_BASE_DELTA]}`
199
200
  )
200
201
 
201
202
  parentPath = parentPath.slice(1) // remove leading slash
@@ -4,12 +4,13 @@ import { formatDateTime } from '@xen-orchestra/xapi'
4
4
 
5
5
  import { formatFilenameDate } from '../../_filenameDate.mjs'
6
6
  import { getOldEntries } from '../../_getOldEntries.mjs'
7
- import { importIncrementalVm, TAG_COPY_SRC } from '../../_incrementalVm.mjs'
7
+ import { importIncrementalVm, TAG_BACKUP_SR, TAG_BASE_DELTA, TAG_COPY_SRC } from '../../_incrementalVm.mjs'
8
8
  import { Task } from '../../Task.mjs'
9
9
 
10
10
  import { AbstractIncrementalWriter } from './_AbstractIncrementalWriter.mjs'
11
11
  import { MixinXapiWriter } from './_MixinXapiWriter.mjs'
12
12
  import { listReplicatedVms } from './_listReplicatedVms.mjs'
13
+ import find from 'lodash/find.js'
13
14
 
14
15
  export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
15
16
  async checkBaseVdis(baseUuidToSrcVdi, baseVm) {
@@ -81,6 +82,54 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
81
82
  return asyncMapSettled(this._oldEntries, vm => vm.$destroy())
82
83
  }
83
84
 
85
+ #decorateVmMetadata(backup) {
86
+ const { _warmMigration } = this._settings
87
+ const sr = this._sr
88
+ const xapi = sr.$xapi
89
+ const vm = backup.vm
90
+ vm.other_config[TAG_COPY_SRC] = vm.uuid
91
+ const remoteBaseVmUuid = vm.other_config[TAG_BASE_DELTA]
92
+ let baseVm
93
+ if (remoteBaseVmUuid) {
94
+ baseVm = find(
95
+ xapi.objects.all,
96
+ obj => (obj = obj.other_config) && obj[TAG_COPY_SRC] === remoteBaseVmUuid && obj[TAG_BACKUP_SR] === sr.$id
97
+ )
98
+
99
+ if (!baseVm) {
100
+ throw new Error(`could not find the base VM (copy of ${remoteBaseVmUuid})`)
101
+ }
102
+ }
103
+ const baseVdis = {}
104
+ baseVm?.$VBDs.forEach(vbd => {
105
+ const vdi = vbd.$VDI
106
+ if (vdi !== undefined) {
107
+ baseVdis[vbd.VDI] = vbd.$VDI
108
+ }
109
+ })
110
+
111
+ vm.other_config[TAG_COPY_SRC] = vm.uuid
112
+ if (!_warmMigration) {
113
+ vm.tags.push('Continuous Replication')
114
+ }
115
+
116
+ Object.values(backup.vdis).forEach(vdi => {
117
+ vdi.other_config[TAG_COPY_SRC] = vdi.uuid
118
+ vdi.SR = sr.$ref
119
+ // vdi.other_config[TAG_BASE_DELTA] is never defined on a suspend vdi
120
+ if (vdi.other_config[TAG_BASE_DELTA]) {
121
+ const remoteBaseVdiUuid = vdi.other_config[TAG_BASE_DELTA]
122
+ const baseVdi = find(baseVdis, vdi => vdi.other_config[TAG_COPY_SRC] === remoteBaseVdiUuid)
123
+ if (!baseVdi) {
124
+ throw new Error(`missing base VDI (copy of ${remoteBaseVdiUuid})`)
125
+ }
126
+ vdi.baseVdi = baseVdi
127
+ }
128
+ })
129
+
130
+ return backup
131
+ }
132
+
84
133
  async _transfer({ timestamp, deltaExport, sizeContainers, vm }) {
85
134
  const { _warmMigration } = this._settings
86
135
  const sr = this._sr
@@ -91,16 +140,7 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
91
140
 
92
141
  let targetVmRef
93
142
  await Task.run({ name: 'transfer' }, async () => {
94
- targetVmRef = await importIncrementalVm(
95
- {
96
- __proto__: deltaExport,
97
- vm: {
98
- ...deltaExport.vm,
99
- tags: _warmMigration ? deltaExport.vm.tags : [...deltaExport.vm.tags, 'Continuous Replication'],
100
- },
101
- },
102
- sr
103
- )
143
+ targetVmRef = await importIncrementalVm(this.#decorateVmMetadata(deltaExport), sr)
104
144
  return {
105
145
  size: Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0),
106
146
  }
@@ -121,13 +161,13 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
121
161
  )
122
162
  ),
123
163
  targetVm.update_other_config({
124
- 'xo:backup:sr': srUuid,
164
+ [TAG_BACKUP_SR]: srUuid,
125
165
 
126
166
  // these entries need to be added in case of offline backup
127
167
  'xo:backup:datetime': formatDateTime(timestamp),
128
168
  'xo:backup:job': job.id,
129
169
  'xo:backup:schedule': scheduleId,
130
- 'xo:backup:vm': vm.uuid,
170
+ [TAG_BASE_DELTA]: vm.uuid,
131
171
  }),
132
172
  ])
133
173
  }
@@ -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.43.2",
11
+ "version": "0.44.1",
12
12
  "engines": {
13
13
  "node": ">=14.18"
14
14
  },
@@ -23,12 +23,12 @@
23
23
  "@vates/cached-dns.lookup": "^1.0.0",
24
24
  "@vates/compose": "^2.1.0",
25
25
  "@vates/decorate-with": "^2.0.0",
26
- "@vates/disposable": "^0.1.4",
26
+ "@vates/disposable": "^0.1.5",
27
27
  "@vates/fuse-vhd": "^2.0.0",
28
- "@vates/nbd-client": "^2.0.0",
28
+ "@vates/nbd-client": "^2.0.1",
29
29
  "@vates/parse-duration": "^0.1.1",
30
30
  "@xen-orchestra/async-map": "^0.1.2",
31
- "@xen-orchestra/fs": "^4.1.1",
31
+ "@xen-orchestra/fs": "^4.1.3",
32
32
  "@xen-orchestra/log": "^0.6.0",
33
33
  "@xen-orchestra/template": "^0.1.0",
34
34
  "app-conf": "^2.3.0",
@@ -45,18 +45,18 @@
45
45
  "tar": "^6.1.15",
46
46
  "uuid": "^9.0.0",
47
47
  "vhd-lib": "^4.6.1",
48
- "xen-api": "^1.3.6",
48
+ "xen-api": "^2.0.0",
49
49
  "yazl": "^2.5.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "fs-extra": "^11.1.0",
53
53
  "rimraf": "^5.0.1",
54
- "sinon": "^16.0.0",
54
+ "sinon": "^17.0.1",
55
55
  "test": "^3.2.1",
56
56
  "tmp": "^0.2.1"
57
57
  },
58
58
  "peerDependencies": {
59
- "@xen-orchestra/xapi": "^3.3.0"
59
+ "@xen-orchestra/xapi": "^4.0.0"
60
60
  },
61
61
  "license": "AGPL-3.0-or-later",
62
62
  "author": {