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/
npm-debug.log
.xo-server.*

View File

@ -16,7 +16,21 @@ ___
## 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?

View File

@ -1,7 +1,7 @@
#!/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) {
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()
//====================================================================
// 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');
// Import the real main module.
module.exports = require('./dist')

View File

@ -15,6 +15,11 @@
},
"author": "Julien Fontanet <julien.fontanet@vates.fr>",
"preferGlobal": true,
"files": [
"bin/",
"dist/",
"index.js"
],
"directories": {
"bin": "bin"
},
@ -24,10 +29,9 @@
},
"dependencies": {
"app-conf": "^0.3.4",
"babel": "^4.7.13",
"babel-runtime": "^5",
"base64url": "1.0.4",
"bluebird": "^2.9.14",
"coffee-script": "~1.9.1",
"compiled-accessors": "^0.2.0",
"connect": "^3.3.5",
"debug": "^2.1.3",
@ -36,6 +40,7 @@
"fibers": "~1.0.5",
"fs-promise": "^0.3.1",
"graceful-fs": "^3.0.6",
"gulp-sourcemaps": "^1.5.1",
"hashy": "~0.4.2",
"http-server-plus": "^0.5.1",
"human-format": "^0.3.0",
@ -44,12 +49,13 @@
"lodash.assign": "^3.0.0",
"lodash.bind": "^3.0.0",
"lodash.clone": "^3.0.1",
"lodash.contains": "^2.4.1",
"lodash.difference": "^3.0.1",
"lodash.filter": "^3.0.0",
"lodash.filter": "^3.1.0",
"lodash.find": "^3.0.0",
"lodash.findindex": "^3.0.0",
"lodash.foreach": "^3.0.1",
"lodash.has": "^3.0.0",
"lodash.includes": "^3.1.1",
"lodash.isarray": "^3.0.0",
"lodash.isempty": "^3.0.0",
"lodash.isfunction": "^3.0.1",
@ -59,27 +65,45 @@
"lodash.map": "^3.0.0",
"lodash.pick": "^3.0.0",
"lodash.pluck": "^3.0.2",
"make-error": "^0.3.0",
"lodash.result": "^3.0.0",
"make-error": "^1",
"multikey-hash": "^1.0.1",
"proxy-http-request": "0.0.2",
"request": "^2.53.0",
"require-tree": "~1.0.1",
"schema-inspector": "^1.5.1",
"serve-static": "^1.9.2",
"source-map-support": "^0.2.10",
"then-redis": "~1.3.0",
"ws": "~0.7.1",
"xen-api": "^0.3.0",
"xml2js": "~0.4.6",
"xmlrpc": "~1.3.0"
},
"devDependencies": {
"chai": "~2.1.2",
"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",
"node-inspector": "^0.9.2",
"sinon": "^1.14.1"
"sinon": "^1.14.1",
"standard": "*"
},
"scripts": {
"build": "gulp build --production",
"dev": "gulp build",
"prepublish": "in-publish && npm run build || in-install",
"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'
$assign = require 'lodash.assign'
$filter = require 'lodash.filter'
$forEach = require 'lodash.foreach'
$getKeys = require 'lodash.keys'
$isArray = require 'lodash.isarray'
@ -214,6 +215,10 @@ class $MappedCollection extends $EventEmitter
remove: (keys, ignoreMissingItems = false) ->
@_removeItems (@_fetchItems keys, ignoreMissingItems)
removeWithPredicate: (predicate, thisArg) ->
items = ($filter @_byKey, predicate, thisArg)
@_removeItems items
set: (items, {add, update, remove} = {}) ->
add = true unless add?
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.
assign(exports, jsonRpcErrors);
export * from 'json-rpc/errors'
//--------------------------------------------------------------------
// -------------------------------------------------------------------
exportError(function NotImplemented() {
NotImplemented.super.call(this, 'not implemented', 0);
});
export class NotImplemented extends JsonRpcError {
constructor () {
super('not implemented', 0)
}
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
exportError(function NoSuchObject() {
NoSuchObject.super.call(this, 'no such object', 1);
});
export class NoSuchObject extends JsonRpcError {
constructor () {
super('no such object', 1)
}
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
exportError(function Unauthorized() {
Unauthorized.super.call(
this,
'not authenticated or not enough permissions',
2
);
});
export class Unauthorized extends JsonRpcError {
constructor () {
super('not authenticated or not enough permissions', 2)
}
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
exportError(function InvalidCredential() {
InvalidCredential.super.call(this, 'invalid credential', 3);
});
export class InvalidCredential extends JsonRpcError {
constructor () {
super('invalid credential', 3)
}
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
exportError(function AlreadyAuthenticated() {
AlreadyAuthenticated.super.call(this, 'already authenticated', 4);
});
export class AlreadyAuthenticated extends JsonRpcError {
constructor () {
super('already authenticated', 4)
}
}

View File

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

View File

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

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}) {
let xapi = this.getXAPI(sr);
let ref = wait(xapi.call('VDI.create', {
const ref = wait(xapi.call('VDI.create', {
name_label: name,
other_config: {},
read_only: false,
sharable: false,
SR: sr.ref,
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 = {
name: { type: 'string' },
size: { type: 'string' },
sr: { type: 'string' },
};
sr: { type: 'string' }
}
create.resolve = {
sr: ['sr', 'SR'],
};
export {create};
sr: ['sr', 'SR']
}

View File

@ -170,3 +170,39 @@ disable.resolve = {
}
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')) {
throw new AlreadyAuthenticated();
throw new AlreadyAuthenticated()
}
let user = wait(this.authenticateUser(credentials));
const user = wait(this.authenticateUser(credentials))
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};
// -------------------------------------------------------------------
//--------------------------------------------------------------------
let signInWithPassword = deprecate(signIn, 'use session.signIn() instead');
export const signInWithPassword = deprecate(signIn, 'use session.signIn() instead')
signInWithPassword.params = {
email: { 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');
password: { type: 'string' }
}
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.
signOut.permission = '';
signOut.permission = ''
export {signOut};
// -------------------------------------------------------------------
//--------------------------------------------------------------------
let getUser = coroutine(function () {
let userId = this.session.get('user_id');
export const getUser = coroutine(function () {
const userId = this.session.get('user_id')
return userId === undefined ?
null :
this.getUserPublicProperties(wait(this.users.first(userId)))
;
});
})
getUser.description = 'return the currently connected user';
export {getUser};
getUser.description = 'return the currently connected user'

View File

@ -1,109 +1,110 @@
import forEach from 'lodash.foreach';
import {coroutine, wait} from '../fibers-utils';
import {ensureArray, parseXml} from '../utils';
import forEach from 'lodash.foreach'
import {coroutine, wait} from '../fibers-utils'
import {ensureArray, parseXml} from '../utils'
//====================================================================
// ===================================================================
let set = coroutine(function (params) {
let {SR} = params;
let xapi = this.getXAPI();
export const set = coroutine(function (params) {
const {SR} = params
const xapi = this.getXAPI()
forEach(['name_label', 'name_description'], param => {
let value = params[param];
const value = params[param]
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 = {
id: { type: 'string' },
name_label: { type: 'string', optional: true },
name_description: { type: 'string', optional: true },
};
name_description: { type: 'string', optional: true }
}
set.resolve = {
SR: ['id', 'SR'],
};
export {set};
SR: ['id', 'SR']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
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 = {
id: { type: 'string' },
};
id: { type: 'string' }
}
scan.resolve = {
SR: ['id', 'SR'],
};
export {scan};
SR: ['id', 'SR']
}
// -------------------------------------------------------------------
//--------------------------------------------------------------------
// 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 = {
id: { type: 'string' },
};
id: { type: 'string' }
}
destroy.resolve = {
SR: ['id', 'SR'],
};
export {destroy};
SR: ['id', 'SR']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
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 = {
id: { type: 'string' },
};
id: { type: 'string' }
}
forget.resolve = {
SR: ['id', 'SR'],
};
export {forget};
SR: ['id', 'SR']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
let createIso = coroutine(function ({
export const createIso = coroutine(function ({
host,
nameLabel,
nameDescription,
path
}) {
let xapi = this.getXAPI(host);
const xapi = this.getXAPI(host)
// FIXME: won't work for IPv6
// Detect if NFS or local path for ISO files
let deviceConfig = {location: path};
const deviceConfig = {location: path}
if (path.indexOf(':') === -1) { // not NFS share
// 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',
host.ref,
deviceConfig,
@ -114,30 +115,29 @@ let createIso = coroutine(function ({
'iso', // SR content type ISO
true,
{}
));
))
let sr = wait(xapi.call('SR.get_record', srRef));
return sr.uuid;
});
const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid
})
createIso.params = {
host: { type: 'string' },
nameLabel: { type: 'string' },
nameDescription: { type: 'string' },
path: { type: 'string' }
};
createIso.resolve = {
host: ['host', 'host'],
};
export {createIso};
}
//--------------------------------------------------------------------
createIso.resolve = {
host: ['host', 'host']
}
// -------------------------------------------------------------------
// NFS SR
// This functions creates a NFS SR
let createNfs = coroutine(function ({
export const createNfs = coroutine(function ({
host,
nameLabel,
nameDescription,
@ -145,20 +145,19 @@ let createNfs = coroutine(function ({
serverPath,
nfsVersion
}) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host);
let deviceConfig = {
const deviceConfig = {
server,
serverpath: serverPath,
};
serverpath: serverPath
}
// if NFS version given
if (nfsVersion) {
deviceConfig.nfsversion = nfsVersion;
deviceConfig.nfsversion = nfsVersion
}
let srRef = wait(xapi.call(
const srRef = wait(xapi.call(
'SR.create',
host.ref,
deviceConfig,
@ -166,15 +165,14 @@ let createNfs = coroutine(function ({
nameLabel,
nameDescription,
'nfs', // SR LVM over iSCSI
'user', // recommanded by Citrix
'user', // recommended by Citrix
true,
{}
));
))
let sr = wait(xapi.call('SR.get_record', srRef));
return sr.uuid;
});
const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid
})
createNfs.params = {
host: { type: 'string' },
@ -182,29 +180,73 @@ createNfs.params = {
nameDescription: { type: 'string' },
server: { type: 'string' },
serverPath: { type: 'string' },
nfsVersion: { type: 'string' , optional: true},
};
createNfs.resolve = {
host: ['host', 'host'],
};
export {createNfs};
nfsVersion: { type: 'string', optional: true }
}
//--------------------------------------------------------------------
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
// Return a table of exports with their paths and ACLs
let probeNfs = coroutine(function ({
export const probeNfs = coroutine(function ({
host,
server
}) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host);
const deviceConfig = {
server
}
let deviceConfig = {
server,
};
let xml;
let xml
try {
wait(xapi.call(
@ -213,45 +255,43 @@ let probeNfs = coroutine(function ({
deviceConfig,
'nfs',
{}
));
))
throw new Error('the call above should have thrown an error')
} catch (error) {
if (error[0] !== 'SR_BACKEND_FAILURE_101') {
throw error;
throw error
}
xml = error[3];
xml = parseXml(error[3])
}
xml = parseXml(xml);
let nfsExports = [];
const nfsExports = []
forEach(ensureArray(xml['nfs-exports'].Export), nfsExport => {
nfsExports.push({
path: nfsExport.Path.trim(),
acl: nfsExport.Accesslist.trim()
});
});
})
})
return nfsExports;
});
return nfsExports
})
probeNfs.params = {
host: { type: 'string' },
server: { type: 'string' },
};
server: { type: 'string' }
}
probeNfs.resolve = {
host: ['host', 'host'],
};
export {probeNfs};
host: ['host', 'host']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// ISCSI SR
// This functions creates a iSCSI SR
let createIscsi = coroutine(function ({
export const createIscsi = coroutine(function ({
host,
nameLabel,
nameDescription,
@ -263,27 +303,26 @@ let createIscsi = coroutine(function ({
chapUser,
chapPassword
}) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host);
let deviceConfig = {
const deviceConfig = {
target,
targetIQN: targetIqn,
SCSIid: scsiId,
};
SCSIid: scsiId
}
// if we give user and password
if (chapUser && chapPassword) {
deviceConfig.chapUser = chapUser;
deviceConfig.chapPassword = chapPassword;
deviceConfig.chapUser = chapUser
deviceConfig.chapPassword = chapPassword
}
// if we give another port than default iSCSI
if (port) {
deviceConfig.port = port;
deviceConfig.port = port
}
let srRef = wait(xapi.call(
const srRef = wait(xapi.call(
'SR.create',
host.ref,
deviceConfig,
@ -291,62 +330,60 @@ let createIscsi = coroutine(function ({
nameLabel,
nameDescription,
'lvmoiscsi', // SR LVM over iSCSI
'user', // recommanded by Citrix
'user', // recommended by Citrix
true,
{}
));
))
let sr = wait(xapi.call('SR.get_record', srRef));
return sr.uuid;
});
const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid
})
createIscsi.params = {
host: { type: 'string' },
nameLabel: { type: 'string' },
nameDescription: { type: 'string' },
target: { type: 'string' },
port: { type: 'integer' , optional: true},
port: { type: 'integer', optional: true},
targetIqn: { type: 'string' },
scsiId: { type: 'string' },
chapUser: { type: 'string' , optional: true },
chapPassword: { type: 'string' , optional: true },
};
createIscsi.resolve = {
host: ['host', 'host'],
};
export {createIscsi};
chapUser: { type: 'string', optional: true },
chapPassword: { type: 'string', optional: true }
}
//--------------------------------------------------------------------
createIscsi.resolve = {
host: ['host', 'host']
}
// -------------------------------------------------------------------
// 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
let probeIscsiIqns = coroutine(function ({
export const probeIscsiIqns = coroutine(function ({
host,
target:targetIp,
target: targetIp,
port,
chapUser,
chapPassword
}) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host);
let deviceConfig = {
target: targetIp,
};
const deviceConfig = {
target: targetIp
}
// if we give user and password
if (chapUser && chapPassword) {
deviceConfig.chapUser = chapUser;
deviceConfig.chapPassword = chapPassword;
deviceConfig.chapUser = chapUser
deviceConfig.chapPassword = chapPassword
}
// if we give another port than default iSCSI
if (port) {
deviceConfig.port = port;
deviceConfig.port = port
}
let xml;
let xml
try {
wait(xapi.call(
@ -355,79 +392,76 @@ let probeIscsiIqns = coroutine(function ({
deviceConfig,
'lvmoiscsi',
{}
));
))
throw new Error('the call above should have thrown an error')
} catch (error) {
if (error[0] === 'SR_BACKEND_FAILURE_141') {
return [];
return []
}
if (error[0] !== 'SR_BACKEND_FAILURE_96') {
throw error;
throw error
}
xml = error[3];
xml = parseXml(error[3])
}
xml = parseXml(xml);
let targets = [];
const targets = []
forEach(ensureArray(xml['iscsi-target-iqns'].TGT), target => {
// if the target is on another IP adress, do not display it
if (target.IPAddress.trim() === targetIp) {
targets.push({
iqn: target.TargetIQN.trim(),
ip: target.IPAddress.trim()
});
})
}
});
})
return targets;
});
return targets
})
probeIscsiIqns.params = {
host: { type: 'string' },
target: { type: 'string' },
port: { type: 'integer', optional: true },
chapUser: { type: 'string' , optional: true },
chapPassword: { type: 'string' , optional: true },
};
chapUser: { type: 'string', optional: true },
chapPassword: { type: 'string', optional: true }
}
probeIscsiIqns.resolve = {
host: ['host', 'host'],
};
export {probeIscsiIqns};
host: ['host', 'host']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// This function helps to detect all iSCSI ID and LUNs on a Target
// It will return a LUN table
let probeIscsiLuns = coroutine(function ({
export const probeIscsiLuns = coroutine(function ({
host,
target:targetIp,
target: targetIp,
port,
targetIqn,
chapUser,
chapPassword
}) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host);
let deviceConfig = {
const deviceConfig = {
target: targetIp,
targetIQN: targetIqn,
};
targetIQN: targetIqn
}
// if we give user and password
if (chapUser && chapPassword) {
deviceConfig.chapUser = chapUser;
deviceConfig.chapPassword = chapPassword;
deviceConfig.chapUser = chapUser
deviceConfig.chapPassword = chapPassword
}
// if we give another port than default iSCSI
if (port) {
deviceConfig.port = port;
deviceConfig.port = port
}
let xml;
let xml
try {
wait(xapi.call(
@ -436,18 +470,18 @@ let probeIscsiLuns = coroutine(function ({
deviceConfig,
'lvmoiscsi',
{}
));
))
throw new Error('the call above should have thrown an error')
} catch (error) {
if (error[0] !== 'SR_BACKEND_FAILURE_107') {
throw error;
throw error
}
xml = error[3];
xml = parseXml(error[3])
}
xml = parseXml(xml);
let luns = [];
const luns = []
forEach(ensureArray(xml['iscsi-target'].LUN), lun => {
luns.push({
id: lun.LUNid.trim(),
@ -455,72 +489,67 @@ let probeIscsiLuns = coroutine(function ({
serial: lun.serial.trim(),
size: lun.size.trim(),
scsiId: lun.SCSIid.trim()
});
});
})
})
return luns;
});
return luns
})
probeIscsiLuns.params = {
host: { type: 'string' },
target: { type: 'string' },
port: { type: 'integer' , optional: true},
port: { type: 'integer', optional: true},
targetIqn: { type: 'string' },
chapUser: { type: 'string' , optional: true },
chapPassword: { type: 'string' , optional: true },
};
probeIscsiLuns.resolve = {
host: ['host', 'host'],
};
export {probeIscsiLuns};
chapUser: { type: 'string', optional: true },
chapPassword: { type: 'string', optional: true }
}
//--------------------------------------------------------------------
probeIscsiLuns.resolve = {
host: ['host', 'host']
}
// -------------------------------------------------------------------
// This function helps to detect if this target already exists in XAPI
// It returns a table of SR UUID, empty if no existing connections
let probeIscsiExists = coroutine(function ({
export const probeIscsiExists = coroutine(function ({
host,
target:targetIp,
target: targetIp,
port,
targetIqn,
scsiId,
chapUser,
chapPassword
}) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host);
let deviceConfig = {
const deviceConfig = {
target: targetIp,
targetIQN: targetIqn,
SCSIid: scsiId,
};
SCSIid: scsiId
}
// if we give user and password
if (chapUser && chapPassword) {
deviceConfig.chapUser = chapUser;
deviceConfig.chapPassword = chapPassword;
deviceConfig.chapUser = chapUser
deviceConfig.chapPassword = chapPassword
}
// if we give another port than default iSCSI
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);
let srs = [];
const srs = []
forEach(ensureArray(xml['SRlist'].SR), sr => {
// 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 = {
host: { type: 'string' },
@ -528,75 +557,69 @@ probeIscsiExists.params = {
port: { type: 'integer', optional: true },
targetIqn: { type: 'string' },
scsiId: { type: 'string' },
chapUser: { type: 'string' , optional: true },
chapPassword: { type: 'string' , optional: true },
};
probeIscsiExists.resolve = {
host: ['host', 'host'],
};
export {probeIscsiExists};
chapUser: { type: 'string', optional: true },
chapPassword: { type: 'string', optional: true }
}
//--------------------------------------------------------------------
probeIscsiExists.resolve = {
host: ['host', 'host']
}
// -------------------------------------------------------------------
// 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
let probeNfsExists = coroutine(function ({
export const probeNfsExists = coroutine(function ({
host,
server,
serverPath,
}) {
const xapi = this.getXAPI(host)
let xapi = this.getXAPI(host);
let deviceConfig = {
const deviceConfig = {
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);
let srs = [];
const srs = []
forEach(ensureArray(xml['SRlist'].SR), sr => {
// 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 = {
host: { type: 'string' },
server: { type: 'string' },
serverPath: { type: 'string' },
};
serverPath: { type: 'string' }
}
probeNfsExists.resolve = {
host: ['host', 'host'],
};
host: ['host', 'host']
}
export {probeNfsExists};
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// This function helps to reattach a forgotten NFS/iSCSI SR
let reattach = coroutine(function ({
export const reattach = coroutine(function ({
host,
uuid,
nameLabel,
nameDescription,
type,
}) {
let xapi = this.getXAPI(host);
const xapi = this.getXAPI(host)
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',
uuid,
nameLabel,
@ -605,44 +628,41 @@ let reattach = coroutine(function ({
'user',
true,
{}
));
))
let sr = wait(xapi.call('SR.get_record', srRef));
return sr.uuid;
});
const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid
})
reattach.params = {
host: { type: 'string' },
uuid: { type: 'string' },
nameLabel: { type: 'string' },
nameDescription: { type: 'string' },
type: { type: 'string' },
};
type: { type: 'string' }
}
reattach.resolve = {
host: ['host', 'host'],
};
host: ['host', 'host']
}
export {reattach};
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// This function helps to reattach a forgotten ISO SR
let reattachIso = coroutine(function ({
export const reattachIso = coroutine(function ({
host,
uuid,
nameLabel,
nameDescription,
type,
}) {
let xapi = this.getXAPI(host);
const xapi = this.getXAPI(host)
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',
uuid,
nameLabel,
@ -651,22 +671,20 @@ let reattachIso = coroutine(function ({
'iso',
true,
{}
));
))
let sr = wait(xapi.call('SR.get_record', srRef));
return sr.uuid;
});
const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid
})
reattachIso.params = {
host: { type: 'string' },
uuid: { type: 'string' },
nameLabel: { type: 'string' },
nameDescription: { type: 'string' },
type: { type: 'string' },
};
reattachIso.resolve = {
host: ['host', 'host'],
};
type: { type: 'string' }
}
export {reattachIso};
reattachIso.resolve = {
host: ['host', 'host']
}

View File

@ -1,10 +1,14 @@
$debug = (require 'debug') 'xo:api:vm'
$findWhere = require 'lodash.find'
$result = require 'lodash.result'
$forEach = require 'lodash.foreach'
$isArray = require 'lodash.isarray'
$findIndex = require 'lodash.findindex'
$request = require('bluebird').promisify(require('request'))
{$coroutine, $wait} = require '../fibers-utils'
{formatXml: $js2xml} = require '../utils'
{parseXml} = require '../utils'
$isVMRunning = do ->
runningStates = {
@ -35,14 +39,16 @@ create = $coroutine ({
# TODO: remove existing VIFs.
# Creates associated virtual interfaces.
#
# FIXME: device n may already exists, we have to find the first
# free device number.
deviceId = 0
$forEach VIFs, (VIF) =>
network = @getObject VIF.network, 'network'
$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 ? ''
MTU: '1500'
network: network.ref
@ -528,6 +534,14 @@ set = $coroutine (params) ->
else
$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.
for param, fields of {
'name_label'
@ -574,15 +588,10 @@ exports.set = set
restart = $coroutine ({vm, force}) ->
xapi = @getXAPI(vm)
try
# 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
if force
$wait xapi.call 'VM.hard_reboot', vm.ref
else
$wait xapi.call 'VM.clean_reboot', vm.ref
return true
@ -955,3 +964,141 @@ createInterface.resolve = {
}
createInterface.permission = 'admin'
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() {
return this.getObjects();
export function getAllObjects () {
return this.getObjects()
}
getAllObjects.permission = '';
export {getAllObjects};
getAllObjects.permission = ''

View File

@ -1,175 +1,173 @@
import Bluebird from 'bluebird';
import isArray from 'lodash.isarray';
import isObject from 'lodash.isobject';
import makeError from 'make-error';
import Model from './model';
import {EventEmitter} from 'events';
import {mapInPlace} from './utils';
import Bluebird from 'bluebird'
import isArray from 'lodash.isarray'
import isObject from 'lodash.isobject'
import Model from './model'
import {BaseError} from 'make-error'
import {EventEmitter} from 'events'
import {mapInPlace} from './utils'
//====================================================================
// ===================================================================
function ModelAlreadyExists(id) {
ModelAlreadyExists.super.call(this, 'this model already exists: ' + id);
export class ModelAlreadyExists extends BaseError {
constructor (id) {
super('this model already exists: ' + id)
}
}
makeError(ModelAlreadyExists);
export {ModelAlreadyExists};
//====================================================================
// ===================================================================
export default class Collection extends EventEmitter {
// Default value for Model.
get Model() {
return Model;
get Model () {
return Model
}
// Make this property writable.
set Model(Model) {
set Model (Model) {
Object.defineProperty(this, 'Model', {
configurable: true,
enumerale: true,
value: Model,
writable: true,
});
writable: true
})
}
constructor() {
super();
constructor () {
super()
}
add(models, opts) {
let array = isArray(models);
add (models, opts) {
const array = isArray(models)
if (!array) {
models = [models];
models = [models]
}
let {Model} = this;
const {Model} = this
mapInPlace(models, model => {
if (!(model instanceof Model)) {
model = new Model(model);
model = new Model(model)
}
let error = model.validate();
const error = model.validate()
if (error) {
// 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 => {
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) {
if (!isObject(properties)) {
properties = (properties !== undefined) ?
{ id: properties } :
{}
;
}
return Bluebird.try(this._first, [properties], this).then(
model => model && new this.Model(model)
);
)
}
get(properties) {
get (properties) {
if (!isObject(properties)) {
properties = (properties !== undefined) ?
{ id: properties } :
{}
;
}
return Bluebird.try(this._get, [properties], this);
return Bluebird.try(this._get, [properties], this)
}
remove(ids) {
remove (ids) {
if (!isArray(ids)) {
ids = [ids];
ids = [ids]
}
return Bluebird.try(this._remove, [ids], this).then(() => {
this.emit('remove', ids);
return true;
});
this.emit('remove', ids)
return true
})
}
update(models) {
var array = isArray(models);
update (models) {
var array = isArray(models)
if (!isArray(models)) {
models = [models];
models = [models]
}
let {Model} = this;
const {Model} = this
mapInPlace(models, model => {
if (!(model instanceof Model)) {
// TODO: Problems, we may be mixing in some default
// 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.
if (id === undefined){
if (id === undefined) {
// 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) {
// 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 => {
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.
_add() {
throw new Error('not implemented');
_add () {
throw new Error('not implemented')
}
_get() {
throw new Error('not implemented');
_get () {
throw new Error('not implemented')
}
_remove() {
throw new Error('not implemented');
_remove () {
throw new Error('not implemented')
}
_update() {
throw new Error('not implemented');
_update () {
throw new Error('not implemented')
}
// Methods which may be overridden in implementations.
count(properties) {
return this.get(properties).get('count');
count (properties) {
return this.get(properties).get('count')
}
exists(properties) {
exists (properties) {
/* 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(
models => models.length ? models[0] : null
);
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -171,7 +171,11 @@ module.exports = ->
type
# 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).
@ruleConflict = ( -> )
@ -194,9 +198,11 @@ module.exports = ->
else
# This definition are for non singleton items only.
@key = -> @genval.$ref
@val.id = @val.UUID = -> @genval.uuid
@val.id = -> @genval.$id
@val.UUID = -> @genval.uuid
@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.
if @name in ['host', 'pool', 'SR', 'VM', 'VM-controller']
@ -280,14 +286,14 @@ module.exports = ->
hosts: $set {
rule: 'host'
bind: -> @genval.$poolRef
bind: -> @genval.$pool.$ref
}
master: -> @genval.master
networks: $set {
rule: 'network'
bind: -> @genval.$poolRef
bind: -> @genval.$pool.$ref
}
templates: $set {
@ -302,19 +308,19 @@ module.exports = ->
$running_hosts: $set {
rule: 'host'
bind: -> @genval.$poolRef
bind: -> @genval.$pool.$ref
if: $isHostRunning
}
$running_VMs: $set {
rule: 'VM'
bind: -> @genval.$poolRef
bind: -> @genval.$pool.$ref
if: $isVMRunning
}
$VMs: $set {
rule: 'VM'
bind: -> @genval.$poolRef
bind: -> @genval.$pool.$ref
}
}
@ -395,6 +401,12 @@ module.exports = ->
PIFs: -> @genval.PIFs
$PIFs: -> @val.PIFs
PCIs: -> @genval.PCIs
$PCIs: -> @val.PCIs
PGPUs: -> @genval.PGPUs
$PGPUs: -> @val.PGPUs
tasks: $set {
rule: 'task'
bind: -> @genval.resident_on
@ -451,6 +463,12 @@ module.exports = ->
else
false
auto_poweron: ->
if @genval.other_config.auto_poweron
true
else
false
os_version: ->
{guest_metrics} = @data
if guest_metrics
@ -458,8 +476,13 @@ module.exports = ->
else
null
VGPUs: -> @genval.VGPUs
$VGPUs: -> @val.VGPUs
power_state: -> @genval.power_state
other: -> @genval.other_config
memory: ->
{metrics, guest_metrics} = @data
@ -513,7 +536,7 @@ module.exports = ->
@genval.resident_on
else
# TODO: Handle local VMs. (`get_possible_hosts()`).
@genval.$poolRef
@genval.$pool.$ref
snapshots: -> @genval.snapshots
@ -522,6 +545,7 @@ module.exports = ->
$VBDs: -> @genval.VBDs
VIFs: -> @genval.VIFs
}
@rule VM: VMdef
@rule 'VM-controller': VMdef
@ -595,7 +619,7 @@ module.exports = ->
$container: ->
if @genval.shared
@genval.$poolRef
@genval.$pool.$ref
else
@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

View File

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

View File

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

View File

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

View File

@ -2,15 +2,17 @@
{format: $formatUrl, parse: $parseUrl} = require 'url'
$Bluebird = require 'bluebird'
$contains = require 'lodash.contains'
$debug = (require 'debug') 'xo:xo'
$forEach = require 'lodash.foreach'
$includes = require 'lodash.includes'
$isEmpty = require 'lodash.isempty'
$isString = require 'lodash.isstring'
$pluck = require 'lodash.pluck'
$Promise = require 'bluebird'
$proxyRequest = require 'proxy-http-request'
$httpRequest = require 'request'
{createClient: $createRedisClient} = require 'then-redis'
{createClient: $createXapiClient} = require('xen-api')
{
hash: $hash
needsRehash: $needsRehash
@ -21,7 +23,6 @@ $Connection = require './connection'
$Model = require './model'
$RedisCollection = require './collection/redis'
$spec = require './spec'
$XAPI = require './xapi'
{$coroutine, $wait} = require './fibers-utils'
{
generateToken: $generateToken
@ -29,6 +30,8 @@ $XAPI = require './xapi'
} = require './utils'
{$MappedCollection} = require './MappedCollection'
{Set, $for: {getIterator}} = (require 'babel-runtime/core-js').default
#=====================================================================
# Models and collections.
@ -262,142 +265,10 @@ class $XO extends $EventEmitter
# Exports the map from UUIDs to keys.
{$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.
connectSafe = $coroutine (server) ->
connect = $coroutine (server) =>
try
$wait connect server
$wait @connectServer server
catch error
console.error(
"[WARN] #{server.host}:"
@ -405,13 +276,74 @@ class $XO extends $EventEmitter
)
# 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.
getObject: (key, type) ->
@ -423,7 +355,7 @@ class $XO extends $EventEmitter
if type? and (
($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}"
@ -442,16 +374,7 @@ class $XO extends $EventEmitter
# Fetches all objects ignore those missing.
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) ->
connections = @connections
@ -484,6 +407,8 @@ class $XO extends $EventEmitter
return url
#-------------------------------------------------------------------
handleProxyRequest: (req, res, next) ->
unless (
(request = @_proxyRequests[req.url]) and
@ -521,6 +446,8 @@ class $XO extends $EventEmitter
return
#-------------------------------------------------------------------
watchTask: (ref) ->
watcher = @_taskWatchers[ref]
unless watcher?
@ -560,7 +487,7 @@ class $XO extends $EventEmitter
else if credentials.username?
credentials.email = credentials.username
iterator = @_authenticationProviders[Symbol.iterator]()
iterator = getIterator(@_authenticationProviders)
while not (current = iterator.next()).done
try
@ -577,7 +504,10 @@ class $XO extends $EventEmitter
return @users.create(result.email)
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
#=====================================================================