2022-02-22 12:29:26 +01:00
|
|
|
'use strict'
|
|
|
|
|
|
2021-06-22 18:16:22 +02:00
|
|
|
const sum = require('lodash/sum')
|
2022-07-07 16:57:15 +02:00
|
|
|
const UUID = require('uuid')
|
2021-03-24 10:00:47 +01:00
|
|
|
const { asyncMap } = require('@xen-orchestra/async-map')
|
2022-06-29 17:16:43 +02:00
|
|
|
const { Constants, openVhd, VhdAbstract, VhdFile } = require('vhd-lib')
|
2022-01-18 15:33:31 +01:00
|
|
|
const { isVhdAlias, resolveVhdAlias } = require('vhd-lib/aliases')
|
2021-03-24 10:00:47 +01:00
|
|
|
const { dirname, resolve } = require('path')
|
2021-11-25 17:53:20 +01:00
|
|
|
const { DISK_TYPES } = Constants
|
2021-04-22 13:12:14 +02:00
|
|
|
const { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } = require('./_backupType.js')
|
2021-05-19 15:02:57 +02:00
|
|
|
const { limitConcurrency } = require('limit-concurrency-decorator')
|
2022-06-29 18:39:20 +02:00
|
|
|
const { mergeVhdChain } = require('vhd-lib/merge')
|
2021-03-24 10:00:47 +01:00
|
|
|
|
2021-10-13 15:10:35 +02:00
|
|
|
const { Task } = require('./Task.js')
|
2021-11-22 17:14:29 +01:00
|
|
|
const { Disposable } = require('promise-toolbox')
|
2022-08-03 17:53:59 +02:00
|
|
|
const handlerPath = require('@xen-orchestra/fs/path')
|
2021-10-13 15:10:35 +02:00
|
|
|
|
2021-12-20 14:57:54 +01:00
|
|
|
// checking the size of a vhd directory is costly
|
|
|
|
|
// 1 Http Query per 1000 blocks
|
|
|
|
|
// we only check size of all the vhd are VhdFiles
|
2022-08-23 12:04:16 +02:00
|
|
|
function shouldComputeVhdsSize(handler, vhds) {
|
|
|
|
|
if (handler.isEncrypted) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2021-12-20 14:57:54 +01:00
|
|
|
return vhds.every(vhd => vhd instanceof VhdFile)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const computeVhdsSize = (handler, vhdPaths) =>
|
|
|
|
|
Disposable.use(
|
|
|
|
|
vhdPaths.map(vhdPath => openVhd(handler, vhdPath)),
|
|
|
|
|
async vhds => {
|
2022-08-23 12:04:16 +02:00
|
|
|
if (shouldComputeVhdsSize(handler, vhds)) {
|
2021-12-20 14:57:54 +01:00
|
|
|
const sizes = await asyncMap(vhds, vhd => vhd.getSize())
|
|
|
|
|
return sum(sizes)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2022-06-29 18:39:20 +02:00
|
|
|
// chain is [ ancestor, child_1, ..., child_n ]
|
2022-09-16 14:54:33 +02:00
|
|
|
async function _mergeVhdChain(handler, chain, { logInfo, remove, merge, mergeBlockConcurrency }) {
|
2021-03-24 10:00:47 +01:00
|
|
|
if (merge) {
|
2022-06-29 18:39:20 +02:00
|
|
|
logInfo(`merging VHD chain`, { chain })
|
2021-04-28 10:53:40 +02:00
|
|
|
|
2021-03-24 10:00:47 +01:00
|
|
|
let done, total
|
|
|
|
|
const handle = setInterval(() => {
|
|
|
|
|
if (done !== undefined) {
|
2022-07-28 17:53:54 +02:00
|
|
|
logInfo('merge in progress', {
|
|
|
|
|
done,
|
2022-06-29 18:39:20 +02:00
|
|
|
parent: chain[0],
|
2022-07-28 17:53:54 +02:00
|
|
|
progress: Math.round((100 * done) / total),
|
|
|
|
|
total,
|
|
|
|
|
})
|
2021-03-24 10:00:47 +01:00
|
|
|
}
|
|
|
|
|
}, 10e3)
|
2022-06-29 18:39:20 +02:00
|
|
|
try {
|
|
|
|
|
return await mergeVhdChain(handler, chain, {
|
|
|
|
|
logInfo,
|
2022-09-16 14:54:33 +02:00
|
|
|
mergeBlockConcurrency,
|
2022-06-29 18:39:20 +02:00
|
|
|
onProgress({ done: d, total: t }) {
|
|
|
|
|
done = d
|
|
|
|
|
total = t
|
|
|
|
|
},
|
|
|
|
|
removeUnused: remove,
|
|
|
|
|
})
|
|
|
|
|
} finally {
|
|
|
|
|
clearInterval(handle)
|
|
|
|
|
}
|
2021-04-28 15:06:22 +02:00
|
|
|
}
|
2021-10-12 09:17:42 +02:00
|
|
|
}
|
2021-03-24 10:00:47 +01:00
|
|
|
|
|
|
|
|
const noop = Function.prototype
|
|
|
|
|
|
2022-01-11 15:09:01 +01:00
|
|
|
const INTERRUPTED_VHDS_REG = /^\.(.+)\.merge.json$/
|
2022-08-03 17:53:59 +02:00
|
|
|
const listVhds = async (handler, vmDir, logWarn) => {
|
2022-01-11 15:31:56 +01:00
|
|
|
const vhds = new Set()
|
2022-01-13 16:07:28 +01:00
|
|
|
const aliases = {}
|
2022-01-13 10:41:39 +01:00
|
|
|
const interruptedVhds = new Map()
|
2021-04-30 11:43:21 +02:00
|
|
|
|
2021-04-13 16:09:42 +02:00
|
|
|
await asyncMap(
|
|
|
|
|
await handler.list(`${vmDir}/vdis`, {
|
2021-05-02 10:35:06 +02:00
|
|
|
ignoreMissing: true,
|
2021-04-13 16:09:42 +02:00
|
|
|
prependDir: true,
|
|
|
|
|
}),
|
|
|
|
|
async jobDir =>
|
|
|
|
|
asyncMap(
|
|
|
|
|
await handler.list(jobDir, {
|
|
|
|
|
prependDir: true,
|
|
|
|
|
}),
|
2021-04-30 11:43:21 +02:00
|
|
|
async vdiDir => {
|
|
|
|
|
const list = await handler.list(vdiDir, {
|
|
|
|
|
filter: file => isVhdFile(file) || INTERRUPTED_VHDS_REG.test(file),
|
|
|
|
|
})
|
2022-01-18 10:07:56 +01:00
|
|
|
aliases[vdiDir] = list.filter(vhd => isVhdAlias(vhd)).map(file => `${vdiDir}/${file}`)
|
2022-08-03 17:53:59 +02:00
|
|
|
|
|
|
|
|
await asyncMap(list, async file => {
|
2021-04-30 11:43:21 +02:00
|
|
|
const res = INTERRUPTED_VHDS_REG.exec(file)
|
|
|
|
|
if (res === null) {
|
2022-01-11 15:31:56 +01:00
|
|
|
vhds.add(`${vdiDir}/${file}`)
|
2021-04-30 11:43:21 +02:00
|
|
|
} else {
|
2022-08-03 17:53:59 +02:00
|
|
|
try {
|
2022-08-12 16:07:44 +02:00
|
|
|
const mergeState = JSON.parse(await handler.readFile(`${vdiDir}/${file}`))
|
2022-08-03 17:53:59 +02:00
|
|
|
interruptedVhds.set(`${vdiDir}/${res[1]}`, {
|
|
|
|
|
statePath: `${vdiDir}/${file}`,
|
|
|
|
|
chain: mergeState.chain,
|
|
|
|
|
})
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// fall back to a non resuming merge
|
|
|
|
|
vhds.add(`${vdiDir}/${file}`)
|
|
|
|
|
logWarn('failed to read existing merge state', { path: file, error })
|
|
|
|
|
}
|
2021-04-30 11:43:21 +02:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2021-04-13 16:09:42 +02:00
|
|
|
)
|
|
|
|
|
)
|
2021-04-30 11:43:21 +02:00
|
|
|
|
2022-01-13 16:07:28 +01:00
|
|
|
return { vhds, interruptedVhds, aliases }
|
2021-04-13 16:09:42 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-30 15:39:54 +02:00
|
|
|
async function checkAliases(
|
|
|
|
|
aliasPaths,
|
|
|
|
|
targetDataRepository,
|
|
|
|
|
{ handler, logInfo = noop, logWarn = console.warn, remove = false }
|
|
|
|
|
) {
|
2022-01-13 16:07:28 +01:00
|
|
|
const aliasFound = []
|
2022-07-28 17:53:54 +02:00
|
|
|
for (const alias of aliasPaths) {
|
|
|
|
|
const target = await resolveVhdAlias(handler, alias)
|
2022-01-13 16:07:28 +01:00
|
|
|
|
|
|
|
|
if (!isVhdFile(target)) {
|
2022-07-28 17:53:54 +02:00
|
|
|
logWarn('alias references non VHD target', { alias, target })
|
2022-01-13 16:07:28 +01:00
|
|
|
if (remove) {
|
2022-07-28 17:53:54 +02:00
|
|
|
logInfo('removing alias and non VHD target', { alias, target })
|
2022-01-13 16:07:28 +01:00
|
|
|
await handler.unlink(target)
|
2022-07-28 17:53:54 +02:00
|
|
|
await handler.unlink(alias)
|
2022-01-13 16:07:28 +01:00
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const { dispose } = await openVhd(handler, target)
|
|
|
|
|
try {
|
|
|
|
|
await dispose()
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// error during dispose should not trigger a deletion
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2022-07-28 17:53:54 +02:00
|
|
|
logWarn('missing or broken alias target', { alias, target, error })
|
2022-01-13 16:07:28 +01:00
|
|
|
if (remove) {
|
|
|
|
|
try {
|
2022-07-28 17:53:54 +02:00
|
|
|
await VhdAbstract.unlink(handler, alias)
|
2022-05-30 15:39:54 +02:00
|
|
|
} catch (error) {
|
|
|
|
|
if (error.code !== 'ENOENT') {
|
2022-07-28 17:53:54 +02:00
|
|
|
logWarn('error deleting alias target', { alias, target, error })
|
2022-01-13 16:07:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
aliasFound.push(resolve('/', target))
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-28 17:53:54 +02:00
|
|
|
const vhds = await handler.list(targetDataRepository, {
|
2022-01-13 16:07:28 +01:00
|
|
|
ignoreMissing: true,
|
|
|
|
|
prependDir: true,
|
|
|
|
|
})
|
|
|
|
|
|
2022-07-28 17:56:09 +02:00
|
|
|
await asyncMap(vhds, async path => {
|
2022-07-28 17:53:54 +02:00
|
|
|
if (!aliasFound.includes(path)) {
|
|
|
|
|
logWarn('no alias references VHD', { path })
|
2022-01-13 16:07:28 +01:00
|
|
|
if (remove) {
|
2022-07-28 17:53:54 +02:00
|
|
|
logInfo('deleting unused VHD', { path })
|
|
|
|
|
await VhdAbstract.unlink(handler, path)
|
2022-01-13 16:07:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2022-05-30 15:39:54 +02:00
|
|
|
|
2022-01-13 16:07:28 +01:00
|
|
|
exports.checkAliases = checkAliases
|
|
|
|
|
|
2021-10-12 09:17:42 +02:00
|
|
|
const defaultMergeLimiter = limitConcurrency(1)
|
|
|
|
|
|
|
|
|
|
exports.cleanVm = async function cleanVm(
|
|
|
|
|
vmDir,
|
2022-09-16 14:54:33 +02:00
|
|
|
{
|
|
|
|
|
fixMetadata,
|
|
|
|
|
remove,
|
|
|
|
|
merge,
|
|
|
|
|
mergeBlockConcurrency,
|
|
|
|
|
mergeLimiter = defaultMergeLimiter,
|
|
|
|
|
logInfo = noop,
|
|
|
|
|
logWarn = console.warn,
|
|
|
|
|
}
|
2021-10-12 09:17:42 +02:00
|
|
|
) {
|
2022-06-29 18:39:20 +02:00
|
|
|
const limitedMergeVhdChain = mergeLimiter(_mergeVhdChain)
|
2021-10-25 09:13:58 +02:00
|
|
|
|
2021-03-24 10:00:47 +01:00
|
|
|
const handler = this._handler
|
|
|
|
|
|
2021-12-20 14:57:54 +01:00
|
|
|
const vhdsToJSons = new Set()
|
2022-07-07 16:57:15 +02:00
|
|
|
const vhdById = new Map()
|
2021-03-24 10:00:47 +01:00
|
|
|
const vhdParents = { __proto__: null }
|
|
|
|
|
const vhdChildren = { __proto__: null }
|
|
|
|
|
|
2022-08-03 17:53:59 +02:00
|
|
|
const { vhds, interruptedVhds, aliases } = await listVhds(handler, vmDir, logWarn)
|
2021-04-30 11:43:21 +02:00
|
|
|
|
2021-03-24 10:00:47 +01:00
|
|
|
// remove broken VHDs
|
2022-01-11 15:31:56 +01:00
|
|
|
await asyncMap(vhds, async path => {
|
2021-04-13 16:09:42 +02:00
|
|
|
try {
|
2022-01-11 15:31:56 +01:00
|
|
|
await Disposable.use(openVhd(handler, path, { checkSecondFooter: !interruptedVhds.has(path) }), vhd => {
|
2021-11-25 17:53:20 +01:00
|
|
|
if (vhd.footer.diskType === DISK_TYPES.DIFFERENCING) {
|
2021-11-22 17:14:29 +01:00
|
|
|
const parent = resolve('/', dirname(path), vhd.header.parentUnicodeName)
|
|
|
|
|
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?
|
|
|
|
|
}
|
|
|
|
|
vhdChildren[parent] = path
|
2021-03-24 10:00:47 +01:00
|
|
|
}
|
2022-07-07 16:57:15 +02:00
|
|
|
// Detect VHDs with the same UUIDs
|
|
|
|
|
//
|
|
|
|
|
// Due to a bug introduced in a1bcd35e2
|
|
|
|
|
const duplicate = vhdById.get(UUID.stringify(vhd.footer.uuid))
|
|
|
|
|
let vhdKept = vhd
|
|
|
|
|
if (duplicate !== undefined) {
|
|
|
|
|
logWarn('uuid is duplicated', { uuid: UUID.stringify(vhd.footer.uuid) })
|
|
|
|
|
if (duplicate.containsAllDataOf(vhd)) {
|
|
|
|
|
logWarn(`should delete ${path}`)
|
|
|
|
|
vhdKept = duplicate
|
|
|
|
|
vhds.delete(path)
|
|
|
|
|
} else if (vhd.containsAllDataOf(duplicate)) {
|
|
|
|
|
logWarn(`should delete ${duplicate._path}`)
|
|
|
|
|
vhds.delete(duplicate._path)
|
|
|
|
|
} else {
|
2022-07-28 17:53:54 +02:00
|
|
|
logWarn('same ids but different content')
|
2022-07-07 16:57:15 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
vhdById.set(UUID.stringify(vhdKept.footer.uuid), vhdKept)
|
2021-11-22 17:14:29 +01:00
|
|
|
})
|
2021-04-13 16:09:42 +02:00
|
|
|
} catch (error) {
|
2022-01-11 15:31:56 +01:00
|
|
|
vhds.delete(path)
|
2022-05-30 15:39:54 +02:00
|
|
|
logWarn('VHD check error', { path, error })
|
2021-04-13 16:09:42 +02:00
|
|
|
if (error?.code === 'ERR_ASSERTION' && remove) {
|
2022-07-28 17:53:54 +02:00
|
|
|
logInfo('deleting broken VHD', { path })
|
2021-11-22 17:14:29 +01:00
|
|
|
return VhdAbstract.unlink(handler, 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
|
|
|
|
2022-01-13 10:41:39 +01:00
|
|
|
// remove interrupted merge states for missing VHDs
|
|
|
|
|
for (const interruptedVhd of interruptedVhds.keys()) {
|
|
|
|
|
if (!vhds.has(interruptedVhd)) {
|
2022-08-03 17:53:59 +02:00
|
|
|
const { statePath } = interruptedVhds.get(interruptedVhd)
|
2022-01-13 10:41:39 +01:00
|
|
|
interruptedVhds.delete(interruptedVhd)
|
|
|
|
|
|
2022-05-30 15:39:54 +02:00
|
|
|
logWarn('orphan merge state', {
|
2022-01-13 10:41:39 +01:00
|
|
|
mergeStatePath: statePath,
|
|
|
|
|
missingVhdPath: interruptedVhd,
|
|
|
|
|
})
|
|
|
|
|
if (remove) {
|
2022-05-30 15:39:54 +02:00
|
|
|
logInfo('deleting orphan merge state', { statePath })
|
2022-01-13 10:41:39 +01:00
|
|
|
await handler.unlink(statePath)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-13 16:07:28 +01:00
|
|
|
// check if alias are correct
|
|
|
|
|
// check if all vhd in data subfolder have a corresponding alias
|
2022-01-18 10:07:56 +01:00
|
|
|
await asyncMap(Object.keys(aliases), async dir => {
|
2022-05-30 15:39:54 +02:00
|
|
|
await checkAliases(aliases[dir], `${dir}/data`, { handler, logInfo, logWarn, remove })
|
2022-01-18 10:07:56 +01:00
|
|
|
})
|
2021-11-22 17:14:29 +01: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
|
2021-11-22 17:14:29 +01:00
|
|
|
const deleteIfOrphan = vhdPath => {
|
|
|
|
|
const parent = vhdParents[vhdPath]
|
2021-03-24 10:00:47 +01:00
|
|
|
if (parent === undefined) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// no longer needs to be checked
|
2021-11-22 17:14:29 +01:00
|
|
|
delete vhdParents[vhdPath]
|
2021-03-24 10:00:47 +01:00
|
|
|
|
|
|
|
|
deleteIfOrphan(parent)
|
|
|
|
|
|
|
|
|
|
if (!vhds.has(parent)) {
|
2021-11-22 17:14:29 +01:00
|
|
|
vhds.delete(vhdPath)
|
2021-03-24 10:00:47 +01:00
|
|
|
|
2022-07-28 17:53:54 +02:00
|
|
|
logWarn('parent VHD is missing', { parent, child: vhdPath })
|
2021-03-24 10:00:47 +01:00
|
|
|
if (remove) {
|
2022-07-28 17:53:54 +02:00
|
|
|
logInfo('deleting orphan VHD', { path: vhdPath })
|
2021-11-22 17:14:29 +01:00
|
|
|
deletions.push(VhdAbstract.unlink(handler, vhdPath))
|
2021-03-24 10:00:47 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// > 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)
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-20 09:45:44 +01:00
|
|
|
const jsons = new Set()
|
2021-03-24 10:00:47 +01:00
|
|
|
const xvas = new Set()
|
|
|
|
|
const xvaSums = []
|
|
|
|
|
const entries = await handler.list(vmDir, {
|
|
|
|
|
prependDir: true,
|
|
|
|
|
})
|
|
|
|
|
entries.forEach(path => {
|
|
|
|
|
if (isMetadataFile(path)) {
|
2021-12-20 09:45:44 +01:00
|
|
|
jsons.add(path)
|
2021-03-24 10:00:47 +01:00
|
|
|
} else if (isXvaFile(path)) {
|
|
|
|
|
xvas.add(path)
|
|
|
|
|
} else if (isXvaSumFile(path)) {
|
|
|
|
|
xvaSums.push(path)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2022-12-07 13:06:03 +01:00
|
|
|
const cachePath = vmDir + '/cache.json.gz'
|
|
|
|
|
|
|
|
|
|
let mustRegenerateCache
|
|
|
|
|
{
|
|
|
|
|
const cache = await this._readCache(cachePath)
|
|
|
|
|
const actual = cache === undefined ? 0 : Object.keys(cache).length
|
|
|
|
|
const expected = jsons.size
|
|
|
|
|
|
|
|
|
|
mustRegenerateCache = actual !== expected
|
|
|
|
|
if (mustRegenerateCache) {
|
|
|
|
|
logWarn('unexpected number of entries in backup cache', { path: cachePath, actual, expected })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-24 10:00:47 +01:00
|
|
|
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))) {
|
2022-05-30 15:39:54 +02:00
|
|
|
logWarn('XVA might be broken', { path })
|
2021-03-24 10:00:47 +01:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const unusedVhds = new Set(vhds)
|
|
|
|
|
const unusedXvas = new Set(xvas)
|
|
|
|
|
|
2022-12-07 13:06:03 +01:00
|
|
|
const backups = new Map()
|
|
|
|
|
|
2021-03-24 10:00:47 +01:00
|
|
|
// compile the list of unused XVAs and VHDs, and remove backup metadata which
|
|
|
|
|
// reference a missing XVA/VHD
|
|
|
|
|
await asyncMap(jsons, async json => {
|
2021-12-20 09:48:30 +01:00
|
|
|
let metadata
|
|
|
|
|
try {
|
|
|
|
|
metadata = JSON.parse(await handler.readFile(json))
|
|
|
|
|
} catch (error) {
|
2022-07-28 17:53:54 +02:00
|
|
|
logWarn('failed to read backup metadata', { path: json, error })
|
2021-12-20 09:48:30 +01:00
|
|
|
jsons.delete(json)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-07 13:06:03 +01:00
|
|
|
let isBackupComplete
|
|
|
|
|
|
2021-03-24 10:00:47 +01:00
|
|
|
const { mode } = metadata
|
|
|
|
|
if (mode === 'full') {
|
2021-04-20 10:35:28 +02:00
|
|
|
const linkedXva = resolve('/', vmDir, metadata.xva)
|
2022-12-07 13:06:03 +01:00
|
|
|
isBackupComplete = xvas.has(linkedXva)
|
|
|
|
|
if (isBackupComplete) {
|
2021-03-24 10:00:47 +01:00
|
|
|
unusedXvas.delete(linkedXva)
|
|
|
|
|
} else {
|
2022-07-28 17:53:54 +02:00
|
|
|
logWarn('the XVA linked to the backup is missing', { backup: json, xva: linkedXva })
|
2021-03-24 10:00:47 +01:00
|
|
|
}
|
|
|
|
|
} 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
|
|
|
})()
|
2021-12-08 11:35:27 +01:00
|
|
|
|
|
|
|
|
const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
|
2022-12-07 13:06:03 +01:00
|
|
|
isBackupComplete = missingVhds.length === 0
|
2021-12-08 11:35:27 +01:00
|
|
|
|
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
|
2022-12-07 13:06:03 +01:00
|
|
|
if (isBackupComplete) {
|
2021-03-24 10:00:47 +01:00
|
|
|
linkedVhds.forEach(_ => unusedVhds.delete(_))
|
2021-12-20 14:57:54 +01:00
|
|
|
linkedVhds.forEach(path => {
|
|
|
|
|
vhdsToJSons[path] = json
|
|
|
|
|
})
|
2021-03-24 10:00:47 +01:00
|
|
|
} else {
|
2022-07-28 17:53:54 +02:00
|
|
|
logWarn('some VHDs linked to the backup are missing', { backup: json, missingVhds })
|
2022-12-07 13:06:03 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isBackupComplete) {
|
|
|
|
|
backups.set(json, metadata)
|
|
|
|
|
} else {
|
|
|
|
|
jsons.delete(json)
|
|
|
|
|
if (remove) {
|
|
|
|
|
logInfo('deleting incomplete backup', { backup: json })
|
|
|
|
|
mustRegenerateCache = true
|
|
|
|
|
await handler.unlink(json)
|
2021-03-24 10:00:47 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// TODO: parallelize by vm/job/vdi
|
|
|
|
|
const unusedVhdsDeletion = []
|
2021-10-13 15:10:35 +02:00
|
|
|
const toMerge = []
|
2021-03-24 10:00:47 +01:00
|
|
|
{
|
2022-07-07 16:57:15 +02:00
|
|
|
// VHD chains (as list from oldest to most recent) to merge indexed by most recent
|
2021-03-24 10:00:47 +01:00
|
|
|
// 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) {
|
2022-07-07 16:57:15 +02:00
|
|
|
chain.unshift(vhd)
|
2021-03-24 10:00:47 +01:00
|
|
|
return chain
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-28 17:53:54 +02:00
|
|
|
logWarn('unused VHD', { path: vhd })
|
2021-03-24 10:00:47 +01:00
|
|
|
if (remove) {
|
2022-07-28 17:53:54 +02:00
|
|
|
logInfo('deleting unused VHD', { path: vhd })
|
2021-11-22 17:14:29 +01:00
|
|
|
unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
|
2021-03-24 10:00:47 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toCheck.forEach(vhd => {
|
|
|
|
|
vhdChainsToMerge[vhd] = getUsedChildChainOrDelete(vhd)
|
|
|
|
|
})
|
|
|
|
|
|
2021-04-30 11:43:21 +02:00
|
|
|
// merge interrupted VHDs
|
2022-01-13 10:41:39 +01:00
|
|
|
for (const parent of interruptedVhds.keys()) {
|
2022-08-03 17:53:59 +02:00
|
|
|
// before #6349 the chain wasn't in the mergeState
|
|
|
|
|
const { chain, statePath } = interruptedVhds.get(parent)
|
|
|
|
|
if (chain === undefined) {
|
|
|
|
|
vhdChainsToMerge[parent] = [parent, vhdChildren[parent]]
|
|
|
|
|
} else {
|
|
|
|
|
vhdChainsToMerge[parent] = chain.map(vhdPath => handlerPath.resolveFromFile(statePath, vhdPath))
|
|
|
|
|
}
|
2022-01-13 10:41:39 +01:00
|
|
|
}
|
2021-04-30 11:43:21 +02:00
|
|
|
|
2021-10-13 15:10:35 +02:00
|
|
|
Object.values(vhdChainsToMerge).forEach(chain => {
|
2021-03-24 10:00:47 +01:00
|
|
|
if (chain !== undefined) {
|
2021-10-13 15:10:35 +02:00
|
|
|
toMerge.push(chain)
|
2021-03-24 10:00:47 +01:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-20 14:57:54 +01:00
|
|
|
const metadataWithMergedVhd = {}
|
|
|
|
|
const doMerge = async () => {
|
|
|
|
|
await asyncMap(toMerge, async chain => {
|
2022-09-16 14:54:33 +02:00
|
|
|
const merged = await limitedMergeVhdChain(handler, chain, {
|
|
|
|
|
logInfo,
|
|
|
|
|
logWarn,
|
|
|
|
|
remove,
|
|
|
|
|
merge,
|
|
|
|
|
mergeBlockConcurrency,
|
|
|
|
|
})
|
2021-12-20 14:57:54 +01:00
|
|
|
if (merged !== undefined) {
|
2022-08-03 14:06:35 +02:00
|
|
|
const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metada file
|
2021-12-20 14:57:54 +01:00
|
|
|
metadataWithMergedVhd[metadataPath] = true
|
|
|
|
|
}
|
|
|
|
|
})
|
2021-10-13 15:10:35 +02:00
|
|
|
}
|
|
|
|
|
|
2021-03-24 10:00:47 +01:00
|
|
|
await Promise.all([
|
2021-04-28 18:20:01 +02:00
|
|
|
...unusedVhdsDeletion,
|
2021-10-13 15:10:35 +02:00
|
|
|
toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : doMerge()),
|
2021-03-24 10:00:47 +01:00
|
|
|
asyncMap(unusedXvas, path => {
|
2022-05-30 15:39:54 +02:00
|
|
|
logWarn('unused XVA', { path })
|
2021-04-28 10:53:40 +02:00
|
|
|
if (remove) {
|
2022-05-30 15:39:54 +02:00
|
|
|
logInfo('deleting unused XVA', { path })
|
2021-04-28 10:53:40 +02:00
|
|
|
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))) {
|
2022-05-30 15:39:54 +02:00
|
|
|
logInfo('unused XVA checksum', { path })
|
2021-04-28 10:53:40 +02:00
|
|
|
if (remove) {
|
2022-05-30 15:39:54 +02:00
|
|
|
logInfo('deleting unused XVA checksum', { path })
|
2021-04-28 10:53:40 +02:00
|
|
|
return handler.unlink(path)
|
|
|
|
|
}
|
2021-03-24 10:00:47 +01:00
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
])
|
2021-10-13 16:25:21 +02:00
|
|
|
|
2021-12-20 14:57:54 +01:00
|
|
|
// update size for delta metadata with merged VHD
|
|
|
|
|
// check for the other that the size is the same as the real file size
|
|
|
|
|
|
|
|
|
|
await asyncMap(jsons, async metadataPath => {
|
2022-12-07 13:06:03 +01:00
|
|
|
const metadata = backups.get(metadataPath)
|
2021-12-20 14:57:54 +01:00
|
|
|
|
|
|
|
|
let fileSystemSize
|
|
|
|
|
const merged = metadataWithMergedVhd[metadataPath] !== undefined
|
|
|
|
|
|
|
|
|
|
const { mode, size, vhds, xva } = metadata
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (mode === 'full') {
|
|
|
|
|
// a full backup : check size
|
|
|
|
|
const linkedXva = resolve('/', vmDir, xva)
|
2022-08-23 12:04:16 +02:00
|
|
|
try {
|
|
|
|
|
fileSystemSize = await handler.getSize(linkedXva)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// can fail with encrypted remote
|
|
|
|
|
}
|
2021-12-20 14:57:54 +01:00
|
|
|
} else if (mode === 'delta') {
|
2021-12-21 16:16:38 +01:00
|
|
|
const linkedVhds = Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
|
|
|
|
|
fileSystemSize = await computeVhdsSize(handler, linkedVhds)
|
2021-12-20 14:57:54 +01:00
|
|
|
|
2021-12-23 12:09:11 +01:00
|
|
|
// the size is not computed in some cases (e.g. VhdDirectory)
|
|
|
|
|
if (fileSystemSize === undefined) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-20 14:57:54 +01:00
|
|
|
// don't warn if the size has changed after a merge
|
|
|
|
|
if (!merged && fileSystemSize !== size) {
|
2023-03-16 17:11:51 +01:00
|
|
|
// FIXME: figure out why it occurs so often and, once fixed, log the real problems with `logWarn`
|
|
|
|
|
console.warn('cleanVm: incorrect backup size in metadata', {
|
2022-07-28 17:53:54 +02:00
|
|
|
path: metadataPath,
|
|
|
|
|
actual: size ?? 'none',
|
|
|
|
|
expected: fileSystemSize,
|
|
|
|
|
})
|
2021-12-20 14:57:54 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2022-07-28 17:53:54 +02:00
|
|
|
logWarn('failed to get backup size', { backup: metadataPath, error })
|
2021-12-20 14:57:54 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// systematically update size after a merge
|
|
|
|
|
if ((merged || fixMetadata) && size !== fileSystemSize) {
|
|
|
|
|
metadata.size = fileSystemSize
|
2022-12-07 13:06:03 +01:00
|
|
|
mustRegenerateCache = true
|
2021-12-20 14:57:54 +01:00
|
|
|
try {
|
|
|
|
|
await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
|
|
|
|
|
} catch (error) {
|
2022-07-28 17:53:54 +02:00
|
|
|
logWarn('failed to update backup size in metadata', { path: metadataPath, error })
|
2021-12-20 14:57:54 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2022-12-07 13:06:03 +01:00
|
|
|
if (mustRegenerateCache) {
|
|
|
|
|
const cache = {}
|
|
|
|
|
for (const [path, content] of backups.entries()) {
|
|
|
|
|
cache[path] = {
|
|
|
|
|
_filename: path,
|
|
|
|
|
id: path,
|
|
|
|
|
...content,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
await this._writeCache(cachePath, cache)
|
2022-09-09 16:27:12 +02:00
|
|
|
}
|
|
|
|
|
|
2021-10-13 16:25:21 +02:00
|
|
|
return {
|
|
|
|
|
// boolean whether some VHDs were merged (or should be merged)
|
|
|
|
|
merge: toMerge.length !== 0,
|
|
|
|
|
}
|
2021-03-24 10:00:47 +01:00
|
|
|
}
|