@xen-orchestra/backups 0.44.6 → 0.45.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/ImportVmBackup.mjs +2 -2
- package/RemoteAdapter.mjs +3 -2
- package/_cleanVm.mjs +2 -1
- package/_runners/VmsRemote.mjs +54 -4
- package/_runners/VmsXapi.mjs +83 -29
- package/_runners/_vmRunners/IncrementalRemote.mjs +50 -1
- package/_runners/_vmRunners/IncrementalXapi.mjs +17 -5
- package/_runners/_vmRunners/_AbstractRemote.mjs +1 -0
- package/_runners/_writers/IncrementalRemoteWriter.mjs +74 -67
- package/_runners/_writers/IncrementalXapiWriter.mjs +5 -1
- package/_runners/_writers/_AbstractIncrementalWriter.mjs +4 -0
- package/_runners/_writers/_MixinRemoteWriter.mjs +7 -7
- package/package.json +4 -4
package/ImportVmBackup.mjs
CHANGED
|
@@ -160,10 +160,10 @@ export class ImportVmBackup {
|
|
|
160
160
|
// update the stream with the negative vhd stream
|
|
161
161
|
stream = await negativeVhd.stream()
|
|
162
162
|
vdis[vdiRef].baseVdi = snapshotCandidate
|
|
163
|
-
} catch (
|
|
163
|
+
} catch (error) {
|
|
164
164
|
// can be a broken VHD chain, a vhd chain with a key backup, ....
|
|
165
165
|
// not an irrecuperable error, don't dispose parentVhd, and fallback to full restore
|
|
166
|
-
warn(`can't use differential restore`,
|
|
166
|
+
warn(`can't use differential restore`, { error })
|
|
167
167
|
disposableDescendants?.dispose()
|
|
168
168
|
}
|
|
169
169
|
}
|
package/RemoteAdapter.mjs
CHANGED
|
@@ -191,13 +191,14 @@ export class RemoteAdapter {
|
|
|
191
191
|
// check if we will be allowed to merge a a vhd created in this adapter
|
|
192
192
|
// with the vhd at path `path`
|
|
193
193
|
async isMergeableParent(packedParentUid, path) {
|
|
194
|
-
return await Disposable.use(
|
|
194
|
+
return await Disposable.use(VhdSynthetic.fromVhdChain(this.handler, path), vhd => {
|
|
195
195
|
// this baseUuid is not linked with this vhd
|
|
196
196
|
if (!vhd.footer.uuid.equals(packedParentUid)) {
|
|
197
197
|
return false
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
// check if all the chain is composed of vhd directory
|
|
201
|
+
const isVhdDirectory = vhd.checkVhdsClass(VhdDirectory)
|
|
201
202
|
return isVhdDirectory
|
|
202
203
|
? this.useVhdDirectory() && this.#getCompressionType() === vhd.compressionType
|
|
203
204
|
: !this.useVhdDirectory()
|
package/_cleanVm.mjs
CHANGED
|
@@ -437,7 +437,8 @@ export async function cleanVm(
|
|
|
437
437
|
}
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
|
|
440
|
+
// no warning because a VHD can be unused for perfectly good reasons,
|
|
441
|
+
// e.g. the corresponding backup (metadata file) has been deleted
|
|
441
442
|
if (remove) {
|
|
442
443
|
logInfo('deleting unused VHD', { path: vhd })
|
|
443
444
|
unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
|
package/_runners/VmsRemote.mjs
CHANGED
|
@@ -6,11 +6,12 @@ import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
|
|
|
6
6
|
import { Task } from '../Task.mjs'
|
|
7
7
|
import createStreamThrottle from './_createStreamThrottle.mjs'
|
|
8
8
|
import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
|
|
9
|
-
import { runTask } from './_runTask.mjs'
|
|
10
9
|
import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
|
|
11
10
|
import { FullRemote } from './_vmRunners/FullRemote.mjs'
|
|
12
11
|
import { IncrementalRemote } from './_vmRunners/IncrementalRemote.mjs'
|
|
13
12
|
|
|
13
|
+
const noop = Function.prototype
|
|
14
|
+
|
|
14
15
|
const DEFAULT_REMOTE_VM_SETTINGS = {
|
|
15
16
|
concurrency: 2,
|
|
16
17
|
copyRetention: 0,
|
|
@@ -20,6 +21,7 @@ const DEFAULT_REMOTE_VM_SETTINGS = {
|
|
|
20
21
|
healthCheckVmsWithTags: [],
|
|
21
22
|
maxExportRate: 0,
|
|
22
23
|
maxMergedDeltasPerRun: Infinity,
|
|
24
|
+
nRetriesVmBackupFailures: 0,
|
|
23
25
|
timeout: 0,
|
|
24
26
|
validateVhdStreams: false,
|
|
25
27
|
vmTimeout: 0,
|
|
@@ -41,6 +43,7 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
|
|
41
43
|
const throttleStream = createStreamThrottle(settings.maxExportRate)
|
|
42
44
|
|
|
43
45
|
const config = this._config
|
|
46
|
+
|
|
44
47
|
await Disposable.use(
|
|
45
48
|
() => this._getAdapter(job.sourceRemote),
|
|
46
49
|
() => (settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined),
|
|
@@ -62,8 +65,19 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
|
|
62
65
|
const allSettings = this._job.settings
|
|
63
66
|
const baseSettings = this._baseSettings
|
|
64
67
|
|
|
68
|
+
const queue = new Set(vmsUuids)
|
|
69
|
+
const taskByVmId = {}
|
|
70
|
+
const nTriesByVmId = {}
|
|
71
|
+
|
|
65
72
|
const handleVm = vmUuid => {
|
|
73
|
+
if (nTriesByVmId[vmUuid] === undefined) {
|
|
74
|
+
nTriesByVmId[vmUuid] = 0
|
|
75
|
+
}
|
|
76
|
+
nTriesByVmId[vmUuid]++
|
|
77
|
+
|
|
66
78
|
const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
|
|
79
|
+
const vmSettings = { ...settings, ...allSettings[vmUuid] }
|
|
80
|
+
const isLastRun = nTriesByVmId[vmUuid] === vmSettings.nRetriesVmBackupFailures + 1
|
|
67
81
|
|
|
68
82
|
const opts = {
|
|
69
83
|
baseSettings,
|
|
@@ -72,7 +86,7 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
|
|
72
86
|
healthCheckSr,
|
|
73
87
|
remoteAdapters,
|
|
74
88
|
schedule,
|
|
75
|
-
settings:
|
|
89
|
+
settings: vmSettings,
|
|
76
90
|
sourceRemoteAdapter,
|
|
77
91
|
throttleStream,
|
|
78
92
|
vmUuid,
|
|
@@ -86,10 +100,46 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
|
|
86
100
|
throw new Error(`Job mode ${job.mode} not implemented for mirror backup`)
|
|
87
101
|
}
|
|
88
102
|
|
|
89
|
-
return
|
|
103
|
+
return sourceRemoteAdapter
|
|
104
|
+
.listVmBackups(vmUuid, ({ mode }) => mode === job.mode)
|
|
105
|
+
.then(vmBackups => {
|
|
106
|
+
// avoiding to create tasks for empty directories
|
|
107
|
+
if (vmBackups.length > 0) {
|
|
108
|
+
if (taskByVmId[vmUuid] === undefined) {
|
|
109
|
+
taskByVmId[vmUuid] = new Task(taskStart)
|
|
110
|
+
}
|
|
111
|
+
const task = taskByVmId[vmUuid]
|
|
112
|
+
return task
|
|
113
|
+
.run(async () => {
|
|
114
|
+
try {
|
|
115
|
+
const result = await vmBackup.run()
|
|
116
|
+
task.success(result)
|
|
117
|
+
return result
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (isLastRun) {
|
|
120
|
+
throw error
|
|
121
|
+
} else {
|
|
122
|
+
Task.warning(`Retry the VM mirror backup due to an error`, {
|
|
123
|
+
attempt: nTriesByVmId[vmUuid],
|
|
124
|
+
error: error.message,
|
|
125
|
+
})
|
|
126
|
+
queue.add(vmUuid)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
.catch(noop)
|
|
131
|
+
}
|
|
132
|
+
})
|
|
90
133
|
}
|
|
91
134
|
const { concurrency } = settings
|
|
92
|
-
|
|
135
|
+
const _handleVm = !concurrency ? handleVm : limitConcurrency(concurrency)(handleVm)
|
|
136
|
+
|
|
137
|
+
while (queue.size > 0) {
|
|
138
|
+
const vmIds = Array.from(queue)
|
|
139
|
+
queue.clear()
|
|
140
|
+
|
|
141
|
+
await asyncMapSettled(vmIds, _handleVm)
|
|
142
|
+
}
|
|
93
143
|
}
|
|
94
144
|
)
|
|
95
145
|
}
|
package/_runners/VmsXapi.mjs
CHANGED
|
@@ -11,6 +11,8 @@ import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
|
|
|
11
11
|
import { IncrementalXapi } from './_vmRunners/IncrementalXapi.mjs'
|
|
12
12
|
import { FullXapi } from './_vmRunners/FullXapi.mjs'
|
|
13
13
|
|
|
14
|
+
const noop = Function.prototype
|
|
15
|
+
|
|
14
16
|
const DEFAULT_XAPI_VM_SETTINGS = {
|
|
15
17
|
bypassVdiChainsCheck: false,
|
|
16
18
|
checkpointSnapshot: false,
|
|
@@ -24,6 +26,7 @@ const DEFAULT_XAPI_VM_SETTINGS = {
|
|
|
24
26
|
healthCheckVmsWithTags: [],
|
|
25
27
|
maxExportRate: 0,
|
|
26
28
|
maxMergedDeltasPerRun: Infinity,
|
|
29
|
+
nRetriesVmBackupFailures: 0,
|
|
27
30
|
offlineBackup: false,
|
|
28
31
|
offlineSnapshot: false,
|
|
29
32
|
snapshotRetention: 0,
|
|
@@ -53,6 +56,7 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
|
|
|
53
56
|
const throttleStream = createStreamThrottle(settings.maxExportRate)
|
|
54
57
|
|
|
55
58
|
const config = this._config
|
|
59
|
+
|
|
56
60
|
await Disposable.use(
|
|
57
61
|
Disposable.all(
|
|
58
62
|
extractIdsFromSimplePattern(job.srs).map(id =>
|
|
@@ -89,48 +93,98 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
|
|
|
89
93
|
const allSettings = this._job.settings
|
|
90
94
|
const baseSettings = this._baseSettings
|
|
91
95
|
|
|
96
|
+
const queue = new Set(vmIds)
|
|
97
|
+
const taskByVmId = {}
|
|
98
|
+
const nTriesByVmId = {}
|
|
99
|
+
|
|
92
100
|
const handleVm = vmUuid => {
|
|
101
|
+
const getVmTask = () => {
|
|
102
|
+
if (taskByVmId[vmUuid] === undefined) {
|
|
103
|
+
taskByVmId[vmUuid] = new Task(taskStart)
|
|
104
|
+
}
|
|
105
|
+
return taskByVmId[vmUuid]
|
|
106
|
+
}
|
|
107
|
+
const vmBackupFailed = error => {
|
|
108
|
+
if (isLastRun) {
|
|
109
|
+
throw error
|
|
110
|
+
} else {
|
|
111
|
+
Task.warning(`Retry the VM backup due to an error`, {
|
|
112
|
+
attempt: nTriesByVmId[vmUuid],
|
|
113
|
+
error: error.message,
|
|
114
|
+
})
|
|
115
|
+
queue.add(vmUuid)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (nTriesByVmId[vmUuid] === undefined) {
|
|
120
|
+
nTriesByVmId[vmUuid] = 0
|
|
121
|
+
}
|
|
122
|
+
nTriesByVmId[vmUuid]++
|
|
123
|
+
|
|
124
|
+
const vmSettings = { ...settings, ...allSettings[vmUuid] }
|
|
93
125
|
const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
|
|
126
|
+
const isLastRun = nTriesByVmId[vmUuid] === vmSettings.nRetriesVmBackupFailures + 1
|
|
94
127
|
|
|
95
128
|
return this._getRecord('VM', vmUuid).then(
|
|
96
129
|
disposableVm =>
|
|
97
|
-
Disposable.use(disposableVm, vm => {
|
|
98
|
-
taskStart.data.name_label
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
Disposable.use(disposableVm, async vm => {
|
|
131
|
+
if (taskStart.data.name_label === undefined) {
|
|
132
|
+
taskStart.data.name_label = vm.name_label
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const task = getVmTask()
|
|
136
|
+
return task
|
|
137
|
+
.run(async () => {
|
|
138
|
+
const opts = {
|
|
139
|
+
baseSettings,
|
|
140
|
+
config,
|
|
141
|
+
getSnapshotNameLabel,
|
|
142
|
+
healthCheckSr,
|
|
143
|
+
job,
|
|
144
|
+
remoteAdapters,
|
|
145
|
+
schedule,
|
|
146
|
+
settings: vmSettings,
|
|
147
|
+
srs,
|
|
148
|
+
throttleStream,
|
|
149
|
+
vm,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let vmBackup
|
|
153
|
+
if (job.mode === 'delta') {
|
|
154
|
+
vmBackup = new IncrementalXapi(opts)
|
|
119
155
|
} else {
|
|
120
|
-
|
|
156
|
+
if (job.mode === 'full') {
|
|
157
|
+
vmBackup = new FullXapi(opts)
|
|
158
|
+
} else {
|
|
159
|
+
throw new Error(`Job mode ${job.mode} not implemented`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const result = await vmBackup.run()
|
|
165
|
+
task.success(result)
|
|
166
|
+
return result
|
|
167
|
+
} catch (error) {
|
|
168
|
+
vmBackupFailed(error)
|
|
121
169
|
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
})
|
|
170
|
+
})
|
|
171
|
+
.catch(noop) // errors are handled by logs
|
|
125
172
|
}),
|
|
126
173
|
error =>
|
|
127
|
-
|
|
128
|
-
|
|
174
|
+
getVmTask().run(() => {
|
|
175
|
+
vmBackupFailed(error)
|
|
129
176
|
})
|
|
130
177
|
)
|
|
131
178
|
}
|
|
132
179
|
const { concurrency } = settings
|
|
133
|
-
|
|
180
|
+
const _handleVm = concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm)
|
|
181
|
+
|
|
182
|
+
while (queue.size > 0) {
|
|
183
|
+
const vmIds = Array.from(queue)
|
|
184
|
+
queue.clear()
|
|
185
|
+
|
|
186
|
+
await asyncMapSettled(vmIds, _handleVm)
|
|
187
|
+
}
|
|
134
188
|
}
|
|
135
189
|
)
|
|
136
190
|
}
|
|
@@ -2,6 +2,7 @@ import { asyncEach } from '@vates/async-each'
|
|
|
2
2
|
import { decorateMethodsWith } from '@vates/decorate-with'
|
|
3
3
|
import { defer } from 'golike-defer'
|
|
4
4
|
import assert from 'node:assert'
|
|
5
|
+
import * as UUID from 'uuid'
|
|
5
6
|
import isVhdDifferencingDisk from 'vhd-lib/isVhdDifferencingDisk.js'
|
|
6
7
|
import mapValues from 'lodash/mapValues.js'
|
|
7
8
|
|
|
@@ -9,11 +10,48 @@ import { AbstractRemote } from './_AbstractRemote.mjs'
|
|
|
9
10
|
import { forkDeltaExport } from './_forkDeltaExport.mjs'
|
|
10
11
|
import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
|
|
11
12
|
import { Task } from '../../Task.mjs'
|
|
13
|
+
import { Disposable } from 'promise-toolbox'
|
|
14
|
+
import { openVhd } from 'vhd-lib'
|
|
15
|
+
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
|
|
12
16
|
|
|
13
17
|
class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
14
18
|
_getRemoteWriter() {
|
|
15
19
|
return IncrementalRemoteWriter
|
|
16
20
|
}
|
|
21
|
+
async _selectBaseVm(metadata) {
|
|
22
|
+
// for each disk , get the parent
|
|
23
|
+
const baseUuidToSrcVdi = new Map()
|
|
24
|
+
|
|
25
|
+
// no previous backup for a base( =key) backup
|
|
26
|
+
if (metadata.isBase) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
await asyncEach(Object.entries(metadata.vdis), async ([id, vdi]) => {
|
|
30
|
+
const isDifferencing = metadata.isVhdDifferencing[`${id}.vhd`]
|
|
31
|
+
if (isDifferencing) {
|
|
32
|
+
const vmDir = getVmBackupDir(metadata.vm.uuid)
|
|
33
|
+
const path = `${vmDir}/${metadata.vhds[id]}`
|
|
34
|
+
// don't catch error : we can't recover if the source vhd are missing
|
|
35
|
+
await Disposable.use(openVhd(this._sourceRemoteAdapter._handler, path), vhd => {
|
|
36
|
+
baseUuidToSrcVdi.set(UUID.stringify(vhd.header.parentUuid), vdi.$snapshot_of$uuid)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const presentBaseVdis = new Map(baseUuidToSrcVdi)
|
|
42
|
+
await this._callWriters(
|
|
43
|
+
writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis),
|
|
44
|
+
'writer.checkBaseVdis()',
|
|
45
|
+
false
|
|
46
|
+
)
|
|
47
|
+
// check if the parent vdi are present in all the remotes
|
|
48
|
+
baseUuidToSrcVdi.forEach((srcVdiUuid, baseUuid) => {
|
|
49
|
+
if (!presentBaseVdis.has(baseUuid)) {
|
|
50
|
+
throw new Error(`Missing vdi ${baseUuid} which is a base for a delta`)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
// yeah , let's go
|
|
54
|
+
}
|
|
17
55
|
async _run($defer) {
|
|
18
56
|
const transferList = await this._computeTransferList(({ mode }) => mode === 'delta')
|
|
19
57
|
await this._callWriters(async writer => {
|
|
@@ -26,7 +64,7 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
|
26
64
|
if (transferList.length > 0) {
|
|
27
65
|
for (const metadata of transferList) {
|
|
28
66
|
assert.strictEqual(metadata.mode, 'delta')
|
|
29
|
-
|
|
67
|
+
await this._selectBaseVm(metadata)
|
|
30
68
|
await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
|
|
31
69
|
const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, {
|
|
32
70
|
useChain: false,
|
|
@@ -50,6 +88,17 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
|
50
88
|
}),
|
|
51
89
|
'writer.transfer()'
|
|
52
90
|
)
|
|
91
|
+
// this will update parent name with the needed alias
|
|
92
|
+
await this._callWriters(
|
|
93
|
+
writer =>
|
|
94
|
+
writer.updateUuidAndChain({
|
|
95
|
+
isVhdDifferencing,
|
|
96
|
+
timestamp: metadata.timestamp,
|
|
97
|
+
vdis: incrementalExport.vdis,
|
|
98
|
+
}),
|
|
99
|
+
'writer.updateUuidAndChain()'
|
|
100
|
+
)
|
|
101
|
+
|
|
53
102
|
await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
|
|
54
103
|
// for healthcheck
|
|
55
104
|
this._tags = metadata.vm.tags
|
|
@@ -78,6 +78,18 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
|
|
78
78
|
'writer.transfer()'
|
|
79
79
|
)
|
|
80
80
|
|
|
81
|
+
// we want to control the uuid of the vhd in the chain
|
|
82
|
+
// and ensure they are correctly chained
|
|
83
|
+
await this._callWriters(
|
|
84
|
+
writer =>
|
|
85
|
+
writer.updateUuidAndChain({
|
|
86
|
+
isVhdDifferencing,
|
|
87
|
+
timestamp,
|
|
88
|
+
vdis: deltaExport.vdis,
|
|
89
|
+
}),
|
|
90
|
+
'writer.updateUuidAndChain()'
|
|
91
|
+
)
|
|
92
|
+
|
|
81
93
|
this._baseVm = exportedVm
|
|
82
94
|
|
|
83
95
|
if (baseVm !== undefined) {
|
|
@@ -133,7 +145,7 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
|
|
133
145
|
])
|
|
134
146
|
const srcVdi = srcVdis[snapshotOf]
|
|
135
147
|
if (srcVdi !== undefined) {
|
|
136
|
-
baseUuidToSrcVdi.set(baseUuid, srcVdi)
|
|
148
|
+
baseUuidToSrcVdi.set(baseUuid, srcVdi.uuid)
|
|
137
149
|
} else {
|
|
138
150
|
debug('ignore snapshot VDI because no longer present on VM', {
|
|
139
151
|
vdi: baseUuid,
|
|
@@ -154,18 +166,18 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
const fullVdisRequired = new Set()
|
|
157
|
-
baseUuidToSrcVdi.forEach((
|
|
169
|
+
baseUuidToSrcVdi.forEach((srcVdiUuid, baseUuid) => {
|
|
158
170
|
if (presentBaseVdis.has(baseUuid)) {
|
|
159
171
|
debug('found base VDI', {
|
|
160
172
|
base: baseUuid,
|
|
161
|
-
vdi:
|
|
173
|
+
vdi: srcVdiUuid,
|
|
162
174
|
})
|
|
163
175
|
} else {
|
|
164
176
|
debug('missing base VDI', {
|
|
165
177
|
base: baseUuid,
|
|
166
|
-
vdi:
|
|
178
|
+
vdi: srcVdiUuid,
|
|
167
179
|
})
|
|
168
|
-
fullVdisRequired.add(
|
|
180
|
+
fullVdisRequired.add(srcVdiUuid)
|
|
169
181
|
}
|
|
170
182
|
})
|
|
171
183
|
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
import mapValues from 'lodash/mapValues.js'
|
|
3
|
-
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
|
4
3
|
import { asyncEach } from '@vates/async-each'
|
|
5
4
|
import { asyncMap } from '@xen-orchestra/async-map'
|
|
6
|
-
import { chainVhd,
|
|
5
|
+
import { chainVhd, openVhd } from 'vhd-lib'
|
|
7
6
|
import { createLogger } from '@xen-orchestra/log'
|
|
8
7
|
import { decorateClass } from '@vates/decorate-with'
|
|
9
8
|
import { defer } from 'golike-defer'
|
|
10
|
-
import { dirname } from 'node:path'
|
|
9
|
+
import { dirname, basename } from 'node:path'
|
|
11
10
|
|
|
12
11
|
import { formatFilenameDate } from '../../_filenameDate.mjs'
|
|
13
12
|
import { getOldEntries } from '../../_getOldEntries.mjs'
|
|
14
|
-
import { TAG_BASE_DELTA } from '../../_incrementalVm.mjs'
|
|
15
13
|
import { Task } from '../../Task.mjs'
|
|
16
14
|
|
|
17
15
|
import { MixinRemoteWriter } from './_MixinRemoteWriter.mjs'
|
|
@@ -23,42 +21,45 @@ import { Disposable } from 'promise-toolbox'
|
|
|
23
21
|
const { warn } = createLogger('xo:backups:DeltaBackupWriter')
|
|
24
22
|
|
|
25
23
|
export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWriter) {
|
|
24
|
+
#parentVdiPaths
|
|
25
|
+
#vhds
|
|
26
26
|
async checkBaseVdis(baseUuidToSrcVdi) {
|
|
27
|
+
this.#parentVdiPaths = {}
|
|
27
28
|
const { handler } = this._adapter
|
|
28
29
|
const adapter = this._adapter
|
|
29
30
|
|
|
30
31
|
const vdisDir = `${this._vmBackupDir}/vdis/${this._job.id}`
|
|
31
32
|
|
|
32
|
-
await asyncMap(baseUuidToSrcVdi, async ([baseUuid,
|
|
33
|
-
let
|
|
33
|
+
await asyncMap(baseUuidToSrcVdi, async ([baseUuid, srcVdiUuid]) => {
|
|
34
|
+
let parentDestPath
|
|
35
|
+
const vhdDir = `${vdisDir}/${srcVdiUuid}`
|
|
34
36
|
try {
|
|
35
|
-
const vhds = await handler.list(
|
|
37
|
+
const vhds = await handler.list(vhdDir, {
|
|
36
38
|
filter: _ => _[0] !== '.' && _.endsWith('.vhd'),
|
|
37
39
|
ignoreMissing: true,
|
|
38
40
|
prependDir: true,
|
|
39
41
|
})
|
|
40
42
|
const packedBaseUuid = packUuid(baseUuid)
|
|
41
|
-
|
|
43
|
+
// the last one is probably the right one
|
|
44
|
+
|
|
45
|
+
for (let i = vhds.length - 1; i >= 0 && parentDestPath === undefined; i--) {
|
|
46
|
+
const path = vhds[i]
|
|
42
47
|
try {
|
|
43
|
-
await
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// since all the checks of a path are done in parallel, found would be containing
|
|
47
|
-
// only the last answer of isMergeableParent which is probably not the right one
|
|
48
|
-
// this led to the support tickets https://help.vates.fr/#ticket/zoom/4751 , 4729, 4665 and 4300
|
|
49
|
-
|
|
50
|
-
const isMergeable = await adapter.isMergeableParent(packedBaseUuid, path)
|
|
51
|
-
found = found || isMergeable
|
|
48
|
+
if (await adapter.isMergeableParent(packedBaseUuid, path)) {
|
|
49
|
+
parentDestPath = path
|
|
50
|
+
}
|
|
52
51
|
} catch (error) {
|
|
53
52
|
warn('checkBaseVdis', { error })
|
|
54
|
-
await ignoreErrors.call(VhdAbstract.unlink(handler, path))
|
|
55
53
|
}
|
|
56
|
-
}
|
|
54
|
+
}
|
|
57
55
|
} catch (error) {
|
|
58
56
|
warn('checkBaseVdis', { error })
|
|
59
57
|
}
|
|
60
|
-
|
|
58
|
+
// no usable parent => the runner will have to decide to fall back to a full or stop backup
|
|
59
|
+
if (parentDestPath === undefined) {
|
|
61
60
|
baseUuidToSrcVdi.delete(baseUuid)
|
|
61
|
+
} else {
|
|
62
|
+
this.#parentVdiPaths[vhdDir] = parentDestPath
|
|
62
63
|
}
|
|
63
64
|
})
|
|
64
65
|
}
|
|
@@ -123,6 +124,44 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
async updateUuidAndChain({ isVhdDifferencing, vdis }) {
|
|
128
|
+
assert.notStrictEqual(
|
|
129
|
+
this.#vhds,
|
|
130
|
+
undefined,
|
|
131
|
+
'_transfer must be called before updateUuidAndChain for incremental backups'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const parentVdiPaths = this.#parentVdiPaths
|
|
135
|
+
const { handler } = this._adapter
|
|
136
|
+
const vhds = this.#vhds
|
|
137
|
+
await asyncEach(Object.entries(vdis), async ([id, vdi]) => {
|
|
138
|
+
const isDifferencing = isVhdDifferencing[`${id}.vhd`]
|
|
139
|
+
const path = `${this._vmBackupDir}/${vhds[id]}`
|
|
140
|
+
if (isDifferencing) {
|
|
141
|
+
assert.notStrictEqual(
|
|
142
|
+
parentVdiPaths,
|
|
143
|
+
'checkbasevdi must be called before updateUuidAndChain for incremental backups'
|
|
144
|
+
)
|
|
145
|
+
const parentPath = parentVdiPaths[dirname(path)]
|
|
146
|
+
// we are in a incremental backup
|
|
147
|
+
// we already computed the chain in checkBaseVdis
|
|
148
|
+
assert.notStrictEqual(parentPath, undefined, 'A differential VHD must have a parent')
|
|
149
|
+
// forbid any kind of loop
|
|
150
|
+
assert.ok(basename(parentPath) < basename(path), `vhd must be sorted to be chained`)
|
|
151
|
+
await chainVhd(handler, parentPath, handler, path)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// set the correct UUID in the VHD if needed
|
|
155
|
+
await Disposable.use(openVhd(handler, path), async vhd => {
|
|
156
|
+
if (!vhd.footer.uuid.equals(packUuid(vdi.uuid))) {
|
|
157
|
+
vhd.footer.uuid = packUuid(vdi.uuid)
|
|
158
|
+
await vhd.readBlockAllocationTable() // required by writeFooter()
|
|
159
|
+
await vhd.writeFooter()
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
126
165
|
async _deleteOldEntries() {
|
|
127
166
|
const adapter = this._adapter
|
|
128
167
|
const oldEntries = this._oldEntries
|
|
@@ -141,14 +180,10 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
141
180
|
const jobId = job.id
|
|
142
181
|
const handler = adapter.handler
|
|
143
182
|
|
|
144
|
-
let metadataContent = await this._isAlreadyTransferred(timestamp)
|
|
145
|
-
if (metadataContent !== undefined) {
|
|
146
|
-
// @todo : should skip backup while being vigilant to not stuck the forked stream
|
|
147
|
-
Task.info('This backup has already been transfered')
|
|
148
|
-
}
|
|
149
|
-
|
|
150
183
|
const basename = formatFilenameDate(timestamp)
|
|
151
|
-
|
|
184
|
+
// update this.#vhds before eventually skipping transfer, so that
|
|
185
|
+
// updateUuidAndChain has all the mandatory data
|
|
186
|
+
const vhds = (this.#vhds = mapValues(
|
|
152
187
|
deltaExport.vdis,
|
|
153
188
|
vdi =>
|
|
154
189
|
`vdis/${jobId}/${
|
|
@@ -158,7 +193,15 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
158
193
|
vdi.uuid
|
|
159
194
|
: vdi.$snapshot_of$uuid
|
|
160
195
|
}/${adapter.getVhdFileName(basename)}`
|
|
161
|
-
)
|
|
196
|
+
))
|
|
197
|
+
|
|
198
|
+
let metadataContent = await this._isAlreadyTransferred(timestamp)
|
|
199
|
+
if (metadataContent !== undefined) {
|
|
200
|
+
// skip backup while being vigilant to not stuck the forked stream
|
|
201
|
+
Task.info('This backup has already been transfered')
|
|
202
|
+
Object.values(deltaExport.streams).forEach(stream => stream.destroy())
|
|
203
|
+
return { size: 0 }
|
|
204
|
+
}
|
|
162
205
|
|
|
163
206
|
metadataContent = {
|
|
164
207
|
isVhdDifferencing,
|
|
@@ -174,38 +217,13 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
174
217
|
vm,
|
|
175
218
|
vmSnapshot,
|
|
176
219
|
}
|
|
220
|
+
|
|
177
221
|
const { size } = await Task.run({ name: 'transfer' }, async () => {
|
|
178
222
|
let transferSize = 0
|
|
179
223
|
await asyncEach(
|
|
180
|
-
Object.
|
|
181
|
-
async
|
|
224
|
+
Object.keys(deltaExport.vdis),
|
|
225
|
+
async id => {
|
|
182
226
|
const path = `${this._vmBackupDir}/${vhds[id]}`
|
|
183
|
-
|
|
184
|
-
const isDifferencing = isVhdDifferencing[`${id}.vhd`]
|
|
185
|
-
let parentPath
|
|
186
|
-
if (isDifferencing) {
|
|
187
|
-
const vdiDir = dirname(path)
|
|
188
|
-
parentPath = (
|
|
189
|
-
await handler.list(vdiDir, {
|
|
190
|
-
filter: filename => filename[0] !== '.' && filename.endsWith('.vhd'),
|
|
191
|
-
prependDir: true,
|
|
192
|
-
})
|
|
193
|
-
)
|
|
194
|
-
.sort()
|
|
195
|
-
.pop()
|
|
196
|
-
|
|
197
|
-
assert.notStrictEqual(
|
|
198
|
-
parentPath,
|
|
199
|
-
undefined,
|
|
200
|
-
`missing parent of ${id} in ${dirname(path)}, looking for ${vdi.other_config[TAG_BASE_DELTA]}`
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
parentPath = parentPath.slice(1) // remove leading slash
|
|
204
|
-
|
|
205
|
-
// TODO remove when this has been done before the export
|
|
206
|
-
await checkVhd(handler, parentPath)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
227
|
// don't write it as transferSize += await async function
|
|
210
228
|
// since i += await asyncFun lead to race condition
|
|
211
229
|
// as explained : https://eslint.org/docs/latest/rules/require-atomic-updates
|
|
@@ -217,17 +235,6 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
217
235
|
writeBlockConcurrency: this._config.writeBlockConcurrency,
|
|
218
236
|
})
|
|
219
237
|
transferSize += transferSizeOneDisk
|
|
220
|
-
|
|
221
|
-
if (isDifferencing) {
|
|
222
|
-
await chainVhd(handler, parentPath, handler, path)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// set the correct UUID in the VHD
|
|
226
|
-
await Disposable.use(openVhd(handler, path), async vhd => {
|
|
227
|
-
vhd.footer.uuid = packUuid(vdi.uuid)
|
|
228
|
-
await vhd.readBlockAllocationTable() // required by writeFooter()
|
|
229
|
-
await vhd.writeFooter()
|
|
230
|
-
})
|
|
231
238
|
},
|
|
232
239
|
{
|
|
233
240
|
concurrency: settings.diskPerVmConcurrency,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
1
2
|
import { asyncMap, asyncMapSettled } from '@xen-orchestra/async-map'
|
|
2
3
|
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
|
3
4
|
import { formatDateTime } from '@xen-orchestra/xapi'
|
|
@@ -14,6 +15,7 @@ import find from 'lodash/find.js'
|
|
|
14
15
|
|
|
15
16
|
export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
|
|
16
17
|
async checkBaseVdis(baseUuidToSrcVdi, baseVm) {
|
|
18
|
+
assert.notStrictEqual(baseVm, undefined)
|
|
17
19
|
const sr = this._sr
|
|
18
20
|
const replicatedVm = listReplicatedVms(sr.$xapi, this._job.id, sr.uuid, this._vmUuid).find(
|
|
19
21
|
vm => vm.other_config[TAG_COPY_SRC] === baseVm.uuid
|
|
@@ -36,7 +38,9 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
|
-
|
|
41
|
+
updateUuidAndChain() {
|
|
42
|
+
// nothing to do, the chaining is not modified in this case
|
|
43
|
+
}
|
|
40
44
|
prepare({ isFull }) {
|
|
41
45
|
// create the task related to this export and ensure all methods are called in this context
|
|
42
46
|
const task = new Task({
|
|
@@ -77,11 +77,11 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
|
|
|
77
77
|
healthCheck() {
|
|
78
78
|
const sr = this._healthCheckSr
|
|
79
79
|
assert.notStrictEqual(sr, undefined, 'SR should be defined before making a health check')
|
|
80
|
-
|
|
81
|
-
this
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
if (this._metadataFileName === undefined) {
|
|
81
|
+
// this can happen when making a mirror backup with nothing to transfer
|
|
82
|
+
Task.info('no health check, since no backup have been transferred')
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
85
|
return Task.run(
|
|
86
86
|
{
|
|
87
87
|
name: 'health check',
|
|
@@ -113,13 +113,13 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
|
|
|
113
113
|
)
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
_isAlreadyTransferred(timestamp) {
|
|
116
|
+
async _isAlreadyTransferred(timestamp) {
|
|
117
117
|
const vmUuid = this._vmUuid
|
|
118
118
|
const adapter = this._adapter
|
|
119
119
|
const backupDir = getVmBackupDir(vmUuid)
|
|
120
120
|
try {
|
|
121
121
|
const actualMetadata = JSON.parse(
|
|
122
|
-
adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
|
|
122
|
+
await adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
|
|
123
123
|
)
|
|
124
124
|
return actualMetadata
|
|
125
125
|
} catch (error) {}
|
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.
|
|
11
|
+
"version": "0.45.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@vates/nbd-client": "^3.0.0",
|
|
29
29
|
"@vates/parse-duration": "^0.1.1",
|
|
30
30
|
"@xen-orchestra/async-map": "^0.1.2",
|
|
31
|
-
"@xen-orchestra/fs": "^4.1.
|
|
31
|
+
"@xen-orchestra/fs": "^4.1.5",
|
|
32
32
|
"@xen-orchestra/log": "^0.6.0",
|
|
33
33
|
"@xen-orchestra/template": "^0.1.0",
|
|
34
34
|
"app-conf": "^2.3.0",
|
|
@@ -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.9.
|
|
47
|
+
"vhd-lib": "^4.9.1",
|
|
48
48
|
"xen-api": "^2.0.1",
|
|
49
49
|
"yazl": "^2.5.1"
|
|
50
50
|
},
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"tmp": "^0.2.1"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@xen-orchestra/xapi": "^4.
|
|
59
|
+
"@xen-orchestra/xapi": "^4.3.0"
|
|
60
60
|
},
|
|
61
61
|
"license": "AGPL-3.0-or-later",
|
|
62
62
|
"author": {
|