@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.
@@ -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
- }