@xen-orchestra/backups 0.14.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/Backup.js +263 -0
- package/DurablePartition.js +40 -0
- package/ImportVmBackup.js +66 -0
- package/README.md +28 -0
- package/RemoteAdapter.js +552 -0
- package/RestoreMetadataBackup.js +24 -0
- package/Task.js +151 -0
- package/_PoolMetadataBackup.js +75 -0
- package/_VmBackup.js +409 -0
- package/_XoMetadataBackup.js +62 -0
- package/_backupType.js +4 -0
- package/_backupWorker.js +155 -0
- package/_cancelableMap.js +20 -0
- package/_cleanVm.js +378 -0
- package/_deltaVm.js +347 -0
- package/_extractIdsFromSimplePattern.js +29 -0
- package/_filenameDate.js +6 -0
- package/_forkStreamUnpipe.js +28 -0
- package/_getOldEntries.js +4 -0
- package/_getTmpDir.js +20 -0
- package/_getVmBackupDir.js +6 -0
- package/_isValidXva.js +60 -0
- package/_listPartitions.js +52 -0
- package/_lvm.js +31 -0
- package/_watchStreamSize.js +7 -0
- package/formatVmBackups.js +34 -0
- package/merge-worker/cli.js +69 -0
- package/merge-worker/index.js +25 -0
- package/package.json +49 -0
- package/parseMetadataBackupId.js +23 -0
- package/runBackupWorker.js +38 -0
- package/writers/DeltaBackupWriter.js +221 -0
- package/writers/DeltaReplicationWriter.js +126 -0
- package/writers/FullBackupWriter.js +85 -0
- package/writers/FullReplicationWriter.js +88 -0
- package/writers/_AbstractDeltaWriter.js +26 -0
- package/writers/_AbstractFullWriter.js +12 -0
- package/writers/_AbstractWriter.js +10 -0
- package/writers/_MixinBackupWriter.js +51 -0
- package/writers/_MixinReplicationWriter.js +8 -0
- package/writers/_checkVhd.js +5 -0
- package/writers/_listReplicatedVms.js +30 -0
- package/writers/_packUuid.js +5 -0
package/_backupWorker.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
require('@xen-orchestra/log/configure.js').catchGlobalErrors(
|
|
2
|
+
require('@xen-orchestra/log').createLogger('xo:backups:worker')
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
const Disposable = require('promise-toolbox/Disposable.js')
|
|
6
|
+
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
|
|
7
|
+
const { compose } = require('@vates/compose')
|
|
8
|
+
const { createDebounceResource } = require('@vates/disposable/debounceResource.js')
|
|
9
|
+
const { deduped } = require('@vates/disposable/deduped.js')
|
|
10
|
+
const { getHandler } = require('@xen-orchestra/fs')
|
|
11
|
+
const { parseDuration } = require('@vates/parse-duration')
|
|
12
|
+
const { Xapi } = require('@xen-orchestra/xapi')
|
|
13
|
+
|
|
14
|
+
const { Backup } = require('./Backup.js')
|
|
15
|
+
const { RemoteAdapter } = require('./RemoteAdapter.js')
|
|
16
|
+
const { Task } = require('./Task.js')
|
|
17
|
+
|
|
18
|
+
class BackupWorker {
|
|
19
|
+
#config
|
|
20
|
+
#job
|
|
21
|
+
#recordToXapi
|
|
22
|
+
#remoteOptions
|
|
23
|
+
#remotes
|
|
24
|
+
#schedule
|
|
25
|
+
#xapiOptions
|
|
26
|
+
#xapis
|
|
27
|
+
|
|
28
|
+
constructor({ config, job, recordToXapi, remoteOptions, remotes, resourceCacheDelay, schedule, xapiOptions, xapis }) {
|
|
29
|
+
this.#config = config
|
|
30
|
+
this.#job = job
|
|
31
|
+
this.#recordToXapi = recordToXapi
|
|
32
|
+
this.#remoteOptions = remoteOptions
|
|
33
|
+
this.#remotes = remotes
|
|
34
|
+
this.#schedule = schedule
|
|
35
|
+
this.#xapiOptions = xapiOptions
|
|
36
|
+
this.#xapis = xapis
|
|
37
|
+
|
|
38
|
+
const debounceResource = createDebounceResource()
|
|
39
|
+
debounceResource.defaultDelay = parseDuration(resourceCacheDelay)
|
|
40
|
+
this.debounceResource = debounceResource
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
run() {
|
|
44
|
+
return new Backup({
|
|
45
|
+
config: this.#config,
|
|
46
|
+
getAdapter: remoteId => this.getAdapter(this.#remotes[remoteId]),
|
|
47
|
+
getConnectedRecord: Disposable.factory(async function* getConnectedRecord(type, uuid) {
|
|
48
|
+
const xapiId = this.#recordToXapi[uuid]
|
|
49
|
+
if (xapiId === undefined) {
|
|
50
|
+
throw new Error('no XAPI associated to ' + uuid)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const xapi = yield this.getXapi(this.#xapis[xapiId])
|
|
54
|
+
return xapi.getRecordByUuid(type, uuid)
|
|
55
|
+
}).bind(this),
|
|
56
|
+
job: this.#job,
|
|
57
|
+
schedule: this.#schedule,
|
|
58
|
+
}).run()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getAdapter = Disposable.factory(this.getAdapter)
|
|
62
|
+
getAdapter = deduped(this.getAdapter, remote => [remote.url])
|
|
63
|
+
getAdapter = compose(this.getAdapter, function (resource) {
|
|
64
|
+
return this.debounceResource(resource)
|
|
65
|
+
})
|
|
66
|
+
async *getAdapter(remote) {
|
|
67
|
+
const handler = getHandler(remote, this.#remoteOptions)
|
|
68
|
+
await handler.sync()
|
|
69
|
+
try {
|
|
70
|
+
yield new RemoteAdapter(handler, {
|
|
71
|
+
debounceResource: this.debounceResource,
|
|
72
|
+
dirMode: this.#config.dirMode,
|
|
73
|
+
})
|
|
74
|
+
} finally {
|
|
75
|
+
await handler.forget()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getXapi = Disposable.factory(this.getXapi)
|
|
80
|
+
getXapi = deduped(this.getXapi, ({ url }) => [url])
|
|
81
|
+
getXapi = compose(this.getXapi, function (resource) {
|
|
82
|
+
return this.debounceResource(resource)
|
|
83
|
+
})
|
|
84
|
+
async *getXapi({ credentials: { username: user, password }, ...opts }) {
|
|
85
|
+
const xapi = new Xapi({
|
|
86
|
+
...this.#xapiOptions,
|
|
87
|
+
...opts,
|
|
88
|
+
auth: {
|
|
89
|
+
user,
|
|
90
|
+
password,
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
await xapi.connect()
|
|
95
|
+
try {
|
|
96
|
+
await xapi.objectsFetched
|
|
97
|
+
|
|
98
|
+
yield xapi
|
|
99
|
+
} finally {
|
|
100
|
+
await xapi.disconnect()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Received message:
|
|
106
|
+
//
|
|
107
|
+
// Message {
|
|
108
|
+
// action: 'run'
|
|
109
|
+
// data: object
|
|
110
|
+
// runWithLogs: boolean
|
|
111
|
+
// }
|
|
112
|
+
//
|
|
113
|
+
// Sent message:
|
|
114
|
+
//
|
|
115
|
+
// Message {
|
|
116
|
+
// type: 'log' | 'result'
|
|
117
|
+
// data?: object
|
|
118
|
+
// status?: 'success' | 'failure'
|
|
119
|
+
// result?: any
|
|
120
|
+
// }
|
|
121
|
+
process.on('message', async message => {
|
|
122
|
+
if (message.action === 'run') {
|
|
123
|
+
const backupWorker = new BackupWorker(message.data)
|
|
124
|
+
try {
|
|
125
|
+
const result = message.runWithLogs
|
|
126
|
+
? await Task.run(
|
|
127
|
+
{
|
|
128
|
+
name: 'backup run',
|
|
129
|
+
onLog: data =>
|
|
130
|
+
process.send({
|
|
131
|
+
data,
|
|
132
|
+
type: 'log',
|
|
133
|
+
}),
|
|
134
|
+
},
|
|
135
|
+
() => backupWorker.run()
|
|
136
|
+
)
|
|
137
|
+
: await backupWorker.run()
|
|
138
|
+
|
|
139
|
+
process.send({
|
|
140
|
+
type: 'result',
|
|
141
|
+
result,
|
|
142
|
+
status: 'success',
|
|
143
|
+
})
|
|
144
|
+
} catch (error) {
|
|
145
|
+
process.send({
|
|
146
|
+
type: 'result',
|
|
147
|
+
result: error,
|
|
148
|
+
status: 'failure',
|
|
149
|
+
})
|
|
150
|
+
} finally {
|
|
151
|
+
await ignoreErrors.call(backupWorker.debounceResource.flushAll())
|
|
152
|
+
process.disconnect()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const cancelable = require('promise-toolbox/cancelable.js')
|
|
2
|
+
const CancelToken = require('promise-toolbox/CancelToken.js')
|
|
3
|
+
|
|
4
|
+
// Similar to `Promise.all` + `map` but pass a cancel token to the callback
|
|
5
|
+
//
|
|
6
|
+
// If any of the executions fails, the cancel token will be triggered and the
|
|
7
|
+
// first reason will be rejected.
|
|
8
|
+
exports.cancelableMap = cancelable(async function cancelableMap($cancelToken, iterable, callback) {
|
|
9
|
+
const { cancel, token } = CancelToken.source([$cancelToken])
|
|
10
|
+
try {
|
|
11
|
+
return await Promise.all(
|
|
12
|
+
Array.from(iterable, function (item) {
|
|
13
|
+
return callback.call(this, token, item)
|
|
14
|
+
})
|
|
15
|
+
)
|
|
16
|
+
} catch (error) {
|
|
17
|
+
await cancel()
|
|
18
|
+
throw error
|
|
19
|
+
}
|
|
20
|
+
})
|
package/_cleanVm.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
const assert = require('assert')
|
|
2
|
+
const sum = require('lodash/sum')
|
|
3
|
+
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
4
|
+
const { default: Vhd, mergeVhd } = require('vhd-lib')
|
|
5
|
+
const { dirname, resolve } = require('path')
|
|
6
|
+
const { DISK_TYPE_DIFFERENCING } = require('vhd-lib/dist/_constants.js')
|
|
7
|
+
const { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } = require('./_backupType.js')
|
|
8
|
+
const { limitConcurrency } = require('limit-concurrency-decorator')
|
|
9
|
+
|
|
10
|
+
const { Task } = require('./Task.js')
|
|
11
|
+
|
|
12
|
+
// chain is an array of VHDs from child to parent
|
|
13
|
+
//
|
|
14
|
+
// the whole chain will be merged into parent, parent will be renamed to child
|
|
15
|
+
// and all the others will deleted
|
|
16
|
+
async function mergeVhdChain(chain, { handler, onLog, remove, merge }) {
|
|
17
|
+
assert(chain.length >= 2)
|
|
18
|
+
|
|
19
|
+
let child = chain[0]
|
|
20
|
+
const parent = chain[chain.length - 1]
|
|
21
|
+
const children = chain.slice(0, -1).reverse()
|
|
22
|
+
|
|
23
|
+
chain
|
|
24
|
+
.slice(1)
|
|
25
|
+
.reverse()
|
|
26
|
+
.forEach(parent => {
|
|
27
|
+
onLog(`the parent ${parent} of the child ${child} is unused`)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
if (merge) {
|
|
31
|
+
// `mergeVhd` does not work with a stream, either
|
|
32
|
+
// - make it accept a stream
|
|
33
|
+
// - or create synthetic VHD which is not a stream
|
|
34
|
+
if (children.length !== 1) {
|
|
35
|
+
// TODO: implement merging multiple children
|
|
36
|
+
children.length = 1
|
|
37
|
+
child = children[0]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onLog(`merging ${child} into ${parent}`)
|
|
41
|
+
|
|
42
|
+
let done, total
|
|
43
|
+
const handle = setInterval(() => {
|
|
44
|
+
if (done !== undefined) {
|
|
45
|
+
onLog(`merging ${child}: ${done}/${total}`)
|
|
46
|
+
}
|
|
47
|
+
}, 10e3)
|
|
48
|
+
|
|
49
|
+
const mergedSize = await mergeVhd(
|
|
50
|
+
handler,
|
|
51
|
+
parent,
|
|
52
|
+
handler,
|
|
53
|
+
child,
|
|
54
|
+
// children.length === 1
|
|
55
|
+
// ? child
|
|
56
|
+
// : await createSyntheticStream(handler, children),
|
|
57
|
+
{
|
|
58
|
+
onProgress({ done: d, total: t }) {
|
|
59
|
+
done = d
|
|
60
|
+
total = t
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
clearInterval(handle)
|
|
66
|
+
|
|
67
|
+
await Promise.all([
|
|
68
|
+
handler.rename(parent, child),
|
|
69
|
+
asyncMap(children.slice(0, -1), child => {
|
|
70
|
+
onLog(`the VHD ${child} is unused`)
|
|
71
|
+
if (remove) {
|
|
72
|
+
onLog(`deleting unused VHD ${child}`)
|
|
73
|
+
return handler.unlink(child)
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
return mergedSize
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const noop = Function.prototype
|
|
83
|
+
|
|
84
|
+
const INTERRUPTED_VHDS_REG = /^(?:(.+)\/)?\.(.+)\.merge.json$/
|
|
85
|
+
const listVhds = async (handler, vmDir) => {
|
|
86
|
+
const vhds = []
|
|
87
|
+
const interruptedVhds = new Set()
|
|
88
|
+
|
|
89
|
+
await asyncMap(
|
|
90
|
+
await handler.list(`${vmDir}/vdis`, {
|
|
91
|
+
ignoreMissing: true,
|
|
92
|
+
prependDir: true,
|
|
93
|
+
}),
|
|
94
|
+
async jobDir =>
|
|
95
|
+
asyncMap(
|
|
96
|
+
await handler.list(jobDir, {
|
|
97
|
+
prependDir: true,
|
|
98
|
+
}),
|
|
99
|
+
async vdiDir => {
|
|
100
|
+
const list = await handler.list(vdiDir, {
|
|
101
|
+
filter: file => isVhdFile(file) || INTERRUPTED_VHDS_REG.test(file),
|
|
102
|
+
prependDir: true,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
list.forEach(file => {
|
|
106
|
+
const res = INTERRUPTED_VHDS_REG.exec(file)
|
|
107
|
+
if (res === null) {
|
|
108
|
+
vhds.push(file)
|
|
109
|
+
} else {
|
|
110
|
+
const [, dir, file] = res
|
|
111
|
+
interruptedVhds.add(`${dir}/${file}`)
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return { vhds, interruptedVhds }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const defaultMergeLimiter = limitConcurrency(1)
|
|
122
|
+
|
|
123
|
+
exports.cleanVm = async function cleanVm(
|
|
124
|
+
vmDir,
|
|
125
|
+
{ fixMetadata, remove, merge, mergeLimiter = defaultMergeLimiter, onLog = noop }
|
|
126
|
+
) {
|
|
127
|
+
const handler = this._handler
|
|
128
|
+
|
|
129
|
+
const vhds = new Set()
|
|
130
|
+
const vhdParents = { __proto__: null }
|
|
131
|
+
const vhdChildren = { __proto__: null }
|
|
132
|
+
|
|
133
|
+
const vhdsList = await listVhds(handler, vmDir)
|
|
134
|
+
|
|
135
|
+
// remove broken VHDs
|
|
136
|
+
await asyncMap(vhdsList.vhds, async path => {
|
|
137
|
+
try {
|
|
138
|
+
const vhd = new Vhd(handler, path)
|
|
139
|
+
await vhd.readHeaderAndFooter(!vhdsList.interruptedVhds.has(path))
|
|
140
|
+
vhds.add(path)
|
|
141
|
+
if (vhd.footer.diskType === DISK_TYPE_DIFFERENCING) {
|
|
142
|
+
const parent = resolve('/', dirname(path), vhd.header.parentUnicodeName)
|
|
143
|
+
vhdParents[path] = parent
|
|
144
|
+
if (parent in vhdChildren) {
|
|
145
|
+
const error = new Error('this script does not support multiple VHD children')
|
|
146
|
+
error.parent = parent
|
|
147
|
+
error.child1 = vhdChildren[parent]
|
|
148
|
+
error.child2 = path
|
|
149
|
+
throw error // should we throw?
|
|
150
|
+
}
|
|
151
|
+
vhdChildren[parent] = path
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
onLog(`error while checking the VHD with path ${path}`, { error })
|
|
155
|
+
if (error?.code === 'ERR_ASSERTION' && remove) {
|
|
156
|
+
onLog(`deleting broken ${path}`)
|
|
157
|
+
await handler.unlink(path)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// remove VHDs with missing ancestors
|
|
163
|
+
{
|
|
164
|
+
const deletions = []
|
|
165
|
+
|
|
166
|
+
// return true if the VHD has been deleted or is missing
|
|
167
|
+
const deleteIfOrphan = vhd => {
|
|
168
|
+
const parent = vhdParents[vhd]
|
|
169
|
+
if (parent === undefined) {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// no longer needs to be checked
|
|
174
|
+
delete vhdParents[vhd]
|
|
175
|
+
|
|
176
|
+
deleteIfOrphan(parent)
|
|
177
|
+
|
|
178
|
+
if (!vhds.has(parent)) {
|
|
179
|
+
vhds.delete(vhd)
|
|
180
|
+
|
|
181
|
+
onLog(`the parent ${parent} of the VHD ${vhd} is missing`)
|
|
182
|
+
if (remove) {
|
|
183
|
+
onLog(`deleting orphan VHD ${vhd}`)
|
|
184
|
+
deletions.push(handler.unlink(vhd))
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// > A property that is deleted before it has been visited will not be
|
|
190
|
+
// > visited later.
|
|
191
|
+
// >
|
|
192
|
+
// > -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#Deleted_added_or_modified_properties
|
|
193
|
+
for (const child in vhdParents) {
|
|
194
|
+
deleteIfOrphan(child)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await Promise.all(deletions)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const jsons = []
|
|
201
|
+
const xvas = new Set()
|
|
202
|
+
const xvaSums = []
|
|
203
|
+
const entries = await handler.list(vmDir, {
|
|
204
|
+
prependDir: true,
|
|
205
|
+
})
|
|
206
|
+
entries.forEach(path => {
|
|
207
|
+
if (isMetadataFile(path)) {
|
|
208
|
+
jsons.push(path)
|
|
209
|
+
} else if (isXvaFile(path)) {
|
|
210
|
+
xvas.add(path)
|
|
211
|
+
} else if (isXvaSumFile(path)) {
|
|
212
|
+
xvaSums.push(path)
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
await asyncMap(xvas, async path => {
|
|
217
|
+
// check is not good enough to delete the file, the best we can do is report
|
|
218
|
+
// it
|
|
219
|
+
if (!(await this.isValidXva(path))) {
|
|
220
|
+
onLog(`the XVA with path ${path} is potentially broken`)
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const unusedVhds = new Set(vhds)
|
|
225
|
+
const unusedXvas = new Set(xvas)
|
|
226
|
+
|
|
227
|
+
// compile the list of unused XVAs and VHDs, and remove backup metadata which
|
|
228
|
+
// reference a missing XVA/VHD
|
|
229
|
+
await asyncMap(jsons, async json => {
|
|
230
|
+
const metadata = JSON.parse(await handler.readFile(json))
|
|
231
|
+
const { mode } = metadata
|
|
232
|
+
let size
|
|
233
|
+
if (mode === 'full') {
|
|
234
|
+
const linkedXva = resolve('/', vmDir, metadata.xva)
|
|
235
|
+
|
|
236
|
+
if (xvas.has(linkedXva)) {
|
|
237
|
+
unusedXvas.delete(linkedXva)
|
|
238
|
+
|
|
239
|
+
size = await handler.getSize(linkedXva).catch(error => {
|
|
240
|
+
onLog(`failed to get size of ${json}`, { error })
|
|
241
|
+
})
|
|
242
|
+
} else {
|
|
243
|
+
onLog(`the XVA linked to the metadata ${json} is missing`)
|
|
244
|
+
if (remove) {
|
|
245
|
+
onLog(`deleting incomplete backup ${json}`)
|
|
246
|
+
await handler.unlink(json)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} else if (mode === 'delta') {
|
|
250
|
+
const linkedVhds = (() => {
|
|
251
|
+
const { vhds } = metadata
|
|
252
|
+
return Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
|
|
253
|
+
})()
|
|
254
|
+
|
|
255
|
+
// FIXME: find better approach by keeping as much of the backup as
|
|
256
|
+
// possible (existing disks) even if one disk is missing
|
|
257
|
+
if (linkedVhds.every(_ => vhds.has(_))) {
|
|
258
|
+
linkedVhds.forEach(_ => unusedVhds.delete(_))
|
|
259
|
+
|
|
260
|
+
size = await asyncMap(linkedVhds, vhd => handler.getSize(vhd)).then(sum, error => {
|
|
261
|
+
onLog(`failed to get size of ${json}`, { error })
|
|
262
|
+
})
|
|
263
|
+
} else {
|
|
264
|
+
onLog(`Some VHDs linked to the metadata ${json} are missing`)
|
|
265
|
+
if (remove) {
|
|
266
|
+
onLog(`deleting incomplete backup ${json}`)
|
|
267
|
+
await handler.unlink(json)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const metadataSize = metadata.size
|
|
273
|
+
if (size !== undefined && metadataSize !== size) {
|
|
274
|
+
onLog(`incorrect size in metadata: ${metadataSize ?? 'none'} instead of ${size}`)
|
|
275
|
+
|
|
276
|
+
// don't update if the the stored size is greater than found files,
|
|
277
|
+
// it can indicates a problem
|
|
278
|
+
if (fixMetadata && (metadataSize === undefined || metadataSize < size)) {
|
|
279
|
+
try {
|
|
280
|
+
metadata.size = size
|
|
281
|
+
await handler.writeFile(json, JSON.stringify(metadata), { flags: 'w' })
|
|
282
|
+
} catch (error) {
|
|
283
|
+
onLog(`failed to update size in backup metadata ${json}`, { error })
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// TODO: parallelize by vm/job/vdi
|
|
290
|
+
const unusedVhdsDeletion = []
|
|
291
|
+
const toMerge = []
|
|
292
|
+
{
|
|
293
|
+
// VHD chains (as list from child to ancestor) to merge indexed by last
|
|
294
|
+
// ancestor
|
|
295
|
+
const vhdChainsToMerge = { __proto__: null }
|
|
296
|
+
|
|
297
|
+
const toCheck = new Set(unusedVhds)
|
|
298
|
+
|
|
299
|
+
const getUsedChildChainOrDelete = vhd => {
|
|
300
|
+
if (vhd in vhdChainsToMerge) {
|
|
301
|
+
const chain = vhdChainsToMerge[vhd]
|
|
302
|
+
delete vhdChainsToMerge[vhd]
|
|
303
|
+
return chain
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!unusedVhds.has(vhd)) {
|
|
307
|
+
return [vhd]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// no longer needs to be checked
|
|
311
|
+
toCheck.delete(vhd)
|
|
312
|
+
|
|
313
|
+
const child = vhdChildren[vhd]
|
|
314
|
+
if (child !== undefined) {
|
|
315
|
+
const chain = getUsedChildChainOrDelete(child)
|
|
316
|
+
if (chain !== undefined) {
|
|
317
|
+
chain.push(vhd)
|
|
318
|
+
return chain
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
onLog(`the VHD ${vhd} is unused`)
|
|
323
|
+
if (remove) {
|
|
324
|
+
onLog(`deleting unused VHD ${vhd}`)
|
|
325
|
+
unusedVhdsDeletion.push(handler.unlink(vhd))
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
toCheck.forEach(vhd => {
|
|
330
|
+
vhdChainsToMerge[vhd] = getUsedChildChainOrDelete(vhd)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
// merge interrupted VHDs
|
|
334
|
+
vhdsList.interruptedVhds.forEach(parent => {
|
|
335
|
+
vhdChainsToMerge[parent] = [vhdChildren[parent], parent]
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
Object.values(vhdChainsToMerge).forEach(chain => {
|
|
339
|
+
if (chain !== undefined) {
|
|
340
|
+
toMerge.push(chain)
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const doMerge = () => {
|
|
346
|
+
const promise = asyncMap(toMerge, async chain => {
|
|
347
|
+
mergeVhdChain(chain, { handler, onLog, remove, merge })
|
|
348
|
+
})
|
|
349
|
+
return merge ? promise.then(sizes => ({ size: sum(sizes) })) : promise
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
await Promise.all([
|
|
353
|
+
...unusedVhdsDeletion,
|
|
354
|
+
toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : doMerge()),
|
|
355
|
+
asyncMap(unusedXvas, path => {
|
|
356
|
+
onLog(`the XVA ${path} is unused`)
|
|
357
|
+
if (remove) {
|
|
358
|
+
onLog(`deleting unused XVA ${path}`)
|
|
359
|
+
return handler.unlink(path)
|
|
360
|
+
}
|
|
361
|
+
}),
|
|
362
|
+
asyncMap(xvaSums, path => {
|
|
363
|
+
// no need to handle checksums for XVAs deleted by the script, they will be handled by `unlink()`
|
|
364
|
+
if (!xvas.has(path.slice(0, -'.checksum'.length))) {
|
|
365
|
+
onLog(`the XVA checksum ${path} is unused`)
|
|
366
|
+
if (remove) {
|
|
367
|
+
onLog(`deleting unused XVA checksum ${path}`)
|
|
368
|
+
return handler.unlink(path)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}),
|
|
372
|
+
])
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
// boolean whether some VHDs were merged (or should be merged)
|
|
376
|
+
merge: toMerge.length !== 0,
|
|
377
|
+
}
|
|
378
|
+
}
|