@xen-orchestra/backups 0.68.2 → 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 +3 -3
- package/_cleanVm.mjs +22 -4
- package/_runners/_vmRunners/FullRemote.mjs +3 -2
- package/_runners/_vmRunners/FullXapi.mjs +3 -1
- package/_runners/_vmRunners/IncrementalRemote.mjs +3 -2
- package/_runners/_vmRunners/IncrementalXapi.mjs +8 -1
- package/_runners/_vmRunners/_AbstractRemote.mjs +27 -12
- package/_runners/_vmRunners/_AbstractXapi.mjs +58 -22
- package/_runners/_writers/AggregatedFullRemoteWriter.mjs +38 -0
- package/_runners/_writers/AggregatedFullXapiWriter.mjs +31 -0
- package/_runners/_writers/AggregatedIncrementalRemoteWriter.mjs +78 -0
- package/_runners/_writers/AggregatedIncrementalXapiWriter.mjs +85 -0
- package/_runners/_writers/FullRemoteWriter.mjs +3 -3
- package/_runners/_writers/FullXapiWriter.mjs +3 -3
- package/_runners/_writers/IncrementalRemoteWriter.mjs +2 -2
- package/_runners/_writers/IncrementalXapiWriter.mjs +4 -2
- package/_runners/_writers/_AbstractAggregatedRemoteWriter.mjs +157 -0
- package/_runners/_writers/_AbstractAggregatedXapiWriter.mjs +114 -0
- package/_runners/_writers/_listReplicatedVms.mjs +10 -2
- package/disks/MergeRemoteDisk.mjs +317 -0
- package/disks/RemoteDisk.mjs +204 -0
- package/disks/RemoteVhdDisk.mjs +458 -0
- package/disks/RemoteVhdDiskChain.mjs +271 -0
- package/disks/openDiskChain.mjs +8 -3
- package/package.json +2 -2
- package/disks/RemoteVhd.mjs +0 -171
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 {
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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) +
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
|
49
|
-
|
|
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
|
|
57
|
-
|
|
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
|
-
|
|
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] =
|
|
85
|
+
const [BackupWriter, ReplicationWriter, AggregratedBackupWriter, AggregratedReplicationWriter] =
|
|
86
|
+
this._getWriters()
|
|
86
87
|
|
|
87
88
|
const allSettings = job.settings
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
|
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
|
-
|
|
122
|
-
settings
|
|
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,7 +96,9 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
async cleanup() {
|
|
99
|
-
|
|
99
|
+
if (!this._settings.skipDeleteOldEntries) {
|
|
100
|
+
await this._deleteOldEntries()
|
|
101
|
+
}
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
async _deleteOldEntries() {
|