Compare commits

..

1 Commits

Author SHA1 Message Date
Florent Beauchamp
4b9e11e0ff fix(backups/mirror): use stable uuid for chaining 2024-02-12 16:52:56 +00:00
41 changed files with 1307 additions and 1816 deletions

View File

@@ -65,11 +65,10 @@ module.exports = {
typescript: true, typescript: true,
'eslint-import-resolver-custom-alias': { 'eslint-import-resolver-custom-alias': {
alias: { alias: {
'@core': '../web-core/lib',
'@': './src', '@': './src',
}, },
extensions: ['.ts'], extensions: ['.ts'],
packages: ['@xen-orchestra/lite', '@xen-orchestra/web'], packages: ['@xen-orchestra/lite'],
}, },
}, },
}, },

View File

@@ -160,10 +160,10 @@ export class ImportVmBackup {
// update the stream with the negative vhd stream // update the stream with the negative vhd stream
stream = await negativeVhd.stream() stream = await negativeVhd.stream()
vdis[vdiRef].baseVdi = snapshotCandidate vdis[vdiRef].baseVdi = snapshotCandidate
} catch (error) { } catch (err) {
// can be a broken VHD chain, a vhd chain with a key backup, .... // can be a broken VHD chain, a vhd chain with a key backup, ....
// not an irrecuperable error, don't dispose parentVhd, and fallback to full restore // not an irrecuperable error, don't dispose parentVhd, and fallback to full restore
warn(`can't use differential restore`, { error }) warn(`can't use differential restore`, err)
disposableDescendants?.dispose() disposableDescendants?.dispose()
} }
} }

View File

@@ -191,14 +191,13 @@ export class RemoteAdapter {
// check if we will be allowed to merge a a vhd created in this adapter // check if we will be allowed to merge a a vhd created in this adapter
// with the vhd at path `path` // with the vhd at path `path`
async isMergeableParent(packedParentUid, path) { async isMergeableParent(packedParentUid, path) {
return await Disposable.use(VhdSynthetic.fromVhdChain(this.handler, path), vhd => { return await Disposable.use(openVhd(this.handler, path), vhd => {
// this baseUuid is not linked with this vhd // this baseUuid is not linked with this vhd
if (!vhd.footer.uuid.equals(packedParentUid)) { if (!vhd.footer.uuid.equals(packedParentUid)) {
return false return false
} }
// check if all the chain is composed of vhd directory const isVhdDirectory = vhd instanceof VhdDirectory
const isVhdDirectory = vhd.checkVhdsClass(VhdDirectory)
return isVhdDirectory return isVhdDirectory
? this.useVhdDirectory() && this.#getCompressionType() === vhd.compressionType ? this.useVhdDirectory() && this.#getCompressionType() === vhd.compressionType
: !this.useVhdDirectory() : !this.useVhdDirectory()

View File

@@ -2,7 +2,6 @@ import { asyncEach } from '@vates/async-each'
import { decorateMethodsWith } from '@vates/decorate-with' import { decorateMethodsWith } from '@vates/decorate-with'
import { defer } from 'golike-defer' import { defer } from 'golike-defer'
import assert from 'node:assert' import assert from 'node:assert'
import * as UUID from 'uuid'
import isVhdDifferencingDisk from 'vhd-lib/isVhdDifferencingDisk.js' import isVhdDifferencingDisk from 'vhd-lib/isVhdDifferencingDisk.js'
import mapValues from 'lodash/mapValues.js' import mapValues from 'lodash/mapValues.js'
@@ -10,48 +9,11 @@ import { AbstractRemote } from './_AbstractRemote.mjs'
import { forkDeltaExport } from './_forkDeltaExport.mjs' import { forkDeltaExport } from './_forkDeltaExport.mjs'
import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs' import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
import { Task } from '../../Task.mjs' import { Task } from '../../Task.mjs'
import { Disposable } from 'promise-toolbox'
import { openVhd } from 'vhd-lib'
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
class IncrementalRemoteVmBackupRunner extends AbstractRemote { class IncrementalRemoteVmBackupRunner extends AbstractRemote {
_getRemoteWriter() { _getRemoteWriter() {
return IncrementalRemoteWriter return IncrementalRemoteWriter
} }
async _selectBaseVm(metadata) {
// for each disk , get the parent
const baseUuidToSrcVdi = new Map()
// no previous backup for a base( =key) backup
if (metadata.isBase) {
return
}
await asyncEach(Object.entries(metadata.vdis), async ([id, vdi]) => {
const isDifferencing = metadata.isVhdDifferencing[`${id}.vhd`]
if (isDifferencing) {
const vmDir = getVmBackupDir(metadata.vm.uuid)
const path = `${vmDir}/${metadata.vhds[id]}`
// don't catch error : we can't recover if the source vhd are missing
await Disposable.use(openVhd(this._sourceRemoteAdapter._handler, path), vhd => {
baseUuidToSrcVdi.set(UUID.stringify(vhd.header.parentUuid), vdi.$snapshot_of$uuid)
})
}
})
const presentBaseVdis = new Map(baseUuidToSrcVdi)
await this._callWriters(
writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis),
'writer.checkBaseVdis()',
false
)
// check if the parent vdi are present in all the remotes
baseUuidToSrcVdi.forEach((srcVdiUuid, baseUuid) => {
if (!presentBaseVdis.has(baseUuid)) {
throw new Error(`Missing vdi ${baseUuid} which is a base for a delta`)
}
})
// yeah , let's go
}
async _run($defer) { async _run($defer) {
const transferList = await this._computeTransferList(({ mode }) => mode === 'delta') const transferList = await this._computeTransferList(({ mode }) => mode === 'delta')
await this._callWriters(async writer => { await this._callWriters(async writer => {
@@ -64,7 +26,7 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
if (transferList.length > 0) { if (transferList.length > 0) {
for (const metadata of transferList) { for (const metadata of transferList) {
assert.strictEqual(metadata.mode, 'delta') assert.strictEqual(metadata.mode, 'delta')
await this._selectBaseVm(metadata)
await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()') await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, { const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, {
useChain: false, useChain: false,
@@ -88,17 +50,6 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
}), }),
'writer.transfer()' 'writer.transfer()'
) )
// this will update parent name with the needed alias
await this._callWriters(
writer =>
writer.updateUuidAndChain({
isVhdDifferencing,
timestamp: metadata.timestamp,
vdis: incrementalExport.vdis,
}),
'writer.updateUuidAndChain()'
)
await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()') await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
// for healthcheck // for healthcheck
this._tags = metadata.vm.tags this._tags = metadata.vm.tags

View File

@@ -78,18 +78,6 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
'writer.transfer()' 'writer.transfer()'
) )
// we want to control the uuid of the vhd in the chain
// and ensure they are correctly chained
await this._callWriters(
writer =>
writer.updateUuidAndChain({
isVhdDifferencing,
timestamp,
vdis: deltaExport.vdis,
}),
'writer.updateUuidAndChain()'
)
this._baseVm = exportedVm this._baseVm = exportedVm
if (baseVm !== undefined) { if (baseVm !== undefined) {
@@ -145,7 +133,7 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
]) ])
const srcVdi = srcVdis[snapshotOf] const srcVdi = srcVdis[snapshotOf]
if (srcVdi !== undefined) { if (srcVdi !== undefined) {
baseUuidToSrcVdi.set(baseUuid, srcVdi.uuid) baseUuidToSrcVdi.set(baseUuid, srcVdi)
} else { } else {
debug('ignore snapshot VDI because no longer present on VM', { debug('ignore snapshot VDI because no longer present on VM', {
vdi: baseUuid, vdi: baseUuid,
@@ -166,18 +154,18 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
} }
const fullVdisRequired = new Set() const fullVdisRequired = new Set()
baseUuidToSrcVdi.forEach((srcVdiUuid, baseUuid) => { baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
if (presentBaseVdis.has(baseUuid)) { if (presentBaseVdis.has(baseUuid)) {
debug('found base VDI', { debug('found base VDI', {
base: baseUuid, base: baseUuid,
vdi: srcVdiUuid, vdi: srcVdi.uuid,
}) })
} else { } else {
debug('missing base VDI', { debug('missing base VDI', {
base: baseUuid, base: baseUuid,
vdi: srcVdiUuid, vdi: srcVdi.uuid,
}) })
fullVdisRequired.add(srcVdiUuid) fullVdisRequired.add(srcVdi.uuid)
} }
}) })

View File

@@ -1,15 +1,17 @@
import assert from 'node:assert' import assert from 'node:assert'
import mapValues from 'lodash/mapValues.js' import mapValues from 'lodash/mapValues.js'
import ignoreErrors from 'promise-toolbox/ignoreErrors'
import { asyncEach } from '@vates/async-each' import { asyncEach } from '@vates/async-each'
import { asyncMap } from '@xen-orchestra/async-map' import { asyncMap } from '@xen-orchestra/async-map'
import { chainVhd, openVhd } from 'vhd-lib' import { chainVhd, checkVhdChain, openVhd, VhdAbstract } from 'vhd-lib'
import { createLogger } from '@xen-orchestra/log' import { createLogger } from '@xen-orchestra/log'
import { decorateClass } from '@vates/decorate-with' import { decorateClass } from '@vates/decorate-with'
import { defer } from 'golike-defer' import { defer } from 'golike-defer'
import { dirname, basename } from 'node:path' import { dirname } from 'node:path'
import { formatFilenameDate } from '../../_filenameDate.mjs' import { formatFilenameDate } from '../../_filenameDate.mjs'
import { getOldEntries } from '../../_getOldEntries.mjs' import { getOldEntries } from '../../_getOldEntries.mjs'
import { TAG_BASE_DELTA } from '../../_incrementalVm.mjs'
import { Task } from '../../Task.mjs' import { Task } from '../../Task.mjs'
import { MixinRemoteWriter } from './_MixinRemoteWriter.mjs' import { MixinRemoteWriter } from './_MixinRemoteWriter.mjs'
@@ -21,45 +23,42 @@ import { Disposable } from 'promise-toolbox'
const { warn } = createLogger('xo:backups:DeltaBackupWriter') const { warn } = createLogger('xo:backups:DeltaBackupWriter')
export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWriter) { export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWriter) {
#parentVdiPaths
#vhds
async checkBaseVdis(baseUuidToSrcVdi) { async checkBaseVdis(baseUuidToSrcVdi) {
this.#parentVdiPaths = {}
const { handler } = this._adapter const { handler } = this._adapter
const adapter = this._adapter const adapter = this._adapter
const vdisDir = `${this._vmBackupDir}/vdis/${this._job.id}` const vdisDir = `${this._vmBackupDir}/vdis/${this._job.id}`
await asyncMap(baseUuidToSrcVdi, async ([baseUuid, srcVdiUuid]) => { await asyncMap(baseUuidToSrcVdi, async ([baseUuid, srcVdi]) => {
let parentDestPath let found = false
const vhdDir = `${vdisDir}/${srcVdiUuid}`
try { try {
const vhds = await handler.list(vhdDir, { const vhds = await handler.list(`${vdisDir}/${srcVdi.uuid}`, {
filter: _ => _[0] !== '.' && _.endsWith('.vhd'), filter: _ => _[0] !== '.' && _.endsWith('.vhd'),
ignoreMissing: true, ignoreMissing: true,
prependDir: true, prependDir: true,
}) })
const packedBaseUuid = packUuid(baseUuid) const packedBaseUuid = packUuid(baseUuid)
// the last one is probably the right one await asyncMap(vhds, async path => {
for (let i = vhds.length - 1; i >= 0 && parentDestPath === undefined; i--) {
const path = vhds[i]
try { try {
if (await adapter.isMergeableParent(packedBaseUuid, path)) { await checkVhdChain(handler, path)
parentDestPath = path // Warning, this should not be written as found = found || await adapter.isMergeableParent(packedBaseUuid, path)
} //
// since all the checks of a path are done in parallel, found would be containing
// only the last answer of isMergeableParent which is probably not the right one
// this led to the support tickets https://help.vates.fr/#ticket/zoom/4751 , 4729, 4665 and 4300
const isMergeable = await adapter.isMergeableParent(packedBaseUuid, path)
found = found || isMergeable
} catch (error) { } catch (error) {
warn('checkBaseVdis', { error }) warn('checkBaseVdis', { error })
await ignoreErrors.call(VhdAbstract.unlink(handler, path))
} }
} })
} catch (error) { } catch (error) {
warn('checkBaseVdis', { error }) warn('checkBaseVdis', { error })
} }
// no usable parent => the runner will have to decide to fall back to a full or stop backup if (!found) {
if (parentDestPath === undefined) {
baseUuidToSrcVdi.delete(baseUuid) baseUuidToSrcVdi.delete(baseUuid)
} else {
this.#parentVdiPaths[vhdDir] = parentDestPath
} }
}) })
} }
@@ -124,44 +123,6 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
} }
} }
async updateUuidAndChain({ isVhdDifferencing, vdis }) {
assert.notStrictEqual(
this.#vhds,
undefined,
'_transfer must be called before updateUuidAndChain for incremental backups'
)
const parentVdiPaths = this.#parentVdiPaths
const { handler } = this._adapter
const vhds = this.#vhds
await asyncEach(Object.entries(vdis), async ([id, vdi]) => {
const isDifferencing = isVhdDifferencing[`${id}.vhd`]
const path = `${this._vmBackupDir}/${vhds[id]}`
if (isDifferencing) {
assert.notStrictEqual(
parentVdiPaths,
'checkbasevdi must be called before updateUuidAndChain for incremental backups'
)
const parentPath = parentVdiPaths[dirname(path)]
// we are in a incremental backup
// we already computed the chain in checkBaseVdis
assert.notStrictEqual(parentPath, undefined, 'A differential VHD must have a parent')
// forbid any kind of loop
assert.ok(basename(parentPath) < basename(path), `vhd must be sorted to be chained`)
await chainVhd(handler, parentPath, handler, path)
}
// set the correct UUID in the VHD if needed
await Disposable.use(openVhd(handler, path), async vhd => {
if (!vhd.footer.uuid.equals(packUuid(vdi.uuid))) {
vhd.footer.uuid = packUuid(vdi.uuid)
await vhd.readBlockAllocationTable() // required by writeFooter()
await vhd.writeFooter()
}
})
})
}
async _deleteOldEntries() { async _deleteOldEntries() {
const adapter = this._adapter const adapter = this._adapter
const oldEntries = this._oldEntries const oldEntries = this._oldEntries
@@ -180,10 +141,14 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
const jobId = job.id const jobId = job.id
const handler = adapter.handler const handler = adapter.handler
let metadataContent = await this._isAlreadyTransferred(timestamp)
if (metadataContent !== undefined) {
// @todo : should skip backup while being vigilant to not stuck the forked stream
Task.info('This backup has already been transfered')
}
const basename = formatFilenameDate(timestamp) const basename = formatFilenameDate(timestamp)
// update this.#vhds before eventually skipping transfer, so that const vhds = mapValues(
// updateUuidAndChain has all the mandatory data
const vhds = (this.#vhds = mapValues(
deltaExport.vdis, deltaExport.vdis,
vdi => vdi =>
`vdis/${jobId}/${ `vdis/${jobId}/${
@@ -193,15 +158,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
vdi.uuid vdi.uuid
: vdi.$snapshot_of$uuid : vdi.$snapshot_of$uuid
}/${adapter.getVhdFileName(basename)}` }/${adapter.getVhdFileName(basename)}`
)) )
let metadataContent = await this._isAlreadyTransferred(timestamp)
if (metadataContent !== undefined) {
// skip backup while being vigilant to not stuck the forked stream
Task.info('This backup has already been transfered')
Object.values(deltaExport.streams).forEach(stream => stream.destroy())
return { size: 0 }
}
metadataContent = { metadataContent = {
isVhdDifferencing, isVhdDifferencing,
@@ -217,13 +174,38 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
vm, vm,
vmSnapshot, vmSnapshot,
} }
const { size } = await Task.run({ name: 'transfer' }, async () => { const { size } = await Task.run({ name: 'transfer' }, async () => {
let transferSize = 0 let transferSize = 0
await asyncEach( await asyncEach(
Object.keys(deltaExport.vdis), Object.entries(deltaExport.vdis),
async id => { async ([id, vdi]) => {
const path = `${this._vmBackupDir}/${vhds[id]}` const path = `${this._vmBackupDir}/${vhds[id]}`
const isDifferencing = isVhdDifferencing[`${id}.vhd`]
let parentPath
if (isDifferencing) {
const vdiDir = dirname(path)
parentPath = (
await handler.list(vdiDir, {
filter: filename => filename[0] !== '.' && filename.endsWith('.vhd'),
prependDir: true,
})
)
.sort()
.pop()
assert.notStrictEqual(
parentPath,
undefined,
`missing parent of ${id} in ${dirname(path)}, looking for ${vdi.other_config[TAG_BASE_DELTA]}`
)
parentPath = parentPath.slice(1) // remove leading slash
// TODO remove when this has been done before the export
await checkVhd(handler, parentPath)
}
// don't write it as transferSize += await async function // don't write it as transferSize += await async function
// since i += await asyncFun lead to race condition // since i += await asyncFun lead to race condition
// as explained : https://eslint.org/docs/latest/rules/require-atomic-updates // as explained : https://eslint.org/docs/latest/rules/require-atomic-updates
@@ -235,6 +217,17 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
writeBlockConcurrency: this._config.writeBlockConcurrency, writeBlockConcurrency: this._config.writeBlockConcurrency,
}) })
transferSize += transferSizeOneDisk transferSize += transferSizeOneDisk
// set the correct UUID in the VHD
await Disposable.use(openVhd(handler, path), async vhd => {
vhd.footer.uuid = packUuid(vdi.uuid)
await vhd.readBlockAllocationTable() // required by writeFooter()
await vhd.writeFooter()
})
if (isDifferencing) {
await chainVhd(handler, parentPath, handler, path)
}
}, },
{ {
concurrency: settings.diskPerVmConcurrency, concurrency: settings.diskPerVmConcurrency,

View File

@@ -1,4 +1,3 @@
import assert from 'node:assert'
import { asyncMap, asyncMapSettled } from '@xen-orchestra/async-map' import { asyncMap, asyncMapSettled } from '@xen-orchestra/async-map'
import ignoreErrors from 'promise-toolbox/ignoreErrors' import ignoreErrors from 'promise-toolbox/ignoreErrors'
import { formatDateTime } from '@xen-orchestra/xapi' import { formatDateTime } from '@xen-orchestra/xapi'
@@ -15,7 +14,6 @@ import find from 'lodash/find.js'
export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) { export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
async checkBaseVdis(baseUuidToSrcVdi, baseVm) { async checkBaseVdis(baseUuidToSrcVdi, baseVm) {
assert.notStrictEqual(baseVm, undefined)
const sr = this._sr const sr = this._sr
const replicatedVm = listReplicatedVms(sr.$xapi, this._job.id, sr.uuid, this._vmUuid).find( const replicatedVm = listReplicatedVms(sr.$xapi, this._job.id, sr.uuid, this._vmUuid).find(
vm => vm.other_config[TAG_COPY_SRC] === baseVm.uuid vm => vm.other_config[TAG_COPY_SRC] === baseVm.uuid
@@ -38,9 +36,7 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
} }
} }
} }
updateUuidAndChain() {
// nothing to do, the chaining is not modified in this case
}
prepare({ isFull }) { prepare({ isFull }) {
// create the task related to this export and ensure all methods are called in this context // create the task related to this export and ensure all methods are called in this context
const task = new Task({ const task = new Task({

View File

@@ -5,10 +5,6 @@ export class AbstractIncrementalWriter extends AbstractWriter {
throw new Error('Not implemented') throw new Error('Not implemented')
} }
updateUuidAndChain() {
throw new Error('Not implemented')
}
cleanup() { cleanup() {
throw new Error('Not implemented') throw new Error('Not implemented')
} }

View File

@@ -113,13 +113,13 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
) )
} }
async _isAlreadyTransferred(timestamp) { _isAlreadyTransferred(timestamp) {
const vmUuid = this._vmUuid const vmUuid = this._vmUuid
const adapter = this._adapter const adapter = this._adapter
const backupDir = getVmBackupDir(vmUuid) const backupDir = getVmBackupDir(vmUuid)
try { try {
const actualMetadata = JSON.parse( const actualMetadata = JSON.parse(
await adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`) adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
) )
return actualMetadata return actualMetadata
} catch (error) {} } catch (error) {}

View File

@@ -230,7 +230,6 @@ Settings are described in [`@xen-orchestra/backups/\_runners/VmsXapi.mjs``](http
- `checkBaseVdis(baseUuidToSrcVdi, baseVm)` - `checkBaseVdis(baseUuidToSrcVdi, baseVm)`
- `prepare({ isFull })` - `prepare({ isFull })`
- `transfer({ timestamp, deltaExport, sizeContainers })` - `transfer({ timestamp, deltaExport, sizeContainers })`
- `updateUuidAndChain({ isVhdDifferencing, vdis })`
- `cleanup()` - `cleanup()`
- `healthCheck()` // is not executed if no health check sr or tag doesn't match - `healthCheck()` // is not executed if no health check sr or tag doesn't match
- **Full** - **Full**

View File

@@ -20,7 +20,5 @@ export function split(path) {
return parts return parts
} }
// paths are made absolute otherwise fs.relative() would resolve them against working directory export const relativeFromFile = (file, path) => relative(dirname(file), path)
export const relativeFromFile = (file, path) => relative(dirname(normalize(file)), normalize(path))
export const resolveFromFile = (file, path) => resolve('/', dirname(file), path).slice(1) export const resolveFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)

View File

@@ -1,17 +0,0 @@
import { describe, it } from 'test'
import { strict as assert } from 'assert'
import { relativeFromFile } from './path.js'
describe('relativeFromFile()', function () {
for (const [title, args] of Object.entries({
'file absolute and path absolute': ['/foo/bar/file.vhd', '/foo/baz/path.vhd'],
'file relative and path absolute': ['foo/bar/file.vhd', '/foo/baz/path.vhd'],
'file absolute and path relative': ['/foo/bar/file.vhd', 'foo/baz/path.vhd'],
'file relative and path relative': ['foo/bar/file.vhd', 'foo/baz/path.vhd'],
})) {
it('works with ' + title, function () {
assert.equal(relativeFromFile(...args), '../baz/path.vhd')
})
}
})

View File

@@ -54,10 +54,10 @@ async function handleExistingFile(root, indexPath, path) {
await indexFile(fullPath, indexPath) await indexFile(fullPath, indexPath)
} }
} }
} catch (error) { } catch (err) {
if (error.code !== 'EEXIST') { if (err.code !== 'EEXIST') {
// there can be a symbolic link in the tree // there can be a symbolic link in the tree
warn('handleExistingFile', { error }) warn('handleExistingFile', err)
} }
} }
} }
@@ -106,7 +106,7 @@ export async function watchRemote(remoteId, { root, immutabilityDuration, rebuil
await File.liftImmutability(settingPath) await File.liftImmutability(settingPath)
} catch (error) { } catch (error) {
// file may not exists, and it's not really a problem // file may not exists, and it's not really a problem
info('lifting immutability on current settings', { error }) info('lifting immutability on current settings', error)
} }
await fs.writeFile( await fs.writeFile(
settingPath, settingPath,

View File

@@ -50,17 +50,7 @@ const RRD_POINTS_PER_STEP: { [key in RRD_STEP]: number } = {
// Utils // Utils
// ------------------------------------------------------------------- // -------------------------------------------------------------------
function parseNumber(value: number | string) { function convertNanToNull(value: number) {
// Starting from XAPI 23.31, numbers in the JSON payload are encoded as
// strings to support NaN, Infinity and -Infinity
if (typeof value === 'string') {
const asNumber = +value
if (isNaN(asNumber) && value !== 'NaN') {
throw new Error('cannot parse number: ' + value)
}
value = asNumber
}
return isNaN(value) ? null : value return isNaN(value) ? null : value
} }
@@ -69,7 +59,7 @@ function parseNumber(value: number | string) {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
const computeValues = (dataRow: any, legendIndex: number, transformValue = identity) => const computeValues = (dataRow: any, legendIndex: number, transformValue = identity) =>
map(dataRow, ({ values }) => transformValue(parseNumber(values[legendIndex]))) map(dataRow, ({ values }) => transformValue(convertNanToNull(values[legendIndex])))
const createGetProperty = (obj: object, property: string, defaultValue: unknown) => const createGetProperty = (obj: object, property: string, defaultValue: unknown) =>
defaults(obj, { [property]: defaultValue })[property] as any defaults(obj, { [property]: defaultValue })[property] as any
@@ -329,14 +319,8 @@ export default class XapiStats {
}, },
abortSignal, abortSignal,
}) })
const text = await resp.text() // eslint-disable-next-line import/no-named-as-default-member -- https://github.com/json5/json5/issues/287
try { return JSON5.parse(await resp.text())
// starting from XAPI 23.31, the response is valid JSON
return JSON.parse(text)
} catch (error) {
// eslint-disable-next-line import/no-named-as-default-member -- https://github.com/json5/json5/issues/287
return JSON5.parse(text)
}
} }
// To avoid multiple requests, we keep a cache for the stats and // To avoid multiple requests, we keep a cache for the stats and
@@ -399,10 +383,7 @@ export default class XapiStats {
abortSignal, abortSignal,
}) })
const actualStep = parseNumber(json.meta.step) const actualStep = json.meta.step as number
if (actualStep !== step) {
throw new FaultyGranularity(`Unable to get the true granularity: ${actualStep}`)
}
if (json.data.length > 0) { if (json.data.length > 0) {
// fetched data is organized from the newest to the oldest // fetched data is organized from the newest to the oldest
@@ -426,15 +407,14 @@ export default class XapiStats {
let stepStats = xoObjectStats[actualStep] let stepStats = xoObjectStats[actualStep]
let cacheStepStats = cacheXoObjectStats[actualStep] let cacheStepStats = cacheXoObjectStats[actualStep]
const endTimestamp = parseNumber(json.meta.end) if (stepStats === undefined || stepStats.endTimestamp !== json.meta.end) {
if (stepStats === undefined || stepStats.endTimestamp !== endTimestamp) {
stepStats = xoObjectStats[actualStep] = { stepStats = xoObjectStats[actualStep] = {
endTimestamp, endTimestamp: json.meta.end,
interval: actualStep, interval: actualStep,
canBeExpired: false, canBeExpired: false,
} }
cacheStepStats = cacheXoObjectStats[actualStep] = { cacheStepStats = cacheXoObjectStats[actualStep] = {
endTimestamp, endTimestamp: json.meta.end,
interval: actualStep, interval: actualStep,
canBeExpired: true, canBeExpired: true,
} }
@@ -458,6 +438,10 @@ export default class XapiStats {
}) })
}) })
} }
if (actualStep !== step) {
throw new FaultyGranularity(`Unable to get the true granularity: ${actualStep}`)
}
} catch (error) { } catch (error) {
if (error instanceof Error && error.name === 'AbortError') { if (error instanceof Error && error.name === 'AbortError') {
return return

View File

@@ -1,15 +1,13 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "../web-core/lib/**/*", "../web-core/lib/**/*.vue"], "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"noEmit": true, "noEmit": true,
"baseUrl": ".", "baseUrl": ".",
"rootDir": "..",
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"]
"@core/*": ["../web-core/lib/*"]
} }
} }
} }

View File

@@ -23,7 +23,6 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),
'@core': fileURLToPath(new URL('../web-core/lib', import.meta.url)),
}, },
}, },

View File

@@ -27,16 +27,6 @@ log.error('could not join server', {
}) })
``` ```
A logging method has the following signature:
```ts
interface LoggingMethod {
(error): void
(message: string, data?: { error?: Error; [property: string]: any }): void
}
```
### Consumer ### Consumer
Then, at application level, configure the logs are handled: Then, at application level, configure the logs are handled:

View File

@@ -45,16 +45,6 @@ log.error('could not join server', {
}) })
``` ```
A logging method has the following signature:
```ts
interface LoggingMethod {
(error): void
(message: string, data?: { error?: Error; [property: string]: any }): void
}
```
### Consumer ### Consumer
Then, at application level, configure the logs are handled: Then, at application level, configure the logs are handled:

View File

@@ -10,8 +10,7 @@
} }
}, },
"devDependencies": { "devDependencies": {
"vue": "^3.4.13", "vue": "^3.4.13"
"@vue/tsconfig": "^0.5.1"
}, },
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/web-core", "homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/web-core",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues", "bugs": "https://github.com/vatesfr/xen-orchestra/issues",
@@ -26,6 +25,6 @@
}, },
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"engines": { "engines": {
"node": ">=18" "node": ">=8.10"
} }
} }

View File

@@ -1,12 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "lib/**/*", "lib/**/*.vue"],
"exclude": ["lib/**/__tests__/*"],
"compilerOptions": {
"noEmit": true,
"baseUrl": ".",
"paths": {
"@core/*": ["./lib/*"]
}
}
}

View File

@@ -1,22 +1,13 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [ "include": ["env.d.ts", "typed-router.d.ts", "src/**/*", "src/**/*.vue"],
"env.d.ts",
"typed-router.d.ts",
"src/**/*",
"src/**/*.vue",
"../web-core/lib/**/*",
"../web-core/lib/**/*.vue"
],
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"noEmit": true, "noEmit": true,
"baseUrl": ".", "baseUrl": ".",
"rootDir": "..",
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"]
"@core/*": ["../web-core/lib/*"]
} }
} }
} }

View File

@@ -11,7 +11,6 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),
'@core': fileURLToPath(new URL('../web-core/lib', import.meta.url)),
}, },
}, },
}) })

View File

@@ -7,4 +7,3 @@ export { default as VDI } from './vdi.mjs'
export { default as VIF } from './vif.mjs' export { default as VIF } from './vif.mjs'
export { default as VM } from './vm.mjs' export { default as VM } from './vm.mjs'
export { default as VTPM } from './vtpm.mjs' export { default as VTPM } from './vtpm.mjs'
export { default as VUSB } from './vusb.mjs'

View File

@@ -21,23 +21,12 @@ export default class Vif {
MAC = '', MAC = '',
} = {} } = {}
) { ) {
if (device === undefined) {
const allowedDevices = await this.call('VM.get_allowed_VIF_devices', VM)
if (allowedDevices.length === 0) {
const error = new Error('could not find an allowed VIF device')
error.poolUuid = this.pool.uuid
error.vmRef = VM
throw error
}
device = allowedDevices[0]
}
const [powerState, ...rest] = await Promise.all([ const [powerState, ...rest] = await Promise.all([
this.getField('VM', VM, 'power_state'), this.getField('VM', VM, 'power_state'),
MTU ?? this.getField('network', network, 'MTU'), device ?? (await this.call('VM.get_allowed_VIF_devices', VM))[0],
MTU ?? (await this.getField('network', network, 'MTU')),
]) ])
;[MTU] = rest ;[device, MTU] = rest
const vifRef = await this.call('VIF.create', { const vifRef = await this.call('VIF.create', {
currently_attached: powerState === 'Suspended' ? currently_attached : undefined, currently_attached: powerState === 'Suspended' ? currently_attached : undefined,

View File

@@ -1,16 +0,0 @@
import ignoreErrors from 'promise-toolbox/ignoreErrors'
export default class Vusb {
async create(VM, USB_group) {
return this.call('VUSB.create', VM, USB_group)
}
async unplug(ref) {
await this.call('VUSB.unplug', ref)
}
async destroy(ref) {
await ignoreErrors.call(this.VUSB_unplug(ref))
await this.call('VUSB.destroy', ref)
}
}

View File

@@ -8,11 +8,6 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it” > Users must be able to say: “Nice enhancement, I'm eager to test it”
- Disable search engine indexing via a `robots.txt` - Disable search engine indexing via a `robots.txt`
- [Stats] Support format used by XAPI 23.31
- [REST API] Export host [SMT](https://en.wikipedia.org/wiki/Simultaneous_multithreading) status at `/hosts/:id/smt` [Forum#71374](https://xcp-ng.org/forum/post/71374)
- [Home & REST API] `$container` field of an halted VM now points to a host if a VDI is on a local storage [Forum#71769](https://xcp-ng.org/forum/post/71769)
- [Size Input] Ability to select two new units in the dropdown (`TiB`, `PiB`) (PR [#7382](https://github.com/vatesfr/xen-orchestra/pull/7382))
### Bug fixes ### Bug fixes
@@ -21,8 +16,6 @@
- [Settings/XO Config] Sort backups from newest to oldest - [Settings/XO Config] Sort backups from newest to oldest
- [Plugins/audit] Don't log `tag.getAllConfigured` calls - [Plugins/audit] Don't log `tag.getAllConfigured` calls
- [Remotes] Correctly clear error when the remote is tested with success - [Remotes] Correctly clear error when the remote is tested with success
- [Import/VMWare] Fix importing last snapshot (PR [#7370](https://github.com/vatesfr/xen-orchestra/pull/7370))
- [Host/Reboot] Fix false positive warning when restarting an host after updates (PR [#7366](https://github.com/vatesfr/xen-orchestra/pull/7366))
### Packages to release ### Packages to release
@@ -40,12 +33,8 @@
<!--packages-start--> <!--packages-start-->
- @xen-orchestra/backups patch - xo-server patch
- @xen-orchestra/fs patch
- @xen-orchestra/xapi patch
- vhd-lib patch
- xo-server minor
- xo-server-audit patch - xo-server-audit patch
- xo-web minor - xo-web patch
<!--packages-end--> <!--packages-end-->

View File

@@ -93,21 +93,6 @@ Follow the instructions:
You can also download XOA from xen-orchestra.com in an XVA file. Once you've got the XVA file, you can import it with `xe vm-import filename=xoa_unified.xva` or via XenCenter. You can also download XOA from xen-orchestra.com in an XVA file. Once you've got the XVA file, you can import it with `xe vm-import filename=xoa_unified.xva` or via XenCenter.
If you want to use static IP address for your appliance:
```sh
xe vm-param-set uuid="$uuid" \
xenstore-data:vm-data/ip="$ip" \
xenstore-data:vm-data/netmask="$netmask" \
xenstore-data:vm-data/gateway="$gateway"
```
If you want to replace the default DNS server:
```sh
xe vm-param-set uuid="$uuid" xenstore-data:vm-data/dns="$dns"
```
After the VM is imported, you just need to start it with `xe vm-start vm="XOA"` or with XenCenter. After the VM is imported, you just need to start it with `xe vm-start vm="XOA"` or with XenCenter.
## First console connection ## First console connection

View File

@@ -96,7 +96,7 @@
}, },
"private": true, "private": true,
"scripts": { "scripts": {
"build": "TURBO_TELEMETRY_DISABLED=1 turbo run build --scope xo-server --scope xo-server-'*' --scope xo-web", "build": "turbo run build --scope xo-server --scope xo-server-'*' --scope xo-web",
"build:xo-lite": "turbo run build --scope @xen-orchestra/lite", "build:xo-lite": "turbo run build --scope @xen-orchestra/lite",
"clean": "scripts/run-script.js --parallel clean", "clean": "scripts/run-script.js --parallel clean",
"dev": "scripts/run-script.js --parallel --concurrency 0 --verbose dev", "dev": "scripts/run-script.js --parallel --concurrency 0 --verbose dev",

View File

@@ -46,9 +46,9 @@ const VhdSynthetic = class VhdSynthetic extends VhdAbstract {
} }
get compressionType() { get compressionType() {
const compressionType = this.#vhds[0].compressionType const compressionType = this.vhds[0].compressionType
for (let i = 0; i < this.#vhds.length; i++) { for (let i = 0; i < this.vhds.length; i++) {
if (compressionType !== this.#vhds[i].compressionType) { if (compressionType !== this.vhds[i].compressionType) {
return 'MIXED' return 'MIXED'
} }
} }

View File

@@ -1,6 +1,6 @@
'use strict' 'use strict'
const { relativeFromFile } = require('@xen-orchestra/fs/path') const { dirname, relative } = require('path')
const { openVhd } = require('./openVhd') const { openVhd } = require('./openVhd')
const { DISK_TYPES } = require('./_constants') const { DISK_TYPES } = require('./_constants')
@@ -21,7 +21,7 @@ module.exports = async function chain(parentHandler, parentPath, childHandler, c
} }
await childVhd.readBlockAllocationTable() await childVhd.readBlockAllocationTable()
const parentName = relativeFromFile(childPath, parentPath) const parentName = relative(dirname(childPath), parentPath)
header.parentUuid = parentVhd.footer.uuid header.parentUuid = parentVhd.footer.uuid
header.parentUnicodeName = parentName header.parentUnicodeName = parentName
await childVhd.setUniqueParentLocator(parentName) await childVhd.setUniqueParentLocator(parentName)

View File

@@ -27,7 +27,7 @@ async function sendToNagios(app, jobName, vmBackupInfo) {
jobName jobName
) )
} catch (error) { } catch (error) {
warn('sendToNagios:', { error }) warn('sendToNagios:', error)
} }
} }

View File

@@ -1,4 +1,3 @@
import semver from 'semver'
import { createLogger } from '@xen-orchestra/log' import { createLogger } from '@xen-orchestra/log'
import assert from 'assert' import assert from 'assert'
import { format } from 'json-rpc-peer' import { format } from 'json-rpc-peer'
@@ -137,38 +136,13 @@ export async function restart({
const pool = this.getObject(host.$poolId, 'pool') const pool = this.getObject(host.$poolId, 'pool')
const master = this.getObject(pool.master, 'host') const master = this.getObject(pool.master, 'host')
const hostRebootRequired = host.rebootRequired const hostRebootRequired = host.rebootRequired
if (hostRebootRequired && host.id !== master.id && host.version === master.version) {
// we are currently in an host upgrade process throw incorrectState({
if (hostRebootRequired && host.id !== master.id) { actual: hostRebootRequired,
// this error is not ideal but it means that the pool master must be fully upgraded/rebooted before the current host can be rebooted. expected: false,
// object: master.id,
// there is a single error for the 3 cases because the client must handle them the same way property: 'rebootRequired',
const throwError = () => })
incorrectState({
actual: hostRebootRequired,
expected: false,
object: master.id,
property: 'rebootRequired',
})
if (semver.lt(master.version, host.version)) {
log.error(`master version (${master.version}) is older than the host version (${host.version})`, {
masterId: master.id,
hostId: host.id,
})
throwError()
}
if (semver.eq(master.version, host.version)) {
if ((await this.getXapi(host).listMissingPatches(master._xapiId)).length > 0) {
log.error('master has missing patches', { masterId: master.id })
throwError()
}
if (master.rebootRequired) {
log.error('master needs to reboot')
throwError()
}
}
} }
} }

View File

@@ -1,42 +0,0 @@
// Creates a VUSB which will be plugged to the VM at its next restart
// Only one VUSB can be attached to a given USB_group, and up to six VUSB can be attached to a VM.
export async function create({ vm, usbGroup }) {
const xapi = this.getXapi(vm)
const vusbRef = await xapi.VUSB_create(vm._xapiRef, usbGroup._xapiRef)
return xapi.getField('VUSB', vusbRef, 'uuid')
}
create.params = {
vmId: { type: 'string' },
usbGroupId: { type: 'string' },
}
create.resolve = {
vm: ['vmId', 'VM', 'administrate'],
usbGroup: ['usbGroupId', 'USB_group', 'administrate'],
}
// Unplug VUSB until next VM restart
export async function unplug({ vusb }) {
await this.getXapi(vusb).VUSB_unplug(vusb._xapiRef)
}
unplug.params = {
id: { type: 'string' },
}
unplug.resolve = {
vusb: ['id', 'VUSB', 'administrate'],
}
export async function destroy({ vusb }) {
await this.getXapi(vusb).VUSB_destroy(vusb._xapiRef)
}
destroy.params = {
id: { type: 'string' },
}
destroy.resolve = {
vusb: ['id', 'VUSB', 'administrate'],
}

View File

@@ -328,34 +328,6 @@ const TRANSFORMS = {
const { creation } = xoData.extract(obj) ?? {} const { creation } = xoData.extract(obj) ?? {}
let $container
if (obj.resident_on !== 'OpaqueRef:NULL') {
// resident_on is set when the VM is running (or paused or suspended on a host)
$container = link(obj, 'resident_on')
} else {
// if the VM is halted, the $container is the pool
$container = link(obj, 'pool')
// unless one of its VDI is on a non shared SR
//
// linked objects may not be there when this code run, and it will only be
// refreshed when the VM XAPI record change, this value is not guaranteed
// to be up-to-date, but it practice it appears to work fine thanks to
// `VBDs` and `current_operations` changing when a VDI is
// added/removed/migrated
for (const vbd of obj.$VBDs) {
const sr = vbd?.$VDI?.$SR
if (sr !== undefined && !sr.shared) {
const pbd = sr.$PBDs[0]
const hostId = pbd && link(pbd, 'host')
if (hostId !== undefined) {
$container = hostId
break
}
}
}
}
const vm = { const vm = {
// type is redefined after for controllers/, templates & // type is redefined after for controllers/, templates &
// snapshots. // snapshots.
@@ -450,7 +422,8 @@ const TRANSFORMS = {
xenTools, xenTools,
...getVmGuestToolsProps(obj), ...getVmGuestToolsProps(obj),
$container, // TODO: handle local VMs (`VM.get_possible_hosts()`).
$container: isRunning ? link(obj, 'resident_on') : link(obj, 'pool'),
$VBDs: link(obj, 'VBDs'), $VBDs: link(obj, 'VBDs'),
// TODO: dedupe // TODO: dedupe
@@ -882,8 +855,6 @@ const TRANSFORMS = {
} }
}, },
// -----------------------------------------------------------------
vtpm(obj) { vtpm(obj) {
return { return {
type: 'VTPM', type: 'VTPM',
@@ -891,32 +862,6 @@ const TRANSFORMS = {
vm: link(obj, 'VM'), vm: link(obj, 'VM'),
} }
}, },
// -----------------------------------------------------------------
vusb(obj) {
return {
type: 'VUSB',
vm: link(obj, 'VM'),
currentlyAttached: obj.currently_attached,
usbGroup: link(obj, 'USB_group'),
}
},
// -----------------------------------------------------------------
usb_group(obj) {
return {
type: 'USB_group',
PUSB: link(obj, 'PUSBs'),
VUSB: link(obj, 'VUSBs'),
nameDescription: obj.name_description,
nameLabel: obj.name_label,
otherConfig: obj.other_config,
}
},
} }
// =================================================================== // ===================================================================

View File

@@ -45,17 +45,7 @@ const RRD_POINTS_PER_STEP = {
// Utils // Utils
// ------------------------------------------------------------------- // -------------------------------------------------------------------
function parseNumber(value) { function convertNanToNull(value) {
// Starting from XAPI 23.31, numbers in the JSON payload are encoded as
// strings to support NaN, Infinity and -Infinity
if (typeof value === 'string') {
const asNumber = +value
if (isNaN(asNumber) && value !== 'NaN') {
throw new Error('cannot parse number: ' + value)
}
value = asNumber
}
return isNaN(value) ? null : value return isNaN(value) ? null : value
} }
@@ -68,7 +58,7 @@ async function getServerTimestamp(xapi, hostRef) {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
const computeValues = (dataRow, legendIndex, transformValue = identity) => const computeValues = (dataRow, legendIndex, transformValue = identity) =>
map(dataRow, ({ values }) => transformValue(parseNumber(values[legendIndex]))) map(dataRow, ({ values }) => transformValue(convertNanToNull(values[legendIndex])))
const combineStats = (stats, path, combineValues) => zipWith(...map(stats, path), (...values) => combineValues(values)) const combineStats = (stats, path, combineValues) => zipWith(...map(stats, path), (...values) => combineValues(values))
@@ -255,15 +245,7 @@ export default class XapiStats {
start: timestamp, start: timestamp,
}, },
}) })
.then(response => response.text()) .then(response => response.text().then(JSON5.parse))
.then(data => {
try {
// starting from XAPI 23.31, the response is valid JSON
return JSON.parse(data)
} catch (_) {
return JSON5.parse(data)
}
})
.catch(err => { .catch(err => {
delete this.#hostCache[hostUuid][step] delete this.#hostCache[hostUuid][step]
throw err throw err
@@ -317,7 +299,7 @@ export default class XapiStats {
// To avoid crossing over the boundary, we ask for one less step // To avoid crossing over the boundary, we ask for one less step
const optimumTimestamp = currentTimeStamp - maxDuration + step const optimumTimestamp = currentTimeStamp - maxDuration + step
const json = await this._getJson(xapi, host, optimumTimestamp, step) const json = await this._getJson(xapi, host, optimumTimestamp, step)
const actualStep = parseNumber(json.meta.step) const actualStep = json.meta.step
if (actualStep !== step) { if (actualStep !== step) {
throw new FaultyGranularity(`Unable to get the true granularity: ${actualStep}`) throw new FaultyGranularity(`Unable to get the true granularity: ${actualStep}`)
@@ -344,10 +326,9 @@ export default class XapiStats {
return return
} }
const endTimestamp = parseNumber(json.meta.end) if (stepStats === undefined || stepStats.endTimestamp !== json.meta.end) {
if (stepStats === undefined || stepStats.endTimestamp !== endTimestamp) {
stepStats = { stepStats = {
endTimestamp, endTimestamp: json.meta.end,
interval: actualStep, interval: actualStep,
stats: {}, stats: {},
} }

View File

@@ -280,7 +280,7 @@ export default class MigrateVm {
const stream = vhd.stream() const stream = vhd.stream()
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD }) await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
} }
return { vdi, vhd } return vhd
}) })
) )
) )

View File

@@ -1,6 +1,5 @@
import asyncMapSettled from '@xen-orchestra/async-map/legacy.js' import asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
import { basename } from 'path' import { basename } from 'path'
import { createLogger } from '@xen-orchestra/log'
import { format, parse } from 'xo-remote-parser' import { format, parse } from 'xo-remote-parser'
import { import {
DEFAULT_ENCRYPTION_ALGORITHM, DEFAULT_ENCRYPTION_ALGORITHM,
@@ -18,35 +17,17 @@ import { Remotes } from '../models/remote.mjs'
// =================================================================== // ===================================================================
const { warn } = createLogger('xo:mixins:remotes')
const obfuscateRemote = ({ url, ...remote }) => { const obfuscateRemote = ({ url, ...remote }) => {
const parsedUrl = parse(url) const parsedUrl = parse(url)
remote.url = format(sensitiveValues.obfuscate(parsedUrl)) remote.url = format(sensitiveValues.obfuscate(parsedUrl))
return remote return remote
} }
// these properties should be defined on the remote object itself and not as function validatePath(url) {
// part of the remote URL const { path } = parse(url)
//
// there is a bug somewhere that keep putting them into the URL, this list
// is here to help track it
const INVALID_URL_PARAMS = ['benchmarks', 'id', 'info', 'name', 'proxy', 'enabled', 'error', 'url']
function validateUrl(url) {
const parsedUrl = parse(url)
const { path } = parsedUrl
if (path !== undefined && basename(path) === 'xo-vm-backups') { if (path !== undefined && basename(path) === 'xo-vm-backups') {
throw invalidParameters('remote url should not end with xo-vm-backups') throw invalidParameters('remote url should not end with xo-vm-backups')
} }
for (const param of INVALID_URL_PARAMS) {
if (Object.hasOwn(parsedUrl, param)) {
// log with stack trace
warn(new Error('invalid remote URL param ' + param))
}
}
} }
export default class { export default class {
@@ -201,22 +182,6 @@ export default class {
if (remote === undefined) { if (remote === undefined) {
throw noSuchObject(id, 'remote') throw noSuchObject(id, 'remote')
} }
const parsedUrl = parse(remote.url)
let fixed = false
for (const param of INVALID_URL_PARAMS) {
if (Object.hasOwn(parsedUrl, param)) {
// delete the value to trace its real origin when it's added back
// with `updateRemote()`
delete parsedUrl[param]
fixed = true
}
}
if (fixed) {
remote.url = format(parsedUrl)
this._remotes.update(remote).catch(warn)
}
return remote return remote
} }
@@ -237,7 +202,7 @@ export default class {
} }
async createRemote({ name, options, proxy, url }) { async createRemote({ name, options, proxy, url }) {
validateUrl(url) validatePath(url)
const params = { const params = {
enabled: false, enabled: false,
@@ -254,10 +219,6 @@ export default class {
} }
updateRemote(id, { enabled, name, options, proxy, url }) { updateRemote(id, { enabled, name, options, proxy, url }) {
if (url !== undefined) {
validateUrl(url)
}
const handlers = this._handlers const handlers = this._handlers
const handler = handlers[id] const handler = handlers[id]
if (handler !== undefined) { if (handler !== undefined) {
@@ -277,7 +238,7 @@ export default class {
@synchronized() @synchronized()
async _updateRemote(id, { url, ...props }) { async _updateRemote(id, { url, ...props }) {
if (url !== undefined) { if (url !== undefined) {
validateUrl(url) validatePath(url)
} }
const remote = await this._getRemote(id) const remote = await this._getRemote(id)

View File

@@ -253,10 +253,6 @@ export default class RestApi {
const host = req.xapiObject const host = req.xapiObject
res.json(await host.$xapi.listMissingPatches(host)) res.json(await host.$xapi.listMissingPatches(host))
}, },
async smt({ xapiObject }, res) {
res.json({ enabled: await xapiObject.$xapi.isHyperThreadingEnabled(xapiObject.$id) })
},
} }
collections.pools.routes = { collections.pools.routes = {

View File

@@ -138,7 +138,7 @@ export class Range extends Component {
export Toggle from './toggle' export Toggle from './toggle'
const UNITS = ['kiB', 'MiB', 'GiB', 'TiB', 'PiB'] const UNITS = ['kiB', 'MiB', 'GiB']
const DEFAULT_UNIT = 'GiB' const DEFAULT_UNIT = 'GiB'
export class SizeInput extends BaseComponent { export class SizeInput extends BaseComponent {

View File

@@ -75,7 +75,7 @@ export const reportOnSupportPanel = async ({ files = [], formatMessage = identit
ADDITIONAL_FILES.map(({ fetch, name }) => ADDITIONAL_FILES.map(({ fetch, name }) =>
timeout.call(fetch(), ADDITIONAL_FILES_FETCH_TIMEOUT).then( timeout.call(fetch(), ADDITIONAL_FILES_FETCH_TIMEOUT).then(
file => formData.append('attachments', createBlobFromString(file), name), file => formData.append('attachments', createBlobFromString(file), name),
error => logger.warn(`cannot get ${name}`, { error }) error => logger.warn(`cannot get ${name}`, error)
) )
) )
) )

2448
yarn.lock

File diff suppressed because it is too large Load Diff