Compare commits
1 Commits
xo-server-
...
split-proc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
766175b4a0 |
@@ -1,56 +1,40 @@
|
||||
'use strict'
|
||||
|
||||
const PLUGINS_RE = /^(?:@babel\/|babel-)plugin-.+$/
|
||||
const PLUGINS_RE = /^(?:@babel\/plugin-.+|babel-plugin-lodash)$/
|
||||
const PRESETS_RE = /^@babel\/preset-.+$/
|
||||
|
||||
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.49",
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"@babel/preset-flow": "7.0.0-beta.49",
|
||||
"@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"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/fs",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "The File System for Xen Orchestra backups.",
|
||||
"keywords": [],
|
||||
@@ -20,10 +20,10 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0-beta.49",
|
||||
"@babel/runtime": "^7.0.0-beta.44",
|
||||
"@marsaud/smb2-promise": "^0.2.1",
|
||||
"execa": "^0.10.0",
|
||||
"fs-extra": "^6.0.1",
|
||||
"fs-extra": "^5.0.0",
|
||||
"get-stream": "^3.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.9.5",
|
||||
@@ -32,12 +32,12 @@
|
||||
"xo-remote-parser": "^0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.49",
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/plugin-proposal-function-bind": "7.0.0-beta.49",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"@babel/preset-flow": "7.0.0-beta.49",
|
||||
"@babel/cli": "7.0.0-beta.44",
|
||||
"@babel/core": "7.0.0-beta.44",
|
||||
"@babel/plugin-proposal-function-bind": "7.0.0-beta.44",
|
||||
"@babel/plugin-transform-runtime": "^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",
|
||||
|
||||
@@ -126,10 +126,7 @@ export default class RemoteHandlerAbstract {
|
||||
prependDir = false,
|
||||
}: { filter?: (name: string) => boolean, prependDir?: boolean } = {}
|
||||
): Promise<string[]> {
|
||||
let entries = await this._list(dir)
|
||||
if (filter !== undefined) {
|
||||
entries = entries.filter(filter)
|
||||
}
|
||||
const entries = await this._list(dir)
|
||||
|
||||
if (prependDir) {
|
||||
entries.forEach((entry, i) => {
|
||||
@@ -137,7 +134,7 @@ export default class RemoteHandlerAbstract {
|
||||
})
|
||||
}
|
||||
|
||||
return entries
|
||||
return filter === undefined ? entries : entries.filter(filter)
|
||||
}
|
||||
|
||||
async _list (dir: string): Promise<string[]> {
|
||||
|
||||
24
@xen-orchestra/log/.npmignore
Normal file
24
@xen-orchestra/log/.npmignore
Normal file
@@ -0,0 +1,24 @@
|
||||
/benchmark/
|
||||
/benchmarks/
|
||||
*.bench.js
|
||||
*.bench.js.map
|
||||
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/fixture/
|
||||
/fixtures/
|
||||
*.fixture.js
|
||||
*.fixture.js.map
|
||||
*.fixtures.js
|
||||
*.fixtures.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
|
||||
__snapshots__/
|
||||
149
@xen-orchestra/log/README.md
Normal file
149
@xen-orchestra/log/README.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# @xen-orchestra/log [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> ${pkg.description}
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/log):
|
||||
|
||||
```
|
||||
> npm install --save @xen-orchestra/log
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Everywhere something should be logged:
|
||||
|
||||
```js
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
|
||||
const log = createLogger('xo-server-api')
|
||||
log.warn('foo')
|
||||
```
|
||||
|
||||
Then at application level you can choose how to handle these logs:
|
||||
|
||||
```js
|
||||
import configure from '@xen-orchestra/log/configure'
|
||||
import createConsoleTransport from '@xen-orchestra/log/transports/console'
|
||||
import createEmailTransport from '@xen-orchestra/log/transports/email'
|
||||
|
||||
configure([
|
||||
{
|
||||
// if filter is a string, then it is pattern
|
||||
// (https://github.com/visionmedia/debug#wildcards) which is
|
||||
// matched against the namespace of the logs
|
||||
filter: process.env.DEBUG,
|
||||
|
||||
transport: createConsoleTransport()
|
||||
},
|
||||
{
|
||||
// only levels >= warn
|
||||
level: 'warn',
|
||||
|
||||
transport: createEmaileTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: 'jane.smith@gmail.com',
|
||||
pass: 'H&NbECcpXF|pyXe#%ZEb'
|
||||
},
|
||||
from: 'jane.smith@gmail.com',
|
||||
to: [
|
||||
'jane.smith@gmail.com',
|
||||
'sam.doe@yahoo.com'
|
||||
]
|
||||
})
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
### Transports
|
||||
|
||||
#### Console
|
||||
|
||||
```js
|
||||
import createConsoleTransport from '@xen-orchestra/log/transports/console'
|
||||
|
||||
configure(createConsoleTransport())
|
||||
```
|
||||
|
||||
#### Email
|
||||
|
||||
Optional dependency:
|
||||
|
||||
```
|
||||
> yarn add nodemailer pretty-format
|
||||
```
|
||||
|
||||
Configuration:
|
||||
|
||||
```js
|
||||
import createEmailTransport from '@xen-orchestra/log/transports/email'
|
||||
|
||||
configure(createEmailTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: 'jane.smith@gmail.com',
|
||||
pass: 'H&NbECcpXF|pyXe#%ZEb'
|
||||
},
|
||||
from: 'jane.smith@gmail.com',
|
||||
to: [
|
||||
'jane.smith@gmail.com',
|
||||
'sam.doe@yahoo.com'
|
||||
]
|
||||
}))
|
||||
```
|
||||
|
||||
#### Syslog
|
||||
|
||||
Optional dependency:
|
||||
|
||||
```
|
||||
> yarn add split-host syslog-client
|
||||
```
|
||||
|
||||
Configuration:
|
||||
|
||||
```js
|
||||
import createSyslogTransport from '@xen-orchestra/log/transports/syslog'
|
||||
|
||||
// By default, log to udp://localhost:514
|
||||
configure(createSyslogTransport())
|
||||
|
||||
// But TCP, a different host, or a different port can be used
|
||||
configure(createSyslogTransport('tcp://syslog.company.lan'))
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> yarn
|
||||
|
||||
# Run the tests
|
||||
> yarn test
|
||||
|
||||
# Continuously compile
|
||||
> yarn dev
|
||||
|
||||
# Continuously run the tests
|
||||
> yarn dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> yarn build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xo-web/issues/)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
1
@xen-orchestra/log/configure.js
Normal file
1
@xen-orchestra/log/configure.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('./dist/configure')
|
||||
52
@xen-orchestra/log/package.json
Normal file
52
@xen-orchestra/log/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/log",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/log",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"browserslist": [
|
||||
">2%"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "7.0.0-beta.42",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.42",
|
||||
"@babel/core": "7.0.0-beta.42",
|
||||
"@babel/preset-env": "7.0.0-beta.42",
|
||||
"@babel/preset-flow": "7.0.0-beta.42",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
}
|
||||
}
|
||||
105
@xen-orchestra/log/src/configure.js
Normal file
105
@xen-orchestra/log/src/configure.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import createConsoleTransport from './transports/console'
|
||||
import LEVELS, { resolve } from './levels'
|
||||
import { compileGlobPattern } from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const createTransport = config => {
|
||||
if (typeof config === 'function') {
|
||||
return config
|
||||
}
|
||||
|
||||
if (Array.isArray(config)) {
|
||||
const transports = config.map(createTransport)
|
||||
const { length } = transports
|
||||
return function () {
|
||||
for (let i = 0; i < length; ++i) {
|
||||
transports[i].apply(this, arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let { filter, transport } = config
|
||||
const level = resolve(config.level)
|
||||
|
||||
if (filter !== undefined) {
|
||||
if (typeof filter === 'string') {
|
||||
const re = compileGlobPattern(filter)
|
||||
filter = log => re.test(log.namespace)
|
||||
}
|
||||
|
||||
const orig = transport
|
||||
transport = function (log) {
|
||||
if ((level !== undefined && log.level >= level) || filter(log)) {
|
||||
return orig.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
} else if (level !== undefined) {
|
||||
const orig = transport
|
||||
transport = function (log) {
|
||||
if (log.level >= level) {
|
||||
return orig.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transport
|
||||
}
|
||||
|
||||
let transport = createTransport({
|
||||
// display warnings or above, and all that are enabled via DEBUG or
|
||||
// NODE_DEBUG env
|
||||
filter: process.env.DEBUG || process.env.NODE_DEBUG,
|
||||
level: LEVELS.INFO,
|
||||
|
||||
transport: createConsoleTransport(),
|
||||
})
|
||||
|
||||
const symbol =
|
||||
typeof Symbol !== 'undefined'
|
||||
? Symbol.for('@xen-orchestra/log')
|
||||
: '@@@xen-orchestra/log'
|
||||
global[symbol] = log => transport(log)
|
||||
|
||||
export const configure = config => {
|
||||
transport = createTransport(config)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const catchGlobalErrors = logger => {
|
||||
// patch process
|
||||
const onUncaughtException = error => {
|
||||
logger.error('uncaught exception', { error })
|
||||
}
|
||||
const onUnhandledRejection = error => {
|
||||
logger.warn('possibly unhandled rejection', { error })
|
||||
}
|
||||
const onWarning = error => {
|
||||
logger.warn('Node warning', { error })
|
||||
}
|
||||
process.on('uncaughtException', onUncaughtException)
|
||||
process.on('unhandledRejection', onUnhandledRejection)
|
||||
process.on('warning', onWarning)
|
||||
|
||||
// patch EventEmitter
|
||||
const EventEmitter = require('events')
|
||||
const { prototype } = EventEmitter
|
||||
const { emit } = prototype
|
||||
function patchedEmit (event, error) {
|
||||
event === 'error' && !this.listenerCount(event)
|
||||
? logger.error('unhandled error event', { error })
|
||||
: emit.apply(this, arguments)
|
||||
}
|
||||
prototype.emit = patchedEmit
|
||||
|
||||
return () => {
|
||||
process.removeListener('uncaughtException', onUncaughtException)
|
||||
process.removeListener('unhandledRejection', onUnhandledRejection)
|
||||
process.removeListener('warning', onWarning)
|
||||
|
||||
if (prototype.emit === patchedEmit) {
|
||||
prototype.emit = emit
|
||||
}
|
||||
}
|
||||
}
|
||||
65
@xen-orchestra/log/src/index.js
Normal file
65
@xen-orchestra/log/src/index.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import createTransport from './transports/console'
|
||||
import LEVELS from './levels'
|
||||
|
||||
const symbol =
|
||||
typeof Symbol !== 'undefined'
|
||||
? Symbol.for('@xen-orchestra/log')
|
||||
: '@@@xen-orchestra/log'
|
||||
if (!(symbol in global)) {
|
||||
// the default behavior, without requiring `configure` is to avoid
|
||||
// logging anything unless it's a real error
|
||||
const transport = createTransport()
|
||||
global[symbol] = log => log.level > LEVELS.WARN && transport(log)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function Log (data, level, namespace, message, time) {
|
||||
this.data = data
|
||||
this.level = level
|
||||
this.namespace = namespace
|
||||
this.message = message
|
||||
this.time = time
|
||||
}
|
||||
|
||||
function Logger (namespace) {
|
||||
this._namespace = namespace
|
||||
|
||||
// bind all logging methods
|
||||
for (const name in LEVELS) {
|
||||
const lowerCase = name.toLowerCase()
|
||||
this[lowerCase] = this[lowerCase].bind(this)
|
||||
}
|
||||
}
|
||||
|
||||
const { prototype } = Logger
|
||||
|
||||
for (const name in LEVELS) {
|
||||
const level = LEVELS[name]
|
||||
|
||||
prototype[name.toLowerCase()] = function (message, data) {
|
||||
global[symbol](new Log(data, level, this._namespace, message, new Date()))
|
||||
}
|
||||
}
|
||||
|
||||
prototype.wrap = function (message, fn) {
|
||||
const logger = this
|
||||
const warnAndRethrow = error => {
|
||||
logger.warn(message, { error })
|
||||
throw error
|
||||
}
|
||||
return function () {
|
||||
try {
|
||||
const result = fn.apply(this, arguments)
|
||||
const then = result != null && result.then
|
||||
return typeof then === 'function'
|
||||
? then.call(result, warnAndRethrow)
|
||||
: result
|
||||
} catch (error) {
|
||||
warnAndRethrow(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createLogger = namespace => new Logger(namespace)
|
||||
export { createLogger }
|
||||
24
@xen-orchestra/log/src/levels.js
Normal file
24
@xen-orchestra/log/src/levels.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const LEVELS = Object.create(null)
|
||||
export { LEVELS as default }
|
||||
|
||||
// https://github.com/trentm/node-bunyan#levels
|
||||
LEVELS.FATAL = 60 // service/app is going to down
|
||||
LEVELS.ERROR = 50 // fatal for current action
|
||||
LEVELS.WARN = 40 // something went wrong but it's not fatal
|
||||
LEVELS.INFO = 30 // detail on unusual but normal operation
|
||||
LEVELS.DEBUG = 20
|
||||
|
||||
export const NAMES = Object.create(null)
|
||||
for (const name in LEVELS) {
|
||||
NAMES[LEVELS[name]] = name
|
||||
}
|
||||
|
||||
export const resolve = level => {
|
||||
if (typeof level === 'string') {
|
||||
level = LEVELS[level.toUpperCase()]
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
Object.freeze(LEVELS)
|
||||
Object.freeze(NAMES)
|
||||
32
@xen-orchestra/log/src/levels.spec.js
Normal file
32
@xen-orchestra/log/src/levels.spec.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { forEach, isInteger } from 'lodash'
|
||||
|
||||
import LEVELS, { NAMES, resolve } from './levels'
|
||||
|
||||
describe('LEVELS', () => {
|
||||
it('maps level names to their integer values', () => {
|
||||
forEach(LEVELS, (value, name) => {
|
||||
expect(isInteger(value)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('NAMES', () => {
|
||||
it('maps level values to their names', () => {
|
||||
forEach(LEVELS, (value, name) => {
|
||||
expect(NAMES[value]).toBe(name)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolve()', () => {
|
||||
it('returns level values either from values or names', () => {
|
||||
forEach(LEVELS, value => {
|
||||
expect(resolve(value)).toBe(value)
|
||||
})
|
||||
forEach(NAMES, (name, value) => {
|
||||
expect(resolve(name)).toBe(+value)
|
||||
})
|
||||
})
|
||||
})
|
||||
0
@xen-orchestra/log/src/transports/.index-modules
Normal file
0
@xen-orchestra/log/src/transports/.index-modules
Normal file
20
@xen-orchestra/log/src/transports/console.js
Normal file
20
@xen-orchestra/log/src/transports/console.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import LEVELS, { NAMES } from '../levels'
|
||||
|
||||
// Bind console methods (necessary for browsers)
|
||||
const debugConsole = console.log.bind(console)
|
||||
const infoConsole = console.info.bind(console)
|
||||
const warnConsole = console.warn.bind(console)
|
||||
const errorConsole = console.error.bind(console)
|
||||
|
||||
const { ERROR, INFO, WARN } = LEVELS
|
||||
|
||||
const consoleTransport = ({ data, level, namespace, message, time }) => {
|
||||
const fn =
|
||||
level < INFO
|
||||
? debugConsole
|
||||
: level < WARN ? infoConsole : level < ERROR ? warnConsole : errorConsole
|
||||
|
||||
fn('%s - %s - [%s] %s', time.toISOString(), namespace, NAMES[level], message)
|
||||
data != null && fn(data)
|
||||
}
|
||||
export default () => consoleTransport
|
||||
68
@xen-orchestra/log/src/transports/email.js
Normal file
68
@xen-orchestra/log/src/transports/email.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import prettyFormat from 'pretty-format' // eslint-disable-line node/no-extraneous-import
|
||||
import { createTransport } from 'nodemailer' // eslint-disable-line node/no-extraneous-import
|
||||
import { fromCallback } from 'promise-toolbox'
|
||||
|
||||
import { evalTemplate, required } from '../utils'
|
||||
import { NAMES } from '../levels'
|
||||
|
||||
export default ({
|
||||
// transport options (https://nodemailer.com/smtp/)
|
||||
auth,
|
||||
authMethod,
|
||||
host,
|
||||
ignoreTLS,
|
||||
port,
|
||||
proxy,
|
||||
requireTLS,
|
||||
secure,
|
||||
service,
|
||||
tls,
|
||||
|
||||
// message options (https://nodemailer.com/message/)
|
||||
bcc,
|
||||
cc,
|
||||
from = required('from'),
|
||||
to = required('to'),
|
||||
subject = '[{{level}} - {{namespace}}] {{time}} {{message}}',
|
||||
}) => {
|
||||
const transporter = createTransport(
|
||||
{
|
||||
auth,
|
||||
authMethod,
|
||||
host,
|
||||
ignoreTLS,
|
||||
port,
|
||||
proxy,
|
||||
requireTLS,
|
||||
secure,
|
||||
service,
|
||||
tls,
|
||||
|
||||
disableFileAccess: true,
|
||||
disableUrlAccess: true,
|
||||
},
|
||||
{
|
||||
bcc,
|
||||
cc,
|
||||
from,
|
||||
to,
|
||||
}
|
||||
)
|
||||
|
||||
return log =>
|
||||
fromCallback(cb =>
|
||||
transporter.sendMail(
|
||||
{
|
||||
subject: evalTemplate(
|
||||
subject,
|
||||
key =>
|
||||
key === 'level'
|
||||
? NAMES[log.level]
|
||||
: key === 'time' ? log.time.toISOString() : log[key]
|
||||
),
|
||||
text: prettyFormat(log.data),
|
||||
},
|
||||
cb
|
||||
)
|
||||
)
|
||||
}
|
||||
42
@xen-orchestra/log/src/transports/syslog.js
Normal file
42
@xen-orchestra/log/src/transports/syslog.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import splitHost from 'split-host' // eslint-disable-line node/no-extraneous-import node/no-missing-import
|
||||
import { createClient, Facility, Severity, Transport } from 'syslog-client' // eslint-disable-line node/no-extraneous-import node/no-missing-import
|
||||
import { fromCallback } from 'promise-toolbox'
|
||||
import { startsWith } from 'lodash'
|
||||
|
||||
import LEVELS from '../levels'
|
||||
|
||||
// https://github.com/paulgrove/node-syslog-client#syslogseverity
|
||||
const LEVEL_TO_SEVERITY = {
|
||||
[LEVELS.FATAL]: Severity.Critical,
|
||||
[LEVELS.ERROR]: Severity.Error,
|
||||
[LEVELS.WARN]: Severity.Warning,
|
||||
[LEVELS.INFO]: Severity.Informational,
|
||||
[LEVELS.DEBUG]: Severity.Debug,
|
||||
}
|
||||
|
||||
const facility = Facility.User
|
||||
|
||||
export default target => {
|
||||
const opts = {}
|
||||
if (target !== undefined) {
|
||||
if (startsWith(target, 'tcp://')) {
|
||||
target = target.slice(6)
|
||||
opts.transport = Transport.Tcp
|
||||
} else if (startsWith(target, 'udp://')) {
|
||||
target = target.slice(6)
|
||||
opts.transport = Transport.Ucp
|
||||
}
|
||||
|
||||
;({ host: target, port: opts.port } = splitHost(target))
|
||||
}
|
||||
|
||||
const client = createClient(target, opts)
|
||||
|
||||
return log =>
|
||||
fromCallback(cb =>
|
||||
client.log(log.message, {
|
||||
facility,
|
||||
severity: LEVEL_TO_SEVERITY[log.level],
|
||||
})
|
||||
)
|
||||
}
|
||||
62
@xen-orchestra/log/src/utils.js
Normal file
62
@xen-orchestra/log/src/utils.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const TPL_RE = /\{\{(.+?)\}\}/g
|
||||
export const evalTemplate = (tpl, data) => {
|
||||
const getData =
|
||||
typeof data === 'function' ? (_, key) => data(key) : (_, key) => data[key]
|
||||
|
||||
return tpl.replace(TPL_RE, getData)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const compileGlobPatternFragment = pattern =>
|
||||
pattern
|
||||
.split('*')
|
||||
.map(escapeRegExp)
|
||||
.join('.*')
|
||||
|
||||
export const compileGlobPattern = pattern => {
|
||||
const no = []
|
||||
const yes = []
|
||||
pattern.split(/[\s,]+/).forEach(pattern => {
|
||||
if (pattern[0] === '-') {
|
||||
no.push(pattern.slice(1))
|
||||
} else {
|
||||
yes.push(pattern)
|
||||
}
|
||||
})
|
||||
|
||||
const raw = ['^']
|
||||
|
||||
if (no.length !== 0) {
|
||||
raw.push('(?!', no.map(compileGlobPatternFragment).join('|'), ')')
|
||||
}
|
||||
|
||||
if (yes.length !== 0) {
|
||||
raw.push('(?:', yes.map(compileGlobPatternFragment).join('|'), ')')
|
||||
} else {
|
||||
raw.push('.*')
|
||||
}
|
||||
|
||||
raw.push('$')
|
||||
|
||||
return new RegExp(raw.join(''))
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const required = name => {
|
||||
throw new Error(`missing required arg ${name}`)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const serializeError = error => ({
|
||||
...error,
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
stack: error.stack,
|
||||
})
|
||||
13
@xen-orchestra/log/src/utils.spec.js
Normal file
13
@xen-orchestra/log/src/utils.spec.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { compileGlobPattern } from './utils'
|
||||
|
||||
describe('compileGlobPattern()', () => {
|
||||
it('works', () => {
|
||||
const re = compileGlobPattern('foo, ba*, -bar')
|
||||
expect(re.test('foo')).toBe(true)
|
||||
expect(re.test('bar')).toBe(false)
|
||||
expect(re.test('baz')).toBe(true)
|
||||
expect(re.test('qux')).toBe(false)
|
||||
})
|
||||
})
|
||||
1
@xen-orchestra/log/transports
Symbolic link
1
@xen-orchestra/log/transports
Symbolic link
@@ -0,0 +1 @@
|
||||
dist/transports
|
||||
@@ -1,16 +0,0 @@
|
||||
### Check list
|
||||
|
||||
- [ ] if UI changes, a screenshot has been added to the PR
|
||||
- [ ] CHANGELOG updated
|
||||
- [ ] documentation updated
|
||||
|
||||
### Process
|
||||
|
||||
1. create a PR as soon as possible
|
||||
1. mark it as `WiP:` (Work in Progress) if not ready to be merged
|
||||
1. when you want a review, add a reviewer
|
||||
1. if necessary, update your PR, and readd a reviewer
|
||||
|
||||
### List of packages to release
|
||||
|
||||
> No need to mention xo-server and xo-web.
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
// Necessary for jest to be able to find the `.babelrc.js` closest to the file
|
||||
// instead of only the one in this directory.
|
||||
babelrcRoots: true,
|
||||
}
|
||||
6
flow-typed/limit-concurrency-decorator.js
vendored
6
flow-typed/limit-concurrency-decorator.js
vendored
@@ -1,6 +0,0 @@
|
||||
declare module 'limit-concurrency-decorator' {
|
||||
declare function limitConcurrencyDecorator(
|
||||
concurrency: number
|
||||
): <T: Function>(T) => T
|
||||
declare export default typeof limitConcurrencyDecorator
|
||||
}
|
||||
5
flow-typed/lodash.js
vendored
5
flow-typed/lodash.js
vendored
@@ -20,10 +20,5 @@ declare module 'lodash' {
|
||||
iteratee: (V1, K) => V2
|
||||
): { [K]: V2 }
|
||||
declare export function noop(...args: mixed[]): void
|
||||
declare export function some<T>(
|
||||
collection: T[],
|
||||
iteratee: (T, number) => boolean
|
||||
): boolean
|
||||
declare export function sum(values: number[]): number
|
||||
declare export function values<K, V>(object: { [K]: V }): V[]
|
||||
}
|
||||
|
||||
23
package.json
23
package.json
@@ -1,10 +1,8 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0-beta.49",
|
||||
"@babel/register": "^7.0.0-beta.49",
|
||||
"babel-core": "^7.0.0-0",
|
||||
"@babel/register": "^7.0.0-beta.44",
|
||||
"babel-7-jest": "^21.3.2",
|
||||
"babel-eslint": "^8.1.2",
|
||||
"babel-jest": "^23.0.1",
|
||||
"benchmark": "^2.1.4",
|
||||
"eslint": "^4.14.0",
|
||||
"eslint-config-standard": "^11.0.0-beta.0",
|
||||
@@ -15,22 +13,23 @@
|
||||
"eslint-plugin-react": "^7.6.1",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"exec-promise": "^0.7.0",
|
||||
"flow-bin": "^0.73.0",
|
||||
"flow-bin": "^0.69.0",
|
||||
"globby": "^8.0.0",
|
||||
"husky": "^0.14.3",
|
||||
"jest": "^23.0.1",
|
||||
"jest": "^22.0.4",
|
||||
"lodash": "^4.17.4",
|
||||
"prettier": "^1.10.2",
|
||||
"promise-toolbox": "^0.9.5",
|
||||
"sorted-object": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"yarn": "^1.7.0"
|
||||
"yarn": "^1.2.1"
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverage": true,
|
||||
"projects": [
|
||||
"<rootDir>"
|
||||
"<rootDir>",
|
||||
"<rootDir>/packages/xo-web"
|
||||
],
|
||||
"testEnvironment": "node",
|
||||
"testPathIgnorePatterns": [
|
||||
@@ -39,6 +38,14 @@
|
||||
],
|
||||
"testRegex": "\\.spec\\.js$",
|
||||
"transform": {
|
||||
"/@xen-orchestra/cron/.+\\.jsx?$": "babel-7-jest",
|
||||
"/@xen-orchestra/fs/.+\\.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",
|
||||
"/packages/xo-cli/.+\\.jsx?$": "babel-7-jest",
|
||||
"/packages/xo-server/.+\\.jsx?$": "babel-7-jest",
|
||||
"/packages/xo-vmdk-to-vhd/.+\\.jsx?$": "babel-7-jest",
|
||||
"\\.jsx?$": "babel-jest"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
"lodash": "^4.17.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.49",
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"@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.49",
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"@babel/preset-flow": "7.0.0-beta.49",
|
||||
"@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"
|
||||
},
|
||||
|
||||
@@ -23,20 +23,21 @@
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^0.0.1",
|
||||
"@xen-orchestra/fs": "^0.0.0",
|
||||
"babel-runtime": "^6.22.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"struct-fu": "^1.2.0",
|
||||
"vhd-lib": "^0.1.1"
|
||||
"vhd-lib": "^0.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0-beta.49",
|
||||
"@babel/core": "^7.0.0-beta.49",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0-beta.49",
|
||||
"@babel/preset-env": "^7.0.0-beta.49",
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.5.2",
|
||||
"babel-preset-stage-3": "^6.24.1",
|
||||
"cross-env": "^5.1.3",
|
||||
"execa": "^0.10.0",
|
||||
"index-modules": "^0.3.0",
|
||||
@@ -50,5 +51,22 @@
|
||||
"prebuild": "rimraf dist/ && index-modules --cjs-lazy src/commands",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepare": "yarn run build"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"lodash",
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"node": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"stage-3"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vhd-lib",
|
||||
"version": "0.1.1",
|
||||
"version": "0.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Primitives for VHD file handling",
|
||||
"keywords": [],
|
||||
@@ -20,30 +20,30 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0-beta.49",
|
||||
"@babel/runtime": "^7.0.0-beta.44",
|
||||
"@xen-orchestra/fs": "^0.0.0",
|
||||
"async-iterator-to-stream": "^1.0.2",
|
||||
"execa": "^0.10.0",
|
||||
"from2": "^2.3.0",
|
||||
"fs-extra": "^6.0.1",
|
||||
"fs-extra": "^5.0.0",
|
||||
"get-stream": "^3.0.0",
|
||||
"limit-concurrency-decorator": "^0.4.0",
|
||||
"promise-toolbox": "^0.9.5",
|
||||
"struct-fu": "^1.2.0",
|
||||
"uuid": "^3.0.1"
|
||||
"uuid": "^3.0.1",
|
||||
"tmp": "^0.0.33"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.49",
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"@babel/preset-flow": "7.0.0-beta.49",
|
||||
"@xen-orchestra/fs": "^0.0.1",
|
||||
"@babel/cli": "7.0.0-beta.44",
|
||||
"@babel/core": "7.0.0-beta.44",
|
||||
"@babel/plugin-transform-runtime": "^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",
|
||||
"execa": "^0.10.0",
|
||||
"fs-promise": "^2.0.0",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"tmp": "^0.0.33"
|
||||
"rimraf": "^2.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import assert from 'assert'
|
||||
import asyncIteratorToStream from 'async-iterator-to-stream'
|
||||
|
||||
import computeGeometryForSize from './_computeGeometryForSize'
|
||||
@@ -26,16 +25,62 @@ function createBAT (
|
||||
bat,
|
||||
bitmapSize
|
||||
) {
|
||||
const vhdOccupationTable = []
|
||||
let currentVhdPositionSector = firstBlockPosition / SECTOR_SIZE
|
||||
blockAddressList.forEach(blockPosition => {
|
||||
assert.strictEqual(blockPosition % 512, 0)
|
||||
const vhdTableIndex = Math.floor(blockPosition / VHD_BLOCK_SIZE_BYTES)
|
||||
const scaled = blockPosition / VHD_BLOCK_SIZE_BYTES
|
||||
const vhdTableIndex = Math.floor(scaled)
|
||||
if (bat.readUInt32BE(vhdTableIndex * 4) === BLOCK_UNUSED) {
|
||||
bat.writeUInt32BE(currentVhdPositionSector, vhdTableIndex * 4)
|
||||
currentVhdPositionSector +=
|
||||
(bitmapSize + VHD_BLOCK_SIZE_BYTES) / SECTOR_SIZE
|
||||
}
|
||||
// not using bit operators to avoid the int32 coercion, that way we can go to 53 bits
|
||||
vhdOccupationTable[vhdTableIndex] =
|
||||
(vhdOccupationTable[vhdTableIndex] || 0) +
|
||||
Math.pow(2, (scaled % 1) * ratio)
|
||||
})
|
||||
return vhdOccupationTable
|
||||
}
|
||||
|
||||
function createBitmap (bitmapSize, ratio, vhdOccupationBucket) {
|
||||
const bitmap = Buffer.alloc(bitmapSize)
|
||||
for (let i = 0; i < VHD_BLOCK_SIZE_SECTORS / ratio; i++) {
|
||||
// do not shift to avoid int32 coercion
|
||||
if ((vhdOccupationBucket * Math.pow(2, -i)) & 1) {
|
||||
for (let j = 0; j < ratio; j++) {
|
||||
setBitmap(bitmap, i * ratio + j)
|
||||
}
|
||||
}
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
|
||||
function * yieldIfNotEmpty (buffer) {
|
||||
if (buffer.length > 0) {
|
||||
yield buffer
|
||||
}
|
||||
}
|
||||
|
||||
async function * generateFileContent (
|
||||
blockIterator,
|
||||
bitmapSize,
|
||||
ratio,
|
||||
vhdOccupationTable
|
||||
) {
|
||||
let currentVhdBlockIndex = -1
|
||||
let currentBlockBuffer = Buffer.alloc(0)
|
||||
for await (const next of blockIterator) {
|
||||
const batEntry = Math.floor(next.offsetBytes / VHD_BLOCK_SIZE_BYTES)
|
||||
if (batEntry !== currentVhdBlockIndex) {
|
||||
yield * yieldIfNotEmpty(currentBlockBuffer)
|
||||
currentBlockBuffer = Buffer.alloc(VHD_BLOCK_SIZE_BYTES)
|
||||
currentVhdBlockIndex = batEntry
|
||||
yield createBitmap(bitmapSize, ratio, vhdOccupationTable[batEntry])
|
||||
}
|
||||
next.data.copy(currentBlockBuffer, next.offsetBytes % VHD_BLOCK_SIZE_BYTES)
|
||||
}
|
||||
yield * yieldIfNotEmpty(currentBlockBuffer)
|
||||
}
|
||||
|
||||
export default asyncIteratorToStream(async function * (
|
||||
@@ -78,49 +123,21 @@ export default asyncIteratorToStream(async function * (
|
||||
const bitmapSize =
|
||||
Math.ceil(VHD_BLOCK_SIZE_SECTORS / 8 / SECTOR_SIZE) * SECTOR_SIZE
|
||||
const bat = Buffer.alloc(tablePhysicalSizeBytes, 0xff)
|
||||
createBAT(firstBlockPosition, blockAddressList, ratio, bat, bitmapSize)
|
||||
let position = 0
|
||||
function * yieldAndTrack (buffer, expectedPosition) {
|
||||
if (expectedPosition !== undefined) {
|
||||
assert.strictEqual(position, expectedPosition)
|
||||
}
|
||||
if (buffer.length > 0) {
|
||||
yield buffer
|
||||
position += buffer.length
|
||||
}
|
||||
}
|
||||
async function * generateFileContent (blockIterator, bitmapSize, ratio) {
|
||||
let currentBlock = -1
|
||||
let currentVhdBlockIndex = -1
|
||||
let currentBlockWithBitmap = Buffer.alloc(0)
|
||||
for await (const next of blockIterator) {
|
||||
currentBlock++
|
||||
assert.strictEqual(blockAddressList[currentBlock], next.offsetBytes)
|
||||
const batIndex = Math.floor(next.offsetBytes / VHD_BLOCK_SIZE_BYTES)
|
||||
if (batIndex !== currentVhdBlockIndex) {
|
||||
if (currentVhdBlockIndex >= 0) {
|
||||
yield * yieldAndTrack(
|
||||
currentBlockWithBitmap,
|
||||
bat.readUInt32BE(currentVhdBlockIndex * 4) * 512
|
||||
)
|
||||
}
|
||||
currentBlockWithBitmap = Buffer.alloc(bitmapSize + VHD_BLOCK_SIZE_BYTES)
|
||||
currentVhdBlockIndex = batIndex
|
||||
}
|
||||
const blockOffset = (next.offsetBytes / 512) % VHD_BLOCK_SIZE_SECTORS
|
||||
for (let bitPos = 0; bitPos < VHD_BLOCK_SIZE_SECTORS / ratio; bitPos++) {
|
||||
setBitmap(currentBlockWithBitmap, blockOffset + bitPos)
|
||||
}
|
||||
next.data.copy(
|
||||
currentBlockWithBitmap,
|
||||
bitmapSize + next.offsetBytes % VHD_BLOCK_SIZE_BYTES
|
||||
)
|
||||
}
|
||||
yield * yieldAndTrack(currentBlockWithBitmap)
|
||||
}
|
||||
yield * yieldAndTrack(footer, 0)
|
||||
yield * yieldAndTrack(header, FOOTER_SIZE)
|
||||
yield * yieldAndTrack(bat, FOOTER_SIZE + HEADER_SIZE)
|
||||
yield * generateFileContent(blockIterator, bitmapSize, ratio)
|
||||
yield * yieldAndTrack(footer)
|
||||
const vhdOccupationTable = createBAT(
|
||||
firstBlockPosition,
|
||||
blockAddressList,
|
||||
ratio,
|
||||
bat,
|
||||
bitmapSize
|
||||
)
|
||||
yield footer
|
||||
yield header
|
||||
yield bat
|
||||
yield * generateFileContent(
|
||||
blockIterator,
|
||||
bitmapSize,
|
||||
ratio,
|
||||
vhdOccupationTable
|
||||
)
|
||||
yield footer
|
||||
})
|
||||
|
||||
@@ -102,15 +102,15 @@ test('ReadableSparseVHDStream can handle a sparse file', async () => {
|
||||
data: Buffer.alloc(blockSize, 'azerzaerazeraze', 'ascii'),
|
||||
},
|
||||
{
|
||||
offsetBytes: blockSize * 100,
|
||||
offsetBytes: blockSize * 5,
|
||||
data: Buffer.alloc(blockSize, 'gdfslkdfguer', 'ascii'),
|
||||
},
|
||||
]
|
||||
const fileSize = blockSize * 110
|
||||
const fileSize = blockSize * 10
|
||||
const stream = createReadableSparseVHDStream(
|
||||
fileSize,
|
||||
blockSize,
|
||||
blocks.map(b => b.offsetBytes),
|
||||
[100, 700],
|
||||
blocks
|
||||
)
|
||||
const pipe = stream.pipe(createWriteStream('output.vhd'))
|
||||
|
||||
@@ -476,12 +476,12 @@ export default class Vhd {
|
||||
|
||||
// For each sector of block data...
|
||||
const { sectorsPerBlock } = child
|
||||
let parentBitmap = null
|
||||
for (let i = 0; i < sectorsPerBlock; i++) {
|
||||
// If no changes on one sector, skip.
|
||||
if (!mapTestBit(bitmap, i)) {
|
||||
continue
|
||||
}
|
||||
let parentBitmap = null
|
||||
let endSector = i + 1
|
||||
|
||||
// Count changed sectors.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-acl-resolver",
|
||||
"version": "0.2.4",
|
||||
"version": "0.2.3",
|
||||
"license": "ISC",
|
||||
"description": "Xen-Orchestra internal: do ACLs resolution",
|
||||
"keywords": [],
|
||||
|
||||
@@ -50,9 +50,7 @@ const checkAuthorizationByTypes = {
|
||||
|
||||
network: or(checkSelf, checkMember('$pool')),
|
||||
|
||||
PIF: checkMember('$host'),
|
||||
|
||||
SR: or(checkSelf, checkMember('$container')),
|
||||
SR: or(checkSelf, checkMember('$pool')),
|
||||
|
||||
task: checkMember('$host'),
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "7.0.0-beta.49",
|
||||
"@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.49",
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"@babel/preset-flow": "7.0.0-beta.49",
|
||||
"@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')
|
||||
)
|
||||
@@ -25,16 +25,17 @@
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0-beta.49",
|
||||
"babel-runtime": "^6.18.0",
|
||||
"kindof": "^2.0.0",
|
||||
"lodash": "^4.17.2",
|
||||
"make-error": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0-beta.49",
|
||||
"@babel/core": "^7.0.0-beta.49",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0-beta.49",
|
||||
"@babel/preset-env": "^7.0.0-beta.49",
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.5.2",
|
||||
"babel-preset-stage-3": "^6.24.1",
|
||||
"cross-env": "^5.1.3",
|
||||
"event-to-promise": "^0.8.0",
|
||||
"rimraf": "^2.6.1"
|
||||
@@ -45,5 +46,22 @@
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"lodash",
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"node": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"stage-3"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
@@ -27,10 +27,10 @@
|
||||
"lodash": "^4.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0-beta.49",
|
||||
"@babel/core": "^7.0.0-beta.49",
|
||||
"@babel/preset-env": "^7.0.0-beta.49",
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"babel-preset-env": "^1.5.2",
|
||||
"babel-preset-stage-3": "^6.24.1",
|
||||
"cross-env": "^5.1.3",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"rimraf": "^2.6.1"
|
||||
@@ -41,5 +41,22 @@
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepare": "yarn run build"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"lodash"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"browsers": "> 5%",
|
||||
"node": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"stage-3"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-backup-reports",
|
||||
"version": "0.12.0",
|
||||
"version": "0.11.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Backup reports plugin for XO-Server",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import humanFormat from 'human-format'
|
||||
import moment from 'moment-timezone'
|
||||
import { forEach, get, startCase } from 'lodash'
|
||||
import { find, forEach, get, startCase } from 'lodash'
|
||||
|
||||
import pkg from '../package'
|
||||
|
||||
export const configurationSchema = {
|
||||
@@ -36,12 +37,6 @@ const ICON_FAILURE = '🚨'
|
||||
const ICON_SKIPPED = '⏩'
|
||||
const ICON_SUCCESS = '✔'
|
||||
|
||||
const STATUS_ICON = {
|
||||
skipped: ICON_SKIPPED,
|
||||
success: ICON_SUCCESS,
|
||||
failure: ICON_FAILURE,
|
||||
}
|
||||
|
||||
const DATE_FORMAT = 'dddd, MMMM Do YYYY, h:mm:ss a'
|
||||
const createDateFormater = timezone =>
|
||||
timezone !== undefined
|
||||
@@ -100,41 +95,43 @@ class BackupReportsXoPlugin {
|
||||
this._xo.removeListener('job:terminated', this._report)
|
||||
}
|
||||
|
||||
_wrapper (status, job, schedule, runJobId) {
|
||||
_wrapper (status, job, schedule) {
|
||||
return new Promise(resolve =>
|
||||
resolve(
|
||||
job.type === 'backup'
|
||||
? this._backupNgListener(status, job, schedule, runJobId)
|
||||
: this._listener(status, job, schedule, runJobId)
|
||||
? this._backupNgListener(status, job, schedule)
|
||||
: this._listener(status, job, schedule)
|
||||
)
|
||||
).catch(logError)
|
||||
}
|
||||
|
||||
async _backupNgListener (_1, _2, { timezone }, runJobId) {
|
||||
async _backupNgListener (runJobId, _, { timezone }) {
|
||||
const xo = this._xo
|
||||
const log = await xo.getBackupNgLogs(runJobId)
|
||||
const logs = await xo.getBackupNgLogs(runJobId)
|
||||
const jobLog = logs['roots'][0]
|
||||
const vmsTaskLog = logs[jobLog.id]
|
||||
|
||||
const { reportWhen, mode } = log.data || {}
|
||||
const { reportWhen, mode } = jobLog.data || {}
|
||||
if (reportWhen === 'never') {
|
||||
return
|
||||
}
|
||||
|
||||
const formatDate = createDateFormater(timezone)
|
||||
const jobName = (await xo.getJob(jobLog.jobId, 'backup')).name
|
||||
|
||||
if (log.status === 'success' && reportWhen === 'failure') {
|
||||
return
|
||||
}
|
||||
|
||||
const jobName = (await xo.getJob(log.jobId, 'backup')).name
|
||||
if (log.result !== undefined) {
|
||||
if (jobLog.error !== undefined) {
|
||||
const [globalStatus, icon] =
|
||||
jobLog.error.message === NO_VMS_MATCH_THIS_PATTERN
|
||||
? ['Skipped', ICON_SKIPPED]
|
||||
: ['Failure', ICON_FAILURE]
|
||||
let markdown = [
|
||||
`## Global status: ${log.status}`,
|
||||
`## Global status: ${globalStatus}`,
|
||||
'',
|
||||
`- **mode**: ${mode}`,
|
||||
`- **Start time**: ${formatDate(log.start)}`,
|
||||
`- **End time**: ${formatDate(log.end)}`,
|
||||
`- **Duration**: ${formatDuration(log.end - log.start)}`,
|
||||
`- **Error**: ${log.result.message}`,
|
||||
`- **Start time**: ${formatDate(jobLog.start)}`,
|
||||
`- **End time**: ${formatDate(jobLog.end)}`,
|
||||
`- **Duration**: ${formatDuration(jobLog.duration)}`,
|
||||
`- **Error**: ${jobLog.error.message}`,
|
||||
'---',
|
||||
'',
|
||||
`*${pkg.name} v${pkg.version}*`,
|
||||
@@ -142,14 +139,12 @@ class BackupReportsXoPlugin {
|
||||
|
||||
markdown = markdown.join('\n')
|
||||
return this._sendReport({
|
||||
subject: `[Xen Orchestra] ${
|
||||
log.status
|
||||
} − Backup report for ${jobName} ${STATUS_ICON[log.status]}`,
|
||||
subject: `[Xen Orchestra] ${globalStatus} − Backup report for ${jobName} ${icon}`,
|
||||
markdown,
|
||||
nagiosStatus: 2,
|
||||
nagiosMarkdown: `[Xen Orchestra] [${
|
||||
log.status
|
||||
}] Backup report for ${jobName} - Error : ${log.result.message}`,
|
||||
nagiosMarkdown: `[Xen Orchestra] [${globalStatus}] Backup report for ${jobName} - Error : ${
|
||||
jobLog.error.message
|
||||
}`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -162,12 +157,14 @@ class BackupReportsXoPlugin {
|
||||
let globalTransferSize = 0
|
||||
let nFailures = 0
|
||||
let nSkipped = 0
|
||||
for (const taskLog of log.tasks) {
|
||||
if (taskLog.status === 'success' && reportWhen === 'failure') {
|
||||
|
||||
for (const vmTaskLog of vmsTaskLog || []) {
|
||||
const vmTaskStatus = vmTaskLog.status
|
||||
if (vmTaskStatus === 'success' && reportWhen === 'failure') {
|
||||
return
|
||||
}
|
||||
|
||||
const vmId = taskLog.data.id
|
||||
const vmId = vmTaskLog.data.id
|
||||
let vm
|
||||
try {
|
||||
vm = xo.getObject(vmId)
|
||||
@@ -176,143 +173,132 @@ class BackupReportsXoPlugin {
|
||||
`### ${vm !== undefined ? vm.name_label : 'VM not found'}`,
|
||||
'',
|
||||
`- **UUID**: ${vm !== undefined ? vm.uuid : vmId}`,
|
||||
`- **Start time**: ${formatDate(taskLog.start)}`,
|
||||
`- **End time**: ${formatDate(taskLog.end)}`,
|
||||
`- **Duration**: ${formatDuration(taskLog.end - taskLog.start)}`,
|
||||
`- **Start time**: ${formatDate(vmTaskLog.start)}`,
|
||||
`- **End time**: ${formatDate(vmTaskLog.end)}`,
|
||||
`- **Duration**: ${formatDuration(vmTaskLog.duration)}`,
|
||||
]
|
||||
|
||||
const failedSubTasks = []
|
||||
const snapshotText = []
|
||||
const operationsText = []
|
||||
const srsText = []
|
||||
const remotesText = []
|
||||
for (const subTaskLog of logs[vmTaskLog.taskId] || []) {
|
||||
const { data, status, result, message } = subTaskLog
|
||||
const icon =
|
||||
subTaskLog.status === 'success' ? ICON_SUCCESS : ICON_FAILURE
|
||||
const errorMessage = ` **Error**: ${get(result, 'message')}`
|
||||
|
||||
for (const subTaskLog of taskLog.tasks || []) {
|
||||
const icon = STATUS_ICON[subTaskLog.status]
|
||||
const errorMessage = ` - **Error**: ${get(
|
||||
subTaskLog.result,
|
||||
'message'
|
||||
)}`
|
||||
|
||||
if (subTaskLog.message === 'snapshot') {
|
||||
snapshotText.push(
|
||||
`- **Snapshot** ${icon}`,
|
||||
` - **Start time**: ${formatDate(subTaskLog.start)}`,
|
||||
` - **End time**: ${formatDate(subTaskLog.end)}`
|
||||
)
|
||||
} else if (subTaskLog.data.type === 'remote') {
|
||||
const id = subTaskLog.data.id
|
||||
const remote = await xo.getRemote(id).catch(() => {})
|
||||
if (message === 'snapshot') {
|
||||
operationsText.push(`- **Snapshot** ${icon}`)
|
||||
if (status === 'failure') {
|
||||
failedSubTasks.push('Snapshot')
|
||||
operationsText.push('', errorMessage)
|
||||
}
|
||||
} else if (data.type === 'remote') {
|
||||
const remoteId = data.id
|
||||
const remote = await xo.getRemote(remoteId).catch(() => {})
|
||||
remotesText.push(
|
||||
` - **${
|
||||
`- **${
|
||||
remote !== undefined ? remote.name : `Remote Not found`
|
||||
}** (${id}) ${icon}`,
|
||||
` - **Start time**: ${formatDate(subTaskLog.start)}`,
|
||||
` - **End time**: ${formatDate(subTaskLog.end)}`,
|
||||
` - **Duration**: ${formatDuration(
|
||||
subTaskLog.end - subTaskLog.start
|
||||
)}`
|
||||
}** (${remoteId}) ${icon}`
|
||||
)
|
||||
if (subTaskLog.status === 'failure') {
|
||||
failedSubTasks.push(remote !== undefined ? remote.name : id)
|
||||
if (status === 'failure') {
|
||||
failedSubTasks.push(remote !== undefined ? remote.name : remoteId)
|
||||
remotesText.push('', errorMessage)
|
||||
}
|
||||
} else {
|
||||
const id = subTaskLog.data.id
|
||||
const srId = data.id
|
||||
let sr
|
||||
try {
|
||||
sr = xo.getObject(id)
|
||||
sr = xo.getObject(srId)
|
||||
} catch (e) {}
|
||||
const [srName, srUuid] =
|
||||
sr !== undefined ? [sr.name_label, sr.uuid] : [`SR Not found`, id]
|
||||
srsText.push(
|
||||
` - **${srName}** (${srUuid}) ${icon}`,
|
||||
` - **Start time**: ${formatDate(subTaskLog.start)}`,
|
||||
` - **End time**: ${formatDate(subTaskLog.end)}`,
|
||||
` - **Duration**: ${formatDuration(
|
||||
subTaskLog.end - subTaskLog.start
|
||||
)}`
|
||||
)
|
||||
if (subTaskLog.status === 'failure') {
|
||||
failedSubTasks.push(sr !== undefined ? sr.name_label : id)
|
||||
sr !== undefined ? [sr.name_label, sr.uuid] : [`SR Not found`, srId]
|
||||
srsText.push(`- **${srName}** (${srUuid}) ${icon}`)
|
||||
if (status === 'failure') {
|
||||
failedSubTasks.push(sr !== undefined ? sr.name_label : srId)
|
||||
srsText.push('', errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
forEach(subTaskLog.tasks, operationLog => {
|
||||
const size = operationLog.result.size
|
||||
if (operationLog.message === 'merge') {
|
||||
globalMergeSize += size
|
||||
} else {
|
||||
globalTransferSize += size
|
||||
}
|
||||
const operationText = [
|
||||
` - **${operationLog.message}** ${
|
||||
STATUS_ICON[operationLog.status]
|
||||
}`,
|
||||
` - **Start time**: ${formatDate(operationLog.start)}`,
|
||||
` - **End time**: ${formatDate(operationLog.end)}`,
|
||||
` - **Duration**: ${formatDuration(
|
||||
operationLog.end - operationLog.start
|
||||
)}`,
|
||||
operationLog.status === 'failure'
|
||||
? `- **Error**: ${get(operationLog.result, 'message')}`
|
||||
: ` - **Size**: ${formatSize(size)}`,
|
||||
` - **Speed**: ${formatSpeed(
|
||||
size,
|
||||
operationLog.end - operationLog.start
|
||||
)}`,
|
||||
].join('\n')
|
||||
if (get(subTaskLog, 'data.type') === 'remote') {
|
||||
remotesText.push(operationText)
|
||||
remotesText.join('\n')
|
||||
}
|
||||
if (get(subTaskLog, 'data.type') === 'SR') {
|
||||
srsText.push(operationText)
|
||||
srsText.join('\n')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (operationsText.length !== 0) {
|
||||
operationsText.unshift(`#### Operations`, '')
|
||||
}
|
||||
if (srsText.length !== 0) {
|
||||
srsText.unshift(`- **SRs**`)
|
||||
srsText.unshift(`#### SRs`, '')
|
||||
}
|
||||
if (remotesText.length !== 0) {
|
||||
remotesText.unshift(`- **Remotes**`)
|
||||
remotesText.unshift(`#### remotes`, '')
|
||||
}
|
||||
const subText = [...snapshotText, '', ...srsText, '', ...remotesText]
|
||||
if (taskLog.result !== undefined) {
|
||||
if (taskLog.status === 'skipped') {
|
||||
const subText = [...operationsText, '', ...srsText, '', ...remotesText]
|
||||
const result = vmTaskLog.result
|
||||
if (vmTaskStatus === 'failure' && result !== undefined) {
|
||||
const { message } = result
|
||||
if (isSkippedError(result)) {
|
||||
++nSkipped
|
||||
skippedVmsText.push(
|
||||
...text,
|
||||
`- **Reason**: ${
|
||||
taskLog.result.message === UNHEALTHY_VDI_CHAIN_ERROR
|
||||
message === UNHEALTHY_VDI_CHAIN_ERROR
|
||||
? UNHEALTHY_VDI_CHAIN_MESSAGE
|
||||
: taskLog.result.message
|
||||
: message
|
||||
}`,
|
||||
''
|
||||
)
|
||||
nagiosText.push(
|
||||
`[(Skipped) ${vm !== undefined ? vm.name_label : 'undefined'} : ${
|
||||
taskLog.result.message
|
||||
} ]`
|
||||
`[(Skipped) ${
|
||||
vm !== undefined ? vm.name_label : 'undefined'
|
||||
} : ${message} ]`
|
||||
)
|
||||
} else {
|
||||
++nFailures
|
||||
failedVmsText.push(
|
||||
...text,
|
||||
`- **Error**: ${taskLog.result.message}`,
|
||||
''
|
||||
)
|
||||
failedVmsText.push(...text, `- **Error**: ${message}`, '')
|
||||
|
||||
nagiosText.push(
|
||||
`[(Failed) ${vm !== undefined ? vm.name_label : 'undefined'} : ${
|
||||
taskLog.result.message
|
||||
} ]`
|
||||
`[(Failed) ${
|
||||
vm !== undefined ? vm.name_label : 'undefined'
|
||||
} : ${message} ]`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (taskLog.status === 'failure') {
|
||||
let transferSize, transferDuration, mergeSize, mergeDuration
|
||||
|
||||
forEach(logs[vmTaskLog.taskId], ({ taskId }) => {
|
||||
if (transferSize !== undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
const transferTask = find(logs[taskId], { message: 'transfer' })
|
||||
if (transferTask !== undefined) {
|
||||
transferSize = transferTask.result.size
|
||||
transferDuration = transferTask.end - transferTask.start
|
||||
}
|
||||
|
||||
const mergeTask = find(logs[taskId], { message: 'merge' })
|
||||
if (mergeTask !== undefined) {
|
||||
mergeSize = mergeTask.result.size
|
||||
mergeDuration = mergeTask.end - mergeTask.start
|
||||
}
|
||||
})
|
||||
if (transferSize !== undefined) {
|
||||
globalTransferSize += transferSize
|
||||
text.push(
|
||||
`- **Transfer size**: ${formatSize(transferSize)}`,
|
||||
`- **Transfer speed**: ${formatSpeed(
|
||||
transferSize,
|
||||
transferDuration
|
||||
)}`
|
||||
)
|
||||
}
|
||||
if (mergeSize !== undefined) {
|
||||
globalMergeSize += mergeSize
|
||||
text.push(
|
||||
`- **Merge size**: ${formatSize(mergeSize)}`,
|
||||
`- **Merge speed**: ${formatSpeed(mergeSize, mergeDuration)}`
|
||||
)
|
||||
}
|
||||
if (vmTaskStatus === 'failure') {
|
||||
++nFailures
|
||||
failedVmsText.push(...text, '', '', ...subText, '')
|
||||
nagiosText.push(
|
||||
@@ -325,16 +311,23 @@ class BackupReportsXoPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
const globalSuccess = nFailures === 0 && nSkipped === 0
|
||||
if (reportWhen === 'failure' && globalSuccess) {
|
||||
return
|
||||
}
|
||||
|
||||
const nVms = log.tasks.length
|
||||
const nVms = vmsTaskLog.length
|
||||
const nSuccesses = nVms - nFailures - nSkipped
|
||||
const globalStatus = globalSuccess
|
||||
? `Success`
|
||||
: nFailures !== 0 ? `Failure` : `Skipped`
|
||||
let markdown = [
|
||||
`## Global status: ${log.status}`,
|
||||
`## Global status: ${globalStatus}`,
|
||||
'',
|
||||
`- **mode**: ${mode}`,
|
||||
`- **Start time**: ${formatDate(log.start)}`,
|
||||
`- **End time**: ${formatDate(log.end)}`,
|
||||
`- **Duration**: ${formatDuration(log.start - log.end)}`,
|
||||
`- **Start time**: ${formatDate(jobLog.start)}`,
|
||||
`- **End time**: ${formatDate(jobLog.end)}`,
|
||||
`- **Duration**: ${formatDuration(jobLog.duration)}`,
|
||||
`- **Successes**: ${nSuccesses} / ${nVms}`,
|
||||
]
|
||||
|
||||
@@ -374,16 +367,17 @@ class BackupReportsXoPlugin {
|
||||
markdown = markdown.join('\n')
|
||||
return this._sendReport({
|
||||
markdown,
|
||||
subject: `[Xen Orchestra] ${log.status} − Backup report for ${jobName} ${
|
||||
STATUS_ICON[log.status]
|
||||
subject: `[Xen Orchestra] ${globalStatus} − Backup report for ${jobName} ${
|
||||
globalSuccess
|
||||
? ICON_SUCCESS
|
||||
: nFailures !== 0 ? ICON_FAILURE : ICON_SKIPPED
|
||||
}`,
|
||||
nagiosStatus: log.status === 'success' ? 0 : 2,
|
||||
nagiosMarkdown:
|
||||
log.status === 'success'
|
||||
? `[Xen Orchestra] [Success] Backup report for ${jobName}`
|
||||
: `[Xen Orchestra] [${
|
||||
nFailures !== 0 ? 'Failure' : 'Skipped'
|
||||
}] Backup report for ${jobName} - VMs : ${nagiosText.join(' ')}`,
|
||||
nagiosStatus: globalSuccess ? 0 : 2,
|
||||
nagiosMarkdown: globalSuccess
|
||||
? `[Xen Orchestra] [Success] Backup report for ${jobName}`
|
||||
: `[Xen Orchestra] [${
|
||||
nFailures !== 0 ? 'Failure' : 'Skipped'
|
||||
}] Backup report for ${jobName} - VMs : ${nagiosText.join(' ')}`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -573,9 +567,7 @@ class BackupReportsXoPlugin {
|
||||
const nSuccesses = nCalls - nFailures - nSkipped
|
||||
const globalStatus = globalSuccess
|
||||
? `Success`
|
||||
: nFailures !== 0
|
||||
? `Failure`
|
||||
: `Skipped`
|
||||
: nFailures !== 0 ? `Failure` : `Skipped`
|
||||
|
||||
let markdown = [
|
||||
`## Global status: ${globalStatus}`,
|
||||
@@ -633,9 +625,7 @@ class BackupReportsXoPlugin {
|
||||
subject: `[Xen Orchestra] ${globalStatus} − Backup report for ${tag} ${
|
||||
globalSuccess
|
||||
? ICON_SUCCESS
|
||||
: nFailures !== 0
|
||||
? ICON_FAILURE
|
||||
: ICON_SKIPPED
|
||||
: nFailures !== 0 ? ICON_FAILURE : ICON_SKIPPED
|
||||
}`,
|
||||
nagiosStatus: globalSuccess ? 0 : 2,
|
||||
nagiosMarkdown: globalSuccess
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
"lodash": "^4.17.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.49",
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"@babel/preset-flow": "^7.0.0-beta.49",
|
||||
"@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"
|
||||
|
||||
@@ -139,8 +139,8 @@ Handlebars.registerHelper(
|
||||
new Handlebars.SafeString(
|
||||
isFinite(+value) && +value !== 0
|
||||
? (value = round(value, 2)) > 0
|
||||
? `(<b style="color: green;">▲ ${value}%</b>)`
|
||||
: `(<b style="color: red;">▼ ${String(value).slice(1)}%</b>)`
|
||||
? `(<b style="color: green;">▲ ${value}</b>)`
|
||||
: `(<b style="color: red;">▼ ${String(value).slice(1)}</b>)`
|
||||
: ''
|
||||
)
|
||||
)
|
||||
@@ -270,16 +270,12 @@ async function getHostsStats ({ runningHosts, xo }) {
|
||||
|
||||
function getSrsStats (xoObjects) {
|
||||
return orderBy(
|
||||
map(filter(xoObjects, obj => obj.type === 'SR' && obj.size > 0), sr => {
|
||||
map(filter(xoObjects, { type: 'SR' }), sr => {
|
||||
const total = sr.size / gibPower
|
||||
const used = sr.physical_usage / gibPower
|
||||
let name = sr.name_label
|
||||
if (!sr.shared) {
|
||||
name += ` (${find(xoObjects, { id: sr.$container }).name_label})`
|
||||
}
|
||||
return {
|
||||
uuid: sr.uuid,
|
||||
name,
|
||||
name: sr.name_label,
|
||||
total,
|
||||
used,
|
||||
free: total - used,
|
||||
|
||||
@@ -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.9",
|
||||
"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,15 +34,15 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "7.0.0-beta.49",
|
||||
"@babel/polyfill": "7.0.0-beta.44",
|
||||
"@marsaud/smb2-promise": "^0.2.1",
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/fs": "^0.0.1",
|
||||
"@xen-orchestra/fs": "^0.0.0",
|
||||
"ajv": "^6.1.1",
|
||||
"app-conf": "^0.5.0",
|
||||
"archiver": "^2.1.0",
|
||||
"async-iterator-to-stream": "^1.0.1",
|
||||
"base64url": "^3.0.0",
|
||||
"base64url": "^2.0.0",
|
||||
"bind-property-descriptor": "^1.0.0",
|
||||
"blocked": "^1.2.1",
|
||||
"bluebird": "^3.5.1",
|
||||
@@ -59,10 +62,10 @@
|
||||
"express-session": "^1.15.6",
|
||||
"fatfs": "^0.10.4",
|
||||
"from2": "^2.3.0",
|
||||
"fs-extra": "^6.0.1",
|
||||
"fs-extra": "^5.0.0",
|
||||
"get-stream": "^3.0.0",
|
||||
"golike-defer": "^0.4.1",
|
||||
"hashy": "^0.7.1",
|
||||
"hashy": "^0.6.2",
|
||||
"helmet": "^3.9.0",
|
||||
"highland": "^2.11.1",
|
||||
"http-proxy": "^1.16.2",
|
||||
@@ -70,14 +73,14 @@
|
||||
"http-server-plus": "^0.10.0",
|
||||
"human-format": "^0.10.0",
|
||||
"is-redirect": "^1.0.0",
|
||||
"jest-worker": "^23.0.0",
|
||||
"jest-worker": "^22.4.3",
|
||||
"js-yaml": "^3.10.0",
|
||||
"json-rpc-peer": "^0.15.3",
|
||||
"json5": "^1.0.0",
|
||||
"julien-f-source-map-support": "0.1.0",
|
||||
"julien-f-unzip": "^0.2.1",
|
||||
"kindof": "^2.0.0",
|
||||
"level": "^4.0.0",
|
||||
"level": "^3.0.0",
|
||||
"level-party": "^3.0.4",
|
||||
"level-sublevel": "^6.6.1",
|
||||
"limit-concurrency-decorator": "^0.4.0",
|
||||
@@ -93,16 +96,16 @@
|
||||
"partial-stream": "0.0.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"pretty-format": "^23.0.0",
|
||||
"pretty-format": "^22.0.3",
|
||||
"promise-toolbox": "^0.9.5",
|
||||
"proxy-agent": "^3.0.0",
|
||||
"proxy-agent": "^2.1.0",
|
||||
"pug": "^2.0.0-rc.4",
|
||||
"pw": "^0.0.4",
|
||||
"redis": "^2.8.0",
|
||||
"schema-inspector": "^1.6.8",
|
||||
"semver": "^5.4.1",
|
||||
"serve-static": "^1.13.1",
|
||||
"split-lines": "^2.0.0",
|
||||
"split-lines": "^1.1.0",
|
||||
"stack-chain": "^2.0.0",
|
||||
"stoppable": "^1.0.5",
|
||||
"struct-fu": "^1.2.0",
|
||||
@@ -111,29 +114,29 @@
|
||||
"tmp": "^0.0.33",
|
||||
"uuid": "^3.0.1",
|
||||
"value-matcher": "^0.2.0",
|
||||
"vhd-lib": "^0.1.1",
|
||||
"vhd-lib": "^0.0.0",
|
||||
"ws": "^5.0.0",
|
||||
"xen-api": "^0.16.9",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.2.4",
|
||||
"xo-acl-resolver": "^0.2.3",
|
||||
"xo-collection": "^0.4.1",
|
||||
"xo-common": "^0.1.1",
|
||||
"xo-remote-parser": "^0.3",
|
||||
"xo-vmdk-to-vhd": "^0.1.2",
|
||||
"xo-vmdk-to-vhd": "0.1.0",
|
||||
"yazl": "^2.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.49",
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/plugin-proposal-decorators": "7.0.0-beta.49",
|
||||
"@babel/plugin-proposal-export-default-from": "7.0.0-beta.49",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.0.0-beta.49",
|
||||
"@babel/plugin-proposal-function-bind": "7.0.0-beta.49",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.0.0-beta.49",
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.0.0-beta.49",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"@babel/preset-flow": "7.0.0-beta.49",
|
||||
"@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",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { basename } from 'path'
|
||||
import { isEmpty, pickBy } from 'lodash'
|
||||
|
||||
import { safeDateFormat } from '../utils'
|
||||
|
||||
@@ -118,8 +117,8 @@ getJob.params = {
|
||||
},
|
||||
}
|
||||
|
||||
export async function runJob ({ id, schedule, vm }) {
|
||||
return this.runJobSequence([id], await this.getSchedule(schedule), vm)
|
||||
export async function runJob ({ id, schedule }) {
|
||||
return this.runJobSequence([id], await this.getSchedule(schedule))
|
||||
}
|
||||
|
||||
runJob.permission = 'admin'
|
||||
@@ -131,17 +130,12 @@ runJob.params = {
|
||||
schedule: {
|
||||
type: 'string',
|
||||
},
|
||||
vm: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export async function getAllLogs (filter) {
|
||||
const logs = await this.getBackupNgLogs()
|
||||
return isEmpty(filter) ? logs : pickBy(logs, filter)
|
||||
export function getAllLogs () {
|
||||
return this.getBackupNgLogs()
|
||||
}
|
||||
|
||||
getAllLogs.permission = 'admin'
|
||||
|
||||
@@ -76,21 +76,6 @@ export { restartAgent as restart_agent } // eslint-disable-line camelcase
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function setRemoteSyslogHost ({ host, syslogDestination }) {
|
||||
return this.getXapi(host).setRemoteSyslogHost(host._xapiId, syslogDestination)
|
||||
}
|
||||
|
||||
setRemoteSyslogHost.params = {
|
||||
id: { type: 'string' },
|
||||
syslogDestination: { type: 'string' },
|
||||
}
|
||||
|
||||
setRemoteSyslogHost.resolve = {
|
||||
host: ['id', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function start ({ host }) {
|
||||
return this.getXapi(host).powerOnHost(host._xapiId)
|
||||
}
|
||||
|
||||
@@ -204,8 +204,8 @@ export async function createNfs ({
|
||||
}
|
||||
|
||||
// if NFS options given
|
||||
if (nfsOptions) {
|
||||
deviceConfig.options = nfsOptions
|
||||
if (nfsVersion) {
|
||||
deviceConfig.options = nfsVersion
|
||||
}
|
||||
|
||||
const srRef = await xapi.call(
|
||||
|
||||
@@ -12,10 +12,6 @@ import { forEach, map, mapFilter, parseSize } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export function getHaValues () {
|
||||
return ['best-effort', 'restart', '']
|
||||
}
|
||||
|
||||
function checkPermissionOnSrs (vm, permission = 'operate') {
|
||||
const permissions = []
|
||||
forEach(vm.$VBDs, vbdId => {
|
||||
@@ -50,16 +46,11 @@ const extract = (obj, prop) => {
|
||||
export async function create (params) {
|
||||
const { user } = this
|
||||
const resourceSet = extract(params, 'resourceSet')
|
||||
const template = extract(params, 'template')
|
||||
if (
|
||||
resourceSet === undefined &&
|
||||
!(await this.hasPermissions(this.user.id, [
|
||||
[template.$pool, 'administrate'],
|
||||
]))
|
||||
) {
|
||||
if (resourceSet === undefined && user.permission !== 'admin') {
|
||||
throw unauthorized()
|
||||
}
|
||||
|
||||
const template = extract(params, 'template')
|
||||
params.template = template._xapiId
|
||||
|
||||
const xapi = this.getXapi(template)
|
||||
@@ -472,7 +463,7 @@ export async function migrate ({
|
||||
})
|
||||
}
|
||||
|
||||
if (!(await this.hasPermissions(this.session.get('user_id'), permissions))) {
|
||||
if (!await this.hasPermissions(this.session.get('user_id'), permissions)) {
|
||||
throw unauthorized()
|
||||
}
|
||||
|
||||
@@ -565,11 +556,11 @@ set.params = {
|
||||
|
||||
name_description: { type: 'string', optional: true },
|
||||
|
||||
high_availability: {
|
||||
optional: true,
|
||||
pattern: new RegExp(`^(${getHaValues().join('|')})$`),
|
||||
type: 'string',
|
||||
},
|
||||
// TODO: provides better filtering of values for HA possible values: "best-
|
||||
// effort" meaning "try to restart this VM if possible but don't consider the
|
||||
// Pool to be overcommitted if this is not possible"; "restart" meaning "this
|
||||
// VM should be restarted"; "" meaning "do not try to restart this VM"
|
||||
high_availability: { type: 'boolean', optional: true },
|
||||
|
||||
// Number of virtual CPUs to allocate.
|
||||
CPUs: { type: 'integer', optional: true },
|
||||
@@ -712,9 +703,9 @@ copy.resolve = {
|
||||
export async function convertToTemplate ({ vm }) {
|
||||
// Convert to a template requires pool admin permission.
|
||||
if (
|
||||
!(await this.hasPermissions(this.session.get('user_id'), [
|
||||
!await this.hasPermissions(this.session.get('user_id'), [
|
||||
[vm.$pool, 'administrate'],
|
||||
]))
|
||||
])
|
||||
) {
|
||||
throw unauthorized()
|
||||
}
|
||||
@@ -1274,9 +1265,7 @@ export async function createInterface ({
|
||||
await this.checkResourceSetConstraints(resourceSet, this.user.id, [
|
||||
network.id,
|
||||
])
|
||||
} else if (
|
||||
!(await this.hasPermissions(this.user.id, [[network.id, 'view']]))
|
||||
) {
|
||||
} else if (!await this.hasPermissions(this.user.id, [[network.id, 'view']])) {
|
||||
throw unauthorized()
|
||||
}
|
||||
|
||||
|
||||
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,664 +0,0 @@
|
||||
import appConf from 'app-conf'
|
||||
import bind from 'lodash/bind'
|
||||
import blocked from 'blocked'
|
||||
import createExpress from 'express'
|
||||
import createLogger from 'debug'
|
||||
import has from 'lodash/has'
|
||||
import helmet from 'helmet'
|
||||
import includes from 'lodash/includes'
|
||||
import proxyConsole from './proxy-console'
|
||||
import pw from 'pw'
|
||||
import serveStatic from 'serve-static'
|
||||
import startsWith from 'lodash/startsWith'
|
||||
import stoppable from 'stoppable'
|
||||
import WebSocket from 'ws'
|
||||
import { compile as compilePug } from 'pug'
|
||||
import { createServer as createProxyServer } from 'http-proxy'
|
||||
import { fromEvent } from 'promise-toolbox'
|
||||
import { join as joinPath } from 'path'
|
||||
|
||||
import JsonRpcPeer from 'json-rpc-peer'
|
||||
import { invalidCredentials } from 'xo-common/api-errors'
|
||||
import { ensureDir, readdir, readFile } from 'fs-extra'
|
||||
|
||||
import WebServer from 'http-server-plus'
|
||||
import Xo from './xo'
|
||||
import {
|
||||
forEach,
|
||||
isArray,
|
||||
isFunction,
|
||||
mapToArray,
|
||||
pFromCallback,
|
||||
} from './utils'
|
||||
|
||||
import bodyParser from 'body-parser'
|
||||
import connectFlash from 'connect-flash'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import expressSession from 'express-session'
|
||||
import passport from 'passport'
|
||||
import { parse as parseCookies } from 'cookie'
|
||||
import { Strategy as LocalStrategy } from 'passport-local'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const debug = createLogger('xo:main')
|
||||
|
||||
const warn = (...args) => {
|
||||
console.warn('[Warn]', ...args)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const DEPRECATED_ENTRIES = ['users', 'servers']
|
||||
|
||||
async function loadConfiguration () {
|
||||
const config = await appConf.load('xo-server', {
|
||||
appDir: joinPath(__dirname, '..'),
|
||||
ignoreUnknownFormats: true,
|
||||
})
|
||||
|
||||
debug('Configuration loaded.')
|
||||
|
||||
// Print a message if deprecated entries are specified.
|
||||
forEach(DEPRECATED_ENTRIES, entry => {
|
||||
if (has(config, entry)) {
|
||||
warn(`${entry} configuration is deprecated.`)
|
||||
}
|
||||
})
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function createExpressApp () {
|
||||
const app = createExpress()
|
||||
|
||||
app.use(helmet())
|
||||
|
||||
// Registers the cookie-parser and express-session middlewares,
|
||||
// necessary for connect-flash.
|
||||
app.use(cookieParser())
|
||||
app.use(
|
||||
expressSession({
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
|
||||
// TODO: should be in the config file.
|
||||
secret: 'CLWguhRZAZIXZcbrMzHCYmefxgweItKnS',
|
||||
})
|
||||
)
|
||||
|
||||
// Registers the connect-flash middleware, necessary for Passport to
|
||||
// display error messages.
|
||||
app.use(connectFlash())
|
||||
|
||||
// Registers the body-parser middleware, necessary for Passport to
|
||||
// access the username and password from the sign in form.
|
||||
app.use(bodyParser.urlencoded({ extended: false }))
|
||||
|
||||
// Registers Passport's middlewares.
|
||||
app.use(passport.initialize())
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
async function setUpPassport (express, xo) {
|
||||
const strategies = { __proto__: null }
|
||||
xo.registerPassportStrategy = strategy => {
|
||||
passport.use(strategy)
|
||||
|
||||
const { name } = strategy
|
||||
if (name !== 'local') {
|
||||
strategies[name] = strategy.label || name
|
||||
}
|
||||
}
|
||||
|
||||
// Registers the sign in form.
|
||||
const signInPage = compilePug(
|
||||
await readFile(joinPath(__dirname, '..', 'signin.pug'))
|
||||
)
|
||||
express.get('/signin', (req, res, next) => {
|
||||
res.send(
|
||||
signInPage({
|
||||
error: req.flash('error')[0],
|
||||
strategies,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
express.get('/signout', (req, res) => {
|
||||
res.clearCookie('token')
|
||||
res.redirect('/')
|
||||
})
|
||||
|
||||
const SIGNIN_STRATEGY_RE = /^\/signin\/([^/]+)(\/callback)?(:?\?.*)?$/
|
||||
express.use(async (req, res, next) => {
|
||||
const { url } = req
|
||||
const matches = url.match(SIGNIN_STRATEGY_RE)
|
||||
|
||||
if (matches) {
|
||||
return passport.authenticate(matches[1], async (err, user, info) => {
|
||||
if (err) {
|
||||
return next(err)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
req.flash('error', info ? info.message : 'Invalid credentials')
|
||||
return res.redirect('/signin')
|
||||
}
|
||||
|
||||
// The cookie will be set in via the next request because some
|
||||
// browsers do not save cookies on redirect.
|
||||
req.flash(
|
||||
'token',
|
||||
(await xo.createAuthenticationToken({ userId: user.id })).id
|
||||
)
|
||||
|
||||
// The session is only persistent for internal provider and if 'Remember me' box is checked
|
||||
req.flash(
|
||||
'session-is-persistent',
|
||||
matches[1] === 'local' && req.body['remember-me'] === 'on'
|
||||
)
|
||||
|
||||
res.redirect(req.flash('return-url')[0] || '/')
|
||||
})(req, res, next)
|
||||
}
|
||||
|
||||
const token = req.flash('token')[0]
|
||||
|
||||
if (token) {
|
||||
const isPersistent = req.flash('session-is-persistent')[0]
|
||||
|
||||
if (isPersistent) {
|
||||
// Persistent cookie ? => 1 year
|
||||
res.cookie('token', token, { maxAge: 1000 * 60 * 60 * 24 * 365 })
|
||||
} else {
|
||||
// Non-persistent : external provider as Github, Twitter...
|
||||
res.cookie('token', token)
|
||||
}
|
||||
|
||||
next()
|
||||
} else if (req.cookies.token) {
|
||||
next()
|
||||
} else if (
|
||||
/favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/.test(url)
|
||||
) {
|
||||
next()
|
||||
} else {
|
||||
req.flash('return-url', url)
|
||||
return res.redirect('/signin')
|
||||
}
|
||||
})
|
||||
|
||||
// Install the local strategy.
|
||||
xo.registerPassportStrategy(
|
||||
new LocalStrategy(async (username, password, done) => {
|
||||
try {
|
||||
const user = await xo.authenticateUser({ username, password })
|
||||
done(null, user)
|
||||
} catch (error) {
|
||||
done(null, false, { message: error.message })
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
async function registerPlugin (pluginPath, pluginName) {
|
||||
const plugin = require(pluginPath)
|
||||
const { description, version = 'unknown' } = (() => {
|
||||
try {
|
||||
return require(pluginPath + '/package.json')
|
||||
} catch (_) {
|
||||
return {}
|
||||
}
|
||||
})()
|
||||
|
||||
// Supports both “normal” CommonJS and Babel's ES2015 modules.
|
||||
const {
|
||||
default: factory = plugin,
|
||||
configurationSchema,
|
||||
configurationPresets,
|
||||
testSchema,
|
||||
} = plugin
|
||||
|
||||
// The default export can be either a factory or directly a plugin
|
||||
// instance.
|
||||
const instance = isFunction(factory)
|
||||
? factory({
|
||||
xo: this,
|
||||
getDataDir: () => {
|
||||
const dir = `${this._config.datadir}/${pluginName}`
|
||||
return ensureDir(dir).then(() => dir)
|
||||
},
|
||||
})
|
||||
: factory
|
||||
|
||||
await this.registerPlugin(
|
||||
pluginName,
|
||||
instance,
|
||||
configurationSchema,
|
||||
configurationPresets,
|
||||
description,
|
||||
testSchema,
|
||||
version
|
||||
)
|
||||
}
|
||||
|
||||
const debugPlugin = createLogger('xo:plugin')
|
||||
|
||||
function registerPluginWrapper (pluginPath, pluginName) {
|
||||
debugPlugin('register %s', pluginName)
|
||||
|
||||
return registerPlugin.call(this, pluginPath, pluginName).then(
|
||||
() => {
|
||||
debugPlugin(`successfully register ${pluginName}`)
|
||||
},
|
||||
error => {
|
||||
debugPlugin(`failed register ${pluginName}`)
|
||||
debugPlugin(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const PLUGIN_PREFIX = 'xo-server-'
|
||||
const PLUGIN_PREFIX_LENGTH = PLUGIN_PREFIX.length
|
||||
|
||||
async function registerPluginsInPath (path) {
|
||||
const files = await readdir(path).catch(error => {
|
||||
if (error.code === 'ENOENT') {
|
||||
return []
|
||||
}
|
||||
throw error
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
mapToArray(files, name => {
|
||||
if (startsWith(name, PLUGIN_PREFIX)) {
|
||||
return registerPluginWrapper.call(
|
||||
this,
|
||||
`${path}/${name}`,
|
||||
name.slice(PLUGIN_PREFIX_LENGTH)
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async function registerPlugins (xo) {
|
||||
await Promise.all(
|
||||
mapToArray(
|
||||
[`${__dirname}/../node_modules/`, '/usr/local/lib/node_modules/'],
|
||||
xo::registerPluginsInPath
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
async function makeWebServerListen (
|
||||
webServer,
|
||||
{
|
||||
certificate,
|
||||
|
||||
// The properties was called `certificate` before.
|
||||
cert = certificate,
|
||||
|
||||
key,
|
||||
...opts
|
||||
}
|
||||
) {
|
||||
if (cert && key) {
|
||||
;[opts.cert, opts.key] = await Promise.all([readFile(cert), readFile(key)])
|
||||
if (opts.key.includes('ENCRYPTED')) {
|
||||
opts.passphrase = await new Promise(resolve => {
|
||||
console.log('Encrypted key %s', key)
|
||||
process.stdout.write(`Enter pass phrase: `)
|
||||
pw(resolve)
|
||||
})
|
||||
}
|
||||
}
|
||||
try {
|
||||
const niceAddress = await webServer.listen(opts)
|
||||
debug(`Web server listening on ${niceAddress}`)
|
||||
} catch (error) {
|
||||
if (error.niceAddress) {
|
||||
warn(`Web server could not listen on ${error.niceAddress}`)
|
||||
|
||||
const { code } = error
|
||||
if (code === 'EACCES') {
|
||||
warn(' Access denied.')
|
||||
warn(' Ports < 1024 are often reserved to privileges users.')
|
||||
} else if (code === 'EADDRINUSE') {
|
||||
warn(' Address already in use.')
|
||||
}
|
||||
} else {
|
||||
warn('Web server could not listen:', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createWebServer ({ listen, listenOptions }) {
|
||||
const webServer = stoppable(new WebServer())
|
||||
|
||||
await Promise.all(
|
||||
mapToArray(listen, opts =>
|
||||
makeWebServerListen(webServer, { ...listenOptions, ...opts })
|
||||
)
|
||||
)
|
||||
|
||||
return webServer
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const setUpProxies = (express, opts, xo) => {
|
||||
if (!opts) {
|
||||
return
|
||||
}
|
||||
|
||||
const proxy = createProxyServer({
|
||||
ignorePath: true,
|
||||
}).on('error', error => console.error(error))
|
||||
|
||||
// TODO: sort proxies by descending prefix length.
|
||||
|
||||
// HTTP request proxy.
|
||||
express.use((req, res, next) => {
|
||||
const { url } = req
|
||||
|
||||
for (const prefix in opts) {
|
||||
if (startsWith(url, prefix)) {
|
||||
const target = opts[prefix]
|
||||
|
||||
proxy.web(req, res, {
|
||||
target: target + url.slice(prefix.length),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
// WebSocket proxy.
|
||||
const webSocketServer = new WebSocket.Server({
|
||||
noServer: true,
|
||||
})
|
||||
xo.on('stop', () => pFromCallback(cb => webSocketServer.close(cb)))
|
||||
|
||||
express.on('upgrade', (req, socket, head) => {
|
||||
const { url } = req
|
||||
|
||||
for (const prefix in opts) {
|
||||
if (startsWith(url, prefix)) {
|
||||
const target = opts[prefix]
|
||||
|
||||
proxy.ws(req, socket, head, {
|
||||
target: target + url.slice(prefix.length),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const setUpStaticFiles = (express, opts) => {
|
||||
forEach(opts, (paths, url) => {
|
||||
if (!isArray(paths)) {
|
||||
paths = [paths]
|
||||
}
|
||||
|
||||
forEach(paths, path => {
|
||||
debug('Setting up %s → %s', url, path)
|
||||
|
||||
express.use(url, serveStatic(path))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const setUpApi = (webServer, xo, verboseLogsOnErrors) => {
|
||||
const webSocketServer = new WebSocket.Server({
|
||||
noServer: true,
|
||||
})
|
||||
xo.on('stop', () => pFromCallback(cb => webSocketServer.close(cb)))
|
||||
|
||||
const onConnection = (socket, upgradeReq) => {
|
||||
const { remoteAddress } = upgradeReq.socket
|
||||
|
||||
debug('+ WebSocket connection (%s)', remoteAddress)
|
||||
|
||||
// Create the abstract XO object for this connection.
|
||||
const connection = xo.createUserConnection()
|
||||
connection.once('close', () => {
|
||||
socket.close()
|
||||
})
|
||||
|
||||
// Create the JSON-RPC server for this connection.
|
||||
const jsonRpc = new JsonRpcPeer(message => {
|
||||
if (message.type === 'request') {
|
||||
return xo.callApiMethod(connection, message.method, message.params)
|
||||
}
|
||||
})
|
||||
connection.notify = bind(jsonRpc.notify, jsonRpc)
|
||||
|
||||
// Close the XO connection with this WebSocket.
|
||||
socket.once('close', () => {
|
||||
debug('- WebSocket connection (%s)', remoteAddress)
|
||||
|
||||
connection.close()
|
||||
})
|
||||
|
||||
// Connect the WebSocket to the JSON-RPC server.
|
||||
socket.on('message', message => {
|
||||
jsonRpc.write(message)
|
||||
})
|
||||
|
||||
const onSend = error => {
|
||||
if (error) {
|
||||
warn('WebSocket send:', error.stack)
|
||||
}
|
||||
}
|
||||
jsonRpc.on('data', data => {
|
||||
// The socket may have been closed during the API method
|
||||
// execution.
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(data, onSend)
|
||||
}
|
||||
})
|
||||
}
|
||||
webServer.on('upgrade', (req, socket, head) => {
|
||||
if (req.url === '/api/') {
|
||||
webSocketServer.handleUpgrade(req, socket, head, ws =>
|
||||
onConnection(ws, req)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const CONSOLE_PROXY_PATH_RE = /^\/api\/consoles\/(.*)$/
|
||||
|
||||
const setUpConsoleProxy = (webServer, xo) => {
|
||||
const webSocketServer = new WebSocket.Server({
|
||||
noServer: true,
|
||||
})
|
||||
xo.on('stop', () => pFromCallback(cb => webSocketServer.close(cb)))
|
||||
|
||||
webServer.on('upgrade', async (req, socket, head) => {
|
||||
const matches = CONSOLE_PROXY_PATH_RE.exec(req.url)
|
||||
if (!matches) {
|
||||
return
|
||||
}
|
||||
|
||||
const [, id] = matches
|
||||
try {
|
||||
// TODO: factorize permissions checking in an Express middleware.
|
||||
{
|
||||
const { token } = parseCookies(req.headers.cookie)
|
||||
|
||||
const user = await xo.authenticateUser({ token })
|
||||
if (!(await xo.hasPermissions(user.id, [[id, 'operate']]))) {
|
||||
throw invalidCredentials()
|
||||
}
|
||||
|
||||
const { remoteAddress } = socket
|
||||
debug('+ Console proxy (%s - %s)', user.name, remoteAddress)
|
||||
socket.on('close', () => {
|
||||
debug('- Console proxy (%s - %s)', user.name, remoteAddress)
|
||||
})
|
||||
}
|
||||
|
||||
const xapi = xo.getXapi(id, ['VM', 'VM-controller'])
|
||||
const vmConsole = xapi.getVmConsole(id)
|
||||
|
||||
// FIXME: lost connection due to VM restart is not detected.
|
||||
webSocketServer.handleUpgrade(req, socket, head, connection => {
|
||||
proxyConsole(connection, vmConsole, xapi.sessionId)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error((error && error.stack) || error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const USAGE = (({ name, version }) => `Usage: ${name} [--safe-mode]
|
||||
|
||||
${name} v${version}`)(require('../package.json'))
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default async function main (args) {
|
||||
if (includes(args, '--help') || includes(args, '-h')) {
|
||||
return USAGE
|
||||
}
|
||||
|
||||
{
|
||||
const debug = createLogger('xo:perf')
|
||||
blocked(
|
||||
ms => {
|
||||
debug('blocked for %sms', ms | 0)
|
||||
},
|
||||
{
|
||||
threshold: 500,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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 :-)')
|
||||
}
|
||||
@@ -13,10 +13,6 @@ export default {
|
||||
type: 'string',
|
||||
description: 'identifier of this job',
|
||||
},
|
||||
scheduleId: {
|
||||
type: 'string',
|
||||
description: 'identifier of the schedule which ran the job',
|
||||
},
|
||||
key: {
|
||||
type: 'string',
|
||||
},
|
||||
|
||||
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])
|
||||
)
|
||||
@@ -146,7 +146,6 @@ const TRANSFORMS = {
|
||||
license_params: obj.license_params,
|
||||
license_server: obj.license_server,
|
||||
license_expiry: toTimestamp(obj.license_params.expiry),
|
||||
logging: obj.logging,
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label,
|
||||
memory: (function () {
|
||||
@@ -187,14 +186,9 @@ const TRANSFORMS = {
|
||||
}
|
||||
}),
|
||||
agentStartTime: toTimestamp(otherConfig.agent_start_time),
|
||||
rebootRequired:
|
||||
softwareVersion.product_brand === 'XCP-ng'
|
||||
? toTimestamp(otherConfig.boot_time) <
|
||||
+otherConfig.rpm_patch_installation_time
|
||||
: !isEmpty(obj.updates_requiring_reboot),
|
||||
rebootRequired: !isEmpty(obj.updates_requiring_reboot),
|
||||
tags: obj.tags,
|
||||
version: softwareVersion.product_version,
|
||||
productBrand: softwareVersion.product_brand,
|
||||
|
||||
// TODO: dedupe.
|
||||
PIFs: link(obj, 'PIFs'),
|
||||
@@ -233,16 +227,12 @@ const TRANSFORMS = {
|
||||
return
|
||||
}
|
||||
|
||||
if (guestMetrics === undefined) {
|
||||
if (!guestMetrics) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { major, minor } = guestMetrics.PV_drivers_version
|
||||
|
||||
if (major === undefined || minor === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
return {
|
||||
major,
|
||||
minor,
|
||||
@@ -302,7 +292,8 @@ const TRANSFORMS = {
|
||||
}
|
||||
})(),
|
||||
|
||||
high_availability: obj.ha_restart_priority,
|
||||
// TODO: there is two possible value: "best-effort" and "restart"
|
||||
high_availability: Boolean(obj.ha_restart_priority),
|
||||
|
||||
memory: (function () {
|
||||
const dynamicMin = +obj.memory_dynamic_min
|
||||
|
||||
@@ -426,14 +426,6 @@ export default class Xapi extends XapiBase {
|
||||
await this.call('host.restart_agent', this.getObject(hostId).$ref)
|
||||
}
|
||||
|
||||
async setRemoteSyslogHost (hostId, syslogDestination) {
|
||||
const host = this.getObject(hostId)
|
||||
await this.call('host.set_logging', host.$ref, {
|
||||
syslog_destination: syslogDestination,
|
||||
})
|
||||
await this.call('host.syslog_reconfigure', host.$ref)
|
||||
}
|
||||
|
||||
async shutdownHost (hostId, force = false) {
|
||||
const host = this.getObject(hostId)
|
||||
|
||||
@@ -824,14 +816,12 @@ export default class Xapi extends XapiBase {
|
||||
} = {}
|
||||
): Promise<DeltaVmExport> {
|
||||
let vm = this.getObject(vmId)
|
||||
|
||||
if (!bypassVdiChainsCheck) {
|
||||
this._assertHealthyVdiChains(vm)
|
||||
}
|
||||
// do not use the snapshot name in the delta export
|
||||
const exportedNameLabel = vm.name_label
|
||||
if (!vm.is_a_snapshot) {
|
||||
if (!bypassVdiChainsCheck) {
|
||||
this._assertHealthyVdiChains(vm)
|
||||
}
|
||||
|
||||
vm = await this._snapshotVm($cancelToken, vm, snapshotNameLabel)
|
||||
$defer.onFailure(() => this._deleteVm(vm))
|
||||
}
|
||||
@@ -968,9 +958,7 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
|
||||
if (!baseVm) {
|
||||
throw new Error(
|
||||
`could not find the base VM (copy of ${remoteBaseVmUuid})`
|
||||
)
|
||||
throw new Error('could not find the base VM')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1154,9 +1142,7 @@ export default class Xapi extends XapiBase {
|
||||
vdis[vdi.$ref] =
|
||||
mapVdisSrs && mapVdisSrs[vdi.$id]
|
||||
? hostXapi.getObject(mapVdisSrs[vdi.$id]).$ref
|
||||
: sr !== undefined
|
||||
? hostXapi.getObject(sr).$ref
|
||||
: defaultSr.$ref // Will error if there are no default SR.
|
||||
: sr !== undefined ? hostXapi.getObject(sr).$ref : defaultSr.$ref // Will error if there are no default SR.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,15 +35,8 @@ declare class XapiObject {
|
||||
}
|
||||
|
||||
type Id = string | XapiObject
|
||||
|
||||
declare export class Vbd extends XapiObject {
|
||||
type: string;
|
||||
VDI: string;
|
||||
}
|
||||
|
||||
declare export class Vm extends XapiObject {
|
||||
$snapshots: Vm[];
|
||||
$VBDs: Vbd[];
|
||||
is_a_snapshot: boolean;
|
||||
is_a_template: boolean;
|
||||
name_label: string;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import deferrable from 'golike-defer'
|
||||
import every from 'lodash/every'
|
||||
import filter from 'lodash/filter'
|
||||
import find from 'lodash/find'
|
||||
import includes from 'lodash/includes'
|
||||
import isObject from 'lodash/isObject'
|
||||
@@ -12,7 +11,6 @@ import unzip from 'julien-f-unzip'
|
||||
|
||||
import { debounce } from '../../decorators'
|
||||
import {
|
||||
asyncMap,
|
||||
ensureArray,
|
||||
forEach,
|
||||
mapFilter,
|
||||
@@ -151,12 +149,9 @@ export default {
|
||||
},
|
||||
|
||||
async listMissingPoolPatchesOnHost (hostId) {
|
||||
const host = this.getObject(hostId)
|
||||
// Returns an array to not break compatibility.
|
||||
return mapToArray(
|
||||
await (host.software_version.product_brand === 'XCP-ng'
|
||||
? this._xcpListHostUpdates(host)
|
||||
: this._listMissingPoolPatchesOnHost(host))
|
||||
await this._listMissingPoolPatchesOnHost(this.getObject(hostId))
|
||||
)
|
||||
},
|
||||
|
||||
@@ -445,14 +440,8 @@ export default {
|
||||
},
|
||||
|
||||
async installAllPoolPatchesOnHost (hostId) {
|
||||
const host = this.getObject(hostId)
|
||||
if (host.software_version.product_brand === 'XCP-ng') {
|
||||
return this._xcpInstallHostUpdates(host)
|
||||
}
|
||||
return this._installAllPoolPatchesOnHost(host)
|
||||
},
|
||||
let host = this.getObject(hostId)
|
||||
|
||||
async _installAllPoolPatchesOnHost (host) {
|
||||
const installableByUuid =
|
||||
host.license_params.sku_type !== 'free'
|
||||
? await this._listMissingPoolPatchesOnHost(host)
|
||||
@@ -490,13 +479,6 @@ export default {
|
||||
},
|
||||
|
||||
async installAllPoolPatchesOnAllHosts () {
|
||||
if (this.pool.$master.software_version.product_brand === 'XCP-ng') {
|
||||
return this._xcpInstallAllPoolUpdatesOnHost()
|
||||
}
|
||||
return this._installAllPoolPatchesOnAllHosts()
|
||||
},
|
||||
|
||||
async _installAllPoolPatchesOnAllHosts () {
|
||||
const installableByUuid = assign(
|
||||
{},
|
||||
...(await Promise.all(
|
||||
@@ -536,47 +518,4 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// XCP-ng dedicated zone for patching
|
||||
// ----------------------------------
|
||||
|
||||
// list all yum updates available for a XCP-ng host
|
||||
async _xcpListHostUpdates (host) {
|
||||
return JSON.parse(
|
||||
await this.call(
|
||||
'host.call_plugin',
|
||||
host.$ref,
|
||||
'updater.py',
|
||||
'check_update',
|
||||
{}
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
// install all yum updates for a XCP-ng host
|
||||
async _xcpInstallHostUpdates (host) {
|
||||
const update = await this.call(
|
||||
'host.call_plugin',
|
||||
host.$ref,
|
||||
'updater.py',
|
||||
'update',
|
||||
{}
|
||||
)
|
||||
|
||||
if (JSON.parse(update).exit !== 0) {
|
||||
throw new Error('Update install failed')
|
||||
} else {
|
||||
await this._updateObjectMapProperty(host, 'other_config', {
|
||||
rpm_patch_installation_time: String(Date.now() / 1000),
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// install all yum updates for all XCP-ng hosts in a give pool
|
||||
async _xcpInstallAllPoolUpdatesOnHost () {
|
||||
await asyncMap(filter(this.objects.all, { $type: 'host' }), host =>
|
||||
this._xcpInstallHostUpdates(host)
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -310,7 +310,11 @@ export default {
|
||||
|
||||
highAvailability: {
|
||||
set (ha, vm) {
|
||||
return this.call('VM.set_ha_restart_priority', vm.$ref, ha)
|
||||
return this.call(
|
||||
'VM.set_ha_restart_priority',
|
||||
vm.$ref,
|
||||
ha ? 'restart' : ''
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import { forEach } from 'lodash'
|
||||
|
||||
const isSkippedError = error =>
|
||||
error.message === 'no disks found' ||
|
||||
error.message === 'no such object' ||
|
||||
error.message === 'no VMs match this pattern' ||
|
||||
error.message === 'unhealthy VDI chain'
|
||||
|
||||
const getStatus = (
|
||||
error,
|
||||
status = error === undefined ? 'success' : 'failure'
|
||||
) => (status === 'failure' && isSkippedError(error) ? 'skipped' : status)
|
||||
|
||||
const computeStatus = (status, tasks) => {
|
||||
if (status === 'failure' || tasks === undefined) {
|
||||
return status
|
||||
}
|
||||
|
||||
for (let i = 0, n = tasks.length; i < n; ++i) {
|
||||
const taskStatus = tasks[i].status
|
||||
if (taskStatus === 'failure') {
|
||||
return taskStatus
|
||||
}
|
||||
if (taskStatus === 'skipped') {
|
||||
status = taskStatus
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
export default {
|
||||
async getBackupNgLogs (runId?: string) {
|
||||
const { runningJobs } = this
|
||||
const consolidated = {}
|
||||
const started = {}
|
||||
forEach(await this.getLogs('jobs'), ({ data, time, message }, id) => {
|
||||
const { event } = data
|
||||
if (event === 'job.start') {
|
||||
if (
|
||||
(data.type === 'backup' || data.key === undefined) &&
|
||||
(runId === undefined || runId === id)
|
||||
) {
|
||||
const { jobId } = data
|
||||
consolidated[id] = started[id] = {
|
||||
data: data.data,
|
||||
id,
|
||||
jobId,
|
||||
start: time,
|
||||
status: runningJobs[jobId] === id ? 'pending' : 'interrupted',
|
||||
}
|
||||
}
|
||||
} else if (event === 'job.end') {
|
||||
const { runJobId } = data
|
||||
const log = started[runJobId]
|
||||
if (log !== undefined) {
|
||||
delete started[runJobId]
|
||||
log.end = time
|
||||
log.status = computeStatus(
|
||||
getStatus((log.result = data.error)),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.start') {
|
||||
const parent = started[data.parentId]
|
||||
if (parent !== undefined) {
|
||||
;(parent.tasks || (parent.tasks = [])).push(
|
||||
(started[id] = {
|
||||
data: data.data,
|
||||
id,
|
||||
message,
|
||||
start: time,
|
||||
status: parent.status,
|
||||
})
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.end') {
|
||||
const { taskId } = data
|
||||
const log = started[taskId]
|
||||
if (log !== undefined) {
|
||||
// TODO: merge/transfer work-around
|
||||
delete started[taskId]
|
||||
log.end = time
|
||||
log.status = computeStatus(
|
||||
getStatus((log.result = data.result), data.status),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'jobCall.start') {
|
||||
const parent = started[data.runJobId]
|
||||
if (parent !== undefined) {
|
||||
;(parent.tasks || (parent.tasks = [])).push(
|
||||
(started[id] = {
|
||||
data: {
|
||||
type: 'VM',
|
||||
id: data.params.id,
|
||||
},
|
||||
id,
|
||||
start: time,
|
||||
status: parent.status,
|
||||
})
|
||||
)
|
||||
}
|
||||
} else if (event === 'jobCall.end') {
|
||||
const { runCallId } = data
|
||||
const log = started[runCallId]
|
||||
if (log !== undefined) {
|
||||
delete started[runCallId]
|
||||
log.end = time
|
||||
log.status = computeStatus(
|
||||
getStatus((log.result = data.error)),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
return runId === undefined ? consolidated : consolidated[runId]
|
||||
},
|
||||
}
|
||||
@@ -3,12 +3,19 @@
|
||||
// $FlowFixMe
|
||||
import type RemoteHandler from '@xen-orchestra/fs'
|
||||
import defer from 'golike-defer'
|
||||
import limitConcurrency from 'limit-concurrency-decorator'
|
||||
import { type Pattern, createPredicate } from 'value-matcher'
|
||||
import { type Readable, PassThrough } from 'stream'
|
||||
import { basename, dirname } from 'path'
|
||||
import { isEmpty, last, mapValues, noop, some, sum, values } from 'lodash'
|
||||
import { fromEvent as pFromEvent, timeout as pTimeout } from 'promise-toolbox'
|
||||
import {
|
||||
forEach,
|
||||
groupBy,
|
||||
isEmpty,
|
||||
last,
|
||||
mapValues,
|
||||
noop,
|
||||
values,
|
||||
} from 'lodash'
|
||||
import { timeout as pTimeout } from 'promise-toolbox'
|
||||
import Vhd, {
|
||||
chainVhd,
|
||||
createSyntheticStream as createVhdReadStream,
|
||||
@@ -33,11 +40,10 @@ import {
|
||||
|
||||
import { translateLegacyJob } from './migration'
|
||||
|
||||
export type Mode = 'full' | 'delta'
|
||||
export type ReportWhen = 'always' | 'failure' | 'never'
|
||||
type Mode = 'full' | 'delta'
|
||||
type ReportWhen = 'always' | 'failure' | 'never'
|
||||
|
||||
type Settings = {|
|
||||
concurrency?: number,
|
||||
deleteFirst?: boolean,
|
||||
exportRetention?: number,
|
||||
reportWhen?: ReportWhen,
|
||||
@@ -84,6 +90,33 @@ type MetadataFull = {|
|
||||
|}
|
||||
type Metadata = MetadataDelta | MetadataFull
|
||||
|
||||
type ConsolidatedJob = {|
|
||||
duration?: number,
|
||||
end?: number,
|
||||
error?: Object,
|
||||
id: string,
|
||||
jobId: string,
|
||||
mode: Mode,
|
||||
start: number,
|
||||
type: 'backup' | 'call',
|
||||
userId: string,
|
||||
|}
|
||||
type ConsolidatedTask = {|
|
||||
data?: Object,
|
||||
duration?: number,
|
||||
end?: number,
|
||||
parentId: string,
|
||||
message: string,
|
||||
result?: Object,
|
||||
start: number,
|
||||
status: 'canceled' | 'failure' | 'success',
|
||||
taskId: string,
|
||||
|}
|
||||
type ConsolidatedBackupNgLog = {
|
||||
roots: Array<ConsolidatedJob>,
|
||||
[parentId: string]: Array<ConsolidatedTask>,
|
||||
}
|
||||
|
||||
const compareSnapshotTime = (a: Vm, b: Vm): number =>
|
||||
a.snapshot_time < b.snapshot_time ? -1 : 1
|
||||
|
||||
@@ -97,9 +130,7 @@ const compareTimestamp = (a: Metadata, b: Metadata): number =>
|
||||
const getOldEntries = <T>(retention: number, entries?: T[]): T[] =>
|
||||
entries === undefined
|
||||
? []
|
||||
: --retention > 0
|
||||
? entries.slice(0, -retention)
|
||||
: entries
|
||||
: --retention > 0 ? entries.slice(0, -retention) : entries
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
deleteFirst: false,
|
||||
@@ -128,7 +159,6 @@ const getSetting = (
|
||||
const BACKUP_DIR = 'xo-vm-backups'
|
||||
const getVmBackupDir = (uuid: string) => `${BACKUP_DIR}/${uuid}`
|
||||
|
||||
const isHiddenFile = (filename: string) => filename[0] === '.'
|
||||
const isMetadataFile = (filename: string) => filename.endsWith('.json')
|
||||
const isVhd = (filename: string) => filename.endsWith('.vhd')
|
||||
|
||||
@@ -274,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
|
||||
@@ -301,9 +330,7 @@ const wrapTask = async <T>(opts: any, task: Promise<T>): Promise<T> => {
|
||||
result:
|
||||
result === undefined
|
||||
? value
|
||||
: typeof result === 'function'
|
||||
? result(value)
|
||||
: result,
|
||||
: typeof result === 'function' ? result(value) : result,
|
||||
status: 'success',
|
||||
taskId,
|
||||
})
|
||||
@@ -342,9 +369,7 @@ const wrapTaskFn = <T>(
|
||||
result:
|
||||
result === undefined
|
||||
? value
|
||||
: typeof result === 'function'
|
||||
? result(value)
|
||||
: result,
|
||||
: typeof result === 'function' ? result(value) : result,
|
||||
status: 'success',
|
||||
taskId,
|
||||
})
|
||||
@@ -406,7 +431,6 @@ export default class BackupNg {
|
||||
app.on('start', () => {
|
||||
const executor: Executor = async ({
|
||||
cancelToken,
|
||||
data: vmId,
|
||||
job: job_,
|
||||
logger,
|
||||
runJobId,
|
||||
@@ -417,21 +441,18 @@ export default class BackupNg {
|
||||
}
|
||||
|
||||
const job: BackupJob = (job_: any)
|
||||
let vms: $Dict<Vm>
|
||||
if (vmId === undefined) {
|
||||
vms = app.getObjects({
|
||||
filter: createPredicate({
|
||||
type: 'VM',
|
||||
...job.vms,
|
||||
}),
|
||||
})
|
||||
if (isEmpty(vms)) {
|
||||
throw new Error('no VMs match this pattern')
|
||||
}
|
||||
const vms: $Dict<Vm> = app.getObjects({
|
||||
filter: createPredicate({
|
||||
type: 'VM',
|
||||
...job.vms,
|
||||
}),
|
||||
})
|
||||
if (isEmpty(vms)) {
|
||||
throw new Error('no VMs match this pattern')
|
||||
}
|
||||
const jobId = job.id
|
||||
const scheduleId = schedule.id
|
||||
let handleVm = async vm => {
|
||||
await asyncMap(vms, async vm => {
|
||||
const { name_label: name, uuid } = vm
|
||||
const taskId: string = logger.notice(
|
||||
`Starting backup of ${name}. (${jobId})`,
|
||||
@@ -483,21 +504,7 @@ export default class BackupNg {
|
||||
: serializeError(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (vmId !== undefined) {
|
||||
return handleVm(await app.getObject(vmId))
|
||||
}
|
||||
|
||||
const concurrency: number | void = getSetting(
|
||||
job.settings,
|
||||
'concurrency',
|
||||
''
|
||||
)
|
||||
if (concurrency !== undefined) {
|
||||
handleVm = limitConcurrency(concurrency)(handleVm)
|
||||
}
|
||||
await asyncMap(vms, handleVm)
|
||||
})
|
||||
}
|
||||
app.registerJobExecutor('backup', executor)
|
||||
})
|
||||
@@ -652,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
|
||||
@@ -685,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,
|
||||
@@ -730,15 +736,6 @@ export default class BackupNg {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!some(
|
||||
vm.$VBDs,
|
||||
vbd => vbd.type === 'Disk' && vbd.VDI !== 'OpaqueRef:NULL'
|
||||
)
|
||||
) {
|
||||
throw new Error('no disks found')
|
||||
}
|
||||
|
||||
const snapshots = vm.$snapshots
|
||||
.filter(_ => _.other_config['xo:backup:job'] === jobId)
|
||||
.sort(compareSnapshotTime)
|
||||
@@ -872,7 +869,9 @@ export default class BackupNg {
|
||||
logger,
|
||||
message: 'transfer',
|
||||
parentId: taskId,
|
||||
result: () => ({ size: xva.size }),
|
||||
result: {
|
||||
size: 0,
|
||||
},
|
||||
},
|
||||
writeStream(fork, handler, dataFilename)
|
||||
)
|
||||
@@ -915,7 +914,9 @@ export default class BackupNg {
|
||||
logger,
|
||||
message: 'transfer',
|
||||
parentId: taskId,
|
||||
result: () => ({ size: xva.size }),
|
||||
result: {
|
||||
size: 0,
|
||||
},
|
||||
},
|
||||
xapi._importVm($cancelToken, fork, sr, vm =>
|
||||
xapi._setObjectProperties(vm, {
|
||||
@@ -1047,7 +1048,9 @@ export default class BackupNg {
|
||||
logger,
|
||||
message: 'merge',
|
||||
parentId: taskId,
|
||||
result: size => ({ size }),
|
||||
result: {
|
||||
size: 0,
|
||||
},
|
||||
},
|
||||
this._deleteDeltaVmBackups(handler, oldBackups)
|
||||
)
|
||||
@@ -1064,7 +1067,9 @@ export default class BackupNg {
|
||||
logger,
|
||||
message: 'transfer',
|
||||
parentId: taskId,
|
||||
result: size => ({ size }),
|
||||
result: {
|
||||
size: 0,
|
||||
},
|
||||
},
|
||||
asyncMap(
|
||||
fork.vdis,
|
||||
@@ -1076,16 +1081,11 @@ export default class BackupNg {
|
||||
let parentPath
|
||||
if (isDelta) {
|
||||
const vdiDir = dirname(path)
|
||||
parentPath = (await handler.list(vdiDir, {
|
||||
filter: filename =>
|
||||
!isHiddenFile(filename) && isVhd(filename),
|
||||
prependDir: true,
|
||||
}))
|
||||
const parent = (await handler.list(vdiDir))
|
||||
.filter(isVhd)
|
||||
.sort()
|
||||
.pop()
|
||||
|
||||
// ensure parent exists and is a valid VHD
|
||||
await new Vhd(handler, parentPath).readHeaderAndFooter()
|
||||
parentPath = `${vdiDir}/${parent}`
|
||||
}
|
||||
|
||||
await writeStream(
|
||||
@@ -1103,11 +1103,10 @@ export default class BackupNg {
|
||||
if (isDelta) {
|
||||
await chainVhd(handler, parentPath, handler, path)
|
||||
}
|
||||
|
||||
return handler.getSize(path)
|
||||
})
|
||||
).then(sum)
|
||||
)
|
||||
)
|
||||
|
||||
await handler.outputFile(metadataFilename, jsonMetadata)
|
||||
|
||||
if (!deleteFirst) {
|
||||
@@ -1145,7 +1144,9 @@ export default class BackupNg {
|
||||
logger,
|
||||
message: 'transfer',
|
||||
parentId: taskId,
|
||||
result: ({ transferSize }) => ({ size: transferSize }),
|
||||
result: {
|
||||
size: 0,
|
||||
},
|
||||
},
|
||||
xapi.importDeltaVm(fork, {
|
||||
disableStartAfterImport: false, // we'll take care of that
|
||||
@@ -1184,17 +1185,19 @@ export default class BackupNg {
|
||||
async _deleteDeltaVmBackups (
|
||||
handler: RemoteHandler,
|
||||
backups: MetadataDelta[]
|
||||
): Promise<number> {
|
||||
return asyncMap(backups, async backup => {
|
||||
): Promise<void> {
|
||||
// TODO: remove VHD as well
|
||||
await asyncMap(backups, async backup => {
|
||||
const filename = ((backup._filename: any): string)
|
||||
|
||||
await handler.unlink(filename)
|
||||
|
||||
return asyncMap(backup.vhds, _ =>
|
||||
// $FlowFixMe injected $defer param
|
||||
this._deleteVhd(handler, resolveRelativeFromFile(filename, _))
|
||||
).then(sum)
|
||||
}).then(sum)
|
||||
return Promise.all([
|
||||
handler.unlink(filename),
|
||||
asyncMap(backup.vhds, _ =>
|
||||
// $FlowFixMe injected $defer param
|
||||
this._deleteVhd(handler, resolveRelativeFromFile(filename, _))
|
||||
),
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
async _deleteFullVmBackups (
|
||||
@@ -1212,50 +1215,35 @@ export default class BackupNg {
|
||||
|
||||
// FIXME: synchronize by job/VDI, otherwise it can cause issues with the merge
|
||||
@defer
|
||||
async _deleteVhd (
|
||||
$defer: any,
|
||||
handler: RemoteHandler,
|
||||
path: string
|
||||
): Promise<number> {
|
||||
async _deleteVhd ($defer: any, handler: RemoteHandler, path: string) {
|
||||
const vhds = await asyncMap(
|
||||
await handler.list(dirname(path), { filter: isVhd, prependDir: true }),
|
||||
async path => {
|
||||
try {
|
||||
const vhd = new Vhd(handler, path)
|
||||
await vhd.readHeaderAndFooter()
|
||||
return {
|
||||
footer: vhd.footer,
|
||||
header: vhd.header,
|
||||
path,
|
||||
}
|
||||
} catch (error) {
|
||||
// Do not fail on corrupted VHDs (usually uncleaned temporary files),
|
||||
// they are probably inconsequent to the backup process and should not
|
||||
// fail it.
|
||||
console.warn('BackupNg#_deleteVhd', path, error)
|
||||
const vhd = new Vhd(handler, path)
|
||||
await vhd.readHeaderAndFooter()
|
||||
return {
|
||||
footer: vhd.footer,
|
||||
header: vhd.header,
|
||||
path,
|
||||
}
|
||||
}
|
||||
)
|
||||
const base = basename(path)
|
||||
const child = vhds.find(
|
||||
_ => _ !== undefined && _.header.parentUnicodeName === base
|
||||
)
|
||||
const child = vhds.find(_ => _.header.parentUnicodeName === base)
|
||||
if (child === undefined) {
|
||||
await handler.unlink(path)
|
||||
return 0
|
||||
return handler.unlink(path)
|
||||
}
|
||||
|
||||
$defer.onFailure.call(handler, 'unlink', path)
|
||||
|
||||
const childPath = child.path
|
||||
const mergedDataSize: number = await this._app.worker.mergeVhd(
|
||||
await this._app.worker.mergeVhd(
|
||||
handler._remote,
|
||||
path,
|
||||
handler._remote,
|
||||
childPath
|
||||
)
|
||||
await handler.rename(path, childPath)
|
||||
return mergedDataSize
|
||||
}
|
||||
|
||||
async _deleteVms (xapi: Xapi, vms: Vm[]): Promise<void> {
|
||||
@@ -1300,4 +1288,54 @@ export default class BackupNg {
|
||||
|
||||
return backups.sort(compareTimestamp)
|
||||
}
|
||||
|
||||
async getBackupNgLogs (runId?: string): Promise<ConsolidatedBackupNgLog> {
|
||||
const rawLogs = await this._app.getLogs('jobs')
|
||||
|
||||
const logs: $Dict<ConsolidatedJob & ConsolidatedTask> = {}
|
||||
forEach(rawLogs, (log, id) => {
|
||||
const { data, time, message } = log
|
||||
const { event } = data
|
||||
delete data.event
|
||||
|
||||
switch (event) {
|
||||
case 'job.start':
|
||||
if (data.type === 'backup' && (runId === undefined || runId === id)) {
|
||||
logs[id] = {
|
||||
...data,
|
||||
id,
|
||||
start: time,
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'job.end':
|
||||
const job = logs[data.runJobId]
|
||||
if (job !== undefined) {
|
||||
job.end = time
|
||||
job.duration = time - job.start
|
||||
job.error = data.error
|
||||
}
|
||||
break
|
||||
case 'task.start':
|
||||
if (logs[data.parentId] !== undefined) {
|
||||
logs[id] = {
|
||||
...data,
|
||||
start: time,
|
||||
message,
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'task.end':
|
||||
const task = logs[data.taskId]
|
||||
if (task !== undefined) {
|
||||
task.status = data.status
|
||||
task.taskId = data.taskId
|
||||
task.result = data.result
|
||||
task.end = time
|
||||
task.duration = time - task.start
|
||||
}
|
||||
}
|
||||
})
|
||||
return groupBy(logs, log => log.parentId || 'roots')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -903,8 +903,6 @@ export default class {
|
||||
const xapi = this._xo.getXapi(vm)
|
||||
vm = xapi.getObject(vm._xapiId)
|
||||
|
||||
await xapi._assertHealthyVdiChains(vm)
|
||||
|
||||
const reg = new RegExp(
|
||||
'^rollingSnapshot_[^_]+_' + escapeStringRegexp(tag) + '_'
|
||||
)
|
||||
|
||||
@@ -120,11 +120,7 @@ export default class Jobs {
|
||||
_executors: { __proto__: null, [string]: Executor }
|
||||
_jobs: JobsDb
|
||||
_logger: Logger
|
||||
_runningJobs: { __proto__: null, [string]: string }
|
||||
|
||||
get runningJobs () {
|
||||
return this._runningJobs
|
||||
}
|
||||
_runningJobs: { __proto__: null, [string]: boolean }
|
||||
|
||||
constructor (xo: any) {
|
||||
this._app = xo
|
||||
@@ -205,7 +201,7 @@ export default class Jobs {
|
||||
return /* await */ this._jobs.remove(id)
|
||||
}
|
||||
|
||||
async _runJob (cancelToken: any, job: Job, schedule?: Schedule, data_?: any) {
|
||||
async _runJob (cancelToken: any, job: Job, schedule?: Schedule) {
|
||||
const { id } = job
|
||||
|
||||
const runningJobs = this._runningJobs
|
||||
@@ -236,7 +232,6 @@ export default class Jobs {
|
||||
event: 'job.start',
|
||||
userId: job.userId,
|
||||
jobId: id,
|
||||
scheduleId: schedule?.id,
|
||||
// $FlowFixMe only defined for CallJob
|
||||
key: job.key,
|
||||
type,
|
||||
@@ -250,10 +245,9 @@ export default class Jobs {
|
||||
session = app.createUserConnection()
|
||||
session.set('user_id', job.userId)
|
||||
|
||||
const status = await executor({
|
||||
await executor({
|
||||
app,
|
||||
cancelToken,
|
||||
data: data_,
|
||||
job,
|
||||
logger,
|
||||
runJobId,
|
||||
@@ -265,7 +259,7 @@ export default class Jobs {
|
||||
runJobId,
|
||||
})
|
||||
|
||||
app.emit('job:terminated', status, job, schedule, runJobId)
|
||||
app.emit('job:terminated', runJobId, job, schedule)
|
||||
} catch (error) {
|
||||
logger.error(`The execution of ${id} has failed.`, {
|
||||
event: 'job.end',
|
||||
@@ -285,8 +279,7 @@ export default class Jobs {
|
||||
async runJobSequence (
|
||||
$cancelToken: any,
|
||||
idSequence: Array<string>,
|
||||
schedule?: Schedule,
|
||||
data?: any
|
||||
schedule?: Schedule
|
||||
) {
|
||||
const jobs = await Promise.all(
|
||||
mapToArray(idSequence, id => this.getJob(id))
|
||||
@@ -296,7 +289,7 @@ export default class Jobs {
|
||||
if ($cancelToken.requested) {
|
||||
break
|
||||
}
|
||||
await this._runJob($cancelToken, job, schedule, data)
|
||||
await this._runJob($cancelToken, job, schedule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-vmdk-to-vhd",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "JS lib streaming a vmdk file to a vhd",
|
||||
"keywords": [
|
||||
@@ -23,23 +23,24 @@
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0-beta.49",
|
||||
"@babel/runtime": "^7.0.0-beta.44",
|
||||
"child-process-promise": "^2.0.3",
|
||||
"fs-promise": "^2.0.0",
|
||||
"pipette": "^0.9.3",
|
||||
"promise-toolbox": "^0.9.5",
|
||||
"tmp": "^0.0.33",
|
||||
"vhd-lib": "^0.1.1"
|
||||
"vhd-lib": "^0.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.49",
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"@babel/cli": "7.0.0-beta.44",
|
||||
"@babel/core": "7.0.0-beta.44",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0-beta.44",
|
||||
"@babel/preset-env": "7.0.0-beta.44",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"event-to-promise": "^0.8.0",
|
||||
"execa": "^0.10.0",
|
||||
"fs-extra": "^6.0.1",
|
||||
"fs-extra": "^5.0.0",
|
||||
"get-stream": "^3.0.0",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^2.6.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { createReadStream, readFile } from 'fs-extra'
|
||||
import { createReadStream, readFile } from 'fs-promise'
|
||||
import { exec } from 'child-process-promise'
|
||||
import { fromCallback as pFromCallback } from 'promise-toolbox'
|
||||
import rimraf from 'rimraf'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { createReadStream } from 'fs-extra'
|
||||
import { createReadStream } from 'fs-promise'
|
||||
import { exec } from 'child-process-promise'
|
||||
import { fromCallback as pFromCallback } from 'promise-toolbox'
|
||||
import rimraf from 'rimraf'
|
||||
|
||||
@@ -308,15 +308,17 @@ export class VMDKDirectParser {
|
||||
|
||||
export async function readVmdkGrainTable (fileAccessor) {
|
||||
let headerBuffer = await fileAccessor(0, 512)
|
||||
let grainAddrBuffer = headerBuffer.slice(56, 56 + 8)
|
||||
let grainDirAddr = headerBuffer.slice(56, 56 + 8)
|
||||
if (
|
||||
new Int8Array(grainAddrBuffer).reduce((acc, val) => acc && val === -1, true)
|
||||
new Int8Array(grainDirAddr).reduce((acc, val) => acc && val === -1, true)
|
||||
) {
|
||||
headerBuffer = await fileAccessor(-1024, -1024 + 512)
|
||||
grainAddrBuffer = headerBuffer.slice(56, 56 + 8)
|
||||
grainDirAddr = new DataView(headerBuffer.slice(56, 56 + 8)).getUint32(
|
||||
0,
|
||||
true
|
||||
)
|
||||
}
|
||||
const grainDirPosBytes =
|
||||
new DataView(grainAddrBuffer).getUint32(0, true) * 512
|
||||
const grainDirPosBytes = grainDirAddr * 512
|
||||
const capacity =
|
||||
new DataView(headerBuffer.slice(12, 12 + 8)).getUint32(0, true) * 512
|
||||
const grainSize =
|
||||
|
||||
@@ -6,7 +6,7 @@ import getStream from 'get-stream'
|
||||
import rimraf from 'rimraf'
|
||||
import tmp from 'tmp'
|
||||
|
||||
import { createReadStream, createWriteStream, stat } from 'fs-extra'
|
||||
import { createReadStream, createWriteStream, stat } from 'fs-promise'
|
||||
import { fromCallback as pFromCallback } from 'promise-toolbox'
|
||||
import convertFromVMDK, { readVmdkGrainTable } from '.'
|
||||
|
||||
@@ -49,7 +49,7 @@ test('VMDK to VHD can convert a random data file with VMDKDirectParser', async (
|
||||
const vhdFileName = 'from-vmdk-VMDKDirectParser.vhd'
|
||||
const reconvertedFromVhd = 'from-vhd.raw'
|
||||
const reconvertedFromVmdk = 'from-vhd-by-vbox.raw'
|
||||
const dataSize = 100 * 1024 * 1024 // this number is an integer head/cylinder/count equation solution
|
||||
const dataSize = 8355840 // this number is an integer head/cylinder/count equation solution
|
||||
try {
|
||||
await execa.shell(
|
||||
'base64 /dev/urandom | head -c ' + dataSize + ' > ' + inputRawFileName
|
||||
@@ -82,7 +82,6 @@ test('VMDK to VHD can convert a random data file with VMDKDirectParser', async (
|
||||
reconvertedFromVhd,
|
||||
])
|
||||
await execa('qemu-img', ['compare', inputRawFileName, vhdFileName])
|
||||
await execa('qemu-img', ['compare', vmdkFileName, vhdFileName])
|
||||
} catch (error) {
|
||||
console.error(error.stdout)
|
||||
console.error(error.stderr)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.19.8",
|
||||
"version": "5.19.2",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -30,9 +30,10 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@julien-f/freactal": "0.1.1",
|
||||
"@julien-f/freactal": "0.1.0",
|
||||
"@nraynaud/novnc": "0.6.1",
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"xo-vmdk-to-vhd": "0.1.0",
|
||||
"ansi_up": "^3.0.0",
|
||||
"asap": "^2.0.6",
|
||||
"babel-core": "^6.26.0",
|
||||
@@ -59,7 +60,6 @@
|
||||
"classnames": "^2.2.3",
|
||||
"complex-matcher": "^0.3.0",
|
||||
"cookies-js": "^1.2.2",
|
||||
"copy-to-clipboard": "^3.0.8",
|
||||
"d3": "^5.0.0",
|
||||
"debounce-input-decorator": "^0.1.0",
|
||||
"enzyme": "^3.3.0",
|
||||
@@ -89,8 +89,8 @@
|
||||
"lodash": "^4.6.1",
|
||||
"loose-envify": "^1.1.0",
|
||||
"make-error": "^1.3.2",
|
||||
"marked": "^0.4.0",
|
||||
"modular-cssify": "^10.0.0",
|
||||
"marked": "^0.3.9",
|
||||
"modular-cssify": "^8.0.0",
|
||||
"moment": "^2.20.1",
|
||||
"moment-timezone": "^0.5.14",
|
||||
"notifyjs": "^3.0.0",
|
||||
@@ -120,7 +120,7 @@
|
||||
"react-test-renderer": "^15.6.2",
|
||||
"react-virtualized": "^9.15.0",
|
||||
"readable-stream": "^2.3.3",
|
||||
"redux": "^4.0.0",
|
||||
"redux": "^3.7.2",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.5.4",
|
||||
"rimraf": "^2.6.2",
|
||||
@@ -134,11 +134,10 @@
|
||||
"watchify": "^3.7.0",
|
||||
"whatwg-fetch": "^2.0.3",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.2.4",
|
||||
"xo-acl-resolver": "^0.2.3",
|
||||
"xo-common": "^0.1.1",
|
||||
"xo-lib": "^0.8.0",
|
||||
"xo-remote-parser": "^0.3",
|
||||
"xo-vmdk-to-vhd": "^0.1.2"
|
||||
"xo-remote-parser": "^0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production gulp build",
|
||||
|
||||
@@ -142,15 +142,15 @@ export default class Select extends React.PureComponent {
|
||||
simpleValue,
|
||||
value,
|
||||
} = props
|
||||
let option
|
||||
if (
|
||||
autoSelectSingleOption &&
|
||||
options != null &&
|
||||
options.length === 1 &&
|
||||
(value == null ||
|
||||
(simpleValue && value === '') ||
|
||||
(multi && value.length === 0)) &&
|
||||
([option] = options.filter(_ => !_.disabled)).length === 1
|
||||
(multi && value.length === 0))
|
||||
) {
|
||||
const option = options[0]
|
||||
props.onChange(
|
||||
simpleValue ? option[props.valueKey] : multi ? [option] : option
|
||||
)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import isString from 'lodash/isString'
|
||||
import moment from 'moment'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { FormattedMessage, IntlProvider as IntlProvider_ } from 'react-intl'
|
||||
import { every, isFunction, isString } from 'lodash'
|
||||
|
||||
import locales from './locales'
|
||||
import messages from './messages'
|
||||
@@ -101,16 +102,8 @@ export class FormattedDuration extends Component {
|
||||
)
|
||||
|
||||
render () {
|
||||
const parsedDuration = this._parseDuration()
|
||||
return (
|
||||
<Tooltip
|
||||
content={getMessage(
|
||||
every(parsedDuration, n => n === 0)
|
||||
? 'secondsFormat'
|
||||
: 'durationFormat',
|
||||
parsedDuration
|
||||
)}
|
||||
>
|
||||
<Tooltip content={getMessage('durationFormat', this._parseDuration())}>
|
||||
<span>{this._humanizeDuration()}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@@ -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 !',
|
||||
|
||||
@@ -41,7 +41,6 @@ const messages = {
|
||||
|
||||
// ----- Copiable component -----
|
||||
copyToClipboard: 'Copy to clipboard',
|
||||
copyUuid: 'Copy {uuid}',
|
||||
|
||||
// ----- Pills -----
|
||||
pillMaster: 'Master',
|
||||
@@ -259,8 +258,6 @@ const messages = {
|
||||
jobCallInProgess: 'In progress',
|
||||
jobTransferredDataSize: 'Transfer size:',
|
||||
jobTransferredDataSpeed: 'Transfer speed:',
|
||||
operationSize: 'Size',
|
||||
operationSpeed: 'Speed',
|
||||
jobMergedDataSize: 'Merge size:',
|
||||
jobMergedDataSpeed: 'Merge speed:',
|
||||
allJobCalls: 'All',
|
||||
@@ -308,7 +305,6 @@ const messages = {
|
||||
taskMergedDataSize: 'Merge size',
|
||||
taskMergedDataSpeed: 'Merge speed',
|
||||
taskError: 'Error',
|
||||
taskReason: 'Reason',
|
||||
saveBackupJob: 'Save',
|
||||
deleteBackupSchedule: 'Remove backup job',
|
||||
deleteBackupScheduleQuestion:
|
||||
@@ -352,7 +348,6 @@ const messages = {
|
||||
reportWhenFailure: 'Failure',
|
||||
reportWhenNever: 'Never',
|
||||
reportWhen: 'Report when',
|
||||
concurrency: 'Concurrency',
|
||||
newBackupSelection: 'Select your backup type:',
|
||||
smartBackupModeSelection: 'Select backup mode:',
|
||||
normalBackup: 'Normal backup',
|
||||
@@ -601,15 +596,11 @@ const messages = {
|
||||
vmsTabName: 'Vms',
|
||||
srsTabName: 'Srs',
|
||||
// ----- Pool advanced tab -----
|
||||
poolEditAll: 'Edit all',
|
||||
poolEditRemoteSyslog: 'Edit remote syslog for all hosts',
|
||||
poolHaStatus: 'High Availability',
|
||||
poolHaEnabled: 'Enabled',
|
||||
poolHaDisabled: 'Disabled',
|
||||
poolGpuGroups: 'GPU groups',
|
||||
poolRemoteSyslogPlaceHolder: 'Logging host',
|
||||
setpoolMaster: 'Master',
|
||||
syslogRemoteHost: 'Remote syslog host',
|
||||
poolGpuGroups: 'GPU groups',
|
||||
// ----- Pool host tab -----
|
||||
hostNameLabel: 'Name',
|
||||
hostDescription: 'Description',
|
||||
@@ -689,7 +680,6 @@ const messages = {
|
||||
hostLicenseType: 'Type',
|
||||
hostLicenseSocket: 'Socket',
|
||||
hostLicenseExpiry: 'Expiry',
|
||||
hostRemoteSyslog: 'Remote syslog',
|
||||
supplementalPacks: 'Installed supplemental packs',
|
||||
supplementalPackNew: 'Install new supplemental pack',
|
||||
supplementalPackPoolNew: 'Install supplemental pack on every host',
|
||||
@@ -744,7 +734,6 @@ const messages = {
|
||||
patchNameLabel: 'Name',
|
||||
patchUpdateButton: 'Install all patches',
|
||||
patchDescription: 'Description',
|
||||
patchVersion: 'Version',
|
||||
patchApplied: 'Applied date',
|
||||
patchSize: 'Size',
|
||||
patchStatus: 'Status',
|
||||
@@ -762,15 +751,6 @@ const messages = {
|
||||
'This will install a patch only on this host. This is NOT the recommended way: please go into the Pool patch view and follow instructions there. If you are sure about this, you can continue anyway',
|
||||
installPatchWarningReject: 'Go to pool',
|
||||
installPatchWarningResolve: 'Install',
|
||||
patchRelease: 'Release',
|
||||
updatePluginNotInstalled:
|
||||
'Update plugin is not installed on this host. Please run `yum install xcp-ng-updater` first.',
|
||||
showChangelog: 'Show changelog',
|
||||
changelog: 'Changelog',
|
||||
changelogPatch: 'Patch',
|
||||
changelogAuthor: 'Author',
|
||||
changelogDate: 'Date',
|
||||
changelogDescription: 'Description',
|
||||
// ----- Pool patch tabs -----
|
||||
refreshPatches: 'Refresh patches',
|
||||
installPoolPatches: 'Install pool patches',
|
||||
@@ -952,7 +932,6 @@ const messages = {
|
||||
defaultCpuCap: 'Default ({value, number})',
|
||||
pvArgsLabel: 'PV args',
|
||||
xenToolsStatus: 'Xen tools version',
|
||||
xenToolsNotInstalled: 'Not installed',
|
||||
osName: 'OS name',
|
||||
osKernel: 'OS kernel',
|
||||
autoPowerOn: 'Auto power on',
|
||||
@@ -977,7 +956,6 @@ const messages = {
|
||||
vmCoresPerSocketIncorrectValue: 'Incorrect cores per socket value',
|
||||
vmCoresPerSocketIncorrectValueSolution:
|
||||
'Please change the selected value to fix it.',
|
||||
vmHaDisabled: 'disabled',
|
||||
vmMemoryLimitsLabel: 'Memory limits (min/max)',
|
||||
vmMaxVcpus: 'vCPUs max:',
|
||||
vmMaxRam: 'Memory max:',
|
||||
@@ -1126,11 +1104,6 @@ const messages = {
|
||||
newVmSshKey: 'SSH key',
|
||||
newVmConfigDrive: 'Config drive',
|
||||
newVmCustomConfig: 'Custom config',
|
||||
availableTemplateVarsInfo:
|
||||
'Click here to see the available template variables',
|
||||
availableTemplateVarsTitle: 'Available template variables',
|
||||
templateNameInfo: 'the VM\'s name. It must not contain "_"',
|
||||
templateIndexInfo: "the VM's index, it will take 0 in case of single VM",
|
||||
newVmBootAfterCreate: 'Boot VM after creation',
|
||||
newVmMacPlaceholder: 'Auto-generated if empty',
|
||||
newVmCpuWeightLabel: 'CPU weight',
|
||||
@@ -1699,7 +1672,6 @@ const messages = {
|
||||
logIndicationToDisable: 'Click to disable',
|
||||
reportBug: 'Report a bug',
|
||||
unhealthyVdiChainError: 'Job canceled to protect the VDI chain',
|
||||
backupRestartVm: "Restart VM's backup",
|
||||
clickForMoreInformation: 'Click for more information',
|
||||
|
||||
// ----- IPs ------
|
||||
@@ -1794,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',
|
||||
@@ -1823,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}',
|
||||
@@ -1847,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',
|
||||
@@ -1931,7 +1898,6 @@ const messages = {
|
||||
xosanLoadXoaPlugin: 'Load XOA plugin first',
|
||||
|
||||
// ----- Utils -----
|
||||
secondsFormat: '{seconds, plural, one {# second} other {# seconds}}',
|
||||
durationFormat:
|
||||
'{days, plural, =0 {} one {# day } other {# days }}{hours, plural, =0 {} one {# hour } other {# hours }}{minutes, plural, =0 {} one {# minute } other {# minutes }}{seconds, plural, =0 {} one {# second} other {# seconds}}',
|
||||
}
|
||||
|
||||
@@ -50,17 +50,19 @@ const SrItem = propTypes({
|
||||
return (state, props) => ({
|
||||
container: getContainer(state, props),
|
||||
})
|
||||
})(({ sr, container }) => (
|
||||
<span>
|
||||
<Icon icon='sr' /> {sr.name_label || sr.id}
|
||||
{container !== undefined && (
|
||||
<span className='text-muted'> - {container.name_label}</span>
|
||||
)}
|
||||
{isSrWritable(sr) && (
|
||||
<span>{` (${formatSize(sr.size - sr.physical_usage)} free)`}</span>
|
||||
)}
|
||||
</span>
|
||||
))
|
||||
})(({ sr, container }) => {
|
||||
let label = `${sr.name_label || sr.id}`
|
||||
|
||||
if (isSrWritable(sr)) {
|
||||
label += ` (${formatSize(sr.size - sr.physical_usage)} free)`
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Icon icon='sr' /> {label}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
// VM.
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
filter,
|
||||
flatten,
|
||||
forEach,
|
||||
get,
|
||||
groupBy,
|
||||
includes,
|
||||
isArray,
|
||||
@@ -361,10 +362,40 @@ export const SelectSr = makeStoreSelect(
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
const getHosts = createGetObjectsOfType('host')
|
||||
|
||||
const getSrsByContainer = createGetObjectsOfType('SR')
|
||||
.filter((_, { predicate }) => predicate || isSrWritable)
|
||||
.sort()
|
||||
.groupBy('$container')
|
||||
const getSrsByContainer = createSelector(
|
||||
createGetObjectsOfType('SR')
|
||||
.filter((_, { predicate }) => predicate || isSrWritable)
|
||||
.sort(),
|
||||
createSelector(getHosts, getPools, (hosts, pools) => id =>
|
||||
hosts[id] || pools[id]
|
||||
),
|
||||
(srs, containerFinder) => {
|
||||
const { length } = srs
|
||||
|
||||
if (length >= 2) {
|
||||
let sr1, sr2
|
||||
const srsToModify = {}
|
||||
for (let i = 1; i < length; ++i) {
|
||||
sr1 = srs[i]
|
||||
for (let j = 0; j < i; ++j) {
|
||||
sr2 = srs[j]
|
||||
if (sr1.name_label === sr2.name_label) {
|
||||
srsToModify[sr1.id] = sr1
|
||||
srsToModify[sr2.id] = sr2
|
||||
}
|
||||
}
|
||||
}
|
||||
forEach(srsToModify, sr => {
|
||||
sr.name_label = `(${get(
|
||||
containerFinder(sr.$container),
|
||||
'name_label'
|
||||
)}) ${sr.name_label}`
|
||||
})
|
||||
}
|
||||
|
||||
return groupBy(srs, '$container')
|
||||
}
|
||||
)
|
||||
|
||||
const getContainerIds = createSelector(getSrsByContainer, srsByContainer =>
|
||||
keys(srsByContainer)
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
pickBy,
|
||||
size,
|
||||
slice,
|
||||
some,
|
||||
} from 'lodash'
|
||||
|
||||
import invoke from './invoke'
|
||||
@@ -148,9 +147,7 @@ export const createFilter = (collection, predicate) =>
|
||||
_createCollectionWrapper(
|
||||
(collection, predicate) =>
|
||||
predicate === false
|
||||
? isArrayLike(collection)
|
||||
? EMPTY_ARRAY
|
||||
: EMPTY_OBJECT
|
||||
? isArrayLike(collection) ? EMPTY_ARRAY : EMPTY_OBJECT
|
||||
: predicate
|
||||
? (isArrayLike(collection) ? filter : pickBy)(collection, predicate)
|
||||
: collection
|
||||
@@ -544,9 +541,3 @@ export const createGetVmDisks = vmSelector =>
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
export const getIsPoolAdmin = create(
|
||||
create(createGetObjectsOfType('pool'), _createCollectionWrapper(Object.keys)),
|
||||
getCheckPermissions,
|
||||
(poolsIds, check) => some(poolsIds, poolId => check(poolId, 'administrate'))
|
||||
)
|
||||
|
||||
@@ -209,21 +209,13 @@ class IndividualAction extends Component {
|
||||
(disabled, item, userData) =>
|
||||
isFunction(disabled) ? disabled(item, userData) : disabled
|
||||
)
|
||||
_getLabel = createSelector(
|
||||
() => this.props.label,
|
||||
() => this.props.item,
|
||||
() => this.props.userData,
|
||||
(label, item, userData) =>
|
||||
isFunction(label) ? label(item, userData) : label
|
||||
)
|
||||
|
||||
_executeAction = () => {
|
||||
const p = this.props
|
||||
return p.handler(p.item, p.userData)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { icon, item, level, redirectOnSuccess, userData } = this.props
|
||||
const { icon, item, label, level, redirectOnSuccess, userData } = this.props
|
||||
|
||||
return (
|
||||
<ActionRowButton
|
||||
@@ -234,7 +226,7 @@ class IndividualAction extends Component {
|
||||
handler={this._executeAction}
|
||||
icon={icon}
|
||||
redirectOnSuccess={redirectOnSuccess}
|
||||
tooltip={this._getLabel()}
|
||||
tooltip={label}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -248,13 +240,6 @@ class GroupedAction extends Component {
|
||||
(disabled, selectedItems, userData) =>
|
||||
isFunction(disabled) ? disabled(selectedItems, userData) : disabled
|
||||
)
|
||||
_getLabel = createSelector(
|
||||
() => this.props.label,
|
||||
() => this.props.selectedItems,
|
||||
() => this.props.userData,
|
||||
(label, selectedItems, userData) =>
|
||||
isFunction(label) ? label(selectedItems, userData) : label
|
||||
)
|
||||
|
||||
_executeAction = () => {
|
||||
const p = this.props
|
||||
@@ -262,7 +247,7 @@ class GroupedAction extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { icon, level } = this.props
|
||||
const { icon, label, level } = this.props
|
||||
|
||||
return (
|
||||
<ActionRowButton
|
||||
@@ -270,7 +255,7 @@ class GroupedAction extends Component {
|
||||
disabled={this._getIsDisabled()}
|
||||
handler={this._executeAction}
|
||||
icon={icon}
|
||||
tooltip={this._getLabel()}
|
||||
tooltip={label}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }) =>
|
||||
|
||||
@@ -23,7 +23,7 @@ class CreateNetworkModalBody extends Component {
|
||||
pool: container.$pool,
|
||||
name: refs.name.value,
|
||||
description: refs.description.value,
|
||||
pif: refs.pif.value && refs.pif.value.id,
|
||||
pif: refs.pif.value.id,
|
||||
mtu: refs.mtu.value,
|
||||
vlan: refs.vlan.value,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
assign,
|
||||
filter,
|
||||
forEach,
|
||||
get,
|
||||
includes,
|
||||
isEmpty,
|
||||
isEqual,
|
||||
@@ -462,15 +461,10 @@ export const exportConfig = () =>
|
||||
|
||||
// Server ------------------------------------------------------------
|
||||
|
||||
export const addServer = (host, username, password, label, allowUnauthorized) =>
|
||||
_call('server.add', {
|
||||
allowUnauthorized,
|
||||
host,
|
||||
label,
|
||||
password,
|
||||
username,
|
||||
})::tap(subscribeServers.forceRefresh, () =>
|
||||
error(_('serverError'), _('serverAddFailed'))
|
||||
export const addServer = (host, username, password, label) =>
|
||||
_call('server.add', { host, label, password, username })::tap(
|
||||
subscribeServers.forceRefresh,
|
||||
() => error(_('serverError'), _('serverAddFailed'))
|
||||
)
|
||||
|
||||
export const editServer = (server, props) =>
|
||||
@@ -577,15 +571,6 @@ export const editHost = (host, props) =>
|
||||
export const fetchHostStats = (host, granularity) =>
|
||||
_call('host.stats', { host: resolveId(host), granularity })
|
||||
|
||||
export const setRemoteSyslogHost = (host, syslogDestination) =>
|
||||
_call('host.setRemoteSyslogHost', {
|
||||
id: resolveId(host),
|
||||
syslogDestination,
|
||||
})
|
||||
|
||||
export const setRemoteSyslogHosts = (hosts, syslogDestination) =>
|
||||
Promise.all(map(hosts, host => setRemoteSyslogHost(host, syslogDestination)))
|
||||
|
||||
export const restartHost = (host, force = false) =>
|
||||
confirm({
|
||||
title: _('restartHostModalTitle'),
|
||||
@@ -665,26 +650,14 @@ export const enableHost = host => _call('host.enable', { id: resolveId(host) })
|
||||
export const disableHost = host =>
|
||||
_call('host.disable', { id: resolveId(host) })
|
||||
|
||||
const missingUpdatePluginByHost = { __proto__: null }
|
||||
export const getHostMissingPatches = async host => {
|
||||
const hostId = resolveId(host)
|
||||
if (host.productBrand !== 'XCP-ng') {
|
||||
const patches = await _call('host.listMissingPatches', { host: hostId })
|
||||
// Hide paid patches to XS-free users
|
||||
return host.license_params.sku_type !== 'free'
|
||||
? patches
|
||||
: filter(patches, { paid: false })
|
||||
}
|
||||
if (missingUpdatePluginByHost[hostId]) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return await _call('host.listMissingPatches', { host: hostId })
|
||||
} catch (_) {
|
||||
missingUpdatePluginByHost[hostId] = true
|
||||
return null
|
||||
}
|
||||
}
|
||||
export const getHostMissingPatches = host =>
|
||||
_call('host.listMissingPatches', { host: resolveId(host) }).then(
|
||||
patches =>
|
||||
// Hide paid patches to XS-free users
|
||||
host.license_params.sku_type !== 'free'
|
||||
? patches
|
||||
: filter(patches, ['paid', false])
|
||||
)
|
||||
|
||||
export const emergencyShutdownHost = host =>
|
||||
confirm({
|
||||
@@ -1129,7 +1102,7 @@ export const migrateVms = vms =>
|
||||
|
||||
export const createVm = args => _call('vm.create', args)
|
||||
|
||||
export const createVms = (args, nameLabels, cloudConfigs) =>
|
||||
export const createVms = (args, nameLabels) =>
|
||||
confirm({
|
||||
title: _('newVmCreateVms'),
|
||||
body: _('newVmCreateVmsConfirm', { nbVms: nameLabels.length }),
|
||||
@@ -1137,15 +1110,8 @@ export const createVms = (args, nameLabels, cloudConfigs) =>
|
||||
() =>
|
||||
Promise.all(
|
||||
map(nameLabels, (
|
||||
name_label, // eslint-disable-line camelcase
|
||||
i
|
||||
) =>
|
||||
_call('vm.create', {
|
||||
...args,
|
||||
name_label,
|
||||
cloudConfig: get(cloudConfigs, i),
|
||||
})
|
||||
)
|
||||
name_label // eslint-disable-line camelcase
|
||||
) => _call('vm.create', { ...args, name_label }))
|
||||
),
|
||||
noop
|
||||
)
|
||||
@@ -1225,8 +1191,6 @@ export const editVm = (vm, props) =>
|
||||
export const fetchVmStats = (vm, granularity) =>
|
||||
_call('vm.stats', { id: resolveId(vm), granularity })
|
||||
|
||||
export const getVmsHaValues = () => _call('vm.getHaValues')
|
||||
|
||||
export const importVm = (file, type = 'xva', data = undefined, sr) => {
|
||||
const { name } = file
|
||||
|
||||
@@ -2448,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
|
||||
@@ -2456,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>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
|
||||
import Schedules from './schedules'
|
||||
import SmartBackup from './smart-backup'
|
||||
import { FormGroup, getRandomId, Input, Number, Ul, Li } from './utils'
|
||||
import { FormGroup, getRandomId, Input, Ul, Li } from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -112,7 +112,6 @@ const getInitialState = () => ({
|
||||
$pool: {},
|
||||
backupMode: false,
|
||||
compression: true,
|
||||
concurrency: 0,
|
||||
crMode: false,
|
||||
deltaMode: false,
|
||||
drMode: false,
|
||||
@@ -159,7 +158,6 @@ export default [
|
||||
...getNewSettings(state.newSchedules),
|
||||
'': {
|
||||
reportWhen: state.reportWhen,
|
||||
concurrency: state.concurrency || undefined,
|
||||
},
|
||||
},
|
||||
remotes:
|
||||
@@ -229,7 +227,6 @@ export default [
|
||||
|
||||
if (id === '') {
|
||||
oldSetting.reportWhen = state.reportWhen
|
||||
oldSetting.concurrency = state.concurrency || undefined
|
||||
} else if (!(id in settings)) {
|
||||
delete oldSettings[id]
|
||||
} else if (
|
||||
@@ -333,7 +330,6 @@ export default [
|
||||
remotes,
|
||||
srs,
|
||||
reportWhen: get(globalSettings, 'reportWhen') || 'failure',
|
||||
concurrency: get(globalSettings, 'concurrency') || 0,
|
||||
settings,
|
||||
schedules,
|
||||
...destructVmsPattern(job.vms),
|
||||
@@ -495,10 +491,6 @@ export default [
|
||||
...state,
|
||||
reportWhen: value,
|
||||
}),
|
||||
setConcurrency: (_, concurrency) => state => ({
|
||||
...state,
|
||||
concurrency,
|
||||
}),
|
||||
},
|
||||
computed: {
|
||||
needUpdateParams: (state, { job, schedules }) =>
|
||||
@@ -759,15 +751,6 @@ export default [
|
||||
valueKey='value'
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label>
|
||||
<strong>{_('concurrency')}</strong>
|
||||
</label>
|
||||
<Number
|
||||
onChange={effects.setConcurrency}
|
||||
value={state.concurrency}
|
||||
/>
|
||||
</FormGroup>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
@@ -1,13 +1,41 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import moment from 'moment-timezone'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import Scheduler, { SchedulePreview } from 'scheduling'
|
||||
import { Card, CardBlock } from 'card'
|
||||
import { injectState, provideState } from '@julien-f/freactal'
|
||||
import { isEqual } from 'lodash'
|
||||
|
||||
import { FormGroup, getRandomId, Number } from './utils'
|
||||
import { FormGroup, getRandomId, Input } from './utils'
|
||||
|
||||
const Number = [
|
||||
provideState({
|
||||
effects: {
|
||||
onChange: (_, { target: { value } }) => (state, props) => {
|
||||
if (value === '') {
|
||||
return
|
||||
}
|
||||
props.onChange(+value)
|
||||
},
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ effects, state, value }) => (
|
||||
<Input
|
||||
type='number'
|
||||
onChange={effects.onChange}
|
||||
value={String(value)}
|
||||
min='0'
|
||||
/>
|
||||
),
|
||||
].reduceRight((value, decorator) => decorator(value))
|
||||
|
||||
Number.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.number.isRequired,
|
||||
}
|
||||
|
||||
export default [
|
||||
injectState,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { injectState, provideState } from '@julien-f/freactal'
|
||||
|
||||
export const FormGroup = props => <div {...props} className='form-group' />
|
||||
export const Input = props => <input {...props} className='form-control' />
|
||||
@@ -11,30 +9,3 @@ export const getRandomId = () =>
|
||||
Math.random()
|
||||
.toString(36)
|
||||
.slice(2)
|
||||
|
||||
export const Number = [
|
||||
provideState({
|
||||
effects: {
|
||||
onChange: (_, { target: { value } }) => (state, props) => {
|
||||
if (value === '') {
|
||||
return
|
||||
}
|
||||
props.onChange(+value)
|
||||
},
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ effects, state, value }) => (
|
||||
<Input
|
||||
type='number'
|
||||
onChange={effects.onChange}
|
||||
value={String(value)}
|
||||
min='0'
|
||||
/>
|
||||
),
|
||||
].reduceRight((value, decorator) => decorator(value))
|
||||
|
||||
Number.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.number.isRequired,
|
||||
}
|
||||
|
||||
@@ -74,7 +74,6 @@ import {
|
||||
createPager,
|
||||
createSelector,
|
||||
createSort,
|
||||
getIsPoolAdmin,
|
||||
getUser,
|
||||
isAdmin,
|
||||
} from 'selectors'
|
||||
@@ -303,7 +302,6 @@ const DEFAULT_TYPE = 'VM'
|
||||
})
|
||||
@propTypes({
|
||||
isAdmin: propTypes.bool.isRequired,
|
||||
isPoolAdmin: propTypes.bool.isRequired,
|
||||
noResourceSets: propTypes.bool.isRequired,
|
||||
})
|
||||
class NoObjects_ extends Component {
|
||||
@@ -311,7 +309,6 @@ class NoObjects_ extends Component {
|
||||
const {
|
||||
areObjectsFetched,
|
||||
isAdmin,
|
||||
isPoolAdmin,
|
||||
noRegisteredServers,
|
||||
noResourceSets,
|
||||
noServersConnected,
|
||||
@@ -381,9 +378,7 @@ class NoObjects_ extends Component {
|
||||
<CenterPanel>
|
||||
<Card shadow>
|
||||
<CardHeader>{_('homeNoVms')}</CardHeader>
|
||||
{(isAdmin ||
|
||||
(isPoolAdmin && process.env.XOA_PLAN > 3) ||
|
||||
!noResourceSets) && (
|
||||
{(isAdmin || !noResourceSets) && (
|
||||
<CardBlock>
|
||||
<Row>
|
||||
<Col>
|
||||
@@ -433,7 +428,6 @@ class NoObjects_ extends Component {
|
||||
|
||||
return {
|
||||
isAdmin,
|
||||
isPoolAdmin: getIsPoolAdmin,
|
||||
items: createSelector(
|
||||
createSelector(
|
||||
createGetObjectsOfType('host'),
|
||||
@@ -824,7 +818,7 @@ export default class Home extends Component {
|
||||
const customFilters = this._getCustomFilters()
|
||||
const filteredItems = this._getFilteredItems()
|
||||
const nItems = this._getNumberOfItems()
|
||||
const { isAdmin, isPoolAdmin, items, noResourceSets, type } = this.props
|
||||
const { isAdmin, items, noResourceSets, type } = this.props
|
||||
|
||||
const {
|
||||
selectedHosts,
|
||||
@@ -912,9 +906,7 @@ export default class Home extends Component {
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
{(isAdmin ||
|
||||
(isPoolAdmin && process.env.XOA_PLAN > 3) ||
|
||||
!noResourceSets) && (
|
||||
{(isAdmin || !noResourceSets) && (
|
||||
<Col mediumSize={3} className='text-xs-right'>
|
||||
<Link className='btn btn-success' to='/vms/new'>
|
||||
<Icon icon='vm-new' /> {_('homeNewVm')}
|
||||
@@ -1113,18 +1105,12 @@ export default class Home extends Component {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
render () {
|
||||
const { isAdmin, isPoolAdmin, noResourceSets } = this.props
|
||||
const { isAdmin, noResourceSets } = this.props
|
||||
|
||||
const nItems = this._getNumberOfItems()
|
||||
|
||||
if (nItems < 1) {
|
||||
return (
|
||||
<NoObjects_
|
||||
isAdmin={isAdmin}
|
||||
isPoolAdmin={isPoolAdmin}
|
||||
noResourceSets={noResourceSets}
|
||||
/>
|
||||
)
|
||||
return <NoObjects_ isAdmin={isAdmin} noResourceSets={noResourceSets} />
|
||||
}
|
||||
|
||||
const filteredItems = this._getFilteredItems()
|
||||
|
||||
@@ -8,7 +8,13 @@ import React, { cloneElement, Component } from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Text } from 'editable'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { editHost, fetchHostStats, subscribeHostMissingPatches } from 'xo'
|
||||
import {
|
||||
editHost,
|
||||
fetchHostStats,
|
||||
installAllHostPatches,
|
||||
installHostPatch,
|
||||
subscribeHostMissingPatches,
|
||||
} from 'xo'
|
||||
import { connectStore, routes } from 'utils'
|
||||
import {
|
||||
createDoesHostNeedRestart,
|
||||
@@ -104,8 +110,7 @@ const isRunning = host => host && host.power_state === 'Running'
|
||||
|
||||
return {
|
||||
host,
|
||||
hostPatches:
|
||||
host.productBrand !== 'XCP-ng' && getHostPatches(state, props),
|
||||
hostPatches: getHostPatches(state, props),
|
||||
logs: getLogs(state, props),
|
||||
memoryUsed: getMemoryUsed(state, props),
|
||||
needsRestart: doesNeedRestart(state, props),
|
||||
@@ -202,12 +207,21 @@ export default class Host extends Component {
|
||||
host,
|
||||
missingPatches =>
|
||||
this.setState({
|
||||
missingPatches:
|
||||
missingPatches && sortBy(missingPatches, patch => -patch.time),
|
||||
missingPatches: sortBy(missingPatches, patch => -patch.time),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
_installAllPatches = () => {
|
||||
const { host } = this.props
|
||||
return installAllHostPatches(host)
|
||||
}
|
||||
|
||||
_installPatch = patch => {
|
||||
const { host } = this.props
|
||||
return installHostPatch(host, patch)
|
||||
}
|
||||
|
||||
_setNameDescription = nameDescription =>
|
||||
editHost(this.props.host, { name_description: nameDescription })
|
||||
_setNameLabel = nameLabel =>
|
||||
@@ -317,7 +331,11 @@ export default class Host extends Component {
|
||||
'vmController',
|
||||
'vms',
|
||||
]),
|
||||
pick(this.state, ['missingPatches', 'statsOverview'])
|
||||
pick(this.state, ['missingPatches', 'statsOverview']),
|
||||
{
|
||||
installAllPatches: this._installAllPatches,
|
||||
installPatch: this._installPatch,
|
||||
}
|
||||
)
|
||||
return (
|
||||
<Page
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
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 { Text } from 'editable'
|
||||
import { connectStore } from 'utils'
|
||||
import { Toggle } from 'form'
|
||||
import { compareVersions, connectStore } from 'utils'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { forEach, map, noop } from 'lodash'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import {
|
||||
enableHost,
|
||||
detachHost,
|
||||
disableHost,
|
||||
forgetHost,
|
||||
setRemoteSyslogHost,
|
||||
restartHost,
|
||||
installSupplementalPack,
|
||||
} from 'xo'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { map, noop } from 'lodash'
|
||||
|
||||
const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1
|
||||
|
||||
@@ -34,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()
|
||||
@@ -49,243 +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
|
||||
}
|
||||
)
|
||||
_setRemoteSyslogHost = value => setRemoteSyslogHost(this.props.host, value)
|
||||
|
||||
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>
|
||||
<tr>
|
||||
<th>{_('hostRemoteSyslog')}</th>
|
||||
<td>
|
||||
<Text
|
||||
value={host.logging.syslog_destination || ''}
|
||||
onChange={this._setRemoteSyslogHost}
|
||||
/>
|
||||
</td>
|
||||
</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>
|
||||
))
|
||||
|
||||
@@ -85,9 +85,9 @@ export default ({
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<p>
|
||||
{host.productBrand} {host.version} ({host.productBrand !== 'XCP-ng'
|
||||
? host.license_params.sku_type
|
||||
: 'GPLv2'})
|
||||
{host.license_params.sku_marketing_name} {host.version} ({
|
||||
host.license_params.sku_type
|
||||
})
|
||||
</p>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import React from 'react'
|
||||
import Icon from 'icon'
|
||||
import pick from 'lodash/pick'
|
||||
@@ -15,7 +14,6 @@ import { connectStore, noop } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { error } from 'notification'
|
||||
import { get } from 'xo-defined'
|
||||
import { Select, Number } from 'editable'
|
||||
import { Toggle } from 'form'
|
||||
import {
|
||||
@@ -193,18 +191,12 @@ class PifItemLock extends Component {
|
||||
|
||||
render () {
|
||||
const { networks, pif, vifsByNetwork } = this.props
|
||||
|
||||
const network = networks[pif.$network]
|
||||
if (network === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
const pifInUse = some(vifsByNetwork[pif.$network], vif => vif.attached)
|
||||
return _toggleDefaultLockingMode(
|
||||
<Toggle
|
||||
disabled={pifInUse}
|
||||
onChange={this._editNetwork}
|
||||
value={network.defaultIsLocked}
|
||||
value={networks[pif.$network].defaultIsLocked}
|
||||
/>,
|
||||
pifInUse && _('pifInUse')
|
||||
)
|
||||
@@ -219,11 +211,9 @@ const COLUMNS = [
|
||||
sortCriteria: 'device',
|
||||
},
|
||||
{
|
||||
itemRenderer: (pif, userData) =>
|
||||
get(() => userData.networks[pif.$network].name_label),
|
||||
itemRenderer: (pif, userData) => userData.networks[pif.$network].name_label,
|
||||
name: _('pifNetworkLabel'),
|
||||
sortCriteria: (pif, userData) =>
|
||||
get(() => userData.networks[pif.$network].name_label),
|
||||
sortCriteria: (pif, userData) => userData.networks[pif.$network].name_label,
|
||||
},
|
||||
{
|
||||
component: PifItemVlan,
|
||||
@@ -294,11 +284,6 @@ const COLUMNS = [
|
||||
]
|
||||
|
||||
const INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
handler: pif => copy(pif.uuid),
|
||||
icon: 'clipboard',
|
||||
label: pif => _('copyUuid', { uuid: pif.uuid }),
|
||||
},
|
||||
{
|
||||
handler: deletePif,
|
||||
icon: 'delete',
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import React, { Component } from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { alert, chooseAction } from 'modal'
|
||||
import { chooseAction } from 'modal'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createDoesHostNeedRestart, createSelector } from 'selectors'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { restartHost, installAllHostPatches, installHostPatch } from 'xo'
|
||||
import { restartHost } from 'xo'
|
||||
import { isEmpty, isString } from 'lodash'
|
||||
|
||||
const MISSING_PATCH_COLUMNS = [
|
||||
@@ -47,76 +48,15 @@ const MISSING_PATCH_COLUMNS = [
|
||||
itemRenderer: patch => patch.guidance,
|
||||
sortCriteria: patch => patch.guidance,
|
||||
},
|
||||
]
|
||||
|
||||
const MISSING_PATCH_COLUMNS_XCP = [
|
||||
{
|
||||
name: _('patchNameLabel'),
|
||||
itemRenderer: patch => patch.name,
|
||||
sortCriteria: 'name',
|
||||
},
|
||||
{
|
||||
name: _('patchDescription'),
|
||||
itemRenderer: patch => patch.description,
|
||||
sortCriteria: 'description',
|
||||
},
|
||||
{
|
||||
name: _('patchVersion'),
|
||||
itemRenderer: patch => patch.version,
|
||||
},
|
||||
{
|
||||
name: _('patchRelease'),
|
||||
itemRenderer: patch => patch.release,
|
||||
},
|
||||
{
|
||||
name: _('patchSize'),
|
||||
itemRenderer: patch => formatSize(patch.size),
|
||||
sortCriteria: 'size',
|
||||
},
|
||||
]
|
||||
|
||||
const INDIVIDUAL_ACTIONS_XCP = [
|
||||
{
|
||||
disabled: patch => patch.changelog === null,
|
||||
handler: patch =>
|
||||
alert(
|
||||
_('changelog'),
|
||||
<Container>
|
||||
<Row className='mb-1'>
|
||||
<Col size={3}>
|
||||
<strong>{_('changelogPatch')}</strong>
|
||||
</Col>
|
||||
<Col size={9}>{patch.name}</Col>
|
||||
</Row>
|
||||
<Row className='mb-1'>
|
||||
<Col size={3}>
|
||||
<strong>{_('changelogDate')}</strong>
|
||||
</Col>
|
||||
<Col size={9}>
|
||||
<FormattedTime
|
||||
value={patch.changelog.date * 1000}
|
||||
day='numeric'
|
||||
month='long'
|
||||
year='numeric'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='mb-1'>
|
||||
<Col size={3}>
|
||||
<strong>{_('changelogAuthor')}</strong>
|
||||
</Col>
|
||||
<Col size={9}>{patch.changelog.author}</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col size={3}>
|
||||
<strong>{_('changelogDescription')}</strong>
|
||||
</Col>
|
||||
<Col size={9}>{patch.changelog.description}</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
),
|
||||
icon: 'preview',
|
||||
label: _('showChangelog'),
|
||||
name: _('patchAction'),
|
||||
itemRenderer: (patch, { installPatch, _installPatchWarning }) => (
|
||||
<ActionRowButton
|
||||
btnStyle='primary'
|
||||
handler={() => _installPatchWarning(patch, installPatch)}
|
||||
icon='host-patch-update'
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -178,139 +118,10 @@ const INSTALLED_PATCH_COLUMNS_2 = [
|
||||
},
|
||||
]
|
||||
|
||||
class XcpPatches extends Component {
|
||||
render () {
|
||||
const { missingPatches, host, installAllPatches } = this.props
|
||||
const hasMissingPatches = !isEmpty(missingPatches)
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
{this.props.needsRestart && (
|
||||
<TabButton
|
||||
btnStyle='warning'
|
||||
handler={restartHost}
|
||||
handlerParam={host}
|
||||
icon='host-reboot'
|
||||
labelId='rebootUpdateHostLabel'
|
||||
/>
|
||||
)}
|
||||
<TabButton
|
||||
disabled={!hasMissingPatches}
|
||||
btnStyle={hasMissingPatches ? 'primary' : undefined}
|
||||
handler={installAllPatches}
|
||||
icon={hasMissingPatches ? 'host-patch-update' : 'success'}
|
||||
labelId={hasMissingPatches ? 'patchUpdateButton' : 'hostUpToDate'}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{hasMissingPatches && (
|
||||
<Row>
|
||||
<Col>
|
||||
<SortedTable
|
||||
columns={MISSING_PATCH_COLUMNS_XCP}
|
||||
collection={missingPatches}
|
||||
individualActions={INDIVIDUAL_ACTIONS_XCP}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@connectStore(() => ({
|
||||
needsRestart: createDoesHostNeedRestart((_, props) => props.host),
|
||||
}))
|
||||
class XenServerPatches extends Component {
|
||||
_getPatches = createSelector(
|
||||
() => this.props.host,
|
||||
() => this.props.hostPatches,
|
||||
(host, hostPatches) => {
|
||||
if (isEmpty(host.patches) && isEmpty(hostPatches)) {
|
||||
return { patches: null }
|
||||
}
|
||||
|
||||
if (isString(host.patches[0])) {
|
||||
return {
|
||||
patches: hostPatches,
|
||||
columns: INSTALLED_PATCH_COLUMNS,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
patches: host.patches,
|
||||
columns: INSTALLED_PATCH_COLUMNS_2,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
_individualActions = [
|
||||
{
|
||||
name: _('patchAction'),
|
||||
level: 'primary',
|
||||
handler: this.props.installPatch,
|
||||
icon: 'host-patch-update',
|
||||
},
|
||||
]
|
||||
|
||||
render () {
|
||||
const { host, missingPatches, installAllPatches } = this.props
|
||||
const { patches, columns } = this._getPatches()
|
||||
const hasMissingPatches = !isEmpty(missingPatches)
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
{this.props.needsRestart && (
|
||||
<TabButton
|
||||
btnStyle='warning'
|
||||
handler={restartHost}
|
||||
handlerParam={host}
|
||||
icon='host-reboot'
|
||||
labelId='rebootUpdateHostLabel'
|
||||
/>
|
||||
)}
|
||||
<TabButton
|
||||
disabled={!hasMissingPatches}
|
||||
btnStyle={hasMissingPatches ? 'primary' : undefined}
|
||||
handler={installAllPatches}
|
||||
icon={hasMissingPatches ? 'host-patch-update' : 'success'}
|
||||
labelId={hasMissingPatches ? 'patchUpdateButton' : 'hostUpToDate'}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{hasMissingPatches && (
|
||||
<Row>
|
||||
<Col>
|
||||
<h3>{_('hostMissingPatches')}</h3>
|
||||
<SortedTable
|
||||
individualActions={this._individualActions}
|
||||
collection={missingPatches}
|
||||
columns={MISSING_PATCH_COLUMNS}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row>
|
||||
<Col>
|
||||
{patches ? (
|
||||
<span>
|
||||
<h3>{_('hostAppliedPatches')}</h3>
|
||||
<SortedTable collection={patches} columns={columns} />
|
||||
</span>
|
||||
) : (
|
||||
<h4 className='text-xs-center'>{_('patchNothing')}</h4>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default class TabPatches extends Component {
|
||||
export default class HostPatches extends Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object,
|
||||
}
|
||||
@@ -334,31 +145,93 @@ export default class TabPatches extends Component {
|
||||
: this.context.router.push(`/pools/${this.props.host.$pool}/patches`)
|
||||
}
|
||||
|
||||
_installAllPatches = () =>
|
||||
this._chooseActionPatch(() => installAllHostPatches(this.props.host))
|
||||
_installPatchWarning = (patch, installPatch) =>
|
||||
this._chooseActionPatch(() => installPatch(patch))
|
||||
|
||||
_installPatch = patch =>
|
||||
this._chooseActionPatch(() => installHostPatch(this.props.host, patch))
|
||||
_installAllPatchesWarning = installAllPatches =>
|
||||
this._chooseActionPatch(installAllPatches)
|
||||
|
||||
_getPatches = createSelector(
|
||||
() => this.props.host,
|
||||
() => this.props.hostPatches,
|
||||
(host, hostPatches) => {
|
||||
if (isEmpty(host.patches) && isEmpty(hostPatches)) {
|
||||
return { patches: null }
|
||||
}
|
||||
|
||||
if (isString(host.patches[0])) {
|
||||
return {
|
||||
patches: hostPatches,
|
||||
columns: INSTALLED_PATCH_COLUMNS,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
patches: host.patches,
|
||||
columns: INSTALLED_PATCH_COLUMNS_2,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
if (process.env.XOA_PLAN < 2) {
|
||||
return (
|
||||
<Container>
|
||||
<Upgrade place='hostPatches' available={2} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
if (this.props.missingPatches === null) {
|
||||
return <em>{_('updatePluginNotInstalled')}</em>
|
||||
}
|
||||
return this.props.host.productBrand === 'XCP-ng' ? (
|
||||
<XcpPatches {...this.props} installAllPatches={this._installAllPatches} />
|
||||
const { host, missingPatches, installAllPatches, installPatch } = this.props
|
||||
const { patches, columns } = this._getPatches()
|
||||
const hasMissingPatches = !isEmpty(missingPatches)
|
||||
return process.env.XOA_PLAN > 1 ? (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
{this.props.needsRestart && (
|
||||
<TabButton
|
||||
btnStyle='warning'
|
||||
handler={restartHost}
|
||||
handlerParam={host}
|
||||
icon='host-reboot'
|
||||
labelId='rebootUpdateHostLabel'
|
||||
/>
|
||||
)}
|
||||
<TabButton
|
||||
disabled={!hasMissingPatches}
|
||||
btnStyle={hasMissingPatches ? 'primary' : undefined}
|
||||
handler={this._installAllPatchesWarning}
|
||||
handlerParam={installAllPatches}
|
||||
icon={hasMissingPatches ? 'host-patch-update' : 'success'}
|
||||
labelId={hasMissingPatches ? 'patchUpdateButton' : 'hostUpToDate'}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{hasMissingPatches && (
|
||||
<Row>
|
||||
<Col>
|
||||
<h3>{_('hostMissingPatches')}</h3>
|
||||
<SortedTable
|
||||
collection={missingPatches}
|
||||
userData={{
|
||||
installPatch,
|
||||
_installPatchWarning: this._installPatchWarning,
|
||||
}}
|
||||
columns={MISSING_PATCH_COLUMNS}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row>
|
||||
<Col>
|
||||
{patches ? (
|
||||
<span>
|
||||
<h3>{_('hostAppliedPatches')}</h3>
|
||||
<SortedTable collection={patches} columns={columns} />
|
||||
</span>
|
||||
) : (
|
||||
<h4 className='text-xs-center'>{_('patchNothing')}</h4>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
) : (
|
||||
<XenServerPatches
|
||||
{...this.props}
|
||||
installAllPatches={this._installAllPatches}
|
||||
installPatch={this._installPatch}
|
||||
/>
|
||||
<Container>
|
||||
<Upgrade place='hostPatches' available={2} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,12 +374,9 @@ export default class Jobs extends Component {
|
||||
forEach(item.values, valueItem => {
|
||||
forEach(valueItem, (value, key) => {
|
||||
if (data[key] === undefined) {
|
||||
data[key] = value
|
||||
} else if (Array.isArray(data[key])) {
|
||||
data[key].push(value)
|
||||
} else {
|
||||
data[key] = [data[key], value]
|
||||
data[key] = []
|
||||
}
|
||||
data[key].push(value)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import { alert } from 'modal'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { keyBy } from 'lodash'
|
||||
import { forEach, keyBy } from 'lodash'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import { get } from 'xo-defined'
|
||||
import {
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from 'xo'
|
||||
|
||||
import LogAlertBody from './log-alert-body'
|
||||
import { isSkippedError, NO_VMS_MATCH_THIS_PATTERN } from './utils'
|
||||
|
||||
const STATUS_LABELS = {
|
||||
failure: {
|
||||
@@ -30,7 +31,7 @@ const STATUS_LABELS = {
|
||||
className: 'success',
|
||||
label: 'jobSuccess',
|
||||
},
|
||||
pending: {
|
||||
started: {
|
||||
className: 'warning',
|
||||
label: 'jobStarted',
|
||||
},
|
||||
@@ -93,10 +94,10 @@ const LOG_COLUMNS = [
|
||||
{
|
||||
name: _('jobDuration'),
|
||||
itemRenderer: log =>
|
||||
log.end !== undefined && (
|
||||
<FormattedDuration duration={log.end - log.start} />
|
||||
log.duration !== undefined && (
|
||||
<FormattedDuration duration={log.duration} />
|
||||
),
|
||||
sortCriteria: log => log.end - log.start,
|
||||
sortCriteria: log => log.duration,
|
||||
},
|
||||
{
|
||||
name: _('jobStatus'),
|
||||
@@ -107,20 +108,15 @@ const LOG_COLUMNS = [
|
||||
},
|
||||
]
|
||||
|
||||
const showTasks = log =>
|
||||
const showCalls = (log, { logs, jobs }) =>
|
||||
alert(
|
||||
<span>
|
||||
{_('jobModalTitle', { job: log.jobId.slice(4, 8) })}{' '}
|
||||
<span style={{ fontSize: '0.5em' }} className='text-muted'>
|
||||
{log.id}
|
||||
</span>
|
||||
</span>,
|
||||
<LogAlertBody log={log} />
|
||||
_('jobModalTitle', { job: log.jobId.slice(4, 8) }),
|
||||
<LogAlertBody log={log} job={get(() => jobs[log.jobId])} logs={logs} />
|
||||
)
|
||||
|
||||
const LOG_INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
handler: showTasks,
|
||||
handler: showCalls,
|
||||
icon: 'preview',
|
||||
label: _('logDisplayDetails'),
|
||||
},
|
||||
@@ -142,6 +138,38 @@ const LOG_FILTERS = {
|
||||
jobSuccess: 'status: success',
|
||||
}
|
||||
|
||||
const rowTransform = (log, { logs, jobs }) => {
|
||||
let status
|
||||
if (log.end !== undefined) {
|
||||
if (log.error !== undefined) {
|
||||
status =
|
||||
log.error.message === NO_VMS_MATCH_THIS_PATTERN ? 'skipped' : 'failure'
|
||||
} else {
|
||||
let hasError = false
|
||||
let hasTaskSkipped = false
|
||||
forEach(logs[log.id], ({ status, result }) => {
|
||||
if (status !== 'failure') {
|
||||
return
|
||||
}
|
||||
if (result === undefined || !isSkippedError(result)) {
|
||||
hasError = true
|
||||
return false
|
||||
}
|
||||
hasTaskSkipped = true
|
||||
})
|
||||
status = hasError ? 'failure' : hasTaskSkipped ? 'skipped' : 'success'
|
||||
}
|
||||
} else {
|
||||
status =
|
||||
log.id === get(() => jobs[log.jobId].runId) ? 'started' : 'interrupted'
|
||||
}
|
||||
|
||||
return {
|
||||
...log,
|
||||
status,
|
||||
}
|
||||
}
|
||||
|
||||
export default [
|
||||
addSubscriptions({
|
||||
logs: subscribeBackupNgLogs,
|
||||
@@ -155,13 +183,15 @@ export default [
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
actions={LOG_ACTIONS}
|
||||
collection={logs}
|
||||
collection={get(() => logs['roots'])}
|
||||
columns={LOG_COLUMNS}
|
||||
component={SortedTable}
|
||||
data-jobs={jobs}
|
||||
data-logs={logs}
|
||||
emptyMessage={_('noLogs')}
|
||||
filters={LOG_FILTERS}
|
||||
individualActions={LOG_INDIVIDUAL_ACTIONS}
|
||||
rowTransform={rowTransform}
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
|
||||
@@ -13,8 +13,8 @@ import Tooltip from 'tooltip'
|
||||
import { alert } from 'modal'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { connectStore, formatSize, formatSpeed } from 'utils'
|
||||
import { createGetObject, createSelector } from 'selectors'
|
||||
import { filter, forEach, includes, keyBy, map, orderBy } from 'lodash'
|
||||
import { createFilter, createGetObject, createSelector } from 'selectors'
|
||||
import { forEach, includes, keyBy, map, orderBy } from 'lodash'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import { get } from 'xo-defined'
|
||||
import {
|
||||
@@ -141,6 +141,8 @@ const isSkippedError = error =>
|
||||
error.message === UNHEALTHY_VDI_CHAIN_ERROR ||
|
||||
error.message === NO_SUCH_OBJECT_ERROR
|
||||
|
||||
const filterOptionRenderer = ({ label }) => _(label)
|
||||
|
||||
class Log extends BaseComponent {
|
||||
state = {
|
||||
filter: DEFAULT_CALL_FILTER,
|
||||
@@ -152,29 +154,21 @@ class Log extends BaseComponent {
|
||||
(logId, runId) => logId !== runId
|
||||
)
|
||||
|
||||
_getCallsByState = createSelector(
|
||||
_getFilteredCalls = createFilter(
|
||||
() => this.props.log.calls,
|
||||
this._getIsJobInterrupted,
|
||||
(calls, isInterrupted) => {
|
||||
const callsByState = {}
|
||||
forEach(CALL_FILTER_OPTIONS, ({ value }) => {
|
||||
callsByState[value] = filter(calls, PREDICATES[value](isInterrupted))
|
||||
})
|
||||
return callsByState
|
||||
}
|
||||
createSelector(
|
||||
() => this.state.filter.value,
|
||||
this._getIsJobInterrupted,
|
||||
(value, isInterrupted) => PREDICATES[value](isInterrupted)
|
||||
)
|
||||
)
|
||||
|
||||
_getFilteredCalls = createSelector(
|
||||
() => this.state.filter.value,
|
||||
this._getCallsByState,
|
||||
(value, calls) => calls[value]
|
||||
)
|
||||
|
||||
_getFilterOptionRenderer = createSelector(
|
||||
this._getCallsByState,
|
||||
calls => ({ label, value }) => (
|
||||
_filterValueRenderer = createSelector(
|
||||
() => this._getFilteredCalls().length,
|
||||
({ label }) => label,
|
||||
(size, label) => (
|
||||
<span>
|
||||
{_(label)} ({calls[value].length})
|
||||
{_(label)} ({size})
|
||||
</span>
|
||||
)
|
||||
)
|
||||
@@ -196,11 +190,12 @@ class Log extends BaseComponent {
|
||||
<Select
|
||||
labelKey='label'
|
||||
onChange={this.linkState('filter')}
|
||||
optionRenderer={this._getFilterOptionRenderer()}
|
||||
optionRenderer={filterOptionRenderer}
|
||||
options={CALL_FILTER_OPTIONS}
|
||||
required
|
||||
value={this.state.filter}
|
||||
valueKey='value'
|
||||
valueRenderer={this._filterValueRenderer}
|
||||
/>
|
||||
<br />
|
||||
<ul className='list-group'>
|
||||
@@ -399,9 +394,7 @@ const LOG_COLUMNS = [
|
||||
'tag',
|
||||
log.hasErrors
|
||||
? 'tag-danger'
|
||||
: log.callSkipped
|
||||
? 'tag-info'
|
||||
: 'tag-success'
|
||||
: log.callSkipped ? 'tag-info' : 'tag-success'
|
||||
)}
|
||||
>
|
||||
{_('jobFinished')}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import _, { FormattedDuration } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Copiable from 'copiable'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
@@ -7,10 +6,37 @@ import renderXoItem, { renderXoItemFromId } from 'render-xo-item'
|
||||
import Select from 'form/select'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addSubscriptions, formatSize, formatSpeed } from 'utils'
|
||||
import { filter, isEmpty, get, keyBy, map } from 'lodash'
|
||||
import { createSelector } from 'selectors'
|
||||
import { find, filter, isEmpty, get, keyBy, map, forEach } from 'lodash'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import { injectState, provideState } from '@julien-f/freactal'
|
||||
import { runBackupNgJob, subscribeRemotes } from 'xo'
|
||||
import { subscribeRemotes } from 'xo'
|
||||
|
||||
import {
|
||||
isSkippedError,
|
||||
NO_VMS_MATCH_THIS_PATTERN,
|
||||
UNHEALTHY_VDI_CHAIN_ERROR,
|
||||
} from './utils'
|
||||
|
||||
const getTaskStatus = createSelector(
|
||||
taskLog => taskLog,
|
||||
isJobRunning => isJobRunning,
|
||||
({ end, status, result }, isJobRunning) =>
|
||||
end !== undefined
|
||||
? status === 'success'
|
||||
? 'success'
|
||||
: result !== undefined && isSkippedError(result) ? 'skipped' : 'failure'
|
||||
: isJobRunning ? 'started' : 'interrupted'
|
||||
)
|
||||
|
||||
const getSubTaskStatus = createSelector(
|
||||
taskLog => taskLog,
|
||||
isJobRunning => isJobRunning,
|
||||
({ end, status, result }, isJobRunning) =>
|
||||
end !== undefined
|
||||
? status === 'success' ? 'success' : 'failure'
|
||||
: isJobRunning ? 'started' : 'interrupted'
|
||||
)
|
||||
|
||||
const TASK_STATUS = {
|
||||
failure: {
|
||||
@@ -25,7 +51,7 @@ const TASK_STATUS = {
|
||||
icon: 'running',
|
||||
label: 'taskSuccess',
|
||||
},
|
||||
pending: {
|
||||
started: {
|
||||
icon: 'busy',
|
||||
label: 'taskStarted',
|
||||
},
|
||||
@@ -44,60 +70,90 @@ const TaskStateInfos = ({ status }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const TaskDate = ({ label, value }) =>
|
||||
_.keyValue(
|
||||
_(label),
|
||||
<FormattedDate
|
||||
value={new Date(value)}
|
||||
month='short'
|
||||
day='numeric'
|
||||
year='numeric'
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
second='2-digit'
|
||||
/>
|
||||
const VmTaskDataInfos = ({ logs, vmTaskId }) => {
|
||||
let transferSize, transferDuration, mergeSize, mergeDuration
|
||||
forEach(logs[vmTaskId], ({ taskId }) => {
|
||||
if (transferSize !== undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
const transferTask = find(logs[taskId], { message: 'transfer' })
|
||||
if (transferTask !== undefined) {
|
||||
transferSize = transferTask.result.size
|
||||
transferDuration = transferTask.end - transferTask.start
|
||||
}
|
||||
|
||||
const mergeTask = find(logs[taskId], { message: 'merge' })
|
||||
if (mergeTask !== undefined) {
|
||||
mergeSize = mergeTask.result.size
|
||||
mergeDuration = mergeTask.end - mergeTask.start
|
||||
}
|
||||
})
|
||||
|
||||
if (transferSize === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{_.keyValue(_('taskTransferredDataSize'), formatSize(transferSize))}
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('taskTransferredDataSpeed'),
|
||||
formatSpeed(transferSize, transferDuration)
|
||||
)}
|
||||
{mergeSize !== undefined && (
|
||||
<div>
|
||||
{_.keyValue(_('taskMergedDataSize'), formatSize(mergeSize))}
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('taskMergedDataSpeed'),
|
||||
formatSpeed(mergeSize, mergeDuration)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
const UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'
|
||||
}
|
||||
|
||||
const UNHEALTHY_VDI_CHAIN_LINK =
|
||||
'https://xen-orchestra.com/docs/backup_troubleshooting.html#vdi-chain-protection'
|
||||
|
||||
const ALL_FILTER_OPTION = { label: 'allTasks', value: 'all' }
|
||||
const FAILURE_FILTER_OPTION = { label: 'taskFailed', value: 'failure' }
|
||||
const PENDING_FILTER_OPTION = { label: 'taskStarted', value: 'pending' }
|
||||
const INTERRUPTED_FILTER_OPTION = {
|
||||
label: 'taskInterrupted',
|
||||
value: 'interrupted',
|
||||
}
|
||||
const STARTED_FILTER_OPTION = { label: 'taskStarted', value: 'started' }
|
||||
const TASK_FILTER_OPTIONS = [
|
||||
ALL_FILTER_OPTION,
|
||||
FAILURE_FILTER_OPTION,
|
||||
PENDING_FILTER_OPTION,
|
||||
INTERRUPTED_FILTER_OPTION,
|
||||
STARTED_FILTER_OPTION,
|
||||
{ label: 'taskInterrupted', value: 'interrupted' },
|
||||
{ label: 'taskSkipped', value: 'skipped' },
|
||||
{ label: 'taskSuccess', value: 'success' },
|
||||
]
|
||||
|
||||
const getFilteredTaskLogs = (logs, filterValue) =>
|
||||
const getFilteredTaskLogs = (logs, isJobRunning, filterValue) =>
|
||||
filterValue === 'all'
|
||||
? logs
|
||||
: filter(logs, ({ status }) => status === filterValue)
|
||||
: filter(logs, log => getTaskStatus(log, isJobRunning) === filterValue)
|
||||
|
||||
const getInitialFilter = tasks => {
|
||||
const getInitialFilter = (job, logs, log) => {
|
||||
const isEmptyFilter = filterValue =>
|
||||
isEmpty(getFilteredTaskLogs(tasks, filterValue))
|
||||
isEmpty(
|
||||
getFilteredTaskLogs(
|
||||
logs[log.id],
|
||||
get(job, 'runId') === log.id,
|
||||
filterValue
|
||||
)
|
||||
)
|
||||
|
||||
if (!isEmptyFilter('pending')) {
|
||||
return PENDING_FILTER_OPTION
|
||||
if (!isEmptyFilter('started')) {
|
||||
return STARTED_FILTER_OPTION
|
||||
}
|
||||
|
||||
if (!isEmptyFilter('failure')) {
|
||||
return FAILURE_FILTER_OPTION
|
||||
}
|
||||
|
||||
if (!isEmptyFilter('interrupted')) {
|
||||
return INTERRUPTED_FILTER_OPTION
|
||||
}
|
||||
|
||||
return ALL_FILTER_OPTION
|
||||
}
|
||||
|
||||
@@ -109,41 +165,43 @@ export default [
|
||||
}),
|
||||
}),
|
||||
provideState({
|
||||
initialState: ({ log }) => ({
|
||||
filter: getInitialFilter(log.tasks),
|
||||
initialState: ({ job, logs, log }) => ({
|
||||
filter: getInitialFilter(job, logs, log),
|
||||
}),
|
||||
effects: {
|
||||
setFilter: (_, filter) => state => ({
|
||||
...state,
|
||||
filter,
|
||||
}),
|
||||
restartVmJob: (_, { vm }) => async (
|
||||
_,
|
||||
{ log: { scheduleId, jobId } }
|
||||
) => {
|
||||
await runBackupNgJob({
|
||||
id: jobId,
|
||||
vm,
|
||||
schedule: scheduleId,
|
||||
})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
filteredTaskLogs: ({ filter: { value } }, { log }) =>
|
||||
getFilteredTaskLogs(log.tasks, value),
|
||||
optionRenderer: (state, { log }) => ({ label, value }) => (
|
||||
isJobRunning: (_, { job, log }) => get(job, 'runId') === log.id,
|
||||
filteredTaskLogs: ({ filter: { value }, isJobRunning }, { log, logs }) =>
|
||||
getFilteredTaskLogs(logs[log.id], isJobRunning, value),
|
||||
optionRenderer: ({ isJobRunning }, { log, logs }) => ({
|
||||
label,
|
||||
value,
|
||||
}) => (
|
||||
<span>
|
||||
{_(label)} ({getFilteredTaskLogs(log.tasks, value).length})
|
||||
{_(label)} ({
|
||||
getFilteredTaskLogs(logs[log.id], isJobRunning, value).length
|
||||
})
|
||||
</span>
|
||||
),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ log, remotes, state, effects }) =>
|
||||
log.result !== undefined ? (
|
||||
<span className={log.status === 'skipped' ? 'text-info' : 'text-danger'}>
|
||||
<Copiable tagName='p' data={JSON.stringify(log.result, null, 2)}>
|
||||
<Icon icon='alarm' /> {log.result.message}
|
||||
({ job, log, logs, remotes, state, effects }) =>
|
||||
log.error !== undefined ? (
|
||||
<span
|
||||
className={
|
||||
log.error.message === NO_VMS_MATCH_THIS_PATTERN
|
||||
? 'text-info'
|
||||
: 'text-danger'
|
||||
}
|
||||
>
|
||||
<Copiable tagName='p' data={JSON.stringify(log.error, null, 2)}>
|
||||
<Icon icon='alarm' /> {log.error.message}
|
||||
</Copiable>
|
||||
</span>
|
||||
) : (
|
||||
@@ -159,25 +217,18 @@ export default [
|
||||
/>
|
||||
<br />
|
||||
<ul className='list-group'>
|
||||
{map(state.filteredTaskLogs, taskLog => (
|
||||
<li key={taskLog.data.id} className='list-group-item'>
|
||||
{renderXoItemFromId(taskLog.data.id)} ({taskLog.data.id.slice(
|
||||
{map(state.filteredTaskLogs, vmTaskLog => (
|
||||
<li key={vmTaskLog.data.id} className='list-group-item'>
|
||||
{renderXoItemFromId(vmTaskLog.data.id)} ({vmTaskLog.data.id.slice(
|
||||
4,
|
||||
8
|
||||
)}) <TaskStateInfos status={taskLog.status} />{' '}
|
||||
{log.scheduleId !== undefined &&
|
||||
taskLog.status === 'failure' && (
|
||||
<ActionButton
|
||||
handler={effects.restartVmJob}
|
||||
icon='run'
|
||||
size='small'
|
||||
tooltip={_('backupRestartVm')}
|
||||
data-vm={taskLog.data.id}
|
||||
/>
|
||||
)}
|
||||
)}){' '}
|
||||
<TaskStateInfos
|
||||
status={getTaskStatus(vmTaskLog, state.isJobRunning)}
|
||||
/>
|
||||
<ul>
|
||||
{map(taskLog.tasks, subTaskLog => (
|
||||
<li key={subTaskLog.id}>
|
||||
{map(logs[vmTaskLog.taskId], subTaskLog => (
|
||||
<li key={subTaskLog.taskId}>
|
||||
{subTaskLog.message === 'snapshot' ? (
|
||||
<span>
|
||||
<Icon icon='task' /> {_('snapshotVmLabel')}
|
||||
@@ -200,116 +251,61 @@ export default [
|
||||
)})
|
||||
</span>
|
||||
)}{' '}
|
||||
<TaskStateInfos status={subTaskLog.status} />
|
||||
<ul>
|
||||
{map(subTaskLog.tasks, operationLog => (
|
||||
<li key={operationLog.id}>
|
||||
<span>
|
||||
<Icon icon='task' /> {operationLog.message}
|
||||
</span>{' '}
|
||||
<TaskStateInfos status={operationLog.status} />
|
||||
<br />
|
||||
<TaskDate
|
||||
label='taskStart'
|
||||
value={operationLog.start}
|
||||
/>
|
||||
{operationLog.end !== undefined && (
|
||||
<div>
|
||||
<TaskDate
|
||||
label='taskEnd'
|
||||
value={operationLog.end}
|
||||
/>
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('taskDuration'),
|
||||
<FormattedDuration
|
||||
duration={
|
||||
operationLog.end - operationLog.start
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
{operationLog.status === 'failure' ? (
|
||||
<Copiable
|
||||
tagName='p'
|
||||
data={JSON.stringify(
|
||||
operationLog.result,
|
||||
null,
|
||||
2
|
||||
)}
|
||||
>
|
||||
{_.keyValue(
|
||||
_('taskError'),
|
||||
<span className='text-danger'>
|
||||
{operationLog.result.message}
|
||||
</span>
|
||||
)}
|
||||
</Copiable>
|
||||
) : (
|
||||
<div>
|
||||
{_.keyValue(
|
||||
_('operationSize'),
|
||||
formatSize(operationLog.result.size)
|
||||
)}
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('operationSpeed'),
|
||||
formatSpeed(
|
||||
operationLog.result.size,
|
||||
operationLog.end - operationLog.start
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<TaskDate label='taskStart' value={subTaskLog.start} />
|
||||
{subTaskLog.end !== undefined && (
|
||||
<div>
|
||||
<TaskDate label='taskEnd' value={subTaskLog.end} />
|
||||
<br />
|
||||
{subTaskLog.message !== 'snapshot' &&
|
||||
_.keyValue(
|
||||
_('taskDuration'),
|
||||
<FormattedDuration
|
||||
duration={subTaskLog.end - subTaskLog.start}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
{subTaskLog.status === 'failure' &&
|
||||
subTaskLog.result !== undefined && (
|
||||
<Copiable
|
||||
tagName='p'
|
||||
data={JSON.stringify(subTaskLog.result, null, 2)}
|
||||
>
|
||||
{_.keyValue(
|
||||
_('taskError'),
|
||||
<span className='text-danger'>
|
||||
{subTaskLog.result.message}
|
||||
</span>
|
||||
)}
|
||||
</Copiable>
|
||||
)}
|
||||
</div>
|
||||
<TaskStateInfos
|
||||
status={getSubTaskStatus(subTaskLog, state.isJobRunning)}
|
||||
/>
|
||||
<br />
|
||||
{subTaskLog.status === 'failure' && (
|
||||
<Copiable
|
||||
tagName='p'
|
||||
data={JSON.stringify(subTaskLog.result, null, 2)}
|
||||
>
|
||||
{_.keyValue(
|
||||
_('taskError'),
|
||||
<span className={'text-danger'}>
|
||||
{subTaskLog.result.message}
|
||||
</span>
|
||||
)}
|
||||
</Copiable>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<TaskDate label='taskStart' value={taskLog.start} />
|
||||
{taskLog.end !== undefined && (
|
||||
{_.keyValue(
|
||||
_('taskStart'),
|
||||
<FormattedDate
|
||||
value={new Date(vmTaskLog.start)}
|
||||
month='short'
|
||||
day='numeric'
|
||||
year='numeric'
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
second='2-digit'
|
||||
/>
|
||||
)}
|
||||
{vmTaskLog.end !== undefined && (
|
||||
<div>
|
||||
<TaskDate label='taskEnd' value={taskLog.end} />
|
||||
{_.keyValue(
|
||||
_('taskEnd'),
|
||||
<FormattedDate
|
||||
value={new Date(vmTaskLog.end)}
|
||||
month='short'
|
||||
day='numeric'
|
||||
year='numeric'
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
second='2-digit'
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('taskDuration'),
|
||||
<FormattedDuration duration={taskLog.end - taskLog.start} />
|
||||
<FormattedDuration duration={vmTaskLog.duration} />
|
||||
)}
|
||||
<br />
|
||||
{taskLog.result !== undefined ? (
|
||||
taskLog.result.message === UNHEALTHY_VDI_CHAIN_ERROR ? (
|
||||
{vmTaskLog.status === 'failure' &&
|
||||
vmTaskLog.result !== undefined ? (
|
||||
vmTaskLog.result.message === UNHEALTHY_VDI_CHAIN_ERROR ? (
|
||||
<Tooltip content={_('clickForMoreInformation')}>
|
||||
<a
|
||||
className='text-info'
|
||||
@@ -323,59 +319,24 @@ export default [
|
||||
) : (
|
||||
<Copiable
|
||||
tagName='p'
|
||||
data={JSON.stringify(taskLog.result, null, 2)}
|
||||
data={JSON.stringify(vmTaskLog.result, null, 2)}
|
||||
>
|
||||
{_.keyValue(
|
||||
taskLog.status === 'skipped'
|
||||
? _('taskReason')
|
||||
: _('taskError'),
|
||||
_('taskError'),
|
||||
<span
|
||||
className={
|
||||
taskLog.status === 'skipped'
|
||||
isSkippedError(vmTaskLog.result)
|
||||
? 'text-info'
|
||||
: 'text-danger'
|
||||
}
|
||||
>
|
||||
{taskLog.result.message}
|
||||
{vmTaskLog.result.message}
|
||||
</span>
|
||||
)}
|
||||
</Copiable>
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
{taskLog.transfer !== undefined && (
|
||||
<div>
|
||||
{_.keyValue(
|
||||
_('taskTransferredDataSize'),
|
||||
formatSize(taskLog.transfer.size)
|
||||
)}
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('taskTransferredDataSpeed'),
|
||||
formatSpeed(
|
||||
taskLog.transfer.size,
|
||||
taskLog.transfer.duration
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{taskLog.merge !== undefined && (
|
||||
<div>
|
||||
{_.keyValue(
|
||||
_('taskMergedDataSize'),
|
||||
formatSize(taskLog.merge.size)
|
||||
)}
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('taskMergedDataSpeed'),
|
||||
formatSpeed(
|
||||
taskLog.merge.size,
|
||||
taskLog.merge.duration
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<VmTaskDataInfos logs={logs} vmTaskId={vmTaskLog.taskId} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
7
packages/xo-web/src/xo-app/logs/utils.js
Normal file
7
packages/xo-web/src/xo-app/logs/utils.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export const NO_VMS_MATCH_THIS_PATTERN = 'no VMs match this pattern'
|
||||
export const UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'
|
||||
const NO_SUCH_OBJECT_ERROR = 'no such object'
|
||||
|
||||
export const isSkippedError = error =>
|
||||
error.message === UNHEALTHY_VDI_CHAIN_ERROR ||
|
||||
error.message === NO_SUCH_OBJECT_ERROR
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
createFilter,
|
||||
createGetObjectsOfType,
|
||||
createSelector,
|
||||
getIsPoolAdmin,
|
||||
getStatus,
|
||||
getUser,
|
||||
isAdmin,
|
||||
@@ -32,7 +31,6 @@ const returnTrue = () => true
|
||||
@connectStore(
|
||||
() => ({
|
||||
isAdmin,
|
||||
isPoolAdmin: getIsPoolAdmin,
|
||||
nTasks: createGetObjectsOfType('task').count([
|
||||
task => task.status === 'pending',
|
||||
]),
|
||||
@@ -82,6 +80,11 @@ export default class Menu extends Component {
|
||||
isEmpty
|
||||
)
|
||||
|
||||
_getNoOperatableSrs = createSelector(
|
||||
createFilter(() => this.props.srs, this._checkPermissions),
|
||||
isEmpty
|
||||
)
|
||||
|
||||
_getNoResourceSets = createSelector(() => this.props.resourceSets, isEmpty)
|
||||
|
||||
get height () {
|
||||
@@ -105,17 +108,9 @@ export default class Menu extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
isAdmin,
|
||||
isPoolAdmin,
|
||||
nTasks,
|
||||
status,
|
||||
user,
|
||||
pools,
|
||||
nHosts,
|
||||
srs,
|
||||
} = this.props
|
||||
const { isAdmin, nTasks, status, user, pools, nHosts } = this.props
|
||||
const noOperatablePools = this._getNoOperatablePools()
|
||||
const noOperatableSrs = this._getNoOperatableSrs()
|
||||
const noResourceSets = this._getNoResourceSets()
|
||||
|
||||
/* eslint-disable object-property-newline */
|
||||
@@ -141,7 +136,7 @@ export default class Menu extends Component {
|
||||
icon: 'template',
|
||||
label: 'homeTemplatePage',
|
||||
},
|
||||
!isEmpty(srs) && {
|
||||
!noOperatableSrs && {
|
||||
to: '/home?t=SR',
|
||||
icon: 'sr',
|
||||
label: 'homeSrPage',
|
||||
@@ -318,9 +313,7 @@ export default class Menu extends Component {
|
||||
icon: 'menu-new',
|
||||
label: 'newMenu',
|
||||
subMenu: [
|
||||
(isAdmin ||
|
||||
(isPoolAdmin && process.env.XOA_PLAN > 3) ||
|
||||
!noResourceSets) && {
|
||||
(isAdmin || !noResourceSets) && {
|
||||
to: '/vms/new',
|
||||
icon: 'menu-new-vm',
|
||||
label: 'newVmPage',
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.refreshNames, .availableTemplateVars {
|
||||
.refreshNames {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user