@xen-orchestra/backups 0.27.3 → 0.27.4

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/Backup.js CHANGED
@@ -245,7 +245,7 @@ exports.Backup = class Backup {
245
245
  })
246
246
  )
247
247
  ),
248
- () => settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined,
248
+ () => (settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined),
249
249
  async (srs, remoteAdapters, healthCheckSr) => {
250
250
  // remove adapters that failed (already handled)
251
251
  remoteAdapters = remoteAdapters.filter(_ => _ !== undefined)
package/Task.js CHANGED
@@ -3,8 +3,10 @@
3
3
  const CancelToken = require('promise-toolbox/CancelToken')
4
4
  const Zone = require('node-zone')
5
5
 
6
- const logAfterEnd = () => {
7
- throw new Error('task has already ended')
6
+ const logAfterEnd = log => {
7
+ const error = new Error('task has already ended')
8
+ error.log = log
9
+ throw error
8
10
  }
9
11
 
10
12
  const noop = Function.prototype
package/_VmBackup.js CHANGED
@@ -128,42 +128,49 @@ class VmBackup {
128
128
  }
129
129
 
130
130
  // calls fn for each function, warns of any errors, and throws only if there are no writers left
131
- async _callWriters(fn, warnMessage, parallel = true) {
131
+ async _callWriters(fn, step, parallel = true) {
132
132
  const writers = this._writers
133
133
  const n = writers.size
134
134
  if (n === 0) {
135
135
  return
136
136
  }
137
- if (n === 1) {
138
- const [writer] = writers
137
+
138
+ async function callWriter(writer) {
139
+ const { name } = writer.constructor
139
140
  try {
141
+ debug('writer step starting', { step, writer: name })
140
142
  await fn(writer)
143
+ debug('writer step succeeded', { duration: step, writer: name })
141
144
  } catch (error) {
142
145
  writers.delete(writer)
146
+
147
+ warn('writer step failed', { error, step, writer: name })
148
+
149
+ // these two steps are the only one that are not already in their own sub tasks
150
+ if (step === 'writer.checkBaseVdis()' || step === 'writer.beforeBackup()') {
151
+ Task.warning(
152
+ `the writer ${name} has failed the step ${step} with error ${error.message}. It won't be used anymore in this job execution.`
153
+ )
154
+ }
155
+
143
156
  throw error
144
157
  }
145
- return
158
+ }
159
+ if (n === 1) {
160
+ const [writer] = writers
161
+ return callWriter(writer)
146
162
  }
147
163
 
148
164
  const errors = []
149
165
  await (parallel ? asyncMap : asyncEach)(writers, async function (writer) {
150
166
  try {
151
- await fn(writer)
167
+ await callWriter(writer)
152
168
  } catch (error) {
153
169
  errors.push(error)
154
- this.delete(writer)
155
- warn(warnMessage, { error, writer: writer.constructor.name })
156
-
157
- // these two steps are the only one that are not already in their own sub tasks
158
- if (warnMessage === 'writer.checkBaseVdis()' || warnMessage === 'writer.beforeBackup()') {
159
- Task.warning(
160
- `the writer ${writer.constructor.name} has failed the step ${warnMessage} with error ${error.message}. It won't be used anymore in this job execution.`
161
- )
162
- }
163
170
  }
164
171
  })
165
172
  if (writers.size === 0) {
166
- throw new AggregateError(errors, 'all targets have failed, step: ' + warnMessage)
173
+ throw new AggregateError(errors, 'all targets have failed, step: ' + step)
167
174
  }
168
175
  }
169
176
 
package/_cleanVm.js CHANGED
@@ -1,15 +1,15 @@
1
1
  'use strict'
2
2
 
3
- const assert = require('assert')
4
3
  const sum = require('lodash/sum')
5
4
  const UUID = require('uuid')
6
5
  const { asyncMap } = require('@xen-orchestra/async-map')
7
- const { Constants, mergeVhd, openVhd, VhdAbstract, VhdFile } = require('vhd-lib')
6
+ const { Constants, openVhd, VhdAbstract, VhdFile } = require('vhd-lib')
8
7
  const { isVhdAlias, resolveVhdAlias } = require('vhd-lib/aliases')
9
8
  const { dirname, resolve } = require('path')
10
9
  const { DISK_TYPES } = Constants
11
10
  const { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } = require('./_backupType.js')
12
11
  const { limitConcurrency } = require('limit-concurrency-decorator')
12
+ const { mergeVhdChain } = require('vhd-lib/merge')
13
13
 
14
14
  const { Task } = require('./Task.js')
15
15
  const { Disposable } = require('promise-toolbox')
@@ -18,7 +18,10 @@ const handlerPath = require('@xen-orchestra/fs/path')
18
18
  // checking the size of a vhd directory is costly
19
19
  // 1 Http Query per 1000 blocks
20
20
  // we only check size of all the vhd are VhdFiles
21
- function shouldComputeVhdsSize(vhds) {
21
+ function shouldComputeVhdsSize(handler, vhds) {
22
+ if (handler.isEncrypted) {
23
+ return false
24
+ }
22
25
  return vhds.every(vhd => vhd instanceof VhdFile)
23
26
  }
24
27
 
@@ -26,61 +29,41 @@ const computeVhdsSize = (handler, vhdPaths) =>
26
29
  Disposable.use(
27
30
  vhdPaths.map(vhdPath => openVhd(handler, vhdPath)),
28
31
  async vhds => {
29
- if (shouldComputeVhdsSize(vhds)) {
32
+ if (shouldComputeVhdsSize(handler, vhds)) {
30
33
  const sizes = await asyncMap(vhds, vhd => vhd.getSize())
31
34
  return sum(sizes)
32
35
  }
33
36
  }
34
37
  )
35
38
 
36
- // chain is [ ancestor, child1, ..., childn]
37
- // 1. Create a VhdSynthetic from all children
38
- // 2. Merge the VhdSynthetic into the ancestor
39
- // 3. Delete all (now) unused VHDs
40
- // 4. Rename the ancestor with the merged data to the latest child
41
- //
42
- // VhdSynthetic
43
- // |
44
- // /‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\
45
- // [ ancestor, child1, ...,child n-1, childn ]
46
- // | \___________________/ ^
47
- // | | |
48
- // | unused VHDs |
49
- // | |
50
- // \___________rename_____________/
51
-
52
- async function mergeVhdChain(chain, { handler, logInfo, remove, merge }) {
53
- assert(chain.length >= 2)
54
- const chainCopy = [...chain]
55
- const parent = chainCopy.shift()
56
- const children = chainCopy
57
-
39
+ // chain is [ ancestor, child_1, ..., child_n ]
40
+ async function _mergeVhdChain(handler, chain, { logInfo, remove, merge }) {
58
41
  if (merge) {
59
- logInfo('will merge children into parent', { children, parent })
42
+ logInfo(`merging VHD chain`, { chain })
60
43
 
61
44
  let done, total
62
45
  const handle = setInterval(() => {
63
46
  if (done !== undefined) {
64
47
  logInfo('merge in progress', {
65
48
  done,
66
- parent,
49
+ parent: chain[0],
67
50
  progress: Math.round((100 * done) / total),
68
51
  total,
69
52
  })
70
53
  }
71
54
  }, 10e3)
72
-
73
- const mergedSize = await mergeVhd(handler, parent, handler, children, {
74
- logInfo,
75
- onProgress({ done: d, total: t }) {
76
- done = d
77
- total = t
78
- },
79
- remove,
80
- })
81
-
82
- clearInterval(handle)
83
- return mergedSize
55
+ try {
56
+ return await mergeVhdChain(handler, chain, {
57
+ logInfo,
58
+ onProgress({ done: d, total: t }) {
59
+ done = d
60
+ total = t
61
+ },
62
+ removeUnused: remove,
63
+ })
64
+ } finally {
65
+ clearInterval(handle)
66
+ }
84
67
  }
85
68
  }
86
69
 
@@ -114,7 +97,7 @@ const listVhds = async (handler, vmDir, logWarn) => {
114
97
  vhds.add(`${vdiDir}/${file}`)
115
98
  } else {
116
99
  try {
117
- const mergeState = JSON.parse(await handler.readFile(file))
100
+ const mergeState = JSON.parse(await handler.readFile(`${vdiDir}/${file}`))
118
101
  interruptedVhds.set(`${vdiDir}/${res[1]}`, {
119
102
  statePath: `${vdiDir}/${file}`,
120
103
  chain: mergeState.chain,
@@ -200,7 +183,7 @@ exports.cleanVm = async function cleanVm(
200
183
  vmDir,
201
184
  { fixMetadata, remove, merge, mergeLimiter = defaultMergeLimiter, logInfo = noop, logWarn = console.warn }
202
185
  ) {
203
- const limitedMergeVhdChain = mergeLimiter(mergeVhdChain)
186
+ const limitedMergeVhdChain = mergeLimiter(_mergeVhdChain)
204
187
 
205
188
  const handler = this._handler
206
189
 
@@ -461,7 +444,7 @@ exports.cleanVm = async function cleanVm(
461
444
  const metadataWithMergedVhd = {}
462
445
  const doMerge = async () => {
463
446
  await asyncMap(toMerge, async chain => {
464
- const merged = await limitedMergeVhdChain(chain, { handler, logInfo, logWarn, remove, merge })
447
+ const merged = await limitedMergeVhdChain(handler, chain, { logInfo, logWarn, remove, merge })
465
448
  if (merged !== undefined) {
466
449
  const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metada file
467
450
  metadataWithMergedVhd[metadataPath] = true
@@ -506,7 +489,11 @@ exports.cleanVm = async function cleanVm(
506
489
  if (mode === 'full') {
507
490
  // a full backup : check size
508
491
  const linkedXva = resolve('/', vmDir, xva)
509
- fileSystemSize = await handler.getSize(linkedXva)
492
+ try {
493
+ fileSystemSize = await handler.getSize(linkedXva)
494
+ } catch (error) {
495
+ // can fail with encrypted remote
496
+ }
510
497
  } else if (mode === 'delta') {
511
498
  const linkedVhds = Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
512
499
  fileSystemSize = await computeVhdsSize(handler, linkedVhds)
@@ -3,6 +3,8 @@
3
3
  const eos = require('end-of-stream')
4
4
  const { PassThrough } = require('stream')
5
5
 
6
+ const { debug } = require('@xen-orchestra/log').createLogger('xo:backups:forkStreamUnpipe')
7
+
6
8
  // create a new readable stream from an existing one which may be piped later
7
9
  //
8
10
  // in case of error in the new readable stream, it will simply be unpiped
@@ -11,18 +13,23 @@ exports.forkStreamUnpipe = function forkStreamUnpipe(stream) {
11
13
  const { forks = 0 } = stream
12
14
  stream.forks = forks + 1
13
15
 
16
+ debug('forking', { forks: stream.forks })
17
+
14
18
  const proxy = new PassThrough()
15
19
  stream.pipe(proxy)
16
20
  eos(stream, error => {
17
21
  if (error !== undefined) {
22
+ debug('error on original stream, destroying fork', { error })
18
23
  proxy.destroy(error)
19
24
  }
20
25
  })
21
- eos(proxy, _ => {
22
- stream.forks--
26
+ eos(proxy, error => {
27
+ debug('end of stream, unpiping', { error, forks: --stream.forks })
28
+
23
29
  stream.unpipe(proxy)
24
30
 
25
31
  if (stream.forks === 0) {
32
+ debug('no more forks, destroying original stream')
26
33
  stream.destroy(new Error('no more consumers for this stream'))
27
34
  }
28
35
  })
package/_isValidXva.js CHANGED
@@ -49,6 +49,11 @@ const isValidTar = async (handler, size, fd) => {
49
49
  // TODO: find an heuristic for compressed files
50
50
  async function isValidXva(path) {
51
51
  const handler = this._handler
52
+
53
+ // size is longer when encrypted + reading part of an encrypted file is not implemented
54
+ if (handler.isEncrypted) {
55
+ return true
56
+ }
52
57
  try {
53
58
  const fd = await handler.openFile(path, 'r')
54
59
  try {
@@ -66,7 +71,6 @@ async function isValidXva(path) {
66
71
  }
67
72
  } catch (error) {
68
73
  // never throw, log and report as valid to avoid side effects
69
- console.error('isValidXva', path, error)
70
74
  return true
71
75
  }
72
76
  }
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.3",
11
+ "version": "0.27.4",
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": "^2.0.0",
25
+ "@xen-orchestra/fs": "^3.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.4",
41
+ "vhd-lib": "^4.0.0",
42
42
  "yazl": "^2.5.1"
43
43
  },
44
44
  "devDependencies": {