Merge branch 'nodejs'.

This new version of XO-Server is now based on node.js instead of PHP.
This commit is contained in:
Julien Fontanet 2013-08-13 16:47:09 +02:00
commit a6f391dd68
14 changed files with 2543 additions and 78 deletions

6
.gitignore vendored
View File

@ -1,4 +1,2 @@
/config/local.php
/database.json
/log
/vendor/
/node_modules/
npm-debug.log

23
.jshintrc Normal file
View File

@ -0,0 +1,23 @@
{
"bitwise": true,
"curly": true,
"eqeqeq": true,
"es5": true,
"latedef": true,
"laxbreak": true,
"maxcomplexity": 10,
"maxdepth": 5,
"maxlen": 80,
"maxparams": 4,
"maxstatements": 15,
"newcap": true,
"node": true,
"noempty": true,
"nonew": true,
"quotmark": true,
"smarttabs": true,
"strict": false,
"trailing": true,
"undef": true,
"unused": true
}

View File

@ -1,10 +1,10 @@
# Xen Orchestra Server
XO-Server is part of [Xen Orchestra](https://github.com/vatesfr/xo), a web interface for XenServer (or XAPI enabled) hosts.
XO-Server is part of [Xen Orchestra](https://github.com/vatesfr/xo), a web interface for XenServer or XAPI enabled hosts.
It contains all the logic of XO and handles:
- connections to all XCP servers/pools;
- connections to all XAPI servers/pools;
- a cache system to provide the best response time possible;
- users authentication and authorizations;
- a JSON-RPC based interface for XO clients (i.e. [XO-Web](https://github.com/vatesfr/xo-web)).
@ -16,11 +16,9 @@ __XO is currently under development and may be subject to important bugs.__
_There is currently no package available for XO-Server, you must therefore use the following procedure._
1. Download the code, you may either use git `git clone git://github.com/vatesfr/xo-server` or download a [Zip archive](https://github.com/vatesfr/xo-server/archive/master.zip).
2. XO-Web uses [Composer](https://getcomposer.org) for its dependency management, so, once you have [installed it](https://getcomposer.org/download/), juste run `php composer.phar install`.
3. Copy `config/local.php.dist` to `config/local.php` and complete the configuration.
4. Finally, run `./xo-server`.
The first time you start XO-Server an `admin` user with the `admin` password is created.
2. You need [node.js](http://nodejs.org/) running. Go in the xo-server folder and do a `npm update && npm install`.
3. Go into `public/http` folder and symlink to xo-web by doing this: `for f in ../../../xo-web/public/*; do ln -s "$f" .;done`
4. Finally, run `./xo-server`, your XO install is available on `http://IPADDRESS:8080`
## How to report a bug?

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@vates.fr",
"url": "http://vates.fr/"
},
"name": "xo-server",
"version": "0.1.0",
"homepage": "http://github.com/vatesfr/xo-server/",
"repository": {
"type": "git",
"url": "git://github.com/vatesfr/xo-server.git"
},
"main": "src/main.js",
"dependencies": {
"connect": ">=2.8.4",
"extendable": ">=0.0.3",
"hashy": ">=0.1.0",
"q": ">=0.9.6",
"sync": ">=0.2.2",
"underscore": ">=1.4.4",
"validator": ">=1.2.1",
"ws": ">=0.4.27",
"xmlrpc": ">=1.1.0"
},
"devDependencies": {},
"optionalDependencies": {},
"engines": {
"node": "*"
}
}

0
public/http/.keepme Normal file
View File

736
src/api.js Normal file
View File

@ -0,0 +1,736 @@
var _ = require('underscore');
var Q = require('q');
//////////////////////////////////////////////////////////////////////
function deprecated(fn)
{
return function (session, req) {
console.warn(req.method +' is deprecated!');
return fn.apply(this, arguments);
};
}
//////////////////////////////////////////////////////////////////////
function Api(xo)
{
if ( !(this instanceof Api) )
{
return new Api(xo);
}
this.xo = xo;
}
Api.prototype.exec = function (session, request) {
/* jshint newcap: false */
var method = this.getMethod(request.method);
if (!method)
{
console.warn('Invalid method: '+ request.method);
return Q.reject(Api.err.INVALID_METHOD);
}
try
{
return Q(method.call(this, session, request));
}
catch (e)
{
return Q.reject(e);
}
};
Api.prototype.getMethod = function (name) {
/* jshint noempty: false */
var parts = name.split('.');
var current = Api.fn;
for (
var i = 0, n = parts.length;
(i < n) && (current = current[parts[i]]);
++i
)
{}
// Method found.
if (_.isFunction(current))
{
return current;
}
// It's a (deprecated) alias.
if (_.isString(current))
{
return deprecated(this.getMethod(current));
}
// No entry found, looking for a catch-all method.
current = Api.fn;
var catch_all;
for (i = 0; (i < n) && (current = current[parts[i]]); ++i)
{
catch_all = current.__catchAll || catch_all;
}
return catch_all;
};
module.exports = Api;
//////////////////////////////////////////////////////////////////////
function err(code, message)
{
return {
'code': code,
'message': message
};
}
Api.err = {
//////////////////////////////////////////////////////////////////
// JSON-RPC errors.
//////////////////////////////////////////////////////////////////
'INVALID_JSON': err(-32700, 'invalid JSON'),
'INVALID_REQUEST': err(-32600, 'invalid JSON-RPC request'),
'INVALID_METHOD': err(-32601, 'method not found'),
'INVALID_PARAMS': err(-32602, 'invalid parameter(s)'),
'SERVER_ERROR': err(-32603, 'unknown error from the server'),
//////////////////////////////////////////////////////////////////
// XO errors.
//////////////////////////////////////////////////////////////////
'NOT_IMPLEMENTED': err(0, 'not implemented'),
'NO_SUCH_OBJECT': err(1, 'no such object'),
// Not authenticated or not enough permissions.
'UNAUTHORIZED': err(2, 'not authenticated or not enough permissions'),
// Invalid email & passwords or token.
'INVALID_CREDENTIAL': err(3, 'invalid credential'),
'ALREADY_AUTHENTICATED': err(4, 'already authenticated'),
};
//////////////////////////////////////////////////////////////////////
// Helper functions that should be written:
// - checkParams(req.params, param1, ..., paramN).then(...)
// - checkPermission(xo, session, [permission]).then(...)
// @todo Put helpers in their own namespace.
Api.prototype.checkPermission = function (session, permission)
{
// @todo Handle token permission.
var user_id = session.get('user_id');
if (undefined === user_id)
{
return Q.reject(Api.err.UNAUTHORIZED);
}
if (!permission)
{
/* jshint newcap: false */
return Q();
}
return this.xo.users.first(user_id).then(function (user) {
if (!user.hasPermission(permission))
{
throw Api.err.UNAUTHORIZED;
}
});
};
//////////////////////////////////////////////////////////////////////
Api.fn = {};
Api.fn.api = {
'getVersion' : function () {
return '0.1';
},
};
// Session management
Api.fn.session = {
'signInWithPassword': function (session, req) {
var p_email = req.params.email;
var p_pass = req.params.password;
if (!p_email || !p_pass)
{
throw Api.err.INVALID_PARAMS;
}
if (session.has('user_id'))
{
throw Api.err.ALREADY_AUTHENTICATED;
}
return this.xo.users.first({'email': p_email}).then(function (user) {
if (!user)
{
throw Api.err.INVALID_CREDENTIAL;
}
return user.checkPassword(p_pass).then(function (success) {
if (!success)
{
throw Api.err.INVALID_CREDENTIAL;
}
session.set('user_id', user.get('id'));
return true;
});
});
},
'signInWithToken': function (session, req) {
var p_token = req.params.token;
if (!p_token)
{
throw Api.err.INVALID_PARAMS;
}
if (session.has('user_id'))
{
throw Api.err.ALREADY_AUTHENTICATED;
}
return this.xo.tokens.first(p_token).then(function (token) {
if (!token)
{
throw Api.err.INVALID_CREDENTIAL;
}
session.set('token_id', token.get('id'));
session.set('user_id', token.get('user_id'));
return true;
});
},
'getUser': deprecated(function (session) {
var user_id = session.get('user_id');
if (undefined === user_id)
{
return null;
}
return this.xo.users.first(user_id).then(function (user) {
return _.pick(user.properties, 'id', 'email', 'permission');
});
}),
'getUserId': function (session) {
return session.get('user_id', null);
},
'createToken': 'token.create',
'destroyToken': 'token.delete',
};
// User management.
Api.fn.user = {
'create': function (session, req) {
var p_email = req.params.email;
var p_pass = req.params.password;
var p_perm = req.params.permission;
if (!p_email || !p_pass)
{
throw Api.err.INVALID_PARAMS;
}
var users = this.xo.users;
return this.checkPermission(session, 'admin').then(function () {
return users.create(p_email, p_pass, p_perm);
}).then(function (user) {
return (''+ user.get('id'));
});
},
'delete': function (session, req) {
var p_id = req.params.id;
if (undefined === p_id)
{
throw Api.err.INVALID_PARAMS;
}
var users = this.xo.users;
return this.checkPermission(session, 'admin').then(function () {
return users.remove(p_id);
}).then(function (success) {
if (!success)
{
throw Api.err.NO_SUCH_OBJECT;
}
return true;
});
},
'changePassword': function (session, req) {
var p_old = req.params.old;
var p_new = req.params['new'];
if ((undefined === p_old) || (undefined === p_new))
{
throw Api.err.INVALID_PARAMS;
}
var user_id = session.get('user_id');
if (undefined === user_id)
{
throw Api.err.UNAUTHORIZED;
}
var user;
var users = this.xo.users;
return users.first(user_id).then(function (u) {
user = u;
return user.checkPassword(p_old);
}).then(function (success) {
if (!success)
{
throw Api.err.INVALID_CREDENTIAL;
}
return user.setPassword(p_new);
}).then(function () {
return users.update(user).thenResolve(true);
});
},
'get': function (session, req) {
var p_id = req.params.id;
if (undefined === p_id)
{
throw Api.err.INVALID_PARAMS;
}
var promise;
if (session.get('user_id') === p_id)
{
/* jshint newcap: false */
promise = Q();
}
else
{
promise = this.checkPermission(session, 'admin');
}
var users = this.xo.users;
return promise.then(function () {
return users.first(p_id);
}).then(function (user) {
if (!user)
{
throw Api.err.NO_SUCH_OBJECT;
}
return _.pick(user.properties, 'id', 'email', 'permission');
});
},
'getAll': function (session) {
var users = this.xo.users;
return this.checkPermission(session, 'admin').then(function () {
return users.get();
}).then(function (all_users) {
for (var i = 0, n = all_users.length; i < n; ++i)
{
all_users[i] = _.pick(
all_users[i],
'id', 'email', 'permission'
);
}
return all_users;
});
},
'set': function (session, request) {
var p_id = request.params.id;
var p_email = request.params.email;
var p_password = request.params.password;
var p_permission = request.params.permission;
if ((undefined === p_id)
|| ((undefined === p_email)
&& (undefined === p_password)
&& (undefined === p_permission)))
{
throw Api.err.INVALID_PARAMS;
}
var user_id = session.get('user_id');
if (undefined === user_id)
{
throw Api.err.UNAUTHORIZED;
}
var users = this.xo.users;
return users.first(user_id).then(function (user) {
// Get the current user to check its permission.
if (!user.hasPermission('admin'))
{
throw Api.err.UNAUTHORIZED;
}
// @todo Check there are no invalid parameter.
return users.first(p_id).fail(function () {
throw Api.err.INVALID_PARAMS;
});
}).then(function (user) {
// @todo Check user exists.
// Gets the user to update.
// @todo Check undefined value are ignored.
user.set({
'email': p_email,
'permission': p_permission,
});
if (p_password)
{
return user.setPassword(p_password).thenResolve(user);
}
return user;
}).then(function (user) {
// Save the updated user.
return users.update(user);
}).thenResolve(true);
},
};
// Token management.
Api.fn.token = {
'create': function (session) {
var user_id = session.get('user_id');
if ((undefined === user_id)
|| session.has('token_id'))
{
throw Api.err.UNAUTHORIZED;
}
// @todo Token permission.
return this.xo.tokens.generate(user_id).then(function (token) {
return token.get('id');
});
},
'delete': function (session, req) {
var p_token = req.params.token;
var tokens = this.xo.tokens;
return tokens.first(p_token).then(function (token) {
if (!token)
{
throw Api.err.INVALID_PARAMS;
}
// @todo Returns NO_SUCH_OBJECT if the token does not exists.
return tokens.remove(p_token).thenResolve(true);
});
},
};
// Pool management.
Api.fn.server = {
'add': function (session, req) {
var p_host = req.params.host;
var p_username = req.params.username;
var p_password = req.params.password;
if (!p_host || !p_username || !p_password)
{
throw Api.err.INVALID_PARAMS;
}
var servers = this.xo.servers;
return this.checkPermission(session, 'admin').then(function () {
// @todo We are storing passwords which is bad!
// Can we use tokens instead?
return servers.add({
'host': p_host,
'username': p_username,
'password': p_password,
});
}).then(function (server) {
return (''+ server.id);
});
},
'remove': function (session, req) {
var p_id = req.params.id;
if (undefined === p_id)
{
throw Api.err.INVALID_PARAMS;
}
var servers = this.xo.servers;
return this.checkPermission(session, 'admin').then(function () {
return servers.remove(p_id);
}).then(function(success) {
if (!success)
{
throw Api.err.NO_SUCH_OBJECT;
}
return true;
});
},
'getAll': function (session) {
var servers = this.xo.servers;
return this.checkPermission(session, 'admin').then(function () {
return servers.get();
}).then(function (all_servers) {
_.each(all_servers, function (server, i) {
all_servers[i] = _.pick(server, 'id', 'host', 'username');
});
return all_servers;
});
},
'connect': function () {
throw Api.err.NOT_IMPLEMENTED;
},
'disconnect': function () {
throw Api.err.NOT_IMPLEMENTED;
},
};
// Extra methods not really bound to an object.
Api.fn.xo = {
'getStats': function () {
// @todo Keep up-to-date stats in this.xo to avoid unecessary
// (and possibly heavy) computing.
var xo = this.xo;
return Q.all([
xo.hosts.count(),
xo.vms.get({
'is_a_template': false,
'is_control_domain': false,
}),
xo.srs.count(),
]).spread(function (n_hosts, vms, n_srs) {
var running_vms = _.where(vms, {
'power_state': 'Running',
});
var n_vifs = 0;
var n_vcpus = 0;
var memory = 0;
_.each(vms, function (vm) {
n_vifs += vm.VIFs.length;
n_vcpus += +vm.metrics.VCPUs_number;
memory += +vm.metrics.memory_actual;
});
return {
'hosts': n_hosts,
'vms': vms.length,
'running_vms': running_vms.length,
'memory': memory,
'vcpus': n_vcpus,
'vifs': n_vifs,
'srs': n_srs,
};
});
},
'getSessionId': function (req) {
var p_pool_id = req.params.id;
if (undefined === p_pool_id)
{
throw Api.err.INVALID_PARAMS;
}
return this.xo.pools.first(p_pool_id).then(function (pool) {
return pool.get('sessionId');
});
},
};
Api.fn.xapi = {
'__catchAll': function (session, req) {
var RE = /^xapi\.(pool|host|vm|network|sr|vdi|pif|vif)\.getAll$/;
var match;
if (!(match = req.method.match(RE)))
{
throw Api.err.INVALID_METHOD;
}
return this.xo[match[1] +'s'].get();
},
'vm': {
'pause': function (session, req) {
var p_id = req.params.id;
if (!p_id)
{
throw Api.err.INVALID_PARAMS;
}
var xo = this.xo;
var vm;
return this.checkPermission(session, 'write').then(function () {
return xo.vms.first(p_id);
}).then(function (tmp) {
vm = tmp;
if (!vm)
{
throw Api.err.NO_SUCH_OBJECT;
}
return xo.pools.first(vm.get('pool_uuid'));
}).then(function (pool) {
var xapi = xo.connections[pool.get('uuid')];
return xapi.call('VM.pause', vm.get('ref'));
}).thenResolve(true);
},
'unpause': function (session, req) {
var p_id = req.params.id;
if (!p_id)
{
throw Api.err.INVALID_PARAMS;
}
var xo = this.xo;
var vm;
return this.checkPermission(session, 'write').then(function () {
return xo.vms.first(p_id);
}).then(function (tmp) {
vm = tmp;
if (!vm)
{
throw Api.err.NO_SUCH_OBJECT;
}
return xo.pools.first(vm.get('pool_uuid'));
}).then(function (pool) {
var xapi = xo.connections[pool.get('uuid')];
return xapi.call('VM.unpause', vm.get('ref'));
}).thenResolve(true);
},
'reboot': function (session, req) {
var p_id = req.params.id;
if (!p_id)
{
throw Api.err.INVALID_PARAMS;
}
var xo = this.xo;
var vm;
return this.checkPermission(session, 'write').then(function () {
return xo.vms.first(p_id);
}).then(function (tmp) {
vm = tmp;
if (!vm)
{
throw Api.err.NO_SUCH_OBJECT;
}
return xo.pools.first(vm.get('pool_uuid'));
}).then(function (pool) {
var xapi = xo.connections[pool.get('uuid')];
// @todo If XS tools are unavailable, do a hard reboot.
return xapi.call('VM.clean_reboot', vm.get('ref'));
}).thenResolve(true);
},
'shutdown': function (session, req) {
var p_id = req.params.id;
if (!p_id)
{
throw Api.err.INVALID_PARAMS;
}
var xo = this.xo;
var vm;
return this.checkPermission(session, 'write').then(function () {
return xo.vms.first(p_id);
}).then(function (tmp) {
vm = tmp;
if (!vm)
{
throw Api.err.NO_SUCH_OBJECT;
}
return xo.pools.first(vm.get('pool_uuid'));
}).then(function (pool) {
var xapi = xo.connections[pool.get('uuid')];
// @todo If XS tools are unavailable, do a hard shutdown.
return xapi.call('VM.clean_shutdown', vm.get('ref'));
}).thenResolve(true);
},
// we choose to start with default additional parameters:
// false (don't start paused) and false (don't skip pre-boot checks)
'start': function (session, req) {
var p_id = req.params.id;
if (!p_id)
{
throw Api.err.INVALID_PARAMS;
}
var xo = this.xo;
var vm;
return this.checkPermission(session, 'write').then(function () {
return xo.vms.first(p_id);
}).then(function (tmp) {
vm = tmp;
if (!vm)
{
throw Api.err.NO_SUCH_OBJECT;
}
return xo.pools.first(vm.get('pool_uuid'));
}).then(function (pool) {
var xapi = xo.connections[pool.get('uuid')];
return xapi.call('VM.start', vm.get('ref'), false, false);
}).thenResolve(true);
},
},
};

222
src/collection.js Normal file
View File

@ -0,0 +1,222 @@
var _ = require('underscore');
var Q = require('q');
//////////////////////////////////////////////////////////////////////
// @todo Add events.
function Collection(models)
{
// Parent constructor.
Collection.super_.call(this);
this.models = {};
this.next_id = 0;
if (models)
{
this.add(models);
}
}
require('util').inherits(Collection, require('events').EventEmitter);
Collection.prototype.model = require('./model');
/**
* Adds new models to this collection.
*/
Collection.prototype.add = function (models, options) {
var array = true;
if (!_.isArray(models))
{
models = [models];
array = false;
}
// @todo Temporary mesure, implement “set()” instead.
var replace = !!(options && options.replace);
for (var i = 0, n = models.length; i < n; ++i)
{
var model = models[i];
if ( !(model instanceof this.model) )
{
model = new this.model(model);
}
var error = model.validate();
if (undefined !== error)
{
// @todo Better system inspired by Backbone.js.
throw error;
}
var id = model.get('id');
if (undefined === id)
{
id = this.next_id++;
model.set('id', id);
}
// Existing models are ignored.
if (!replace && this.models[id])
{
return Q.reject('cannot add existing models!');
}
this.models[id] = models[i] = model.properties;
}
this.emit('add', models);
/* jshint newcap: false */
return Q(array ? models : models[0]);
};
/**
*
*/
Collection.prototype.count = function (properties) {
return this.get(properties).then(function (models) {
return models.length;
});
};
/**
*
*/
Collection.prototype.exists = function (properties) {
return this.first(properties).then(function (model) {
return (null !== model);
});
};
/**
*
*/
Collection.prototype.first = function (properties) {
/* jshint newcap:false */
var model;
if (_.isObject(properties))
{
model = _.findWhere(this.models, properties);
}
else
{
// Research by id.
model = this.models[properties];
}
if (!model)
{
return Q(null);
}
return Q(new this.model(model));
};
/**
* Find all models which have a given set of properties.
*
* /!\: Does not return instance of this.model.
*/
Collection.prototype.get = function (properties) {
/* jshint newcap: false */
// For coherence with other methods.
if ((undefined !== properties) && !_.isObject(properties))
{
properties = {
'id': properties,
};
}
if (_.isEmpty(properties))
{
return Q(_.values(this.models));
}
return Q(_.where(this.models, properties));
};
/**
* Removes models from this collection.
*/
Collection.prototype.remove = function (ids) {
if (!_.isArray(ids))
{
ids = [ids];
}
_.each(ids, function (id) {
delete this.models[id];
}, this);
this.emit('remove', ids);
// @todo Maybe return a more meaningful value.
/* jshint newcap: false */
return Q(true); // @todo Returns false if it fails.
};
/**
* Smartly updates the collection.
*
* - Adds new models.
* - Updates existing models.
* - Removes missing models.
*/
// Collection.prototype.set = function (/*models*/) {
// // @todo
// };
/**
* Updates existing models.
*/
Collection.prototype.update = function (models) {
var array = true;
if (!_.isArray(models))
{
models = [models];
array = false;
}
// @todo Rewrite.
for (var i = 0; i < models.length; i++)
{
var model = models[i];
if (model instanceof this.model)
{
model = model.properties;
}
var id = model.id;
// Missing models should be added not updated.
if (!this.models[id])
{
return Q.reject('missing model');
}
// @todo Model validation.
// @todo Event handling.
_.extend(this.models[id], model);
}
/* jshint newcap: false */
return Q(array ? models : models[0]);
};
Collection.extend = require('extendable');
//////////////////////////////////////////////////////////////////////
module.exports = Collection;

267
src/main.js Normal file
View File

@ -0,0 +1,267 @@
var _ = require('underscore');
var connect = require('connect');
var Q = require('q');
var Session = require('./session');
var tcp = require('net');
var WSServer = require('ws').Server;
//--------------------------------------
var xo = require('./xo')();
var Api = require('./api');
var api = new Api(xo);
// @todo Port should be configurable.
var http_serv = require('http').createServer().listen(8080);
//////////////////////////////////////////////////////////////////////
function json_api_call(session, message)
{
/* jshint newcap:false */
var req = {
'id': null,
};
function format_error(error)
{
return JSON.stringify({
'jsonrpc': '2.0',
'error': error,
'id': req.id,
});
}
try
{
req = JSON.parse(message.toString());
}
catch (e)
{
if (e instanceof SyntaxError)
{
return Q(format_error(Api.err.INVALID_JSON));
}
return Q(format_error(Api.err.SERVER_ERROR));
}
/* jshint laxbreak: true */
if (!req.method || !req.params
|| (undefined === req.id)
|| ('2.0' !== req.jsonrpc))
{
return Q(format_error(Api.err.INVALID_REQUEST));
}
return api.exec(
session,
{
'method': req.method,
'params': req.params,
}
).then(
function (result) {
return JSON.stringify({
'jsonrpc': '2.0',
'result': result,
'id': req.id,
});
},
function (error) {
if (error instanceof Error)
{
console.error(error.stack);
return format_error(Api.err.SERVER_ERROR);
}
return format_error(error);
}
);
}
//////////////////////////////////////////////////////////////////////
// Static file serving (for XO-Web for instance).
//////////////////////////////////////////////////////////////////////
xo.on('started', function () {
http_serv.on('request', connect()
// Compresses reponses using GZip.
.use(connect.compress())
// Caches the responses in memory.
//.use(connect.staticCache())
// Serve static files.
.use(connect.static(__dirname +'/../public/http'))
);
});
//////////////////////////////////////////////////////////////////////
// WebSocket to TCP proxy (used for consoles).
//////////////////////////////////////////////////////////////////////
// Protocol:
//
// 1. The web browser connects to the server via WebSocket.
//
// 2. It sends a first message containing the “host” and “port” to
// connect to in a JSON object.
//
// 3. All messages to send to the TCP server and received from it will
// be encoded using Base64.
// @todo Avoid Base64 encoding and directly use binary streams.
xo.on('started', function () {
var server = new WSServer({
'server': http_serv,
'path': '/websockify',
});
server.on('connection', function (socket) {
// Parses the first message which SHOULD contains the host and
// port of the host to connect to.
socket.once('message', function (message) {
try
{
message = JSON.parse(message);
}
catch (e)
{
socket.close();
return;
}
if (!message.host && !message.port)
{
socket.close();
return;
}
var target = tcp.createConnection(message.port, message.host);
target.on('data', function (data) {
socket.send(data.toString('base64'));
});
target.on('end', function () {
socket.close();
});
target.on('error', function () {
target.end();
});
socket.on('message', function (message) {
target.write(new Buffer(message, 'base64'));
});
socket.on('close', function () {
target.end();
});
});
socket.on('error', function () {
socket.close();
});
});
});
//////////////////////////////////////////////////////////////////////
// JSON-RPC over WebSocket.
//////////////////////////////////////////////////////////////////////
xo.on('started', function () {
var server = new WSServer({
'server': http_serv,
'path': '/api/',
});
server.on('connection', function (socket) {
var session = new Session(xo);
session.once('close', function () {
socket.close();
});
socket.on('message', function (request) {
json_api_call(session, request).then(function (response) {
// Send response if session still open.
if (socket.readyState === socket.OPEN)
{
socket.send(response);
}
}).done();
});
// @todo Ugly inter dependency.
socket.once('close', function () {
session.close();
});
});
});
//////////////////////////////////////////////////////////////////////
// JSON-RPC over TCP.
//////////////////////////////////////////////////////////////////////
xo.on('started', function () {
require('net').createServer(function (socket) {
var session = new Session(xo);
session.on('close', function () {
socket.end(); // @todo Check it is enough.
});
var length = null; // Expected message length.
var buffer = new Buffer(1024); // @todo I hate hardcoded values!
socket.on('data', function (data) {
data.copy(buffer);
// Read the message length.
if (!length)
{
var i = _.indexOf(buffer, 10);
if (-1 === i)
{
return;
}
length = +(buffer.toString('ascii', 0, i));
// If the length is NaN, we cannot do anything except
// closing the connection.
if (length !== length)
{
session.close();
return;
}
buffer = buffer.slice(i + 1);
}
// We do not have received everything.
if (buffer.length < length)
{
return;
}
json_api_call(
session,
buffer.slice(0, length).toString()
).then(function (response) {
// @todo Handle long messages.
socket.write(response.length +'\n'+ response);
}).done();
// @todo Check it frees the memory.
buffer = buffer.slice(length);
length = null;
});
// @todo Ugly inter dependency.
socket.once('close', function () {
session.close();
});
}).listen(1024); // @todo Should be configurable.
});
//////////////////////////////////////////////////////////////////////
xo.start();

111
src/model.js Normal file
View File

@ -0,0 +1,111 @@
var _ = require('underscore');
//////////////////////////////////////////////////////////////////////
function Model(properties)
{
// Parent constructor.
Model.super_.call(this);
this.properties = _.extend({}, this['default']);
if (properties)
{
this.set(properties);
}
}
require('util').inherits(Model, require('events').EventEmitter);
/**
* Initializes the model after construction.
*/
Model.prototype.initialize = function () {};
/**
* Validates the model.
*
* @returns {undefined|mixed} Returns something else than undefined if
* there was an error.
*/
Model.prototype.validate = function (/*properties*/) {};
/**
* Gets property.
*/
Model.prototype.get = function (property, def) {
var prop = this.properties[property];
if (undefined !== prop)
{
return prop;
}
return def;
};
/**
* Checks if a property exists.
*/
Model.prototype.has = function (property) {
return (undefined !== this.properties[property]);
};
/**
* Sets properties.
*/
Model.prototype.set = function (properties, value) {
if (undefined !== value)
{
var property = properties;
properties = {};
properties[property] = value;
}
var previous = {};
var model = this;
_.each(properties, function (value, key) {
if (undefined === value)
{
return;
}
var prev = model.get(key);
// New value.
if (value !== prev)
{
previous[key] = prev;
model.properties[key] = value;
}
});
if (!_.isEmpty(previous))
{
this.emit('change', previous);
_.each(previous, function (previous, property) {
this.emit('change:'+ property, previous);
}, this);
}
};
/**
* Unsets properties.
*/
Model.prototype.unset = function (properties) {
// @todo Events.
this.properties = _.omit(this.properties, properties);
};
/**
* Default properties.
*
* @type {Object}
*/
Model.prototype['default'] = {};
Model.extend = require('extendable');
//////////////////////////////////////////////////////////////////////
module.exports = Model;

53
src/session.js Normal file
View File

@ -0,0 +1,53 @@
var Model = require('./model');
//////////////////////////////////////////////////////////////////////
var Session = Model.extend({
'constructor': function (xo) {
Model.call(this);
var self = this;
var close = function () {
self.close();
};
// If the user associated to this session is deleted or
// disabled, the session must close.
this.on('change:user_id', function () {
var event = 'user.revoked:'+ this.get('user_id');
xo.on(event, close);
// Prevents a memory leak.
self.on('close', function () {
xo.removeListener(event, close);
});
});
// If the token associated to this session is deleted, the
// session must close.
this.on('change:token_id', function () {
var event = 'token.revoked:'+ this.get('token_id');
xo.on(event, close);
// Prevents a memory leak.
self.on('close', function () {
xo.removeListener(event, close);
});
});
},
'close': function () {
// This function can be called multiple times but will only
// emit an event once.
this.close = function () {};
this.emit('close');
},
});
//////////////////////////////////////////////////////////////////////
module.exports = Session;

52
src/xapi.js Normal file
View File

@ -0,0 +1,52 @@
var Q = require('q');
var xmlrpc = require('xmlrpc');
Q.longStackSupport = true;
//////////////////////////////////////////////////////////////////////
function Xapi(host)
{
// Parent constructor.
Xapi.super_.call(this);
this.xmlrpc = xmlrpc.createSecureClient({
hostname: host,
port: '443',
rejectUnauthorized: false,
}); // @todo Handle connection success/error.
}
require('util').inherits(Xapi, require('events').EventEmitter);
Xapi.prototype.call = function (method) {
var params = Array.prototype.slice.call(arguments, 1);
if (this.sessionId)
{
params.unshift(this.sessionId);
}
var self = this;
return Q.ninvoke(this.xmlrpc, 'methodCall', method, params)
.then(function (value) {
if ('Success' !== value.Status)
{
throw value;
}
return value.Value;
});
};
Xapi.prototype.connect = function (username, password) {
var self = this;
return this.call('session.login_with_password', username, password)
.then(function (session_id) {
self.sessionId = session_id;
});
};
//////////////////////////////////////////////////////////////////////
module.exports = Xapi;

606
src/xo.js Normal file
View File

@ -0,0 +1,606 @@
var _ = require('underscore');
var crypto = require('crypto');
var hashy = require('hashy');
var Q = require('q');
var Collection = require('./collection');
var Model = require('./model');
var Xapi = require('./xapi');
//////////////////////////////////////////////////////////////////////
var check = function () {
var errors;
var validator = new (require('validator').Validator)();
validator.error = function (err) {
(errors || (errors = [])).push(err);
return this;
};
var check = function (data) {
validator.check(data);
};
check.pop = function () {
var res = errors;
errors = undefined;
return res;
};
return check;
}();
//////////////////////////////////////////////////////////////////////
// Models & Collections.
//////////////////////////////////////////////////////////////////////
var Server = Model.extend({
'validate': function () {
// @todo
},
});
var Servers = Collection.extend({
'model': Server,
});
//--------------------------------------------------------------------
// @todo We could also give a permission level to tokens (<=
// user.permission).
var Token = Model.extend({
// Validates model attributes.
// 'validate': function (attr) {
// check(attr.id).len(10);
// check(attr.user_id).isInt();
// return check.pop();
// },
}, {
'generate': function (user_id) {
return Q.ninvoke(crypto, 'randomBytes', 32).then(function (buf) {
return new Token({
'id': buf.toString('base64'),
'user_id': user_id,
});
});
},
});
var Tokens = Collection.extend({
'model': Token,
'generate': function (user_id) {
var self = this;
return Token.generate(user_id).then(function (token) {
return self.add(token);
});
}
});
//--------------------------------------------------------------------
var User = Model.extend({
'default': {
'permission': 'none',
},
// Validates model attributes.
// 'validate': function (attr) {
// check(attr.id).isInt();
// check(attr.email).isEmail();
// check(attr.pw_hash).len(40);
// check(attr.permission).isIn('none', 'read', 'write', 'admin');
// return check.pop();
// },
'setPassword': function (password) {
var self = this;
return hashy.hash(password).then(function (hash) {
self.set('pw_hash', hash);
});
},
// Checks and updates the hash if necessary.
'checkPassword': function (password) {
var hash = this.get('pw_hash');
var user = this;
return hashy.verify(password, hash).then(function (success) {
if (!success)
{
return false;
}
if (hashy.needsRehash(hash))
{
return user.setPassword(password).thenResolve(true);
}
return true;
});
},
'hasPermission': function (permission) {
var perms = {
'none': 0,
'read': 1,
'write': 2,
'admin': 3,
};
return (perms[this.get('permission')] >= perms[permission]);
},
});
// @todo handle email uniqueness.
var Users = Collection.extend({
'model': User,
'create': function (email, password, permission) {
var user = new User({
'email': email,
});
if (permission)
{
user.set('permission', permission);
}
var self = this;
return user.setPassword(password).then(function () {
return self.add(user);
});
}
});
//--------------------------------------------------------------------
var Pool = Model.extend({});
var Pools = Collection.extend({
'model': Pool,
});
//--------------------------------------------------------------------
var Host = Model.extend({});
var Hosts = Collection.extend({
'model': Host,
});
//--------------------------------------------------------------------
var VM = Model.extend({});
var VMs = Collection.extend({
'model': VM,
});
//--------------------------------------------------------------------
var Network = Model.extend({});
var Networks = Collection.extend({
'model': Network,
});
//--------------------------------------------------------------------
var SR = Model.extend({});
var SRs = Collection.extend({
'model': SR,
});
//--------------------------------------------------------------------
var VDI = Model.extend({});
var VDIs = Collection.extend({
'model': VDI,
});
//--------------------------------------------------------------------
var PIF = Model.extend({});
var PIFs = Collection.extend({
'model': PIF,
});
//--------------------------------------------------------------------
var VIF = Model.extend({});
var VIFs = Collection.extend({
'model': VIF,
});
//////////////////////////////////////////////////////////////////////
// Collections
//////////////////////////////////////////////////////////////////////
var VDIs = Collection.extend({
'model': VDI,
});
//////////////////////////////////////////////////////////////////////
// @todo Really ugly.
function refresh(xo, xapi)
{
var get_records = function (classes) {
var promises = [];
for (var i = 0, n = classes.length; i < n; i++)
{
!function (klass) {
promises.push(
xapi.call(klass +'.get_all_records')
.fail(function (error) {
console.error(klass, error);
return {};
})
);
}(classes[i]);
}
return Q.all(promises);
};
return get_records([
// Main classes.
'pool',
'host',
'VM',
'network',
'SR',
'VDI',
'PIF',
'VIF',
// Associated classes (e.g. metrics).
'console',
'crashdump',
'DR_task',
'host_cpu',
'host_crashdump',
'host_metrics',
'host_patch',
'message',
'PBD',
'PCI',
'PGPU',
'PIF_metrics',
'VBD',
'VGPU',
'VIF_metrics',
'VM_appliance',
'VM_metrics',
'VM_guest_metrics',
//'VMPP',
//'VTPM',
]).spread(function (
pools,
hosts,
vms,
networks,
srs,
vdis,
pifs,
vifs,
consoles,
crashdumps,
dr_tasks,
host_cpus,
host_crashdumps,
host_metrics,
host_patches,
messages,
pbds,
pcis,
pgpus,
pif_metrics,
vbds,
vgpus,
vif_metrics,
vm_appliances,
vm_metrics,
vm_guest_metrics
//vmpps
//vtpms
) {
// Special case for pools.
pools = _.values(pools);
var pool_uuid = pools[0].id = pools[0].uuid;
xo.connections[pool_uuid] = xapi;
// @todo Remove: security concerns.
pools[0].sessionId = xapi.sessionId;
var resolve = function (model, collection, props, include) {
/* jshint laxbreak: true */
if (!_.isArray(props))
{
props = [props];
}
var helper;
if (include)
{
helper = function (ref) {
return collection[ref] || null;
};
}
else
{
helper = function (ref) {
var model = collection[ref];
return model && model.uuid || null;
};
}
var map = function (list, iterator) {
var result = _.isArray(list) ? [] : {};
_.each(list, function (value, key) {
result[key] = iterator(value);
});
return result;
};
for (var i = 0, n = props.length; i < n; ++i)
{
var prop = props[i];
var ref = model[prop];
model[prop] = _.isArray(ref)
? map(ref, helper)
: helper(ref);
}
};
// @todo Messages are linked differently.
messages = _.groupBy(messages, 'obj_uuid');
// @todo Cast numerical/boolean properties to correct types.
// Resolves dependencies.
//
// 1. Associated objects are included.
// 2. Linked objects are relinked using their uuid instead of
// their reference.
_.each(pools, function (pool) {
// @todo Blobs?
resolve(pool, srs, [
'crash_dump_SR',
'default_SR',
'suspend_image_SR',
]);
resolve(pool, hosts, 'master');
resolve(pool, vdis, [
'metadata_VDIs',
'redo_log_vdi',
]);
});
_.each(hosts, function (host) {
// @todo Blobs?
resolve(host, srs, [
'crash_dump_sr',
'local_cache_sr',
'suspend_image_SR',
]);
resolve(host, host_crashdumps, 'host_crashdumps', true);
resolve(host, host_cpus, 'host_CPUs', true);
resolve(host, host_metrics, 'metrics', true);
resolve(host, host_patches, 'patches', true);
resolve(host, pbds, 'PBDs', true);
resolve(host, pcis, 'PCIs', true);
resolve(host, pgpus, 'PGPUs', true);
resolve(host, pifs, 'PIFs');
resolve(host, vms, 'resident_VMs');
});
_.each(vms, function (vm) {
// @todo Blobs?
resolve(vm, hosts, [
'affinity',
'resident_on',
]);
resolve(vm, vm_appliances, 'appliance', true);
resolve(vm, pcis, 'attached_PCIs', true);
resolve(vm, vms, [
'children', // Snapshots?
'parent',
'snapshot_of',
]);
resolve(vm, consoles, 'consoles', true);
resolve(vm, crashdumps, 'crash_dumps', true);
resolve(vm, vm_guest_metrics, 'guest_metrics', true);
vm.messages = messages[vm.uuid] || []; // @todo
resolve(vm, vm_metrics, 'metrics', true);
//resolve(vm, vmpps, 'protection_policy', true);
resolve(vm, srs, 'suspend_SR');
resolve(vm, vdis, 'suspend_VDI');
resolve(vm, vbds, 'VBDs');
resolve(vm, vgpus, 'VGPUs');
resolve(vm, vifs, 'VIFs');
//resolve(vm, vtpms, 'VTPMs');
});
_.each(networks, function (network) {
// @todo Blobs?
resolve(network, pifs, 'PIFs');
resolve(network, vifs, 'VIFs');
});
_.each(srs, function (sr) {
// @todo Blobs?
resolve(sr, dr_tasks, 'introduced_by'); // @todo.
resolve(sr, pbds, 'PBDs');
resolve(sr, vdis, 'VDIs');
});
_.each(vdis, function (vdi) {
resolve(vdi, crashdumps, 'crash_dumps', true);
resolve(vdi, pools, 'metadata_of_pool');
resolve(vdi, vdis, [
'parent',
'snapshot_of',
'snapshots',
]);
resolve(vdi, srs, 'SR');
resolve(vdi, vbds, 'VBDs');
});
_.each(pifs, function (pif) {
// @todo Bonds, tunnels & VLANs.
resolve(pif, hosts, 'host');
resolve(pif, pif_metrics, 'metrics');
resolve(pif, networks, 'network');
});
_.each(vifs, function (vif) {
resolve(vif, vif_metrics, 'metrics');
resolve(vif, networks, 'network');
resolve(vif, vms, 'VM');
});
// Normalizes the collections.
//
// 1. The collection is converted to an array.
// 2. For each object, an identifier based on its uuid is
// created.
var normalize = function (items) {
return _.map(items, function (item, ref) {
item.id = item.uuid;
item.pool_uuid = pool_uuid;
item.ref = ref;
return item;
});
};
var opts = {
'replace': true,
};
return Q.all([
xo.pools.add(pools, opts), // Special case.
xo.hosts.add(normalize(hosts), opts),
xo.vms.add(normalize(vms), opts),
xo.networks.add(normalize(networks), opts),
xo.srs.add(normalize(srs), opts),
xo.vdis.add(normalize(vdis), opts),
]);
});
}
function Xo()
{
if ( !(this instanceof Xo) )
{
return new Xo();
}
var xo = this;
//--------------------------------------
// Main objects (@todo should be persistent).
xo.servers = new Servers();
xo.tokens = new Tokens();
xo.users = new Users();
// When a server is added we should connect to it and fetch data.
xo.servers.on('add', function (servers) {
_.each(servers, function (server) {
var xapi = new Xapi(server.host);
xapi.connect(server.username, server.password).then(function () {
// @todo Use events.
!function helper() {
refresh(xo, xapi).then(function () {
setTimeout(helper, 5000);
}).done();
}();
}).fail(function (error) {
console.log(error);
}).done();
});
});
xo.servers.on('remove', function (server_ids) {
// @todo
});
// xo events are used to automatically close connections if the
// associated credentials are invalidated.
xo.tokens.on('remove', function (token_ids) {
_.each(token_ids, function (token_id) {
xo.emit('token.revoked:'+ token_id);
});
});
xo.users.on('remove', function (user_ids) {
_.each(user_ids, function (user_id) {
xo.emit('user.revoked:'+ user_id);
});
});
// Connections to Xen pools/servers.
xo.connections = {};
//--------------------------------------
// Xen objects.
xo.pools = new Pools();
xo.hosts = new Hosts();
xo.vms = new VMs();
xo.networks = new Networks();
xo.srs = new SRs();
xo.vdis = new VDIs();
// Connecting classes. (@todo VBD & SR).
xo.vifs = new VIFs();
xo.pifs = new PIFs();
// -------------------------------------
// Temporary data for testing purposes.
xo.servers.add([{
'host': '192.168.1.116',
'username': 'root',
'password': 'qwerty',
}]).done();
xo.users.add([{
'email': 'bob@gmail.com',
'pw_hash': '$2a$10$PsSOXflmnNMEOd0I5ohJQ.cLty0R29koYydD0FBKO9Rb7.jvCelZq',
'permission': 'admin',
}, {
'email': 'toto@gmail.com',
'pw_hash': '$2a$10$PsSOXflmnNMEOd0I5ohJQ.cLty0R29koYydD0FBKO9Rb7.jvCelZq',
'permission': 'none',
}]).done();
}
require('util').inherits(Xo, require('events').EventEmitter);
Xo.prototype.start = function () {
var xo = this;
// @todo Connect to persistent collection.
//--------------------------------------
xo.emit('started');
};
//////////////////////////////////////////////////////////////////////
module.exports = Xo;

380
tests/websocket.js Normal file
View File

@ -0,0 +1,380 @@
/* jshint loopfunc:false */
var assert = require('assert');
var sync = require('sync');
var WS = require('ws');
var _ = require('underscore');
//////////////////////////////////////////////////////////////////////
var tests = {
'Session management': {
'Password sign in': function () {
// Connects, signs in (with a password).
var conn = this.connect();
assert(conn('session.signInWithPassword', {
'email': 'bob@gmail.com',
'password': '123',
}));
},
'Password sign in with a inexistent user': function() {
// Connects
var conn = this.connect();
try
{
conn('session.signInWithPassword', {
'email': ' @gmail.com',
'password': '123',
});
}
catch (e)
{
// Check the error.
assert.strictEqual(e.code, 3);
return;
}
assert(false);
},
/* jshint maxlen:90 */
'Password sign in withan existing user and incorrect password': function () {
// Connects
var conn = this.connect();
try
{
// Try to sign in (with password).
conn('session.signInWithPassword', {
'email': 'bob@gmail.com',
'password': 'abc',
});
}
catch (e)
{
// Check if received invalid credential error.
assert.strictEqual(e.code, 3);
return;
}
assert(false);
},
'Password sign in with user already authenticated': function() {
// Connects, signs in (with a password).
var conn = this.connect();
conn('session.signInWithPassword', {
'email': 'bob@gmail.com',
'password': '123',
});
try
{
// Try to connect with other user account
conn('session.signInWithPassword', {
'email': 'toto@gmail.com',
'password': '123',
});
}
catch (e)
{
// Check if received already authenticated error.
assert.strictEqual(e.code, 4);
return;
}
assert(false);
},
},
///////////////////////////////////////
'Token management': {
'Token sign in': function () {
// Creates a token.
var token = this.master('token.create');
// Connects, signs in (with a token).
var conn = this.connect();
assert(conn('session.signInWithToken', {
'token': token
}));
// Check if connected with the same user.
assert.strictEqual(
conn('session.getUserId'),
this.master('session.getUserId')
);
},
'Token sign in with invalid parameter': function() {
// Connects.
var conn = this.connect();
try
{
// Try to sign in (with a token).
conn('session.signInWithToken', {
'token': ' ',
});
}
catch (e)
{
// Check if received invalid credential error.
assert.strictEqual(e.code, 3);
return;
}
assert(false);
},
'Connection close out when token removed': function () {
// Creates a token.
var token = this.master('token.create');
// Connects again and uses the token to sign in.
var conn = this.connect();
conn('session.signInWithToken', {'token': token});
// Delete the tokens.
this.master('token.delete', {'token': token});
// Checks the connection is closed.
assert.throws(function () {
conn('session.getUserId');
});
},
},
///////////////////////////////////////
'User management': {
'Create user': function() {
// Connects, sign in (with a password).
var conn = this.connect();
conn('session.signInWithPassword', {
'email': 'bob@gmail.com',
'password': '123',
});
// Create a user account.
assert(conn('user.create', {
'email': 'tintin@gmail.com',
'password': 'abc',
'permission': 'admin',
}));
},
'Delete user': function() {
// Connects, sign in (with a password).
var user_id = this.master('user.create', {
'email': 'fox@gmail.com',
'password': '123',
'permission': 'none',
});
// Delete user
assert(this.master('user.delete', {
'id': user_id,
}));
},
'Connection close out when user removed': function() {
// Connects, sign in (with a password).
var user_id = this.master('user.create', {
'email': 'fox@gmail.com',
'password': '123',
'permission': 'none',
});
// Connects, sign in (with a password)
var conn = this.connect();
conn('session.signInWithPassword', {
'email': 'fox@gmail.com',
'password': '123',
});
// Delete the user
this.master('user.delete', {
'id': user_id,
});
// Checks the connection is closed.
assert.throws(function () {
conn('session.getUserId');
});
},
'Change password': function() {
// Create new account.
this.master('user.create', {
'email': 'fox@gmail.com',
'password': '123',
'permission': 'none',
});
// Connects, sign in (with a password).
var conn = this.connect();
conn('session.signInWithPassword', {
'email': 'fox@gmail.com',
'password': '123',
});
// Change password.
conn('user.changePassword', {
'old': '123',
'new': 'abc',
});
// Check if password has changed
var conn2 = this.connect();
assert(conn2('session.signInWithPassword', {
'email': 'fox@gmail.com',
'password': 'abc',
}));
},
'Get all users': function() {
var users = this.master('user.getAll');
assert(_.isArray(users));
},
'Set user': function() {
var user_id = this.master('user.create', {
'email': 'link@gmail.com',
'password': 'abc',
'permission': 'none',
});
this.master('user.set', {
'id': user_id,
'email': 'mario@gmail.com',
'password': '123',
});
var conn = this.connect();
assert(conn('session.signInWithPassword', {
'email': 'mario@gmail.com',
'password': '123',
}));
},
},
};
//////////////////////////////////////////////////////////////////////
var next_id = 0;
function call(method, params)
{
return function (callback)
{
var request = {
'jsonrpc': '2.0',
'id': next_id++,
'method': method,
'params': params || {},
};
this.send(JSON.stringify(request), function (error) {
if (error)
{
callback(error);
}
});
this.once('message', function (response) {
try
{
response = JSON.parse(response.toString());
}
catch (e)
{
callback(e);
return;
}
if (response.error)
{
// To help find the problem, the request is included
// in the error.
var error = response.error;
error.request = request;
callback(error);
return;
}
callback(null, response.result);
});
}.sync(this);
}
function connect(url)
{
var socket;
(function (callback)
{
socket = new WS(url);
socket.on('open', function () {
callback(null, socket);
});
socket.on('error', function (error) {
callback(error);
});
}).sync();
var conn = function (method, params) {
return call.call(socket, method, params);
};
return conn;
}
//////////////////////////////////////////////////////////////////////
sync(function () {
// All tests have access to this master connection to create
// initial data.
var master = connect('ws://localhost:8080/');
master('session.signInWithPassword', {
'email': 'bob@gmail.com',
'password': '123',
});
var self = {
'connect': function () {
return connect('ws://localhost:8080/');
},
'master': master,
};
for (var category in tests)
{
console.log();
console.log(category);
console.log('====================');
for (var test in tests[category])
{
console.log('- '+ test);
var f = tests[category][test];
try
{
f.call(self);
}
catch (error)
{
console.error(error);
}
}
}
}, function () {
process.exit();
});

122
xo-server
View File

@ -1,72 +1,60 @@
#!/usr/bin/php
<?php
/**
* This file is a part of Xen Orchestra Server.
*
* Xen Orchestra Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Xen Orchestra Server is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xen Orchestra Server. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Julien Fontanet <julien.fontanet@vates.fr>
* @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
*
* @package Xen Orchestra Server
*/
#/bin/sh -eu
/**
* Bootstraps and returns the application singleton.
*/
function _bootstrap()
# This file is a part of Xen Orchestra Server.
#
# Xen Orchestra Server is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
#
# Xen Orchestra Server is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Xen Orchestra Server. If not, see
# <http://www.gnu.org/licenses/>.
#
# @author Julien Fontanet <julien.fontanet@vates.fr>
# @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
#
# @package Xen Orchestra Server
# _fail message
_fail()
{
static $application;
if (!isset($application))
{
// Class autoloading is done by composer.
require(__DIR__.'/vendor/autoload.php');
// Reads configuration.
$config = new Config;
foreach (array('global', 'local') as $file)
{
$file = __DIR__.'/config/'.$file.'.php';
if (is_file($file))
{
$config->merge(require($file));
}
}
// Injects some variables.
$config['root_dir'] = __DIR__;
// Dependency injector.
$di = new DI;
$di->set('config', $config);
// Logs all errors.
$error_logger = $di->get('error_logger');
set_error_handler(array($error_logger, 'log'));
register_shutdown_function(array($error_logger, 'handleShutdown'));
// Finally, creates the application.
$application = $di->get('application');
}
return $application;
printf '%s\n' "$1" >&2
exit 1
}
_bootstrap()->run();
# _have <command>
_have()
{
type "$1" 2> /dev/null >&2
}
// Local Variables:
// mode: php
// End:
########################################
cd -P "$(dirname "$(which "$0")")"
########################################
if [ "${NODE:-}" ]
then
node=$NODE
unset NODE # Unexports it.
elif _have node
then
node=node
elif _have nodejs
then
node=nodejs
else
_fail 'node.js could not be found'
fi
########################################
exec "$node" "src/main.js" "$@"