Compare commits

..

1 Commits

Author SHA1 Message Date
Julien Fontanet
766175b4a0 feat(xo-server): multi processes 2018-05-15 15:47:32 +02:00
62 changed files with 2481 additions and 2049 deletions

View File

@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -0,0 +1,24 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1,149 @@
# @xen-orchestra/log [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
> ${pkg.description}
## Install
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/log):
```
> npm install --save @xen-orchestra/log
```
## Usage
Everywhere something should be logged:
```js
import { createLogger } from '@xen-orchestra/log'
const log = createLogger('xo-server-api')
log.warn('foo')
```
Then at application level you can choose how to handle these logs:
```js
import configure from '@xen-orchestra/log/configure'
import createConsoleTransport from '@xen-orchestra/log/transports/console'
import createEmailTransport from '@xen-orchestra/log/transports/email'
configure([
{
// if filter is a string, then it is pattern
// (https://github.com/visionmedia/debug#wildcards) which is
// matched against the namespace of the logs
filter: process.env.DEBUG,
transport: createConsoleTransport()
},
{
// only levels >= warn
level: 'warn',
transport: createEmaileTransport({
service: 'gmail',
auth: {
user: 'jane.smith@gmail.com',
pass: 'H&NbECcpXF|pyXe#%ZEb'
},
from: 'jane.smith@gmail.com',
to: [
'jane.smith@gmail.com',
'sam.doe@yahoo.com'
]
})
}
])
```
### Transports
#### Console
```js
import createConsoleTransport from '@xen-orchestra/log/transports/console'
configure(createConsoleTransport())
```
#### Email
Optional dependency:
```
> yarn add nodemailer pretty-format
```
Configuration:
```js
import createEmailTransport from '@xen-orchestra/log/transports/email'
configure(createEmailTransport({
service: 'gmail',
auth: {
user: 'jane.smith@gmail.com',
pass: 'H&NbECcpXF|pyXe#%ZEb'
},
from: 'jane.smith@gmail.com',
to: [
'jane.smith@gmail.com',
'sam.doe@yahoo.com'
]
}))
```
#### Syslog
Optional dependency:
```
> yarn add split-host syslog-client
```
Configuration:
```js
import createSyslogTransport from '@xen-orchestra/log/transports/syslog'
// By default, log to udp://localhost:514
configure(createSyslogTransport())
// But TCP, a different host, or a different port can be used
configure(createSyslogTransport('tcp://syslog.company.lan'))
```
## Development
```
# Install dependencies
> yarn
# Run the tests
> yarn test
# Continuously compile
> yarn dev
# Continuously run the tests
> yarn dev-test
# Build for production (automatically called by npm install)
> yarn build
```
## Contributions
Contributions are *very* welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xo-web/issues/)
you've encountered;
- fork and create a pull request.
## License
ISC © [Vates SAS](https://vates.fr)

View File

@@ -0,0 +1 @@
module.exports = require('./dist/configure')

View File

@@ -0,0 +1,52 @@
{
"private": true,
"name": "@xen-orchestra/log",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/log",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@vates.fr"
},
"preferGlobal": false,
"main": "dist/",
"bin": {},
"files": [
"dist/"
],
"browserslist": [
">2%"
],
"engines": {
"node": ">=4"
},
"dependencies": {
"@babel/polyfill": "7.0.0-beta.42",
"lodash": "^4.17.4",
"promise-toolbox": "^0.9.5"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.42",
"@babel/core": "7.0.0-beta.42",
"@babel/preset-env": "7.0.0-beta.42",
"@babel/preset-flow": "7.0.0-beta.42",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
}

View File

@@ -0,0 +1,105 @@
import createConsoleTransport from './transports/console'
import LEVELS, { resolve } from './levels'
import { compileGlobPattern } from './utils'
// ===================================================================
const createTransport = config => {
if (typeof config === 'function') {
return config
}
if (Array.isArray(config)) {
const transports = config.map(createTransport)
const { length } = transports
return function () {
for (let i = 0; i < length; ++i) {
transports[i].apply(this, arguments)
}
}
}
let { filter, transport } = config
const level = resolve(config.level)
if (filter !== undefined) {
if (typeof filter === 'string') {
const re = compileGlobPattern(filter)
filter = log => re.test(log.namespace)
}
const orig = transport
transport = function (log) {
if ((level !== undefined && log.level >= level) || filter(log)) {
return orig.apply(this, arguments)
}
}
} else if (level !== undefined) {
const orig = transport
transport = function (log) {
if (log.level >= level) {
return orig.apply(this, arguments)
}
}
}
return transport
}
let transport = createTransport({
// display warnings or above, and all that are enabled via DEBUG or
// NODE_DEBUG env
filter: process.env.DEBUG || process.env.NODE_DEBUG,
level: LEVELS.INFO,
transport: createConsoleTransport(),
})
const symbol =
typeof Symbol !== 'undefined'
? Symbol.for('@xen-orchestra/log')
: '@@@xen-orchestra/log'
global[symbol] = log => transport(log)
export const configure = config => {
transport = createTransport(config)
}
// -------------------------------------------------------------------
export const catchGlobalErrors = logger => {
// patch process
const onUncaughtException = error => {
logger.error('uncaught exception', { error })
}
const onUnhandledRejection = error => {
logger.warn('possibly unhandled rejection', { error })
}
const onWarning = error => {
logger.warn('Node warning', { error })
}
process.on('uncaughtException', onUncaughtException)
process.on('unhandledRejection', onUnhandledRejection)
process.on('warning', onWarning)
// patch EventEmitter
const EventEmitter = require('events')
const { prototype } = EventEmitter
const { emit } = prototype
function patchedEmit (event, error) {
event === 'error' && !this.listenerCount(event)
? logger.error('unhandled error event', { error })
: emit.apply(this, arguments)
}
prototype.emit = patchedEmit
return () => {
process.removeListener('uncaughtException', onUncaughtException)
process.removeListener('unhandledRejection', onUnhandledRejection)
process.removeListener('warning', onWarning)
if (prototype.emit === patchedEmit) {
prototype.emit = emit
}
}
}

View File

@@ -0,0 +1,65 @@
import createTransport from './transports/console'
import LEVELS from './levels'
const symbol =
typeof Symbol !== 'undefined'
? Symbol.for('@xen-orchestra/log')
: '@@@xen-orchestra/log'
if (!(symbol in global)) {
// the default behavior, without requiring `configure` is to avoid
// logging anything unless it's a real error
const transport = createTransport()
global[symbol] = log => log.level > LEVELS.WARN && transport(log)
}
// -------------------------------------------------------------------
function Log (data, level, namespace, message, time) {
this.data = data
this.level = level
this.namespace = namespace
this.message = message
this.time = time
}
function Logger (namespace) {
this._namespace = namespace
// bind all logging methods
for (const name in LEVELS) {
const lowerCase = name.toLowerCase()
this[lowerCase] = this[lowerCase].bind(this)
}
}
const { prototype } = Logger
for (const name in LEVELS) {
const level = LEVELS[name]
prototype[name.toLowerCase()] = function (message, data) {
global[symbol](new Log(data, level, this._namespace, message, new Date()))
}
}
prototype.wrap = function (message, fn) {
const logger = this
const warnAndRethrow = error => {
logger.warn(message, { error })
throw error
}
return function () {
try {
const result = fn.apply(this, arguments)
const then = result != null && result.then
return typeof then === 'function'
? then.call(result, warnAndRethrow)
: result
} catch (error) {
warnAndRethrow(error)
}
}
}
const createLogger = namespace => new Logger(namespace)
export { createLogger }

View File

@@ -0,0 +1,24 @@
const LEVELS = Object.create(null)
export { LEVELS as default }
// https://github.com/trentm/node-bunyan#levels
LEVELS.FATAL = 60 // service/app is going to down
LEVELS.ERROR = 50 // fatal for current action
LEVELS.WARN = 40 // something went wrong but it's not fatal
LEVELS.INFO = 30 // detail on unusual but normal operation
LEVELS.DEBUG = 20
export const NAMES = Object.create(null)
for (const name in LEVELS) {
NAMES[LEVELS[name]] = name
}
export const resolve = level => {
if (typeof level === 'string') {
level = LEVELS[level.toUpperCase()]
}
return level
}
Object.freeze(LEVELS)
Object.freeze(NAMES)

View File

@@ -0,0 +1,32 @@
/* eslint-env jest */
import { forEach, isInteger } from 'lodash'
import LEVELS, { NAMES, resolve } from './levels'
describe('LEVELS', () => {
it('maps level names to their integer values', () => {
forEach(LEVELS, (value, name) => {
expect(isInteger(value)).toBe(true)
})
})
})
describe('NAMES', () => {
it('maps level values to their names', () => {
forEach(LEVELS, (value, name) => {
expect(NAMES[value]).toBe(name)
})
})
})
describe('resolve()', () => {
it('returns level values either from values or names', () => {
forEach(LEVELS, value => {
expect(resolve(value)).toBe(value)
})
forEach(NAMES, (name, value) => {
expect(resolve(name)).toBe(+value)
})
})
})

View File

@@ -0,0 +1,20 @@
import LEVELS, { NAMES } from '../levels'
// Bind console methods (necessary for browsers)
const debugConsole = console.log.bind(console)
const infoConsole = console.info.bind(console)
const warnConsole = console.warn.bind(console)
const errorConsole = console.error.bind(console)
const { ERROR, INFO, WARN } = LEVELS
const consoleTransport = ({ data, level, namespace, message, time }) => {
const fn =
level < INFO
? debugConsole
: level < WARN ? infoConsole : level < ERROR ? warnConsole : errorConsole
fn('%s - %s - [%s] %s', time.toISOString(), namespace, NAMES[level], message)
data != null && fn(data)
}
export default () => consoleTransport

View File

@@ -0,0 +1,68 @@
import prettyFormat from 'pretty-format' // eslint-disable-line node/no-extraneous-import
import { createTransport } from 'nodemailer' // eslint-disable-line node/no-extraneous-import
import { fromCallback } from 'promise-toolbox'
import { evalTemplate, required } from '../utils'
import { NAMES } from '../levels'
export default ({
// transport options (https://nodemailer.com/smtp/)
auth,
authMethod,
host,
ignoreTLS,
port,
proxy,
requireTLS,
secure,
service,
tls,
// message options (https://nodemailer.com/message/)
bcc,
cc,
from = required('from'),
to = required('to'),
subject = '[{{level}} - {{namespace}}] {{time}} {{message}}',
}) => {
const transporter = createTransport(
{
auth,
authMethod,
host,
ignoreTLS,
port,
proxy,
requireTLS,
secure,
service,
tls,
disableFileAccess: true,
disableUrlAccess: true,
},
{
bcc,
cc,
from,
to,
}
)
return log =>
fromCallback(cb =>
transporter.sendMail(
{
subject: evalTemplate(
subject,
key =>
key === 'level'
? NAMES[log.level]
: key === 'time' ? log.time.toISOString() : log[key]
),
text: prettyFormat(log.data),
},
cb
)
)
}

View File

@@ -0,0 +1,42 @@
import splitHost from 'split-host' // eslint-disable-line node/no-extraneous-import node/no-missing-import
import { createClient, Facility, Severity, Transport } from 'syslog-client' // eslint-disable-line node/no-extraneous-import node/no-missing-import
import { fromCallback } from 'promise-toolbox'
import { startsWith } from 'lodash'
import LEVELS from '../levels'
// https://github.com/paulgrove/node-syslog-client#syslogseverity
const LEVEL_TO_SEVERITY = {
[LEVELS.FATAL]: Severity.Critical,
[LEVELS.ERROR]: Severity.Error,
[LEVELS.WARN]: Severity.Warning,
[LEVELS.INFO]: Severity.Informational,
[LEVELS.DEBUG]: Severity.Debug,
}
const facility = Facility.User
export default target => {
const opts = {}
if (target !== undefined) {
if (startsWith(target, 'tcp://')) {
target = target.slice(6)
opts.transport = Transport.Tcp
} else if (startsWith(target, 'udp://')) {
target = target.slice(6)
opts.transport = Transport.Ucp
}
;({ host: target, port: opts.port } = splitHost(target))
}
const client = createClient(target, opts)
return log =>
fromCallback(cb =>
client.log(log.message, {
facility,
severity: LEVEL_TO_SEVERITY[log.level],
})
)
}

View File

@@ -0,0 +1,62 @@
import escapeRegExp from 'lodash/escapeRegExp'
// ===================================================================
const TPL_RE = /\{\{(.+?)\}\}/g
export const evalTemplate = (tpl, data) => {
const getData =
typeof data === 'function' ? (_, key) => data(key) : (_, key) => data[key]
return tpl.replace(TPL_RE, getData)
}
// -------------------------------------------------------------------
const compileGlobPatternFragment = pattern =>
pattern
.split('*')
.map(escapeRegExp)
.join('.*')
export const compileGlobPattern = pattern => {
const no = []
const yes = []
pattern.split(/[\s,]+/).forEach(pattern => {
if (pattern[0] === '-') {
no.push(pattern.slice(1))
} else {
yes.push(pattern)
}
})
const raw = ['^']
if (no.length !== 0) {
raw.push('(?!', no.map(compileGlobPatternFragment).join('|'), ')')
}
if (yes.length !== 0) {
raw.push('(?:', yes.map(compileGlobPatternFragment).join('|'), ')')
} else {
raw.push('.*')
}
raw.push('$')
return new RegExp(raw.join(''))
}
// -------------------------------------------------------------------
export const required = name => {
throw new Error(`missing required arg ${name}`)
}
// -------------------------------------------------------------------
export const serializeError = error => ({
...error,
message: error.message,
name: error.name,
stack: error.stack,
})

View File

@@ -0,0 +1,13 @@
/* eslint-env jest */
import { compileGlobPattern } from './utils'
describe('compileGlobPattern()', () => {
it('works', () => {
const re = compileGlobPattern('foo, ba*, -bar')
expect(re.test('foo')).toBe(true)
expect(re.test('bar')).toBe(false)
expect(re.test('baz')).toBe(true)
expect(re.test('qux')).toBe(false)
})
})

View File

@@ -0,0 +1 @@
dist/transports

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import assert from 'assert'
import asyncIteratorToStream from 'async-iterator-to-stream'
import computeGeometryForSize from './_computeGeometryForSize'
@@ -26,16 +25,62 @@ function createBAT (
bat,
bitmapSize
) {
const vhdOccupationTable = []
let currentVhdPositionSector = firstBlockPosition / SECTOR_SIZE
blockAddressList.forEach(blockPosition => {
assert.strictEqual(blockPosition % 512, 0)
const vhdTableIndex = Math.floor(blockPosition / VHD_BLOCK_SIZE_BYTES)
const scaled = blockPosition / VHD_BLOCK_SIZE_BYTES
const vhdTableIndex = Math.floor(scaled)
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 * (
@@ -78,49 +123,21 @@ export default asyncIteratorToStream(async function * (
const bitmapSize =
Math.ceil(VHD_BLOCK_SIZE_SECTORS / 8 / SECTOR_SIZE) * SECTOR_SIZE
const bat = Buffer.alloc(tablePhysicalSizeBytes, 0xff)
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)
const vhdOccupationTable = createBAT(
firstBlockPosition,
blockAddressList,
ratio,
bat,
bitmapSize
)
yield footer
yield header
yield bat
yield * generateFileContent(
blockIterator,
bitmapSize,
ratio,
vhdOccupationTable
)
yield footer
})

View File

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

View File

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

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env node
'use strict'
// ===================================================================
// Better stack traces if possible.
require('../better-stacks')
// Use Bluebird for all promises as it provides better performance and
// less memory usage.
global.Promise = require('bluebird')
// Make unhandled rejected promises visible.
process.on('unhandledRejection', function (reason) {
console.warn('[Warn] Possibly unhandled rejection:', reason && reason.stack || reason)
})
;(function (EE) {
var proto = EE.prototype
var emit = proto.emit
proto.emit = function patchedError (event, error) {
if (event === 'error' && !this.listenerCount(event)) {
return console.warn('[Warn] Unhandled error event:', error && error.stack || error)
}
return emit.apply(this, arguments)
}
})(require('events').EventEmitter)
require('exec-promise')(require('../'))

View File

@@ -1,11 +0,0 @@
'use strict'
// ===================================================================
// Enable xo logs by default.
if (process.env.DEBUG === undefined) {
process.env.DEBUG = 'app-conf,xo:*,-xo:api'
}
// Import the real main module.
module.exports = require('./dist').default

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server",
"version": "5.19.8",
"version": "5.19.4",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -16,6 +16,9 @@
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"preferGlobal": true,
"bin": {
"xo-server": "dist/cli"
},
"files": [
"better-stacks.js",
"bin/",
@@ -111,7 +114,7 @@
"tmp": "^0.0.33",
"uuid": "^3.0.1",
"value-matcher": "^0.2.0",
"vhd-lib": "^0.1.0",
"vhd-lib": "^0.0.0",
"ws": "^5.0.0",
"xen-api": "^0.16.9",
"xml2js": "^0.4.19",
@@ -119,7 +122,7 @@
"xo-collection": "^0.4.1",
"xo-common": "^0.1.1",
"xo-remote-parser": "^0.3",
"xo-vmdk-to-vhd": "^0.1.2",
"xo-vmdk-to-vhd": "0.1.0",
"yazl": "^2.4.3"
},
"devDependencies": {

View File

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

View File

@@ -12,10 +12,6 @@ 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 => {
@@ -560,11 +556,11 @@ set.params = {
name_description: { type: 'string', optional: true },
high_availability: {
optional: true,
pattern: new RegExp(`^(${getHaValues().join('|')})$`),
type: 'string',
},
// 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 },
// Number of virtual CPUs to allocate.
CPUs: { type: 'integer', optional: true },

176
packages/xo-server/src/cli.js Executable file
View File

@@ -0,0 +1,176 @@
#!/usr/bin/env node
const APP_NAME = 'xo-server'
// Enable xo logs by default.
if (process.env.DEBUG === undefined) {
process.env.DEBUG = 'app-conf,xo:*,-xo:api'
}
// -------------------------------------------------------------------
require('@xen-orchestra/log/configure').configure([
{
filter: process.env.DEBUG,
level: 'warn',
transport: require('@xen-orchestra/log/transports/console').default(),
},
])
const { info, warn } = require('@xen-orchestra/log').createLogger('bootstrap')
process.on('unhandledRejection', reason => {
warn('possibly unhandled rejection', reason)
})
process.on('warning', warning => {
warn('Node warning', warning)
})
;(({ prototype }) => {
const { emit } = prototype
prototype.emit = function (event, error) {
event === 'error' && !this.listenerCount(event)
? warn('unhandled error event', error)
: emit.apply(this, arguments)
}
})(require('events').EventEmitter)
// Use Bluebird for all promises as it provides better performance and
// less memory usage.
const Bluebird = require('bluebird')
Bluebird.config({
longStackTraces: true,
warnings: true,
})
global.Promise = Bluebird
// -------------------------------------------------------------------
const main = async args => {
if (args.includes('--help') || args.includes('-h')) {
const { name, version } = require('../package.json')
return console.log(`Usage: ${name} [--safe-mode]
${name} v${version}`)
}
info('starting')
const config = await require('app-conf').load(APP_NAME, {
appDir: `${__dirname}/..`,
ignoreUnknownFormats: true,
})
// Print a message if deprecated entries are specified.
;['users', 'servers'].forEach(entry => {
if (entry in config) {
warn(`${entry} configuration is deprecated`)
}
})
const httpServer = require('stoppable')(new (require('http-server-plus'))())
const readFile = Bluebird.promisify(require('fs').readFile)
await Promise.all(
config.http.listen.map(
async ({
certificate,
// The properties was called `certificate` before.
cert = certificate,
key,
...opts
}) => {
if (cert !== undefined && key !== undefined) {
;[opts.cert, opts.key] = await Promise.all([
readFile(cert),
readFile(key),
])
}
try {
const niceAddress = await httpServer.listen(opts)
info(`web server listening on ${niceAddress}`)
} catch (error) {
if (error.niceAddress !== undefined) {
warn(`web server could not listen on ${error.niceAddress}`)
const { code } = error
if (code === 'EACCES') {
warn(' access denied.')
warn(' ports < 1024 are often reserved to privileges users.')
} else if (code === 'EADDRINUSE') {
warn(' address already in use.')
}
} else {
warn('web server could not listen', error)
}
}
}
)
)
// Now the web server is listening, drop privileges.
try {
const { group, user } = config
if (group !== undefined) {
process.setgid(group)
info('group changed to', group)
}
if (user !== undefined) {
process.setuid(user)
info('user changed to', user)
}
} catch (error) {
warn('failed to change group/user', error)
}
const child = require('child_process').fork(require.resolve('./worker.js'))
child.send([''])
const App = require('./xo').default
const app = new App({
appName: APP_NAME,
config,
httpServer,
safeMode: require('lodash/includes')(args, '--safe-mode'),
})
// Register web server close on XO stop.
app.on('stop', () => Bluebird.fromCallback(cb => httpServer.stop(cb)))
await app.start()
// Trigger a clean job.
await app.clean()
// Gracefully shutdown on signals.
//
// TODO: implements a timeout? (or maybe it is the services launcher
// responsibility?)
require('lodash/forEach')(['SIGINT', 'SIGTERM'], signal => {
let alreadyCalled = false
process.on(signal, () => {
if (alreadyCalled) {
warn('forced exit')
process.exit(1)
}
alreadyCalled = true
info(`${signal} caught, closing…`)
app.stop()
})
})
await require('event-to-promise')(app, 'stopped')
}
main(process.argv.slice(2)).then(
() => info('bye :-)'),
error => {
if (typeof error === 'number') {
process.exitCode = error
} else {
warn('fatal error', error)
}
}
)

View File

@@ -0,0 +1,348 @@
const compilePug = require('pug').compile
const createProxyServer = require('http-proxy').createServer
const JsonRpcPeer = require('json-rpc-peer')
const LocalStrategy = require('passport-local').Strategy
const parseCookies = require('cookie').parse
const Passport = require('passport')
const serveStatic = require('serve-static')
const WebSocket = require('ws')
const { fromCallback } = require('promise-toolbox')
const { invalidCredentials } = require('xo-common/api-errors')
const { readFile } = require('fs')
const proxyConsole = require('../proxy-console')
const { debug, warn } = require('@xen-orchestra/log').createLogger('front')
function createExpressApp ({ http: config }, httpServer) {
const express = require('express')()
express.use(require('helmet')())
if (config.redirectToHttps) {
const https = config.listen.find(
_ =>
_.port !== undefined &&
(_.cert !== undefined || _.certificate !== undefined)
)
if (https === undefined) {
warn('could not setup HTTPs redirection: no HTTPs config found')
} else {
const { port } = https
express.use((req, res, next) => {
if (req.secure) {
return next()
}
res.redirect(`https://${req.hostname}:${port}${req.originalUrl}`)
})
}
}
Object.keys(config.mounts).forEach(url => {
const paths = config.mounts[url]
;(Array.isArray(paths) ? paths : [paths]).forEach(path => {
debug('Setting up %s → %s', url, path)
express.use(url, serveStatic(path))
})
})
return express
}
function setUpApi (config, httpServer, xo) {
const webSocketServer = new WebSocket.Server({
noServer: true,
})
xo.on('stop', () => fromCallback(cb => webSocketServer.close(cb)))
const onConnection = (socket, upgradeReq) => {
const { remoteAddress } = upgradeReq.socket
debug('+ WebSocket connection (%s)', remoteAddress)
// Create the abstract XO object for this connection.
const connection = xo.createUserConnection()
connection.once('close', () => {
socket.close()
})
// Create the JSON-RPC server for this connection.
const jsonRpc = new JsonRpcPeer(message => {
if (message.type === 'request') {
return xo.callApiMethod(connection, message.method, message.params)
}
})
connection.notify = jsonRpc.notify.bind(jsonRpc)
// Close the XO connection with this WebSocket.
socket.once('close', () => {
debug('- WebSocket connection (%s)', remoteAddress)
connection.close()
})
// Connect the WebSocket to the JSON-RPC server.
socket.on('message', message => {
jsonRpc.write(message)
})
const onSend = error => {
if (error) {
warn('WebSocket send:', error.stack)
}
}
jsonRpc.on('data', data => {
// The socket may have been closed during the API method
// execution.
if (socket.readyState === WebSocket.OPEN) {
socket.send(data, onSend)
}
})
}
httpServer.on('upgrade', (req, socket, head) => {
if (req.url === '/api/') {
webSocketServer.handleUpgrade(req, socket, head, ws =>
onConnection(ws, req)
)
}
})
}
function setUpConsoleProxy (httpServer, xo) {
const webSocketServer = new WebSocket.Server({
noServer: true,
})
const CONSOLE_PROXY_PATH_RE = /^\/api\/consoles\/(.*)$/
httpServer.on('upgrade', async (req, socket, head) => {
const matches = CONSOLE_PROXY_PATH_RE.exec(req.url)
if (!matches) {
return
}
const [, id] = matches
try {
// TODO: factorize permissions checking in an Express middleware.
{
const { token } = parseCookies(req.headers.cookie)
const user = await xo.authenticateUser({ token })
if (!await xo.hasPermissions(user.id, [[id, 'operate']])) {
throw invalidCredentials()
}
const { remoteAddress } = socket
debug('+ Console proxy (%s - %s)', user.name, remoteAddress)
socket.on('close', () => {
debug('- Console proxy (%s - %s)', user.name, remoteAddress)
})
}
const xapi = xo.getXapi(id, ['VM', 'VM-controller'])
const vmConsole = xapi.getVmConsole(id)
// FIXME: lost connection due to VM restart is not detected.
webSocketServer.handleUpgrade(req, socket, head, connection => {
proxyConsole(connection, vmConsole, xapi.sessionId)
})
} catch (error) {
console.error((error && error.stack) || error)
}
})
}
async function setUpPassport (express, xo) {
// necessary for connect-flash
express.use(require('cookie-parser')())
express.use(
require('express-session')({
resave: false,
saveUninitialized: false,
// TODO: should be in the config file.
secret: 'CLWguhRZAZIXZcbrMzHCYmefxgweItKnS',
})
)
// necessary for Passport to display error messages
express.use(require('connect-flash')())
// necessary for Passport to access the username and password from the sign
// in form
express.use(require('body-parser').urlencoded({ extended: false }))
express.use(Passport.initialize())
const strategies = { __proto__: null }
xo.registerPassportStrategy = strategy => {
Passport.use(strategy)
const { name } = strategy
if (name !== 'local') {
strategies[name] = strategy.label || name
}
}
// Registers the sign in form.
const signInPage = compilePug(
await fromCallback(cb => readFile(`${__dirname}/../signin.pug`, cb))
)
express.get('/signin', (req, res, next) => {
res.send(
signInPage({
error: req.flash('error')[0],
strategies,
})
)
})
express.get('/signout', (req, res) => {
res.clearCookie('token')
res.redirect('/')
})
const SIGNIN_STRATEGY_RE = /^\/signin\/([^/]+)(\/callback)?(:?\?.*)?$/
express.use(async (req, res, next) => {
const { url } = req
const matches = url.match(SIGNIN_STRATEGY_RE)
if (matches !== null) {
return Passport.authenticate(matches[1], async (err, user, info) => {
if (err) {
return next(err)
}
if (!user) {
req.flash('error', info ? info.message : 'Invalid credentials')
return res.redirect('/signin')
}
// The cookie will be set in via the next request because some
// browsers do not save cookies on redirect.
req.flash(
'token',
(await xo.createAuthenticationToken({ userId: user.id })).id
)
// The session is only persistent for internal provider and if 'Remember me' box is checked
req.flash(
'session-is-persistent',
matches[1] === 'local' && req.body['remember-me'] === 'on'
)
res.redirect(req.flash('return-url')[0] || '/')
})(req, res, next)
}
const token = req.flash('token')[0]
if (token) {
const isPersistent = req.flash('session-is-persistent')[0]
if (isPersistent) {
// Persistent cookie ? => 1 year
res.cookie('token', token, { maxAge: 1000 * 60 * 60 * 24 * 365 })
} else {
// Non-persistent : external provider as Github, Twitter...
res.cookie('token', token)
}
next()
} else if (req.cookies.token) {
next()
} else if (
/favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/.test(url)
) {
next()
} else {
req.flash('return-url', url)
return res.redirect('/signin')
}
})
// Install the local strategy.
xo.registerPassportStrategy(
new LocalStrategy(async (username, password, done) => {
try {
const user = await xo.authenticateUser({ username, password })
done(null, user)
} catch (error) {
done(null, false, { message: error.message })
}
})
)
}
function setUpProxies ({ http: { proxies } }, httpServer, express, xo) {
if (proxies === undefined) {
return
}
const proxy = createProxyServer({
ignorePath: true,
}).on('error', error => console.error(error))
const prefixes = Object.keys(proxies).sort((a, b) => a.length - b.length)
const n = prefixes.length
// HTTP request proxy.
express.use((req, res, next) => {
const { url } = req
for (let i = 0; i < n; ++i) {
const prefix = prefixes[i]
if (url.startsWith(prefix)) {
const target = proxies[prefix]
proxy.web(req, res, {
target: target + url.slice(prefix.length),
})
return
}
}
next()
})
// WebSocket proxy.
const webSocketServer = new WebSocket.Server({
noServer: true,
})
xo.on('stop', () => fromCallback(cb => webSocketServer.close(cb)))
httpServer.on('upgrade', (req, socket, head) => {
const { url } = req
for (let i = 0; i < n; ++i) {
const prefix = prefixes[i]
if (url.startsWith(prefix)) {
const target = proxies[prefix]
proxy.ws(req, socket, head, {
target: target + url.slice(prefix.length),
})
return
}
}
})
}
export default async function main ({ config, httpServer, safeMode }) {
const express = createExpressApp(config, httpServer)
setUpProxies(config, httpServer, express, xo)
setUpApi(config, httpServer, xo)
// must be set up before the API
setUpConsoleProxy(httpServer, xo)
await setUpPassport(express, xo)
// TODO: express.use(xo._handleHttpRequest.bind(xo))
}

View File

@@ -1,664 +0,0 @@
import appConf from 'app-conf'
import bind from 'lodash/bind'
import blocked from 'blocked'
import createExpress from 'express'
import createLogger from 'debug'
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'
import WebSocket from 'ws'
import { compile as compilePug } from 'pug'
import { createServer as createProxyServer } from 'http-proxy'
import { fromEvent } from 'promise-toolbox'
import { join as joinPath } from 'path'
import JsonRpcPeer from 'json-rpc-peer'
import { invalidCredentials } from 'xo-common/api-errors'
import { ensureDir, readdir, readFile } from 'fs-extra'
import WebServer from 'http-server-plus'
import Xo from './xo'
import {
forEach,
isArray,
isFunction,
mapToArray,
pFromCallback,
} from './utils'
import bodyParser from 'body-parser'
import connectFlash from 'connect-flash'
import cookieParser from 'cookie-parser'
import expressSession from 'express-session'
import passport from 'passport'
import { parse as parseCookies } from 'cookie'
import { Strategy as LocalStrategy } from 'passport-local'
// ===================================================================
const debug = createLogger('xo:main')
const warn = (...args) => {
console.warn('[Warn]', ...args)
}
// ===================================================================
const DEPRECATED_ENTRIES = ['users', 'servers']
async function loadConfiguration () {
const config = await appConf.load('xo-server', {
appDir: joinPath(__dirname, '..'),
ignoreUnknownFormats: true,
})
debug('Configuration loaded.')
// Print a message if deprecated entries are specified.
forEach(DEPRECATED_ENTRIES, entry => {
if (has(config, entry)) {
warn(`${entry} configuration is deprecated.`)
}
})
return config
}
// ===================================================================
function createExpressApp () {
const app = createExpress()
app.use(helmet())
// Registers the cookie-parser and express-session middlewares,
// necessary for connect-flash.
app.use(cookieParser())
app.use(
expressSession({
resave: false,
saveUninitialized: false,
// TODO: should be in the config file.
secret: 'CLWguhRZAZIXZcbrMzHCYmefxgweItKnS',
})
)
// Registers the connect-flash middleware, necessary for Passport to
// display error messages.
app.use(connectFlash())
// Registers the body-parser middleware, necessary for Passport to
// access the username and password from the sign in form.
app.use(bodyParser.urlencoded({ extended: false }))
// Registers Passport's middlewares.
app.use(passport.initialize())
return app
}
async function setUpPassport (express, xo) {
const strategies = { __proto__: null }
xo.registerPassportStrategy = strategy => {
passport.use(strategy)
const { name } = strategy
if (name !== 'local') {
strategies[name] = strategy.label || name
}
}
// Registers the sign in form.
const signInPage = compilePug(
await readFile(joinPath(__dirname, '..', 'signin.pug'))
)
express.get('/signin', (req, res, next) => {
res.send(
signInPage({
error: req.flash('error')[0],
strategies,
})
)
})
express.get('/signout', (req, res) => {
res.clearCookie('token')
res.redirect('/')
})
const SIGNIN_STRATEGY_RE = /^\/signin\/([^/]+)(\/callback)?(:?\?.*)?$/
express.use(async (req, res, next) => {
const { url } = req
const matches = url.match(SIGNIN_STRATEGY_RE)
if (matches) {
return passport.authenticate(matches[1], async (err, user, info) => {
if (err) {
return next(err)
}
if (!user) {
req.flash('error', info ? info.message : 'Invalid credentials')
return res.redirect('/signin')
}
// The cookie will be set in via the next request because some
// browsers do not save cookies on redirect.
req.flash(
'token',
(await xo.createAuthenticationToken({ userId: user.id })).id
)
// The session is only persistent for internal provider and if 'Remember me' box is checked
req.flash(
'session-is-persistent',
matches[1] === 'local' && req.body['remember-me'] === 'on'
)
res.redirect(req.flash('return-url')[0] || '/')
})(req, res, next)
}
const token = req.flash('token')[0]
if (token) {
const isPersistent = req.flash('session-is-persistent')[0]
if (isPersistent) {
// Persistent cookie ? => 1 year
res.cookie('token', token, { maxAge: 1000 * 60 * 60 * 24 * 365 })
} else {
// Non-persistent : external provider as Github, Twitter...
res.cookie('token', token)
}
next()
} else if (req.cookies.token) {
next()
} else if (
/favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/.test(url)
) {
next()
} else {
req.flash('return-url', url)
return res.redirect('/signin')
}
})
// Install the local strategy.
xo.registerPassportStrategy(
new LocalStrategy(async (username, password, done) => {
try {
const user = await xo.authenticateUser({ username, password })
done(null, user)
} catch (error) {
done(null, false, { message: error.message })
}
})
)
}
// ===================================================================
async function registerPlugin (pluginPath, pluginName) {
const plugin = require(pluginPath)
const { description, version = 'unknown' } = (() => {
try {
return require(pluginPath + '/package.json')
} catch (_) {
return {}
}
})()
// Supports both “normal” CommonJS and Babel's ES2015 modules.
const {
default: factory = plugin,
configurationSchema,
configurationPresets,
testSchema,
} = plugin
// The default export can be either a factory or directly a plugin
// instance.
const instance = isFunction(factory)
? factory({
xo: this,
getDataDir: () => {
const dir = `${this._config.datadir}/${pluginName}`
return ensureDir(dir).then(() => dir)
},
})
: factory
await this.registerPlugin(
pluginName,
instance,
configurationSchema,
configurationPresets,
description,
testSchema,
version
)
}
const debugPlugin = createLogger('xo:plugin')
function registerPluginWrapper (pluginPath, pluginName) {
debugPlugin('register %s', pluginName)
return registerPlugin.call(this, pluginPath, pluginName).then(
() => {
debugPlugin(`successfully register ${pluginName}`)
},
error => {
debugPlugin(`failed register ${pluginName}`)
debugPlugin(error)
}
)
}
const PLUGIN_PREFIX = 'xo-server-'
const PLUGIN_PREFIX_LENGTH = PLUGIN_PREFIX.length
async function registerPluginsInPath (path) {
const files = await readdir(path).catch(error => {
if (error.code === 'ENOENT') {
return []
}
throw error
})
await Promise.all(
mapToArray(files, name => {
if (startsWith(name, PLUGIN_PREFIX)) {
return registerPluginWrapper.call(
this,
`${path}/${name}`,
name.slice(PLUGIN_PREFIX_LENGTH)
)
}
})
)
}
async function registerPlugins (xo) {
await Promise.all(
mapToArray(
[`${__dirname}/../node_modules/`, '/usr/local/lib/node_modules/'],
xo::registerPluginsInPath
)
)
}
// ===================================================================
async function makeWebServerListen (
webServer,
{
certificate,
// The properties was called `certificate` before.
cert = certificate,
key,
...opts
}
) {
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)
debug(`Web server listening on ${niceAddress}`)
} catch (error) {
if (error.niceAddress) {
warn(`Web server could not listen on ${error.niceAddress}`)
const { code } = error
if (code === 'EACCES') {
warn(' Access denied.')
warn(' Ports < 1024 are often reserved to privileges users.')
} else if (code === 'EADDRINUSE') {
warn(' Address already in use.')
}
} else {
warn('Web server could not listen:', error.message)
}
}
}
async function createWebServer ({ listen, listenOptions }) {
const webServer = stoppable(new WebServer())
await Promise.all(
mapToArray(listen, opts =>
makeWebServerListen(webServer, { ...listenOptions, ...opts })
)
)
return webServer
}
// ===================================================================
const setUpProxies = (express, opts, xo) => {
if (!opts) {
return
}
const proxy = createProxyServer({
ignorePath: true,
}).on('error', error => console.error(error))
// TODO: sort proxies by descending prefix length.
// HTTP request proxy.
express.use((req, res, next) => {
const { url } = req
for (const prefix in opts) {
if (startsWith(url, prefix)) {
const target = opts[prefix]
proxy.web(req, res, {
target: target + url.slice(prefix.length),
})
return
}
}
next()
})
// WebSocket proxy.
const webSocketServer = new WebSocket.Server({
noServer: true,
})
xo.on('stop', () => pFromCallback(cb => webSocketServer.close(cb)))
express.on('upgrade', (req, socket, head) => {
const { url } = req
for (const prefix in opts) {
if (startsWith(url, prefix)) {
const target = opts[prefix]
proxy.ws(req, socket, head, {
target: target + url.slice(prefix.length),
})
return
}
}
})
}
// ===================================================================
const setUpStaticFiles = (express, opts) => {
forEach(opts, (paths, url) => {
if (!isArray(paths)) {
paths = [paths]
}
forEach(paths, path => {
debug('Setting up %s → %s', url, path)
express.use(url, serveStatic(path))
})
})
}
// ===================================================================
const setUpApi = (webServer, xo, verboseLogsOnErrors) => {
const webSocketServer = new WebSocket.Server({
noServer: true,
})
xo.on('stop', () => pFromCallback(cb => webSocketServer.close(cb)))
const onConnection = (socket, upgradeReq) => {
const { remoteAddress } = upgradeReq.socket
debug('+ WebSocket connection (%s)', remoteAddress)
// Create the abstract XO object for this connection.
const connection = xo.createUserConnection()
connection.once('close', () => {
socket.close()
})
// Create the JSON-RPC server for this connection.
const jsonRpc = new JsonRpcPeer(message => {
if (message.type === 'request') {
return xo.callApiMethod(connection, message.method, message.params)
}
})
connection.notify = bind(jsonRpc.notify, jsonRpc)
// Close the XO connection with this WebSocket.
socket.once('close', () => {
debug('- WebSocket connection (%s)', remoteAddress)
connection.close()
})
// Connect the WebSocket to the JSON-RPC server.
socket.on('message', message => {
jsonRpc.write(message)
})
const onSend = error => {
if (error) {
warn('WebSocket send:', error.stack)
}
}
jsonRpc.on('data', data => {
// The socket may have been closed during the API method
// execution.
if (socket.readyState === WebSocket.OPEN) {
socket.send(data, onSend)
}
})
}
webServer.on('upgrade', (req, socket, head) => {
if (req.url === '/api/') {
webSocketServer.handleUpgrade(req, socket, head, ws =>
onConnection(ws, req)
)
}
})
}
// ===================================================================
const CONSOLE_PROXY_PATH_RE = /^\/api\/consoles\/(.*)$/
const setUpConsoleProxy = (webServer, xo) => {
const webSocketServer = new WebSocket.Server({
noServer: true,
})
xo.on('stop', () => pFromCallback(cb => webSocketServer.close(cb)))
webServer.on('upgrade', async (req, socket, head) => {
const matches = CONSOLE_PROXY_PATH_RE.exec(req.url)
if (!matches) {
return
}
const [, id] = matches
try {
// TODO: factorize permissions checking in an Express middleware.
{
const { token } = parseCookies(req.headers.cookie)
const user = await xo.authenticateUser({ token })
if (!await xo.hasPermissions(user.id, [[id, 'operate']])) {
throw invalidCredentials()
}
const { remoteAddress } = socket
debug('+ Console proxy (%s - %s)', user.name, remoteAddress)
socket.on('close', () => {
debug('- Console proxy (%s - %s)', user.name, remoteAddress)
})
}
const xapi = xo.getXapi(id, ['VM', 'VM-controller'])
const vmConsole = xapi.getVmConsole(id)
// FIXME: lost connection due to VM restart is not detected.
webSocketServer.handleUpgrade(req, socket, head, connection => {
proxyConsole(connection, vmConsole, xapi.sessionId)
})
} catch (error) {
console.error((error && error.stack) || error)
}
})
}
// ===================================================================
const USAGE = (({ name, version }) => `Usage: ${name} [--safe-mode]
${name} v${version}`)(require('../package.json'))
// ===================================================================
export default async function main (args) {
if (includes(args, '--help') || includes(args, '-h')) {
return USAGE
}
{
const debug = createLogger('xo:perf')
blocked(
ms => {
debug('blocked for %sms', ms | 0)
},
{
threshold: 50,
}
)
}
const config = await loadConfiguration()
const webServer = await createWebServer(config.http)
// Now the web server is listening, drop privileges.
try {
const { user, group } = config
if (group) {
process.setgid(group)
debug('Group changed to', group)
}
if (user) {
process.setuid(user)
debug('User changed to', user)
}
} catch (error) {
warn('Failed to change user/group:', error)
}
// Creates main object.
const xo = new Xo(config)
// Register web server close on XO stop.
xo.on('stop', () => pFromCallback(cb => webServer.stop(cb)))
// Connects to all registered servers.
await xo.start()
// Trigger a clean job.
await xo.clean()
// Express is used to manage non WebSocket connections.
const express = createExpressApp()
if (config.http.redirectToHttps) {
let port
forEach(config.http.listen, listen => {
if (listen.port && (listen.cert || listen.certificate)) {
port = listen.port
return false
}
})
if (port === undefined) {
warn('Could not setup HTTPs redirection: no HTTPs port found')
} else {
express.use((req, res, next) => {
if (req.secure) {
return next()
}
res.redirect(`https://${req.hostname}:${port}${req.originalUrl}`)
})
}
}
// Must be set up before the API.
setUpConsoleProxy(webServer, xo)
// Must be set up before the API.
express.use(bind(xo._handleHttpRequest, xo))
// Everything above is not protected by the sign in, allowing xo-cli
// to work properly.
await setUpPassport(express, xo)
// Attaches express to the web server.
webServer.on('request', express)
webServer.on('upgrade', (req, socket, head) => {
express.emit('upgrade', req, socket, head)
})
// Must be set up before the static files.
setUpApi(webServer, xo, config.verboseApiLogsOnErrors)
setUpProxies(express, config.http.proxies, xo)
setUpStaticFiles(express, config.http.mounts)
if (!includes(args, '--safe-mode')) {
await registerPlugins(xo)
}
// Gracefully shutdown on signals.
//
// TODO: implements a timeout? (or maybe it is the services launcher
// responsibility?)
forEach(['SIGINT', 'SIGTERM'], signal => {
let alreadyCalled = false
process.on(signal, () => {
if (alreadyCalled) {
warn('forced exit')
process.exit(1)
}
alreadyCalled = true
debug('%s caught, closing…', signal)
xo.stop()
})
})
await fromEvent(xo, 'stopped')
debug('bye :-)')
}

View File

@@ -0,0 +1,3 @@
process.on('message', ([action, ...args]) => {
console.log(action, args)
})

View File

@@ -0,0 +1,143 @@
import blocked from 'blocked'
import { createLogger } from '@xen-orchestra/log'
import { fromEvent } from 'promise-toolbox'
import { ensureDir, readdir } from 'fs-extra'
import Xo from './xo'
// ===================================================================
const { debug } = createLogger('xo:main')
// ===================================================================
async function registerPlugin (pluginPath, pluginName) {
const plugin = require(pluginPath)
const { description, version = 'unknown' } = (() => {
try {
return require(pluginPath + '/package.json')
} catch (_) {
return {}
}
})()
// Supports both “normal” CommonJS and Babel's ES2015 modules.
const {
default: factory = plugin,
configurationSchema,
configurationPresets,
testSchema,
} = plugin
// The default export can be either a factory or directly a plugin
// instance.
const instance =
typeof factory === 'function'
? factory({
xo: this,
getDataDir: () => {
const dir = `${this._config.datadir}/${pluginName}`
return ensureDir(dir).then(() => dir)
},
})
: factory
await this.registerPlugin(
pluginName,
instance,
configurationSchema,
configurationPresets,
description,
testSchema,
version
)
}
const debugPlugin = createLogger('xo:plugin')
function registerPluginWrapper (pluginPath, pluginName) {
debugPlugin('register %s', pluginName)
return registerPlugin.call(this, pluginPath, pluginName).then(
() => {
debugPlugin(`successfully register ${pluginName}`)
},
error => {
debugPlugin(`failed register ${pluginName}`)
debugPlugin(error)
}
)
}
const PLUGIN_PREFIX = 'xo-server-'
const PLUGIN_PREFIX_LENGTH = PLUGIN_PREFIX.length
async function registerPluginsInPath (path) {
const files = await readdir(path).catch(error => {
if (error.code === 'ENOENT') {
return []
}
throw error
})
await Promise.all(
files.map(name => {
if (name.startsWith(PLUGIN_PREFIX)) {
return registerPluginWrapper.call(
this,
`${path}/${name}`,
name.slice(PLUGIN_PREFIX_LENGTH)
)
}
})
)
}
async function registerPlugins (xo) {
await Promise.all(
[`${__dirname}/../node_modules/`, '/usr/local/lib/node_modules/'].map(
xo::registerPluginsInPath
)
)
}
// ===================================================================
async function main ({ config, safeMode }) {
{
const debug = createLogger('xo:perf')
blocked(ms => {
debug('blocked for %sms', ms | 0)
})
}
// Creates main object.
const xo = new Xo(config)
// Connects to all registered servers.
await xo.start()
// Trigger a clean job.
await xo.clean()
if (!safeMode) {
await registerPlugins(xo)
}
await new Promise(resolve => {
const onMessage = message => {
if (message[0] === 'STOP') {
process.removeListener('message', onMessage)
resolve()
}
}
process.on('message', onMessage)
})
await fromEvent(xo, 'stopped')
}
main().then(
() => process.send(['STOPPED']),
error => process.send(['STOPPED_WITH_ERROR', error])
)

View File

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

View File

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

View File

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

View File

@@ -13,11 +13,9 @@ import {
last,
mapValues,
noop,
some,
sum,
values,
} from 'lodash'
import { fromEvent as pFromEvent, timeout as pTimeout } from 'promise-toolbox'
import { timeout as pTimeout } from 'promise-toolbox'
import Vhd, {
chainVhd,
createSyntheticStream as createVhdReadStream,
@@ -306,7 +304,6 @@ 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
@@ -662,7 +659,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
// - [ ] in case of failure, correctly clean VHDs for all VDIs
// - [ ] fix backup reports
//
// Low:
// - [ ] jobs should be cancelable
@@ -695,7 +692,6 @@ 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,
@@ -740,15 +736,6 @@ 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)
@@ -882,7 +869,9 @@ export default class BackupNg {
logger,
message: 'transfer',
parentId: taskId,
result: () => ({ size: xva.size }),
result: {
size: 0,
},
},
writeStream(fork, handler, dataFilename)
)
@@ -925,7 +914,9 @@ export default class BackupNg {
logger,
message: 'transfer',
parentId: taskId,
result: () => ({ size: xva.size }),
result: {
size: 0,
},
},
xapi._importVm($cancelToken, fork, sr, vm =>
xapi._setObjectProperties(vm, {
@@ -1057,7 +1048,9 @@ export default class BackupNg {
logger,
message: 'merge',
parentId: taskId,
result: size => ({ size }),
result: {
size: 0,
},
},
this._deleteDeltaVmBackups(handler, oldBackups)
)
@@ -1074,7 +1067,9 @@ export default class BackupNg {
logger,
message: 'transfer',
parentId: taskId,
result: size => ({ size }),
result: {
size: 0,
},
},
asyncMap(
fork.vdis,
@@ -1108,11 +1103,10 @@ 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) {
@@ -1150,7 +1144,9 @@ export default class BackupNg {
logger,
message: 'transfer',
parentId: taskId,
result: ({ transferSize }) => ({ size: transferSize }),
result: {
size: 0,
},
},
xapi.importDeltaVm(fork, {
disableStartAfterImport: false, // we'll take care of that
@@ -1189,17 +1185,19 @@ export default class BackupNg {
async _deleteDeltaVmBackups (
handler: RemoteHandler,
backups: MetadataDelta[]
): Promise<number> {
return asyncMap(backups, async backup => {
): Promise<void> {
// TODO: remove VHD as well
await asyncMap(backups, async backup => {
const filename = ((backup._filename: any): string)
await handler.unlink(filename)
return asyncMap(backup.vhds, _ =>
// $FlowFixMe injected $defer param
this._deleteVhd(handler, resolveRelativeFromFile(filename, _))
).then(sum)
}).then(sum)
return Promise.all([
handler.unlink(filename),
asyncMap(backup.vhds, _ =>
// $FlowFixMe injected $defer param
this._deleteVhd(handler, resolveRelativeFromFile(filename, _))
),
])
})
}
async _deleteFullVmBackups (
@@ -1217,50 +1215,35 @@ 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
): Promise<number> {
async _deleteVhd ($defer: any, handler: RemoteHandler, path: string) {
const vhds = await asyncMap(
await handler.list(dirname(path), { filter: isVhd, prependDir: true }),
async 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 vhd = new Vhd(handler, path)
await vhd.readHeaderAndFooter()
return {
footer: vhd.footer,
header: vhd.header,
path,
}
}
)
const base = basename(path)
const child = vhds.find(
_ => _ !== undefined && _.header.parentUnicodeName === base
)
const child = vhds.find(_ => _.header.parentUnicodeName === base)
if (child === undefined) {
await handler.unlink(path)
return 0
return handler.unlink(path)
}
$defer.onFailure.call(handler, 'unlink', path)
const childPath = child.path
const mergedDataSize: number = await this._app.worker.mergeVhd(
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> {
@@ -1345,19 +1328,11 @@ export default class BackupNg {
case 'task.end':
const task = logs[data.taskId]
if (task !== undefined) {
// 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
}
task.status = data.status
task.taskId = data.taskId
task.result = data.result
task.end = time
task.duration = time - task.start
}
}
})

View File

@@ -1,6 +1,6 @@
{
"name": "xo-vmdk-to-vhd",
"version": "0.1.2",
"version": "0.1.0",
"license": "AGPL-3.0",
"description": "JS lib streaming a vmdk file to a vhd",
"keywords": [
@@ -25,10 +25,11 @@
"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.1.0"
"vhd-lib": "^0.0.0"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.19.7",
"version": "5.19.2",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -33,6 +33,7 @@
"@julien-f/freactal": "0.1.0",
"@nraynaud/novnc": "0.6.1",
"@xen-orchestra/cron": "^1.0.3",
"xo-vmdk-to-vhd": "0.1.0",
"ansi_up": "^3.0.0",
"asap": "^2.0.6",
"babel-core": "^6.26.0",
@@ -59,7 +60,6 @@
"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,8 +137,7 @@
"xo-acl-resolver": "^0.2.3",
"xo-common": "^0.1.1",
"xo-lib": "^0.8.0",
"xo-remote-parser": "^0.3",
"xo-vmdk-to-vhd": "^0.1.2"
"xo-remote-parser": "^0.3"
},
"scripts": {
"build": "NODE_ENV=production gulp build",

View File

@@ -1,9 +1,10 @@
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'
@@ -101,16 +102,8 @@ export class FormattedDuration extends Component {
)
render () {
const parsedDuration = this._parseDuration()
return (
<Tooltip
content={getMessage(
every(parsedDuration, n => n === 0)
? 'secondsFormat'
: 'durationFormat',
parsedDuration
)}
>
<Tooltip content={getMessage('durationFormat', this._parseDuration())}>
<span>{this._humanizeDuration()}</span>
</Tooltip>
)

View File

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

View File

@@ -41,7 +41,6 @@ const messages = {
// ----- Copiable component -----
copyToClipboard: 'Copy to clipboard',
copyUuid: 'Copy {uuid}',
// ----- Pills -----
pillMaster: 'Master',
@@ -933,7 +932,6 @@ 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',
@@ -958,7 +956,6 @@ 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:',
@@ -1769,8 +1766,7 @@ const messages = {
xosanUsedSpace: 'Used space',
xosanLicense: 'License',
xosanMultipleLicenses: 'This XOSAN has more than 1 license!',
xosanNeedPack:
'XOSAN pack needs to be installed and up to date on each host of the pool.',
xosanNeedPack: 'XOSAN pack needs to be installed 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',
@@ -1798,14 +1794,6 @@ 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}',
@@ -1822,8 +1810,12 @@ 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',
@@ -1906,7 +1898,6 @@ const messages = {
xosanLoadXoaPlugin: 'Load XOA plugin first',
// ----- Utils -----
secondsFormat: '{seconds, plural, one {# second} other {# seconds}}',
durationFormat:
'{days, plural, =0 {} one {# day } other {# days }}{hours, plural, =0 {} one {# hour } other {# hours }}{minutes, plural, =0 {} one {# minute } other {# minutes }}{seconds, plural, =0 {} one {# second} other {# seconds}}',
}

View File

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

View File

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

View File

@@ -461,15 +461,10 @@ export const exportConfig = () =>
// Server ------------------------------------------------------------
export const addServer = (host, username, password, label, allowUnauthorized) =>
_call('server.add', {
allowUnauthorized,
host,
label,
password,
username,
})::tap(subscribeServers.forceRefresh, () =>
error(_('serverError'), _('serverAddFailed'))
export const addServer = (host, username, password, label) =>
_call('server.add', { host, label, password, username })::tap(
subscribeServers.forceRefresh,
() => error(_('serverError'), _('serverAddFailed'))
)
export const editServer = (server, props) =>
@@ -1196,8 +1191,6 @@ 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
@@ -2419,6 +2412,20 @@ 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
@@ -2427,31 +2434,6 @@ export const registerXosan = () =>
export const fixHostNotInXosanNetwork = (xosanSr, host) =>
_call('xosan.fixHostNotInNetwork', { xosanSr, host })
// XOSAN packs -----------------------------------------------------------------
export const getResourceCatalog = () => _call('cloud.getResourceCatalog')
const downloadAndInstallXosanPack = (pack, pool, { version }) =>
_call('xosan.downloadAndInstallXosanPack', {
id: resolveId(pack),
version,
pool: resolveId(pool),
})
import UpdateXosanPacksModal from './update-xosan-packs-modal' // eslint-disable-line import/first
export const updateXosanPacks = pool =>
confirm({
title: _('xosanUpdatePacks'),
icon: 'host-patch-update',
body: <UpdateXosanPacksModal pool={pool} />,
}).then(pack => {
if (pack === undefined) {
return
}
return downloadAndInstallXosanPack(pack, pool, { version: pack.version })
})
// Licenses --------------------------------------------------------------------
export const getLicenses = productId => _call('xoa.getLicenses', { productId })

View File

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

View File

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

View File

@@ -1,11 +1,10 @@
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 { compareVersions, connectStore } from 'utils'
import { connectStore } from 'utils'
import { Toggle } from 'form'
import {
enableHost,
@@ -18,7 +17,7 @@ import {
import { FormattedRelative, FormattedTime } from 'react-intl'
import { Container, Row, Col } from 'grid'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { forEach, map, noop } from 'lodash'
import { map, noop } from 'lodash'
const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1
@@ -32,9 +31,7 @@ const formatPack = ({ name, author, description, version }, key) => (
</tr>
)
const getPackId = ({ author, name }) => `${author}\0${name}`
@connectStore(() => {
export default connectStore(() => {
const getPgpus = createGetObjectsOfType('PGPU')
.pick((_, { host }) => host.$PGPUs)
.sort()
@@ -47,233 +44,207 @@ const getPackId = ({ author, name }) => `${author}\0${name}`
pcis: getPcis,
pgpus: getPgpus,
}
})
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'
/>
})(({ 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>
)}
{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>
)
}
}
</tbody>
</table>
{!ALLOW_INSTALL_SUPP_PACK && [
<h3>{_('supplementalPackNew')}</h3>,
<Container>
<Upgrade place='supplementalPacks' available={2} />
</Container>,
]}
</Col>
</Row>
</Container>
))

View File

@@ -1,6 +1,5 @@
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'
@@ -285,11 +284,6 @@ const COLUMNS = [
]
const INDIVIDUAL_ACTIONS = [
{
handler: pif => copy(pif.uuid),
icon: 'clipboard',
label: pif => _('copyUuid', { uuid: pif.uuid }),
},
{
handler: deletePif,
icon: 'delete',

View File

@@ -3,7 +3,6 @@ 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'
@@ -281,11 +280,6 @@ class NetworkActions extends Component {
return (
<ButtonGroup>
<ActionRowButton
handler={() => copy(network.uuid)}
icon='clipboard'
tooltip={_('copyUuid', { uuid: network.uuid })}
/>
<ActionRowButton
disabled={disableNetworkDelete}
handler={deleteNetwork}

View File

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

View File

@@ -235,10 +235,9 @@ 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) {

View File

@@ -29,7 +29,6 @@ import {
deleteVgpu,
deleteVm,
editVm,
getVmsHaValues,
isVmRunning,
recoveryStartVm,
restartVm,
@@ -279,8 +278,13 @@ class CoresPerSocket extends Component {
}
}
@connectStore(() => {
export default 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'))
)
@@ -289,394 +293,367 @@ class CoresPerSocket extends Component {
gpuGroup: getGpuGroup,
isAdmin,
vgpus: getVgpus,
vgpuTypes: getVgpuTypes,
}
})
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>
)}
})(({ 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'
/>
<TabButton
btnStyle='danger'
handler={deleteVm}
handler={convertVmToTemplate}
handlerParam={vm}
icon='vm-delete'
labelId='vmRemoveButton'
icon='vm-create-template'
labelId='vmConvertButton'
redirectOnSuccess='/'
/>
</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>
</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 })}
/>
)}
<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>
<select
className='form-control'
onChange={event =>
editVm(vm, { high_availability: getEventValue(event) })
</td>
</tr>
<tr>
<th>{_('vmCpuTopology')}</th>
<td>
<CoresPerSocket container={container} vm={vm} />
</td>
</tr>
<tr>
<th>{_('vmMemoryLimitsLabel')}</th>
<td>
<p>
Static: {formatSize(vm.memory.static[0])}/<Size
value={defined(vm.memory.static[1], null)}
onChange={memoryStaticMax =>
editVm(vm, { memoryStaticMax })
}
/>
</p>
<p>
Dynamic:{' '}
<Size
value={defined(vm.memory.dynamic[0], null)}
onChange={memoryMin => editVm(vm, { memoryMin })}
/>/<Size
value={defined(vm.memory.dynamic[1], null)}
onChange={memoryMax => editVm(vm, { memoryMax })}
/>
</p>
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('guestOsLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('xenToolsStatus')}</th>
<td>
{vm.xenTools && `${vm.xenTools.major}.${vm.xenTools.minor}`}
</td>
</tr>
<tr>
<th>{_('osName')}</th>
<td>
{isEmpty(vm.os_version) ? (
_('unknownOsName')
) : (
<span>
<Icon
className='text-info'
icon={osFamily(vm.os_version.distro)}
/>&nbsp;{vm.os_version.name}
</span>
)}
</td>
</tr>
<tr>
<th>{_('osKernel')}</th>
<td>
{(vm.os_version && vm.os_version.uname) || _('unknownOsKernel')}
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('miscLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('originalTemplate')}</th>
<td>
{vm.other.base_template_name
? vm.other.base_template_name
: _('unknownOriginalTemplate')}
</td>
</tr>
<tr>
<th>{_('resourceSet')}</th>
<td>
{isAdmin ? (
<div className='input-group'>
<SelectResourceSet
onChange={resourceSet =>
editVm(vm, {
resourceSet:
resourceSet != null ? resourceSet.id : resourceSet,
})
}
value={vm.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 })}
value={vm.resourceSet}
/>
/
{vm.power_state === 'Running' ? (
vm.CPUs.max
) : (
<Number
value={vm.CPUs.max}
onChange={cpusStaticMax =>
editVm(vm, { cpusStaticMax })
}
/>
)}
</td>
</tr>
<tr>
<th>{_('vmCpuTopology')}</th>
<td>
<CoresPerSocket container={container} vm={vm} />
</td>
</tr>
<tr>
<th>{_('vmMemoryLimitsLabel')}</th>
<td>
<p>
Static: {formatSize(vm.memory.static[0])}/<Size
value={defined(vm.memory.static[1], null)}
onChange={memoryStaticMax =>
editVm(vm, { memoryStaticMax })
}
/>
</p>
<p>
Dynamic:{' '}
<Size
value={defined(vm.memory.dynamic[0], null)}
onChange={memoryMin => editVm(vm, { memoryMin })}
/>/<Size
value={defined(vm.memory.dynamic[1], null)}
onChange={memoryMax => editVm(vm, { memoryMax })}
/>
</p>
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('guestOsLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('xenToolsStatus')}</th>
<td>
{vm.xenTools
? `${vm.xenTools.major}.${vm.xenTools.minor}`
: _('xenToolsNotInstalled')}
</td>
</tr>
<tr>
<th>{_('osName')}</th>
<td>
{isEmpty(vm.os_version) ? (
_('unknownOsName')
) : (
<span>
<Icon
className='text-info'
icon={osFamily(vm.os_version.distro)}
/>&nbsp;{vm.os_version.name}
</span>
)}
</td>
</tr>
<tr>
<th>{_('osKernel')}</th>
<td>
{(vm.os_version && vm.os_version.uname) ||
_('unknownOsKernel')}
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('miscLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('originalTemplate')}</th>
<td>
{vm.other.base_template_name
? vm.other.base_template_name
: _('unknownOriginalTemplate')}
</td>
</tr>
<tr>
<th>{_('resourceSet')}</th>
<td>
{isAdmin ? (
<div className='input-group'>
<SelectResourceSet
onChange={resourceSet =>
editVm(vm, {
resourceSet:
resourceSet != null
? resourceSet.id
: resourceSet,
})
}
value={vm.resourceSet}
/>
{vm.resourceSet !== undefined && (
<span className='input-group-btn'>
<ActionButton
btnStyle='primary'
handler={shareVmProxy}
handlerParam={vm}
icon='vm-share'
style={SHARE_BUTTON_STYLE}
tooltip={_('vmShareButton')}
/>
</span>
)}
</div>
) : vm.resourceSet !== undefined ? (
<span>
<ResourceSetItem id={vm.resourceSet} />{' '}
{vm.resourceSet !== undefined && (
<span className='input-group-btn'>
<ActionButton
btnStyle='primary'
handler={shareVmProxy}
handlerParam={vm}
icon='vm-share'
size='small'
style={SHARE_BUTTON_STYLE}
tooltip={_('vmShareButton')}
/>
</span>
) : (
_('resourceSetNone')
)}
</td>
</tr>
</tbody>
</table>
</Col>
</Row>
</Container>
)
}
}
</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>
))

View File

@@ -1,7 +1,6 @@
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'
@@ -657,11 +656,6 @@ export default class TabDisks extends Component {
icon: 'vdi-migrate',
label: _('vdiMigrate'),
},
{
handler: vdi => copy(vdi.uuid),
icon: 'clipboard',
label: vdi => _('copyUuid', { uuid: vdi.uuid }),
},
]
render () {

View File

@@ -2,7 +2,6 @@ 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'
@@ -352,11 +351,6 @@ const GROUPED_ACTIONS = [
},
]
const INDIVIDUAL_ACTIONS = [
{
handler: vif => copy(vif.uuid),
icon: 'clipboard',
label: vif => _('copyUuid', { uuid: vif.uuid }),
},
{
disabled: vif => vif.attached,
handler: deleteVif,

View File

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

View File

@@ -10,13 +10,24 @@ 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 } from 'lodash'
import {
every,
filter,
find,
flatten,
forEach,
isEmpty,
map,
mapValues,
some,
} from 'lodash'
import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
import {
addSubscriptions,
connectStore,
cowSet,
formatSize,
isXosanPack,
ShortDate,
} from 'utils'
import {
@@ -26,7 +37,6 @@ import {
subscribePlugins,
subscribeResourceCatalog,
subscribeVolumeInfo,
updateXosanPacks,
} from 'xo'
import NewXosan from './new-xosan'
@@ -198,12 +208,6 @@ 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',
@@ -217,6 +221,14 @@ 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([
@@ -279,6 +291,7 @@ const XOSAN_INDIVIDUAL_ACTIONS = [
isAdmin,
isMasterOfflineByPool: getIsMasterOfflineByPool,
hostsNeedRestartByPool: getHostsNeedRestartByPool,
noPacksByPool,
poolPredicate: getPoolPredicate,
pools: getPools,
xoaRegistration: state => state.xoaRegisterState,
@@ -406,8 +419,8 @@ export default class Xosan extends Component {
const {
hostsNeedRestartByPool,
isAdmin,
noPacksByPool,
poolPredicate,
pools,
xoaRegistration,
xosanSrs,
} = this.props
@@ -443,6 +456,7 @@ export default class Xosan extends Component {
(this._isXosanRegistered() ? (
<NewXosan
hostsNeedRestartByPool={hostsNeedRestartByPool}
noPacksByPool={noPacksByPool}
poolPredicate={poolPredicate}
onSrCreationFinished={this._updateLicenses}
onSrCreationStarted={this._onSrCreationStarted}
@@ -484,7 +498,6 @@ export default class Xosan extends Component {
isAdmin,
licensesByXosan: this._getLicensesByXosan(),
licenseError,
pools,
status: this.state.status,
}}
/>

View File

@@ -29,18 +29,15 @@ import {
} from 'selectors'
import {
addSubscriptions,
isLatestXosanPackInstalled,
compareVersions,
connectStore,
findLatestPack,
formatSize,
mapPlus,
} from 'utils'
import {
computeXosanPossibleOptions,
createXosanSR,
updateXosanPacks,
getResourceCatalog,
downloadAndInstallXosanPack,
restartHostsAgents,
subscribeResourceCatalog,
} from 'xo'
@@ -79,47 +76,14 @@ 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 () {
@@ -279,12 +243,10 @@ export default class NewXosan extends Component {
const {
brickSize,
checkPackError,
customBrickSize,
customIpRange,
ipRange,
memorySize,
needsUpdate,
pif,
pool,
selectedSrs,
@@ -294,7 +256,12 @@ export default class NewXosan extends Component {
vlan,
} = this.state
const { hostsNeedRestartByPool, poolPredicate, notRegistered } = this.props
const {
hostsNeedRestartByPool,
noPacksByPool,
poolPredicate,
notRegistered,
} = this.props
if (notRegistered) {
return (
@@ -329,7 +296,9 @@ export default class NewXosan extends Component {
<Col size={4}>
<SelectPif
disabled={
pool == null || needsUpdate || !isEmpty(hostsNeedRestart)
pool == null ||
noPacksByPool[pool.id] ||
!isEmpty(hostsNeedRestart)
}
onChange={this.linkState('pif')}
predicate={this._getPifPredicate()}
@@ -338,273 +307,261 @@ export default class NewXosan extends Component {
</Col>
</Row>
{pool != null &&
(checkPackError ? (
<em>{_('xosanPackUpdateError')}</em>
) : needsUpdate ? (
noPacksByPool[pool.id] && (
<Row>
<Col>
<Icon icon='error' /> {_('xosanNeedPack')}
<br />
<ActionButton
btnStyle='success'
handler={this._updateXosanPacks}
handlerParam={pool}
icon='export'
>
{_('xosanInstallIt')}
</ActionButton>
</Col>
<Icon icon='error' /> {_('xosanNeedPack')}
<br />
<ActionButton
btnStyle='success'
handler={downloadAndInstallXosanPack}
handlerParam={pool}
icon='export'
>
{_('xosanInstallIt')}
</ActionButton>
</Row>
) : !isEmpty(hostsNeedRestart) ? (
)}
{!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) && [
<Row>
<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>
<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>
<table className='table table-striped'>
<thead>
<tr>
<th />
<th>{_('xosanName')}</th>
<th>{_('xosanHost')}</th>
<th>{_('xosanSize')}</th>
<th>{_('xosanUsedSpace')}</th>
<th>{_('xosanLayout')}</th>
<th>{_('xosanRedundancy')}</th>
<th>{_('xosanCapacity')}</th>
<th>{_('xosanAvailableSpace')}</th>
</tr>
</thead>
<tbody>
{map(lvmsrs, sr => {
const host = find(hosts, ['id', sr.$container])
return (
<tr key={sr.id}>
{map(
suggestions,
(
{ layout, redundancy, capacity, availableSpace },
index
) => (
<tr key={index}>
<td>
<input
checked={selectedSrs[sr.id] || false}
disabled={disableSrCheckbox(sr)}
onChange={event => this._selectSr(event, sr)}
type='checkbox'
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>
<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>
{availableSpace === 0 ? (
<strong className='text-danger'>0</strong>
) : (
formatSize(availableSpace)
)}
</td>
</tr>
)
})}
)}
</tbody>
</table>
</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 />
{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>
)}
</Col>
</Row>,
<Row>
<Col>
<ActionButton
btnStyle='success'
disabled={this._getDisableCreation()}
handler={this._createXosanVm}
icon='add'
>
{_('xosanCreate')}
</ActionButton>
</Col>
</Row>,
]
))}
<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>,
]}
<hr />
</Container>
)

View File

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