@xen-orchestra/backups 0.68.1 → 0.69.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/RemoteAdapter.mjs CHANGED
@@ -31,7 +31,7 @@ import { listPartitions, LVM_PARTITION_TYPE_MBR, LVM_PARTITION_TYPE_GPT } from '
31
31
  import { lvs, pvs } from './_lvm.mjs'
32
32
  import { watchStreamSize } from './_watchStreamSize.mjs'
33
33
 
34
- import { RemoteVhd } from './disks/RemoteVhd.mjs'
34
+ import { RemoteVhdDisk } from './disks/RemoteVhdDisk.mjs'
35
35
  import { openDiskChain } from './disks/openDiskChain.mjs'
36
36
  import { toVhdStream, writeToVhdDirectory } from 'vhd-lib/disk-consumer/index.mjs'
37
37
  import { ReadAhead } from '@xen-orchestra/disk-transform'
@@ -44,7 +44,7 @@ const IMMUTABILITY_METADATA_FILENAME = '/immutability.json'
44
44
 
45
45
  const { debug, warn } = createLogger('xo:backups:RemoteAdapter')
46
46
 
47
- const compareTimestamp = (a, b) => a.timestamp - b.timestamp
47
+ export const compareTimestamp = (a, b) => a.timestamp - b.timestamp
48
48
 
49
49
  const noop = Function.prototype
50
50
 
@@ -744,7 +744,7 @@ export class RemoteAdapter {
744
744
  if (useChain) {
745
745
  disk = await openDiskChain({ handler, path })
746
746
  } else {
747
- disk = new RemoteVhd({ handler, path })
747
+ disk = new RemoteVhdDisk({ handler, path })
748
748
  await disk.init()
749
749
  }
750
750
  disk = new ReadAhead(disk)
package/_cleanVm.mjs CHANGED
@@ -6,7 +6,9 @@ import { isVhdAlias, resolveVhdAlias } from 'vhd-lib/aliases.js'
6
6
  import { basename, dirname, resolve } from 'node:path'
7
7
  import { isMetadataFile, isVhdFile, isVhdSumFile, isXvaFile, isXvaSumFile } from './_backupType.mjs'
8
8
  import { limitConcurrency } from 'limit-concurrency-decorator'
9
- import { mergeVhdChain } from 'vhd-lib/merge.js'
9
+ import { RemoteVhdDisk } from './disks/RemoteVhdDisk.mjs'
10
+ import { RemoteVhdDiskChain } from './disks/RemoteVhdDiskChain.mjs'
11
+ import { MergeRemoteDisk } from './disks/MergeRemoteDisk.mjs'
10
12
 
11
13
  import { Task } from './Task.mjs'
12
14
  import { Disposable } from 'promise-toolbox'
@@ -51,7 +53,15 @@ async function _mergeVhdChain(handler, chain, { logInfo, remove, mergeBlockConcu
51
53
  }
52
54
  }, 10e3)
53
55
  try {
54
- return await mergeVhdChain(handler, chain, {
56
+ const parentDisk = new RemoteVhdDisk({ handler, path: chain.shift() })
57
+
58
+ const childDisks = []
59
+ for (const path of chain) {
60
+ childDisks.push(new RemoteVhdDisk({ handler, path }))
61
+ }
62
+ const childDiskChain = new RemoteVhdDiskChain({ disks: childDisks })
63
+
64
+ const mergeRemoteDisk = new MergeRemoteDisk(handler, {
55
65
  logInfo,
56
66
  mergeBlockConcurrency,
57
67
  onProgress({ done: d, total: t }) {
@@ -60,6 +70,14 @@ async function _mergeVhdChain(handler, chain, { logInfo, remove, mergeBlockConcu
60
70
  },
61
71
  removeUnused: remove,
62
72
  })
73
+
74
+ const isResumingMerge = await mergeRemoteDisk.isResuming(parentDisk)
75
+ await parentDisk.init({ force: isResumingMerge })
76
+ await childDiskChain.init({ force: isResumingMerge })
77
+
78
+ const result = await mergeRemoteDisk.merge(parentDisk, childDiskChain)
79
+
80
+ return result
63
81
  } finally {
64
82
  clearInterval(handle)
65
83
  }
@@ -521,14 +539,14 @@ export async function cleanVm(
521
539
  const metadataWithMergedVhd = {}
522
540
  const doMerge = async () => {
523
541
  await asyncMap(toMerge, async chain => {
524
- const { finalVhdSize } = await limitedMergeVhdChain(handler, chain, {
542
+ const { finalDiskSize } = await limitedMergeVhdChain(handler, chain, {
525
543
  logInfo,
526
544
  logWarn,
527
545
  remove,
528
546
  mergeBlockConcurrency,
529
547
  })
530
548
  const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metadata file
531
- metadataWithMergedVhd[metadataPath] = (metadataWithMergedVhd[metadataPath] ?? 0) + finalVhdSize
549
+ metadataWithMergedVhd[metadataPath] = (metadataWithMergedVhd[metadataPath] ?? 0) + finalDiskSize
532
550
  })
533
551
  }
534
552
 
@@ -2,10 +2,11 @@ import { AbstractRemote } from './_AbstractRemote.mjs'
2
2
  import { FullRemoteWriter } from '../_writers/FullRemoteWriter.mjs'
3
3
  import { forkStreamUnpipe } from '../_forkStreamUnpipe.mjs'
4
4
  import { watchStreamSize } from '../../_watchStreamSize.mjs'
5
+ import { AggregatedFullRemoteWriter } from '../_writers/AggregatedFullRemoteWriter.mjs'
5
6
 
6
7
  export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote {
7
- _getRemoteWriter() {
8
- return FullRemoteWriter
8
+ _getRemoteWriters() {
9
+ return [FullRemoteWriter, AggregatedFullRemoteWriter]
9
10
  }
10
11
 
11
12
  _filterTransferList(transferList) {
@@ -5,12 +5,14 @@ import { FullRemoteWriter } from '../_writers/FullRemoteWriter.mjs'
5
5
  import { FullXapiWriter } from '../_writers/FullXapiWriter.mjs'
6
6
  import { watchStreamSize } from '../../_watchStreamSize.mjs'
7
7
  import { AbstractXapi } from './_AbstractXapi.mjs'
8
+ import { AggregatedFullRemoteWriter } from '../_writers/AggregatedFullRemoteWriter.mjs'
9
+ import { AggregatedFullXapiWriter } from '../_writers/AggregatedFullXapiWriter.mjs'
8
10
 
9
11
  const { debug } = createLogger('xo:backups:FullXapiVmBackup')
10
12
 
11
13
  export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
12
14
  _getWriters() {
13
- return [FullRemoteWriter, FullXapiWriter]
15
+ return [FullRemoteWriter, FullXapiWriter, AggregatedFullRemoteWriter, AggregatedFullXapiWriter]
14
16
  }
15
17
 
16
18
  async _mustDoSnapshot() {
@@ -11,11 +11,12 @@ import { Disposable } from 'promise-toolbox'
11
11
  import { openVhd } from 'vhd-lib'
12
12
  import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
13
13
  import { SynchronizedDisk, ThrottledDisk } from '@xen-orchestra/disk-transform'
14
+ import { AggregatedIncrementalRemoteWriter } from '../_writers/AggregatedIncrementalRemoteWriter.mjs'
14
15
 
15
16
  const { warn } = createLogger('xo:backups:Incrementalremote')
16
17
  class IncrementalRemoteVmBackupRunner extends AbstractRemote {
17
- _getRemoteWriter() {
18
- return IncrementalRemoteWriter
18
+ _getRemoteWriters() {
19
+ return [IncrementalRemoteWriter, AggregatedIncrementalRemoteWriter]
19
20
  }
20
21
 
21
22
  // we'll transfer the full list if at least one backup should be transferred
@@ -15,12 +15,19 @@ import {
15
15
  markExportSuccessfull,
16
16
  } from '../../_otherConfig.mjs'
17
17
  import { ThrottledDisk, SynchronizedDisk } from '@xen-orchestra/disk-transform'
18
+ import { AggregatedIncrementalRemoteWriter } from '../_writers/AggregatedIncrementalRemoteWriter.mjs'
19
+ import { AggregatedIncrementalXapiWriter } from '../_writers/AggregatedIncrementalXapiWriter.mjs'
18
20
 
19
21
  const { debug } = createLogger('xo:backups:IncrementalXapiVmBackup')
20
22
 
21
23
  export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXapi {
22
24
  _getWriters() {
23
- return [IncrementalRemoteWriter, IncrementalXapiWriter]
25
+ return [
26
+ IncrementalRemoteWriter,
27
+ IncrementalXapiWriter,
28
+ AggregatedIncrementalRemoteWriter,
29
+ AggregatedIncrementalXapiWriter,
30
+ ]
24
31
  }
25
32
 
26
33
  async _mustDoSnapshot() {
@@ -45,26 +45,41 @@ export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstrac
45
45
  const writers = new Set()
46
46
  this._writers = writers
47
47
 
48
- const RemoteWriter = this._getRemoteWriter()
49
- extractIdsFromSimplePattern(job.remotes).forEach(remoteId => {
50
- const adapter = remoteAdapters[remoteId]
51
- const targetSettings = {
52
- ...settings,
53
- ...allSettings[remoteId],
54
- }
48
+ const [BackupWriter, AggregratedBackupWriter] = this._getRemoteWriters()
49
+ if (settings.distributeBackups) {
55
50
  writers.add(
56
- new RemoteWriter({
57
- adapter,
51
+ new AggregratedBackupWriter({
52
+ adapters: remoteAdapters,
53
+ BackupWriter,
58
54
  config,
59
55
  healthCheckSr,
60
56
  job,
61
57
  scheduleId: schedule.id,
62
58
  vmUuid,
63
- remoteId,
64
- settings: targetSettings,
59
+ settings,
65
60
  })
66
61
  )
67
- })
62
+ } else {
63
+ extractIdsFromSimplePattern(job.remotes).forEach(remoteId => {
64
+ const adapter = remoteAdapters[remoteId]
65
+ const targetSettings = {
66
+ ...settings,
67
+ ...allSettings[remoteId],
68
+ }
69
+ writers.add(
70
+ new BackupWriter({
71
+ adapter,
72
+ config,
73
+ healthCheckSr,
74
+ job,
75
+ scheduleId: schedule.id,
76
+ vmUuid,
77
+ remoteId,
78
+ settings: targetSettings,
79
+ })
80
+ )
81
+ })
82
+ }
68
83
  const { filter } = job
69
84
  if (filter === undefined) {
70
85
  this._filterPredicate = () => true
@@ -82,48 +82,84 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
82
82
  const writers = new Set()
83
83
  this._writers = writers
84
84
 
85
- const [BackupWriter, ReplicationWriter] = this._getWriters()
85
+ const [BackupWriter, ReplicationWriter, AggregratedBackupWriter, AggregratedReplicationWriter] =
86
+ this._getWriters()
86
87
 
87
88
  const allSettings = job.settings
88
- Object.entries(remoteAdapters).forEach(([remoteId, adapter]) => {
89
- const targetSettings = {
90
- ...settings,
91
- ...allSettings[remoteId],
92
- }
93
- if (targetSettings.exportRetention !== 0) {
89
+
90
+ if (settings.exportRetention > 0) {
91
+ if (settings.distributeBackups) {
94
92
  writers.add(
95
- new BackupWriter({
96
- adapter,
93
+ new AggregratedBackupWriter({
94
+ adapters: remoteAdapters,
95
+ BackupWriter,
97
96
  config,
98
97
  healthCheckSr,
99
98
  job,
100
99
  scheduleId: schedule.id,
101
100
  vmUuid: vm.uuid,
102
- remoteId,
103
- settings: targetSettings,
101
+ settings,
104
102
  })
105
103
  )
104
+ } else {
105
+ Object.entries(remoteAdapters).forEach(([remoteId, adapter]) => {
106
+ const targetSettings = {
107
+ ...settings,
108
+ ...allSettings[remoteId],
109
+ }
110
+ if (targetSettings.exportRetention !== 0) {
111
+ writers.add(
112
+ new BackupWriter({
113
+ adapter,
114
+ config,
115
+ healthCheckSr,
116
+ job,
117
+ scheduleId: schedule.id,
118
+ vmUuid: vm.uuid,
119
+ remoteId,
120
+ settings: targetSettings,
121
+ })
122
+ )
123
+ }
124
+ })
106
125
  }
107
- })
108
- srs.forEach(sr => {
109
- const targetSettings = {
110
- ...settings,
111
- ...allSettings[sr.uuid],
112
- }
113
- if (targetSettings.copyRetention !== 0) {
126
+ }
127
+ if (settings.copyRetention) {
128
+ if (settings.distributeReplications) {
114
129
  writers.add(
115
- new ReplicationWriter({
130
+ new AggregratedReplicationWriter({
116
131
  config,
117
132
  healthCheckSr,
118
133
  job,
134
+ ReplicationWriter,
119
135
  scheduleId: schedule.id,
120
136
  vmUuid: vm.uuid,
121
- sr,
122
- settings: targetSettings,
137
+ srs,
138
+ settings,
123
139
  })
124
140
  )
141
+ } else {
142
+ srs.forEach(sr => {
143
+ const targetSettings = {
144
+ ...settings,
145
+ ...allSettings[sr.uuid],
146
+ }
147
+ if (targetSettings.copyRetention !== 0) {
148
+ writers.add(
149
+ new ReplicationWriter({
150
+ config,
151
+ healthCheckSr,
152
+ job,
153
+ scheduleId: schedule.id,
154
+ vmUuid: vm.uuid,
155
+ sr,
156
+ settings: targetSettings,
157
+ })
158
+ )
159
+ }
160
+ })
125
161
  }
126
- })
162
+ }
127
163
  }
128
164
  }
129
165
 
@@ -0,0 +1,38 @@
1
+ import { AbstractAggregatedRemoteWriter } from './_AbstractAggregatedRemoteWriter.mjs'
2
+
3
+ export class AggregatedFullRemoteWriter extends AbstractAggregatedRemoteWriter {
4
+ async deleteOldBackupsOnAdapter(adapter, backups) {
5
+ await adapter.deleteFullVmBackups(backups)
6
+ }
7
+
8
+ // take the lock on every adapters with this VM
9
+ // cleanup if deleteFirst is selected
10
+ async beforeBackup() {
11
+ await this.setupWriters()
12
+ await Promise.all(this.writers.map(writer => writer.beforeBackup()))
13
+
14
+ await this.setOldBackups()
15
+ if (this.props.settings.deleteFirst) {
16
+ await this.deleteOldBackups()
17
+ }
18
+ }
19
+
20
+ async run(props) {
21
+ const mainAdapter = await this.computeAdapterCandidate()
22
+ this.mainWriter = this.getWriterByAdapter(mainAdapter)
23
+ return this.mainWriter.run(props)
24
+ }
25
+
26
+ // cleanup if deleteFirst is NOT selected
27
+ // release the lock on every adapters with this VM
28
+ async afterBackup() {
29
+ if (!this.props.settings.deleteFirst) {
30
+ await this.deleteOldBackups()
31
+ }
32
+ await Promise.all(this.writers.map(writer => writer.afterBackup()))
33
+ }
34
+
35
+ healthCheck() {
36
+ return this.mainWriter.healthCheck()
37
+ }
38
+ }
@@ -0,0 +1,31 @@
1
+ import { AbstractAggregatedXapiWriter } from './_AbstractAggregatedXapiWriter.mjs'
2
+
3
+ export class AggregatedFullXapiWriter extends AbstractAggregatedXapiWriter {
4
+ // delete replica if deleteFirst is selected
5
+ async beforeBackup() {
6
+ await this.setupWriters()
7
+ await Promise.all(this.writers.map(writer => writer.beforeBackup()))
8
+ this.setOldReplicaList()
9
+ if (this.props.settings.deleteFirst) {
10
+ await this.deleteOldReplicas()
11
+ }
12
+ }
13
+
14
+ async run(props) {
15
+ const mainSr = await this.computeSRCandidate()
16
+ this.mainWriter = this.getWriterBySr(mainSr)
17
+ await this.mainWriter.run(props)
18
+ }
19
+
20
+ // delete replica if not using deleteFirst
21
+ async afterBackup() {
22
+ if (!this.props.settings.deleteFirst) {
23
+ await this.deleteOldReplicas()
24
+ }
25
+ await Promise.all(this.writers.map(writer => writer.afterBackup()))
26
+ }
27
+
28
+ healthCheck() {
29
+ return this.mainWriter.healthCheck()
30
+ }
31
+ }
@@ -0,0 +1,78 @@
1
+ import { AbstractAggregatedRemoteWriter } from './_AbstractAggregatedRemoteWriter.mjs'
2
+ export class AggregatedIncrementalRemoteWriter extends AbstractAggregatedRemoteWriter {
3
+ async deleteOldBackupsOnAdapter(adapter, backups) {
4
+ await adapter.deleteDeltaVmBackups(backups)
5
+ }
6
+
7
+ /**
8
+ *
9
+ * @param {Map<string,string>} baseUuidToSrcVdi
10
+ */
11
+ async checkBaseVdis(baseUuidToSrcVdi) {
12
+ let selectedCopy = new Map()
13
+ for (const writer of this.writers) {
14
+ const copy = new Map(baseUuidToSrcVdi)
15
+ await writer.checkBaseVdis(copy)
16
+ if (copy.size > 0) {
17
+ // there can be multiple candidates
18
+ // we use the last one in this case
19
+ this.mainWriter = writer
20
+ selectedCopy = copy
21
+ }
22
+ }
23
+ // update the parameter with our best candidate
24
+ // that can be an empty map
25
+ baseUuidToSrcVdi.forEach(key => {
26
+ if (!selectedCopy.has(key)) {
27
+ baseUuidToSrcVdi.delete(key)
28
+ }
29
+ })
30
+ }
31
+
32
+ // take the lock on every adapters with this VM
33
+ // cleanup if deleteFirst is selected
34
+ async beforeBackup() {
35
+ await this.setupWriters()
36
+ await Promise.all(this.writers.map(writer => writer.beforeBackup()))
37
+
38
+ await this.setOldBackups()
39
+ if (this.props.settings.deleteFirst) {
40
+ await this.deleteOldBackups()
41
+ }
42
+ }
43
+
44
+ async prepare({ isFull }) {
45
+ if (this.mainWriter === undefined) {
46
+ // use the remote with the most empty space for starting a new chain
47
+ const mainAdapter = await this.computeAdapterCandidate()
48
+ this.mainWriter = this.getWriterByAdapter(mainAdapter)
49
+ }
50
+ await Promise.all(this.writers.map(writer => writer.prepare({ isFull })))
51
+ }
52
+ // write only on the main writer
53
+ transfer({ isVhdDifferencing, timestamp, deltaExport, vm, vmSnapshot }) {
54
+ return this.mainWriter.transfer({ isVhdDifferencing, timestamp, deltaExport, vm, vmSnapshot })
55
+ }
56
+
57
+ // chain only on the main writer
58
+ updateUuidAndChain({ isVhdDifferencing, vdis }) {
59
+ return this.mainWriter.updateUuidAndChain({ isVhdDifferencing, vdis })
60
+ }
61
+
62
+ // remove the backups and remove the entries
63
+ async cleanup() {
64
+ if (!this.props.settings.deleteFirst) {
65
+ await this.deleteOldBackups()
66
+ }
67
+ await Promise.all(this.writers.map(writer => writer.cleanup()))
68
+ }
69
+ // cleanVM on all, since we may want to delete a chani
70
+ // that is not on the main writer
71
+ async afterBackup() {
72
+ await Promise.all(this.writers.map(writer => writer.afterBackup()))
73
+ }
74
+
75
+ healthCheck() {
76
+ return this.mainWriter.healthCheck()
77
+ }
78
+ }
@@ -0,0 +1,85 @@
1
+ import { AbstractAggregatedXapiWriter } from './_AbstractAggregatedXapiWriter.mjs'
2
+ import { createLogger } from '@xen-orchestra/log'
3
+
4
+ const { debug } = createLogger('xo:backups:AggregatedIncrementalXapiWriter')
5
+ export class AggregatedIncrementalXapiWriter extends AbstractAggregatedXapiWriter {
6
+ /**
7
+ *
8
+ * @param {Map<string,string>} baseUuidToSrcVdi
9
+ */
10
+ async checkBaseVdis(baseUuidToSrcVdi) {
11
+ debug('checkBaseVdis', { baseUuidToSrcVdi })
12
+ let selectedCopy = new Map()
13
+ for (const writer of this.writers) {
14
+ const copy = new Map(baseUuidToSrcVdi)
15
+ await writer.checkBaseVdis(copy)
16
+ if (copy.size > 0) {
17
+ debug('checkBaseVdis found a mainwriter candidate ', writer._sr.name_label)
18
+ // there can be multiple candidates
19
+ // we use the last one in this case
20
+ this.mainWriter = writer
21
+ selectedCopy = copy
22
+ } else {
23
+ debug(writer._sr.name_label, 'is not a delta candidate')
24
+ }
25
+ }
26
+ // update the parameter with our best candidate
27
+ // that can be an empty map
28
+ baseUuidToSrcVdi.forEach(key => {
29
+ if (!selectedCopy.has(key)) {
30
+ baseUuidToSrcVdi.delete(key)
31
+ }
32
+ })
33
+ debug('checkBaseVdis', { baseUuidToSrcVdi })
34
+ }
35
+
36
+ // take the lock on every adapters with this VM
37
+ // cleanup if deleteFirst is selected
38
+ async beforeBackup() {
39
+ await this.setupWriters()
40
+ await Promise.all(this.writers.map(writer => writer.beforeBackup()))
41
+ this.setOldReplicaList()
42
+ if (this.props.settings.deleteFirst) {
43
+ await this.deleteOldReplicas()
44
+ }
45
+ }
46
+
47
+ async prepare(args) {
48
+ if (this.mainWriter === undefined) {
49
+ debug('no mainwriter found, fallback to a new SR')
50
+ // use the SR with the most empty space for starting a new chain
51
+ const mainSr = await this.computeSRCandidate()
52
+ this.mainWriter = this.getWriterBySr(mainSr)
53
+ }
54
+ await Promise.all(this.writers.map(writer => writer.prepare(args)))
55
+ }
56
+
57
+ // write only on the main writer
58
+ transfer(args) {
59
+ return this.mainWriter.transfer(args)
60
+ }
61
+
62
+ // chain only on the main writer
63
+ updateUuidAndChain(args) {
64
+ return this.mainWriter.updateUuidAndChain(args)
65
+ }
66
+
67
+ // remove the backups and remove the entries
68
+ async cleanup() {
69
+ debug('cleanup')
70
+ if (!this.props.settings.deleteFirst) {
71
+ await this.deleteOldReplicas()
72
+ }
73
+ await Promise.all(this.writers.map(writer => writer.cleanup()))
74
+ }
75
+
76
+ // cleanVM on all, since we may want to delete a chani
77
+ // that is not on the main writer
78
+ async afterBackup() {
79
+ await Promise.all(this.writers.map(writer => writer.afterBackup()))
80
+ }
81
+
82
+ healthCheck() {
83
+ return this.mainWriter.healthCheck()
84
+ }
85
+ }
@@ -59,8 +59,8 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
59
59
  xva: './' + dataBasename,
60
60
  }
61
61
 
62
- const { deleteFirst } = settings
63
- if (deleteFirst) {
62
+ const { skipDeleteOldEntries, deleteFirst } = settings
63
+ if (!skipDeleteOldEntries && deleteFirst) {
64
64
  await deleteOldBackups()
65
65
  }
66
66
 
@@ -76,7 +76,7 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
76
76
  metadata.tags = await this.getLongTermRetentionTags(metadata)
77
77
  this._metadataFileName = await adapter.writeVmBackupMetadata(vm.uuid, metadata)
78
78
 
79
- if (!deleteFirst) {
79
+ if (!skipDeleteOldEntries && !deleteFirst) {
80
80
  await deleteOldBackups()
81
81
  }
82
82
 
@@ -46,8 +46,8 @@ export class FullXapiWriter extends MixinXapiWriter(AbstractFullWriter) {
46
46
  const oldVms = getOldEntries(settings.copyRetention - 1, listReplicatedVms(xapi, scheduleId, srUuid, vm.uuid))
47
47
 
48
48
  const deleteOldBackups = () => asyncMapSettled(oldVms, vm => xapi.VM_destroy(vm.$ref))
49
- const { deleteFirst, _warmMigration } = settings
50
- if (deleteFirst) {
49
+ const { deleteFirst, skipDeleteOldEntries, _warmMigration } = settings
50
+ if (!skipDeleteOldEntries && deleteFirst) {
51
51
  await deleteOldBackups()
52
52
  }
53
53
 
@@ -85,7 +85,7 @@ export class FullXapiWriter extends MixinXapiWriter(AbstractFullWriter) {
85
85
  }),
86
86
  ])
87
87
 
88
- if (!deleteFirst) {
88
+ if (!skipDeleteOldEntries && !deleteFirst) {
89
89
  await deleteOldBackups()
90
90
  }
91
91
  }
@@ -114,13 +114,13 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
114
114
  oldEntries.length = maxMergedDeltasPerRun
115
115
  }
116
116
 
117
- if (settings.deleteFirst) {
117
+ if (settings.deleteFirst && !settings.skipDeleteOldEntries) {
118
118
  await this._deleteOldEntries()
119
119
  }
120
120
  }
121
121
 
122
122
  async cleanup() {
123
- if (!this._settings.deleteFirst) {
123
+ if (!this._settings.deleteFirst && !this._settings.skipDeleteOldEntries) {
124
124
  await this._deleteOldEntries()
125
125
  }
126
126
  }
@@ -83,7 +83,7 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
83
83
 
84
84
  this._oldEntries = getOldEntries(settings.copyRetention - 1, listReplicatedVms(xapi, scheduleId, srUuid, vmUuid))
85
85
 
86
- if (settings.deleteFirst) {
86
+ if (settings.deleteFirst && settings.skipDeleteOldEntries) {
87
87
  // we want to keep the baseVM when copying a delta
88
88
  // even if we want to keep only one after
89
89
  let mostRecentEntry
@@ -96,11 +96,13 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
96
96
  }
97
97
 
98
98
  async cleanup() {
99
- await this._deleteOldEntries()
99
+ if (!this._settings.skipDeleteOldEntries) {
100
+ await this._deleteOldEntries()
101
+ }
100
102
  }
101
103
 
102
104
  async _deleteOldEntries() {
103
- return asyncMapSettled(this._oldEntries, vm => vm.$destroy())
105
+ return asyncMapSettled(this._oldEntries, vm => vm.$destroy({ bypassBlockedOperation: true }))
104
106
  }
105
107
 
106
108
  #decorateVmMetadata(backup, timestamp) {
@@ -175,7 +177,7 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
175
177
  const job = this._job
176
178
  const scheduleId = this._scheduleId
177
179
  const { uuid: srUuid, $xapi: xapi } = sr
178
-
180
+
179
181
  let targetVmRef
180
182
  await Task.run({ name: 'transfer' }, async () => {
181
183
  targetVmRef = await importIncrementalVm(this.#decorateVmMetadata(deltaExport, timestamp), sr)