Compare commits

..

38 Commits

Author SHA1 Message Date
Julien Fontanet
9cae978923 feat(xo-server): 5.19.9 2018-05-22 19:43:19 +02:00
Julien Fontanet
311d914b96 feat(xo-web/import): remove restriction for Free 2018-05-22 16:27:17 +02:00
Julien Fontanet
592cb4ef9e feat(xo-web): 5.19.8 2018-05-22 15:53:45 +02:00
Julien Fontanet
ec2db7f2d0 feat(xo-vmdk-to-vhd): 0.1.2 2018-05-22 15:53:03 +02:00
Julien Fontanet
71eab7ba9b feat(xo-web): 5.19.7 2018-05-22 15:36:54 +02:00
badrAZ
5e07171d60 fix(xo-server/backup-ng): fix incorrect condition (#2971) 2018-05-22 11:45:02 +02:00
Rajaa.BARHTAOUI
3f73e3d964 fix(xo-server,xo-web): message when no Xen tools (#2916)
Fixes #2911
2018-05-22 09:57:46 +02:00
badrAZ
0ebe78b4a2 feat(xo-server-usage-report): improve the report (#2970)
Fixes #2968
2018-05-21 16:25:40 +02:00
Julien Fontanet
61c3379298 feat(xo-server): 5.19.8 2018-05-21 11:00:19 +02:00
Julien Fontanet
44866f3316 feat(xo-web): 5.19.6 2018-05-21 10:42:37 +02:00
Julien Fontanet
4bb8ce8779 feat(vhd-lib): 0.1.0 2018-05-21 10:03:37 +02:00
Julien Fontanet
58eb6a8b5f feat(xo-server): 5.19.7 2018-05-18 18:44:34 +02:00
Julien Fontanet
52f6a79e01 fix(xo-server/backupNg/logs): include merge/transfer size (#2965) 2018-05-18 18:44:07 +02:00
Julien Fontanet
129f79d44b feat(xo-web): 5.19.5 2018-05-18 18:42:31 +02:00
Julien Fontanet
385c3eb563 feat(xo-vmdk-to-vhd): 0.1.1 2018-05-18 18:40:26 +02:00
Julien Fontanet
e56be51b45 chore(xo-server/backups-ng): remove incorrect TODO 2018-05-18 17:14:50 +02:00
Olivier Lambert
24ae65b254 fix(xo-server/sr.createNfs): nfsVersion → nfsOptions (#2904) 2018-05-18 16:28:02 +02:00
badrAZ
d5dffbacbd fix(xo-web/FormattedDuration): handle duration < 0 seconds (#2964) 2018-05-18 15:06:23 +02:00
Julien Fontanet
c6ae969a82 fix(xo-server/https): ask for passphrase (#2963)
Fixes #2962
2018-05-18 15:05:49 +02:00
Nicolas Raynaud
005a9fdc01 fix(xo-vmdk-to-vhd): various bugs (#2961) 2018-05-18 14:02:19 +02:00
Jerome Charaoui
f505d4d911 Fix SR creation when using options or NFSv4 (#2960) 2018-05-17 22:12:09 +02:00
badrAZ
8ada6b121e fix(backup-ng/logs): handle the case when transfer duration equals 0 (#2954) 2018-05-17 16:58:29 +02:00
Julien Fontanet
b9a87efb0d fix(xo-server/backupNg): dont fail on corrupted VHDs (#2957)
Corrupted VHD files (usually uncleaned temporary) could fail the job.
2018-05-17 11:27:02 +02:00
Pierre Donias
89485a82d2 feat(xo-web): make many objects' UUID copiable (#2955)
Fixes #2925

- host/tab-network
- pool/tab-network
- vm/tab-disks
- vm/tab-network
- vm/tab-snapshots
2018-05-16 17:39:47 +02:00
Pierre Donias
451f87c6b4 feat(xo-web/servers): allow unauthorized cert. when adding server (#2953)
Fixes #2926
2018-05-16 13:44:27 +02:00
Rajaa.BARHTAOUI
c3cb5a3221 feat(xo-server,xo-web): VM HA options (#2946)
Fixes #2917
2018-05-16 13:27:40 +02:00
Julien Fontanet
458609ed2e feat(xo-server): 5.19.6 2018-05-16 10:32:59 +02:00
Julien Fontanet
fcec8113f3 fix(xo-server/backupNg): await writeStream (#2951) 2018-05-16 10:32:38 +02:00
Julien Fontanet
ebbd882ee4 feat(xo-web): 5.19.4 2018-05-15 17:44:25 +02:00
Julien Fontanet
0506e19a66 chore(xo-server/backups-ng): update todo list 2018-05-15 17:44:09 +02:00
Pierre Donias
ecc62e4f54 fix(xo-web/xosan): install packs button condition (#2950) 2018-05-15 17:40:40 +02:00
Julien Fontanet
2b95eb4e4d feat(xo-web): 5.19.3 2018-05-15 16:11:53 +02:00
Julien Fontanet
bcde9e0f74 feat(xo-server): 5.19.5 2018-05-15 16:11:34 +02:00
Pierre Donias
114501ebc7 feat(XOSAN): allow user to update packs (#2782) 2018-05-15 16:11:04 +02:00
badrAZ
ebab7c0867 fix(backup-ng/logs): handle the case when transfer/merge duration equals 0 (#2949) 2018-05-15 16:10:17 +02:00
Julien Fontanet
0e2270fb6e feat(xo-web): 5.19.2 2018-05-15 14:46:33 +02:00
Julien Fontanet
593493ec0c feat(xo-server): 5.19.4 2018-05-15 14:46:07 +02:00
Julien Fontanet
d92898a806 feat(xo-vmdk-to-vhd): 0.1.0 2018-05-15 14:45:19 +02:00
40 changed files with 1448 additions and 1251 deletions

View File

@@ -20,5 +20,10 @@ declare module 'lodash' {
iteratee: (V1, K) => V2
): { [K]: V2 }
declare export function noop(...args: mixed[]): void
declare export function some<T>(
collection: T[],
iteratee: (T, number) => boolean
): boolean
declare export function sum(values: number[]): number
declare export function values<K, V>(object: { [K]: V }): V[]
}

View File

@@ -30,7 +30,7 @@
"babel-runtime": "^6.22.0",
"exec-promise": "^0.7.0",
"struct-fu": "^1.2.0",
"vhd-lib": "^0.0.0"
"vhd-lib": "^0.1.0"
},
"devDependencies": {
"babel-cli": "^6.24.1",

View File

@@ -1,6 +1,6 @@
{
"name": "vhd-lib",
"version": "0.0.0",
"version": "0.1.0",
"license": "AGPL-3.0",
"description": "Primitives for VHD file handling",
"keywords": [],

View File

@@ -1,3 +1,4 @@
import assert from 'assert'
import asyncIteratorToStream from 'async-iterator-to-stream'
import computeGeometryForSize from './_computeGeometryForSize'
@@ -25,62 +26,16 @@ function createBAT (
bat,
bitmapSize
) {
const vhdOccupationTable = []
let currentVhdPositionSector = firstBlockPosition / SECTOR_SIZE
blockAddressList.forEach(blockPosition => {
const scaled = blockPosition / VHD_BLOCK_SIZE_BYTES
const vhdTableIndex = Math.floor(scaled)
assert.strictEqual(blockPosition % 512, 0)
const vhdTableIndex = Math.floor(blockPosition / VHD_BLOCK_SIZE_BYTES)
if (bat.readUInt32BE(vhdTableIndex * 4) === BLOCK_UNUSED) {
bat.writeUInt32BE(currentVhdPositionSector, vhdTableIndex * 4)
currentVhdPositionSector +=
(bitmapSize + VHD_BLOCK_SIZE_BYTES) / SECTOR_SIZE
}
// not using bit operators to avoid the int32 coercion, that way we can go to 53 bits
vhdOccupationTable[vhdTableIndex] =
(vhdOccupationTable[vhdTableIndex] || 0) +
Math.pow(2, (scaled % 1) * ratio)
})
return vhdOccupationTable
}
function createBitmap (bitmapSize, ratio, vhdOccupationBucket) {
const bitmap = Buffer.alloc(bitmapSize)
for (let i = 0; i < VHD_BLOCK_SIZE_SECTORS / ratio; i++) {
// do not shift to avoid int32 coercion
if ((vhdOccupationBucket * Math.pow(2, -i)) & 1) {
for (let j = 0; j < ratio; j++) {
setBitmap(bitmap, i * ratio + j)
}
}
}
return bitmap
}
function * yieldIfNotEmpty (buffer) {
if (buffer.length > 0) {
yield buffer
}
}
async function * generateFileContent (
blockIterator,
bitmapSize,
ratio,
vhdOccupationTable
) {
let currentVhdBlockIndex = -1
let currentBlockBuffer = Buffer.alloc(0)
for await (const next of blockIterator) {
const batEntry = Math.floor(next.offsetBytes / VHD_BLOCK_SIZE_BYTES)
if (batEntry !== currentVhdBlockIndex) {
yield * yieldIfNotEmpty(currentBlockBuffer)
currentBlockBuffer = Buffer.alloc(VHD_BLOCK_SIZE_BYTES)
currentVhdBlockIndex = batEntry
yield createBitmap(bitmapSize, ratio, vhdOccupationTable[batEntry])
}
next.data.copy(currentBlockBuffer, next.offsetBytes % VHD_BLOCK_SIZE_BYTES)
}
yield * yieldIfNotEmpty(currentBlockBuffer)
}
export default asyncIteratorToStream(async function * (
@@ -123,21 +78,49 @@ export default asyncIteratorToStream(async function * (
const bitmapSize =
Math.ceil(VHD_BLOCK_SIZE_SECTORS / 8 / SECTOR_SIZE) * SECTOR_SIZE
const bat = Buffer.alloc(tablePhysicalSizeBytes, 0xff)
const vhdOccupationTable = createBAT(
firstBlockPosition,
blockAddressList,
ratio,
bat,
bitmapSize
)
yield footer
yield header
yield bat
yield * generateFileContent(
blockIterator,
bitmapSize,
ratio,
vhdOccupationTable
)
yield footer
createBAT(firstBlockPosition, blockAddressList, ratio, bat, bitmapSize)
let position = 0
function * yieldAndTrack (buffer, expectedPosition) {
if (expectedPosition !== undefined) {
assert.strictEqual(position, expectedPosition)
}
if (buffer.length > 0) {
yield buffer
position += buffer.length
}
}
async function * generateFileContent (blockIterator, bitmapSize, ratio) {
let currentBlock = -1
let currentVhdBlockIndex = -1
let currentBlockWithBitmap = Buffer.alloc(0)
for await (const next of blockIterator) {
currentBlock++
assert.strictEqual(blockAddressList[currentBlock], next.offsetBytes)
const batIndex = Math.floor(next.offsetBytes / VHD_BLOCK_SIZE_BYTES)
if (batIndex !== currentVhdBlockIndex) {
if (currentVhdBlockIndex >= 0) {
yield * yieldAndTrack(
currentBlockWithBitmap,
bat.readUInt32BE(currentVhdBlockIndex * 4) * 512
)
}
currentBlockWithBitmap = Buffer.alloc(bitmapSize + VHD_BLOCK_SIZE_BYTES)
currentVhdBlockIndex = batIndex
}
const blockOffset = (next.offsetBytes / 512) % VHD_BLOCK_SIZE_SECTORS
for (let bitPos = 0; bitPos < VHD_BLOCK_SIZE_SECTORS / ratio; bitPos++) {
setBitmap(currentBlockWithBitmap, blockOffset + bitPos)
}
next.data.copy(
currentBlockWithBitmap,
bitmapSize + next.offsetBytes % VHD_BLOCK_SIZE_BYTES
)
}
yield * yieldAndTrack(currentBlockWithBitmap)
}
yield * yieldAndTrack(footer, 0)
yield * yieldAndTrack(header, FOOTER_SIZE)
yield * yieldAndTrack(bat, FOOTER_SIZE + HEADER_SIZE)
yield * generateFileContent(blockIterator, bitmapSize, ratio)
yield * yieldAndTrack(footer)
})

View File

@@ -102,15 +102,15 @@ test('ReadableSparseVHDStream can handle a sparse file', async () => {
data: Buffer.alloc(blockSize, 'azerzaerazeraze', 'ascii'),
},
{
offsetBytes: blockSize * 5,
offsetBytes: blockSize * 100,
data: Buffer.alloc(blockSize, 'gdfslkdfguer', 'ascii'),
},
]
const fileSize = blockSize * 10
const fileSize = blockSize * 110
const stream = createReadableSparseVHDStream(
fileSize,
blockSize,
[100, 700],
blocks.map(b => b.offsetBytes),
blocks
)
const pipe = stream.pipe(createWriteStream('output.vhd'))

View File

@@ -139,8 +139,8 @@ Handlebars.registerHelper(
new Handlebars.SafeString(
isFinite(+value) && +value !== 0
? (value = round(value, 2)) > 0
? `(<b style="color: green;">▲ ${value}</b>)`
: `(<b style="color: red;">▼ ${String(value).slice(1)}</b>)`
? `(<b style="color: green;">▲ ${value}%</b>)`
: `(<b style="color: red;">▼ ${String(value).slice(1)}%</b>)`
: ''
)
)
@@ -270,12 +270,16 @@ async function getHostsStats ({ runningHosts, xo }) {
function getSrsStats (xoObjects) {
return orderBy(
map(filter(xoObjects, { type: 'SR' }), sr => {
map(filter(xoObjects, obj => obj.type === 'SR' && obj.size > 0), sr => {
const total = sr.size / gibPower
const used = sr.physical_usage / gibPower
let name = sr.name_label
if (!sr.shared) {
name += ` (${find(xoObjects, { id: sr.$container }).name_label})`
}
return {
uuid: sr.uuid,
name: sr.name_label,
name,
total,
used,
free: total - used,

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server",
"version": "5.19.3",
"version": "5.19.9",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -111,7 +111,7 @@
"tmp": "^0.0.33",
"uuid": "^3.0.1",
"value-matcher": "^0.2.0",
"vhd-lib": "^0.0.0",
"vhd-lib": "^0.1.0",
"ws": "^5.0.0",
"xen-api": "^0.16.9",
"xml2js": "^0.4.19",
@@ -119,7 +119,7 @@
"xo-collection": "^0.4.1",
"xo-common": "^0.1.1",
"xo-remote-parser": "^0.3",
"xo-vmdk-to-vhd": "0.0.12",
"xo-vmdk-to-vhd": "^0.1.2",
"yazl": "^2.4.3"
},
"devDependencies": {

View File

@@ -204,8 +204,8 @@ export async function createNfs ({
}
// if NFS options given
if (nfsVersion) {
deviceConfig.options = nfsVersion
if (nfsOptions) {
deviceConfig.options = nfsOptions
}
const srRef = await xapi.call(

View File

@@ -12,6 +12,10 @@ import { forEach, map, mapFilter, parseSize } from '../utils'
// ===================================================================
export function getHaValues () {
return ['best-effort', 'restart', '']
}
function checkPermissionOnSrs (vm, permission = 'operate') {
const permissions = []
forEach(vm.$VBDs, vbdId => {
@@ -556,11 +560,11 @@ set.params = {
name_description: { type: 'string', optional: true },
// TODO: provides better filtering of values for HA possible values: "best-
// effort" meaning "try to restart this VM if possible but don't consider the
// Pool to be overcommitted if this is not possible"; "restart" meaning "this
// VM should be restarted"; "" meaning "do not try to restart this VM"
high_availability: { type: 'boolean', optional: true },
high_availability: {
optional: true,
pattern: new RegExp(`^(${getHaValues().join('|')})$`),
type: 'string',
},
// Number of virtual CPUs to allocate.
CPUs: { type: 'integer', optional: true },

View File

@@ -7,6 +7,7 @@ import has from 'lodash/has'
import helmet from 'helmet'
import includes from 'lodash/includes'
import proxyConsole from './proxy-console'
import pw from 'pw'
import serveStatic from 'serve-static'
import startsWith from 'lodash/startsWith'
import stoppable from 'stoppable'
@@ -227,12 +228,12 @@ async function registerPlugin (pluginPath, pluginName) {
// instance.
const instance = isFunction(factory)
? factory({
xo: this,
getDataDir: () => {
const dir = `${this._config.datadir}/${pluginName}`
return ensureDir(dir).then(() => dir)
},
})
xo: this,
getDataDir: () => {
const dir = `${this._config.datadir}/${pluginName}`
return ensureDir(dir).then(() => dir)
},
})
: factory
await this.registerPlugin(
@@ -311,6 +312,13 @@ async function makeWebServerListen (
) {
if (cert && key) {
;[opts.cert, opts.key] = await Promise.all([readFile(cert), readFile(key)])
if (opts.key.includes('ENCRYPTED')) {
opts.passphrase = await new Promise(resolve => {
console.log('Encrypted key %s', key)
process.stdout.write(`Enter pass phrase: `)
pw(resolve)
})
}
}
try {
const niceAddress = await webServer.listen(opts)

View File

@@ -227,12 +227,16 @@ const TRANSFORMS = {
return
}
if (!guestMetrics) {
if (guestMetrics === undefined) {
return false
}
const { major, minor } = guestMetrics.PV_drivers_version
if (major === undefined || minor === undefined) {
return false
}
return {
major,
minor,
@@ -292,8 +296,7 @@ const TRANSFORMS = {
}
})(),
// TODO: there is two possible value: "best-effort" and "restart"
high_availability: Boolean(obj.ha_restart_priority),
high_availability: obj.ha_restart_priority,
memory: (function () {
const dynamicMin = +obj.memory_dynamic_min

View File

@@ -35,8 +35,15 @@ declare class XapiObject {
}
type Id = string | XapiObject
declare export class Vbd extends XapiObject {
type: string;
VDI: string;
}
declare export class Vm extends XapiObject {
$snapshots: Vm[];
$VBDs: Vbd[];
is_a_snapshot: boolean;
is_a_template: boolean;
name_label: string;

View File

@@ -310,11 +310,7 @@ export default {
highAvailability: {
set (ha, vm) {
return this.call(
'VM.set_ha_restart_priority',
vm.$ref,
ha ? 'restart' : ''
)
return this.call('VM.set_ha_restart_priority', vm.$ref, ha)
},
},

View File

@@ -13,9 +13,11 @@ import {
last,
mapValues,
noop,
some,
sum,
values,
} from 'lodash'
import { timeout as pTimeout } from 'promise-toolbox'
import { fromEvent as pFromEvent, timeout as pTimeout } from 'promise-toolbox'
import Vhd, {
chainVhd,
createSyntheticStream as createVhdReadStream,
@@ -304,6 +306,7 @@ const writeStream = async (
const output = await handler.createOutputStream(tmpPath, { checksum })
try {
input.pipe(output)
await pFromEvent(output, 'finish')
await output.checksumWritten
// $FlowFixMe
await input.task
@@ -659,7 +662,7 @@ export default class BackupNg {
// 2. next run should be a full
// - [ ] add a lock on the job/VDI during merge which should prevent other merges and restoration
// - [ ] check merge/transfert duration/size are what we want for delta
// - [ ] fix backup reports
// - [ ] in case of failure, correctly clean VHDs for all VDIs
//
// Low:
// - [ ] jobs should be cancelable
@@ -692,6 +695,7 @@ export default class BackupNg {
// - [x] replicated VMs should be discriminated by VM (vatesfr/xen-orchestra#2807)
// - [x] clones of replicated VMs should not be garbage collected
// - [x] import for delta
// - [x] fix backup reports
@defer
async _backupVm (
$defer: any,
@@ -736,6 +740,15 @@ export default class BackupNg {
}
}
if (
!some(
vm.$VBDs,
vbd => vbd.type === 'Disk' && vbd.VDI !== 'OpaqueRef:NULL'
)
) {
throw new Error('no disks found')
}
const snapshots = vm.$snapshots
.filter(_ => _.other_config['xo:backup:job'] === jobId)
.sort(compareSnapshotTime)
@@ -869,9 +882,7 @@ export default class BackupNg {
logger,
message: 'transfer',
parentId: taskId,
result: {
size: 0,
},
result: () => ({ size: xva.size }),
},
writeStream(fork, handler, dataFilename)
)
@@ -914,9 +925,7 @@ export default class BackupNg {
logger,
message: 'transfer',
parentId: taskId,
result: {
size: 0,
},
result: () => ({ size: xva.size }),
},
xapi._importVm($cancelToken, fork, sr, vm =>
xapi._setObjectProperties(vm, {
@@ -1048,9 +1057,7 @@ export default class BackupNg {
logger,
message: 'merge',
parentId: taskId,
result: {
size: 0,
},
result: size => ({ size }),
},
this._deleteDeltaVmBackups(handler, oldBackups)
)
@@ -1067,9 +1074,7 @@ export default class BackupNg {
logger,
message: 'transfer',
parentId: taskId,
result: {
size: 0,
},
result: size => ({ size }),
},
asyncMap(
fork.vdis,
@@ -1103,10 +1108,11 @@ export default class BackupNg {
if (isDelta) {
await chainVhd(handler, parentPath, handler, path)
}
})
)
)
return handler.getSize(path)
})
).then(sum)
)
await handler.outputFile(metadataFilename, jsonMetadata)
if (!deleteFirst) {
@@ -1144,9 +1150,7 @@ export default class BackupNg {
logger,
message: 'transfer',
parentId: taskId,
result: {
size: 0,
},
result: ({ transferSize }) => ({ size: transferSize }),
},
xapi.importDeltaVm(fork, {
disableStartAfterImport: false, // we'll take care of that
@@ -1185,19 +1189,17 @@ export default class BackupNg {
async _deleteDeltaVmBackups (
handler: RemoteHandler,
backups: MetadataDelta[]
): Promise<void> {
// TODO: remove VHD as well
await asyncMap(backups, async backup => {
): Promise<number> {
return asyncMap(backups, async backup => {
const filename = ((backup._filename: any): string)
return Promise.all([
handler.unlink(filename),
asyncMap(backup.vhds, _ =>
// $FlowFixMe injected $defer param
this._deleteVhd(handler, resolveRelativeFromFile(filename, _))
),
])
})
await handler.unlink(filename)
return asyncMap(backup.vhds, _ =>
// $FlowFixMe injected $defer param
this._deleteVhd(handler, resolveRelativeFromFile(filename, _))
).then(sum)
}).then(sum)
}
async _deleteFullVmBackups (
@@ -1215,35 +1217,50 @@ export default class BackupNg {
// FIXME: synchronize by job/VDI, otherwise it can cause issues with the merge
@defer
async _deleteVhd ($defer: any, handler: RemoteHandler, path: string) {
async _deleteVhd (
$defer: any,
handler: RemoteHandler,
path: string
): Promise<number> {
const vhds = await asyncMap(
await handler.list(dirname(path), { filter: isVhd, prependDir: true }),
async path => {
const vhd = new Vhd(handler, path)
await vhd.readHeaderAndFooter()
return {
footer: vhd.footer,
header: vhd.header,
path,
try {
const vhd = new Vhd(handler, path)
await vhd.readHeaderAndFooter()
return {
footer: vhd.footer,
header: vhd.header,
path,
}
} catch (error) {
// Do not fail on corrupted VHDs (usually uncleaned temporary files),
// they are probably inconsequent to the backup process and should not
// fail it.
console.warn('BackupNg#_deleteVhd', path, error)
}
}
)
const base = basename(path)
const child = vhds.find(_ => _.header.parentUnicodeName === base)
const child = vhds.find(
_ => _ !== undefined && _.header.parentUnicodeName === base
)
if (child === undefined) {
return handler.unlink(path)
await handler.unlink(path)
return 0
}
$defer.onFailure.call(handler, 'unlink', path)
const childPath = child.path
await this._app.worker.mergeVhd(
const mergedDataSize: number = await this._app.worker.mergeVhd(
handler._remote,
path,
handler._remote,
childPath
)
await handler.rename(path, childPath)
return mergedDataSize
}
async _deleteVms (xapi: Xapi, vms: Vm[]): Promise<void> {
@@ -1328,11 +1345,19 @@ export default class BackupNg {
case 'task.end':
const task = logs[data.taskId]
if (task !== undefined) {
task.status = data.status
task.taskId = data.taskId
task.result = data.result
task.end = time
task.duration = time - task.start
// work-around
if (
time === task.start &&
(message === 'merge' || message === 'transfer')
) {
delete logs[data.taskId]
} else {
task.status = data.status
task.taskId = data.taskId
task.result = data.result
task.end = time
task.duration = time - task.start
}
}
}
})

View File

@@ -1,6 +1,6 @@
{
"name": "xo-vmdk-to-vhd",
"version": "0.0.12",
"version": "0.1.2",
"license": "AGPL-3.0",
"description": "JS lib streaming a vmdk file to a vhd",
"keywords": [
@@ -25,11 +25,10 @@
"dependencies": {
"@babel/runtime": "^7.0.0-beta.44",
"child-process-promise": "^2.0.3",
"fs-promise": "^2.0.0",
"pipette": "^0.9.3",
"promise-toolbox": "^0.9.5",
"tmp": "^0.0.33",
"vhd-lib": "^0.0.0"
"vhd-lib": "^0.1.0"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",

View File

@@ -1,6 +1,6 @@
/* eslint-env jest */
import { createReadStream, readFile } from 'fs-promise'
import { createReadStream, readFile } from 'fs-extra'
import { exec } from 'child-process-promise'
import { fromCallback as pFromCallback } from 'promise-toolbox'
import rimraf from 'rimraf'

View File

@@ -1,6 +1,6 @@
/* eslint-env jest */
import { createReadStream } from 'fs-promise'
import { createReadStream } from 'fs-extra'
import { exec } from 'child-process-promise'
import { fromCallback as pFromCallback } from 'promise-toolbox'
import rimraf from 'rimraf'

View File

@@ -308,17 +308,15 @@ export class VMDKDirectParser {
export async function readVmdkGrainTable (fileAccessor) {
let headerBuffer = await fileAccessor(0, 512)
let grainDirAddr = headerBuffer.slice(56, 56 + 8)
let grainAddrBuffer = headerBuffer.slice(56, 56 + 8)
if (
new Int8Array(grainDirAddr).reduce((acc, val) => acc && val === -1, true)
new Int8Array(grainAddrBuffer).reduce((acc, val) => acc && val === -1, true)
) {
headerBuffer = await fileAccessor(-1024, -1024 + 512)
grainDirAddr = new DataView(headerBuffer.slice(56, 56 + 8)).getUint32(
0,
true
)
grainAddrBuffer = headerBuffer.slice(56, 56 + 8)
}
const grainDirPosBytes = grainDirAddr * 512
const grainDirPosBytes =
new DataView(grainAddrBuffer).getUint32(0, true) * 512
const capacity =
new DataView(headerBuffer.slice(12, 12 + 8)).getUint32(0, true) * 512
const grainSize =

View File

@@ -6,7 +6,7 @@ import getStream from 'get-stream'
import rimraf from 'rimraf'
import tmp from 'tmp'
import { createReadStream, createWriteStream, stat } from 'fs-promise'
import { createReadStream, createWriteStream, stat } from 'fs-extra'
import { fromCallback as pFromCallback } from 'promise-toolbox'
import convertFromVMDK, { readVmdkGrainTable } from '.'
@@ -49,7 +49,7 @@ test('VMDK to VHD can convert a random data file with VMDKDirectParser', async (
const vhdFileName = 'from-vmdk-VMDKDirectParser.vhd'
const reconvertedFromVhd = 'from-vhd.raw'
const reconvertedFromVmdk = 'from-vhd-by-vbox.raw'
const dataSize = 8355840 // this number is an integer head/cylinder/count equation solution
const dataSize = 100 * 1024 * 1024 // this number is an integer head/cylinder/count equation solution
try {
await execa.shell(
'base64 /dev/urandom | head -c ' + dataSize + ' > ' + inputRawFileName
@@ -82,6 +82,7 @@ test('VMDK to VHD can convert a random data file with VMDKDirectParser', async (
reconvertedFromVhd,
])
await execa('qemu-img', ['compare', inputRawFileName, vhdFileName])
await execa('qemu-img', ['compare', vmdkFileName, vhdFileName])
} catch (error) {
console.error(error.stdout)
console.error(error.stderr)

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.19.1",
"version": "5.19.8",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -33,7 +33,6 @@
"@julien-f/freactal": "0.1.0",
"@nraynaud/novnc": "0.6.1",
"@xen-orchestra/cron": "^1.0.3",
"xo-vmdk-to-vhd": "0.0.12",
"ansi_up": "^3.0.0",
"asap": "^2.0.6",
"babel-core": "^6.26.0",
@@ -60,6 +59,7 @@
"classnames": "^2.2.3",
"complex-matcher": "^0.3.0",
"cookies-js": "^1.2.2",
"copy-to-clipboard": "^3.0.8",
"d3": "^5.0.0",
"debounce-input-decorator": "^0.1.0",
"enzyme": "^3.3.0",
@@ -137,7 +137,8 @@
"xo-acl-resolver": "^0.2.3",
"xo-common": "^0.1.1",
"xo-lib": "^0.8.0",
"xo-remote-parser": "^0.3"
"xo-remote-parser": "^0.3",
"xo-vmdk-to-vhd": "^0.1.2"
},
"scripts": {
"build": "NODE_ENV=production gulp build",

View File

@@ -1,10 +1,9 @@
import isFunction from 'lodash/isFunction'
import isString from 'lodash/isString'
import moment from 'moment'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { FormattedMessage, IntlProvider as IntlProvider_ } from 'react-intl'
import { every, isFunction, isString } from 'lodash'
import locales from './locales'
import messages from './messages'
@@ -102,8 +101,16 @@ export class FormattedDuration extends Component {
)
render () {
const parsedDuration = this._parseDuration()
return (
<Tooltip content={getMessage('durationFormat', this._parseDuration())}>
<Tooltip
content={getMessage(
every(parsedDuration, n => n === 0)
? 'secondsFormat'
: 'durationFormat',
parsedDuration
)}
>
<span>{this._humanizeDuration()}</span>
</Tooltip>
)

View File

@@ -3857,7 +3857,8 @@ export default {
xosanUsedSpace: 'Espace utilisé',
// Original text: "XOSAN pack needs to be installed on each host of the pool."
xosanNeedPack: 'La pack XOSAN doit être installé sur tous les hôtes du pool.',
xosanNeedPack:
'Le pack XOSAN doit être installé et à jour sur tous les hôtes du pool.',
// Original text: "Install it now!"
xosanInstallIt: 'Installer maintenant !',

View File

@@ -41,6 +41,7 @@ const messages = {
// ----- Copiable component -----
copyToClipboard: 'Copy to clipboard',
copyUuid: 'Copy {uuid}',
// ----- Pills -----
pillMaster: 'Master',
@@ -932,6 +933,7 @@ const messages = {
defaultCpuCap: 'Default ({value, number})',
pvArgsLabel: 'PV args',
xenToolsStatus: 'Xen tools version',
xenToolsNotInstalled: 'Not installed',
osName: 'OS name',
osKernel: 'OS kernel',
autoPowerOn: 'Auto power on',
@@ -956,6 +958,7 @@ const messages = {
vmCoresPerSocketIncorrectValue: 'Incorrect cores per socket value',
vmCoresPerSocketIncorrectValueSolution:
'Please change the selected value to fix it.',
vmHaDisabled: 'disabled',
vmMemoryLimitsLabel: 'Memory limits (min/max)',
vmMaxVcpus: 'vCPUs max:',
vmMaxRam: 'Memory max:',
@@ -1766,7 +1769,8 @@ const messages = {
xosanUsedSpace: 'Used space',
xosanLicense: 'License',
xosanMultipleLicenses: 'This XOSAN has more than 1 license!',
xosanNeedPack: 'XOSAN pack needs to be installed on each host of the pool.',
xosanNeedPack:
'XOSAN pack needs to be installed and up to date on each host of the pool.',
xosanInstallIt: 'Install it now!',
xosanNeedRestart:
'Some hosts need their toolstack to be restarted before you can create an XOSAN',
@@ -1794,6 +1798,14 @@ const messages = {
xosanPbdsDetached: 'Some SRs are detached from the XOSAN',
xosanBadStatus: 'Something is wrong with: {badStatuses}',
xosanRunning: 'Running',
xosanUpdatePacks: 'Update packs',
xosanPackUpdateChecking: 'Checking for updates',
xosanPackUpdateError:
'Error while checking XOSAN packs. Please make sure that the Cloud plugin is installed and loaded and that the updater is reachable.',
xosanPackUpdateUnavailable: 'XOSAN resources are unavailable',
xosanPackUpdateUnregistered: 'Not registered for XOSAN resources',
xosanPackUpdateUpToDate: "✓ This pool's XOSAN packs are up to date!",
xosanPackUpdateVersion: 'Update pool with latest pack v{version}',
xosanDelete: 'Delete XOSAN',
xosanFixIssue: 'Fix',
xosanCreatingOn: 'Creating XOSAN on {pool}',
@@ -1810,12 +1822,8 @@ const messages = {
xosanRegister: 'Register your appliance first',
xosanLoading: 'Loading…',
xosanNotAvailable: 'XOSAN is not available at the moment',
xosanInstallPackOnHosts: 'Install XOSAN pack on these hosts:',
xosanInstallPack: 'Install {pack} v{version}?',
xosanNoPackFound:
'No compatible XOSAN pack found for your XenServer versions.',
xosanPackRequirements:
'At least one of these version requirements must be satisfied by all the hosts in this pool:',
// SR tab XOSAN
xosanVmsNotRunning: 'Some XOSAN Virtual Machines are not running',
xosanVmsNotFound: 'Some XOSAN Virtual Machines could not be found',
@@ -1898,6 +1906,7 @@ const messages = {
xosanLoadXoaPlugin: 'Load XOA plugin first',
// ----- Utils -----
secondsFormat: '{seconds, plural, one {# second} other {# seconds}}',
durationFormat:
'{days, plural, =0 {} one {# day } other {# days }}{hours, plural, =0 {} one {# hour } other {# hours }}{minutes, plural, =0 {} one {# minute } other {# minutes }}{seconds, plural, =0 {} one {# second} other {# seconds}}',
}

View File

@@ -209,13 +209,21 @@ class IndividualAction extends Component {
(disabled, item, userData) =>
isFunction(disabled) ? disabled(item, userData) : disabled
)
_getLabel = createSelector(
() => this.props.label,
() => this.props.item,
() => this.props.userData,
(label, item, userData) =>
isFunction(label) ? label(item, userData) : label
)
_executeAction = () => {
const p = this.props
return p.handler(p.item, p.userData)
}
render () {
const { icon, item, label, level, redirectOnSuccess, userData } = this.props
const { icon, item, level, redirectOnSuccess, userData } = this.props
return (
<ActionRowButton
@@ -226,7 +234,7 @@ class IndividualAction extends Component {
handler={this._executeAction}
icon={icon}
redirectOnSuccess={redirectOnSuccess}
tooltip={label}
tooltip={this._getLabel()}
/>
)
}
@@ -240,6 +248,13 @@ class GroupedAction extends Component {
(disabled, selectedItems, userData) =>
isFunction(disabled) ? disabled(selectedItems, userData) : disabled
)
_getLabel = createSelector(
() => this.props.label,
() => this.props.selectedItems,
() => this.props.userData,
(label, selectedItems, userData) =>
isFunction(label) ? label(selectedItems, userData) : label
)
_executeAction = () => {
const p = this.props
@@ -247,7 +262,7 @@ class GroupedAction extends Component {
}
render () {
const { icon, label, level } = this.props
const { icon, level } = this.props
return (
<ActionRowButton
@@ -255,7 +270,7 @@ class GroupedAction extends Component {
disabled={this._getIsDisabled()}
handler={this._executeAction}
icon={icon}
tooltip={label}
tooltip={this._getLabel()}
/>
)
}

View File

@@ -20,6 +20,7 @@ import {
mapValues,
replace,
sample,
some,
startsWith,
} from 'lodash'
@@ -28,6 +29,7 @@ import * as actions from './store/actions'
import invoke from './invoke'
import store from './store'
import { getObject } from './selectors'
import { satisfies as versionSatisfies } from 'semver'
export const EMPTY_ARRAY = Object.freeze([])
export const EMPTY_OBJECT = Object.freeze({})
@@ -523,6 +525,40 @@ export const ShortDate = ({ timestamp }) => (
<FormattedDate value={timestamp} month='short' day='numeric' year='numeric' />
)
export const findLatestPack = (packs, hostsVersions) => {
const checkVersion = version =>
!version ||
every(hostsVersions, hostVersion => versionSatisfies(hostVersion, version))
let latestPack = { version: '0' }
forEach(packs, pack => {
if (
pack.type === 'iso' &&
compareVersions(pack.version, '>', latestPack.version) &&
checkVersion(pack.requirements && pack.requirements.xenserver)
) {
latestPack = pack
}
})
if (latestPack.version === '0') {
// No compatible pack was found
return
}
return latestPack
}
export const isLatestXosanPackInstalled = (latestXosanPack, hosts) =>
latestXosanPack !== undefined &&
every(hosts, host =>
some(
host.supplementalPacks,
({ name, version }) =>
name === 'XOSAN' && version === latestXosanPack.version
)
)
// ===================================================================
export const getMemoryUsedMetric = ({ memory, memoryFree = memory }) =>

View File

@@ -461,10 +461,15 @@ export const exportConfig = () =>
// Server ------------------------------------------------------------
export const addServer = (host, username, password, label) =>
_call('server.add', { host, label, password, username })::tap(
subscribeServers.forceRefresh,
() => error(_('serverError'), _('serverAddFailed'))
export const addServer = (host, username, password, label, allowUnauthorized) =>
_call('server.add', {
allowUnauthorized,
host,
label,
password,
username,
})::tap(subscribeServers.forceRefresh, () =>
error(_('serverError'), _('serverAddFailed'))
)
export const editServer = (server, props) =>
@@ -1191,6 +1196,8 @@ export const editVm = (vm, props) =>
export const fetchVmStats = (vm, granularity) =>
_call('vm.stats', { id: resolveId(vm), granularity })
export const getVmsHaValues = () => _call('vm.getHaValues')
export const importVm = (file, type = 'xva', data = undefined, sr) => {
const { name } = file
@@ -2412,20 +2419,6 @@ export const removeXosanBricks = (xosansr, bricks) =>
export const computeXosanPossibleOptions = (lvmSrs, brickSize) =>
_call('xosan.computeXosanPossibleOptions', { lvmSrs, brickSize })
import InstallXosanPackModal from './install-xosan-pack-modal' // eslint-disable-line import/first
export const downloadAndInstallXosanPack = pool =>
confirm({
title: _('xosanInstallPackTitle', { pool: pool.name_label }),
icon: 'export',
body: <InstallXosanPackModal pool={pool} />,
}).then(pack =>
_call('xosan.downloadAndInstallXosanPack', {
id: pack.id,
version: pack.version,
pool: resolveId(pool),
})
)
export const registerXosan = () =>
_call('cloud.registerResource', { namespace: 'xosan' })::tap(
subscribeResourceCatalog.forceRefresh
@@ -2434,6 +2427,31 @@ export const registerXosan = () =>
export const fixHostNotInXosanNetwork = (xosanSr, host) =>
_call('xosan.fixHostNotInNetwork', { xosanSr, host })
// XOSAN packs -----------------------------------------------------------------
export const getResourceCatalog = () => _call('cloud.getResourceCatalog')
const downloadAndInstallXosanPack = (pack, pool, { version }) =>
_call('xosan.downloadAndInstallXosanPack', {
id: resolveId(pack),
version,
pool: resolveId(pool),
})
import UpdateXosanPacksModal from './update-xosan-packs-modal' // eslint-disable-line import/first
export const updateXosanPacks = pool =>
confirm({
title: _('xosanUpdatePacks'),
icon: 'host-patch-update',
body: <UpdateXosanPacksModal pool={pool} />,
}).then(pack => {
if (pack === undefined) {
return
}
return downloadAndInstallXosanPack(pack, pool, { version: pack.version })
})
// Licenses --------------------------------------------------------------------
export const getLicenses = productId => _call('xoa.getLicenses', { productId })

View File

@@ -1,130 +0,0 @@
import _ from 'intl'
import Component from 'base-component'
import React from 'react'
import { connectStore, compareVersions, isXosanPack } from 'utils'
import { subscribeResourceCatalog, subscribePlugins } from 'xo'
import {
createGetObjectsOfType,
createSelector,
createCollectionWrapper,
} from 'selectors'
import { satisfies as versionSatisfies } from 'semver'
import { every, filter, forEach, map, some } from 'lodash'
const findLatestPack = (packs, hostsVersions) => {
const checkVersion = version =>
every(hostsVersions, hostVersion => versionSatisfies(hostVersion, version))
let latestPack = { version: '0' }
forEach(packs, pack => {
const xsVersionRequirement =
pack.requirements && pack.requirements.xenserver
if (
pack.type === 'iso' &&
compareVersions(pack.version, latestPack.version) > 0 &&
(!xsVersionRequirement || checkVersion(xsVersionRequirement))
) {
latestPack = pack
}
})
if (latestPack.version === '0') {
// No compatible pack was found
return
}
return latestPack
}
@connectStore(
() => ({
hosts: createGetObjectsOfType('host').filter(
createSelector(
(_, { pool }) => pool != null && pool.id,
poolId =>
poolId
? host =>
host.$pool === poolId &&
!some(host.supplementalPacks, isXosanPack)
: false
)
),
}),
{ withRef: true }
)
export default class InstallXosanPackModal extends Component {
componentDidMount () {
this._unsubscribePlugins = subscribePlugins(plugins =>
this.setState({ plugins })
)
this._unsubscribeResourceCatalog = subscribeResourceCatalog(catalog =>
this.setState({ catalog })
)
}
componentWillUnmount () {
this._unsubscribePlugins()
this._unsubscribeResourceCatalog()
}
_getXosanLatestPack = createSelector(
() => this.state.catalog && this.state.catalog.xosan,
createSelector(
() => this.props.hosts,
createCollectionWrapper(hosts => map(hosts, 'version'))
),
findLatestPack
)
_getXosanPacks = createSelector(
() => this.state.catalog && this.state.catalog.xosan,
packs => filter(packs, ({ type }) => type === 'iso')
)
get value () {
return this._getXosanLatestPack()
}
render () {
const { hosts } = this.props
const latestPack = this._getXosanLatestPack()
return (
<div>
{latestPack ? (
<div>
{_('xosanInstallPackOnHosts')}
<ul>
{map(hosts, host => <li key={host.id}>{host.name_label}</li>)}
</ul>
<div className='mt-1'>
{_('xosanInstallPack', {
pack: latestPack.name,
version: latestPack.version,
})}
</div>
</div>
) : (
<div>
{_('xosanNoPackFound')}
<br />
{_('xosanPackRequirements')}
<ul>
{map(this._getXosanPacks(), ({ name, requirements }, key) => (
<li key={key}>
{_.keyValue(
name,
requirements && requirements.xenserver
? requirements.xenserver
: '/'
)}
</li>
))}
</ul>
</div>
)}
</div>
)
}
}

View File

@@ -0,0 +1,83 @@
import _ from 'intl'
import React from 'react'
import Component from 'base-component'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { map } from 'lodash'
import { subscribeResourceCatalog } from 'xo'
import { isLatestXosanPackInstalled, connectStore, findLatestPack } from 'utils'
@connectStore(
{
hosts: createGetObjectsOfType('host').filter((_, { pool }) => host =>
host.$pool === pool.id
),
},
{ withRef: true }
)
export default class UpdateXosanPacksModal extends Component {
componentDidMount () {
this.componentWillUnmount = subscribeResourceCatalog(catalog =>
this.setState({ catalog })
)
}
get value () {
return this._getStatus().pack
}
_getStatus = createSelector(
() => this.state.catalog,
() => this.props.hosts,
(catalog, hosts) => {
if (catalog === undefined) {
return { status: 'error' }
}
if (catalog._namespaces.xosan === undefined) {
return { status: 'unavailable' }
}
if (!catalog._namespaces.xosan.registered) {
return { status: 'unregistered' }
}
const pack = findLatestPack(catalog.xosan, map(hosts, 'version'))
if (pack === undefined) {
return { status: 'noPack' }
}
if (isLatestXosanPackInstalled(pack, hosts)) {
return { status: 'upToDate' }
}
return { status: 'packFound', pack }
}
)
render () {
const { status, pack } = this._getStatus()
switch (status) {
case 'checking':
return <em>{_('xosanPackUpdateChecking')}</em>
case 'error':
return <em>{_('xosanPackUpdateError')}</em>
case 'unavailable':
return <em>{_('xosanPackUpdateUnavailable')}</em>
case 'unregistered':
return <em>{_('xosanPackUpdateUnregistered')}</em>
case 'noPack':
return <em>{_('xosanNoPackFound')}</em>
case 'upToDate':
return <em>{_('xosanPackUpdateUpToDate')}</em>
case 'packFound':
return (
<div>
{_('xosanPackUpdateVersion', {
version: pack.version,
})}
</div>
)
}
}
}

View File

@@ -1,10 +1,11 @@
import _ from 'intl'
import Component from 'base-component'
import Copiable from 'copiable'
import React from 'react'
import TabButton from 'tab-button'
import SelectFiles from 'select-files'
import Upgrade from 'xoa-upgrade'
import { connectStore } from 'utils'
import { compareVersions, connectStore } from 'utils'
import { Toggle } from 'form'
import {
enableHost,
@@ -17,7 +18,7 @@ import {
import { FormattedRelative, FormattedTime } from 'react-intl'
import { Container, Row, Col } from 'grid'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { map, noop } from 'lodash'
import { forEach, map, noop } from 'lodash'
const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1
@@ -31,7 +32,9 @@ const formatPack = ({ name, author, description, version }, key) => (
</tr>
)
export default connectStore(() => {
const getPackId = ({ author, name }) => `${author}\0${name}`
@connectStore(() => {
const getPgpus = createGetObjectsOfType('PGPU')
.pick((_, { host }) => host.$PGPUs)
.sort()
@@ -44,207 +47,233 @@ export default connectStore(() => {
pcis: getPcis,
pgpus: getPgpus,
}
})(({ host, pcis, pgpus }) => (
<Container>
<Row>
<Col className='text-xs-right'>
{host.power_state === 'Running' && (
<TabButton
btnStyle='warning'
handler={forceReboot}
handlerParam={host}
icon='host-force-reboot'
labelId='forceRebootHostLabel'
/>
)}
{host.enabled ? (
<TabButton
btnStyle='warning'
handler={disableHost}
handlerParam={host}
icon='host-disable'
labelId='disableHostLabel'
/>
) : (
<TabButton
btnStyle='success'
handler={enableHost}
handlerParam={host}
icon='host-enable'
labelId='enableHostLabel'
/>
)}
<TabButton
btnStyle='danger'
handler={detachHost}
handlerParam={host}
icon='host-eject'
labelId='detachHost'
/>
{host.power_state !== 'Running' && (
<TabButton
btnStyle='danger'
handler={forgetHost}
handlerParam={host}
icon='host-forget'
labelId='forgetHost'
/>
)}
</Col>
</Row>
<Row>
<Col>
<h3>{_('xenSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('uuid')}</th>
<Copiable tagName='td'>{host.uuid}</Copiable>
</tr>
<tr>
<th>{_('hostAddress')}</th>
<Copiable tagName='td'>{host.address}</Copiable>
</tr>
<tr>
<th>{_('hostStatus')}</th>
<td>
{host.enabled
? _('hostStatusEnabled')
: _('hostStatusDisabled')}
</td>
</tr>
<tr>
<th>{_('hostPowerOnMode')}</th>
<td>
<Toggle
disabled
onChange={noop}
value={Boolean(host.powerOnMode)}
/>
</td>
</tr>
<tr>
<th>{_('hostStartedSince')}</th>
<td>
{_('started', {
ago: <FormattedRelative value={host.startTime * 1000} />,
})}
</td>
</tr>
<tr>
<th>{_('hostStackStartedSince')}</th>
<td>
{_('started', {
ago: <FormattedRelative value={host.agentStartTime * 1000} />,
})}
</td>
</tr>
<tr>
<th>{_('hostXenServerVersion')}</th>
<Copiable tagName='td' data={host.version}>
{host.license_params.sku_marketing_name} {host.version} ({
host.license_params.sku_type
})
</Copiable>
</tr>
<tr>
<th>{_('hostBuildNumber')}</th>
<Copiable tagName='td'>{host.build}</Copiable>
</tr>
<tr>
<th>{_('hostIscsiName')}</th>
<Copiable tagName='td'>{host.iSCSI_name}</Copiable>
</tr>
</tbody>
</table>
<br />
<h3>{_('hardwareHostSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('hostCpusModel')}</th>
<Copiable tagName='td'>{host.CPUs.modelname}</Copiable>
</tr>
<tr>
<th>{_('hostGpus')}</th>
<td>
{map(pgpus, pgpu => pcis[pgpu.pci].device_name).join(', ')}
</td>
</tr>
<tr>
<th>{_('hostCpusNumber')}</th>
<td>
{host.cpus.cores} ({host.cpus.sockets})
</td>
</tr>
<tr>
<th>{_('hostManufacturerinfo')}</th>
<Copiable tagName='td'>
{host.bios_strings['system-manufacturer']} ({
host.bios_strings['system-product-name']
})
</Copiable>
</tr>
<tr>
<th>{_('hostBiosinfo')}</th>
<td>
{host.bios_strings['bios-vendor']} ({
host.bios_strings['bios-version']
})
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('licenseHostSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('hostLicenseType')}</th>
<td>{host.license_params.sku_type}</td>
</tr>
<tr>
<th>{_('hostLicenseSocket')}</th>
<td>{host.license_params.sockets}</td>
</tr>
<tr>
<th>{_('hostLicenseExpiry')}</th>
<td>
<FormattedTime
value={host.license_expiry * 1000}
day='numeric'
month='long'
year='numeric'
/>
<br />
</td>
</tr>
</tbody>
</table>
<h3>{_('supplementalPacks')}</h3>
<table className='table'>
<tbody>
{map(host.supplementalPacks, formatPack)}
{ALLOW_INSTALL_SUPP_PACK && (
<tr>
<th>{_('supplementalPackNew')}</th>
<td>
<SelectFiles
type='file'
onChange={file => installSupplementalPack(host, file)}
/>
</td>
</tr>
})
export default class extends Component {
_getPacks = createSelector(
() => this.props.host.supplementalPacks,
packs => {
const uniqPacks = {}
let packId, previousPack
forEach(packs, pack => {
packId = getPackId(pack)
if (
(previousPack = uniqPacks[packId]) === undefined ||
compareVersions(pack.version, previousPack.version) > 0
) {
uniqPacks[packId] = pack
}
})
return uniqPacks
}
)
render () {
const { host, pcis, pgpus } = this.props
return (
<Container>
<Row>
<Col className='text-xs-right'>
{host.power_state === 'Running' && (
<TabButton
btnStyle='warning'
handler={forceReboot}
handlerParam={host}
icon='host-force-reboot'
labelId='forceRebootHostLabel'
/>
)}
</tbody>
</table>
{!ALLOW_INSTALL_SUPP_PACK && [
<h3>{_('supplementalPackNew')}</h3>,
<Container>
<Upgrade place='supplementalPacks' available={2} />
</Container>,
]}
</Col>
</Row>
</Container>
))
{host.enabled ? (
<TabButton
btnStyle='warning'
handler={disableHost}
handlerParam={host}
icon='host-disable'
labelId='disableHostLabel'
/>
) : (
<TabButton
btnStyle='success'
handler={enableHost}
handlerParam={host}
icon='host-enable'
labelId='enableHostLabel'
/>
)}
<TabButton
btnStyle='danger'
handler={detachHost}
handlerParam={host}
icon='host-eject'
labelId='detachHost'
/>
{host.power_state !== 'Running' && (
<TabButton
btnStyle='danger'
handler={forgetHost}
handlerParam={host}
icon='host-forget'
labelId='forgetHost'
/>
)}
</Col>
</Row>
<Row>
<Col>
<h3>{_('xenSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('uuid')}</th>
<Copiable tagName='td'>{host.uuid}</Copiable>
</tr>
<tr>
<th>{_('hostAddress')}</th>
<Copiable tagName='td'>{host.address}</Copiable>
</tr>
<tr>
<th>{_('hostStatus')}</th>
<td>
{host.enabled
? _('hostStatusEnabled')
: _('hostStatusDisabled')}
</td>
</tr>
<tr>
<th>{_('hostPowerOnMode')}</th>
<td>
<Toggle
disabled
onChange={noop}
value={Boolean(host.powerOnMode)}
/>
</td>
</tr>
<tr>
<th>{_('hostStartedSince')}</th>
<td>
{_('started', {
ago: <FormattedRelative value={host.startTime * 1000} />,
})}
</td>
</tr>
<tr>
<th>{_('hostStackStartedSince')}</th>
<td>
{_('started', {
ago: (
<FormattedRelative value={host.agentStartTime * 1000} />
),
})}
</td>
</tr>
<tr>
<th>{_('hostXenServerVersion')}</th>
<Copiable tagName='td' data={host.version}>
{host.license_params.sku_marketing_name} {host.version} ({
host.license_params.sku_type
})
</Copiable>
</tr>
<tr>
<th>{_('hostBuildNumber')}</th>
<Copiable tagName='td'>{host.build}</Copiable>
</tr>
<tr>
<th>{_('hostIscsiName')}</th>
<Copiable tagName='td'>{host.iSCSI_name}</Copiable>
</tr>
</tbody>
</table>
<br />
<h3>{_('hardwareHostSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('hostCpusModel')}</th>
<Copiable tagName='td'>{host.CPUs.modelname}</Copiable>
</tr>
<tr>
<th>{_('hostGpus')}</th>
<td>
{map(pgpus, pgpu => pcis[pgpu.pci].device_name).join(', ')}
</td>
</tr>
<tr>
<th>{_('hostCpusNumber')}</th>
<td>
{host.cpus.cores} ({host.cpus.sockets})
</td>
</tr>
<tr>
<th>{_('hostManufacturerinfo')}</th>
<Copiable tagName='td'>
{host.bios_strings['system-manufacturer']} ({
host.bios_strings['system-product-name']
})
</Copiable>
</tr>
<tr>
<th>{_('hostBiosinfo')}</th>
<td>
{host.bios_strings['bios-vendor']} ({
host.bios_strings['bios-version']
})
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('licenseHostSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('hostLicenseType')}</th>
<td>{host.license_params.sku_type}</td>
</tr>
<tr>
<th>{_('hostLicenseSocket')}</th>
<td>{host.license_params.sockets}</td>
</tr>
<tr>
<th>{_('hostLicenseExpiry')}</th>
<td>
<FormattedTime
value={host.license_expiry * 1000}
day='numeric'
month='long'
year='numeric'
/>
<br />
</td>
</tr>
</tbody>
</table>
<h3>{_('supplementalPacks')}</h3>
<table className='table'>
<tbody>
{map(this._getPacks(), formatPack)}
{ALLOW_INSTALL_SUPP_PACK && (
<tr>
<th>{_('supplementalPackNew')}</th>
<td>
<SelectFiles
type='file'
onChange={file => installSupplementalPack(host, file)}
/>
</td>
</tr>
)}
</tbody>
</table>
{!ALLOW_INSTALL_SUPP_PACK && [
<h3>{_('supplementalPackNew')}</h3>,
<Container>
<Upgrade place='supplementalPacks' available={2} />
</Container>,
]}
</Col>
</Row>
</Container>
)
}
}

View File

@@ -1,5 +1,6 @@
import _ from 'intl'
import Component from 'base-component'
import copy from 'copy-to-clipboard'
import React from 'react'
import Icon from 'icon'
import pick from 'lodash/pick'
@@ -284,6 +285,11 @@ const COLUMNS = [
]
const INDIVIDUAL_ACTIONS = [
{
handler: pif => copy(pif.uuid),
icon: 'clipboard',
label: pif => _('copyUuid', { uuid: pif.uuid }),
},
{
handler: deletePif,
icon: 'delete',

View File

@@ -3,6 +3,7 @@ import ActionRowButton from 'action-row-button'
import BaseComponent from 'base-component'
import Button from 'button'
import ButtonGroup from 'button-group'
import copy from 'copy-to-clipboard'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
@@ -280,6 +281,11 @@ class NetworkActions extends Component {
return (
<ButtonGroup>
<ActionRowButton
handler={() => copy(network.uuid)}
icon='clipboard'
tooltip={_('copyUuid', { uuid: network.uuid })}
/>
<ActionRowButton
disabled={disableNetworkDelete}
handler={deleteNetwork}

View File

@@ -170,12 +170,22 @@ const INDIVIDUAL_ACTIONS = [
})
@injectIntl
export default class Servers extends Component {
state = {
allowUnauthorized: false,
}
_addServer = async () => {
const { label, host, password, username } = this.state
const { label, host, password, username, allowUnauthorized } = this.state
await addServer(host, username, password, label)
await addServer(host, username, password, label, allowUnauthorized)
this.setState({ label: '', host: '', password: '', username: '' })
this.setState({
allowUnauthorized: false,
host: '',
label: '',
password: '',
username: '',
})
}
render () {
@@ -228,6 +238,14 @@ export default class Servers extends Component {
value={state.password}
/>
</div>{' '}
<div className='form-group'>
<Tooltip content={_('serverAllowUnauthorizedCertificates')}>
<Toggle
onChange={this.linkState('allowUnauthorized')}
value={state.allowUnauthorized}
/>
</Tooltip>
</div>{' '}
<ActionButton
btnStyle='primary'
form='form-add-server'

View File

@@ -10,7 +10,6 @@ import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
import propTypes from 'prop-types-decorator'
import React from 'react'
import Upgrade from 'xoa-upgrade'
import { Container, Col, Row } from 'grid'
import { importVms, isSrWritable } from 'xo'
import { SizeInput } from 'form'
@@ -235,9 +234,10 @@ const parseFile = async (file, type, func) => {
}
}
const getRedirectionUrl = vms => vms.length === 1
? `/vms/${vms[0]}`
: `/home?s=${encodeURIComponent(`id:|(${vms.join(' ')})`)}&t=VM`
const getRedirectionUrl = vms =>
vms.length === 1
? `/vms/${vms[0]}`
: `/home?s=${encodeURIComponent(`id:|(${vms.join(' ')})`)}&t=VM`
export default class Import extends Component {
constructor (props) {
@@ -318,107 +318,101 @@ export default class Import extends Component {
return (
<Page header={HEADER} title='newImport' formatTitle>
{process.env.XOA_PLAN > 1 ? (
<Container>
<form id='import-form'>
<FormGrid.Row>
<FormGrid.LabelCol>{_('vmImportToPool')}</FormGrid.LabelCol>
<FormGrid.InputCol>
<SelectPool
value={pool}
onChange={this._handleSelectedPool}
required
/>
</FormGrid.InputCol>
</FormGrid.Row>
<FormGrid.Row>
<FormGrid.LabelCol>{_('vmImportToSr')}</FormGrid.LabelCol>
<FormGrid.InputCol>
<SelectSr
disabled={!pool}
onChange={this._handleSelectedSr}
predicate={srPredicate}
required
value={sr}
/>
</FormGrid.InputCol>
</FormGrid.Row>
{sr && (
<div>
<Dropzone
onDrop={this._handleDrop}
message={_('importVmsList')}
/>
<hr />
<h5>{_('vmsToImport')}</h5>
{vms.length > 0 ? (
<div>
{map(vms, ({ data, error, file, type }, vmIndex) => (
<div key={file.preview} className={styles.vmContainer}>
<strong>{file.name}</strong>
<span className='pull-right'>
<strong>{`(${formatSize(file.size)})`}</strong>
</span>
{!error ? (
data && (
<div>
<hr />
<div className='alert alert-info' role='alert'>
<strong>
{_('vmImportFileType', { type })}
</strong>{' '}
{_('vmImportConfigAlert')}
</div>
<VmData
{...data}
ref={`vm-data-${vmIndex}`}
pool={pool}
/>
</div>
)
) : (
<Container>
<form id='import-form'>
<FormGrid.Row>
<FormGrid.LabelCol>{_('vmImportToPool')}</FormGrid.LabelCol>
<FormGrid.InputCol>
<SelectPool
value={pool}
onChange={this._handleSelectedPool}
required
/>
</FormGrid.InputCol>
</FormGrid.Row>
<FormGrid.Row>
<FormGrid.LabelCol>{_('vmImportToSr')}</FormGrid.LabelCol>
<FormGrid.InputCol>
<SelectSr
disabled={!pool}
onChange={this._handleSelectedSr}
predicate={srPredicate}
required
value={sr}
/>
</FormGrid.InputCol>
</FormGrid.Row>
{sr && (
<div>
<Dropzone
onDrop={this._handleDrop}
message={_('importVmsList')}
/>
<hr />
<h5>{_('vmsToImport')}</h5>
{vms.length > 0 ? (
<div>
{map(vms, ({ data, error, file, type }, vmIndex) => (
<div key={file.preview} className={styles.vmContainer}>
<strong>{file.name}</strong>
<span className='pull-right'>
<strong>{`(${formatSize(file.size)})`}</strong>
</span>
{!error ? (
data && (
<div>
<hr />
<div className='alert alert-danger' role='alert'>
<strong>{_('vmImportError')}</strong>{' '}
{(error && error.message) ||
_('noVmImportErrorDescription')}
<div className='alert alert-info' role='alert'>
<strong>
{_('vmImportFileType', { type })}
</strong>{' '}
{_('vmImportConfigAlert')}
</div>
<VmData
{...data}
ref={`vm-data-${vmIndex}`}
pool={pool}
/>
</div>
)}
</div>
))}
</div>
) : (
<p>{_('noSelectedVms')}</p>
)}
<hr />
<div className='form-group pull-right'>
<ActionButton
btnStyle='primary'
disabled={!vms.length}
className='mr-1'
form='import-form'
handler={this._import}
icon='import'
redirectOnSuccess={getRedirectionUrl}
type='submit'
>
{_('newImport')}
</ActionButton>
<Button onClick={this._handleCleanSelectedVms}>
{_('importVmsCleanList')}
</Button>
)
) : (
<div>
<hr />
<div className='alert alert-danger' role='alert'>
<strong>{_('vmImportError')}</strong>{' '}
{(error && error.message) ||
_('noVmImportErrorDescription')}
</div>
</div>
)}
</div>
))}
</div>
) : (
<p>{_('noSelectedVms')}</p>
)}
<hr />
<div className='form-group pull-right'>
<ActionButton
btnStyle='primary'
disabled={!vms.length}
className='mr-1'
form='import-form'
handler={this._import}
icon='import'
redirectOnSuccess={getRedirectionUrl}
type='submit'
>
{_('newImport')}
</ActionButton>
<Button onClick={this._handleCleanSelectedVms}>
{_('importVmsCleanList')}
</Button>
</div>
)}
</form>
</Container>
) : (
<Container>
<Upgrade place='vmImport' available={2} />
</Container>
)}
</div>
)}
</form>
</Container>
</Page>
)
}

View File

@@ -29,6 +29,7 @@ import {
deleteVgpu,
deleteVm,
editVm,
getVmsHaValues,
isVmRunning,
recoveryStartVm,
restartVm,
@@ -278,13 +279,8 @@ class CoresPerSocket extends Component {
}
}
export default connectStore(() => {
@connectStore(() => {
const getVgpus = createGetObjectsOfType('vgpu').pick((_, { vm }) => vm.$VGPUs)
const getVgpuTypes = createGetObjectsOfType('vgpuType').pick(
createSelector(getVgpus, vgpus => map(vgpus, 'vgpuType'))
)
const getGpuGroup = createGetObjectsOfType('gpuGroup').pick(
createSelector(getVgpus, vgpus => map(vgpus, 'gpuGroup'))
)
@@ -293,367 +289,394 @@ export default connectStore(() => {
gpuGroup: getGpuGroup,
isAdmin,
vgpus: getVgpus,
vgpuTypes: getVgpuTypes,
}
})(({ container, gpuGroup, isAdmin, vgpus, vgpuTypes, vm }) => (
<Container>
<Row>
<Col className='text-xs-right'>
{vm.power_state === 'Running' && (
<span>
<TabButton
btnStyle='primary'
handler={suspendVm}
handlerParam={vm}
icon='vm-suspend'
labelId='suspendVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceReboot}
handlerParam={vm}
icon='vm-force-reboot'
labelId='forceRebootVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceShutdown}
handlerParam={vm}
icon='vm-force-shutdown'
labelId='forceShutdownVmLabel'
/>
</span>
)}
{vm.power_state === 'Halted' && (
<span>
<TabButton
btnStyle='primary'
handler={recoveryStartVm}
handlerParam={vm}
icon='vm-recovery-mode'
labelId='recoveryModeLabel'
/>
<TabButton
btnStyle='primary'
handler={fullCopy}
handlerParam={vm}
icon='vm-clone'
labelId='cloneVmLabel'
/>
})
export default class TabAdvanced extends Component {
componentDidMount () {
getVmsHaValues().then(vmsHaValues => this.setState({ vmsHaValues }))
}
render () {
const { container, isAdmin, vgpus, vm } = this.props
return (
<Container>
<Row>
<Col className='text-xs-right'>
{vm.power_state === 'Running' && (
<span>
<TabButton
btnStyle='primary'
handler={suspendVm}
handlerParam={vm}
icon='vm-suspend'
labelId='suspendVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceReboot}
handlerParam={vm}
icon='vm-force-reboot'
labelId='forceRebootVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceShutdown}
handlerParam={vm}
icon='vm-force-shutdown'
labelId='forceShutdownVmLabel'
/>
</span>
)}
{vm.power_state === 'Halted' && (
<span>
<TabButton
btnStyle='primary'
handler={recoveryStartVm}
handlerParam={vm}
icon='vm-recovery-mode'
labelId='recoveryModeLabel'
/>
<TabButton
btnStyle='primary'
handler={fullCopy}
handlerParam={vm}
icon='vm-clone'
labelId='cloneVmLabel'
/>
<TabButton
btnStyle='danger'
handler={convertVmToTemplate}
handlerParam={vm}
icon='vm-create-template'
labelId='vmConvertButton'
redirectOnSuccess='/'
/>
</span>
)}
{vm.power_state === 'Suspended' && (
<span>
<TabButton
btnStyle='primary'
handler={resumeVm}
handlerParam={vm}
icon='vm-start'
labelId='resumeVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceShutdown}
handlerParam={vm}
icon='vm-force-shutdown'
labelId='forceShutdownVmLabel'
/>
</span>
)}
<TabButton
btnStyle='danger'
handler={convertVmToTemplate}
handler={deleteVm}
handlerParam={vm}
icon='vm-create-template'
labelId='vmConvertButton'
redirectOnSuccess='/'
icon='vm-delete'
labelId='vmRemoveButton'
/>
</span>
)}
{vm.power_state === 'Suspended' && (
<span>
<TabButton
btnStyle='primary'
handler={resumeVm}
handlerParam={vm}
icon='vm-start'
labelId='resumeVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceShutdown}
handlerParam={vm}
icon='vm-force-shutdown'
labelId='forceShutdownVmLabel'
/>
</span>
)}
<TabButton
btnStyle='danger'
handler={deleteVm}
handlerParam={vm}
icon='vm-delete'
labelId='vmRemoveButton'
/>
</Col>
</Row>
<Row>
<Col>
<h3>{_('xenSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('uuid')}</th>
<Copiable tagName='td'>{vm.uuid}</Copiable>
</tr>
<tr>
<th>{_('virtualizationMode')}</th>
<td>
{vm.virtualizationMode === 'pv'
? _('paraVirtualizedMode')
: _('hardwareVirtualizedMode')}
</td>
</tr>
{vm.virtualizationMode === 'pv' && (
<tr>
<th>{_('pvArgsLabel')}</th>
<td>
<Text
value={vm.PV_args}
onChange={value => editVm(vm, { PV_args: value })}
/>
</td>
</tr>
)}
<tr>
<th>{_('cpuWeightLabel')}</th>
<td>
<Number
value={vm.cpuWeight == null ? null : vm.cpuWeight}
onChange={value => editVm(vm, { cpuWeight: value })}
nullable
>
{vm.cpuWeight == null
? _('defaultCpuWeight', { value: XEN_DEFAULT_CPU_WEIGHT })
: vm.cpuWeight}
</Number>
</td>
</tr>
<tr>
<th>{_('cpuCapLabel')}</th>
<td>
<Number
value={vm.cpuCap == null ? null : vm.cpuCap}
onChange={value => editVm(vm, { cpuCap: value })}
nullable
>
{vm.cpuCap == null
? _('defaultCpuCap', { value: XEN_DEFAULT_CPU_CAP })
: vm.cpuCap}
</Number>
</td>
</tr>
<tr>
<th>{_('autoPowerOn')}</th>
<td>
<Toggle
value={Boolean(vm.auto_poweron)}
onChange={value => editVm(vm, { auto_poweron: value })}
/>
</td>
</tr>
<tr>
<th>{_('windowsUpdateTools')}</th>
<td>
<Toggle
value={vm.hasVendorDevice}
onChange={value => editVm(vm, { hasVendorDevice: value })}
/>
</td>
</tr>
<tr>
<th>{_('ha')}</th>
<td>
<Toggle
value={vm.high_availability}
onChange={value => editVm(vm, { high_availability: value })}
/>
</td>
</tr>
<tr>
<th>{_('vmAffinityHost')}</th>
<td>
<AffinityHost vm={vm} />
</td>
</tr>
{vm.virtualizationMode === 'hvm' && (
<tr>
<th>{_('vmVgpus')}</th>
<td>
<Vgpus vgpus={vgpus} vm={vm} />
</td>
</tr>
)}
{vm.virtualizationMode === 'hvm' && (
<tr>
<th>{_('vmVga')}</th>
<td>
<Toggle
value={vm.vga === 'std'}
onChange={value =>
editVm(vm, { vga: value ? 'std' : 'cirrus' })
}
/>
</td>
</tr>
)}
{vm.vga === 'std' && (
<tr>
<th>{_('vmVideoram')}</th>
<td>
<select
className='form-control'
onChange={event =>
editVm(vm, { videoram: +getEventValue(event) })
}
value={vm.videoram}
>
{map(XEN_VIDEORAM_VALUES, val => (
<option key={val} value={val}>
{formatSize(val * 1048576)}
</option>
))}
</select>
</td>
</tr>
)}
</tbody>
</table>
<br />
<h3>{_('vmLimitsLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('vmCpuLimitsLabel')}</th>
<td>
<Number
value={vm.CPUs.number}
onChange={cpus => editVm(vm, { cpus })}
/>
/
{vm.power_state === 'Running' ? (
vm.CPUs.max
) : (
<Number
value={vm.CPUs.max}
onChange={cpusStaticMax => editVm(vm, { cpusStaticMax })}
/>
</Col>
</Row>
<Row>
<Col>
<h3>{_('xenSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('uuid')}</th>
<Copiable tagName='td'>{vm.uuid}</Copiable>
</tr>
<tr>
<th>{_('virtualizationMode')}</th>
<td>
{vm.virtualizationMode === 'pv'
? _('paraVirtualizedMode')
: _('hardwareVirtualizedMode')}
</td>
</tr>
{vm.virtualizationMode === 'pv' && (
<tr>
<th>{_('pvArgsLabel')}</th>
<td>
<Text
value={vm.PV_args}
onChange={value => editVm(vm, { PV_args: value })}
/>
</td>
</tr>
)}
</td>
</tr>
<tr>
<th>{_('vmCpuTopology')}</th>
<td>
<CoresPerSocket container={container} vm={vm} />
</td>
</tr>
<tr>
<th>{_('vmMemoryLimitsLabel')}</th>
<td>
<p>
Static: {formatSize(vm.memory.static[0])}/<Size
value={defined(vm.memory.static[1], null)}
onChange={memoryStaticMax =>
editVm(vm, { memoryStaticMax })
}
/>
</p>
<p>
Dynamic:{' '}
<Size
value={defined(vm.memory.dynamic[0], null)}
onChange={memoryMin => editVm(vm, { memoryMin })}
/>/<Size
value={defined(vm.memory.dynamic[1], null)}
onChange={memoryMax => editVm(vm, { memoryMax })}
/>
</p>
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('guestOsLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('xenToolsStatus')}</th>
<td>
{vm.xenTools && `${vm.xenTools.major}.${vm.xenTools.minor}`}
</td>
</tr>
<tr>
<th>{_('osName')}</th>
<td>
{isEmpty(vm.os_version) ? (
_('unknownOsName')
) : (
<span>
<Icon
className='text-info'
icon={osFamily(vm.os_version.distro)}
/>&nbsp;{vm.os_version.name}
</span>
)}
</td>
</tr>
<tr>
<th>{_('osKernel')}</th>
<td>
{(vm.os_version && vm.os_version.uname) || _('unknownOsKernel')}
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('miscLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('originalTemplate')}</th>
<td>
{vm.other.base_template_name
? vm.other.base_template_name
: _('unknownOriginalTemplate')}
</td>
</tr>
<tr>
<th>{_('resourceSet')}</th>
<td>
{isAdmin ? (
<div className='input-group'>
<SelectResourceSet
onChange={resourceSet =>
editVm(vm, {
resourceSet:
resourceSet != null ? resourceSet.id : resourceSet,
})
}
value={vm.resourceSet}
<tr>
<th>{_('cpuWeightLabel')}</th>
<td>
<Number
value={vm.cpuWeight == null ? null : vm.cpuWeight}
onChange={value => editVm(vm, { cpuWeight: value })}
nullable
>
{vm.cpuWeight == null
? _('defaultCpuWeight', {
value: XEN_DEFAULT_CPU_WEIGHT,
})
: vm.cpuWeight}
</Number>
</td>
</tr>
<tr>
<th>{_('cpuCapLabel')}</th>
<td>
<Number
value={vm.cpuCap == null ? null : vm.cpuCap}
onChange={value => editVm(vm, { cpuCap: value })}
nullable
>
{vm.cpuCap == null
? _('defaultCpuCap', { value: XEN_DEFAULT_CPU_CAP })
: vm.cpuCap}
</Number>
</td>
</tr>
<tr>
<th>{_('autoPowerOn')}</th>
<td>
<Toggle
value={Boolean(vm.auto_poweron)}
onChange={value => editVm(vm, { auto_poweron: value })}
/>
{vm.resourceSet !== undefined && (
<span className='input-group-btn'>
</td>
</tr>
<tr>
<th>{_('windowsUpdateTools')}</th>
<td>
<Toggle
value={vm.hasVendorDevice}
onChange={value => editVm(vm, { hasVendorDevice: value })}
/>
</td>
</tr>
<tr>
<th>{_('ha')}</th>
<td>
<select
className='form-control'
onChange={event =>
editVm(vm, { high_availability: getEventValue(event) })
}
value={vm.high_availability}
>
{map(this.state.vmsHaValues, vmsHaValue => (
<option key={vmsHaValue} value={vmsHaValue}>
{vmsHaValue === '' ? _('vmHaDisabled') : vmsHaValue}
</option>
))}
</select>
</td>
</tr>
<tr>
<th>{_('vmAffinityHost')}</th>
<td>
<AffinityHost vm={vm} />
</td>
</tr>
{vm.virtualizationMode === 'hvm' && (
<tr>
<th>{_('vmVgpus')}</th>
<td>
<Vgpus vgpus={vgpus} vm={vm} />
</td>
</tr>
)}
{vm.virtualizationMode === 'hvm' && (
<tr>
<th>{_('vmVga')}</th>
<td>
<Toggle
value={vm.vga === 'std'}
onChange={value =>
editVm(vm, { vga: value ? 'std' : 'cirrus' })
}
/>
</td>
</tr>
)}
{vm.vga === 'std' && (
<tr>
<th>{_('vmVideoram')}</th>
<td>
<select
className='form-control'
onChange={event =>
editVm(vm, { videoram: +getEventValue(event) })
}
value={vm.videoram}
>
{map(XEN_VIDEORAM_VALUES, val => (
<option key={val} value={val}>
{formatSize(val * 1048576)}
</option>
))}
</select>
</td>
</tr>
)}
</tbody>
</table>
<br />
<h3>{_('vmLimitsLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('vmCpuLimitsLabel')}</th>
<td>
<Number
value={vm.CPUs.number}
onChange={cpus => editVm(vm, { cpus })}
/>
/
{vm.power_state === 'Running' ? (
vm.CPUs.max
) : (
<Number
value={vm.CPUs.max}
onChange={cpusStaticMax =>
editVm(vm, { cpusStaticMax })
}
/>
)}
</td>
</tr>
<tr>
<th>{_('vmCpuTopology')}</th>
<td>
<CoresPerSocket container={container} vm={vm} />
</td>
</tr>
<tr>
<th>{_('vmMemoryLimitsLabel')}</th>
<td>
<p>
Static: {formatSize(vm.memory.static[0])}/<Size
value={defined(vm.memory.static[1], null)}
onChange={memoryStaticMax =>
editVm(vm, { memoryStaticMax })
}
/>
</p>
<p>
Dynamic:{' '}
<Size
value={defined(vm.memory.dynamic[0], null)}
onChange={memoryMin => editVm(vm, { memoryMin })}
/>/<Size
value={defined(vm.memory.dynamic[1], null)}
onChange={memoryMax => editVm(vm, { memoryMax })}
/>
</p>
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('guestOsLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('xenToolsStatus')}</th>
<td>
{vm.xenTools
? `${vm.xenTools.major}.${vm.xenTools.minor}`
: _('xenToolsNotInstalled')}
</td>
</tr>
<tr>
<th>{_('osName')}</th>
<td>
{isEmpty(vm.os_version) ? (
_('unknownOsName')
) : (
<span>
<Icon
className='text-info'
icon={osFamily(vm.os_version.distro)}
/>&nbsp;{vm.os_version.name}
</span>
)}
</td>
</tr>
<tr>
<th>{_('osKernel')}</th>
<td>
{(vm.os_version && vm.os_version.uname) ||
_('unknownOsKernel')}
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('miscLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('originalTemplate')}</th>
<td>
{vm.other.base_template_name
? vm.other.base_template_name
: _('unknownOriginalTemplate')}
</td>
</tr>
<tr>
<th>{_('resourceSet')}</th>
<td>
{isAdmin ? (
<div className='input-group'>
<SelectResourceSet
onChange={resourceSet =>
editVm(vm, {
resourceSet:
resourceSet != null
? resourceSet.id
: resourceSet,
})
}
value={vm.resourceSet}
/>
{vm.resourceSet !== undefined && (
<span className='input-group-btn'>
<ActionButton
btnStyle='primary'
handler={shareVmProxy}
handlerParam={vm}
icon='vm-share'
style={SHARE_BUTTON_STYLE}
tooltip={_('vmShareButton')}
/>
</span>
)}
</div>
) : vm.resourceSet !== undefined ? (
<span>
<ResourceSetItem id={vm.resourceSet} />{' '}
<ActionButton
btnStyle='primary'
handler={shareVmProxy}
handlerParam={vm}
icon='vm-share'
style={SHARE_BUTTON_STYLE}
size='small'
tooltip={_('vmShareButton')}
/>
</span>
) : (
_('resourceSetNone')
)}
</div>
) : vm.resourceSet !== undefined ? (
<span>
<ResourceSetItem id={vm.resourceSet} />{' '}
<ActionButton
btnStyle='primary'
handler={shareVmProxy}
handlerParam={vm}
icon='vm-share'
size='small'
tooltip={_('vmShareButton')}
/>
</span>
) : (
_('resourceSetNone')
)}
</td>
</tr>
</tbody>
</table>
</Col>
</Row>
</Container>
))
</td>
</tr>
</tbody>
</table>
</Col>
</Row>
</Container>
)
}
}

View File

@@ -1,6 +1,7 @@
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import copy from 'copy-to-clipboard'
import HTML5Backend from 'react-dnd-html5-backend'
import Icon from 'icon'
import IsoDevice from 'iso-device'
@@ -656,6 +657,11 @@ export default class TabDisks extends Component {
icon: 'vdi-migrate',
label: _('vdiMigrate'),
},
{
handler: vdi => copy(vdi.uuid),
icon: 'clipboard',
label: vdi => _('copyUuid', { uuid: vdi.uuid }),
},
]
render () {

View File

@@ -2,6 +2,7 @@ import _, { messages } from 'intl'
import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import BaseComponent from 'base-component'
import copy from 'copy-to-clipboard'
import Icon from 'icon'
import propTypes from 'prop-types-decorator'
import React from 'react'
@@ -351,6 +352,11 @@ const GROUPED_ACTIONS = [
},
]
const INDIVIDUAL_ACTIONS = [
{
handler: vif => copy(vif.uuid),
icon: 'clipboard',
label: vif => _('copyUuid', { uuid: vif.uuid }),
},
{
disabled: vif => vif.attached,
handler: deleteVif,

View File

@@ -1,4 +1,5 @@
import _ from 'intl'
import copy from 'copy-to-clipboard'
import Icon from 'icon'
import React, { Component } from 'react'
import SortedTable from 'sorted-table'
@@ -92,6 +93,11 @@ const INDIVIDUAL_ACTIONS = [
label: _('revertSnapshot'),
level: 'warning',
},
{
handler: snapshot => copy(snapshot.uuid),
icon: 'clipboard',
label: snapshot => _('copyUuid', { uuid: snapshot.uuid }),
},
{
handler: deleteSnapshot,
icon: 'delete',

View File

@@ -10,24 +10,13 @@ import Tooltip from 'tooltip'
import { Container, Col, Row } from 'grid'
import { get } from 'xo-defined'
import { ignoreErrors } from 'promise-toolbox'
import {
every,
filter,
find,
flatten,
forEach,
isEmpty,
map,
mapValues,
some,
} from 'lodash'
import { every, filter, find, flatten, forEach, isEmpty, map } from 'lodash'
import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
import {
addSubscriptions,
connectStore,
cowSet,
formatSize,
isXosanPack,
ShortDate,
} from 'utils'
import {
@@ -37,6 +26,7 @@ import {
subscribePlugins,
subscribeResourceCatalog,
subscribeVolumeInfo,
updateXosanPacks,
} from 'xo'
import NewXosan from './new-xosan'
@@ -208,6 +198,12 @@ const XOSAN_COLUMNS = [
]
const XOSAN_INDIVIDUAL_ACTIONS = [
{
handler: (xosan, { pools }) => updateXosanPacks(pools[xosan.$pool]),
icon: 'host-patch-update',
label: _('xosanUpdatePacks'),
level: 'primary',
},
{
handler: deleteSr,
icon: 'delete',
@@ -221,14 +217,6 @@ const XOSAN_INDIVIDUAL_ACTIONS = [
const getHostsByPool = getHosts.groupBy('$pool')
const getPools = createGetObjectsOfType('pool')
const noPacksByPool = createSelector(getHostsByPool, hostsByPool =>
mapValues(
hostsByPool,
(poolHosts, poolId) =>
!every(poolHosts, host => some(host.supplementalPacks, isXosanPack))
)
)
const getPbdsBySr = createGetObjectsOfType('PBD').groupBy('SR')
const getXosanSrs = createSelector(
createGetObjectsOfType('SR').filter([
@@ -291,7 +279,6 @@ const XOSAN_INDIVIDUAL_ACTIONS = [
isAdmin,
isMasterOfflineByPool: getIsMasterOfflineByPool,
hostsNeedRestartByPool: getHostsNeedRestartByPool,
noPacksByPool,
poolPredicate: getPoolPredicate,
pools: getPools,
xoaRegistration: state => state.xoaRegisterState,
@@ -419,8 +406,8 @@ export default class Xosan extends Component {
const {
hostsNeedRestartByPool,
isAdmin,
noPacksByPool,
poolPredicate,
pools,
xoaRegistration,
xosanSrs,
} = this.props
@@ -456,7 +443,6 @@ export default class Xosan extends Component {
(this._isXosanRegistered() ? (
<NewXosan
hostsNeedRestartByPool={hostsNeedRestartByPool}
noPacksByPool={noPacksByPool}
poolPredicate={poolPredicate}
onSrCreationFinished={this._updateLicenses}
onSrCreationStarted={this._onSrCreationStarted}
@@ -498,6 +484,7 @@ export default class Xosan extends Component {
isAdmin,
licensesByXosan: this._getLicensesByXosan(),
licenseError,
pools,
status: this.state.status,
}}
/>

View File

@@ -29,15 +29,18 @@ import {
} from 'selectors'
import {
addSubscriptions,
isLatestXosanPackInstalled,
compareVersions,
connectStore,
findLatestPack,
formatSize,
mapPlus,
} from 'utils'
import {
computeXosanPossibleOptions,
createXosanSR,
downloadAndInstallXosanPack,
updateXosanPacks,
getResourceCatalog,
restartHostsAgents,
subscribeResourceCatalog,
} from 'xo'
@@ -76,14 +79,47 @@ export default class NewXosan extends Component {
suggestion: 0,
}
_checkPacks = pool =>
getResourceCatalog().then(
catalog => {
if (catalog === undefined || catalog.xosan === undefined) {
this.setState({
checkPackError: true,
})
return
}
const hosts = filter(this.props.hosts, { $pool: pool.id })
const pack = findLatestPack(catalog.xosan, map(hosts, 'version'))
if (!isLatestXosanPackInstalled(pack, hosts)) {
this.setState({
needsUpdate: true,
})
}
},
() => {
this.setState({
checkPackError: true,
})
}
)
_updateXosanPacks = pool =>
updateXosanPacks(pool).then(() => this._checkPacks(pool))
_selectPool = pool => {
this.setState({
selectedSrs: {},
brickSize: DEFAULT_BRICKSIZE,
checkPackError: false,
memorySize: DEFAULT_MEMORY,
needsUpdate: false,
pif: undefined,
pool,
selectedSrs: {},
})
return this._checkPacks(pool)
}
componentDidUpdate () {
@@ -243,10 +279,12 @@ export default class NewXosan extends Component {
const {
brickSize,
checkPackError,
customBrickSize,
customIpRange,
ipRange,
memorySize,
needsUpdate,
pif,
pool,
selectedSrs,
@@ -256,12 +294,7 @@ export default class NewXosan extends Component {
vlan,
} = this.state
const {
hostsNeedRestartByPool,
noPacksByPool,
poolPredicate,
notRegistered,
} = this.props
const { hostsNeedRestartByPool, poolPredicate, notRegistered } = this.props
if (notRegistered) {
return (
@@ -296,9 +329,7 @@ export default class NewXosan extends Component {
<Col size={4}>
<SelectPif
disabled={
pool == null ||
noPacksByPool[pool.id] ||
!isEmpty(hostsNeedRestart)
pool == null || needsUpdate || !isEmpty(hostsNeedRestart)
}
onChange={this.linkState('pif')}
predicate={this._getPifPredicate()}
@@ -307,261 +338,273 @@ export default class NewXosan extends Component {
</Col>
</Row>
{pool != null &&
noPacksByPool[pool.id] && (
(checkPackError ? (
<em>{_('xosanPackUpdateError')}</em>
) : needsUpdate ? (
<Row>
<Icon icon='error' /> {_('xosanNeedPack')}
<br />
<ActionButton
btnStyle='success'
handler={downloadAndInstallXosanPack}
handlerParam={pool}
icon='export'
>
{_('xosanInstallIt')}
</ActionButton>
<Col>
<Icon icon='error' /> {_('xosanNeedPack')}
<br />
<ActionButton
btnStyle='success'
handler={this._updateXosanPacks}
handlerParam={pool}
icon='export'
>
{_('xosanInstallIt')}
</ActionButton>
</Col>
</Row>
)}
{!isEmpty(hostsNeedRestart) && (
<Row>
<Icon icon='error' /> {_('xosanNeedRestart')}
<br />
<ActionButton
btnStyle='success'
handler={restartHostsAgents}
handlerParam={hostsNeedRestart}
icon='host-restart-agent'
>
{_('xosanRestartAgents')}
</ActionButton>
</Row>
)}
{pool != null &&
!noPacksByPool[pool.id] &&
isEmpty(hostsNeedRestart) && [
) : !isEmpty(hostsNeedRestart) ? (
<Row>
<em>{_('xosanSelect2Srs')}</em>
<table className='table table-striped'>
<thead>
<tr>
<th />
<th>{_('xosanName')}</th>
<th>{_('xosanHost')}</th>
<th>{_('xosanSize')}</th>
<th>{_('xosanUsedSpace')}</th>
</tr>
</thead>
<tbody>
{map(lvmsrs, sr => {
const host = find(hosts, ['id', sr.$container])
return (
<tr key={sr.id}>
<td>
<input
checked={selectedSrs[sr.id] || false}
disabled={disableSrCheckbox(sr)}
onChange={event => this._selectSr(event, sr)}
type='checkbox'
/>
</td>
<td>
<Link to={`/srs/${sr.id}/general`}>
{sr.name_label}
</Link>
</td>
<td>
<Link to={`/hosts/${host.id}/general`}>
{host.name_label}
</Link>
</td>
<td>{formatSize(sr.size)}</td>
<td>
{sr.size > 0 && (
<Tooltip
content={_('spaceLeftTooltip', {
used: String(
Math.round(sr.physical_usage / sr.size * 100)
),
free: formatSize(sr.size - sr.physical_usage),
})}
>
<progress
className='progress'
max='100'
value={sr.physical_usage / sr.size * 100}
/>
</Tooltip>
)}
</td>
</tr>
)
})}
</tbody>
</table>
</Row>,
<Row>
{!isEmpty(suggestions) && (
<div>
<h3>{_('xosanSuggestions')}</h3>
<Col>
<Icon icon='error' /> {_('xosanNeedRestart')}
<br />
<ActionButton
btnStyle='success'
handler={restartHostsAgents}
handlerParam={hostsNeedRestart}
icon='host-restart-agent'
>
{_('xosanRestartAgents')}
</ActionButton>
</Col>
</Row>
) : (
[
<Row>
<Col>
<em>{_('xosanSelect2Srs')}</em>
<table className='table table-striped'>
<thead>
<tr>
<th />
<th>{_('xosanLayout')}</th>
<th>{_('xosanRedundancy')}</th>
<th>{_('xosanCapacity')}</th>
<th>{_('xosanAvailableSpace')}</th>
<th>{_('xosanName')}</th>
<th>{_('xosanHost')}</th>
<th>{_('xosanSize')}</th>
<th>{_('xosanUsedSpace')}</th>
</tr>
</thead>
<tbody>
{map(
suggestions,
(
{ layout, redundancy, capacity, availableSpace },
index
) => (
<tr key={index}>
{map(lvmsrs, sr => {
const host = find(hosts, ['id', sr.$container])
return (
<tr key={sr.id}>
<td>
<input
checked={+suggestion === index}
name={`suggestion_${pool.id}`}
onChange={this.linkState('suggestion')}
type='radio'
value={index}
checked={selectedSrs[sr.id] || false}
disabled={disableSrCheckbox(sr)}
onChange={event => this._selectSr(event, sr)}
type='checkbox'
/>
</td>
<td>{layout}</td>
<td>{redundancy}</td>
<td>{capacity}</td>
<td>
{availableSpace === 0 ? (
<strong className='text-danger'>0</strong>
) : (
formatSize(availableSpace)
<Link to={`/srs/${sr.id}/general`}>
{sr.name_label}
</Link>
</td>
<td>
<Link to={`/hosts/${host.id}/general`}>
{host.name_label}
</Link>
</td>
<td>{formatSize(sr.size)}</td>
<td>
{sr.size > 0 && (
<Tooltip
content={_('spaceLeftTooltip', {
used: String(
Math.round(
sr.physical_usage / sr.size * 100
)
),
free: formatSize(
sr.size - sr.physical_usage
),
})}
>
<progress
className='progress'
max='100'
value={sr.physical_usage / sr.size * 100}
/>
</Tooltip>
)}
</td>
</tr>
)
)}
})}
</tbody>
</table>
{architecture.layout === 'disperse' && (
<div className='alert alert-danger'>
{_('xosanDisperseWarning', {
link: (
<a href='https://xen-orchestra.com/docs/xosan_types.html'>
xen-orchestra.com/docs/xosan_types.html
</a>
),
})}
</Col>
</Row>,
<Row>
<Col>
{!isEmpty(suggestions) && (
<div>
<h3>{_('xosanSuggestions')}</h3>
<table className='table table-striped'>
<thead>
<tr>
<th />
<th>{_('xosanLayout')}</th>
<th>{_('xosanRedundancy')}</th>
<th>{_('xosanCapacity')}</th>
<th>{_('xosanAvailableSpace')}</th>
</tr>
</thead>
<tbody>
{map(
suggestions,
(
{ layout, redundancy, capacity, availableSpace },
index
) => (
<tr key={index}>
<td>
<input
checked={+suggestion === index}
name={`suggestion_${pool.id}`}
onChange={this.linkState('suggestion')}
type='radio'
value={index}
/>
</td>
<td>{layout}</td>
<td>{redundancy}</td>
<td>{capacity}</td>
<td>
{availableSpace === 0 ? (
<strong className='text-danger'>0</strong>
) : (
formatSize(availableSpace)
)}
</td>
</tr>
)
)}
</tbody>
</table>
{architecture.layout === 'disperse' && (
<div className='alert alert-danger'>
{_('xosanDisperseWarning', {
link: (
<a href='https://xen-orchestra.com/docs/xosan_types.html'>
xen-orchestra.com/docs/xosan_types.html
</a>
),
})}
</div>
)}
<Graph
height={160}
layout={architecture.layout}
nSrs={this._getNSelectedSrs()}
redundancy={architecture.redundancy}
width={600}
/>
<hr />
<Toggle
onChange={this.toggleState('showAdvanced')}
value={this.state.showAdvanced}
/>{' '}
{_('xosanAdvanced')}{' '}
{this.state.showAdvanced && (
<Container className='mb-1'>
<SingleLineRow>
<Col>{_('xosanVlan')}</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={1}>
<Toggle
onChange={this.linkState('useVlan')}
value={useVlan}
/>
</Col>
<Col size={3}>
<input
className='form-control'
disabled={!useVlan}
onChange={this.linkState('vlan')}
placeholder='VLAN'
type='text'
value={vlan}
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col>{_('xosanCustomIpNetwork')}</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={1}>
<Toggle
onChange={this.linkState('customIpRange')}
value={customIpRange}
/>
</Col>
<Col size={3}>
<input
className='form-control'
disabled={!customIpRange}
onChange={this.linkState('ipRange')}
placeholder='ipRange'
type='text'
value={ipRange}
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col>{_('xosanBrickSize')}</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={1}>
<Toggle
className='mr-1'
onChange={this._onCustomBrickSizeChange}
value={customBrickSize}
/>
</Col>
<Col size={3}>
<SizeInput
readOnly={!customBrickSize}
value={brickSize}
onChange={this._onBrickSizeChange}
required
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={4}>
<label>{_('xosanMemorySize')}</label>
<SizeInput
value={memorySize}
onChange={this.linkState('memorySize')}
required
/>
</Col>
</SingleLineRow>
</Container>
)}
<hr />
</div>
)}
<Graph
height={160}
layout={architecture.layout}
nSrs={this._getNSelectedSrs()}
redundancy={architecture.redundancy}
width={600}
/>
<hr />
<Toggle
onChange={this.toggleState('showAdvanced')}
value={this.state.showAdvanced}
/>{' '}
{_('xosanAdvanced')}{' '}
{this.state.showAdvanced && (
<Container className='mb-1'>
<SingleLineRow>
<Col>{_('xosanVlan')}</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={1}>
<Toggle
onChange={this.linkState('useVlan')}
value={useVlan}
/>
</Col>
<Col size={3}>
<input
className='form-control'
disabled={!useVlan}
onChange={this.linkState('vlan')}
placeholder='VLAN'
type='text'
value={vlan}
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col>{_('xosanCustomIpNetwork')}</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={1}>
<Toggle
onChange={this.linkState('customIpRange')}
value={customIpRange}
/>
</Col>
<Col size={3}>
<input
className='form-control'
disabled={!customIpRange}
onChange={this.linkState('ipRange')}
placeholder='ipRange'
type='text'
value={ipRange}
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col>{_('xosanBrickSize')}</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={1}>
<Toggle
className='mr-1'
onChange={this._onCustomBrickSizeChange}
value={customBrickSize}
/>
</Col>
<Col size={3}>
<SizeInput
readOnly={!customBrickSize}
value={brickSize}
onChange={this._onBrickSizeChange}
required
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={4}>
<label>{_('xosanMemorySize')}</label>
<SizeInput
value={memorySize}
onChange={this.linkState('memorySize')}
required
/>
</Col>
</SingleLineRow>
</Container>
)}
<hr />
</div>
)}
</Row>,
<Row>
<Col>
<ActionButton
btnStyle='success'
disabled={this._getDisableCreation()}
handler={this._createXosanVm}
icon='add'
>
{_('xosanCreate')}
</ActionButton>
</Col>
</Row>,
]}
</Col>
</Row>,
<Row>
<Col>
<ActionButton
btnStyle='success'
disabled={this._getDisableCreation()}
handler={this._createXosanVm}
icon='add'
>
{_('xosanCreate')}
</ActionButton>
</Col>
</Row>,
]
))}
<hr />
</Container>
)

View File

@@ -3157,7 +3157,7 @@ copy-props@^2.0.1:
each-props "^1.3.0"
is-plain-object "^2.0.1"
copy-to-clipboard@^3:
copy-to-clipboard@^3, copy-to-clipboard@^3.0.8:
version "3.0.8"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9"
dependencies: