@xen-orchestra/backups 0.27.0 → 0.27.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/RemoteAdapter.js CHANGED
@@ -279,7 +279,7 @@ class RemoteAdapter {
279
279
  const dirs = new Set(files.map(file => dirname(file)))
280
280
  for (const dir of dirs) {
281
281
  // don't merge in main process, unused VHDs will be merged in the next backup run
282
- await this.cleanVm(dir, { remove: true, onLog: warn })
282
+ await this.cleanVm(dir, { remove: true, logWarn: warn })
283
283
  }
284
284
 
285
285
  const dedupedVmUuid = new Set(metadatas.map(_ => _.vm.uuid))
package/_cleanVm.js CHANGED
@@ -13,6 +13,7 @@ const { limitConcurrency } = require('limit-concurrency-decorator')
13
13
 
14
14
  const { Task } = require('./Task.js')
15
15
  const { Disposable } = require('promise-toolbox')
16
+ const handlerPath = require('@xen-orchestra/fs/path')
16
17
 
17
18
  // checking the size of a vhd directory is costly
18
19
  // 1 Http Query per 1000 blocks
@@ -55,12 +56,17 @@ async function mergeVhdChain(chain, { handler, logInfo, remove, merge }) {
55
56
  const children = chainCopy
56
57
 
57
58
  if (merge) {
58
- logInfo(`merging children into parent`, { childrenCount: children.length, parent })
59
+ logInfo('will merge children into parent', { children, parent })
59
60
 
60
61
  let done, total
61
62
  const handle = setInterval(() => {
62
63
  if (done !== undefined) {
63
- logInfo(`merging children in progress`, { children, parent, doneCount: done, totalCount: total })
64
+ logInfo('merge in progress', {
65
+ done,
66
+ parent,
67
+ progress: Math.round((100 * done) / total),
68
+ total,
69
+ })
64
70
  }
65
71
  }, 10e3)
66
72
 
@@ -81,7 +87,7 @@ async function mergeVhdChain(chain, { handler, logInfo, remove, merge }) {
81
87
  const noop = Function.prototype
82
88
 
83
89
  const INTERRUPTED_VHDS_REG = /^\.(.+)\.merge.json$/
84
- const listVhds = async (handler, vmDir) => {
90
+ const listVhds = async (handler, vmDir, logWarn) => {
85
91
  const vhds = new Set()
86
92
  const aliases = {}
87
93
  const interruptedVhds = new Map()
@@ -101,12 +107,23 @@ const listVhds = async (handler, vmDir) => {
101
107
  filter: file => isVhdFile(file) || INTERRUPTED_VHDS_REG.test(file),
102
108
  })
103
109
  aliases[vdiDir] = list.filter(vhd => isVhdAlias(vhd)).map(file => `${vdiDir}/${file}`)
104
- list.forEach(file => {
110
+
111
+ await asyncMap(list, async file => {
105
112
  const res = INTERRUPTED_VHDS_REG.exec(file)
106
113
  if (res === null) {
107
114
  vhds.add(`${vdiDir}/${file}`)
108
115
  } else {
109
- interruptedVhds.set(`${vdiDir}/${res[1]}`, `${vdiDir}/${file}`)
116
+ try {
117
+ const mergeState = JSON.parse(await handler.readFile(file))
118
+ interruptedVhds.set(`${vdiDir}/${res[1]}`, {
119
+ statePath: `${vdiDir}/${file}`,
120
+ chain: mergeState.chain,
121
+ })
122
+ } catch (error) {
123
+ // fall back to a non resuming merge
124
+ vhds.add(`${vdiDir}/${file}`)
125
+ logWarn('failed to read existing merge state', { path: file, error })
126
+ }
110
127
  }
111
128
  })
112
129
  }
@@ -122,15 +139,15 @@ async function checkAliases(
122
139
  { handler, logInfo = noop, logWarn = console.warn, remove = false }
123
140
  ) {
124
141
  const aliasFound = []
125
- for (const path of aliasPaths) {
126
- const target = await resolveVhdAlias(handler, path)
142
+ for (const alias of aliasPaths) {
143
+ const target = await resolveVhdAlias(handler, alias)
127
144
 
128
145
  if (!isVhdFile(target)) {
129
- logWarn('alias references non VHD target', { path, target })
146
+ logWarn('alias references non VHD target', { alias, target })
130
147
  if (remove) {
131
- logInfo('removing alias and non VHD target', { path, target })
148
+ logInfo('removing alias and non VHD target', { alias, target })
132
149
  await handler.unlink(target)
133
- await handler.unlink(path)
150
+ await handler.unlink(alias)
134
151
  }
135
152
  continue
136
153
  }
@@ -143,13 +160,13 @@ async function checkAliases(
143
160
  // error during dispose should not trigger a deletion
144
161
  }
145
162
  } catch (error) {
146
- logWarn('missing or broken alias target', { target, path, error })
163
+ logWarn('missing or broken alias target', { alias, target, error })
147
164
  if (remove) {
148
165
  try {
149
- await VhdAbstract.unlink(handler, path)
166
+ await VhdAbstract.unlink(handler, alias)
150
167
  } catch (error) {
151
168
  if (error.code !== 'ENOENT') {
152
- logWarn('error deleting alias target', { target, path, error })
169
+ logWarn('error deleting alias target', { alias, target, error })
153
170
  }
154
171
  }
155
172
  }
@@ -159,17 +176,17 @@ async function checkAliases(
159
176
  aliasFound.push(resolve('/', target))
160
177
  }
161
178
 
162
- const entries = await handler.list(targetDataRepository, {
179
+ const vhds = await handler.list(targetDataRepository, {
163
180
  ignoreMissing: true,
164
181
  prependDir: true,
165
182
  })
166
183
 
167
- entries.forEach(async entry => {
168
- if (!aliasFound.includes(entry)) {
169
- logWarn('no alias references VHD', { entry })
184
+ await asyncMap(vhds, async path => {
185
+ if (!aliasFound.includes(path)) {
186
+ logWarn('no alias references VHD', { path })
170
187
  if (remove) {
171
- logInfo('deleting unaliased VHD')
172
- await VhdAbstract.unlink(handler, entry)
188
+ logInfo('deleting unused VHD', { path })
189
+ await VhdAbstract.unlink(handler, path)
173
190
  }
174
191
  }
175
192
  })
@@ -192,7 +209,7 @@ exports.cleanVm = async function cleanVm(
192
209
  const vhdParents = { __proto__: null }
193
210
  const vhdChildren = { __proto__: null }
194
211
 
195
- const { vhds, interruptedVhds, aliases } = await listVhds(handler, vmDir)
212
+ const { vhds, interruptedVhds, aliases } = await listVhds(handler, vmDir, logWarn)
196
213
 
197
214
  // remove broken VHDs
198
215
  await asyncMap(vhds, async path => {
@@ -225,10 +242,8 @@ exports.cleanVm = async function cleanVm(
225
242
  logWarn(`should delete ${duplicate._path}`)
226
243
  vhds.delete(duplicate._path)
227
244
  } else {
228
- logWarn(`same ids but different content`)
245
+ logWarn('same ids but different content')
229
246
  }
230
- } else {
231
- logInfo('not duplicate', UUID.stringify(vhd.footer.uuid), path)
232
247
  }
233
248
  vhdById.set(UUID.stringify(vhdKept.footer.uuid), vhdKept)
234
249
  })
@@ -236,7 +251,7 @@ exports.cleanVm = async function cleanVm(
236
251
  vhds.delete(path)
237
252
  logWarn('VHD check error', { path, error })
238
253
  if (error?.code === 'ERR_ASSERTION' && remove) {
239
- logInfo('deleting broken path', { path })
254
+ logInfo('deleting broken VHD', { path })
240
255
  return VhdAbstract.unlink(handler, path)
241
256
  }
242
257
  }
@@ -245,7 +260,7 @@ exports.cleanVm = async function cleanVm(
245
260
  // remove interrupted merge states for missing VHDs
246
261
  for (const interruptedVhd of interruptedVhds.keys()) {
247
262
  if (!vhds.has(interruptedVhd)) {
248
- const statePath = interruptedVhds.get(interruptedVhd)
263
+ const { statePath } = interruptedVhds.get(interruptedVhd)
249
264
  interruptedVhds.delete(interruptedVhd)
250
265
 
251
266
  logWarn('orphan merge state', {
@@ -284,9 +299,9 @@ exports.cleanVm = async function cleanVm(
284
299
  if (!vhds.has(parent)) {
285
300
  vhds.delete(vhdPath)
286
301
 
287
- logWarn('parent VHD is missing', { parent, vhdPath })
302
+ logWarn('parent VHD is missing', { parent, child: vhdPath })
288
303
  if (remove) {
289
- logInfo('deleting orphan VHD', { vhdPath })
304
+ logInfo('deleting orphan VHD', { path: vhdPath })
290
305
  deletions.push(VhdAbstract.unlink(handler, vhdPath))
291
306
  }
292
307
  }
@@ -337,7 +352,7 @@ exports.cleanVm = async function cleanVm(
337
352
  try {
338
353
  metadata = JSON.parse(await handler.readFile(json))
339
354
  } catch (error) {
340
- logWarn('failed to read metadata file', { json, error })
355
+ logWarn('failed to read backup metadata', { path: json, error })
341
356
  jsons.delete(json)
342
357
  return
343
358
  }
@@ -348,9 +363,9 @@ exports.cleanVm = async function cleanVm(
348
363
  if (xvas.has(linkedXva)) {
349
364
  unusedXvas.delete(linkedXva)
350
365
  } else {
351
- logWarn('metadata XVA is missing', { json })
366
+ logWarn('the XVA linked to the backup is missing', { backup: json, xva: linkedXva })
352
367
  if (remove) {
353
- logInfo('deleting incomplete backup', { json })
368
+ logInfo('deleting incomplete backup', { path: json })
354
369
  jsons.delete(json)
355
370
  await handler.unlink(json)
356
371
  }
@@ -371,9 +386,9 @@ exports.cleanVm = async function cleanVm(
371
386
  vhdsToJSons[path] = json
372
387
  })
373
388
  } else {
374
- logWarn('some metadata VHDs are missing', { json, missingVhds })
389
+ logWarn('some VHDs linked to the backup are missing', { backup: json, missingVhds })
375
390
  if (remove) {
376
- logInfo('deleting incomplete backup', { json })
391
+ logInfo('deleting incomplete backup', { path: json })
377
392
  jsons.delete(json)
378
393
  await handler.unlink(json)
379
394
  }
@@ -414,9 +429,9 @@ exports.cleanVm = async function cleanVm(
414
429
  }
415
430
  }
416
431
 
417
- logWarn('unused VHD', { vhd })
432
+ logWarn('unused VHD', { path: vhd })
418
433
  if (remove) {
419
- logInfo('deleting unused VHD', { vhd })
434
+ logInfo('deleting unused VHD', { path: vhd })
420
435
  unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
421
436
  }
422
437
  }
@@ -427,7 +442,13 @@ exports.cleanVm = async function cleanVm(
427
442
 
428
443
  // merge interrupted VHDs
429
444
  for (const parent of interruptedVhds.keys()) {
430
- vhdChainsToMerge[parent] = [vhdChildren[parent], parent]
445
+ // before #6349 the chain wasn't in the mergeState
446
+ const { chain, statePath } = interruptedVhds.get(parent)
447
+ if (chain === undefined) {
448
+ vhdChainsToMerge[parent] = [parent, vhdChildren[parent]]
449
+ } else {
450
+ vhdChainsToMerge[parent] = chain.map(vhdPath => handlerPath.resolveFromFile(statePath, vhdPath))
451
+ }
431
452
  }
432
453
 
433
454
  Object.values(vhdChainsToMerge).forEach(chain => {
@@ -442,7 +463,7 @@ exports.cleanVm = async function cleanVm(
442
463
  await asyncMap(toMerge, async chain => {
443
464
  const merged = await limitedMergeVhdChain(chain, { handler, logInfo, logWarn, remove, merge })
444
465
  if (merged !== undefined) {
445
- const metadataPath = vhdsToJSons[chain[0]] // all the chain should have the same metada file
466
+ const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metada file
446
467
  metadataWithMergedVhd[metadataPath] = true
447
468
  }
448
469
  })
@@ -497,11 +518,15 @@ exports.cleanVm = async function cleanVm(
497
518
 
498
519
  // don't warn if the size has changed after a merge
499
520
  if (!merged && fileSystemSize !== size) {
500
- logWarn('incorrect size in metadata', { size: size ?? 'none', fileSystemSize })
521
+ logWarn('incorrect backup size in metadata', {
522
+ path: metadataPath,
523
+ actual: size ?? 'none',
524
+ expected: fileSystemSize,
525
+ })
501
526
  }
502
527
  }
503
528
  } catch (error) {
504
- logWarn('failed to get metadata size', { metadataPath, error })
529
+ logWarn('failed to get backup size', { backup: metadataPath, error })
505
530
  return
506
531
  }
507
532
 
@@ -511,7 +536,7 @@ exports.cleanVm = async function cleanVm(
511
536
  try {
512
537
  await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
513
538
  } catch (error) {
514
- logWarn('metadata size update failed', { metadataPath, error })
539
+ logWarn('failed to update backup size in metadata', { path: metadataPath, error })
515
540
  }
516
541
  }
517
542
  })
@@ -64,7 +64,7 @@ const main = Disposable.wrap(async function* main(args) {
64
64
  try {
65
65
  const vmDir = getVmBackupDir(String(await handler.readFile(taskFile)))
66
66
  try {
67
- await adapter.cleanVm(vmDir, { merge: true, onLog: info, remove: true })
67
+ await adapter.cleanVm(vmDir, { merge: true, logInfo: info, logWarn: warn, remove: true })
68
68
  } catch (error) {
69
69
  // consider the clean successful if the VM dir is missing
70
70
  if (error.code !== 'ENOENT') {
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.27.0",
11
+ "version": "0.27.3",
12
12
  "engines": {
13
13
  "node": ">=14.6"
14
14
  },
@@ -22,7 +22,7 @@
22
22
  "@vates/disposable": "^0.1.1",
23
23
  "@vates/parse-duration": "^0.1.1",
24
24
  "@xen-orchestra/async-map": "^0.1.2",
25
- "@xen-orchestra/fs": "^1.1.0",
25
+ "@xen-orchestra/fs": "^2.0.0",
26
26
  "@xen-orchestra/log": "^0.3.0",
27
27
  "@xen-orchestra/template": "^0.1.0",
28
28
  "compare-versions": "^4.0.1",
@@ -38,7 +38,7 @@
38
38
  "promise-toolbox": "^0.21.0",
39
39
  "proper-lockfile": "^4.1.2",
40
40
  "uuid": "^8.3.2",
41
- "vhd-lib": "^3.3.1",
41
+ "vhd-lib": "^3.3.4",
42
42
  "yazl": "^2.5.1"
43
43
  },
44
44
  "devDependencies": {
@@ -46,7 +46,7 @@
46
46
  "tmp": "^0.2.1"
47
47
  },
48
48
  "peerDependencies": {
49
- "@xen-orchestra/xapi": "^1.4.0"
49
+ "@xen-orchestra/xapi": "^1.4.1"
50
50
  },
51
51
  "license": "AGPL-3.0-or-later",
52
52
  "author": {