Compare commits

...

2 Commits

Author SHA1 Message Date
Florent Beauchamp
aa9610ffe6 feat(backups): store vhd path of a backup unencrypted 2023-12-11 10:13:16 +00:00
Florent Beauchamp
664164df60 feat(vhd-lib): alias are not encrypted 2023-12-11 09:46:31 +00:00
7 changed files with 71 additions and 4 deletions

View File

@@ -30,6 +30,7 @@ import { isValidXva } from './_isValidXva.mjs'
import { listPartitions, LVM_PARTITION_TYPE } from './_listPartitions.mjs' import { listPartitions, LVM_PARTITION_TYPE } from './_listPartitions.mjs'
import { lvs, pvs } from './_lvm.mjs' import { lvs, pvs } from './_lvm.mjs'
import { watchStreamSize } from './_watchStreamSize.mjs' import { watchStreamSize } from './_watchStreamSize.mjs'
import { pick } from 'lodash'
export const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups' export const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
@@ -253,7 +254,9 @@ export class RemoteAdapter {
const handler = this._handler const handler = this._handler
// this will delete the json, unused VHDs will be detected by `cleanVm` // this will delete the json, unused VHDs will be detected by `cleanVm`
await asyncMapSettled(backups, ({ _filename }) => handler.unlink(_filename)) await asyncMapSettled(backups, ({ _filename }) =>
Promise.all([handler.unlink(_filename), handler.unlink(_filename + '.plaintext').catch(() => {})])
)
await this.#removeVmBackupsFromCache(backups) await this.#removeVmBackupsFromCache(backups)
} }
@@ -646,6 +649,14 @@ export class RemoteAdapter {
await this.handler.outputFile(path, JSON.stringify(metadata), { await this.handler.outputFile(path, JSON.stringify(metadata), {
dirMode: this._dirMode, dirMode: this._dirMode,
}) })
// using _outputFile means this will NOT be encrypted, to allow for immutability
await this.handler._outputFile(
path + '.plaintext',
JSON.stringify(pick(metadata, ['type', 'differentialVhds', 'vhds', 'xva'])),
{
dirMode: this._dirMode,
}
)
// will not throw // will not throw
await this._updateCache(this.#getVmBackupsCache(vmUuid), backups => { await this._updateCache(this.#getVmBackupsCache(vmUuid), backups => {

View File

@@ -1,4 +1,5 @@
export const isMetadataFile = filename => filename.endsWith('.json') export const isMetadataFile = filename => filename.endsWith('.json')
export const isMetadataPlainTextFile = filename => filename.endsWith('.json.plaintext')
export const isVhdFile = filename => filename.endsWith('.vhd') export const isVhdFile = filename => filename.endsWith('.vhd')
export const isXvaFile = filename => filename.endsWith('.xva') export const isXvaFile = filename => filename.endsWith('.xva')
export const isXvaSumFile = filename => filename.endsWith('.xva.checksum') export const isXvaSumFile = filename => filename.endsWith('.xva.checksum')

View File

@@ -4,7 +4,7 @@ import { asyncMap } from '@xen-orchestra/async-map'
import { Constants, openVhd, VhdAbstract, VhdFile } from 'vhd-lib' import { Constants, openVhd, VhdAbstract, VhdFile } from 'vhd-lib'
import { isVhdAlias, resolveVhdAlias } from 'vhd-lib/aliases.js' import { isVhdAlias, resolveVhdAlias } from 'vhd-lib/aliases.js'
import { dirname, resolve } from 'node:path' import { dirname, resolve } from 'node:path'
import { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } from './_backupType.mjs' import { isMetadataFile, isMetadataPlainTextFile, isVhdFile, isXvaFile, isXvaSumFile } from './_backupType.mjs'
import { limitConcurrency } from 'limit-concurrency-decorator' import { limitConcurrency } from 'limit-concurrency-decorator'
import { mergeVhdChain } from 'vhd-lib/merge.js' import { mergeVhdChain } from 'vhd-lib/merge.js'
@@ -310,6 +310,7 @@ export async function cleanVm(
const jsons = new Set() const jsons = new Set()
const xvas = new Set() const xvas = new Set()
const xvaSums = [] const xvaSums = []
const plainTexts = new Set()
const entries = await handler.list(vmDir, { const entries = await handler.list(vmDir, {
prependDir: true, prependDir: true,
}) })
@@ -320,9 +321,18 @@ export async function cleanVm(
xvas.add(path) xvas.add(path)
} else if (isXvaSumFile(path)) { } else if (isXvaSumFile(path)) {
xvaSums.push(path) xvaSums.push(path)
} else if (isMetadataPlainTextFile(path)) {
plainTexts.push(path)
} }
}) })
for (const plainTextPath of plainTexts) {
if (!jsons.has(plainTextPath.substring(0, -15))) {
logWarn('plaintext without metadata', { plainTextPath })
await handler.unlink(plainTextPath)
}
}
const cachePath = vmDir + '/cache.json.gz' const cachePath = vmDir + '/cache.json.gz'
let mustRegenerateCache let mustRegenerateCache

View File

@@ -66,6 +66,20 @@ describe('VhdAbstract', async () => {
}) })
}) })
it('Creates alias unencrypted in encrypted remote', async () => {
await Disposable.use(async function* () {
const handler = yield getSyncedHandler({
url: `file://${tempDir}/?encryptionKey="85704F498279D6FCD2B2AA7B793944DF"`,
})
const aliasPath = `alias.alias.vhd`
const aliasFsPath = `${tempDir}/${aliasPath}`
await VhdAbstract.createAlias(handler, aliasPath, 'target.vhd')
assert.equal(await fs.exists(aliasFsPath), true)
const content = await fs.readFile(aliasFsPath, 'utf-8')
assert.equal(content, 'target.vhd')
})
})
it('has *.alias.vhd extension', async () => { it('has *.alias.vhd extension', async () => {
await Disposable.use(async function* () { await Disposable.use(async function* () {
const handler = yield getSyncedHandler({ url: 'file:///' }) const handler = yield getSyncedHandler({ url: 'file:///' })

View File

@@ -236,7 +236,8 @@ exports.VhdAbstract = class VhdAbstract {
`Alias relative path ${relativePathToTarget} is too long : ${relativePathToTarget.length} chars, max is ${ALIAS_MAX_PATH_LENGTH}` `Alias relative path ${relativePathToTarget} is too long : ${relativePathToTarget.length} chars, max is ${ALIAS_MAX_PATH_LENGTH}`
) )
} }
await handler.writeFile(aliasPath, relativePathToTarget) // using _outputFile means the checksum will NOT be encrypted
await handler._outputFile(path.resolve('/', aliasPath), relativePathToTarget, { flags: 'wx' })
} }
streamSize() { streamSize() {

View File

@@ -10,6 +10,7 @@ const { Disposable, pFromCallback } = require('promise-toolbox')
const { isVhdAlias, resolveVhdAlias } = require('./aliases') const { isVhdAlias, resolveVhdAlias } = require('./aliases')
const { ALIAS_MAX_PATH_LENGTH } = require('./_constants') const { ALIAS_MAX_PATH_LENGTH } = require('./_constants')
const fs = require('node:fs/promises')
let tempDir let tempDir
@@ -74,4 +75,17 @@ describe('resolveVhdAlias()', async () => {
await assert.rejects(async () => await resolveVhdAlias(handler, 'toobig.alias.vhd'), Error) await assert.rejects(async () => await resolveVhdAlias(handler, 'toobig.alias.vhd'), Error)
}) })
}) })
it('read an plaintext alias from an encrypted remote', async () => {
await Disposable.use(async function* () {
const handler = yield getSyncedHandler({
url: `file://${tempDir}/?encryptionKey="85704F498279D6FCD2B2AA7B793944DF"`,
})
await handler.writeFile(`crypted.alias.vhd`, `target.vhd`)
await fs.writeFile(`${tempDir}/plain.alias.vhd`, `target.vhd`)
assert.equal(await resolveVhdAlias(handler, `plain.alias.vhd`), `target.vhd`)
assert.equal(await resolveVhdAlias(handler, `crypted.alias.vhd`), `target.vhd`)
})
})
}) })

View File

@@ -2,6 +2,7 @@
const { ALIAS_MAX_PATH_LENGTH } = require('./_constants') const { ALIAS_MAX_PATH_LENGTH } = require('./_constants')
const resolveRelativeFromFile = require('./_resolveRelativeFromFile') const resolveRelativeFromFile = require('./_resolveRelativeFromFile')
const path = require('node:path')
function isVhdAlias(filename) { function isVhdAlias(filename) {
return filename.endsWith('.alias.vhd') return filename.endsWith('.alias.vhd')
@@ -20,7 +21,22 @@ exports.resolveVhdAlias = async function resolveVhdAlias(handler, filename) {
} }
} }
const aliasContent = (await handler.readFile(filename)).toString().trim() let aliasContent
try {
aliasContent = (await handler.readFile(filename)).toString().trim()
} catch (err) {
try {
// try to read as a plain text
aliasContent = (await handler._readFile(path.resolve('/', filename))).toString().trim()
if (!aliasContent.endsWith('.vhd')) {
throw new Error(`The alias file ${filename} is not a plaint text alias`)
}
} catch (plainTextErr) {
// throw original error
err.cause = plainTextErr
throw err
}
}
// also handle circular references and unreasonnably long chains // also handle circular references and unreasonnably long chains
if (isVhdAlias(aliasContent)) { if (isVhdAlias(aliasContent)) {
throw new Error(`Chaining alias is forbidden ${filename} to ${aliasContent}`) throw new Error(`Chaining alias is forbidden ${filename} to ${aliasContent}`)