Merge branch 'next-release'

This commit is contained in:
Julien Fontanet 2015-04-20 17:40:01 +02:00
commit da58458fb7
32 changed files with 2115 additions and 1818 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
/dist/
/node_modules/ /node_modules/
npm-debug.log npm-debug.log
.xo-server.* .xo-server.*

View File

@ -16,7 +16,21 @@ ___
## Installation ## Installation
Manual install procedure is [available here](https://github.com/vatesfr/xo/blob/master/doc/installation/README.md#installation) Manual install procedure is [available here](https://github.com/vatesfr/xo/blob/master/doc/installation/README.md#installation).
## Compilation
Production build:
```
$ npm run build
```
Development build:
```
$ npm run dev
```
## How to report a bug? ## How to report a bug?

View File

@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict'; 'use strict'
//==================================================================== // ===================================================================
require('exec-promise')(require('../')); require('exec-promise')(require('../'))

65
gulpfile.js Normal file
View File

@ -0,0 +1,65 @@
'use strict'
// ===================================================================
var gulp = require('gulp')
var babel = require('gulp-babel')
var coffee = require('gulp-coffee')
var plumber = require('gulp-plumber')
var sourceMaps = require('gulp-sourcemaps')
var watch = require('gulp-watch')
// ===================================================================
var SRC_DIR = __dirname + '/src'
var DIST_DIR = __dirname + '/dist'
var PRODUCTION = process.argv.indexOf('--production') !== -1
// ===================================================================
function src (patterns) {
return PRODUCTION ?
gulp.src(patterns, {
base: SRC_DIR,
cwd: SRC_DIR
}) :
watch(patterns, {
base: SRC_DIR,
cwd: SRC_DIR,
ignoreInitial: false,
verbose: true
})
.pipe(plumber())
}
// ===================================================================
gulp.task(function buildCoffee () {
return src('**/*.coffee')
.pipe(sourceMaps.init())
.pipe(coffee({
bare: true
}))
.pipe(sourceMaps.write('.'))
.pipe(gulp.dest(DIST_DIR))
})
gulp.task(function buildEs6 () {
return src('**/*.js')
.pipe(sourceMaps.init())
.pipe(babel({
compact: PRODUCTION,
comments: false,
optional: [
'runtime'
]
}))
.pipe(sourceMaps.write('.'))
.pipe(gulp.dest(DIST_DIR))
})
// ===================================================================
gulp.task('build', gulp.parallel('buildCoffee', 'buildEs6'))

View File

@ -1,24 +1,14 @@
'use strict'; 'use strict'
//==================================================================== // ===================================================================
// Enable xo logs by default.
if (process.env.DEBUG === undefined) { if (process.env.DEBUG === undefined) {
process.env.DEBUG = 'xo:*'; process.env.DEBUG = 'xen-api,xo:*'
} }
var debug = require('debug')('xo:runner'); // Enable source maps support for traces.
require('source-map-support').install()
//==================================================================== // Import the real main module.
module.exports = require('./dist')
// Some modules are written in CoffeeScript.
debug('Loading CoffeeScript...');
require('coffee-script/register');
// Some modules are written in ES6.
debug('Loading Babel (ES6 support)...');
require('babel/register')({
ignore: /xo-.*\/node_modules/
});
debug('Loading main module...');
module.exports = require('./src');

View File

@ -15,6 +15,11 @@
}, },
"author": "Julien Fontanet <julien.fontanet@vates.fr>", "author": "Julien Fontanet <julien.fontanet@vates.fr>",
"preferGlobal": true, "preferGlobal": true,
"files": [
"bin/",
"dist/",
"index.js"
],
"directories": { "directories": {
"bin": "bin" "bin": "bin"
}, },
@ -24,10 +29,9 @@
}, },
"dependencies": { "dependencies": {
"app-conf": "^0.3.4", "app-conf": "^0.3.4",
"babel": "^4.7.13", "babel-runtime": "^5",
"base64url": "1.0.4", "base64url": "1.0.4",
"bluebird": "^2.9.14", "bluebird": "^2.9.14",
"coffee-script": "~1.9.1",
"compiled-accessors": "^0.2.0", "compiled-accessors": "^0.2.0",
"connect": "^3.3.5", "connect": "^3.3.5",
"debug": "^2.1.3", "debug": "^2.1.3",
@ -36,6 +40,7 @@
"fibers": "~1.0.5", "fibers": "~1.0.5",
"fs-promise": "^0.3.1", "fs-promise": "^0.3.1",
"graceful-fs": "^3.0.6", "graceful-fs": "^3.0.6",
"gulp-sourcemaps": "^1.5.1",
"hashy": "~0.4.2", "hashy": "~0.4.2",
"http-server-plus": "^0.5.1", "http-server-plus": "^0.5.1",
"human-format": "^0.3.0", "human-format": "^0.3.0",
@ -44,12 +49,13 @@
"lodash.assign": "^3.0.0", "lodash.assign": "^3.0.0",
"lodash.bind": "^3.0.0", "lodash.bind": "^3.0.0",
"lodash.clone": "^3.0.1", "lodash.clone": "^3.0.1",
"lodash.contains": "^2.4.1",
"lodash.difference": "^3.0.1", "lodash.difference": "^3.0.1",
"lodash.filter": "^3.0.0", "lodash.filter": "^3.1.0",
"lodash.find": "^3.0.0", "lodash.find": "^3.0.0",
"lodash.findindex": "^3.0.0",
"lodash.foreach": "^3.0.1", "lodash.foreach": "^3.0.1",
"lodash.has": "^3.0.0", "lodash.has": "^3.0.0",
"lodash.includes": "^3.1.1",
"lodash.isarray": "^3.0.0", "lodash.isarray": "^3.0.0",
"lodash.isempty": "^3.0.0", "lodash.isempty": "^3.0.0",
"lodash.isfunction": "^3.0.1", "lodash.isfunction": "^3.0.1",
@ -59,27 +65,45 @@
"lodash.map": "^3.0.0", "lodash.map": "^3.0.0",
"lodash.pick": "^3.0.0", "lodash.pick": "^3.0.0",
"lodash.pluck": "^3.0.2", "lodash.pluck": "^3.0.2",
"make-error": "^0.3.0", "lodash.result": "^3.0.0",
"make-error": "^1",
"multikey-hash": "^1.0.1", "multikey-hash": "^1.0.1",
"proxy-http-request": "0.0.2", "proxy-http-request": "0.0.2",
"request": "^2.53.0",
"require-tree": "~1.0.1", "require-tree": "~1.0.1",
"schema-inspector": "^1.5.1", "schema-inspector": "^1.5.1",
"serve-static": "^1.9.2", "serve-static": "^1.9.2",
"source-map-support": "^0.2.10",
"then-redis": "~1.3.0", "then-redis": "~1.3.0",
"ws": "~0.7.1", "ws": "~0.7.1",
"xen-api": "^0.3.0",
"xml2js": "~0.4.6", "xml2js": "~0.4.6",
"xmlrpc": "~1.3.0" "xmlrpc": "~1.3.0"
}, },
"devDependencies": { "devDependencies": {
"chai": "~2.1.2", "chai": "~2.1.2",
"coffeelint-no-implicit-returns": "0.0.4", "coffeelint-no-implicit-returns": "0.0.4",
"glob": "~5.0.3", "gulp": "git://github.com/gulpjs/gulp#4.0",
"gulp-babel": "^5",
"gulp-coffee": "^2.3.1",
"gulp-plumber": "^1.0.0",
"gulp-watch": "^4.2.2",
"in-publish": "^1.1.1",
"mocha": "^2.2.1", "mocha": "^2.2.1",
"node-inspector": "^0.9.2", "node-inspector": "^0.9.2",
"sinon": "^1.14.1" "sinon": "^1.14.1",
"standard": "*"
}, },
"scripts": { "scripts": {
"build": "gulp build --production",
"dev": "gulp build",
"prepublish": "in-publish && npm run build || in-install",
"start": "node bin/xo-server", "start": "node bin/xo-server",
"test": "coffee run-tests" "test": "standard && mocha 'dist/**/*.spec.js'"
},
"standard": {
"ignore": [
"dist/**"
]
} }
} }

View File

@ -1,32 +0,0 @@
#!/usr/bin/env coffee
# Some modules are written in ES6.
require('babel/register')
# Tests runner.
$mocha = require 'mocha'
# Used to find the specification files.
$glob = require 'glob'
#=====================================================================
do ->
# Instantiates the tests runner.
mocha = new $mocha {
reporter: 'spec'
}
# Processes arguments.
do ->
{argv} = process
i = 2
n = argv.length
mocha.grep argv[i++] while i < n
$glob 'src/**/*.spec.{coffee,js}', (error, files) ->
console.error(error) if error
mocha.addFile file for file in files
mocha.run( -> )

View File

@ -1,6 +1,7 @@
{EventEmitter: $EventEmitter} = require 'events' {EventEmitter: $EventEmitter} = require 'events'
$assign = require 'lodash.assign' $assign = require 'lodash.assign'
$filter = require 'lodash.filter'
$forEach = require 'lodash.foreach' $forEach = require 'lodash.foreach'
$getKeys = require 'lodash.keys' $getKeys = require 'lodash.keys'
$isArray = require 'lodash.isarray' $isArray = require 'lodash.isarray'
@ -214,6 +215,10 @@ class $MappedCollection extends $EventEmitter
remove: (keys, ignoreMissingItems = false) -> remove: (keys, ignoreMissingItems = false) ->
@_removeItems (@_fetchItems keys, ignoreMissingItems) @_removeItems (@_fetchItems keys, ignoreMissingItems)
removeWithPredicate: (predicate, thisArg) ->
items = ($filter @_byKey, predicate, thisArg)
@_removeItems items
set: (items, {add, update, remove} = {}) -> set: (items, {add, update, remove} = {}) ->
add = true unless add? add = true unless add?
update = true unless update? update = true unless update?

View File

@ -4,7 +4,7 @@ $sinon = require 'sinon'
#--------------------------------------------------------------------- #---------------------------------------------------------------------
{$MappedCollection} = require './MappedCollection.coffee' {$MappedCollection} = require './MappedCollection'
#===================================================================== #=====================================================================

View File

@ -1,54 +1,47 @@
'use strict'; import assign from 'lodash.assign'
import {JsonRpcError} from 'json-rpc/errors'
//==================================================================== // ===================================================================
var assign = require('lodash.assign');
var JsonRpcError = require('json-rpc/errors').JsonRpcError;
var jsonRpcErrors = require('json-rpc/errors');
var makeError = require('make-error');
//====================================================================
function exportError(constructor) {
makeError(constructor, JsonRpcError);
exports[constructor.name] = constructor;
}
//====================================================================
// Export standard JSON-RPC errors. // Export standard JSON-RPC errors.
assign(exports, jsonRpcErrors); export * from 'json-rpc/errors'
//-------------------------------------------------------------------- // -------------------------------------------------------------------
exportError(function NotImplemented() { export class NotImplemented extends JsonRpcError {
NotImplemented.super.call(this, 'not implemented', 0); constructor () {
}); super('not implemented', 0)
}
}
//-------------------------------------------------------------------- // -------------------------------------------------------------------
exportError(function NoSuchObject() { export class NoSuchObject extends JsonRpcError {
NoSuchObject.super.call(this, 'no such object', 1); constructor () {
}); super('no such object', 1)
}
}
//-------------------------------------------------------------------- // -------------------------------------------------------------------
exportError(function Unauthorized() { export class Unauthorized extends JsonRpcError {
Unauthorized.super.call( constructor () {
this, super('not authenticated or not enough permissions', 2)
'not authenticated or not enough permissions', }
2 }
);
});
//-------------------------------------------------------------------- // -------------------------------------------------------------------
exportError(function InvalidCredential() { export class InvalidCredential extends JsonRpcError {
InvalidCredential.super.call(this, 'invalid credential', 3); constructor () {
}); super('invalid credential', 3)
}
}
//-------------------------------------------------------------------- // -------------------------------------------------------------------
exportError(function AlreadyAuthenticated() { export class AlreadyAuthenticated extends JsonRpcError {
AlreadyAuthenticated.super.call(this, 'already authenticated', 4); constructor () {
}); super('already authenticated', 4)
}
}

View File

@ -1,82 +1,83 @@
import debug from 'debug'; import createDebug from 'debug'
debug = debug('xo:api'); const debug = createDebug('xo:api')
import assign from 'lodash.assign'; import assign from 'lodash.assign'
import Bluebird from 'bluebird'; import Bluebird from 'bluebird'
import forEach from 'lodash.foreach'; import forEach from 'lodash.foreach'
import getKeys from 'lodash.keys'; import getKeys from 'lodash.keys'
import isFunction from 'lodash.isfunction'; import isFunction from 'lodash.isfunction'
import map from 'lodash.map'; import map from 'lodash.map'
import requireTree from 'require-tree'; import requireTree from 'require-tree'
import schemaInspector from 'schema-inspector'; import schemaInspector from 'schema-inspector'
import { import {
InvalidParameters, InvalidParameters,
MethodNotFound, MethodNotFound,
NoSuchObject, NoSuchObject,
Unauthorized, Unauthorized
} from './api-errors'; } from './api-errors'
//==================================================================== // ===================================================================
// FIXME: this function is specific to XO and should not be defined in // FIXME: this function is specific to XO and should not be defined in
// this file. // this file.
function checkPermission (method) { function checkPermission (method) {
/* jshint validthis: true */ /* jshint validthis: true */
let {permission} = method; const {permission} = method
// No requirement. // No requirement.
if (permission === undefined) { if (permission === undefined) {
return; return
} }
let {user} = this; const {user} = this
if (!user) { if (!user) {
throw new Unauthorized(); throw new Unauthorized()
} }
// The only requirement is login. // The only requirement is login.
if (!permission) { if (!permission) {
return; return
} }
if (!user.hasPermission(permission)) { if (!user.hasPermission(permission)) {
throw new Unauthorized(); throw new Unauthorized()
} }
} }
//-------------------------------------------------------------------- // -------------------------------------------------------------------
function checkParams (method, params) { function checkParams (method, params) {
var schema = method.params; var schema = method.params
if (!schema) { if (!schema) {
return; return
} }
let result = schemaInspector.validate({ const result = schemaInspector.validate({
type: 'object', type: 'object',
properties: schema, properties: schema
}, params); }, params)
if (!result.valid) { if (!result.valid) {
throw new InvalidParameters(result.error); throw new InvalidParameters(result.error)
} }
} }
//-------------------------------------------------------------------- // -------------------------------------------------------------------
let checkAuthorization; // Forward declaration.
let checkAuthorization
function authorized () {} function authorized () {}
function forbiddden() { // function forbiddden () {
throw new Unauthorized(); // throw new Unauthorized()
} // }
function checkMemberAuthorization (member) { function checkMemberAuthorization (member) {
return function (userId, object) { return function (userId, object) {
let memberObject = this.getObject(object[member]); const memberObject = this.getObject(object[member])
return checkAuthorization.call(this, userId, memberObject); return checkAuthorization.call(this, userId, memberObject)
}; }
} }
const checkAuthorizationByTypes = { const checkAuthorizationByTypes = {
@ -94,127 +95,127 @@ const checkAuthorizationByTypes = {
// containing SR or to a linked VM. // containing SR or to a linked VM.
VDI (userId, vdi) { VDI (userId, vdi) {
// Check authorization for each of the connected VMs. // Check authorization for each of the connected VMs.
let promises = map(this.getObjects(vdi.$VBDs, 'VBD'), vbd => { const promises = map(this.getObjects(vdi.$VBDs, 'VBD'), vbd => {
let vm = this.getObject(vbd.VM, 'VM'); const vm = this.getObject(vbd.VM, 'VM')
return checkAuthorization.call(this, userId, vm); return checkAuthorization.call(this, userId, vm)
}); })
// Check authorization for the containing SR. // Check authorization for the containing SR.
let sr = this.getObject(vdi.$SR, 'SR'); const sr = this.getObject(vdi.$SR, 'SR')
promises.push(checkAuthorization.call(this, userId, sr)); promises.push(checkAuthorization.call(this, userId, sr))
// We need at least one success // We need at least one success
return Bluebird.any(promises).catch(function (aggregateError) { return Bluebird.any(promises).catch(function (aggregateError) {
throw aggregateError[0]; throw aggregateError[0]
}); })
}, },
VIF (userId, vif) { VIF (userId, vif) {
let network = this.getObject(vif.$network); const network = this.getObject(vif.$network)
let vm = this.getObject(vif.$VM); const vm = this.getObject(vif.$VM)
return Bluebird.any([ return Bluebird.any([
checkAuthorization.call(this, userId, network), checkAuthorization.call(this, userId, network),
checkAuthorization.call(this, userId, vm), checkAuthorization.call(this, userId, vm)
]); ])
}, },
'VM-snapshot': checkMemberAuthorization('$snapshot_of'), 'VM-snapshot': checkMemberAuthorization('$snapshot_of')
}; }
function defaultCheckAuthorization (userId, object) { function defaultCheckAuthorization (userId, object) {
return this.acls.exists({ return this.acls.exists({
subject: userId, subject: userId,
object: object.id, object: object.id
}).then(success => { }).then(success => {
if (!success) { if (!success) {
throw new Unauthorized(); throw new Unauthorized()
} }
}); })
} }
checkAuthorization = Bluebird.method(function (userId, object) { checkAuthorization = Bluebird.method(function (userId, object) {
let fn = checkAuthorizationByTypes[object.type] || defaultCheckAuthorization; const fn = checkAuthorizationByTypes[object.type] || defaultCheckAuthorization
return fn.call(this, userId, object); return fn.call(this, userId, object)
}); })
function resolveParams (method, params) { function resolveParams (method, params) {
var resolve = method.resolve; var resolve = method.resolve
if (!resolve) { if (!resolve) {
return params; return params
} }
let {user} = this; const {user} = this
if (!user) { if (!user) {
throw new Unauthorized(); throw new Unauthorized()
} }
let userId = user.get('id'); const userId = user.get('id')
let isAdmin = this.user.hasPermission('admin'); const isAdmin = this.user.hasPermission('admin')
let promises = []; const promises = []
try { try {
forEach(resolve, ([param, types], key) => { forEach(resolve, ([param, types], key) => {
let id = params[param]; const id = params[param]
if (id === undefined) { if (id === undefined) {
return; return
} }
let object = this.getObject(params[param], types); const object = this.getObject(params[param], types)
// This parameter has been handled, remove it. // This parameter has been handled, remove it.
delete params[param]; delete params[param]
// Register this new value. // Register this new value.
params[key] = object; params[key] = object
if (!isAdmin) { if (!isAdmin) {
promises.push(checkAuthorization.call(this, userId, object)); promises.push(checkAuthorization.call(this, userId, object))
} }
}); })
} catch (error) { } catch (error) {
throw new NoSuchObject(); throw new NoSuchObject()
} }
return Bluebird.all(promises).return(params); return Bluebird.all(promises).return(params)
} }
//==================================================================== // ===================================================================
function getMethodsInfo () { function getMethodsInfo () {
let methods = {}; const methods = {}
forEach(this.api._methods, function (method, name) { forEach(this.api._methods, function (method, name) {
this[name] = assign({}, { this[name] = assign({}, {
description: method.description, description: method.description,
params: method.params || {}, params: method.params || {},
permission: method.permission, permission: method.permission
}); })
}, methods); }, methods)
return methods; return methods
} }
getMethodsInfo.description = 'returns the signatures of all available API methods'; getMethodsInfo.description = 'returns the signatures of all available API methods'
//-------------------------------------------------------------------- // -------------------------------------------------------------------
let getVersion = () => '0.1'; const getVersion = () => '0.1'
getVersion.description = 'API version (unstable)'; getVersion.description = 'API version (unstable)'
//-------------------------------------------------------------------- // -------------------------------------------------------------------
function listMethods () { function listMethods () {
return getKeys(this.api._methods); return getKeys(this.api._methods)
} }
listMethods.description = 'returns the name of all available API methods'; listMethods.description = 'returns the name of all available API methods'
//-------------------------------------------------------------------- // -------------------------------------------------------------------
function methodSignature ({method: name}) { function methodSignature ({method: name}) {
let method = this.api.getMethod(name); const method = this.api.getMethod(name)
if (!method) { if (!method) {
throw new NoSuchObject(); throw new NoSuchObject()
} }
// Return an array for compatibility with XML-RPC. // Return an array for compatibility with XML-RPC.
@ -223,105 +224,104 @@ function methodSignature({method: name}) {
assign({ name }, { assign({ name }, {
description: method.description, description: method.description,
params: method.params || {}, params: method.params || {},
permission: method.permission, permission: method.permission
}) })
]; ]
} }
methodSignature.description = 'returns the signature of an API method'; methodSignature.description = 'returns the signature of an API method'
//====================================================================
// ===================================================================
export default class Api { export default class Api {
constructor ({context} = {}) { constructor ({context} = {}) {
this._methods = Object.create(null); this._methods = Object.create(null)
this.context = context; this.context = context
this.addMethods({ this.addMethods({
system: { system: {
getMethodsInfo, getMethodsInfo,
getVersion, getVersion,
listMethods, listMethods,
methodSignature, methodSignature
} }
}); })
// FIXME: this too is specific to XO and should be moved out of this file. // FIXME: this too is specific to XO and should be moved out of this file.
this.addMethods(requireTree('./api')); this.addMethods(requireTree('./api'))
} }
addMethod (name, method) { addMethod (name, method) {
this._methods[name] = method; this._methods[name] = method
} }
addMethods (methods) { addMethods (methods) {
let base = ''; let base = ''
forEach(methods, function addMethod (method, name) { forEach(methods, function addMethod (method, name) {
name = base + name; name = base + name
if (isFunction(method)) { if (isFunction(method)) {
this.addMethod(name, method); this.addMethod(name, method)
return; return
} }
let oldBase = base; const oldBase = base
base = name + '.'; base = name + '.'
forEach(method, addMethod, this); forEach(method, addMethod, this)
base = oldBase; base = oldBase
}, this); }, this)
} }
call (session, name, params) { call (session, name, params) {
debug('%s(...)', name); debug('%s(...)', name)
let method; let method
let context; let context
return Bluebird.try(() => { return Bluebird.try(() => {
method = this.getMethod(name); method = this.getMethod(name)
if (!method) { if (!method) {
throw new MethodNotFound(name); throw new MethodNotFound(name)
} }
context = Object.create(this.context); context = Object.create(this.context)
context.api = this; // Used by system.*(). context.api = this // Used by system.*().
context.session = session; context.session = session
// FIXME: too coupled with XO. // FIXME: too coupled with XO.
// Fetch and inject the current user. // Fetch and inject the current user.
let userId = session.get('user_id', undefined); const userId = session.get('user_id', undefined)
return userId === undefined ? null : context.users.first(userId); return userId === undefined ? null : context.users.first(userId)
}).then(function (user) { }).then(function (user) {
context.user = user; context.user = user
return checkPermission.call(context, method); return checkPermission.call(context, method)
}).then(() => { }).then(() => {
checkParams(method, params); checkParams(method, params)
return resolveParams.call(context, method, params); return resolveParams.call(context, method, params)
}).then(params => { }).then(params => {
return method.call(context, params); return method.call(context, params)
}).then( }).then(
result => { result => {
// If nothing was returned, consider this operation a success // If nothing was returned, consider this operation a success
// and return true. // and return true.
if (result === undefined) { if (result === undefined) {
result = true; result = true
} }
debug('%s(...) → %s', name, typeof result); debug('%s(...) → %s', name, typeof result)
return result; return result
}, },
error => { error => {
debug('Error: %s(...) → %s', name, error); debug('Error: %s(...) → %s', name, error)
throw error; throw error
} }
); )
} }
getMethod (name) { getMethod (name) {
return this._methods[name]; return this._methods[name]
} }
} }

View File

@ -1,90 +1,82 @@
import {coroutine} from 'bluebird'; import {coroutine} from 'bluebird'
import {ModelAlreadyExists} from '../collection'; import {ModelAlreadyExists} from '../collection'
//==================================================================== // ===================================================================
let get = coroutine(function *({subject, object}) { export const get = coroutine(function * ({subject, object}) {
let sieve = {}; const sieve = {}
try { try {
if (subject !== undefined) { if (subject !== undefined) {
sieve.subject = (yield this.users.first(subject)).get('id'); sieve.subject = (yield this.users.first(subject)).get('id')
} }
if (object !== undefined) { if (object !== undefined) {
sieve.object = this.getObject(object).id; sieve.object = this.getObject(object).id
} }
} catch (error) { } catch (error) {
this.throw('NO_SUCH_OBJECT'); this.throw('NO_SUCH_OBJECT')
} }
return this.acls.get(sieve); return this.acls.get(sieve)
}); })
get.permission = 'admin'; get.permission = 'admin'
get.params = { get.params = {
subject: { type: 'string', optional: true }, subject: { type: 'string', optional: true },
object: { type: 'string', optional: true }, object: { type: 'string', optional: true }
}; }
get.description = 'get existing ACLs'; get.description = 'get existing ACLs'
export {get}; // -------------------------------------------------------------------
//-------------------------------------------------------------------- export const getCurrent = coroutine(function * () {
return this.acls.get({ subject: this.session.get('user_id') })
})
let getCurrent = coroutine(function *() { getCurrent.permission = ''
return this.acls.get({ subject: this.session.get('user_id') });
});
getCurrent.permission = ''; getCurrent.description = 'get existing ACLs concerning current user'
getCurrent.description = 'get existing ACLs concerning current user'; // -------------------------------------------------------------------
export {getCurrent}; export const add = coroutine(function * ({subject, object}) {
//--------------------------------------------------------------------
let add = coroutine(function *({subject, object}) {
try { try {
subject = (yield this.users.first(subject)).get('id'); subject = (yield this.users.first(subject)).get('id')
object = this.getObject(object).id; object = this.getObject(object).id
} catch (error) { } catch (error) {
this.throw('NO_SUCH_OBJECT'); this.throw('NO_SUCH_OBJECT')
} }
try { try {
yield this.acls.create(subject, object); yield this.acls.create(subject, object)
} catch (error) { } catch (error) {
if (!(error instanceof ModelAlreadyExists)) { if (!(error instanceof ModelAlreadyExists)) {
throw error; throw error
} }
} }
}); })
add.permission = 'admin'; add.permission = 'admin'
add.params = { add.params = {
subject: { type: 'string' }, subject: { type: 'string' },
object: { type: 'string' }, object: { type: 'string' }
}; }
add.description = 'add a new ACL entry'; add.description = 'add a new ACL entry'
export {add}; // -------------------------------------------------------------------
//-------------------------------------------------------------------- export const remove = coroutine(function * ({subject, object}) {
yield this.acls.delete(subject, object)
})
let remove = coroutine(function *({subject, object}) { remove.permission = 'admin'
yield this.acls.delete(subject, object);
});
remove.permission = 'admin';
remove.params = { remove.params = {
subject: { type: 'string' }, subject: { type: 'string' },
object: { type: 'string' }, object: { type: 'string' }
}; }
remove.description = 'remove an existing ACL entry'; remove.description = 'remove an existing ACL entry'
export {remove};

View File

@ -1,35 +1,32 @@
import {coroutine, wait} from '../fibers-utils'
import {parseSize} from '../utils'
import {coroutine, wait} from '../fibers-utils'; // ===================================================================
import {parseSize} from '../utils';
//==================================================================== export const create = coroutine(function ({name, size, sr}) {
const xapi = this.getXAPI(sr)
let create = coroutine(function ({name, size, sr}) { const ref = wait(xapi.call('VDI.create', {
let xapi = this.getXAPI(sr);
let ref = wait(xapi.call('VDI.create', {
name_label: name, name_label: name,
other_config: {}, other_config: {},
read_only: false, read_only: false,
sharable: false, sharable: false,
SR: sr.ref, SR: sr.ref,
type: 'user', type: 'user',
virtual_size: String(parseSize(size)), virtual_size: String(parseSize(size))
})); }))
return wait(xapi.call('VDI.get_record', ref)).uuid; return wait(xapi.call('VDI.get_record', ref)).uuid
}); })
create.description = 'create a new disk on a SR'; create.description = 'create a new disk on a SR'
create.params = { create.params = {
name: { type: 'string' }, name: { type: 'string' },
size: { type: 'string' }, size: { type: 'string' },
sr: { type: 'string' }, sr: { type: 'string' }
}; }
create.resolve = { create.resolve = {
sr: ['sr', 'SR'], sr: ['sr', 'SR']
}; }
export {create};

View File

@ -170,3 +170,39 @@ disable.resolve = {
} }
exports.disable = disable exports.disable = disable
#---------------------------------------------------------------------
createNetwork = $coroutine ({host, name, description, pif, mtu, vlan}) ->
xapi = @getXAPI host
description = description ? 'Created with Xen Orchestra'
network_ref = $wait xapi.call 'network.create', {
name_label: name,
name_description: description,
MTU: mtu ? '1500'
other_config: {}
}
if pif?
vlan = vlan ? '0'
pif = @getObject pif, 'PIF'
$wait xapi.call 'pool.create_VLAN_from_PIF', pif.ref, network_ref, vlan
return true
createNetwork.params = {
host: { type: 'string' }
name: { type: 'string' }
description: { type: 'string', optional: true }
pif: { type: 'string', optional: true }
mtu: { type: 'string', optional: true }
vlan: { type: 'string', optional: true }
}
createNetwork.resolve = {
host: ['host', 'host'],
}
createNetwork.permission = 'admin'
exports.createNetwork = createNetwork

View File

@ -1,97 +0,0 @@
{$coroutine, $wait} = require '../fibers-utils'
#=====================================================================
# FIXME: We are storing passwords which is bad!
# Could we use tokens instead?
# Adds a new server.
exports.add = $coroutine ({host, username, password}) ->
server = $wait @servers.add {
host
username
password
}
return server.get('id')
exports.add.description = 'Add a new Xen server to XO'
exports.add.permission = 'admin'
exports.add.params =
host:
type: 'string'
username:
type: 'string'
password:
type: 'string'
#---------------------------------------------------------------------
# Removes an existing server.
exports.remove = $coroutine ({id}) ->
# Throws an error if the server did not exist.
@throw 'NO_SUCH_OBJECT' unless $wait @servers.remove id
return true
exports.remove.permission = 'admin'
exports.remove.params =
id:
type: 'string'
#---------------------------------------------------------------------
# Returns all servers.
exports.getAll = $coroutine ->
# Retrieves the servers.
servers = $wait @servers.get()
# Filters out private properties.
for server, i in servers
servers[i] = @getServerPublicProperties server
return servers
exports.getAll.permission = 'admin'
#---------------------------------------------------------------------
# Changes the properties of an existing server.
exports.set = $coroutine ({id, host, username, password}) ->
# Retrieves the server.
server = $wait @servers.first id
# Throws an error if it did not exist.
@throw 'NO_SUCH_OBJECT' unless server
# Updates the provided properties.
server.set {host} if host?
server.set {username} if username?
server.set {password} if password?
# Updates the server.
$wait @servers.update server
return true
exports.set.permission = 'admin'
exports.set.params =
id:
type: 'string'
host:
type: 'string'
optional: true
username:
type: 'string'
optional: true
password:
type: 'string'
optional: true
#---------------------------------------------------------------------
# Connects to an existing server.
exports.connect = ->
@throw 'NOT_IMPLEMENTED'
#---------------------------------------------------------------------
# Disconnects from an existing server.
exports.disconnect = ->
@throw 'NOT_IMPLEMENTED'

172
src/api/server.js Normal file
View File

@ -0,0 +1,172 @@
import {coroutine} from 'bluebird'
import {JsonRpcError, NoSuchObject} from '../api-errors'
// ===================================================================
// FIXME: We are storing passwords which is bad!
// Could we use tokens instead?
export const add = coroutine(function * ({
host,
username,
password,
autoConnect = true
}) {
const server = yield this.servers.add({
host: host,
username: username,
password: password
})
if (autoConnect) {
// Connect asynchronously, ignore any error.
this.connectServer(server).catch(() => {})
}
return server.get('id')
})
add.description = 'Add a new Xen server to XO'
add.permission = 'admin'
add.params = {
host: {
type: 'string'
},
username: {
type: 'string'
},
password: {
type: 'string'
},
autoConnect: {
optional: true,
type: 'boolean'
}
}
// -------------------------------------------------------------------
export const remove = coroutine(function * ({id}) {
try {
yield this.disconnectServer(id)
} catch (error) {}
if (!(yield this.servers.remove(id))) {
throw new NoSuchObject()
}
})
remove.permission = 'admin'
remove.params = {
id: {
type: 'string'
}
}
// -------------------------------------------------------------------
export const getAll = coroutine(function * () {
const servers = yield this.servers.get()
for (let i = 0, n = servers.length; i < n; ++i) {
servers[i] = this.getServerPublicProperties(servers[i])
}
return servers
})
getAll.permission = 'admin'
// -------------------------------------------------------------------
export const set = coroutine(function * ({id, host, username, password}) {
const server = yield this.servers.first(id)
if (!server) {
throw new NoSuchObject()
}
if (host != null) {
server.set({
host: host
})
}
if (username != null) {
server.set({
username: username
})
}
if (password != null) {
server.set({
password: password
})
}
yield this.servers.update(server)
})
set.permission = 'admin'
set.params = {
id: {
type: 'string'
},
host: {
type: 'string',
optional: true
},
username: {
type: 'string',
optional: true
},
password: {
type: 'string',
optional: true
}
}
// -------------------------------------------------------------------
export const connect = coroutine(function * ({id}) {
const server = yield this.servers.first(id)
if (!server) {
throw new NoSuchObject()
}
try {
yield this.connectServer(server)
} catch (error) {
if (error.code === 'SESSION_AUTHENTICATION_FAILED') {
throw new JsonRpcError('authentication failed')
}
throw error
}
})
connect.permission = 'admin'
connect.params = {
id: {
type: 'string'
}
}
// -------------------------------------------------------------------
export const disconnect = coroutine(function * ({id}) {
const server = yield this.servers.first(id)
if (!server) {
throw new NoSuchObject()
}
return this.disconnectServer(server)
})
disconnect.permission = 'admin'
disconnect.params = {
id: {
type: 'string'
}
}

View File

@ -1,74 +1,63 @@
import {deprecate} from 'util'; import {deprecate} from 'util'
import {InvalidCredential, AlreadyAuthenticated} from '../api-errors'; import {InvalidCredential, AlreadyAuthenticated} from '../api-errors'
import {coroutine, wait} from '../fibers-utils'; import {coroutine, wait} from '../fibers-utils'
//==================================================================== // ===================================================================
let signIn = coroutine(function (credentials) { export const signIn = coroutine(function (credentials) {
if (this.session.has('user_id')) { if (this.session.has('user_id')) {
throw new AlreadyAuthenticated(); throw new AlreadyAuthenticated()
} }
let user = wait(this.authenticateUser(credentials)); const user = wait(this.authenticateUser(credentials))
if (!user) { if (!user) {
throw new InvalidCredential(); throw new InvalidCredential()
} }
this.session.set('user_id', user.get('id')); this.session.set('user_id', user.get('id'))
return this.getUserPublicProperties(user); return this.getUserPublicProperties(user)
}); })
signIn.description = 'sign in'; signIn.description = 'sign in'
export {signIn}; // -------------------------------------------------------------------
//-------------------------------------------------------------------- export const signInWithPassword = deprecate(signIn, 'use session.signIn() instead')
let signInWithPassword = deprecate(signIn, 'use session.signIn() instead');
signInWithPassword.params = { signInWithPassword.params = {
email: { type: 'string' }, email: { type: 'string' },
password: { type: 'string' }, password: { type: 'string' }
};
export {signInWithPassword};
//--------------------------------------------------------------------
let signInWithToken = deprecate(signIn, 'use session.signIn() instead');
signInWithToken.params = {
token: { type: 'string' },
};
export {signInWithToken};
//--------------------------------------------------------------------
function signOut() {
this.session.unset('user_id');
} }
signOut.description = 'sign out the user from the current session'; // -------------------------------------------------------------------
export const signInWithToken = deprecate(signIn, 'use session.signIn() instead')
signInWithToken.params = {
token: { type: 'string' }
}
// -------------------------------------------------------------------
export function signOut () {
this.session.unset('user_id')
}
signOut.description = 'sign out the user from the current session'
// This method requires the user to be signed in. // This method requires the user to be signed in.
signOut.permission = ''; signOut.permission = ''
export {signOut}; // -------------------------------------------------------------------
//-------------------------------------------------------------------- export const getUser = coroutine(function () {
const userId = this.session.get('user_id')
let getUser = coroutine(function () {
let userId = this.session.get('user_id');
return userId === undefined ? return userId === undefined ?
null : null :
this.getUserPublicProperties(wait(this.users.first(userId))) this.getUserPublicProperties(wait(this.users.first(userId)))
; })
});
getUser.description = 'return the currently connected user'; getUser.description = 'return the currently connected user'
export {getUser};

View File

@ -1,109 +1,110 @@
import forEach from 'lodash.foreach'; import forEach from 'lodash.foreach'
import {coroutine, wait} from '../fibers-utils'; import {coroutine, wait} from '../fibers-utils'
import {ensureArray, parseXml} from '../utils'; import {ensureArray, parseXml} from '../utils'
//==================================================================== // ===================================================================
let set = coroutine(function (params) { export const set = coroutine(function (params) {
let {SR} = params; const {SR} = params
let xapi = this.getXAPI(); const xapi = this.getXAPI()
forEach(['name_label', 'name_description'], param => { forEach(['name_label', 'name_description'], param => {
let value = params[param]; const value = params[param]
if (value === undefined) { if (value === undefined) {
return; return
} }
wait(xapi.call(`SR.set_${value}`, SR.ref, params[param])); wait(xapi.call(`SR.set_${value}`, SR.ref, params[param]))
}); })
return true
})
return true;
});
set.params = { set.params = {
id: { type: 'string' }, id: { type: 'string' },
name_label: { type: 'string', optional: true }, name_label: { type: 'string', optional: true },
name_description: { type: 'string', optional: true }, name_description: { type: 'string', optional: true }
}; }
set.resolve = { set.resolve = {
SR: ['id', 'SR'], SR: ['id', 'SR']
}; }
export {set};
//-------------------------------------------------------------------- // -------------------------------------------------------------------
let scan = coroutine(function ({SR}) { export const scan = coroutine(function ({SR}) {
const xapi = this.getXAPI(SR)
let xapi = this.getXAPI(SR); wait(xapi.call('SR.scan', SR.ref))
wait(xapi.call('SR.scan', SR.ref)); return true
})
return true;
});
scan.params = { scan.params = {
id: { type: 'string' }, id: { type: 'string' }
}; }
scan.resolve = { scan.resolve = {
SR: ['id', 'SR'], SR: ['id', 'SR']
}; }
export {scan};
// -------------------------------------------------------------------
//--------------------------------------------------------------------
// TODO: find a way to call this "delete" and not destroy // TODO: find a way to call this "delete" and not destroy
let destroy = coroutine(function ({SR}) { export const destroy = coroutine(function ({SR}) {
const xapi = this.getXAPI(SR)
let xapi = this.getXAPI(SR); wait(xapi.call('SR.destroy', SR.ref))
wait(xapi.call('SR.destroy', SR.ref)); return true
})
return true;
});
destroy.params = { destroy.params = {
id: { type: 'string' }, id: { type: 'string' }
}; }
destroy.resolve = { destroy.resolve = {
SR: ['id', 'SR'], SR: ['id', 'SR']
}; }
export {destroy};
//-------------------------------------------------------------------- // -------------------------------------------------------------------
let forget = coroutine(function ({SR}) { export const forget = coroutine(function ({SR}) {
const xapi = this.getXAPI(SR)
let xapi = this.getXAPI(SR); wait(xapi.call('SR.forget', SR.ref))
wait(xapi.call('SR.forget', SR.ref)); return true
})
return true;
});
forget.params = { forget.params = {
id: { type: 'string' }, id: { type: 'string' }
}; }
forget.resolve = { forget.resolve = {
SR: ['id', 'SR'], SR: ['id', 'SR']
}; }
export {forget};
//-------------------------------------------------------------------- // -------------------------------------------------------------------
let createIso = coroutine(function ({ export const createIso = coroutine(function ({
host, host,
nameLabel, nameLabel,
nameDescription, nameDescription,
path path
}) { }) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host);
// FIXME: won't work for IPv6 // FIXME: won't work for IPv6
// Detect if NFS or local path for ISO files // Detect if NFS or local path for ISO files
let deviceConfig = {location: path}; const deviceConfig = {location: path}
if (path.indexOf(':') === -1) { // not NFS share if (path.indexOf(':') === -1) { // not NFS share
// TODO: legacy will be removed in XAPI soon by FileSR // TODO: legacy will be removed in XAPI soon by FileSR
deviceConfig.legacy_mode = 'true'; deviceConfig.legacy_mode = 'true'
} }
let srRef = wait(xapi.call( const srRef = wait(xapi.call(
'SR.create', 'SR.create',
host.ref, host.ref,
deviceConfig, deviceConfig,
@ -114,30 +115,29 @@ let createIso = coroutine(function ({
'iso', // SR content type ISO 'iso', // SR content type ISO
true, true,
{} {}
)); ))
let sr = wait(xapi.call('SR.get_record', srRef)); const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid; return sr.uuid
})
});
createIso.params = { createIso.params = {
host: { type: 'string' }, host: { type: 'string' },
nameLabel: { type: 'string' }, nameLabel: { type: 'string' },
nameDescription: { type: 'string' }, nameDescription: { type: 'string' },
path: { type: 'string' } path: { type: 'string' }
}; }
createIso.resolve = {
host: ['host', 'host'],
};
export {createIso};
//-------------------------------------------------------------------- createIso.resolve = {
host: ['host', 'host']
}
// -------------------------------------------------------------------
// NFS SR // NFS SR
// This functions creates a NFS SR // This functions creates a NFS SR
let createNfs = coroutine(function ({ export const createNfs = coroutine(function ({
host, host,
nameLabel, nameLabel,
nameDescription, nameDescription,
@ -145,20 +145,19 @@ let createNfs = coroutine(function ({
serverPath, serverPath,
nfsVersion nfsVersion
}) { }) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host); const deviceConfig = {
let deviceConfig = {
server, server,
serverpath: serverPath, serverpath: serverPath
}; }
// if NFS version given // if NFS version given
if (nfsVersion) { if (nfsVersion) {
deviceConfig.nfsversion = nfsVersion; deviceConfig.nfsversion = nfsVersion
} }
let srRef = wait(xapi.call( const srRef = wait(xapi.call(
'SR.create', 'SR.create',
host.ref, host.ref,
deviceConfig, deviceConfig,
@ -166,15 +165,14 @@ let createNfs = coroutine(function ({
nameLabel, nameLabel,
nameDescription, nameDescription,
'nfs', // SR LVM over iSCSI 'nfs', // SR LVM over iSCSI
'user', // recommanded by Citrix 'user', // recommended by Citrix
true, true,
{} {}
)); ))
let sr = wait(xapi.call('SR.get_record', srRef)); const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid; return sr.uuid
})
});
createNfs.params = { createNfs.params = {
host: { type: 'string' }, host: { type: 'string' },
@ -182,29 +180,73 @@ createNfs.params = {
nameDescription: { type: 'string' }, nameDescription: { type: 'string' },
server: { type: 'string' }, server: { type: 'string' },
serverPath: { type: 'string' }, serverPath: { type: 'string' },
nfsVersion: { type: 'string' , optional: true}, nfsVersion: { type: 'string', optional: true }
}; }
createNfs.resolve = {
host: ['host', 'host'],
};
export {createNfs};
//-------------------------------------------------------------------- createNfs.resolve = {
host: ['host', 'host']
}
// -------------------------------------------------------------------
// Local LVM SR
// This functions creates a local LVM SR
export const createLvm = coroutine(function ({
host,
nameLabel,
nameDescription,
device
}) {
const xapi = this.getXAPI(host)
const deviceConfig = {
device
}
const srRef = wait(xapi.call(
'SR.create',
host.ref,
deviceConfig,
'0',
nameLabel,
nameDescription,
'lvm', // SR LVM
'user', // recommended by Citrix
false,
{}
))
const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid
})
createLvm.params = {
host: { type: 'string' },
nameLabel: { type: 'string' },
nameDescription: { type: 'string' },
device: { type: 'string' }
}
createLvm.resolve = {
host: ['host', 'host']
}
// -------------------------------------------------------------------
// This function helps to detect all NFS shares (exports) on a NFS server // This function helps to detect all NFS shares (exports) on a NFS server
// Return a table of exports with their paths and ACLs // Return a table of exports with their paths and ACLs
let probeNfs = coroutine(function ({ export const probeNfs = coroutine(function ({
host, host,
server server
}) { }) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host); const deviceConfig = {
server
}
let deviceConfig = { let xml
server,
};
let xml;
try { try {
wait(xapi.call( wait(xapi.call(
@ -213,45 +255,43 @@ let probeNfs = coroutine(function ({
deviceConfig, deviceConfig,
'nfs', 'nfs',
{} {}
)); ))
throw new Error('the call above should have thrown an error')
} catch (error) { } catch (error) {
if (error[0] !== 'SR_BACKEND_FAILURE_101') { if (error[0] !== 'SR_BACKEND_FAILURE_101') {
throw error; throw error
} }
xml = error[3]; xml = parseXml(error[3])
} }
xml = parseXml(xml); const nfsExports = []
let nfsExports = [];
forEach(ensureArray(xml['nfs-exports'].Export), nfsExport => { forEach(ensureArray(xml['nfs-exports'].Export), nfsExport => {
nfsExports.push({ nfsExports.push({
path: nfsExport.Path.trim(), path: nfsExport.Path.trim(),
acl: nfsExport.Accesslist.trim() acl: nfsExport.Accesslist.trim()
}); })
}); })
return nfsExports; return nfsExports
})
});
probeNfs.params = { probeNfs.params = {
host: { type: 'string' }, host: { type: 'string' },
server: { type: 'string' }, server: { type: 'string' }
}; }
probeNfs.resolve = { probeNfs.resolve = {
host: ['host', 'host'], host: ['host', 'host']
}; }
export {probeNfs};
// -------------------------------------------------------------------
//--------------------------------------------------------------------
// ISCSI SR // ISCSI SR
// This functions creates a iSCSI SR // This functions creates a iSCSI SR
let createIscsi = coroutine(function ({ export const createIscsi = coroutine(function ({
host, host,
nameLabel, nameLabel,
nameDescription, nameDescription,
@ -263,27 +303,26 @@ let createIscsi = coroutine(function ({
chapUser, chapUser,
chapPassword chapPassword
}) { }) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host); const deviceConfig = {
let deviceConfig = {
target, target,
targetIQN: targetIqn, targetIQN: targetIqn,
SCSIid: scsiId, SCSIid: scsiId
}; }
// if we give user and password // if we give user and password
if (chapUser && chapPassword) { if (chapUser && chapPassword) {
deviceConfig.chapUser = chapUser; deviceConfig.chapUser = chapUser
deviceConfig.chapPassword = chapPassword; deviceConfig.chapPassword = chapPassword
} }
// if we give another port than default iSCSI // if we give another port than default iSCSI
if (port) { if (port) {
deviceConfig.port = port; deviceConfig.port = port
} }
let srRef = wait(xapi.call( const srRef = wait(xapi.call(
'SR.create', 'SR.create',
host.ref, host.ref,
deviceConfig, deviceConfig,
@ -291,15 +330,14 @@ let createIscsi = coroutine(function ({
nameLabel, nameLabel,
nameDescription, nameDescription,
'lvmoiscsi', // SR LVM over iSCSI 'lvmoiscsi', // SR LVM over iSCSI
'user', // recommanded by Citrix 'user', // recommended by Citrix
true, true,
{} {}
)); ))
let sr = wait(xapi.call('SR.get_record', srRef)); const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid; return sr.uuid
})
});
createIscsi.params = { createIscsi.params = {
host: { type: 'string' }, host: { type: 'string' },
@ -310,43 +348,42 @@ createIscsi.params = {
targetIqn: { type: 'string' }, targetIqn: { type: 'string' },
scsiId: { type: 'string' }, scsiId: { type: 'string' },
chapUser: { type: 'string', optional: true }, chapUser: { type: 'string', optional: true },
chapPassword: { type: 'string' , optional: true }, chapPassword: { type: 'string', optional: true }
}; }
createIscsi.resolve = {
host: ['host', 'host'],
};
export {createIscsi};
//-------------------------------------------------------------------- createIscsi.resolve = {
host: ['host', 'host']
}
// -------------------------------------------------------------------
// This function helps to detect all iSCSI IQN on a Target (iSCSI "server") // This function helps to detect all iSCSI IQN on a Target (iSCSI "server")
// Return a table of IQN or empty table if no iSCSI connection to the target // Return a table of IQN or empty table if no iSCSI connection to the target
let probeIscsiIqns = coroutine(function ({ export const probeIscsiIqns = coroutine(function ({
host, host,
target: targetIp, target: targetIp,
port, port,
chapUser, chapUser,
chapPassword chapPassword
}) { }) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host); const deviceConfig = {
target: targetIp
let deviceConfig = { }
target: targetIp,
};
// if we give user and password // if we give user and password
if (chapUser && chapPassword) { if (chapUser && chapPassword) {
deviceConfig.chapUser = chapUser; deviceConfig.chapUser = chapUser
deviceConfig.chapPassword = chapPassword; deviceConfig.chapPassword = chapPassword
} }
// if we give another port than default iSCSI // if we give another port than default iSCSI
if (port) { if (port) {
deviceConfig.port = port; deviceConfig.port = port
} }
let xml; let xml
try { try {
wait(xapi.call( wait(xapi.call(
@ -355,52 +392,50 @@ let probeIscsiIqns = coroutine(function ({
deviceConfig, deviceConfig,
'lvmoiscsi', 'lvmoiscsi',
{} {}
)); ))
throw new Error('the call above should have thrown an error')
} catch (error) { } catch (error) {
if (error[0] === 'SR_BACKEND_FAILURE_141') { if (error[0] === 'SR_BACKEND_FAILURE_141') {
return []; return []
} }
if (error[0] !== 'SR_BACKEND_FAILURE_96') { if (error[0] !== 'SR_BACKEND_FAILURE_96') {
throw error; throw error
} }
xml = error[3]; xml = parseXml(error[3])
} }
xml = parseXml(xml); const targets = []
let targets = [];
forEach(ensureArray(xml['iscsi-target-iqns'].TGT), target => { forEach(ensureArray(xml['iscsi-target-iqns'].TGT), target => {
// if the target is on another IP adress, do not display it // if the target is on another IP adress, do not display it
if (target.IPAddress.trim() === targetIp) { if (target.IPAddress.trim() === targetIp) {
targets.push({ targets.push({
iqn: target.TargetIQN.trim(), iqn: target.TargetIQN.trim(),
ip: target.IPAddress.trim() ip: target.IPAddress.trim()
}); })
} }
}); })
return targets; return targets
})
});
probeIscsiIqns.params = { probeIscsiIqns.params = {
host: { type: 'string' }, host: { type: 'string' },
target: { type: 'string' }, target: { type: 'string' },
port: { type: 'integer', optional: true }, port: { type: 'integer', optional: true },
chapUser: { type: 'string', optional: true }, chapUser: { type: 'string', optional: true },
chapPassword: { type: 'string' , optional: true }, chapPassword: { type: 'string', optional: true }
}; }
probeIscsiIqns.resolve = { probeIscsiIqns.resolve = {
host: ['host', 'host'], host: ['host', 'host']
}; }
export {probeIscsiIqns};
//-------------------------------------------------------------------- // -------------------------------------------------------------------
// This function helps to detect all iSCSI ID and LUNs on a Target // This function helps to detect all iSCSI ID and LUNs on a Target
// It will return a LUN table // It will return a LUN table
let probeIscsiLuns = coroutine(function ({ export const probeIscsiLuns = coroutine(function ({
host, host,
target: targetIp, target: targetIp,
port, port,
@ -408,26 +443,25 @@ let probeIscsiLuns = coroutine(function ({
chapUser, chapUser,
chapPassword chapPassword
}) { }) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host); const deviceConfig = {
let deviceConfig = {
target: targetIp, target: targetIp,
targetIQN: targetIqn, targetIQN: targetIqn
}; }
// if we give user and password // if we give user and password
if (chapUser && chapPassword) { if (chapUser && chapPassword) {
deviceConfig.chapUser = chapUser; deviceConfig.chapUser = chapUser
deviceConfig.chapPassword = chapPassword; deviceConfig.chapPassword = chapPassword
} }
// if we give another port than default iSCSI // if we give another port than default iSCSI
if (port) { if (port) {
deviceConfig.port = port; deviceConfig.port = port
} }
let xml; let xml
try { try {
wait(xapi.call( wait(xapi.call(
@ -436,18 +470,18 @@ let probeIscsiLuns = coroutine(function ({
deviceConfig, deviceConfig,
'lvmoiscsi', 'lvmoiscsi',
{} {}
)); ))
throw new Error('the call above should have thrown an error')
} catch (error) { } catch (error) {
if (error[0] !== 'SR_BACKEND_FAILURE_107') { if (error[0] !== 'SR_BACKEND_FAILURE_107') {
throw error; throw error
} }
xml = error[3]; xml = parseXml(error[3])
} }
xml = parseXml(xml); const luns = []
let luns = [];
forEach(ensureArray(xml['iscsi-target'].LUN), lun => { forEach(ensureArray(xml['iscsi-target'].LUN), lun => {
luns.push({ luns.push({
id: lun.LUNid.trim(), id: lun.LUNid.trim(),
@ -455,12 +489,11 @@ let probeIscsiLuns = coroutine(function ({
serial: lun.serial.trim(), serial: lun.serial.trim(),
size: lun.size.trim(), size: lun.size.trim(),
scsiId: lun.SCSIid.trim() scsiId: lun.SCSIid.trim()
}); })
}); })
return luns; return luns
})
});
probeIscsiLuns.params = { probeIscsiLuns.params = {
host: { type: 'string' }, host: { type: 'string' },
@ -468,18 +501,18 @@ probeIscsiLuns.params = {
port: { type: 'integer', optional: true}, port: { type: 'integer', optional: true},
targetIqn: { type: 'string' }, targetIqn: { type: 'string' },
chapUser: { type: 'string', optional: true }, chapUser: { type: 'string', optional: true },
chapPassword: { type: 'string' , optional: true }, chapPassword: { type: 'string', optional: true }
}; }
probeIscsiLuns.resolve = {
host: ['host', 'host'],
};
export {probeIscsiLuns};
//-------------------------------------------------------------------- probeIscsiLuns.resolve = {
host: ['host', 'host']
}
// -------------------------------------------------------------------
// This function helps to detect if this target already exists in XAPI // This function helps to detect if this target already exists in XAPI
// It returns a table of SR UUID, empty if no existing connections // It returns a table of SR UUID, empty if no existing connections
let probeIscsiExists = coroutine(function ({ export const probeIscsiExists = coroutine(function ({
host, host,
target: targetIp, target: targetIp,
port, port,
@ -488,39 +521,35 @@ let probeIscsiExists = coroutine(function ({
chapUser, chapUser,
chapPassword chapPassword
}) { }) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host); const deviceConfig = {
let deviceConfig = {
target: targetIp, target: targetIp,
targetIQN: targetIqn, targetIQN: targetIqn,
SCSIid: scsiId, SCSIid: scsiId
}; }
// if we give user and password // if we give user and password
if (chapUser && chapPassword) { if (chapUser && chapPassword) {
deviceConfig.chapUser = chapUser; deviceConfig.chapUser = chapUser
deviceConfig.chapPassword = chapPassword; deviceConfig.chapPassword = chapPassword
} }
// if we give another port than default iSCSI // if we give another port than default iSCSI
if (port) { if (port) {
deviceConfig.port = port; deviceConfig.port = port
} }
let xml = wait(xapi.call('SR.probe', host.ref, deviceConfig, 'lvmoiscsi', {})); const xml = parseXml(wait(xapi.call('SR.probe', host.ref, deviceConfig, 'lvmoiscsi', {})))
xml = parseXml(xml); const srs = []
let srs = [];
forEach(ensureArray(xml['SRlist'].SR), sr => { forEach(ensureArray(xml['SRlist'].SR), sr => {
// get the UUID of SR connected to this LUN // get the UUID of SR connected to this LUN
srs.push({uuid: sr.UUID.trim()}); srs.push({uuid: sr.UUID.trim()})
}); })
return srs; return srs
})
});
probeIscsiExists.params = { probeIscsiExists.params = {
host: { type: 'string' }, host: { type: 'string' },
@ -529,74 +558,68 @@ probeIscsiExists.params = {
targetIqn: { type: 'string' }, targetIqn: { type: 'string' },
scsiId: { type: 'string' }, scsiId: { type: 'string' },
chapUser: { type: 'string', optional: true }, chapUser: { type: 'string', optional: true },
chapPassword: { type: 'string' , optional: true }, chapPassword: { type: 'string', optional: true }
}; }
probeIscsiExists.resolve = {
host: ['host', 'host'],
};
export {probeIscsiExists};
//-------------------------------------------------------------------- probeIscsiExists.resolve = {
host: ['host', 'host']
}
// -------------------------------------------------------------------
// This function helps to detect if this NFS SR already exists in XAPI // This function helps to detect if this NFS SR already exists in XAPI
// It returns a table of SR UUID, empty if no existing connections // It returns a table of SR UUID, empty if no existing connections
let probeNfsExists = coroutine(function ({ export const probeNfsExists = coroutine(function ({
host, host,
server, server,
serverPath, serverPath,
}) { }) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host); const deviceConfig = {
let deviceConfig = {
server, server,
serverpath: serverPath, serverpath: serverPath
}; }
let xml = wait(xapi.call('SR.probe', host.ref, deviceConfig, 'nfs', {})); const xml = parseXml(wait(xapi.call('SR.probe', host.ref, deviceConfig, 'nfs', {})))
xml = parseXml(xml); const srs = []
let srs = [];
forEach(ensureArray(xml['SRlist'].SR), sr => { forEach(ensureArray(xml['SRlist'].SR), sr => {
// get the UUID of SR connected to this LUN // get the UUID of SR connected to this LUN
srs.push({uuid: sr.UUID.trim()}); srs.push({uuid: sr.UUID.trim()})
}); })
return srs; return srs
})
});
probeNfsExists.params = { probeNfsExists.params = {
host: { type: 'string' }, host: { type: 'string' },
server: { type: 'string' }, server: { type: 'string' },
serverPath: { type: 'string' }, serverPath: { type: 'string' }
}; }
probeNfsExists.resolve = { probeNfsExists.resolve = {
host: ['host', 'host'], host: ['host', 'host']
}; }
export {probeNfsExists}; // -------------------------------------------------------------------
//--------------------------------------------------------------------
// This function helps to reattach a forgotten NFS/iSCSI SR // This function helps to reattach a forgotten NFS/iSCSI SR
let reattach = coroutine(function ({ export const reattach = coroutine(function ({
host, host,
uuid, uuid,
nameLabel, nameLabel,
nameDescription, nameDescription,
type, type,
}) { }) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host);
if (type === 'iscsi') { if (type === 'iscsi') {
type = 'lvmoiscsi'; // the internal XAPI name type = 'lvmoiscsi' // the internal XAPI name
} }
let srRef = wait(xapi.call( const srRef = wait(xapi.call(
'SR.introduce', 'SR.introduce',
uuid, uuid,
nameLabel, nameLabel,
@ -605,44 +628,41 @@ let reattach = coroutine(function ({
'user', 'user',
true, true,
{} {}
)); ))
let sr = wait(xapi.call('SR.get_record', srRef)); const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid; return sr.uuid
})
});
reattach.params = { reattach.params = {
host: { type: 'string' }, host: { type: 'string' },
uuid: { type: 'string' }, uuid: { type: 'string' },
nameLabel: { type: 'string' }, nameLabel: { type: 'string' },
nameDescription: { type: 'string' }, nameDescription: { type: 'string' },
type: { type: 'string' }, type: { type: 'string' }
}; }
reattach.resolve = { reattach.resolve = {
host: ['host', 'host'], host: ['host', 'host']
}; }
export {reattach}; // -------------------------------------------------------------------
//--------------------------------------------------------------------
// This function helps to reattach a forgotten ISO SR // This function helps to reattach a forgotten ISO SR
let reattachIso = coroutine(function ({ export const reattachIso = coroutine(function ({
host, host,
uuid, uuid,
nameLabel, nameLabel,
nameDescription, nameDescription,
type, type,
}) { }) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host);
if (type === 'iscsi') { if (type === 'iscsi') {
type = 'lvmoiscsi'; // the internal XAPI name type = 'lvmoiscsi' // the internal XAPI name
} }
let srRef = wait(xapi.call( const srRef = wait(xapi.call(
'SR.introduce', 'SR.introduce',
uuid, uuid,
nameLabel, nameLabel,
@ -651,22 +671,20 @@ let reattachIso = coroutine(function ({
'iso', 'iso',
true, true,
{} {}
)); ))
let sr = wait(xapi.call('SR.get_record', srRef)); const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid; return sr.uuid
})
});
reattachIso.params = { reattachIso.params = {
host: { type: 'string' }, host: { type: 'string' },
uuid: { type: 'string' }, uuid: { type: 'string' },
nameLabel: { type: 'string' }, nameLabel: { type: 'string' },
nameDescription: { type: 'string' }, nameDescription: { type: 'string' },
type: { type: 'string' }, type: { type: 'string' }
}; }
reattachIso.resolve = {
host: ['host', 'host'],
};
export {reattachIso}; reattachIso.resolve = {
host: ['host', 'host']
}

View File

@ -1,10 +1,14 @@
$debug = (require 'debug') 'xo:api:vm' $debug = (require 'debug') 'xo:api:vm'
$findWhere = require 'lodash.find' $findWhere = require 'lodash.find'
$result = require 'lodash.result'
$forEach = require 'lodash.foreach' $forEach = require 'lodash.foreach'
$isArray = require 'lodash.isarray' $isArray = require 'lodash.isarray'
$findIndex = require 'lodash.findindex'
$request = require('bluebird').promisify(require('request'))
{$coroutine, $wait} = require '../fibers-utils' {$coroutine, $wait} = require '../fibers-utils'
{formatXml: $js2xml} = require '../utils' {formatXml: $js2xml} = require '../utils'
{parseXml} = require '../utils'
$isVMRunning = do -> $isVMRunning = do ->
runningStates = { runningStates = {
@ -35,14 +39,16 @@ create = $coroutine ({
# TODO: remove existing VIFs. # TODO: remove existing VIFs.
# Creates associated virtual interfaces. # Creates associated virtual interfaces.
#
# FIXME: device n may already exists, we have to find the first
# free device number.
deviceId = 0
$forEach VIFs, (VIF) => $forEach VIFs, (VIF) =>
network = @getObject VIF.network, 'network' network = @getObject VIF.network, 'network'
$wait xapi.call 'VIF.create', { $wait xapi.call 'VIF.create', {
# FIXME: device n may already exists, we have to find the first
# free device number.
device: '0' device: String(deviceId++)
MAC: VIF.MAC ? '' MAC: VIF.MAC ? ''
MTU: '1500' MTU: '1500'
network: network.ref network: network.ref
@ -528,6 +534,14 @@ set = $coroutine (params) ->
else else
$wait xapi.call 'VM.set_ha_restart_priority', ref, "" $wait xapi.call 'VM.set_ha_restart_priority', ref, ""
if 'auto_poweron' of params
{auto_poweron} = params
if auto_poweron
$wait xapi.call 'VM.add_to_other_config', ref, 'auto_poweron', 'true'
else
$wait xapi.call 'VM.remove_from_other_config', ref, 'auto_poweron'
# Other fields. # Other fields.
for param, fields of { for param, fields of {
'name_label' 'name_label'
@ -574,15 +588,10 @@ exports.set = set
restart = $coroutine ({vm, force}) -> restart = $coroutine ({vm, force}) ->
xapi = @getXAPI(vm) xapi = @getXAPI(vm)
try if force
# Attempts a clean reboot.
$wait xapi.call 'VM.clean_reboot', vm.ref
catch error
return unless error[0] is 'VM_MISSING_PV_DRIVERS'
@throw 'INVALID_PARAMS' unless force
$wait xapi.call 'VM.hard_reboot', vm.ref $wait xapi.call 'VM.hard_reboot', vm.ref
else
$wait xapi.call 'VM.clean_reboot', vm.ref
return true return true
@ -955,3 +964,141 @@ createInterface.resolve = {
} }
createInterface.permission = 'admin' createInterface.permission = 'admin'
exports.createInterface = createInterface exports.createInterface = createInterface
#---------------------------------------------------------------------
attachPci = $coroutine ({vm, pciId}) ->
xapi = @getXAPI vm
$wait xapi.call 'VM.add_to_other_config', vm.ref, 'pci', pciId
return true
attachPci.params = {
vm: { type: 'string' }
pciId: { type: 'string' }
}
attachPci.resolve = {
vm: ['vm', 'VM'],
}
attachPci.permission = 'admin'
exports.attachPci = attachPci
#---------------------------------------------------------------------
detachPci = $coroutine ({vm}) ->
xapi = @getXAPI vm
$wait xapi.call 'VM.remove_from_other_config', vm.ref, 'pci'
return true
detachPci.params = {
vm: { type: 'string' }
}
detachPci.resolve = {
vm: ['vm', 'VM'],
}
detachPci.permission = 'admin'
exports.detachPci = detachPci
#---------------------------------------------------------------------
stats = $coroutine ({vm}) ->
xapi = @getXAPI vm
host = @getObject vm.$container
do (type = host.type) =>
if type is 'pool'
host = @getObject host.master, 'host'
else unless type is 'host'
throw new Error "unexpected type: got #{type} instead of host"
[response, body] = $wait $request {
method: 'get'
rejectUnauthorized: false
url: 'https://'+host.address+'/vm_rrd?session_id='+xapi.sessionId+'&uuid='+vm.UUID
}
if response.statusCode isnt 200
throw new Error('Cannot fetch the RRDs')
json = parseXml(body)
# Find index of needed objects for getting their values after
cpusIndexes = []
index = 0
while (pos = $findIndex(json.rrd.ds, 'name', 'cpu' + index++)) isnt -1
cpusIndexes.push(pos)
vifsIndexes = []
index = 0
while (pos = $findIndex(json.rrd.ds, 'name', 'vif_' + index + '_rx')) isnt -1
vifsIndexes.push(pos)
vifsIndexes.push($findIndex(json.rrd.ds, 'name', 'vif_' + (index++) + '_tx'))
xvdsIndexes = []
index = 97 # Starting to browse ascii table from 'a' to 'z' (122)
while index <= 122 and (pos = $findIndex(json.rrd.ds, 'name', 'vbd_xvd' + String.fromCharCode(index) + '_read')) isnt -1
xvdsIndexes.push(pos)
xvdsIndexes.push($findIndex(json.rrd.ds, 'name', 'vbd_xvd' + String.fromCharCode(index++) + '_write'))
memoryFreeIndex = $findIndex(json.rrd.ds, 'name': 'memory_internal_free')
memoryIndex = $findIndex(json.rrd.ds, 'name': 'memory')
memoryFree = []
memoryUsed = []
memory = []
cpus = []
vifs = []
xvds = []
date = [] #TODO
baseDate = json.rrd.lastupdate
dateStep = json.rrd.step
numStep = json.rrd.rra[0].database.row.length - 1
$forEach json.rrd.rra[0].database.row, (n, key) ->
# WARNING! memoryFree is in Kb not in b, memory is in b
memoryFree.push(n.v[memoryFreeIndex]*1024)
memoryUsed.push(Math.round(parseInt(n.v[memoryIndex])-(n.v[memoryFreeIndex]*1024)))
memory.push(parseInt(n.v[memoryIndex]))
date.push(baseDate - (dateStep * (numStep - key)))
# build the multi dimensional arrays
$forEach cpusIndexes, (value, key) ->
cpus[key] ?= []
cpus[key].push(n.v[value]*100)
return
$forEach vifsIndexes, (value, key) ->
vifs[key] ?= []
vifs[key].push(if n.v[value] == 'NaN' then null else n.v[value]) # * (if key % 2 then -1 else 1))
return
$forEach xvdsIndexes, (value, key) ->
xvds[key] ?= []
xvds[key].push(if n.v[value] == 'NaN' then null else n.v[value]) # * (if key % 2 then -1 else 1))
return
return
# the final object
return {
memoryFree: memoryFree
memoryUsed: memoryUsed
memory: memory
date: date
cpus: cpus
vifs: vifs
xvds: xvds
}
stats.params = {
id: { type: 'string' }
}
stats.resolve = {
vm: ['id', ['VM', 'VM-snapshot']],
}
exports.stats = stats;

View File

@ -1,7 +1,5 @@
function getAllObjects() { export function getAllObjects () {
return this.getObjects(); return this.getObjects()
} }
getAllObjects.permission = ''; getAllObjects.permission = ''
export {getAllObjects};

View File

@ -1,25 +1,25 @@
import Bluebird from 'bluebird'; import Bluebird from 'bluebird'
import isArray from 'lodash.isarray'; import isArray from 'lodash.isarray'
import isObject from 'lodash.isobject'; import isObject from 'lodash.isobject'
import makeError from 'make-error'; import Model from './model'
import Model from './model'; import {BaseError} from 'make-error'
import {EventEmitter} from 'events'; import {EventEmitter} from 'events'
import {mapInPlace} from './utils'; import {mapInPlace} from './utils'
//==================================================================== // ===================================================================
function ModelAlreadyExists(id) { export class ModelAlreadyExists extends BaseError {
ModelAlreadyExists.super.call(this, 'this model already exists: ' + id); constructor (id) {
super('this model already exists: ' + id)
}
} }
makeError(ModelAlreadyExists);
export {ModelAlreadyExists};
//==================================================================== // ===================================================================
export default class Collection extends EventEmitter { export default class Collection extends EventEmitter {
// Default value for Model. // Default value for Model.
get Model () { get Model () {
return Model; return Model
} }
// Make this property writable. // Make this property writable.
@ -28,41 +28,41 @@ export default class Collection extends EventEmitter {
configurable: true, configurable: true,
enumerale: true, enumerale: true,
value: Model, value: Model,
writable: true, writable: true
}); })
} }
constructor () { constructor () {
super(); super()
} }
add (models, opts) { add (models, opts) {
let array = isArray(models); const array = isArray(models)
if (!array) { if (!array) {
models = [models]; models = [models]
} }
let {Model} = this; const {Model} = this
mapInPlace(models, model => { mapInPlace(models, model => {
if (!(model instanceof Model)) { if (!(model instanceof Model)) {
model = new Model(model); model = new Model(model)
} }
let error = model.validate(); const error = model.validate()
if (error) { if (error) {
// TODO: Better system inspired by Backbone.js // TODO: Better system inspired by Backbone.js
throw error; throw error
} }
return model.properties; return model.properties
}); })
return Bluebird.try(this._add, [models, opts], this).then(models => { return Bluebird.try(this._add, [models, opts], this).then(models => {
this.emit('add', models); this.emit('add', models)
return array ? models : new this.Model(models[0]); return array ? models : new this.Model(models[0])
}); })
} }
first (properties) { first (properties) {
@ -70,12 +70,11 @@ export default class Collection extends EventEmitter {
properties = (properties !== undefined) ? properties = (properties !== undefined) ?
{ id: properties } : { id: properties } :
{} {}
;
} }
return Bluebird.try(this._first, [properties], this).then( return Bluebird.try(this._first, [properties], this).then(
model => model && new this.Model(model) model => model && new this.Model(model)
); )
} }
get (properties) { get (properties) {
@ -83,93 +82,92 @@ export default class Collection extends EventEmitter {
properties = (properties !== undefined) ? properties = (properties !== undefined) ?
{ id: properties } : { id: properties } :
{} {}
;
} }
return Bluebird.try(this._get, [properties], this); return Bluebird.try(this._get, [properties], this)
} }
remove (ids) { remove (ids) {
if (!isArray(ids)) { if (!isArray(ids)) {
ids = [ids]; ids = [ids]
} }
return Bluebird.try(this._remove, [ids], this).then(() => { return Bluebird.try(this._remove, [ids], this).then(() => {
this.emit('remove', ids); this.emit('remove', ids)
return true; return true
}); })
} }
update (models) { update (models) {
var array = isArray(models); var array = isArray(models)
if (!isArray(models)) { if (!isArray(models)) {
models = [models]; models = [models]
} }
let {Model} = this; const {Model} = this
mapInPlace(models, model => { mapInPlace(models, model => {
if (!(model instanceof Model)) { if (!(model instanceof Model)) {
// TODO: Problems, we may be mixing in some default // TODO: Problems, we may be mixing in some default
// properties which will overwrite existing ones. // properties which will overwrite existing ones.
model = new Model(model); model = new Model(model)
} }
let id = model.get('id'); const id = model.get('id')
// Missing models should be added not updated. // Missing models should be added not updated.
if (id === undefined) { if (id === undefined) {
// FIXME: should not throw an exception but return a rejected promise. // FIXME: should not throw an exception but return a rejected promise.
throw new Error('a model without an id cannot be updated'); throw new Error('a model without an id cannot be updated')
} }
var error = model.validate(); var error = model.validate()
if (error !== undefined) { if (error !== undefined) {
// TODO: Better system inspired by Backbone.js. // TODO: Better system inspired by Backbone.js.
throw error; throw error
} }
return model.properties; return model.properties
}); })
return Bluebird.try(this._update, [models], this).then(models => { return Bluebird.try(this._update, [models], this).then(models => {
this.emit('update', models); this.emit('update', models)
return array ? models : new this.Model(models[0]); return array ? models : new this.Model(models[0])
}); })
} }
// Methods to override in implementations. // Methods to override in implementations.
_add () { _add () {
throw new Error('not implemented'); throw new Error('not implemented')
} }
_get () { _get () {
throw new Error('not implemented'); throw new Error('not implemented')
} }
_remove () { _remove () {
throw new Error('not implemented'); throw new Error('not implemented')
} }
_update () { _update () {
throw new Error('not implemented'); throw new Error('not implemented')
} }
// Methods which may be overridden in implementations. // Methods which may be overridden in implementations.
count (properties) { count (properties) {
return this.get(properties).get('count'); return this.get(properties).get('count')
} }
exists (properties) { exists (properties) {
/* jshint eqnull: true */ /* jshint eqnull: true */
return this.first(properties).then(model => model != null); return this.first(properties).then(model => model != null)
} }
_first (properties) { _first (properties) {
return Bluebird.try(this.get, [properties], this).then( return Bluebird.try(this.get, [properties], this).then(
models => models.length ? models[0] : null models => models.length ? models[0] : null
); )
} }
} }

View File

@ -1,21 +1,21 @@
import Bluebird, {coroutine} from 'bluebird'; import Bluebird, {coroutine} from 'bluebird'
import Collection, {ModelAlreadyExists} from '../collection'; import Collection, {ModelAlreadyExists} from '../collection'
import difference from 'lodash.difference'; import difference from 'lodash.difference'
import filter from 'lodash.filter'; import filter from 'lodash.filter'
import forEach from 'lodash.foreach'; import forEach from 'lodash.foreach'
import getKey from 'lodash.keys'; import getKey from 'lodash.keys'
import isEmpty from 'lodash.isempty'; import isEmpty from 'lodash.isempty'
import map from 'lodash.map'; import map from 'lodash.map'
import thenRedis from 'then-redis'; import thenRedis from 'then-redis'
////////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////
// Data model: // Data model:
// - prefix +'_id': value of the last generated identifier; // - prefix +'_id': value of the last generated identifier;
// - prefix +'_ids': set containing identifier of all models; // - prefix +'_ids': set containing identifier of all models;
// - prefix +'_'+ index +':' + value: set of identifiers which have // - prefix +'_'+ index +':' + value: set of identifiers which have
// value for the given index. // value for the given index.
// - prefix +':'+ id: hash containing the properties of a model; // - prefix +':'+ id: hash containing the properties of a model;
////////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////
// TODO: then-redis sends commands in order, we should use this // TODO: then-redis sends commands in order, we should use this
// semantic to simplify the code. // semantic to simplify the code.
@ -32,118 +32,118 @@ export default class Redis extends Collection {
prefix, prefix,
uri = 'tcp://localhost:6379', uri = 'tcp://localhost:6379',
}) { }) {
super(); super()
this.indexes = indexes; this.indexes = indexes
this.prefix = prefix; this.prefix = prefix
this.redis = connection || thenRedis.createClient(uri); this.redis = connection || thenRedis.createClient(uri)
} }
_extract (ids) { _extract (ids) {
let prefix = this.prefix + ':'; const prefix = this.prefix + ':'
let {redis} = this; const {redis} = this
let models = []; const models = []
return Bluebird.map(ids, id => { return Bluebird.map(ids, id => {
return redis.hgetall(prefix + id).then(model => { return redis.hgetall(prefix + id).then(model => {
// If empty, consider it a no match. // If empty, consider it a no match.
if (isEmpty(model)) { if (isEmpty(model)) {
return; return
} }
// Mix the identifier in. // Mix the identifier in.
model.id = id; model.id = id
models.push(model); models.push(model)
}); })
}).return(models); }).return(models)
} }
_add (models, {replace = false} = {}) { _add (models, {replace = false} = {}) {
// TODO: remove “replace” which is a temporary measure, implement // TODO: remove “replace” which is a temporary measure, implement
// “set()” instead. // “set()” instead.
let {indexes, prefix, redis} = this; const {indexes, prefix, redis} = this
return Bluebird.map(models, coroutine(function * (model) { return Bluebird.map(models, coroutine(function * (model) {
// Generate a new identifier if necessary. // Generate a new identifier if necessary.
if (model.id === undefined) { if (model.id === undefined) {
model.id = String(yield redis.incr(prefix + '_id')); model.id = String(yield redis.incr(prefix + '_id'))
} }
let success = yield redis.sadd(prefix + '_ids', model.id); const success = yield redis.sadd(prefix + '_ids', model.id)
// The entry already exists an we are not in replace mode. // The entry already exists an we are not in replace mode.
if (!success && !replace) { if (!success && !replace) {
throw new ModelAlreadyExists(model.id); throw new ModelAlreadyExists(model.id)
} }
// TODO: Remove existing fields. // TODO: Remove existing fields.
let params = []; const params = []
forEach(model, (value, name) => { forEach(model, (value, name) => {
// No need to store the identifier (already in the key). // No need to store the identifier (already in the key).
if (name === 'id') { if (name === 'id') {
return; return
} }
params.push(name, value); params.push(name, value)
}); })
let promises = [ const promises = [
redis.hmset(prefix + ':' + model.id, ...params), redis.hmset(prefix + ':' + model.id, ...params)
]; ]
// Update indexes. // Update indexes.
forEach(indexes, (index) => { forEach(indexes, (index) => {
let value = model[index]; const value = model[index]
if (value === undefined) { if (value === undefined) {
return; return
} }
let key = prefix + '_' + index + ':' + value; const key = prefix + '_' + index + ':' + value
promises.push(redis.sadd(key, model.id)); promises.push(redis.sadd(key, model.id))
}); })
yield Bluebird.all(promises); yield Bluebird.all(promises)
return model; return model
})); }))
} }
_get (properties) { _get (properties) {
let {prefix, redis} = this; const {prefix, redis} = this
if (isEmpty(properties)) { if (isEmpty(properties)) {
return redis.smembers(prefix + '_ids').then(ids => this._extract(ids)); return redis.smembers(prefix + '_ids').then(ids => this._extract(ids))
} }
// Special treatment for the identifier. // Special treatment for the identifier.
let id = properties.id; const id = properties.id
if (id !== undefined) { if (id !== undefined) {
delete properties.id delete properties.id
return this._extract([id]).then(models => { return this._extract([id]).then(models => {
return (models.length && !isEmpty(properties)) ? return (models.length && !isEmpty(properties)) ?
filter(models) : filter(models) :
models models
;
}); })
} }
let {indexes} = this; const {indexes} = this
// Check for non indexed fields. // Check for non indexed fields.
let unfit = difference(getKey(properties), indexes); const unfit = difference(getKey(properties), indexes)
if (unfit.length) { if (unfit.length) {
throw new Error('fields not indexed: ' + unfit.join()); throw new Error('fields not indexed: ' + unfit.join())
} }
let keys = map(properties, (value, index) => prefix + '_' + index + ':' + value); const keys = map(properties, (value, index) => prefix + '_' + index + ':' + value)
return redis.sinter(...keys).then(ids => this._extract(ids)); return redis.sinter(...keys).then(ids => this._extract(ids))
} }
_remove (ids) { _remove (ids) {
let {prefix, redis} = this; const {prefix, redis} = this
// TODO: handle indexes. // TODO: handle indexes.
@ -152,11 +152,11 @@ export default class Redis extends Collection {
redis.srem(prefix + '_ids', ...ids), redis.srem(prefix + '_ids', ...ids),
// Remove the models. // Remove the models.
redis.del(map(ids, id => prefix + ':' + id)), redis.del(map(ids, id => prefix + ':' + id))
]); ])
} }
_update (models) { _update (models) {
return this._add(models, { replace: true }); return this._add(models, { replace: true })
} }
} }

View File

@ -1,83 +1,79 @@
'use strict'; 'use strict'
//==================================================================== // ===================================================================
var EventEmitter = require('events').EventEmitter; var EventEmitter = require('events').EventEmitter
var inherits = require('util').inherits; var inherits = require('util').inherits
var assign = require('lodash.assign'); var assign = require('lodash.assign')
//==================================================================== // ===================================================================
var has = Object.prototype.hasOwnProperty; var has = Object.prototype.hasOwnProperty
has = has.call.bind(has); has = has.call.bind(has)
function noop () {} function noop () {}
//==================================================================== // ===================================================================
function Connection (opts) { function Connection (opts) {
EventEmitter.call(this); EventEmitter.call(this)
this.data = Object.create(null); this.data = Object.create(null)
this._close = opts.close; this._close = opts.close
this.notify = opts.notify; this.notify = opts.notify
} }
inherits(Connection, EventEmitter); inherits(Connection, EventEmitter)
assign(Connection.prototype, { assign(Connection.prototype, {
// Close the connection. // Close the connection.
close: function () { close: function () {
// Prevent errors when the connection is closed more than once. // Prevent errors when the connection is closed more than once.
this.close = noop; this.close = noop
this._close(); this._close()
this.emit('close'); this.emit('close')
// Releases values AMAP to ease the garbage collecting. // Releases values AMAP to ease the garbage collecting.
for (var key in this) for (var key in this) {
{ if (key !== 'close' && has(this, key)) {
if (key !== 'close' && has(this, key)) delete this[key]
{
delete this[key];
} }
} }
}, },
// Gets the value for this key. // Gets the value for this key.
get: function (key, defaultValue) { get: function (key, defaultValue) {
var data = this.data; var data = this.data
if (key in data) if (key in data) {
{ return data[key]
return data[key];
} }
if (arguments.length >= 2) if (arguments.length >= 2) {
{ return defaultValue
return defaultValue;
} }
throw new Error('no value for `'+ key +'`'); throw new Error('no value for `' + key + '`')
}, },
// Checks whether there is a value for this key. // Checks whether there is a value for this key.
has: function (key) { has: function (key) {
return key in this.data; return key in this.data
}, },
// Sets the value for this key. // Sets the value for this key.
set: function (key, value) { set: function (key, value) {
this.data[key] = value; this.data[key] = value
}, },
unset: function (key) { unset: function (key) {
delete this.data[key]; delete this.data[key]
}, }
}); })
//==================================================================== // ===================================================================
module.exports = Connection; module.exports = Connection

View File

@ -1,182 +1,182 @@
'use strict'; 'use strict'
//==================================================================== // ===================================================================
/* jshint mocha: true */ /* eslint-env mocha */
var expect = require('chai').expect; var expect = require('chai').expect
//-------------------------------------------------------------------- // -------------------------------------------------------------------
var Promise = require('bluebird'); var Promise = require('bluebird')
//-------------------------------------------------------------------- // -------------------------------------------------------------------
var utils = require('./fibers-utils'); var utils = require('./fibers-utils')
var $coroutine = utils.$coroutine; var $coroutine = utils.$coroutine
//==================================================================== // ===================================================================
describe('$coroutine', function () { describe('$coroutine', function () {
it('creates a on which returns promises', function () { it('creates a on which returns promises', function () {
var fn = $coroutine(function () {}); var fn = $coroutine(function () {})
expect(fn().then).to.be.a('function'); expect(fn().then).to.be.a('function')
}); })
it('creates a function which runs in a new fiber', function () { it('creates a function which runs in a new fiber', function () {
var previous = require('fibers').current; var previous = require('fibers').current
var fn = $coroutine(function () { var fn = $coroutine(function () {
var current = require('fibers').current; var current = require('fibers').current
expect(current).to.exists; expect(current).to.exists
expect(current).to.not.equal(previous); expect(current).to.not.equal(previous)
}); })
fn(); fn()
}); })
it('forwards all arguments (even this)', function () { it('forwards all arguments (even this)', function () {
var self = {}; var self = {}
var arg1 = {}; var arg1 = {}
var arg2 = {}; var arg2 = {}
$coroutine(function (arg1_, arg2_) { $coroutine(function (arg1_, arg2_) {
expect(this).to.equal(self); expect(this).to.equal(self)
expect(arg1_).to.equal(arg1); expect(arg1_).to.equal(arg1)
expect(arg2_).to.equal(arg2); expect(arg2_).to.equal(arg2)
}).call(self, arg1, arg2); }).call(self, arg1, arg2)
}); })
}); })
//-------------------------------------------------------------------- // -------------------------------------------------------------------
describe('$wait', function () { describe('$wait', function () {
var $wait = utils.$wait; var $wait = utils.$wait
it('waits for a promise', function (done) { it('waits for a promise', function (done) {
$coroutine(function () { $coroutine(function () {
var value = {}; var value = {}
var promise = Promise.cast(value); var promise = Promise.cast(value)
expect($wait(promise)).to.equal(value); expect($wait(promise)).to.equal(value)
done(); done()
})(); })()
}); })
it('handles promise rejection', function (done) { it('handles promise rejection', function (done) {
$coroutine(function () { $coroutine(function () {
var promise = Promise.reject('an exception'); var promise = Promise.reject('an exception')
expect(function () { expect(function () {
$wait(promise); $wait(promise)
}).to.throw('an exception'); }).to.throw('an exception')
done(); done()
})(); })()
}); })
it('waits for a continuable', function (done) { it('waits for a continuable', function (done) {
$coroutine(function () { $coroutine(function () {
var value = {}; var value = {}
var continuable = function (callback) { var continuable = function (callback) {
callback(null, value); callback(null, value)
}; }
expect($wait(continuable)).to.equal(value); expect($wait(continuable)).to.equal(value)
done(); done()
})(); })()
}); })
it('handles continuable error', function (done) { it('handles continuable error', function (done) {
$coroutine(function () { $coroutine(function () {
var continuable = function (callback) { var continuable = function (callback) {
callback('an exception'); callback('an exception')
}; }
expect(function () { expect(function () {
$wait(continuable); $wait(continuable)
}).to.throw('an exception'); }).to.throw('an exception')
done(); done()
})(); })()
}); })
it('forwards scalar values', function (done) { it('forwards scalar values', function (done) {
$coroutine(function () { $coroutine(function () {
var value = 'a scalar value'; var value = 'a scalar value'
expect($wait(value)).to.equal(value); expect($wait(value)).to.equal(value)
value = [ value = [
'foo', 'foo',
'bar', 'bar',
'baz', 'baz'
]; ]
expect($wait(value)).to.deep.equal(value); expect($wait(value)).to.deep.equal(value)
value = []; value = []
expect($wait(value)).to.deep.equal(value); expect($wait(value)).to.deep.equal(value)
value = { value = {
foo: 'foo', foo: 'foo',
bar: 'bar', bar: 'bar',
baz: 'baz', baz: 'baz'
}; }
expect($wait(value)).to.deep.equal(value); expect($wait(value)).to.deep.equal(value)
value = {}; value = {}
expect($wait(value)).to.deep.equal(value); expect($wait(value)).to.deep.equal(value)
done(); done()
})(); })()
}); })
it('handles arrays of promises/continuables', function (done) { it('handles arrays of promises/continuables', function (done) {
$coroutine(function () { $coroutine(function () {
var value1 = {}; var value1 = {}
var value2 = {}; var value2 = {}
var promise = Promise.cast(value1); var promise = Promise.cast(value1)
var continuable = function (callback) { var continuable = function (callback) {
callback(null, value2); callback(null, value2)
}; }
var results = $wait([promise, continuable]); var results = $wait([promise, continuable])
expect(results[0]).to.equal(value1); expect(results[0]).to.equal(value1)
expect(results[1]).to.equal(value2); expect(results[1]).to.equal(value2)
done(); done()
})(); })()
}); })
it('handles maps of promises/continuable', function (done) { it('handles maps of promises/continuable', function (done) {
$coroutine(function () { $coroutine(function () {
var value1 = {}; var value1 = {}
var value2 = {}; var value2 = {}
var promise = Promise.cast(value1); var promise = Promise.cast(value1)
var continuable = function (callback) { var continuable = function (callback) {
callback(null, value2); callback(null, value2)
}; }
var results = $wait({ var results = $wait({
foo: promise, foo: promise,
bar: continuable bar: continuable
}); })
expect(results.foo).to.equal(value1); expect(results.foo).to.equal(value1)
expect(results.bar).to.equal(value2); expect(results.bar).to.equal(value2)
done(); done()
})(); })()
}); })
it('handles nested arrays/maps', function (done) { it('handles nested arrays/maps', function (done) {
var promise = Promise.cast('a promise'); var promise = Promise.cast('a promise')
var continuable = function (callback) { var continuable = function (callback) {
callback(null, 'a continuable'); callback(null, 'a continuable')
}; }
$coroutine(function () { $coroutine(function () {
expect($wait({ expect($wait({
@ -191,9 +191,9 @@ describe('$wait', function () {
'a continuable', 'a continuable',
'a scalar' 'a scalar'
] ]
}); })
done(); done()
})(); })()
}); })
}); })

View File

@ -4,7 +4,7 @@ $sinon = require 'sinon'
#--------------------------------------------------------------------- #---------------------------------------------------------------------
{$MappedCollection} = require './MappedCollection.coffee' {$MappedCollection} = require './MappedCollection'
$nonBindedHelpers = require './helpers' $nonBindedHelpers = require './helpers'

View File

@ -1,413 +1,425 @@
import createLogger from 'debug'; import createLogger from 'debug'
let debug = createLogger('xo:main'); const debug = createLogger('xo:main')
let debugPlugin = createLogger('xo:plugin'); const debugPlugin = createLogger('xo:plugin')
import Bluebird from 'bluebird'; import Bluebird from 'bluebird'
Bluebird.longStackTraces(); Bluebird.longStackTraces()
import appConf from 'app-conf'; import appConf from 'app-conf'
import assign from 'lodash.assign'; import assign from 'lodash.assign'
import bind from 'lodash.bind'; import bind from 'lodash.bind'
import createConnectApp from 'connect'; import createConnectApp from 'connect'
import eventToPromise from 'event-to-promise'; import eventToPromise from 'event-to-promise'
import forEach from 'lodash.foreach'; import forEach from 'lodash.foreach'
import has from 'lodash.has'; import has from 'lodash.has'
import isArray from 'lodash.isarray'; import isArray from 'lodash.isarray'
import isFunction from 'lodash.isfunction'; import isFunction from 'lodash.isfunction'
import map from 'lodash.map'; import map from 'lodash.map'
import pick from 'lodash.pick'; import pick from 'lodash.pick'
import serveStatic from 'serve-static'; import serveStatic from 'serve-static'
import WebSocket from 'ws'; import WebSocket from 'ws'
import { import {
AlreadyAuthenticated, AlreadyAuthenticated,
InvalidCredential, InvalidCredential,
InvalidParameters, InvalidParameters,
NoSuchObject, NoSuchObject,
NotImplementd, NotImplemented
} from './api-errors'; } from './api-errors'
import {coroutine} from 'bluebird'; import {coroutine} from 'bluebird'
import {createServer as createJsonRpcServer} from 'json-rpc'; import {createServer as createJsonRpcServer} from 'json-rpc'
import {readFile} from 'fs-promise'; import {readFile} from 'fs-promise'
import Api from './api'; import Api from './api'
import WebServer from 'http-server-plus'; import WebServer from 'http-server-plus'
import wsProxy from './ws-proxy'; import wsProxy from './ws-proxy'
import XO from './xo'; import XO from './xo'
//==================================================================== // ===================================================================
let info = (...args) => { const info = (...args) => {
console.info('[Info]', ...args); console.info('[Info]', ...args)
}; }
let warn = (...args) => { const warn = (...args) => {
console.warn('[Warn]', ...args); console.warn('[Warn]', ...args)
}; }
//==================================================================== // ===================================================================
const DEFAULTS = { const DEFAULTS = {
http: { http: {
listen: [ listen: [
{ port: 80 }, { port: 80 }
], ],
mounts: {}, mounts: {}
}, }
}; }
const DEPRECATED_ENTRIES = [ const DEPRECATED_ENTRIES = [
'users', 'users',
'servers', 'servers'
]; ]
let loadConfiguration = coroutine(function *() { const loadConfiguration = coroutine(function * () {
let config = yield appConf.load('xo-server', { const config = yield appConf.load('xo-server', {
defaults: DEFAULTS, defaults: DEFAULTS,
ignoreUnknownFormats: true, ignoreUnknownFormats: true
}); })
debug('Configuration loaded.'); debug('Configuration loaded.')
// Print a message if deprecated entries are specified. // Print a message if deprecated entries are specified.
forEach(DEPRECATED_ENTRIES, entry => { forEach(DEPRECATED_ENTRIES, entry => {
if (has(config, entry)) { if (has(config, entry)) {
warn(`${entry} configuration is deprecated.`); warn(`${entry} configuration is deprecated.`)
} }
}); })
return config; return config
}); })
//==================================================================== // ===================================================================
let loadPlugin = Bluebird.method(function (pluginConf, pluginName) { const loadPlugin = Bluebird.method(function (pluginConf, pluginName) {
debugPlugin('loading %s', pluginName); debugPlugin('loading %s', pluginName)
var pluginPath; var pluginPath
try { try {
pluginPath = require.resolve('xo-server-' + pluginName); pluginPath = require.resolve('xo-server-' + pluginName)
} catch (e) { } catch (e) {
pluginPath = require.resolve(pluginName); pluginPath = require.resolve(pluginName)
} }
var plugin = require(pluginPath); var plugin = require(pluginPath)
if (isFunction(plugin)) { if (isFunction(plugin)) {
plugin = plugin(pluginConf); plugin = plugin(pluginConf)
} }
return plugin.load(this); return plugin.load(this)
}); })
let loadPlugins = function (plugins, xo) { const loadPlugins = function (plugins, xo) {
return Bluebird.all(map(plugins, loadPlugin, xo)).then(() => { return Bluebird.all(map(plugins, loadPlugin, xo)).then(() => {
debugPlugin('all plugins loaded'); debugPlugin('all plugins loaded')
}); })
}; }
//==================================================================== // ===================================================================
let makeWebServerListen = coroutine(function *(opts) { const makeWebServerListen = coroutine(function * (opts) {
// Read certificate and key if necessary. // Read certificate and key if necessary.
let {certificate, key} = opts; const {certificate, key} = opts
if (certificate && key) { if (certificate && key) {
[opts.certificate, opts.key] = yield Bluebird.all([ [opts.certificate, opts.key] = yield Bluebird.all([
readFile(certificate), readFile(certificate),
readFile(key), readFile(key)
]); ])
} }
try { try {
let niceAddress = yield this.listen(opts); const niceAddress = yield this.listen(opts)
debug(`Web server listening on ${niceAddress}`); debug(`Web server listening on ${niceAddress}`)
} catch (error) { } catch (error) {
warn(`Web server could not listen on ${error.niceAddress}`); warn(`Web server could not listen on ${error.niceAddress}`)
let {code} = error; const {code} = error
if (code === 'EACCES') { if (code === 'EACCES') {
warn(' Access denied.'); warn(' Access denied.')
warn(' Ports < 1024 are often reserved to privileges users.'); warn(' Ports < 1024 are often reserved to privileges users.')
} else if (code === 'EADDRINUSE') { } else if (code === 'EADDRINUSE') {
warn(' Address already in use.'); warn(' Address already in use.')
} }
} }
}); })
let createWebServer = opts => { const createWebServer = opts => {
let webServer = new WebServer(); const webServer = new WebServer()
return Bluebird return Bluebird
.bind(webServer).return(opts).map(makeWebServerListen) .bind(webServer).return(opts).map(makeWebServerListen)
.return(webServer) .return(webServer)
;
};
//==================================================================== }
let setUpStaticFiles = (connect, opts) => { // ===================================================================
const setUpStaticFiles = (connect, opts) => {
forEach(opts, (paths, url) => { forEach(opts, (paths, url) => {
if (!isArray(paths)) { if (!isArray(paths)) {
paths = [paths]; paths = [paths]
} }
forEach(paths, path => { forEach(paths, path => {
debug('Setting up %s → %s', url, path); debug('Setting up %s → %s', url, path)
connect.use(url, serveStatic(path)); connect.use(url, serveStatic(path))
}); })
}); })
}; }
//==================================================================== // ===================================================================
let errorClasses = { const errorClasses = {
ALREADY_AUTHENTICATED: AlreadyAuthenticated, ALREADY_AUTHENTICATED: AlreadyAuthenticated,
INVALID_CREDENTIAL: InvalidCredential, INVALID_CREDENTIAL: InvalidCredential,
INVALID_PARAMS: InvalidParameters, INVALID_PARAMS: InvalidParameters,
NO_SUCH_OBJECT: NoSuchObject, NO_SUCH_OBJECT: NoSuchObject,
NOT_IMPLEMENTED: NotImplementd, NOT_IMPLEMENTED: NotImplemented
}; }
let apiHelpers = { const apiHelpers = {
getUserPublicProperties (user) { getUserPublicProperties (user) {
// Handles both properties and wrapped models. // Handles both properties and wrapped models.
let properties = user.properties || user; const properties = user.properties || user
return pick(properties, 'id', 'email', 'permission'); return pick(properties, 'id', 'email', 'permission')
}, },
getServerPublicProperties (server) { getServerPublicProperties (server) {
// Handles both properties and wrapped models. // Handles both properties and wrapped models.
let properties = server.properties || server; const properties = server.properties || server
return pick(properties, 'id', 'host', 'username'); server = pick(properties, 'id', 'host', 'username')
// Injects connection status.
const xapi = this._xapis[server.id]
server.status = xapi ? xapi.status : 'disconnected'
return server
}, },
throw (errorId, data) { throw (errorId, data) {
throw new (errorClasses[errorId])(data); throw new (errorClasses[errorId])(data)
}
} }
};
let setUpApi = (webServer, xo) => { const setUpApi = (webServer, xo) => {
let context = Object.create(xo); const context = Object.create(xo)
assign(xo, apiHelpers); assign(xo, apiHelpers)
let api = new Api({ const api = new Api({
context, context
}); })
let webSocketServer = new WebSocket.Server({ const webSocketServer = new WebSocket.Server({
server: webServer, server: webServer,
path: '/api/', path: '/api/'
}); })
webSocketServer.on('connection', connection => { webSocketServer.on('connection', connection => {
debug('+ WebSocket connection'); debug('+ WebSocket connection')
let xoConnection; let xoConnection
// Create the JSON-RPC server for this connection. // Create the JSON-RPC server for this connection.
let jsonRpc = createJsonRpcServer(message => { const jsonRpc = createJsonRpcServer(message => {
if (message.type === 'request') { if (message.type === 'request') {
return api.call(xoConnection, message.method, message.params); return api.call(xoConnection, message.method, message.params)
} }
}); })
// Create the abstract XO object for this connection. // Create the abstract XO object for this connection.
xoConnection = xo.createUserConnection({ xoConnection = xo.createUserConnection({
close: bind(connection.close, connection), close: bind(connection.close, connection),
notify: bind(jsonRpc.notify, jsonRpc), notify: bind(jsonRpc.notify, jsonRpc)
}); })
// Close the XO connection with this WebSocket. // Close the XO connection with this WebSocket.
connection.once('close', () => { connection.once('close', () => {
debug('- WebSocket connection'); debug('- WebSocket connection')
xoConnection.close(); xoConnection.close()
}); })
// Connect the WebSocket to the JSON-RPC server. // Connect the WebSocket to the JSON-RPC server.
connection.on('message', message => { connection.on('message', message => {
jsonRpc.write(message); jsonRpc.write(message)
}); })
let onSend = error => { const onSend = error => {
if (error) { if (error) {
warn('WebSocket send:', error.stack); warn('WebSocket send:', error.stack)
}
} }
};
jsonRpc.on('data', data => { jsonRpc.on('data', data => {
connection.send(JSON.stringify(data), onSend); connection.send(JSON.stringify(data), onSend)
}); })
}); })
};
//====================================================================
let getVmConsoleUrl = (xo, id) => {
let vm = xo.getObject(id, ['VM', 'VM-controller']);
if (!vm || vm.power_state !== 'Running') {
return;
} }
let {sessionId} = xo.getXAPI(vm); // ===================================================================
let url; const getVmConsoleUrl = (xo, id) => {
const vm = xo.getObject(id, ['VM', 'VM-controller'])
if (!vm || vm.power_state !== 'Running') {
return
}
const {sessionId} = xo.getXAPI(vm)
let url
forEach(vm.consoles, console => { forEach(vm.consoles, console => {
if (console.protocol === 'rfb') { if (console.protocol === 'rfb') {
url = `${console.location}&session_id=${sessionId}`; url = `${console.location}&session_id=${sessionId}`
return false; return false
} }
}); })
return url; return url
}; }
const CONSOLE_PROXY_PATH_RE = /^\/consoles\/(.*)$/; const CONSOLE_PROXY_PATH_RE = /^\/consoles\/(.*)$/
let setUpConsoleProxy = (webServer, xo) => { const setUpConsoleProxy = (webServer, xo) => {
let webSocketServer = new WebSocket.Server({ const webSocketServer = new WebSocket.Server({
noServer: true, noServer: true
}); })
webServer.on('upgrade', (req, res, head) => { webServer.on('upgrade', (req, res, head) => {
let matches = CONSOLE_PROXY_PATH_RE.exec(req.url); const matches = CONSOLE_PROXY_PATH_RE.exec(req.url)
if (!matches) { if (!matches) {
return; return
} }
let url = getVmConsoleUrl(xo, matches[1]); const url = getVmConsoleUrl(xo, matches[1])
if (!url) { if (!url) {
return; return
} }
// FIXME: lost connection due to VM restart is not detected. // FIXME: lost connection due to VM restart is not detected.
webSocketServer.handleUpgrade(req, res, head, connection => { webSocketServer.handleUpgrade(req, res, head, connection => {
wsProxy(connection, url); wsProxy(connection, url)
}); })
}); })
}; }
//==================================================================== // ===================================================================
let registerPasswordAuthenticationProvider = (xo) => { const registerPasswordAuthenticationProvider = (xo) => {
let passwordAuthenticationProvider = coroutine(function *({ const passwordAuthenticationProvider = coroutine(function * ({
email, email,
password, password,
}) { }) {
/* eslint no-throw-literal: 0 */
if (email === undefined || password === undefined) { if (email === undefined || password === undefined) {
throw new Error('invalid credentials'); throw null
} }
let user = yield xo.users.first({email}); const user = yield xo.users.first({email})
if (!user || !yield user.checkPassword(password)) { if (!user || !(yield user.checkPassword(password))) {
throw new Error('invalid credentials'); throw null
} }
return user; return user
}); })
xo.registerAuthenticationProvider(passwordAuthenticationProvider); xo.registerAuthenticationProvider(passwordAuthenticationProvider)
}; }
let registerTokenAuthenticationProvider = (xo) => { const registerTokenAuthenticationProvider = (xo) => {
let tokenAuthenticationProvider = coroutine(function *({ const tokenAuthenticationProvider = coroutine(function * ({
token: tokenId, token: tokenId,
}) { }) {
/* eslint no-throw-literal: 0 */
if (!tokenId) { if (!tokenId) {
throw new Error('invalid credentials'); throw null
} }
let token = yield xo.tokens.first(tokenId); const token = yield xo.tokens.first(tokenId)
if (!token) { if (!token) {
throw new Error('invalid credentials'); throw null
} }
return token.get('user_id'); return token.get('user_id')
}); })
xo.registerAuthenticationProvider(tokenAuthenticationProvider); xo.registerAuthenticationProvider(tokenAuthenticationProvider)
}; }
//==================================================================== // ===================================================================
let help; let help
{ {
let {name, version} = require('../package'); /* eslint no-lone-blocks: 0 */
help = () => `${name} v${version}`;
const {name, version} = require('../package')
help = () => `${name} v${version}`
} }
//==================================================================== // ===================================================================
let main = coroutine(function *(args) { const main = coroutine(function * (args) {
if (args.indexOf('--help') !== -1 || args.indexOf('-h') !== -1) { if (args.indexOf('--help') !== -1 || args.indexOf('-h') !== -1) {
return help(); return help()
} }
let config = yield loadConfiguration(); const config = yield loadConfiguration()
let webServer = yield createWebServer(config.http.listen); const webServer = yield createWebServer(config.http.listen)
// Now the web server is listening, drop privileges. // Now the web server is listening, drop privileges.
try { try {
let {user, group} = config; const {user, group} = config
if (group) { if (group) {
process.setgid(group); process.setgid(group)
debug('Group changed to', group); debug('Group changed to', group)
} }
if (user) { if (user) {
process.setuid(user); process.setuid(user)
debug('User changed to', user); debug('User changed to', user)
} }
} catch (error) { } catch (error) {
warn('Failed to change user/group:', error); warn('Failed to change user/group:', error)
} }
// Create the main object which will connects to Xen servers and // Create the main object which will connects to Xen servers and
// manages all the models. // manages all the models.
let xo = new XO(); const xo = new XO()
xo.start({ xo.start({
redis: { redis: {
uri: config.redis && config.redis.uri, uri: config.redis && config.redis.uri
}, }
}); })
// Loads default authentication providers. // Loads default authentication providers.
registerPasswordAuthenticationProvider(xo); registerPasswordAuthenticationProvider(xo)
registerTokenAuthenticationProvider(xo); registerTokenAuthenticationProvider(xo)
if (config.plugins) { if (config.plugins) {
yield loadPlugins(config.plugins, xo); yield loadPlugins(config.plugins, xo)
} }
// Connect is used to manage non WebSocket connections. // Connect is used to manage non WebSocket connections.
let connect = createConnectApp(); const connect = createConnectApp()
webServer.on('request', connect); webServer.on('request', connect)
// Must be set up before the API. // Must be set up before the API.
setUpConsoleProxy(webServer, xo); setUpConsoleProxy(webServer, xo)
// Must be set up before the API. // Must be set up before the API.
connect.use(bind(xo.handleProxyRequest, xo)); connect.use(bind(xo.handleProxyRequest, xo))
// Must be set up before the static files. // Must be set up before the static files.
setUpApi(webServer, xo); setUpApi(webServer, xo)
setUpStaticFiles(connect, config.http.mounts); setUpStaticFiles(connect, config.http.mounts)
if (!yield xo.users.exists()) { if (!(yield xo.users.exists())) {
let email = 'admin@admin.net'; const email = 'admin@admin.net'
let password = 'admin'; const password = 'admin'
xo.users.create(email, password, 'admin'); xo.users.create(email, password, 'admin')
info('Default user created:', email, ' with password', password); info('Default user created:', email, ' with password', password)
} }
// Handle gracefully shutdown. // Handle gracefully shutdown.
let closeWebServer = () => { webServer.close(); }; const closeWebServer = () => { webServer.close() }
process.on('SIGINT', closeWebServer); process.on('SIGINT', closeWebServer)
process.on('SIGTERM', closeWebServer); process.on('SIGTERM', closeWebServer)
return eventToPromise(webServer, 'close'); return eventToPromise(webServer, 'close')
}); })
exports = module.exports = main; exports = module.exports = main

View File

@ -1,18 +1,18 @@
import assign from 'lodash.assign'; import assign from 'lodash.assign'
import forEach from 'lodash.foreach'; import forEach from 'lodash.foreach'
import isEmpty from 'lodash.isempty'; import isEmpty from 'lodash.isempty'
import {EventEmitter} from 'events'; import {EventEmitter} from 'events'
//==================================================================== // ===================================================================
export default class Model extends EventEmitter { export default class Model extends EventEmitter {
constructor (properties) { constructor (properties) {
super(); super()
this.properties = assign({}, this.default); this.properties = assign({}, this.default)
if (properties) { if (properties) {
this.set(properties); this.set(properties)
} }
} }
@ -26,13 +26,13 @@ export default class Model extends EventEmitter {
// Get a property. // Get a property.
get (name, def) { get (name, def) {
let value = this.properties[name]; const value = this.properties[name]
return value !== undefined ? value : def; return value !== undefined ? value : def
} }
// Check whether a property exists. // Check whether a property exists.
has (name) { has (name) {
return (this.properties[name] !== undefined); return (this.properties[name] !== undefined)
} }
// Set properties. // Set properties.
@ -40,31 +40,31 @@ export default class Model extends EventEmitter {
// This method can also be used with two arguments to set a single // This method can also be used with two arguments to set a single
// property. // property.
if (value !== undefined) { if (value !== undefined) {
properties = { [properties]: value }; properties = { [properties]: value }
} }
let previous = {}; const previous = {}
forEach(properties, (value, name) => { forEach(properties, (value, name) => {
let prev = this.properties[name]; const prev = this.properties[name]
if (value !== prev) { if (value !== prev) {
previous[name] = prev; previous[name] = prev
if (value === undefined) { if (value === undefined) {
delete this.properties[name]; delete this.properties[name]
} else { } else {
this.properties[name] = value; this.properties[name] = value
} }
} }
}); })
if (!isEmpty(previous)) { if (!isEmpty(previous)) {
this.emit('change', previous); this.emit('change', previous)
forEach(previous, (value, name) => { forEach(previous, (value, name) => {
this.emit('change:' + name, value); this.emit('change:' + name, value)
}); })
} }
} }
} }

View File

@ -171,7 +171,11 @@ module.exports = ->
type type
# Missing rules should be created. # Missing rules should be created.
@missingRule = @rule @missingRule = (name) ->
@rule(name, ->
@key = -> @genval.id
@val = -> @genval
)
# Rule conflicts are possible (e.g. VM-template to VM). # Rule conflicts are possible (e.g. VM-template to VM).
@ruleConflict = ( -> ) @ruleConflict = ( -> )
@ -194,9 +198,11 @@ module.exports = ->
else else
# This definition are for non singleton items only. # This definition are for non singleton items only.
@key = -> @genval.$ref @key = -> @genval.$ref
@val.id = @val.UUID = -> @genval.uuid @val.id = -> @genval.$id
@val.UUID = -> @genval.uuid
@val.ref = -> @genval.$ref @val.ref = -> @genval.$ref
@val.poolRef = -> @genval.$poolRef @val.poolRef = -> @genval.$pool.$ref
@val.$poolId = -> @genval.$pool.$id
# Main objects all can have associated messages and tags. # Main objects all can have associated messages and tags.
if @name in ['host', 'pool', 'SR', 'VM', 'VM-controller'] if @name in ['host', 'pool', 'SR', 'VM', 'VM-controller']
@ -280,14 +286,14 @@ module.exports = ->
hosts: $set { hosts: $set {
rule: 'host' rule: 'host'
bind: -> @genval.$poolRef bind: -> @genval.$pool.$ref
} }
master: -> @genval.master master: -> @genval.master
networks: $set { networks: $set {
rule: 'network' rule: 'network'
bind: -> @genval.$poolRef bind: -> @genval.$pool.$ref
} }
templates: $set { templates: $set {
@ -302,19 +308,19 @@ module.exports = ->
$running_hosts: $set { $running_hosts: $set {
rule: 'host' rule: 'host'
bind: -> @genval.$poolRef bind: -> @genval.$pool.$ref
if: $isHostRunning if: $isHostRunning
} }
$running_VMs: $set { $running_VMs: $set {
rule: 'VM' rule: 'VM'
bind: -> @genval.$poolRef bind: -> @genval.$pool.$ref
if: $isVMRunning if: $isVMRunning
} }
$VMs: $set { $VMs: $set {
rule: 'VM' rule: 'VM'
bind: -> @genval.$poolRef bind: -> @genval.$pool.$ref
} }
} }
@ -395,6 +401,12 @@ module.exports = ->
PIFs: -> @genval.PIFs PIFs: -> @genval.PIFs
$PIFs: -> @val.PIFs $PIFs: -> @val.PIFs
PCIs: -> @genval.PCIs
$PCIs: -> @val.PCIs
PGPUs: -> @genval.PGPUs
$PGPUs: -> @val.PGPUs
tasks: $set { tasks: $set {
rule: 'task' rule: 'task'
bind: -> @genval.resident_on bind: -> @genval.resident_on
@ -451,6 +463,12 @@ module.exports = ->
else else
false false
auto_poweron: ->
if @genval.other_config.auto_poweron
true
else
false
os_version: -> os_version: ->
{guest_metrics} = @data {guest_metrics} = @data
if guest_metrics if guest_metrics
@ -458,8 +476,13 @@ module.exports = ->
else else
null null
VGPUs: -> @genval.VGPUs
$VGPUs: -> @val.VGPUs
power_state: -> @genval.power_state power_state: -> @genval.power_state
other: -> @genval.other_config
memory: -> memory: ->
{metrics, guest_metrics} = @data {metrics, guest_metrics} = @data
@ -513,7 +536,7 @@ module.exports = ->
@genval.resident_on @genval.resident_on
else else
# TODO: Handle local VMs. (`get_possible_hosts()`). # TODO: Handle local VMs. (`get_possible_hosts()`).
@genval.$poolRef @genval.$pool.$ref
snapshots: -> @genval.snapshots snapshots: -> @genval.snapshots
@ -522,6 +545,7 @@ module.exports = ->
$VBDs: -> @genval.VBDs $VBDs: -> @genval.VBDs
VIFs: -> @genval.VIFs VIFs: -> @genval.VIFs
} }
@rule VM: VMdef @rule VM: VMdef
@rule 'VM-controller': VMdef @rule 'VM-controller': VMdef
@ -595,7 +619,7 @@ module.exports = ->
$container: -> $container: ->
if @genval.shared if @genval.shared
@genval.$poolRef @genval.$pool.$ref
else else
@data.host @data.host
@ -826,4 +850,41 @@ module.exports = ->
} }
@rule pci: ->
@val = {
pci_id: -> @genval.pci_id
class_name: -> @genval.class_name
device_name: -> @genval.device_name
$host: -> @genval.host
}
@rule pgpu: ->
@val = {
pci: -> @genval.PCI
host: -> @genval.host
vgpus: -> @genval.resident_VGPUs
$vgpus: -> @val.vgpus
$host: -> @genval.host
}
@rule vgpu: ->
@val = {
currentAttached: -> @genval.currently_attached
vm: -> @genval.VM
device: -> @genval.device
resident_on: -> @genval.resident_on
}
return return

View File

@ -4,7 +4,7 @@ $sinon = require 'sinon'
#--------------------------------------------------------------------- #---------------------------------------------------------------------
{$MappedCollection} = require './MappedCollection.coffee' {$MappedCollection} = require './MappedCollection'
# Helpers for dealing with fibers. # Helpers for dealing with fibers.
{$coroutine} = require './fibers-utils' {$coroutine} = require './fibers-utils'

View File

@ -1,38 +1,37 @@
import base64url from 'base64url'; import base64url from 'base64url'
import forEach from 'lodash.foreach'; import forEach from 'lodash.foreach'
import has from 'lodash.has'; import has from 'lodash.has'
import humanFormat from 'human-format'; import humanFormat from 'human-format'
import isArray from 'lodash.isarray'; import isArray from 'lodash.isarray'
import multiKeyHash from 'multikey-hash'; import multiKeyHashInt from 'multikey-hash'
import xml2js from 'xml2js'; import xml2js from 'xml2js'
import {promisify, method} from 'bluebird'; import {promisify, method} from 'bluebird'
import {randomBytes} from 'crypto'; import {randomBytes} from 'crypto'
randomBytes = promisify(randomBytes); /* eslint no-lone-blocks: 0 */
//==================================================================== // ===================================================================
// Ensure the value is an array, wrap it if necessary. // Ensure the value is an array, wrap it if necessary.
let ensureArray = (value) => { export const ensureArray = (value) => {
if (value === undefined) { if (value === undefined) {
return []; return []
} }
return isArray(value) ? value : [value]; return isArray(value) ? value : [value]
}; }
export {ensureArray};
//-------------------------------------------------------------------- // -------------------------------------------------------------------
// Generate a secure random Base64 string. // Generate a secure random Base64 string.
let generateToken = (n = 32) => randomBytes(n).then(base64url); export const generateToken = (function (randomBytes) {
export {generateToken}; return (n = 32) => randomBytes(n).then(base64url)
})(promisify(randomBytes))
//-------------------------------------------------------------------- // -------------------------------------------------------------------
let formatXml; export const formatXml = (function () {
{ const builder = new xml2js.Builder({
let builder = new xml2js.Builder({
xmldec: { xmldec: {
// Do not include an XML header. // Do not include an XML header.
// //
@ -42,109 +41,97 @@ let formatXml;
// TODO: Find a better alternative. // TODO: Find a better alternative.
headless: true headless: true
} }
}); })
formatXml = (...args) => builder.buildObject(...args); return (...args) => builder.buildObject(...args)
} })()
export {formatXml};
let parseXml; export const parseXml = (function () {
{ const opts = {
let opts = {
mergeAttrs: true, mergeAttrs: true,
explicitArray: false, explicitArray: false
}; }
parseXml = (xml) => { return (xml) => {
let result; let result
// xml2js.parseString() use a callback for synchronous code. // xml2js.parseString() use a callback for synchronous code.
xml2js.parseString(xml, opts, (error, result_) => { xml2js.parseString(xml, opts, (error, result_) => {
if (error) { if (error) {
throw error; throw error
} }
result = result_; result = result_
}); })
return result; return result
};
} }
export {parseXml}; })()
//-------------------------------------------------------------------- // -------------------------------------------------------------------
function parseSize(size) { export function parseSize (size) {
let bytes = humanFormat.parse.raw(size, { scale: 'binary' }); let bytes = humanFormat.parse.raw(size, { scale: 'binary' })
if (bytes.unit && bytes.unit !== 'B') { if (bytes.unit && bytes.unit !== 'B') {
bytes = humanFormat.parse.raw(size); bytes = humanFormat.parse.raw(size)
if (bytes.unit && bytes.unit !== 'B') { if (bytes.unit && bytes.unit !== 'B') {
throw new Error('invalid size: ' + size); throw new Error('invalid size: ' + size)
} }
} }
return Math.floor(bytes.value * bytes.factor); return Math.floor(bytes.value * bytes.factor)
} }
export {parseSize}; // -------------------------------------------------------------------
//--------------------------------------------------------------------
// Special value which can be returned to stop an iteration in map() // Special value which can be returned to stop an iteration in map()
// and mapInPlace(). // and mapInPlace().
let done = {}; export const done = {}
export {done};
// Similar to `lodash.map()` for array and `lodash.mapValues()` for // Similar to `lodash.map()` for array and `lodash.mapValues()` for
// objects. // objects.
// //
// Note: can be interrupted by returning the special value `done` // Note: can be interrupted by returning the special value `done`
// provided as the forth argument. // provided as the forth argument.
function map(col, iterator, thisArg = this) { export function map (col, iterator, thisArg = this) {
let result = has(col, 'length') ? [] : {}; const result = has(col, 'length') ? [] : {}
forEach(col, (item, i) => { forEach(col, (item, i) => {
let value = iterator.call(thisArg, item, i, done); const value = iterator.call(thisArg, item, i, done)
if (value === done) { if (value === done) {
return false; return false
} }
result[i] = value; result[i] = value
}); })
return result; return result
} }
export {map};
// Create a hash from multiple values. // Create a hash from multiple values.
multiKeyHash = (function (multiKeyHash) { export const multiKeyHash = method((...args) => {
return method((...args) => { const hash = multiKeyHashInt(...args)
let hash = multiKeyHash(...args);
let buf = new Buffer(4); const buf = new Buffer(4)
buf.writeUInt32LE(hash, 0); buf.writeUInt32LE(hash, 0)
return base64url(buf); return base64url(buf)
}); })
})(multiKeyHash);
export {multiKeyHash};
// Similar to `map()` but change the current collection. // Similar to `map()` but change the current collection.
// //
// Note: can be interrupted by returning the special value `done` // Note: can be interrupted by returning the special value `done`
// provided as the forth argument. // provided as the forth argument.
function mapInPlace(col, iterator, thisArg = this) { export function mapInPlace (col, iterator, thisArg = this) {
forEach(col, (item, i) => { forEach(col, (item, i) => {
let value = iterator.call(thisArg, item, i, done); const value = iterator.call(thisArg, item, i, done)
if (value === done) { if (value === done) {
return false; return false
} }
col[i] = value; col[i] = value
}); })
return col; return col
} }
export {mapInPlace};
// Wrap a value in a function. // Wrap a value in a function.
let wrap = (value) => () => value; export const wrap = (value) => () => value
export {wrap};

View File

@ -1,51 +1,51 @@
import assign from 'lodash.assign'; import assign from 'lodash.assign'
import debug from 'debug'; import createDebug from 'debug'
import WebSocket from 'ws'; import WebSocket from 'ws'
debug = debug('xo:wsProxy'); const debug = createDebug('xo:wsProxy')
let defaults = { const defaults = {
// Automatically close the client connection when the remote close. // Automatically close the client connection when the remote close.
autoClose: true, autoClose: true,
// Reject secure connections to unauthorized remotes (bad CA). // Reject secure connections to unauthorized remotes (bad CA).
rejectUnauthorized: false, rejectUnauthorized: false
}; }
// Proxy a WebSocket `client` to a remote server which has `url` as // Proxy a WebSocket `client` to a remote server which has `url` as
// address. // address.
export default function wsProxy (client, url, opts) { export default function wsProxy (client, url, opts) {
opts = assign({}, defaults, opts); opts = assign({}, defaults, opts)
let remote = new WebSocket(url, { const remote = new WebSocket(url, {
protocol: opts.protocol || client.protocol, protocol: opts.protocol || client.protocol,
rejectUnauthorized: opts.rejectUnauthorized, rejectUnauthorized: opts.rejectUnauthorized
}).once('open', function () { }).once('open', function () {
debug('connected to', url); debug('connected to', url)
}).once('close', function () { }).once('close', function () {
debug('remote closed'); debug('remote closed')
if (opts.autoClose) { if (opts.autoClose) {
client.close(); client.close()
} }
}).once('error', function (error) { }).once('error', function (error) {
debug('remote error', error); debug('remote error', error)
}).on('message', function (message) { }).on('message', function (message) {
client.send(message, function (error) { client.send(message, function (error) {
if (error) { if (error) {
debug('client send error', error); debug('client send error', error)
} }
}); })
}); })
client.once('close', function () { client.once('close', function () {
debug('client closed'); debug('client closed')
remote.close(); remote.close()
}).on('message', function (message) { }).on('message', function (message) {
remote.send(message, function (error) { remote.send(message, function (error) {
if (error) { if (error) {
debug('remote send error', error); debug('remote send error', error)
} }
}); })
}); })
} }

View File

@ -2,15 +2,17 @@
{format: $formatUrl, parse: $parseUrl} = require 'url' {format: $formatUrl, parse: $parseUrl} = require 'url'
$Bluebird = require 'bluebird' $Bluebird = require 'bluebird'
$contains = require 'lodash.contains'
$debug = (require 'debug') 'xo:xo' $debug = (require 'debug') 'xo:xo'
$forEach = require 'lodash.foreach' $forEach = require 'lodash.foreach'
$includes = require 'lodash.includes'
$isEmpty = require 'lodash.isempty' $isEmpty = require 'lodash.isempty'
$isString = require 'lodash.isstring' $isString = require 'lodash.isstring'
$pluck = require 'lodash.pluck' $pluck = require 'lodash.pluck'
$Promise = require 'bluebird' $Promise = require 'bluebird'
$proxyRequest = require 'proxy-http-request' $proxyRequest = require 'proxy-http-request'
$httpRequest = require 'request'
{createClient: $createRedisClient} = require 'then-redis' {createClient: $createRedisClient} = require 'then-redis'
{createClient: $createXapiClient} = require('xen-api')
{ {
hash: $hash hash: $hash
needsRehash: $needsRehash needsRehash: $needsRehash
@ -21,7 +23,6 @@ $Connection = require './connection'
$Model = require './model' $Model = require './model'
$RedisCollection = require './collection/redis' $RedisCollection = require './collection/redis'
$spec = require './spec' $spec = require './spec'
$XAPI = require './xapi'
{$coroutine, $wait} = require './fibers-utils' {$coroutine, $wait} = require './fibers-utils'
{ {
generateToken: $generateToken generateToken: $generateToken
@ -29,6 +30,8 @@ $XAPI = require './xapi'
} = require './utils' } = require './utils'
{$MappedCollection} = require './MappedCollection' {$MappedCollection} = require './MappedCollection'
{Set, $for: {getIterator}} = (require 'babel-runtime/core-js').default
#===================================================================== #=====================================================================
# Models and collections. # Models and collections.
@ -262,142 +265,10 @@ class $XO extends $EventEmitter
# Exports the map from UUIDs to keys. # Exports the map from UUIDs to keys.
{$UUIDsToKeys: @_UUIDsToKeys} = (@_xobjs.get 'xo') {$UUIDsToKeys: @_UUIDsToKeys} = (@_xobjs.get 'xo')
# This function asynchronously connects to a server, retrieves
# all its objects and monitors events.
connect = $coroutine (server) =>
# Identifier of the connection.
id = server.id
# Reference of the pool of this connection.
poolRef = undefined
xapi = @_xapis[id] = new $XAPI {
host: server.host
username: server.username
password: server.password
}
# First construct the list of retrievable types. except pool
# which will handled specifically.
retrievableTypes = do ->
methods = $wait xapi.call 'system.listMethods'
types = []
for method in methods
[type, method] = method.split '.'
if method is 'get_all_records' and type isnt 'pool'
types.push type
return types
# This helper normalizes a record by inserting its type.
normalizeObject = (object, ref, type) ->
object.$poolRef = poolRef
object.$ref = ref
object.$type = type
return
objects = {}
# Then retrieve the pool.
pools = $wait xapi.call 'pool.get_all_records'
# Gets the first pool and ensures it is the only one.
ref = pool = null
for ref of pools
throw new Error 'more than one pool!' if pool?
pool = pools[ref]
throw new Error 'no pool found' unless pool?
# Remembers its reference.
poolRef = ref
# Makes the connection accessible through the pool reference.
# TODO: Properly handle disconnections.
@_xapis[poolRef] = xapi
# Normalizes the records.
normalizeObject pool, ref, 'pool'
objects[ref] = pool
# Then retrieve all other objects.
n = 0
$wait $Bluebird.map retrievableTypes, $coroutine (type) ->
try
for ref, object of $wait xapi.call "#{type}.get_all_records"
normalizeObject object, ref, type
objects[ref] = object
n++
catch error
# It is possible that the method `TYPE.get_all_records` has
# been deprecated, if that's the case, just ignores it.
throw error unless error[0] is 'MESSAGE_REMOVED'
$debug '%s objects fetched from %s@%s', n, server.username, server.host
# Stores all objects.
@_xobjs.set objects, {
add: true
update: false
remove: false
}
$debug 'objects inserted into the database'
# Finally, monitors events.
#
# TODO: maybe close events (500ms) could be merged to limit
# CPU & network consumption.
loop
$wait xapi.call 'event.register', ['*']
try
# Once the session is registered, just handle events.
loop
event = $wait xapi.call 'event.next'
updatedObjects = {}
removedObjects = {}
for {operation, class: type, ref, snapshot: object} in event
# Normalizes the object.
normalizeObject object, ref, type
# Adds the object to the corresponding list (and ensures
# it is not in the other).
if operation is 'del'
delete updatedObjects[ref]
removedObjects[ref] = object
else
delete removedObjects[ref]
updatedObjects[ref] = object
# Records the changes.
@_xobjs.remove removedObjects, true
@_xobjs.set updatedObjects, {
add: true
update: true
remove: false
}
catch error
# FIXME: The proper approach with events loss or
# disconnection is to redownload all objects.
if error[0] is 'EVENTS_LOST'
# XAPI error, the program must unregister from events and then
# register again.
try
$wait xapi.call 'event.unregister', ['*']
else
throw error unless error[0] is 'SESSION_NOT_REGISTERED'
# Prevents errors from stopping the server. # Prevents errors from stopping the server.
connectSafe = $coroutine (server) -> connect = $coroutine (server) =>
try try
$wait connect server $wait @connectServer server
catch error catch error
console.error( console.error(
"[WARN] #{server.host}:" "[WARN] #{server.host}:"
@ -405,13 +276,74 @@ class $XO extends $EventEmitter
) )
# Connects to existing servers. # Connects to existing servers.
connectSafe server for server in $wait @servers.get() connect server for server in $wait @servers.get()
# Automatically connects to new servers. #-------------------------------------------------------------------
@servers.on 'add', (servers) ->
connectSafe server for server in servers
# TODO: Automatically disconnects from removed servers. connectServer: (server) ->
if server.properties
server = server.properties
xapi = @_xapis[server.id] = $createXapiClient({
url: server.host,
auth: {
user: server.username,
password: server.password
}
})
xapi.objects.on('add', (objects) =>
@_xapis[xapi.pool.$id] = xapi
@_xobjs.set(objects, {
add: true,
update: false,
remove: false
})
)
xapi.objects.on('update', (objects) =>
@_xapis[xapi.pool.$id] = xapi
@_xobjs.set(objects, {
add: true,
update: true,
remove: false
})
)
xapi.objects.on('remove', (objects) =>
@_xobjs.removeWithPredicate (object) =>
return object.genval?.$id of objects
)
return xapi.connect()
disconnectServer: (server) ->
id = server and (server.properties?.id ? server.id) ? server
xapi = @_xapis[id]
return $Bluebird.reject(new Error('no such server')) if not xapi
delete @_xapis[id]
delete @_xapis[xapi.pool.id] if xapi.pool
return xapi.disconnect()
# Returns the XAPI connection associated to an object.
getXAPI: (object, type) ->
if $isString object
object = @getObject object, type
{$poolId: poolId} = object
unless poolId
throw new Error "object #{object.id} does not belong to a pool"
xapi = @_xapis[poolId]
unless xapi
throw new Error "no connection found for object #{object.id}"
return xapi
#-------------------------------------------------------------------
# Returns an object from its key or UUID. # Returns an object from its key or UUID.
getObject: (key, type) -> getObject: (key, type) ->
@ -423,7 +355,7 @@ class $XO extends $EventEmitter
if type? and ( if type? and (
($isString type and type isnt obj.type) or ($isString type and type isnt obj.type) or
not $contains type, obj.type # Array not $includes type, obj.type # Array
) )
throw new Error "unexpected type: got #{obj.type} instead of #{type}" throw new Error "unexpected type: got #{obj.type} instead of #{type}"
@ -442,16 +374,7 @@ class $XO extends $EventEmitter
# Fetches all objects ignore those missing. # Fetches all objects ignore those missing.
return @_xobjs.get keys, true return @_xobjs.get keys, true
# Returns the XAPI connection associated to an object. #-------------------------------------------------------------------
getXAPI: (object, type) ->
if $isString object
object = @getObject object, type
{poolRef} = object
unless poolRef
throw new Error "no XAPI found for #{object.UUID}"
return @_xapis[poolRef]
createUserConnection: (opts) -> createUserConnection: (opts) ->
connections = @connections connections = @connections
@ -484,6 +407,8 @@ class $XO extends $EventEmitter
return url return url
#-------------------------------------------------------------------
handleProxyRequest: (req, res, next) -> handleProxyRequest: (req, res, next) ->
unless ( unless (
(request = @_proxyRequests[req.url]) and (request = @_proxyRequests[req.url]) and
@ -521,6 +446,8 @@ class $XO extends $EventEmitter
return return
#-------------------------------------------------------------------
watchTask: (ref) -> watchTask: (ref) ->
watcher = @_taskWatchers[ref] watcher = @_taskWatchers[ref]
unless watcher? unless watcher?
@ -560,7 +487,7 @@ class $XO extends $EventEmitter
else if credentials.username? else if credentials.username?
credentials.email = credentials.username credentials.email = credentials.username
iterator = @_authenticationProviders[Symbol.iterator]() iterator = getIterator(@_authenticationProviders)
while not (current = iterator.next()).done while not (current = iterator.next()).done
try try
@ -577,7 +504,10 @@ class $XO extends $EventEmitter
return @users.create(result.email) return @users.create(result.email)
catch e catch e
console.error(e) # Authentication providers may just throw `null` to indicate
# they could not authenticate the user without any special
# errors.
console.error(e) if e?
return false return false
#===================================================================== #=====================================================================