Compare commits
1 Commits
xo-server/
...
split-proc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
766175b4a0 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -8,8 +8,6 @@
|
||||
/packages/*/dist/
|
||||
/packages/*/node_modules/
|
||||
|
||||
/@xen-orchestra/log/src/transports/index.js
|
||||
|
||||
/packages/vhd-cli/src/commands/index.js
|
||||
|
||||
/packages/xen-api/plot.dat
|
||||
@@ -24,8 +22,6 @@
|
||||
/packages/xo-web/src/common/intl/locales/index.js
|
||||
/packages/xo-web/src/common/themes/index.js
|
||||
|
||||
/packages/xo-server-rework/src/app/mixins/index.js
|
||||
|
||||
npm-debug.log
|
||||
npm-debug.log.*
|
||||
pnpm-debug.log
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
/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__/
|
||||
@@ -1,49 +0,0 @@
|
||||
# ${pkg.name} [](https://travis-ci.org/${pkg.shortGitHubPath})
|
||||
|
||||
> ${pkg.description}
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/${pkg.name}):
|
||||
|
||||
```
|
||||
> npm install --save ${pkg.name}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
**TODO**
|
||||
|
||||
## 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](${pkg.bugs})
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
${pkg.license} © [${pkg.author.name}](${pkg.author.url})
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/async-fs",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/async-fs",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"promise-toolbox": "^0.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.46",
|
||||
"@babel/core": "7.0.0-beta.46",
|
||||
"@babel/preset-env": "7.0.0-beta.46",
|
||||
"@babel/preset-flow": "7.0.0-beta.46",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import fs from 'fs'
|
||||
import { promisifyAll } from 'promise-toolbox'
|
||||
|
||||
const NOT_PROMISIFIABLE_RE = /^(?:[_A-Z]|exists$)|(?:Async|Stream|Sync)$/
|
||||
|
||||
module.exports = promisifyAll(fs, {
|
||||
mapper: name => !NOT_PROMISIFIABLE_RE.test(name) && name,
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
/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__/
|
||||
@@ -1,49 +0,0 @@
|
||||
# ${pkg.name} [](https://travis-ci.org/${pkg.shortGitHubPath})
|
||||
|
||||
> ${pkg.description}
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/${pkg.name}):
|
||||
|
||||
```
|
||||
> npm install --save ${pkg.name}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
**TODO**
|
||||
|
||||
## 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](${pkg.bugs})
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
${pkg.license} © [${pkg.author.name}](${pkg.author.url})
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/async-map",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/async-map",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"browserslist": [
|
||||
">2%"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.46",
|
||||
"@babel/core": "7.0.0-beta.46",
|
||||
"@babel/preset-env": "7.0.0-beta.46",
|
||||
"@babel/preset-flow": "7.0.0-beta.46",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { map } from 'lodash'
|
||||
|
||||
// Similar to map() + Promise.all() but wait for all promises to
|
||||
// settle before rejecting (with the first error)
|
||||
const asyncMap = <T1, T2>(
|
||||
collection: Array<T1> | Promise<Array<T1>>,
|
||||
iteratee: (value: T1, key: number, collection: Array<T1>) => T2
|
||||
): Promise<Array<T2>> => {
|
||||
if (!Array.isArray(collection)) {
|
||||
return collection.then(collection => asyncMap(collection, iteratee))
|
||||
}
|
||||
|
||||
let errorContainer
|
||||
const onError = error => {
|
||||
if (errorContainer === undefined) {
|
||||
errorContainer = { error }
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
map(collection, (item, key, collection) =>
|
||||
new Promise(resolve => {
|
||||
resolve(iteratee(item, key, collection))
|
||||
}).catch(onError)
|
||||
)
|
||||
).then(values => {
|
||||
if (errorContainer !== undefined) {
|
||||
throw errorContainer.error
|
||||
}
|
||||
return values
|
||||
})
|
||||
}
|
||||
|
||||
export { asyncMap as default }
|
||||
@@ -7,46 +7,34 @@ const NODE_ENV = process.env.NODE_ENV || 'development'
|
||||
const __PROD__ = NODE_ENV === 'production'
|
||||
const __TEST__ = NODE_ENV === 'test'
|
||||
|
||||
const configs = {
|
||||
'@babel/plugin-proposal-decorators': {
|
||||
legacy: true,
|
||||
},
|
||||
'@babel/preset-env' (pkg) {
|
||||
return {
|
||||
debug: !__TEST__,
|
||||
loose: true,
|
||||
shippedProposals: true,
|
||||
targets: __PROD__
|
||||
? (() => {
|
||||
let node = (pkg.engines || {}).node
|
||||
if (node !== undefined) {
|
||||
const trimChars = '^=>~'
|
||||
while (trimChars.includes(node[0])) {
|
||||
node = node.slice(1)
|
||||
}
|
||||
return { node: node }
|
||||
}
|
||||
})()
|
||||
: { browsers: '', node: 'current' },
|
||||
useBuiltIns: '@babel/polyfill' in (pkg.dependencies || {}) && 'usage',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const getConfig = (key, ...args) => {
|
||||
const config = configs[key]
|
||||
return config === undefined ? {} : typeof config === 'function' ? config(...args) : config
|
||||
}
|
||||
|
||||
module.exports = function (pkg, plugins, presets) {
|
||||
plugins === undefined && (plugins = {})
|
||||
|
||||
presets === undefined && (presets = {})
|
||||
presets['@babel/preset-env'] = {
|
||||
debug: !__TEST__,
|
||||
loose: true,
|
||||
shippedProposals: true,
|
||||
targets: __PROD__
|
||||
? (() => {
|
||||
let node = (pkg.engines || {}).node
|
||||
if (node !== undefined) {
|
||||
const trimChars = '^=>~'
|
||||
while (trimChars.includes(node[0])) {
|
||||
node = node.slice(1)
|
||||
}
|
||||
return { node: node }
|
||||
}
|
||||
})()
|
||||
: { browsers: '', node: 'current' },
|
||||
useBuiltIns: '@babel/polyfill' in (pkg.dependencies || {}) && 'usage',
|
||||
}
|
||||
|
||||
Object.keys(pkg.devDependencies || {}).forEach(name => {
|
||||
if (!(name in presets) && PLUGINS_RE.test(name)) {
|
||||
plugins[name] = getConfig(name, pkg)
|
||||
plugins[name] = {}
|
||||
} else if (!(name in presets) && PRESETS_RE.test(name)) {
|
||||
presets[name] = getConfig(name, pkg)
|
||||
presets[name] = {}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -41,10 +41,10 @@
|
||||
"moment-timezone": "^0.5.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.46",
|
||||
"@babel/core": "7.0.0-beta.46",
|
||||
"@babel/preset-env": "7.0.0-beta.46",
|
||||
"@babel/preset-flow": "7.0.0-beta.46",
|
||||
"@babel/cli": "7.0.0-beta.44",
|
||||
"@babel/core": "7.0.0-beta.44",
|
||||
"@babel/preset-env": "7.0.0-beta.44",
|
||||
"@babel/preset-flow": "7.0.0-beta.44",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
},
|
||||
@@ -53,7 +53,7 @@
|
||||
"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",
|
||||
"predev": "yarn run clean",
|
||||
"prepublishOnly": "yarn run build"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
/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__/
|
||||
@@ -1,49 +0,0 @@
|
||||
# ${pkg.name} [](https://travis-ci.org/${pkg.shortGitHubPath})
|
||||
|
||||
> ${pkg.description}
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/${pkg.name}):
|
||||
|
||||
```
|
||||
> npm install --save ${pkg.name}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
**TODO**
|
||||
|
||||
## 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](${pkg.bugs})
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
${pkg.license} © [${pkg.author.name}](${pkg.author.url})
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/defined",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/defined",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/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": {},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.46",
|
||||
"@babel/core": "7.0.0-beta.46",
|
||||
"@babel/preset-env": "7.0.0-beta.46",
|
||||
"@babel/preset-flow": "7.0.0-beta.46",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
// @flow
|
||||
|
||||
// Usage:
|
||||
//
|
||||
// ```js
|
||||
// const httpProxy = defined(
|
||||
// process.env.HTTP_PROXY,
|
||||
// process.env.http_proxy
|
||||
// )
|
||||
//
|
||||
// const httpProxy = defined([
|
||||
// process.env.HTTP_PROXY,
|
||||
// process.env.http_proxy
|
||||
// ])
|
||||
// ```
|
||||
export default function defined () {
|
||||
let args = arguments
|
||||
let n = args.length
|
||||
if (n === 1) {
|
||||
args = arguments[0]
|
||||
n = args.length
|
||||
}
|
||||
|
||||
for (let i = 0; i < n; ++i) {
|
||||
let arg = arguments[i]
|
||||
if (typeof arg === 'function') {
|
||||
arg = get(arg)
|
||||
}
|
||||
if (arg !== undefined) {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
//
|
||||
// ```js
|
||||
// const friendName = get(() => props.user.friends[0].name)
|
||||
//
|
||||
// // this form can be used to avoid recreating functions:
|
||||
// const getFriendName = _ => _.friends[0].name
|
||||
// const friendName = get(getFriendName, props.user)
|
||||
// ```
|
||||
export const get = (accessor: (input: ?any) => any, arg: ?any) => {
|
||||
try {
|
||||
return accessor(arg)
|
||||
} catch (error) {
|
||||
if (!(error instanceof TypeError)) { // avoid hidding other errors
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
//
|
||||
// ```js
|
||||
// const httpAgent = ifDef(
|
||||
// process.env.HTTP_PROXY,
|
||||
// _ => new ProxyAgent(_)
|
||||
// )
|
||||
// ```
|
||||
export const ifDef = (value: ?any, thenFn: (value: any) => any) =>
|
||||
value !== undefined
|
||||
? thenFn(value)
|
||||
: value
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
/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__/
|
||||
@@ -1,49 +0,0 @@
|
||||
# ${pkg.name} [](https://travis-ci.org/${pkg.shortGitHubPath})
|
||||
|
||||
> ${pkg.description}
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/${pkg.name}):
|
||||
|
||||
```
|
||||
> npm install --save ${pkg.name}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
**TODO**
|
||||
|
||||
## 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](${pkg.bugs})
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
${pkg.license} © [${pkg.author.name}](${pkg.author.url})
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/emit-async",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/emit-async",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/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": {},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.46",
|
||||
"@babel/core": "7.0.0-beta.46",
|
||||
"@babel/preset-env": "7.0.0-beta.46",
|
||||
"@babel/preset-flow": "7.0.0-beta.46",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
export default function emitAsync (event) {
|
||||
let opts
|
||||
let i = 1
|
||||
|
||||
// an option object has been passed as first param
|
||||
if (typeof event !== 'string') {
|
||||
opts = event
|
||||
event = arguments[i++]
|
||||
}
|
||||
|
||||
const n = arguments.length - i
|
||||
const args = new Array(n)
|
||||
for (let j = 0; j < n; ++j) {
|
||||
args[j] = arguments[j + i]
|
||||
}
|
||||
|
||||
const onError = opts != null && opts.onError
|
||||
|
||||
return Promise.all(this.listeners(event).map(
|
||||
listener => new Promise(resolve => {
|
||||
resolve(listener.apply(this, args))
|
||||
}).catch(onError)
|
||||
))
|
||||
}
|
||||
@@ -15,7 +15,7 @@ Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/log):
|
||||
Everywhere something should be logged:
|
||||
|
||||
```js
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
|
||||
const log = createLogger('xo-server-api')
|
||||
log.warn('foo')
|
||||
@@ -24,7 +24,9 @@ log.warn('foo')
|
||||
Then at application level you can choose how to handle these logs:
|
||||
|
||||
```js
|
||||
import { configure, transports } from '@xen-orchestra/log'
|
||||
import configure from '@xen-orchestra/log/configure'
|
||||
import createConsoleTransport from '@xen-orchestra/log/transports/console'
|
||||
import createEmailTransport from '@xen-orchestra/log/transports/email'
|
||||
|
||||
configure([
|
||||
{
|
||||
@@ -33,13 +35,13 @@ configure([
|
||||
// matched against the namespace of the logs
|
||||
filter: process.env.DEBUG,
|
||||
|
||||
transport: transports.console()
|
||||
transport: createConsoleTransport()
|
||||
},
|
||||
{
|
||||
// only levels >= warn
|
||||
level: 'warn',
|
||||
|
||||
transport: transports.email({
|
||||
transport: createEmaileTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: 'jane.smith@gmail.com',
|
||||
@@ -60,7 +62,9 @@ configure([
|
||||
#### Console
|
||||
|
||||
```js
|
||||
configure(transports.console())
|
||||
import createConsoleTransport from '@xen-orchestra/log/transports/console'
|
||||
|
||||
configure(createConsoleTransport())
|
||||
```
|
||||
|
||||
#### Email
|
||||
@@ -74,7 +78,9 @@ Optional dependency:
|
||||
Configuration:
|
||||
|
||||
```js
|
||||
configure(transports.email({
|
||||
import createEmailTransport from '@xen-orchestra/log/transports/email'
|
||||
|
||||
configure(createEmailTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: 'jane.smith@gmail.com',
|
||||
@@ -99,11 +105,13 @@ Optional dependency:
|
||||
Configuration:
|
||||
|
||||
```js
|
||||
import createSyslogTransport from '@xen-orchestra/log/transports/syslog'
|
||||
|
||||
// By default, log to udp://localhost:514
|
||||
configure(transports.syslog())
|
||||
configure(createSyslogTransport())
|
||||
|
||||
// But TCP, a different host, or a different port can be used
|
||||
configure(transports.syslog('tcp://syslog.company.lan'))
|
||||
configure(createSyslogTransport('tcp://syslog.company.lan'))
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
dist/configure.js
|
||||
1
@xen-orchestra/log/configure.js
Normal file
1
@xen-orchestra/log/configure.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('./dist/configure')
|
||||
@@ -28,24 +28,23 @@
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "7.0.0-beta.46",
|
||||
"@babel/polyfill": "7.0.0-beta.42",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.46",
|
||||
"@babel/core": "7.0.0-beta.46",
|
||||
"@babel/preset-env": "7.0.0-beta.46",
|
||||
"@babel/preset-flow": "7.0.0-beta.46",
|
||||
"@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",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^2.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "index-modules --cjs-lazy src/transports && cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "index-modules --cjs-lazy src/transports && cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"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"
|
||||
|
||||
@@ -55,6 +55,12 @@ let transport = createTransport({
|
||||
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)
|
||||
}
|
||||
@@ -69,8 +75,12 @@ export const catchGlobalErrors = logger => {
|
||||
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')
|
||||
@@ -86,6 +96,7 @@ export const catchGlobalErrors = logger => {
|
||||
return () => {
|
||||
process.removeListener('uncaughtException', onUncaughtException)
|
||||
process.removeListener('unhandledRejection', onUnhandledRejection)
|
||||
process.removeListener('warning', onWarning)
|
||||
|
||||
if (prototype.emit === patchedEmit) {
|
||||
prototype.emit = emit
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import createTransport from './transports/console'
|
||||
import LEVELS from './levels'
|
||||
|
||||
const symbol = typeof Symbol !== 'undefined' ? Symbol.for('@xen-orchestra/log') : '@@@xen-orchestra/log'
|
||||
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 > LEVEL.WARN && transport(log)
|
||||
global[symbol] = log => log.level > LEVELS.WARN && transport(log)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export default () => {
|
||||
const memoryLogger = log => {
|
||||
logs.push(log)
|
||||
}
|
||||
const logs = (memoryLogger.logs = [])
|
||||
return memoryLogger
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export default target => {
|
||||
opts.transport = Transport.Ucp
|
||||
}
|
||||
|
||||
({ host: target, port: opts.port } = splitHost(target))
|
||||
;({ host: target, port: opts.port } = splitHost(target))
|
||||
}
|
||||
|
||||
const client = createClient(target, opts)
|
||||
|
||||
@@ -5,9 +5,7 @@ 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]
|
||||
typeof data === 'function' ? (_, key) => data(key) : (_, key) => data[key]
|
||||
|
||||
return tpl.replace(TPL_RE, getData)
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
/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__/
|
||||
@@ -1,49 +0,0 @@
|
||||
# ${pkg.name} [](https://travis-ci.org/${pkg.shortGitHubPath})
|
||||
|
||||
> ${pkg.description}
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/${pkg.name}):
|
||||
|
||||
```
|
||||
> npm install --save ${pkg.name}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
**TODO**
|
||||
|
||||
## 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](${pkg.bugs})
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
${pkg.license} © [${pkg.author.name}](${pkg.author.url})
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/mixin",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/mixin",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/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": {
|
||||
"bind-property-descriptor": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.46",
|
||||
"@babel/core": "7.0.0-beta.46",
|
||||
"@babel/preset-env": "7.0.0-beta.46",
|
||||
"@babel/preset-flow": "7.0.0-beta.46",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const { defineProperties, getOwnPropertyDescriptor } = Object
|
||||
|
||||
const isIgnoredProperty = name => name[0] === '_' || name === 'constructor'
|
||||
|
||||
const IGNORED_STATIC_PROPERTIES = {
|
||||
__proto__: null,
|
||||
|
||||
arguments: true,
|
||||
caller: true,
|
||||
length: true,
|
||||
name: true,
|
||||
prototype: true,
|
||||
}
|
||||
const isIgnoredStaticProperty = name => name in IGNORED_STATIC_PROPERTIES
|
||||
|
||||
const ownKeys =
|
||||
(typeof Reflect !== 'undefined' && Reflect.ownKeys) ||
|
||||
(({ getOwnPropertyNames: names, getOwnPropertySymbols: symbols }) =>
|
||||
symbols !== undefined ? obj => names(obj).concat(symbols(obj)) : names)(Object)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const mixin = Mixins => Class => {
|
||||
if (__DEV__ && !Array.isArray(Mixins)) {
|
||||
throw new TypeError('Mixins should be an array')
|
||||
}
|
||||
|
||||
const { name } = Class
|
||||
|
||||
// Copy properties of plain object mix-ins to the prototype.
|
||||
{
|
||||
const allMixins = Mixins
|
||||
Mixins = []
|
||||
const { prototype } = Class
|
||||
const descriptors = { __proto__: null }
|
||||
allMixins.forEach(Mixin => {
|
||||
if (typeof Mixin === 'function') {
|
||||
Mixins.push(Mixin)
|
||||
return
|
||||
}
|
||||
|
||||
for (const prop of ownKeys(Mixin)) {
|
||||
if (__DEV__ && prop in prototype) {
|
||||
throw new Error(`${name}#${prop} is already defined`)
|
||||
}
|
||||
|
||||
;(descriptors[prop] = getOwnPropertyDescriptor(
|
||||
Mixin,
|
||||
prop
|
||||
)).enumerable = false // Object methods are enumerable but class methods are not.
|
||||
}
|
||||
})
|
||||
defineProperties(prototype, descriptors)
|
||||
}
|
||||
|
||||
const n = Mixins.length
|
||||
|
||||
function DecoratedClass (...args) {
|
||||
const instance = new Class(...args)
|
||||
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const Mixin = Mixins[i]
|
||||
const { prototype } = Mixin
|
||||
const mixinInstance = new Mixin(instance, ...args)
|
||||
const descriptors = { __proto__: null }
|
||||
const props = ownKeys(prototype)
|
||||
for (let j = 0, m = props.length; j < m; ++j) {
|
||||
const prop = props[j]
|
||||
|
||||
if (isIgnoredProperty(prop)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (prop in instance) {
|
||||
throw new Error(`${name}#${prop} is already defined`)
|
||||
}
|
||||
|
||||
descriptors[prop] = getBoundPropertyDescriptor(
|
||||
prototype,
|
||||
prop,
|
||||
mixinInstance
|
||||
)
|
||||
}
|
||||
defineProperties(instance, descriptors)
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
// Copy original and mixed-in static properties on Decorator class.
|
||||
const descriptors = { __proto__: null }
|
||||
ownKeys(Class).forEach(prop => {
|
||||
let descriptor
|
||||
if (!(
|
||||
// Special properties are not defined...
|
||||
isIgnoredStaticProperty(prop) &&
|
||||
|
||||
// if they already exist...
|
||||
(descriptor = getOwnPropertyDescriptor(DecoratedClass, prop)) !== undefined &&
|
||||
|
||||
// and are not configurable.
|
||||
!descriptor.configurable
|
||||
)) {
|
||||
descriptors[prop] = getOwnPropertyDescriptor(Class, prop)
|
||||
}
|
||||
})
|
||||
Mixins.forEach(Mixin => {
|
||||
ownKeys(Mixin).forEach(prop => {
|
||||
if (isIgnoredStaticProperty(prop)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (__DEV__ && prop in descriptors) {
|
||||
throw new Error(`${name}.${prop} is already defined`)
|
||||
}
|
||||
|
||||
descriptors[prop] = getOwnPropertyDescriptor(Mixin, prop)
|
||||
})
|
||||
})
|
||||
defineProperties(DecoratedClass, descriptors)
|
||||
|
||||
return DecoratedClass
|
||||
}
|
||||
export { mixin as default }
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@babel/register": "^7.0.0-beta.46",
|
||||
"@babel/register": "^7.0.0-beta.44",
|
||||
"babel-7-jest": "^21.3.2",
|
||||
"babel-eslint": "^8.1.2",
|
||||
"benchmark": "^2.1.4",
|
||||
@@ -40,7 +40,6 @@
|
||||
"transform": {
|
||||
"/@xen-orchestra/cron/.+\\.jsx?$": "babel-7-jest",
|
||||
"/@xen-orchestra/fs/.+\\.jsx?$": "babel-7-jest",
|
||||
"/@xen-orchestra/log/.+\\.jsx?$": "babel-7-jest",
|
||||
"/packages/complex-matcher/.+\\.jsx?$": "babel-7-jest",
|
||||
"/packages/value-matcher/.+\\.jsx?$": "babel-7-jest",
|
||||
"/packages/vhd-lib/.+\\.jsx?$": "babel-7-jest",
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
"lodash": "^4.17.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.46",
|
||||
"@babel/core": "7.0.0-beta.46",
|
||||
"@babel/preset-env": "7.0.0-beta.46",
|
||||
"@babel/cli": "7.0.0-beta.44",
|
||||
"@babel/core": "7.0.0-beta.44",
|
||||
"@babel/preset-env": "7.0.0-beta.44",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.1",
|
||||
"rimraf": "^2.6.2"
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.46",
|
||||
"@babel/core": "7.0.0-beta.46",
|
||||
"@babel/preset-env": "7.0.0-beta.46",
|
||||
"@babel/preset-flow": "7.0.0-beta.46",
|
||||
"@babel/cli": "7.0.0-beta.44",
|
||||
"@babel/core": "7.0.0-beta.44",
|
||||
"@babel/preset-env": "7.0.0-beta.44",
|
||||
"@babel/preset-flow": "7.0.0-beta.44",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "7.0.0-beta.46",
|
||||
"@babel/polyfill": "7.0.0-beta.44",
|
||||
"bluebird": "^3.5.1",
|
||||
"chalk": "^2.2.0",
|
||||
"event-to-promise": "^0.8.0",
|
||||
@@ -49,10 +49,10 @@
|
||||
"xo-lib": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.46",
|
||||
"@babel/core": "7.0.0-beta.46",
|
||||
"@babel/preset-env": "7.0.0-beta.46",
|
||||
"@babel/preset-flow": "7.0.0-beta.46",
|
||||
"@babel/cli": "7.0.0-beta.44",
|
||||
"@babel/core": "7.0.0-beta.44",
|
||||
"@babel/preset-env": "7.0.0-beta.44",
|
||||
"@babel/preset-flow": "7.0.0-beta.44",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
"lodash": "^4.17.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.46",
|
||||
"@babel/core": "7.0.0-beta.46",
|
||||
"@babel/preset-env": "7.0.0-beta.46",
|
||||
"@babel/preset-flow": "^7.0.0-beta.46",
|
||||
"@babel/cli": "7.0.0-beta.44",
|
||||
"@babel/core": "7.0.0-beta.44",
|
||||
"@babel/preset-env": "7.0.0-beta.44",
|
||||
"@babel/preset-flow": "^7.0.0-beta.44",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
/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__/
|
||||
@@ -1,51 +0,0 @@
|
||||
# xo-server [](https://travis-ci.org/vatesfr/xo-server)
|
||||
|
||||
> Server part of [Xen Orchestra](https://xen-orchestra.com)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/xo-server):
|
||||
|
||||
```
|
||||
> npm install --global xo-server
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
> xo-server
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
AGPL3 © [Vates SAS](http://vates.fr)
|
||||
@@ -1,40 +0,0 @@
|
||||
// Vendor config: DO NOT TOUCH!
|
||||
//
|
||||
// See sample.config.yaml to override.
|
||||
{
|
||||
|
||||
// Should users be created on first sign in?
|
||||
//
|
||||
// Necessary for external authentication providers.
|
||||
"createUserOnFirstSignin": true,
|
||||
|
||||
"datadir": "/var/lib/xo-server/data",
|
||||
|
||||
"http": {
|
||||
"listen": [
|
||||
{
|
||||
"port": 80
|
||||
}
|
||||
],
|
||||
|
||||
"mounts": {},
|
||||
|
||||
// Ciphers to use.
|
||||
//
|
||||
// These are the default ciphers in Node 4.2.6, we are setting
|
||||
// them explicitly for older Node versions.
|
||||
"ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA",
|
||||
|
||||
// Tell Node to respect the cipher order.
|
||||
"honorCipherOrder": true,
|
||||
|
||||
// Specify to use at least TLSv1.1.
|
||||
// See: https://github.com/certsimple/minimum-tls-version
|
||||
"secureOptions": 117440512
|
||||
},
|
||||
|
||||
"jwt": {
|
||||
"expiresIn": "7d",
|
||||
"secret": "P],7x#cRhuy,wCR'$}'N?<2yOQ3v6.!b*|1B2P36(wKsYICH|6"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,81 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server-rework",
|
||||
"version": "0.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen Orchestra",
|
||||
"keywords": [
|
||||
"orchestra",
|
||||
"server",
|
||||
"xen",
|
||||
"xen-orchestra"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-rework",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": true,
|
||||
"main": "dist/",
|
||||
"bin": {
|
||||
"xo-server": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "7.0.0-beta.46",
|
||||
"app-conf": "^0.5.0",
|
||||
"base64url": "^2.0.0",
|
||||
"bind-property-descriptor": "^1.0.0",
|
||||
"bluebird": "^3.5.1",
|
||||
"cuid": "^2.0.2",
|
||||
"dataloader": "^1.3.0",
|
||||
"event-to-promise": "^0.8.0",
|
||||
"golike-defer": "^0.4.1",
|
||||
"graphql": "^0.13.0",
|
||||
"http-request-plus": "^0.5.0",
|
||||
"http-server-plus": "^0.9.0",
|
||||
"immutable": "^4.0.0-rc.4",
|
||||
"index-modules": "^0.3.0",
|
||||
"jsonwebtoken": "^8.1.0",
|
||||
"lodash": "^4.17.4",
|
||||
"mnemonist": "^0.21.0",
|
||||
"promise-toolbox": "^0.9.5",
|
||||
"proxy-agent": "^2.1.0",
|
||||
"spdy": "^3.4.7",
|
||||
"uuid": "^3.1.0",
|
||||
"zen-observable": "^0.8.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.46",
|
||||
"@babel/core": "7.0.0-beta.46",
|
||||
"@babel/plugin-proposal-decorators": "7.0.0-beta.46",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.0.0-beta.46",
|
||||
"@babel/plugin-proposal-pipeline-operator": "7.0.0-beta.46",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.0.0-beta.46",
|
||||
"@babel/preset-env": "7.0.0-beta.46",
|
||||
"@babel/preset-flow": "7.0.0-beta.46",
|
||||
"babel-plugin-dev": "^1.0.0",
|
||||
"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 && index-modules --auto src/",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"start": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import EventEmitter from 'events'
|
||||
import mixin from '@xen-orchestra/mixin'
|
||||
import { values } from 'lodash'
|
||||
|
||||
import Mixins from './mixins'
|
||||
|
||||
@mixin(values(Mixins))
|
||||
export default class App extends EventEmitter {}
|
||||
@@ -1,51 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import emitAsync from '@xen-orchestra/emit-async'
|
||||
|
||||
const { debug, warn } = createLogger('hooks')
|
||||
|
||||
const makeSingletonHook = (hook, postEvent) => {
|
||||
let promise
|
||||
return function () {
|
||||
if (promise === undefined) {
|
||||
promise = runHook(this, hook)
|
||||
promise.then(() => {
|
||||
this.removeAllListeners(hook)
|
||||
this.emit(postEvent)
|
||||
this.removeAllListeners(postEvent)
|
||||
})
|
||||
}
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
const runHook = (app, hook) => {
|
||||
debug(`${hook} start…`)
|
||||
const promise = emitAsync.call(app, {
|
||||
onError: error => warn(`${hook} failure`, error),
|
||||
}, hook)
|
||||
promise.then(() => {
|
||||
debug(`${hook} finished`)
|
||||
})
|
||||
return promise
|
||||
}
|
||||
|
||||
export default {
|
||||
// Run *clean* async listeners.
|
||||
//
|
||||
// They normalize existing data, clear invalid entries, etc.
|
||||
clean () {
|
||||
return runHook(this, 'clean')
|
||||
},
|
||||
|
||||
// Run *start* async listeners.
|
||||
//
|
||||
// They initialize the application.
|
||||
start: makeSingletonHook('start', 'started'),
|
||||
|
||||
// Run *stop* async listeners.
|
||||
//
|
||||
// They close connections, unmount file systems, save states, etc.
|
||||
stop: makeSingletonHook('stop', 'stopped'),
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import defined, { ifDef } from '@xen-orchestra/defined'
|
||||
import hrp from 'http-request-plus'
|
||||
import ProxyAgent from 'proxy-agent'
|
||||
|
||||
export default class HttpRequest {
|
||||
constructor (_, {
|
||||
httpProxy = defined(process.env.http_proxy, process.env.HTTP_PROXY),
|
||||
}) {
|
||||
this.httpRequest = hrp.extend({
|
||||
agent: ifDef(httpProxy, _ => new ProxyAgent(_)),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import type EventEmitter from 'events'
|
||||
import type {
|
||||
IncomingMessage,
|
||||
Server,
|
||||
ServerResponse,
|
||||
} from 'http'
|
||||
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import generateToken from 'generate-token'
|
||||
import { fromCallback } from 'promise-toolbox'
|
||||
import { once } from 'lodash'
|
||||
|
||||
const HTTP_REQUEST_VALIDITY = 1e3 * 60 * 60
|
||||
const { warn } = createLogger('web-server')
|
||||
|
||||
type Handler = (IncomingMessage, ServerResponse, any) => void
|
||||
type HandlerInfo = {|
|
||||
data: any,
|
||||
handler: Handler,
|
||||
method: ?string,
|
||||
once: boolean,
|
||||
path: string,
|
||||
unregister: () => void
|
||||
|}
|
||||
|
||||
type HandlerInfoMap = { [url: string]: HandlerInfo }
|
||||
|
||||
export default class httpServer {
|
||||
_handlers: HandlerInfoMap
|
||||
|
||||
constructor (app: EventEmitter, { httpServer }: { httpServer: Server }) {
|
||||
const openConnections = new Set()
|
||||
httpServer.on('connection', connection => {
|
||||
openConnections.add(connection)
|
||||
connection.once('close', () => {
|
||||
openConnections.delete(connection)
|
||||
})
|
||||
})
|
||||
app.on('stop', () => {
|
||||
const timeout = setTimeout(() => {
|
||||
openConnections.forEach(connection => {
|
||||
connection.end()
|
||||
})
|
||||
}, 5e3).unref()
|
||||
|
||||
return fromCallback(cb => httpServer.close(cb)).then(() => {
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
})
|
||||
|
||||
const handlers = this._handlers = Object.create(null)
|
||||
httpServer.on('request', (req, res) => {
|
||||
const handler = handlers[req.url]
|
||||
if (
|
||||
handler !== undefined &&
|
||||
(handler.method !== undefined || handler.method === req.method)
|
||||
) {
|
||||
if (handler.once) {
|
||||
handler.unregister()
|
||||
}
|
||||
|
||||
try {
|
||||
handler.handler.call(app, req, res, handler.data)
|
||||
} catch (error) {
|
||||
warn('handler error', { error })
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500)
|
||||
}
|
||||
res.end()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
res.writeHead(404)
|
||||
res.end(`Page not found: ${req.url}`)
|
||||
})
|
||||
}
|
||||
|
||||
registerHttpHandler (
|
||||
handler: Handler,
|
||||
{ data, method, once: once_ = false, path, ttl }
|
||||
) {
|
||||
const handlers = this._handlers
|
||||
|
||||
if (path in handlers) {
|
||||
throw new Error(`there is already an HTTP handler for ${path}`)
|
||||
}
|
||||
|
||||
const unregister = once(() => {
|
||||
delete handlers[path]
|
||||
})
|
||||
|
||||
handlers[path] = {
|
||||
data,
|
||||
handler,
|
||||
method: method && method.toUpperCase(),
|
||||
once: once_,
|
||||
path,
|
||||
unregister,
|
||||
}
|
||||
|
||||
if (ttl !== undefined) {
|
||||
setTimeout(unregister, ttl)
|
||||
}
|
||||
|
||||
return unregister
|
||||
}
|
||||
|
||||
registerHttpRequest (
|
||||
handler: Handler,
|
||||
{ data, path, method = 'GET', suffix }
|
||||
) {
|
||||
return generateToken().then(token => {
|
||||
let path = `/${token}`
|
||||
if (suffix) {
|
||||
path += `/${encodeURI(token)}`
|
||||
}
|
||||
|
||||
this.registerHttpHandler(handler, {
|
||||
data,
|
||||
method,
|
||||
once: true,
|
||||
path,
|
||||
ttl: HTTP_REQUEST_VALIDITY,
|
||||
})
|
||||
|
||||
return path
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { fromCallback } from 'promise-toolbox'
|
||||
|
||||
export default class JsonWebToken {
|
||||
_encodeOpts: Object
|
||||
_secret: string
|
||||
|
||||
constructor (_: any, {
|
||||
config: { jwt: { expiresIn, secret } },
|
||||
}: {
|
||||
config: { jwt: { expiresIn?: string, secret: string } }
|
||||
}) {
|
||||
this._encodeOpts = { expiresIn }
|
||||
this._secret = secret
|
||||
}
|
||||
|
||||
decodeJwt (token: string): Promise<any> {
|
||||
return fromCallback(cb => jwt.verify(token, this._secret, cb))
|
||||
}
|
||||
|
||||
encodeJwt (payload: any): Promise<string> {
|
||||
return fromCallback(cb =>
|
||||
jwt.sign(payload, this._secret, this._encodeOpts, cb)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import { forEach, startsWith } from 'lodash'
|
||||
import { join } from 'path'
|
||||
import { readdir } from '@xen-orchestra/async-fs'
|
||||
|
||||
const { info, warn } = createLogger('plugins')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const LOOKUP_PATHS = [
|
||||
'/usr/local/lib/node_modules',
|
||||
join(__dirname, '../../../node_modules'),
|
||||
]
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export default class Plugins {
|
||||
_plugins: Object
|
||||
_prefix: string
|
||||
|
||||
constructor (app: any, { appName, safeMode }: { appName: string, safeMode: ?boolean }) {
|
||||
this._plugins = {}
|
||||
this._prefix = `${appName}-`
|
||||
|
||||
app.on('start', () => {
|
||||
if (!safeMode) {
|
||||
return this.discoverPlugins()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
discoverPlugins () {
|
||||
return asyncMap(this._listPlugins(), async (plugin, name) => {})
|
||||
}
|
||||
|
||||
_listPlugins () {
|
||||
const plugins = { __proto__: null }
|
||||
|
||||
const prefix = this._prefix
|
||||
const prefixLength = prefix.length
|
||||
|
||||
return asyncMap(LOOKUP_PATHS, lookupPath =>
|
||||
readdir(lookupPath).then(
|
||||
basenames =>
|
||||
forEach(basenames, basename => {
|
||||
if (startsWith(basename, prefix)) {
|
||||
const name = basename.slice(prefixLength)
|
||||
const path = join(lookupPath, basename)
|
||||
|
||||
const previous = plugins[name]
|
||||
if (name in plugins) {
|
||||
warn(`duplicate plugins ${name}`, {
|
||||
name,
|
||||
paths: [previous.path, path].sort(),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let plugin
|
||||
try {
|
||||
plugin = require(path)
|
||||
info(`successfully imported plugin ${name}`, {
|
||||
name,
|
||||
path,
|
||||
})
|
||||
} catch (error) {
|
||||
warn(`failed to import plugin ${name}`, {
|
||||
error,
|
||||
name,
|
||||
path,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let version
|
||||
try {
|
||||
;({ version } = require(join(path, 'package.json')))
|
||||
} catch (_) {}
|
||||
|
||||
// Supports both “normal” CommonJS and Babel's ES2015 modules.
|
||||
const {
|
||||
default: factory = plugin,
|
||||
configurationSchema,
|
||||
configurationPresets,
|
||||
} = plugin
|
||||
|
||||
plugins[name] = {
|
||||
configurationPresets,
|
||||
configurationSchema,
|
||||
factory,
|
||||
version,
|
||||
}
|
||||
}
|
||||
}),
|
||||
error => {
|
||||
if (error.code !== 'ENOENT') {
|
||||
warn('plugins', 'failed to read directory', {
|
||||
error,
|
||||
lookupPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// import generateId from 'cuid'
|
||||
// import mkdirp from 'simple-mkdirp'
|
||||
// import { pCatch, pTap } from 'promise-toolbox'
|
||||
// import { join } from 'path'
|
||||
// import { readFile, writeFile } from '@xen-orchestra/async-fs'
|
||||
|
||||
// TODO: transition from in-memory database to a real database system.
|
||||
export default class Store {
|
||||
// constructor (app, { config: { datadir } }) {
|
||||
// this._get = () => {
|
||||
// const datafile = join(datadir, 'store.json')
|
||||
//
|
||||
// const promise = readFile(datafile)
|
||||
// .then(JSON.parse)
|
||||
// .catch(pCatch({ code: 'ENOENT' }, () => ({})))
|
||||
// .then(pTap(data => {
|
||||
// app.on('stop', data =>
|
||||
// mkdirp(datadir)
|
||||
// .then(() => writeFile(datafile, JSON.stringify(data)))
|
||||
// )
|
||||
// }))
|
||||
//
|
||||
// // Inline future accesses.
|
||||
// this._get = () => promise
|
||||
//
|
||||
// return promise
|
||||
// }
|
||||
//
|
||||
// this._types = {}
|
||||
// }
|
||||
//
|
||||
// registerType (name, spec) {
|
||||
// const types = this._types
|
||||
//
|
||||
// if (__DEV__ && name in types) {
|
||||
// throw new Error(`type ${name} is already registered`)
|
||||
// }
|
||||
//
|
||||
// types[name] = spec
|
||||
// }
|
||||
//
|
||||
// async createObject ({ type, ...props }) {
|
||||
// if (__DEV__ && !type) {
|
||||
// throw new Error('missing type')
|
||||
// }
|
||||
//
|
||||
// const db = await this._get()
|
||||
// const byType = db.byType || (db.byType = {})
|
||||
// const collection = byType[type] || (byType[type] = {})
|
||||
//
|
||||
// let { id } = props
|
||||
// if (!id) {
|
||||
// props.id = id = generateId()
|
||||
// }
|
||||
//
|
||||
// collection[id] = props
|
||||
// }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export default class Users {
|
||||
// constructor (app) {
|
||||
// app.on('start', async () => {
|
||||
//
|
||||
// })
|
||||
//
|
||||
// app.on('export')
|
||||
// }
|
||||
//
|
||||
// createUser ({ name }) {
|
||||
//
|
||||
// }
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { cpus as getCpus } from 'os'
|
||||
import { ChildProcess, fork } from 'child_process'
|
||||
|
||||
const MAX = getCpus().length
|
||||
const WORKER = `${__dirname}/../../worker.js`
|
||||
|
||||
class Task {
|
||||
data: any
|
||||
reject: (error: any) => void
|
||||
resolve: (result: any) => void
|
||||
|
||||
constructor (data, resolve, reject) {
|
||||
this.data = data
|
||||
this.reject = reject
|
||||
this.resolve = resolve
|
||||
}
|
||||
}
|
||||
|
||||
export default class Workers {
|
||||
_idleWorker: ?ChildProcess
|
||||
_nWorkers: number
|
||||
_tasksQueue: Array<Task>
|
||||
|
||||
constructor () {
|
||||
this._idleWorker = undefined
|
||||
this._nWorkers = 0
|
||||
this._tasksQueue = []
|
||||
}
|
||||
|
||||
callWorker (data: any): any {
|
||||
return new Promise((resolve, reject) => {
|
||||
const task = new Task(data, resolve, reject)
|
||||
|
||||
const worker = this._getWorker()
|
||||
if (worker !== undefined) {
|
||||
this._submitTask(worker, task)
|
||||
} else {
|
||||
this._tasksQueue.push(task)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_getWorker () {
|
||||
let worker = this._idleWorker
|
||||
if (worker !== undefined) {
|
||||
this._idleWorker = undefined
|
||||
return worker
|
||||
}
|
||||
|
||||
if (this._nWorkers < MAX) {
|
||||
this._nWorkers++
|
||||
|
||||
worker = fork(WORKER)
|
||||
worker.on('error', error => {
|
||||
console.error('worker error', error)
|
||||
})
|
||||
worker.on('exit', (code, signal) => {
|
||||
console.log('worker exit', code, signal)
|
||||
this._nWorkers--
|
||||
})
|
||||
|
||||
return worker
|
||||
}
|
||||
}
|
||||
|
||||
_submitTask (worker: ChildProcess, task: Task) {
|
||||
worker.once('message', response => {
|
||||
if ('error' in response) {
|
||||
task.reject(response.error)
|
||||
} else {
|
||||
task.resolve(response.result)
|
||||
}
|
||||
|
||||
const nextTask = this._tasksQueue.shift()
|
||||
if (nextTask !== undefined) {
|
||||
this._submitTask(worker, nextTask)
|
||||
} else if (this._idleWorker !== undefined) {
|
||||
worker.kill()
|
||||
} else {
|
||||
this._idleWorker = worker
|
||||
}
|
||||
})
|
||||
|
||||
worker.send(task.data)
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const APP_NAME = 'xo-server'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const { info, warn } = require('@xen-orchestra/log').default('bootstrap')
|
||||
|
||||
process.on('unhandledRejection', reason => {
|
||||
warn('possibly unhandled rejection', reason)
|
||||
})
|
||||
;(({ 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)
|
||||
|
||||
const Bluebird = require('bluebird')
|
||||
Bluebird.longStackTraces()
|
||||
global.Promise = Bluebird
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const main = async args => {
|
||||
info('starting')
|
||||
|
||||
const config = await require('app-conf').load(APP_NAME)
|
||||
|
||||
const httpServer = new (require('http-server-plus'))()
|
||||
|
||||
const readFile = Bluebird.promisify(require('fs').readFile)
|
||||
await require('@xen-orchestra/async-map').default(
|
||||
config.http.listen,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
const { group, user } = config
|
||||
group != null && process.setgid(group)
|
||||
user != null && process.setuid(user)
|
||||
} catch (error) {
|
||||
warn('failed to change group/user', error)
|
||||
}
|
||||
|
||||
global.Observable = require('zen-observable')
|
||||
|
||||
const App = require('./app').default
|
||||
const app = new App({
|
||||
appName: APP_NAME,
|
||||
config,
|
||||
httpServer,
|
||||
safeMode: require('lodash/includes')(args, '--safe-mode'),
|
||||
})
|
||||
await app.start()
|
||||
|
||||
// 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()
|
||||
})
|
||||
})
|
||||
|
||||
return require('event-to-promise')(app, 'stopped')
|
||||
}
|
||||
main(process.argv.slice(2)).then(
|
||||
() => info('bye :-)'),
|
||||
error => warn('fatal error', error)
|
||||
)
|
||||
10
packages/xo-server-rework/src/node_modules/generate-token.js
generated
vendored
10
packages/xo-server-rework/src/node_modules/generate-token.js
generated
vendored
@@ -1,10 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import base64url from 'base64url'
|
||||
import { fromCallback } from 'promise-toolbox'
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
const generateSecureToken = (bytes: number = 32): Promise<string> =>
|
||||
fromCallback(cb => randomBytes(bytes, cb)).then(base64url)
|
||||
|
||||
export { generateSecureToken as default }
|
||||
103
packages/xo-server-rework/src/node_modules/mp-atch.js
generated
vendored
103
packages/xo-server-rework/src/node_modules/mp-atch.js
generated
vendored
@@ -1,103 +0,0 @@
|
||||
import {
|
||||
every,
|
||||
forEach,
|
||||
isArray,
|
||||
isPlainObject,
|
||||
some,
|
||||
} from 'lodash'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const { hasOwnProperty } = Object.prototype
|
||||
const getSingleKey = obj => {
|
||||
let prop
|
||||
for (const key in obj) {
|
||||
if (hasOwnProperty.call(obj, key)) {
|
||||
if (prop !== undefined) {
|
||||
return
|
||||
}
|
||||
prop = key
|
||||
}
|
||||
}
|
||||
return prop
|
||||
}
|
||||
|
||||
const OPERATORS = {
|
||||
__not: (pattern, value) => !match(pattern, value),
|
||||
__or: (pattern, value) => some(pattern, subpattern => match(subpattern, value)),
|
||||
}
|
||||
|
||||
export const match = (pattern, value) => {
|
||||
if (isPlainObject(pattern)) {
|
||||
const key = getSingleKey(pattern)
|
||||
let operator
|
||||
if (key !== undefined && (operator = OPERATORS[key]) !== undefined) {
|
||||
return operator(pattern[key], value)
|
||||
}
|
||||
|
||||
return isPlainObject(value) && every(pattern, (subpattern, key) => (
|
||||
value[key] !== undefined && match(subpattern, value[key])
|
||||
))
|
||||
}
|
||||
|
||||
if (isArray(pattern)) {
|
||||
return isArray(value) && every(pattern, subpattern =>
|
||||
some(value, subvalue => match(subpattern, subvalue))
|
||||
)
|
||||
}
|
||||
|
||||
return pattern === value
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const createPredicate = pattern => pattern == null
|
||||
? () => false
|
||||
: value => match(pattern, value)
|
||||
|
||||
export const patch = (value, patchData) => {
|
||||
if (isPlainObject(patchData)) {
|
||||
if (isArray(value)) {
|
||||
const toRemove = createPredicate(patchData['-'])
|
||||
const tmp = []
|
||||
forEach(value, (v, i) => {
|
||||
if (i in patchData) {
|
||||
const p = patchData[i]
|
||||
if (p === null) {
|
||||
return
|
||||
}
|
||||
tmp.push(patch(v, p))
|
||||
} else if (!toRemove(v)) {
|
||||
tmp.push(v)
|
||||
}
|
||||
})
|
||||
const toAdd = patchData['+']
|
||||
if (toAdd) {
|
||||
tmp.push.apply(tmp, toAdd)
|
||||
}
|
||||
return tmp
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
value = { ...value }
|
||||
forEach(patchData, (v, k) => {
|
||||
if (v === null) {
|
||||
delete value[k]
|
||||
} else {
|
||||
value[k] = patch(value[k], v)
|
||||
}
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
value = {}
|
||||
forEach(patchData, (v, k) => {
|
||||
if (v !== null) {
|
||||
value[k] = patch(null, v)
|
||||
}
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
return patchData
|
||||
}
|
||||
103
packages/xo-server-rework/src/node_modules/mp-atch.spec.js
generated
vendored
103
packages/xo-server-rework/src/node_modules/mp-atch.spec.js
generated
vendored
@@ -1,103 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { forEach } from 'lodash'
|
||||
|
||||
import { match, patch } from './mp-atch'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('match()', () => {
|
||||
const data = {
|
||||
'matches object properties': {
|
||||
pattern: { foo: 'bar' },
|
||||
nope: [
|
||||
null,
|
||||
{ },
|
||||
{ foo: 'baz' },
|
||||
],
|
||||
yep: [
|
||||
{ foo: 'bar' },
|
||||
{ foo: 'bar', bar: 'baz' },
|
||||
],
|
||||
},
|
||||
'matches set items': {
|
||||
pattern: [ 'foo', 'bar' ],
|
||||
nope: [
|
||||
[],
|
||||
[ 'foo' ],
|
||||
[ 'bar' ],
|
||||
[ 'foo', 'baz' ],
|
||||
],
|
||||
yep: [
|
||||
[ 'bar', 'foo' ],
|
||||
[ 'bar', 'baz', 'foo' ],
|
||||
],
|
||||
},
|
||||
'supports a __or operator': {
|
||||
pattern: { __or: [
|
||||
'foo',
|
||||
{ },
|
||||
] },
|
||||
nope: [
|
||||
'bar',
|
||||
[],
|
||||
],
|
||||
yep: [
|
||||
'foo',
|
||||
{ 'foo': 'bar' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
forEach(data, ({ pattern, nope, yep }, desc) => {
|
||||
if (!pattern) {
|
||||
it(desc)
|
||||
} else {
|
||||
it(desc, () => {
|
||||
forEach(nope, value => {
|
||||
expect(match(pattern, value)).toBe(false)
|
||||
})
|
||||
forEach(yep, value => {
|
||||
expect(match(pattern, value)).toBe(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('supports a __not operator', () => {
|
||||
forEach(data, ({ pattern, nope, yep }) => {
|
||||
if (!pattern) {
|
||||
return
|
||||
}
|
||||
|
||||
pattern = { __not: pattern }
|
||||
|
||||
forEach(nope, value => {
|
||||
expect(match(pattern, value)).toBe(true)
|
||||
})
|
||||
forEach(yep, value => {
|
||||
expect(match(pattern, value)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('patch', () => {
|
||||
it('can patch arrays', () => {
|
||||
expect(patch(
|
||||
[ 'foo', 'bar', 'quuz' ],
|
||||
{ 0: null, '-': 'quuz', '+': [ 'baz' ] }
|
||||
)).toEqual(
|
||||
[ 'bar', 'baz' ]
|
||||
)
|
||||
})
|
||||
|
||||
it('can patch objects', () => {
|
||||
expect(patch(
|
||||
{ foo: 1, bar: 2 },
|
||||
{ foo: null, bar: 3, baz: 4 }
|
||||
)).toEqual(
|
||||
{ bar: 3, baz: 4 }
|
||||
)
|
||||
})
|
||||
})
|
||||
26
packages/xo-server-rework/src/node_modules/simple-mkdirp.js
generated
vendored
26
packages/xo-server-rework/src/node_modules/simple-mkdirp.js
generated
vendored
@@ -1,26 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { dirname } from 'path'
|
||||
import { mkdir, stat } from 'fs'
|
||||
import { promisify } from 'promise-toolbox'
|
||||
|
||||
const simpleMkdirp = (
|
||||
path: string,
|
||||
cb: (error: ?Error) => void
|
||||
) => mkdir(path, undefined, error => {
|
||||
if (error == null) {
|
||||
return cb()
|
||||
}
|
||||
|
||||
if (error.code === 'ENOENT') {
|
||||
return simpleMkdirp(dirname(path), error =>
|
||||
error != null ? cb(error) : simpleMkdirp(path, cb)
|
||||
)
|
||||
}
|
||||
|
||||
return stat(path, (_, stats) =>
|
||||
stats != null && stats.isDirectory() ? cb() : cb(error)
|
||||
)
|
||||
})
|
||||
|
||||
export default promisify(simpleMkdirp)
|
||||
@@ -1,71 +0,0 @@
|
||||
import toDecorator from './to-decorator'
|
||||
import { EventEmitter } from 'events'
|
||||
import { CancelToken } from 'promise-toolbox'
|
||||
|
||||
class Task extends EventEmitter {
|
||||
constructor (name, { cancelToken, parent, steps } = {}) {
|
||||
super()
|
||||
|
||||
this._cancelToken = cancelToken || (parent && parent.cancelToken)
|
||||
this._name = name
|
||||
this._step = 0
|
||||
this._steps = steps || 0
|
||||
}
|
||||
|
||||
get cancelToken () {
|
||||
return this._cancelToken
|
||||
}
|
||||
|
||||
plan (n) {
|
||||
this._steps += n
|
||||
}
|
||||
|
||||
step (stepName) {
|
||||
this.emit('progress', {
|
||||
step: ++this._step,
|
||||
stepName,
|
||||
steps: this._steps,
|
||||
})
|
||||
|
||||
const token = this._cancelToken
|
||||
token && token.throwIfRequested()
|
||||
}
|
||||
}
|
||||
|
||||
export default opts =>
|
||||
toDecorator(
|
||||
fn =>
|
||||
function () {
|
||||
const { name = fn.name, steps } = opts || {}
|
||||
|
||||
const n = arguments.length
|
||||
const i = 0
|
||||
let task
|
||||
if (n !== 0) {
|
||||
const arg = arguments[0]
|
||||
if (arg instanceof Task) {
|
||||
task = new Task(name, { parent: arg, steps })
|
||||
} else if (CancelToken.isCancelToken(arg)) {
|
||||
task = new Task(name, { cancelToken: arg, steps })
|
||||
}
|
||||
}
|
||||
if (task === undefined) {
|
||||
task = new Task(name, { steps })
|
||||
}
|
||||
|
||||
const args = new Array(i + n)
|
||||
args[0] = task
|
||||
for (let j = 1; j < n; ++j) {
|
||||
args[j] = arguments[j + i]
|
||||
}
|
||||
|
||||
const promise = new Promise(resolve => resolve(fn.apply(this, args)))
|
||||
promise.onProgress = cb => {
|
||||
task.on('progress', cb)
|
||||
|
||||
return () => task.removeListener('progress', cb)
|
||||
}
|
||||
|
||||
return promise
|
||||
}
|
||||
)
|
||||
@@ -1,54 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { CancelToken } from 'promise-toolbox'
|
||||
|
||||
import task from './task'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const rejectionOf = promise =>
|
||||
promise.then(result => {
|
||||
throw result
|
||||
}, reason => reason)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const sleep = () => new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
describe('@task', () => {
|
||||
const fn = task()(async $task => {
|
||||
await sleep()
|
||||
$task.step('foo')
|
||||
await sleep()
|
||||
$task.step('bar')
|
||||
await sleep()
|
||||
$task.step('baz')
|
||||
})
|
||||
|
||||
it('', () => {
|
||||
const promise = fn()
|
||||
promise.onProgress((...args) => console.log(args))
|
||||
|
||||
return promise
|
||||
})
|
||||
|
||||
it('supports cancel tokens', async () => {
|
||||
const { cancel, token } = CancelToken.source()
|
||||
const promise = fn(token)
|
||||
|
||||
cancel('foo')
|
||||
expect((await rejectionOf(promise)).message).toBe('foo')
|
||||
})
|
||||
|
||||
it('supports subtasks', async () => {
|
||||
const fn2 = task()(async $task => {
|
||||
await sleep()
|
||||
await fn($task)
|
||||
await sleep()
|
||||
await fn($task)
|
||||
})
|
||||
|
||||
const promise = fn2()
|
||||
await promise
|
||||
})
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
// @flow
|
||||
|
||||
type Descriptor = {
|
||||
value: any
|
||||
}
|
||||
|
||||
const toDecorator = (
|
||||
wrapFunction: Function,
|
||||
wrapMethod: Function = wrapFunction
|
||||
) => (
|
||||
target: Function | any,
|
||||
key?: string,
|
||||
descriptor?: Descriptor
|
||||
) =>
|
||||
descriptor === undefined
|
||||
? wrapFunction(target)
|
||||
: {
|
||||
...descriptor,
|
||||
value: (typeof target === 'function' ? wrapFunction : wrapMethod)(
|
||||
descriptor.value
|
||||
),
|
||||
}
|
||||
export { toDecorator as default }
|
||||
@@ -1,23 +0,0 @@
|
||||
const METHODS = {
|
||||
add: ([ a, b ]) => a + b,
|
||||
sleep: duration => new Promise(resolve => setTimeout(resolve, duration)),
|
||||
}
|
||||
|
||||
process.on('message', ({ method, arg }) => {
|
||||
const fn = METHODS[method]
|
||||
if (fn === undefined) {
|
||||
return process.send(new Error('no such method'))
|
||||
}
|
||||
|
||||
new Promise(resolve => resolve(fn(arg)))
|
||||
.then(
|
||||
result => process.send({ result }),
|
||||
error => {
|
||||
console.error(error)
|
||||
process.send({ error })
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
console.error('worker error', error)
|
||||
})
|
||||
})
|
||||
@@ -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('../'))
|
||||
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server",
|
||||
"version": "5.19.6",
|
||||
"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/",
|
||||
@@ -31,7 +34,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "7.0.0-beta.46",
|
||||
"@babel/polyfill": "7.0.0-beta.44",
|
||||
"@marsaud/smb2-promise": "^0.2.1",
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/fs": "^0.0.0",
|
||||
@@ -123,17 +126,17 @@
|
||||
"yazl": "^2.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.46",
|
||||
"@babel/core": "7.0.0-beta.46",
|
||||
"@babel/plugin-proposal-decorators": "7.0.0-beta.46",
|
||||
"@babel/plugin-proposal-export-default-from": "7.0.0-beta.46",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.0.0-beta.46",
|
||||
"@babel/plugin-proposal-function-bind": "7.0.0-beta.46",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.0.0-beta.46",
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.0.0-beta.46",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.0.0-beta.46",
|
||||
"@babel/preset-env": "7.0.0-beta.46",
|
||||
"@babel/preset-flow": "7.0.0-beta.46",
|
||||
"@babel/cli": "7.0.0-beta.44",
|
||||
"@babel/core": "7.0.0-beta.44",
|
||||
"@babel/plugin-proposal-decorators": "7.0.0-beta.44",
|
||||
"@babel/plugin-proposal-export-default-from": "7.0.0-beta.44",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.0.0-beta.44",
|
||||
"@babel/plugin-proposal-function-bind": "7.0.0-beta.44",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.0.0-beta.44",
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.0.0-beta.44",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.0.0-beta.44",
|
||||
"@babel/preset-env": "7.0.0-beta.44",
|
||||
"@babel/preset-flow": "7.0.0-beta.44",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"index-modules": "^0.3.0",
|
||||
|
||||
176
packages/xo-server/src/cli.js
Executable file
176
packages/xo-server/src/cli.js
Executable 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)
|
||||
}
|
||||
}
|
||||
)
|
||||
348
packages/xo-server/src/front/index.js
Normal file
348
packages/xo-server/src/front/index.js
Normal 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))
|
||||
}
|
||||
@@ -1,656 +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 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)])
|
||||
}
|
||||
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 :-)')
|
||||
}
|
||||
3
packages/xo-server/src/worker-wrapper.js
Normal file
3
packages/xo-server/src/worker-wrapper.js
Normal file
@@ -0,0 +1,3 @@
|
||||
process.on('message', ([action, ...args]) => {
|
||||
console.log(action, args)
|
||||
})
|
||||
143
packages/xo-server/src/worker.js
Normal file
143
packages/xo-server/src/worker.js
Normal 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])
|
||||
)
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
noop,
|
||||
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,
|
||||
@@ -304,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
|
||||
@@ -660,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
|
||||
@@ -693,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,
|
||||
@@ -1330,16 +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') {
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.19.4",
|
||||
"version": "5.19.2",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -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 !',
|
||||
|
||||
@@ -1766,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',
|
||||
@@ -1795,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}',
|
||||
@@ -1819,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',
|
||||
|
||||
@@ -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 }) =>
|
||||
|
||||
@@ -2412,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
|
||||
@@ -2420,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 })
|
||||
|
||||
130
packages/xo-web/src/common/xo/install-xosan-pack-modal/index.js
Normal file
130
packages/xo-web/src/common/xo/install-xosan-pack-modal/index.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
))
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user