@xen-orchestra/backups 0.26.0 → 0.27.2

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
@@ -2,6 +2,7 @@
2
2
 
3
3
  const assert = require('assert')
4
4
  const sum = require('lodash/sum')
5
+ const UUID = require('uuid')
5
6
  const { asyncMap } = require('@xen-orchestra/async-map')
6
7
  const { Constants, mergeVhd, openVhd, VhdAbstract, VhdFile } = require('vhd-lib')
7
8
  const { isVhdAlias, resolveVhdAlias } = require('vhd-lib/aliases')
@@ -50,16 +51,21 @@ const computeVhdsSize = (handler, vhdPaths) =>
50
51
  async function mergeVhdChain(chain, { handler, logInfo, remove, merge }) {
51
52
  assert(chain.length >= 2)
52
53
  const chainCopy = [...chain]
53
- const parent = chainCopy.pop()
54
+ const parent = chainCopy.shift()
54
55
  const children = chainCopy
55
56
 
56
57
  if (merge) {
57
- logInfo(`merging children into parent`, { childrenCount: children.length, parent })
58
+ logInfo('will merge children into parent', { children, parent })
58
59
 
59
60
  let done, total
60
61
  const handle = setInterval(() => {
61
62
  if (done !== undefined) {
62
- logInfo(`merging children in progress`, { children, parent, doneCount: done, totalCount: total })
63
+ logInfo('merge in progress', {
64
+ done,
65
+ parent,
66
+ progress: Math.round((100 * done) / total),
67
+ total,
68
+ })
63
69
  }
64
70
  }, 10e3)
65
71
 
@@ -121,15 +127,15 @@ async function checkAliases(
121
127
  { handler, logInfo = noop, logWarn = console.warn, remove = false }
122
128
  ) {
123
129
  const aliasFound = []
124
- for (const path of aliasPaths) {
125
- const target = await resolveVhdAlias(handler, path)
130
+ for (const alias of aliasPaths) {
131
+ const target = await resolveVhdAlias(handler, alias)
126
132
 
127
133
  if (!isVhdFile(target)) {
128
- logWarn('alias references non VHD target', { path, target })
134
+ logWarn('alias references non VHD target', { alias, target })
129
135
  if (remove) {
130
- logInfo('removing alias and non VHD target', { path, target })
136
+ logInfo('removing alias and non VHD target', { alias, target })
131
137
  await handler.unlink(target)
132
- await handler.unlink(path)
138
+ await handler.unlink(alias)
133
139
  }
134
140
  continue
135
141
  }
@@ -142,13 +148,13 @@ async function checkAliases(
142
148
  // error during dispose should not trigger a deletion
143
149
  }
144
150
  } catch (error) {
145
- logWarn('missing or broken alias target', { target, path, error })
151
+ logWarn('missing or broken alias target', { alias, target, error })
146
152
  if (remove) {
147
153
  try {
148
- await VhdAbstract.unlink(handler, path)
154
+ await VhdAbstract.unlink(handler, alias)
149
155
  } catch (error) {
150
156
  if (error.code !== 'ENOENT') {
151
- logWarn('error deleting alias target', { target, path, error })
157
+ logWarn('error deleting alias target', { alias, target, error })
152
158
  }
153
159
  }
154
160
  }
@@ -158,17 +164,17 @@ async function checkAliases(
158
164
  aliasFound.push(resolve('/', target))
159
165
  }
160
166
 
161
- const entries = await handler.list(targetDataRepository, {
167
+ const vhds = await handler.list(targetDataRepository, {
162
168
  ignoreMissing: true,
163
169
  prependDir: true,
164
170
  })
165
171
 
166
- entries.forEach(async entry => {
167
- if (!aliasFound.includes(entry)) {
168
- logWarn('no alias references VHD', { entry })
172
+ await asyncMap(vhds, async path => {
173
+ if (!aliasFound.includes(path)) {
174
+ logWarn('no alias references VHD', { path })
169
175
  if (remove) {
170
- logInfo('deleting unaliased VHD')
171
- await VhdAbstract.unlink(handler, entry)
176
+ logInfo('deleting unused VHD', { path })
177
+ await VhdAbstract.unlink(handler, path)
172
178
  }
173
179
  }
174
180
  })
@@ -187,6 +193,7 @@ exports.cleanVm = async function cleanVm(
187
193
  const handler = this._handler
188
194
 
189
195
  const vhdsToJSons = new Set()
196
+ const vhdById = new Map()
190
197
  const vhdParents = { __proto__: null }
191
198
  const vhdChildren = { __proto__: null }
192
199
 
@@ -208,12 +215,33 @@ exports.cleanVm = async function cleanVm(
208
215
  }
209
216
  vhdChildren[parent] = path
210
217
  }
218
+ // Detect VHDs with the same UUIDs
219
+ //
220
+ // Due to a bug introduced in a1bcd35e2
221
+ const duplicate = vhdById.get(UUID.stringify(vhd.footer.uuid))
222
+ let vhdKept = vhd
223
+ if (duplicate !== undefined) {
224
+ logWarn('uuid is duplicated', { uuid: UUID.stringify(vhd.footer.uuid) })
225
+ if (duplicate.containsAllDataOf(vhd)) {
226
+ logWarn(`should delete ${path}`)
227
+ vhdKept = duplicate
228
+ vhds.delete(path)
229
+ } else if (vhd.containsAllDataOf(duplicate)) {
230
+ logWarn(`should delete ${duplicate._path}`)
231
+ vhds.delete(duplicate._path)
232
+ } else {
233
+ logWarn('same ids but different content')
234
+ }
235
+ } else {
236
+ logInfo('not duplicate', UUID.stringify(vhd.footer.uuid), path)
237
+ }
238
+ vhdById.set(UUID.stringify(vhdKept.footer.uuid), vhdKept)
211
239
  })
212
240
  } catch (error) {
213
241
  vhds.delete(path)
214
242
  logWarn('VHD check error', { path, error })
215
243
  if (error?.code === 'ERR_ASSERTION' && remove) {
216
- logInfo('deleting broken path', { path })
244
+ logInfo('deleting broken VHD', { path })
217
245
  return VhdAbstract.unlink(handler, path)
218
246
  }
219
247
  }
@@ -261,9 +289,9 @@ exports.cleanVm = async function cleanVm(
261
289
  if (!vhds.has(parent)) {
262
290
  vhds.delete(vhdPath)
263
291
 
264
- logWarn('parent VHD is missing', { parent, vhdPath })
292
+ logWarn('parent VHD is missing', { parent, child: vhdPath })
265
293
  if (remove) {
266
- logInfo('deleting orphan VHD', { vhdPath })
294
+ logInfo('deleting orphan VHD', { path: vhdPath })
267
295
  deletions.push(VhdAbstract.unlink(handler, vhdPath))
268
296
  }
269
297
  }
@@ -314,7 +342,7 @@ exports.cleanVm = async function cleanVm(
314
342
  try {
315
343
  metadata = JSON.parse(await handler.readFile(json))
316
344
  } catch (error) {
317
- logWarn('failed to read metadata file', { json, error })
345
+ logWarn('failed to read backup metadata', { path: json, error })
318
346
  jsons.delete(json)
319
347
  return
320
348
  }
@@ -325,9 +353,9 @@ exports.cleanVm = async function cleanVm(
325
353
  if (xvas.has(linkedXva)) {
326
354
  unusedXvas.delete(linkedXva)
327
355
  } else {
328
- logWarn('metadata XVA is missing', { json })
356
+ logWarn('the XVA linked to the backup is missing', { backup: json, xva: linkedXva })
329
357
  if (remove) {
330
- logInfo('deleting incomplete backup', { json })
358
+ logInfo('deleting incomplete backup', { path: json })
331
359
  jsons.delete(json)
332
360
  await handler.unlink(json)
333
361
  }
@@ -348,9 +376,9 @@ exports.cleanVm = async function cleanVm(
348
376
  vhdsToJSons[path] = json
349
377
  })
350
378
  } else {
351
- logWarn('some metadata VHDs are missing', { json, missingVhds })
379
+ logWarn('some VHDs linked to the backup are missing', { backup: json, missingVhds })
352
380
  if (remove) {
353
- logInfo('deleting incomplete backup', { json })
381
+ logInfo('deleting incomplete backup', { path: json })
354
382
  jsons.delete(json)
355
383
  await handler.unlink(json)
356
384
  }
@@ -362,7 +390,7 @@ exports.cleanVm = async function cleanVm(
362
390
  const unusedVhdsDeletion = []
363
391
  const toMerge = []
364
392
  {
365
- // VHD chains (as list from child to ancestor) to merge indexed by last
393
+ // VHD chains (as list from oldest to most recent) to merge indexed by most recent
366
394
  // ancestor
367
395
  const vhdChainsToMerge = { __proto__: null }
368
396
 
@@ -386,14 +414,14 @@ exports.cleanVm = async function cleanVm(
386
414
  if (child !== undefined) {
387
415
  const chain = getUsedChildChainOrDelete(child)
388
416
  if (chain !== undefined) {
389
- chain.push(vhd)
417
+ chain.unshift(vhd)
390
418
  return chain
391
419
  }
392
420
  }
393
421
 
394
- logWarn('unused VHD', { vhd })
422
+ logWarn('unused VHD', { path: vhd })
395
423
  if (remove) {
396
- logInfo('deleting unused VHD', { vhd })
424
+ logInfo('deleting unused VHD', { path: vhd })
397
425
  unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
398
426
  }
399
427
  }
@@ -474,11 +502,15 @@ exports.cleanVm = async function cleanVm(
474
502
 
475
503
  // don't warn if the size has changed after a merge
476
504
  if (!merged && fileSystemSize !== size) {
477
- logWarn('incorrect size in metadata', { size: size ?? 'none', fileSystemSize })
505
+ logWarn('incorrect backup size in metadata', {
506
+ path: metadataPath,
507
+ actual: size ?? 'none',
508
+ expected: fileSystemSize,
509
+ })
478
510
  }
479
511
  }
480
512
  } catch (error) {
481
- logWarn('failed to get metadata size', { metadataPath, error })
513
+ logWarn('failed to get backup size', { backup: metadataPath, error })
482
514
  return
483
515
  }
484
516
 
@@ -488,7 +520,7 @@ exports.cleanVm = async function cleanVm(
488
520
  try {
489
521
  await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
490
522
  } catch (error) {
491
- logWarn('metadata size update failed', { metadataPath, error })
523
+ logWarn('failed to update backup size in metadata', { path: metadataPath, error })
492
524
  }
493
525
  }
494
526
  })
@@ -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.26.0",
11
+ "version": "0.27.2",
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.0.3",
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.2.0",
41
+ "vhd-lib": "^3.3.3",
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.2.0"
49
+ "@xen-orchestra/xapi": "^1.4.1"
50
50
  },
51
51
  "license": "AGPL-3.0-or-later",
52
52
  "author": {