Use standard style.

This commit is contained in:
Julien Fontanet 2015-04-13 18:32:45 +02:00
parent b4a3b832dc
commit 0eec1c1f61
18 changed files with 1386 additions and 1396 deletions

View File

@ -1,7 +1,7 @@
#!/usr/bin/env node
'use strict';
'use strict'
//====================================================================
// ===================================================================
require('exec-promise')(require('../'));
require('exec-promise')(require('../'))

View File

@ -1,4 +1,4 @@
'use strict';
'use strict'
// ===================================================================
@ -12,18 +12,18 @@ var watch = require('gulp-watch')
// ===================================================================
var SRC_DIR = __dirname + '/src';
var DIST_DIR = __dirname + '/dist';
var SRC_DIR = __dirname + '/src'
var DIST_DIR = __dirname + '/dist'
var PRODUCTION = process.argv.indexOf('--production') !== -1
// ===================================================================
function src(patterns) {
function src (patterns) {
return PRODUCTION ?
gulp.src(patterns, {
base: SRC_DIR,
cwd: SRC_DIR,
cwd: SRC_DIR
}) :
watch(patterns, {
base: SRC_DIR,
@ -36,7 +36,7 @@ function src(patterns) {
// ===================================================================
gulp.task(function buildCoffee() {
gulp.task(function buildCoffee () {
return src('**/*.coffee')
.pipe(sourceMaps.init())
.pipe(coffee({
@ -46,7 +46,7 @@ gulp.task(function buildCoffee() {
.pipe(gulp.dest(DIST_DIR))
})
gulp.task(function buildEs6() {
gulp.task(function buildEs6 () {
return src('**/*.js')
.pipe(sourceMaps.init())
.pipe(babel({

View File

@ -91,13 +91,19 @@
"in-publish": "^1.1.1",
"mocha": "^2.2.1",
"node-inspector": "^0.9.2",
"sinon": "^1.14.1"
"sinon": "^1.14.1",
"standard": "*"
},
"scripts": {
"build": "gulp build --production",
"dev": "gulp build",
"prepublish": "in-publish && npm run build || in-install",
"start": "node bin/xo-server",
"test": "mocha 'dist/**/*.spec.js'"
"test": "standard && mocha 'dist/**/*.spec.js'"
},
"standard": {
"ignore": [
"dist/**"
]
}
}

View File

@ -217,7 +217,6 @@ class $MappedCollection extends $EventEmitter
removeWithPredicate: (predicate, thisArg) ->
items = ($filter @_byKey, predicate, thisArg)
console.log('%s items to remove', items.length)
@_removeItems items
set: (items, {add, update, remove} = {}) ->

View File

@ -1,54 +1,51 @@
'use strict';
import assign from 'lodash.assign'
import {JsonRpcError} from 'json-rpc/errors'
//====================================================================
var assign = require('lodash.assign');
var JsonRpcError = require('json-rpc/errors').JsonRpcError;
var jsonRpcErrors = require('json-rpc/errors');
var makeError = require('make-error');
//====================================================================
function exportError(constructor) {
makeError(constructor, JsonRpcError);
exports[constructor.name] = constructor;
}
//====================================================================
// ===================================================================
// Export standard JSON-RPC errors.
assign(exports, jsonRpcErrors);
export * from 'json-rpc/errors'
//--------------------------------------------------------------------
// -------------------------------------------------------------------
exportError(function NotImplemented() {
NotImplemented.super.call(this, 'not implemented', 0);
});
export class NotImplemented extends JsonRpcError {
constructor () {
super('not implemented', 0)
}
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
exportError(function NoSuchObject() {
NoSuchObject.super.call(this, 'no such object', 1);
});
export class NoSuchObject extends JsonRpcError {
constructor () {
super(this, 'no such object', 1)
}
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
exportError(function Unauthorized() {
Unauthorized.super.call(
this,
'not authenticated or not enough permissions',
2
);
});
export class Unauthorized extends JsonRpcError {
constructor () {
super(
this,
'not authenticated or not enough permissions',
2
)
}
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
exportError(function InvalidCredential() {
InvalidCredential.super.call(this, 'invalid credential', 3);
});
export class InvalidCredential extends JsonRpcError {
constructor () {
super(this, 'invalid credential', 3)
}
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
exportError(function AlreadyAuthenticated() {
AlreadyAuthenticated.super.call(this, 'already authenticated', 4);
});
export class AlreadyAuthenticated extends JsonRpcError {
constructor () {
super(this, 'already authenticated', 4)
}
}

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import {coroutine, wait} from '../fibers-utils';
import {parseSize} from '../utils';
import {coroutine, wait} from '../fibers-utils'
import {parseSize} from '../utils'
//====================================================================
// ===================================================================
export const create = coroutine(function ({name, size, sr}) {
const xapi = this.getXAPI(sr);
const xapi = this.getXAPI(sr)
const ref = wait(xapi.call('VDI.create', {
name_label: name,
@ -13,20 +13,20 @@ export const create = coroutine(function ({name, size, sr}) {
sharable: false,
SR: sr.ref,
type: 'user',
virtual_size: String(parseSize(size)),
}));
virtual_size: String(parseSize(size))
}))
return wait(xapi.call('VDI.get_record', ref)).uuid;
});
return wait(xapi.call('VDI.get_record', ref)).uuid
})
create.description = 'create a new disk on a SR';
create.description = 'create a new disk on a SR'
create.params = {
name: { type: 'string' },
size: { type: 'string' },
sr: { type: 'string' },
};
sr: { type: 'string' }
}
create.resolve = {
sr: ['sr', 'SR'],
};
sr: ['sr', 'SR']
}

View File

@ -1,64 +1,63 @@
import {deprecate} from 'util';
import {deprecate} from 'util'
import {InvalidCredential, AlreadyAuthenticated} from '../api-errors';
import {InvalidCredential, AlreadyAuthenticated} from '../api-errors'
import {coroutine, wait} from '../fibers-utils';
import {coroutine, wait} from '../fibers-utils'
//====================================================================
// ===================================================================
export const signIn = coroutine(function (credentials) {
if (this.session.has('user_id')) {
throw new AlreadyAuthenticated();
throw new AlreadyAuthenticated()
}
const user = wait(this.authenticateUser(credentials));
const user = wait(this.authenticateUser(credentials))
if (!user) {
throw new InvalidCredential();
throw new InvalidCredential()
}
this.session.set('user_id', user.get('id'));
this.session.set('user_id', user.get('id'))
return this.getUserPublicProperties(user);
});
return this.getUserPublicProperties(user)
})
signIn.description = 'sign in';
signIn.description = 'sign in'
//--------------------------------------------------------------------
// -------------------------------------------------------------------
export const signInWithPassword = deprecate(signIn, 'use session.signIn() instead');
export const signInWithPassword = deprecate(signIn, 'use session.signIn() instead')
signInWithPassword.params = {
email: { type: 'string' },
password: { type: 'string' },
};
//--------------------------------------------------------------------
export const signInWithToken = deprecate(signIn, 'use session.signIn() instead');
signInWithToken.params = {
token: { type: 'string' },
};
//--------------------------------------------------------------------
export function signOut() {
this.session.unset('user_id');
password: { type: 'string' }
}
signOut.description = 'sign out the user from the current session';
// -------------------------------------------------------------------
export const signInWithToken = deprecate(signIn, 'use session.signIn() instead')
signInWithToken.params = {
token: { type: 'string' }
}
// -------------------------------------------------------------------
export function signOut () {
this.session.unset('user_id')
}
signOut.description = 'sign out the user from the current session'
// This method requires the user to be signed in.
signOut.permission = '';
signOut.permission = ''
//--------------------------------------------------------------------
// -------------------------------------------------------------------
export const getUser = coroutine(function () {
const userId = this.session.get('user_id');
const userId = this.session.get('user_id')
return userId === undefined ?
null :
this.getUserPublicProperties(wait(this.users.first(userId)))
;
});
})
getUser.description = 'return the currently connected user';
getUser.description = 'return the currently connected user'

View File

@ -1,93 +1,93 @@
import forEach from 'lodash.foreach';
import {coroutine, wait} from '../fibers-utils';
import {ensureArray, parseXml} from '../utils';
import forEach from 'lodash.foreach'
import {coroutine, wait} from '../fibers-utils'
import {ensureArray, parseXml} from '../utils'
//====================================================================
// ===================================================================
export const set = coroutine(function (params) {
const {SR} = params;
const xapi = this.getXAPI();
const {SR} = params
const xapi = this.getXAPI()
forEach(['name_label', 'name_description'], param => {
const value = params[param];
const value = params[param]
if (value === undefined) {
return;
return
}
wait(xapi.call(`SR.set_${value}`, SR.ref, params[param]));
});
wait(xapi.call(`SR.set_${value}`, SR.ref, params[param]))
})
return true;
});
return true
})
set.params = {
id: { type: 'string' },
name_label: { type: 'string', optional: true },
name_description: { type: 'string', optional: true },
};
name_description: { type: 'string', optional: true }
}
set.resolve = {
SR: ['id', 'SR'],
};
SR: ['id', 'SR']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
export const scan = coroutine(function ({SR}) {
const xapi = this.getXAPI(SR);
const xapi = this.getXAPI(SR)
wait(xapi.call('SR.scan', SR.ref));
wait(xapi.call('SR.scan', SR.ref))
return true;
});
return true
})
scan.params = {
id: { type: 'string' },
};
id: { type: 'string' }
}
scan.resolve = {
SR: ['id', 'SR'],
};
SR: ['id', 'SR']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// TODO: find a way to call this "deconste" and not destroy
export const destroy = coroutine(function ({SR}) {
const xapi = this.getXAPI(SR);
const xapi = this.getXAPI(SR)
wait(xapi.call('SR.destroy', SR.ref));
wait(xapi.call('SR.destroy', SR.ref))
return true;
});
return true
})
destroy.params = {
id: { type: 'string' },
};
id: { type: 'string' }
}
destroy.resolve = {
SR: ['id', 'SR'],
};
SR: ['id', 'SR']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
export const forget = coroutine(function ({SR}) {
const xapi = this.getXAPI(SR);
const xapi = this.getXAPI(SR)
wait(xapi.call('SR.forget', SR.ref));
wait(xapi.call('SR.forget', SR.ref))
return true;
});
return true
})
forget.params = {
id: { type: 'string' },
};
id: { type: 'string' }
}
forget.resolve = {
SR: ['id', 'SR'],
};
SR: ['id', 'SR']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
export const createIso = coroutine(function ({
host,
@ -95,14 +95,14 @@ export const createIso = coroutine(function ({
nameDescription,
path
}) {
const xapi = this.getXAPI(host);
const xapi = this.getXAPI(host)
// FIXME: won't work for IPv6
// Detect if NFS or local path for ISO files
const deviceConfig = {location: path};
const deviceConfig = {location: path}
if (path.indexOf(':') === -1) { // not NFS share
// TODO: legacy will be removed in XAPI soon by FileSR
deviceConfig.legacy_mode = 'true';
deviceConfig.legacy_mode = 'true'
}
const srRef = wait(xapi.call(
'SR.create',
@ -115,24 +115,24 @@ export const createIso = coroutine(function ({
'iso', // SR content type ISO
true,
{}
));
))
const sr = wait(xapi.call('SR.get_record', srRef));
return sr.uuid;
});
const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid
})
createIso.params = {
host: { type: 'string' },
nameLabel: { type: 'string' },
nameDescription: { type: 'string' },
path: { type: 'string' }
};
}
createIso.resolve = {
host: ['host', 'host'],
};
host: ['host', 'host']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// NFS SR
// This functions creates a NFS SR
@ -145,16 +145,16 @@ export const createNfs = coroutine(function ({
serverPath,
nfsVersion
}) {
const xapi = this.getXAPI(host);
const xapi = this.getXAPI(host)
const deviceConfig = {
server,
serverpath: serverPath,
};
serverpath: serverPath
}
// if NFS version given
if (nfsVersion) {
deviceConfig.nfsversion = nfsVersion;
deviceConfig.nfsversion = nfsVersion
}
const srRef = wait(xapi.call(
@ -168,11 +168,11 @@ export const createNfs = coroutine(function ({
'user', // recommended by Citrix
true,
{}
));
))
const sr = wait(xapi.call('SR.get_record', srRef));
return sr.uuid;
});
const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid
})
createNfs.params = {
host: { type: 'string' },
@ -180,14 +180,14 @@ createNfs.params = {
nameDescription: { type: 'string' },
server: { type: 'string' },
serverPath: { type: 'string' },
nfsVersion: { type: 'string' , optional: true},
};
nfsVersion: { type: 'string', optional: true }
}
createNfs.resolve = {
host: ['host', 'host'],
};
host: ['host', 'host']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// Local LVM SR
// This functions creates a local LVM SR
@ -198,11 +198,11 @@ export const createLvm = coroutine(function ({
nameDescription,
device
}) {
const xapi = this.getXAPI(host);
const xapi = this.getXAPI(host)
const deviceConfig = {
device
};
}
const srRef = wait(xapi.call(
'SR.create',
@ -215,24 +215,24 @@ export const createLvm = coroutine(function ({
'user', // recommended by Citrix
false,
{}
));
))
const sr = wait(xapi.call('SR.get_record', srRef));
return sr.uuid;
});
const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid
})
createLvm.params = {
host: { type: 'string' },
nameLabel: { type: 'string' },
nameDescription: { type: 'string' },
device: { type: 'string' },
};
device: { type: 'string' }
}
createLvm.resolve = {
host: ['host', 'host'],
};
host: ['host', 'host']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// This function helps to detect all NFS shares (exports) on a NFS server
// Return a table of exports with their paths and ACLs
@ -240,13 +240,13 @@ export const probeNfs = coroutine(function ({
host,
server
}) {
const xapi = this.getXAPI(host);
const xapi = this.getXAPI(host)
const deviceConfig = {
server,
};
server
}
let xml;
let xml
try {
wait(xapi.call(
@ -255,38 +255,38 @@ export const probeNfs = coroutine(function ({
deviceConfig,
'nfs',
{}
));
))
throw new Error('the call above should have thrown an error')
} catch (error) {
if (error[0] !== 'SR_BACKEND_FAILURE_101') {
throw error;
throw error
}
xml = parseXml(error[3]);
xml = parseXml(error[3])
}
const nfsExports = [];
const nfsExports = []
forEach(ensureArray(xml['nfs-exports'].Export), nfsExport => {
nfsExports.push({
path: nfsExport.Path.trim(),
acl: nfsExport.Accesslist.trim()
});
});
})
})
return nfsExports;
});
return nfsExports
})
probeNfs.params = {
host: { type: 'string' },
server: { type: 'string' },
};
server: { type: 'string' }
}
probeNfs.resolve = {
host: ['host', 'host'],
};
host: ['host', 'host']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// ISCSI SR
// This functions creates a iSCSI SR
@ -303,23 +303,23 @@ export const createIscsi = coroutine(function ({
chapUser,
chapPassword
}) {
const xapi = this.getXAPI(host);
const xapi = this.getXAPI(host)
const deviceConfig = {
target,
targetIQN: targetIqn,
SCSIid: scsiId,
};
SCSIid: scsiId
}
// if we give user and password
if (chapUser && chapPassword) {
deviceConfig.chapUser = chapUser;
deviceConfig.chapPassword = chapPassword;
deviceConfig.chapUser = chapUser
deviceConfig.chapPassword = chapPassword
}
// if we give another port than default iSCSI
if (port) {
deviceConfig.port = port;
deviceConfig.port = port
}
const srRef = wait(xapi.call(
@ -333,57 +333,57 @@ export const createIscsi = coroutine(function ({
'user', // recommended by Citrix
true,
{}
));
))
const sr = wait(xapi.call('SR.get_record', srRef));
return sr.uuid;
});
const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid
})
createIscsi.params = {
host: { type: 'string' },
nameLabel: { type: 'string' },
nameDescription: { type: 'string' },
target: { type: 'string' },
port: { type: 'integer' , optional: true},
port: { type: 'integer', optional: true},
targetIqn: { type: 'string' },
scsiId: { type: 'string' },
chapUser: { type: 'string' , optional: true },
chapPassword: { type: 'string' , optional: true },
};
chapUser: { type: 'string', optional: true },
chapPassword: { type: 'string', optional: true }
}
createIscsi.resolve = {
host: ['host', 'host'],
};
host: ['host', 'host']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// This function helps to detect all iSCSI IQN on a Target (iSCSI "server")
// Return a table of IQN or empty table if no iSCSI connection to the target
export const probeIscsiIqns = coroutine(function ({
host,
target:targetIp,
target: targetIp,
port,
chapUser,
chapPassword
}) {
const xapi = this.getXAPI(host);
const xapi = this.getXAPI(host)
const deviceConfig = {
target: targetIp,
};
target: targetIp
}
// if we give user and password
if (chapUser && chapPassword) {
deviceConfig.chapUser = chapUser;
deviceConfig.chapPassword = chapPassword;
deviceConfig.chapUser = chapUser
deviceConfig.chapPassword = chapPassword
}
// if we give another port than default iSCSI
if (port) {
deviceConfig.port = port;
deviceConfig.port = port
}
let xml;
let xml
try {
wait(xapi.call(
@ -392,76 +392,76 @@ export const probeIscsiIqns = coroutine(function ({
deviceConfig,
'lvmoiscsi',
{}
));
))
throw new Error('the call above should have thrown an error')
} catch (error) {
if (error[0] === 'SR_BACKEND_FAILURE_141') {
return [];
return []
}
if (error[0] !== 'SR_BACKEND_FAILURE_96') {
throw error;
throw error
}
xml = parseXml(error[3]);
xml = parseXml(error[3])
}
const targets = [];
const targets = []
forEach(ensureArray(xml['iscsi-target-iqns'].TGT), target => {
// if the target is on another IP adress, do not display it
if (target.IPAddress.trim() === targetIp) {
targets.push({
iqn: target.TargetIQN.trim(),
ip: target.IPAddress.trim()
});
})
}
});
})
return targets;
});
return targets
})
probeIscsiIqns.params = {
host: { type: 'string' },
target: { type: 'string' },
port: { type: 'integer', optional: true },
chapUser: { type: 'string' , optional: true },
chapPassword: { type: 'string' , optional: true },
};
chapUser: { type: 'string', optional: true },
chapPassword: { type: 'string', optional: true }
}
probeIscsiIqns.resolve = {
host: ['host', 'host'],
};
host: ['host', 'host']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// This function helps to detect all iSCSI ID and LUNs on a Target
// It will return a LUN table
export const probeIscsiLuns = coroutine(function ({
host,
target:targetIp,
target: targetIp,
port,
targetIqn,
chapUser,
chapPassword
}) {
const xapi = this.getXAPI(host);
const xapi = this.getXAPI(host)
const deviceConfig = {
target: targetIp,
targetIQN: targetIqn,
};
targetIQN: targetIqn
}
// if we give user and password
if (chapUser && chapPassword) {
deviceConfig.chapUser = chapUser;
deviceConfig.chapPassword = chapPassword;
deviceConfig.chapUser = chapUser
deviceConfig.chapPassword = chapPassword
}
// if we give another port than default iSCSI
if (port) {
deviceConfig.port = port;
deviceConfig.port = port
}
let xml;
let xml
try {
wait(xapi.call(
@ -470,18 +470,18 @@ export const probeIscsiLuns = coroutine(function ({
deviceConfig,
'lvmoiscsi',
{}
));
))
throw new Error('the call above should have thrown an error')
} catch (error) {
if (error[0] !== 'SR_BACKEND_FAILURE_107') {
throw error;
throw error
}
xml = parseXml(error[3]);
xml = parseXml(error[3])
}
const luns = [];
const luns = []
forEach(ensureArray(xml['iscsi-target'].LUN), lun => {
luns.push({
id: lun.LUNid.trim(),
@ -489,67 +489,67 @@ export const probeIscsiLuns = coroutine(function ({
serial: lun.serial.trim(),
size: lun.size.trim(),
scsiId: lun.SCSIid.trim()
});
});
})
})
return luns;
});
return luns
})
probeIscsiLuns.params = {
host: { type: 'string' },
target: { type: 'string' },
port: { type: 'integer' , optional: true},
port: { type: 'integer', optional: true},
targetIqn: { type: 'string' },
chapUser: { type: 'string' , optional: true },
chapPassword: { type: 'string' , optional: true },
};
chapUser: { type: 'string', optional: true },
chapPassword: { type: 'string', optional: true }
}
probeIscsiLuns.resolve = {
host: ['host', 'host'],
};
host: ['host', 'host']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// This function helps to detect if this target already exists in XAPI
// It returns a table of SR UUID, empty if no existing connections
export const probeIscsiExists = coroutine(function ({
host,
target:targetIp,
target: targetIp,
port,
targetIqn,
scsiId,
chapUser,
chapPassword
}) {
const xapi = this.getXAPI(host);
const xapi = this.getXAPI(host)
const deviceConfig = {
target: targetIp,
targetIQN: targetIqn,
SCSIid: scsiId,
};
SCSIid: scsiId
}
// if we give user and password
if (chapUser && chapPassword) {
deviceConfig.chapUser = chapUser;
deviceConfig.chapPassword = chapPassword;
deviceConfig.chapUser = chapUser
deviceConfig.chapPassword = chapPassword
}
// if we give another port than default iSCSI
if (port) {
deviceConfig.port = port;
deviceConfig.port = port
}
const xml = parseXml(wait(xapi.call('SR.probe', host.ref, deviceConfig, 'lvmoiscsi', {})));
const xml = parseXml(wait(xapi.call('SR.probe', host.ref, deviceConfig, 'lvmoiscsi', {})))
const srs = [];
const srs = []
forEach(ensureArray(xml['SRlist'].SR), sr => {
// get the UUID of SR connected to this LUN
srs.push({uuid: sr.UUID.trim()});
});
srs.push({uuid: sr.UUID.trim()})
})
return srs;
});
return srs
})
probeIscsiExists.params = {
host: { type: 'string' },
@ -557,15 +557,15 @@ probeIscsiExists.params = {
port: { type: 'integer', optional: true },
targetIqn: { type: 'string' },
scsiId: { type: 'string' },
chapUser: { type: 'string' , optional: true },
chapPassword: { type: 'string' , optional: true },
};
chapUser: { type: 'string', optional: true },
chapPassword: { type: 'string', optional: true }
}
probeIscsiExists.resolve = {
host: ['host', 'host'],
};
host: ['host', 'host']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// This function helps to detect if this NFS SR already exists in XAPI
// It returns a table of SR UUID, empty if no existing connections
@ -574,36 +574,36 @@ export const probeNfsExists = coroutine(function ({
server,
serverPath,
}) {
const xapi = this.getXAPI(host);
const xapi = this.getXAPI(host)
const deviceConfig = {
server,
serverpath: serverPath,
};
serverpath: serverPath
}
const xml = parseXml(wait(xapi.call('SR.probe', host.ref, deviceConfig, 'nfs', {})));
const xml = parseXml(wait(xapi.call('SR.probe', host.ref, deviceConfig, 'nfs', {})))
const srs = [];
const srs = []
forEach(ensureArray(xml['SRlist'].SR), sr => {
// get the UUID of SR connected to this LUN
srs.push({uuid: sr.UUID.trim()});
});
srs.push({uuid: sr.UUID.trim()})
})
return srs;
});
return srs
})
probeNfsExists.params = {
host: { type: 'string' },
server: { type: 'string' },
serverPath: { type: 'string' },
};
serverPath: { type: 'string' }
}
probeNfsExists.resolve = {
host: ['host', 'host'],
};
host: ['host', 'host']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// This function helps to reattach a forgotten NFS/iSCSI SR
export const reattach = coroutine(function ({
@ -613,10 +613,10 @@ export const reattach = coroutine(function ({
nameDescription,
type,
}) {
const xapi = this.getXAPI(host);
const xapi = this.getXAPI(host)
if (type === 'iscsi') {
type = 'lvmoiscsi'; // the internal XAPI name
type = 'lvmoiscsi' // the internal XAPI name
}
const srRef = wait(xapi.call(
@ -628,25 +628,25 @@ export const reattach = coroutine(function ({
'user',
true,
{}
));
))
const sr = wait(xapi.call('SR.get_record', srRef));
return sr.uuid;
});
const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid
})
reattach.params = {
host: { type: 'string' },
uuid: { type: 'string' },
nameLabel: { type: 'string' },
nameDescription: { type: 'string' },
type: { type: 'string' },
};
type: { type: 'string' }
}
reattach.resolve = {
host: ['host', 'host'],
};
host: ['host', 'host']
}
//--------------------------------------------------------------------
// -------------------------------------------------------------------
// This function helps to reattach a forgotten ISO SR
export const reattachIso = coroutine(function ({
@ -656,10 +656,10 @@ export const reattachIso = coroutine(function ({
nameDescription,
type,
}) {
const xapi = this.getXAPI(host);
const xapi = this.getXAPI(host)
if (type === 'iscsi') {
type = 'lvmoiscsi'; // the internal XAPI name
type = 'lvmoiscsi' // the internal XAPI name
}
const srRef = wait(xapi.call(
@ -671,20 +671,20 @@ export const reattachIso = coroutine(function ({
'iso',
true,
{}
));
))
const sr = wait(xapi.call('SR.get_record', srRef));
return sr.uuid;
});
const sr = wait(xapi.call('SR.get_record', srRef))
return sr.uuid
})
reattachIso.params = {
host: { type: 'string' },
uuid: { type: 'string' },
nameLabel: { type: 'string' },
nameDescription: { type: 'string' },
type: { type: 'string' },
};
type: { type: 'string' }
}
reattachIso.resolve = {
host: ['host', 'host'],
};
host: ['host', 'host']
}

View File

@ -1,175 +1,173 @@
import Bluebird from 'bluebird';
import isArray from 'lodash.isarray';
import isObject from 'lodash.isobject';
import makeError from 'make-error';
import Model from './model';
import {EventEmitter} from 'events';
import {mapInPlace} from './utils';
import Bluebird from 'bluebird'
import isArray from 'lodash.isarray'
import isObject from 'lodash.isobject'
import Model from './model'
import {BaseError} from 'make-error'
import {EventEmitter} from 'events'
import {mapInPlace} from './utils'
//====================================================================
// ===================================================================
function ModelAlreadyExists(id) {
ModelAlreadyExists.super.call(this, 'this model already exists: ' + id);
export class ModelAlreadyExists {
constructor (id) {
super('this model already exists: ' + id)
}
}
makeError(ModelAlreadyExists);
export {ModelAlreadyExists};
//====================================================================
// ===================================================================
export default class Collection extends EventEmitter {
// Default value for Model.
get Model() {
return Model;
get Model () {
return Model
}
// Make this property writable.
set Model(Model) {
set Model (Model) {
Object.defineProperty(this, 'Model', {
configurable: true,
enumerale: true,
value: Model,
writable: true,
});
writable: true
})
}
constructor() {
super();
constructor () {
super()
}
add(models, opts) {
let array = isArray(models);
add (models, opts) {
const array = isArray(models)
if (!array) {
models = [models];
models = [models]
}
let {Model} = this;
const {Model} = this
mapInPlace(models, model => {
if (!(model instanceof Model)) {
model = new Model(model);
model = new Model(model)
}
let error = model.validate();
const error = model.validate()
if (error) {
// TODO: Better system inspired by Backbone.js
throw error;
throw error
}
return model.properties;
});
return model.properties
})
return Bluebird.try(this._add, [models, opts], this).then(models => {
this.emit('add', models);
this.emit('add', models)
return array ? models : new this.Model(models[0]);
});
return array ? models : new this.Model(models[0])
})
}
first(properties) {
first (properties) {
if (!isObject(properties)) {
properties = (properties !== undefined) ?
{ id: properties } :
{}
;
}
return Bluebird.try(this._first, [properties], this).then(
model => model && new this.Model(model)
);
)
}
get(properties) {
get (properties) {
if (!isObject(properties)) {
properties = (properties !== undefined) ?
{ id: properties } :
{}
;
}
return Bluebird.try(this._get, [properties], this);
return Bluebird.try(this._get, [properties], this)
}
remove(ids) {
remove (ids) {
if (!isArray(ids)) {
ids = [ids];
ids = [ids]
}
return Bluebird.try(this._remove, [ids], this).then(() => {
this.emit('remove', ids);
return true;
});
this.emit('remove', ids)
return true
})
}
update(models) {
var array = isArray(models);
update (models) {
var array = isArray(models)
if (!isArray(models)) {
models = [models];
models = [models]
}
let {Model} = this;
const {Model} = this
mapInPlace(models, model => {
if (!(model instanceof Model)) {
// TODO: Problems, we may be mixing in some default
// properties which will overwrite existing ones.
model = new Model(model);
model = new Model(model)
}
let id = model.get('id');
const id = model.get('id')
// Missing models should be added not updated.
if (id === undefined){
if (id === undefined) {
// FIXME: should not throw an exception but return a rejected promise.
throw new Error('a model without an id cannot be updated');
throw new Error('a model without an id cannot be updated')
}
var error = model.validate();
var error = model.validate()
if (error !== undefined) {
// TODO: Better system inspired by Backbone.js.
throw error;
throw error
}
return model.properties;
});
return model.properties
})
return Bluebird.try(this._update, [models], this).then(models => {
this.emit('update', models);
this.emit('update', models)
return array ? models : new this.Model(models[0]);
});
return array ? models : new this.Model(models[0])
})
}
// Methods to override in implementations.
_add() {
throw new Error('not implemented');
_add () {
throw new Error('not implemented')
}
_get() {
throw new Error('not implemented');
_get () {
throw new Error('not implemented')
}
_remove() {
throw new Error('not implemented');
_remove () {
throw new Error('not implemented')
}
_update() {
throw new Error('not implemented');
_update () {
throw new Error('not implemented')
}
// Methods which may be overridden in implementations.
count(properties) {
return this.get(properties).get('count');
count (properties) {
return this.get(properties).get('count')
}
exists(properties) {
exists (properties) {
/* jshint eqnull: true */
return this.first(properties).then(model => model != null);
return this.first(properties).then(model => model != null)
}
_first(properties) {
_first (properties) {
return Bluebird.try(this.get, [properties], this).then(
models => models.length ? models[0] : null
);
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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