mirror of
https://github.com/mclueppers/xo-server.git
synced 2025-04-08 20:55:02 +02:00
Merge branch 'nodejs'.
This new version of XO-Server is now based on node.js instead of PHP.
This commit is contained in:
commit
a6f391dd68
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,4 +1,2 @@
|
||||
/config/local.php
|
||||
/database.json
|
||||
/log
|
||||
/vendor/
|
||||
/node_modules/
|
||||
npm-debug.log
|
||||
|
23
.jshintrc
Normal file
23
.jshintrc
Normal 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
|
||||
}
|
12
README.md
12
README.md
@ -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
31
package.json
Normal 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
0
public/http/.keepme
Normal file
736
src/api.js
Normal file
736
src/api.js
Normal 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
222
src/collection.js
Normal 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
267
src/main.js
Normal 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
111
src/model.js
Normal 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
53
src/session.js
Normal 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
52
src/xapi.js
Normal 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
606
src/xo.js
Normal 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
380
tests/websocket.js
Normal 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
122
xo-server
@ -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" "$@"
|
||||
|
Loading…
x
Reference in New Issue
Block a user