Compare commits
16 Commits
rest-api-v
...
xo6/improv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d46fa9e3e | ||
|
|
e8eb2fe6a7 | ||
|
|
aefcce45ff | ||
|
|
367fb4d8a6 | ||
|
|
e54a0bfc80 | ||
|
|
9e5541703b | ||
|
|
039d5687c0 | ||
|
|
b89195eb80 | ||
|
|
822cdc3fb8 | ||
|
|
c7b5b715a3 | ||
|
|
56b427c09c | ||
|
|
0e45c52bbc | ||
|
|
4fd2b91fc4 | ||
|
|
7890320a7d | ||
|
|
1718649e0c | ||
|
|
7fc5d62ca9 |
@@ -191,13 +191,14 @@ export class RemoteAdapter {
|
||||
// check if we will be allowed to merge a a vhd created in this adapter
|
||||
// with the vhd at path `path`
|
||||
async isMergeableParent(packedParentUid, path) {
|
||||
return await Disposable.use(openVhd(this.handler, path), vhd => {
|
||||
return await Disposable.use(VhdSynthetic.fromVhdChain(this.handler, path), vhd => {
|
||||
// this baseUuid is not linked with this vhd
|
||||
if (!vhd.footer.uuid.equals(packedParentUid)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isVhdDirectory = vhd instanceof VhdDirectory
|
||||
// check if all the chain is composed of vhd directory
|
||||
const isVhdDirectory = vhd.checkVhdsClass(VhdDirectory)
|
||||
return isVhdDirectory
|
||||
? this.useVhdDirectory() && this.#getCompressionType() === vhd.compressionType
|
||||
: !this.useVhdDirectory()
|
||||
|
||||
@@ -2,6 +2,7 @@ import { asyncEach } from '@vates/async-each'
|
||||
import { decorateMethodsWith } from '@vates/decorate-with'
|
||||
import { defer } from 'golike-defer'
|
||||
import assert from 'node:assert'
|
||||
import * as UUID from 'uuid'
|
||||
import isVhdDifferencingDisk from 'vhd-lib/isVhdDifferencingDisk.js'
|
||||
import mapValues from 'lodash/mapValues.js'
|
||||
|
||||
@@ -9,11 +10,48 @@ import { AbstractRemote } from './_AbstractRemote.mjs'
|
||||
import { forkDeltaExport } from './_forkDeltaExport.mjs'
|
||||
import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.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 {
|
||||
_getRemoteWriter() {
|
||||
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) {
|
||||
const transferList = await this._computeTransferList(({ mode }) => mode === 'delta')
|
||||
await this._callWriters(async writer => {
|
||||
@@ -26,7 +64,7 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
||||
if (transferList.length > 0) {
|
||||
for (const metadata of transferList) {
|
||||
assert.strictEqual(metadata.mode, 'delta')
|
||||
|
||||
await this._selectBaseVm(metadata)
|
||||
await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
|
||||
const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, {
|
||||
useChain: false,
|
||||
@@ -50,6 +88,17 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
||||
}),
|
||||
'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()')
|
||||
// for healthcheck
|
||||
this._tags = metadata.vm.tags
|
||||
|
||||
@@ -78,6 +78,18 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
||||
'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
|
||||
|
||||
if (baseVm !== undefined) {
|
||||
@@ -133,7 +145,7 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
||||
])
|
||||
const srcVdi = srcVdis[snapshotOf]
|
||||
if (srcVdi !== undefined) {
|
||||
baseUuidToSrcVdi.set(baseUuid, srcVdi)
|
||||
baseUuidToSrcVdi.set(baseUuid, srcVdi.uuid)
|
||||
} else {
|
||||
debug('ignore snapshot VDI because no longer present on VM', {
|
||||
vdi: baseUuid,
|
||||
@@ -154,18 +166,18 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
||||
}
|
||||
|
||||
const fullVdisRequired = new Set()
|
||||
baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
|
||||
baseUuidToSrcVdi.forEach((srcVdiUuid, baseUuid) => {
|
||||
if (presentBaseVdis.has(baseUuid)) {
|
||||
debug('found base VDI', {
|
||||
base: baseUuid,
|
||||
vdi: srcVdi.uuid,
|
||||
vdi: srcVdiUuid,
|
||||
})
|
||||
} else {
|
||||
debug('missing base VDI', {
|
||||
base: baseUuid,
|
||||
vdi: srcVdi.uuid,
|
||||
vdi: srcVdiUuid,
|
||||
})
|
||||
fullVdisRequired.add(srcVdi.uuid)
|
||||
fullVdisRequired.add(srcVdiUuid)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import assert from 'node:assert'
|
||||
import mapValues from 'lodash/mapValues.js'
|
||||
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
||||
import { asyncEach } from '@vates/async-each'
|
||||
import { asyncMap } from '@xen-orchestra/async-map'
|
||||
import { chainVhd, checkVhdChain, openVhd, VhdAbstract } from 'vhd-lib'
|
||||
import { chainVhd, openVhd } from 'vhd-lib'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import { decorateClass } from '@vates/decorate-with'
|
||||
import { defer } from 'golike-defer'
|
||||
import { dirname } from 'node:path'
|
||||
import { dirname, basename } from 'node:path'
|
||||
|
||||
import { formatFilenameDate } from '../../_filenameDate.mjs'
|
||||
import { getOldEntries } from '../../_getOldEntries.mjs'
|
||||
import { TAG_BASE_DELTA } from '../../_incrementalVm.mjs'
|
||||
import { Task } from '../../Task.mjs'
|
||||
|
||||
import { MixinRemoteWriter } from './_MixinRemoteWriter.mjs'
|
||||
@@ -23,42 +21,45 @@ import { Disposable } from 'promise-toolbox'
|
||||
const { warn } = createLogger('xo:backups:DeltaBackupWriter')
|
||||
|
||||
export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWriter) {
|
||||
#parentVdiPaths
|
||||
#vhds
|
||||
async checkBaseVdis(baseUuidToSrcVdi) {
|
||||
this.#parentVdiPaths = {}
|
||||
const { handler } = this._adapter
|
||||
const adapter = this._adapter
|
||||
|
||||
const vdisDir = `${this._vmBackupDir}/vdis/${this._job.id}`
|
||||
|
||||
await asyncMap(baseUuidToSrcVdi, async ([baseUuid, srcVdi]) => {
|
||||
let found = false
|
||||
await asyncMap(baseUuidToSrcVdi, async ([baseUuid, srcVdiUuid]) => {
|
||||
let parentDestPath
|
||||
const vhdDir = `${vdisDir}/${srcVdiUuid}`
|
||||
try {
|
||||
const vhds = await handler.list(`${vdisDir}/${srcVdi.uuid}`, {
|
||||
const vhds = await handler.list(vhdDir, {
|
||||
filter: _ => _[0] !== '.' && _.endsWith('.vhd'),
|
||||
ignoreMissing: true,
|
||||
prependDir: true,
|
||||
})
|
||||
const packedBaseUuid = packUuid(baseUuid)
|
||||
await asyncMap(vhds, async path => {
|
||||
try {
|
||||
await checkVhdChain(handler, 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
|
||||
// the last one is probably the right one
|
||||
|
||||
const isMergeable = await adapter.isMergeableParent(packedBaseUuid, path)
|
||||
found = found || isMergeable
|
||||
for (let i = vhds.length - 1; i >= 0 && parentDestPath === undefined; i--) {
|
||||
const path = vhds[i]
|
||||
try {
|
||||
if (await adapter.isMergeableParent(packedBaseUuid, path)) {
|
||||
parentDestPath = path
|
||||
}
|
||||
} catch (error) {
|
||||
warn('checkBaseVdis', { error })
|
||||
await ignoreErrors.call(VhdAbstract.unlink(handler, path))
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
warn('checkBaseVdis', { error })
|
||||
}
|
||||
if (!found) {
|
||||
// no usable parent => the runner will have to decide to fall back to a full or stop backup
|
||||
if (parentDestPath === undefined) {
|
||||
baseUuidToSrcVdi.delete(baseUuid)
|
||||
} else {
|
||||
this.#parentVdiPaths[vhdDir] = parentDestPath
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -123,6 +124,44 @@ 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() {
|
||||
const adapter = this._adapter
|
||||
const oldEntries = this._oldEntries
|
||||
@@ -141,16 +180,10 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
||||
const jobId = job.id
|
||||
const handler = adapter.handler
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
const basename = formatFilenameDate(timestamp)
|
||||
const vhds = mapValues(
|
||||
// update this.#vhds before eventually skipping transfer, so that
|
||||
// updateUuidAndChain has all the mandatory data
|
||||
const vhds = (this.#vhds = mapValues(
|
||||
deltaExport.vdis,
|
||||
vdi =>
|
||||
`vdis/${jobId}/${
|
||||
@@ -160,7 +193,15 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
||||
vdi.uuid
|
||||
: vdi.$snapshot_of$uuid
|
||||
}/${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 = {
|
||||
isVhdDifferencing,
|
||||
@@ -176,38 +217,13 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
||||
vm,
|
||||
vmSnapshot,
|
||||
}
|
||||
|
||||
const { size } = await Task.run({ name: 'transfer' }, async () => {
|
||||
let transferSize = 0
|
||||
await asyncEach(
|
||||
Object.entries(deltaExport.vdis),
|
||||
async ([id, vdi]) => {
|
||||
Object.keys(deltaExport.vdis),
|
||||
async 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
|
||||
// since i += await asyncFun lead to race condition
|
||||
// as explained : https://eslint.org/docs/latest/rules/require-atomic-updates
|
||||
@@ -219,17 +235,6 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
||||
writeBlockConcurrency: this._config.writeBlockConcurrency,
|
||||
})
|
||||
transferSize += transferSizeOneDisk
|
||||
|
||||
if (isDifferencing) {
|
||||
await chainVhd(handler, parentPath, handler, path)
|
||||
}
|
||||
|
||||
// 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()
|
||||
})
|
||||
},
|
||||
{
|
||||
concurrency: settings.diskPerVmConcurrency,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import assert from 'node:assert'
|
||||
import { asyncMap, asyncMapSettled } from '@xen-orchestra/async-map'
|
||||
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
||||
import { formatDateTime } from '@xen-orchestra/xapi'
|
||||
@@ -14,6 +15,7 @@ import find from 'lodash/find.js'
|
||||
|
||||
export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
|
||||
async checkBaseVdis(baseUuidToSrcVdi, baseVm) {
|
||||
assert.notStrictEqual(baseVm, undefined)
|
||||
const sr = this._sr
|
||||
const replicatedVm = listReplicatedVms(sr.$xapi, this._job.id, sr.uuid, this._vmUuid).find(
|
||||
vm => vm.other_config[TAG_COPY_SRC] === baseVm.uuid
|
||||
@@ -36,7 +38,9 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateUuidAndChain() {
|
||||
// nothing to do, the chaining is not modified in this case
|
||||
}
|
||||
prepare({ isFull }) {
|
||||
// create the task related to this export and ensure all methods are called in this context
|
||||
const task = new Task({
|
||||
|
||||
@@ -5,6 +5,10 @@ export class AbstractIncrementalWriter extends AbstractWriter {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
updateUuidAndChain() {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
@@ -230,6 +230,7 @@ Settings are described in [`@xen-orchestra/backups/\_runners/VmsXapi.mjs``](http
|
||||
- `checkBaseVdis(baseUuidToSrcVdi, baseVm)`
|
||||
- `prepare({ isFull })`
|
||||
- `transfer({ timestamp, deltaExport, sizeContainers })`
|
||||
- `updateUuidAndChain({ isVhdDifferencing, vdis })`
|
||||
- `cleanup()`
|
||||
- `healthCheck()` // is not executed if no health check sr or tag doesn't match
|
||||
- **Full**
|
||||
|
||||
@@ -50,7 +50,17 @@ const RRD_POINTS_PER_STEP: { [key in RRD_STEP]: number } = {
|
||||
// Utils
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function convertNanToNull(value: number) {
|
||||
function parseNumber(value: number | string) {
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -59,7 +69,7 @@ function convertNanToNull(value: number) {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const computeValues = (dataRow: any, legendIndex: number, transformValue = identity) =>
|
||||
map(dataRow, ({ values }) => transformValue(convertNanToNull(values[legendIndex])))
|
||||
map(dataRow, ({ values }) => transformValue(parseNumber(values[legendIndex])))
|
||||
|
||||
const createGetProperty = (obj: object, property: string, defaultValue: unknown) =>
|
||||
defaults(obj, { [property]: defaultValue })[property] as any
|
||||
@@ -319,8 +329,14 @@ export default class XapiStats {
|
||||
},
|
||||
abortSignal,
|
||||
})
|
||||
// eslint-disable-next-line import/no-named-as-default-member -- https://github.com/json5/json5/issues/287
|
||||
return JSON5.parse(await resp.text())
|
||||
const text = await resp.text()
|
||||
try {
|
||||
// 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
|
||||
@@ -383,7 +399,10 @@ export default class XapiStats {
|
||||
abortSignal,
|
||||
})
|
||||
|
||||
const actualStep = json.meta.step as number
|
||||
const actualStep = parseNumber(json.meta.step)
|
||||
if (actualStep !== step) {
|
||||
throw new FaultyGranularity(`Unable to get the true granularity: ${actualStep}`)
|
||||
}
|
||||
|
||||
if (json.data.length > 0) {
|
||||
// fetched data is organized from the newest to the oldest
|
||||
@@ -407,14 +426,15 @@ export default class XapiStats {
|
||||
|
||||
let stepStats = xoObjectStats[actualStep]
|
||||
let cacheStepStats = cacheXoObjectStats[actualStep]
|
||||
if (stepStats === undefined || stepStats.endTimestamp !== json.meta.end) {
|
||||
const endTimestamp = parseNumber(json.meta.end)
|
||||
if (stepStats === undefined || stepStats.endTimestamp !== endTimestamp) {
|
||||
stepStats = xoObjectStats[actualStep] = {
|
||||
endTimestamp: json.meta.end,
|
||||
endTimestamp,
|
||||
interval: actualStep,
|
||||
canBeExpired: false,
|
||||
}
|
||||
cacheStepStats = cacheXoObjectStats[actualStep] = {
|
||||
endTimestamp: json.meta.end,
|
||||
endTimestamp,
|
||||
interval: actualStep,
|
||||
canBeExpired: true,
|
||||
}
|
||||
@@ -438,10 +458,6 @@ export default class XapiStats {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (actualStep !== step) {
|
||||
throw new FaultyGranularity(`Unable to get the true granularity: ${actualStep}`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
return
|
||||
|
||||
158
@xen-orchestra/web-core/docs/composables/context.composable.md
Normal file
158
@xen-orchestra/web-core/docs/composables/context.composable.md
Normal file
@@ -0,0 +1,158 @@
|
||||
<!-- TOC -->
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Simple Context](#simple-context)
|
||||
- [1. Create the context](#1-create-the-context)
|
||||
- [2. Use the context](#2-use-the-context)
|
||||
- [2.1. Read](#21-read)
|
||||
- [2.2. Update](#22-update)
|
||||
- [Advanced Context](#advanced-context)
|
||||
- [1. Create the context](#1-create-the-context-1)
|
||||
- [2. Use the context](#2-use-the-context-1)
|
||||
- [2.1. Read](#21-read-1)
|
||||
- [2.2. Update](#22-update-1)
|
||||
- [Caveats (boolean props)](#caveats-boolean-props)
|
||||
<!-- TOC -->
|
||||
|
||||
# Overview
|
||||
|
||||
`createContext` lets you create a context that is both readable and writable, and is accessible by a component and all
|
||||
its descendants at any depth.
|
||||
|
||||
Each descendant has the ability to change the context value, affecting itself and all of its descendants at any level.
|
||||
|
||||
## Simple Context
|
||||
|
||||
### 1. Create the context
|
||||
|
||||
`createContext` takes the initial context value as first argument.
|
||||
|
||||
```ts
|
||||
// context.ts
|
||||
|
||||
const CounterContext = createContext(0)
|
||||
```
|
||||
|
||||
### 2. Use the context
|
||||
|
||||
#### 2.1. Read
|
||||
|
||||
You can get the current Context value by using `useContext(CounterContext)`.
|
||||
|
||||
```ts
|
||||
const counter = useContext(CounterContext)
|
||||
|
||||
console.log(counter.value) // 0
|
||||
```
|
||||
|
||||
#### 2.2. Update
|
||||
|
||||
You can pass a `MaybeRefOrGetter` as second argument to update the context value.
|
||||
|
||||
```ts
|
||||
// MyComponent.vue
|
||||
|
||||
const props = defineProps<{
|
||||
counter?: number
|
||||
}>()
|
||||
|
||||
const counter = useContext(CounterContext, () => props.counter)
|
||||
|
||||
// When calling <MyComponent />
|
||||
console.log(counter.value) // 0
|
||||
|
||||
// When calling <MyComponent :counter="20" />
|
||||
console.log(counter.value) // 20
|
||||
```
|
||||
|
||||
## Advanced Context
|
||||
|
||||
To customize the context output, you can pass a custom context builder as the second argument of `createContext`.
|
||||
|
||||
### 1. Create the context
|
||||
|
||||
```ts
|
||||
// context.ts
|
||||
|
||||
// Example 1. Return a object
|
||||
const CounterContext = createContext(10, counter => ({
|
||||
counter,
|
||||
isEven: computed(() => counter.value % 2 === 0),
|
||||
}))
|
||||
|
||||
// Example 2. Return a computed value
|
||||
const DoubleContext = createContext(10, num => computed(() => num.value * 2))
|
||||
|
||||
// Example 3. Use a previous value
|
||||
const ColorContext = createContext('info' as Color, (color, previousColor) => ({
|
||||
name: color,
|
||||
colorContextClass: computed(() => (previousColor.value === color.value ? undefined : `color-context-${color.value}`)),
|
||||
}))
|
||||
```
|
||||
|
||||
### 2. Use the context
|
||||
|
||||
#### 2.1. Read
|
||||
|
||||
When using the context, it will return your custom value.
|
||||
|
||||
```ts
|
||||
const { counter, isEven } = useContext(CounterContext)
|
||||
const double = useContext(DoubleContext)
|
||||
|
||||
console.log(counter.value) // 10
|
||||
console.log(isEven.value) // true
|
||||
console.log(double.value) // 20
|
||||
```
|
||||
|
||||
#### 2.2. Update
|
||||
|
||||
Same as with a simple context, you can pass a `MaybeRefOrGetter` as second argument.
|
||||
|
||||
```ts
|
||||
// Parent.vue
|
||||
useContext(CounterContext, 99)
|
||||
useContext(DoubleContext, 99)
|
||||
|
||||
// Child.vue
|
||||
const { isEven } = useContext(CounterContext)
|
||||
const double = useContext(DoubleContext)
|
||||
|
||||
console.log(isEven.value) // false
|
||||
console.log(double.value) // 198
|
||||
```
|
||||
|
||||
## Caveats (boolean props)
|
||||
|
||||
When working with `boolean` props, there's an important caveat to be aware of.
|
||||
|
||||
If the `MaybeRefOrGetter` returns any other value than `undefined`, the context will be updated according to this value.
|
||||
|
||||
This could be problematic if the value comes from a `boolean` prop.
|
||||
|
||||
```ts
|
||||
const props = defineProps<{
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
useContext(MyBooleanContext, () => props.disabled) // Update to `false` if `undefined`
|
||||
```
|
||||
|
||||
In that case, Vue will automatically set the default value for `disabled` prop to `false`.
|
||||
|
||||
Even if the `disabled` prop in not provided at all, the current context will not be used and will be replaced
|
||||
by `false`.
|
||||
|
||||
To circumvent this issue, you need to use `withDefaults` and specifically set the default value for `boolean` props
|
||||
to `undefined`:
|
||||
|
||||
```ts
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
}>(),
|
||||
{ disabled: undefined }
|
||||
)
|
||||
|
||||
useContext(MyBoolean, () => props.disabled) // Keep parent value if `undefined`
|
||||
```
|
||||
142
@xen-orchestra/web-core/docs/contexts/color-context.md
Normal file
142
@xen-orchestra/web-core/docs/contexts/color-context.md
Normal file
@@ -0,0 +1,142 @@
|
||||
<!-- TOC -->
|
||||
|
||||
- [Overview](#overview)
|
||||
- [CSS variables](#css-variables)
|
||||
- [Available color contexts](#available-color-contexts)
|
||||
- [Usage](#usage)
|
||||
<!-- TOC -->
|
||||
|
||||
# Overview
|
||||
|
||||
The color context provides a way to apply a set of colors variants to a component and all its descendants at any depth.
|
||||
|
||||
Each descendant has the ability to change the context value, affecting itself and all of its descendants at any level.
|
||||
|
||||
The purpose is to colorize a component and its descendants by applying a single CSS class on the parent component (e.g., applying the class on a modal component container will style all children components using the context).
|
||||
|
||||
## CSS variables
|
||||
|
||||
The color context relies on the usage of the following variables:
|
||||
|
||||
```css
|
||||
--color-context-primary;
|
||||
--color-context-primary-hover;
|
||||
--color-context-primary-active;
|
||||
--color-context-primary-disabled;
|
||||
|
||||
--color-context-secondary;
|
||||
--color-context-secondary-hover;
|
||||
--color-context-secondary-active;
|
||||
|
||||
--color-context-tertiary;
|
||||
```
|
||||
|
||||
Any component can use these variables for `color`, `background-color` or any other CSS property, to be usable with the color context.
|
||||
|
||||
When you set a color context, the variables are updated with the help of CSS classes defined in `_context.pcss`:
|
||||
|
||||
```css
|
||||
.color-context-info {
|
||||
--color-context-primary: var(--color-purple-base);
|
||||
--color-context-primary-hover: var(--color-purple-d20);
|
||||
--color-context-primary-active: var(--color-purple-d40);
|
||||
--color-context-primary-disabled: var(--color-grey-400);
|
||||
|
||||
--color-context-secondary: var(--background-color-purple-10);
|
||||
--color-context-secondary-hover: var(--background-color-purple-20);
|
||||
--color-context-secondary-active: var(--background-color-purple-30);
|
||||
}
|
||||
|
||||
.color-context-success {
|
||||
--color-context-primary: var(--color-green-base);
|
||||
--color-context-primary-hover: var(--color-green-d20);
|
||||
/*...*/
|
||||
}
|
||||
```
|
||||
|
||||
You can add any other context by adding a `color-context-<my-context>` class and setting the desired values for the variables.
|
||||
|
||||
**Important note: remember to set a value for all variables to avoid any missing styles.**
|
||||
|
||||
## Available color contexts
|
||||
|
||||
Color contexts rely on the type `Color` defined in `/lib/types/color.type.ts`:
|
||||
|
||||
- `info` (_purple_)
|
||||
- `success` (_green_)
|
||||
- `warning` (_orange_)
|
||||
- `error` (_red_)
|
||||
|
||||
## Usage
|
||||
|
||||
To get and set the color context in a component, you can pass the `ColorContext` to `useContext` and apply the `colorContextClass` to the root component:
|
||||
|
||||
```ts
|
||||
// ParentComponent.vue
|
||||
|
||||
import { useContext } from '@core/composables/context.composable'
|
||||
import { ColorContext } from '@core/context'
|
||||
import type { Color } from '@core/types/color.type'
|
||||
import { defineProps } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
color?: Color
|
||||
}>()
|
||||
|
||||
const { colorContextClass } = useContext(ColorContext, () => props.color)
|
||||
```
|
||||
|
||||
All the components using the CSS variables will inherit the color context applied by the `colorContextClass`.
|
||||
It's possible to change the color of a component on demand, if the component has a `color` prop, and passing it as the second parameter of the composable.
|
||||
|
||||
Then, the only thing to do is to apply the class in the component's `template`:
|
||||
|
||||
```vue
|
||||
<!-- ParentComponent.vue -->
|
||||
|
||||
<template>
|
||||
<div :class="colorContextClass">
|
||||
<!-- Will use the color context defined by the class above-->
|
||||
<MyComponent />
|
||||
|
||||
<!-- Will use the color "info" instead of the context-->
|
||||
<MyComponent color="info" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
`MyComponent` using the context:
|
||||
|
||||
```vue
|
||||
<!-- MyComponent.vue -->
|
||||
|
||||
<template>
|
||||
<div :class="colorContextClass" class="my-component">
|
||||
<p>Lorem ipsum dolor sit amet.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useContext } from '@core/composables/context.composable'
|
||||
import { ColorContext } from '@core/context'
|
||||
import type { Color } from '@core/types/color.type'
|
||||
import { defineProps } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
color?: Color
|
||||
}>()
|
||||
|
||||
const { colorContextClass } = useContext(ColorContext, () => props.color)
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.my-component {
|
||||
background-color: var(--color-context-secondary);
|
||||
color: var(--color-context-primary);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
In the example above, if the `color` prop is not set, the component will use the color context (i.e., if its parent uses a `success` color context, `MyComponent` will be styled with the `success` colors defined in `_context.pcss`).
|
||||
|
||||
If the `color` prop is set, the component will use the prop value to update the context for itself and its descendants.
|
||||
@@ -45,3 +45,55 @@
|
||||
.context-border-color-info {
|
||||
border-color: var(--color-purple-base);
|
||||
}
|
||||
|
||||
.color-context-info {
|
||||
--color-context-primary: var(--color-purple-base);
|
||||
--color-context-primary-hover: var(--color-purple-d20);
|
||||
--color-context-primary-active: var(--color-purple-d40);
|
||||
--color-context-primary-disabled: var(--color-grey-400);
|
||||
|
||||
--color-context-secondary: var(--background-color-purple-10);
|
||||
--color-context-secondary-hover: var(--background-color-purple-20);
|
||||
--color-context-secondary-active: var(--background-color-purple-30);
|
||||
|
||||
--color-context-tertiary: var(--background-color-primary);
|
||||
}
|
||||
|
||||
.color-context-success {
|
||||
--color-context-primary: var(--color-green-base);
|
||||
--color-context-primary-hover: var(--color-green-d20);
|
||||
--color-context-primary-active: var(--color-green-d40);
|
||||
--color-context-primary-disabled: var(--color-green-l60);
|
||||
|
||||
--color-context-secondary: var(--background-color-green-10);
|
||||
--color-context-secondary-hover: var(--background-color-green-20);
|
||||
--color-context-secondary-active: var(--background-color-green-30);
|
||||
|
||||
--color-context-tertiary: var(--background-color-primary);
|
||||
}
|
||||
|
||||
.color-context-warning {
|
||||
--color-context-primary: var(--color-orange-base);
|
||||
--color-context-primary-hover: var(--color-orange-d20);
|
||||
--color-context-primary-active: var(--color-orange-d40);
|
||||
--color-context-primary-disabled: var(--color-orange-l60);
|
||||
|
||||
--color-context-secondary: var(--background-color-orange-10);
|
||||
--color-context-secondary-hover: var(--background-color-orange-20);
|
||||
--color-context-secondary-active: var(--background-color-orange-30);
|
||||
|
||||
--color-context-tertiary: var(--background-color-primary);
|
||||
}
|
||||
|
||||
.color-context-error {
|
||||
--color-context-primary: var(--color-red-base);
|
||||
--color-context-primary-hover: var(--color-red-d20);
|
||||
--color-context-primary-active: var(--color-red-d40);
|
||||
--color-context-primary-disabled: var(--color-red-l60);
|
||||
|
||||
--color-context-secondary: var(--background-color-red-10);
|
||||
--color-context-secondary-hover: var(--background-color-red-20);
|
||||
--color-context-secondary-active: var(--background-color-red-30);
|
||||
|
||||
--color-context-tertiary: var(--background-color-primary);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ComputedRef, InjectionKey, MaybeRefOrGetter } from 'vue'
|
||||
import { computed, inject, provide, toValue } from 'vue'
|
||||
|
||||
export const createContext = <T, Output = ComputedRef<T>>(
|
||||
initialValue: MaybeRefOrGetter<T>,
|
||||
customBuilder?: (value: ComputedRef<T>, previousValue: ComputedRef<T>) => Output
|
||||
) => {
|
||||
return {
|
||||
id: Symbol('CONTEXT_ID') as InjectionKey<MaybeRefOrGetter<T>>,
|
||||
initialValue,
|
||||
builder: customBuilder ?? (value => value as Output),
|
||||
}
|
||||
}
|
||||
|
||||
type Context<T = any, Output = any> = ReturnType<typeof createContext<T, Output>>
|
||||
|
||||
type ContextOutput<Ctx extends Context> = Ctx extends Context<any, infer Output> ? Output : never
|
||||
|
||||
type ContextValue<Ctx extends Context> = Ctx extends Context<infer T> ? T : never
|
||||
|
||||
export const useContext = <Ctx extends Context, T extends ContextValue<Ctx>>(
|
||||
context: Ctx,
|
||||
newValue?: MaybeRefOrGetter<T | undefined>
|
||||
): ContextOutput<Ctx> => {
|
||||
const currentValue = inject(context.id, undefined)
|
||||
|
||||
const updatedValue = () => toValue(newValue) ?? toValue(currentValue) ?? context.initialValue
|
||||
provide(context.id, updatedValue)
|
||||
|
||||
return context.builder(
|
||||
computed(() => toValue(updatedValue)),
|
||||
computed(() => toValue(currentValue))
|
||||
)
|
||||
}
|
||||
8
@xen-orchestra/web-core/lib/context.ts
Normal file
8
@xen-orchestra/web-core/lib/context.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createContext } from '@core/composables/context.composable'
|
||||
import type { Color } from '@core/types/color.type'
|
||||
import { computed } from 'vue'
|
||||
|
||||
export const ColorContext = createContext('info' as Color, (color, previousColor) => ({
|
||||
name: color,
|
||||
colorContextClass: computed(() => (previousColor.value === color.value ? undefined : `color-context-${color.value}`)),
|
||||
}))
|
||||
1
@xen-orchestra/web-core/lib/types/color.type.ts
Normal file
1
@xen-orchestra/web-core/lib/types/color.type.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type Color = 'info' | 'error' | 'warning' | 'success'
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
- 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
|
||||
|
||||
@@ -17,6 +21,8 @@
|
||||
- [Settings/XO Config] Sort backups from newest to oldest
|
||||
- [Plugins/audit] Don't log `tag.getAllConfigured` calls
|
||||
- [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
|
||||
|
||||
@@ -40,6 +46,6 @@
|
||||
- vhd-lib patch
|
||||
- xo-server minor
|
||||
- xo-server-audit patch
|
||||
- xo-web patch
|
||||
- xo-web minor
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
@@ -46,9 +46,9 @@ const VhdSynthetic = class VhdSynthetic extends VhdAbstract {
|
||||
}
|
||||
|
||||
get compressionType() {
|
||||
const compressionType = this.vhds[0].compressionType
|
||||
for (let i = 0; i < this.vhds.length; i++) {
|
||||
if (compressionType !== this.vhds[i].compressionType) {
|
||||
const compressionType = this.#vhds[0].compressionType
|
||||
for (let i = 0; i < this.#vhds.length; i++) {
|
||||
if (compressionType !== this.#vhds[i].compressionType) {
|
||||
return 'MIXED'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import semver from 'semver'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import assert from 'assert'
|
||||
import { format } from 'json-rpc-peer'
|
||||
@@ -136,13 +137,38 @@ export async function restart({
|
||||
const pool = this.getObject(host.$poolId, 'pool')
|
||||
const master = this.getObject(pool.master, 'host')
|
||||
const hostRebootRequired = host.rebootRequired
|
||||
if (hostRebootRequired && host.id !== master.id && host.version === master.version) {
|
||||
throw incorrectState({
|
||||
actual: hostRebootRequired,
|
||||
expected: false,
|
||||
object: master.id,
|
||||
property: 'rebootRequired',
|
||||
})
|
||||
|
||||
// we are currently in an host upgrade process
|
||||
if (hostRebootRequired && host.id !== master.id) {
|
||||
// this error is not ideal but it means that the pool master must be fully upgraded/rebooted before the current host can be rebooted.
|
||||
//
|
||||
// there is a single error for the 3 cases because the client must handle them the same way
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
27
packages/xo-server/src/api/pusb.mjs
Normal file
27
packages/xo-server/src/api/pusb.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
export async function scan({ host }) {
|
||||
await this.getXapi(host).call('PUSB.scan', host._xapiRef)
|
||||
}
|
||||
|
||||
scan.params = {
|
||||
host: { type: 'string' },
|
||||
}
|
||||
scan.resolve = {
|
||||
host: ['host', 'host', 'operate'],
|
||||
}
|
||||
|
||||
export async function set({ pusb, enabled }) {
|
||||
const xapi = this.getXapi(pusb)
|
||||
|
||||
if (enabled !== undefined && enabled !== pusb.passthroughEnabled) {
|
||||
await xapi.call('PUSB.set_passthrough_enabled', pusb._xapiRef, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
set.params = {
|
||||
id: { type: 'string' },
|
||||
enabled: { type: 'boolean', optional: true },
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
pusb: ['id', 'PUSB', 'administrate'],
|
||||
}
|
||||
@@ -328,6 +328,34 @@ const TRANSFORMS = {
|
||||
|
||||
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 = {
|
||||
// type is redefined after for controllers/, templates &
|
||||
// snapshots.
|
||||
@@ -422,8 +450,7 @@ const TRANSFORMS = {
|
||||
xenTools,
|
||||
...getVmGuestToolsProps(obj),
|
||||
|
||||
// TODO: handle local VMs (`VM.get_possible_hosts()`).
|
||||
$container: isRunning ? link(obj, 'resident_on') : link(obj, 'pool'),
|
||||
$container,
|
||||
$VBDs: link(obj, 'VBDs'),
|
||||
|
||||
// TODO: dedupe
|
||||
@@ -862,6 +889,17 @@ const TRANSFORMS = {
|
||||
vm: link(obj, 'VM'),
|
||||
}
|
||||
},
|
||||
|
||||
pusb(obj) {
|
||||
return {
|
||||
type: 'PUSB',
|
||||
|
||||
description: obj.description,
|
||||
host: link(obj, 'host'),
|
||||
passthroughEnabled: obj.passthrough_enabled,
|
||||
usbGroup: link(obj, 'USB_group'),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -280,7 +280,7 @@ export default class MigrateVm {
|
||||
const stream = vhd.stream()
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
|
||||
}
|
||||
return vhd
|
||||
return { vdi, vhd }
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
@@ -72,11 +72,8 @@ async function* makeObjectsStream(iterable, makeResult, json) {
|
||||
async function sendObjects(iterable, req, res, path = req.path) {
|
||||
const { query } = req
|
||||
|
||||
const { baseUrl } = req
|
||||
const makeUrl = item => {
|
||||
const { id } = item
|
||||
return join(baseUrl, typeof path === 'function' ? path(item) : path, typeof id === 'number' ? String(id) : id)
|
||||
}
|
||||
const basePath = join(req.baseUrl, path)
|
||||
const makeUrl = ({ id }) => join(basePath, typeof id === 'number' ? String(id) : id)
|
||||
|
||||
let makeResult
|
||||
let { fields } = query
|
||||
@@ -256,6 +253,10 @@ export default class RestApi {
|
||||
const host = req.xapiObject
|
||||
res.json(await host.$xapi.listMissingPatches(host))
|
||||
},
|
||||
|
||||
async smt({ xapiObject }, res) {
|
||||
res.json({ enabled: await xapiObject.$xapi.isHyperThreadingEnabled(xapiObject.$id) })
|
||||
},
|
||||
}
|
||||
|
||||
collections.pools.routes = {
|
||||
@@ -280,36 +281,6 @@ export default class RestApi {
|
||||
},
|
||||
}
|
||||
|
||||
{
|
||||
async function vdis(req, res) {
|
||||
const { object, query } = req
|
||||
|
||||
const vdiIds = new Set()
|
||||
for (const vbdId of object.$VBDs) {
|
||||
const vbd = app.getObject(vbdId, 'VBD')
|
||||
const vdiId = vbd.VDI
|
||||
if (vdiId !== undefined) {
|
||||
vdiIds.add(vdiId)
|
||||
}
|
||||
}
|
||||
|
||||
await sendObjects(
|
||||
handleArray(
|
||||
Array.from(vdiIds, id => app.getObject(id, ['VDI', 'VDI-snapshot'])),
|
||||
query.filter,
|
||||
ifDef(query.limit, Number)
|
||||
),
|
||||
req,
|
||||
res,
|
||||
({ type }) => type.toLowerCase() + 's'
|
||||
)
|
||||
}
|
||||
|
||||
for (const collection of ['vms', 'vm-snapshots', 'vm-templates']) {
|
||||
collections[collection].routes.vdis = vdis
|
||||
}
|
||||
}
|
||||
|
||||
collections.pools.actions = {
|
||||
create_vm: withParams(
|
||||
defer(async ($defer, { xapiObject: { $xapi } }, { affinity, boot, install, template, ...params }, req) => {
|
||||
|
||||
@@ -138,7 +138,7 @@ export class Range extends Component {
|
||||
|
||||
export Toggle from './toggle'
|
||||
|
||||
const UNITS = ['kiB', 'MiB', 'GiB']
|
||||
const UNITS = ['kiB', 'MiB', 'GiB', 'TiB', 'PiB']
|
||||
const DEFAULT_UNIT = 'GiB'
|
||||
|
||||
export class SizeInput extends BaseComponent {
|
||||
|
||||
@@ -54,13 +54,9 @@ export default class IsoDevice extends Component {
|
||||
() => this.props.vm.$pool,
|
||||
() => this.props.vm.$container,
|
||||
(vmPool, vmContainer) => sr => {
|
||||
const vmRunning = vmContainer !== vmPool
|
||||
const sameHost = vmContainer === sr.$container
|
||||
const samePool = vmPool === sr.$pool
|
||||
|
||||
return (
|
||||
samePool &&
|
||||
(vmRunning ? sr.shared || sameHost : true) &&
|
||||
vmPool === sr.$pool &&
|
||||
(sr.shared || vmContainer === sr.$container) &&
|
||||
(sr.SR_type === 'iso' || (sr.SR_type === 'udev' && sr.size))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import _ from 'intl'
|
||||
import Copiable from 'copiable'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import store from 'store'
|
||||
import HomeTags from 'home-tags'
|
||||
@@ -24,10 +23,21 @@ export default decorate([
|
||||
provideState({
|
||||
computed: {
|
||||
areHostsVersionsEqual: ({ areHostsVersionsEqualByPool }, { host }) => areHostsVersionsEqualByPool[host.$pool],
|
||||
inMemoryVms: (_, { vms }) => {
|
||||
const result = []
|
||||
for (const key of Object.keys(vms)) {
|
||||
const vm = vms[key]
|
||||
const { power_state } = vm
|
||||
if (power_state === 'Running' || power_state === 'Paused') {
|
||||
result.push(vm)
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ statsOverview, host, nVms, vmController, vms, state: { areHostsVersionsEqual } }) => {
|
||||
({ statsOverview, host, nVms, vmController, state: { areHostsVersionsEqual, inMemoryVms } }) => {
|
||||
const pool = getObject(store.getState(), host.$pool)
|
||||
const vmsFilter = encodeURIComponent(new CM.Property('$container', new CM.String(host.id)).toString())
|
||||
return (
|
||||
@@ -120,7 +130,7 @@ export default decorate([
|
||||
tooltip={`${host.productBrand} (${formatSize(vmController.memory.size)})`}
|
||||
value={vmController.memory.size}
|
||||
/>
|
||||
{map(vms, vm => (
|
||||
{inMemoryVms.map(vm => (
|
||||
<UsageElement
|
||||
tooltip={`${vm.name_label} (${formatSize(vm.memory.size)})`}
|
||||
key={vm.id}
|
||||
|
||||
Reference in New Issue
Block a user