@xen-orchestra/backups 0.22.0 → 0.23.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/gitignore-cleanVm-debug.js +516 -0
- package/package.json +2 -2
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('assert')
|
|
4
|
+
const sum = require('lodash/sum')
|
|
5
|
+
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
6
|
+
const { Constants, mergeVhd, openVhd, VhdAbstract, VhdFile } = require('vhd-lib')
|
|
7
|
+
const { isVhdAlias, resolveVhdAlias } = require('vhd-lib/aliases')
|
|
8
|
+
const { dirname, resolve } = require('path')
|
|
9
|
+
const { DISK_TYPES } = Constants
|
|
10
|
+
const { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } = require('./_backupType.js')
|
|
11
|
+
const { limitConcurrency } = require('limit-concurrency-decorator')
|
|
12
|
+
|
|
13
|
+
const { Task } = require('./Task.js')
|
|
14
|
+
const { Disposable } = require('promise-toolbox')
|
|
15
|
+
|
|
16
|
+
// checking the size of a vhd directory is costly
|
|
17
|
+
// 1 Http Query per 1000 blocks
|
|
18
|
+
// we only check size of all the vhd are VhdFiles
|
|
19
|
+
function shouldComputeVhdsSize(vhds) {
|
|
20
|
+
return vhds.every(vhd => vhd instanceof VhdFile)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const computeVhdsSize = (handler, vhdPaths) =>
|
|
24
|
+
Disposable.use(
|
|
25
|
+
vhdPaths.map(vhdPath => openVhd(handler, vhdPath)),
|
|
26
|
+
async vhds => {
|
|
27
|
+
if (shouldComputeVhdsSize(vhds)) {
|
|
28
|
+
const sizes = await asyncMap(vhds, vhd => vhd.getSize())
|
|
29
|
+
return sum(sizes)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// chain is an array of VHDs from child to parent
|
|
35
|
+
//
|
|
36
|
+
// the whole chain will be merged into parent, parent will be renamed to child
|
|
37
|
+
// and all the others will deleted
|
|
38
|
+
async function mergeVhdChain(chain, { handler, onLog, remove, merge }) {
|
|
39
|
+
assert(chain.length >= 2)
|
|
40
|
+
|
|
41
|
+
let child = chain[0]
|
|
42
|
+
const parent = chain[chain.length - 1]
|
|
43
|
+
const children = chain.slice(0, -1).reverse()
|
|
44
|
+
|
|
45
|
+
chain
|
|
46
|
+
.slice(1)
|
|
47
|
+
.reverse()
|
|
48
|
+
.forEach(parent => {
|
|
49
|
+
onLog(`the parent ${parent} of the child ${child} is unused`)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (merge) {
|
|
53
|
+
// `mergeVhd` does not work with a stream, either
|
|
54
|
+
// - make it accept a stream
|
|
55
|
+
// - or create synthetic VHD which is not a stream
|
|
56
|
+
if (children.length !== 1) {
|
|
57
|
+
// TODO: implement merging multiple children
|
|
58
|
+
children.length = 1
|
|
59
|
+
child = children[0]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onLog(`merging ${child} into ${parent}`)
|
|
63
|
+
|
|
64
|
+
let done, total
|
|
65
|
+
const handle = setInterval(() => {
|
|
66
|
+
if (done !== undefined) {
|
|
67
|
+
onLog(`merging ${child}: ${done}/${total}`)
|
|
68
|
+
}
|
|
69
|
+
}, 10e3)
|
|
70
|
+
|
|
71
|
+
const mergedSize = await mergeVhd(
|
|
72
|
+
handler,
|
|
73
|
+
parent,
|
|
74
|
+
handler,
|
|
75
|
+
child,
|
|
76
|
+
// children.length === 1
|
|
77
|
+
// ? child
|
|
78
|
+
// : await createSyntheticStream(handler, children),
|
|
79
|
+
{
|
|
80
|
+
onProgress({ done: d, total: t }) {
|
|
81
|
+
done = d
|
|
82
|
+
total = t
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
clearInterval(handle)
|
|
88
|
+
await Promise.all([
|
|
89
|
+
VhdAbstract.rename(handler, parent, child),
|
|
90
|
+
asyncMap(children.slice(0, -1), child => {
|
|
91
|
+
onLog(`the VHD ${child} is unused`)
|
|
92
|
+
if (remove) {
|
|
93
|
+
onLog(`deleting unused VHD ${child}`)
|
|
94
|
+
return VhdAbstract.unlink(handler, child)
|
|
95
|
+
}
|
|
96
|
+
}),
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
return mergedSize
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const noop = Function.prototype
|
|
104
|
+
|
|
105
|
+
const INTERRUPTED_VHDS_REG = /^\.(.+)\.merge.json$/
|
|
106
|
+
const listVhds = async (handler, vmDir) => {
|
|
107
|
+
const vhds = new Set()
|
|
108
|
+
const aliases = {}
|
|
109
|
+
const interruptedVhds = new Map()
|
|
110
|
+
|
|
111
|
+
await asyncMap(
|
|
112
|
+
await handler.list(`${vmDir}/vdis`, {
|
|
113
|
+
ignoreMissing: true,
|
|
114
|
+
prependDir: true,
|
|
115
|
+
}),
|
|
116
|
+
async jobDir =>
|
|
117
|
+
asyncMap(
|
|
118
|
+
await handler.list(jobDir, {
|
|
119
|
+
prependDir: true,
|
|
120
|
+
}),
|
|
121
|
+
async vdiDir => {
|
|
122
|
+
const list = await handler.list(vdiDir, {
|
|
123
|
+
filter: file => isVhdFile(file) || INTERRUPTED_VHDS_REG.test(file),
|
|
124
|
+
})
|
|
125
|
+
aliases[vdiDir] = list.filter(vhd => isVhdAlias(vhd)).map(file => `${vdiDir}/${file}`)
|
|
126
|
+
list.forEach(file => {
|
|
127
|
+
const res = INTERRUPTED_VHDS_REG.exec(file)
|
|
128
|
+
if (res === null) {
|
|
129
|
+
vhds.add(`${vdiDir}/${file}`)
|
|
130
|
+
} else {
|
|
131
|
+
interruptedVhds.set(`${vdiDir}/${res[1]}`, `${vdiDir}/${file}`)
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return { vhds, interruptedVhds, aliases }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function checkAliases(aliasPaths, targetDataRepository, { handler, onLog = noop, remove = false }) {
|
|
142
|
+
const aliasFound = []
|
|
143
|
+
for (const path of aliasPaths) {
|
|
144
|
+
const target = await resolveVhdAlias(handler, path)
|
|
145
|
+
|
|
146
|
+
if (!isVhdFile(target)) {
|
|
147
|
+
onLog(`Alias ${path} references a non vhd target: ${target}`)
|
|
148
|
+
if (remove) {
|
|
149
|
+
await handler.unlink(target)
|
|
150
|
+
await handler.unlink(path)
|
|
151
|
+
}
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const { dispose } = await openVhd(handler, target)
|
|
157
|
+
try {
|
|
158
|
+
await dispose()
|
|
159
|
+
} catch (e) {
|
|
160
|
+
// error during dispose should not trigger a deletion
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
onLog(`target ${target} of alias ${path} is missing or broken`, { error })
|
|
164
|
+
if (remove) {
|
|
165
|
+
try {
|
|
166
|
+
await VhdAbstract.unlink(handler, path)
|
|
167
|
+
} catch (e) {
|
|
168
|
+
if (e.code !== 'ENOENT') {
|
|
169
|
+
onLog(`Error while deleting target ${target} of alias ${path}`, { error: e })
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
aliasFound.push(resolve('/', target))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const entries = await handler.list(targetDataRepository, {
|
|
180
|
+
ignoreMissing: true,
|
|
181
|
+
prependDir: true,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
entries.forEach(async entry => {
|
|
185
|
+
if (!aliasFound.includes(entry)) {
|
|
186
|
+
onLog(`the Vhd ${entry} is not referenced by a an alias`)
|
|
187
|
+
if (remove) {
|
|
188
|
+
await VhdAbstract.unlink(handler, entry)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
exports.checkAliases = checkAliases
|
|
194
|
+
|
|
195
|
+
const defaultMergeLimiter = limitConcurrency(1)
|
|
196
|
+
|
|
197
|
+
exports.cleanVm = async function cleanVm(
|
|
198
|
+
vmDir,
|
|
199
|
+
{ fixMetadata, remove, merge, mergeLimiter = defaultMergeLimiter, onLog = noop }
|
|
200
|
+
) {
|
|
201
|
+
const limitedMergeVhdChain = mergeLimiter(mergeVhdChain)
|
|
202
|
+
|
|
203
|
+
const handler = this._handler
|
|
204
|
+
|
|
205
|
+
const vhdsToJSons = new Set()
|
|
206
|
+
const vhdParents = { __proto__: null }
|
|
207
|
+
const vhdChildren = { __proto__: null }
|
|
208
|
+
|
|
209
|
+
const { vhds, interruptedVhds, aliases } = await listVhds(handler, vmDir)
|
|
210
|
+
|
|
211
|
+
// remove broken VHDs
|
|
212
|
+
await asyncMap(vhds, async path => {
|
|
213
|
+
try {
|
|
214
|
+
await Disposable.use(openVhd(handler, path, { checkSecondFooter: !interruptedVhds.has(path) }), vhd => {
|
|
215
|
+
if (vhd.footer.diskType === DISK_TYPES.DIFFERENCING) {
|
|
216
|
+
const parent = resolve('/', dirname(path), vhd.header.parentUnicodeName)
|
|
217
|
+
vhdParents[path] = parent
|
|
218
|
+
if (parent in vhdChildren) {
|
|
219
|
+
const error = new Error('this script does not support multiple VHD children')
|
|
220
|
+
error.parent = parent
|
|
221
|
+
error.child1 = vhdChildren[parent]
|
|
222
|
+
error.child2 = path
|
|
223
|
+
throw error // should we throw?
|
|
224
|
+
}
|
|
225
|
+
vhdChildren[parent] = path
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
} catch (error) {
|
|
229
|
+
vhds.delete(path)
|
|
230
|
+
onLog(`error while checking the VHD with path ${path}`, { error })
|
|
231
|
+
if (error?.code === 'ERR_ASSERTION' && remove) {
|
|
232
|
+
onLog(`deleting broken ${path}`)
|
|
233
|
+
return VhdAbstract.unlink(handler, path)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// remove interrupted merge states for missing VHDs
|
|
239
|
+
for (const interruptedVhd of interruptedVhds.keys()) {
|
|
240
|
+
if (!vhds.has(interruptedVhd)) {
|
|
241
|
+
const statePath = interruptedVhds.get(interruptedVhd)
|
|
242
|
+
interruptedVhds.delete(interruptedVhd)
|
|
243
|
+
|
|
244
|
+
onLog('orphan merge state', {
|
|
245
|
+
mergeStatePath: statePath,
|
|
246
|
+
missingVhdPath: interruptedVhd,
|
|
247
|
+
})
|
|
248
|
+
if (remove) {
|
|
249
|
+
onLog(`deleting orphan merge state ${statePath}`)
|
|
250
|
+
await handler.unlink(statePath)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// check if alias are correct
|
|
256
|
+
// check if all vhd in data subfolder have a corresponding alias
|
|
257
|
+
await asyncMap(Object.keys(aliases), async dir => {
|
|
258
|
+
await checkAliases(aliases[dir], `${dir}/data`, { handler, onLog, remove })
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// remove VHDs with missing ancestors
|
|
262
|
+
{
|
|
263
|
+
const deletions = []
|
|
264
|
+
|
|
265
|
+
// return true if the VHD has been deleted or is missing
|
|
266
|
+
const deleteIfOrphan = vhdPath => {
|
|
267
|
+
const parent = vhdParents[vhdPath]
|
|
268
|
+
if (parent === undefined) {
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// no longer needs to be checked
|
|
273
|
+
delete vhdParents[vhdPath]
|
|
274
|
+
|
|
275
|
+
deleteIfOrphan(parent)
|
|
276
|
+
|
|
277
|
+
if (!vhds.has(parent)) {
|
|
278
|
+
vhds.delete(vhdPath)
|
|
279
|
+
|
|
280
|
+
onLog(`the parent ${parent} of the VHD ${vhdPath} is missing`)
|
|
281
|
+
if (remove) {
|
|
282
|
+
onLog(`deleting orphan VHD ${vhdPath}`)
|
|
283
|
+
deletions.push(VhdAbstract.unlink(handler, vhdPath))
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// > A property that is deleted before it has been visited will not be
|
|
289
|
+
// > visited later.
|
|
290
|
+
// >
|
|
291
|
+
// > -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#Deleted_added_or_modified_properties
|
|
292
|
+
for (const child in vhdParents) {
|
|
293
|
+
deleteIfOrphan(child)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await Promise.all(deletions)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const jsons = new Set()
|
|
300
|
+
const xvas = new Set()
|
|
301
|
+
const xvaSums = []
|
|
302
|
+
const entries = await handler.list(vmDir, {
|
|
303
|
+
prependDir: true,
|
|
304
|
+
})
|
|
305
|
+
entries.forEach(path => {
|
|
306
|
+
if (isMetadataFile(path)) {
|
|
307
|
+
jsons.add(path)
|
|
308
|
+
} else if (isXvaFile(path)) {
|
|
309
|
+
xvas.add(path)
|
|
310
|
+
} else if (isXvaSumFile(path)) {
|
|
311
|
+
xvaSums.push(path)
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
await asyncMap(xvas, async path => {
|
|
316
|
+
// check is not good enough to delete the file, the best we can do is report
|
|
317
|
+
// it
|
|
318
|
+
if (!(await this.isValidXva(path))) {
|
|
319
|
+
onLog(`the XVA with path ${path} is potentially broken`)
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
const unusedVhds = new Set(vhds)
|
|
324
|
+
const unusedXvas = new Set(xvas)
|
|
325
|
+
|
|
326
|
+
// compile the list of unused XVAs and VHDs, and remove backup metadata which
|
|
327
|
+
// reference a missing XVA/VHD
|
|
328
|
+
await asyncMap(jsons, async json => {
|
|
329
|
+
let metadata
|
|
330
|
+
try {
|
|
331
|
+
metadata = JSON.parse(await handler.readFile(json))
|
|
332
|
+
} catch (error) {
|
|
333
|
+
onLog(`failed to read metadata file ${json}`, { error })
|
|
334
|
+
jsons.delete(json)
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const { mode } = metadata
|
|
339
|
+
if (mode === 'full') {
|
|
340
|
+
const linkedXva = resolve('/', vmDir, metadata.xva)
|
|
341
|
+
if (xvas.has(linkedXva)) {
|
|
342
|
+
unusedXvas.delete(linkedXva)
|
|
343
|
+
} else {
|
|
344
|
+
onLog(`the XVA linked to the metadata ${json} is missing`)
|
|
345
|
+
if (remove) {
|
|
346
|
+
onLog(`deleting incomplete backup ${json}`)
|
|
347
|
+
jsons.delete(json)
|
|
348
|
+
await handler.unlink(json)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} else if (mode === 'delta') {
|
|
352
|
+
const linkedVhds = (() => {
|
|
353
|
+
const { vhds } = metadata
|
|
354
|
+
return Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
|
|
355
|
+
})()
|
|
356
|
+
|
|
357
|
+
const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
|
|
358
|
+
|
|
359
|
+
// FIXME: find better approach by keeping as much of the backup as
|
|
360
|
+
// possible (existing disks) even if one disk is missing
|
|
361
|
+
if (missingVhds.length === 0) {
|
|
362
|
+
linkedVhds.forEach(_ => unusedVhds.delete(_))
|
|
363
|
+
linkedVhds.forEach(path => {
|
|
364
|
+
vhdsToJSons[path] = json
|
|
365
|
+
})
|
|
366
|
+
} else {
|
|
367
|
+
onLog(`Some VHDs linked to the metadata ${json} are missing`, { missingVhds })
|
|
368
|
+
if (remove) {
|
|
369
|
+
onLog(`deleting incomplete backup ${json}`)
|
|
370
|
+
jsons.delete(json)
|
|
371
|
+
await handler.unlink(json)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
// TODO: parallelize by vm/job/vdi
|
|
378
|
+
const unusedVhdsDeletion = []
|
|
379
|
+
const toMerge = []
|
|
380
|
+
{
|
|
381
|
+
// VHD chains (as list from child to ancestor) to merge indexed by last
|
|
382
|
+
// ancestor
|
|
383
|
+
const vhdChainsToMerge = { __proto__: null }
|
|
384
|
+
|
|
385
|
+
const toCheck = new Set(unusedVhds)
|
|
386
|
+
|
|
387
|
+
const getUsedChildChainOrDelete = vhd => {
|
|
388
|
+
if (vhd in vhdChainsToMerge) {
|
|
389
|
+
const chain = vhdChainsToMerge[vhd]
|
|
390
|
+
delete vhdChainsToMerge[vhd]
|
|
391
|
+
return chain
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!unusedVhds.has(vhd)) {
|
|
395
|
+
return [vhd]
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// no longer needs to be checked
|
|
399
|
+
toCheck.delete(vhd)
|
|
400
|
+
|
|
401
|
+
const child = vhdChildren[vhd]
|
|
402
|
+
if (child !== undefined) {
|
|
403
|
+
const chain = getUsedChildChainOrDelete(child)
|
|
404
|
+
if (chain !== undefined) {
|
|
405
|
+
chain.push(vhd)
|
|
406
|
+
return chain
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
onLog(`the VHD ${vhd} is unused`)
|
|
411
|
+
if (remove) {
|
|
412
|
+
onLog(`deleting unused VHD ${vhd}`)
|
|
413
|
+
unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
toCheck.forEach(vhd => {
|
|
418
|
+
vhdChainsToMerge[vhd] = getUsedChildChainOrDelete(vhd)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
// merge interrupted VHDs
|
|
422
|
+
for (const parent of interruptedVhds.keys()) {
|
|
423
|
+
vhdChainsToMerge[parent] = [vhdChildren[parent], parent]
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
Object.values(vhdChainsToMerge).forEach(chain => {
|
|
427
|
+
if (chain !== undefined) {
|
|
428
|
+
toMerge.push(chain)
|
|
429
|
+
}
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const metadataWithMergedVhd = {}
|
|
434
|
+
const doMerge = async () => {
|
|
435
|
+
await asyncMap(toMerge, async chain => {
|
|
436
|
+
const merged = await limitedMergeVhdChain(chain, { handler, onLog, remove, merge })
|
|
437
|
+
if (merged !== undefined) {
|
|
438
|
+
const metadataPath = vhdsToJSons[chain[0]] // all the chain should have the same metada file
|
|
439
|
+
metadataWithMergedVhd[metadataPath] = true
|
|
440
|
+
}
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await Promise.all([
|
|
445
|
+
...unusedVhdsDeletion,
|
|
446
|
+
toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : doMerge()),
|
|
447
|
+
asyncMap(unusedXvas, path => {
|
|
448
|
+
onLog(`the XVA ${path} is unused`)
|
|
449
|
+
if (remove) {
|
|
450
|
+
onLog(`deleting unused XVA ${path}`)
|
|
451
|
+
return handler.unlink(path)
|
|
452
|
+
}
|
|
453
|
+
}),
|
|
454
|
+
asyncMap(xvaSums, path => {
|
|
455
|
+
// no need to handle checksums for XVAs deleted by the script, they will be handled by `unlink()`
|
|
456
|
+
if (!xvas.has(path.slice(0, -'.checksum'.length))) {
|
|
457
|
+
onLog(`the XVA checksum ${path} is unused`)
|
|
458
|
+
if (remove) {
|
|
459
|
+
onLog(`deleting unused XVA checksum ${path}`)
|
|
460
|
+
return handler.unlink(path)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}),
|
|
464
|
+
])
|
|
465
|
+
|
|
466
|
+
// update size for delta metadata with merged VHD
|
|
467
|
+
// check for the other that the size is the same as the real file size
|
|
468
|
+
|
|
469
|
+
await asyncMap(jsons, async metadataPath => {
|
|
470
|
+
const metadata = JSON.parse(await handler.readFile(metadataPath))
|
|
471
|
+
|
|
472
|
+
let fileSystemSize
|
|
473
|
+
const merged = metadataWithMergedVhd[metadataPath] !== undefined
|
|
474
|
+
|
|
475
|
+
const { mode, size, vhds, xva } = metadata
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
if (mode === 'full') {
|
|
479
|
+
// a full backup : check size
|
|
480
|
+
const linkedXva = resolve('/', vmDir, xva)
|
|
481
|
+
fileSystemSize = await handler.getSize(linkedXva)
|
|
482
|
+
} else if (mode === 'delta') {
|
|
483
|
+
const linkedVhds = Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
|
|
484
|
+
fileSystemSize = await computeVhdsSize(handler, linkedVhds)
|
|
485
|
+
|
|
486
|
+
// the size is not computed in some cases (e.g. VhdDirectory)
|
|
487
|
+
if (fileSystemSize === undefined) {
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// don't warn if the size has changed after a merge
|
|
492
|
+
if (!merged && fileSystemSize !== size) {
|
|
493
|
+
onLog(`incorrect size in metadata: ${size ?? 'none'} instead of ${fileSystemSize}`)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} catch (error) {
|
|
497
|
+
onLog(`failed to get size of ${metadataPath}`, { error })
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// systematically update size after a merge
|
|
502
|
+
if ((merged || fixMetadata) && size !== fileSystemSize) {
|
|
503
|
+
metadata.size = fileSystemSize
|
|
504
|
+
try {
|
|
505
|
+
await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
|
|
506
|
+
} catch (error) {
|
|
507
|
+
onLog(`failed to update size in backup metadata ${metadataPath} after merge`, { error })
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
// boolean whether some VHDs were merged (or should be merged)
|
|
514
|
+
merge: toMerge.length !== 0,
|
|
515
|
+
}
|
|
516
|
+
}
|
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.23.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.6"
|
|
14
14
|
},
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"tmp": "^0.2.1"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@xen-orchestra/xapi": "^0.
|
|
48
|
+
"@xen-orchestra/xapi": "^1.0.0"
|
|
49
49
|
},
|
|
50
50
|
"license": "AGPL-3.0-or-later",
|
|
51
51
|
"author": {
|