Merge branch 'spec-refactoring' into next-release

This commit is contained in:
Julien Fontanet 2014-01-16 16:57:50 +01:00
commit 05c45e1b6b
17 changed files with 2280 additions and 1097 deletions

View File

@ -11,7 +11,7 @@
"type": "git",
"url": "git://github.com/vatesfr/xo-server.git"
},
"main": "src/main.js",
"main": "src/main.coffee",
"dependencies": {
"coffee-script": "~1.6.3",
"connect": "~2.11.2",
@ -29,15 +29,30 @@
"underscore": "~1.5.2",
"validator": "~2.0.0",
"ws": "~0.4.31",
"xml2js": "~0.4.1",
"xmlrpc": "~1.1.1"
},
"devDependencies": {
"chai": "~1.8.1",
"glob": "~3.2.8",
"mocha": "~1.14.0",
"node-inspector": "~0.6.1"
"mocha-as-promised": "~2.0.0",
"node-inspector": "~0.6.1",
"sinon": "~1.7.3"
},
"optionalDependencies": {},
"engines": {
"node": "*"
}
},
"description": "XO-Server is part of Xen-Orchestra, a web interface for XenServer or XAPI enabled hosts.",
"bugs": {
"url": "https://github.com/vatesfr/xo-server/issues"
},
"directories": {
"test": "tests"
},
"scripts": {
"test": "./run-tests"
},
"license": "AGPL3"
}

32
run-tests Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env coffee
# Tests runner.
$mocha = require 'mocha'
# Promises support for Mocha.
(require 'mocha-as-promised')()
# Used to find the specification files.
$glob = require 'glob'
#=====================================================================
do ->
# Instantiates the tests runner.
mocha = new $mocha {
reporter: 'spec'
}
# Processes arguments.
do ->
{argv} = process
i = 2
n = argv.length
mocha.grep argv[i++] while i < n
$glob 'src/**/*.spec.{coffee,js}', (error, files) ->
console.error(error) if error
mocha.addFile file for file in files
mocha.run()

View File

@ -1,387 +1,492 @@
# Low level tools.
{EventEmitter: $EventEmitter} = require 'events'
#----------------------------------------------------------------------
$_ = require 'underscore'
#=====================================================================
class $DynamicProperty
# TODO: move these helpers in a dedicated module.
constructor: (@value, @hooks) ->
$done = {}
# Similar to `$_.each()` but can be interrupted by returning the
# special value `done` provided as the forth argument.
$each = (col, iterator, ctx) ->
# The default context is inherited.
ctx ?= this
if (n = col.length)?
# Array-like object.
i = 0
while i < n and (iterator.call ctx, col[i], "#{i}", col, $done) isnt $done
++i
else
for key of col
break if (iterator.call ctx, col[key], key, $done) is $done
# For performance.
undefined
$makeFunction = (val) -> -> val
# Similar to `$_.map()` for array and `$_.mapValues()` for objects.
#
# Note: can be interrupted by returning the special value `done`
# provided as the forth argument.
$map = (col, iterator, ctx) ->
# The default context is inherited.
ctx ?= this
if (n = col.length)?
result = []
# Array-like object.
i = 0
while i < n
value = iterator.call ctx, col[i], "#{i}", col, $done
break if value is $done
result.push value
++i
else
result = {}
for key of col
value = iterator.call ctx, col[key], key, $done
break if value is $done
result.push value
# The new collection is returned.
result
# Similar to `$map()` but change the current collection.
#
# Note: can be interrupted by returning the special value `done`
# provided as the forth argument.
$mapInPlace = (col, iterator, ctx) ->
# The default context is inherited.
ctx ?= this
if (n = col.length)?
# Array-like object.
i = 0
while i < n
value = iterator.call ctx, col[i], "#{i}", col, $done
break if value is $done
col[i] = value
++i
else
for key of col
value = iterator.call ctx, col[key], key, $done
break if value is $done
col[key] = value
# The collection is returned.
col
#=====================================================================
$noop = ->
class $MappedCollection extends $EventEmitter
$copyDeep = (value) ->
if value instanceof Array
return ($copyDeep item for item in value)
if value instanceof Object
result = {}
result[key] = $copyDeep item for key, item of value
return result
return value
$getDeep = (obj, path) ->
return obj if path.length is 0
current = obj
i = 0
n = path.length - 1
while i < n
current = current[path[i++]]
throw new Error 'invalid path component' if current is undefined
current[path[i]]
$setDeep = (obj, path, value) ->
throw new Error 'invalid path' if path.length is 0
current = obj
i = 0
n = path.length - 1
while i < n
current = current[path[i++]]
throw new Error 'invalid path component' if current is undefined
current[path[i]] = value
# @param rule Rule of the current item.
# @param item Current item.
# @param value Value of the generator item.
$computeValue = (collection, rule, item) ->
value = item.generator
# @param parent The parent object of this entry (necessary for
# assignment).
# @param name The name of this entry.
# @param spec Specification for the current entry.
# The dispatch function is called whenever a new item has to be
# processed and returns the name of the rule to use.
#
# @returns The generated value for this entry.
helper = (parent, name, spec) ->
if not $_.isObject spec
parent[name] = spec
else if spec instanceof $DynamicProperty
# If there was no previous value use $DynamicProperty.value,
# otherwise, just keep the previous value.
if parent[name] is undefined
# Helper is re-called for the initial value.
helper parent, name, spec.value
else if $_.isFunction spec
ctx = {collection, rule}
ctx.__proto__ = item # Links to the current item.
parent[name] = spec.call ctx, value, item.key
else if $_.isArray spec
current = parent[name] or= new Array spec.length
for entry, index in spec
helper current, index, entry
else
# It's a plain object.
current = parent[name] or= {}
for key, property of spec
helper current, key, property
# To change the way it is dispatched, just override this it.
dispatch: ->
(@genval and (@genval.rule ? @genval.type)) ? 'unknown'
helper item, 'value', rule.value
# This function is called when an item has been dispatched to a
# missing rule.
#
# The default behavior is to throw an error but you may instead
# choose to create a rule:
#
# collection.missingRule = collection.rule
missingRule: (name) ->
throw new Error "undefined rule “#{name}"
######################################################################
class $MappedCollection
constructor: (spec) ->
# If spec is a function, it is called with various helpers in
# `this`.
if $_.isFunction spec
ctx =
dynamic: (initialValue, hooks) ->
new $DynamicProperty initialValue, hooks
noop: $noop
spec = spec.call ctx
# This key function returns the identifier used to map the
# generator to the generated item.
constructor: ->
# Items are stored here indexed by key.
#
# The default function uses the index if the generator collection
# is an array or the property name if it is an object.
# The prototype of this object is set to `null` to avoid pollution
# from enumerable properties of `Object.prototype` and the
# performance hit of `hasOwnProperty o`.
@_byKey = Object.create null
# Hooks are stored here indexed by moment.
@_hooks = {
beforeDispatch: []
beforeUpdate: []
beforeSave: []
afterRule: []
}
# Rules are stored here indexed by name.
#
# /!\: This entry MUST be overridden in rules for new items.
if spec.key?
@_key = spec.key
throw new Error 'key must be a function' unless $_.isFunction @_key
# The prototype of this object is set to `null` to avoid pollution
# from enumerable properties of `Object.prototype` and to be able
# to use the `name of @_rules` syntax.
@_rules = Object.create null
# Register a hook to run at a given point.
#
# A hook receives as parameter an event object with the following
# properties:
# - `preventDefault()`: prevents the next default action from
# happening;
# - `stopPropagation()`: prevents other hooks from being run.
#
# Note: if a hook throws an exception, `event.stopPropagation()`
# then `event.preventDefault()` will be called and the exception
# will be forwarded.
#
# # Item hook
#
# Valid items related moments are:
# - beforeDispatch: even before the item has been dispatched;
# - beforeUpdate: after the item has been dispatched but before
# updating its value.
# - beforeSave: after the item has been updated.
#
# An item hook is run in the context of the current item.
#
# # Rule hook
#
# Valid rules related moments are:
# - afterRule: just after a new rule has been defined (even
# singleton).
#
# An item hook is run in the context of the current rule.
hook: (name, hook) ->
# Allows a nicer syntax for CoffeeScript.
if $_.isObject name
# Extracts the name and the value from the first property of the
# object.
do ->
object = name
return for own name, hook of object
hooks = @_hooks[name]
@_assert(
hooks?
"invalid hook moment “#{name}"
)
hooks.push hook
# Register a new singleton rule.
#
# See the `rule()` method for more information.
item: (name, definition) ->
# Creates the corresponding rule.
rule = @rule name, definition, true
# Creates the singleton.
item = {
rule: rule.name
key: rule.key() # No context because there is not generator.
val: undefined
}
@_updateItems [item], true
# Register a new rule.
#
# If the definition is a function, it will be run in the context of
# an item-like object with the following properties:
# - `key`: the definition for the key of this item;
# - `val`: the definition for the value of this item.
#
# Warning: The definition function is run only once!
rule: (name, definition, singleton = false) ->
# Allows a nicer syntax for CoffeeScript.
if $_.isObject name
# Extracts the name and the definition from the first property
# of the object.
do ->
object = name
return for own name, definition of object
@_assert(
name not of @_rules
"the rule “#{name}” is already defined"
)
# Extracts the rule definition.
if $_.isFunction definition
ctx = {
name
key: undefined
val: undefined
singleton
}
definition.call ctx
else
@_key = (_, key) -> key
ctx = {
name
key: definition?.key
val: definition?.val
singleton
}
spec.rules or= {}
# Runs the `afterRule` hook and returns if the registration has
# been prevented.
return unless @_runHook 'afterRule', ctx
# Rules are the core of $MappedCollection, they allow to categorize
# objects and to treat them differently.
@_rules = {}
{key, val} = ctx
# Hooks are functions which are run when a item of a given rule
# enters, exists or is updated.
@_hooks = {}
# The default key.
key ?= if singleton then -> name else -> @genkey
# Initially the collection is empty.
@_byKey = {}
# The default value.
val ?= -> @genval
# For performance concerns, items are also categorized by rules.
@_byRule = {}
# Makes sure `key` is a function for uniformity.
key = $makeFunction key unless $_.isFunction key
# Rules are checked for conformity and created in the system.
for name, def of spec.rules
# If it's a function, runs it.
def = def() if $_.isFunction def
# Register the new rule.
@_rules[name] = {
name
key
val
singleton
}
unless $_.isObject def
throw new Error "#{name} definition must be an object"
#--------------------------------
# A rule can extends another (not recursive for now!).
if def.extends?
unless $_.isString def.extends
throw new Error "#{name}.extends must be a string"
get: (keys) ->
if keys is undefined
items = $_.map @_byKey, (item) -> item.val
else
items = $mapInPlace (@_fetchItems keys), (item) -> item.val
if spec.rules[def.extends] is undefined
throw new Error "#{name}.extends must reference a valid rule (#{def.extends})"
if $_.isString keys then items[0] else items
$_.defaults def, spec.rules[def.extends]
getRaw: (keys) ->
if keys is undefined
item for _, item of @_byKey
else
items = @_fetchItems keys
rule = {name}
if $_.isString keys then items[0] else items
if def.key?
# Static rule, used to create a new item (without generator).
remove: (keys) ->
@_removeItems (@_fetchItems keys)
if def.test?
throw new Error "both #{name}.key and #{name}.test cannot be defined"
set: (items, {add, update, remove} = {}) ->
add = true unless add?
update = true unless update?
remove = false unless remove?
unless $_.isString def.key
throw new Error "#{name}.key must be a string"
itemsToAdd = {}
itemsToUpdate = {}
rule.key = if $_.isFunction def.key then def.key() else def.key
else if def.test?
# Dynamic rule, used to create new items from generator items.
itemsToRemove = {}
$_.extend itemsToRemove, @_byKey if remove
unless $_.isFunction def.test
throw new Error "#{name}.test must be a function"
$each items, (genval, genkey) =>
item = {
rule: undefined
key: undefined
val: undefined
genkey
genval
}
rule.test = def.test
else
# Invalid rule!
throw new Error "#{name} must have either a key or a test entry"
return unless @_runHook 'beforeDispatch', item
# A rule must have a value.
throw new Error "#{name}.value must be defined" unless def.value?
rule.value = def.value
rule.private = !!def.private
@_rules[name] = rule
@_hooks[name] =
enter: []
update: []
exit: []
@_byRule[name] = {}
# For each rules, values are browsed and hooks are created when
# necessary (dynamic properties).
for name, rule of @_rules or {}
# Browse the value searching for dynamic properties/entries.
#
# An immediately invoked function is used to easily handle
# recursion.
browse = (value, path) =>
# Unless the value is an object, there is nothing to browse.
return unless $_.isObject value
# If the value is a function, it is a factory which will be
# called later, when an item will be created.
return if $_.isFunction value
# If the value is a dynamic property, grabs the initial value
# and registers its hooks.
if value instanceof $DynamicProperty
hooks = value.hooks
# Browse hooks for each rules.
for name_, hooks_ of hooks
# Wraps a hook.
#
# A hook is run with a defined environment
wrap = (hook, rule, items) =>
# Last two parameters are here to be protected from the
# environment.
# FIXME: @_rules[name] and @_byRule are not defined at
# this point.
rule = @_rules[name]
items = @_byRule[name]
(value, key) ->
# The current hook is runs for all items of the
# current rule.
for _, item of items
# Value of the current field.
field = $getDeep item.value, path
ctx = {rule, field}
ctx.__proto__ = item # Links to the current item.
hook.call ctx, value, key
# Updates the value if it changed.
$setDeep item.value, path, ctx.field if ctx.field isnt field
# Checks each hook is correctly defined.
{enter, update, exit} = hooks_
enter ?= update
@_hooks[name_].enter.push wrap(enter) if enter?
if not update? and exit? and enter?
update = (args...) ->
exit.apply this, args
enter.apply this, args
@_hooks[name_].update.push wrap(update) if update?
@_hooks[name_].exit.push wrap(exit) if exit?
# OPTIMIZE: do not register hooks if they are `noop`.
# FIXME: Hooks must be associated to the rule (because they
# must be run for each object of this type), and to the
# field (because it must be available through @field).
return
# If the value is an array, browse each entry.
if $_.isArray value
for entry, index in value
browse entry, path.concat(index)
return
# The value is an object, browse each property.
for key, property of value
browse property, path.concat(key)
browse rule.value, []
# If it is a static rule, creates its item right now.
if rule.key?
# Adds the item.
item = @_byKey[rule.key] = @_byRule[rule.name][rule.key] =
_ruleName: rule.name
key: rule.key
value: undefined
# Computes the value.
$computeValue this, rule, item
# No events for static items.
get: (key) -> @_byKey[key]?.value
getAll: ->
items = {}
for ruleName, ruleItems of @_byRule
# Searches for a rule to handle it.
ruleName = @dispatch.call item
rule = @_rules[ruleName]
# Items of private rules are not exported.
continue if rule.private
unless rule?
@missingRule ruleName
for key, {value} of ruleItems
items[key] = value
# If `missingRule()` has not created the rule, just keep this
# item.
rule = @_rules[ruleName]
return unless rule?
items
# Checks if this is a singleton.
@_assert(
not rule.singleton
"cannot add items to singleton rule “#{rule.name}"
)
remove: (items) ->
itemsToRemove = {}
$_.each items, (value, key) =>
key = @_key key
item = @_byKey[key]
if item?
itemsToRemove[key] = item
# Computes its key.
key = rule.key.call item
@_remove items
@_assert(
$_.isString key
"the key “#{key}” is not a string"
)
# Adds, updates or removes items from the collections. Items not
# present are added, present are updated, and present in the
# generated collection but not in the generator are removed.
set: (items, {add, update, remove} = {}) ->
add = true if add is undefined
update = true if update is undefined
remove = false if remove is undefined
# Updates known values.
item.rule = rule.name
item.key = key
itemsToRemove = {}
if remove
$_.extend(itemsToRemove, @_byKey)
$_.each items, (value, generatorKey) =>
key = @_key value, generatorKey
# If the item already existed.
if @_byKey[key]?
if key of @_byKey
# Marks this item as not to be removed.
delete itemsToRemove[key] if remove
delete itemsToRemove[key]
if update
item = @_byKey[key]
rule = @_rules[item._ruleName]
# Fetches the existing entry.
prev = @_byKey[key]
# Compute the new value.
item.generator = value
item.generatorKey = generatorKey
$computeValue this, rule, item
# Checks if there is a conflict in rules.
@_assert(
item.rule is prev.rule
"the key “#{key}” cannot be of rule “#{item.rule}”, "
"already used by “#{prev.rule}"
)
# Runs related hooks.
for hook in @_hooks[rule.name]?.update or []
hook item.value, item.key
else if add
# First we have to find to which rule this item belongs to.
rule = do =>
for _, rule of @_rules
ctx = {rule}
return rule if rule.test? and rule.test.call ctx, value, key
# Gets its previous value.
item.val = prev.val
# If no rule has been found, just stops.
return unless rule
# Registers the item to be updated.
itemsToUpdate[key] = item
# Adds the item.
item = @_byKey[key] = @_byRule[rule.name][key] = {
_ruleName: rule.name
key
value: undefined
generator: value
generatorKey
}
# Note: an item will be updated only once per `set()` and
# only the last generator will be used.
else
if add
# Registers the item to be added.
itemsToAdd[key] = item
# Computes the value.
$computeValue this, rule, item
# Adds items.
@_updateItems itemsToAdd, true
# Runs related hooks.
for hook in @_hooks[rule.name]?.enter or []
hook item.value, item.key
# Updates items.
@_updateItems itemsToUpdate
# There are keys inside only if remove is `true`.
@_remove itemsToRemove if remove
# Removes any items not seen (iff `remove` is true).
@_removeItems itemsToRemove
_remove: (items) ->
for {_ruleName: ruleName, value}, key in items
# If there are some hooks registered, runs them.
for hook in @_hooks[ruleName]?.remove or []
hook value, key
# Forces items to update their value.
touch: (keys) ->
@_updateItems (@_fetchItems keys, true)
# Removes effectively the item.
delete @_byKey[key] @_byRule[ruleName][key]
#--------------------------------
_assert: (cond, message) ->
throw new Error message unless cond
# Emits item related event.
_emitEvent: (event, items) ->
byRule = {}
# One per item.
$each items, (item) =>
@emit "key=#{item.key}", event, item
(byRule[item.rule] ?= []).push item
# One per rule.
@emit "rule=#{rule}", event, items for rule, items of byRule
# One for everything.
@emit "any", event, items
_fetchItems: (keys, ignoreMissingItems = false) ->
unless $_.isArray keys
keys = if $_.isObject keys then $_.keys keys else [keys]
items = []
for key in keys
item = @_byKey[key]
if item?
items.push item
else
@_assert(
ignoreMissingItems
"no item with key “#{key}"
)
items
_removeItems: (items) ->
return if $_.isEmpty items
$each items, (item) => delete @_byKey[item.key]
@_emitEvent 'exit', items
# Runs hooks for the moment `name` with the given context and
# returns false if the default action has been prevented.
_runHook: (name, ctx) ->
hooks = @_hooks[name]
# If no hooks, nothing to do.
return true unless hooks? and (n = hooks.length) isnt 0
# Flags controlling the run.
notStopped = true
actionNotPrevented = true
# Creates the event object.
event = {
stopPropagation: -> notStopped = false
# TODO: Should `preventDefault()` imply `stopPropagation()`?
preventDefault: -> actionNotPrevented = false
}
i = 0
while notStopped and i < n
hooks[i++].call ctx, event
# TODO: Is exception handling necessary to have the wanted
# behavior?
return actionNotPrevented
_updateItems: (items, areNew) ->
return if $_.isEmpty items
# An update is similar to an exit followed by an enter.
@_emitEvent 'exit', items unless areNew
$each items, (item) =>
return unless @_runHook 'beforeUpdate', item
{rule: ruleName} = item
# Computes its value.
do =>
# Item is not passed directly to function to avoid direct
# modification.
#
# This is not a true security but better than nothing.
proxy = Object.create item
updateValue = (parent, prop, def) ->
if not $_.isObject def
parent[prop] = def
else if $_.isFunction def
parent[prop] = def.call proxy, parent[prop]
else if $_.isArray def
i = 0
n = def.length
current = parent[prop] ?= new Array n
while i < n
updateValue current, i, def[i]
++i
else
# It's a plain object.
current = parent[prop] ?= {}
for i of def
updateValue current, i, def[i]
updateValue item, 'val', @_rules[ruleName].val
return unless @_runHook 'beforeSave', item
# Registers the new item.
@_byKey[item.key] = item
@_emitEvent 'enter', items
# TODO: checks for loops.
#=====================================================================
module.exports = $MappedCollection
module.exports = {$MappedCollection}

View File

@ -0,0 +1,85 @@
{expect: $expect} = require 'chai'
$sinon = require 'sinon'
#---------------------------------------------------------------------
{$MappedCollection} = require './MappedCollection.coffee'
#=====================================================================
describe '$MappedCollection', ->
# Shared variables.
collection = null
beforeEach ->
collection = new $MappedCollection()
#-------------------------------------------------------------------
describe '#dispatch()', ->
# Test data.
beforeEach ->
collection.rule test: {}
#------------------------------
it 'should have genkey and genval', ->
collection.dispatch = ->
$expect(@genkey).to.equal 'a key'
$expect(@genval).to.equal 'a value'
'test'
collection.set {
'a key': 'a value'
}
#------------------------------
it 'should be used to dispatch an item', ->
collection.dispatch = -> 'test'
collection.set [
'any value'
]
$expect(collection.getRaw('0').rule).to.equal 'test'
#-------------------------------------------------------------------
describe 'item hooks', ->
# Test data.
beforeEach ->
collection.rule test: {}
#------------------------------
it 'should be called in the correct order', ->
beforeDispatch = $sinon.spy()
collection.hook {beforeDispatch}
dispatcher = $sinon.spy ->
$expect(beforeDispatch.called).to.true
# It still is a dispatcher.
'test'
collection.dispatch = dispatcher
beforeUpdate = $sinon.spy ->
$expect(dispatcher.called).to.true
collection.hook {beforeUpdate}
beforeSave = $sinon.spy ->
$expect(beforeUpdate.called).to.true
collection.hook {beforeSave}
collection.set [
'any value'
]
$expect(beforeSave.called).to.be.true

View File

@ -4,7 +4,7 @@ var $requireTree = require('require-tree');
//--------------------------------------------------------------------
var $waitPromise = require('./fibers-utils').$waitPromise;
var $wait = require('./fibers-utils').$wait;
//////////////////////////////////////////////////////////////////////
@ -144,7 +144,7 @@ Api.prototype.checkPermission = function (session, permission)
return;
}
var user = $waitPromise(this.xo.users.first(user_id));
var user = $wait(this.xo.users.first(user_id));
// The user MUST exist at this time.
if (!user.hasPermission(permission))
@ -221,7 +221,7 @@ Api.fn = $requireTree('./api');
$register('api.getVersion', '0.1');
$register('xo.getAllObjects', function () {
return this.xo.xobjs.getAll();
return this.xo.xobjs.get();
});
// Returns the list of available methods similar to XML-RPC

View File

@ -1,4 +1,4 @@
{$waitPromise} = require '../fibers-utils'
{$wait} = require '../fibers-utils'
#=====================================================================
@ -14,7 +14,7 @@ exports.add = (session, request) ->
@checkPermission session, 'admin'
# Adds the server.
server = $waitPromise @xo.servers.add {
server = $wait @xo.servers.add {
host
username
password
@ -32,7 +32,7 @@ exports.remove = (session, request) ->
@checkPermission session, 'admin'
# Throws an error if the server did not exist.
@throw 'NO_SUCH_OBJECT' unless $waitPromise @xo.servers.remove id
@throw 'NO_SUCH_OBJECT' unless $wait @xo.servers.remove id
# Returns true.
true
@ -43,7 +43,7 @@ exports.getAll = (session) ->
@checkPermission session, 'admin'
# Retrieves the servers.
servers = $waitPromise @xo.servers.get()
servers = $wait @xo.servers.get()
# Filters out private properties.
for server, i in servers
@ -61,7 +61,7 @@ exports.set = (session, request) ->
@checkPermission session, 'admin'
# Retrieves the server.
server = $waitPromise @xo.servers.first id
server = $wait @xo.servers.first id
# Throws an error if it did not exist.
@throw 'NO_SUCH_OBJECT' unless server
@ -72,7 +72,7 @@ exports.set = (session, request) ->
server.set {password} if password?
# Updates the server.
$waitPromise @xo.servers.update server
$wait @xo.servers.update server
# Returns true.
true

View File

@ -1,4 +1,4 @@
{$waitPromise} = require '../fibers-utils'
{$wait} = require '../fibers-utils'
#=====================================================================
@ -10,7 +10,7 @@ exports.signInWithPassword = (session, req) ->
@throw 'ALREADY_AUTHENTICATED' if session.has 'user_id'
# Gets the user.
user = $waitPromise @xo.users.first {email: email}
user = $wait @xo.users.first {email: email}
# Invalid credentials if the user does not exists or if the password
# does not check.
@ -30,7 +30,7 @@ exports.signInWithToken = (session, req) ->
@throw 'ALREADY_AUTHENTICATED' if session.has('user_id')
# Gets the token.
token = $waitPromise @xo.tokens.first token
token = $wait @xo.tokens.first token
@throw 'INVALID_CREDENTIAL' unless token?
# Stores the user and the token identifiers in the session.
@ -39,7 +39,7 @@ exports.signInWithToken = (session, req) ->
session.set 'user_id', user_id
# Returns the user.
user = $waitPromise @xo.users.first user_id
user = $wait @xo.users.first user_id
@getUserPublicProperties user
# Gets the the currently signed in user.
@ -50,5 +50,5 @@ exports.getUser = (session) ->
return null unless id?
# Returns the user.
user = $waitPromise @xo.users.first id
user = $wait @xo.users.first id
@getUserPublicProperties user

View File

@ -1,4 +1,4 @@
{$waitPromise} = require '../fibers-utils'
{$wait} = require '../fibers-utils'
#=====================================================================
@ -12,7 +12,7 @@ exports.create = (session) ->
@throw 'UNAUTHORIZED' if not userId? or session.has 'token_id'
# Creates the token.
token = $waitPromise @xo.tokens.generate userId
token = $wait @xo.tokens.generate userId
# Returns its identifier.
token.id
@ -23,11 +23,11 @@ exports.delete = (session, req) ->
@throw 'INVALID_PARAMS' unless token?
# Gets the token.
token = $waitPromise @xo.tokens.first tokenId
token = $wait @xo.tokens.first tokenId
@throw 'NO_SUCH_OBJECT' unless token?
# Deletes the token.
$waitPromise @xo.tokens.remove tokenId
$wait @xo.tokens.remove tokenId
# Returns true.
true

View File

@ -1,4 +1,4 @@
{$waitPromise} = require '../fibers-utils'
{$wait} = require '../fibers-utils'
#=====================================================================
@ -11,7 +11,7 @@ exports.create = (session, request) ->
@checkPermission session, 'admin'
# Creates the user.
user = $waitPromise @xo.users.create email, password, permission
user = $wait @xo.users.create email, password, permission
# Returns the identifier of the new user.
user.id
@ -27,7 +27,7 @@ exports.delete = (session, request) ->
@checkPermission session, 'admin'
# Throws an error if the user did not exist.
@throw 'NO_SUCH_OBJECT' unless $waitPromise @xo.users.remove id
@throw 'NO_SUCH_OBJECT' unless $wait @xo.users.remove id
# Returns true.
true
@ -41,7 +41,7 @@ exports.changePassword = (session, request) ->
@checkPermission session
# Gets the current user (which MUST exist).
user = $waitPromise @xo.users.first session.get 'user_id'
user = $wait @xo.users.first session.get 'user_id'
# Checks its old password.
@throw 'INVALID_CREDENTIAL' unless user.checkPassword old
@ -50,7 +50,7 @@ exports.changePassword = (session, request) ->
user.setPassword newP
# Updates the user.
$waitPromise @xo.users.update user
$wait @xo.users.update user
# Returns true.
true
@ -64,7 +64,7 @@ exports.get = (session, request) ->
@checkPermission session, 'admin' unless session.get 'user_id' is id
# Retrieves the user.
user = $waitPromise @xo.users.first id
user = $wait @xo.users.first id
# Throws an error if it did not exist.
@throw 'NO_SUCH_OBJECT' unless user
@ -78,7 +78,7 @@ exports.getAll = (session) ->
@checkPermission session, 'admin'
# Retrieves the users.
users = $waitPromise @xo.users.get()
users = $wait @xo.users.get()
# Filters out private properties.
for user, i in users
@ -96,7 +96,7 @@ exports.set = (session, request) ->
@checkPermission session, 'admin'
# Retrieves the user.
user = $waitPromise @xo.users.first id
user = $wait @xo.users.first id
# Throws an error if it did not exist.
@throw 'NO_SUCH_OBJECT' unless user
@ -107,7 +107,7 @@ exports.set = (session, request) ->
user.setPassword password if password?
# Updates the user.
$waitPromise @xo.users.update user
$wait @xo.users.update user
# Returns true.
true

View File

@ -4,6 +4,8 @@ $_ = require 'underscore'
# Async code is easier with fibers (light threads)!
$fiber = require 'fibers'
$Q = require 'q'
#=====================================================================
$isPromise = (obj) -> obj? and $_.isFunction obj.then
@ -20,7 +22,7 @@ $fiberize = (fn) ->
# TODO: should we keep it?
$promisify = (fn) ->
(args...) ->
deferred = Q.defer()
deferred = $Q.defer()
$fiber(=>
try
@ -43,6 +45,7 @@ $synchronize = (fn, ctx) ->
(args...) ->
fiber = $fiber.current
throw new Error 'not running in a fiber' unless fiber?
args.push (error, result) ->
if error?
@ -58,6 +61,7 @@ $synchronize = (fn, ctx) ->
# Note: if the *error* event is emitted, this function will throw.
$waitEvent = (emitter, event) ->
fiber = $fiber.current
throw new Error 'not running in a fiber' unless fiber?
errorHandler = null
handler = (args...) ->
@ -72,17 +76,28 @@ $waitEvent = (emitter, event) ->
$fiber.yield()
# Waits for a promise to be fulfilled or broken.
$waitPromise = (promise) ->
# If it is not a promise, just forwards it.
return promise unless $isPromise promise
# Waits for a promise or a thunk to end.
$wait = (value) ->
fiber = $fiber.current
throw new Error 'not running in a fiber' unless fiber?
promise.then(
(result) -> fiber.run result
(error) -> fiber.throwInto error
)
if $isPromise value
value.then(
(result) -> fiber.run result
(error) -> fiber.throwInto error
)
else if $_.isFunction value
# It should be a thunk.
value (error, result) ->
if error?
fiber.throwInto error
else
fibre.run result
else
# TODO: handle array and object of promises/thunks.
# No idea what is it, just forwards.
return value
$fiber.yield()
@ -90,8 +105,9 @@ $waitPromise = (promise) ->
module.exports = {
$fiberize
$promisify
$sleep
$synchronize
$waitEvent
$waitPromise
$wait
}

353
src/helpers.coffee Normal file
View File

@ -0,0 +1,353 @@
$_ = require 'underscore'
# FIXME: This file name should reflect what's inside!
#=====================================================================
$asArray = (val) -> if $_.isArray val then val else [val]
$asFunction = (val) -> if $_.isFunction val then val else -> val
$each = $_.each
$first = (collection, def) ->
if (n = collection.length)?
return collection[0] unless n is 0
else
return value for own _, value of collection
# Nothing was found, returns the `def` value.
def
$removeValue = (array, value) ->
index = array.indexOf value
return false if index is -1
array.splice index, 1
true
#---------------------------------------------------------------------
# TODO: currently the watch can be updated multiple times per
# $MappedCollection.set() which is inefficient: it should be
# possible to address that.
$watch = (collection, {
# Key(s) of the remote objects watched.
#
# If it is a function, it is evaluated in the scope of the current
# object. (TODO)
#
# Default: undefined
keys
# Alias for `keys`.
key
# Rule(s) of the remote objects watched.
#
# If it is a function, it is evaluated in the scope of the current
# object. (TODO)
#
# Note: `key`/`keys` and `rule`/`rules` cannot be used both.
#
# Default: undefined
rules
# Alias for `rules`.
rule
# Value to add to the set.
#
# If it is a function, it is evaluated in the scope of the remote
# object.
#
# Default: -> @val
val
# Predicates the remote object must fulfill to be used.
#
# Default: -> true
if: cond
# Function evaluated in the scope of the remote object which
# returns the key of the object to update (usually the current one).
#
# TODO: Does it make sense to return an array?
#
# Default: undefined
bind
# Initial value.
init
# Function called when a loop is detected.
#
# Usually it is used to either throw an exception or do nothing to
# stop the loop.
#
# Note: The function may also returns `true` to force the processing
# to continue.
#
# Default: -> throw new Error 'loop detected'
loopDetected
}, fn) ->
val = if val is undefined
# The default value is simply the value of the item.
-> @val
else
$asFunction val
loopDetected ?= -> throw new Error 'loop detected'
# Method allowing the cleanup when the helper is no longer used.
#cleanUp = -> # TODO: noop for now.
# Keys of items using the current helper.
consumers = Object.create null
# Current values.
values = Object.create null
values.common = init
isProcessing = false
process = (event, items) ->
# Values are grouped by namespace.
valuesByNamespace = Object.create null
$each items, (item, key) -> # `key` is a local variable.
return unless not cond? or cond.call item
if bind?
key = bind.call item
# If bind did not return a key, ignores this value.
return unless key?
namespace = "$#{key}"
else
namespace = 'common'
# Computes the current value.
value = val.call item
(valuesByNamespace[namespace] ?= []).push value
# Stops here if no values were computed.
return if do ->
return false for _ of valuesByNamespace
true
if isProcessing
return unless loopDetected() is true
isProcessing = true
# For each namespace.
for namespace, values_ of valuesByNamespace
# Updates the value.
value = values[namespace]
ctx = {
# TODO: test the $_.clone
value: if value is undefined then $_.clone init else value
}
changed = if event is 'enter'
fn.call ctx, values_, {}
else
fn.call ctx, {}, values_
# Notifies watchers unless it is known the value has not
# changed.
unless changed is false
values[namespace] = ctx.value
if namespace is 'common'
collection.touch consumers
else
collection.touch (namespace.substr 1)
isProcessing = false
processOne = (event, item) ->
process event, [item]
# Sets up the watch based on the provided criteria.
#
# TODO: provides a way to clean this when no longer used.
keys = $asArray (keys ? key ? [])
rules = $asArray (rules ? rule ? [])
if not $_.isEmpty keys
# Matching is done on the keys.
throw new Error 'cannot use keys and rules' unless $_.isEmpty rules
$each keys, (key) -> collection.on "key=#{key}", processOne
# Handles existing items.
process 'enter', collection.getRaw keys
else if not $_.isEmpty rules
# Matching is done the rules.
$each rules, (rule) -> collection.on "rule=#{rule}", process
# TODO: Inefficient, is there another way?
rules = do -> # Minor optimization.
tmp = Object.create null
tmp[rule] = true for rule in rules
tmp
$each collection.getRaw(), (item) ->
processOne 'enter', item if item.rule of rules
else
# No matching done.
collection.on 'any', process
# Handles existing items.
process 'enter', collection.getRaw()
# Creates the generator: the function which items will used to
# register to this watcher and to get the current value.
generator = do (key) -> # Declare a local variable.
->
{key} = this
# Register this item has a consumer.
consumers[key] = true
# Returns the value for this item if any or the common value.
namespace = "$#{key}"
if namespace of values
values[namespace]
else
values.common
# Creates a helper to unregister an item from this watcher.
generator.unregister = do (key) -> # Declare a local variable.
->
{key} = this
delete consumers[key]
delete values["$#{key}"]
# Creates a helper to get the value without using an item.
generator.raw = (key) ->
values[if key? then "$#{key}" else 'common']
# Returns the generator.
generator
#=====================================================================
$map = (options) ->
options.init = Object.create null
$watch this, options, (entered, exited) ->
changed = false
$each entered, ([key, value]) =>
unless @value[key] is value
@value[key] = value
changed = true
$each exited, ([key, value]) =>
if key of @value
delete @value[key]
changed = true
changed
#---------------------------------------------------------------------
# Creates a set of value from various items.
$set = (options) ->
# Contrary to other helpers, the default value is the key.
options.val ?= -> @key
options.init = []
$watch this, options, (entered, exited) ->
changed = false
$each entered, (value) =>
if (@value.indexOf value) is -1
@value.push value
changed = true
$each exited, (value) =>
changed = true if $removeValue @value, value
changed
#---------------------------------------------------------------------
$sum = (options) ->
init = options.init ?= 0
add = (a, b) ->
if $_.isArray a
n = a.length
throw new Error 'invalid sum' unless $_.isArray b and b.length is n
i = 0
while i < n
a[i] = add a[i], b[i]
++i
else if $_.isObject a
throw new Error 'invalid sum' unless $_.isObject b
for key of a
a[key] = add a[key], b[key]
else
a += b
a
sub = (a, b) ->
if $_.isArray a
n = a.length
throw new Error 'invalid sum' unless $_.isArray b and b.length is n
i = 0
while i < n
a[i] = sub a[i], b[i]
++i
else if $_.isObject a
throw new Error 'invalid sum' unless $_.isObject b
for key of a
a[key] = sub a[key], b[key]
else
a -= b
a
$watch this, options, (entered, exited) ->
prev = @value
$each entered, (value) => @value = add @value, value
$each exited, (value) => @value = sub @value, value
@value isnt prev
#---------------------------------------------------------------------
# Uses a value from another item.
#
# Important note: Behavior is not specified when binding to multiple
# items.
$val = (options) ->
# The default value.
def = options.default
delete options.default
options.init ?= def
# Should the last value be kept instead of returning to the default
# value when no items are available!
keepLast = !!options.keepLast
delete options.keepLast
$watch this, options, (entered, exited) ->
prev = @value
@value = $first entered, (if keepLast then @value else def)
@value isnt prev
#=====================================================================
module.exports = {
$map
$set
$sum
$val
}

243
src/helpers.spec.coffee Normal file
View File

@ -0,0 +1,243 @@
{expect: $expect} = require 'chai'
$sinon = require 'sinon'
#---------------------------------------------------------------------
{$MappedCollection} = require './MappedCollection.coffee'
$nonBindedHelpers = require './helpers'
#=====================================================================
describe 'Helper', ->
# Shared variables.
collection = $set = $sum = $val = null
beforeEach ->
# Creates the collection.
collection = new $MappedCollection()
# Dispatcher used for tests.
collection.dispatch = -> (@genkey.split '.')[0]
# Missing rules should be automatically created.
collection.missingRule = collection.rule
# # Monkey patch the collection to see all emitted events.
# emit = collection.emit
# collection.emit = (args...) ->
# console.log args...
# emit.call collection, args...
# Binds helpers to this collection.
{$set, $sum, $val} = do ->
helpers = {}
helpers[name] = fn.bind collection for name, fn of $nonBindedHelpers
helpers
#-------------------------------------------------------------------
# All helpers share the same logical code, we need only to test one
# extensively and test the others basically.
#
# $sum was chosen because it is the simplest helper to test.
describe '$sum', ->
it 'with single key', ->
collection.set foo: 1
collection.item sum: ->
@val = $sum {
key: 'foo'
}
$expect(collection.get 'sum').to.equal 1
collection.set foo:2
$expect(collection.get 'sum').to.equal 2
collection.remove 'foo'
$expect(collection.get 'sum').to.equal 0
it 'with multiple keys', ->
collection.set {
foo: 1
bar: 2
}
collection.item sum: ->
@val = $sum {
keys: ['foo', 'bar']
}
$expect(collection.get 'sum').to.equal 3
collection.set bar:3
$expect(collection.get 'sum').to.equal 4
collection.remove 'foo'
$expect(collection.get 'sum').to.equal 3
it 'with dynamic keys', ->
collection.set {
foo: 1
bar: 2
}
collection.rule sum: ->
@val = $sum {
key: -> (@key.split '.')[1]
}
collection.set {
'sum.foo': null
'sum.bar': null
}
$expect(collecter.get 'sum.foo').to.equal 1
$expect(collecter.get 'sum.bar').to.equal 2
collection.remove 'bar'
$expect(collecter.get 'sum.foo').to.equal 1
$expect(collecter.get 'sum.bar').to.equal 0
it 'with single rule', ->
collection.set {
'foo.1': 1
'foo.2': 2
}
collection.item sum: ->
@val = $sum {
rule: 'foo'
}
$expect(collection.get 'sum').to.equal 3
collection.set 'foo.2':3
$expect(collection.get 'sum').to.equal 4
collection.remove 'foo.1'
$expect(collection.get 'sum').to.equal 3
it 'with multiple rules', ->
collection.set {
'foo': 1
'bar.1': 2
'bar.2': 3
}
collection.item sum: ->
@val = $sum {
rules: ['foo', 'bar']
}
$expect(collection.get 'sum').to.equal 6
collection.set 'bar.1':3
$expect(collection.get 'sum').to.equal 7
collection.remove 'bar.2'
$expect(collection.get 'sum').to.equal 4
it 'with bind', ->
collection.set {
'foo': {
sum: 2 # This item will participate to `sum.2`.
val: 1
}
'bar': {
sum: 1 # This item will participate to `sum.1`.
val: 2
}
}
collection.rule sum: ->
@val = $sum {
bind: ->
id = @val.sum
return unless id?
"sum.#{id}"
val: -> @val.val
}
collection.set {
'sum.1': null
'sum.2': null
}
$expect(collection.get 'sum.1').equal 2
$expect(collection.get 'sum.2').equal 1
collection.set {
'foo': {
sum: 1
val: 3
}
}
$expect(collection.get 'sum.1').equal 5
$expect(collection.get 'sum.2').equal 0
collection.remove 'bar'
$expect(collection.get 'sum.1').equal 3
$expect(collection.get 'sum.2').equal 0
it 'with predicate', ->
collection.set {
foo: 1
bar: 2
baz: 3
}
collection.item sum: ->
@val = $sum {
if: -> /^b/.test @rule
}
$expect(collection.get 'sum').equal 5
collection.set foo:4
$expect(collection.get 'sum').equal 5
collection.set bar:5
$expect(collection.get 'sum').equal 8
collection.remove 'baz'
$expect(collection.get 'sum').equal 5
it 'with initial value', ->
collection.set foo: 1
collection.item sum: ->
@val = $sum {
key: 'foo'
init: 2
}
$expect(collection.get 'sum').to.equal 3
collection.set foo:2
$expect(collection.get 'sum').to.equal 4
collection.remove 'foo'
$expect(collection.get 'sum').to.equal 2
# TODO:
# - dynamic keys
# - dynamic rules

View File

@ -25,7 +25,7 @@ $Session = require './session'
$XO = require './xo'
# Helpers for dealing with fibers.
{$fiberize, $waitEvent, $waitPromise} = require './fibers-utils'
{$fiberize, $waitEvent, $wait} = require './fibers-utils'
# HTTP/HTTPS server which can listen on multiple ports.
$WebServer = require './web-server'
@ -171,7 +171,7 @@ do $fiberize ->
socket.send response if socket.readyState is socket.OPEN
# Creates a default user if there is none.
unless $waitPromise xo.users.exists()
unless $wait xo.users.exists()
email = 'admin@admin.net'
password = 'admin' # TODO: Should be generated.
xo.users.create email, password, 'admin'

File diff suppressed because it is too large Load Diff

1
src/spec.spec-data.json Normal file

File diff suppressed because one or more lines are too long

533
src/spec.spec.coffee Normal file
View File

@ -0,0 +1,533 @@
{expect: $expect} = require 'chai'
$sinon = require 'sinon'
#---------------------------------------------------------------------
{$MappedCollection} = require './MappedCollection.coffee'
# Helpers for dealing with fibers.
{$promisify} = require './fibers-utils'
#=====================================================================
describe 'spec', ->
collection = null
before $promisify ->
# Creates the collection.
collection = new $MappedCollection()
# Loads the spec.
(require './spec').call collection
# Skips missing rules.
collection.missingRule = ( -> )
# Loads the mockup data.
collection.set (require './spec.spec-data')
#console.log collection.get()
it 'xo', ->
xo = collection.get '00000000-0000-0000-0000-000000000000'
#console.log xo
$expect(xo).to.be.an 'object'
$expect(xo.type).to.equal 'xo'
$expect(xo.pools).to.have.members [
'OpaqueRef:6462d0b3-8f20-ef76-fddf-002f7af3452e'
]
$expect(xo.$CPUs).to.equal 8
$expect(xo.$vCPUs).to.equal 0 # TODO: 10
$expect(xo.$running_VMs).to.have.members [
'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
'OpaqueRef:46fa4c52-5e93-6cf7-32e3-c51fb4ed106d'
'OpaqueRef:c0fa9288-2a6b-cd8e-b9a8-cc5afc63b386'
'OpaqueRef:be2390b2-cd08-53f5-3fae-b76f6f3725bf'
'OpaqueRef:8f9966ea-38ef-ac4c-b634-81e31ef1e7c1'
'OpaqueRef:646297e5-4fd6-c70d-6365-ef19b9807f64'
'OpaqueRef:1ef43ee8-bc18-6c4f-4919-0e42a3ac6e4b'
]
# TODO
# $expect(xo.memory).to.be.an 'object'
# $expect(xo.memory.usage).to.equal 0
# $expect(xo.memory.size).to.equal 0
it 'pool', ->
pool = collection.get 'OpaqueRef:6462d0b3-8f20-ef76-fddf-002f7af3452e'
#console.log pool
$expect(pool).to.be.an 'object'
$expect(pool.type).to.equal 'pool'
$expect(pool.name_label).to.equal 'Lab Pool'
$expect(pool.name_description).to.equal 'Vates dev pool at our HQ'
$expect(pool.tags).to.have.members []
$expect(pool.SRs).to.have.members [
'OpaqueRef:d6fe49bf-dd48-c929-5aab-b2786a2e7aee'
'OpaqueRef:6637b7d7-9e5c-f331-c7e4-a7f68f77a047'
'OpaqueRef:557155b2-f092-3417-f509-7ee35b1d42da'
]
$expect(pool.HA_enabled).to.be.false
$expect(pool.hosts).to.have.members [
'OpaqueRef:cd0f68c5-5245-5ae8-f0e1-324e2201c692'
'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
]
$expect(pool.master).to.equal 'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
$expect(pool.VMs).to.have.members [
'OpaqueRef:d4fa8fba-ec86-5928-a1bb-dd78b6fb5944'
'OpaqueRef:8491f148-3e78-9c74-ab98-84445c5f2861'
'OpaqueRef:13b9ec24-04ea-ae04-78e6-6ec4b81a8deb'
]
$expect(pool.$running_hosts).to.have.members [
'OpaqueRef:cd0f68c5-5245-5ae8-f0e1-324e2201c692'
'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
]
$expect(pool.$running_VMs).to.have.members [
'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
'OpaqueRef:46fa4c52-5e93-6cf7-32e3-c51fb4ed106d'
'OpaqueRef:c0fa9288-2a6b-cd8e-b9a8-cc5afc63b386'
'OpaqueRef:be2390b2-cd08-53f5-3fae-b76f6f3725bf'
'OpaqueRef:8f9966ea-38ef-ac4c-b634-81e31ef1e7c1'
'OpaqueRef:646297e5-4fd6-c70d-6365-ef19b9807f64'
'OpaqueRef:1ef43ee8-bc18-6c4f-4919-0e42a3ac6e4b'
]
$expect(pool.$VMs).to.have.members [
'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
'OpaqueRef:46fa4c52-5e93-6cf7-32e3-c51fb4ed106d'
'OpaqueRef:d4fa8fba-ec86-5928-a1bb-dd78b6fb5944'
'OpaqueRef:8491f148-3e78-9c74-ab98-84445c5f2861'
'OpaqueRef:13b9ec24-04ea-ae04-78e6-6ec4b81a8deb'
'OpaqueRef:c0fa9288-2a6b-cd8e-b9a8-cc5afc63b386'
'OpaqueRef:be2390b2-cd08-53f5-3fae-b76f6f3725bf'
'OpaqueRef:8f9966ea-38ef-ac4c-b634-81e31ef1e7c1'
'OpaqueRef:646297e5-4fd6-c70d-6365-ef19b9807f64'
'OpaqueRef:1ef43ee8-bc18-6c4f-4919-0e42a3ac6e4b'
]
it 'host', ->
host = collection.get 'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
#console.log host
$expect(host).to.be.an 'object'
$expect(host.type).to.equal 'host'
$expect(host.name_label).to.equal 'lab1'
$expect(host.name_description).to.equal 'Default install of XenServer'
$expect(host.tags).to.have.members []
$expect(host.address).to.equal '192.168.1.1'
$expect(host.controller).to.equal 'OpaqueRef:719e4877-c7ad-68be-6b04-5750c8dcfeed'
# Burk.
$expect(host.CPUs).to.deep.equal {
cpu_count: '4'
socket_count: '1'
vendor: 'GenuineIntel'
speed: '3192.858'
modelname: 'Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz'
family: '6'
model: '58'
stepping: '9'
flags: 'fpu de tsc msr pae mce cx8 apic sep mtrr mca cmov pat clflush acpi mmx fxsr sse sse2 ss ht nx constant_tsc nonstop_tsc aperfmperf pni pclmulqdq vmx est ssse3 sse4_1 sse4_2 x2apic popcnt aes hypervisor ida arat tpr_shadow vnmi flexpriority ept vpid'
features: '77bae3ff-bfebfbff-00000001-28100800'
features_after_reboot: '77bae3ff-bfebfbff-00000001-28100800'
physical_features: '77bae3ff-bfebfbff-00000001-28100800'
maskable: 'full'
}
$expect(host.enabled).to.be.true
$expect(host.hostname).to.equal 'lab1'
$expect(host.iSCSI_name).to.equal 'iqn.2013-07.com.example:83ba9261'
# $expect(host.memory).to.be.an 'object'
# $expect(host.memory.usage).to.equal 0 # TODO
# $expect(host.memory.size).to.equal 0 # TODO
$expect(host.power_state).to.equal 'Running'
$expect(host.SRs).to.have.members [
# TODO
]
$expect(host.VMs).to.have.members [
'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
'OpaqueRef:46fa4c52-5e93-6cf7-32e3-c51fb4ed106d'
]
$expect(host.$PBDs).to.have.members [
'OpaqueRef:ff32de74-138c-9d80-ab58-c631d2aa0e71'
'OpaqueRef:f0f98779-5cf8-cabc-edc3-631a2d63d89c'
'OpaqueRef:b70f8e06-07a8-a5e7-2856-f221c822e9b2'
'OpaqueRef:b641552a-8c92-71b3-c0a2-e4dd3d04c215'
'OpaqueRef:93320534-824f-850a-64a2-bcbfdc2e0927'
'OpaqueRef:0c1d3862-5a38-e4cc-4a46-d8358a622461'
]
$expect(host.$PIFs).to.have.members [
'OpaqueRef:aef57ed4-e4d9-7f72-0376-b781a19bb9d2'
'OpaqueRef:06f53e3d-d8de-d4ed-6359-9e20b4fb0d21'
]
$expect(host.messages).to.have.members [
'OpaqueRef:cb515b9a-ef8c-13d4-88ea-e0d3ee88d22a'
'OpaqueRef:6ba7c244-3b44-2ed2-ec81-4fa13ea82465'
'OpaqueRef:0e3fc97f-45ce-26c3-9435-899be96b35c4'
'OpaqueRef:6ca16f45-6266-6cff-55cd-19a8ef0acf1a'
'OpaqueRef:11452a2a-1ccd-e4df-25d8-ba99bba710db'
'OpaqueRef:9ddc8eb2-969f-ba56-757a-efd482da5ce9'
'OpaqueRef:68c8d0c6-e5a2-8ade-569a-dfc732e7994d'
'OpaqueRef:ddb628ca-24f1-04d2-0b2c-9996aaab59f2'
'OpaqueRef:0e7044a7-542b-4dd9-65bc-cded0e41853a'
'OpaqueRef:ee26daf0-2ff7-734e-438d-9a521aaaa0c5'
'OpaqueRef:40f8459f-1b6b-1625-1284-0f2878c3203d'
'OpaqueRef:739ca434-6dca-b633-0097-b3f3183150a7'
'OpaqueRef:cf655e45-c8c7-bdb9-e56c-5b67d6952f15'
'OpaqueRef:3e33b140-f7e8-7dcc-3475-97dcc2fbfb5b'
'OpaqueRef:8f3e2923-e690-e859-4f9e-a3e711a1e230'
'OpaqueRef:ed7b1960-1ab7-4f47-8ef1-7a7769e09207'
'OpaqueRef:6a0c4183-2f95-661f-9b19-0df0015867ca'
'OpaqueRef:8d04b3fa-e81d-c6ae-d072-bd3a1ea22189'
'OpaqueRef:dada1bd4-d7ed-429f-0a1a-585a3bfbf7e6'
'OpaqueRef:a5648ca1-b37a-0765-9192-ebfb9ff376e8'
'OpaqueRef:78c09b42-ad6f-0e66-0349-80b45264120d'
'OpaqueRef:9c657a2b-560c-2050-014a-20e8cf5bd235'
'OpaqueRef:1d50d25b-41f6-ffd3-5410-0de4fbed8543'
'OpaqueRef:cb515b9a-ef8c-13d4-88ea-e0d3ee88d22a'
'OpaqueRef:6ba7c244-3b44-2ed2-ec81-4fa13ea82465'
'OpaqueRef:0e3fc97f-45ce-26c3-9435-899be96b35c4'
'OpaqueRef:6ca16f45-6266-6cff-55cd-19a8ef0acf1a'
'OpaqueRef:11452a2a-1ccd-e4df-25d8-ba99bba710db'
'OpaqueRef:9ddc8eb2-969f-ba56-757a-efd482da5ce9'
'OpaqueRef:68c8d0c6-e5a2-8ade-569a-dfc732e7994d'
'OpaqueRef:ddb628ca-24f1-04d2-0b2c-9996aaab59f2'
'OpaqueRef:0e7044a7-542b-4dd9-65bc-cded0e41853a'
'OpaqueRef:ee26daf0-2ff7-734e-438d-9a521aaaa0c5'
'OpaqueRef:40f8459f-1b6b-1625-1284-0f2878c3203d'
'OpaqueRef:739ca434-6dca-b633-0097-b3f3183150a7'
'OpaqueRef:cf655e45-c8c7-bdb9-e56c-5b67d6952f15'
'OpaqueRef:3e33b140-f7e8-7dcc-3475-97dcc2fbfb5b'
'OpaqueRef:8f3e2923-e690-e859-4f9e-a3e711a1e230'
'OpaqueRef:ed7b1960-1ab7-4f47-8ef1-7a7769e09207'
'OpaqueRef:6a0c4183-2f95-661f-9b19-0df0015867ca'
'OpaqueRef:8d04b3fa-e81d-c6ae-d072-bd3a1ea22189'
'OpaqueRef:dada1bd4-d7ed-429f-0a1a-585a3bfbf7e6'
'OpaqueRef:a5648ca1-b37a-0765-9192-ebfb9ff376e8'
'OpaqueRef:78c09b42-ad6f-0e66-0349-80b45264120d'
'OpaqueRef:9c657a2b-560c-2050-014a-20e8cf5bd235'
'OpaqueRef:1d50d25b-41f6-ffd3-5410-0de4fbed8543'
]
$expect(host.tasks).to.have.members [
# TODO
]
$expect(host.$running_VMs).to.have.members [
'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
'OpaqueRef:46fa4c52-5e93-6cf7-32e3-c51fb4ed106d'
]
$expect(host.$vCPUs).to.equal 0
it 'VM', ->
vm = collection.get 'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
$expect(vm).to.be.an 'object'
$expect(vm.type).to.equal 'VM'
$expect(vm.name_label).to.equal 'ceph3'
$expect(vm.name_description).to.equal ''
$expect(vm.tags).to.have.members []
$expect(vm.memory).to.be.an 'object'
$expect(vm.memory.usage).to.be.null
#$expect(vm.memory.size).to.equal '' # FIXME
$expect(vm.messages).to.have.members []
$expect(vm.power_state).to.equal 'Running'
#$expect(vm.CPUs).to.be.an # FIXME
$expect(vm.$CPU_usage).to.be.null
$expect(vm.$container).to.equal 'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
$expect(vm.snapshots).to.have.members []
$expect(vm.snapshot_time).to.equal '1969-12-31T23:00:00.000Z'
$expect(vm.$VBDs).to.have.members [
'OpaqueRef:dbb53525-e1a3-741b-4924-9944b845bc0c'
'OpaqueRef:1bd20244-01a0-fec3-eb00-79a453a56446'
]
$expect(vm.VIFs).to.have.members [
'OpaqueRef:20349ad5-0a0d-4b80-dcc0-0037fa647182'
]
it 'VM-template', ->
vm = collection.get 'OpaqueRef:f02a3c19-447b-c618-fb51-a9cde79be17c'
#console.log vm
# Only specific VM-templates fields will be tested.
$expect(vm.type).to.equal 'VM-template'
$expect(vm.template_info).to.be.an 'object'
$expect(vm.template_info.arch).to.equal 'amd64'
$expect(vm.template_info.disks).to.deep.equal [
{
device: '0'
size: 8589934592
SR: ''
bootable: true
type: 'system'
}
]
$expect(vm.template_info.install_methods).to.have.members [
'cdrom'
'http'
'ftp'
]
it 'SR', ->
sr = collection.get 'OpaqueRef:d6fe49bf-dd48-c929-5aab-b2786a2e7aee'
#console.log sr
$expect(sr).to.be.an 'object'
$expect(sr.type).to.equal 'SR'
$expect(sr.name_label).to.equal 'Zfs'
$expect(sr.name_description).to.equal 'iSCSI SR [192.168.0.100 (iqn.1986-03.com.sun:02:ba2ab54c-2d14-eb74-d6f9-ef7c4f28ff1e; LUN 0: A83BCKLAF: 2048 GB (NEXENTA))]'
$expect(sr.SR_type).to.equal 'lvmoiscsi'
$expect(sr.content_type).to.equal ''
$expect(sr.physical_usage).to.equal 205831274496
$expect(sr.usage).to.equal 202358390784
$expect(sr.size).to.equal 2199010672640
$expect(sr.$container).to.equal 'OpaqueRef:6462d0b3-8f20-ef76-fddf-002f7af3452e'
$expect(sr.$PBDs).to.have.members [
'OpaqueRef:ff32de74-138c-9d80-ab58-c631d2aa0e71'
'OpaqueRef:200674ae-d9ab-2caa-a283-4fa3d14592fd'
]
$expect(sr.VDIs).to.have.members [
'OpaqueRef:b4a1573f-c235-8acd-4625-dfbcb2beb523'
'OpaqueRef:098a2155-605b-241e-f775-a05c2133874e'
'OpaqueRef:f7d900f9-a4fe-9a3e-ead8-28db301d26e8'
'OpaqueRef:f26d2af5-b529-4d16-21d1-a56965e7bfb1'
'OpaqueRef:ec5ce10e-023e-9a9f-eef7-a64e4c6d7b28'
'OpaqueRef:e0eb5eb1-a485-fcfc-071e-fafa17f9ac48'
'OpaqueRef:c4aa5d87-4115-c359-9cdf-c16fbf56cf2c'
'OpaqueRef:b06a9d3f-5132-e58f-25c4-ef94d5b38986'
'OpaqueRef:a4dd8a73-5393-81ce-abce-fc1502490a6d'
'OpaqueRef:83331526-8bd8-9644-0a7d-9f645f5fcd70'
'OpaqueRef:693bef17-aa19-63f8-3775-7d3b2dbce9d6'
'OpaqueRef:67618138-57df-e90a-74c6-402ad62d657b'
'OpaqueRef:5f1d5117-1033-b12a-92a8-99f206c9dbba'
'OpaqueRef:287084c1-241a-58df-929a-cbe2e7454a56'
'OpaqueRef:1f7f9828-f4e7-41dd-20e6-3bf57c559a78'
]
it 'PBD', ->
pbd = collection.get 'OpaqueRef:ff32de74-138c-9d80-ab58-c631d2aa0e71'
#console.log pbd
$expect(pbd).to.an 'object'
$expect(pbd.type).to.equal 'PBD'
$expect(pbd.attached).to.be.true
$expect(pbd.host).to.equal 'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
$expect(pbd.SR).to.equal 'OpaqueRef:d6fe49bf-dd48-c929-5aab-b2786a2e7aee'
it 'PIF', ->
pif = collection.get 'OpaqueRef:aef57ed4-e4d9-7f72-0376-b781a19bb9d2'
#console.log pif
$expect(pif).to.an 'object'
$expect(pif.type).to.equal 'PIF'
$expect(pif.attached).to.be.true
$expect(pif.device).to.equal 'eth0'
$expect(pif.IP).to.equal '192.168.1.1'
$expect(pif.$host).to.equal 'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
$expect(pif.MAC).to.equal '90:2b:34:d3:ce:75'
$expect(pif.management).to.be.true
$expect(pif.mode).to.equal 'Static'
$expect(pif.MTU).to.equal 1500
$expect(pif.netmask).to.equal '255.255.255.0'
$expect(pif.$network).to.equal 'OpaqueRef:dbc93777-f2c0-e888-967d-dd9beeffb3c0'
it 'VDI', ->
vdi = collection.get 'OpaqueRef:1f7f9828-f4e7-41dd-20e6-3bf57c559a78'
#console.log vdi
$expect(vdi).to.an 'object'
$expect(vdi.type).to.equal 'VDI'
$expect(vdi.name_label).to.equal 'ceph'
$expect(vdi.name_description).to.equal ''
$expect(vdi.usage).to.equal 21525168128
$expect(vdi.size).to.equal 21474836480
$expect(vdi.$snapshot_of).to.equal null
$expect(vdi.snapshots).to.have.members [
'OpaqueRef:b4a1573f-c235-8acd-4625-dfbcb2beb523'
]
$expect(vdi.$SR).to.equal 'OpaqueRef:d6fe49bf-dd48-c929-5aab-b2786a2e7aee'
$expect(vdi.$VBDs).to.have.members [
'OpaqueRef:9f15200b-3cac-7a61-b3e8-dd2fc0a5572d'
]
it 'VBD', ->
vbd = collection.get 'OpaqueRef:9f15200b-3cac-7a61-b3e8-dd2fc0a5572d'
#console.log vbd
$expect(vbd).to.an 'object'
$expect(vbd.type).to.equal 'VBD'
$expect(vbd.attached).to.be.true
$expect(vbd.bootable).to.be.false
$expect(vbd.read_only).to.be.false
$expect(vbd.VDI).to.equal 'OpaqueRef:1f7f9828-f4e7-41dd-20e6-3bf57c559a78'
$expect(vbd.VM).to.equal 'OpaqueRef:be2390b2-cd08-53f5-3fae-b76f6f3725bf'
it 'VIF', ->
vif = collection.get 'OpaqueRef:20349ad5-0a0d-4b80-dcc0-0037fa647182'
#console.log vif
$expect(vif).to.an 'object'
$expect(vif.type).to.equal 'VIF'
$expect(vif.attached).to.be.true
$expect(vif.device).to.equal '0'
$expect(vif.MAC).to.equal 'ce:20:2b:38:7f:fd'
$expect(vif.MTU).to.equal 1500
$expect(vif.$network).to.equal 'OpaqueRef:dbc93777-f2c0-e888-967d-dd9beeffb3c0'
$expect(vif.$VM).to.equal 'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
it 'network', ->
network = collection.get 'OpaqueRef:dbc93777-f2c0-e888-967d-dd9beeffb3c0'
#console.log network
$expect(network).to.be.an 'object'
$expect(network.type).to.equal 'network'
$expect(network.name_label).to.equal 'Pool-wide network associated with eth0'
$expect(network.name_description).to.equal ''
$expect(network.bridge).to.equal 'xenbr0'
$expect(network.MTU).to.equal 1500
$expect(network.PIFs).to.have.members [
'OpaqueRef:aef57ed4-e4d9-7f72-0376-b781a19bb9d2'
'OpaqueRef:971d6bc5-60f4-a331-bdee-444ee7cbf678'
]
$expect(network.VIFs).to.have.members [
'OpaqueRef:fc86d17e-d9d1-5534-69d6-d15edbe36d22'
'OpaqueRef:ed2d89ca-1f4e-09ff-f80e-991d6b01de45'
'OpaqueRef:c6651d03-cefe-accf-920b-636e32fee23c'
'OpaqueRef:c5977d9b-cb50-a615-8488-1dd105d69802'
'OpaqueRef:c391575b-168f-e52b-59f7-9f852a2c6854'
'OpaqueRef:bf4da755-480b-e3fd-2bfe-f53e7204c8ae'
'OpaqueRef:ba41d1a6-724e-aae8-3447-20f74014eb75'
'OpaqueRef:b8df4453-542e-6c14-0eb1-174d48373bca'
'OpaqueRef:b5980de3-1a74-9f57-1e98-2a74184211dc'
'OpaqueRef:aaae3669-faee-4338-3156-0ce8c06c75cf'
'OpaqueRef:aa874254-b67c-e9e3-6a08-1c770c2dd8ac'
'OpaqueRef:7b8ecb18-5bc5-7650-3ac4-6bc22322e8ba'
'OpaqueRef:59b884b0-521f-7b3e-6a91-319ded893e68'
'OpaqueRef:20349ad5-0a0d-4b80-dcc0-0037fa647182'
]
it 'message', ->
# FIXME
#console.log collection.get()
it 'task', ->
# FIXME: we need to update the tests data to complete this test.

View File

@ -19,7 +19,7 @@ $createRedisClient = (require 'then-redis').createClient
# A mapped collection is generated from another collection through a
# specification.
$MappedCollection = require './MappedCollection'
{$MappedCollection} = require './MappedCollection'
# Collection where models are stored in a Redis DB.
$RedisCollection = require './collection/redis'
@ -31,7 +31,7 @@ $Model = require './model'
$XAPI = require './xapi'
# Helpers for dealing with fibers.
{$fiberize, $synchronize, $waitPromise} = require './fibers-utils'
{$fiberize, $synchronize, $wait} = require './fibers-utils'
#=====================================================================
@ -150,13 +150,8 @@ class $XO extends $EventEmitter
@tokens.remove (token.id for token in tokens)
# Collections of XAPI objects mapped to XO API.
refsToUUIDs = { # Needed for the mapping.
'OpaqueRef:NULL': null
}
@xobjs = do ->
spec = (require './spec') refsToUUIDs
new $MappedCollection spec
@xobjs = new $MappedCollection()
(require './spec').call @xobjs
# XAPI connections.
@xapis = {}
@ -168,7 +163,8 @@ class $XO extends $EventEmitter
id = server.id
# UUID of the pool of this connection.
poolUUID = undefined
poolUUID = undefined #TODO: Remove.
poolRef = undefined
xapi = @xapis[id] = new $XAPI {
host: server.host
@ -188,11 +184,10 @@ class $XO extends $EventEmitter
types.push type
types
# This helper normalizes a record by inserting its type and by
# storing its UUID in the `refsToUUIDs` map if any.
# This helper normalizes a record by inserting its type.
normalizeObject = (object, ref, type) ->
refsToUUIDs[ref] = object.uuid if object.uuid?
object.$pool = poolUUID unless type is 'pool'
object.$poolRef = poolRef unless type is 'pool'
object.$ref = ref
object.$type = type
objects = {}
@ -207,9 +202,12 @@ class $XO extends $EventEmitter
pool = pools[ref]
throw new Error 'no pool found' unless pool?
# Remembers its UUID.
# Remembers its UUID. TODO: remove
poolUUID = pool.uuid
# Remembers its reference.
poolRef = ref
# Makes the connection accessible through the pool UUID.
# TODO: Properly handle disconnections.
@xapis[poolUUID] = xapi
@ -284,14 +282,17 @@ class $XO extends $EventEmitter
throw error unless error[0] is 'SESSION_NOT_REGISTERED'
# Prevents errors from stopping the server.
connectSafe = $fiberize (server) =>
connectSafe = $fiberize (server) ->
try
connect server
catch error
console.log "[WARN] #{server.host}:", error[0] ? error.code ? error
console.error(
"[WARN] #{server.host}:"
error[0] ? error.stack ? error.code ? error
)
# Connects to existing servers.
connectSafe server for server in $waitPromise @servers.get()
connectSafe server for server in $wait @servers.get()
# Automatically connects to new servers.
@servers.on 'add', (servers) ->