@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 +1 -1
- package/Task.js +4 -2
- package/_VmBackup.js +22 -15
- package/_cleanVm.js +31 -44
- package/_forkStreamUnpipe.js +9 -2
- package/_isValidXva.js +5 -1
- package/package.json +3 -3
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
|
-
|
|
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,
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
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: ' +
|
|
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,
|
|
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,
|
|
37
|
-
|
|
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(
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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(
|
|
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, {
|
|
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
|
-
|
|
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)
|
package/_forkStreamUnpipe.js
CHANGED
|
@@ -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.
|
|
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": "^
|
|
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": "^
|
|
41
|
+
"vhd-lib": "^4.0.0",
|
|
42
42
|
"yazl": "^2.5.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|