@xen-orchestra/backups 0.27.1 → 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/RemoteAdapter.js +1 -1
- package/Task.js +4 -2
- package/_VmBackup.js +22 -15
- package/_cleanVm.js +92 -80
- package/_forkStreamUnpipe.js +9 -2
- package/_isValidXva.js +5 -1
- package/merge-worker/cli.js +1 -1
- package/package.json +4 -4
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/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,
|
|
282
|
+
await this.cleanVm(dir, { remove: true, logWarn: warn })
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
const dedupedVmUuid = new Set(metadatas.map(_ => _.vm.uuid))
|
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,23 +1,27 @@
|
|
|
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')
|
|
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
|
|
19
20
|
// we only check size of all the vhd are VhdFiles
|
|
20
|
-
function shouldComputeVhdsSize(vhds) {
|
|
21
|
+
function shouldComputeVhdsSize(handler, vhds) {
|
|
22
|
+
if (handler.isEncrypted) {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
21
25
|
return vhds.every(vhd => vhd instanceof VhdFile)
|
|
22
26
|
}
|
|
23
27
|
|
|
@@ -25,63 +29,48 @@ const computeVhdsSize = (handler, vhdPaths) =>
|
|
|
25
29
|
Disposable.use(
|
|
26
30
|
vhdPaths.map(vhdPath => openVhd(handler, vhdPath)),
|
|
27
31
|
async vhds => {
|
|
28
|
-
if (shouldComputeVhdsSize(vhds)) {
|
|
32
|
+
if (shouldComputeVhdsSize(handler, vhds)) {
|
|
29
33
|
const sizes = await asyncMap(vhds, vhd => vhd.getSize())
|
|
30
34
|
return sum(sizes)
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
)
|
|
34
38
|
|
|
35
|
-
// chain is [ ancestor,
|
|
36
|
-
|
|
37
|
-
// 2. Merge the VhdSynthetic into the ancestor
|
|
38
|
-
// 3. Delete all (now) unused VHDs
|
|
39
|
-
// 4. Rename the ancestor with the merged data to the latest child
|
|
40
|
-
//
|
|
41
|
-
// VhdSynthetic
|
|
42
|
-
// |
|
|
43
|
-
// /‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\
|
|
44
|
-
// [ ancestor, child1, ...,child n-1, childn ]
|
|
45
|
-
// | \___________________/ ^
|
|
46
|
-
// | | |
|
|
47
|
-
// | unused VHDs |
|
|
48
|
-
// | |
|
|
49
|
-
// \___________rename_____________/
|
|
50
|
-
|
|
51
|
-
async function mergeVhdChain(chain, { handler, logInfo, remove, merge }) {
|
|
52
|
-
assert(chain.length >= 2)
|
|
53
|
-
const chainCopy = [...chain]
|
|
54
|
-
const parent = chainCopy.shift()
|
|
55
|
-
const children = chainCopy
|
|
56
|
-
|
|
39
|
+
// chain is [ ancestor, child_1, ..., child_n ]
|
|
40
|
+
async function _mergeVhdChain(handler, chain, { logInfo, remove, merge }) {
|
|
57
41
|
if (merge) {
|
|
58
|
-
logInfo(`merging
|
|
42
|
+
logInfo(`merging VHD chain`, { chain })
|
|
59
43
|
|
|
60
44
|
let done, total
|
|
61
45
|
const handle = setInterval(() => {
|
|
62
46
|
if (done !== undefined) {
|
|
63
|
-
logInfo(
|
|
47
|
+
logInfo('merge in progress', {
|
|
48
|
+
done,
|
|
49
|
+
parent: chain[0],
|
|
50
|
+
progress: Math.round((100 * done) / total),
|
|
51
|
+
total,
|
|
52
|
+
})
|
|
64
53
|
}
|
|
65
54
|
}, 10e3)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
}
|
|
78
67
|
}
|
|
79
68
|
}
|
|
80
69
|
|
|
81
70
|
const noop = Function.prototype
|
|
82
71
|
|
|
83
72
|
const INTERRUPTED_VHDS_REG = /^\.(.+)\.merge.json$/
|
|
84
|
-
const listVhds = async (handler, vmDir) => {
|
|
73
|
+
const listVhds = async (handler, vmDir, logWarn) => {
|
|
85
74
|
const vhds = new Set()
|
|
86
75
|
const aliases = {}
|
|
87
76
|
const interruptedVhds = new Map()
|
|
@@ -101,12 +90,23 @@ const listVhds = async (handler, vmDir) => {
|
|
|
101
90
|
filter: file => isVhdFile(file) || INTERRUPTED_VHDS_REG.test(file),
|
|
102
91
|
})
|
|
103
92
|
aliases[vdiDir] = list.filter(vhd => isVhdAlias(vhd)).map(file => `${vdiDir}/${file}`)
|
|
104
|
-
|
|
93
|
+
|
|
94
|
+
await asyncMap(list, async file => {
|
|
105
95
|
const res = INTERRUPTED_VHDS_REG.exec(file)
|
|
106
96
|
if (res === null) {
|
|
107
97
|
vhds.add(`${vdiDir}/${file}`)
|
|
108
98
|
} else {
|
|
109
|
-
|
|
99
|
+
try {
|
|
100
|
+
const mergeState = JSON.parse(await handler.readFile(`${vdiDir}/${file}`))
|
|
101
|
+
interruptedVhds.set(`${vdiDir}/${res[1]}`, {
|
|
102
|
+
statePath: `${vdiDir}/${file}`,
|
|
103
|
+
chain: mergeState.chain,
|
|
104
|
+
})
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// fall back to a non resuming merge
|
|
107
|
+
vhds.add(`${vdiDir}/${file}`)
|
|
108
|
+
logWarn('failed to read existing merge state', { path: file, error })
|
|
109
|
+
}
|
|
110
110
|
}
|
|
111
111
|
})
|
|
112
112
|
}
|
|
@@ -122,15 +122,15 @@ async function checkAliases(
|
|
|
122
122
|
{ handler, logInfo = noop, logWarn = console.warn, remove = false }
|
|
123
123
|
) {
|
|
124
124
|
const aliasFound = []
|
|
125
|
-
for (const
|
|
126
|
-
const target = await resolveVhdAlias(handler,
|
|
125
|
+
for (const alias of aliasPaths) {
|
|
126
|
+
const target = await resolveVhdAlias(handler, alias)
|
|
127
127
|
|
|
128
128
|
if (!isVhdFile(target)) {
|
|
129
|
-
logWarn('alias references non VHD target', {
|
|
129
|
+
logWarn('alias references non VHD target', { alias, target })
|
|
130
130
|
if (remove) {
|
|
131
|
-
logInfo('removing alias and non VHD target', {
|
|
131
|
+
logInfo('removing alias and non VHD target', { alias, target })
|
|
132
132
|
await handler.unlink(target)
|
|
133
|
-
await handler.unlink(
|
|
133
|
+
await handler.unlink(alias)
|
|
134
134
|
}
|
|
135
135
|
continue
|
|
136
136
|
}
|
|
@@ -143,13 +143,13 @@ async function checkAliases(
|
|
|
143
143
|
// error during dispose should not trigger a deletion
|
|
144
144
|
}
|
|
145
145
|
} catch (error) {
|
|
146
|
-
logWarn('missing or broken alias target', {
|
|
146
|
+
logWarn('missing or broken alias target', { alias, target, error })
|
|
147
147
|
if (remove) {
|
|
148
148
|
try {
|
|
149
|
-
await VhdAbstract.unlink(handler,
|
|
149
|
+
await VhdAbstract.unlink(handler, alias)
|
|
150
150
|
} catch (error) {
|
|
151
151
|
if (error.code !== 'ENOENT') {
|
|
152
|
-
logWarn('error deleting alias target', {
|
|
152
|
+
logWarn('error deleting alias target', { alias, target, error })
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
}
|
|
@@ -159,17 +159,17 @@ async function checkAliases(
|
|
|
159
159
|
aliasFound.push(resolve('/', target))
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
const
|
|
162
|
+
const vhds = await handler.list(targetDataRepository, {
|
|
163
163
|
ignoreMissing: true,
|
|
164
164
|
prependDir: true,
|
|
165
165
|
})
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
if (!aliasFound.includes(
|
|
169
|
-
logWarn('no alias references VHD', {
|
|
167
|
+
await asyncMap(vhds, async path => {
|
|
168
|
+
if (!aliasFound.includes(path)) {
|
|
169
|
+
logWarn('no alias references VHD', { path })
|
|
170
170
|
if (remove) {
|
|
171
|
-
logInfo('deleting
|
|
172
|
-
await VhdAbstract.unlink(handler,
|
|
171
|
+
logInfo('deleting unused VHD', { path })
|
|
172
|
+
await VhdAbstract.unlink(handler, path)
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
})
|
|
@@ -183,7 +183,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
183
183
|
vmDir,
|
|
184
184
|
{ fixMetadata, remove, merge, mergeLimiter = defaultMergeLimiter, logInfo = noop, logWarn = console.warn }
|
|
185
185
|
) {
|
|
186
|
-
const limitedMergeVhdChain = mergeLimiter(
|
|
186
|
+
const limitedMergeVhdChain = mergeLimiter(_mergeVhdChain)
|
|
187
187
|
|
|
188
188
|
const handler = this._handler
|
|
189
189
|
|
|
@@ -192,7 +192,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
192
192
|
const vhdParents = { __proto__: null }
|
|
193
193
|
const vhdChildren = { __proto__: null }
|
|
194
194
|
|
|
195
|
-
const { vhds, interruptedVhds, aliases } = await listVhds(handler, vmDir)
|
|
195
|
+
const { vhds, interruptedVhds, aliases } = await listVhds(handler, vmDir, logWarn)
|
|
196
196
|
|
|
197
197
|
// remove broken VHDs
|
|
198
198
|
await asyncMap(vhds, async path => {
|
|
@@ -225,10 +225,8 @@ exports.cleanVm = async function cleanVm(
|
|
|
225
225
|
logWarn(`should delete ${duplicate._path}`)
|
|
226
226
|
vhds.delete(duplicate._path)
|
|
227
227
|
} else {
|
|
228
|
-
logWarn(
|
|
228
|
+
logWarn('same ids but different content')
|
|
229
229
|
}
|
|
230
|
-
} else {
|
|
231
|
-
logInfo('not duplicate', UUID.stringify(vhd.footer.uuid), path)
|
|
232
230
|
}
|
|
233
231
|
vhdById.set(UUID.stringify(vhdKept.footer.uuid), vhdKept)
|
|
234
232
|
})
|
|
@@ -236,7 +234,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
236
234
|
vhds.delete(path)
|
|
237
235
|
logWarn('VHD check error', { path, error })
|
|
238
236
|
if (error?.code === 'ERR_ASSERTION' && remove) {
|
|
239
|
-
logInfo('deleting broken
|
|
237
|
+
logInfo('deleting broken VHD', { path })
|
|
240
238
|
return VhdAbstract.unlink(handler, path)
|
|
241
239
|
}
|
|
242
240
|
}
|
|
@@ -245,7 +243,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
245
243
|
// remove interrupted merge states for missing VHDs
|
|
246
244
|
for (const interruptedVhd of interruptedVhds.keys()) {
|
|
247
245
|
if (!vhds.has(interruptedVhd)) {
|
|
248
|
-
const statePath = interruptedVhds.get(interruptedVhd)
|
|
246
|
+
const { statePath } = interruptedVhds.get(interruptedVhd)
|
|
249
247
|
interruptedVhds.delete(interruptedVhd)
|
|
250
248
|
|
|
251
249
|
logWarn('orphan merge state', {
|
|
@@ -284,9 +282,9 @@ exports.cleanVm = async function cleanVm(
|
|
|
284
282
|
if (!vhds.has(parent)) {
|
|
285
283
|
vhds.delete(vhdPath)
|
|
286
284
|
|
|
287
|
-
logWarn('parent VHD is missing', { parent, vhdPath })
|
|
285
|
+
logWarn('parent VHD is missing', { parent, child: vhdPath })
|
|
288
286
|
if (remove) {
|
|
289
|
-
logInfo('deleting orphan VHD', { vhdPath })
|
|
287
|
+
logInfo('deleting orphan VHD', { path: vhdPath })
|
|
290
288
|
deletions.push(VhdAbstract.unlink(handler, vhdPath))
|
|
291
289
|
}
|
|
292
290
|
}
|
|
@@ -337,7 +335,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
337
335
|
try {
|
|
338
336
|
metadata = JSON.parse(await handler.readFile(json))
|
|
339
337
|
} catch (error) {
|
|
340
|
-
logWarn('failed to read metadata
|
|
338
|
+
logWarn('failed to read backup metadata', { path: json, error })
|
|
341
339
|
jsons.delete(json)
|
|
342
340
|
return
|
|
343
341
|
}
|
|
@@ -348,9 +346,9 @@ exports.cleanVm = async function cleanVm(
|
|
|
348
346
|
if (xvas.has(linkedXva)) {
|
|
349
347
|
unusedXvas.delete(linkedXva)
|
|
350
348
|
} else {
|
|
351
|
-
logWarn('
|
|
349
|
+
logWarn('the XVA linked to the backup is missing', { backup: json, xva: linkedXva })
|
|
352
350
|
if (remove) {
|
|
353
|
-
logInfo('deleting incomplete backup', { json })
|
|
351
|
+
logInfo('deleting incomplete backup', { path: json })
|
|
354
352
|
jsons.delete(json)
|
|
355
353
|
await handler.unlink(json)
|
|
356
354
|
}
|
|
@@ -371,9 +369,9 @@ exports.cleanVm = async function cleanVm(
|
|
|
371
369
|
vhdsToJSons[path] = json
|
|
372
370
|
})
|
|
373
371
|
} else {
|
|
374
|
-
logWarn('some
|
|
372
|
+
logWarn('some VHDs linked to the backup are missing', { backup: json, missingVhds })
|
|
375
373
|
if (remove) {
|
|
376
|
-
logInfo('deleting incomplete backup', { json })
|
|
374
|
+
logInfo('deleting incomplete backup', { path: json })
|
|
377
375
|
jsons.delete(json)
|
|
378
376
|
await handler.unlink(json)
|
|
379
377
|
}
|
|
@@ -414,9 +412,9 @@ exports.cleanVm = async function cleanVm(
|
|
|
414
412
|
}
|
|
415
413
|
}
|
|
416
414
|
|
|
417
|
-
logWarn('unused VHD', { vhd })
|
|
415
|
+
logWarn('unused VHD', { path: vhd })
|
|
418
416
|
if (remove) {
|
|
419
|
-
logInfo('deleting unused VHD', { vhd })
|
|
417
|
+
logInfo('deleting unused VHD', { path: vhd })
|
|
420
418
|
unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
|
|
421
419
|
}
|
|
422
420
|
}
|
|
@@ -427,7 +425,13 @@ exports.cleanVm = async function cleanVm(
|
|
|
427
425
|
|
|
428
426
|
// merge interrupted VHDs
|
|
429
427
|
for (const parent of interruptedVhds.keys()) {
|
|
430
|
-
|
|
428
|
+
// before #6349 the chain wasn't in the mergeState
|
|
429
|
+
const { chain, statePath } = interruptedVhds.get(parent)
|
|
430
|
+
if (chain === undefined) {
|
|
431
|
+
vhdChainsToMerge[parent] = [parent, vhdChildren[parent]]
|
|
432
|
+
} else {
|
|
433
|
+
vhdChainsToMerge[parent] = chain.map(vhdPath => handlerPath.resolveFromFile(statePath, vhdPath))
|
|
434
|
+
}
|
|
431
435
|
}
|
|
432
436
|
|
|
433
437
|
Object.values(vhdChainsToMerge).forEach(chain => {
|
|
@@ -440,9 +444,9 @@ exports.cleanVm = async function cleanVm(
|
|
|
440
444
|
const metadataWithMergedVhd = {}
|
|
441
445
|
const doMerge = async () => {
|
|
442
446
|
await asyncMap(toMerge, async chain => {
|
|
443
|
-
const merged = await limitedMergeVhdChain(chain, {
|
|
447
|
+
const merged = await limitedMergeVhdChain(handler, chain, { logInfo, logWarn, remove, merge })
|
|
444
448
|
if (merged !== undefined) {
|
|
445
|
-
const metadataPath = vhdsToJSons[chain[
|
|
449
|
+
const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metada file
|
|
446
450
|
metadataWithMergedVhd[metadataPath] = true
|
|
447
451
|
}
|
|
448
452
|
})
|
|
@@ -485,7 +489,11 @@ exports.cleanVm = async function cleanVm(
|
|
|
485
489
|
if (mode === 'full') {
|
|
486
490
|
// a full backup : check size
|
|
487
491
|
const linkedXva = resolve('/', vmDir, xva)
|
|
488
|
-
|
|
492
|
+
try {
|
|
493
|
+
fileSystemSize = await handler.getSize(linkedXva)
|
|
494
|
+
} catch (error) {
|
|
495
|
+
// can fail with encrypted remote
|
|
496
|
+
}
|
|
489
497
|
} else if (mode === 'delta') {
|
|
490
498
|
const linkedVhds = Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
|
|
491
499
|
fileSystemSize = await computeVhdsSize(handler, linkedVhds)
|
|
@@ -497,11 +505,15 @@ exports.cleanVm = async function cleanVm(
|
|
|
497
505
|
|
|
498
506
|
// don't warn if the size has changed after a merge
|
|
499
507
|
if (!merged && fileSystemSize !== size) {
|
|
500
|
-
logWarn('incorrect size in metadata', {
|
|
508
|
+
logWarn('incorrect backup size in metadata', {
|
|
509
|
+
path: metadataPath,
|
|
510
|
+
actual: size ?? 'none',
|
|
511
|
+
expected: fileSystemSize,
|
|
512
|
+
})
|
|
501
513
|
}
|
|
502
514
|
}
|
|
503
515
|
} catch (error) {
|
|
504
|
-
logWarn('failed to get
|
|
516
|
+
logWarn('failed to get backup size', { backup: metadataPath, error })
|
|
505
517
|
return
|
|
506
518
|
}
|
|
507
519
|
|
|
@@ -511,7 +523,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
511
523
|
try {
|
|
512
524
|
await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
|
|
513
525
|
} catch (error) {
|
|
514
|
-
logWarn('
|
|
526
|
+
logWarn('failed to update backup size in metadata', { path: metadataPath, error })
|
|
515
527
|
}
|
|
516
528
|
}
|
|
517
529
|
})
|
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/merge-worker/cli.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"tmp": "^0.2.1"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@xen-orchestra/xapi": "^1.4.
|
|
49
|
+
"@xen-orchestra/xapi": "^1.4.1"
|
|
50
50
|
},
|
|
51
51
|
"license": "AGPL-3.0-or-later",
|
|
52
52
|
"author": {
|