Compare commits
164 Commits
xo-web-v5.
...
xo-server-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
552482275d | ||
|
|
f06d40cf95 | ||
|
|
cf3f1a1705 | ||
|
|
08583c06ef | ||
|
|
5271a5c984 | ||
|
|
e69610643b | ||
|
|
ef61e4fe6d | ||
|
|
4f776e1370 | ||
|
|
aa72708996 | ||
|
|
8751180634 | ||
|
|
2e327be49d | ||
|
|
f06a937c9c | ||
|
|
e65b3200cd | ||
|
|
30d3701ab1 | ||
|
|
05fa76dad3 | ||
|
|
4020081492 | ||
|
|
2fbd4a62b2 | ||
|
|
b773f5e821 | ||
|
|
76c5ced1dd | ||
|
|
197768875b | ||
|
|
f0483862a5 | ||
|
|
ac46d3a5a2 | ||
|
|
2da576a1f8 | ||
|
|
2e1ac27cf5 | ||
|
|
258404affc | ||
|
|
5121d9d1d7 | ||
|
|
f2a38c5ddd | ||
|
|
97a77b1a33 | ||
|
|
88ca41231f | ||
|
|
9a8f84ccb5 | ||
|
|
dd50fc37fe | ||
|
|
cafcadb286 | ||
|
|
db3d6bba79 | ||
|
|
11a0fc2a22 | ||
|
|
1e0a8a5034 | ||
|
|
34ef3e5998 | ||
|
|
e73fcc450d | ||
|
|
2946eaa156 | ||
|
|
6dcae9a7d7 | ||
|
|
abeb36f06c | ||
|
|
41139578ba | ||
|
|
cda7621b5d | ||
|
|
b75dd2d424 | ||
|
|
273f208722 | ||
|
|
c01e8e892e | ||
|
|
9dfd81c28f | ||
|
|
5dd26ebe33 | ||
|
|
4c0fe3c14f | ||
|
|
2353581da8 | ||
|
|
2934b23d2f | ||
|
|
82e4197237 | ||
|
|
a23189f132 | ||
|
|
47fa1ec81e | ||
|
|
4b468663f3 | ||
|
|
6628dc777d | ||
|
|
3ef3ae0166 | ||
|
|
bc6dbe2771 | ||
|
|
5651160d1c | ||
|
|
6da2669c6f | ||
|
|
8094b5097f | ||
|
|
bdb0547b86 | ||
|
|
ea08fbbfba | ||
|
|
b4cbd8b2b5 | ||
|
|
f8fbb6b7d3 | ||
|
|
c8da9fec0a | ||
|
|
79fb3ec8bd | ||
|
|
2243966ce1 | ||
|
|
ca7d520997 | ||
|
|
df44487363 | ||
|
|
b39eb0f60d | ||
|
|
a3dcdc4fd5 | ||
|
|
2daac73c17 | ||
|
|
23eb3c3094 | ||
|
|
776d0f9e4a | ||
|
|
54bdcc6dd2 | ||
|
|
38084c8199 | ||
|
|
4525ee7491 | ||
|
|
66a476bd21 | ||
|
|
be6cc12632 | ||
|
|
673475dcb2 | ||
|
|
7dc1a80a83 | ||
|
|
d49294849f | ||
|
|
6b394302c1 | ||
|
|
00e1601f85 | ||
|
|
b75e746586 | ||
|
|
32a9fa9bb0 | ||
|
|
79d68dece4 | ||
|
|
1701e1d4ba | ||
|
|
497b3eb296 | ||
|
|
ecfafa0fea | ||
|
|
def66d8218 | ||
|
|
eeb08abec2 | ||
|
|
90923c657d | ||
|
|
4ff6eeb424 | ||
|
|
2d98fb40f1 | ||
|
|
256a58ded2 | ||
|
|
bf3b31a9ef | ||
|
|
7fc8d59605 | ||
|
|
1a39b2113a | ||
|
|
cb9f3fbb2c | ||
|
|
487f413cdd | ||
|
|
f847969206 | ||
|
|
5d9aad44c2 | ||
|
|
ba2027e6d7 | ||
|
|
087da9376f | ||
|
|
218e3b46e0 | ||
|
|
f9921e354e | ||
|
|
341148a7d3 | ||
|
|
7216165f1e | ||
|
|
a9557af04b | ||
|
|
873db3bf26 | ||
|
|
c795887a35 | ||
|
|
23824bafe8 | ||
|
|
5cca58f2b3 | ||
|
|
d05c9b6133 | ||
|
|
39a84a1ac0 | ||
|
|
b1c851c9d6 | ||
|
|
6280a9365c | ||
|
|
2741dacd64 | ||
|
|
4c2c2390bd | ||
|
|
635b8ce5f0 | ||
|
|
efc13cc456 | ||
|
|
078f319fe1 | ||
|
|
0f0e785871 | ||
|
|
4e4c85121c | ||
|
|
019d6f4cb6 | ||
|
|
725b0342d1 | ||
|
|
c93ccb8111 | ||
|
|
670befdaf6 | ||
|
|
55eefd865f | ||
|
|
43e5d610e3 | ||
|
|
b1245bc5be | ||
|
|
c2feab245e | ||
|
|
cb3753213e | ||
|
|
ec8c7a24af | ||
|
|
2456be2da3 | ||
|
|
8c5d4240f9 | ||
|
|
b1e12d1542 | ||
|
|
a58d7d2ff4 | ||
|
|
5308b8b9ed | ||
|
|
c15dffce8f | ||
|
|
874680462e | ||
|
|
bb42540775 | ||
|
|
b18511c905 | ||
|
|
5c660f4f64 | ||
|
|
f2bae73f77 | ||
|
|
e54d34f269 | ||
|
|
6470cbd2ee | ||
|
|
c06ebcb4a4 | ||
|
|
3eaa72c98c | ||
|
|
694fff060d | ||
|
|
2705062ac3 | ||
|
|
3df055a296 | ||
|
|
802bc15e0c | ||
|
|
ad2de40a9d | ||
|
|
19298570f8 | ||
|
|
1da4d1f1e9 | ||
|
|
fe4e9c18fa | ||
|
|
2c9f84f17f | ||
|
|
0b2e76600b | ||
|
|
873554fc01 | ||
|
|
82e2d013ae | ||
|
|
1eb5e80f1f | ||
|
|
9c0ab5b3cb |
@@ -1,9 +1,10 @@
|
||||
module.exports = {
|
||||
extends: ['standard', 'standard-jsx'],
|
||||
extends: ['standard', 'standard-jsx', 'prettier'],
|
||||
globals: {
|
||||
__DEV__: true,
|
||||
$Dict: true,
|
||||
$Diff: true,
|
||||
$ElementType: true,
|
||||
$Exact: true,
|
||||
$Keys: true,
|
||||
$PropertyType: true,
|
||||
@@ -16,12 +17,9 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
indent: 'off',
|
||||
'no-var': 'error',
|
||||
'node/no-extraneous-import': 'error',
|
||||
'node/no-extraneous-require': 'error',
|
||||
'prefer-const': 'error',
|
||||
'react/jsx-indent': 'off',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
jsxSingleQuote: true,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
|
||||
@@ -42,8 +42,8 @@ const getConfig = (key, ...args) => {
|
||||
return config === undefined
|
||||
? {}
|
||||
: typeof config === 'function'
|
||||
? config(...args)
|
||||
: config
|
||||
? config(...args)
|
||||
: config
|
||||
}
|
||||
|
||||
module.exports = function (pkg, plugins, presets) {
|
||||
|
||||
@@ -15,6 +15,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.4.1",
|
||||
"xen-api": "^0.20.0"
|
||||
"xen-api": "^0.22.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ class Schedule {
|
||||
zone.toLowerCase() === 'utc'
|
||||
? moment.utc
|
||||
: zone === 'local'
|
||||
? moment
|
||||
: () => moment.tz(zone)
|
||||
? moment
|
||||
: () => moment.tz(zone)
|
||||
}
|
||||
|
||||
createJob (fn) {
|
||||
|
||||
@@ -21,12 +21,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@marsaud/smb2": "^0.9.0",
|
||||
"@xen-orchestra/async-map": "^0.0.0",
|
||||
"execa": "^1.0.0",
|
||||
"fs-extra": "^7.0.0",
|
||||
"get-stream": "^4.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.11.0",
|
||||
"through2": "^2.0.3",
|
||||
"through2": "^3.0.0",
|
||||
"tmp": "^0.0.33",
|
||||
"xo-remote-parser": "^0.5.0"
|
||||
},
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// @flow
|
||||
|
||||
// $FlowFixMe
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import getStream from 'get-stream'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
|
||||
import { type Readable, type Writable } from 'stream'
|
||||
import { parse } from 'xo-remote-parser'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { resolve } from 'path'
|
||||
import { type Readable, type Writable } from 'stream'
|
||||
|
||||
import { createChecksumStream, validChecksumOfReadStream } from './checksum'
|
||||
|
||||
@@ -17,13 +20,18 @@ type File = FileDescriptor | string
|
||||
|
||||
const checksumFile = file => file + '.checksum'
|
||||
|
||||
// normalize the path:
|
||||
// - does not contains `.` or `..` (cannot escape root dir)
|
||||
// - always starts with `/`
|
||||
const normalizePath = path => resolve('/', path)
|
||||
|
||||
const DEFAULT_TIMEOUT = 6e5 // 10 min
|
||||
|
||||
export default class RemoteHandlerAbstract {
|
||||
_remote: Object
|
||||
_timeout: number
|
||||
|
||||
constructor (remote: any, options: Object = {}) {
|
||||
constructor(remote: any, options: Object = {}) {
|
||||
if (remote.url === 'test://') {
|
||||
this._remote = remote
|
||||
} else {
|
||||
@@ -35,34 +43,34 @@ export default class RemoteHandlerAbstract {
|
||||
;({ timeout: this._timeout = DEFAULT_TIMEOUT } = options)
|
||||
}
|
||||
|
||||
get type (): string {
|
||||
get type(): string {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the handler to sync the state of the effective remote with its' metadata
|
||||
*/
|
||||
async sync (): Promise<mixed> {
|
||||
async sync(): Promise<mixed> {
|
||||
return this._sync()
|
||||
}
|
||||
|
||||
async _sync (): Promise<mixed> {
|
||||
async _sync(): Promise<mixed> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the resources possibly dedicated to put the remote at work, when it is no more needed
|
||||
*/
|
||||
async forget (): Promise<void> {
|
||||
async forget(): Promise<void> {
|
||||
await this._forget()
|
||||
}
|
||||
|
||||
async _forget (): Promise<void> {
|
||||
async _forget(): Promise<void> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async test (): Promise<Object> {
|
||||
const testFileName = `${Date.now()}.test`
|
||||
async test(): Promise<Object> {
|
||||
const testFileName = `/${Date.now()}.test`
|
||||
const data = await fromCallback(cb => randomBytes(1024 * 1024, cb))
|
||||
let step = 'write'
|
||||
try {
|
||||
@@ -87,29 +95,33 @@ export default class RemoteHandlerAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
async outputFile (file: string, data: Data, options?: Object): Promise<void> {
|
||||
return this._outputFile(file, data, {
|
||||
async outputFile(file: string, data: Data, options?: Object): Promise<void> {
|
||||
return this._outputFile(normalizePath(file), data, {
|
||||
flags: 'wx',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
async _outputFile (file: string, data: Data, options?: Object): Promise<void> {
|
||||
const stream = await this.createOutputStream(file, options)
|
||||
async _outputFile(file: string, data: Data, options?: Object): Promise<void> {
|
||||
const stream = await this.createOutputStream(normalizePath(file), options)
|
||||
const promise = fromEvent(stream, 'finish')
|
||||
stream.end(data)
|
||||
await promise
|
||||
}
|
||||
|
||||
async read (
|
||||
async read(
|
||||
file: File,
|
||||
buffer: Buffer,
|
||||
position?: number
|
||||
): Promise<{| bytesRead: number, buffer: Buffer |}> {
|
||||
return this._read(file, buffer, position)
|
||||
return this._read(
|
||||
typeof file === 'string' ? normalizePath(file) : file,
|
||||
buffer,
|
||||
position
|
||||
)
|
||||
}
|
||||
|
||||
_read (
|
||||
_read(
|
||||
file: File,
|
||||
buffer: Buffer,
|
||||
position?: number
|
||||
@@ -117,19 +129,22 @@ export default class RemoteHandlerAbstract {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async readFile (file: string, options?: Object): Promise<Buffer> {
|
||||
return this._readFile(file, options)
|
||||
async readFile(file: string, options?: Object): Promise<Buffer> {
|
||||
return this._readFile(normalizePath(file), options)
|
||||
}
|
||||
|
||||
_readFile (file: string, options?: Object): Promise<Buffer> {
|
||||
_readFile(file: string, options?: Object): Promise<Buffer> {
|
||||
return this.createReadStream(file, options).then(getStream.buffer)
|
||||
}
|
||||
|
||||
async rename (
|
||||
async rename(
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
{ checksum = false }: Object = {}
|
||||
) {
|
||||
oldPath = normalizePath(oldPath)
|
||||
newPath = normalizePath(newPath)
|
||||
|
||||
let p = timeout.call(this._rename(oldPath, newPath), this._timeout)
|
||||
if (checksum) {
|
||||
p = Promise.all([
|
||||
@@ -140,17 +155,52 @@ export default class RemoteHandlerAbstract {
|
||||
return p
|
||||
}
|
||||
|
||||
async _rename (oldPath: string, newPath: string) {
|
||||
async _rename(oldPath: string, newPath: string) {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async list (
|
||||
async rmdir(
|
||||
dir: string,
|
||||
{ recursive = false }: { recursive?: boolean } = {}
|
||||
) {
|
||||
dir = normalizePath(dir)
|
||||
await (recursive ? this._rmtree(dir) : this._rmdir(dir))
|
||||
}
|
||||
|
||||
async _rmdir(dir: string) {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async _rmtree(dir: string) {
|
||||
try {
|
||||
return await this._rmdir(dir)
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOTEMPTY') {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const files = await this._list(dir)
|
||||
await asyncMap(files, file =>
|
||||
this._unlink(`${dir}/${file}`).catch(error => {
|
||||
if (error.code === 'EISDIR') {
|
||||
return this._rmtree(`${dir}/${file}`)
|
||||
}
|
||||
throw error
|
||||
})
|
||||
)
|
||||
return this._rmtree(dir)
|
||||
}
|
||||
|
||||
async list(
|
||||
dir: string = '.',
|
||||
{
|
||||
filter,
|
||||
prependDir = false,
|
||||
}: { filter?: (name: string) => boolean, prependDir?: boolean } = {}
|
||||
): Promise<string[]> {
|
||||
dir = normalizePath(dir)
|
||||
|
||||
let entries = await timeout.call(this._list(dir), this._timeout)
|
||||
if (filter !== undefined) {
|
||||
entries = entries.filter(filter)
|
||||
@@ -165,14 +215,17 @@ export default class RemoteHandlerAbstract {
|
||||
return entries
|
||||
}
|
||||
|
||||
async _list (dir: string): Promise<string[]> {
|
||||
async _list(dir: string): Promise<string[]> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
createReadStream (
|
||||
file: string,
|
||||
createReadStream(
|
||||
file: File,
|
||||
{ checksum = false, ignoreMissingChecksum = false, ...options }: Object = {}
|
||||
): Promise<LaxReadable> {
|
||||
if (typeof file === 'string') {
|
||||
file = normalizePath(file)
|
||||
}
|
||||
const path = typeof file === 'string' ? file : file.path
|
||||
const streamP = timeout
|
||||
.call(this._createReadStream(file, options), this._timeout)
|
||||
@@ -227,33 +280,34 @@ export default class RemoteHandlerAbstract {
|
||||
)
|
||||
}
|
||||
|
||||
async _createReadStream (
|
||||
file: string,
|
||||
options?: Object
|
||||
): Promise<LaxReadable> {
|
||||
async _createReadStream(file: File, options?: Object): Promise<LaxReadable> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async openFile (path: string, flags?: string): Promise<FileDescriptor> {
|
||||
async openFile(path: string, flags?: string): Promise<FileDescriptor> {
|
||||
path = normalizePath(path)
|
||||
|
||||
return {
|
||||
fd: await timeout.call(this._openFile(path, flags), this._timeout),
|
||||
path,
|
||||
}
|
||||
}
|
||||
|
||||
async _openFile (path: string, flags?: string): Promise<mixed> {
|
||||
async _openFile(path: string, flags?: string): Promise<mixed> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async closeFile (fd: FileDescriptor): Promise<void> {
|
||||
async closeFile(fd: FileDescriptor): Promise<void> {
|
||||
await timeout.call(this._closeFile(fd.fd), this._timeout)
|
||||
}
|
||||
|
||||
async _closeFile (fd: mixed): Promise<void> {
|
||||
async _closeFile(fd: mixed): Promise<void> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async refreshChecksum (path: string): Promise<void> {
|
||||
async refreshChecksum(path: string): Promise<void> {
|
||||
path = normalizePath(path)
|
||||
|
||||
const stream = (await this.createReadStream(path)).pipe(
|
||||
createChecksumStream()
|
||||
)
|
||||
@@ -261,10 +315,13 @@ export default class RemoteHandlerAbstract {
|
||||
await this.outputFile(checksumFile(path), await stream.checksum)
|
||||
}
|
||||
|
||||
async createOutputStream (
|
||||
async createOutputStream(
|
||||
file: File,
|
||||
{ checksum = false, ...options }: Object = {}
|
||||
): Promise<LaxWritable> {
|
||||
if (typeof file === 'string') {
|
||||
file = normalizePath(file)
|
||||
}
|
||||
const path = typeof file === 'string' ? file : file.path
|
||||
const streamP = timeout.call(
|
||||
this._createOutputStream(file, {
|
||||
@@ -295,14 +352,16 @@ export default class RemoteHandlerAbstract {
|
||||
return checksumStream
|
||||
}
|
||||
|
||||
async _createOutputStream (
|
||||
file: mixed,
|
||||
async _createOutputStream(
|
||||
file: File,
|
||||
options?: Object
|
||||
): Promise<LaxWritable> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async unlink (file: string, { checksum = true }: Object = {}): Promise<void> {
|
||||
async unlink(file: string, { checksum = true }: Object = {}): Promise<void> {
|
||||
file = normalizePath(file)
|
||||
|
||||
if (checksum) {
|
||||
ignoreErrors.call(this._unlink(checksumFile(file)))
|
||||
}
|
||||
@@ -310,15 +369,18 @@ export default class RemoteHandlerAbstract {
|
||||
await timeout.call(this._unlink(file), this._timeout)
|
||||
}
|
||||
|
||||
async _unlink (file: mixed): Promise<void> {
|
||||
async _unlink(file: string): Promise<void> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async getSize (file: mixed): Promise<number> {
|
||||
return timeout.call(this._getSize(file), this._timeout)
|
||||
async getSize(file: File): Promise<number> {
|
||||
return timeout.call(
|
||||
this._getSize(typeof file === 'string' ? normalizePath(file) : file),
|
||||
this._timeout
|
||||
)
|
||||
}
|
||||
|
||||
async _getSize (file: mixed): Promise<number> {
|
||||
async _getSize(file: File): Promise<number> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import AbstractHandler from './abstract'
|
||||
const TIMEOUT = 10e3
|
||||
|
||||
class TestHandler extends AbstractHandler {
|
||||
constructor (impl) {
|
||||
constructor(impl) {
|
||||
super({ url: 'test://' }, { timeout: TIMEOUT })
|
||||
|
||||
Object.keys(impl).forEach(method => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
|
||||
// $FlowFixMe
|
||||
import through2 from 'through2'
|
||||
import { createHash } from 'crypto'
|
||||
import { defer, fromEvent } from 'promise-toolbox'
|
||||
|
||||
@@ -1,51 +1,41 @@
|
||||
import fs from 'fs-extra'
|
||||
import { dirname, resolve } from 'path'
|
||||
import { noop, startsWith } from 'lodash'
|
||||
import { dirname } from 'path'
|
||||
import { noop } from 'lodash'
|
||||
|
||||
import RemoteHandlerAbstract from './abstract'
|
||||
|
||||
export default class LocalHandler extends RemoteHandlerAbstract {
|
||||
get type () {
|
||||
get type() {
|
||||
return 'file'
|
||||
}
|
||||
|
||||
_getRealPath () {
|
||||
_getRealPath() {
|
||||
return this._remote.path
|
||||
}
|
||||
|
||||
_getFilePath (file) {
|
||||
const realPath = this._getRealPath()
|
||||
const parts = [realPath]
|
||||
if (file) {
|
||||
parts.push(file)
|
||||
}
|
||||
const path = resolve.apply(null, parts)
|
||||
if (!startsWith(path, realPath)) {
|
||||
throw new Error('Remote path is unavailable')
|
||||
}
|
||||
return path
|
||||
_getFilePath(file) {
|
||||
return this._getRealPath() + file
|
||||
}
|
||||
|
||||
async _sync () {
|
||||
if (this._remote.enabled) {
|
||||
const path = this._getRealPath()
|
||||
await fs.ensureDir(path)
|
||||
await fs.access(path, fs.R_OK | fs.W_OK)
|
||||
}
|
||||
async _sync() {
|
||||
const path = this._getRealPath()
|
||||
await fs.ensureDir(path)
|
||||
await fs.access(path, fs.R_OK | fs.W_OK)
|
||||
|
||||
return this._remote
|
||||
}
|
||||
|
||||
async _forget () {
|
||||
async _forget() {
|
||||
return noop()
|
||||
}
|
||||
|
||||
async _outputFile (file, data, options) {
|
||||
async _outputFile(file, data, options) {
|
||||
const path = this._getFilePath(file)
|
||||
await fs.ensureDir(dirname(path))
|
||||
await fs.writeFile(path, data, options)
|
||||
}
|
||||
|
||||
async _read (file, buffer, position) {
|
||||
async _read(file, buffer, position) {
|
||||
const needsClose = typeof file === 'string'
|
||||
file = needsClose ? await fs.open(this._getFilePath(file), 'r') : file.fd
|
||||
try {
|
||||
@@ -63,19 +53,19 @@ export default class LocalHandler extends RemoteHandlerAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
async _readFile (file, options) {
|
||||
async _readFile(file, options) {
|
||||
return fs.readFile(this._getFilePath(file), options)
|
||||
}
|
||||
|
||||
async _rename (oldPath, newPath) {
|
||||
async _rename(oldPath, newPath) {
|
||||
return fs.rename(this._getFilePath(oldPath), this._getFilePath(newPath))
|
||||
}
|
||||
|
||||
async _list (dir = '.') {
|
||||
async _list(dir = '.') {
|
||||
return fs.readdir(this._getFilePath(dir))
|
||||
}
|
||||
|
||||
async _createReadStream (file, options) {
|
||||
async _createReadStream(file, options) {
|
||||
return typeof file === 'string'
|
||||
? fs.createReadStream(this._getFilePath(file), options)
|
||||
: fs.createReadStream('', {
|
||||
@@ -85,7 +75,7 @@ export default class LocalHandler extends RemoteHandlerAbstract {
|
||||
})
|
||||
}
|
||||
|
||||
async _createOutputStream (file, options) {
|
||||
async _createOutputStream(file, options) {
|
||||
if (typeof file === 'string') {
|
||||
const path = this._getFilePath(file)
|
||||
await fs.ensureDir(dirname(path))
|
||||
@@ -98,7 +88,7 @@ export default class LocalHandler extends RemoteHandlerAbstract {
|
||||
})
|
||||
}
|
||||
|
||||
async _unlink (file) {
|
||||
async _unlink(file) {
|
||||
return fs.unlink(this._getFilePath(file)).catch(error => {
|
||||
// do not throw if the file did not exist
|
||||
if (error == null || error.code !== 'ENOENT') {
|
||||
@@ -107,18 +97,22 @@ export default class LocalHandler extends RemoteHandlerAbstract {
|
||||
})
|
||||
}
|
||||
|
||||
async _getSize (file) {
|
||||
async _getSize(file) {
|
||||
const stats = await fs.stat(
|
||||
this._getFilePath(typeof file === 'string' ? file : file.path)
|
||||
)
|
||||
return stats.size
|
||||
}
|
||||
|
||||
async _openFile (path, flags) {
|
||||
async _openFile(path, flags) {
|
||||
return fs.open(this._getFilePath(path), flags)
|
||||
}
|
||||
|
||||
async _closeFile (fd) {
|
||||
async _closeFile(fd) {
|
||||
return fs.close(fd)
|
||||
}
|
||||
|
||||
async _rmdir(dir) {
|
||||
return fs.rmdir(dir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import LocalHandler from './local'
|
||||
const DEFAULT_NFS_OPTIONS = 'vers=3'
|
||||
|
||||
export default class NfsHandler extends LocalHandler {
|
||||
constructor (
|
||||
constructor(
|
||||
remote,
|
||||
{ mountsDir = join(tmpdir(), 'xo-fs-mounts'), ...opts } = {}
|
||||
) {
|
||||
@@ -17,15 +17,15 @@ export default class NfsHandler extends LocalHandler {
|
||||
this._realPath = join(mountsDir, remote.id)
|
||||
}
|
||||
|
||||
get type () {
|
||||
get type() {
|
||||
return 'nfs'
|
||||
}
|
||||
|
||||
_getRealPath () {
|
||||
_getRealPath() {
|
||||
return this._realPath
|
||||
}
|
||||
|
||||
async _mount () {
|
||||
async _mount() {
|
||||
await fs.ensureDir(this._getRealPath())
|
||||
const { host, path, port, options } = this._remote
|
||||
return execa(
|
||||
@@ -54,17 +54,13 @@ export default class NfsHandler extends LocalHandler {
|
||||
})
|
||||
}
|
||||
|
||||
async _sync () {
|
||||
if (this._remote.enabled) {
|
||||
await this._mount()
|
||||
} else {
|
||||
await this._umount()
|
||||
}
|
||||
async _sync() {
|
||||
await this._mount()
|
||||
|
||||
return this._remote
|
||||
}
|
||||
|
||||
async _forget () {
|
||||
async _forget() {
|
||||
try {
|
||||
await this._umount(this._remote)
|
||||
} catch (_) {
|
||||
@@ -72,7 +68,7 @@ export default class NfsHandler extends LocalHandler {
|
||||
}
|
||||
}
|
||||
|
||||
async _umount () {
|
||||
async _umount() {
|
||||
await execa('umount', ['--force', this._getRealPath()], {
|
||||
env: {
|
||||
LANG: 'C',
|
||||
|
||||
@@ -6,33 +6,38 @@ import RemoteHandlerAbstract from './abstract'
|
||||
const noop = () => {}
|
||||
|
||||
// Normalize the error code for file not found.
|
||||
const normalizeError = error => {
|
||||
class ErrorWrapper extends Error {
|
||||
constructor(error, newCode) {
|
||||
super(error.message)
|
||||
this.cause = error
|
||||
this.code = newCode
|
||||
}
|
||||
}
|
||||
const normalizeError = (error, shouldBeDirectory) => {
|
||||
const { code } = error
|
||||
|
||||
return code === 'STATUS_OBJECT_NAME_NOT_FOUND' ||
|
||||
code === 'STATUS_OBJECT_PATH_NOT_FOUND'
|
||||
? Object.create(error, {
|
||||
code: {
|
||||
configurable: true,
|
||||
readable: true,
|
||||
value: 'ENOENT',
|
||||
writable: true,
|
||||
},
|
||||
})
|
||||
? new ErrorWrapper(error, 'ENOENT')
|
||||
: code === 'STATUS_NOT_SUPPORTED' || code === 'STATUS_INVALID_PARAMETER'
|
||||
? new ErrorWrapper(error, shouldBeDirectory ? 'ENOTDIR' : 'EISDIR')
|
||||
: error
|
||||
}
|
||||
|
||||
export default class SmbHandler extends RemoteHandlerAbstract {
|
||||
constructor (remote, opts) {
|
||||
constructor(remote, opts) {
|
||||
super(remote, opts)
|
||||
this._forget = noop
|
||||
|
||||
const prefix = this._remote.path
|
||||
this._prefix = prefix !== '' ? prefix + '\\' : prefix
|
||||
}
|
||||
|
||||
get type () {
|
||||
get type() {
|
||||
return 'smb'
|
||||
}
|
||||
|
||||
_getClient () {
|
||||
_getClient() {
|
||||
const remote = this._remote
|
||||
|
||||
return new Smb2({
|
||||
@@ -44,40 +49,24 @@ export default class SmbHandler extends RemoteHandlerAbstract {
|
||||
})
|
||||
}
|
||||
|
||||
_getFilePath (file) {
|
||||
if (file === '.') {
|
||||
file = undefined
|
||||
}
|
||||
|
||||
let path = this._remote.path !== '' ? this._remote.path : ''
|
||||
|
||||
// Ensure remote path is a directory.
|
||||
if (path !== '' && path[path.length - 1] !== '\\') {
|
||||
path += '\\'
|
||||
}
|
||||
|
||||
if (file) {
|
||||
path += file.replace(/\//g, '\\')
|
||||
}
|
||||
|
||||
return path
|
||||
_getFilePath(file) {
|
||||
return this._prefix + file.slice(1).replace(/\//g, '\\')
|
||||
}
|
||||
|
||||
_dirname (file) {
|
||||
_dirname(file) {
|
||||
const parts = file.split('\\')
|
||||
parts.pop()
|
||||
return parts.join('\\')
|
||||
}
|
||||
|
||||
async _sync () {
|
||||
if (this._remote.enabled) {
|
||||
// Check access (smb2 does not expose connect in public so far...)
|
||||
await this.list()
|
||||
}
|
||||
async _sync() {
|
||||
// Check access (smb2 does not expose connect in public so far...)
|
||||
await this.list()
|
||||
|
||||
return this._remote
|
||||
}
|
||||
|
||||
async _outputFile (file, data, options = {}) {
|
||||
async _outputFile(file, data, options = {}) {
|
||||
const client = this._getClient()
|
||||
const path = this._getFilePath(file)
|
||||
const dir = this._dirname(path)
|
||||
@@ -91,7 +80,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
|
||||
})
|
||||
}
|
||||
|
||||
async _read (file, buffer, position) {
|
||||
async _read(file, buffer, position) {
|
||||
const needsClose = typeof file === 'string'
|
||||
|
||||
let client
|
||||
@@ -112,7 +101,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
async _readFile (file, options = {}) {
|
||||
async _readFile(file, options = {}) {
|
||||
const client = this._getClient()
|
||||
let content
|
||||
|
||||
@@ -129,7 +118,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
|
||||
return content
|
||||
}
|
||||
|
||||
async _rename (oldPath, newPath) {
|
||||
async _rename(oldPath, newPath) {
|
||||
const client = this._getClient()
|
||||
|
||||
try {
|
||||
@@ -145,7 +134,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
async _list (dir = '.') {
|
||||
async _list(dir = '.') {
|
||||
const client = this._getClient()
|
||||
let list
|
||||
|
||||
@@ -154,13 +143,13 @@ export default class SmbHandler extends RemoteHandlerAbstract {
|
||||
client.disconnect()
|
||||
})
|
||||
} catch (error) {
|
||||
throw normalizeError(error)
|
||||
throw normalizeError(error, true)
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
async _createReadStream (file, options = {}) {
|
||||
async _createReadStream(file, options = {}) {
|
||||
if (typeof file !== 'string') {
|
||||
file = file.path
|
||||
}
|
||||
@@ -178,7 +167,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
|
||||
return stream
|
||||
}
|
||||
|
||||
async _createOutputStream (file, options = {}) {
|
||||
async _createOutputStream(file, options = {}) {
|
||||
if (typeof file !== 'string') {
|
||||
file = file.path
|
||||
}
|
||||
@@ -199,7 +188,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
|
||||
return stream
|
||||
}
|
||||
|
||||
async _unlink (file) {
|
||||
async _unlink(file) {
|
||||
const client = this._getClient()
|
||||
|
||||
try {
|
||||
@@ -211,7 +200,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
async _getSize (file) {
|
||||
async _getSize(file) {
|
||||
const client = await this._getClient()
|
||||
let size
|
||||
|
||||
@@ -229,7 +218,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
|
||||
}
|
||||
|
||||
// TODO: add flags
|
||||
async _openFile (path) {
|
||||
async _openFile(path) {
|
||||
const client = this._getClient()
|
||||
return {
|
||||
client,
|
||||
@@ -237,7 +226,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
async _closeFile ({ client, file }) {
|
||||
async _closeFile({ client, file }) {
|
||||
await client.close(file)
|
||||
client.disconnect()
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ const consoleTransport = ({ data, level, namespace, message, time }) => {
|
||||
level < INFO
|
||||
? debugConsole
|
||||
: level < WARN
|
||||
? infoConsole
|
||||
: level < ERROR
|
||||
? warnConsole
|
||||
: errorConsole
|
||||
? infoConsole
|
||||
: level < ERROR
|
||||
? warnConsole
|
||||
: errorConsole
|
||||
|
||||
fn('%s - %s - [%s] %s', time.toISOString(), namespace, NAMES[level], message)
|
||||
data != null && fn(data)
|
||||
|
||||
@@ -53,14 +53,12 @@ export default ({
|
||||
fromCallback(cb =>
|
||||
transporter.sendMail(
|
||||
{
|
||||
subject: evalTemplate(
|
||||
subject,
|
||||
key =>
|
||||
key === 'level'
|
||||
? NAMES[log.level]
|
||||
: key === 'time'
|
||||
? log.time.toISOString()
|
||||
: log[key]
|
||||
subject: evalTemplate(subject, key =>
|
||||
key === 'level'
|
||||
? NAMES[log.level]
|
||||
: key === 'time'
|
||||
? log.time.toISOString()
|
||||
: log[key]
|
||||
),
|
||||
text: prettyFormat(log.data),
|
||||
},
|
||||
|
||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -4,19 +4,63 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Perf alert] Ability to trigger an alarm if a host/VM/SR usage value is below the threshold [#3612](https://github.com/vatesfr/xen-orchestra/issues/3612) (PR [#3675](https://github.com/vatesfr/xen-orchestra/pull/3675))
|
||||
- [Home/VMs] Display pool's name [#2226](https://github.com/vatesfr/xen-orchestra/issues/2226) (PR [#3709](https://github.com/vatesfr/xen-orchestra/pull/3709))
|
||||
- [Servers] Prevent new connection if pool is already connected [#2238](https://github.com/vatesfr/xen-orchestra/issues/2238) (PR [#3724](https://github.com/vatesfr/xen-orchestra/pull/3724))
|
||||
- [VM] Pause (like Suspend but doesn't copy RAM on disk) [#3727](https://github.com/vatesfr/xen-orchestra/issues/3727) (PR [#3731](https://github.com/vatesfr/xen-orchestra/pull/3731))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Servers] Fix deleting server on joining a pool [#2238](https://github.com/vatesfr/xen-orchestra/issues/2238) (PR [#3728](https://github.com/vatesfr/xen-orchestra/pull/3728))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server v5.30.0
|
||||
- xen-api v0.22.0
|
||||
- xo-server-perf-alert v0.2.0
|
||||
- xo-server-usage-report v0.7.1
|
||||
- xo-server v5.31.0
|
||||
- xo-web v5.31.0
|
||||
|
||||
## **5.28.2** (2018-11-16)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [VM] Ability to set nested virtualization in settings [#3619](https://github.com/vatesfr/xen-orchestra/issues/3619) (PR [#3625](https://github.com/vatesfr/xen-orchestra/pull/3625))
|
||||
- [Legacy Backup] Restore and File restore functionalities moved to the Backup NG view [#3499](https://github.com/vatesfr/xen-orchestra/issues/3499) (PR [#3610](https://github.com/vatesfr/xen-orchestra/pull/3610))
|
||||
- [Backup NG logs] Display warning in case of missing VMs instead of a ghosts VMs tasks (PR [#3647](https://github.com/vatesfr/xen-orchestra/pull/3647))
|
||||
- [VM] On migration, automatically selects the host and SR when only one is available [#3502](https://github.com/vatesfr/xen-orchestra/issues/3502) (PR [#3654](https://github.com/vatesfr/xen-orchestra/pull/3654))
|
||||
- [VM] Display VGA and video RAM for PVHVM guests [#3576](https://github.com/vatesfr/xen-orchestra/issues/3576) (PR [#3664](https://github.com/vatesfr/xen-orchestra/pull/3664))
|
||||
- [Backup NG form] Display a warning to let the user know that the Delta Backup and the Continuous Replication are not supported on XenServer < 6.5 [#3540](https://github.com/vatesfr/xen-orchestra/issues/3540) (PR [#3668](https://github.com/vatesfr/xen-orchestra/pull/3668))
|
||||
- [Backup NG form] Omit VMs(Simple Backup)/pools(Smart Backup/Resident on) with XenServer < 6.5 from the selection when the Delta Backup mode or the Continuous Replication mode are selected [#3540](https://github.com/vatesfr/xen-orchestra/issues/3540) (PR [#3668](https://github.com/vatesfr/xen-orchestra/pull/3668))
|
||||
- [VM] Allow to switch the Virtualization mode [#2372](https://github.com/vatesfr/xen-orchestra/issues/2372) (PR [#3669](https://github.com/vatesfr/xen-orchestra/pull/3669))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Backup ng logs] Fix restarting VMs with concurrency issue [#3603](https://github.com/vatesfr/xen-orchestra/issues/3603) (PR [#3634](https://github.com/vatesfr/xen-orchestra/pull/3634))
|
||||
- Validate modal containing a confirm text input by pressing the Enter key [#2735](https://github.com/vatesfr/xen-orchestra/issues/2735) (PR [#2890](https://github.com/vatesfr/xen-orchestra/pull/2890))
|
||||
- [Patches] Bulk install correctly ignores upgrade patches on licensed hosts (PR [#3651](https://github.com/vatesfr/xen-orchestra/pull/3651))
|
||||
- [Backup NG logs] Handle failed restores (PR [#3648](https://github.com/vatesfr/xen-orchestra/pull/3648))
|
||||
- [Self/New VM] Incorrect limit computation [#3658](https://github.com/vatesfr/xen-orchestra/issues/3658) (PR [#3666](https://github.com/vatesfr/xen-orchestra/pull/3666))
|
||||
- [Plugins] Don't expose credentials in config to users (PR [#3671](https://github.com/vatesfr/xen-orchestra/pull/3671))
|
||||
- [Self/New VM] `not enough … available in the set …` error in some cases (PR [#3667](https://github.com/vatesfr/xen-orchestra/pull/3667))
|
||||
- [XOSAN] Creation stuck at "Configuring VMs" [#3688](https://github.com/vatesfr/xen-orchestra/issues/3688) (PR [#3689](https://github.com/vatesfr/xen-orchestra/pull/3689))
|
||||
- [Backup NG] Errors listing backups on SMB remotes with extraneous files (PR [#3685](https://github.com/vatesfr/xen-orchestra/pull/3685))
|
||||
- [Remotes] Don't expose credentials to users [#3682](https://github.com/vatesfr/xen-orchestra/issues/3682) (PR [#3687](https://github.com/vatesfr/xen-orchestra/pull/3687))
|
||||
- [VM] Correctly display guest metrics updates (tools, network, etc.) [#3533](https://github.com/vatesfr/xen-orchestra/issues/3533) (PR [#3694](https://github.com/vatesfr/xen-orchestra/pull/3694))
|
||||
- [VM Templates] Fix deletion [#3498](https://github.com/vatesfr/xen-orchestra/issues/3498) (PR [#3695](https://github.com/vatesfr/xen-orchestra/pull/3695))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xen-api v0.21.0
|
||||
- xo-common v0.2.0
|
||||
- xo-acl-resolver v0.4.0
|
||||
- xo-server v5.30.1
|
||||
- xo-web v5.30.0
|
||||
|
||||
## **5.28.1** (2018-11-05)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [VM] Ability to set nested virtualization in settings [#3619](https://github.com/vatesfr/xen-orchestra/issues/3619) (PR [#3625](https://github.com/vatesfr/xen-orchestra/pull/3625))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Backup NG] Increase timeout in stale remotes detection to limit false positives (PR [#3632](https://github.com/vatesfr/xen-orchestra/pull/3632))
|
||||
@@ -27,8 +71,8 @@
|
||||
### Released packages
|
||||
|
||||
- @xen-orchestra/fs v0.4.1
|
||||
- xo-server v5.29.3
|
||||
- xo-web v5.29.2
|
||||
- xo-server v5.29.4
|
||||
- xo-web v5.29.3
|
||||
|
||||
## **5.28.0** (2018-10-31)
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ As you may have seen,in other parts of the documentation, XO is composed of two
|
||||
|
||||
### NodeJS
|
||||
|
||||
XO needs Node.js. **Please always use the LTS version of Node**.
|
||||
XO needs Node.js. **Please use Node 8**.
|
||||
|
||||
We'll consider at this point that you've got a working node on your box. E.g:
|
||||
|
||||
```
|
||||
$ node -v
|
||||
v8.9.1
|
||||
v8.12.0
|
||||
```
|
||||
|
||||
If not, see [this page](https://nodejs.org/en/download/package-manager/) for instructions on how to install Node.
|
||||
|
||||
@@ -103,6 +103,6 @@ encoding by prefixing with `json:`:
|
||||
##### VM import
|
||||
|
||||
```
|
||||
> xo-cli vm.import host=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
|
||||
> xo-cli vm.import sr=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
|
||||
```
|
||||
> Note: `xo-cli` only supports the import of XVA files. It will not import OVA files. To import OVA images, you must use the XOA web UI.
|
||||
|
||||
@@ -7,15 +7,16 @@
|
||||
"babel-jest": "^23.0.1",
|
||||
"benchmark": "^2.1.4",
|
||||
"eslint": "^5.1.0",
|
||||
"eslint-config-prettier": "^3.3.0",
|
||||
"eslint-config-standard": "12.0.0",
|
||||
"eslint-config-standard-jsx": "^6.0.2",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-node": "^7.0.1",
|
||||
"eslint-plugin-node": "^8.0.0",
|
||||
"eslint-plugin-promise": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.6.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"flow-bin": "^0.82.0",
|
||||
"flow-bin": "^0.86.0",
|
||||
"globby": "^8.0.0",
|
||||
"husky": "^1.0.0-rc.15",
|
||||
"jest": "^23.0.1",
|
||||
|
||||
@@ -410,11 +410,10 @@ class P {
|
||||
|
||||
static text (text) {
|
||||
const { length } = text
|
||||
return new P(
|
||||
(input, pos) =>
|
||||
input.startsWith(text, pos)
|
||||
? new Success(pos + length, text)
|
||||
: new Failure(pos, `'${text}'`)
|
||||
return new P((input, pos) =>
|
||||
input.startsWith(text, pos)
|
||||
? new Success(pos + length, text)
|
||||
: new Failure(pos, `'${text}'`)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -478,17 +477,16 @@ class P {
|
||||
}
|
||||
}
|
||||
|
||||
P.eof = new P(
|
||||
(input, pos, end) =>
|
||||
pos < end ? new Failure(pos, 'end of input') : new Success(pos)
|
||||
P.eof = new P((input, pos, end) =>
|
||||
pos < end ? new Failure(pos, 'end of input') : new Success(pos)
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const parser = P.grammar({
|
||||
default: r =>
|
||||
P.seq(r.ws, r.term.repeat(), P.eof).map(
|
||||
([, terms]) => (terms.length === 0 ? new Null() : new And(terms))
|
||||
P.seq(r.ws, r.term.repeat(), P.eof).map(([, terms]) =>
|
||||
terms.length === 0 ? new Null() : new And(terms)
|
||||
),
|
||||
globPattern: new P((input, pos, end) => {
|
||||
let value = ''
|
||||
|
||||
@@ -228,16 +228,15 @@ export default class Vhd {
|
||||
return this._read(
|
||||
sectorsToBytes(blockAddr),
|
||||
onlyBitmap ? this.bitmapSize : this.fullBlockSize
|
||||
).then(
|
||||
buf =>
|
||||
onlyBitmap
|
||||
? { id: blockId, bitmap: buf }
|
||||
: {
|
||||
id: blockId,
|
||||
bitmap: buf.slice(0, this.bitmapSize),
|
||||
data: buf.slice(this.bitmapSize),
|
||||
buffer: buf,
|
||||
}
|
||||
).then(buf =>
|
||||
onlyBitmap
|
||||
? { id: blockId, bitmap: buf }
|
||||
: {
|
||||
id: blockId,
|
||||
bitmap: buf.slice(0, this.bitmapSize),
|
||||
data: buf.slice(this.bitmapSize),
|
||||
buffer: buf,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
3
packages/xapi-explore-sr/.babelrc.js
Normal file
3
packages/xapi-explore-sr/.babelrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
24
packages/xapi-explore-sr/.npmignore
Normal file
24
packages/xapi-explore-sr/.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__/
|
||||
52
packages/xapi-explore-sr/README.md
Normal file
52
packages/xapi-explore-sr/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# xapi-explore-sr [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> Display the list of VDIs (unmanaged and snapshots included) of a SR
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/xapi-explore-sr):
|
||||
|
||||
```
|
||||
> npm install --global xapi-explore-sr
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
> xapi-explore-sr
|
||||
Usage: xapi-explore-sr [--full] <SR UUID> <XenServer URL> <XenServer user> [<XenServer password>]
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> npm install
|
||||
|
||||
# Run the tests
|
||||
> npm test
|
||||
|
||||
# Continuously compile
|
||||
> npm run dev
|
||||
|
||||
# Continuously run the tests
|
||||
> npm run dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> npm run build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
60
packages/xapi-explore-sr/package.json
Normal file
60
packages/xapi-explore-sr/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "xapi-explore-sr",
|
||||
"version": "0.2.1",
|
||||
"license": "ISC",
|
||||
"description": "Display the list of VDIs (unmanaged and snapshots included) of a SR",
|
||||
"keywords": [
|
||||
"api",
|
||||
"sr",
|
||||
"vdi",
|
||||
"vdis",
|
||||
"xen",
|
||||
"xen-api",
|
||||
"xenapi"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xapi-explore-sr",
|
||||
"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@isonoe.net"
|
||||
},
|
||||
"preferGlobal": true,
|
||||
"main": "dist/",
|
||||
"bin": {
|
||||
"xapi-explore-sr": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"archy": "^1.0.0",
|
||||
"chalk": "^2.3.2",
|
||||
"exec-promise": "^0.7.0",
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"pw": "^0.0.4",
|
||||
"xen-api": "^0.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.1.5",
|
||||
"@babel/core": "^7.1.5",
|
||||
"@babel/preset-env": "^7.1.5",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"cross-env": "^5.1.4",
|
||||
"rimraf": "^2.6.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
}
|
||||
}
|
||||
161
packages/xapi-explore-sr/src/index.js
Executable file
161
packages/xapi-explore-sr/src/index.js
Executable file
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import archy from 'archy'
|
||||
import chalk from 'chalk'
|
||||
import execPromise from 'exec-promise'
|
||||
import humanFormat from 'human-format'
|
||||
import pw from 'pw'
|
||||
import { createClient } from 'xen-api'
|
||||
import { forEach, map, orderBy } from 'lodash'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const askPassword = prompt =>
|
||||
new Promise(resolve => {
|
||||
prompt && process.stderr.write(`${prompt}: `)
|
||||
pw(resolve)
|
||||
})
|
||||
|
||||
const formatSize = bytes =>
|
||||
humanFormat(bytes, {
|
||||
prefix: 'Gi',
|
||||
scale: 'binary',
|
||||
})
|
||||
|
||||
const required = name => {
|
||||
const e = `missing required argument <${name}>`
|
||||
throw e
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const STYLES = [
|
||||
[
|
||||
vdi => !vdi.managed,
|
||||
chalk.enabled ? chalk.red : label => `[unmanaged] ${label}`,
|
||||
],
|
||||
[
|
||||
vdi => vdi.is_a_snapshot,
|
||||
chalk.enabled ? chalk.yellow : label => `[snapshot] ${label}`,
|
||||
],
|
||||
]
|
||||
const getStyle = vdi => {
|
||||
for (let i = 0, n = STYLES.length; i < n; ++i) {
|
||||
const entry = STYLES[i]
|
||||
if (entry[0](vdi)) {
|
||||
return entry[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapFilter = (collection, iteratee, results = []) => {
|
||||
forEach(collection, function () {
|
||||
const result = iteratee.apply(this, arguments)
|
||||
if (result !== undefined) {
|
||||
results.push(result)
|
||||
}
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
execPromise(async args => {
|
||||
if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
|
||||
return `Usage: xapi-explore-sr [--full] <SR UUID> <XenServer URL> <XenServer user> [<XenServer password>]`
|
||||
}
|
||||
|
||||
const full = args[0] === '--full'
|
||||
if (full) {
|
||||
args.shift()
|
||||
}
|
||||
|
||||
const [
|
||||
srUuid = required('SR UUID'),
|
||||
url = required('XenServer URL'),
|
||||
user = required('XenServer user'),
|
||||
password = await askPassword('XenServer password'),
|
||||
] = args
|
||||
|
||||
const xapi = createClient({
|
||||
allowUnauthorized: true,
|
||||
auth: { user, password },
|
||||
readOnly: true,
|
||||
url,
|
||||
watchEvents: false,
|
||||
})
|
||||
await xapi.connect()
|
||||
|
||||
const srRef = await xapi.call('SR.get_by_uuid', srUuid)
|
||||
const sr = await xapi.call('SR.get_record', srRef)
|
||||
|
||||
const vdisByRef = {}
|
||||
await Promise.all(
|
||||
map(sr.VDIs, async ref => {
|
||||
const vdi = await xapi.call('VDI.get_record', ref)
|
||||
vdisByRef[ref] = vdi
|
||||
})
|
||||
)
|
||||
|
||||
const hasParents = {}
|
||||
const vhdChildrenByUuid = {}
|
||||
forEach(vdisByRef, vdi => {
|
||||
const vhdParent = vdi.sm_config['vhd-parent']
|
||||
if (vhdParent) {
|
||||
;(
|
||||
vhdChildrenByUuid[vhdParent] || (vhdChildrenByUuid[vhdParent] = [])
|
||||
).push(vdi)
|
||||
} else if (!(vdi.snapshot_of in vdisByRef)) {
|
||||
return
|
||||
}
|
||||
|
||||
hasParents[vdi.uuid] = true
|
||||
})
|
||||
|
||||
const makeVdiNode = vdi => {
|
||||
const { uuid } = vdi
|
||||
|
||||
let label = `${vdi.name_label} - ${uuid} - ${formatSize(
|
||||
+vdi.physical_utilisation
|
||||
)}`
|
||||
const nodes = []
|
||||
|
||||
const vhdChildren = vhdChildrenByUuid[uuid]
|
||||
if (vhdChildren) {
|
||||
mapFilter(
|
||||
orderBy(vhdChildren, 'is_a_snapshot', 'desc'),
|
||||
makeVdiNode,
|
||||
nodes
|
||||
)
|
||||
}
|
||||
|
||||
mapFilter(
|
||||
vdi.snapshots,
|
||||
ref => {
|
||||
const vdi = vdisByRef[ref]
|
||||
if (full || !vdi.sm_config['vhd-parent']) {
|
||||
return makeVdiNode(vdi)
|
||||
}
|
||||
},
|
||||
nodes
|
||||
)
|
||||
|
||||
const style = getStyle(vdi)
|
||||
if (style) {
|
||||
label = style(label)
|
||||
}
|
||||
|
||||
return { label, nodes }
|
||||
}
|
||||
|
||||
const nodes = mapFilter(orderBy(vdisByRef, ['name_label', 'uuid']), vdi => {
|
||||
if (!hasParents[vdi.uuid]) {
|
||||
return makeVdiNode(vdi)
|
||||
}
|
||||
})
|
||||
|
||||
return archy({
|
||||
label: `${sr.name_label} (${sr.VDIs.length} VDIs)`,
|
||||
nodes,
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
Tested with:
|
||||
|
||||
- XenServer 7.6
|
||||
- XenServer 7.5
|
||||
- XenServer 7.4
|
||||
- XenServer 7.3
|
||||
- XenServer 7.2
|
||||
- XenServer 7.1
|
||||
@@ -44,6 +47,7 @@ Options:
|
||||
- `allowUnauthorized`: whether to accept self-signed certificates
|
||||
- `auth`: credentials used to sign in (can also be specified in the URL)
|
||||
- `readOnly = false`: if true, no methods with side-effects can be called
|
||||
- `callTimeout`: number of milliseconds after which a call is considered failed (can also be a map of timeouts by methods)
|
||||
|
||||
```js
|
||||
// Force connection.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xen-api",
|
||||
"version": "0.20.0",
|
||||
"version": "0.22.0",
|
||||
"license": "ISC",
|
||||
"description": "Connector to the Xen API",
|
||||
"keywords": [
|
||||
@@ -39,7 +39,7 @@
|
||||
"http-request-plus": "^0.6.0",
|
||||
"iterable-backoff": "^0.0.0",
|
||||
"jest-diff": "^23.5.0",
|
||||
"json-rpc-protocol": "^0.12.0",
|
||||
"json-rpc-protocol": "^0.13.1",
|
||||
"kindof": "^2.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"make-error": "^1.3.0",
|
||||
|
||||
17
packages/xen-api/src/_replaceSensitiveValues.js
Normal file
17
packages/xen-api/src/_replaceSensitiveValues.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import mapValues from 'lodash/mapValues'
|
||||
|
||||
export default function replaceSensitiveValues (value, replacement) {
|
||||
function helper (value, name) {
|
||||
if (name === 'password' && typeof value === 'string') {
|
||||
return replacement
|
||||
}
|
||||
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return value
|
||||
}
|
||||
|
||||
return Array.isArray(value) ? value.map(helper) : mapValues(value, helper)
|
||||
}
|
||||
|
||||
return helper(value)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
cancelable,
|
||||
defer,
|
||||
fromEvents,
|
||||
ignoreErrors,
|
||||
pCatch,
|
||||
pDelay,
|
||||
pFinally,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
} from 'promise-toolbox'
|
||||
|
||||
import autoTransport from './transports/auto'
|
||||
import replaceSensitiveValues from './_replaceSensitiveValues'
|
||||
|
||||
const debug = createDebug('xen-api')
|
||||
|
||||
@@ -86,14 +88,14 @@ const isSessionInvalid = ({ code }) => code === 'SESSION_INVALID'
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
class XapiError extends BaseError {
|
||||
constructor (code, params) {
|
||||
constructor(code, params) {
|
||||
super(`${code}(${params.join(', ')})`)
|
||||
|
||||
this.code = code
|
||||
this.params = params
|
||||
|
||||
// slots than can be assigned later
|
||||
this.method = undefined
|
||||
this.call = undefined
|
||||
this.url = undefined
|
||||
this.task = undefined
|
||||
}
|
||||
@@ -208,6 +210,24 @@ const getTaskResult = task => {
|
||||
}
|
||||
}
|
||||
|
||||
function defined() {
|
||||
for (let i = 0, n = arguments.length; i < n; ++i) {
|
||||
const arg = arguments[i]
|
||||
if (arg !== undefined) {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const makeCallSetting = (setting, defaultValue) =>
|
||||
setting === undefined
|
||||
? () => defaultValue
|
||||
: typeof setting === 'function'
|
||||
? setting
|
||||
: typeof setting !== 'object'
|
||||
? () => setting
|
||||
: method => defined(setting[method], setting['*'], defaultValue)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const RESERVED_FIELDS = {
|
||||
@@ -226,15 +246,19 @@ const DISCONNECTED = 'disconnected'
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class Xapi extends EventEmitter {
|
||||
constructor (opts) {
|
||||
constructor(opts) {
|
||||
super()
|
||||
|
||||
this._allowUnauthorized = opts.allowUnauthorized
|
||||
this._auth = opts.auth
|
||||
this._callTimeout = makeCallSetting(opts.callTimeout, 0)
|
||||
this._debounce = opts.debounce == null ? 200 : opts.debounce
|
||||
this._pool = null
|
||||
this._readOnly = Boolean(opts.readOnly)
|
||||
this._RecordsByType = createObject(null)
|
||||
this._sessionId = null
|
||||
;(this._objects = new Collection()).getKey = getKey
|
||||
;(this._objectsByRef = createObject(null))[NULL_REF] = undefined
|
||||
const url = (this._url = parseUrl(opts.url))
|
||||
|
||||
if (this._auth === undefined) {
|
||||
@@ -249,39 +273,39 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// Memoize this function _addObject().
|
||||
this._getPool = () => this._pool
|
||||
|
||||
if (opts.watchEvents !== false) {
|
||||
this._debounce = opts.debounce == null ? 200 : opts.debounce
|
||||
|
||||
this._eventWatchers = createObject(null)
|
||||
|
||||
this._fromToken = ''
|
||||
|
||||
// Memoize this function _addObject().
|
||||
this._getPool = () => this._pool
|
||||
|
||||
this._nTasks = 0
|
||||
|
||||
const objects = (this._objects = new Collection())
|
||||
objects.getKey = getKey
|
||||
|
||||
this._objectsByRef = createObject(null)
|
||||
this._objectsByRef[NULL_REF] = undefined
|
||||
|
||||
this._taskWatchers = Object.create(null)
|
||||
|
||||
this.on('connected', this._watchEvents)
|
||||
this.on('disconnected', () => {
|
||||
this._fromToken = ''
|
||||
objects.clear()
|
||||
})
|
||||
this.watchEvents()
|
||||
}
|
||||
}
|
||||
|
||||
get _url () {
|
||||
watchEvents() {
|
||||
this._eventWatchers = createObject(null)
|
||||
|
||||
this._fromToken = ''
|
||||
|
||||
this._nTasks = 0
|
||||
|
||||
this._taskWatchers = Object.create(null)
|
||||
|
||||
if (this.status === CONNECTED) {
|
||||
ignoreErrors.call(this._watchEvents())
|
||||
}
|
||||
|
||||
this.on('connected', this._watchEvents)
|
||||
this.on('disconnected', () => {
|
||||
this._fromToken = ''
|
||||
this._objects.clear()
|
||||
})
|
||||
}
|
||||
|
||||
get _url() {
|
||||
return this.__url
|
||||
}
|
||||
|
||||
set _url (url) {
|
||||
set _url(url) {
|
||||
this.__url = url
|
||||
this._call = autoTransport({
|
||||
allowUnauthorized: this._allowUnauthorized,
|
||||
@@ -289,15 +313,15 @@ export class Xapi extends EventEmitter {
|
||||
})
|
||||
}
|
||||
|
||||
get readOnly () {
|
||||
get readOnly() {
|
||||
return this._readOnly
|
||||
}
|
||||
|
||||
set readOnly (ro) {
|
||||
set readOnly(ro) {
|
||||
this._readOnly = Boolean(ro)
|
||||
}
|
||||
|
||||
get sessionId () {
|
||||
get sessionId() {
|
||||
const id = this._sessionId
|
||||
|
||||
if (!id || id === CONNECTING) {
|
||||
@@ -307,20 +331,20 @@ export class Xapi extends EventEmitter {
|
||||
return id
|
||||
}
|
||||
|
||||
get status () {
|
||||
get status() {
|
||||
const id = this._sessionId
|
||||
|
||||
return id ? (id === CONNECTING ? CONNECTING : CONNECTED) : DISCONNECTED
|
||||
}
|
||||
|
||||
get _humanId () {
|
||||
get _humanId() {
|
||||
return `${this._auth.user}@${this._url.hostname}`
|
||||
}
|
||||
|
||||
// ensure we have received all events up to this call
|
||||
//
|
||||
// optionally returns the up to date object for the given ref
|
||||
barrier (ref) {
|
||||
barrier(ref) {
|
||||
const eventWatchers = this._eventWatchers
|
||||
if (eventWatchers === undefined) {
|
||||
return Promise.reject(
|
||||
@@ -361,7 +385,7 @@ export class Xapi extends EventEmitter {
|
||||
)
|
||||
}
|
||||
|
||||
connect () {
|
||||
connect() {
|
||||
const { status } = this
|
||||
|
||||
if (status === CONNECTED) {
|
||||
@@ -398,7 +422,7 @@ export class Xapi extends EventEmitter {
|
||||
)
|
||||
}
|
||||
|
||||
disconnect () {
|
||||
disconnect() {
|
||||
return Promise.resolve().then(() => {
|
||||
const { status } = this
|
||||
|
||||
@@ -417,14 +441,14 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
|
||||
// High level calls.
|
||||
call (method, ...args) {
|
||||
call(method, ...args) {
|
||||
return this._readOnly && !isReadOnlyCall(method, args)
|
||||
? Promise.reject(new Error(`cannot call ${method}() in read only mode`))
|
||||
: this._sessionCall(method, prepareParam(args))
|
||||
}
|
||||
|
||||
@cancelable
|
||||
callAsync ($cancelToken, method, ...args) {
|
||||
callAsync($cancelToken, method, ...args) {
|
||||
return this._readOnly && !isReadOnlyCall(method, args)
|
||||
? Promise.reject(new Error(`cannot call ${method}() in read only mode`))
|
||||
: this._sessionCall(`Async.${method}`, args).then(taskRef => {
|
||||
@@ -443,7 +467,7 @@ export class Xapi extends EventEmitter {
|
||||
//
|
||||
// allowed even in read-only mode because it does not have impact on the
|
||||
// XenServer and it's necessary for getResource()
|
||||
createTask (nameLabel, nameDescription = '') {
|
||||
createTask(nameLabel, nameDescription = '') {
|
||||
const promise = this._sessionCall('task.create', [
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
@@ -461,7 +485,7 @@ export class Xapi extends EventEmitter {
|
||||
// Nice getter which returns the object for a given $id (internal to
|
||||
// this lib), UUID (unique identifier that some objects have) or
|
||||
// opaque reference (internal to XAPI).
|
||||
getObject (idOrUuidOrRef, defaultValue) {
|
||||
getObject(idOrUuidOrRef, defaultValue) {
|
||||
if (typeof idOrUuidOrRef === 'object') {
|
||||
idOrUuidOrRef = idOrUuidOrRef.$id
|
||||
}
|
||||
@@ -478,7 +502,7 @@ export class Xapi extends EventEmitter {
|
||||
|
||||
// Returns the object for a given opaque reference (internal to
|
||||
// XAPI).
|
||||
getObjectByRef (ref, defaultValue) {
|
||||
getObjectByRef(ref, defaultValue) {
|
||||
const object = this._objectsByRef[ref]
|
||||
|
||||
if (object !== undefined) return object
|
||||
@@ -490,7 +514,7 @@ export class Xapi extends EventEmitter {
|
||||
|
||||
// Returns the object for a given UUID (unique identifier that some
|
||||
// objects have).
|
||||
getObjectByUuid (uuid, defaultValue) {
|
||||
getObjectByUuid(uuid, defaultValue) {
|
||||
// Objects ids are already UUIDs if they have one.
|
||||
const object = this._objects.all[uuid]
|
||||
|
||||
@@ -501,13 +525,20 @@ export class Xapi extends EventEmitter {
|
||||
throw new Error('no object with UUID: ' + uuid)
|
||||
}
|
||||
|
||||
async getRecord (type, ref) {
|
||||
async getRecord(type, ref) {
|
||||
return this._wrapRecord(
|
||||
await this._sessionCall(`${type}.get_record`, [ref])
|
||||
)
|
||||
}
|
||||
|
||||
async getRecordByUuid (type, uuid) {
|
||||
async getAllRecords(type) {
|
||||
return map(
|
||||
await this._sessionCall(`${type}.get_all_records`),
|
||||
(record, ref) => this._wrapRecord(type, ref, record)
|
||||
)
|
||||
}
|
||||
|
||||
async getRecordByUuid(type, uuid) {
|
||||
return this.getRecord(
|
||||
type,
|
||||
await this._sessionCall(`${type}.get_by_uuid`, [uuid])
|
||||
@@ -515,7 +546,7 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
|
||||
@cancelable
|
||||
getResource ($cancelToken, pathname, { host, query, task }) {
|
||||
getResource($cancelToken, pathname, { host, query, task }) {
|
||||
return this._autoTask(task, `Xapi#getResource ${pathname}`).then(
|
||||
taskRef => {
|
||||
query = { ...query, session_id: this.sessionId }
|
||||
@@ -555,7 +586,7 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
|
||||
@cancelable
|
||||
putResource ($cancelToken, body, pathname, { host, query, task } = {}) {
|
||||
putResource($cancelToken, body, pathname, { host, query, task } = {}) {
|
||||
if (this._readOnly) {
|
||||
return Promise.reject(
|
||||
new Error(new Error('cannot put resource in read only mode'))
|
||||
@@ -661,11 +692,11 @@ export class Xapi extends EventEmitter {
|
||||
)
|
||||
}
|
||||
|
||||
setField ({ $type, $ref }, field, value) {
|
||||
setField({ $type, $ref }, field, value) {
|
||||
return this.call(`${$type}.set_${field}`, $ref, value).then(noop)
|
||||
}
|
||||
|
||||
setFieldEntries (record, field, entries) {
|
||||
setFieldEntries(record, field, entries) {
|
||||
return Promise.all(
|
||||
getKeys(entries).map(entry => {
|
||||
const value = entries[entry]
|
||||
@@ -678,7 +709,7 @@ export class Xapi extends EventEmitter {
|
||||
).then(noop)
|
||||
}
|
||||
|
||||
async setFieldEntry ({ $type, $ref }, field, entry, value) {
|
||||
async setFieldEntry({ $type, $ref }, field, entry, value) {
|
||||
while (true) {
|
||||
try {
|
||||
await this.call(`${$type}.add_to_${field}`, $ref, entry, value)
|
||||
@@ -692,11 +723,11 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
unsetFieldEntry ({ $type, $ref }, field, entry) {
|
||||
unsetFieldEntry({ $type, $ref }, field, entry) {
|
||||
return this.call(`${$type}.remove_from_${field}`, $ref, entry)
|
||||
}
|
||||
|
||||
watchTask (ref) {
|
||||
watchTask(ref) {
|
||||
const watchers = this._taskWatchers
|
||||
if (watchers === undefined) {
|
||||
throw new Error('Xapi#watchTask() requires events watching')
|
||||
@@ -721,16 +752,16 @@ export class Xapi extends EventEmitter {
|
||||
return watcher.promise
|
||||
}
|
||||
|
||||
get pool () {
|
||||
get pool() {
|
||||
return this._pool
|
||||
}
|
||||
|
||||
get objects () {
|
||||
get objects() {
|
||||
return this._objects
|
||||
}
|
||||
|
||||
// return a promise which resolves to a task ref or undefined
|
||||
_autoTask (task = this._taskWatchers !== undefined, name) {
|
||||
_autoTask(task = this._taskWatchers !== undefined, name) {
|
||||
if (task === false) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
@@ -744,7 +775,7 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
|
||||
// Medium level call: handle session errors.
|
||||
_sessionCall (method, args) {
|
||||
_sessionCall(method, args) {
|
||||
try {
|
||||
if (startsWith(method, 'session.')) {
|
||||
throw new Error('session.*() methods are disabled from this interface')
|
||||
@@ -755,24 +786,27 @@ export class Xapi extends EventEmitter {
|
||||
newArgs.push.apply(newArgs, args)
|
||||
}
|
||||
|
||||
return pCatch.call(
|
||||
this._transportCall(method, newArgs),
|
||||
isSessionInvalid,
|
||||
() => {
|
||||
// XAPI is sometimes reinitialized and sessions are lost.
|
||||
// Try to login again.
|
||||
debug('%s: the session has been reinitialized', this._humanId)
|
||||
return pTimeout.call(
|
||||
pCatch.call(
|
||||
this._transportCall(method, newArgs),
|
||||
isSessionInvalid,
|
||||
() => {
|
||||
// XAPI is sometimes reinitialized and sessions are lost.
|
||||
// Try to login again.
|
||||
debug('%s: the session has been reinitialized', this._humanId)
|
||||
|
||||
this._sessionId = null
|
||||
return this.connect().then(() => this._sessionCall(method, args))
|
||||
}
|
||||
this._sessionId = null
|
||||
return this.connect().then(() => this._sessionCall(method, args))
|
||||
}
|
||||
),
|
||||
this._callTimeout(method, args)
|
||||
)
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
_addObject (type, ref, object) {
|
||||
_addObject(type, ref, object) {
|
||||
object = this._wrapRecord(type, ref, object)
|
||||
|
||||
// Finally freezes the object.
|
||||
@@ -819,7 +853,7 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_removeObject (type, ref) {
|
||||
_removeObject(type, ref) {
|
||||
const byRefs = this._objectsByRef
|
||||
const object = byRefs[ref]
|
||||
if (object !== undefined) {
|
||||
@@ -842,7 +876,7 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_processEvents (events) {
|
||||
_processEvents(events) {
|
||||
forEach(events, event => {
|
||||
const { class: type, ref } = event
|
||||
if (event.operation === 'del') {
|
||||
@@ -853,7 +887,7 @@ export class Xapi extends EventEmitter {
|
||||
})
|
||||
}
|
||||
|
||||
_watchEvents () {
|
||||
_watchEvents() {
|
||||
const loop = () =>
|
||||
this.status === CONNECTED &&
|
||||
pTimeout
|
||||
@@ -926,7 +960,7 @@ export class Xapi extends EventEmitter {
|
||||
// methods.
|
||||
//
|
||||
// It also has to manually get all objects first.
|
||||
_watchEventsLegacy () {
|
||||
_watchEventsLegacy() {
|
||||
const getAllObjects = () => {
|
||||
return this._sessionCall('system.listMethods').then(methods => {
|
||||
// Uses introspection to determine the methods to use to get
|
||||
@@ -978,7 +1012,7 @@ export class Xapi extends EventEmitter {
|
||||
return getAllObjects().then(watchEvents)
|
||||
}
|
||||
|
||||
_wrapRecord (type, ref, data) {
|
||||
_wrapRecord(type, ref, data) {
|
||||
const RecordsByType = this._RecordsByType
|
||||
let Record = RecordsByType[type]
|
||||
if (Record === undefined) {
|
||||
@@ -989,7 +1023,7 @@ export class Xapi extends EventEmitter {
|
||||
const objectsByRef = this._objectsByRef
|
||||
const getObjectByRef = ref => objectsByRef[ref]
|
||||
|
||||
Record = function (ref, data) {
|
||||
Record = function(ref, data) {
|
||||
defineProperties(this, {
|
||||
$id: { value: data.uuid || ref },
|
||||
$ref: { value: ref },
|
||||
@@ -1003,7 +1037,7 @@ export class Xapi extends EventEmitter {
|
||||
const getters = { $pool: this._getPool }
|
||||
const props = { $type: type }
|
||||
fields.forEach(field => {
|
||||
props[`set_${field}`] = function (value) {
|
||||
props[`set_${field}`] = function(value) {
|
||||
return xapi.setField(this, field, value)
|
||||
}
|
||||
|
||||
@@ -1012,19 +1046,19 @@ export class Xapi extends EventEmitter {
|
||||
const value = data[field]
|
||||
if (isArray(value)) {
|
||||
if (value.length === 0 || isOpaqueRef(value[0])) {
|
||||
getters[$field] = function () {
|
||||
getters[$field] = function() {
|
||||
const value = this[field]
|
||||
return value.length === 0 ? value : value.map(getObjectByRef)
|
||||
}
|
||||
}
|
||||
|
||||
props[`add_to_${field}`] = function (...values) {
|
||||
props[`add_to_${field}`] = function(...values) {
|
||||
return xapi
|
||||
.call(`${type}.add_${field}`, this.$ref, values)
|
||||
.then(noop)
|
||||
}
|
||||
} else if (value !== null && typeof value === 'object') {
|
||||
getters[$field] = function () {
|
||||
getters[$field] = function() {
|
||||
const value = this[field]
|
||||
const result = {}
|
||||
getKeys(value).forEach(key => {
|
||||
@@ -1032,11 +1066,11 @@ export class Xapi extends EventEmitter {
|
||||
})
|
||||
return result
|
||||
}
|
||||
props[`update_${field}`] = function (entries) {
|
||||
props[`update_${field}`] = function(entries) {
|
||||
return xapi.setFieldEntries(this, field, entries)
|
||||
}
|
||||
} else if (isOpaqueRef(value)) {
|
||||
getters[$field] = function () {
|
||||
getters[$field] = function() {
|
||||
return objectsByRef[this[field]]
|
||||
}
|
||||
}
|
||||
@@ -1065,18 +1099,21 @@ export class Xapi extends EventEmitter {
|
||||
|
||||
Xapi.prototype._transportCall = reduce(
|
||||
[
|
||||
function (method, args) {
|
||||
function(method, args) {
|
||||
return this._call(method, args).catch(error => {
|
||||
if (!(error instanceof Error)) {
|
||||
error = wrapError(error)
|
||||
}
|
||||
|
||||
error.method = method
|
||||
error.call = {
|
||||
method,
|
||||
params: replaceSensitiveValues(args, '* obfuscated *'),
|
||||
}
|
||||
throw error
|
||||
})
|
||||
},
|
||||
call =>
|
||||
function () {
|
||||
function() {
|
||||
let iterator // lazily created
|
||||
const loop = () =>
|
||||
pCatch.call(
|
||||
@@ -1117,7 +1154,7 @@ Xapi.prototype._transportCall = reduce(
|
||||
return loop()
|
||||
},
|
||||
call =>
|
||||
function loop () {
|
||||
function loop() {
|
||||
return pCatch.call(
|
||||
call.apply(this, arguments),
|
||||
isHostSlave,
|
||||
@@ -1140,7 +1177,7 @@ Xapi.prototype._transportCall = reduce(
|
||||
)
|
||||
},
|
||||
call =>
|
||||
function (method) {
|
||||
function(method) {
|
||||
const startTime = Date.now()
|
||||
return call.apply(this, arguments).then(
|
||||
result => {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
'use strict'
|
||||
|
||||
const { unauthorized } = require('xo-common/api-errors')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// These global variables are not a problem because the algorithm is
|
||||
// synchronous.
|
||||
let permissionsByObject
|
||||
@@ -105,23 +109,26 @@ function checkAuthorization (objectId, permission) {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
module.exports = (
|
||||
function assertPermissions (
|
||||
permissionsByObject_,
|
||||
getObject_,
|
||||
permissions,
|
||||
permission
|
||||
) => {
|
||||
) {
|
||||
// Assign global variables.
|
||||
permissionsByObject = permissionsByObject_
|
||||
getObject = getObject_
|
||||
|
||||
try {
|
||||
if (permission) {
|
||||
return checkAuthorization(permissions, permission)
|
||||
if (permission !== undefined) {
|
||||
const objectId = permissions
|
||||
if (!checkAuthorization(objectId, permission)) {
|
||||
throw unauthorized(permission, objectId)
|
||||
}
|
||||
} else {
|
||||
for (const [objectId, permission] of permissions) {
|
||||
if (!checkAuthorization(objectId, permission)) {
|
||||
return false
|
||||
throw unauthorized(permission, objectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,3 +139,16 @@ module.exports = (
|
||||
permissionsByObject = getObject = null
|
||||
}
|
||||
}
|
||||
exports.assert = assertPermissions
|
||||
|
||||
exports.check = function checkPermissions () {
|
||||
try {
|
||||
assertPermissions.apply(undefined, arguments)
|
||||
return true
|
||||
} catch (error) {
|
||||
if (unauthorized.is(error)) {
|
||||
return false
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-acl-resolver",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"license": "ISC",
|
||||
"description": "Xen-Orchestra internal: do ACLs resolution",
|
||||
"keywords": [],
|
||||
@@ -21,5 +21,8 @@
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"xo-common": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ encoding by prefixing with `json:`:
|
||||
##### VM import
|
||||
|
||||
```
|
||||
> xo-cli vm.import host=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
|
||||
> xo-cli vm.import sr=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-common",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Code shared between [XO](https://xen-orchestra.com) server and clients",
|
||||
"keywords": [],
|
||||
|
||||
@@ -37,8 +37,15 @@ export const noSuchObject = create(1, (id, type) => ({
|
||||
message: `no such ${type || 'object'} ${id}`,
|
||||
}))
|
||||
|
||||
export const unauthorized = create(2, () => ({
|
||||
message: 'not authenticated or not enough permissions',
|
||||
export const unauthorized = create(2, (permission, objectId, objectType) => ({
|
||||
data: {
|
||||
permission,
|
||||
object: {
|
||||
id: objectId,
|
||||
type: objectType,
|
||||
},
|
||||
},
|
||||
message: 'not enough permissions',
|
||||
}))
|
||||
|
||||
export const invalidCredentials = create(3, () => ({
|
||||
|
||||
10
packages/xo-import-servers-csv/.npmignore
Normal file
10
packages/xo-import-servers-csv/.npmignore
Normal file
@@ -0,0 +1,10 @@
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
64
packages/xo-import-servers-csv/README.md
Normal file
64
packages/xo-import-servers-csv/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# xo-import-servers-csv [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> CLI to import servers in XO from a CSV file
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/xo-import-servers-csv):
|
||||
|
||||
```
|
||||
> npm install --global xo-import-servers-csv
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
`servers.csv`:
|
||||
|
||||
```csv
|
||||
host,username,password
|
||||
xs1.company.net,user1,password1
|
||||
xs2.company.net:8080,user2,password2
|
||||
http://xs3.company.net,user3,password3
|
||||
```
|
||||
|
||||
> The CSV file can also contains these optional fields: `label`, `autoConnect`, `allowUnauthorized`.
|
||||
|
||||
Shell command:
|
||||
|
||||
```
|
||||
> xo-import-servers-csv 'https://xo.company.tld' admin@admin.net admin < servers.csv
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> npm install
|
||||
|
||||
# Run the tests
|
||||
> npm test
|
||||
|
||||
# Continuously compile
|
||||
> npm run dev
|
||||
|
||||
# Continuously run the tests
|
||||
> npm run dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> npm run build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](http://vates.fr)
|
||||
59
packages/xo-import-servers-csv/package.json
Normal file
59
packages/xo-import-servers-csv/package.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "xo-import-servers-csv",
|
||||
"version": "1.1.0",
|
||||
"license": "ISC",
|
||||
"description": "CLI to import servers in XO from a CSV file",
|
||||
"keywords": [
|
||||
"csv",
|
||||
"host",
|
||||
"import",
|
||||
"orchestra",
|
||||
"pool",
|
||||
"server",
|
||||
"xen",
|
||||
"xen-orchestra"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-import-servers-csv",
|
||||
"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"
|
||||
},
|
||||
"main": "dist/",
|
||||
"bin": {
|
||||
"xo-import-servers-csv": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"csv-parser": "^2.1.0",
|
||||
"end-of-stream": "^1.1.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"highland": "^2.10.1",
|
||||
"through2": "^3.0.0",
|
||||
"xo-lib": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^10.12.2",
|
||||
"@types/through2": "^2.0.31",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-config-standard": "^8.0.1",
|
||||
"typescript": "^3.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc -w",
|
||||
"lint": "tslint 'src/*.ts'",
|
||||
"posttest": "yarn run lint",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"start": "node dist/index.js"
|
||||
}
|
||||
}
|
||||
23
packages/xo-import-servers-csv/src/index.d.ts
vendored
Normal file
23
packages/xo-import-servers-csv/src/index.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
declare module 'csv-parser' {
|
||||
function csvParser(opts?: Object): any
|
||||
export = csvParser
|
||||
}
|
||||
|
||||
declare module 'exec-promise' {
|
||||
function execPromise(cb: (args: string[]) => any): void
|
||||
export = execPromise
|
||||
}
|
||||
|
||||
declare module 'xo-lib' {
|
||||
export default class Xo {
|
||||
user?: { email: string }
|
||||
|
||||
constructor(opts?: { credentials?: {}; url: string })
|
||||
|
||||
call(method: string, ...params: any[]): Promise<any>
|
||||
|
||||
open(): Promise<void>
|
||||
|
||||
signIn(credentials: {}): Promise<void>
|
||||
}
|
||||
}
|
||||
87
packages/xo-import-servers-csv/src/index.ts
Executable file
87
packages/xo-import-servers-csv/src/index.ts
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/// <reference path="./index.d.ts" />
|
||||
|
||||
import csvParser = require('csv-parser')
|
||||
import execPromise = require('exec-promise')
|
||||
import through2 = require('through2')
|
||||
import Xo from 'xo-lib'
|
||||
|
||||
const parseBoolean = (
|
||||
value: string,
|
||||
defaultValue?: boolean
|
||||
): boolean | undefined => {
|
||||
if (value === undefined || value === '') {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
const lcValue = value.toLocaleLowerCase()
|
||||
|
||||
if (value === '0' || lcValue === 'false') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (value === '1' || lcValue === 'true') {
|
||||
return true
|
||||
}
|
||||
|
||||
throw new Error(`invalid boolean value: ${value}`)
|
||||
}
|
||||
|
||||
const requiredParam = (name: string) => {
|
||||
throw `missing param: ${name}
|
||||
|
||||
Usage: xo-import-servers-csv $url $username $password < $csvFile`
|
||||
}
|
||||
|
||||
execPromise(
|
||||
async ([
|
||||
url = requiredParam('url'),
|
||||
username = requiredParam('username'),
|
||||
password = requiredParam('password'),
|
||||
]): Promise<void> => {
|
||||
const xo = new Xo({ url })
|
||||
|
||||
await xo.open()
|
||||
await xo.signIn({ username, password })
|
||||
console.log('connected as', xo.user!.email)
|
||||
|
||||
const errors: any[] = []
|
||||
|
||||
const stream = process.stdin.pipe(csvParser()).pipe(
|
||||
through2.obj(
|
||||
(
|
||||
{ allowUnauthorized, autoConnect, host, label, password, username },
|
||||
_,
|
||||
next
|
||||
) => {
|
||||
console.log('server', host)
|
||||
|
||||
xo.call('server.add', {
|
||||
allowUnauthorized: parseBoolean(allowUnauthorized),
|
||||
autoConnect: parseBoolean(autoConnect, false),
|
||||
host,
|
||||
label,
|
||||
password,
|
||||
username,
|
||||
}).then(
|
||||
() => next(),
|
||||
(error: any) => {
|
||||
errors.push({ host, error })
|
||||
return next()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
stream.on('error', reject)
|
||||
stream.on('finish', resolve)
|
||||
})
|
||||
|
||||
if (errors.length) {
|
||||
console.error(errors)
|
||||
}
|
||||
}
|
||||
)
|
||||
14
packages/xo-import-servers-csv/tsconfig.json
Normal file
14
packages/xo-import-servers-csv/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"newLine": "lf",
|
||||
"noImplicitAny": true,
|
||||
"outDir": "dist/",
|
||||
"removeComments": true,
|
||||
"sourceMap": true,
|
||||
"strictNullChecks": true,
|
||||
"target": "es2015"
|
||||
},
|
||||
"includes": "src/**/*"
|
||||
}
|
||||
3
packages/xo-import-servers-csv/tslint.json
Normal file
3
packages/xo-import-servers-csv/tslint.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "tslint-config-standard"
|
||||
}
|
||||
@@ -667,8 +667,8 @@ class BackupReportsXoPlugin {
|
||||
const globalStatus = globalSuccess
|
||||
? `Success`
|
||||
: nFailures !== 0
|
||||
? `Failure`
|
||||
: `Skipped`
|
||||
? `Failure`
|
||||
: `Skipped`
|
||||
|
||||
let markdown = [
|
||||
`## Global status: ${globalStatus}`,
|
||||
@@ -727,8 +727,8 @@ class BackupReportsXoPlugin {
|
||||
globalSuccess
|
||||
? ICON_SUCCESS
|
||||
: nFailures !== 0
|
||||
? ICON_FAILURE
|
||||
: ICON_SKIPPED
|
||||
? ICON_FAILURE
|
||||
: ICON_SKIPPED
|
||||
}`,
|
||||
nagiosStatus: globalSuccess ? 0 : 2,
|
||||
nagiosMarkdown: globalSuccess
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import Client, { createBackoff } from 'jsonrpc-websocket-client'
|
||||
import hrp from 'http-request-plus'
|
||||
|
||||
const UPDATER_URL = 'localhost'
|
||||
const WS_PORT = 9001
|
||||
const HTTP_PORT = 9002
|
||||
const WS_URL = 'ws://localhost:9001'
|
||||
const HTTP_URL = 'http://localhost:9002'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -47,7 +46,7 @@ class XoServerCloud {
|
||||
this
|
||||
)
|
||||
|
||||
const updater = (this._updater = new Client(`${UPDATER_URL}:${WS_PORT}`))
|
||||
const updater = (this._updater = new Client(WS_URL))
|
||||
const connect = () =>
|
||||
updater.open(createBackoff()).catch(error => {
|
||||
console.error('xo-server-cloud: fail to connect to updater', error)
|
||||
@@ -143,7 +142,7 @@ class XoServerCloud {
|
||||
throw new Error('cannot get download token')
|
||||
}
|
||||
|
||||
const response = await hrp(`${UPDATER_URL}:${HTTP_PORT}/`, {
|
||||
const response = await hrp(HTTP_URL, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${downloadToken}`,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-perf-alert",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
|
||||
@@ -3,14 +3,18 @@ import { createSchedule } from '@xen-orchestra/cron'
|
||||
import { assign, forOwn, map, mean } from 'lodash'
|
||||
import { utcParse } from 'd3-time-format'
|
||||
|
||||
const COMPARATOR_FN = {
|
||||
'>': (a, b) => a > b,
|
||||
'<': (a, b) => a < b,
|
||||
}
|
||||
|
||||
const VM_FUNCTIONS = {
|
||||
cpuUsage: {
|
||||
name: 'VM CPU usage',
|
||||
description:
|
||||
'Raises an alarm when the average usage of any CPU is higher than the threshold',
|
||||
'Raises an alarm when the average usage of any CPU is higher/lower than the threshold',
|
||||
unit: '%',
|
||||
comparator: '>',
|
||||
createParser: (legend, threshold) => {
|
||||
createParser: (comparator, legend, threshold) => {
|
||||
const regex = /cpu[0-9]+/
|
||||
const filteredLegends = legend.filter(l => l.name.match(regex))
|
||||
const accumulator = Object.assign(
|
||||
@@ -27,17 +31,17 @@ const VM_FUNCTIONS = {
|
||||
})
|
||||
},
|
||||
getDisplayableValue,
|
||||
shouldAlarm: () => getDisplayableValue() > threshold,
|
||||
shouldAlarm: () =>
|
||||
COMPARATOR_FN[comparator](getDisplayableValue(), threshold),
|
||||
}
|
||||
},
|
||||
},
|
||||
memoryUsage: {
|
||||
name: 'VM memory usage',
|
||||
description:
|
||||
'Raises an alarm when the used memory % is higher than the threshold',
|
||||
'Raises an alarm when the used memory % is higher/lower than the threshold',
|
||||
unit: '% used',
|
||||
comparator: '>',
|
||||
createParser: (legend, threshold) => {
|
||||
createParser: (comparator, legend, threshold) => {
|
||||
const memoryBytesLegend = legend.find(l => l.name === 'memory')
|
||||
const memoryKBytesFreeLegend = legend.find(
|
||||
l => l.name === 'memory_internal_free'
|
||||
@@ -52,9 +56,8 @@ const VM_FUNCTIONS = {
|
||||
)
|
||||
},
|
||||
getDisplayableValue,
|
||||
shouldAlarm: () => {
|
||||
return getDisplayableValue() > threshold
|
||||
},
|
||||
shouldAlarm: () =>
|
||||
COMPARATOR_FN[comparator](getDisplayableValue(), threshold),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -64,10 +67,9 @@ const HOST_FUNCTIONS = {
|
||||
cpuUsage: {
|
||||
name: 'host CPU usage',
|
||||
description:
|
||||
'Raises an alarm when the average usage of any CPU is higher than the threshold',
|
||||
'Raises an alarm when the average usage of any CPU is higher/lower than the threshold',
|
||||
unit: '%',
|
||||
comparator: '>',
|
||||
createParser: (legend, threshold) => {
|
||||
createParser: (comparator, legend, threshold) => {
|
||||
const regex = /^cpu[0-9]+$/
|
||||
const filteredLegends = legend.filter(l => l.name.match(regex))
|
||||
const accumulator = Object.assign(
|
||||
@@ -84,17 +86,17 @@ const HOST_FUNCTIONS = {
|
||||
})
|
||||
},
|
||||
getDisplayableValue,
|
||||
shouldAlarm: () => getDisplayableValue() > threshold,
|
||||
shouldAlarm: () =>
|
||||
COMPARATOR_FN[comparator](getDisplayableValue(), threshold),
|
||||
}
|
||||
},
|
||||
},
|
||||
memoryUsage: {
|
||||
name: 'host memory usage',
|
||||
description:
|
||||
'Raises an alarm when the used memory % is higher than the threshold',
|
||||
'Raises an alarm when the used memory % is higher/lower than the threshold',
|
||||
unit: '% used',
|
||||
comparator: '>',
|
||||
createParser: (legend, threshold) => {
|
||||
createParser: (comparator, legend, threshold) => {
|
||||
const memoryKBytesLegend = legend.find(l => l.name === 'memory_total_kib')
|
||||
const memoryKBytesFreeLegend = legend.find(
|
||||
l => l.name === 'memory_free_kib'
|
||||
@@ -109,7 +111,8 @@ const HOST_FUNCTIONS = {
|
||||
)
|
||||
},
|
||||
getDisplayableValue,
|
||||
shouldAlarm: () => getDisplayableValue() > threshold,
|
||||
shouldAlarm: () =>
|
||||
COMPARATOR_FN[comparator](getDisplayableValue(), threshold),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -119,15 +122,15 @@ const SR_FUNCTIONS = {
|
||||
storageUsage: {
|
||||
name: 'SR storage usage',
|
||||
description:
|
||||
'Raises an alarm when the used disk space % is higher than the threshold',
|
||||
'Raises an alarm when the used disk space % is higher/lower than the threshold',
|
||||
unit: '% used',
|
||||
comparator: '>',
|
||||
createGetter: threshold => sr => {
|
||||
createGetter: (comparator, threshold) => sr => {
|
||||
const getDisplayableValue = () =>
|
||||
(sr.physical_utilisation * 100) / sr.physical_size
|
||||
return {
|
||||
getDisplayableValue,
|
||||
shouldAlarm: () => getDisplayableValue() > threshold,
|
||||
shouldAlarm: () =>
|
||||
COMPARATOR_FN[comparator](getDisplayableValue(), threshold),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -139,6 +142,13 @@ const TYPE_FUNCTION_MAP = {
|
||||
sr: SR_FUNCTIONS,
|
||||
}
|
||||
|
||||
const COMPARATOR_ENTRY = {
|
||||
title: 'Comparator',
|
||||
type: 'string',
|
||||
default: Object.keys(COMPARATOR_FN)[0],
|
||||
enum: Object.keys(COMPARATOR_FN),
|
||||
}
|
||||
|
||||
// list of currently ringing alarms, to avoid double notification
|
||||
const currentAlarms = {}
|
||||
|
||||
@@ -182,10 +192,11 @@ export const configurationSchema = {
|
||||
default: Object.keys(HOST_FUNCTIONS)[0],
|
||||
enum: Object.keys(HOST_FUNCTIONS),
|
||||
},
|
||||
comparator: COMPARATOR_ENTRY,
|
||||
alarmTriggerLevel: {
|
||||
title: 'Threshold',
|
||||
description:
|
||||
'The direction of the crossing is given by the Alarm type',
|
||||
'The direction of the crossing is given by the comparator type',
|
||||
type: 'number',
|
||||
default: 40,
|
||||
},
|
||||
@@ -222,7 +233,7 @@ export const configurationSchema = {
|
||||
description: Object.keys(VM_FUNCTIONS)
|
||||
.map(
|
||||
k =>
|
||||
` * ${k} (${VM_FUNCTIONS[k].unit}):${
|
||||
` * ${k} (${VM_FUNCTIONS[k].unit}): ${
|
||||
VM_FUNCTIONS[k].description
|
||||
}`
|
||||
)
|
||||
@@ -231,10 +242,11 @@ export const configurationSchema = {
|
||||
default: Object.keys(VM_FUNCTIONS)[0],
|
||||
enum: Object.keys(VM_FUNCTIONS),
|
||||
},
|
||||
comparator: COMPARATOR_ENTRY,
|
||||
alarmTriggerLevel: {
|
||||
title: 'Threshold',
|
||||
description:
|
||||
'The direction of the crossing is given by the Alarm type',
|
||||
'The direction of the crossing is given by the comparator type',
|
||||
type: 'number',
|
||||
default: 40,
|
||||
},
|
||||
@@ -281,10 +293,11 @@ export const configurationSchema = {
|
||||
default: Object.keys(SR_FUNCTIONS)[0],
|
||||
enum: Object.keys(SR_FUNCTIONS),
|
||||
},
|
||||
comparator: COMPARATOR_ENTRY,
|
||||
alarmTriggerLevel: {
|
||||
title: 'Threshold',
|
||||
description:
|
||||
'The direction of the crossing is given by the Alarm type',
|
||||
'The direction of the crossing is given by the comparator type',
|
||||
type: 'number',
|
||||
default: 80,
|
||||
},
|
||||
@@ -440,6 +453,7 @@ ${monitorBodies.join('\n')}`
|
||||
relatedNode[l.name] = l
|
||||
})
|
||||
const parser = typeFunction.createParser(
|
||||
definition.comparator,
|
||||
parsedLegend.filter(l => l.uuid === uuid),
|
||||
definition.alarmTriggerLevel
|
||||
)
|
||||
@@ -454,7 +468,7 @@ ${monitorBodies.join('\n')}`
|
||||
...definition,
|
||||
alarmId,
|
||||
vmFunction: typeFunction,
|
||||
title: `${typeFunction.name} ${typeFunction.comparator} ${
|
||||
title: `${typeFunction.name} ${definition.comparator} ${
|
||||
definition.alarmTriggerLevel
|
||||
}${typeFunction.unit}`,
|
||||
snapshot: async () => {
|
||||
@@ -463,7 +477,6 @@ ${monitorBodies.join('\n')}`
|
||||
try {
|
||||
const result = {
|
||||
uuid,
|
||||
name: definition.name,
|
||||
object: this._xo.getXapi(uuid).getObject(uuid),
|
||||
}
|
||||
|
||||
@@ -489,6 +502,7 @@ ${monitorBodies.join('\n')}`
|
||||
} else {
|
||||
// Stats via XAPI
|
||||
const getter = typeFunction.createGetter(
|
||||
definition.comparator,
|
||||
definition.alarmTriggerLevel
|
||||
)
|
||||
const data = getter(result.object)
|
||||
@@ -535,6 +549,40 @@ ${monitorBodies.join('\n')}`
|
||||
)
|
||||
}
|
||||
|
||||
// Sample of a monitor
|
||||
// {
|
||||
// uuids: ['8485ea1f-b475-f6f2-58a7-895ab626ce5d'],
|
||||
// variableName: 'cpuUsage',
|
||||
// comparator: '>',
|
||||
// alarmTriggerLevel: 50,
|
||||
// alarmTriggerPeriod: 60,
|
||||
// objectType: 'host',
|
||||
// alarmId: 'host|cpuUsage|50',
|
||||
// title: 'host CPU usage > 50',
|
||||
// vmFunction: {
|
||||
// name: 'host CPU usage',
|
||||
// description: 'Raises an alarm when the average usage of any CPU is higher/lower than the threshold',
|
||||
// unit: '%',
|
||||
// createParser: [Function: createParser],
|
||||
// },
|
||||
// snapshot: [Function: snapshot],
|
||||
// }
|
||||
//
|
||||
// Sample of an entry of a snapshot
|
||||
// {
|
||||
// uuid: '8485ea1f-b475-f6f2-58a7-895ab626ce5d',
|
||||
// object: host,
|
||||
// objectLink: '[lab1](localhost:3000#/hosts/485ea1f-b475-f6f2-58a7-895ab626ce5d/stats)'
|
||||
// rrd: stats,
|
||||
// data: {
|
||||
// parseRow: [Function: parseRow],
|
||||
// getDisplayableValue: [Function: getDisplayableValue],
|
||||
// shouldAlarm: [Function: shouldAlarm],
|
||||
// },
|
||||
// value: 70,
|
||||
// shouldAlarm: true,
|
||||
// listItem: ' * [lab1](localhost:3000#/hosts/485ea1f-b475-f6f2-58a7-895ab626ce5d/stats): 70%\n'
|
||||
// }
|
||||
async _checkMonitors () {
|
||||
const monitors = this._getMonitors()
|
||||
for (const monitor of monitors) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-usage-report",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
@@ -36,6 +36,7 @@
|
||||
"dependencies": {
|
||||
"@xen-orchestra/async-map": "^0.0.0",
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"handlebars": "^4.0.6",
|
||||
"html-minifier": "^3.5.8",
|
||||
"human-format": "^0.10.0",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import Handlebars from 'handlebars'
|
||||
import humanFormat from 'human-format'
|
||||
import { createSchedule } from '@xen-orchestra/cron'
|
||||
@@ -23,6 +24,8 @@ import { readFile, writeFile } from 'fs'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const log = createLogger('xo:xo-server-usage-report')
|
||||
|
||||
const GRANULARITY = 'days'
|
||||
|
||||
const pReadFile = promisify(readFile)
|
||||
@@ -251,12 +254,10 @@ function getTop (objects, options) {
|
||||
function computePercentage (curr, prev, options) {
|
||||
return zipObject(
|
||||
options,
|
||||
map(
|
||||
options,
|
||||
opt =>
|
||||
prev[opt] === 0 || prev[opt] === null
|
||||
? 'NONE'
|
||||
: `${((curr[opt] - prev[opt]) * 100) / prev[opt]}`
|
||||
map(options, opt =>
|
||||
prev[opt] === 0 || prev[opt] === null
|
||||
? 'NONE'
|
||||
: `${((curr[opt] - prev[opt]) * 100) / prev[opt]}`
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -287,7 +288,18 @@ async function getVmsStats ({ runningVms, xo }) {
|
||||
return orderBy(
|
||||
await Promise.all(
|
||||
map(runningVms, async vm => {
|
||||
const { stats } = await xo.getXapiVmStats(vm, GRANULARITY)
|
||||
const { stats } = await xo
|
||||
.getXapiVmStats(vm, GRANULARITY)
|
||||
.catch(error => {
|
||||
log.warn('Error on fetching VM stats', {
|
||||
error,
|
||||
vmId: vm.id,
|
||||
})
|
||||
return {
|
||||
stats: {},
|
||||
}
|
||||
})
|
||||
|
||||
const iopsRead = METRICS_MEAN.iops(get(stats.iops, 'r'))
|
||||
const iopsWrite = METRICS_MEAN.iops(get(stats.iops, 'w'))
|
||||
return {
|
||||
@@ -314,7 +326,18 @@ async function getHostsStats ({ runningHosts, xo }) {
|
||||
return orderBy(
|
||||
await Promise.all(
|
||||
map(runningHosts, async host => {
|
||||
const { stats } = await xo.getXapiHostStats(host, GRANULARITY)
|
||||
const { stats } = await xo
|
||||
.getXapiHostStats(host, GRANULARITY)
|
||||
.catch(error => {
|
||||
log.warn('Error on fetching host stats', {
|
||||
error,
|
||||
hostId: host.id,
|
||||
})
|
||||
return {
|
||||
stats: {},
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
uuid: host.uuid,
|
||||
name: host.name_label,
|
||||
@@ -351,7 +374,18 @@ async function getSrsStats ({ xo, xoObjects }) {
|
||||
name += ` (${container.name_label})`
|
||||
}
|
||||
|
||||
const { stats } = await xo.getXapiSrStats(sr.id, GRANULARITY)
|
||||
const { stats } = await xo
|
||||
.getXapiSrStats(sr.id, GRANULARITY)
|
||||
.catch(error => {
|
||||
log.warn('Error on fetching SR stats', {
|
||||
error,
|
||||
srId: sr.id,
|
||||
})
|
||||
return {
|
||||
stats: {},
|
||||
}
|
||||
})
|
||||
|
||||
const iopsRead = computeMean(get(stats.iops, 'r'))
|
||||
const iopsWrite = computeMean(get(stats.iops, 'w'))
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
//
|
||||
// See sample.config.yaml to override.
|
||||
{
|
||||
"apiWebSocketOptions": {
|
||||
// https://github.com/websockets/ws#websocket-compression
|
||||
// "perMessageDeflate": {
|
||||
// "threshold": 524288 // 512kiB
|
||||
// }
|
||||
},
|
||||
|
||||
"http": {
|
||||
"listen": [
|
||||
{
|
||||
@@ -27,6 +34,7 @@
|
||||
|
||||
"mounts": {}
|
||||
},
|
||||
|
||||
"datadir": "/var/lib/xo-server/data",
|
||||
|
||||
// Should users be created on first sign in?
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server",
|
||||
"version": "5.29.4",
|
||||
"version": "5.30.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -38,7 +38,7 @@
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"@xen-orchestra/mixin": "^0.0.0",
|
||||
"ajv": "^6.1.1",
|
||||
"app-conf": "^0.5.0",
|
||||
"app-conf": "^0.6.0",
|
||||
"archiver": "^3.0.0",
|
||||
"async-iterator-to-stream": "^1.0.1",
|
||||
"base64url": "^3.0.0",
|
||||
@@ -109,17 +109,17 @@
|
||||
"stoppable": "^1.0.5",
|
||||
"struct-fu": "^1.2.0",
|
||||
"tar-stream": "^1.5.5",
|
||||
"through2": "^2.0.3",
|
||||
"through2": "^3.0.0",
|
||||
"tmp": "^0.0.33",
|
||||
"uuid": "^3.0.1",
|
||||
"value-matcher": "^0.2.0",
|
||||
"vhd-lib": "^0.4.0",
|
||||
"ws": "^6.0.0",
|
||||
"xen-api": "^0.20.0",
|
||||
"xen-api": "^0.22.0",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.3.0",
|
||||
"xo-acl-resolver": "^0.4.0",
|
||||
"xo-collection": "^0.4.1",
|
||||
"xo-common": "^0.1.2",
|
||||
"xo-common": "^0.2.0",
|
||||
"xo-remote-parser": "^0.5.0",
|
||||
"xo-vmdk-to-vhd": "^0.1.5",
|
||||
"yazl": "^2.4.3"
|
||||
@@ -138,6 +138,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"babel-plugin-transform-dev": "^2.0.1",
|
||||
"cross-env": "^5.1.3",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^2.6.2"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import pump from 'pump'
|
||||
import { format } from 'json-rpc-peer'
|
||||
import { noSuchObject, unauthorized } from 'xo-common/api-errors'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
import { parseSize } from '../utils'
|
||||
|
||||
@@ -31,9 +31,7 @@ export async function create ({ name, size, sr, vm, bootable, position, mode })
|
||||
// the resource set does not exist, falls back to normal check
|
||||
}
|
||||
|
||||
if (!(await this.hasPermissions(this.user.id, [[sr.id, 'administrate']]))) {
|
||||
throw unauthorized()
|
||||
}
|
||||
await this.checkPermissions(this.user.id, [[sr.id, 'administrate']])
|
||||
} while (false)
|
||||
|
||||
const xapi = this.getXapi(sr)
|
||||
@@ -125,6 +123,7 @@ async function handleImportContent (req, res, { xapi, id }) {
|
||||
req.setTimeout(43200000) // 12 hours
|
||||
|
||||
try {
|
||||
req.length = +req.headers['content-length']
|
||||
await xapi.importVdiContent(id, req)
|
||||
res.end(format.response(0, true))
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { unauthorized } from 'xo-common/api-errors'
|
||||
|
||||
export function create (props) {
|
||||
return this.createIpPool(props)
|
||||
}
|
||||
@@ -22,15 +20,12 @@ delete_.description = 'Delete an ipPool'
|
||||
export function getAll (params) {
|
||||
const { user } = this
|
||||
|
||||
if (!user) {
|
||||
throw unauthorized()
|
||||
}
|
||||
|
||||
return this.getAllIpPools(
|
||||
user.permission === 'admin' ? params && params.userId : user.id
|
||||
)
|
||||
}
|
||||
|
||||
getAll.permission = ''
|
||||
getAll.description = 'List all ipPools'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import { unauthorized } from 'xo-common/api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export function create ({ name, subjects, objects, limits }) {
|
||||
return this.createResourceSet(name, subjects, objects, limits)
|
||||
}
|
||||
@@ -99,14 +95,10 @@ set.params = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function get ({ id }) {
|
||||
const { user } = this
|
||||
if (!user) {
|
||||
throw unauthorized()
|
||||
}
|
||||
|
||||
return this.getResourceSet(id)
|
||||
}
|
||||
|
||||
get.permission = ''
|
||||
get.params = {
|
||||
id: {
|
||||
type: 'string',
|
||||
@@ -116,14 +108,10 @@ get.params = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function getAll () {
|
||||
const { user } = this
|
||||
if (!user) {
|
||||
throw unauthorized()
|
||||
}
|
||||
|
||||
return this.getAllResourceSets(user.id)
|
||||
return this.getAllResourceSets(this.user.id)
|
||||
}
|
||||
|
||||
getAll.permission = ''
|
||||
getAll.description = 'Get the list of all existing resource set'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -4,7 +4,7 @@ export async function add ({ autoConnect = true, ...props }) {
|
||||
const server = await this.registerXenServer(props)
|
||||
|
||||
if (autoConnect) {
|
||||
;this.connectXenServer(server.id)::ignoreErrors()
|
||||
this.connectXenServer(server.id)::ignoreErrors()
|
||||
}
|
||||
|
||||
return server.id
|
||||
@@ -105,7 +105,7 @@ set.params = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function connect ({ id }) {
|
||||
;this.updateXenServer(id, { enabled: true })::ignoreErrors()
|
||||
this.updateXenServer(id, { enabled: true })::ignoreErrors()
|
||||
await this.connectXenServer(id)
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ connect.params = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function disconnect ({ id }) {
|
||||
;this.updateXenServer(id, { enabled: false })::ignoreErrors()
|
||||
this.updateXenServer(id, { enabled: false })::ignoreErrors()
|
||||
await this.disconnectXenServer(id)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: rename to disk.*
|
||||
|
||||
import { invalidParameters, unauthorized } from 'xo-common/api-errors'
|
||||
import { invalidParameters } from 'xo-common/api-errors'
|
||||
import { isArray, reduce } from 'lodash'
|
||||
|
||||
import { parseSize } from '../utils'
|
||||
@@ -67,13 +67,8 @@ export async function set (params) {
|
||||
{ disk: size - vdi.size },
|
||||
resourceSetId
|
||||
)
|
||||
} else if (
|
||||
!(
|
||||
this.user.permission === 'admin' ||
|
||||
(await this.hasPermissions(this.user.id, [[vdi.$SR, 'operate']]))
|
||||
)
|
||||
) {
|
||||
throw unauthorized()
|
||||
} else {
|
||||
await this.checkPermissions(this.user.id, [[vdi.$SR, 'operate']])
|
||||
}
|
||||
|
||||
await xapi.resizeVdi(ref, size)
|
||||
|
||||
@@ -6,7 +6,7 @@ import { diffItems } from '../utils'
|
||||
|
||||
// TODO: move into vm and rename to removeInterface
|
||||
async function delete_ ({ vif }) {
|
||||
;this.allocIpAddresses(
|
||||
this.allocIpAddresses(
|
||||
vif.id,
|
||||
null,
|
||||
vif.allowedIpv4Addresses.concat(vif.allowedIpv6Addresses)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import concat from 'lodash/concat'
|
||||
import defer from 'golike-defer'
|
||||
import { format } from 'json-rpc-peer'
|
||||
import { ignoreErrors } from 'promise-toolbox'
|
||||
import { assignWith, concat } from 'lodash'
|
||||
import {
|
||||
forbiddenOperation,
|
||||
invalidParameters,
|
||||
@@ -13,11 +13,11 @@ import { forEach, map, mapFilter, parseSize } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export function getHaValues () {
|
||||
export function getHaValues() {
|
||||
return ['best-effort', 'restart', '']
|
||||
}
|
||||
|
||||
function checkPermissionOnSrs (vm, permission = 'operate') {
|
||||
function checkPermissionOnSrs(vm, permission = 'operate') {
|
||||
const permissions = []
|
||||
forEach(vm.$VBDs, vbdId => {
|
||||
const vbd = this.getObject(vbdId, 'VBD')
|
||||
@@ -32,13 +32,7 @@ function checkPermissionOnSrs (vm, permission = 'operate') {
|
||||
])
|
||||
})
|
||||
|
||||
return this.hasPermissions(this.session.get('user_id'), permissions).then(
|
||||
success => {
|
||||
if (!success) {
|
||||
throw unauthorized()
|
||||
}
|
||||
}
|
||||
)
|
||||
return this.checkPermissions(this.session.get('user_id'), permissions)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
@@ -50,17 +44,14 @@ const extract = (obj, prop) => {
|
||||
}
|
||||
|
||||
// TODO: Implement ACLs
|
||||
export async function create (params) {
|
||||
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, [
|
||||
if (resourceSet === undefined) {
|
||||
await this.checkPermissions(this.user.id, [
|
||||
[template.$pool, 'administrate'],
|
||||
]))
|
||||
) {
|
||||
throw unauthorized()
|
||||
])
|
||||
}
|
||||
|
||||
params.template = template._xapiId
|
||||
@@ -68,11 +59,10 @@ export async function create (params) {
|
||||
const xapi = this.getXapi(template)
|
||||
|
||||
const objectIds = [template.id]
|
||||
const { CPUs, memoryMax } = params
|
||||
const limits = {
|
||||
cpus: CPUs !== undefined ? CPUs : template.CPUs.number,
|
||||
cpus: template.CPUs.number,
|
||||
disk: 0,
|
||||
memory: memoryMax !== undefined ? memoryMax : template.memory.dynamic[1],
|
||||
memory: template.memory.dynamic[1],
|
||||
vms: 1,
|
||||
}
|
||||
const vdiSizesByDevice = {}
|
||||
@@ -152,8 +142,10 @@ export async function create (params) {
|
||||
if (resourceSet) {
|
||||
await this.checkResourceSetConstraints(resourceSet, user.id, objectIds)
|
||||
checkLimits = async limits2 => {
|
||||
await this.allocateLimitsInResourceSet(limits, resourceSet)
|
||||
await this.allocateLimitsInResourceSet(limits2, resourceSet)
|
||||
await this.allocateLimitsInResourceSet(
|
||||
assignWith({}, limits, limits2, (l1 = 0, l2) => l1 + l2),
|
||||
resourceSet
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +325,7 @@ create.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
async function delete_ ({
|
||||
async function delete_({
|
||||
delete_disks, // eslint-disable-line camelcase
|
||||
force,
|
||||
forceDeleteDefaultTemplate,
|
||||
@@ -374,7 +366,7 @@ async function delete_ ({
|
||||
vm.type === 'VM' && // only regular VMs
|
||||
xapi.xo.getData(vm._xapiId, 'resourceSet') != null
|
||||
) {
|
||||
;this.setVmResourceSet(vm._xapiId, null)::ignoreErrors()
|
||||
this.setVmResourceSet(vm._xapiId, null)::ignoreErrors()
|
||||
}
|
||||
|
||||
return xapi.deleteVm(
|
||||
@@ -411,7 +403,7 @@ export { delete_ as delete }
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function ejectCd ({ vm }) {
|
||||
export async function ejectCd({ vm }) {
|
||||
await this.getXapi(vm).ejectCdFromVm(vm._xapiId)
|
||||
}
|
||||
|
||||
@@ -425,7 +417,7 @@ ejectCd.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function insertCd ({ vm, vdi, force = true }) {
|
||||
export async function insertCd({ vm, vdi, force = true }) {
|
||||
await this.getXapi(vm).insertCdIntoVm(vdi._xapiId, vm._xapiId, { force })
|
||||
}
|
||||
|
||||
@@ -444,7 +436,7 @@ insertCd.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function migrate ({
|
||||
export async function migrate({
|
||||
vm,
|
||||
host,
|
||||
sr,
|
||||
@@ -476,9 +468,7 @@ export async function migrate ({
|
||||
})
|
||||
}
|
||||
|
||||
if (!(await this.hasPermissions(this.session.get('user_id'), permissions))) {
|
||||
throw unauthorized()
|
||||
}
|
||||
await this.checkPermissions(this.user.id, permissions)
|
||||
|
||||
await this.getXapi(vm).migrateVm(
|
||||
vm._xapiId,
|
||||
@@ -522,7 +512,7 @@ migrate.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function set (params) {
|
||||
export async function set(params) {
|
||||
const VM = extract(params, 'VM')
|
||||
const xapi = this.getXapi(VM)
|
||||
const vmId = VM._xapiId
|
||||
@@ -630,7 +620,7 @@ set.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function restart ({ vm, force = false }) {
|
||||
export async function restart({ vm, force = false }) {
|
||||
const xapi = this.getXapi(vm)
|
||||
|
||||
if (force) {
|
||||
@@ -651,7 +641,7 @@ restart.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const clone = defer(async function (
|
||||
export const clone = defer(async function(
|
||||
$defer,
|
||||
{ vm, name, full_copy: fullCopy }
|
||||
) {
|
||||
@@ -693,7 +683,7 @@ clone.resolve = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: implement resource sets
|
||||
export async function copy ({ compress, name: nameLabel, sr, vm }) {
|
||||
export async function copy({ compress, name: nameLabel, sr, vm }) {
|
||||
if (vm.$pool === sr.$pool) {
|
||||
if (vm.power_state === 'Running') {
|
||||
await checkPermissionOnSrs.call(this, vm)
|
||||
@@ -734,15 +724,9 @@ copy.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function convertToTemplate ({ vm }) {
|
||||
export async function convertToTemplate({ vm }) {
|
||||
// Convert to a template requires pool admin permission.
|
||||
if (
|
||||
!(await this.hasPermissions(this.session.get('user_id'), [
|
||||
[vm.$pool, 'administrate'],
|
||||
]))
|
||||
) {
|
||||
throw unauthorized()
|
||||
}
|
||||
await this.checkPermissions(this.user.id, [[vm.$pool, 'administrate']])
|
||||
|
||||
await this.getXapi(vm).call('VM.set_is_a_template', vm._xapiRef, true)
|
||||
}
|
||||
@@ -761,7 +745,7 @@ export { convertToTemplate as convert }
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: implement resource sets
|
||||
export const snapshot = defer(async function (
|
||||
export const snapshot = defer(async function(
|
||||
$defer,
|
||||
{ vm, name = `${vm.name_label}_${new Date().toISOString()}` }
|
||||
) {
|
||||
@@ -789,7 +773,7 @@ snapshot.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function rollingDeltaBackup ({
|
||||
export function rollingDeltaBackup({
|
||||
vm,
|
||||
remote,
|
||||
tag,
|
||||
@@ -821,7 +805,7 @@ rollingDeltaBackup.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function importDeltaBackup ({ sr, remote, filePath, mapVdisSrs }) {
|
||||
export function importDeltaBackup({ sr, remote, filePath, mapVdisSrs }) {
|
||||
const mapVdisSrsXapi = {}
|
||||
|
||||
forEach(mapVdisSrs, (srId, vdiId) => {
|
||||
@@ -852,7 +836,7 @@ importDeltaBackup.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function deltaCopy ({ force, vm, retention, sr }) {
|
||||
export function deltaCopy({ force, vm, retention, sr }) {
|
||||
return this.deltaCopyVm(vm, sr, force, retention)
|
||||
}
|
||||
|
||||
@@ -870,7 +854,7 @@ deltaCopy.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function rollingSnapshot ({ vm, tag, depth, retention = depth }) {
|
||||
export async function rollingSnapshot({ vm, tag, depth, retention = depth }) {
|
||||
await checkPermissionOnSrs.call(this, vm)
|
||||
return this.rollingSnapshotVm(vm, tag, retention)
|
||||
}
|
||||
@@ -892,7 +876,7 @@ rollingSnapshot.description =
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function backup ({ vm, remoteId, file, compress }) {
|
||||
export function backup({ vm, remoteId, file, compress }) {
|
||||
return this.backupVm({ vm, remoteId, file, compress })
|
||||
}
|
||||
|
||||
@@ -913,7 +897,7 @@ backup.description = 'Exports a VM to the file system'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function importBackup ({ remote, file, sr }) {
|
||||
export function importBackup({ remote, file, sr }) {
|
||||
return this.importVmBackup(remote, file, sr)
|
||||
}
|
||||
|
||||
@@ -934,7 +918,7 @@ importBackup.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function rollingBackup ({
|
||||
export function rollingBackup({
|
||||
vm,
|
||||
remoteId,
|
||||
tag,
|
||||
@@ -972,7 +956,7 @@ rollingBackup.description =
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function rollingDrCopy ({
|
||||
export function rollingDrCopy({
|
||||
vm,
|
||||
pool,
|
||||
sr,
|
||||
@@ -1026,7 +1010,7 @@ rollingDrCopy.description =
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function start ({ vm, force, host }) {
|
||||
export function start({ vm, force, host }) {
|
||||
return this.getXapi(vm).startVm(vm._xapiId, host?._xapiId, force)
|
||||
}
|
||||
|
||||
@@ -1047,7 +1031,7 @@ start.resolve = {
|
||||
// - if !force → clean shutdown
|
||||
// - if force is true → hard shutdown
|
||||
// - if force is integer → clean shutdown and after force seconds, hard shutdown.
|
||||
export async function stop ({ vm, force }) {
|
||||
export async function stop({ vm, force }) {
|
||||
const xapi = this.getXapi(vm)
|
||||
|
||||
// Hard shutdown
|
||||
@@ -1082,7 +1066,7 @@ stop.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function suspend ({ vm }) {
|
||||
export async function suspend({ vm }) {
|
||||
await this.getXapi(vm).call('VM.suspend', vm._xapiRef)
|
||||
}
|
||||
|
||||
@@ -1096,7 +1080,21 @@ suspend.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function resume ({ vm }) {
|
||||
export async function pause({ vm }) {
|
||||
await this.getXapi(vm).call('VM.pause', vm._xapiRef)
|
||||
}
|
||||
|
||||
pause.params = {
|
||||
id: { type: 'string' },
|
||||
}
|
||||
|
||||
pause.resolve = {
|
||||
vm: ['id', 'VM', 'operate'],
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function resume({ vm }) {
|
||||
return this.getXapi(vm).resumeVm(vm._xapiId)
|
||||
}
|
||||
|
||||
@@ -1110,7 +1108,7 @@ resume.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function revert ({ snapshot, snapshotBefore }) {
|
||||
export function revert({ snapshot, snapshotBefore }) {
|
||||
return this.getXapi(snapshot).revertVm(snapshot._xapiId, snapshotBefore)
|
||||
}
|
||||
|
||||
@@ -1125,7 +1123,7 @@ revert.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
async function handleExport (req, res, { xapi, id, compress }) {
|
||||
async function handleExport(req, res, { xapi, id, compress }) {
|
||||
const stream = await xapi.exportVm(id, {
|
||||
compress: compress != null ? compress : true,
|
||||
})
|
||||
@@ -1142,7 +1140,7 @@ async function handleExport (req, res, { xapi, id, compress }) {
|
||||
}
|
||||
|
||||
// TODO: integrate in xapi.js
|
||||
async function export_ ({ vm, compress }) {
|
||||
async function export_({ vm, compress }) {
|
||||
if (vm.power_state === 'Running') {
|
||||
await checkPermissionOnSrs.call(this, vm)
|
||||
}
|
||||
@@ -1173,7 +1171,7 @@ export { export_ as export }
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
async function handleVmImport (req, res, { data, srId, type, xapi }) {
|
||||
async function handleVmImport(req, res, { data, srId, type, xapi }) {
|
||||
// Timeout seems to be broken in Node 4.
|
||||
// See https://github.com/nodejs/node/issues/3319
|
||||
req.setTimeout(43200000) // 12 hours
|
||||
@@ -1188,34 +1186,17 @@ async function handleVmImport (req, res, { data, srId, type, xapi }) {
|
||||
}
|
||||
|
||||
// TODO: "sr_id" can be passed in URL to target a specific SR
|
||||
async function import_ ({ data, host, sr, type }) {
|
||||
let xapi
|
||||
async function import_({ data, sr, type }) {
|
||||
if (data && type === 'xva') {
|
||||
throw invalidParameters('unsupported field data for the file type xva')
|
||||
}
|
||||
|
||||
if (!sr) {
|
||||
if (!host) {
|
||||
throw invalidParameters('you must provide either host or SR')
|
||||
}
|
||||
|
||||
xapi = this.getXapi(host)
|
||||
sr = xapi.pool.$default_SR
|
||||
if (!sr) {
|
||||
throw invalidParameters('there is not default SR in this pool')
|
||||
}
|
||||
|
||||
// FIXME: must have administrate permission on default SR.
|
||||
} else {
|
||||
xapi = this.getXapi(sr)
|
||||
}
|
||||
|
||||
return {
|
||||
$sendTo: await this.registerHttpRequest(handleVmImport, {
|
||||
data,
|
||||
srId: sr._xapiId,
|
||||
type,
|
||||
xapi,
|
||||
xapi: this.getXapi(sr),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1250,13 +1231,11 @@ import_.params = {
|
||||
},
|
||||
},
|
||||
},
|
||||
host: { type: 'string', optional: true },
|
||||
type: { type: 'string', optional: true },
|
||||
sr: { type: 'string', optional: true },
|
||||
sr: { type: 'string' },
|
||||
}
|
||||
|
||||
import_.resolve = {
|
||||
host: ['host', 'host', 'administrate'],
|
||||
sr: ['sr', 'SR', 'administrate'],
|
||||
}
|
||||
|
||||
@@ -1266,7 +1245,7 @@ export { import_ as import }
|
||||
|
||||
// FIXME: if position is used, all other disks after this position
|
||||
// should be shifted.
|
||||
export async function attachDisk ({ vm, vdi, position, mode, bootable }) {
|
||||
export async function attachDisk({ vm, vdi, position, mode, bootable }) {
|
||||
await this.getXapi(vm).createVbd({
|
||||
bootable,
|
||||
mode,
|
||||
@@ -1295,7 +1274,7 @@ attachDisk.resolve = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: implement resource sets
|
||||
export async function createInterface ({
|
||||
export async function createInterface({
|
||||
vm,
|
||||
network,
|
||||
position,
|
||||
@@ -1308,10 +1287,8 @@ export async function createInterface ({
|
||||
await this.checkResourceSetConstraints(resourceSet, this.user.id, [
|
||||
network.id,
|
||||
])
|
||||
} else if (
|
||||
!(await this.hasPermissions(this.user.id, [[network.id, 'view']]))
|
||||
) {
|
||||
throw unauthorized()
|
||||
} else {
|
||||
await this.checkPermissions(this.user.id, [[network.id, 'view']])
|
||||
}
|
||||
|
||||
let ipAddresses
|
||||
@@ -1366,7 +1343,7 @@ createInterface.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function attachPci ({ vm, pciId }) {
|
||||
export async function attachPci({ vm, pciId }) {
|
||||
const xapi = this.getXapi(vm)
|
||||
|
||||
await xapi.call('VM.add_to_other_config', vm._xapiRef, 'pci', pciId)
|
||||
@@ -1383,7 +1360,7 @@ attachPci.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function detachPci ({ vm }) {
|
||||
export async function detachPci({ vm }) {
|
||||
const xapi = this.getXapi(vm)
|
||||
|
||||
await xapi.call('VM.remove_from_other_config', vm._xapiRef, 'pci')
|
||||
@@ -1398,7 +1375,7 @@ detachPci.resolve = {
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function stats ({ vm, granularity }) {
|
||||
export function stats({ vm, granularity }) {
|
||||
return this.getXapiVmStats(vm._xapiId, granularity)
|
||||
}
|
||||
|
||||
@@ -1418,7 +1395,7 @@ stats.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function setBootOrder ({ vm, order }) {
|
||||
export async function setBootOrder({ vm, order }) {
|
||||
const xapi = this.getXapi(vm)
|
||||
|
||||
order = { order }
|
||||
@@ -1441,7 +1418,7 @@ setBootOrder.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function recoveryStart ({ vm }) {
|
||||
export function recoveryStart({ vm }) {
|
||||
return this.getXapi(vm).startVmOnCd(vm._xapiId)
|
||||
}
|
||||
|
||||
@@ -1455,7 +1432,7 @@ recoveryStart.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function getCloudInitConfig ({ template }) {
|
||||
export function getCloudInitConfig({ template }) {
|
||||
return this.getXapi(template).getCloudInitConfig(template._xapiId)
|
||||
}
|
||||
|
||||
@@ -1469,7 +1446,7 @@ getCloudInitConfig.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function createCloudInitConfigDrive ({ vm, sr, config, coreos }) {
|
||||
export async function createCloudInitConfigDrive({ vm, sr, config, coreos }) {
|
||||
const xapi = this.getXapi(vm)
|
||||
if (coreos) {
|
||||
// CoreOS is a special CloudConfig drive created by XS plugin
|
||||
@@ -1496,7 +1473,7 @@ createCloudInitConfigDrive.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function createVgpu ({ vm, gpuGroup, vgpuType }) {
|
||||
export async function createVgpu({ vm, gpuGroup, vgpuType }) {
|
||||
// TODO: properly handle device. Can a VM have 2 vGPUS?
|
||||
await this.getXapi(vm).createVgpu(
|
||||
vm._xapiId,
|
||||
@@ -1519,7 +1496,7 @@ createVgpu.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function deleteVgpu ({ vgpu }) {
|
||||
export async function deleteVgpu({ vgpu }) {
|
||||
await this.getXapi(vgpu).deleteVgpu(vgpu._xapiId)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import execa from 'execa'
|
||||
import fs from 'fs-extra'
|
||||
import map from 'lodash/map'
|
||||
import { tap, delay } from 'promise-toolbox'
|
||||
import { NULL_REF } from 'xen-api'
|
||||
import { invalidParameters } from 'xo-common/api-errors'
|
||||
import { v4 as generateUuid } from 'uuid'
|
||||
import { includes, remove, filter, find, range } from 'lodash'
|
||||
@@ -25,7 +26,7 @@ const XOSAN_LICENSE_QUOTA = 50 * GIGABYTE
|
||||
|
||||
const CURRENT_POOL_OPERATIONS = {}
|
||||
|
||||
function getXosanConfig (xosansr, xapi = this.getXapi(xosansr)) {
|
||||
function getXosanConfig(xosansr, xapi = this.getXapi(xosansr)) {
|
||||
const data = xapi.xo.getData(xosansr, 'xosan_config')
|
||||
if (data && data.networkPrefix === undefined) {
|
||||
// some xosan might have been created before this field was added
|
||||
@@ -36,7 +37,7 @@ function getXosanConfig (xosansr, xapi = this.getXapi(xosansr)) {
|
||||
return data
|
||||
}
|
||||
|
||||
function _getIPToVMDict (xapi, sr) {
|
||||
function _getIPToVMDict(xapi, sr) {
|
||||
const dict = {}
|
||||
const data = getXosanConfig(sr, xapi)
|
||||
if (data && data.nodes) {
|
||||
@@ -54,7 +55,7 @@ function _getIPToVMDict (xapi, sr) {
|
||||
return dict
|
||||
}
|
||||
|
||||
function _getGlusterEndpoint (sr) {
|
||||
function _getGlusterEndpoint(sr) {
|
||||
const xapi = this.getXapi(sr)
|
||||
const data = getXosanConfig(sr, xapi)
|
||||
if (!data || !data.nodes) {
|
||||
@@ -68,7 +69,7 @@ function _getGlusterEndpoint (sr) {
|
||||
}
|
||||
}
|
||||
|
||||
async function rateLimitedRetry (action, shouldRetry, retryCount = 20) {
|
||||
async function rateLimitedRetry(action, shouldRetry, retryCount = 20) {
|
||||
let retryDelay = 500 * (1 + Math.random() / 20)
|
||||
let result
|
||||
while (retryCount > 0 && (result = await action()) && shouldRetry(result)) {
|
||||
@@ -80,8 +81,8 @@ async function rateLimitedRetry (action, shouldRetry, retryCount = 20) {
|
||||
return result
|
||||
}
|
||||
|
||||
function createVolumeInfoTypes () {
|
||||
function parseHeal (parsed) {
|
||||
function createVolumeInfoTypes() {
|
||||
function parseHeal(parsed) {
|
||||
const bricks = []
|
||||
parsed['healInfo']['bricks']['brick'].forEach(brick => {
|
||||
bricks.push(brick)
|
||||
@@ -92,7 +93,7 @@ function createVolumeInfoTypes () {
|
||||
return { commandStatus: true, result: { bricks } }
|
||||
}
|
||||
|
||||
function parseStatus (parsed) {
|
||||
function parseStatus(parsed) {
|
||||
const brickDictByUuid = {}
|
||||
const volume = parsed['volStatus']['volumes']['volume']
|
||||
volume['node'].forEach(node => {
|
||||
@@ -105,7 +106,7 @@ function createVolumeInfoTypes () {
|
||||
}
|
||||
}
|
||||
|
||||
async function parseInfo (parsed) {
|
||||
async function parseInfo(parsed) {
|
||||
const volume = parsed['volInfo']['volumes']['volume']
|
||||
volume['bricks'] = volume['bricks']['brick']
|
||||
volume['options'] = volume['options']['option']
|
||||
@@ -113,7 +114,7 @@ function createVolumeInfoTypes () {
|
||||
}
|
||||
|
||||
const sshInfoType = (command, handler) => {
|
||||
return async function (sr) {
|
||||
return async function(sr) {
|
||||
const glusterEndpoint = this::_getGlusterEndpoint(sr)
|
||||
const cmdShouldRetry = result =>
|
||||
!result['commandStatus'] &&
|
||||
@@ -128,8 +129,8 @@ function createVolumeInfoTypes () {
|
||||
}
|
||||
}
|
||||
|
||||
async function profileType (sr) {
|
||||
async function parseProfile (parsed) {
|
||||
async function profileType(sr) {
|
||||
async function parseProfile(parsed) {
|
||||
const volume = parsed['volProfile']
|
||||
volume['bricks'] = ensureArray(volume['brick'])
|
||||
delete volume['brick']
|
||||
@@ -139,8 +140,8 @@ function createVolumeInfoTypes () {
|
||||
return this::sshInfoType('profile xosan info', parseProfile)(sr)
|
||||
}
|
||||
|
||||
async function profileTopType (sr) {
|
||||
async function parseTop (parsed) {
|
||||
async function profileTopType(sr) {
|
||||
async function parseTop(parsed) {
|
||||
const volume = parsed['volTop']
|
||||
volume['bricks'] = ensureArray(volume['brick'])
|
||||
delete volume['brick']
|
||||
@@ -154,7 +155,7 @@ function createVolumeInfoTypes () {
|
||||
}))
|
||||
}
|
||||
|
||||
function checkHosts (sr) {
|
||||
function checkHosts(sr) {
|
||||
const xapi = this.getXapi(sr)
|
||||
const data = getXosanConfig(sr, xapi)
|
||||
const network = xapi.getObject(data.network)
|
||||
@@ -179,7 +180,7 @@ function createVolumeInfoTypes () {
|
||||
|
||||
const VOLUME_INFO_TYPES = createVolumeInfoTypes()
|
||||
|
||||
export async function getVolumeInfo ({ sr, infoType }) {
|
||||
export async function getVolumeInfo({ sr, infoType }) {
|
||||
await this.checkXosanLicense({ srId: sr.uuid })
|
||||
|
||||
const glusterEndpoint = this::_getGlusterEndpoint(sr)
|
||||
@@ -210,7 +211,7 @@ getVolumeInfo.resolve = {
|
||||
sr: ['sr', 'SR', 'administrate'],
|
||||
}
|
||||
|
||||
export async function profileStatus ({ sr, changeStatus = null }) {
|
||||
export async function profileStatus({ sr, changeStatus = null }) {
|
||||
await this.checkXosanLicense({ srId: sr.uuid })
|
||||
|
||||
const glusterEndpoint = this::_getGlusterEndpoint(sr)
|
||||
@@ -239,7 +240,7 @@ profileStatus.resolve = {
|
||||
sr: ['sr', 'SR', 'administrate'],
|
||||
}
|
||||
|
||||
function reconfigurePifIP (xapi, pif, newIP) {
|
||||
function reconfigurePifIP(xapi, pif, newIP) {
|
||||
xapi.call(
|
||||
'PIF.reconfigure_ip',
|
||||
pif.$ref,
|
||||
@@ -252,7 +253,7 @@ function reconfigurePifIP (xapi, pif, newIP) {
|
||||
}
|
||||
|
||||
// this function should probably become fixSomething(thingToFix, parmas)
|
||||
export async function fixHostNotInNetwork ({ xosanSr, host }) {
|
||||
export async function fixHostNotInNetwork({ xosanSr, host }) {
|
||||
await this.checkXosanLicense({ srId: xosanSr.uuid })
|
||||
|
||||
const xapi = this.getXapi(xosanSr)
|
||||
@@ -296,22 +297,22 @@ fixHostNotInNetwork.resolve = {
|
||||
sr: ['sr', 'SR', 'administrate'],
|
||||
}
|
||||
|
||||
function floor2048 (value) {
|
||||
function floor2048(value) {
|
||||
return 2048 * Math.floor(value / 2048)
|
||||
}
|
||||
|
||||
async function copyVm (xapi, originalVm, sr) {
|
||||
async function copyVm(xapi, originalVm, sr) {
|
||||
return { sr, vm: await xapi.copyVm(originalVm, sr) }
|
||||
}
|
||||
|
||||
async function callPlugin (xapi, host, command, params) {
|
||||
async function callPlugin(xapi, host, command, params) {
|
||||
log.debug(`calling plugin ${host.address} ${command}`)
|
||||
return JSON.parse(
|
||||
await xapi.call('host.call_plugin', host.$ref, 'xosan.py', command, params)
|
||||
)
|
||||
}
|
||||
|
||||
async function remoteSsh (glusterEndpoint, cmd, ignoreError = false) {
|
||||
async function remoteSsh(glusterEndpoint, cmd, ignoreError = false) {
|
||||
let result
|
||||
const formatSshError = result => {
|
||||
const messageArray = []
|
||||
@@ -367,7 +368,7 @@ async function remoteSsh (glusterEndpoint, cmd, ignoreError = false) {
|
||||
)
|
||||
}
|
||||
|
||||
function findErrorMessage (commandResut) {
|
||||
function findErrorMessage(commandResut) {
|
||||
if (commandResut['exit'] === 0 && commandResut.parsed) {
|
||||
const cliOut = commandResut.parsed['cliOutput']
|
||||
if (cliOut['opErrstr'] && cliOut['opErrstr'].length) {
|
||||
@@ -383,7 +384,7 @@ function findErrorMessage (commandResut) {
|
||||
: commandResut['stdout']
|
||||
}
|
||||
|
||||
async function glusterCmd (glusterEndpoint, cmd, ignoreError = false) {
|
||||
async function glusterCmd(glusterEndpoint, cmd, ignoreError = false) {
|
||||
const result = await remoteSsh(
|
||||
glusterEndpoint,
|
||||
`gluster --mode=script --xml ${cmd}`,
|
||||
@@ -413,7 +414,7 @@ async function glusterCmd (glusterEndpoint, cmd, ignoreError = false) {
|
||||
return result
|
||||
}
|
||||
|
||||
const createNetworkAndInsertHosts = defer(async function (
|
||||
const createNetworkAndInsertHosts = defer(async function(
|
||||
$defer,
|
||||
xapi,
|
||||
pif,
|
||||
@@ -453,7 +454,7 @@ const createNetworkAndInsertHosts = defer(async function (
|
||||
return xosanNetwork
|
||||
})
|
||||
|
||||
async function getOrCreateSshKey (xapi) {
|
||||
async function getOrCreateSshKey(xapi) {
|
||||
let sshKey = xapi.xo.getData(xapi.pool, 'xosan_ssh_key')
|
||||
|
||||
if (!sshKey) {
|
||||
@@ -486,7 +487,7 @@ async function getOrCreateSshKey (xapi) {
|
||||
return sshKey
|
||||
}
|
||||
|
||||
const _probePoolAndWaitForPresence = defer(async function (
|
||||
const _probePoolAndWaitForPresence = defer(async function(
|
||||
$defer,
|
||||
glusterEndpoint,
|
||||
addresses
|
||||
@@ -498,7 +499,7 @@ const _probePoolAndWaitForPresence = defer(async function (
|
||||
)
|
||||
})
|
||||
|
||||
function shouldRetry (peers) {
|
||||
function shouldRetry(peers) {
|
||||
for (const peer of peers) {
|
||||
if (peer.state === '4') {
|
||||
return true
|
||||
@@ -516,7 +517,7 @@ const _probePoolAndWaitForPresence = defer(async function (
|
||||
return rateLimitedRetry(getPoolStatus, shouldRetry)
|
||||
})
|
||||
|
||||
async function configureGluster (
|
||||
async function configureGluster(
|
||||
redundancy,
|
||||
ipAndHosts,
|
||||
glusterEndpoint,
|
||||
@@ -603,7 +604,7 @@ async function configureGluster (
|
||||
await _setQuota(glusterEndpoint)
|
||||
}
|
||||
|
||||
async function _setQuota (glusterEndpoint) {
|
||||
async function _setQuota(glusterEndpoint) {
|
||||
await glusterCmd(glusterEndpoint, 'volume quota xosan enable', true)
|
||||
await glusterCmd(
|
||||
glusterEndpoint,
|
||||
@@ -617,11 +618,11 @@ async function _setQuota (glusterEndpoint) {
|
||||
)
|
||||
}
|
||||
|
||||
async function _removeQuota (glusterEndpoint) {
|
||||
async function _removeQuota(glusterEndpoint) {
|
||||
await glusterCmd(glusterEndpoint, 'volume quota xosan disable', true)
|
||||
}
|
||||
|
||||
export const createSR = defer(async function (
|
||||
export const createSR = defer(async function(
|
||||
$defer,
|
||||
{
|
||||
template,
|
||||
@@ -854,7 +855,7 @@ createSR.resolve = {
|
||||
pif: ['pif', 'PIF', 'administrate'],
|
||||
}
|
||||
|
||||
async function umountDisk (localEndpoint, diskMountPoint) {
|
||||
async function umountDisk(localEndpoint, diskMountPoint) {
|
||||
await remoteSsh(
|
||||
localEndpoint,
|
||||
`killall -v -w /usr/sbin/xfs_growfs; fuser -v ${diskMountPoint}; umount ${diskMountPoint} && sed -i '\\_${diskMountPoint}\\S_d' /etc/fstab && rm -rf ${diskMountPoint}`
|
||||
@@ -862,7 +863,7 @@ async function umountDisk (localEndpoint, diskMountPoint) {
|
||||
}
|
||||
|
||||
// this is mostly what the LVM SR driver does, but we are avoiding the 2To limit it imposes.
|
||||
async function createVDIOnLVMWithoutSizeLimit (xapi, lvmSr, diskSize) {
|
||||
async function createVDIOnLVMWithoutSizeLimit(xapi, lvmSr, diskSize) {
|
||||
const VG_PREFIX = 'VG_XenStorage-'
|
||||
const LV_PREFIX = 'LV-'
|
||||
const { type, uuid: srUuid, $PBDs } = xapi.getObject(lvmSr)
|
||||
@@ -893,7 +894,7 @@ async function createVDIOnLVMWithoutSizeLimit (xapi, lvmSr, diskSize) {
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewDisk (xapi, sr, vm, diskSize) {
|
||||
async function createNewDisk(xapi, sr, vm, diskSize) {
|
||||
const newDisk = await createVDIOnLVMWithoutSizeLimit(xapi, sr, diskSize)
|
||||
await xapi.createVbd({ vdi: newDisk, vm })
|
||||
let vbd = await xapi._waitObjectState(newDisk.$id, disk =>
|
||||
@@ -903,7 +904,7 @@ async function createNewDisk (xapi, sr, vm, diskSize) {
|
||||
return '/dev/' + vbd.device
|
||||
}
|
||||
|
||||
async function mountNewDisk (localEndpoint, hostname, newDeviceFiledeviceFile) {
|
||||
async function mountNewDisk(localEndpoint, hostname, newDeviceFiledeviceFile) {
|
||||
const brickRootCmd =
|
||||
'bash -c \'mkdir -p /bricks; for TESTVAR in {1..9}; do TESTDIR="/bricks/xosan$TESTVAR" ;if mkdir $TESTDIR; then echo $TESTDIR; exit 0; fi ; done ; exit 1\''
|
||||
const newBrickRoot = (await remoteSsh(
|
||||
@@ -916,7 +917,7 @@ async function mountNewDisk (localEndpoint, hostname, newDeviceFiledeviceFile) {
|
||||
return brickName
|
||||
}
|
||||
|
||||
async function replaceBrickOnSameVM (
|
||||
async function replaceBrickOnSameVM(
|
||||
xosansr,
|
||||
previousBrick,
|
||||
newLvmSr,
|
||||
@@ -993,7 +994,7 @@ async function replaceBrickOnSameVM (
|
||||
}
|
||||
}
|
||||
|
||||
export async function replaceBrick ({
|
||||
export async function replaceBrick({
|
||||
xosansr,
|
||||
previousBrick,
|
||||
newLvmSr,
|
||||
@@ -1085,7 +1086,7 @@ replaceBrick.resolve = {
|
||||
xosansr: ['sr', 'SR', 'administrate'],
|
||||
}
|
||||
|
||||
async function _prepareGlusterVm (
|
||||
async function _prepareGlusterVm(
|
||||
xapi,
|
||||
lvmSr,
|
||||
newVM,
|
||||
@@ -1122,7 +1123,7 @@ async function _prepareGlusterVm (
|
||||
}
|
||||
}
|
||||
}
|
||||
await xapi.addTag(newVM.$id, `XOSAN-${xapi.pool.name_label}`)
|
||||
await xapi.addTag(newVM.$id, 'XOSAN')
|
||||
await xapi.editVm(newVM, {
|
||||
name_label: `XOSAN - ${lvmSr.name_label} - ${
|
||||
host.name_label
|
||||
@@ -1138,9 +1139,12 @@ async function _prepareGlusterVm (
|
||||
await xapi.startVm(newVM)
|
||||
log.debug(`waiting for boot of ${ip}`)
|
||||
// wait until we find the assigned IP in the networks, we are just checking the boot is complete
|
||||
const vmIsUp = vm =>
|
||||
Boolean(vm.$guest_metrics && includes(vm.$guest_metrics.networks, ip))
|
||||
const vm = await xapi._waitObjectState(newVM.$id, vmIsUp)
|
||||
// fix #3688
|
||||
const vm = await xapi._waitObjectState(
|
||||
newVM.$id,
|
||||
_ => _.guest_metrics !== NULL_REF
|
||||
)
|
||||
await xapi._waitObjectState(vm.guest_metrics, _ => includes(_.networks, ip))
|
||||
log.debug(`booted ${ip}`)
|
||||
const localEndpoint = { xapi: xapi, hosts: [host], addresses: [ip] }
|
||||
const srFreeSpace = sr.physical_size - sr.physical_utilisation
|
||||
@@ -1162,7 +1166,7 @@ async function _prepareGlusterVm (
|
||||
return { address: ip, host, vm, underlyingSr: lvmSr, brickName }
|
||||
}
|
||||
|
||||
async function _importGlusterVM (xapi, template, lvmsrId) {
|
||||
async function _importGlusterVM(xapi, template, lvmsrId) {
|
||||
const templateStream = await this.requestResource(
|
||||
'xosan',
|
||||
template.id,
|
||||
@@ -1180,11 +1184,11 @@ async function _importGlusterVM (xapi, template, lvmsrId) {
|
||||
return xapi.barrier(newVM.$ref)
|
||||
}
|
||||
|
||||
function _findAFreeIPAddress (nodes, networkPrefix) {
|
||||
function _findAFreeIPAddress(nodes, networkPrefix) {
|
||||
return _findIPAddressOutsideList(map(nodes, n => n.vm.ip), networkPrefix)
|
||||
}
|
||||
|
||||
function _findIPAddressOutsideList (
|
||||
function _findIPAddressOutsideList(
|
||||
reservedList,
|
||||
networkPrefix,
|
||||
vmIpLastNumber = 101
|
||||
@@ -1203,7 +1207,7 @@ const _median = arr => {
|
||||
return arr[Math.floor(arr.length / 2)]
|
||||
}
|
||||
|
||||
const insertNewGlusterVm = defer(async function (
|
||||
const insertNewGlusterVm = defer(async function(
|
||||
$defer,
|
||||
xapi,
|
||||
xosansr,
|
||||
@@ -1253,7 +1257,7 @@ const insertNewGlusterVm = defer(async function (
|
||||
return { data, newVM, addressAndHost, glusterEndpoint }
|
||||
})
|
||||
|
||||
export const addBricks = defer(async function (
|
||||
export const addBricks = defer(async function(
|
||||
$defer,
|
||||
{ xosansr, lvmsrs, brickSize }
|
||||
) {
|
||||
@@ -1349,7 +1353,7 @@ addBricks.resolve = {
|
||||
lvmsrs: ['sr', 'SR', 'administrate'],
|
||||
}
|
||||
|
||||
export const removeBricks = defer(async function ($defer, { xosansr, bricks }) {
|
||||
export const removeBricks = defer(async function($defer, { xosansr, bricks }) {
|
||||
await this.checkXosanLicense({ srId: xosansr.uuid })
|
||||
|
||||
const xapi = this.getXapi(xosansr)
|
||||
@@ -1395,7 +1399,7 @@ removeBricks.params = {
|
||||
}
|
||||
removeBricks.resolve = { xosansr: ['sr', 'SR', 'administrate'] }
|
||||
|
||||
export function checkSrCurrentState ({ poolId }) {
|
||||
export function checkSrCurrentState({ poolId }) {
|
||||
return CURRENT_POOL_OPERATIONS[poolId]
|
||||
}
|
||||
|
||||
@@ -1455,7 +1459,7 @@ POSSIBLE_CONFIGURATIONS[16] = [
|
||||
{ layout: 'replica', redundancy: 2, capacity: 8 },
|
||||
]
|
||||
|
||||
function computeBrickSize (srs, brickSize = Infinity) {
|
||||
function computeBrickSize(srs, brickSize = Infinity) {
|
||||
const xapi = this.getXapi(srs[0])
|
||||
const srsObjects = map(srs, srId => xapi.getObject(srId))
|
||||
const srSizes = map(
|
||||
@@ -1468,7 +1472,7 @@ function computeBrickSize (srs, brickSize = Infinity) {
|
||||
)
|
||||
}
|
||||
|
||||
export async function computeXosanPossibleOptions ({
|
||||
export async function computeXosanPossibleOptions({
|
||||
lvmSrs,
|
||||
brickSize = Infinity,
|
||||
}) {
|
||||
@@ -1501,7 +1505,7 @@ computeXosanPossibleOptions.params = {
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
export async function unlock ({ licenseId, sr }) {
|
||||
export async function unlock({ licenseId, sr }) {
|
||||
await this.unlockXosanLicense({ licenseId, srId: sr.id })
|
||||
|
||||
const glusterEndpoint = this::_getGlusterEndpoint(sr.id)
|
||||
@@ -1528,7 +1532,7 @@ unlock.resolve = {
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
export async function downloadAndInstallXosanPack ({ id, version, pool }) {
|
||||
export async function downloadAndInstallXosanPack({ id, version, pool }) {
|
||||
if (!this.requestResource) {
|
||||
throw new Error('requestResource is not a function')
|
||||
}
|
||||
|
||||
@@ -78,19 +78,18 @@ export default class Redis extends Collection {
|
||||
.then(keys => keys.length !== 0 && redis.del(keys))
|
||||
).then(() =>
|
||||
asyncMap(redis.smembers(idsIndex), id =>
|
||||
redis.hgetall(`${prefix}:${id}`).then(
|
||||
values =>
|
||||
values == null
|
||||
? redis.srem(idsIndex, id) // entry no longer exists
|
||||
: asyncMap(indexes, index => {
|
||||
const value = values[index]
|
||||
if (value !== undefined) {
|
||||
return redis.sadd(
|
||||
`${prefix}_${index}:${String(value).toLowerCase()}`,
|
||||
id
|
||||
)
|
||||
}
|
||||
})
|
||||
redis.hgetall(`${prefix}:${id}`).then(values =>
|
||||
values == null
|
||||
? redis.srem(idsIndex, id) // entry no longer exists
|
||||
: asyncMap(indexes, index => {
|
||||
const value = values[index]
|
||||
if (value !== undefined) {
|
||||
return redis.sadd(
|
||||
`${prefix}_${index}:${String(value).toLowerCase()}`,
|
||||
id
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ const log = createLogger('xo:main')
|
||||
|
||||
const DEPRECATED_ENTRIES = ['users', 'servers']
|
||||
|
||||
async function loadConfiguration () {
|
||||
async function loadConfiguration() {
|
||||
const config = await appConf.load('xo-server', {
|
||||
appDir: joinPath(__dirname, '..'),
|
||||
ignoreUnknownFormats: true,
|
||||
@@ -79,7 +79,7 @@ async function loadConfiguration () {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function createExpressApp () {
|
||||
function createExpressApp() {
|
||||
const app = createExpress()
|
||||
|
||||
app.use(helmet())
|
||||
@@ -111,7 +111,7 @@ function createExpressApp () {
|
||||
return app
|
||||
}
|
||||
|
||||
async function setUpPassport (express, xo) {
|
||||
async function setUpPassport(express, xo) {
|
||||
const strategies = { __proto__: null }
|
||||
xo.registerPassportStrategy = strategy => {
|
||||
passport.use(strategy)
|
||||
@@ -214,7 +214,7 @@ async function setUpPassport (express, xo) {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
async function registerPlugin (pluginPath, pluginName) {
|
||||
async function registerPlugin(pluginPath, pluginName) {
|
||||
const plugin = require(pluginPath)
|
||||
const { description, version = 'unknown' } = (() => {
|
||||
try {
|
||||
@@ -257,7 +257,7 @@ async function registerPlugin (pluginPath, pluginName) {
|
||||
|
||||
const logPlugin = createLogger('xo:plugin')
|
||||
|
||||
function registerPluginWrapper (pluginPath, pluginName) {
|
||||
function registerPluginWrapper(pluginPath, pluginName) {
|
||||
logPlugin.info(`register ${pluginName}`)
|
||||
|
||||
return registerPlugin.call(this, pluginPath, pluginName).then(
|
||||
@@ -274,7 +274,7 @@ function registerPluginWrapper (pluginPath, pluginName) {
|
||||
const PLUGIN_PREFIX = 'xo-server-'
|
||||
const PLUGIN_PREFIX_LENGTH = PLUGIN_PREFIX.length
|
||||
|
||||
async function registerPluginsInPath (path) {
|
||||
async function registerPluginsInPath(path) {
|
||||
const files = await readdir(path).catch(error => {
|
||||
if (error.code === 'ENOENT') {
|
||||
return []
|
||||
@@ -295,7 +295,7 @@ async function registerPluginsInPath (path) {
|
||||
)
|
||||
}
|
||||
|
||||
async function registerPlugins (xo) {
|
||||
async function registerPlugins(xo) {
|
||||
await Promise.all(
|
||||
mapToArray(
|
||||
[`${__dirname}/../node_modules/`, '/usr/local/lib/node_modules/'],
|
||||
@@ -306,7 +306,7 @@ async function registerPlugins (xo) {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
async function makeWebServerListen (
|
||||
async function makeWebServerListen(
|
||||
webServer,
|
||||
{
|
||||
certificate,
|
||||
@@ -348,7 +348,7 @@ async function makeWebServerListen (
|
||||
}
|
||||
}
|
||||
|
||||
async function createWebServer ({ listen, listenOptions }) {
|
||||
async function createWebServer({ listen, listenOptions }) {
|
||||
const webServer = stoppable(new WebServer())
|
||||
|
||||
await Promise.all(
|
||||
@@ -433,8 +433,10 @@ const setUpStaticFiles = (express, opts) => {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const setUpApi = (webServer, xo, verboseLogsOnErrors) => {
|
||||
const setUpApi = (webServer, xo, config) => {
|
||||
const webSocketServer = new WebSocket.Server({
|
||||
...config.apiWebSocketOptions,
|
||||
|
||||
noServer: true,
|
||||
})
|
||||
xo.on('stop', () => pFromCallback(cb => webSocketServer.close(cb)))
|
||||
@@ -547,7 +549,7 @@ ${name} v${version}`)(require('../package.json'))
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default async function main (args) {
|
||||
export default async function main(args) {
|
||||
// makes sure the global Promise has not been changed by a lib
|
||||
assert(global.Promise === require('bluebird'))
|
||||
|
||||
@@ -640,7 +642,7 @@ export default async function main (args) {
|
||||
})
|
||||
|
||||
// Must be set up before the static files.
|
||||
setUpApi(webServer, xo, config.verboseApiLogsOnErrors)
|
||||
setUpApi(webServer, xo, config)
|
||||
|
||||
setUpProxies(express, config.http.proxies, xo)
|
||||
|
||||
|
||||
43
packages/xo-server/src/sensitive-values.js
Normal file
43
packages/xo-server/src/sensitive-values.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import mapValues from 'lodash/mapValues'
|
||||
|
||||
// this random value is used to obfuscate real data
|
||||
const OBFUSCATED_VALUE = 'q3oi6d9X8uenGvdLnHk2'
|
||||
|
||||
export const merge = (newValue, oldValue) => {
|
||||
if (newValue === OBFUSCATED_VALUE) {
|
||||
return oldValue
|
||||
}
|
||||
|
||||
let isArray
|
||||
|
||||
if (
|
||||
newValue === null ||
|
||||
oldValue === null ||
|
||||
typeof newValue !== 'object' ||
|
||||
typeof oldValue !== 'object' ||
|
||||
(isArray = Array.isArray(newValue)) !== Array.isArray(oldValue)
|
||||
) {
|
||||
return newValue
|
||||
}
|
||||
|
||||
const iteratee = (v, k) => merge(v, oldValue[k])
|
||||
return isArray ? newValue.map(iteratee) : mapValues(newValue, iteratee)
|
||||
}
|
||||
|
||||
export const obfuscate = value => replace(value, OBFUSCATED_VALUE)
|
||||
|
||||
export function replace (value, replacement) {
|
||||
function helper (value, name) {
|
||||
if (name === 'password' && typeof value === 'string') {
|
||||
return replacement
|
||||
}
|
||||
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return value
|
||||
}
|
||||
|
||||
return Array.isArray(value) ? value.map(helper) : mapValues(value, helper)
|
||||
}
|
||||
|
||||
return helper(value)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
|
||||
// $FlowFixMe
|
||||
import through2 from 'through2'
|
||||
import { type Readable } from 'stream'
|
||||
|
||||
|
||||
@@ -10,7 +10,12 @@ import {
|
||||
mapToArray,
|
||||
parseXml,
|
||||
} from './utils'
|
||||
import { isHostRunning, isVmHvm, isVmRunning, parseDateTime } from './xapi'
|
||||
import {
|
||||
getVmDomainType,
|
||||
isHostRunning,
|
||||
isVmRunning,
|
||||
parseDateTime,
|
||||
} from './xapi'
|
||||
import { useUpdateSystem } from './xapi/utils'
|
||||
|
||||
// ===================================================================
|
||||
@@ -218,14 +223,18 @@ const TRANSFORMS = {
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
vm (obj) {
|
||||
vm (obj, dependents) {
|
||||
dependents[obj.guest_metrics] = obj.$id
|
||||
dependents[obj.metrics] = obj.$id
|
||||
|
||||
const {
|
||||
$guest_metrics: guestMetrics,
|
||||
$metrics: metrics,
|
||||
other_config: otherConfig,
|
||||
} = obj
|
||||
|
||||
const isHvm = isVmHvm(obj)
|
||||
const domainType = getVmDomainType(obj)
|
||||
const isHvm = domainType === 'hvm'
|
||||
const isRunning = isVmRunning(obj)
|
||||
const xenTools = (() => {
|
||||
if (!isRunning || !metrics) {
|
||||
@@ -343,11 +352,7 @@ const TRANSFORMS = {
|
||||
startTime: metrics && toTimestamp(metrics.start_time),
|
||||
tags: obj.tags,
|
||||
VIFs: link(obj, 'VIFs'),
|
||||
virtualizationMode: isHvm
|
||||
? guestMetrics !== undefined && guestMetrics.PV_drivers_detected
|
||||
? 'pvhvm'
|
||||
: 'hvm'
|
||||
: 'pv',
|
||||
virtualizationMode: domainType,
|
||||
|
||||
// <=> Are the Xen Server tools installed?
|
||||
//
|
||||
@@ -739,13 +744,13 @@ const TRANSFORMS = {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default xapiObj => {
|
||||
export default function xapiObjectToXo (xapiObj, dependents) {
|
||||
const transform = TRANSFORMS[xapiObj.$type.toLowerCase()]
|
||||
if (!transform) {
|
||||
return
|
||||
}
|
||||
|
||||
const xoObj = transform(xapiObj)
|
||||
const xoObj = transform(xapiObj, dependents)
|
||||
if (!xoObj) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -99,8 +99,8 @@ const testMetric = (test, type) =>
|
||||
typeof test === 'string'
|
||||
? test === type
|
||||
: typeof test === 'function'
|
||||
? test(type)
|
||||
: test.exec(type)
|
||||
? test(type)
|
||||
: test.exec(type)
|
||||
|
||||
const findMetric = (metrics, metricType) => {
|
||||
let testResult
|
||||
@@ -321,8 +321,12 @@ export default class XapiStats {
|
||||
const hostUuid = host.uuid
|
||||
|
||||
if (
|
||||
!(
|
||||
vmUuid !== undefined &&
|
||||
get(this._statsByObject, [vmUuid, step]) === undefined
|
||||
) &&
|
||||
get(this._statsByObject, [hostUuid, step, 'localTimestamp']) + step >
|
||||
getCurrentTimestamp()
|
||||
getCurrentTimestamp()
|
||||
) {
|
||||
return this._getStats(hostUuid, step, vmUuid)
|
||||
}
|
||||
@@ -414,7 +418,7 @@ export default class XapiStats {
|
||||
})
|
||||
}
|
||||
|
||||
getVmStats (xapi, vmId, granularity) {
|
||||
async getVmStats (xapi, vmId, granularity) {
|
||||
const vm = xapi.getObject(vmId)
|
||||
const host = vm.$resident_on
|
||||
if (!host) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import createLogger from '@xen-orchestra/log'
|
||||
import deferrable from 'golike-defer'
|
||||
import fatfs from 'fatfs'
|
||||
import mixin from '@xen-orchestra/mixin'
|
||||
import ms from 'ms'
|
||||
import synchronized from 'decorator-synchronized'
|
||||
import tarStream from 'tar-stream'
|
||||
import vmdkToVhd from 'xo-vmdk-to-vhd'
|
||||
@@ -92,7 +93,7 @@ export const IPV6_CONFIG_MODES = ['None', 'DHCP', 'Static', 'Autoconf']
|
||||
|
||||
@mixin(mapToArray(mixins))
|
||||
export default class Xapi extends XapiBase {
|
||||
constructor (...args) {
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
|
||||
// Patch getObject to resolve _xapiId property.
|
||||
@@ -131,7 +132,7 @@ export default class Xapi extends XapiBase {
|
||||
this.objects.on('update', onAddOrUpdate)
|
||||
}
|
||||
|
||||
call (...args) {
|
||||
call(...args) {
|
||||
const fn = super.call
|
||||
|
||||
const loop = () =>
|
||||
@@ -145,13 +146,13 @@ export default class Xapi extends XapiBase {
|
||||
return loop()
|
||||
}
|
||||
|
||||
createTask (name = 'untitled task', description) {
|
||||
createTask(name = 'untitled task', description) {
|
||||
return super.createTask(`[XO] ${name}`, description)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
_registerGenericWatcher (fn) {
|
||||
_registerGenericWatcher(fn) {
|
||||
const watchers = this._genericWatchers
|
||||
const id = String(Math.random())
|
||||
|
||||
@@ -168,7 +169,7 @@ export default class Xapi extends XapiBase {
|
||||
// function.
|
||||
//
|
||||
// TODO: implements a timeout.
|
||||
_waitObject (predicate) {
|
||||
_waitObject(predicate) {
|
||||
if (isFunction(predicate)) {
|
||||
const { promise, resolve } = defer()
|
||||
|
||||
@@ -200,22 +201,22 @@ export default class Xapi extends XapiBase {
|
||||
// Wait for an object to be in a given state.
|
||||
//
|
||||
// Faster than _waitObject() with a function.
|
||||
_waitObjectState (idOrUuidOrRef, predicate) {
|
||||
_waitObjectState(idOrUuidOrRef, predicate) {
|
||||
const object = this.getObject(idOrUuidOrRef, null)
|
||||
if (object && predicate(object)) {
|
||||
return object
|
||||
}
|
||||
|
||||
const loop = () =>
|
||||
this._waitObject(idOrUuidOrRef).then(
|
||||
object => (predicate(object) ? object : loop())
|
||||
this._waitObject(idOrUuidOrRef).then(object =>
|
||||
predicate(object) ? object : loop()
|
||||
)
|
||||
|
||||
return loop()
|
||||
}
|
||||
|
||||
// Returns the objects if already presents or waits for it.
|
||||
async _getOrWaitObject (idOrUuidOrRef) {
|
||||
async _getOrWaitObject(idOrUuidOrRef) {
|
||||
return (
|
||||
this.getObject(idOrUuidOrRef, null) || this._waitObject(idOrUuidOrRef)
|
||||
)
|
||||
@@ -223,7 +224,7 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// =================================================================
|
||||
|
||||
_setObjectProperty (object, name, value) {
|
||||
_setObjectProperty(object, name, value) {
|
||||
return this.call(
|
||||
`${getNamespaceForType(object.$type)}.set_${camelToSnakeCase(name)}`,
|
||||
object.$ref,
|
||||
@@ -231,7 +232,7 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
_setObjectProperties (object, props) {
|
||||
_setObjectProperties(object, props) {
|
||||
const { $ref: ref, $type: type } = object
|
||||
|
||||
const namespace = getNamespaceForType(type)
|
||||
@@ -251,7 +252,7 @@ export default class Xapi extends XapiBase {
|
||||
)::ignoreErrors()
|
||||
}
|
||||
|
||||
async _updateObjectMapProperty (object, prop, values) {
|
||||
async _updateObjectMapProperty(object, prop, values) {
|
||||
const { $ref: ref, $type: type } = object
|
||||
|
||||
prop = camelToSnakeCase(prop)
|
||||
@@ -276,14 +277,14 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
async setHostProperties (id, { nameLabel, nameDescription }) {
|
||||
async setHostProperties(id, { nameLabel, nameDescription }) {
|
||||
await this._setObjectProperties(this.getObject(id), {
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
})
|
||||
}
|
||||
|
||||
async setPoolProperties ({ autoPoweron, nameLabel, nameDescription }) {
|
||||
async setPoolProperties({ autoPoweron, nameLabel, nameDescription }) {
|
||||
const { pool } = this
|
||||
|
||||
await Promise.all([
|
||||
@@ -298,14 +299,14 @@ export default class Xapi extends XapiBase {
|
||||
])
|
||||
}
|
||||
|
||||
async setSrProperties (id, { nameLabel, nameDescription }) {
|
||||
async setSrProperties(id, { nameLabel, nameDescription }) {
|
||||
await this._setObjectProperties(this.getObject(id), {
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
})
|
||||
}
|
||||
|
||||
async setNetworkProperties (
|
||||
async setNetworkProperties(
|
||||
id,
|
||||
{ nameLabel, nameDescription, defaultIsLocked }
|
||||
) {
|
||||
@@ -322,14 +323,14 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// =================================================================
|
||||
|
||||
async addTag (id, tag) {
|
||||
async addTag(id, tag) {
|
||||
const { $ref: ref, $type: type } = this.getObject(id)
|
||||
|
||||
const namespace = getNamespaceForType(type)
|
||||
await this.call(`${namespace}.add_tags`, ref, tag)
|
||||
}
|
||||
|
||||
async removeTag (id, tag) {
|
||||
async removeTag(id, tag) {
|
||||
const { $ref: ref, $type: type } = this.getObject(id)
|
||||
|
||||
const namespace = getNamespaceForType(type)
|
||||
@@ -338,7 +339,7 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// =================================================================
|
||||
|
||||
async setDefaultSr (srId) {
|
||||
async setDefaultSr(srId) {
|
||||
this._setObjectProperties(this.pool, {
|
||||
default_SR: this.getObject(srId).$ref,
|
||||
})
|
||||
@@ -346,13 +347,13 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// =================================================================
|
||||
|
||||
async setPoolMaster (hostId) {
|
||||
async setPoolMaster(hostId) {
|
||||
await this.call('pool.designate_new_master', this.getObject(hostId).$ref)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
async joinPool (masterAddress, masterUsername, masterPassword, force = false) {
|
||||
async joinPool(masterAddress, masterUsername, masterPassword, force = false) {
|
||||
await this.call(
|
||||
force ? 'pool.join_force' : 'pool.join',
|
||||
masterAddress,
|
||||
@@ -363,7 +364,7 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// =================================================================
|
||||
|
||||
async emergencyShutdownHost (hostId) {
|
||||
async emergencyShutdownHost(hostId) {
|
||||
const host = this.getObject(hostId)
|
||||
const vms = host.$resident_VMs
|
||||
log.debug(`Emergency shutdown: ${host.name_label}`)
|
||||
@@ -384,7 +385,7 @@ export default class Xapi extends XapiBase {
|
||||
//
|
||||
// If `force` is false and the evacuation failed, the host is re-
|
||||
// enabled and the error is thrown.
|
||||
async _clearHost ({ $ref: ref }, force) {
|
||||
async _clearHost({ $ref: ref }, force) {
|
||||
await this.call('host.disable', ref)
|
||||
|
||||
try {
|
||||
@@ -398,38 +399,38 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
}
|
||||
|
||||
async disableHost (hostId) {
|
||||
async disableHost(hostId) {
|
||||
await this.call('host.disable', this.getObject(hostId).$ref)
|
||||
}
|
||||
|
||||
async forgetHost (hostId) {
|
||||
async forgetHost(hostId) {
|
||||
await this.call('host.destroy', this.getObject(hostId).$ref)
|
||||
}
|
||||
|
||||
async ejectHostFromPool (hostId) {
|
||||
async ejectHostFromPool(hostId) {
|
||||
await this.call('pool.eject', this.getObject(hostId).$ref)
|
||||
}
|
||||
|
||||
async enableHost (hostId) {
|
||||
async enableHost(hostId) {
|
||||
await this.call('host.enable', this.getObject(hostId).$ref)
|
||||
}
|
||||
|
||||
async powerOnHost (hostId) {
|
||||
async powerOnHost(hostId) {
|
||||
await this.call('host.power_on', this.getObject(hostId).$ref)
|
||||
}
|
||||
|
||||
async rebootHost (hostId, force = false) {
|
||||
async rebootHost(hostId, force = false) {
|
||||
const host = this.getObject(hostId)
|
||||
|
||||
await this._clearHost(host, force)
|
||||
await this.call('host.reboot', host.$ref)
|
||||
}
|
||||
|
||||
async restartHostAgent (hostId) {
|
||||
async restartHostAgent(hostId) {
|
||||
await this.call('host.restart_agent', this.getObject(hostId).$ref)
|
||||
}
|
||||
|
||||
async setRemoteSyslogHost (hostId, syslogDestination) {
|
||||
async setRemoteSyslogHost(hostId, syslogDestination) {
|
||||
const host = this.getObject(hostId)
|
||||
await this.call('host.set_logging', host.$ref, {
|
||||
syslog_destination: syslogDestination,
|
||||
@@ -437,7 +438,7 @@ export default class Xapi extends XapiBase {
|
||||
await this.call('host.syslog_reconfigure', host.$ref)
|
||||
}
|
||||
|
||||
async shutdownHost (hostId, force = false) {
|
||||
async shutdownHost(hostId, force = false) {
|
||||
const host = this.getObject(hostId)
|
||||
|
||||
await this._clearHost(host, force)
|
||||
@@ -448,7 +449,7 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// Clone a VM: make a fast copy by fast copying each of its VDIs
|
||||
// (using snapshots where possible) on the same SRs.
|
||||
_cloneVm (vm, nameLabel = vm.name_label) {
|
||||
_cloneVm(vm, nameLabel = vm.name_label) {
|
||||
log.debug(
|
||||
`Cloning VM ${vm.name_label}${
|
||||
nameLabel !== vm.name_label ? ` as ${nameLabel}` : ''
|
||||
@@ -462,7 +463,7 @@ export default class Xapi extends XapiBase {
|
||||
//
|
||||
// If a SR is specified, it will contains the copies of the VDIs,
|
||||
// otherwise they will use the SRs they are on.
|
||||
async _copyVm (vm, nameLabel = vm.name_label, sr = undefined) {
|
||||
async _copyVm(vm, nameLabel = vm.name_label, sr = undefined) {
|
||||
let snapshot
|
||||
if (isVmRunning(vm)) {
|
||||
snapshot = await this._snapshotVm(vm)
|
||||
@@ -488,7 +489,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
}
|
||||
|
||||
async cloneVm (vmId, { nameLabel = undefined, fast = true } = {}) {
|
||||
async cloneVm(vmId, { nameLabel = undefined, fast = true } = {}) {
|
||||
const vm = this.getObject(vmId)
|
||||
|
||||
const cloneRef = await (fast
|
||||
@@ -498,13 +499,13 @@ export default class Xapi extends XapiBase {
|
||||
return /* await */ this._getOrWaitObject(cloneRef)
|
||||
}
|
||||
|
||||
async copyVm (vmId, srId, { nameLabel = undefined } = {}) {
|
||||
async copyVm(vmId, srId, { nameLabel = undefined } = {}) {
|
||||
return /* await */ this._getOrWaitObject(
|
||||
await this._copyVm(this.getObject(vmId), nameLabel, this.getObject(srId))
|
||||
)
|
||||
}
|
||||
|
||||
async remoteCopyVm (
|
||||
async remoteCopyVm(
|
||||
vmId,
|
||||
targetXapi,
|
||||
targetSrId,
|
||||
@@ -544,7 +545,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
// Low level create VM.
|
||||
_createVmRecord ({
|
||||
_createVmRecord({
|
||||
actions_after_crash,
|
||||
actions_after_reboot,
|
||||
actions_after_shutdown,
|
||||
@@ -644,7 +645,7 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
async _deleteVm (
|
||||
async _deleteVm(
|
||||
vm,
|
||||
deleteDisks = true,
|
||||
force = false,
|
||||
@@ -657,23 +658,32 @@ export default class Xapi extends XapiBase {
|
||||
// ensure the vm record is up-to-date
|
||||
vm = await this.barrier($ref)
|
||||
|
||||
if (!force && 'destroy' in vm.blocked_operations) {
|
||||
throw forbiddenOperation('destroy', vm.blocked_operations.destroy.reason)
|
||||
}
|
||||
|
||||
if (
|
||||
!forceDeleteDefaultTemplate &&
|
||||
vm.other_config.default_template === 'true'
|
||||
) {
|
||||
throw forbiddenOperation('destroy', 'VM is default template')
|
||||
}
|
||||
|
||||
// It is necessary for suspended VMs to be shut down
|
||||
// to be able to delete their VDIs.
|
||||
if (vm.power_state !== 'Halted') {
|
||||
await this.call('VM.hard_shutdown', $ref)
|
||||
}
|
||||
|
||||
if (force) {
|
||||
await this._updateObjectMapProperty(vm, 'blocked_operations', {
|
||||
await Promise.all([
|
||||
this.call('VM.set_is_a_template', vm.$ref, false),
|
||||
this._updateObjectMapProperty(vm, 'blocked_operations', {
|
||||
destroy: null,
|
||||
})
|
||||
}
|
||||
|
||||
if (forceDeleteDefaultTemplate) {
|
||||
await this._updateObjectMapProperty(vm, 'other_config', {
|
||||
}),
|
||||
this._updateObjectMapProperty(vm, 'other_config', {
|
||||
default_template: null,
|
||||
})
|
||||
}
|
||||
}),
|
||||
])
|
||||
|
||||
// must be done before destroying the VM
|
||||
const disks = getVmDisks(vm)
|
||||
@@ -720,7 +730,7 @@ export default class Xapi extends XapiBase {
|
||||
])
|
||||
}
|
||||
|
||||
async deleteVm (vmId, deleteDisks, force, forceDeleteDefaultTemplate) {
|
||||
async deleteVm(vmId, deleteDisks, force, forceDeleteDefaultTemplate) {
|
||||
return /* await */ this._deleteVm(
|
||||
this.getObject(vmId),
|
||||
deleteDisks,
|
||||
@@ -729,7 +739,7 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
getVmConsole (vmId) {
|
||||
getVmConsole(vmId) {
|
||||
const vm = this.getObject(vmId)
|
||||
|
||||
const console = find(vm.$consoles, { protocol: 'rfb' })
|
||||
@@ -743,7 +753,7 @@ export default class Xapi extends XapiBase {
|
||||
// Returns a stream to the exported VM.
|
||||
@concurrency(2, stream => stream.then(stream => fromEvent(stream, 'end')))
|
||||
@cancelable
|
||||
async exportVm ($cancelToken, vmId, { compress = true } = {}) {
|
||||
async exportVm($cancelToken, vmId, { compress = true } = {}) {
|
||||
const vm = this.getObject(vmId)
|
||||
const useSnapshot = isVmRunning(vm)
|
||||
const exportedVm = useSnapshot
|
||||
@@ -772,7 +782,7 @@ export default class Xapi extends XapiBase {
|
||||
return promise
|
||||
}
|
||||
|
||||
_assertHealthyVdiChain (vdi, cache) {
|
||||
_assertHealthyVdiChain(vdi, cache) {
|
||||
if (vdi == null) {
|
||||
return
|
||||
}
|
||||
@@ -804,7 +814,7 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
_assertHealthyVdiChains (vm) {
|
||||
_assertHealthyVdiChains(vm) {
|
||||
const cache = { __proto__: null }
|
||||
forEach(vm.$VBDs, ({ $VDI }) => {
|
||||
this._assertHealthyVdiChain($VDI, cache)
|
||||
@@ -815,7 +825,7 @@ export default class Xapi extends XapiBase {
|
||||
// object.
|
||||
@cancelable
|
||||
@deferrable
|
||||
async exportDeltaVm (
|
||||
async exportDeltaVm(
|
||||
$defer,
|
||||
$cancelToken,
|
||||
vmId: string,
|
||||
@@ -876,7 +886,7 @@ export default class Xapi extends XapiBase {
|
||||
//
|
||||
// The snapshot must not exist otherwise it could break the
|
||||
// next export.
|
||||
;this._deleteVdi(vdi)::ignoreErrors()
|
||||
this._deleteVdi(vdi)::ignoreErrors()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -946,7 +956,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
@deferrable
|
||||
async importDeltaVm (
|
||||
async importDeltaVm(
|
||||
$defer,
|
||||
delta: DeltaVmExport,
|
||||
{
|
||||
@@ -1127,7 +1137,7 @@ export default class Xapi extends XapiBase {
|
||||
])
|
||||
|
||||
if (deleteBase && baseVm) {
|
||||
;this._deleteVm(baseVm)::ignoreErrors()
|
||||
this._deleteVm(baseVm)::ignoreErrors()
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
@@ -1145,7 +1155,7 @@ export default class Xapi extends XapiBase {
|
||||
return { transferSize, vm }
|
||||
}
|
||||
|
||||
async _migrateVmWithStorageMotion (
|
||||
async _migrateVmWithStorageMotion(
|
||||
vm,
|
||||
hostXapi,
|
||||
host,
|
||||
@@ -1167,8 +1177,8 @@ export default class Xapi extends XapiBase {
|
||||
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.
|
||||
? hostXapi.getObject(sr).$ref
|
||||
: defaultSr.$ref // Will error if there are no default SR.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1211,7 +1221,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
@synchronized
|
||||
_callInstallationPlugin (hostRef, vdi) {
|
||||
_callInstallationPlugin(hostRef, vdi) {
|
||||
return this.call(
|
||||
'host.call_plugin',
|
||||
hostRef,
|
||||
@@ -1227,7 +1237,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
@deferrable
|
||||
async installSupplementalPack ($defer, stream, { hostId }) {
|
||||
async installSupplementalPack($defer, stream, { hostId }) {
|
||||
if (!stream.length) {
|
||||
throw new Error('stream must have a length')
|
||||
}
|
||||
@@ -1244,7 +1254,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
@deferrable
|
||||
async installSupplementalPackOnAllHosts ($defer, stream) {
|
||||
async installSupplementalPackOnAllHosts($defer, stream) {
|
||||
if (!stream.length) {
|
||||
throw new Error('stream must have a length')
|
||||
}
|
||||
@@ -1306,7 +1316,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
@cancelable
|
||||
async _importVm ($cancelToken, stream, sr, onVmCreation = undefined) {
|
||||
async _importVm($cancelToken, stream, sr, onVmCreation = undefined) {
|
||||
const taskRef = await this.createTask('VM import')
|
||||
const query = {}
|
||||
|
||||
@@ -1315,7 +1325,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
if (onVmCreation != null) {
|
||||
;this._waitObject(
|
||||
this._waitObject(
|
||||
obj =>
|
||||
obj != null &&
|
||||
obj.current_operations != null &&
|
||||
@@ -1340,7 +1350,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
@deferrable
|
||||
async _importOvaVm (
|
||||
async _importOvaVm(
|
||||
$defer,
|
||||
stream,
|
||||
{ descriptionLabel, disks, memory, nameLabel, networks, nCpus, tables },
|
||||
@@ -1436,7 +1446,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
// TODO: an XVA can contain multiple VMs
|
||||
async importVm (stream, { data, srId, type = 'xva' } = {}) {
|
||||
async importVm(stream, { data, srId, type = 'xva' } = {}) {
|
||||
const sr = srId && this.getObject(srId)
|
||||
|
||||
if (type === 'xva') {
|
||||
@@ -1450,7 +1460,7 @@ export default class Xapi extends XapiBase {
|
||||
throw new Error(`unsupported type: '${type}'`)
|
||||
}
|
||||
|
||||
async migrateVm (
|
||||
async migrateVm(
|
||||
vmId,
|
||||
hostXapi,
|
||||
hostId,
|
||||
@@ -1493,7 +1503,7 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
@concurrency(2)
|
||||
@cancelable
|
||||
async _snapshotVm ($cancelToken, vm, nameLabel = vm.name_label) {
|
||||
async _snapshotVm($cancelToken, vm, nameLabel = vm.name_label) {
|
||||
log.debug(
|
||||
`Snapshotting VM ${vm.name_label}${
|
||||
nameLabel !== vm.name_label ? ` as ${nameLabel}` : ''
|
||||
@@ -1545,17 +1555,17 @@ export default class Xapi extends XapiBase {
|
||||
return snapshot
|
||||
}
|
||||
|
||||
async snapshotVm (vmId, nameLabel = undefined) {
|
||||
async snapshotVm(vmId, nameLabel = undefined) {
|
||||
return /* await */ this._snapshotVm(this.getObject(vmId), nameLabel)
|
||||
}
|
||||
|
||||
async setVcpuWeight (vmId, weight) {
|
||||
async setVcpuWeight(vmId, weight) {
|
||||
weight = weight || null // Take all falsy values as a removal (0 included)
|
||||
const vm = this.getObject(vmId)
|
||||
await this._updateObjectMapProperty(vm, 'VCPUs_params', { weight })
|
||||
}
|
||||
|
||||
async _startVm (vm, host, force) {
|
||||
async _startVm(vm, host, force) {
|
||||
log.debug(`Starting VM ${vm.name_label}`)
|
||||
|
||||
if (force) {
|
||||
@@ -1574,7 +1584,7 @@ export default class Xapi extends XapiBase {
|
||||
: this.call('VM.start_on', vm.$ref, host.$ref, false, false)
|
||||
}
|
||||
|
||||
async startVm (vmId, hostId, force) {
|
||||
async startVm(vmId, hostId, force) {
|
||||
try {
|
||||
await this._startVm(
|
||||
this.getObject(vmId),
|
||||
@@ -1586,13 +1596,15 @@ export default class Xapi extends XapiBase {
|
||||
throw forbiddenOperation('Start', e.params[1])
|
||||
}
|
||||
if (e.code === 'VM_BAD_POWER_STATE') {
|
||||
return this.resumeVm(vmId)
|
||||
return e.params[2] === 'paused'
|
||||
? this.unpauseVm(vmId)
|
||||
: this.resumeVm(vmId)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async startVmOnCd (vmId) {
|
||||
async startVmOnCd(vmId) {
|
||||
const vm = this.getObject(vmId)
|
||||
|
||||
if (isVmHvm(vm)) {
|
||||
@@ -1653,19 +1665,19 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
await this._startVm(vm)
|
||||
} finally {
|
||||
;this._setObjectProperties(vm, {
|
||||
this._setObjectProperties(vm, {
|
||||
PV_bootloader: bootloader,
|
||||
})::ignoreErrors()
|
||||
|
||||
forEach(bootables, ([vbd, bootable]) => {
|
||||
;this._setObjectProperties(vbd, { bootable })::ignoreErrors()
|
||||
this._setObjectProperties(vbd, { bootable })::ignoreErrors()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// vm_operations: http://xapi-project.github.io/xen-api/classes/vm.html
|
||||
async addForbiddenOperationToVm (vmId, operation, reason) {
|
||||
async addForbiddenOperationToVm(vmId, operation, reason) {
|
||||
await this.call(
|
||||
'VM.add_to_blocked_operations',
|
||||
this.getObject(vmId).$ref,
|
||||
@@ -1674,7 +1686,7 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
async removeForbiddenOperationFromVm (vmId, operation) {
|
||||
async removeForbiddenOperationFromVm(vmId, operation) {
|
||||
await this.call(
|
||||
'VM.remove_from_blocked_operations',
|
||||
this.getObject(vmId).$ref,
|
||||
@@ -1684,7 +1696,7 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// =================================================================
|
||||
|
||||
async createVbd ({
|
||||
async createVbd({
|
||||
bootable = false,
|
||||
other_config = {},
|
||||
qos_algorithm_params = {},
|
||||
@@ -1746,13 +1758,13 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
}
|
||||
|
||||
_cloneVdi (vdi) {
|
||||
_cloneVdi(vdi) {
|
||||
log.debug(`Cloning VDI ${vdi.name_label}`)
|
||||
|
||||
return this.call('VDI.clone', vdi.$ref)
|
||||
}
|
||||
|
||||
async createVdi ({
|
||||
async createVdi({
|
||||
name_description,
|
||||
name_label,
|
||||
other_config = {},
|
||||
@@ -1788,7 +1800,7 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
async moveVdi (vdiId, srId) {
|
||||
async moveVdi(vdiId, srId) {
|
||||
const vdi = this.getObject(vdiId)
|
||||
const sr = this.getObject(srId)
|
||||
|
||||
@@ -1828,13 +1840,13 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
// TODO: check whether the VDI is attached.
|
||||
async _deleteVdi (vdi) {
|
||||
async _deleteVdi(vdi) {
|
||||
log.debug(`Deleting VDI ${vdi.name_label}`)
|
||||
|
||||
await this.call('VDI.destroy', vdi.$ref)
|
||||
}
|
||||
|
||||
_resizeVdi (vdi, size) {
|
||||
_resizeVdi(vdi, size) {
|
||||
log.debug(
|
||||
`Resizing VDI ${vdi.name_label} from ${vdi.virtual_size} to ${size}`
|
||||
)
|
||||
@@ -1842,7 +1854,7 @@ export default class Xapi extends XapiBase {
|
||||
return this.call('VDI.resize', vdi.$ref, size)
|
||||
}
|
||||
|
||||
_getVmCdDrive (vm) {
|
||||
_getVmCdDrive(vm) {
|
||||
for (const vbd of vm.$VBDs) {
|
||||
if (vbd.type === 'CD') {
|
||||
return vbd
|
||||
@@ -1850,14 +1862,14 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
}
|
||||
|
||||
async _ejectCdFromVm (vm) {
|
||||
async _ejectCdFromVm(vm) {
|
||||
const cdDrive = this._getVmCdDrive(vm)
|
||||
if (cdDrive) {
|
||||
await this.call('VBD.eject', cdDrive.$ref)
|
||||
}
|
||||
}
|
||||
|
||||
async _insertCdIntoVm (cd, vm, { bootable = false, force = false } = {}) {
|
||||
async _insertCdIntoVm(cd, vm, { bootable = false, force = false } = {}) {
|
||||
const cdDrive = await this._getVmCdDrive(vm)
|
||||
if (cdDrive) {
|
||||
try {
|
||||
@@ -1886,11 +1898,11 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
}
|
||||
|
||||
async connectVbd (vbdId) {
|
||||
async connectVbd(vbdId) {
|
||||
await this.call('VBD.plug', vbdId)
|
||||
}
|
||||
|
||||
async _disconnectVbd (vbd) {
|
||||
async _disconnectVbd(vbd) {
|
||||
// TODO: check if VBD is attached before
|
||||
try {
|
||||
await this.call('VBD.unplug_force', vbd.$ref)
|
||||
@@ -1902,21 +1914,21 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
}
|
||||
|
||||
async disconnectVbd (vbdId) {
|
||||
async disconnectVbd(vbdId) {
|
||||
await this._disconnectVbd(this.getObject(vbdId))
|
||||
}
|
||||
|
||||
async _deleteVbd (vbd) {
|
||||
async _deleteVbd(vbd) {
|
||||
await this._disconnectVbd(vbd)::ignoreErrors()
|
||||
await this.call('VBD.destroy', vbd.$ref)
|
||||
}
|
||||
|
||||
deleteVbd (vbdId) {
|
||||
deleteVbd(vbdId) {
|
||||
return this._deleteVbd(this.getObject(vbdId))
|
||||
}
|
||||
|
||||
// TODO: remove when no longer used.
|
||||
async destroyVbdsFromVm (vmId) {
|
||||
async destroyVbdsFromVm(vmId) {
|
||||
await Promise.all(
|
||||
mapToArray(this.getObject(vmId).$VBDs, async vbd => {
|
||||
await this.disconnectVbd(vbd.$ref)::ignoreErrors()
|
||||
@@ -1925,25 +1937,25 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
async deleteVdi (vdiId) {
|
||||
async deleteVdi(vdiId) {
|
||||
await this._deleteVdi(this.getObject(vdiId))
|
||||
}
|
||||
|
||||
async resizeVdi (vdiId, size) {
|
||||
async resizeVdi(vdiId, size) {
|
||||
await this._resizeVdi(this.getObject(vdiId), size)
|
||||
}
|
||||
|
||||
async ejectCdFromVm (vmId) {
|
||||
async ejectCdFromVm(vmId) {
|
||||
await this._ejectCdFromVm(this.getObject(vmId))
|
||||
}
|
||||
|
||||
async insertCdIntoVm (cdId, vmId, opts = undefined) {
|
||||
async insertCdIntoVm(cdId, vmId, opts = undefined) {
|
||||
await this._insertCdIntoVm(this.getObject(cdId), this.getObject(vmId), opts)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async snapshotVdi (vdiId, nameLabel) {
|
||||
async snapshotVdi(vdiId, nameLabel) {
|
||||
const vdi = this.getObject(vdiId)
|
||||
|
||||
const snap = await this._getOrWaitObject(
|
||||
@@ -1959,7 +1971,7 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
@concurrency(12, stream => stream.then(stream => fromEvent(stream, 'end')))
|
||||
@cancelable
|
||||
_exportVdi ($cancelToken, vdi, base, format = VDI_FORMAT_VHD) {
|
||||
_exportVdi($cancelToken, vdi, base, format = VDI_FORMAT_VHD) {
|
||||
const query = {
|
||||
format,
|
||||
vdi: vdi.$ref,
|
||||
@@ -1988,13 +2000,18 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
@cancelable
|
||||
exportVdiContent ($cancelToken, vdi, { format } = {}) {
|
||||
exportVdiContent($cancelToken, vdi, { format } = {}) {
|
||||
return this._exportVdi($cancelToken, this.getObject(vdi), undefined, format)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async _importVdiContent (vdi, body, format = VDI_FORMAT_VHD) {
|
||||
async _importVdiContent(vdi, body, format = VDI_FORMAT_VHD) {
|
||||
if (__DEV__ && body.length == null) {
|
||||
throw new Error(
|
||||
'Trying to import a VDI without a length field. Please report this error to Xen Orchestra.'
|
||||
)
|
||||
}
|
||||
await Promise.all([
|
||||
body.task,
|
||||
body.checksumVerified,
|
||||
@@ -2015,13 +2032,13 @@ export default class Xapi extends XapiBase {
|
||||
})
|
||||
}
|
||||
|
||||
importVdiContent (vdiId, body, { format } = {}) {
|
||||
importVdiContent(vdiId, body, { format } = {}) {
|
||||
return this._importVdiContent(this.getObject(vdiId), body, format)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
async _createVif (
|
||||
async _createVif(
|
||||
vm,
|
||||
network,
|
||||
{
|
||||
@@ -2071,7 +2088,7 @@ export default class Xapi extends XapiBase {
|
||||
return vifRef
|
||||
}
|
||||
|
||||
async createVif (vmId, networkId, opts = undefined) {
|
||||
async createVif(vmId, networkId, opts = undefined) {
|
||||
return /* await */ this._getOrWaitObject(
|
||||
await this._createVif(
|
||||
this.getObject(vmId),
|
||||
@@ -2081,7 +2098,7 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
@deferrable
|
||||
async createNetwork (
|
||||
async createNetwork(
|
||||
$defer,
|
||||
{ name, description = 'Created with Xen Orchestra', pifId, mtu, vlan }
|
||||
) {
|
||||
@@ -2106,7 +2123,7 @@ export default class Xapi extends XapiBase {
|
||||
return this._getOrWaitObject(networkRef)
|
||||
}
|
||||
|
||||
async editPif (pifId, { vlan }) {
|
||||
async editPif(pifId, { vlan }) {
|
||||
const pif = this.getObject(pifId)
|
||||
const physPif = find(
|
||||
this.objects.all,
|
||||
@@ -2153,7 +2170,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
@deferrable
|
||||
async createBondedNetwork ($defer, { bondMode, mac = '', pifIds, ...params }) {
|
||||
async createBondedNetwork($defer, { bondMode, mac = '', pifIds, ...params }) {
|
||||
const network = await this.createNetwork(params)
|
||||
$defer.onFailure(() => this.deleteNetwork(network))
|
||||
// TODO: test and confirm:
|
||||
@@ -2170,7 +2187,7 @@ export default class Xapi extends XapiBase {
|
||||
return network
|
||||
}
|
||||
|
||||
async deleteNetwork (networkId) {
|
||||
async deleteNetwork(networkId) {
|
||||
const network = this.getObject(networkId)
|
||||
const pifs = network.$PIFs
|
||||
|
||||
@@ -2192,7 +2209,7 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// =================================================================
|
||||
|
||||
async _doDockerAction (vmId, action, containerId) {
|
||||
async _doDockerAction(vmId, action, containerId) {
|
||||
const vm = this.getObject(vmId)
|
||||
const host = vm.$resident_on || this.pool.$master
|
||||
|
||||
@@ -2208,35 +2225,35 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
async registerDockerContainer (vmId) {
|
||||
async registerDockerContainer(vmId) {
|
||||
await this._doDockerAction(vmId, 'register')
|
||||
}
|
||||
|
||||
async deregisterDockerContainer (vmId) {
|
||||
async deregisterDockerContainer(vmId) {
|
||||
await this._doDockerAction(vmId, 'deregister')
|
||||
}
|
||||
|
||||
async startDockerContainer (vmId, containerId) {
|
||||
async startDockerContainer(vmId, containerId) {
|
||||
await this._doDockerAction(vmId, 'start', containerId)
|
||||
}
|
||||
|
||||
async stopDockerContainer (vmId, containerId) {
|
||||
async stopDockerContainer(vmId, containerId) {
|
||||
await this._doDockerAction(vmId, 'stop', containerId)
|
||||
}
|
||||
|
||||
async restartDockerContainer (vmId, containerId) {
|
||||
async restartDockerContainer(vmId, containerId) {
|
||||
await this._doDockerAction(vmId, 'restart', containerId)
|
||||
}
|
||||
|
||||
async pauseDockerContainer (vmId, containerId) {
|
||||
async pauseDockerContainer(vmId, containerId) {
|
||||
await this._doDockerAction(vmId, 'pause', containerId)
|
||||
}
|
||||
|
||||
async unpauseDockerContainer (vmId, containerId) {
|
||||
async unpauseDockerContainer(vmId, containerId) {
|
||||
await this._doDockerAction(vmId, 'unpause', containerId)
|
||||
}
|
||||
|
||||
async getCloudInitConfig (templateId) {
|
||||
async getCloudInitConfig(templateId) {
|
||||
const template = this.getObject(templateId)
|
||||
const host = this.pool.$master
|
||||
|
||||
@@ -2253,7 +2270,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
// Specific CoreOS Config Drive
|
||||
async createCoreOsCloudInitConfigDrive (vmId, srId, config) {
|
||||
async createCoreOsCloudInitConfigDrive(vmId, srId, config) {
|
||||
const vm = this.getObject(vmId)
|
||||
const host = this.pool.$master
|
||||
const sr = this.getObject(srId)
|
||||
@@ -2274,7 +2291,7 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// Generic Config Drive
|
||||
@deferrable
|
||||
async createCloudInitConfigDrive ($defer, vmId, srId, config) {
|
||||
async createCloudInitConfigDrive($defer, vmId, srId, config) {
|
||||
const vm = this.getObject(vmId)
|
||||
const sr = this.getObject(srId)
|
||||
|
||||
@@ -2310,7 +2327,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
@deferrable
|
||||
async createTemporaryVdiOnSr (
|
||||
async createTemporaryVdiOnSr(
|
||||
$defer,
|
||||
stream,
|
||||
sr,
|
||||
@@ -2331,7 +2348,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
// Create VDI on an adequate local SR
|
||||
async createTemporaryVdiOnHost (stream, hostId, name_label, name_description) {
|
||||
async createTemporaryVdiOnHost(stream, hostId, name_label, name_description) {
|
||||
const pbd = find(this.getObject(hostId).$PBDs, pbd =>
|
||||
canSrHaveNewVdiOfSize(pbd.$SR, stream.length)
|
||||
)
|
||||
@@ -2348,7 +2365,7 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
findAvailableSharedSr (minSize) {
|
||||
findAvailableSharedSr(minSize) {
|
||||
return find(
|
||||
this.objects.all,
|
||||
obj =>
|
||||
@@ -2356,16 +2373,15 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
async _assertConsistentHostServerTime (hostRef) {
|
||||
if (
|
||||
Math.abs(
|
||||
parseDateTime(
|
||||
await this.call('host.get_servertime', hostRef)
|
||||
).getTime() - Date.now()
|
||||
) > 2e3
|
||||
) {
|
||||
async _assertConsistentHostServerTime(hostRef) {
|
||||
const delta =
|
||||
parseDateTime(await this.call('host.get_servertime', hostRef)).getTime() -
|
||||
Date.now()
|
||||
if (Math.abs(delta) > 30e3) {
|
||||
throw new Error(
|
||||
'host server time and XOA date are not consistent with each other'
|
||||
`host server time and XOA date are not consistent with each other (${ms(
|
||||
delta
|
||||
)})`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,7 +462,9 @@ export default {
|
||||
async _installAllPoolPatchesOnHost (host) {
|
||||
const installableByUuid =
|
||||
host.license_params.sku_type !== 'free'
|
||||
? await this._listMissingPoolPatchesOnHost(host)
|
||||
? pickBy(await this._listMissingPoolPatchesOnHost(host), {
|
||||
upgrade: false,
|
||||
})
|
||||
: pickBy(await this._listMissingPoolPatchesOnHost(host), {
|
||||
paid: false,
|
||||
upgrade: false,
|
||||
@@ -509,11 +511,10 @@ export default {
|
||||
...(await Promise.all(
|
||||
mapFilter(this.objects.all, host => {
|
||||
if (host.$type === 'host') {
|
||||
return this._listMissingPoolPatchesOnHost(host).then(
|
||||
patches =>
|
||||
host.license_params.sku_type !== 'free'
|
||||
? patches
|
||||
: pickBy(patches, { paid: false, upgrade: false })
|
||||
return this._listMissingPoolPatchesOnHost(host).then(patches =>
|
||||
host.license_params.sku_type !== 'free'
|
||||
? pickBy(patches, { upgrade: false })
|
||||
: pickBy(patches, { paid: false, upgrade: false })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ const XEN_VIDEORAM_VALUES = [1, 2, 4, 8, 16]
|
||||
export default {
|
||||
// TODO: clean up on error.
|
||||
@deferrable
|
||||
async createVm (
|
||||
async createVm(
|
||||
$defer,
|
||||
templateId,
|
||||
{
|
||||
@@ -234,7 +234,7 @@ export default {
|
||||
_editVm: makeEditObject({
|
||||
affinityHost: {
|
||||
get: 'affinity',
|
||||
set (value, vm) {
|
||||
set(value, vm) {
|
||||
return this._setObjectProperty(
|
||||
vm,
|
||||
'affinity',
|
||||
@@ -244,7 +244,7 @@ export default {
|
||||
},
|
||||
|
||||
autoPoweron: {
|
||||
set (value, vm) {
|
||||
set(value, vm) {
|
||||
return Promise.all([
|
||||
this._updateObjectMapProperty(vm, 'other_config', {
|
||||
autoPoweron: value ? 'true' : null,
|
||||
@@ -257,8 +257,24 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
virtualizationMode: {
|
||||
set(virtualizationMode, vm) {
|
||||
if (virtualizationMode !== 'pv' && virtualizationMode !== 'hvm') {
|
||||
throw new Error(`The virtualization mode must be 'pv' or 'hvm'`)
|
||||
}
|
||||
return this._set('domain_type', virtualizationMode)::pCatch(
|
||||
{ code: 'MESSAGE_METHOD_UNKNOWN' },
|
||||
() =>
|
||||
this._set(
|
||||
'HVM_boot_policy',
|
||||
virtualizationMode === 'hvm' ? 'Boot order' : ''
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
coresPerSocket: {
|
||||
set (coresPerSocket, vm) {
|
||||
set(coresPerSocket, vm) {
|
||||
return this._updateObjectMapProperty(vm, 'platform', {
|
||||
'cores-per-socket': coresPerSocket,
|
||||
})
|
||||
@@ -280,7 +296,7 @@ export default {
|
||||
get: vm => +vm.VCPUs_at_startup,
|
||||
set: [
|
||||
'VCPUs_at_startup',
|
||||
function (value, vm) {
|
||||
function(value, vm) {
|
||||
return isVmRunning(vm) && this._set('VCPUs_number_live', value)
|
||||
},
|
||||
],
|
||||
@@ -288,7 +304,7 @@ export default {
|
||||
|
||||
cpuCap: {
|
||||
get: vm => vm.VCPUs_params.cap && +vm.VCPUs_params.cap,
|
||||
set (cap, vm) {
|
||||
set(cap, vm) {
|
||||
return this._updateObjectMapProperty(vm, 'VCPUs_params', { cap })
|
||||
},
|
||||
},
|
||||
@@ -304,13 +320,13 @@ export default {
|
||||
|
||||
cpuWeight: {
|
||||
get: vm => vm.VCPUs_params.weight && +vm.VCPUs_params.weight,
|
||||
set (weight, vm) {
|
||||
set(weight, vm) {
|
||||
return this._updateObjectMapProperty(vm, 'VCPUs_params', { weight })
|
||||
},
|
||||
},
|
||||
|
||||
highAvailability: {
|
||||
set (ha, vm) {
|
||||
set(ha, vm) {
|
||||
return this.call('VM.set_ha_restart_priority', vm.$ref, ha)
|
||||
},
|
||||
},
|
||||
@@ -330,7 +346,7 @@ export default {
|
||||
limitName: 'memory',
|
||||
get: vm => +vm.memory_dynamic_max,
|
||||
preprocess: parseSize,
|
||||
set (dynamicMax, vm) {
|
||||
set(dynamicMax, vm) {
|
||||
const { $ref } = vm
|
||||
const dynamicMin = Math.min(vm.memory_dynamic_min, dynamicMax)
|
||||
|
||||
@@ -384,7 +400,7 @@ export default {
|
||||
hasVendorDevice: true,
|
||||
|
||||
expNestedHvm: {
|
||||
set (expNestedHvm, vm) {
|
||||
set(expNestedHvm, vm) {
|
||||
return this._updateObjectMapProperty(vm, 'platform', {
|
||||
'exp-nested-hvm': expNestedHvm ? 'true' : null,
|
||||
})
|
||||
@@ -392,7 +408,7 @@ export default {
|
||||
},
|
||||
|
||||
nicType: {
|
||||
set (nicType, vm) {
|
||||
set(nicType, vm) {
|
||||
return this._updateObjectMapProperty(vm, 'platform', {
|
||||
nic_type: nicType,
|
||||
})
|
||||
@@ -400,7 +416,7 @@ export default {
|
||||
},
|
||||
|
||||
vga: {
|
||||
set (vga, vm) {
|
||||
set(vga, vm) {
|
||||
if (!includes(XEN_VGA_VALUES, vga)) {
|
||||
throw new Error(
|
||||
`The different values that the VGA can take are: ${XEN_VGA_VALUES}`
|
||||
@@ -411,7 +427,7 @@ export default {
|
||||
},
|
||||
|
||||
videoram: {
|
||||
set (videoram, vm) {
|
||||
set(videoram, vm) {
|
||||
if (!includes(XEN_VIDEORAM_VALUES, videoram)) {
|
||||
throw new Error(
|
||||
`The different values that the video RAM can take are: ${XEN_VIDEORAM_VALUES}`
|
||||
@@ -422,32 +438,36 @@ export default {
|
||||
},
|
||||
}),
|
||||
|
||||
async editVm (id, props, checkLimits) {
|
||||
async editVm(id, props, checkLimits) {
|
||||
return /* await */ this._editVm(this.getObject(id), props, checkLimits)
|
||||
},
|
||||
|
||||
async revertVm (snapshotId, snapshotBefore = true) {
|
||||
async revertVm(snapshotId, snapshotBefore = true) {
|
||||
const snapshot = this.getObject(snapshotId)
|
||||
if (snapshotBefore) {
|
||||
await this._snapshotVm(snapshot.$snapshot_of)
|
||||
}
|
||||
await this.call('VM.revert', snapshot.$ref)
|
||||
if (snapshot.snapshot_info['power-state-at-snapshot'] === 'Running') {
|
||||
const vm = snapshot.$snapshot_of
|
||||
const vm = await this.barrier(snapshot.snapshot_of)
|
||||
if (vm.power_state === 'Halted') {
|
||||
;this.startVm(vm.$id)::ignoreErrors()
|
||||
this.startVm(vm.$id)::ignoreErrors()
|
||||
} else if (vm.power_state === 'Suspended') {
|
||||
;this.resumeVm(vm.$id)::ignoreErrors()
|
||||
this.resumeVm(vm.$id)::ignoreErrors()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async resumeVm (vmId) {
|
||||
async resumeVm(vmId) {
|
||||
// the force parameter is always true
|
||||
return this.call('VM.resume', this.getObject(vmId).$ref, false, true)
|
||||
},
|
||||
|
||||
shutdownVm (vmId, { hard = false } = {}) {
|
||||
async unpauseVm(vmId) {
|
||||
return this.call('VM.unpause', this.getObject(vmId).$ref)
|
||||
},
|
||||
|
||||
shutdownVm(vmId, { hard = false } = {}) {
|
||||
return this.call(
|
||||
`VM.${hard ? 'hard' : 'clean'}_shutdown`,
|
||||
this.getObject(vmId).$ref
|
||||
|
||||
@@ -143,7 +143,18 @@ export const isHostRunning = host => {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const isVmHvm = vm => Boolean(vm.HVM_boot_policy)
|
||||
export const getVmDomainType = vm => {
|
||||
const dt = vm.domain_type
|
||||
if (
|
||||
dt !== undefined && // XS < 7.5
|
||||
dt !== 'unspecified' // detection failed
|
||||
) {
|
||||
return dt
|
||||
}
|
||||
return vm.HVM_boot_policy === '' ? 'pv' : 'hvm'
|
||||
}
|
||||
|
||||
export const isVmHvm = vm => getVmDomainType(vm) === 'hvm'
|
||||
|
||||
const VM_RUNNING_POWER_STATES = {
|
||||
Running: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import checkAuthorization from 'xo-acl-resolver'
|
||||
import aclResolver from 'xo-acl-resolver'
|
||||
import { forEach, includes, map } from 'lodash'
|
||||
|
||||
import { ModelAlreadyExists } from '../collection'
|
||||
@@ -102,6 +102,21 @@ export default class {
|
||||
return permissions
|
||||
}
|
||||
|
||||
async checkPermissions (userId, permissions) {
|
||||
const user = await this._xo.getUser(userId)
|
||||
|
||||
// Special case for super XO administrators.
|
||||
if (user.permission === 'admin') {
|
||||
return true
|
||||
}
|
||||
|
||||
aclResolver.assert(
|
||||
await this.getPermissionsForUser(userId),
|
||||
id => this._xo.getObject(id),
|
||||
permissions
|
||||
)
|
||||
}
|
||||
|
||||
async hasPermissions (userId, permissions) {
|
||||
const user = await this._xo.getUser(userId)
|
||||
|
||||
@@ -110,7 +125,7 @@ export default class {
|
||||
return true
|
||||
}
|
||||
|
||||
return checkAuthorization(
|
||||
return aclResolver.check(
|
||||
await this.getPermissionsForUser(userId),
|
||||
id => this._xo.getObject(id),
|
||||
permissions
|
||||
|
||||
@@ -2,10 +2,11 @@ import createLogger from '@xen-orchestra/log'
|
||||
import kindOf from 'kindof'
|
||||
import ms from 'ms'
|
||||
import schemaInspector from 'schema-inspector'
|
||||
import { forEach, isArray, isFunction, map, mapValues } from 'lodash'
|
||||
import { forEach, isFunction } from 'lodash'
|
||||
import { MethodNotFound } from 'json-rpc-peer'
|
||||
|
||||
import * as methods from '../api'
|
||||
import { MethodNotFound } from 'json-rpc-peer'
|
||||
import * as sensitiveValues from '../sensitive-values'
|
||||
import { noop, serializeError } from '../utils'
|
||||
|
||||
import * as errors from 'xo-common/api-errors'
|
||||
@@ -82,7 +83,7 @@ function checkPermission (method) {
|
||||
|
||||
const { user } = this
|
||||
if (!user) {
|
||||
throw errors.unauthorized()
|
||||
throw errors.unauthorized(permission)
|
||||
}
|
||||
|
||||
// The only requirement is login.
|
||||
@@ -91,11 +92,11 @@ function checkPermission (method) {
|
||||
}
|
||||
|
||||
if (!hasPermission(user, permission)) {
|
||||
throw errors.unauthorized()
|
||||
throw errors.unauthorized(permission)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveParams (method, params) {
|
||||
async function resolveParams (method, params) {
|
||||
const resolve = method.resolve
|
||||
if (!resolve) {
|
||||
return params
|
||||
@@ -134,33 +135,13 @@ function resolveParams (method, params) {
|
||||
}
|
||||
})
|
||||
|
||||
return this.hasPermissions(userId, permissions).then(success => {
|
||||
if (success) {
|
||||
return params
|
||||
}
|
||||
await this.checkPermissions(userId, permissions)
|
||||
|
||||
throw errors.unauthorized()
|
||||
})
|
||||
return params
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const removeSensitiveParams = (value, name) => {
|
||||
if (name === 'password' && typeof value === 'string') {
|
||||
return '* obfuscated *'
|
||||
}
|
||||
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return value
|
||||
}
|
||||
|
||||
return isArray(value)
|
||||
? map(value, removeSensitiveParams)
|
||||
: mapValues(value, removeSensitiveParams)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Api {
|
||||
constructor (xo) {
|
||||
this._logger = null
|
||||
@@ -295,7 +276,7 @@ export default class Api {
|
||||
const data = {
|
||||
userId,
|
||||
method: name,
|
||||
params: removeSensitiveParams(params),
|
||||
params: sensitiveValues.replace(params, '* obfuscated *'),
|
||||
duration: Date.now() - startTime,
|
||||
error: serializeError(error),
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ export default class {
|
||||
token = token.properties
|
||||
|
||||
if (!(token.expiration > Date.now())) {
|
||||
;this._tokens.remove(id)::ignoreErrors()
|
||||
this._tokens.remove(id)::ignoreErrors()
|
||||
|
||||
throw noSuchAuthenticationToken(id)
|
||||
}
|
||||
|
||||
@@ -134,8 +134,8 @@ const getOldEntries = <T>(retention: number, entries?: T[]): T[] =>
|
||||
entries === undefined
|
||||
? []
|
||||
: retention > 0
|
||||
? entries.slice(0, -retention)
|
||||
: entries
|
||||
? entries.slice(0, -retention)
|
||||
: entries
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
concurrency: 0,
|
||||
@@ -147,12 +147,12 @@ const defaultSettings: Settings = {
|
||||
timeout: 0,
|
||||
vmTimeout: 0,
|
||||
}
|
||||
const getSetting = <T>(
|
||||
const getSetting = <T, K: $Keys<Settings>>(
|
||||
settings: $Dict<Settings>,
|
||||
name: $Keys<Settings>,
|
||||
name: K,
|
||||
keys: string[],
|
||||
defaultValue?: T
|
||||
): T | any => {
|
||||
): T | $ElementType<Settings, K> => {
|
||||
for (let i = 0, n = keys.length; i < n; ++i) {
|
||||
const objectSettings = settings[keys[i]]
|
||||
if (objectSettings !== undefined) {
|
||||
@@ -475,6 +475,7 @@ const extractIdsFromSimplePattern = (pattern: mixed) => {
|
||||
//
|
||||
// job.start(data: { mode: Mode, reportWhen: ReportWhen })
|
||||
// ├─ task.info(message: 'vms', data: { vms: string[] })
|
||||
// ├─ task.warning(message: 'missingVms', data: { vms: string[] })
|
||||
// ├─ task.warning(message: string)
|
||||
// ├─ task.start(data: { type: 'VM', id: string })
|
||||
// │ ├─ task.warning(message: string)
|
||||
@@ -543,29 +544,24 @@ export default class BackupNg {
|
||||
(vmsId = extractIdsFromSimplePattern(vmsPattern)) !== undefined
|
||||
) {
|
||||
vms = {}
|
||||
const missingVms = []
|
||||
vmsId.forEach(id => {
|
||||
try {
|
||||
vms[id] = app.getObject(id, 'VM')
|
||||
} catch (error) {
|
||||
const taskId: string = logger.notice(
|
||||
`Starting backup of ${id}. (${job.id})`,
|
||||
{
|
||||
event: 'task.start',
|
||||
parentId: runJobId,
|
||||
data: {
|
||||
type: 'VM',
|
||||
id,
|
||||
},
|
||||
}
|
||||
)
|
||||
logger.error(`Backuping ${id} has failed. (${job.id})`, {
|
||||
event: 'task.end',
|
||||
taskId,
|
||||
status: 'failure',
|
||||
result: serializeError(error),
|
||||
})
|
||||
missingVms.push(id)
|
||||
}
|
||||
})
|
||||
|
||||
if (missingVms.length !== 0) {
|
||||
logger.warning('missingVms', {
|
||||
event: 'task.warning',
|
||||
taskId: runJobId,
|
||||
data: {
|
||||
vms: missingVms,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
vms = app.getObjects({
|
||||
filter: createPredicate({
|
||||
@@ -738,6 +734,12 @@ export default class BackupNg {
|
||||
}
|
||||
}
|
||||
|
||||
// Task logs emitted in a restore execution:
|
||||
//
|
||||
// task.start(message: 'restore', data: { jobId: string, srId: string, time: number })
|
||||
// ├─ task.start(message: 'transfer')
|
||||
// │ └─ task.end(result: { id: string, size: number })
|
||||
// └─ task.end
|
||||
async importVmBackupNg (id: string, srId: string): Promise<string> {
|
||||
const app = this._app
|
||||
const { metadataFilename, remoteId } = parseVmBackupId(id)
|
||||
|
||||
@@ -144,8 +144,8 @@ const listPartitions = (() => {
|
||||
key === 'start' || key === 'size'
|
||||
? +value
|
||||
: key === 'type'
|
||||
? TYPES[+value] || value
|
||||
: value,
|
||||
? TYPES[+value] || value
|
||||
: value,
|
||||
})
|
||||
|
||||
return device =>
|
||||
@@ -445,17 +445,17 @@ export default class {
|
||||
// Once done, (asynchronously) remove the (now obsolete) local
|
||||
// base.
|
||||
if (localBaseUuid) {
|
||||
;promise.then(() => srcXapi.deleteVm(localBaseUuid))::ignoreErrors()
|
||||
promise.then(() => srcXapi.deleteVm(localBaseUuid))::ignoreErrors()
|
||||
}
|
||||
|
||||
if (toRemove !== undefined) {
|
||||
;promise
|
||||
promise
|
||||
.then(() => asyncMap(toRemove, _ => targetXapi.deleteVm(_.$id)))
|
||||
::ignoreErrors()
|
||||
}
|
||||
|
||||
// (Asynchronously) Identify snapshot as future base.
|
||||
;promise
|
||||
promise
|
||||
.then(() => {
|
||||
return srcXapi._updateObjectMapProperty(srcVm, 'other_config', {
|
||||
[TAG_LAST_BASE_DELTA]: delta.vm.uuid,
|
||||
@@ -593,7 +593,7 @@ export default class {
|
||||
base => base.snapshot_time
|
||||
)
|
||||
forEach(bases, base => {
|
||||
;xapi.deleteVdi(base.$id)::ignoreErrors()
|
||||
xapi.deleteVdi(base.$id)::ignoreErrors()
|
||||
})
|
||||
|
||||
// Export full or delta backup.
|
||||
@@ -652,7 +652,7 @@ export default class {
|
||||
)
|
||||
const baseVm = bases.pop()
|
||||
forEach(bases, base => {
|
||||
;xapi.deleteVm(base.$id)::ignoreErrors()
|
||||
xapi.deleteVm(base.$id)::ignoreErrors()
|
||||
})
|
||||
|
||||
// Check backup dirs.
|
||||
@@ -780,7 +780,7 @@ export default class {
|
||||
await this._removeOldDeltaVmBackups(xapi, { vm, handler, dir, retention })
|
||||
|
||||
if (baseVm) {
|
||||
;xapi.deleteVm(baseVm.$id)::ignoreErrors()
|
||||
xapi.deleteVm(baseVm.$id)::ignoreErrors()
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -56,8 +56,8 @@ const parsePartxLine = createPairsParser({
|
||||
key === 'start' || key === 'size'
|
||||
? +value
|
||||
: key === 'type'
|
||||
? PARTITION_TYPE_NAMES[+value] || value
|
||||
: value,
|
||||
? PARTITION_TYPE_NAMES[+value] || value
|
||||
: value,
|
||||
})
|
||||
|
||||
const listLvmLogicalVolumes = defer(
|
||||
|
||||
@@ -328,7 +328,7 @@ export default class Jobs {
|
||||
app.emit('job:terminated', undefined, job, schedule, runJobId)
|
||||
throw error
|
||||
} finally {
|
||||
;this.updateJob({ id, runId: null })::ignoreErrors()
|
||||
this.updateJob({ id, runId: null })::ignoreErrors()
|
||||
delete runningJobs[id]
|
||||
delete runs[runJobId]
|
||||
if (session !== undefined) {
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class LevelDbLogger extends AbstractLogger {
|
||||
return promise.then(() => key)
|
||||
}
|
||||
|
||||
;promise::ignoreErrors()
|
||||
promise::ignoreErrors()
|
||||
return key
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Ajv from 'ajv'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
|
||||
import { PluginsMetadata } from '../models/plugin-metadata'
|
||||
import { invalidParameters, noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
import * as sensitiveValues from '../sensitive-values'
|
||||
import { PluginsMetadata } from '../models/plugin-metadata'
|
||||
import { isFunction, mapToArray } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
@@ -119,7 +120,7 @@ export default class {
|
||||
loaded,
|
||||
unloadable,
|
||||
version,
|
||||
configuration,
|
||||
configuration: sensitiveValues.obfuscate(configuration),
|
||||
configurationPresets,
|
||||
configurationSchema,
|
||||
testable,
|
||||
@@ -165,6 +166,14 @@ export default class {
|
||||
// save the new configuration.
|
||||
async configurePlugin (id, configuration) {
|
||||
const plugin = this._getRawPlugin(id)
|
||||
const metadata = await this._getPluginMetadata()
|
||||
|
||||
if (metadata !== undefined) {
|
||||
configuration = sensitiveValues.merge(
|
||||
configuration,
|
||||
metadata.configuration
|
||||
)
|
||||
}
|
||||
|
||||
await this._configurePlugin(plugin, configuration)
|
||||
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import synchronized from 'decorator-synchronized'
|
||||
import { format, parse } from 'xo-remote-parser'
|
||||
import { getHandler } from '@xen-orchestra/fs'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
import { ignoreErrors } from 'promise-toolbox'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
import * as sensitiveValues from '../sensitive-values'
|
||||
import patch from '../patch'
|
||||
import { mapToArray } from '../utils'
|
||||
import { Remotes } from '../models/remote'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const obfuscateRemote = ({ url, ...remote }) => {
|
||||
remote.url = format(sensitiveValues.obfuscate(parse(url)))
|
||||
return remote
|
||||
}
|
||||
|
||||
export default class {
|
||||
constructor (xo, { remoteOptions }) {
|
||||
this._remoteOptions = remoteOptions
|
||||
@@ -30,7 +37,7 @@ export default class {
|
||||
)
|
||||
)
|
||||
|
||||
const remotes = await this.getAllRemotes()
|
||||
const remotes = await this._remotes.get()
|
||||
remotes.forEach(remote => {
|
||||
ignoreErrors.call(this.updateRemote(remote.id, {}))
|
||||
})
|
||||
@@ -47,7 +54,7 @@ export default class {
|
||||
|
||||
async getRemoteHandler (remote) {
|
||||
if (typeof remote === 'string') {
|
||||
remote = await this.getRemote(remote)
|
||||
remote = await this._getRemote(remote)
|
||||
}
|
||||
|
||||
if (!remote.enabled) {
|
||||
@@ -78,10 +85,10 @@ export default class {
|
||||
}
|
||||
|
||||
async getAllRemotes () {
|
||||
return this._remotes.get()
|
||||
return (await this._remotes.get()).map(_ => obfuscateRemote(_))
|
||||
}
|
||||
|
||||
async getRemote (id) {
|
||||
async _getRemote (id) {
|
||||
const remote = await this._remotes.first(id)
|
||||
if (remote === undefined) {
|
||||
throw noSuchObject(id, 'remote')
|
||||
@@ -89,6 +96,10 @@ export default class {
|
||||
return remote.properties
|
||||
}
|
||||
|
||||
getRemote (id) {
|
||||
return this._getRemote(id).then(obfuscateRemote)
|
||||
}
|
||||
|
||||
async createRemote ({ name, url, options }) {
|
||||
const params = {
|
||||
name,
|
||||
@@ -120,9 +131,16 @@ export default class {
|
||||
}
|
||||
|
||||
@synchronized()
|
||||
async _updateRemote (id, props) {
|
||||
const remote = await this.getRemote(id)
|
||||
async _updateRemote (id, { url, ...props }) {
|
||||
const remote = await this._getRemote(id)
|
||||
|
||||
// url is handled separately to take care of obfuscated values
|
||||
if (typeof url === 'string') {
|
||||
remote.url = format(sensitiveValues.merge(parse(url), parse(remote.url)))
|
||||
}
|
||||
|
||||
patch(remote, props)
|
||||
|
||||
return (await this._remotes.update(remote)).properties
|
||||
}
|
||||
|
||||
|
||||
@@ -57,15 +57,13 @@ const normalize = set => ({
|
||||
id: set.id,
|
||||
ipPools: set.ipPools || [],
|
||||
limits: set.limits
|
||||
? map(
|
||||
set.limits,
|
||||
limit =>
|
||||
isObject(limit)
|
||||
? limit
|
||||
: {
|
||||
available: limit,
|
||||
total: limit,
|
||||
}
|
||||
? map(set.limits, limit =>
|
||||
isObject(limit)
|
||||
? limit
|
||||
: {
|
||||
available: limit,
|
||||
total: limit,
|
||||
}
|
||||
)
|
||||
: {},
|
||||
name: set.name || '',
|
||||
|
||||
@@ -10,10 +10,8 @@ import { forEach, isFunction, promisify } from '../utils'
|
||||
|
||||
const _levelHas = function has (key, cb) {
|
||||
if (cb) {
|
||||
return this.get(
|
||||
key,
|
||||
(error, value) =>
|
||||
error ? (error.notFound ? cb(null, false) : cb(error)) : cb(null, true)
|
||||
return this.get(key, (error, value) =>
|
||||
error ? (error.notFound ? cb(null, false) : cb(error)) : cb(null, true)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ export default class {
|
||||
.getAuthenticationTokensForUser(id)
|
||||
.then(tokens => {
|
||||
forEach(tokens, token => {
|
||||
;this._xo.deleteAuthenticationToken(id)::ignoreErrors()
|
||||
this._xo.deleteAuthenticationToken(id)::ignoreErrors()
|
||||
})
|
||||
})
|
||||
::ignoreErrors()
|
||||
@@ -112,13 +112,13 @@ export default class {
|
||||
// Remove ACLs for this user.
|
||||
this._xo.getAclsForSubject(id).then(acls => {
|
||||
forEach(acls, acl => {
|
||||
;this._xo.removeAcl(id, acl.object, acl.action)::ignoreErrors()
|
||||
this._xo.removeAcl(id, acl.object, acl.action)::ignoreErrors()
|
||||
})
|
||||
})
|
||||
|
||||
// Remove the user from all its groups.
|
||||
forEach(user.groups, groupId => {
|
||||
;this.getGroup(groupId)
|
||||
this.getGroup(groupId)
|
||||
.then(group => this._removeUserFromGroup(id, group))
|
||||
::ignoreErrors()
|
||||
})
|
||||
@@ -264,13 +264,13 @@ export default class {
|
||||
// Remove ACLs for this group.
|
||||
this._xo.getAclsForSubject(id).then(acls => {
|
||||
forEach(acls, acl => {
|
||||
;this._xo.removeAcl(id, acl.object, acl.action)::ignoreErrors()
|
||||
this._xo.removeAcl(id, acl.object, acl.action)::ignoreErrors()
|
||||
})
|
||||
})
|
||||
|
||||
// Remove the group from all its users.
|
||||
forEach(group.users, userId => {
|
||||
;this.getUser(userId)
|
||||
this.getUser(userId)
|
||||
.then(user => this._removeGroupFromUser(id, user))
|
||||
::ignoreErrors()
|
||||
})
|
||||
|
||||
@@ -5,8 +5,11 @@ import { mergeVhd as mergeVhd_ } from 'vhd-lib'
|
||||
|
||||
// Use Bluebird for all promises as it provides better performance and
|
||||
// less memory usage.
|
||||
//
|
||||
// $FlowFixMe
|
||||
global.Promise = require('bluebird')
|
||||
|
||||
// $FlowFixMe
|
||||
const config: Object = JSON.parse(process.env.XO_CONFIG)
|
||||
|
||||
export function mergeVhd (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import { findKey } from 'lodash'
|
||||
import { ignoreErrors } from 'promise-toolbox'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
@@ -20,7 +21,7 @@ import { Servers } from '../models/server'
|
||||
const log = createLogger('xo:xo-mixins:xen-servers')
|
||||
|
||||
export default class {
|
||||
constructor (xo) {
|
||||
constructor(xo, { xapiOptions }) {
|
||||
this._objectConflicts = { __proto__: null } // TODO: clean when a server is disconnected.
|
||||
const serversDb = (this._servers = new Servers({
|
||||
connection: xo._redis,
|
||||
@@ -28,6 +29,7 @@ export default class {
|
||||
indexes: ['host'],
|
||||
}))
|
||||
this._stats = new XapiStats()
|
||||
this._xapiOptions = xapiOptions
|
||||
this._xapis = { __proto__: null }
|
||||
this._xapisByPool = { __proto__: null }
|
||||
this._xo = xo
|
||||
@@ -57,7 +59,7 @@ export default class {
|
||||
// TODO: disconnect servers on stop.
|
||||
}
|
||||
|
||||
async registerXenServer ({
|
||||
async registerXenServer({
|
||||
allowUnauthorized,
|
||||
host,
|
||||
label,
|
||||
@@ -81,15 +83,15 @@ export default class {
|
||||
return server.properties
|
||||
}
|
||||
|
||||
async unregisterXenServer (id) {
|
||||
;this.disconnectXenServer(id)::ignoreErrors()
|
||||
async unregisterXenServer(id) {
|
||||
this.disconnectXenServer(id)::ignoreErrors()
|
||||
|
||||
if (!(await this._servers.remove(id))) {
|
||||
throw noSuchObject(id, 'xenServer')
|
||||
}
|
||||
}
|
||||
|
||||
async updateXenServer (
|
||||
async updateXenServer(
|
||||
id,
|
||||
{
|
||||
allowUnauthorized,
|
||||
@@ -149,7 +151,7 @@ export default class {
|
||||
|
||||
// TODO: this method will no longer be async when servers are
|
||||
// integrated to the main collection.
|
||||
async _getXenServer (id) {
|
||||
async _getXenServer(id) {
|
||||
const server = await this._servers.first(id)
|
||||
if (server === undefined) {
|
||||
throw noSuchObject(id, 'xenServer')
|
||||
@@ -158,13 +160,28 @@ export default class {
|
||||
return server
|
||||
}
|
||||
|
||||
_onXenAdd (xapiObjects, xapiIdsToXo, toRetry, conId) {
|
||||
_onXenAdd(
|
||||
newXapiObjects,
|
||||
xapiIdsToXo,
|
||||
toRetry,
|
||||
conId,
|
||||
dependents,
|
||||
xapiObjects
|
||||
) {
|
||||
const conflicts = this._objectConflicts
|
||||
const objects = this._xo._objects
|
||||
|
||||
forEach(xapiObjects, (xapiObject, xapiId) => {
|
||||
forEach(newXapiObjects, function handleObject(xapiObject, xapiId) {
|
||||
const { $ref } = xapiObject
|
||||
|
||||
const dependent = dependents[$ref]
|
||||
if (dependent !== undefined) {
|
||||
delete dependents[$ref]
|
||||
return handleObject(xapiObjects[dependent], dependent)
|
||||
}
|
||||
|
||||
try {
|
||||
const xoObject = xapiObjectToXo(xapiObject)
|
||||
const xoObject = xapiObjectToXo(xapiObject, dependents)
|
||||
if (!xoObject) {
|
||||
return
|
||||
}
|
||||
@@ -173,7 +190,7 @@ export default class {
|
||||
xapiIdsToXo[xapiId] = xoId
|
||||
|
||||
const previous = objects.get(xoId, undefined)
|
||||
if (previous && previous._xapiRef !== xapiObject.$ref) {
|
||||
if (previous && previous._xapiRef !== $ref) {
|
||||
const conflicts_ =
|
||||
conflicts[xoId] || (conflicts[xoId] = { __proto__: null })
|
||||
conflicts_[conId] = xoObject
|
||||
@@ -188,7 +205,7 @@ export default class {
|
||||
})
|
||||
}
|
||||
|
||||
_onXenRemove (xapiObjects, xapiIdsToXo, toRetry, conId) {
|
||||
_onXenRemove(xapiObjects, xapiIdsToXo, toRetry, conId) {
|
||||
const conflicts = this._objectConflicts
|
||||
const objects = this._xo._objects
|
||||
|
||||
@@ -220,121 +237,138 @@ export default class {
|
||||
})
|
||||
}
|
||||
|
||||
async connectXenServer (id) {
|
||||
async connectXenServer(id) {
|
||||
const server = (await this._getXenServer(id)).properties
|
||||
|
||||
const xapi = (this._xapis[server.id] = new Xapi({
|
||||
const xapi = new Xapi({
|
||||
allowUnauthorized: Boolean(server.allowUnauthorized),
|
||||
readOnly: Boolean(server.readOnly),
|
||||
|
||||
...this._xapiOptions,
|
||||
|
||||
auth: {
|
||||
user: server.username,
|
||||
password: server.password,
|
||||
},
|
||||
readOnly: Boolean(server.readOnly),
|
||||
url: server.host,
|
||||
}))
|
||||
watchEvents: false,
|
||||
})
|
||||
|
||||
xapi.xo = (() => {
|
||||
const conId = server.id
|
||||
|
||||
// Maps ids of XAPI objects to ids of XO objects.
|
||||
const xapiIdsToXo = { __proto__: null }
|
||||
|
||||
// Map of XAPI objects which failed to be transformed to XO
|
||||
// objects.
|
||||
//
|
||||
// At each `finish` there will be another attempt to transform
|
||||
// until they succeed.
|
||||
let toRetry
|
||||
let toRetryNext = { __proto__: null }
|
||||
|
||||
const onAddOrUpdate = objects => {
|
||||
this._onXenAdd(objects, xapiIdsToXo, toRetryNext, conId)
|
||||
}
|
||||
const onRemove = objects => {
|
||||
this._onXenRemove(objects, xapiIdsToXo, toRetry, conId)
|
||||
}
|
||||
try {
|
||||
await xapi.connect()
|
||||
|
||||
const xapisByPool = this._xapisByPool
|
||||
const onFinish = () => {
|
||||
const { pool } = xapi
|
||||
if (pool) {
|
||||
xapisByPool[pool.$id] = xapi
|
||||
}
|
||||
|
||||
if (!isEmpty(toRetry)) {
|
||||
onAddOrUpdate(toRetry)
|
||||
toRetry = null
|
||||
}
|
||||
|
||||
if (!isEmpty(toRetryNext)) {
|
||||
toRetry = toRetryNext
|
||||
toRetryNext = { __proto__: null }
|
||||
}
|
||||
const [{ $id: poolId }] = await xapi.getAllRecords('pool')
|
||||
if (xapisByPool[poolId] !== undefined) {
|
||||
throw new Error("the server's pool is already connected")
|
||||
}
|
||||
|
||||
const { objects } = xapi
|
||||
this._xapis[server.id] = xapisByPool[poolId] = xapi
|
||||
|
||||
const addObject = object => {
|
||||
// TODO: optimize.
|
||||
onAddOrUpdate({ [object.$id]: object })
|
||||
return xapiObjectToXo(object)
|
||||
}
|
||||
xapi.xo = (() => {
|
||||
const conId = server.id
|
||||
|
||||
return {
|
||||
httpRequest: this._xo.httpRequest.bind(this),
|
||||
// Maps ids of XAPI objects to ids of XO objects.
|
||||
const xapiIdsToXo = { __proto__: null }
|
||||
|
||||
install () {
|
||||
objects.on('add', onAddOrUpdate)
|
||||
objects.on('update', onAddOrUpdate)
|
||||
objects.on('remove', onRemove)
|
||||
objects.on('finish', onFinish)
|
||||
// Map of XAPI objects which failed to be transformed to XO
|
||||
// objects.
|
||||
//
|
||||
// At each `finish` there will be another attempt to transform
|
||||
// until they succeed.
|
||||
let toRetry
|
||||
let toRetryNext = { __proto__: null }
|
||||
|
||||
onAddOrUpdate(objects.all)
|
||||
},
|
||||
uninstall () {
|
||||
objects.removeListener('add', onAddOrUpdate)
|
||||
objects.removeListener('update', onAddOrUpdate)
|
||||
objects.removeListener('remove', onRemove)
|
||||
objects.removeListener('finish', onFinish)
|
||||
const dependents = { __proto__: null }
|
||||
|
||||
onRemove(objects.all)
|
||||
},
|
||||
|
||||
addObject,
|
||||
getData: (id, key) => {
|
||||
const value = (typeof id === 'string' ? xapi.getObject(id) : id)
|
||||
.other_config[`xo:${camelToSnakeCase(key)}`]
|
||||
return value && JSON.parse(value)
|
||||
},
|
||||
setData: async (id, key, value) => {
|
||||
await xapi._updateObjectMapProperty(
|
||||
xapi.getObject(id),
|
||||
'other_config',
|
||||
{
|
||||
[`xo:${camelToSnakeCase(key)}`]:
|
||||
value !== null ? JSON.stringify(value) : value,
|
||||
}
|
||||
const onAddOrUpdate = objects => {
|
||||
this._onXenAdd(
|
||||
objects,
|
||||
xapiIdsToXo,
|
||||
toRetryNext,
|
||||
conId,
|
||||
dependents,
|
||||
xapi.objects.all
|
||||
)
|
||||
}
|
||||
const onRemove = objects => {
|
||||
this._onXenRemove(objects, xapiIdsToXo, toRetry, conId, dependents)
|
||||
}
|
||||
|
||||
// Register the updated object.
|
||||
addObject(await xapi._waitObject(id))
|
||||
},
|
||||
}
|
||||
})()
|
||||
const onFinish = () => {
|
||||
if (!isEmpty(toRetry)) {
|
||||
onAddOrUpdate(toRetry)
|
||||
toRetry = null
|
||||
}
|
||||
|
||||
xapi.xo.install()
|
||||
if (!isEmpty(toRetryNext)) {
|
||||
toRetry = toRetryNext
|
||||
toRetryNext = { __proto__: null }
|
||||
}
|
||||
}
|
||||
|
||||
await xapi.connect().then(
|
||||
() => this.updateXenServer(id, { error: null }),
|
||||
error => {
|
||||
this.updateXenServer(id, { error: serializeError(error) })
|
||||
const { objects } = xapi
|
||||
|
||||
throw error
|
||||
}
|
||||
)
|
||||
const addObject = object => {
|
||||
// TODO: optimize.
|
||||
onAddOrUpdate({ [object.$id]: object })
|
||||
return xapiObjectToXo(object, dependents)
|
||||
}
|
||||
|
||||
return {
|
||||
httpRequest: this._xo.httpRequest.bind(this),
|
||||
|
||||
install() {
|
||||
objects.on('add', onAddOrUpdate)
|
||||
objects.on('update', onAddOrUpdate)
|
||||
objects.on('remove', onRemove)
|
||||
objects.on('finish', onFinish)
|
||||
|
||||
onAddOrUpdate(objects.all)
|
||||
},
|
||||
uninstall() {
|
||||
objects.removeListener('add', onAddOrUpdate)
|
||||
objects.removeListener('update', onAddOrUpdate)
|
||||
objects.removeListener('remove', onRemove)
|
||||
objects.removeListener('finish', onFinish)
|
||||
|
||||
onRemove(objects.all)
|
||||
},
|
||||
|
||||
addObject,
|
||||
getData: (id, key) => {
|
||||
const value = (typeof id === 'string' ? xapi.getObject(id) : id)
|
||||
.other_config[`xo:${camelToSnakeCase(key)}`]
|
||||
return value && JSON.parse(value)
|
||||
},
|
||||
setData: async (id, key, value) => {
|
||||
await xapi._updateObjectMapProperty(
|
||||
xapi.getObject(id),
|
||||
'other_config',
|
||||
{
|
||||
[`xo:${camelToSnakeCase(key)}`]:
|
||||
value !== null ? JSON.stringify(value) : value,
|
||||
}
|
||||
)
|
||||
|
||||
// Register the updated object.
|
||||
addObject(await xapi._waitObject(id))
|
||||
},
|
||||
}
|
||||
})()
|
||||
|
||||
xapi.xo.install()
|
||||
xapi.watchEvents()
|
||||
|
||||
this.updateXenServer(id, { error: null })::ignoreErrors()
|
||||
} catch (error) {
|
||||
xapi.disconnect()::ignoreErrors()
|
||||
this.updateXenServer(id, { error: serializeError(error) })::ignoreErrors()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async disconnectXenServer (id) {
|
||||
async disconnectXenServer(id) {
|
||||
const xapi = this._xapis[id]
|
||||
if (!xapi) {
|
||||
throw noSuchObject(id, 'xenServer')
|
||||
@@ -343,20 +377,20 @@ export default class {
|
||||
delete this._xapis[id]
|
||||
|
||||
const { pool } = xapi
|
||||
if (pool) {
|
||||
delete this._xapisByPool[pool.id]
|
||||
if (pool != null) {
|
||||
delete this._xapisByPool[pool.$id]
|
||||
}
|
||||
|
||||
xapi.xo.uninstall()
|
||||
return xapi.disconnect()
|
||||
}
|
||||
|
||||
getAllXapis () {
|
||||
getAllXapis() {
|
||||
return this._xapis
|
||||
}
|
||||
|
||||
// Returns the XAPI connection associated to an object.
|
||||
getXapi (object, type) {
|
||||
getXapi(object, type) {
|
||||
if (isString(object)) {
|
||||
object = this._xo.getObject(object, type)
|
||||
}
|
||||
@@ -374,7 +408,7 @@ export default class {
|
||||
return xapi
|
||||
}
|
||||
|
||||
async getAllXenServers () {
|
||||
async getAllXenServers() {
|
||||
const servers = await this._servers.get()
|
||||
const xapis = this._xapis
|
||||
forEach(servers, server => {
|
||||
@@ -395,24 +429,24 @@ export default class {
|
||||
return servers
|
||||
}
|
||||
|
||||
getXapiVmStats (vmId, granularity) {
|
||||
getXapiVmStats(vmId, granularity) {
|
||||
return this._stats.getVmStats(this.getXapi(vmId), vmId, granularity)
|
||||
}
|
||||
|
||||
getXapiHostStats (hostId, granularity) {
|
||||
getXapiHostStats(hostId, granularity) {
|
||||
return this._stats.getHostStats(this.getXapi(hostId), hostId, granularity)
|
||||
}
|
||||
|
||||
getXapiSrStats (srId, granularity) {
|
||||
getXapiSrStats(srId, granularity) {
|
||||
return this._stats.getSrStats(this.getXapi(srId), srId, granularity)
|
||||
}
|
||||
|
||||
async mergeXenPools (sourceId, targetId, force = false) {
|
||||
const sourceXapi = this.getXapi(sourceId)
|
||||
async mergeXenPools(sourcePoolId, targetPoolId, force = false) {
|
||||
const sourceXapi = this.getXapi(sourcePoolId)
|
||||
const {
|
||||
_auth: { user, password },
|
||||
_url: { hostname },
|
||||
} = this.getXapi(targetId)
|
||||
} = this.getXapi(targetPoolId)
|
||||
|
||||
// We don't want the events of the source XAPI to interfere with
|
||||
// the events of the new XAPI.
|
||||
@@ -426,6 +460,8 @@ export default class {
|
||||
throw e
|
||||
}
|
||||
|
||||
await this.unregisterXenServer(sourceId)
|
||||
this.unregisterXenServer(
|
||||
findKey(this._xapis, candidate => candidate === sourceXapi)
|
||||
)::ignoreErrors()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const GRAIN_ADDRESS_OFFSET = 56
|
||||
* the grain table is the array of LBAs (in byte, not in sector) ordered by their position in the VDMK file
|
||||
* THIS CODE RUNS ON THE BROWSER
|
||||
*/
|
||||
export default async function readVmdkGrainTable (fileAccessor) {
|
||||
export default async function readVmdkGrainTable(fileAccessor) {
|
||||
const getLongLong = (buffer, offset, name) => {
|
||||
if (buffer.length < offset + 8) {
|
||||
throw new Error(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.29.3",
|
||||
"version": "5.30.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -133,10 +133,10 @@
|
||||
"value-matcher": "^0.2.0",
|
||||
"vinyl": "^2.1.0",
|
||||
"watchify": "^3.7.0",
|
||||
"whatwg-fetch": "^2.0.3",
|
||||
"whatwg-fetch": "^3.0.0",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.3.0",
|
||||
"xo-common": "^0.1.2",
|
||||
"xo-acl-resolver": "^0.4.0",
|
||||
"xo-common": "^0.2.0",
|
||||
"xo-lib": "^0.8.0",
|
||||
"xo-remote-parser": "^0.5.0",
|
||||
"xo-vmdk-to-vhd": "^0.1.5"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import { map, mapValues, noop } from 'lodash'
|
||||
|
||||
const call = fn => fn()
|
||||
|
||||
@@ -7,30 +7,33 @@ const call = fn => fn()
|
||||
// callbacks have been correctly initialized when there are circular dependencies
|
||||
const addSubscriptions = subscriptions => Component =>
|
||||
class SubscriptionWrapper extends React.PureComponent {
|
||||
_unsubscribes = null
|
||||
|
||||
componentWillMount () {
|
||||
const state = {}
|
||||
this._unsubscribes = map(
|
||||
typeof subscriptions === 'function'
|
||||
? subscriptions(this.props)
|
||||
: subscriptions,
|
||||
(subscribe, prop) => {
|
||||
state[prop] = undefined
|
||||
return subscribe(value => this.setState({ [prop]: value }))
|
||||
}
|
||||
)
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
// provide all props since the beginning (better behavior with Freactal)
|
||||
this.setState(state)
|
||||
this.state = mapValues(
|
||||
(this._subscribes =
|
||||
typeof subscriptions === 'function'
|
||||
? subscriptions(props)
|
||||
: subscriptions),
|
||||
noop
|
||||
)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
_unsubscribes = undefined
|
||||
|
||||
componentDidMount() {
|
||||
this._unsubscribes = map(this._subscribes, (subscribe, prop) =>
|
||||
subscribe(value => this.setState({ [prop]: value }))
|
||||
)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unsubscribes.forEach(call)
|
||||
this._unsubscribes = null
|
||||
this._unsubscribes = undefined
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return <Component {...this.props} {...this.state} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,16 +15,14 @@ export { Ellipsis as default }
|
||||
|
||||
export const EllipsisContainer = ({ children }) => (
|
||||
<div style={ellipsisContainerStyle}>
|
||||
{React.Children.map(
|
||||
children,
|
||||
child =>
|
||||
child == null ||
|
||||
child.type === Ellipsis ||
|
||||
(child.type != null && child.type.originalRender === Ellipsis) ? (
|
||||
child
|
||||
) : (
|
||||
<span>{child}</span>
|
||||
)
|
||||
{React.Children.map(children, child =>
|
||||
child == null ||
|
||||
child.type === Ellipsis ||
|
||||
(child.type != null && child.type.originalRender === Ellipsis) ? (
|
||||
child
|
||||
) : (
|
||||
<span>{child}</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -88,6 +88,7 @@ const messages = {
|
||||
xosan: 'XOSAN',
|
||||
backupDeprecatedMessage:
|
||||
'Warning: Backup is deprecated, use Backup NG instead.',
|
||||
moveRestoreLegacyMessage: 'Warning: Your legacy backups can be found here',
|
||||
backupMigrationLink: 'How to migrate to Backup NG',
|
||||
backupNgNewPage: 'Create a new backup with Backup NG',
|
||||
backupOverviewPage: 'Overview',
|
||||
@@ -418,6 +419,8 @@ const messages = {
|
||||
'Tip: using a thin-provisioned storage will consume less space. Please click on the icon to get more information',
|
||||
vmsOnThinProvisionedSrTip:
|
||||
'Tip: creating VMs on a thin-provisioned storage will consume less space when backuping them. Please click on the icon to get more information',
|
||||
deltaBackupOnOutdatedXenServerWarning:
|
||||
'Delta Backup and Continuous Replication require at least XenServer 6.5.',
|
||||
localRemoteWarningMessage:
|
||||
'Warning: local remotes will use limited XOA disk space. Only for advanced users.',
|
||||
backupVersionWarning:
|
||||
@@ -615,6 +618,7 @@ const messages = {
|
||||
startVmOnMissingHostMessage: 'You must select a host',
|
||||
recoveryModeLabel: 'Recovery start',
|
||||
suspendVmLabel: 'Suspend',
|
||||
pauseVmLabel: 'Pause',
|
||||
stopVmLabel: 'Stop',
|
||||
forceShutdownVmLabel: 'Force shutdown',
|
||||
rebootVmLabel: 'Reboot',
|
||||
@@ -868,6 +872,7 @@ const messages = {
|
||||
powerStateHalted: 'halted',
|
||||
powerStateRunning: 'running',
|
||||
powerStateSuspended: 'suspended',
|
||||
powerStatePaused: 'paused',
|
||||
|
||||
// ----- VM home -----
|
||||
vmCurrentStatus: 'Current status:',
|
||||
@@ -1020,6 +1025,10 @@ const messages = {
|
||||
// ----- VM advanced tab -----
|
||||
vmRemoveButton: 'Remove',
|
||||
vmConvertToTemplateButton: 'Convert to template',
|
||||
vmSwitchVirtualizationMode: 'Convert to {mode}',
|
||||
vmVirtualizationModeModalTitle: 'Change virtualization mode',
|
||||
vmVirtualizationModeModalBody:
|
||||
"You must know what you are doing, because it could break your setup (if you didn't install the bootloader in the MBR while switching from PV to HVM, or even worse, in HVM to PV, if you don't have the correct PV args)",
|
||||
vmShareButton: 'Share',
|
||||
xenSettingsLabel: 'Xen settings',
|
||||
guestOsLabel: 'Guest OS',
|
||||
@@ -1343,7 +1352,6 @@ const messages = {
|
||||
remoteError: 'Error',
|
||||
remoteErrorMessage:
|
||||
'The URL ({url}) is invalid (colon in path). Click this button to change the URL to {newUrl}.',
|
||||
noBackup: 'No backup available',
|
||||
backupVmNameColumn: 'VM Name',
|
||||
backupVmDescriptionColumn: 'VM Description',
|
||||
backupTags: 'Tags',
|
||||
@@ -1361,6 +1369,8 @@ const messages = {
|
||||
importBackupMessage: 'Starting your backup import',
|
||||
vmsToBackup: 'VMs to backup',
|
||||
restoreResfreshList: 'Refresh backup list',
|
||||
restoreLegacy: 'Legacy restore',
|
||||
restoreFileLegacy: 'Legacy file restore',
|
||||
restoreVmBackups: 'Restore',
|
||||
restoreVmBackupsTitle: 'Restore {vm}',
|
||||
restoreVmBackupsBulkTitle:
|
||||
@@ -1396,6 +1406,7 @@ const messages = {
|
||||
restoreFilesSelectFiles: 'Select a file…',
|
||||
restoreFileContentNotFound: 'Content not found',
|
||||
restoreFilesNoFilesSelected: 'No files selected',
|
||||
restoreFilesSelectedFiles: 'Selected files ({files}):',
|
||||
restoreFilesSelectedFilesAndFolders: 'Selected files/folders ({files}):',
|
||||
restoreFilesDiskError: 'Error while scanning disk',
|
||||
restoreFilesSelectAllFiles: "Select all this folder's files",
|
||||
@@ -1450,6 +1461,9 @@ const messages = {
|
||||
suspendVmsModalTitle: 'Suspend VM{vms, plural, one {} other {s}}',
|
||||
suspendVmsModalMessage:
|
||||
'Are you sure you want to suspend {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
pauseVmsModalTitle: 'Pause VM{vms, plural, one {} other {s}}',
|
||||
pauseVmsModalMessage:
|
||||
'Are you sure you want to pause {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
restartVmsModalTitle: 'Restart VM{vms, plural, one {} other {s}}',
|
||||
restartVmsModalMessage:
|
||||
'Are you sure you want to restart {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
@@ -1807,6 +1821,9 @@ const messages = {
|
||||
logsJobName: 'Job name',
|
||||
logsJobTime: 'Job time',
|
||||
logsVmNotFound: 'VM not found!',
|
||||
logsMissingVms: 'Missing VMs skipped ({ vms })',
|
||||
logsFailedRestoreError: 'Click to show error',
|
||||
logsFailedRestoreTitle: 'Restore error',
|
||||
logDeleteMultiple: 'Delete log{nLogs, plural, one {} other {s}}',
|
||||
logDeleteMultipleMessage:
|
||||
'Are you sure you want to delete {nLogs, number} log{nLogs, plural, one {} other {s}}?',
|
||||
@@ -2036,6 +2053,8 @@ const messages = {
|
||||
licensesManage: 'Manage the licenses',
|
||||
newLicense: 'New license',
|
||||
refreshLicenses: 'Refresh',
|
||||
xoaLicenseNotShown:
|
||||
'XOA license management is not supported yet (current license: {plan})',
|
||||
xosanLicenseRestricted: 'Limited size because XOSAN is in trial',
|
||||
xosanAdminNoLicenseDisclaimer:
|
||||
'You need a license on this SR to manage the XOSAN.',
|
||||
@@ -2061,7 +2080,7 @@ const messages = {
|
||||
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}}',
|
||||
}
|
||||
forEach(messages, function (message, id) {
|
||||
forEach(messages, function(message, id) {
|
||||
if (isString(message)) {
|
||||
messages[id] = {
|
||||
id,
|
||||
|
||||
@@ -107,17 +107,16 @@ export default class IsoDevice extends Component {
|
||||
icon='vm-eject'
|
||||
/>
|
||||
</span>
|
||||
{mountedIso &&
|
||||
!cdDrive.device && (
|
||||
<Tooltip content={_('cdDriveNotInstalled')}>
|
||||
<a
|
||||
className='text-warning btn btn-link'
|
||||
onClick={this._showWarning}
|
||||
>
|
||||
<Icon icon='alarm' size='lg' />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
{mountedIso && !cdDrive.device && (
|
||||
<Tooltip content={_('cdDriveNotInstalled')}>
|
||||
<a
|
||||
className='text-warning btn btn-link'
|
||||
onClick={this._showWarning}
|
||||
>
|
||||
<Icon icon='alarm' size='lg' />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import decorate from './apply-decorators'
|
||||
import getEventValue from './get-event-value'
|
||||
import Icon from './icon'
|
||||
import Tooltip from './tooltip'
|
||||
import { generateRandomId } from './utils'
|
||||
import { generateId } from './reaclette-utils'
|
||||
import {
|
||||
disable as disableShortcuts,
|
||||
enable as enableShortcuts,
|
||||
@@ -182,6 +182,32 @@ class StrongConfirm extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
_confirm = () => {
|
||||
this.props.resolve()
|
||||
instance.close()
|
||||
}
|
||||
|
||||
_handleKeyDown = event => {
|
||||
if (event.keyCode === 13 && !this.state.buttons[0].disabled) {
|
||||
this._confirm()
|
||||
}
|
||||
}
|
||||
|
||||
_focusAndAddEventListener = ref => {
|
||||
if (ref !== null) {
|
||||
// When the modal is triggered by a react-bootstrap Dropdown, the Dropdown takes the focus back
|
||||
// https://github.com/vatesfr/react-bootstrap/blob/bootstrap-4/src/Dropdown.js#L63-L85
|
||||
// FIXME: remove the setTimeout workaround when react-bootstrap-4 is removed
|
||||
// See https://github.com/react-bootstrap/react-bootstrap/issues/2553#issuecomment-324356126
|
||||
setTimeout(() => {
|
||||
ref.focus()
|
||||
})
|
||||
ref.addEventListener('keydown', this._handleKeyDown)
|
||||
this.componentWillUnmount = () =>
|
||||
ref.removeEventListener('keydown', this._handleKeyDown)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
body,
|
||||
@@ -209,9 +235,7 @@ class StrongConfirm extends Component {
|
||||
<div>
|
||||
<input
|
||||
className='form-control'
|
||||
ref={ref => {
|
||||
ref && ref.focus()
|
||||
}}
|
||||
ref={this._focusAndAddEventListener}
|
||||
onChange={this._onInputChange}
|
||||
/>
|
||||
</div>
|
||||
@@ -338,7 +362,7 @@ export const FormModal = decorate([
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
formId: generateRandomId,
|
||||
formId: generateId,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
// this computed can be used to generate a random id for the lifetime of the
|
||||
// component
|
||||
export const generateId = () =>
|
||||
`i${Math.random()
|
||||
.toString(36)
|
||||
.slice(2)}`
|
||||
|
||||
// TODO: remove these functions once the PR: https://github.com/JsCommunity/reaclette/pull/5 has been merged
|
||||
// It only supports native inputs
|
||||
export const linkState = (_, { target }) => () => ({
|
||||
|
||||
@@ -384,9 +384,8 @@ const GenericXoItem = connectStore(() => {
|
||||
return (state, props) => ({
|
||||
xoItem: getObject(state, props),
|
||||
})
|
||||
})(
|
||||
({ xoItem, ...props }) =>
|
||||
xoItem ? renderXoItem(xoItem, props) : renderXoUnknownItem()
|
||||
})(({ xoItem, ...props }) =>
|
||||
xoItem ? renderXoItem(xoItem, props) : renderXoUnknownItem()
|
||||
)
|
||||
|
||||
export const renderXoItemFromId = (id, props) => (
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react'
|
||||
import ActionButton from './action-button'
|
||||
import ActionRowButton from './action-row-button'
|
||||
|
||||
export const CAN_REPORT_BUG = process.env.XOA_PLAN > 1
|
||||
export const CAN_REPORT_BUG = __DEV__ && process.env.XOA_PLAN > 1
|
||||
|
||||
export const reportBug = ({ formatMessage, message, title }) => {
|
||||
const encodedTitle = encodeURIComponent(title)
|
||||
|
||||
@@ -281,8 +281,8 @@ const TimePicker = decorate([
|
||||
step === 1
|
||||
? optionsValues
|
||||
: step !== undefined
|
||||
? optionsValues.filter((_, i) => i % step === 0)
|
||||
: value.split(',').map(Number),
|
||||
? optionsValues.filter((_, i) => i % step === 0)
|
||||
: value.split(',').map(Number),
|
||||
|
||||
// '*' => 1
|
||||
// '*/2' => 2
|
||||
@@ -291,8 +291,8 @@ const TimePicker = decorate([
|
||||
value === '*'
|
||||
? 1
|
||||
: value.indexOf('/') === 1
|
||||
? +value.split('/')[1]
|
||||
: undefined,
|
||||
? +value.split('/')[1]
|
||||
: undefined,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
|
||||
@@ -63,8 +63,8 @@ const getIds = value =>
|
||||
value == null || isString(value) || isInteger(value)
|
||||
? value
|
||||
: isArray(value)
|
||||
? map(value, getIds)
|
||||
: value.id
|
||||
? map(value, getIds)
|
||||
: value.id
|
||||
|
||||
const getOption = (object, container) => ({
|
||||
label: container
|
||||
@@ -162,11 +162,11 @@ class GenericSelect extends React.Component {
|
||||
return isEmpty(missingObjects)
|
||||
? objects
|
||||
: withContainers
|
||||
? {
|
||||
...objects,
|
||||
missingObjects,
|
||||
}
|
||||
: [...objects, ...missingObjects]
|
||||
? {
|
||||
...objects,
|
||||
missingObjects,
|
||||
}
|
||||
: [...objects, ...missingObjects]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -620,13 +620,10 @@ export const SelectVdi = makeStoreSelect(
|
||||
)
|
||||
const getVdis = createGetObjectsOfType('VDI')
|
||||
.filter(
|
||||
createSelector(
|
||||
getSrs,
|
||||
getPredicate,
|
||||
(srs, predicate) =>
|
||||
predicate
|
||||
? vdi => srs[vdi.$SR] && predicate(vdi)
|
||||
: vdi => srs[vdi.$SR]
|
||||
createSelector(getSrs, getPredicate, (srs, predicate) =>
|
||||
predicate
|
||||
? vdi => srs[vdi.$SR] && predicate(vdi)
|
||||
: vdi => srs[vdi.$SR]
|
||||
)
|
||||
)
|
||||
.sort()
|
||||
@@ -691,9 +688,9 @@ export const SelectSubject = makeSubscriptionSelect(
|
||||
const set = newSubjects => {
|
||||
subjects = newSubjects
|
||||
/* We must wait for groups AND users options to be loaded,
|
||||
* or a previously setted value belonging to one type or another might be discarded
|
||||
* by the internal <GenericSelect>
|
||||
*/
|
||||
* or a previously setted value belonging to one type or another might be discarded
|
||||
* by the internal <GenericSelect>
|
||||
*/
|
||||
if (usersLoaded && groupsLoaded) {
|
||||
subscriber({
|
||||
xoObjects: subjects,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import add from 'lodash/add'
|
||||
import checkPermissions from 'xo-acl-resolver'
|
||||
import { check as checkPermissions } from 'xo-acl-resolver'
|
||||
import { createSelector as create } from 'reselect'
|
||||
import {
|
||||
filter,
|
||||
@@ -146,15 +146,14 @@ export const createFilter = (collection, predicate) =>
|
||||
_create2(
|
||||
collection,
|
||||
predicate,
|
||||
_createCollectionWrapper(
|
||||
(collection, predicate) =>
|
||||
predicate === false
|
||||
? isArrayLike(collection)
|
||||
? EMPTY_ARRAY
|
||||
: EMPTY_OBJECT
|
||||
: predicate
|
||||
? (isArrayLike(collection) ? filter : pickBy)(collection, predicate)
|
||||
: collection
|
||||
_createCollectionWrapper((collection, predicate) =>
|
||||
predicate === false
|
||||
? isArrayLike(collection)
|
||||
? EMPTY_ARRAY
|
||||
: EMPTY_OBJECT
|
||||
: predicate
|
||||
? (isArrayLike(collection) ? filter : pickBy)(collection, predicate)
|
||||
: collection
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
findIndex,
|
||||
forEach,
|
||||
get as getProperty,
|
||||
isArray,
|
||||
isEmpty,
|
||||
isFunction,
|
||||
map,
|
||||
@@ -211,28 +212,27 @@ const actionsShape = PropTypes.arrayOf(
|
||||
})
|
||||
)
|
||||
|
||||
const IndividualAction = decorate([
|
||||
const Action = decorate([
|
||||
provideState({
|
||||
computed: {
|
||||
disabled: ({ item }, { disabled, userData }) =>
|
||||
isFunction(disabled) ? disabled(item, userData) : disabled,
|
||||
handler: ({ item }, { handler, userData }) => () =>
|
||||
handler(item, userData),
|
||||
icon: ({ item }, { icon, userData }) =>
|
||||
isFunction(icon) ? icon(item, userData) : icon,
|
||||
item: (_, { item, grouped }) => (grouped ? [item] : item),
|
||||
label: ({ item }, { label, userData }) =>
|
||||
isFunction(label) ? label(item, userData) : label,
|
||||
level: ({ item }, { level, userData }) =>
|
||||
isFunction(level) ? level(item, userData) : level,
|
||||
disabled: ({ items }, { disabled, userData }) =>
|
||||
isFunction(disabled) ? disabled(items, userData) : disabled,
|
||||
handler: ({ items }, { handler, userData }) => () =>
|
||||
handler(items, userData),
|
||||
icon: ({ items }, { icon, userData }) =>
|
||||
isFunction(icon) ? icon(items, userData) : icon,
|
||||
items: (_, { items, grouped }) =>
|
||||
isArray(items) || !grouped ? items : [items],
|
||||
label: ({ items }, { label, userData }) =>
|
||||
isFunction(label) ? label(items, userData) : label,
|
||||
level: ({ items }, { level, userData }) =>
|
||||
isFunction(level) ? level(items, userData) : level,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state, redirectOnSuccess, userData }) => (
|
||||
<ActionRowButton
|
||||
btnStyle={state.level}
|
||||
data-item={state.item}
|
||||
data-userData={userData}
|
||||
disabled={state.disabled}
|
||||
handler={state.handler}
|
||||
icon={state.icon}
|
||||
@@ -242,42 +242,6 @@ const IndividualAction = decorate([
|
||||
),
|
||||
])
|
||||
|
||||
class GroupedAction extends Component {
|
||||
_getIsDisabled = createSelector(
|
||||
() => this.props.disabled,
|
||||
() => this.props.selectedItems,
|
||||
() => this.props.userData,
|
||||
(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
|
||||
return p.handler(p.selectedItems, p.userData)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { icon, level } = this.props
|
||||
|
||||
return (
|
||||
<ActionRowButton
|
||||
btnStyle={level}
|
||||
disabled={this._getIsDisabled()}
|
||||
handler={this._executeAction}
|
||||
icon={icon}
|
||||
tooltip={this._getLabel()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const LEVELS = [undefined, 'primary', 'warning', 'danger']
|
||||
// page number and sort info are optional for backward compatibility
|
||||
const URL_STATE_RE = /^(?:(\d+)(?:_(\d+)(_desc)?)?-)?(.*)$/
|
||||
@@ -502,8 +466,8 @@ export default class SortedTable extends Component {
|
||||
) {
|
||||
this.setState({
|
||||
highlighted:
|
||||
(itemIndex + visibleItems.length + 1) %
|
||||
visibleItems.length || 0,
|
||||
(itemIndex + visibleItems.length + 1) % visibleItems.length ||
|
||||
0,
|
||||
})
|
||||
}
|
||||
break
|
||||
@@ -515,8 +479,8 @@ export default class SortedTable extends Component {
|
||||
) {
|
||||
this.setState({
|
||||
highlighted:
|
||||
(itemIndex + visibleItems.length - 1) %
|
||||
visibleItems.length || 0,
|
||||
(itemIndex + visibleItems.length - 1) % visibleItems.length ||
|
||||
0,
|
||||
})
|
||||
}
|
||||
break
|
||||
@@ -792,12 +756,7 @@ export default class SortedTable extends Component {
|
||||
<div className='pull-right'>
|
||||
<ButtonGroup>
|
||||
{map(this._getIndividualActions(), (props, key) => (
|
||||
<IndividualAction
|
||||
{...props}
|
||||
item={item}
|
||||
key={key}
|
||||
userData={userData}
|
||||
/>
|
||||
<Action {...props} items={item} key={key} userData={userData} />
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
@@ -931,10 +890,10 @@ export default class SortedTable extends Component {
|
||||
<div className='pull-right'>
|
||||
<ButtonGroup>
|
||||
{map(groupedActions, (props, key) => (
|
||||
<GroupedAction
|
||||
<Action
|
||||
{...props}
|
||||
key={key}
|
||||
selectedItems={this._getSelectedItems()}
|
||||
items={this._getSelectedItems()}
|
||||
userData={userData}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -47,6 +47,17 @@ export addSubscriptions from './add-subscriptions'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const getVirtualizationModeLabel = vm => {
|
||||
const virtualizationMode =
|
||||
vm.virtualizationMode === 'hvm' && Boolean(vm.xenTools)
|
||||
? 'pvhvm'
|
||||
: vm.virtualizationMode
|
||||
|
||||
return VIRTUALIZATION_MODE_LABEL[virtualizationMode]
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const ensureArray = value => {
|
||||
if (value === undefined) {
|
||||
return []
|
||||
|
||||
@@ -149,7 +149,7 @@ class XoWeekChart extends Component {
|
||||
.selectAll('path')
|
||||
.remove()
|
||||
forEach(splittedData, data => {
|
||||
;svg
|
||||
svg
|
||||
.select('.horizon-area')
|
||||
.append('path')
|
||||
.datum(data)
|
||||
|
||||
@@ -63,6 +63,7 @@ export default class ChooseSrForEachVdisModal extends Component {
|
||||
onChange={this._onChangeMainSr}
|
||||
placeholder={_('chooseSrForEachVdisModalMainSr')}
|
||||
predicate={mainSrPredicate}
|
||||
required
|
||||
value={mainSr}
|
||||
/>
|
||||
<br />
|
||||
|
||||
@@ -193,7 +193,8 @@ export const resolveUrl = invoke(
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const createSubscription = cb => {
|
||||
const delay = 5e3
|
||||
const delay = 5e3 // 5s
|
||||
const clearCacheDelay = 6e5 // 10m
|
||||
|
||||
const subscribers = Object.create(null)
|
||||
let cache
|
||||
@@ -203,61 +204,69 @@ const createSubscription = cb => {
|
||||
|
||||
let running = false
|
||||
|
||||
const uninstall = () => {
|
||||
clearTimeout(timeout)
|
||||
const clearCache = () => {
|
||||
cache = undefined
|
||||
}
|
||||
|
||||
const uninstall = () => {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(clearCache, clearCacheDelay)
|
||||
}
|
||||
|
||||
const loop = () => {
|
||||
clearTimeout(timeout)
|
||||
|
||||
if (running) {
|
||||
return
|
||||
}
|
||||
|
||||
running = true
|
||||
_signIn.then(() => cb()).then(
|
||||
result => {
|
||||
running = false
|
||||
_signIn
|
||||
.then(() => cb())
|
||||
.then(
|
||||
result => {
|
||||
running = false
|
||||
|
||||
if (n === 0) {
|
||||
return uninstall()
|
||||
if (n === 0) {
|
||||
return uninstall()
|
||||
}
|
||||
|
||||
timeout = setTimeout(loop, delay)
|
||||
|
||||
if (!isEqual(result, cache)) {
|
||||
cache = result
|
||||
|
||||
forEach(subscribers, subscriber => {
|
||||
// A subscriber might have disappeared during iteration.
|
||||
//
|
||||
// E.g.: if a subscriber triggers the subscription of another.
|
||||
if (subscriber) {
|
||||
subscriber(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
error => {
|
||||
running = false
|
||||
|
||||
if (n === 0) {
|
||||
return uninstall()
|
||||
}
|
||||
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
timeout = setTimeout(loop, delay)
|
||||
|
||||
if (!isEqual(result, cache)) {
|
||||
cache = result
|
||||
|
||||
forEach(subscribers, subscriber => {
|
||||
// A subscriber might have disappeared during iteration.
|
||||
//
|
||||
// E.g.: if a subscriber triggers the subscription of another.
|
||||
if (subscriber) {
|
||||
subscriber(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
error => {
|
||||
running = false
|
||||
|
||||
if (n === 0) {
|
||||
return uninstall()
|
||||
}
|
||||
|
||||
console.error(error)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const subscribe = cb => {
|
||||
const id = nextId++
|
||||
subscribers[id] = cb
|
||||
|
||||
if (n++ !== 0) {
|
||||
if (cache !== undefined) {
|
||||
asap(() => cb(cache))
|
||||
}
|
||||
} else {
|
||||
if (cache !== undefined) {
|
||||
asap(() => cb(cache))
|
||||
}
|
||||
|
||||
if (n++ === 0) {
|
||||
loop()
|
||||
}
|
||||
|
||||
@@ -272,7 +281,6 @@ const createSubscription = cb => {
|
||||
|
||||
subscribe.forceRefresh = () => {
|
||||
if (n) {
|
||||
clearTimeout(timeout)
|
||||
loop()
|
||||
}
|
||||
}
|
||||
@@ -938,15 +946,21 @@ export const suspendVm = vm => _call('vm.suspend', { id: resolveId(vm) })
|
||||
|
||||
export const suspendVms = vms =>
|
||||
confirm({
|
||||
title: _('suspendVmsModalTitle', { nVms: vms.length }),
|
||||
body: _('suspendVmsModalMessage', { nVms: vms.length }),
|
||||
title: _('suspendVmsModalTitle', { vms: vms.length }),
|
||||
body: _('suspendVmsModalMessage', { vms: vms.length }),
|
||||
}).then(
|
||||
() =>
|
||||
Promise.all(map(vms, vm => _call('vm.suspend', { id: resolveId(vm) }))),
|
||||
noop
|
||||
)
|
||||
|
||||
export const resumeVm = vm => _call('vm.resume', { id: resolveId(vm) })
|
||||
export const pauseVm = vm => _call('vm.pause', { id: resolveId(vm) })
|
||||
|
||||
export const pauseVms = vms =>
|
||||
confirm({
|
||||
title: _('pauseVmsModalTitle', { vms: vms.length }),
|
||||
body: _('pauseVmsModalMessage', { vms: vms.length }),
|
||||
}).then(() => Promise.all(map(vms, pauseVm)), noop)
|
||||
|
||||
export const recoveryStartVm = vm =>
|
||||
_call('vm.recoveryStart', { id: resolveId(vm) })
|
||||
@@ -1035,6 +1049,16 @@ export const convertVmToTemplate = vm =>
|
||||
),
|
||||
}).then(() => _call('vm.convert', { id: resolveId(vm) }), noop)
|
||||
|
||||
export const changeVirtualizationMode = vm =>
|
||||
confirm({
|
||||
title: _('vmVirtualizationModeModalTitle'),
|
||||
body: _('vmVirtualizationModeModalBody'),
|
||||
}).then(() =>
|
||||
editVm(vm, {
|
||||
virtualizationMode: vm.virtualizationMode === 'hvm' ? 'pv' : 'hvm',
|
||||
})
|
||||
)
|
||||
|
||||
export const deleteTemplates = templates =>
|
||||
confirm({
|
||||
title: _('templateDeleteModalTitle', { templates: templates.length }),
|
||||
@@ -1198,17 +1222,17 @@ export const deleteVm = (vm, retryWithForce = true) =>
|
||||
})
|
||||
.then(() => _call('vm.delete', { id: resolveId(vm) }), noop)
|
||||
.catch(error => {
|
||||
if (forbiddenOperation.is(error) || !retryWithForce) {
|
||||
throw error
|
||||
if (retryWithForce && forbiddenOperation.is(error)) {
|
||||
return confirm({
|
||||
title: _('deleteVmBlockedModalTitle'),
|
||||
body: _('deleteVmBlockedModalMessage'),
|
||||
}).then(
|
||||
() => _call('vm.delete', { id: resolveId(vm), force: true }),
|
||||
noop
|
||||
)
|
||||
}
|
||||
|
||||
return confirm({
|
||||
title: _('deleteVmBlockedModalTitle'),
|
||||
body: _('deleteVmBlockedModalMessage'),
|
||||
}).then(
|
||||
() => _call('vm.delete', { id: resolveId(vm), force: true }),
|
||||
noop
|
||||
)
|
||||
throw error
|
||||
})
|
||||
|
||||
export const deleteVms = vms =>
|
||||
@@ -1986,9 +2010,9 @@ export const getRemote = remote =>
|
||||
)
|
||||
|
||||
export const createRemote = (name, url, options) =>
|
||||
_call('remote.create', { name, url, options })::tap(remote =>
|
||||
_call('remote.create', { name, url, options })::tap(remote => {
|
||||
testRemote(remote).catch(noop)
|
||||
)
|
||||
})
|
||||
|
||||
export const deleteRemote = remote =>
|
||||
_call('remote.delete', { id: resolveId(remote) })::tap(
|
||||
@@ -2020,9 +2044,9 @@ export const disableRemote = remote =>
|
||||
)
|
||||
|
||||
export const editRemote = (remote, { name, url, options }) =>
|
||||
_call('remote.set', resolveIds({ remote, name, url, options }))::tap(() =>
|
||||
_call('remote.set', resolveIds({ remote, name, url, options }))::tap(() => {
|
||||
testRemote(remote).catch(noop)
|
||||
)
|
||||
})
|
||||
|
||||
export const listRemote = remote =>
|
||||
_call('remote.list', resolveIds({ id: remote }))::tap(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user