Compare commits
212 Commits
vhd-lib-v0
...
xo-web-v5.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19d191a472 | ||
|
|
d906fec236 | ||
|
|
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 | ||
|
|
abb80270ad | ||
|
|
72e93384a5 | ||
|
|
663b1b76ec | ||
|
|
24b8c671fa | ||
|
|
986fec1cd3 | ||
|
|
f6c2cbc5cf | ||
|
|
289ed89a78 | ||
|
|
73de421d47 | ||
|
|
dc1eb82295 | ||
|
|
6629c12166 | ||
|
|
ec5bc1db95 | ||
|
|
ac2c40c842 | ||
|
|
61bf669252 | ||
|
|
4105c53155 | ||
|
|
aeab2b2a08 | ||
|
|
95e33ee612 | ||
|
|
093bda7039 | ||
|
|
4e35b19ac5 | ||
|
|
244d8a51e8 | ||
|
|
9d6cc77cc8 | ||
|
|
d5e0150880 | ||
|
|
5cf29a98b3 | ||
|
|
165c2262c0 | ||
|
|
74f5d2e0cd | ||
|
|
2d93456f52 | ||
|
|
fd401ca335 | ||
|
|
97ba93a9ad | ||
|
|
0788c25710 | ||
|
|
82bba951db | ||
|
|
6efd611b80 | ||
|
|
b7d43b42b9 | ||
|
|
801b71d9ae | ||
|
|
0ff7c2188a | ||
|
|
bc1667440f | ||
|
|
227b464a8e | ||
|
|
f6c43650b4 | ||
|
|
597689fde0 | ||
|
|
da6b71fde8 | ||
|
|
5f2590c858 | ||
|
|
37b0867151 | ||
|
|
85031cfb9d | ||
|
|
a13f86fb7c | ||
|
|
7cbc5e642f | ||
|
|
48d4abc259 | ||
|
|
c805f3b1a7 | ||
|
|
40212582a9 | ||
|
|
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.19.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) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/fs",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "The File System for Xen Orchestra backups.",
|
||||
"keywords": [],
|
||||
@@ -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.10.1",
|
||||
"through2": "^2.0.3",
|
||||
"promise-toolbox": "^0.11.0",
|
||||
"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,11 +20,18 @@ type File = FileDescriptor | string
|
||||
|
||||
const checksumFile = file => file + '.checksum'
|
||||
|
||||
export const DEFAULT_TIMEOUT = 10000
|
||||
// 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
|
||||
constructor (remote: any) {
|
||||
_timeout: number
|
||||
|
||||
constructor(remote: any, options: Object = {}) {
|
||||
if (remote.url === 'test://') {
|
||||
this._remote = remote
|
||||
} else {
|
||||
@@ -30,36 +40,37 @@ export default class RemoteHandlerAbstract {
|
||||
throw new Error('Incorrect remote type')
|
||||
}
|
||||
}
|
||||
;({ 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 {
|
||||
@@ -84,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
|
||||
@@ -114,20 +129,23 @@ 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 = {}
|
||||
) {
|
||||
let p = timeout.call(this._rename(oldPath, newPath), DEFAULT_TIMEOUT)
|
||||
oldPath = normalizePath(oldPath)
|
||||
newPath = normalizePath(newPath)
|
||||
|
||||
let p = timeout.call(this._rename(oldPath, newPath), this._timeout)
|
||||
if (checksum) {
|
||||
p = Promise.all([
|
||||
p,
|
||||
@@ -137,18 +155,53 @@ 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[]> {
|
||||
let entries = await timeout.call(this._list(dir), DEFAULT_TIMEOUT)
|
||||
dir = normalizePath(dir)
|
||||
|
||||
let entries = await timeout.call(this._list(dir), this._timeout)
|
||||
if (filter !== undefined) {
|
||||
entries = entries.filter(filter)
|
||||
}
|
||||
@@ -162,17 +215,20 @@ 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), DEFAULT_TIMEOUT)
|
||||
.call(this._createReadStream(file, options), this._timeout)
|
||||
.then(stream => {
|
||||
// detect early errors
|
||||
let promise = fromEvent(stream, 'readable')
|
||||
@@ -224,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), DEFAULT_TIMEOUT),
|
||||
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> {
|
||||
await timeout.call(this._closeFile(fd.fd), DEFAULT_TIMEOUT)
|
||||
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()
|
||||
)
|
||||
@@ -258,17 +315,20 @@ 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, {
|
||||
flags: 'wx',
|
||||
...options,
|
||||
}),
|
||||
DEFAULT_TIMEOUT
|
||||
this._timeout
|
||||
)
|
||||
|
||||
if (!checksum) {
|
||||
@@ -292,30 +352,35 @@ 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)))
|
||||
}
|
||||
|
||||
await timeout.call(this._unlink(file), DEFAULT_TIMEOUT)
|
||||
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), DEFAULT_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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
import { TimeoutError } from 'promise-toolbox'
|
||||
|
||||
import AbstractHandler, { DEFAULT_TIMEOUT } from './abstract'
|
||||
import AbstractHandler from './abstract'
|
||||
|
||||
const TIMEOUT = 10e3
|
||||
|
||||
class TestHandler extends AbstractHandler {
|
||||
constructor (impl) {
|
||||
super({ url: 'test://' })
|
||||
constructor(impl) {
|
||||
super({ url: 'test://' }, { timeout: TIMEOUT })
|
||||
|
||||
Object.keys(impl).forEach(method => {
|
||||
this[`_${method}`] = impl[method]
|
||||
@@ -15,97 +17,97 @@ class TestHandler extends AbstractHandler {
|
||||
}
|
||||
|
||||
describe('rename()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
it(`throws in case of timeout`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
rename: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.rename('oldPath', 'newPath')
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
jest.advanceTimersByTime(TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('list()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
it(`throws in case of timeout`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
list: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.list()
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
jest.advanceTimersByTime(TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createReadStream()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
it(`throws in case of timeout`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
createReadStream: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.createReadStream('file')
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
jest.advanceTimersByTime(TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('openFile()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
it(`throws in case of timeout`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
openFile: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.openFile('path')
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
jest.advanceTimersByTime(TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('closeFile()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
it(`throws in case of timeout`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
closeFile: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.closeFile({ fd: undefined, path: '' })
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
jest.advanceTimersByTime(TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createOutputStream()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
it(`throws in case of timeout`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
createOutputStream: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.createOutputStream('File')
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
jest.advanceTimersByTime(TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unlink()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
it(`throws in case of timeout`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
unlink: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.unlink('')
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
jest.advanceTimersByTime(TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSize()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
it(`throws in case of timeout`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
getSize: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.getSize('')
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
jest.advanceTimersByTime(TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
|
||||
// $FlowFixMe
|
||||
import through2 from 'through2'
|
||||
import { createHash } from 'crypto'
|
||||
import { defer, fromEvent } from 'promise-toolbox'
|
||||
|
||||
@@ -14,7 +14,7 @@ const HANDLERS = {
|
||||
nfs: RemoteHandlerNfs,
|
||||
}
|
||||
|
||||
export const getHandler = (remote: Remote): RemoteHandler => {
|
||||
export const getHandler = (remote: Remote, ...rest: any): RemoteHandler => {
|
||||
// FIXME: should be done in xo-remote-parser.
|
||||
const type = remote.url.split('://')[0]
|
||||
|
||||
@@ -22,5 +22,5 @@ export const getHandler = (remote: Remote): RemoteHandler => {
|
||||
if (!Handler) {
|
||||
throw new Error('Unhandled remote type')
|
||||
}
|
||||
return new Handler(remote)
|
||||
return new Handler(remote, ...rest)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
import execa from 'execa'
|
||||
import fs from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
|
||||
import LocalHandler from './local'
|
||||
|
||||
const DEFAULT_NFS_OPTIONS = 'vers=3'
|
||||
|
||||
export default class NfsHandler extends LocalHandler {
|
||||
get type () {
|
||||
constructor(
|
||||
remote,
|
||||
{ mountsDir = join(tmpdir(), 'xo-fs-mounts'), ...opts } = {}
|
||||
) {
|
||||
super(remote, opts)
|
||||
|
||||
this._realPath = join(mountsDir, remote.id)
|
||||
}
|
||||
|
||||
get type() {
|
||||
return 'nfs'
|
||||
}
|
||||
|
||||
_getRealPath () {
|
||||
return `/run/xo-server/mounts/${this._remote.id}`
|
||||
_getRealPath() {
|
||||
return this._realPath
|
||||
}
|
||||
|
||||
async _mount () {
|
||||
async _mount() {
|
||||
await fs.ensureDir(this._getRealPath())
|
||||
const { host, path, port, options } = this._remote
|
||||
return execa(
|
||||
@@ -33,23 +44,23 @@ export default class NfsHandler extends LocalHandler {
|
||||
},
|
||||
}
|
||||
).catch(error => {
|
||||
if (!error.stderr.includes('already mounted')) {
|
||||
if (
|
||||
error == null ||
|
||||
typeof error.stderr !== 'string' ||
|
||||
!error.stderr.includes('already mounted')
|
||||
) {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 (_) {
|
||||
@@ -57,13 +68,17 @@ export default class NfsHandler extends LocalHandler {
|
||||
}
|
||||
}
|
||||
|
||||
async _umount () {
|
||||
async _umount() {
|
||||
await execa('umount', ['--force', this._getRealPath()], {
|
||||
env: {
|
||||
LANG: 'C',
|
||||
},
|
||||
}).catch(error => {
|
||||
if (!error.stderr.includes('not mounted')) {
|
||||
if (
|
||||
error == null ||
|
||||
typeof error.stderr !== 'string' ||
|
||||
!error.stderr.includes('not mounted')
|
||||
) {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
super(remote)
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/log",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.4",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
@@ -18,7 +18,9 @@
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
"configure.js",
|
||||
"dist/",
|
||||
"transports/"
|
||||
],
|
||||
"browserslist": [
|
||||
">2%"
|
||||
@@ -28,7 +30,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.10.1"
|
||||
"promise-toolbox": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
dist/transports
|
||||
1
@xen-orchestra/log/transports/console.js
Normal file
1
@xen-orchestra/log/transports/console.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../dist/transports/console.js')
|
||||
1
@xen-orchestra/log/transports/email.js
Normal file
1
@xen-orchestra/log/transports/email.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../dist/transports/email.js')
|
||||
1
@xen-orchestra/log/transports/memory.js
Normal file
1
@xen-orchestra/log/transports/memory.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../dist/transports/memory.js')
|
||||
1
@xen-orchestra/log/transports/syslog.js
Normal file
1
@xen-orchestra/log/transports/syslog.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../dist/transports/syslog.js')
|
||||
76
CHANGELOG.md
76
CHANGELOG.md
@@ -4,6 +4,80 @@
|
||||
|
||||
### 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
|
||||
|
||||
- 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
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Backup NG] Increase timeout in stale remotes detection to limit false positives (PR [#3632](https://github.com/vatesfr/xen-orchestra/pull/3632))
|
||||
- Fix re-registration issue ([4e35b19ac](https://github.com/vatesfr/xen-orchestra/commit/4e35b19ac56c60f61c0e771cde70a50402797b8a))
|
||||
- [Backup NG logs] Fix started jobs filter [#3636](https://github.com/vatesfr/xen-orchestra/issues/3636) (PR [#3641](https://github.com/vatesfr/xen-orchestra/pull/3641))
|
||||
- [New VM] CPU and memory user inputs were ignored since previous release [#3644](https://github.com/vatesfr/xen-orchestra/issues/3644) (PR [#3646](https://github.com/vatesfr/xen-orchestra/pull/3646))
|
||||
|
||||
### Released packages
|
||||
|
||||
- @xen-orchestra/fs v0.4.1
|
||||
- xo-server v5.29.4
|
||||
- xo-web v5.29.3
|
||||
|
||||
## **5.28.0** (2018-10-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Usage Report] Add IOPS read/write/total per VM [#3309](https://github.com/vatesfr/xen-orchestra/issues/3309) (PR [#3455](https://github.com/vatesfr/xen-orchestra/pull/3455))
|
||||
- [Self service] Sort resource sets by name (PR [#3507](https://github.com/vatesfr/xen-orchestra/pull/3507))
|
||||
- [Usage Report] Add top 3 SRs which use the most IOPS read/write/total [#3306](https://github.com/vatesfr/xen-orchestra/issues/3306) (PR [#3508](https://github.com/vatesfr/xen-orchestra/pull/3508))
|
||||
@@ -56,7 +130,7 @@
|
||||
- xo-server v5.29.0
|
||||
- xo-web v5.29.0
|
||||
|
||||
## **5.28.0** (2018-10-05)
|
||||
## **5.27.2** (2018-10-05)
|
||||
|
||||
### Enhancements
|
||||
|
||||
|
||||
@@ -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,21 +7,22 @@
|
||||
"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",
|
||||
"lodash": "^4.17.4",
|
||||
"prettier": "^1.10.2",
|
||||
"promise-toolbox": "^0.10.1",
|
||||
"promise-toolbox": "^0.11.0",
|
||||
"sorted-object": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^0.4.0",
|
||||
"@xen-orchestra/fs": "^0.4.1",
|
||||
"cli-progress": "^2.0.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"struct-fu": "^1.2.0",
|
||||
@@ -40,7 +40,7 @@
|
||||
"cross-env": "^5.1.3",
|
||||
"execa": "^1.0.0",
|
||||
"index-modules": "^0.3.0",
|
||||
"promise-toolbox": "^0.10.1",
|
||||
"promise-toolbox": "^0.11.0",
|
||||
"rimraf": "^2.6.1",
|
||||
"tmp": "^0.0.33"
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"from2": "^2.3.0",
|
||||
"fs-extra": "^7.0.0",
|
||||
"limit-concurrency-decorator": "^0.4.0",
|
||||
"promise-toolbox": "^0.10.1",
|
||||
"promise-toolbox": "^0.11.0",
|
||||
"struct-fu": "^1.2.0",
|
||||
"uuid": "^3.0.1"
|
||||
},
|
||||
@@ -34,7 +34,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"@xen-orchestra/fs": "^0.4.0",
|
||||
"@xen-orchestra/fs": "^0.4.1",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"execa": "^1.0.0",
|
||||
|
||||
@@ -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.19.0",
|
||||
"version": "0.22.0",
|
||||
"license": "ISC",
|
||||
"description": "Connector to the Xen API",
|
||||
"keywords": [
|
||||
@@ -39,13 +39,13 @@
|
||||
"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",
|
||||
"minimist": "^1.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"promise-toolbox": "^0.10.1",
|
||||
"promise-toolbox": "^0.11.0",
|
||||
"pw": "0.0.4",
|
||||
"xmlrpc": "^1.3.2",
|
||||
"xo-collection": "^0.4.1"
|
||||
|
||||
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
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"nice-pipe": "0.0.0",
|
||||
"pretty-ms": "^4.0.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"promise-toolbox": "^0.10.1",
|
||||
"promise-toolbox": "^0.11.0",
|
||||
"pump": "^3.0.0",
|
||||
"pw": "^0.0.4",
|
||||
"strip-indent": "^2.0.0",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
"inquirer": "^6.0.0",
|
||||
"ldapjs": "^1.0.1",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.10.1"
|
||||
"promise-toolbox": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"dependencies": {
|
||||
"nodemailer": "^4.4.1",
|
||||
"nodemailer-markdown": "^1.0.1",
|
||||
"promise-toolbox": "^0.10.1"
|
||||
"promise-toolbox": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"promise-toolbox": "^0.10.1",
|
||||
"promise-toolbox": "^0.11.0",
|
||||
"slack-node": "^0.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-usage-report",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
@@ -36,11 +36,12 @@
|
||||
"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",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.10.1"
|
||||
"promise-toolbox": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.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?
|
||||
@@ -34,6 +42,12 @@
|
||||
// Necessary for external authentication providers.
|
||||
"createUserOnFirstSignin": true,
|
||||
|
||||
"remoteOptions": {
|
||||
"mountsDir": "/run/xo-server/mounts",
|
||||
|
||||
"timeout": 600e3
|
||||
},
|
||||
|
||||
// Whether API logs should contains the full request/response on
|
||||
// errors.
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server",
|
||||
"version": "5.28.0",
|
||||
"version": "5.31.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -34,11 +34,11 @@
|
||||
"@xen-orchestra/async-map": "^0.0.0",
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/emit-async": "^0.0.0",
|
||||
"@xen-orchestra/fs": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.1.0",
|
||||
"@xen-orchestra/fs": "^0.4.1",
|
||||
"@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",
|
||||
@@ -95,7 +95,7 @@
|
||||
"passport": "^0.4.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"pretty-format": "^23.0.0",
|
||||
"promise-toolbox": "^0.10.1",
|
||||
"promise-toolbox": "^0.11.0",
|
||||
"proxy-agent": "^3.0.0",
|
||||
"pug": "^2.0.0-rc.4",
|
||||
"pump": "^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.19.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,12 +59,10 @@ export async function create (params) {
|
||||
const xapi = this.getXapi(template)
|
||||
|
||||
const objectIds = [template.id]
|
||||
const cpus = extract(params, 'CPUs')
|
||||
const memoryMax = extract(params, 'memoryMax')
|
||||
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 = {}
|
||||
@@ -153,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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,7 +325,7 @@ create.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
async function delete_ ({
|
||||
async function delete_({
|
||||
delete_disks, // eslint-disable-line camelcase
|
||||
force,
|
||||
forceDeleteDefaultTemplate,
|
||||
@@ -375,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(
|
||||
@@ -412,7 +403,7 @@ export { delete_ as delete }
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function ejectCd ({ vm }) {
|
||||
export async function ejectCd({ vm }) {
|
||||
await this.getXapi(vm).ejectCdFromVm(vm._xapiId)
|
||||
}
|
||||
|
||||
@@ -426,14 +417,14 @@ ejectCd.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function insertCd ({ vm, vdi, force }) {
|
||||
export async function insertCd({ vm, vdi, force = true }) {
|
||||
await this.getXapi(vm).insertCdIntoVm(vdi._xapiId, vm._xapiId, { force })
|
||||
}
|
||||
|
||||
insertCd.params = {
|
||||
id: { type: 'string' },
|
||||
cd_id: { type: 'string' },
|
||||
force: { type: 'boolean' },
|
||||
force: { type: 'boolean', optional: true },
|
||||
}
|
||||
|
||||
insertCd.resolve = {
|
||||
@@ -445,7 +436,7 @@ insertCd.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function migrate ({
|
||||
export async function migrate({
|
||||
vm,
|
||||
host,
|
||||
sr,
|
||||
@@ -477,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,
|
||||
@@ -523,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
|
||||
@@ -614,6 +603,8 @@ set.params = {
|
||||
// Emulate HVM C000 PCI device for Windows Update to fetch or update PV drivers
|
||||
hasVendorDevice: { type: 'boolean', optional: true },
|
||||
|
||||
expNestedHvm: { type: 'boolean', optional: true },
|
||||
|
||||
// Move the vm In to/Out of Self Service
|
||||
resourceSet: { type: ['string', 'null'], optional: true },
|
||||
|
||||
@@ -629,7 +620,7 @@ set.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function restart ({ vm, force }) {
|
||||
export async function restart({ vm, force = false }) {
|
||||
const xapi = this.getXapi(vm)
|
||||
|
||||
if (force) {
|
||||
@@ -641,7 +632,7 @@ export async function restart ({ vm, force }) {
|
||||
|
||||
restart.params = {
|
||||
id: { type: 'string' },
|
||||
force: { type: 'boolean' },
|
||||
force: { type: 'boolean', optional: true },
|
||||
}
|
||||
|
||||
restart.resolve = {
|
||||
@@ -650,7 +641,7 @@ restart.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const clone = defer(async function (
|
||||
export const clone = defer(async function(
|
||||
$defer,
|
||||
{ vm, name, full_copy: fullCopy }
|
||||
) {
|
||||
@@ -692,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)
|
||||
@@ -733,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)
|
||||
}
|
||||
@@ -760,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()}` }
|
||||
) {
|
||||
@@ -788,7 +773,7 @@ snapshot.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function rollingDeltaBackup ({
|
||||
export function rollingDeltaBackup({
|
||||
vm,
|
||||
remote,
|
||||
tag,
|
||||
@@ -820,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) => {
|
||||
@@ -851,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)
|
||||
}
|
||||
|
||||
@@ -869,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)
|
||||
}
|
||||
@@ -891,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 })
|
||||
}
|
||||
|
||||
@@ -912,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)
|
||||
}
|
||||
|
||||
@@ -933,7 +918,7 @@ importBackup.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function rollingBackup ({
|
||||
export function rollingBackup({
|
||||
vm,
|
||||
remoteId,
|
||||
tag,
|
||||
@@ -971,7 +956,7 @@ rollingBackup.description =
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function rollingDrCopy ({
|
||||
export function rollingDrCopy({
|
||||
vm,
|
||||
pool,
|
||||
sr,
|
||||
@@ -1025,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)
|
||||
}
|
||||
|
||||
@@ -1046,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
|
||||
@@ -1081,7 +1066,7 @@ stop.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function suspend ({ vm }) {
|
||||
export async function suspend({ vm }) {
|
||||
await this.getXapi(vm).call('VM.suspend', vm._xapiRef)
|
||||
}
|
||||
|
||||
@@ -1095,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)
|
||||
}
|
||||
|
||||
@@ -1109,7 +1108,7 @@ resume.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function revert ({ snapshot, snapshotBefore }) {
|
||||
export function revert({ snapshot, snapshotBefore }) {
|
||||
return this.getXapi(snapshot).revertVm(snapshot._xapiId, snapshotBefore)
|
||||
}
|
||||
|
||||
@@ -1124,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,
|
||||
})
|
||||
@@ -1141,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)
|
||||
}
|
||||
@@ -1172,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
|
||||
@@ -1187,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),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1249,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'],
|
||||
}
|
||||
|
||||
@@ -1265,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,
|
||||
@@ -1294,7 +1274,7 @@ attachDisk.resolve = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: implement resource sets
|
||||
export async function createInterface ({
|
||||
export async function createInterface({
|
||||
vm,
|
||||
network,
|
||||
position,
|
||||
@@ -1307,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
|
||||
@@ -1365,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)
|
||||
@@ -1382,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')
|
||||
@@ -1397,7 +1375,7 @@ detachPci.resolve = {
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function stats ({ vm, granularity }) {
|
||||
export function stats({ vm, granularity }) {
|
||||
return this.getXapiVmStats(vm._xapiId, granularity)
|
||||
}
|
||||
|
||||
@@ -1417,7 +1395,7 @@ stats.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function setBootOrder ({ vm, order }) {
|
||||
export async function setBootOrder({ vm, order }) {
|
||||
const xapi = this.getXapi(vm)
|
||||
|
||||
order = { order }
|
||||
@@ -1440,7 +1418,7 @@ setBootOrder.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function recoveryStart ({ vm }) {
|
||||
export function recoveryStart({ vm }) {
|
||||
return this.getXapi(vm).startVmOnCd(vm._xapiId)
|
||||
}
|
||||
|
||||
@@ -1454,7 +1432,7 @@ recoveryStart.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function getCloudInitConfig ({ template }) {
|
||||
export function getCloudInitConfig({ template }) {
|
||||
return this.getXapi(template).getCloudInitConfig(template._xapiId)
|
||||
}
|
||||
|
||||
@@ -1468,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
|
||||
@@ -1495,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,
|
||||
@@ -1518,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)
|
||||
|
||||
|
||||
15
packages/xo-server/src/schemas/log/taskInfo.js
Normal file
15
packages/xo-server/src/schemas/log/taskInfo.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
$schema: 'http://json-schema.org/draft-04/schema#',
|
||||
type: 'object',
|
||||
properties: {
|
||||
event: {
|
||||
enum: ['task.info'],
|
||||
},
|
||||
taskId: {
|
||||
type: 'string',
|
||||
description: 'identifier of the parent task or job',
|
||||
},
|
||||
data: {},
|
||||
},
|
||||
required: ['event', 'taskId'],
|
||||
}
|
||||
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) {
|
||||
@@ -302,7 +311,7 @@ const TRANSFORMS = {
|
||||
version: version && parseXml(version).docker_version,
|
||||
}
|
||||
})(),
|
||||
|
||||
expNestedHvm: obj.platform['exp-nested-hvm'] === 'true',
|
||||
high_availability: obj.ha_restart_priority,
|
||||
|
||||
memory: (function () {
|
||||
@@ -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)
|
||||
|
||||
@@ -383,8 +399,16 @@ export default {
|
||||
|
||||
hasVendorDevice: true,
|
||||
|
||||
expNestedHvm: {
|
||||
set(expNestedHvm, vm) {
|
||||
return this._updateObjectMapProperty(vm, 'platform', {
|
||||
'exp-nested-hvm': expNestedHvm ? 'true' : null,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
nicType: {
|
||||
set (nicType, vm) {
|
||||
set(nicType, vm) {
|
||||
return this._updateObjectMapProperty(vm, 'platform', {
|
||||
nic_type: nicType,
|
||||
})
|
||||
@@ -392,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}`
|
||||
@@ -403,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}`
|
||||
@@ -414,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)
|
||||
}
|
||||
|
||||
@@ -129,6 +129,13 @@ export default {
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'task.info') {
|
||||
const parent = started[data.taskId]
|
||||
parent !== undefined &&
|
||||
(parent.infos || (parent.infos = [])).push({
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'jobCall.start') {
|
||||
const parent = started[data.runJobId]
|
||||
if (parent !== undefined) {
|
||||
|
||||
@@ -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) {
|
||||
@@ -470,6 +470,28 @@ const extractIdsFromSimplePattern = (pattern: mixed) => {
|
||||
// - copy in delta mode: `Continuous Replication`
|
||||
// - copy in full mode: `Disaster Recovery`
|
||||
// - imported from backup: `restored from backup`
|
||||
//
|
||||
// Task logs emitted in a backup execution:
|
||||
//
|
||||
// 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)
|
||||
// │ ├─ task.start(message: 'snapshot')
|
||||
// │ │ └─ task.end
|
||||
// │ ├─ task.start(message: 'export', data: { type: 'SR' | 'remote', id: string })
|
||||
// │ │ ├─ task.warning(message: string)
|
||||
// │ │ ├─ task.start(message: 'transfer')
|
||||
// │ │ │ ├─ task.warning(message: string)
|
||||
// │ │ │ └─ task.end(result: { size: number })
|
||||
// │ │ ├─ task.start(message: 'merge')
|
||||
// │ │ │ ├─ task.warning(message: string)
|
||||
// │ │ │ └─ task.end(result: { size: number })
|
||||
// │ │ └─ task.end
|
||||
// │ └─ task.end
|
||||
// └─ job.end
|
||||
export default class BackupNg {
|
||||
_app: {
|
||||
createJob: ($Diff<BackupJob, {| id: string |}>) => Promise<BackupJob>,
|
||||
@@ -521,31 +543,25 @@ export default class BackupNg {
|
||||
vmsId !== undefined ||
|
||||
(vmsId = extractIdsFromSimplePattern(vmsPattern)) !== undefined
|
||||
) {
|
||||
vms = vmsId
|
||||
.map(id => {
|
||||
try {
|
||||
return 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),
|
||||
})
|
||||
}
|
||||
vms = {}
|
||||
const missingVms = []
|
||||
vmsId.forEach(id => {
|
||||
try {
|
||||
vms[id] = app.getObject(id, 'VM')
|
||||
} catch (error) {
|
||||
missingVms.push(id)
|
||||
}
|
||||
})
|
||||
|
||||
if (missingVms.length !== 0) {
|
||||
logger.warning('missingVms', {
|
||||
event: 'task.warning',
|
||||
taskId: runJobId,
|
||||
data: {
|
||||
vms: missingVms,
|
||||
},
|
||||
})
|
||||
.filter(vm => vm !== undefined)
|
||||
}
|
||||
} else {
|
||||
vms = app.getObjects({
|
||||
filter: createPredicate({
|
||||
@@ -646,6 +662,13 @@ export default class BackupNg {
|
||||
])
|
||||
if (concurrency !== 0) {
|
||||
handleVm = limitConcurrency(concurrency)(handleVm)
|
||||
logger.notice('vms', {
|
||||
event: 'task.info',
|
||||
taskId: runJobId,
|
||||
data: {
|
||||
vms: Object.keys(vms),
|
||||
},
|
||||
})
|
||||
}
|
||||
await asyncMap(vms, handleVm)
|
||||
}
|
||||
@@ -711,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,16 +1,24 @@
|
||||
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) {
|
||||
constructor (xo, { remoteOptions }) {
|
||||
this._remoteOptions = remoteOptions
|
||||
this._remotes = new Remotes({
|
||||
connection: xo._redis,
|
||||
prefix: 'xo:remote',
|
||||
@@ -29,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, {}))
|
||||
})
|
||||
@@ -46,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) {
|
||||
@@ -57,7 +65,7 @@ export default class {
|
||||
const handlers = this._handlers
|
||||
let handler = handlers[id]
|
||||
if (handler === undefined) {
|
||||
handler = handlers[id] = getHandler(remote)
|
||||
handler = handlers[id] = getHandler(remote, this._remoteOptions)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -77,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')
|
||||
@@ -88,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,
|
||||
@@ -119,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,10 @@ export default class Workers {
|
||||
return this._worker
|
||||
}
|
||||
|
||||
constructor (app) {
|
||||
constructor (app, config) {
|
||||
app.on('start', () => {
|
||||
process.env.XO_CONFIG = JSON.stringify(config)
|
||||
|
||||
this._worker = new Worker(require.resolve('./worker'))
|
||||
})
|
||||
app.on('stop', () => this._worker.end())
|
||||
|
||||
@@ -5,8 +5,13 @@ 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 (
|
||||
parentRemote: Remote,
|
||||
parentPath: string,
|
||||
@@ -14,9 +19,9 @@ export function mergeVhd (
|
||||
childPath: string
|
||||
) {
|
||||
return mergeVhd_(
|
||||
getHandler(parentRemote),
|
||||
getHandler(parentRemote, config.remoteOptions),
|
||||
parentPath,
|
||||
getHandler(childRemote),
|
||||
getHandler(childRemote, config.remoteOptions),
|
||||
childPath
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"child-process-promise": "^2.0.3",
|
||||
"core-js": "3.0.0-beta.3",
|
||||
"pipette": "^0.9.3",
|
||||
"promise-toolbox": "^0.10.1",
|
||||
"promise-toolbox": "^0.11.0",
|
||||
"tmp": "^0.0.33",
|
||||
"vhd-lib": "^0.4.0"
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user