mirror of
https://github.com/mclueppers/xo-server.git
synced 2025-07-31 01:45:14 +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
|
/node_modules/
|
||||||
/database.json
|
npm-debug.log
|
||||||
/log
|
|
||||||
/vendor/
|
|
||||||
|
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
|
# 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:
|
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;
|
- a cache system to provide the best response time possible;
|
||||||
- users authentication and authorizations;
|
- users authentication and authorizations;
|
||||||
- a JSON-RPC based interface for XO clients (i.e. [XO-Web](https://github.com/vatesfr/xo-web)).
|
- 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._
|
_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).
|
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`.
|
2. You need [node.js](http://nodejs.org/) running. Go in the xo-server folder and do a `npm update && npm install`.
|
||||||
3. Copy `config/local.php.dist` to `config/local.php` and complete the configuration.
|
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`.
|
4. Finally, run `./xo-server`, your XO install is available on `http://IPADDRESS:8080`
|
||||||
|
|
||||||
The first time you start XO-Server an `admin` user with the `admin` password is created.
|
|
||||||
|
|
||||||
## How to report a bug?
|
## 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
|
#/bin/sh -eu
|
||||||
<?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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
# This file is a part of Xen Orchestra Server.
|
||||||
* Bootstraps and returns the application singleton.
|
#
|
||||||
*/
|
# Xen Orchestra Server is free software: you can redistribute it
|
||||||
function _bootstrap()
|
# 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;
|
printf '%s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_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