Use standard style.

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import {coroutine, wait} from '../fibers-utils'; import {coroutine, wait} from '../fibers-utils'
import {parseSize} from '../utils'; import {parseSize} from '../utils'
//==================================================================== // ===================================================================
export const create = coroutine(function ({name, size, sr}) { 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', { const ref = wait(xapi.call('VDI.create', {
name_label: name, name_label: name,
@ -13,20 +13,20 @@ export const create = coroutine(function ({name, size, sr}) {
sharable: false, sharable: false,
SR: sr.ref, SR: sr.ref,
type: 'user', 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 = { create.params = {
name: { type: 'string' }, name: { type: 'string' },
size: { type: 'string' }, size: { type: 'string' },
sr: { type: 'string' }, sr: { type: 'string' }
}; }
create.resolve = { create.resolve = {
sr: ['sr', 'SR'], sr: ['sr', 'SR']
}; }

View File

@ -1,64 +1,63 @@
import {deprecate} from 'util'; import {deprecate} from 'util'
import {InvalidCredential, AlreadyAuthenticated} from '../api-errors'; import {InvalidCredential, AlreadyAuthenticated} from '../api-errors'
import {coroutine, wait} from '../fibers-utils'; import {coroutine, wait} from '../fibers-utils'
//==================================================================== // ===================================================================
export const signIn = coroutine(function (credentials) { export const signIn = coroutine(function (credentials) {
if (this.session.has('user_id')) { 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) { 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 = { signInWithPassword.params = {
email: { type: 'string' }, email: { type: 'string' },
password: { 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');
} }
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. // This method requires the user to be signed in.
signOut.permission = ''; signOut.permission = ''
//-------------------------------------------------------------------- // -------------------------------------------------------------------
export const getUser = coroutine(function () { export const getUser = coroutine(function () {
const userId = this.session.get('user_id'); const userId = this.session.get('user_id')
return userId === undefined ? return userId === undefined ?
null : null :
this.getUserPublicProperties(wait(this.users.first(userId))) this.getUserPublicProperties(wait(this.users.first(userId)))
; })
});
getUser.description = 'return the currently connected user'; getUser.description = 'return the currently connected user'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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