Compare commits
38 Commits
xo-server-
...
xo-server-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cae978923 | ||
|
|
311d914b96 | ||
|
|
592cb4ef9e | ||
|
|
ec2db7f2d0 | ||
|
|
71eab7ba9b | ||
|
|
5e07171d60 | ||
|
|
3f73e3d964 | ||
|
|
0ebe78b4a2 | ||
|
|
61c3379298 | ||
|
|
44866f3316 | ||
|
|
4bb8ce8779 | ||
|
|
58eb6a8b5f | ||
|
|
52f6a79e01 | ||
|
|
129f79d44b | ||
|
|
385c3eb563 | ||
|
|
e56be51b45 | ||
|
|
24ae65b254 | ||
|
|
d5dffbacbd | ||
|
|
c6ae969a82 | ||
|
|
005a9fdc01 | ||
|
|
f505d4d911 | ||
|
|
8ada6b121e | ||
|
|
b9a87efb0d | ||
|
|
89485a82d2 | ||
|
|
451f87c6b4 | ||
|
|
c3cb5a3221 | ||
|
|
458609ed2e | ||
|
|
fcec8113f3 | ||
|
|
ebbd882ee4 | ||
|
|
0506e19a66 | ||
|
|
ecc62e4f54 | ||
|
|
2b95eb4e4d | ||
|
|
bcde9e0f74 | ||
|
|
114501ebc7 | ||
|
|
ebab7c0867 | ||
|
|
0e2270fb6e | ||
|
|
593493ec0c | ||
|
|
d92898a806 |
5
flow-typed/lodash.js
vendored
5
flow-typed/lodash.js
vendored
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 !',
|
||||
|
||||
@@ -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}}',
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }) =>
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
/> {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)}
|
||||
/> {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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user