Compare commits

..

3 Commits

Author SHA1 Message Date
Pizzosaure
b3624ae804 Changes after review 2024-02-23 13:57:22 +01:00
Pizzosaure
73c607a0a7 Changelog entry 2024-02-19 17:06:45 +01:00
Pizzosaure
dcaa2ed75b feat(xo-web/storage): handle link to VM for suspend VDIs 2024-02-19 17:00:51 +01:00
18 changed files with 196 additions and 358 deletions

View File

@@ -191,14 +191,13 @@ 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(VhdSynthetic.fromVhdChain(this.handler, path), vhd => {
return await Disposable.use(openVhd(this.handler, path), vhd => {
// this baseUuid is not linked with this vhd
if (!vhd.footer.uuid.equals(packedParentUid)) {
return false
}
// check if all the chain is composed of vhd directory
const isVhdDirectory = vhd.checkVhdsClass(VhdDirectory)
const isVhdDirectory = vhd instanceof VhdDirectory
return isVhdDirectory
? this.useVhdDirectory() && this.#getCompressionType() === vhd.compressionType
: !this.useVhdDirectory()

View File

@@ -141,7 +141,6 @@ export class Task {
}
#log(event, props) {
console.log({taskId: this.#id,event})
this.#onLog({
...props,
event,

View File

@@ -88,89 +88,49 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
const allSettings = this._job.settings
const baseSettings = this._baseSettings
const handleVm = vmUuid => {
const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
const task = new Task(taskStart)
let nbRun = 0
console.log('runonce', vmUuid)
// ensure all the eecution are run in the same task
const runOnce = async ()=> {
nbRun ++
console.log('Will run backup ', vmUuid, nbRun)
return this._getRecord('VM', vmUuid).then(
disposableVm => Disposable.use(disposableVm, async vm => {
taskStart.data.name_label = vm.name_label
const opts = {
baseSettings,
config,
getSnapshotNameLabel,
healthCheckSr,
job,
remoteAdapters,
schedule,
settings: { ...settings, ...allSettings[vm.uuid] },
srs,
throttleStream,
vm,
}
let vmBackup
if (job.mode === 'delta') {
vmBackup = new IncrementalXapi(opts)
} else {
if (job.mode === 'full') {
vmBackup = new FullXapi(opts)
} else {
throw new Error(`Job mode ${job.mode} not implemented`)
}
}
const res = await vmBackup.run()
console.log(' backup run successfully ', vmUuid, res)
return res
}))
}
// ensure the same task is reused
return task.wrapFn(runOnce)
}
let toHandle = []
console.log('prepare to run ')
for(const vmUuid of vmIds){
// prepare a collection of task to bound task to run
toHandle.push( handleVm(vmUuid))
}
console.log({toHandle})
for(let i=0; i < 4 && toHandle.length >0; i++){
console.log('RUN ', i)
const currentRun = [...toHandle]
toHandle = []
await asyncMapSettled(currentRun, async fn=>{
console.log('got fn ', fn)
try{
await fn()
console.log('run done')
}catch(error){
console.log('will retry ')
toHandle.push(fn)
}
})
return this._getRecord('VM', vmUuid).then(
disposableVm =>
Disposable.use(disposableVm, vm => {
taskStart.data.name_label = vm.name_label
return runTask(taskStart, () => {
const opts = {
baseSettings,
config,
getSnapshotNameLabel,
healthCheckSr,
job,
remoteAdapters,
schedule,
settings: { ...settings, ...allSettings[vm.uuid] },
srs,
throttleStream,
vm,
}
let vmBackup
if (job.mode === 'delta') {
vmBackup = new IncrementalXapi(opts)
} else {
if (job.mode === 'full') {
vmBackup = new FullXapi(opts)
} else {
throw new Error(`Job mode ${job.mode} not implemented`)
}
}
return vmBackup.run()
})
}),
error =>
runTask(taskStart, () => {
throw error
})
)
}
const { concurrency } = settings
if(toHandle.length > 0){
console.log('LAST RUN ')
// last run will really fail this time
await asyncMapSettled(toHandle, fn=>fn())
}
await asyncMapSettled(vmIds, concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm))
}
)
}

View File

@@ -2,7 +2,6 @@ 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'
@@ -10,48 +9,11 @@ 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 => {
@@ -64,7 +26,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,
@@ -88,17 +50,6 @@ 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

View File

@@ -78,18 +78,6 @@ 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) {
@@ -145,7 +133,7 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
])
const srcVdi = srcVdis[snapshotOf]
if (srcVdi !== undefined) {
baseUuidToSrcVdi.set(baseUuid, srcVdi.uuid)
baseUuidToSrcVdi.set(baseUuid, srcVdi)
} else {
debug('ignore snapshot VDI because no longer present on VM', {
vdi: baseUuid,
@@ -166,18 +154,18 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
}
const fullVdisRequired = new Set()
baseUuidToSrcVdi.forEach((srcVdiUuid, baseUuid) => {
baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
if (presentBaseVdis.has(baseUuid)) {
debug('found base VDI', {
base: baseUuid,
vdi: srcVdiUuid,
vdi: srcVdi.uuid,
})
} else {
debug('missing base VDI', {
base: baseUuid,
vdi: srcVdiUuid,
vdi: srcVdi.uuid,
})
fullVdisRequired.add(srcVdiUuid)
fullVdisRequired.add(srcVdi.uuid)
}
})

View File

@@ -64,11 +64,6 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
}
await Task.run({ name: 'transfer' }, async () => {
console.log('run ')
if(Math.random() < 0.8){
throw new Error('NOPE')
}
console.log('OK ')
await adapter.outputStream(dataFilename, stream, {
maxStreamLength,
streamLength,

View File

@@ -1,15 +1,17 @@
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, openVhd } from 'vhd-lib'
import { chainVhd, checkVhdChain, openVhd, VhdAbstract } from 'vhd-lib'
import { createLogger } from '@xen-orchestra/log'
import { decorateClass } from '@vates/decorate-with'
import { defer } from 'golike-defer'
import { dirname, basename } from 'node:path'
import { dirname } 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'
@@ -21,45 +23,42 @@ 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, srcVdiUuid]) => {
let parentDestPath
const vhdDir = `${vdisDir}/${srcVdiUuid}`
await asyncMap(baseUuidToSrcVdi, async ([baseUuid, srcVdi]) => {
let found = false
try {
const vhds = await handler.list(vhdDir, {
const vhds = await handler.list(`${vdisDir}/${srcVdi.uuid}`, {
filter: _ => _[0] !== '.' && _.endsWith('.vhd'),
ignoreMissing: true,
prependDir: true,
})
const packedBaseUuid = packUuid(baseUuid)
// the last one is probably the right one
for (let i = vhds.length - 1; i >= 0 && parentDestPath === undefined; i--) {
const path = vhds[i]
await asyncMap(vhds, async path => {
try {
if (await adapter.isMergeableParent(packedBaseUuid, path)) {
parentDestPath = path
}
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
const isMergeable = await adapter.isMergeableParent(packedBaseUuid, path)
found = found || isMergeable
} catch (error) {
warn('checkBaseVdis', { error })
await ignoreErrors.call(VhdAbstract.unlink(handler, path))
}
}
})
} catch (error) {
warn('checkBaseVdis', { error })
}
// no usable parent => the runner will have to decide to fall back to a full or stop backup
if (parentDestPath === undefined) {
if (!found) {
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() {
const adapter = this._adapter
const oldEntries = this._oldEntries
@@ -180,10 +141,16 @@ 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)
// update this.#vhds before eventually skipping transfer, so that
// updateUuidAndChain has all the mandatory data
const vhds = (this.#vhds = mapValues(
const vhds = mapValues(
deltaExport.vdis,
vdi =>
`vdis/${jobId}/${
@@ -193,15 +160,7 @@ 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,
@@ -217,13 +176,38 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
vm,
vmSnapshot,
}
const { size } = await Task.run({ name: 'transfer' }, async () => {
let transferSize = 0
await asyncEach(
Object.keys(deltaExport.vdis),
async id => {
Object.entries(deltaExport.vdis),
async ([id, vdi]) => {
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
@@ -235,6 +219,17 @@ 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,

View File

@@ -1,4 +1,3 @@
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'
@@ -15,7 +14,6 @@ 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
@@ -38,9 +36,7 @@ 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({

View File

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

View File

@@ -230,7 +230,6 @@ 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**

View File

@@ -50,17 +50,7 @@ const RRD_POINTS_PER_STEP: { [key in RRD_STEP]: number } = {
// Utils
// -------------------------------------------------------------------
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
}
function convertNanToNull(value: number) {
return isNaN(value) ? null : value
}
@@ -69,7 +59,7 @@ function parseNumber(value: number | string) {
// -------------------------------------------------------------------
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) =>
defaults(obj, { [property]: defaultValue })[property] as any
@@ -329,14 +319,8 @@ export default class XapiStats {
},
abortSignal,
})
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)
}
// eslint-disable-next-line import/no-named-as-default-member -- https://github.com/json5/json5/issues/287
return JSON5.parse(await resp.text())
}
// To avoid multiple requests, we keep a cache for the stats and
@@ -399,10 +383,7 @@ export default class XapiStats {
abortSignal,
})
const actualStep = parseNumber(json.meta.step)
if (actualStep !== step) {
throw new FaultyGranularity(`Unable to get the true granularity: ${actualStep}`)
}
const actualStep = json.meta.step as number
if (json.data.length > 0) {
// fetched data is organized from the newest to the oldest
@@ -426,15 +407,14 @@ export default class XapiStats {
let stepStats = xoObjectStats[actualStep]
let cacheStepStats = cacheXoObjectStats[actualStep]
const endTimestamp = parseNumber(json.meta.end)
if (stepStats === undefined || stepStats.endTimestamp !== endTimestamp) {
if (stepStats === undefined || stepStats.endTimestamp !== json.meta.end) {
stepStats = xoObjectStats[actualStep] = {
endTimestamp,
endTimestamp: json.meta.end,
interval: actualStep,
canBeExpired: false,
}
cacheStepStats = cacheXoObjectStats[actualStep] = {
endTimestamp,
endTimestamp: json.meta.end,
interval: actualStep,
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) {
if (error instanceof Error && error.name === 'AbortError') {
return

View File

@@ -9,10 +9,7 @@
- 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))
- [Storage/Disks] Handle link to VM for suspended VDIs (PR [#7391](https://github.com/vatesfr/xen-orchestra/pull/7391))
### Bug fixes
@@ -21,7 +18,6 @@
- [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))
### Packages to release

View File

@@ -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'
}
}

View File

@@ -328,34 +328,6 @@ 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.
@@ -441,6 +413,7 @@ const TRANSFORMS = {
startTime: metrics && toTimestamp(metrics.start_time),
secureBoot: obj.platform.secureboot === 'true',
suspendSr: link(obj, 'suspend_SR'),
suspendVdi: link(obj, 'suspend_VDI'),
tags: obj.tags,
VIFs: link(obj, 'VIFs'),
VTPMs: link(obj, 'VTPMs'),
@@ -450,7 +423,8 @@ const TRANSFORMS = {
xenTools,
...getVmGuestToolsProps(obj),
$container,
// TODO: handle local VMs (`VM.get_possible_hosts()`).
$container: isRunning ? link(obj, 'resident_on') : link(obj, 'pool'),
$VBDs: link(obj, 'VBDs'),
// TODO: dedupe
@@ -476,7 +450,6 @@ const TRANSFORMS = {
vm.snapshot_time = toTimestamp(obj.snapshot_time)
vm.$snapshot_of = link(obj, 'snapshot_of')
vm.suspendVdi = link(obj, 'suspend_VDI')
} else if (obj.is_a_template) {
const defaultTemplate = isDefaultTemplate(obj)
vm.type += '-template'

View File

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

View File

@@ -253,10 +253,6 @@ 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 = {

View File

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

View File

@@ -12,7 +12,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import renderXoItem, { Vdi } from 'render-xo-item'
import renderXoItem, { Vdi, Vm } from 'render-xo-item'
import { confirm } from 'modal'
import { injectIntl } from 'react-intl'
import { Text } from 'editable'
@@ -123,67 +123,76 @@ const COLUMNS = [
vms: getAllVms(state, props),
vbds: getVbds(state, props),
})
})(({ vbds, vms }) => {
})(({ item: vdi, vbds, vms, userData: { vmsSnapshotsBySuspendVdi } }) => {
const vmSnapshot = vmsSnapshotsBySuspendVdi[vdi.uuid]?.[0]
if (isEmpty(vms)) {
return null
}
return (
<Container>
{map(vbds, (vbd, index) => {
const vm = vms[vbd.VM]
{vbds.length > 0 ? (
map(vbds, (vbd, index) => {
const vm = vms[vbd.VM]
if (vm === undefined) {
return null
}
if (vm === undefined) {
return null
}
const type = vm.type
let link
if (type === 'VM') {
link = `/vms/${vm.id}`
} else if (type === 'VM-template') {
link = `/home?s=${vm.id}&t=VM-template`
} else {
link = vm.$snapshot_of === undefined ? '/dashboard/health' : `/vms/${vm.$snapshot_of}/snapshots`
}
const type = vm.type
let link
if (type === 'VM') {
link = `/vms/${vm.id}`
} else if (type === 'VM-template') {
link = `/home?s=${vm.id}&t=VM-template`
} else {
link = vm.$snapshot_of === undefined ? '/dashboard/health' : `/vms/${vm.$snapshot_of}/snapshots`
}
return (
<Row className={index > 0 && 'mt-1'}>
<Col mediumSize={8}>
<Link to={link}>{renderXoItem(vm)}</Link>
</Col>
<Col mediumSize={4}>
<ButtonGroup>
{vbd.attached ? (
return (
<Row className={index > 0 && 'mt-1'}>
<Col mediumSize={8}>
<Link to={link}>{renderXoItem(vm)}</Link>
</Col>
<Col mediumSize={4}>
<ButtonGroup>
{vbd.attached ? (
<ActionRowButton
btnStyle='danger'
handler={disconnectVbd}
handlerParam={vbd}
icon='disconnect'
tooltip={_('vbdDisconnect')}
/>
) : (
<ActionRowButton
btnStyle='primary'
disabled={some(vbds, 'attached') || !isVmRunning(vm)}
handler={connectVbd}
handlerParam={vbd}
icon='connect'
tooltip={_('vbdConnect')}
/>
)}
<ActionRowButton
btnStyle='danger'
handler={disconnectVbd}
handler={deleteVbd}
handlerParam={vbd}
icon='disconnect'
tooltip={_('vbdDisconnect')}
icon='vdi-forget'
tooltip={_('vdiForget')}
/>
) : (
<ActionRowButton
btnStyle='primary'
disabled={some(vbds, 'attached') || !isVmRunning(vm)}
handler={connectVbd}
handlerParam={vbd}
icon='connect'
tooltip={_('vbdConnect')}
/>
)}
<ActionRowButton
btnStyle='danger'
handler={deleteVbd}
handlerParam={vbd}
icon='vdi-forget'
tooltip={_('vdiForget')}
/>
</ButtonGroup>
</Col>
</Row>
)
})}
</ButtonGroup>
</Col>
</Row>
)
})
) : (
<Col mediumSize={8}>
<Link to={`/vms/${vmSnapshot.$snapshot_of}/snapshots`}>
<Vm id={vmSnapshot.$snapshot_of} />
</Link>
</Col>
)}
</Container>
)
}),
@@ -304,6 +313,7 @@ class NewDisk extends Component {
@connectStore(() => ({
checkPermissions: getCheckPermissions,
vbds: createGetObjectsOfType('VBD'),
vmsSnapshotsBySuspendVdi: createGetObjectsOfType('VM-snapshot').groupBy('suspendVdi'),
}))
export default class SrDisks extends Component {
_closeNewDiskForm = () => this.setState({ newDisk: false })
@@ -434,6 +444,7 @@ export default class SrDisks extends Component {
columns={COLUMNS}
data-isVdiAttached={this._getIsVdiAttached()}
data-vdisByBaseCopy={this._getVdisByBaseCopy()}
data-vmsSnapshotsBySuspendVdi={this.props.vmsSnapshotsBySuspendVdi}
defaultFilter='filterOnlyManaged'
filters={FILTERS}
groupedActions={GROUPED_ACTIONS}