diff --git a/bin/xo-server b/bin/xo-server index 1367570be..83435ccfe 100755 --- a/bin/xo-server +++ b/bin/xo-server @@ -1,7 +1,7 @@ #!/usr/bin/env node -'use strict'; +'use strict' -//==================================================================== +// =================================================================== -require('exec-promise')(require('../')); +require('exec-promise')(require('../')) diff --git a/gulpfile.js b/gulpfile.js index 68b7dda0b..2575d63f0 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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({ diff --git a/package.json b/package.json index 83a0fcbdc..8a51345de 100644 --- a/package.json +++ b/package.json @@ -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/**" + ] } } diff --git a/src/MappedCollection.coffee b/src/MappedCollection.coffee index ba5bdeea7..e4c8126e9 100644 --- a/src/MappedCollection.coffee +++ b/src/MappedCollection.coffee @@ -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} = {}) -> diff --git a/src/api-errors.js b/src/api-errors.js index d74f50931..9860902e7 100644 --- a/src/api-errors.js +++ b/src/api-errors.js @@ -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) + } +} diff --git a/src/api.js b/src/api.js index b831e4d01..5177025ec 100644 --- a/src/api.js +++ b/src/api.js @@ -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] } } diff --git a/src/api/acl.js b/src/api/acl.js index e6b84d200..d4a4a7f91 100644 --- a/src/api/acl.js +++ b/src/api/acl.js @@ -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' diff --git a/src/api/disk.js b/src/api/disk.js index 59d7de1e3..fcce29387 100644 --- a/src/api/disk.js +++ b/src/api/disk.js @@ -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'] +} diff --git a/src/api/session.js b/src/api/session.js index 7cc52beda..2b39732aa 100644 --- a/src/api/session.js +++ b/src/api/session.js @@ -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' diff --git a/src/api/sr.js b/src/api/sr.js index 897f002bc..77097b47a 100644 --- a/src/api/sr.js +++ b/src/api/sr.js @@ -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'] +} diff --git a/src/collection.js b/src/collection.js index 1f763efca..66e67c8f1 100644 --- a/src/collection.js +++ b/src/collection.js @@ -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 - ); + ) } } diff --git a/src/collection/redis.js b/src/collection/redis.js index 52529ce08..47b89f44f 100644 --- a/src/collection/redis.js +++ b/src/collection/redis.js @@ -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 }) } } diff --git a/src/connection.js b/src/connection.js index 46e618c2b..d271c7750 100644 --- a/src/connection.js +++ b/src/connection.js @@ -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 diff --git a/src/fibers-utils.spec.js b/src/fibers-utils.spec.js index a6273d9c4..0a2970705 100644 --- a/src/fibers-utils.spec.js +++ b/src/fibers-utils.spec.js @@ -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() + })() + }) +}) diff --git a/src/index.js b/src/index.js index 438088492..ea4a4f352 100644 --- a/src/index.js +++ b/src/index.js @@ -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 diff --git a/src/model.js b/src/model.js index 7a88044f4..d4d62e407 100644 --- a/src/model.js +++ b/src/model.js @@ -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) + }) } } } diff --git a/src/utils.js b/src/utils.js index ca3485199..d9a4c523b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -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 diff --git a/src/ws-proxy.js b/src/ws-proxy.js index 1f4e54d40..557460876 100644 --- a/src/ws-proxy.js +++ b/src/ws-proxy.js @@ -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) + } + }) + }) }