@xen-orchestra/backups 0.23.0 → 0.26.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/_cleanVm.js CHANGED
@@ -31,71 +31,48 @@ const computeVhdsSize = (handler, vhdPaths) =>
31
31
  }
32
32
  )
33
33
 
34
- // chain is an array of VHDs from child to parent
34
+ // chain is [ ancestor, child1, ..., childn]
35
+ // 1. Create a VhdSynthetic from all children
36
+ // 2. Merge the VhdSynthetic into the ancestor
37
+ // 3. Delete all (now) unused VHDs
38
+ // 4. Rename the ancestor with the merged data to the latest child
35
39
  //
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 }) {
40
+ // VhdSynthetic
41
+ // |
42
+ // /‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\
43
+ // [ ancestor, child1, ...,child n-1, childn ]
44
+ // | \___________________/ ^
45
+ // | | |
46
+ // | unused VHDs |
47
+ // | |
48
+ // \___________rename_____________/
49
+
50
+ async function mergeVhdChain(chain, { handler, logInfo, remove, merge }) {
39
51
  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
- })
52
+ const chainCopy = [...chain]
53
+ const parent = chainCopy.pop()
54
+ const children = chainCopy
51
55
 
52
56
  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}`)
57
+ logInfo(`merging children into parent`, { childrenCount: children.length, parent })
63
58
 
64
59
  let done, total
65
60
  const handle = setInterval(() => {
66
61
  if (done !== undefined) {
67
- onLog(`merging ${child}: ${done}/${total}`)
62
+ logInfo(`merging children in progress`, { children, parent, doneCount: done, totalCount: total })
68
63
  }
69
64
  }, 10e3)
70
65
 
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
- )
66
+ const mergedSize = await mergeVhd(handler, parent, handler, children, {
67
+ logInfo,
68
+ onProgress({ done: d, total: t }) {
69
+ done = d
70
+ total = t
71
+ },
72
+ remove,
73
+ })
86
74
 
87
75
  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
76
  return mergedSize
100
77
  }
101
78
  }
@@ -138,14 +115,19 @@ const listVhds = async (handler, vmDir) => {
138
115
  return { vhds, interruptedVhds, aliases }
139
116
  }
140
117
 
141
- async function checkAliases(aliasPaths, targetDataRepository, { handler, onLog = noop, remove = false }) {
118
+ async function checkAliases(
119
+ aliasPaths,
120
+ targetDataRepository,
121
+ { handler, logInfo = noop, logWarn = console.warn, remove = false }
122
+ ) {
142
123
  const aliasFound = []
143
124
  for (const path of aliasPaths) {
144
125
  const target = await resolveVhdAlias(handler, path)
145
126
 
146
127
  if (!isVhdFile(target)) {
147
- onLog(`Alias ${path} references a non vhd target: ${target}`)
128
+ logWarn('alias references non VHD target', { path, target })
148
129
  if (remove) {
130
+ logInfo('removing alias and non VHD target', { path, target })
149
131
  await handler.unlink(target)
150
132
  await handler.unlink(path)
151
133
  }
@@ -160,13 +142,13 @@ async function checkAliases(aliasPaths, targetDataRepository, { handler, onLog =
160
142
  // error during dispose should not trigger a deletion
161
143
  }
162
144
  } catch (error) {
163
- onLog(`target ${target} of alias ${path} is missing or broken`, { error })
145
+ logWarn('missing or broken alias target', { target, path, error })
164
146
  if (remove) {
165
147
  try {
166
148
  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 })
149
+ } catch (error) {
150
+ if (error.code !== 'ENOENT') {
151
+ logWarn('error deleting alias target', { target, path, error })
170
152
  }
171
153
  }
172
154
  }
@@ -183,20 +165,22 @@ async function checkAliases(aliasPaths, targetDataRepository, { handler, onLog =
183
165
 
184
166
  entries.forEach(async entry => {
185
167
  if (!aliasFound.includes(entry)) {
186
- onLog(`the Vhd ${entry} is not referenced by a an alias`)
168
+ logWarn('no alias references VHD', { entry })
187
169
  if (remove) {
170
+ logInfo('deleting unaliased VHD')
188
171
  await VhdAbstract.unlink(handler, entry)
189
172
  }
190
173
  }
191
174
  })
192
175
  }
176
+
193
177
  exports.checkAliases = checkAliases
194
178
 
195
179
  const defaultMergeLimiter = limitConcurrency(1)
196
180
 
197
181
  exports.cleanVm = async function cleanVm(
198
182
  vmDir,
199
- { fixMetadata, remove, merge, mergeLimiter = defaultMergeLimiter, onLog = noop }
183
+ { fixMetadata, remove, merge, mergeLimiter = defaultMergeLimiter, logInfo = noop, logWarn = console.warn }
200
184
  ) {
201
185
  const limitedMergeVhdChain = mergeLimiter(mergeVhdChain)
202
186
 
@@ -227,9 +211,9 @@ exports.cleanVm = async function cleanVm(
227
211
  })
228
212
  } catch (error) {
229
213
  vhds.delete(path)
230
- onLog(`error while checking the VHD with path ${path}`, { error })
214
+ logWarn('VHD check error', { path, error })
231
215
  if (error?.code === 'ERR_ASSERTION' && remove) {
232
- onLog(`deleting broken ${path}`)
216
+ logInfo('deleting broken path', { path })
233
217
  return VhdAbstract.unlink(handler, path)
234
218
  }
235
219
  }
@@ -241,12 +225,12 @@ exports.cleanVm = async function cleanVm(
241
225
  const statePath = interruptedVhds.get(interruptedVhd)
242
226
  interruptedVhds.delete(interruptedVhd)
243
227
 
244
- onLog('orphan merge state', {
228
+ logWarn('orphan merge state', {
245
229
  mergeStatePath: statePath,
246
230
  missingVhdPath: interruptedVhd,
247
231
  })
248
232
  if (remove) {
249
- onLog(`deleting orphan merge state ${statePath}`)
233
+ logInfo('deleting orphan merge state', { statePath })
250
234
  await handler.unlink(statePath)
251
235
  }
252
236
  }
@@ -255,7 +239,7 @@ exports.cleanVm = async function cleanVm(
255
239
  // check if alias are correct
256
240
  // check if all vhd in data subfolder have a corresponding alias
257
241
  await asyncMap(Object.keys(aliases), async dir => {
258
- await checkAliases(aliases[dir], `${dir}/data`, { handler, onLog, remove })
242
+ await checkAliases(aliases[dir], `${dir}/data`, { handler, logInfo, logWarn, remove })
259
243
  })
260
244
 
261
245
  // remove VHDs with missing ancestors
@@ -277,9 +261,9 @@ exports.cleanVm = async function cleanVm(
277
261
  if (!vhds.has(parent)) {
278
262
  vhds.delete(vhdPath)
279
263
 
280
- onLog(`the parent ${parent} of the VHD ${vhdPath} is missing`)
264
+ logWarn('parent VHD is missing', { parent, vhdPath })
281
265
  if (remove) {
282
- onLog(`deleting orphan VHD ${vhdPath}`)
266
+ logInfo('deleting orphan VHD', { vhdPath })
283
267
  deletions.push(VhdAbstract.unlink(handler, vhdPath))
284
268
  }
285
269
  }
@@ -316,7 +300,7 @@ exports.cleanVm = async function cleanVm(
316
300
  // check is not good enough to delete the file, the best we can do is report
317
301
  // it
318
302
  if (!(await this.isValidXva(path))) {
319
- onLog(`the XVA with path ${path} is potentially broken`)
303
+ logWarn('XVA might be broken', { path })
320
304
  }
321
305
  })
322
306
 
@@ -330,7 +314,7 @@ exports.cleanVm = async function cleanVm(
330
314
  try {
331
315
  metadata = JSON.parse(await handler.readFile(json))
332
316
  } catch (error) {
333
- onLog(`failed to read metadata file ${json}`, { error })
317
+ logWarn('failed to read metadata file', { json, error })
334
318
  jsons.delete(json)
335
319
  return
336
320
  }
@@ -341,9 +325,9 @@ exports.cleanVm = async function cleanVm(
341
325
  if (xvas.has(linkedXva)) {
342
326
  unusedXvas.delete(linkedXva)
343
327
  } else {
344
- onLog(`the XVA linked to the metadata ${json} is missing`)
328
+ logWarn('metadata XVA is missing', { json })
345
329
  if (remove) {
346
- onLog(`deleting incomplete backup ${json}`)
330
+ logInfo('deleting incomplete backup', { json })
347
331
  jsons.delete(json)
348
332
  await handler.unlink(json)
349
333
  }
@@ -364,9 +348,9 @@ exports.cleanVm = async function cleanVm(
364
348
  vhdsToJSons[path] = json
365
349
  })
366
350
  } else {
367
- onLog(`Some VHDs linked to the metadata ${json} are missing`, { missingVhds })
351
+ logWarn('some metadata VHDs are missing', { json, missingVhds })
368
352
  if (remove) {
369
- onLog(`deleting incomplete backup ${json}`)
353
+ logInfo('deleting incomplete backup', { json })
370
354
  jsons.delete(json)
371
355
  await handler.unlink(json)
372
356
  }
@@ -407,9 +391,9 @@ exports.cleanVm = async function cleanVm(
407
391
  }
408
392
  }
409
393
 
410
- onLog(`the VHD ${vhd} is unused`)
394
+ logWarn('unused VHD', { vhd })
411
395
  if (remove) {
412
- onLog(`deleting unused VHD ${vhd}`)
396
+ logInfo('deleting unused VHD', { vhd })
413
397
  unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
414
398
  }
415
399
  }
@@ -433,7 +417,7 @@ exports.cleanVm = async function cleanVm(
433
417
  const metadataWithMergedVhd = {}
434
418
  const doMerge = async () => {
435
419
  await asyncMap(toMerge, async chain => {
436
- const merged = await limitedMergeVhdChain(chain, { handler, onLog, remove, merge })
420
+ const merged = await limitedMergeVhdChain(chain, { handler, logInfo, logWarn, remove, merge })
437
421
  if (merged !== undefined) {
438
422
  const metadataPath = vhdsToJSons[chain[0]] // all the chain should have the same metada file
439
423
  metadataWithMergedVhd[metadataPath] = true
@@ -445,18 +429,18 @@ exports.cleanVm = async function cleanVm(
445
429
  ...unusedVhdsDeletion,
446
430
  toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : doMerge()),
447
431
  asyncMap(unusedXvas, path => {
448
- onLog(`the XVA ${path} is unused`)
432
+ logWarn('unused XVA', { path })
449
433
  if (remove) {
450
- onLog(`deleting unused XVA ${path}`)
434
+ logInfo('deleting unused XVA', { path })
451
435
  return handler.unlink(path)
452
436
  }
453
437
  }),
454
438
  asyncMap(xvaSums, path => {
455
439
  // no need to handle checksums for XVAs deleted by the script, they will be handled by `unlink()`
456
440
  if (!xvas.has(path.slice(0, -'.checksum'.length))) {
457
- onLog(`the XVA checksum ${path} is unused`)
441
+ logInfo('unused XVA checksum', { path })
458
442
  if (remove) {
459
- onLog(`deleting unused XVA checksum ${path}`)
443
+ logInfo('deleting unused XVA checksum', { path })
460
444
  return handler.unlink(path)
461
445
  }
462
446
  }
@@ -490,11 +474,11 @@ exports.cleanVm = async function cleanVm(
490
474
 
491
475
  // don't warn if the size has changed after a merge
492
476
  if (!merged && fileSystemSize !== size) {
493
- onLog(`incorrect size in metadata: ${size ?? 'none'} instead of ${fileSystemSize}`)
477
+ logWarn('incorrect size in metadata', { size: size ?? 'none', fileSystemSize })
494
478
  }
495
479
  }
496
480
  } catch (error) {
497
- onLog(`failed to get size of ${metadataPath}`, { error })
481
+ logWarn('failed to get metadata size', { metadataPath, error })
498
482
  return
499
483
  }
500
484
 
@@ -504,7 +488,7 @@ exports.cleanVm = async function cleanVm(
504
488
  try {
505
489
  await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
506
490
  } catch (error) {
507
- onLog(`failed to update size in backup metadata ${metadataPath} after merge`, { error })
491
+ logWarn('metadata size update failed', { metadataPath, error })
508
492
  }
509
493
  }
510
494
  })
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ // eslint-disable-next-line eslint-comments/disable-enable-pair
3
+ /* eslint-disable n/shebang */
2
4
 
3
5
  'use strict'
4
6
 
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.23.0",
11
+ "version": "0.26.0",
12
12
  "engines": {
13
13
  "node": ">=14.6"
14
14
  },
@@ -22,11 +22,12 @@
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.1",
25
+ "@xen-orchestra/fs": "^1.0.3",
26
26
  "@xen-orchestra/log": "^0.3.0",
27
27
  "@xen-orchestra/template": "^0.1.0",
28
28
  "compare-versions": "^4.0.1",
29
29
  "d3-time-format": "^3.0.0",
30
+ "decorator-synchronized": "^0.6.0",
30
31
  "end-of-stream": "^1.4.4",
31
32
  "fs-extra": "^10.0.0",
32
33
  "golike-defer": "^0.5.1",
@@ -37,7 +38,7 @@
37
38
  "promise-toolbox": "^0.21.0",
38
39
  "proper-lockfile": "^4.1.2",
39
40
  "uuid": "^8.3.2",
40
- "vhd-lib": "^3.1.0",
41
+ "vhd-lib": "^3.2.0",
41
42
  "yazl": "^2.5.1"
42
43
  },
43
44
  "devDependencies": {
@@ -45,7 +46,7 @@
45
46
  "tmp": "^0.2.1"
46
47
  },
47
48
  "peerDependencies": {
48
- "@xen-orchestra/xapi": "^1.0.0"
49
+ "@xen-orchestra/xapi": "^1.2.0"
49
50
  },
50
51
  "license": "AGPL-3.0-or-later",
51
52
  "author": {
@@ -19,6 +19,8 @@ const { AbstractDeltaWriter } = require('./_AbstractDeltaWriter.js')
19
19
  const { checkVhd } = require('./_checkVhd.js')
20
20
  const { packUuid } = require('./_packUuid.js')
21
21
  const { Disposable } = require('promise-toolbox')
22
+ const { HealthCheckVmBackup } = require('../HealthCheckVmBackup.js')
23
+ const { ImportVmBackup } = require('../ImportVmBackup.js')
22
24
 
23
25
  const { warn } = createLogger('xo:backups:DeltaBackupWriter')
24
26
 
@@ -69,6 +71,35 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
69
71
  return this._cleanVm({ merge: true })
70
72
  }
71
73
 
74
+ healthCheck(sr) {
75
+ return Task.run(
76
+ {
77
+ name: 'health check',
78
+ },
79
+ async () => {
80
+ const xapi = sr.$xapi
81
+ const srUuid = sr.uuid
82
+ const adapter = this._adapter
83
+ const metadata = await adapter.readVmBackupMetadata(this._metadataFileName)
84
+ const { id: restoredId } = await new ImportVmBackup({
85
+ adapter,
86
+ metadata,
87
+ srUuid,
88
+ xapi,
89
+ }).run()
90
+ const restoredVm = xapi.getObject(restoredId)
91
+ try {
92
+ await new HealthCheckVmBackup({
93
+ restoredVm,
94
+ xapi,
95
+ }).run()
96
+ } finally {
97
+ await xapi.VM_destroy(restoredVm.$ref)
98
+ }
99
+ }
100
+ )
101
+ }
102
+
72
103
  prepare({ isFull }) {
73
104
  // create the task related to this export and ensure all methods are called in this context
74
105
  const task = new Task({
@@ -80,7 +111,9 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
80
111
  },
81
112
  })
82
113
  this.transfer = task.wrapFn(this.transfer)
83
- this.cleanup = task.wrapFn(this.cleanup, true)
114
+ this.healthCheck = task.wrapFn(this.healthCheck)
115
+ this.cleanup = task.wrapFn(this.cleanup)
116
+ this.afterBackup = task.wrapFn(this.afterBackup, true)
84
117
 
85
118
  return task.run(() => this._prepare())
86
119
  }
@@ -156,7 +189,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
156
189
  }/${adapter.getVhdFileName(basename)}`
157
190
  )
158
191
 
159
- const metadataFilename = `${backupDir}/${basename}.json`
192
+ const metadataFilename = (this._metadataFileName = `${backupDir}/${basename}.json`)
160
193
  const metadataContent = {
161
194
  jobId,
162
195
  mode: job.mode,
@@ -9,4 +9,6 @@ exports.AbstractWriter = class AbstractWriter {
9
9
  beforeBackup() {}
10
10
 
11
11
  afterBackup() {}
12
+
13
+ healthCheck(sr) {}
12
14
  }
@@ -6,8 +6,9 @@ const { join } = require('path')
6
6
  const { getVmBackupDir } = require('../_getVmBackupDir.js')
7
7
  const MergeWorker = require('../merge-worker/index.js')
8
8
  const { formatFilenameDate } = require('../_filenameDate.js')
9
+ const { Task } = require('../Task.js')
9
10
 
10
- const { warn } = createLogger('xo:backups:MixinBackupWriter')
11
+ const { info, warn } = createLogger('xo:backups:MixinBackupWriter')
11
12
 
12
13
  exports.MixinBackupWriter = (BaseClass = Object) =>
13
14
  class MixinBackupWriter extends BaseClass {
@@ -25,11 +26,17 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
25
26
 
26
27
  async _cleanVm(options) {
27
28
  try {
28
- return await this._adapter.cleanVm(this.#vmBackupDir, {
29
- ...options,
30
- fixMetadata: true,
31
- onLog: warn,
32
- lock: false,
29
+ return await Task.run({ name: 'clean-vm' }, () => {
30
+ return this._adapter.cleanVm(this.#vmBackupDir, {
31
+ ...options,
32
+ fixMetadata: true,
33
+ logInfo: info,
34
+ logWarn: (message, data) => {
35
+ warn(message, data)
36
+ Task.warning(message, data)
37
+ },
38
+ lock: false,
39
+ })
33
40
  })
34
41
  } catch (error) {
35
42
  warn(error)
@@ -64,5 +71,6 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
64
71
  const remotePath = handler._getRealPath()
65
72
  await MergeWorker.run(remotePath)
66
73
  }
74
+ await this._adapter.invalidateVmBackupListCache(this._backup.vm.uuid)
67
75
  }
68
76
  }