Update Api code.

This commit is contained in:
Julien Fontanet 2015-02-24 18:28:56 +01:00
parent 1192bf6a87
commit 0d4b9b4bce
19 changed files with 318 additions and 389 deletions

View File

@ -1,315 +1,200 @@
'use strict';
import debug from 'debug';
debug = debug('xo:api');
import assign from 'lodash.assign';
import Bluebird from 'bluebird';
import forEach from 'lodash.foreach';
import getKeys from 'lodash.keys';
import isFunction from 'lodash.isfunction';
import requireTree from 'require-tree';
import schemaInspector from 'schema-inspector';
import {coroutine} from 'bluebird';
import {
InvalidParameters,
MethodNotFound,
NoSuchObject,
Unauthorized,
} from './api-errors';
//====================================================================
var assign = require('lodash.assign');
var Bluebird = require('bluebird');
var debug = require('debug')('xo:api');
var forEach = require('lodash.foreach');
var isArray = require('lodash.isarray');
var isFunction = require('lodash.isfunction');
var isObject = require('lodash.isobject');
var isString = require('lodash.isstring');
var keys = require('lodash.keys');
var pick = require('lodash.pick');
var requireTree = require('require-tree');
var schemaInspector = require('schema-inspector');
// FIXME: this function is specific to XO and should not be defined in
// this file.
let checkPermission = coroutine(function *(permission) {
/* jshint validthis: true */
var apiErrors = require('./api-errors');
var coroutine = require('./fibers-utils').$coroutine;
var InvalidParameters = require('./api-errors').InvalidParameters;
var MethodNotFound = require('./api-errors').MethodNotFound;
var Unauthorized = require('./api-errors').Unauthorized;
var wait = require('./fibers-utils').$wait;
var wrap = require('./utils').wrap;
let userId = this.session.get('user_id', undefined);
//====================================================================
if (userId === undefined) {
throw new Unauthorized();
}
function $deprecated(fn)
{
return function (session, req) {
console.warn(req.method +' is deprecated!');
if (!permission) {
return;
}
return fn.apply(this, arguments);
};
}
let user = yield this.users.first(userId);
//====================================================================
// TODO: Helper functions that could be written:
// - checkParams(req.params, param1, ..., paramN)
var helpers = {};
helpers.checkPermission = function (permission)
{
// TODO: Handle token permission.
var userId = this.session.get('user_id', undefined);
if (undefined === userId)
{
throw new Unauthorized();
}
if (!permission)
{
return;
}
var user = wait(this.users.first(userId));
// The user MUST exist at this time.
if (!user.hasPermission(permission))
{
throw new Unauthorized();
}
};
// Checks and returns parameters.
helpers.getParams = function (schema) {
var params = this.request.params;
schema = {
type: 'object',
properties: schema,
};
var result = schemaInspector.validate(schema, params);
if (!result.valid)
{
throw new InvalidParameters(result.error);
}
return params;
};
helpers.getUserPublicProperties = function (user) {
// Handles both properties and wrapped models.
var properties = user.properties || user;
return pick(properties, 'id', 'email', 'permission');
};
helpers.getServerPublicProperties = function (server) {
// Handles both properties and wrapped models.
var properties = server.properties || server;
return pick(properties, 'id', 'host', 'username');
};
// Deprecated!
var errorClasses = {
ALREADY_AUTHENTICATED: apiErrors.AlreadyAuthenticated,
INVALID_CREDENTIAL: apiErrors.InvalidCredential,
INVALID_PARAMS: apiErrors.InvalidParameters,
NO_SUCH_OBJECT: apiErrors.NoSuchObject,
NOT_IMPLEMENTED: apiErrors.NotImplementd,
};
helpers.throw = function (errorId, data) {
throw new (errorClasses[errorId])(data);
};
//====================================================================
function Api(xo)
{
if ( !(this instanceof Api) )
{
return new Api(xo);
}
this.xo = xo;
}
var execHelper = coroutine(function (session, request) {
var ctx = Object.create(this.xo);
assign(ctx, helpers, {
session: session,
request: request,
});
var method = this.getMethod(request.method);
if (!method)
{
console.warn('Invalid method: '+ request.method);
throw new MethodNotFound(request.method);
}
if ('permission' in method)
{
helpers.checkPermission.call(ctx, method.permission);
}
if (method.params)
{
helpers.getParams.call(ctx, method.params);
}
return method.call(ctx, request.params);
if (!user.hasPermission(permission)) {
throw new Unauthorized();
}
});
Api.prototype.exec = function (session, request) {
var method = request.method;
debug('%s(...)', method);
return Bluebird.try(execHelper, [session, request], this).then(
function (result) {
// If nothing was returned, consider this operation a success
// and return true.
if (result === undefined) {
result = true;
}
debug('%s(...) → %s', method, typeof result);
return result;
},
function (error) {
debug('Error: %s(...) → %s', method, error);
throw error;
}
);
};
Api.prototype.getMethod = function (name) {
var parts = name.split('.');
var current = Api.fn;
for (
var i = 0, n = parts.length;
(i < n) && (current = current[parts[i]]);
++i
)
{
/* jshint noempty:false */
}
// 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 catchAll;
for (i = 0; (i < n) && (current = current[parts[i]]); ++i)
{
catchAll = current.__catchAll || catchAll;
}
return catchAll;
};
module.exports = Api;
//====================================================================
var $register = function (path, fn, params) {
var component, current;
if (params)
{
fn.params = params;
}
if (!isArray(path))
{
path = path.split('.');
}
current = Api.fn;
for (var i = 0, n = path.length - 1; i < n; ++i)
{
component = path[i];
current = (current[component] || (current[component] = {}));
}
if (isFunction(fn))
{
current[path[n]] = fn;
}
else if (isObject(fn) && !isArray(fn))
{
// If it is not an function but an object, copies its
// properties.
component = path[n];
current = (current[component] || (current[component] = {}));
assign(current, fn);
}
else
{
current[path[n]] = wrap(fn);
}
};
Api.fn = requireTree('./api');
//--------------------------------------------------------------------
$register('system.getVersion', wrap('0.1'));
function checkParams(method, params) {
var schema = method.params;
if (!schema) {
return;
}
$register('xo.getAllObjects', function () {
return this.getObjects();
});
let result = schemaInspector.validate({
type: 'object',
properties: schema,
}, params);
// Returns the list of available methods similar to XML-RPC
// introspection (http://xmlrpc-c.sourceforge.net/introspection.html).
(function () {
var methods = {};
if (!result.valid) {
throw new InvalidParameters(result.error);
}
}
(function browse(container, path) {
var n = path.length;
forEach(container, function (content, key) {
path[n] = key;
if (isFunction(content))
{
methods[path.join('.')] = {
description: content.description,
params: content.params || {},
permission: content.permission,
};
}
else
{
browse(content, path);
}
});
path.pop();
})(Api.fn, []);
//====================================================================
$register('system.listMethods', wrap(keys(methods)));
$register('system.methodSignature', function (params) {
var method = methods[params.name];
function getMethodsInfo() {
let methods = {};
if (!method)
{
this.throw('NO_SUCH_OBJECT');
}
forEach(this.api._methods, function (method, name) {
this[name] = assign({}, {
description: method.description,
params: method.params || {},
permission: method.permission,
});
}, methods);
// XML-RPC can have multiple signatures per method.
return [
// XML-RPC requires the method name.
assign({name: params.name}, method)
];
}, {
name: {
description: 'method to describe',
type: 'string',
},
});
return methods;
}
getMethodsInfo.description = 'returns the signatures of all available API methods';
$register('system.getMethodsInfo', wrap(methods));
})();
//--------------------------------------------------------------------
let getVersion = () => '0.1';
getVersion.description = 'API version (unstable)';
//--------------------------------------------------------------------
function listMethods() {
return getKeys(this.api._methods);
}
listMethods.description = 'returns the name of all available API methods';
//--------------------------------------------------------------------
function methodSignature({method: name}) {
let method = this.api.getMethod(name);
if (!method) {
throw new NoSuchObject();
}
// Return an array for compatibility with XML-RPC.
return [
// XML-RPC require the name of the method.
assign({ name }, {
description: method.description,
params: method.params || {},
permission: method.permission,
})
];
}
methodSignature.description = 'returns the signature of an API method';
//====================================================================
export default class Api {
constructor({context} = {}) {
this._methods = Object.create(null);
this.context = context;
this.addMethods({
system: {
getMethodsInfo,
getVersion,
listMethods,
methodSignature,
}
});
// FIXME: this too is specific to XO and should be moved out of this file.
this.addMethods(requireTree('./api'));
}
addMethod(name, method) {
this._methods[name] = method;
}
addMethods(methods) {
let base = '';
forEach(methods, function addMethod(method, name) {
name = base + name;
if (isFunction(method)) {
this.addMethod(name, method);
return;
}
let oldBase = base;
base = name + '.';
forEach(method, addMethod, this);
base = oldBase;
}, this);
}
call(session, name, params) {
debug('%s(...)', name);
let method;
let context;
return Bluebird.try(() => {
method = this.getMethod(name);
if (!method) {
throw new MethodNotFound(name);
}
context = Object.create(this.context);
context.api = this; // Used by system.*().
context.session = session;
if ('permission' in method) {
return checkPermission.call(context, method.permission);
}
}).then(() => {
checkParams(method, params);
return method.call(context, params);
}).then(
result => {
// If nothing was returned, consider this operation a success
// and return true.
if (result === undefined) {
result = true;
}
debug('%s(...) → %s', name, typeof result);
return result;
},
error => {
debug('Error: %s(...) → %s', name, error);
throw error;
}
);
}
getMethod(name) {
return this._methods[name];
}
}

View File

@ -26,7 +26,7 @@ get.params = {
object: { type: 'string', optional: true },
};
get.description = 'get existing ACLs'
get.description = 'get existing ACLs';
export {get};
@ -56,7 +56,7 @@ let add = coroutine(function *({subject, object}) {
yield this.acls.create(subject, object);
} catch (error) {
if (!(error instanceof ModelAlreadyExists)) {
throw error
throw error;
}
}
});

View File

@ -1,8 +1,8 @@
{$wait} = require '../fibers-utils'
{$coroutine, $wait} = require '../fibers-utils'
#=====================================================================
exports.set = (params) ->
exports.set = $coroutine (params) ->
try
host = @getObject params.id, 'host'
catch
@ -33,7 +33,7 @@ exports.set.params =
type: 'boolean'
optional: true
exports.restart = ({id}) ->
exports.restart = $coroutine ({id}) ->
@checkPermission 'admin'
try
@ -52,7 +52,7 @@ exports.restart.params = {
id: { type: 'string' }
}
exports.restart_agent = ({id}) ->
exports.restart_agent = $coroutine ({id}) ->
try
host = @getObject id, 'host'
catch
@ -68,7 +68,7 @@ exports.restart_agent.params = {
id: { type: 'string' }
}
exports.start = ({id}) ->
exports.start = $coroutine ({id}) ->
try
host = @getObject id, 'host'
catch
@ -84,7 +84,7 @@ exports.restart_agent.params = {
id: { type: 'string' }
}
exports.stop = ({id}) ->
exports.stop = $coroutine ({id}) ->
try
host = @getObject id, 'host'
catch
@ -101,7 +101,7 @@ exports.stop.params = {
id: { type: 'string' }
}
exports.detach = ({id}) ->
exports.detach = $coroutine ({id}) ->
try
host = @getObject id, 'host'
catch
@ -117,7 +117,7 @@ exports.detach.params = {
id: { type: 'string' }
}
exports.enable = ({id}) ->
exports.enable = $coroutine ({id}) ->
try
host = @getObject id, 'host'
catch
@ -133,7 +133,7 @@ exports.stop.params = {
id: { type: 'string' }
}
exports.disable = ({id}) ->
exports.disable = $coroutine ({id}) ->
try
host = @getObject id, 'host'
catch

View File

@ -1,8 +1,8 @@
{$wait} = require '../fibers-utils'
{$coroutine, $wait} = require '../fibers-utils'
#=====================================================================
exports.delete = ({id}) ->
exports.delete = $coroutine ({id}) ->
try
message = @getObject id, 'message'
catch

View File

@ -1,10 +1,10 @@
# FIXME: too low level, should be removed.
{$wait} = require '../fibers-utils'
{$coroutine, $wait} = require '../fibers-utils'
#=====================================================================
exports.delete = ({id}) ->
exports.delete = $coroutine ({id}) ->
try
VBD = @getObject id, 'PBD'
catch
@ -21,7 +21,7 @@ exports.delete.params = {
id: { type: 'string' }
}
exports.disconnect = ({id}) ->
exports.disconnect = $coroutine ({id}) ->
try
PBD = @getObject id, 'PBD'
catch
@ -38,7 +38,7 @@ exports.disconnect.params = {
id: { type: 'string' }
}
exports.connect = ({id}) ->
exports.connect = $coroutine ({id}) ->
try
PBD = @getObject id, 'PBD'
catch

View File

@ -1,8 +1,8 @@
{$wait} = require '../fibers-utils'
{$coroutine, $wait} = require '../fibers-utils'
#=====================================================================
exports.delete = ({id}) ->
exports.delete = $coroutine ({id}) ->
try
PIF = @getObject id, 'PIF'
catch
@ -19,7 +19,7 @@ exports.delete.params = {
id: { type: 'string' }
}
exports.disconnect = ({id}) ->
exports.disconnect = $coroutine ({id}) ->
try
PIF = @getObject id, 'PIF'
catch
@ -36,7 +36,7 @@ exports.disconnect.params = {
id: { type: 'string' }
}
exports.connect = ({id}) ->
exports.connect = $coroutine ({id}) ->
try
PIF = @getObject id, 'PIF'
catch

View File

@ -3,7 +3,7 @@ $debug = (require 'debug') 'xo:api:vm'
#=====================================================================
exports.set = (params) ->
exports.set = $coroutine (params) ->
try
pool = @getObject params.id, 'pool'
catch
@ -32,7 +32,7 @@ exports.set.params =
optional: true
# FIXME
exports.patch = ({pool}) ->
exports.patch = $coroutine ({pool}) ->
try
pool = @getObject pool, 'pool'
catch

View File

@ -1,4 +1,4 @@
{$wait} = require '../fibers-utils'
{$coroutine, $wait} = require '../fibers-utils'
#=====================================================================
@ -6,7 +6,7 @@
# Could we use tokens instead?
# Adds a new server.
exports.add = ({host, username, password}) ->
exports.add = $coroutine ({host, username, password}) ->
server = $wait @servers.add {
host
username
@ -25,7 +25,7 @@ exports.add.params =
type: 'string'
# Removes an existing server.
exports.remove = ({id}) ->
exports.remove = $coroutine ({id}) ->
# Throws an error if the server did not exist.
@throw 'NO_SUCH_OBJECT' unless $wait @servers.remove id
@ -36,7 +36,7 @@ exports.remove.params =
type: 'string'
# Returns all servers.
exports.getAll = ->
exports.getAll = $coroutine ->
# Retrieves the servers.
servers = $wait @servers.get()
@ -48,7 +48,7 @@ exports.getAll = ->
exports.getAll.permission = 'admin'
# Changes the properties of an existing server.
exports.set = ({id, host, username, password}) ->
exports.set = $coroutine ({id, host, username, password}) ->
# Retrieves the server.
server = $wait @servers.first id

View File

@ -1,9 +1,9 @@
{$wait} = require '../fibers-utils'
{$coroutine, $wait} = require '../fibers-utils'
#=====================================================================
# Signs a user in with its email/password.
exports.signInWithPassword = ({email, password}) ->
exports.signInWithPassword = $coroutine ({email, password}) ->
@throw 'ALREADY_AUTHENTICATED' if @session.has 'user_id'
# Gets the user.
@ -24,7 +24,7 @@ exports.signInWithPassword.params = {
}
# Signs a user in with a token.
exports.signInWithToken = ({token}) ->
exports.signInWithToken = $coroutine ({token}) ->
@throw 'ALREADY_AUTHENTICATED' if @session.has 'user_id'
# Gets the token.
@ -43,14 +43,14 @@ exports.signInWithToken.params = {
token: { type: 'string' }
}
exports.signOut = ->
exports.signOut = $coroutine ->
@session.unset 'token_id'
@session.unset 'user_id'
return true
# Gets the the currently signed in user.
exports.getUser = ->
exports.getUser = $coroutine ->
id = @session.get 'user_id', null
# If the user is not signed in, returns null.

View File

@ -1,8 +1,8 @@
{$wait} = require '../fibers-utils'
{$coroutine, $wait} = require '../fibers-utils'
#=====================================================================
exports.set = (params) ->
exports.set = $coroutine (params) ->
try
SR = @getObject params.id, 'SR'
catch
@ -29,7 +29,7 @@ exports.set.params = {
}
exports.scan = ({id}) ->
exports.scan = $coroutine ({id}) ->
try
SR = @getObject id, 'SR'
catch

View File

@ -1,8 +1,8 @@
{$wait} = require '../fibers-utils'
{$coroutine, $wait} = require '../fibers-utils'
#=====================================================================
exports.cancel = ({id}) ->
exports.cancel = $coroutine ({id}) ->
try
task = @getObject id, 'task'
catch

View File

@ -1,11 +1,11 @@
{$wait} = require '../fibers-utils'
{$coroutine, $wait} = require '../fibers-utils'
#=====================================================================
# Creates a new token.
#
# TODO: Token permission.
exports.create = ->
exports.create = $coroutine ->
userId = @session.get 'user_id'
# The user MUST be signed in and not with a token
@ -17,7 +17,7 @@ exports.create = ->
return token.id
# Deletes a token.
exports.delete = ({token: tokenId}) ->
exports.delete = $coroutine ({token: tokenId}) ->
# Gets the token.
token = $wait @tokens.first tokenId
@throw 'NO_SUCH_OBJECT' unless token?

View File

@ -1,9 +1,9 @@
{$wait} = require '../fibers-utils'
{$coroutine, $wait} = require '../fibers-utils'
#=====================================================================
# Creates a new user.
exports.create = ({email, password, permission}) ->
exports.create = $coroutine ({email, password, permission}) ->
# Creates the user.
user = $wait @users.create email, password, permission
@ -18,7 +18,7 @@ exports.create.params = {
# Deletes an existing user.
#
# FIXME: a user should not be able to delete itself.
exports.delete = ({id}) ->
exports.delete = $coroutine ({id}) ->
# The user cannot delete himself.
@throw 'INVALID_PARAMS' if id is @session.get 'user_id'
@ -32,7 +32,7 @@ exports.delete.params = {
}
# Changes the password of the current user.
exports.changePassword = ({old, new: newP}) ->
exports.changePassword = $coroutine ({old, new: newP}) ->
# Gets the current user (which MUST exist).
user = $wait @users.first @session.get 'user_id'
@ -53,7 +53,7 @@ exports.changePassword.params = {
}
# Returns the user with a given identifier.
exports.get = ({id}) ->
exports.get = $coroutine ({id}) ->
# Only an administrator can see another user.
@checkPermission 'admin' unless @session.get 'user_id' is id
@ -69,7 +69,7 @@ exports.get.params = {
}
# Returns all users.
exports.getAll = ->
exports.getAll = $coroutine ->
# Retrieves the users.
users = $wait @users.get()
@ -81,7 +81,7 @@ exports.getAll = ->
exports.getAll.permission = 'admin'
# Changes the properties of an existing user.
exports.set = ({id, email, password, permission}) ->
exports.set = $coroutine ({id, email, password, permission}) ->
# Retrieves the user.
user = $wait @users.first id

View File

@ -1,10 +1,10 @@
# FIXME: too low level, should be removed.
{$wait} = require '../fibers-utils'
{$coroutine, $wait} = require '../fibers-utils'
#=====================================================================
exports.delete = ({id}) ->
exports.delete = $coroutine ({id}) ->
try
VBD = @getObject id, 'VBD'
catch
@ -21,7 +21,7 @@ exports.delete.params = {
id: { type: 'string' }
}
exports.disconnect = ({id}) ->
exports.disconnect = $coroutine ({id}) ->
try
VBD = @getObject id, 'VBD'
catch
@ -38,7 +38,7 @@ exports.disconnect.params = {
id: { type: 'string' }
}
exports.connect = ({id}) ->
exports.connect = $coroutine ({id}) ->
try
VBD = @getObject id, 'VBD'
catch

View File

@ -4,11 +4,11 @@ $isArray = require 'lodash.isarray'
#---------------------------------------------------------------------
{$wait} = require '../fibers-utils'
{$coroutine, $wait} = require '../fibers-utils'
#=====================================================================
exports.delete = ({id}) ->
exports.delete = $coroutine ({id}) ->
try
VDI = @getObject id, 'VDI'
catch
@ -26,7 +26,7 @@ exports.delete.params = {
}
# FIXME: human readable strings should be handled.
exports.set = (params) ->
exports.set = $coroutine (params) ->
try
VDI = @getObject params.id
catch
@ -72,7 +72,7 @@ exports.set.params = {
size: { type: 'integer', optional: true }
}
exports.migrate = ({id, sr_id}) ->
exports.migrate = $coroutine ({id, sr_id}) ->
try
VDI = @getObject id, 'VDI'
SR = @getObject sr_id, 'SR'

View File

@ -1,8 +1,8 @@
{$wait} = require '../fibers-utils'
{$coroutine, $wait} = require '../fibers-utils'
#=====================================================================
exports.delete = ({id}) ->
exports.delete = $coroutine ({id}) ->
try
VIF = @getObject id, 'VIF'
catch
@ -19,7 +19,7 @@ exports.delete.params = {
id: { type: 'string' }
}
exports.disconnect = ({id}) ->
exports.disconnect = $coroutine ({id}) ->
try
VIF = @getObject id, 'VIF'
catch
@ -36,7 +36,7 @@ exports.disconnect.params = {
id: { type: 'string' }
}
exports.connect = ({id}) ->
exports.connect = $coroutine ({id}) ->
try
VIF = @getObject id, 'VIF'
catch

View File

@ -31,7 +31,7 @@ $isVMRunning = do ->
#=====================================================================
# FIXME: Make the method as atomic as possible.
exports.create = ({
exports.create = $coroutine ({
installation
name_label
template
@ -275,7 +275,7 @@ exports.delete.params = {
}
}
exports.ejectCd = ({id}) ->
exports.ejectCd = $coroutine ({id}) ->
try
VM = @getObject id, 'VM'
catch
@ -301,7 +301,7 @@ exports.ejectCd.params = {
id: { type: 'string' }
}
exports.insertCd = ({id, cd_id, force}) ->
exports.insertCd = $coroutine ({id, cd_id, force}) ->
try
VM = @getObject id, 'VM'
VDI = @getObject cd_id, 'VDI'
@ -350,7 +350,7 @@ exports.insertCd.params = {
force: { type: 'boolean' }
}
exports.migrate = ({id, host_id}) ->
exports.migrate = $coroutine ({id, host_id}) ->
try
VM = @getObject id, 'VM'
host = @getObject host_id, 'host'
@ -374,7 +374,7 @@ exports.migrate.params = {
host_id: { type: 'string' }
}
exports.migrate_pool = ({
exports.migrate_pool = $coroutine ({
id
target_host_id
target_sr_id
@ -464,7 +464,7 @@ exports.migrate_pool.params = {
}
# FIXME: human readable strings should be handled.
exports.set = (params) ->
exports.set = $coroutine (params) ->
try
VM = @getObject params.id, ['VM', 'VM-snapshot']
catch
@ -559,7 +559,7 @@ exports.set.params = {
memory: { type: 'integer', optional: true }
}
exports.restart = ({id, force}) ->
exports.restart = $coroutine ({id, force}) ->
try
VM = @getObject id, 'VM'
catch
@ -584,7 +584,7 @@ exports.restart.params = {
force: { type: 'boolean' }
}
exports.clone = ({id, name, full_copy}) ->
exports.clone = $coroutine ({id, name, full_copy}) ->
try
VM = @getObject id, 'VM'
catch
@ -605,7 +605,7 @@ exports.clone.params = {
}
# TODO: rename convertToTemplate()
exports.convert = ({id}) ->
exports.convert = $coroutine ({id}) ->
try
VM = @getObject id, 'VM'
catch
@ -620,7 +620,7 @@ exports.convert.params = {
id: { type: 'string' }
}
exports.snapshot = ({id, name}) ->
exports.snapshot = $coroutine ({id, name}) ->
try
VM = @getObject id, 'VM'
catch
@ -635,7 +635,7 @@ exports.snapshot.params = {
name: { type: 'string' }
}
exports.start = ({id}) ->
exports.start = $coroutine ({id}) ->
try
VM = @getObject id, 'VM'
catch
@ -657,7 +657,7 @@ exports.start.params = {
# - if !force clean shutdown
# - if force is true hard shutdown
# - if force is integer clean shutdown and after force seconds, hard shutdown.
exports.stop = ({id, force}) ->
exports.stop = $coroutine ({id, force}) ->
try
VM = @getObject id, 'VM'
catch
@ -687,7 +687,7 @@ exports.stop.params = {
force: { type: 'boolean', optional: true }
}
exports.suspend = ({id}) ->
exports.suspend = $coroutine ({id}) ->
try
VM = @getObject id, 'VM'
catch
@ -703,7 +703,7 @@ exports.suspend.params = {
id: { type: 'string' }
}
exports.resume = ({id, force}) ->
exports.resume = $coroutine ({id, force}) ->
try
VM = @getObject id, 'VM'
catch
@ -724,7 +724,7 @@ exports.resume.params = {
}
# revert a snapshot to its parent VM
exports.revert = ({id}) ->
exports.revert = $coroutine ({id}) ->
try
VM = @getObject id, 'VM-snapshot'
catch
@ -741,7 +741,7 @@ exports.revert.params = {
id: { type: 'string' }
}
exports.export = ({vm, compress}) ->
exports.export = $coroutine ({vm, compress}) ->
compress ?= true
try
VM = @getObject vm, ['VM', 'VM-snapshot']
@ -782,7 +782,7 @@ exports.export = ({vm, compress}) ->
if snapshotRef?
$debug 'deleting temp snapshot...'
exports.delete.call this, id: snapshotRef, delete_disks: true
$wait exports.delete.call this, id: snapshotRef, delete_disks: true
return
@ -809,7 +809,7 @@ exports.export.params = {
# FIXME
# TODO: "sr_id" can be passed in URL to target a specific SR
exports.import = ({host}) ->
exports.import = $coroutine ({host}) ->
try
host = @getObject host, 'host'
catch
@ -841,7 +841,7 @@ exports.import.params = {
#
# FIXME: if position is used, all other disks after this position
# should be shifted.
exports.attachDisk = ({vm, vdi, position, mode, bootable}) ->
exports.attachDisk = $coroutine ({vm, vdi, position, mode, bootable}) ->
try
VM = @getObject vm, 'VM'
VDI = @getObject vdi, 'VDI'
@ -886,7 +886,7 @@ exports.attachDisk.params = {
#
# FIXME: disk should be created using disk.create() and then attached
# via vm.attachDisk().
exports.addDisk = ({vm, name, size, sr, position, bootable}) ->
exports.addDisk = $coroutine ({vm, name, size, sr, position, bootable}) ->
try
VM = @getObject vm, 'VM'
SR = @getObject sr, 'SR'

1
src/api/xo.js Normal file
View File

@ -0,0 +1 @@
export var getAllObjects = () => this.getObjects();

View File

@ -1,22 +1,32 @@
import debug from 'debug';
debug = debug('xo:main');
import Bluebird, {coroutine} from 'bluebird';
import Bluebird from 'bluebird';
Bluebird.longStackTraces();
import appConf from 'app-conf';
import assign from 'lodash.assign';
import bind from 'lodash.bind';
import createConnectApp from 'connect';
import eventToPromise from 'event-to-promise';
import {readFile} from 'fs-promise';
import forEach from 'lodash.foreach';
import has from 'lodash.has';
import isArray from 'lodash.isarray';
import {createServer as createJsonRpcServer} from 'json-rpc';
import pick from 'lodash.pick';
import serveStatic from 'serve-static';
import WebSocket from 'ws';
import {
AlreadyAuthenticated,
InvalidCredential,
InvalidParameters,
NoSuchObject,
NotImplementd,
} from './api-errors';
import {coroutine} from 'bluebird';
import {createServer as createJsonRpcServer} from 'json-rpc';
import {readFile} from 'fs-promise';
import API from './api';
import Api from './api';
import WebServer from 'http-server-plus';
import wsProxy from './ws-proxy';
import XO from './xo';
@ -25,11 +35,11 @@ import XO from './xo';
let info = (...args) => {
console.info('[Info]', ...args);
}
};
let warn = (...args) => {
console.warn('[Warn]', ...args);
}
};
//====================================================================
@ -45,7 +55,7 @@ const DEFAULTS = {
const DEPRECATED_ENTRIES = [
'users',
'servers',
]
];
let loadConfiguration = coroutine(function *() {
let config = yield appConf.load('xo-server', {
@ -114,14 +124,47 @@ let setUpStaticFiles = (connect, opts) => {
debug('Setting up %s → %s', url, path);
connect.use(url, serveStatic(path));
})
});
});
};
//====================================================================
let errorClasses = {
ALREADY_AUTHENTICATED: AlreadyAuthenticated,
INVALID_CREDENTIAL: InvalidCredential,
INVALID_PARAMS: InvalidParameters,
NO_SUCH_OBJECT: NoSuchObject,
NOT_IMPLEMENTED: NotImplementd,
};
let apiHelpers = {
getUserPublicProperties(user) {
// Handles both properties and wrapped models.
let properties = user.properties || user;
return pick(properties, 'id', 'email', 'permission');
},
getServerPublicProperties(server) {
// Handles both properties and wrapped models.
let properties = server.properties || server;
return pick(properties, 'id', 'host', 'username');
},
throw(errorId, data) {
throw new (errorClasses[errorId])(data);
}
};
let setUpApi = (webServer, xo) => {
let api = new API(xo);
let context = Object.create(xo);
assign(xo, apiHelpers);
let api = new Api({
context,
});
let webSocketServer = new WebSocket.Server({
server: webServer,
@ -136,7 +179,7 @@ let setUpApi = (webServer, xo) => {
// Create the JSON-RPC server for this connection.
let jsonRpc = createJsonRpcServer(message => {
if (message.type === 'request') {
return api.exec(xoConnection, message);
return api.call(xoConnection, message.method, message.params);
}
});
@ -172,7 +215,7 @@ let setUpApi = (webServer, xo) => {
//====================================================================
let getVmConsoleUrl = (xo, id) => {
let vm = xo.getObject(id, ['VM', 'VM-controller'])
let vm = xo.getObject(id, ['VM', 'VM-controller']);
if (!vm || vm.power_state !== 'Running') {
return;
}
@ -198,12 +241,12 @@ let setUpConsoleProxy = (webServer, xo) => {
});
webServer.on('upgrade', (req, res, head) => {
let matches = CONSOLE_PROXY_PATH_RE.exec(req.url)
let matches = CONSOLE_PROXY_PATH_RE.exec(req.url);
if (!matches) {
return;
}
let url = getVmConsoleUrl(xo, matches[1])
let url = getVmConsoleUrl(xo, matches[1]);
if (!url) {
return;
}