@xen-orchestra/backups 0.72.1 → 0.73.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/ImportVmBackup.mjs +1 -1
- package/RemoteAdapter.mjs +8 -6
- package/_otherConfig.mjs +2 -0
- package/_runners/_vmRunners/IncrementalXapi.mjs +115 -41
- package/_runners/_vmRunners/_AbstractXapi.mjs +38 -34
- package/_runners/_writers/IncrementalXapiWriter.mjs +92 -27
- package/_runners/_writers/_MixinRemoteWriter.mjs +0 -2
- package/package.json +8 -6
- package/tests.fixtures.d.mts +47 -0
- package/_cleanVm.mjs +0 -623
- package/disks/MergeRemoteDisk.mjs +0 -324
- package/disks/RemoteDisk.mjs +0 -223
- package/disks/RemoteVhdDisk.mjs +0 -480
- package/disks/RemoteVhdDiskChain.mjs +0 -304
- package/disks/index.mjs +0 -49
- package/disks/openDiskChain.mjs +0 -40
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export namespace VHDFOOTER {
|
|
2
|
+
let cookie: string
|
|
3
|
+
let features: number
|
|
4
|
+
let fileFormatVersion: number
|
|
5
|
+
let dataOffset: number
|
|
6
|
+
let timestamp: number
|
|
7
|
+
let creatorApplication: string
|
|
8
|
+
let creatorVersion: number
|
|
9
|
+
let creatorHostOs: number
|
|
10
|
+
let originalSize: number
|
|
11
|
+
let currentSize: number
|
|
12
|
+
namespace diskGeometry {
|
|
13
|
+
let cylinders: number
|
|
14
|
+
let heads: number
|
|
15
|
+
let sectorsPerTrackCylinder: number
|
|
16
|
+
}
|
|
17
|
+
let diskType: number
|
|
18
|
+
let checksum: number
|
|
19
|
+
let uuid: Buffer<ArrayBuffer>
|
|
20
|
+
let saved: string
|
|
21
|
+
let hidden: string
|
|
22
|
+
let reserved: string
|
|
23
|
+
}
|
|
24
|
+
export namespace VHDHEADER {
|
|
25
|
+
let cookie_1: string
|
|
26
|
+
export { cookie_1 as cookie }
|
|
27
|
+
let dataOffset_1: any
|
|
28
|
+
export { dataOffset_1 as dataOffset }
|
|
29
|
+
export let tableOffset: number
|
|
30
|
+
export let headerVersion: number
|
|
31
|
+
export let maxTableEntries: number
|
|
32
|
+
export let blockSize: number
|
|
33
|
+
let checksum_1: number
|
|
34
|
+
export { checksum_1 as checksum }
|
|
35
|
+
export let parentUuid: any
|
|
36
|
+
export let parentTimestamp: number
|
|
37
|
+
export let reserved1: number
|
|
38
|
+
export let parentUnicodeName: string
|
|
39
|
+
export let parentLocatorEntry: {
|
|
40
|
+
platformCode: number
|
|
41
|
+
platformDataSpace: number
|
|
42
|
+
platformDataLength: number
|
|
43
|
+
reserved: number
|
|
44
|
+
platformDataOffset: number
|
|
45
|
+
}[]
|
|
46
|
+
export let reserved2: string
|
|
47
|
+
}
|
package/_cleanVm.mjs
DELETED
|
@@ -1,623 +0,0 @@
|
|
|
1
|
-
import * as UUID from 'uuid'
|
|
2
|
-
import { asyncMap } from '@xen-orchestra/async-map'
|
|
3
|
-
import { Constants, openVhd, VhdAbstract } from 'vhd-lib'
|
|
4
|
-
import { isVhdAlias, resolveVhdAlias } from 'vhd-lib/aliases.js'
|
|
5
|
-
import { basename, dirname, resolve } from 'node:path'
|
|
6
|
-
import { isMetadataFile, isVhdFile, isVhdSumFile, isXvaFile, isXvaSumFile } from './_backupType.mjs'
|
|
7
|
-
import { limitConcurrency } from 'limit-concurrency-decorator'
|
|
8
|
-
import { RemoteVhdDisk } from './disks/RemoteVhdDisk.mjs'
|
|
9
|
-
import { RemoteVhdDiskChain } from './disks/RemoteVhdDiskChain.mjs'
|
|
10
|
-
import { MergeRemoteDisk } from './disks/MergeRemoteDisk.mjs'
|
|
11
|
-
|
|
12
|
-
import { Disposable } from 'promise-toolbox'
|
|
13
|
-
import { Task } from '@vates/task'
|
|
14
|
-
import handlerPath from '@xen-orchestra/fs/path'
|
|
15
|
-
|
|
16
|
-
const { DISK_TYPES } = Constants
|
|
17
|
-
|
|
18
|
-
// chain is [ ancestor, child_1, ..., child_n ]
|
|
19
|
-
async function _mergeVhdChain(handler, chain, { logInfo, remove, mergeBlockConcurrency }) {
|
|
20
|
-
logInfo(`merging VHD chain`, { chain })
|
|
21
|
-
|
|
22
|
-
let done, total
|
|
23
|
-
const handle = setInterval(() => {
|
|
24
|
-
if (done !== undefined) {
|
|
25
|
-
logInfo('merge in progress', {
|
|
26
|
-
done,
|
|
27
|
-
parent: chain[0],
|
|
28
|
-
progress: Math.round((100 * done) / total),
|
|
29
|
-
total,
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
}, 10e3)
|
|
33
|
-
try {
|
|
34
|
-
const parentDisk = new RemoteVhdDisk({ handler, path: chain.shift() })
|
|
35
|
-
|
|
36
|
-
const childDisks = []
|
|
37
|
-
for (const path of chain) {
|
|
38
|
-
childDisks.push(new RemoteVhdDisk({ handler, path }))
|
|
39
|
-
}
|
|
40
|
-
const childDiskChain = new RemoteVhdDiskChain({ disks: childDisks })
|
|
41
|
-
|
|
42
|
-
const mergeRemoteDisk = new MergeRemoteDisk(handler, {
|
|
43
|
-
logInfo,
|
|
44
|
-
mergeBlockConcurrency,
|
|
45
|
-
onProgress({ done: d, total: t }) {
|
|
46
|
-
done = d
|
|
47
|
-
total = t
|
|
48
|
-
},
|
|
49
|
-
removeUnused: remove,
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
const isResumingMerge = await mergeRemoteDisk.isResuming(parentDisk)
|
|
53
|
-
await parentDisk.init({ force: isResumingMerge })
|
|
54
|
-
await childDiskChain.init({ force: isResumingMerge })
|
|
55
|
-
|
|
56
|
-
const result = await mergeRemoteDisk.merge(parentDisk, childDiskChain)
|
|
57
|
-
|
|
58
|
-
return result
|
|
59
|
-
} finally {
|
|
60
|
-
clearInterval(handle)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const noop = Function.prototype
|
|
65
|
-
|
|
66
|
-
const INTERRUPTED_VHDS_REG = /^\.(.+)\.merge.json$/
|
|
67
|
-
|
|
68
|
-
const listVhds = async (handler, vmDir, logWarn) => {
|
|
69
|
-
const vhds = new Set()
|
|
70
|
-
const aliases = {}
|
|
71
|
-
const interruptedVhds = new Map()
|
|
72
|
-
const checksums = new Set()
|
|
73
|
-
|
|
74
|
-
await asyncMap(
|
|
75
|
-
await handler.list(`${vmDir}/vdis`, {
|
|
76
|
-
ignoreMissing: true,
|
|
77
|
-
prependDir: true,
|
|
78
|
-
}),
|
|
79
|
-
async jobDir =>
|
|
80
|
-
asyncMap(
|
|
81
|
-
await handler.list(jobDir, {
|
|
82
|
-
prependDir: true,
|
|
83
|
-
}),
|
|
84
|
-
async vdiDir => {
|
|
85
|
-
const list = await handler.list(vdiDir, {
|
|
86
|
-
filter: file => isVhdFile(file) || INTERRUPTED_VHDS_REG.test(file) || isVhdSumFile(file),
|
|
87
|
-
})
|
|
88
|
-
aliases[vdiDir] = list.filter(vhd => isVhdAlias(vhd)).map(file => `${vdiDir}/${file}`)
|
|
89
|
-
|
|
90
|
-
await asyncMap(list, async file => {
|
|
91
|
-
if (isVhdSumFile(file)) {
|
|
92
|
-
checksums.add(`${vdiDir}/${file}`)
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
const res = INTERRUPTED_VHDS_REG.exec(file)
|
|
96
|
-
if (res === null) {
|
|
97
|
-
vhds.add(`${vdiDir}/${file}`)
|
|
98
|
-
} else {
|
|
99
|
-
try {
|
|
100
|
-
const mergeState = JSON.parse(await handler.readFile(`${vdiDir}/${file}`))
|
|
101
|
-
interruptedVhds.set(`${vdiDir}/${res[1]}`, {
|
|
102
|
-
statePath: `${vdiDir}/${file}`,
|
|
103
|
-
chain: mergeState.chain,
|
|
104
|
-
})
|
|
105
|
-
} catch (error) {
|
|
106
|
-
// fall back to a non resuming merge
|
|
107
|
-
vhds.add(`${vdiDir}/${file}`)
|
|
108
|
-
logWarn('failed to read existing merge state', { path: file, error })
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
)
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
return { vhds, interruptedVhds, aliases, checksums }
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export async function checkAliases(
|
|
120
|
-
aliasPaths,
|
|
121
|
-
targetDataRepository,
|
|
122
|
-
{ handler, logInfo = noop, logWarn = console.warn, remove = false }
|
|
123
|
-
) {
|
|
124
|
-
const aliasFound = []
|
|
125
|
-
for (const alias of aliasPaths) {
|
|
126
|
-
let target
|
|
127
|
-
try {
|
|
128
|
-
target = await resolveVhdAlias(handler, alias)
|
|
129
|
-
} catch (err) {
|
|
130
|
-
if (err.code === 'ENOENT') {
|
|
131
|
-
logWarn('missing target of alias', { alias })
|
|
132
|
-
if (remove) {
|
|
133
|
-
logInfo('removing alias and non VHD target', { alias, target })
|
|
134
|
-
await handler.unlink(alias)
|
|
135
|
-
}
|
|
136
|
-
continue
|
|
137
|
-
}
|
|
138
|
-
if (err.code === 'EISDIR') {
|
|
139
|
-
logWarn('alias is a vhd directory', { alias })
|
|
140
|
-
if (remove) {
|
|
141
|
-
logInfo('removing vhd directory named as alias', { alias, target })
|
|
142
|
-
await VhdAbstract.unlink(handler, alias)
|
|
143
|
-
}
|
|
144
|
-
continue
|
|
145
|
-
}
|
|
146
|
-
logWarn('unhandled error while checking alias', { alias, err })
|
|
147
|
-
continue
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (target === '') {
|
|
151
|
-
logWarn('empty target for alias ', { alias })
|
|
152
|
-
if (remove) {
|
|
153
|
-
logInfo('removing alias and non VHD target', { alias, target })
|
|
154
|
-
await handler.unlink(alias)
|
|
155
|
-
}
|
|
156
|
-
continue
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (!isVhdFile(target)) {
|
|
160
|
-
logWarn('alias references non VHD target', { alias, target })
|
|
161
|
-
if (remove) {
|
|
162
|
-
logInfo('removing alias and non VHD target', { alias, target })
|
|
163
|
-
await handler.unlink(target)
|
|
164
|
-
await handler.unlink(alias)
|
|
165
|
-
}
|
|
166
|
-
continue
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
const { dispose } = await openVhd(handler, target)
|
|
171
|
-
try {
|
|
172
|
-
await dispose()
|
|
173
|
-
} catch (e) {
|
|
174
|
-
// error during dispose should not trigger a deletion
|
|
175
|
-
}
|
|
176
|
-
} catch (error) {
|
|
177
|
-
logWarn('missing or broken alias target', { alias, target, error })
|
|
178
|
-
if (remove) {
|
|
179
|
-
try {
|
|
180
|
-
await VhdAbstract.unlink(handler, alias)
|
|
181
|
-
} catch (error) {
|
|
182
|
-
if (error.code !== 'ENOENT') {
|
|
183
|
-
logWarn('error deleting alias target', { alias, target, error })
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
continue
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
aliasFound.push(resolve('/', target))
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const vhds = await handler.list(targetDataRepository, {
|
|
194
|
-
ignoreMissing: true,
|
|
195
|
-
prependDir: true,
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
await asyncMap(vhds, async path => {
|
|
199
|
-
if (!aliasFound.includes(path)) {
|
|
200
|
-
logWarn('no alias references VHD', { path })
|
|
201
|
-
if (remove) {
|
|
202
|
-
logInfo('deleting unused VHD', { path })
|
|
203
|
-
await VhdAbstract.unlink(handler, path)
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
})
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const defaultMergeLimiter = limitConcurrency(1)
|
|
210
|
-
|
|
211
|
-
export async function cleanVm(
|
|
212
|
-
vmDir,
|
|
213
|
-
{
|
|
214
|
-
fixMetadata,
|
|
215
|
-
remove = false,
|
|
216
|
-
removeTmp = remove,
|
|
217
|
-
merge,
|
|
218
|
-
mergeBlockConcurrency,
|
|
219
|
-
mergeLimiter = defaultMergeLimiter,
|
|
220
|
-
logInfo = noop,
|
|
221
|
-
logWarn = console.warn,
|
|
222
|
-
}
|
|
223
|
-
) {
|
|
224
|
-
const limitedMergeVhdChain = mergeLimiter(_mergeVhdChain)
|
|
225
|
-
|
|
226
|
-
const handler = this._handler
|
|
227
|
-
|
|
228
|
-
const vhdsToJSons = new Set()
|
|
229
|
-
const vhdById = new Map()
|
|
230
|
-
const vhdParents = { __proto__: null }
|
|
231
|
-
const vhdChildren = { __proto__: null }
|
|
232
|
-
|
|
233
|
-
const { vhds, interruptedVhds, aliases, checksums } = await listVhds(handler, vmDir, logWarn)
|
|
234
|
-
|
|
235
|
-
// from 5.110 to 5.113 we computed checksum for vhd file
|
|
236
|
-
// but never used nor removed them
|
|
237
|
-
await asyncMap(checksums, async path => {
|
|
238
|
-
if (remove) {
|
|
239
|
-
logInfo('deleting checksum file ', { path })
|
|
240
|
-
return handler.unlink(path)
|
|
241
|
-
}
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
// remove broken VHDs
|
|
245
|
-
await asyncMap(vhds, async path => {
|
|
246
|
-
if (removeTmp && basename(path)[0] === '.') {
|
|
247
|
-
logInfo('deleting temporary VHD', { path })
|
|
248
|
-
return VhdAbstract.unlink(handler, path)
|
|
249
|
-
}
|
|
250
|
-
try {
|
|
251
|
-
await Disposable.use(openVhd(handler, path, { checkSecondFooter: !interruptedVhds.has(path) }), vhd => {
|
|
252
|
-
if (vhd.footer.diskType === DISK_TYPES.DIFFERENCING) {
|
|
253
|
-
const parent = resolve('/', dirname(path), vhd.header.parentUnicodeName)
|
|
254
|
-
vhdParents[path] = parent
|
|
255
|
-
if (parent in vhdChildren) {
|
|
256
|
-
const error = new Error('this script does not support multiple VHD children')
|
|
257
|
-
error.parent = parent
|
|
258
|
-
error.child1 = vhdChildren[parent]
|
|
259
|
-
error.child2 = path
|
|
260
|
-
throw error // should we throw?
|
|
261
|
-
}
|
|
262
|
-
vhdChildren[parent] = path
|
|
263
|
-
}
|
|
264
|
-
// Detect VHDs with the same UUIDs
|
|
265
|
-
//
|
|
266
|
-
// Due to a bug introduced in a1bcd35e2
|
|
267
|
-
const duplicate = vhdById.get(UUID.stringify(vhd.footer.uuid))
|
|
268
|
-
let vhdKept = vhd
|
|
269
|
-
if (duplicate !== undefined) {
|
|
270
|
-
logWarn('uuid is duplicated', { uuid: UUID.stringify(vhd.footer.uuid) })
|
|
271
|
-
if (duplicate.containsAllDataOf(vhd)) {
|
|
272
|
-
logWarn(`should delete ${path}`)
|
|
273
|
-
vhdKept = duplicate
|
|
274
|
-
vhds.delete(path)
|
|
275
|
-
} else if (vhd.containsAllDataOf(duplicate)) {
|
|
276
|
-
logWarn(`should delete ${duplicate._path}`)
|
|
277
|
-
vhds.delete(duplicate._path)
|
|
278
|
-
} else {
|
|
279
|
-
logWarn('same ids but different content')
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
vhdById.set(UUID.stringify(vhdKept.footer.uuid), vhdKept)
|
|
283
|
-
})
|
|
284
|
-
} catch (error) {
|
|
285
|
-
vhds.delete(path)
|
|
286
|
-
logWarn('VHD check error', { path, error })
|
|
287
|
-
if (error?.code === 'ERR_ASSERTION' && remove) {
|
|
288
|
-
logInfo('deleting broken VHD', { path })
|
|
289
|
-
return VhdAbstract.unlink(handler, path)
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
// remove interrupted merge states for missing VHDs
|
|
295
|
-
for (const interruptedVhd of interruptedVhds.keys()) {
|
|
296
|
-
if (!vhds.has(interruptedVhd)) {
|
|
297
|
-
const { statePath } = interruptedVhds.get(interruptedVhd)
|
|
298
|
-
interruptedVhds.delete(interruptedVhd)
|
|
299
|
-
|
|
300
|
-
logWarn('orphan merge state', {
|
|
301
|
-
mergeStatePath: statePath,
|
|
302
|
-
missingVhdPath: interruptedVhd,
|
|
303
|
-
})
|
|
304
|
-
if (remove) {
|
|
305
|
-
logInfo('deleting orphan merge state', { statePath })
|
|
306
|
-
await handler.unlink(statePath)
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// check if alias are correct
|
|
312
|
-
// check if all vhd in data subfolder have a corresponding alias
|
|
313
|
-
await asyncMap(Object.keys(aliases), async dir => {
|
|
314
|
-
await checkAliases(aliases[dir], `${dir}/data`, { handler, logInfo, logWarn, remove })
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
// remove VHDs with missing ancestors
|
|
318
|
-
{
|
|
319
|
-
const deletions = []
|
|
320
|
-
|
|
321
|
-
// return true if the VHD has been deleted or is missing
|
|
322
|
-
const deleteIfOrphan = vhdPath => {
|
|
323
|
-
const parent = vhdParents[vhdPath]
|
|
324
|
-
if (parent === undefined) {
|
|
325
|
-
return
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// no longer needs to be checked
|
|
329
|
-
delete vhdParents[vhdPath]
|
|
330
|
-
|
|
331
|
-
deleteIfOrphan(parent)
|
|
332
|
-
|
|
333
|
-
if (!vhds.has(parent)) {
|
|
334
|
-
vhds.delete(vhdPath)
|
|
335
|
-
|
|
336
|
-
logWarn('parent VHD is missing', { parent, child: vhdPath })
|
|
337
|
-
if (remove) {
|
|
338
|
-
logInfo('deleting orphan VHD', { path: vhdPath })
|
|
339
|
-
deletions.push(VhdAbstract.unlink(handler, vhdPath))
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// > A property that is deleted before it has been visited will not be
|
|
345
|
-
// > visited later.
|
|
346
|
-
// >
|
|
347
|
-
// > -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#Deleted_added_or_modified_properties
|
|
348
|
-
for (const child in vhdParents) {
|
|
349
|
-
deleteIfOrphan(child)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
await Promise.all(deletions)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const jsons = new Set()
|
|
356
|
-
const xvas = new Set()
|
|
357
|
-
const xvaSums = []
|
|
358
|
-
const entries = await handler.list(vmDir, {
|
|
359
|
-
prependDir: true,
|
|
360
|
-
})
|
|
361
|
-
entries.forEach(path => {
|
|
362
|
-
if (isMetadataFile(path)) {
|
|
363
|
-
jsons.add(path)
|
|
364
|
-
} else if (isXvaFile(path)) {
|
|
365
|
-
xvas.add(path)
|
|
366
|
-
} else if (isXvaSumFile(path)) {
|
|
367
|
-
xvaSums.push(path)
|
|
368
|
-
}
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
const cachePath = vmDir + '/cache.json.gz'
|
|
372
|
-
|
|
373
|
-
let mustRegenerateCache
|
|
374
|
-
{
|
|
375
|
-
const cache = await this._readCache(cachePath)
|
|
376
|
-
const actual = cache === undefined ? 0 : Object.keys(cache).length
|
|
377
|
-
const expected = jsons.size
|
|
378
|
-
|
|
379
|
-
mustRegenerateCache = actual !== expected
|
|
380
|
-
if (mustRegenerateCache) {
|
|
381
|
-
logWarn('unexpected number of entries in backup cache', { path: cachePath, actual, expected })
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
await asyncMap(xvas, async path => {
|
|
386
|
-
// check is not good enough to delete the file, the best we can do is report
|
|
387
|
-
// it
|
|
388
|
-
if (!(await this.isValidXva(path))) {
|
|
389
|
-
logWarn('XVA might be broken', { path })
|
|
390
|
-
}
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
const unusedVhds = new Set(vhds)
|
|
394
|
-
const unusedXvas = new Set(xvas)
|
|
395
|
-
|
|
396
|
-
const backups = new Map()
|
|
397
|
-
|
|
398
|
-
// compile the list of unused XVAs and VHDs, and remove backup metadata which
|
|
399
|
-
// reference a missing XVA/VHD
|
|
400
|
-
await asyncMap(jsons, async json => {
|
|
401
|
-
let metadata
|
|
402
|
-
try {
|
|
403
|
-
metadata = JSON.parse(await handler.readFile(json))
|
|
404
|
-
} catch (error) {
|
|
405
|
-
logWarn('failed to read backup metadata', { path: json, error })
|
|
406
|
-
jsons.delete(json)
|
|
407
|
-
return
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
let isBackupComplete
|
|
411
|
-
|
|
412
|
-
const { mode } = metadata
|
|
413
|
-
if (mode === 'full') {
|
|
414
|
-
const linkedXva = resolve('/', vmDir, metadata.xva)
|
|
415
|
-
isBackupComplete = xvas.has(linkedXva)
|
|
416
|
-
if (isBackupComplete) {
|
|
417
|
-
unusedXvas.delete(linkedXva)
|
|
418
|
-
} else {
|
|
419
|
-
logWarn('the XVA linked to the backup is missing', { backup: json, xva: linkedXva })
|
|
420
|
-
}
|
|
421
|
-
} else if (mode === 'delta') {
|
|
422
|
-
const linkedVhds = (() => {
|
|
423
|
-
const { vhds } = metadata
|
|
424
|
-
return Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
|
|
425
|
-
})()
|
|
426
|
-
|
|
427
|
-
const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
|
|
428
|
-
isBackupComplete = missingVhds.length === 0
|
|
429
|
-
|
|
430
|
-
// FIXME: find better approach by keeping as much of the backup as
|
|
431
|
-
// possible (existing disks) even if one disk is missing
|
|
432
|
-
if (isBackupComplete) {
|
|
433
|
-
linkedVhds.forEach(_ => unusedVhds.delete(_))
|
|
434
|
-
linkedVhds.forEach(path => {
|
|
435
|
-
vhdsToJSons[path] = json
|
|
436
|
-
})
|
|
437
|
-
} else {
|
|
438
|
-
logWarn('some VHDs linked to the backup are missing', { backup: json, missingVhds })
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
if (isBackupComplete) {
|
|
443
|
-
backups.set(json, metadata)
|
|
444
|
-
} else {
|
|
445
|
-
jsons.delete(json)
|
|
446
|
-
if (remove) {
|
|
447
|
-
logInfo('deleting incomplete backup', { backup: json })
|
|
448
|
-
mustRegenerateCache = true
|
|
449
|
-
await handler.unlink(json)
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
// TODO: parallelize by vm/job/vdi
|
|
455
|
-
const unusedVhdsDeletion = []
|
|
456
|
-
const toMerge = []
|
|
457
|
-
{
|
|
458
|
-
// VHD chains (as list from oldest to most recent) to merge indexed by most recent
|
|
459
|
-
// ancestor
|
|
460
|
-
const vhdChainsToMerge = { __proto__: null }
|
|
461
|
-
|
|
462
|
-
const toCheck = new Set(unusedVhds)
|
|
463
|
-
|
|
464
|
-
const getUsedChildChainOrDelete = vhd => {
|
|
465
|
-
if (vhd in vhdChainsToMerge) {
|
|
466
|
-
const chain = vhdChainsToMerge[vhd]
|
|
467
|
-
delete vhdChainsToMerge[vhd]
|
|
468
|
-
return chain
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (!unusedVhds.has(vhd)) {
|
|
472
|
-
return [vhd]
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// no longer needs to be checked
|
|
476
|
-
toCheck.delete(vhd)
|
|
477
|
-
|
|
478
|
-
const child = vhdChildren[vhd]
|
|
479
|
-
if (child !== undefined) {
|
|
480
|
-
const chain = getUsedChildChainOrDelete(child)
|
|
481
|
-
if (chain !== undefined) {
|
|
482
|
-
chain.unshift(vhd)
|
|
483
|
-
return chain
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// no warning because a VHD can be unused for perfectly good reasons,
|
|
488
|
-
// e.g. the corresponding backup (metadata file) has been deleted
|
|
489
|
-
if (remove) {
|
|
490
|
-
logInfo('deleting unused VHD', { path: vhd })
|
|
491
|
-
unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
toCheck.forEach(vhd => {
|
|
496
|
-
vhdChainsToMerge[vhd] = getUsedChildChainOrDelete(vhd)
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
// merge interrupted VHDs
|
|
500
|
-
for (const parent of interruptedVhds.keys()) {
|
|
501
|
-
// before #6349 the chain wasn't in the mergeState
|
|
502
|
-
const { chain, statePath } = interruptedVhds.get(parent)
|
|
503
|
-
if (chain === undefined) {
|
|
504
|
-
vhdChainsToMerge[parent] = [parent, vhdChildren[parent]]
|
|
505
|
-
} else {
|
|
506
|
-
vhdChainsToMerge[parent] = chain.map(vhdPath => handlerPath.resolveFromFile(statePath, vhdPath))
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
Object.values(vhdChainsToMerge).forEach(chain => {
|
|
511
|
-
if (chain !== undefined) {
|
|
512
|
-
toMerge.push(chain)
|
|
513
|
-
}
|
|
514
|
-
})
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
let totalMergedDataSize = 0
|
|
518
|
-
const metadataWithMergedVhd = {}
|
|
519
|
-
const doMerge = async () => {
|
|
520
|
-
await asyncMap(toMerge, async chain => {
|
|
521
|
-
const { finalDiskSize, mergedDataSize } = await limitedMergeVhdChain(handler, chain, {
|
|
522
|
-
logInfo,
|
|
523
|
-
logWarn,
|
|
524
|
-
remove,
|
|
525
|
-
mergeBlockConcurrency,
|
|
526
|
-
})
|
|
527
|
-
totalMergedDataSize += mergedDataSize
|
|
528
|
-
const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metadata file
|
|
529
|
-
metadataWithMergedVhd[metadataPath] = (metadataWithMergedVhd[metadataPath] ?? 0) + finalDiskSize
|
|
530
|
-
})
|
|
531
|
-
|
|
532
|
-
return { size: totalMergedDataSize }
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
await Promise.all([
|
|
536
|
-
...unusedVhdsDeletion,
|
|
537
|
-
toMerge.length !== 0 && (merge ? Task.run({ properties: { name: 'merge' } }, doMerge) : () => Promise.resolve()),
|
|
538
|
-
asyncMap(unusedXvas, path => {
|
|
539
|
-
logWarn('unused XVA', { path })
|
|
540
|
-
if (remove) {
|
|
541
|
-
logInfo('deleting unused XVA', { path })
|
|
542
|
-
return handler.unlink(path)
|
|
543
|
-
}
|
|
544
|
-
}),
|
|
545
|
-
asyncMap(xvaSums, path => {
|
|
546
|
-
// no need to handle checksums for XVAs deleted by the script, they will be handled by `unlink()`
|
|
547
|
-
if (!xvas.has(path.slice(0, -'.checksum'.length))) {
|
|
548
|
-
logInfo('unused XVA checksum', { path })
|
|
549
|
-
if (remove) {
|
|
550
|
-
logInfo('deleting unused XVA checksum', { path })
|
|
551
|
-
return handler.unlink(path)
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}),
|
|
555
|
-
])
|
|
556
|
-
|
|
557
|
-
// update size for delta metadata with merged VHD
|
|
558
|
-
// check for the other that the size is the same as the real file size
|
|
559
|
-
await asyncMap(jsons, async metadataPath => {
|
|
560
|
-
const metadata = backups.get(metadataPath)
|
|
561
|
-
|
|
562
|
-
let fileSystemSize
|
|
563
|
-
const mergedSize = metadataWithMergedVhd[metadataPath]
|
|
564
|
-
|
|
565
|
-
const { mode, size, xva } = metadata
|
|
566
|
-
|
|
567
|
-
try {
|
|
568
|
-
if (mode === 'full') {
|
|
569
|
-
// a full backup : check size
|
|
570
|
-
const linkedXva = resolve('/', vmDir, xva)
|
|
571
|
-
try {
|
|
572
|
-
fileSystemSize = await handler.getSize(linkedXva)
|
|
573
|
-
if (fileSystemSize !== size && fileSystemSize !== undefined) {
|
|
574
|
-
logWarn('cleanVm: incorrect backup size in metadata', {
|
|
575
|
-
path: metadataPath,
|
|
576
|
-
actual: size ?? 'none',
|
|
577
|
-
expected: fileSystemSize,
|
|
578
|
-
})
|
|
579
|
-
}
|
|
580
|
-
} catch (error) {
|
|
581
|
-
// will fail with encrypted remote
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
} catch (error) {
|
|
585
|
-
logWarn('failed to get backup size', { backup: metadataPath, error })
|
|
586
|
-
return
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Rewrite metadata when a merge changed the backup layout.
|
|
590
|
-
if (mergedSize) {
|
|
591
|
-
metadata.size = mergedSize
|
|
592
|
-
// all disks are now key disk
|
|
593
|
-
metadata.isVhdDifferencing = {}
|
|
594
|
-
for (const id of Object.keys(metadata.vdis ?? {})) {
|
|
595
|
-
metadata.isVhdDifferencing[id] = false
|
|
596
|
-
}
|
|
597
|
-
mustRegenerateCache = true
|
|
598
|
-
try {
|
|
599
|
-
await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
|
|
600
|
-
} catch (error) {
|
|
601
|
-
logWarn('failed to update backup size in metadata', { path: metadataPath, error })
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
})
|
|
605
|
-
|
|
606
|
-
if (mustRegenerateCache) {
|
|
607
|
-
const cache = {}
|
|
608
|
-
for (const [path, content] of backups.entries()) {
|
|
609
|
-
cache[path] = {
|
|
610
|
-
_filename: path,
|
|
611
|
-
id: path,
|
|
612
|
-
...content,
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
await this._writeCache(cachePath, cache)
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
return {
|
|
619
|
-
// boolean whether some VHDs were merged (or should be merged)
|
|
620
|
-
merge: toMerge.length !== 0,
|
|
621
|
-
size: totalMergedDataSize,
|
|
622
|
-
}
|
|
623
|
-
}
|