@xen-orchestra/backups 0.51.0 → 0.52.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/_backupWorker.mjs CHANGED
@@ -23,7 +23,7 @@ createCachedLookup().patchGlobal()
23
23
 
24
24
  const logger = createLogger('xo:backups:worker')
25
25
  catchGlobalErrors(logger)
26
- const { debug, info } = logger
26
+ const { debug, info, warn } = logger
27
27
 
28
28
  class BackupWorker {
29
29
  #config
@@ -204,6 +204,13 @@ process.on('message', async message => {
204
204
  info('backup has ended')
205
205
  await ignoreErrors.call(backupWorker.debounceResource.flushAll())
206
206
  process.disconnect()
207
+
208
+ setTimeout(() => {
209
+ warn('worker process did not exit automatically, forcing...')
210
+
211
+ // eslint-disable-next-line n/no-process-exit
212
+ process.exit()
213
+ }, 30e3).unref()
207
214
  }
208
215
  }
209
216
  })
@@ -15,6 +15,7 @@ const noop = Function.prototype
15
15
 
16
16
  const DEFAULT_XAPI_VM_SETTINGS = {
17
17
  bypassVdiChainsCheck: false,
18
+ cbtDestroySnapshotData: false,
18
19
  checkpointSnapshot: false,
19
20
  concurrency: 2,
20
21
  copyRetention: 0,
@@ -147,8 +147,13 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
147
147
  ...lastExportedVdis.map(({ other_config }) => Number(other_config[DELTA_CHAIN_LENGTH] ?? 0))
148
148
  )
149
149
  const fullInterval = this._settings.fullInterval
150
- if ( fullInterval !== 0 && fullInterval < deltaChainLength + 1) {
151
- debug('not using base VM because fullInterval reached', { fullInterval, deltaChainLength, eq: fullInterval < deltaChainLength + 1, dc1: deltaChainLength + 1 })
150
+ if (fullInterval !== 0 && fullInterval <= deltaChainLength + 1) {
151
+ debug('not using base VM because fullInterval reached', {
152
+ fullInterval,
153
+ deltaChainLength,
154
+ eq: fullInterval < deltaChainLength + 1,
155
+ dc1: deltaChainLength + 1,
156
+ })
152
157
  return
153
158
  }
154
159
 
@@ -146,6 +146,15 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
146
146
  if (!settings.bypassVdiChainsCheck) {
147
147
  await vm.$assertHealthyVdiChains()
148
148
  }
149
+ if (settings.preferNbd) {
150
+ try {
151
+ // enable CBT on all disks if possible
152
+ const diskRefs = await xapi.VM_getDisks(vm.$ref)
153
+ await Promise.all(diskRefs.map(diskRef => xapi.call('VDI.enable_cbt', diskRef)))
154
+ } catch (error) {
155
+ Task.info(`couldn't enable CBT`, error)
156
+ }
157
+ }
149
158
 
150
159
  const snapshotRef = await vm[settings.checkpointSnapshot ? '$checkpoint' : '$snapshot']({
151
160
  ignoredVdisTag: '[NOBAK]',
@@ -160,7 +169,6 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
160
169
  vmUuid: vm.uuid,
161
170
  })
162
171
  this._exportedVm = await xapi.getRecord('VM', snapshotRef)
163
-
164
172
  return this._exportedVm.uuid
165
173
  })
166
174
  } else {
@@ -208,7 +216,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
208
216
 
209
217
  const snapshotsPerSchedule = groupBy(this._jobSnapshotVdis, _ => _.other_config[SCHEDULE_ID])
210
218
  const xapi = this._xapi
211
- await asyncMap(Object.entries(snapshotsPerSchedule), ([scheduleId, snapshots]) => {
219
+ await asyncMap(Object.entries(snapshotsPerSchedule), async ([scheduleId, snapshots]) => {
212
220
  const snapshotPerDatetime = groupBy(snapshots, _ => _.other_config[DATETIME])
213
221
 
214
222
  const datetimes = Object.keys(snapshotPerDatetime)
@@ -219,12 +227,11 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
219
227
  ...allSettings[scheduleId],
220
228
  ...allSettings[this._vm.uuid],
221
229
  }
222
- // ensure we never delete the last one
223
- const retention = Math.max(settings.snapshotRetention ?? 0, 1)
224
-
225
- return asyncMap(getOldEntries(retention, datetimes), async datetime => {
230
+ // ensure we never delete the last one for delta
231
+ const minRetention = this.job.mode === 'delta' ? 1 : 0
232
+ const retention = Math.max(settings.snapshotRetention ?? 0, minRetention)
233
+ await asyncMap(getOldEntries(retention, datetimes), async datetime => {
226
234
  const vdis = snapshotPerDatetime[datetime]
227
-
228
235
  let vmRef
229
236
  // if there is an attached VM => destroy the VM (Non CBT backups)
230
237
  for (const vdi of vdis) {
@@ -234,8 +241,8 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
234
241
  // this will throw error for VDI still attached to control domain
235
242
  assert.strictEqual(vbds.length, 1, 'VDI must be free or attached to exactly one VM')
236
243
  const vm = vbds[0].$VM
244
+ assert.strictEqual(vm.is_control_domain, false, `Disk is still attached to DOM0 VM`) // don't delete a VM (especially a control domain)
237
245
  assert.strictEqual(vm.is_a_snapshot, true, `VM must be a snapshot`) // don't delete a VM (especially a control domain)
238
- assert.strictEqual(vm.is_control_domain, false, `VM can't be a DOM0 VM`) // don't delete a VM (especially a control domain)
239
246
 
240
247
  const vmRefVdi = vm.$ref
241
248
  // same vm than other vdi of the same batch
@@ -257,6 +264,34 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
257
264
  }
258
265
  })
259
266
  })
267
+
268
+ // now that we use CBT, we can destroy the data of the snapshot used for this backup
269
+ // going back to a previous version of XO not supporting CBT will create a full backup
270
+ // this will only do something after snapshot and transfer
271
+ if (
272
+ // don't modify the VM
273
+ this._exportedVm?.is_a_snapshot &&
274
+ // user don't want to keep the snapshot data
275
+ this._settings.snapshotRetention === 0 &&
276
+ // preferNbd is not a guarantee that the backup used NBD, depending on the network configuration,
277
+ // in that case next runs will be full, but there is not an easy way to prevent that
278
+ this._settings.preferNbd &&
279
+ // only delete snapshost data if the config allows it
280
+ this._settings.cbtDestroySnapshotData
281
+ ) {
282
+ Task.info('will delete snapshot data')
283
+ const vdiRefs = await this._xapi.VM_getDisks(this._exportedVm?.$ref)
284
+ await xapi.call('VM.destroy', this._exportedVm.$ref)
285
+ for (const vdiRef of vdiRefs) {
286
+ try {
287
+ // data_destroy will fail with a VDI_NO_CBT_METADATA error if CBT is not enabled on this VDI
288
+ await xapi.call('VDI.data_destroy', vdiRef)
289
+ Task.info(`Snapshot data has been deleted`, { vdiRef })
290
+ } catch (error) {
291
+ Task.warning(`Couldn't deleted snapshot data`, { error, vdiRef })
292
+ }
293
+ }
294
+ }
260
295
  }
261
296
 
262
297
  async copy() {
@@ -19,10 +19,10 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
19
19
  // @todo use an index if possible
20
20
  // @todo : this seems similare to decorateVmMetadata
21
21
 
22
- const replicatedVdis = sr.$VDIs
23
- .filter(({ other_config }) => {
22
+ const replicatedVdis = sr.$VDIs
23
+ .filter(vdi => {
24
24
  // REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
25
- return baseUuidToSrcVdi.has(other_config?.[COPY_OF])
25
+ return baseUuidToSrcVdi.has(vdi?.other_config[COPY_OF])
26
26
  })
27
27
  .map(({ other_config }) => other_config?.[COPY_OF])
28
28
  .filter(_ => !!_)
@@ -103,10 +103,11 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
103
103
  .filter(_ => !!_)
104
104
  // @todo use index ?
105
105
 
106
- const replicatedVdis = sr.$VDIs.filter(({ other_config }) => {
107
- // REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
108
- return sourceVdiUuids.includes(other_config?.[COPY_OF])
109
- })
106
+ const replicatedVdis = sr.$VDIs
107
+ .filter(vdi => {
108
+ // REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
109
+ return sourceVdiUuids.includes(vdi?.other_config[COPY_OF])
110
+ })
110
111
 
111
112
  Object.values(backup.vdis).forEach(vdi => {
112
113
  vdi.other_config[COPY_OF] = vdi.uuid
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.51.0",
11
+ "version": "0.52.1",
12
12
  "engines": {
13
13
  "node": ">=14.18"
14
14
  },
@@ -46,7 +46,7 @@
46
46
  "proper-lockfile": "^4.1.2",
47
47
  "tar": "^6.1.15",
48
48
  "uuid": "^9.0.0",
49
- "vhd-lib": "^4.10.0",
49
+ "vhd-lib": "^4.11.0",
50
50
  "xen-api": "^4.0.0",
51
51
  "yazl": "^2.5.1"
52
52
  },
@@ -58,7 +58,7 @@
58
58
  "tmp": "^0.2.1"
59
59
  },
60
60
  "peerDependencies": {
61
- "@xen-orchestra/xapi": "^7.0.0"
61
+ "@xen-orchestra/xapi": "^7.1.0"
62
62
  },
63
63
  "license": "AGPL-3.0-or-later",
64
64
  "author": {