2021-03-24 10:00:47 +01:00
|
|
|
const assert = require('assert')
|
|
|
|
|
const limitConcurrency = require('limit-concurrency-decorator').default
|
|
|
|
|
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
|
|
|
const { default: Vhd, mergeVhd } = require('vhd-lib')
|
|
|
|
|
const { dirname, resolve } = require('path')
|
2021-04-22 13:43:57 +02:00
|
|
|
const { DISK_TYPE_DIFFERENCING } = require('vhd-lib/dist/_constants.js')
|
2021-04-22 13:12:14 +02:00
|
|
|
const { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } = require('./_backupType.js')
|
2021-03-24 10:00:47 +01:00
|
|
|
|
|
|
|
|
// chain is an array of VHDs from child to parent
|
|
|
|
|
//
|
|
|
|
|
// the whole chain will be merged into parent, parent will be renamed to child
|
|
|
|
|
// and all the others will deleted
|
|
|
|
|
const mergeVhdChain = limitConcurrency(1)(async function mergeVhdChain(chain, { handler, onLog, remove, merge }) {
|
|
|
|
|
assert(chain.length >= 2)
|
|
|
|
|
|
|
|
|
|
let child = chain[0]
|
|
|
|
|
const parent = chain[chain.length - 1]
|
|
|
|
|
const children = chain.slice(0, -1).reverse()
|
|
|
|
|
|
|
|
|
|
chain
|
|
|
|
|
.slice(1)
|
|
|
|
|
.reverse()
|
|
|
|
|
.forEach(parent => {
|
|
|
|
|
onLog(`the parent ${parent} of the child ${child} is unused`)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (merge) {
|
|
|
|
|
// `mergeVhd` does not work with a stream, either
|
|
|
|
|
// - make it accept a stream
|
|
|
|
|
// - or create synthetic VHD which is not a stream
|
|
|
|
|
if (children.length !== 1) {
|
|
|
|
|
// TODO: implement merging multiple children
|
|
|
|
|
children.length = 1
|
|
|
|
|
child = children[0]
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-28 10:53:40 +02:00
|
|
|
onLog(`merging ${child} into ${parent}`)
|
|
|
|
|
|
2021-03-24 10:00:47 +01:00
|
|
|
let done, total
|
|
|
|
|
const handle = setInterval(() => {
|
|
|
|
|
if (done !== undefined) {
|
|
|
|
|
onLog(`merging ${child}: ${done}/${total}`)
|
|
|
|
|
}
|
|
|
|
|
}, 10e3)
|
|
|
|
|
|
|
|
|
|
await mergeVhd(
|
|
|
|
|
handler,
|
|
|
|
|
parent,
|
|
|
|
|
handler,
|
|
|
|
|
child,
|
|
|
|
|
// children.length === 1
|
|
|
|
|
// ? child
|
|
|
|
|
// : await createSyntheticStream(handler, children),
|
|
|
|
|
{
|
|
|
|
|
onProgress({ done: d, total: t }) {
|
|
|
|
|
done = d
|
|
|
|
|
total = t
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
clearInterval(handle)
|
|
|
|
|
|
2021-04-28 15:06:22 +02:00
|
|
|
await Promise.all([
|
|
|
|
|
handler.rename(parent, child),
|
|
|
|
|
asyncMap(children.slice(0, -1), child => {
|
|
|
|
|
onLog(`the VHD ${child} is unused`)
|
|
|
|
|
if (remove) {
|
|
|
|
|
onLog(`deleting unused VHD ${child}`)
|
|
|
|
|
return handler.unlink(child)
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
])
|
|
|
|
|
}
|
2021-03-24 10:00:47 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const noop = Function.prototype
|
|
|
|
|
|
2021-04-13 16:09:42 +02:00
|
|
|
const listVhds = async (handler, vmDir) => {
|
|
|
|
|
const vhds = []
|
|
|
|
|
await asyncMap(
|
|
|
|
|
await handler.list(`${vmDir}/vdis`, {
|
|
|
|
|
prependDir: true,
|
|
|
|
|
}),
|
|
|
|
|
async jobDir =>
|
|
|
|
|
asyncMap(
|
|
|
|
|
await handler.list(jobDir, {
|
|
|
|
|
prependDir: true,
|
|
|
|
|
}),
|
|
|
|
|
async vdiDir =>
|
|
|
|
|
vhds.push(
|
|
|
|
|
...(await handler.list(vdiDir, {
|
|
|
|
|
filter: isVhdFile,
|
|
|
|
|
prependDir: true,
|
|
|
|
|
}))
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return vhds
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-24 10:00:47 +01:00
|
|
|
exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop }) {
|
|
|
|
|
const handler = this._handler
|
|
|
|
|
|
|
|
|
|
const vhds = new Set()
|
|
|
|
|
const vhdParents = { __proto__: null }
|
|
|
|
|
const vhdChildren = { __proto__: null }
|
|
|
|
|
|
|
|
|
|
// remove broken VHDs
|
2021-04-13 16:09:42 +02:00
|
|
|
await asyncMap(await listVhds(handler, vmDir), async path => {
|
|
|
|
|
try {
|
|
|
|
|
const vhd = new Vhd(handler, path)
|
|
|
|
|
await vhd.readHeaderAndFooter()
|
|
|
|
|
vhds.add(path)
|
|
|
|
|
if (vhd.footer.diskType === DISK_TYPE_DIFFERENCING) {
|
2021-04-20 10:35:28 +02:00
|
|
|
const parent = resolve('/', dirname(path), vhd.header.parentUnicodeName)
|
2021-04-13 16:09:42 +02:00
|
|
|
vhdParents[path] = parent
|
|
|
|
|
if (parent in vhdChildren) {
|
|
|
|
|
const error = new Error('this script does not support multiple VHD children')
|
|
|
|
|
error.parent = parent
|
|
|
|
|
error.child1 = vhdChildren[parent]
|
|
|
|
|
error.child2 = path
|
|
|
|
|
throw error // should we throw?
|
2021-03-24 10:00:47 +01:00
|
|
|
}
|
2021-04-13 16:09:42 +02:00
|
|
|
vhdChildren[parent] = path
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2021-04-28 10:53:40 +02:00
|
|
|
onLog(`error while checking the VHD with path ${path}`, { error })
|
2021-04-13 16:09:42 +02:00
|
|
|
if (error?.code === 'ERR_ASSERTION' && remove) {
|
2021-04-28 10:53:40 +02:00
|
|
|
onLog(`deleting broken ${path}`)
|
2021-04-13 16:09:42 +02:00
|
|
|
await handler.unlink(path)
|
2021-03-24 10:00:47 +01:00
|
|
|
}
|
|
|
|
|
}
|
2021-04-13 16:09:42 +02:00
|
|
|
})
|
2021-03-24 10:00:47 +01:00
|
|
|
|
|
|
|
|
// remove VHDs with missing ancestors
|
|
|
|
|
{
|
|
|
|
|
const deletions = []
|
|
|
|
|
|
|
|
|
|
// return true if the VHD has been deleted or is missing
|
|
|
|
|
const deleteIfOrphan = vhd => {
|
|
|
|
|
const parent = vhdParents[vhd]
|
|
|
|
|
if (parent === undefined) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// no longer needs to be checked
|
|
|
|
|
delete vhdParents[vhd]
|
|
|
|
|
|
|
|
|
|
deleteIfOrphan(parent)
|
|
|
|
|
|
|
|
|
|
if (!vhds.has(parent)) {
|
|
|
|
|
vhds.delete(vhd)
|
|
|
|
|
|
|
|
|
|
onLog(`the parent ${parent} of the VHD ${vhd} is missing`)
|
|
|
|
|
if (remove) {
|
2021-04-28 10:53:40 +02:00
|
|
|
onLog(`deleting orphan VHD ${vhd}`)
|
2021-03-24 10:00:47 +01:00
|
|
|
deletions.push(handler.unlink(vhd))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// > A property that is deleted before it has been visited will not be
|
|
|
|
|
// > visited later.
|
|
|
|
|
// >
|
|
|
|
|
// > -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#Deleted_added_or_modified_properties
|
|
|
|
|
for (const child in vhdParents) {
|
|
|
|
|
deleteIfOrphan(child)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Promise.all(deletions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const jsons = []
|
|
|
|
|
const xvas = new Set()
|
|
|
|
|
const xvaSums = []
|
|
|
|
|
const entries = await handler.list(vmDir, {
|
|
|
|
|
prependDir: true,
|
|
|
|
|
})
|
|
|
|
|
entries.forEach(path => {
|
|
|
|
|
if (isMetadataFile(path)) {
|
|
|
|
|
jsons.push(path)
|
|
|
|
|
} else if (isXvaFile(path)) {
|
|
|
|
|
xvas.add(path)
|
|
|
|
|
} else if (isXvaSumFile(path)) {
|
|
|
|
|
xvaSums.push(path)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await asyncMap(xvas, async path => {
|
|
|
|
|
// check is not good enough to delete the file, the best we can do is report
|
|
|
|
|
// it
|
2021-04-21 16:27:13 +02:00
|
|
|
if (!(await this.isValidXva(path))) {
|
2021-03-24 10:00:47 +01:00
|
|
|
onLog(`the XVA with path ${path} is potentially broken`)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const unusedVhds = new Set(vhds)
|
|
|
|
|
const unusedXvas = new Set(xvas)
|
|
|
|
|
|
|
|
|
|
// compile the list of unused XVAs and VHDs, and remove backup metadata which
|
|
|
|
|
// reference a missing XVA/VHD
|
|
|
|
|
await asyncMap(jsons, async json => {
|
|
|
|
|
const metadata = JSON.parse(await handler.readFile(json))
|
|
|
|
|
const { mode } = metadata
|
|
|
|
|
if (mode === 'full') {
|
2021-04-20 10:35:28 +02:00
|
|
|
const linkedXva = resolve('/', vmDir, metadata.xva)
|
2021-03-24 10:00:47 +01:00
|
|
|
|
|
|
|
|
if (xvas.has(linkedXva)) {
|
|
|
|
|
unusedXvas.delete(linkedXva)
|
|
|
|
|
} else {
|
|
|
|
|
onLog(`the XVA linked to the metadata ${json} is missing`)
|
|
|
|
|
if (remove) {
|
2021-04-28 10:53:40 +02:00
|
|
|
onLog(`deleting incomplete backup ${json}`)
|
2021-03-24 10:00:47 +01:00
|
|
|
await handler.unlink(json)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (mode === 'delta') {
|
|
|
|
|
const linkedVhds = (() => {
|
|
|
|
|
const { vhds } = metadata
|
2021-04-20 10:35:28 +02:00
|
|
|
return Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
|
2021-03-24 10:00:47 +01:00
|
|
|
})()
|
|
|
|
|
|
|
|
|
|
// FIXME: find better approach by keeping as much of the backup as
|
|
|
|
|
// possible (existing disks) even if one disk is missing
|
|
|
|
|
if (linkedVhds.every(_ => vhds.has(_))) {
|
|
|
|
|
linkedVhds.forEach(_ => unusedVhds.delete(_))
|
|
|
|
|
} else {
|
|
|
|
|
onLog(`Some VHDs linked to the metadata ${json} are missing`)
|
|
|
|
|
if (remove) {
|
2021-04-28 10:53:40 +02:00
|
|
|
onLog(`deleting incomplete backup ${json}`)
|
2021-03-24 10:00:47 +01:00
|
|
|
await handler.unlink(json)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// TODO: parallelize by vm/job/vdi
|
|
|
|
|
const unusedVhdsDeletion = []
|
|
|
|
|
{
|
|
|
|
|
// VHD chains (as list from child to ancestor) to merge indexed by last
|
|
|
|
|
// ancestor
|
|
|
|
|
const vhdChainsToMerge = { __proto__: null }
|
|
|
|
|
|
|
|
|
|
const toCheck = new Set(unusedVhds)
|
|
|
|
|
|
|
|
|
|
const getUsedChildChainOrDelete = vhd => {
|
|
|
|
|
if (vhd in vhdChainsToMerge) {
|
|
|
|
|
const chain = vhdChainsToMerge[vhd]
|
|
|
|
|
delete vhdChainsToMerge[vhd]
|
|
|
|
|
return chain
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!unusedVhds.has(vhd)) {
|
|
|
|
|
return [vhd]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// no longer needs to be checked
|
|
|
|
|
toCheck.delete(vhd)
|
|
|
|
|
|
|
|
|
|
const child = vhdChildren[vhd]
|
|
|
|
|
if (child !== undefined) {
|
|
|
|
|
const chain = getUsedChildChainOrDelete(child)
|
|
|
|
|
if (chain !== undefined) {
|
|
|
|
|
chain.push(vhd)
|
|
|
|
|
return chain
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onLog(`the VHD ${vhd} is unused`)
|
|
|
|
|
if (remove) {
|
2021-04-28 10:53:40 +02:00
|
|
|
onLog(`deleting unused VHD ${vhd}`)
|
2021-03-24 10:00:47 +01:00
|
|
|
unusedVhdsDeletion.push(handler.unlink(vhd))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toCheck.forEach(vhd => {
|
|
|
|
|
vhdChainsToMerge[vhd] = getUsedChildChainOrDelete(vhd)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
Object.keys(vhdChainsToMerge).forEach(key => {
|
|
|
|
|
const chain = vhdChainsToMerge[key]
|
|
|
|
|
if (chain !== undefined) {
|
2021-04-28 18:21:23 +02:00
|
|
|
unusedVhdsDeletion.push(mergeVhdChain(chain, { handler, onLog, remove, merge }))
|
2021-03-24 10:00:47 +01:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Promise.all([
|
2021-04-28 18:20:01 +02:00
|
|
|
...unusedVhdsDeletion,
|
2021-03-24 10:00:47 +01:00
|
|
|
asyncMap(unusedXvas, path => {
|
|
|
|
|
onLog(`the XVA ${path} is unused`)
|
2021-04-28 10:53:40 +02:00
|
|
|
if (remove) {
|
|
|
|
|
onLog(`deleting unused XVA ${path}`)
|
|
|
|
|
return handler.unlink(path)
|
|
|
|
|
}
|
2021-03-24 10:00:47 +01:00
|
|
|
}),
|
|
|
|
|
asyncMap(xvaSums, path => {
|
|
|
|
|
// no need to handle checksums for XVAs deleted by the script, they will be handled by `unlink()`
|
|
|
|
|
if (!xvas.has(path.slice(0, -'.checksum'.length))) {
|
|
|
|
|
onLog(`the XVA checksum ${path} is unused`)
|
2021-04-28 10:53:40 +02:00
|
|
|
if (remove) {
|
|
|
|
|
onLog(`deleting unused XVA checksum ${path}`)
|
|
|
|
|
return handler.unlink(path)
|
|
|
|
|
}
|
2021-03-24 10:00:47 +01:00
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
])
|
|
|
|
|
}
|