New collection!

This commit is contained in:
Julien Fontanet 2015-05-26 20:05:14 +02:00
parent 94620748ab
commit 5236441be0
14 changed files with 650 additions and 3126 deletions

View File

@ -80,7 +80,7 @@
"source-map-support": "^0.2.10",
"then-redis": "~1.3.0",
"ws": "~0.7.1",
"xen-api": "^0.3.0",
"xen-api": "^0.5.4",
"xml2js": "~0.4.6",
"xo-collection": "^0.3.2"
},

View File

@ -1,457 +0,0 @@
{EventEmitter: $EventEmitter} = require 'events'
$assign = require 'lodash.assign'
$filter = require 'lodash.filter'
$forEach = require 'lodash.foreach'
$getKeys = require 'lodash.keys'
$isArray = require 'lodash.isarray'
$isEmpty = require 'lodash.isempty'
$isFunction = require 'lodash.isfunction'
$isObject = require 'lodash.isobject'
$isString = require 'lodash.isstring'
$map = require 'lodash.map'
{mapInPlace: $mapInPlace, wrap: $wrap} = require './utils'
#=====================================================================
class $MappedCollection extends $EventEmitter
# The dispatch function is called whenever a new item has to be
# processed and returns the name of the rule to use.
#
# To change the way it is dispatched, just override this it.
dispatch: ->
(@genval and (@genval.rule ? @genval.type)) ? 'unknown'
# 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}"
# This function is called when the new generator of an existing item has been
# matched to a different rule.
#
# The default behavior is to throw an error as it usually indicates a bug but
# you can ignore it.
ruleConflict: (rule, item) ->
throw new Error "the item “#{item.key}” was of rule “#{item.rule}"+
"but matches to “#{rule}"
constructor: ->
# Items are stored here indexed by key.
#
# 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.
#
# 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
data: undefined
val: undefined
singleton
}
definition.call ctx
else
ctx = {
name
key: definition?.key
data: definition?.data
val: definition?.val
singleton
}
# Runs the `afterRule` hook and returns if the registration has
# been prevented.
return unless @_runHook 'afterRule', ctx
{key, data, val} = ctx
# The default key.
key ?= if singleton then -> name else -> @genkey
# The default value.
val ?= -> @genval
# Makes sure `key` is a function for uniformity.
key = $wrap key unless $isFunction key
# Register the new rule.
@_rules[name] = {
name
key
data
val
singleton
}
#--------------------------------
get: (keys, ignoreMissingItems = false) ->
if keys is undefined
items = $map @_byKey, (item) -> item.val
else
items = @_fetchItems keys, ignoreMissingItems
$mapInPlace items, (item) -> item.val
if $isString keys then items[0] else items
getRaw: (keys, ignoreMissingItems = false) ->
if keys is undefined
item for _, item of @_byKey
else
items = @_fetchItems keys, ignoreMissingItems
if $isString keys then items[0] else items
remove: (keys, ignoreMissingItems = false) ->
@_removeItems (@_fetchItems keys, ignoreMissingItems)
removeWithPredicate: (predicate, thisArg) ->
items = ($filter @_byKey, predicate, thisArg)
@_removeItems items
set: (items, {add, update, remove} = {}) ->
add = true unless add?
update = true unless update?
remove = false unless remove?
itemsToAdd = {}
itemsToUpdate = {}
itemsToRemove = {}
$assign itemsToRemove, @_byKey if remove
$forEach items, (genval, genkey) =>
item = {
rule: undefined
key: undefined
data: undefined
val: undefined
genkey
genval
}
return unless @_runHook 'beforeDispatch', item
# Searches for a rule to handle it.
ruleName = @dispatch.call item
rule = @_rules[ruleName]
unless rule?
@missingRule ruleName
# If `missingRule()` has not created the rule, just skip this
# item.
rule = @_rules[ruleName]
return unless rule?
# Checks if this is a singleton.
@_assert(
not rule.singleton
"cannot add items to singleton rule “#{rule.name}"
)
# Computes its key.
key = rule.key.call item
@_assert(
$isString key
"the key “#{key}” is not a string"
)
# Updates known values.
item.rule = rule.name
item.key = key
if key of @_byKey
# Marks this item as not to be removed.
delete itemsToRemove[key]
if update
# Fetches the existing entry.
prev = @_byKey[key]
# Checks if there is a conflict in rules.
unless item.rule is prev.rule
@ruleConflict item.rule, prev
item.prevRule = prev.rule
else
delete item.prevRule
# Gets its previous data/value.
item.data = prev.data
item.val = prev.val
# Registers the item to be updated.
itemsToUpdate[key] = item
# 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
return
# Adds items.
@_updateItems itemsToAdd, true
# Updates items.
@_updateItems itemsToUpdate
# Removes any items not seen (iff `remove` is true).
@_removeItems itemsToRemove
# Forces items to update their value.
touch: (keys) ->
@_updateItems (@_fetchItems keys, true)
#--------------------------------
_assert: (cond, message) ->
throw new Error message unless cond
# Emits item related event.
_emitEvent: (event, items) ->
getRule = if event is 'exit'
(item) -> item.prevRule or item.rule
else
(item) -> item.rule
byRule = Object.create null
# One per item.
$forEach items, (item) =>
@emit "key=#{item.key}", event, item
(byRule[getRule item] ?= []).push item
return
# One per rule.
@emit "rule=#{rule}", event, byRule[rule] for rule of byRule
# One for everything.
@emit 'any', event, items
_fetchItems: (keys, ignoreMissingItems = false) ->
unless $isArray keys
keys = if $isObject keys then $getKeys 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
$forEach items, (item) =>
delete @_byKey[item.key]
return
@_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.
@_removeItems items unless areNew
$forEach 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, 'data', @_rules[ruleName].data
updateValue item, 'val', @_rules[ruleName].val
unless @_runHook 'beforeSave', item
# FIXME: should not be removed, only not saved.
delete @_byKey[item.key]
return
# Really inserts the items and trigger events.
$forEach items, (item) =>
@_byKey[item.key] = item
return
@_emitEvent 'enter', items
#=====================================================================
module.exports = {$MappedCollection}

View File

@ -1,121 +0,0 @@
{expect: $expect} = require 'chai'
$sinon = require 'sinon'
#---------------------------------------------------------------------
{$MappedCollection} = require './MappedCollection'
#=====================================================================
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
#-------------------------------------------------------------------
describe 'adding new items', ->
beforeEach ->
collection.rule test: {}
collection.dispatch = -> 'test'
#------------------------------
it 'should trigger three `enter` events', ->
keySpy = $sinon.spy()
ruleSpy = $sinon.spy()
anySpy = $sinon.spy()
collection.on 'key=a key', keySpy
collection.on 'rule=test', ruleSpy
collection.on 'any', anySpy
collection.set {
'a key': 'a value'
}
item = collection.getRaw 'a key'
# TODO: items can be an array or a object (it is not defined).
$expect(keySpy.args).to.deep.equal [
['enter', item]
]
$expect(ruleSpy.args).to.deep.equal [
['enter', [item]]
]
$expect(anySpy.args).to.deep.equal [
['enter', {'a key': item}]
]

View File

@ -16,8 +16,8 @@ export class NotImplemented extends JsonRpcError {
// -------------------------------------------------------------------
export class NoSuchObject extends JsonRpcError {
constructor (data) {
super('no such object', 1, data)
constructor (id, type) {
super('no such object', 1, {id, type})
}
}

View File

@ -23,6 +23,12 @@ describe('autobind', function () {
expect(getFoo()).to.equal(foo)
})
it('returns the same bound instance each time', function () {
const foo = new Foo()
expect(foo.getFoo).to.equal(foo.getFoo)
})
it('works with multiple instances of the same class', function () {
const foo1 = new Foo()
const foo2 = new Foo()

View File

@ -1,367 +0,0 @@
// FIXME: This file name should reflect what's inside!
// ===================================================================
import $clone from 'lodash.clone'
import $forEach from 'lodash.foreach'
import $isArray from 'lodash.isarray'
import $isEmpty from 'lodash.isempty'
import $isFunction from 'lodash.isfunction'
// ===================================================================
const $asArray = (val) => $isArray(val) ? val : [val]
const $asFunction = (val) => $isFunction(val) ? val : () => val
const $first = (collection, defaultValue) => {
const {length} = collection
if (length == null) {
for (let key in collection) {
return collection[key]
}
} else if (length) {
return collection[0]
}
// Nothing was found, returns the `def` value.
return defaultValue
}
const $removeValue = (array, value) => {
const index = array.indexOf(value)
if (index === -1) {
return false
}
array.splice(index, 1)
return true
}
// -------------------------------------------------------------------
// TODO: currently the watch can be updated multiple times per
// “$MappedCollection.set()” which is inefficient: it should be
// possible to address that.
const $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.
loopDetected = () => { throw new Error('loop detected') }
}, fn) => {
val = val == null ?
// The default value is simply the value of the item.
function () { return this.val } :
$asFunction(val)
// Method allowing the cleanup when the helper is no longer used.
// cleanUp = -> // TODO: noop for now.
// Keys of items using the current helper.
const consumers = Object.create(null)
// Current values.
const values = Object.create(null)
values.common = init
// The number of nested processing for this watcher is counted to
// avoid an infinite loop.
let loops = 0
let updating = false
const process = (event, items) => {
if (updating) return
// Values are grouped by namespace.
const valuesByNamespace = Object.create(null)
$forEach(items, (item) => {
if (cond && !cond.call(item)) return
const namespace = (function () {
if (bind) {
const key = bind.call(item)
return key && `$${key}`
} else {
return 'common'
}
})()
// If not namespace, ignore this item.
if (!namespace) return
(
valuesByNamespace[namespace] ||
(valuesByNamespace[namespace] = [])
).push(val.call(item))
})
// Stops here if no values were computed.
if ($isEmpty(valuesByNamespace)) return
if (loops && loopDetected(loops) !== true) return
const previousLoops = loops++
// For each namespace.
$forEach(valuesByNamespace, (values_, namespace) => {
// Updates the value.
const value = values[namespace]
const ctx = {
// TODO: test the $clone
value: value == null ? $clone(init) : value
}
const changed = event === 'enter' ?
fn.call(ctx, values_, {}) :
fn.call(ctx, {}, values_)
// Notifies watchers unless it is known the value has not
// changed.
if (changed !== false) {
values[namespace] = ctx.value
updating = true
if (namespace === 'common') {
collection.touch(consumers)
} else {
collection.touch(namespace.substr(1))
}
updating = false
}
})
loops = previousLoops
}
const 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 (!$isEmpty(keys)) {
// Matching is done on the keys.
if (!$isEmpty(rules)) {
throw new Error('cannot use both keys and rules')
}
$forEach(keys, key => {
collection.on(`key=${key}`, processOne)
})
// Handles existing items.
process('enter', collection.getRaw(keys, true))
} else if (!$isEmpty(rules)) {
// Matching is done the rules.
$forEach(rules, rule => {
collection.on(`rule=${rule}`, process)
})
// TODO: Inefficient, is there another way?
rules = (function (rules) { // Minor optimization.
const tmp = Object.create(null)
for (let rule of rules) {
tmp[rule] = true
}
return tmp
})(rules)
$forEach(collection.getRaw(), item => {
if (rules[item.rule]) {
processOne('enter', item)
}
})
} 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.
const generator = function () {
const {key} = this
// Register this item has a consumer.
consumers[key] = true
// Returns the value for this item if any or the common value.
const namespace = `$${key}`
return (namespace in values) ?
values[namespace] :
values.common
}
// Creates a helper to unregister an item from this watcher.
generator.unregister = function () {
const {key} = this
delete consumers[key]
delete values[`$${key}`]
}
// Creates a helper to get the value without using an item.
generator.raw = (key) => values[key != null ? `$${key}` : 'common']
// Returns the generator.
return generator
}
// ===================================================================
export const $map = function (options) {
options.init = Object.create(null)
return $watch(this, options, function (entered, exited) {
let changed = false
$forEach(entered, ([key, value]) => {
if (this.value[key] !== value) {
this.value[key] = value
changed = true
}
})
$forEach(exited, ([key, value]) => {
if (key in this.value) {
delete this.value[key]
changed = true
}
})
return changed
})
}
// -------------------------------------------------------------------
// Creates a set of value from various items.
export const $set = function (options) {
// Contrary to other helpers, the default value is the key.
if (!options.val) {
options.val = function () { return this.key }
}
options.init = []
return $watch(this, options, function (entered, exited) {
let changed = false
$forEach(entered, (value) => {
if (this.value.indexOf(value) === -1) {
this.value.push(value)
changed = true
}
})
$forEach(exited, (value) => {
if ($removeValue(this.value, value)) {
changed = true
}
})
return changed
})
}
// -------------------------------------------------------------------
export const $sum = function (options) {
if (!options.init) {
options.init = 0
}
return $watch(this, options, function (entered, exited) {
const prev = this.value
$forEach(entered, (value) => { this.value += value })
$forEach(exited, (value) => { this.value -= value })
return this.value !== prev
})
}
// -------------------------------------------------------------------
// Uses a value from another item.
//
// Important note: Behavior is not specified when binding to multiple
// items.
export const $val = function (options) {
// The default value.
const def = options.default
delete options.default
if (!options.init) {
options.init = def
}
// Should the last value be kept instead of returning to the default
// value when no items are available!
const keepLast = !!options.keepLast
delete options.keepLast
return $watch(this, options, function (entered, exited) {
const prev = this.value
this.value = $first(entered, keepLast ? this.value : def)
return this.value !== prev
})
}

View File

@ -1,269 +0,0 @@
/* eslint-env mocha */
import $bind from 'lodash.bind'
import {expect as $expect} from 'chai'
import {$MappedCollection} from './MappedCollection'
import * as $nonBindedHelpers from './helpers'
// Enable source maps support for traces.
import sourceMapSupport from 'source-map-support'
sourceMapSupport.install()
// ===================================================================
describe('Helper', () => {
// Shared variables.
// let $set
// let $val
let $sum
let collection
beforeEach(() => {
// Creates the collection.
collection = new $MappedCollection()
// Dispatcher used for tests.
collection.dispatch = function () { return this.genkey.split('.')[0] }
// Missing rules should be automatically created.
collection.missingRule = collection.rule
// // Monkey patch the collection to see all emitted events.
// {
// const {emit} = collection
// collection.emit = (...args) => {
// console.log(...args)
// emit.call(collection, ...args)
// }
// }
// Binds helpers to this collection.
// $set = $bind($nonBindedHelpers.$set, collection)
// $val = $bind($nonBindedHelpers.$val, collection)
$sum = $bind($nonBindedHelpers.$sum, collection)
})
// -----------------------------------------------------------------
// 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', function () {
this.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', function () {
this.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)
})
// FIXME: This test fails but this feature is not used.
it.skip('with dynamic keys', () => {
collection.set({
foo: 1,
bar: 2
})
collection.rule('sum', function () {
this.val = $sum({
key: function () { return this.genkey.split('.')[1] }
})
})
collection.set({
'sum.foo': null,
'sum.bar': null
})
$expect(collection.get('sum.foo')).to.equal(1)
$expect(collection.get('sum.bar')).to.equal(2)
collection.remove('bar')
$expect(collection.get('sum.foo')).to.equal(1)
$expect(collection.get('sum.bar')).to.equal(0)
})
it('with single rule', () => {
collection.set({
'foo.1': 1,
'foo.2': 2
})
collection.item('sum', function () {
this.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', function () {
this.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', function () {
this.val = $sum({
bind: function () {
const id = this.val.sum
return id && `sum.${id}`
},
val: function () { return this.val.val }
})
})
collection.set({
'sum.1': null,
'sum.2': null
})
$expect(collection.get('sum.1')).to.equal(2)
$expect(collection.get('sum.2')).to.equal(1)
collection.set({
'foo': {
sum: 1,
val: 3
}
})
$expect(collection.get('sum.1')).to.equal(5)
$expect(collection.get('sum.2')).to.equal(0)
collection.remove('bar')
$expect(collection.get('sum.1')).to.equal(3)
$expect(collection.get('sum.2')).to.equal(0)
})
it('with predicate', () => {
collection.set({
foo: 1,
bar: 2,
baz: 3
})
collection.item('sum', function () {
this.val = $sum({
if: function () { return /^b/.test(this.rule) }
})
})
$expect(collection.get('sum')).to.equal(5)
collection.set({ foo: 4 })
$expect(collection.get('sum')).to.equal(5)
collection.set({ bar: 5 })
$expect(collection.get('sum')).to.equal(8)
collection.remove('baz')
$expect(collection.get('sum')).to.equal(5)
})
it('with initial value', () => {
collection.set({ foo: 1 })
collection.item('sum', function () {
this.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

@ -1,6 +1,5 @@
import createLogger from 'debug'
const debug = createLogger('xo:main')
const debugPlugin = createLogger('xo:plugin')
import Bluebird from 'bluebird'
Bluebird.longStackTraces()
@ -81,6 +80,8 @@ async function loadConfiguration () {
// ===================================================================
const debugPlugin = createLogger('xo:plugin')
const loadPlugin = Bluebird.method(function (pluginConf, pluginName) {
debugPlugin('loading %s', pluginName)

View File

@ -1,929 +0,0 @@
$forEach = require 'lodash.foreach'
$isArray = require 'lodash.isarray'
$isObject = require 'lodash.isobject'
$xml2js = require 'xml2js'
$helpers = require './helpers'
{parseXml: $parseXML} = require './utils'
#=====================================================================
$isVMRunning = ->
return switch @val.power_state
when 'Paused', 'Running'
true
else
false
$isHostRunning = -> @val.power_state is 'Running'
$isTaskLive = -> @val.status is 'pending' or @val.status is 'cancelling'
$retrieveTags = -> [] # TODO
$toTimestamp = (date) ->
# Weird behavior from the XAPI.
return null if date is '1969-12-31T23:00:00.000Z'
return if date?
Math.round (Date.parse date) / 1000
else
null
#=====================================================================
module.exports = ->
# Binds the helpers to the collection.
{
$set
$sum
$val
} = do =>
helpers = {}
helpers[name] = fn.bind this for name, fn of $helpers
helpers
collection = this
# do (emit = collection.emit) ->
# collection.emit = (event, items) ->
# console.log event
# emit.call collection, event, items
$link = (keyFn, valFn = (-> @val)) ->
keyPerItem = Object.create null
listenerPerItem = Object.create null
valuePerItem = Object.create null
updating = false
->
{key} = this
# Gets the key of the remote object.
remoteKey = keyFn.call this
keyHasChanged = remoteKey isnt keyPerItem[key]
if keyHasChanged
keyPerItem[key] = remoteKey
else
# Returns the value if already defined.
return valuePerItem[key] if key of valuePerItem
eventName = "key=#{remoteKey}"
listener = listenerPerItem[key]
if listener and keyHasChanged
collection.remove eventName, listener
listener = null
# Special case for `OpaqueRef:NULL`.
if remoteKey is 'OpaqueRef:NULL'
return valuePerItem[key] = null
unless listener
listener = (event, item) ->
# If the events are due to an update of this link or if the item is
# exiting, just returns.
return if updating or event isnt 'enter'
# Register its value.
valuePerItem[key] = valFn.call item
# Force the object to update.
try
updating = true
collection.touch key
finally
updating = false
collection.on eventName, listener
# Tries to find the remote object in the collection.
try
return valuePerItem[key] = valFn.call (collection.getRaw remoteKey)
# Returns `null` for now.
valuePerItem[key] = null
$map = (valFn) ->
map = Object.create null
subscribers = Object.create null
updating = false
# First, initializes the map with existing items.
$forEach collection.getRaw(), (item) ->
val = valFn.call item
map[val[0]] = val[1] if val
return
# Listens to any new item.
collection.on 'any', (event, items) ->
# If the events are due to an update of this map or if items are exiting,
# just returns.
return if updating or event isnt 'enter'
# No need to trigger an update if nothing has changed.
changed = false
$forEach items, (item) ->
val = valFn.call item
if val and map[val[0]] isnt val[1]
changed = true
map[val[0]] = val[1]
return
if changed
try
updating = true
collection.touch subscribers
finally
updating = false
generator = ->
subscribers[@key] = true
map
generator.unsubscribe = ->
delete subscribers[@key]
generator
# Shared watchers.
UUIDsToKeys = $map ->
{UUID} = @val
return false unless UUID
[UUID, "#{@key}"]
messages = $set {
rule: 'message'
bind: -> @val.$object or @val.poolRef
}
# Classes in XAPI are not always delivered with the same case,
# therefore a map is needed to make sure they always map to the same
# rule.
rulesMap = {}
# Defines which rule should be used for this item.
#
# Note: If the rule does not exists, a temporary item is created. FIXME
@dispatch = ->
{$type: type} = @genval
# Normalizes the type.
type = rulesMap[type.toLowerCase()] ? type
# Subtypes handling for VMs.
if type is 'VM'
return 'VM-controller' if @genval.is_control_domain
return 'VM-snapshot' if @genval.is_a_snapshot
return 'VM-template' if @genval.is_a_template
type
# Missing rules should be created.
@missingRule = (name) ->
@rule(name, ->
@key = -> @genval.id
@val = -> @genval
)
# Rule conflicts are possible (e.g. VM-template to VM).
@ruleConflict = ( -> )
# Used to apply common definition to rules.
@hook afterRule: ->
# Registers this rule in the map.
rulesMap[@name.toLowerCase()] = @name
# TODO: explain.
return unless @val?
unless $isObject @val
throw new Error 'the value should be an object'
# Injects various common definitions.
@val.type = @name
if @singleton
@val.id = @val.ref = -> @key
else
# This definition are for non singleton items only.
@key = -> @genval.$ref
@val.id = -> @genval.$id
@val.UUID = -> @genval.uuid
@val.ref = -> @genval.$ref
@val.poolRef = -> @genval.$pool.$ref
@val.$poolId = -> @genval.$pool.$id
# Main objects all can have associated messages and tags.
if @name in ['host', 'pool', 'SR', 'VM', 'VM-controller']
@val.messages = messages
@val.tags = $retrieveTags
# Helper to create multiple rules with the same definition.
rules = (rules, definition) =>
@rule rule, definition for rule in rules
#===================================================================
# An item is equivalent to a rule but one and only one instance of
# this rule is created without any generator.
@item xo: ->
@val = {
# TODO: Maybe there should be high-level hosts: those who do not
# belong to a pool.
pools: $set {
rule: 'pool'
}
$CPUs: $sum {
rule: 'host'
val: -> +(@val.CPUs.cpu_count)
}
$vCPUs: $sum {
rule: 'VM'
val: -> @val.CPUs.number
if: $isVMRunning
}
# Do not work due to problem in host rule.
$memory: {
usage: $sum {
rule: 'host'
if: $isHostRunning
val: -> @val.memory.usage
}
size: $sum {
rule: 'host'
if: $isHostRunning
val: -> @val.memory.size
}
}
# Maps the UUIDs to keys (i.e. opaque references).
$UUIDsToKeys: UUIDsToKeys
}
#-------------------------------------------------------------------
@rule pool: ->
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
SRs: $set {
rule: 'SR'
bind: -> @val.$container
}
default_SR: ->
SR = @genval.default_SR
if SR is 'OpaqueRef:NULL'
null
else
SR
HA_enabled: -> @genval.ha_enabled
hosts: $set {
rule: 'host'
bind: -> @genval.$pool.$ref
}
master: -> @genval.master
networks: $set {
rule: 'network'
bind: -> @genval.$pool.$ref
}
templates: $set {
rule: 'VM-template'
bind: -> @val.$container
}
VMs: $set {
rule: 'VM'
bind: -> @val.$container
}
$running_hosts: $set {
rule: 'host'
bind: -> @genval.$pool.$ref
if: $isHostRunning
}
$running_VMs: $set {
rule: 'VM'
bind: -> @genval.$pool.$ref
if: $isVMRunning
}
$VMs: $set {
rule: 'VM'
bind: -> @genval.$pool.$ref
}
patches: $set {
rule: 'pool_patch'
bind: -> @genval.$pool.$ref
}
}
#-------------------------------------------------------------------
@rule host: ->
# Private properties used to helps construction.
@data = {
metrics: $link -> @genval.metrics
}
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
address: -> @genval.address
controller: $val {
rule: 'VM-controller'
bind: -> @val.$container
val: -> @key
}
bios_strings: -> @genval.bios_strings
CPUs: -> @genval.cpu_info
enabled: -> @genval.enabled
current_operations: -> @genval.current_operations
hostname: -> @genval.hostname
iSCSI_name: -> @genval.other_config?.iscsi_iqn ? null
memory: ->
{metrics} = @data
if metrics
{
usage: +metrics.memory_total - metrics.memory_free
size: +metrics.memory_total
}
else
{
usage: 0
size: 0
}
patches: -> @genval.patches
power_state: ->
if @data.metrics?.live
'Running'
else
'Halted'
# Local SRs are handled directly in `SR.$container`.
SRs: $set {
rule: 'SR'
bind: -> @val.$container
}
# What are local templates?
templates: $set {
rule: 'VM-template'
bind: -> @val.$container
}
# Local VMs are handled directly in `VM.$container`.
VMs: $set {
rule: 'VM'
bind: -> @val.$container
}
$PBDs: -> @genval.PBDs
PIFs: -> @genval.PIFs
$PIFs: -> @val.PIFs
PCIs: -> @genval.PCIs
$PCIs: -> @val.PCIs
PGPUs: -> @genval.PGPUs
$PGPUs: -> @val.PGPUs
tasks: $set {
rule: 'task'
bind: -> @genval.resident_on
if: $isTaskLive
}
$running_VMs: $set {
rule: 'VM'
bind: -> @val.$container
if: $isVMRunning
}
$vCPUs: $sum {
rule: 'VM'
bind: -> @val.$container
if: $isVMRunning
val: -> @val.CPUs.number
}
version: -> @genval.software_version.product_version
build: -> @genval.software_version.build_number
}
#-------------------------------------------------------------------
# This definition is shared.
VMdef = ->
@data = {
metrics: $link -> @genval.metrics
guest_metrics: $link -> @genval.guest_metrics
}
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
boot: -> @genval.HVM_boot_params
addresses: ->
{guest_metrics} = @data
if guest_metrics
guest_metrics.networks
else
null
consoles: $set {
rule: 'console'
bind: -> @genval.VM
val: -> @val
}
current_operations: -> @genval.current_operations
docker: ->
monitor = @genval.other_config['xscontainer-monitor']
return if not monitor?
if monitor is 'False'
return {
enabled: false
}
if @genval.power_state is 'Running'
if @genval.other_config.docker_ps && @genval.other_config.docker_info && @genval.other_config.docker_version
process = ($parseXML @genval.other_config.docker_ps).docker_ps
info = ($parseXML @genval.other_config.docker_info).docker_info
version = ($parseXML @genval.other_config.docker_version).docker_version
return {
enabled: true
process: process
info: info
version: version
}
return { enabled: true}
# TODO: there is two possible value: "best-effort" and "restart"
high_availability: ->
if @genval.ha_restart_priority
true
else
false
auto_poweron: ->
if @genval.other_config.auto_poweron
true
else
false
os_version: ->
{guest_metrics} = @data
if guest_metrics
guest_metrics.os_version
else
null
VGPUs: -> @genval.VGPUs
$VGPUs: -> @val.VGPUs
power_state: -> @genval.power_state
other: -> @genval.other_config
memory: ->
{metrics, guest_metrics} = @data
memory = {
dynamic: [
+@genval.memory_dynamic_min
+@genval.memory_dynamic_max
]
static: [
+@genval.memory_static_min
+@genval.memory_static_max
]
}
memory.size = if not $isVMRunning.call this
+@genval.memory_dynamic_max
else if (gmmemory = guest_metrics?.memory)?.used
memory.usage = +gmmemory.used
+gmmemory.total
else if metrics
+metrics.memory_actual
else
+@genval.memory_dynamic_max
memory
PV_drivers: ->
{guest_metrics} = @data
if guest_metrics
guest_metrics.PV_drivers_up_to_date
else
false
CPUs: ->
{metrics} = @data
CPUs = {
max: +@genval.VCPUs_max
number: if ($isVMRunning.call this) and metrics
+metrics.VCPUs_number
else
+@genval.VCPUs_at_startup
}
$CPU_usage: null #TODO
# FIXME: $container should contains the pool UUID when the VM is
# not on a host.
$container: ->
if $isVMRunning.call this
@genval.resident_on
else
# TODO: Handle local VMs. (`get_possible_hosts()`).
@genval.$pool.$ref
snapshots: -> @genval.snapshots
snapshot_time: -> $toTimestamp @genval.snapshot_time
$VBDs: -> @genval.VBDs
VIFs: -> @genval.VIFs
}
@rule VM: VMdef
@rule 'VM-controller': VMdef
#-------------------------------------------------------------------
# VM-snapshot starts with the same definition but extends it.
@rule 'VM-snapshot': ->
VMdef.call(this)
@val.$snapshot_of = -> @genval.snapshot_of
#-------------------------------------------------------------------
# VM-template starts with the same definition but extends it.
@rule 'VM-template': ->
VMdef.call this
@val.CPUs.number = -> +@genval.VCPUs_at_startup
@val.template_info = {
arch: -> @genval.other_config?['install-arch']
disks: ->
#console.log @genval.other_config
disks = @genval.other_config?.disks
return [] unless disks?
disks = ($parseXML disks)?.provision?.disk
return [] unless disks?
disks = [disks] unless $isArray disks
# Normalize entries.
for disk in disks
disk.bootable = disk.bootable is 'true'
disk.size = +disk.size
disk.SR = disk.sr
delete disk.sr
disks
install_methods: ->
methods = @genval.other_config?['install-methods']
return [] unless methods?
methods.split ','
}
#-------------------------------------------------------------------
@rule SR: ->
@data = {
# Note: not dynamic.
host: $link(
-> @genval.PBDs[0] ? 'OpaqueRef:NULL'
-> @val.host
)
}
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
SR_type: -> @genval.type
content_type: -> @genval.content_type
physical_usage: -> +@genval.physical_utilisation
usage: -> +@genval.virtual_allocation
size: -> +@genval.physical_size
$container: ->
if @genval.shared
@genval.$pool.$ref
else
@data.host
$PBDs: -> @genval.PBDs
VDIs: -> @genval.VDIs
}
#-------------------------------------------------------------------
@rule PBD: ->
@val = {
attached: -> @genval.currently_attached
host: -> @genval.host
SR: -> @genval.SR
}
#-------------------------------------------------------------------
@rule PIF: ->
@val = {
attached: -> @genval.currently_attached
device: -> @genval.device
IP: -> @genval.IP
$host: -> @genval.host
MAC: -> @genval.MAC
# TODO: Find a more meaningful name.
management: -> @genval.management
mode: -> @genval.ip_configuration_mode
MTU: -> +@genval.MTU
netmask: -> @genval.netmask
$network: -> @genval.network
vlan: -> @genval.VLAN
# TODO: What is it?
#
# Could it mean is this a physical interface?.
# How could a PIF not be physical?
#physical: -> @genval.physical
}
#-------------------------------------------------------------------
@rule VDI: ->
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
# TODO: determine whether or not tags are required for a VDI.
#tags: $retrieveTags
usage: -> +@genval.physical_utilisation
size: -> +@genval.virtual_size
$snapshot_of: ->
original = @genval.snapshot_of
if original is 'OpaqueRef:NULL'
null
else
original
snapshots: -> @genval.snapshots
# TODO: Does the name fit?
#snapshot_time: -> @genval.snapshot_time
$SR: -> @genval.SR
$VBDs: -> @genval.VBDs
$VBD: -> # Deprecated
{VBDs} = @genval
if VBDs.length is 0 then null else VBDs[0]
}
#-------------------------------------------------------------------
@rule VBD: ->
@val = {
attached: -> @genval.currently_attached
bootable: -> @genval.bootable
read_only: -> @genval.mode is 'RO'
is_cd_drive: -> @genval.type is 'CD'
position: -> @genval.userdevice
# null if empty.
#
# TODO: Is it really equivalent?
VDI: ->
VDI = @genval.VDI
if VDI is 'OpaqueRef:NULL'
null
else
VDI
VM: -> @genval.VM
}
#-------------------------------------------------------------------
@rule VIF: ->
@val = {
attached: -> @genval.currently_attached
# TODO: Should it be cast to a number?
device: -> @genval.device
MAC: -> @genval.MAC
MTU: -> +@genval.MTU
$network: -> @genval.network
$VM: -> @genval.VM
}
#-------------------------------------------------------------------
@rule network: ->
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
# TODO: determine whether or not tags are required for a VDI.
#tags: $retrieveTags
bridge: -> @genval.bridge
MTU: -> +@genval.MTU
PIFs: -> @genval.PIFs
VIFs: -> @genval.VIFs
}
#-------------------------------------------------------------------
@rule message: ->
@val = {
time: -> $toTimestamp @genval.timestamp
$object: ->
# If the key of the concerned object has already be resolved
# returns the known value.
return @val.$object if @val.$object?
# Tries to resolve the key of the concerned object.
object = (UUIDsToKeys.call this)[@genval.obj_uuid]
# If resolved, unregister from the watcher.
UUIDsToKeys.unsubscribe.call this if object?
object
# TODO: Are these names meaningful?
name: -> @genval.name
body: -> @genval.body
}
#-------------------------------------------------------------------
@rule task: ->
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
progress: -> +@genval.progress
result: -> @genval.result
$host: -> @genval.resident_on
created: -> @genval.created
finished: -> @genval.finished
current_operations: -> @genval.current_operations
status: -> @genval.status
}
@rule host_patch: ->
@val = {
applied: -> @genval.applied
$host: -> @genval.host
time: -> $toTimestamp @genval.timestamp_applied
pool_patch: -> @genval.pool_patch
}
@rule pool_patch: ->
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
applied: -> @genval.pool_applied
version: -> @genval.version
$host_patches: -> @genval.host_patches
size: -> @genval.size
}
@rule pci: ->
@val = {
pci_id: -> @genval.pci_id
class_name: -> @genval.class_name
device_name: -> @genval.device_name
$host: -> @genval.host
}
@rule pgpu: ->
@val = {
pci: -> @genval.PCI
host: -> @genval.host
vgpus: -> @genval.resident_VGPUs
$vgpus: -> @val.vgpus
$host: -> @genval.host
}
@rule vgpu: ->
@val = {
currentAttached: -> @genval.currently_attached
vm: -> @genval.VM
device: -> @genval.device
resident_on: -> @genval.resident_on
}
return

File diff suppressed because one or more lines are too long

View File

@ -1,753 +0,0 @@
{expect: $expect} = require 'chai'
$sinon = require 'sinon'
#---------------------------------------------------------------------
{$MappedCollection} = require './MappedCollection'
# Helpers for dealing with fibers.
{$coroutine} = require './fibers-utils'
#=====================================================================
describe 'spec', ->
collection = null
before $coroutine ->
# Creates the collection.
collection = new $MappedCollection()
# Loads the spec.
(require './spec').call collection
# Loads the mockup data.
collection.set (require './spec.spec-data')
#console.log collection.get()
it 'xo', ->
xo = collection.get 'xo'
#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 10
$expect(xo.$memory).to.be.an 'object'
$expect(xo.$memory.usage).to.equal 15185723392
$expect(xo.$memory.size).to.equal 33532379136
UUIDsToKeys = {}
UUIDsToKeys[obj.UUID] = "#{obj.ref}" for obj in collection.get() when obj.UUID?
$expect(xo.$UUIDsToKeys).to.deep.equal UUIDsToKeys
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.default_SR).to.equal 'OpaqueRef:d6fe49bf-dd48-c929-5aab-b2786a2e7aee'
$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.networks).to.have.members [
'OpaqueRef:dbc93777-f2c0-e888-967d-dd9beeffb3c0'
'OpaqueRef:4e265829-7517-3520-6a97-56b6ac0730c9'
'OpaqueRef:16013d48-b9eb-84c0-0e62-d809211b0632'
]
$expect(pool.templates).to.have.members [
'OpaqueRef:f81c6db6-4227-55a5-0c2f-b670ca5d8d3f'
'OpaqueRef:f449b8ec-ac86-1b6d-2347-37ec36c41bc5'
'OpaqueRef:f02a3c19-447b-c618-fb51-a9cde79be17c'
'OpaqueRef:ee2e2c00-8011-4847-ba7e-c288d5fb01f5'
'OpaqueRef:ebc96e49-11d4-471d-c21f-625a95c34ff9'
'OpaqueRef:e9fb38c8-acc3-dbb8-cc6f-f1f89b03c1ae'
'OpaqueRef:e803bc1b-d3be-b95f-f3cc-a26a174ec93c'
'OpaqueRef:e373c644-3576-985e-9c8f-67062c81d0d2'
'OpaqueRef:e3035b8b-cd27-3e7c-ecbf-54a18a2da59e'
'OpaqueRef:d99a46bf-1b68-072c-00db-444d099466cd'
'OpaqueRef:d45b3989-7350-5166-eeaa-7b789a32addd'
'OpaqueRef:d18c965e-0cef-48b0-2f8d-d48ef6663c32'
'OpaqueRef:d15de0db-1dc5-2a00-331a-c0f7d3c2e123'
'OpaqueRef:cfe620f9-5c68-0f35-ce9f-8f5227fda1c8'
'OpaqueRef:cb865487-9139-3fbc-4aac-68abdb663925'
'OpaqueRef:c8bf31d6-9888-4256-1547-c722016a0079'
'OpaqueRef:c651901b-0944-be6b-aabf-a87d9a037edd'
'OpaqueRef:c5a9e2de-1916-7f4c-aa2a-ce95d138032b'
'OpaqueRef:c22bce1f-16a0-7745-179d-dcbd5c5deab3'
'OpaqueRef:be6abc7d-dd7a-5ee6-9c95-8e562a69d992'
'OpaqueRef:b9587bb6-6efe-0c71-e01c-2c750c9ab774'
'OpaqueRef:b6f58482-8b60-b3b4-2a01-0d6113411bf2'
'OpaqueRef:ad21fbbb-6cf9-e6ca-c415-1f428f20da1f'
'OpaqueRef:aa2d04ec-0512-c128-8820-c8ecde93baa4'
'OpaqueRef:a247a02f-8909-5044-64a0-82460b25e740'
'OpaqueRef:9d28dba9-aee6-cafd-06af-54ebdfb1c271'
'OpaqueRef:9796cc01-6640-211f-09f9-fee94f9cd720'
'OpaqueRef:922b3a98-f238-4cea-8b75-c38e90ac11ee'
'OpaqueRef:8e720505-e75b-eda3-3b14-fd1471890cc1'
'OpaqueRef:8e3211dc-fdaf-22c7-41b2-c3a892529679'
'OpaqueRef:89919714-1184-ce4b-3cb5-67059640b3a7'
'OpaqueRef:892768c0-4d15-769f-e760-b781a0291ddb'
'OpaqueRef:838ff163-ae6e-d98e-9cef-4d783f81dcb0'
'OpaqueRef:8079d64b-fe87-0ecf-e558-7b607b0e1524'
'OpaqueRef:773d92c9-898b-bc25-a50d-d868bbf933a4'
'OpaqueRef:770d2193-ab69-4fc3-c462-7f75a79d497c'
'OpaqueRef:75441e00-55df-85f5-1780-731110df91de'
'OpaqueRef:6ee1cc24-ebbb-b02a-88b0-a921c7a5f217'
'OpaqueRef:6b5be573-b116-6238-9cff-bde0658d6f18'
'OpaqueRef:6a09a6de-e778-a474-4ebd-f617db5b5d5e'
'OpaqueRef:616942c0-1e1b-e733-3c4c-7236fd3de158'
'OpaqueRef:5e93cf73-a212-a83f-d3f9-a539be98d320'
'OpaqueRef:56af2e14-d4bb-20e9-421b-00d75dfb89f2'
'OpaqueRef:5059cc2d-b414-97eb-6aac-ce816b72b2bd'
'OpaqueRef:4a43ad28-b809-2c8f-aa24-70d8bd4954f2'
'OpaqueRef:466d7dc3-f2df-8c8d-685d-eef256fe2b43'
'OpaqueRef:4347e9d6-7faf-90e4-4f5f-d513cf44b3cc'
'OpaqueRef:3c4558e8-ed88-ce88-81a9-111ac2cc56d6'
'OpaqueRef:3b97e45b-aa4e-d175-95e5-e95ceefa0b6b'
'OpaqueRef:2e3b5ada-5083-87b1-d6fb-aaa0e5bd862d'
'OpaqueRef:2b6e3248-52b0-85d1-7415-4f91a0a90a3a'
'OpaqueRef:2a838052-3aa3-d09d-1eae-8293a565fef5'
'OpaqueRef:2a092eee-7c6a-058b-0368-b37362328678'
'OpaqueRef:2968283f-8656-6e31-816c-e96325e66ebf'
'OpaqueRef:27ad4e06-a7b2-20a2-4fd9-7f1b54fdc5a2'
'OpaqueRef:217d930f-8e65-14e6-eb20-63d55158093f'
'OpaqueRef:20377446-2388-5c8f-d3f2-6e9c883c61d9'
'OpaqueRef:201cf416-bfd0-00d3-a4d2-b19226c43c82'
'OpaqueRef:1ed4ee31-56e0-98da-65d4-00c776716b9c'
'OpaqueRef:1c0b590d-563b-5061-a253-f98535ab8389'
'OpaqueRef:1be0fe3b-1944-06db-3734-b6bb888cfe78'
'OpaqueRef:12d0dfc0-ce63-a072-3cd0-ccba7bd3c200'
'OpaqueRef:039273c3-b4b2-5c68-63e4-c5610a738fe3'
'OpaqueRef:030314a2-0909-9e7a-418a-9f38746aaf0c',
]
$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'
]
$expect(pool.messages).to.have.members [
'OpaqueRef:0241d2be-fcda-64b7-b95d-550399f22000'
'OpaqueRef:08093780-5d87-46f4-400d-fc8406bdd6c2'
'OpaqueRef:0c565205-db69-eb0f-b80b-a8e356ae43ae'
'OpaqueRef:0f955091-d6e6-ed3e-2bbe-94d914e6efbe'
'OpaqueRef:15f61c91-5ac8-6234-78bb-2edbdcf9164f'
'OpaqueRef:1b04b4db-3622-4d54-e8fa-a2f6661c6e43'
'OpaqueRef:20aadafb-47c8-0796-e3c2-4e497bcb0205'
'OpaqueRef:2243e321-e4bd-50dd-1451-f329df240517'
'OpaqueRef:226e9274-77d6-9805-a0f3-396d1e54fe72'
'OpaqueRef:230d01c6-3e25-b877-9e35-13a707335e23'
'OpaqueRef:279e9aed-7d9e-13bc-e4d2-d477abbf9f6a'
'OpaqueRef:2c460c86-2e1c-cd0d-cbaf-95bf771af2bc'
'OpaqueRef:300a2868-2b8a-4f0c-788d-4e2ba4a160da'
'OpaqueRef:323297f9-4a0b-c517-1ff7-eacad80fc796'
'OpaqueRef:33d58ecd-d2a4-f63a-46bb-307a7c7762a6'
'OpaqueRef:3962ad4b-18e9-53ce-ff72-b2ef3d6692ec'
'OpaqueRef:3a8a42d6-f5b3-1479-3ad6-2c7caed94459'
'OpaqueRef:3f77ad7a-de22-0b05-4005-7cfdc5d8bc86'
'OpaqueRef:4147a60c-2b41-4dc7-491d-3470466abbc7'
'OpaqueRef:443c4e46-d98a-87d6-92f5-c35bb5b65a5c'
'OpaqueRef:4a3aebd9-e670-c796-4938-e29e178f1959'
'OpaqueRef:50f02c5f-b2d0-a42a-a156-7905b78a918a'
'OpaqueRef:5f34bfc5-f92f-9830-b3e9-06367ef56a77'
'OpaqueRef:69d3511e-ec73-69c9-819e-14b85236059d'
'OpaqueRef:6b04d423-8991-c838-d981-aca1b9c7be7d'
'OpaqueRef:6e161f6f-df2b-195f-be46-530884a2c24a'
'OpaqueRef:6f9b4c87-c7ba-1a87-073d-569051f307a8'
'OpaqueRef:72360954-3629-1e09-b1bf-b819732bddfd'
'OpaqueRef:79f9e82b-1a0e-75b7-efc5-8689a4cd4aed'
'OpaqueRef:844844c6-5e82-4d9c-7ed9-01c46d46e67c'
'OpaqueRef:84a7efe6-2a37-d4be-5f9a-aa66adfe3104'
'OpaqueRef:9a645810-7308-c296-d9df-cc5d91f8f2a4'
'OpaqueRef:a073f53c-557a-fd67-878d-b3a881ebd935'
'OpaqueRef:a08f1c9a-34de-5441-b847-18533244910d'
'OpaqueRef:a4fd777c-f417-23e9-8338-30d8097a8430'
'OpaqueRef:a5296901-25c3-b600-7be7-16a20ba86600'
'OpaqueRef:a99badbe-75fa-8bc8-22b3-78c616873b62'
'OpaqueRef:ab16dfa7-3c86-56c3-038c-c6bcfe0b64c1'
'OpaqueRef:af840b26-91b6-56aa-e2a0-266ce7dd411b'
'OpaqueRef:b857ac11-36a0-38e4-4d9c-13586e381f7a'
'OpaqueRef:c0b26952-1a46-9dfb-a826-78cbfeaa1b00'
'OpaqueRef:cdeda917-3496-c407-95fd-2ef63bf5e79e'
'OpaqueRef:d5ab7d13-0ebb-5805-b767-608cb7737690'
'OpaqueRef:dae9fbe3-a709-3433-e8e3-491b3a79df84'
'OpaqueRef:dd735a0f-d2fd-9475-7dd3-b387251f4426'
'OpaqueRef:df07d60e-8a03-6979-3e61-4460bc8197b3'
'OpaqueRef:e6a0aa45-f8e0-ae7d-7b3a-d76b95a03c95'
'OpaqueRef:eaad760a-0e23-4e2b-3f96-2f65170a1dd7'
'OpaqueRef:ebead5cf-4a48-ad28-4241-ad5869fa9752'
'OpaqueRef:ecc7b91d-6f50-94c6-6f51-2d609dc3ebe7'
'OpaqueRef:f3492f88-e0b0-405a-5723-f83429e016c5'
]
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 2564788224
$expect(host.memory.size).to.equal 8502759424
$expect(host.power_state).to.equal 'Running'
$expect(host.SRs).to.have.members [
'OpaqueRef:31be9b5e-882a-a8ae-0edf-bf8942b49b5a'
'OpaqueRef:7c88a8c6-fc48-8836-28fa-212f67c42d2f'
'OpaqueRef:ec76bd6a-f2c0-636d-ca72-de8fb42d6eea'
]
$expect(host.templates).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 2
it 'VM', ->
vm = collection.get 'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
#console.log vm
$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.addresses).to.deep.equal {
'0/ip': '192.168.1.116'
'0/ipv6/0': 'fe80::cc20:2bff:fe38:7ffd'
}
$expect(vm.consoles).to.deep.equal [
{
uuid: 'b7f85b67-4b8a-0586-b279-6146da76642f'
protocol: 'rfb'
location: 'https://192.168.1.1/console?uuid=b7f85b67-4b8a-0586-b279-6146da76642f'
VM: 'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
other_config: {}
'$pool': '313624ab-0958-bb1e-45b5-7556a463a10b'
'$poolRef': 'OpaqueRef:6462d0b3-8f20-ef76-fddf-002f7af3452e'
'$ref': 'OpaqueRef:69b8dbde-161c-b3fa-bd1a-3567e7efdbda'
'$type': 'console'
}
]
$expect(vm.current_operations).to.deep.equal {
# No data for this test.
}
$expect(vm.memory).to.deep.equal {
dynamic: [
536870912
536870912
]
static: [
134217728
536870912
]
size: 536838144
}
$expect(vm.messages).to.have.members [
'OpaqueRef:a242799a-03bf-b55e-ecde-ddfe902fa69e'
'OpaqueRef:5cec485b-e276-c45b-09cb-dd02bb1d00f3'
'OpaqueRef:ff3b6df1-b761-0d75-e80e-4ef137eec9e6'
'OpaqueRef:a8d94d7e-7a6e-0cc1-b7a0-8f18940410fd'
'OpaqueRef:35585a79-caf7-6522-18ee-8d3e8459441d'
'OpaqueRef:68d1102f-eadc-e1f3-7949-3f62248c165c'
'OpaqueRef:974bef10-184a-c063-aa32-c318fd39e400'
'OpaqueRef:e092c4e1-a211-204a-f773-49cc3a4611be'
'OpaqueRef:013a4a12-1981-fbc8-92ac-1fa45d2e9c9c'
'OpaqueRef:a77fc714-b5b1-0c37-d006-0935506bb8cd'
'OpaqueRef:554ec983-e67a-fc8b-7d2a-00c55be5f266'
'OpaqueRef:38404a18-4c1b-0bf5-1d45-c47243bbc69d'
'OpaqueRef:0f98e883-a4d5-0fd8-3aa3-92be69adc4e3'
'OpaqueRef:b3e9ac53-f6b8-4c49-f096-57f680136477'
'OpaqueRef:1aa65d64-a00b-4c0b-be07-95f6eec7fd87'
'OpaqueRef:be431f8c-f39b-4a64-5fc2-de9744ced26a'
'OpaqueRef:0e571611-6194-6ce6-bae0-94bbe57576c6'
'OpaqueRef:114fdd8a-844c-6bb5-0855-e3427bc8f073'
'OpaqueRef:a486606c-1c75-e1c3-56de-c6e1bc3df980'
'OpaqueRef:b6975094-843e-a19a-6101-ee7953e40580'
'OpaqueRef:f15d7d4c-32d1-45e1-5f6f-ddc68733bab6'
'OpaqueRef:1b04b1a2-e8b2-df82-6618-0d0a741d8bbb'
'OpaqueRef:dcd41e75-47fc-5ae5-1d59-5176a7b76eaa'
'OpaqueRef:71ed5eba-33c9-6deb-6dc2-ab670a6c968b'
'OpaqueRef:59ee665c-9270-64a4-3829-aef3e045a705'
'OpaqueRef:88979f4b-16ef-3b99-a616-aa1e2787bebe'
'OpaqueRef:80a3e419-5a81-a7df-103d-5cf60bbde793'
'OpaqueRef:38737284-e4e1-5172-2bf3-f9d70dcaadfa'
'OpaqueRef:456d4d7f-77f8-ef40-aadd-f56601bc7c2b'
'OpaqueRef:4a949518-cc01-a003-f386-b3319db6d7a6'
'OpaqueRef:c8834c52-f15b-437d-1e09-958fedbf3c5b'
'OpaqueRef:07d40d2c-4f6e-4f5f-0c3e-c2ea028d4fc4'
'OpaqueRef:6df45555-1b11-2873-8947-2b6e7c9445be'
'OpaqueRef:d3c60e69-2cf8-191f-9679-d6ae0ecdf5f9'
'OpaqueRef:ed499671-2c01-3dc9-f6cd-553fef4b6716'
]
$expect(vm.power_state).to.equal 'Running'
$expect(vm.CPUs).to.deep.equal {
max: 1
number: 1
}
$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 null
$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 [
{
bootable: true
device: '0'
size: 8589934592
SR: ''
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'
]
$expect(sr.messages).to.have.members [
# No data for this test.
]
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.is_cd_drive).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', ->
message = collection.get 'OpaqueRef:cb515b9a-ef8c-13d4-88ea-e0d3ee88d22a'
#console.log message
$expect(message.type).to.equal 'message'
$expect(message.time).to.equal 1389449056
$expect(message.$object).to.equal 'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
$expect(message.name).to.equal 'PBD_PLUG_FAILED_ON_SERVER_START'
$expect(message.body).to.equal ''
it 'task', ->
all = collection.get()
for object in all
if object.type is 'task'
console.log object
# FIXME: we need to update the tests data to complete this test.

493
src/xapi-objects-to-xo.js Normal file
View File

@ -0,0 +1,493 @@
import forEach from 'lodash.foreach'
import isArray from 'lodash.isarray'
import map from 'lodash.map'
import {
ensureArray,
extractProperty,
parseXml
} from './utils'
import {
isHostRunning,
isVmRunning
} from './xapi'
// ===================================================================
function link (obj, prop) {
const dynamicValue = obj[`$${prop}`]
if (dynamicValue == null) {
return dynamicValue // Properly handles null and undefined.
}
if (isArray(dynamicValue)) {
return map(dynamicValue, '$id')
}
return dynamicValue.$id
}
function toTimestamp (date) {
// Weird behavior from the XAPI.
if (!date || date === '1969-12-31T23:00:00.000Z') {
return null
}
return Math.round(Date.parse(date) / 1000)
}
// ===================================================================
export function pool (obj) {
return {
default_SR: link(obj, 'default_SR'),
HA_enabled: obj.ha_enabled,
master: link(obj, 'master'),
name_description: obj.name_description,
name_label: obj.name_label
// TODO
// - ? networks = networksByPool.items[pool.id] (network.$pool.id)
// - hosts = hostsByPool.items[pool.id] (host.$pool.$id)
// - patches = poolPatchesByPool.items[pool.id] (poolPatch.$pool.id)
// - SRs = srsByContainer.items[pool.id] (sr.$container.id)
// - templates = vmTemplatesByContainer.items[pool.id] (vmTemplate.$container.$id)
// - VMs = vmsByContainer.items[pool.id] (vm.$container.id)
// - $running_hosts = runningHostsByPool.items[pool.id] (runningHost.$pool.id)
// - $running_VMs = runningVmsByPool.items[pool.id] (runningHost.$pool.id)
// - $VMs = vmsByPool.items[pool.id] (vm.$pool.id)
}
}
// -------------------------------------------------------------------
export function host (obj) {
const {
$metrics: metrics,
other_config: otherConfig
} = obj
const isRunning = isHostRunning(obj)
return {
address: obj.address,
bios_strings: obj.bios_strings,
build: obj.software_version.build_number,
CPUs: obj.cpu_info,
enabled: obj.enabled,
current_operations: obj.current_operations,
hostname: obj.hostname,
iSCSI_name: otherConfig.iscsi_iqn || null,
name_description: obj.name_description,
name_label: obj.name_label,
memory: (function () {
if (metrics) {
const free = +metrics.memory_free
const total = +metrics.memory_total
return {
usage: total - free,
size: total
}
}
return {
usage: 0,
total: 0
}
})(),
patches: link(obj, 'patches'),
power_state: isRunning ? 'Running' : 'Halted',
version: obj.software_version.product_version,
// TODO: dedupe.
PIFs: link(obj, 'PIFs'),
$PIFs: link(obj, 'PIFs'),
PCIs: link(obj, 'PCIs'),
$PCIs: link(obj, 'PCIs'),
PGPUs: link(obj, 'PGPUs'),
$PGPUs: link(obj, 'PGPUs'),
$PBDs: link(obj, 'PBDs')
// TODO:
// - controller = vmControllersByContainer.items[host.id]
// - SRs = srsByContainer.items[host.id]
// - tasks = tasksByHost.items[host.id]
// - templates = vmTemplatesByContainer.items[host.id]
// - VMs = vmsByContainer.items[host.id]
// - $vCPUs = sum(host.VMs, vm => host.CPUs.number)
}
}
// -------------------------------------------------------------------
export function vm (obj) {
const {
$guest_metrics: guestMetrics,
$metrics: metrics,
other_config: otherConfig
} = obj
const isRunning = isVmRunning(obj)
const vm = {
// type is redefined after for controllers/, templates &
// snapshots.
type: 'VM',
addresses: guestMetrics && guestMetrics.networks || null,
auto_poweron: Boolean(otherConfig.auto_poweron),
boot: obj.HVM_boot_params,
CPUs: {
max: +obj.VCPUs_max,
number: (
isRunning && metrics ?
+metrics.VCPUs_number :
+obj.VCPUs_at_startup
)
},
current_operations: obj.current_operations,
docker: (function () {
const monitor = otherConfig['xscontainer-monitor']
if (!monitor) {
return
}
if (monitor === 'False') {
return {
enabled: false
}
}
const {
docker_ps: process,
docker_info: info,
docker_version: version
} = otherConfig
return {
enabled: true,
info: info && parseXml(info).docker_info,
process: process && parseXml(process).docker_ps,
version: version && parseXml(version).version
}
})(),
// TODO: there is two possible value: "best-effort" and "restart"
high_availability: Boolean(obj.ha_restart_priority),
memory: (function () {
const dynamicMin = +obj.memory_dynamic_min
const dynamicMax = +obj.memory_dynamic_max
const staticMin = +obj.memory_static_min
const staticMax = +obj.memory_static_max
const memory = {
dynamic: [ dynamicMin, dynamicMax ],
static: [ staticMin, staticMax ]
}
const gmMemory = guestMetrics && guestMetrics.memory
if (!isRunning) {
memory.size = dynamicMax
} else if (gmMemory) {
memory.usage = +gmMemory.used
memory.size = +gmMemory.total
} else if (metrics) {
memory.size = +metrics.memory_actual
} else {
memory.size = dynamicMax
}
return memory
})(),
name_description: obj.name_description,
name_label: obj.name_label,
other: otherConfig,
os_version: guestMetrics && guestMetrics.os_version || null,
power_state: obj.power_state,
PV_drivers: Boolean(guestMetrics && guestMetrics.PV_drivers_up_to_date),
snapshot_time: toTimestamp(obj.snapshot_time),
snapshots: link(obj, 'snapshots'),
VIFs: link(obj, 'VIFs'),
$container: (
isRunning ?
link(obj, 'resident_on') :
link(obj, 'pool') // TODO: handle local VMs (`VM.get_possible_hosts()`).
),
$VBDs: link(obj, 'VBDs'),
// TODO: dedupe
VGPUs: link(obj, 'VGPUs'),
$VGPUs: link(obj, 'VGPUs')
}
if (obj.is_control_domain) {
vm.type += '-controller'
} else if (obj.is_a_snapshot) {
vm.type += '-snapshot'
vm.$snapshot_of = link(obj, 'snapshot_of')
} else if (obj.is_a_template) {
vm.type += '-template'
vm.CPUs.number = +obj.VCPUs_at_startup
vm.template_info = {
arch: otherConfig['install-arch'],
disks: (function () {
const {xml} = otherConfig
if (!xml) {
return []
}
const disks = ensureArray(parseXml(xml).provision.disk)
forEach(disks, function normalize (disk) {
disk.bootable = disk.bootable === 'true'
disk.size = +disk.size
disk.SR = extractProperty(disk, 'sr')
})
return disks
})(),
install_methods: (function () {
const {['install-methods']: methods} = otherConfig
return methods ? methods.split(',') : []
})()
}
}
return vm
}
// -------------------------------------------------------------------
export function sr (obj) {
return {
type: 'SR',
content_type: obj.content_type,
name_description: obj.name_description,
name_label: obj.name_label,
physical_usage: +obj.physical_utilisation,
size: +obj.physical_size,
SR_type: obj.type,
usage: +obj.virtual_allocation,
VDI: link(obj, 'VDIs'),
$container: (
obj.shared ?
link(obj, 'pool') :
// FIXME: find host for which host.PBDs includes obj.$ref
null
),
$PBDs: link(obj, 'PBDs')
}
}
// -------------------------------------------------------------------
export function pbd (obj) {
return {
type: 'PBD',
attached: obj.currently_attached,
host: link(obj, 'host'),
SR: link(obj, 'SR')
}
}
// -------------------------------------------------------------------
export function pif (obj) {
return {
type: 'PIF',
attached: obj.currently_attached,
device: obj.device,
IP: obj.IP,
MAC: obj.MAC,
management: obj.management, // TODO: find a better name.
mode: obj.ip_configuration_mode,
MTU: +obj.MTU,
netmask: obj.netmask,
vlan: +obj.VLAN,
// TODO: What is it?
//
// Could it mean “is this a physical interface?”.
// How could a PIF not be physical?
// physical: obj.physical,
$host: link(obj, 'host'),
$network: link(obj, 'network')
}
}
// -------------------------------------------------------------------
// TODO: should we have a VDI-snapshot type like we have with VMs?
export function vdi (obj) {
return {
type: 'VDI',
name_description: obj.name_description,
name_label: obj.name_label,
size: +obj.virtual_size,
snapshots: link(obj, 'snapshots'),
snapshot_time: toTimestamp(obj.snapshot_time),
usage: +obj.physical_utilisation,
$snapshot_of: link(obj, 'snapshot_of'),
$SR: link(obj, 'SR'),
$VBDs: link(obj, 'VBDs')
}
}
// -------------------------------------------------------------------
export function vbd (obj) {
return {
type: 'VBD',
attached: obj.currently_attached,
bootable: obj.bootable,
is_cd_drive: obj.type === 'CD',
position: obj.userdevice,
read_only: obj.mode === 'RO',
VDI: link(obj, 'VDI'),
VM: link(obj, 'VM')
}
}
// -------------------------------------------------------------------
export function vif (obj) {
return {
type: 'VIF',
attached: obj.currently_attached,
device: obj.device, // TODO: should it be cast to a number?
MAC: obj.MAC,
MTU: +obj.MTU,
$network: link(obj, 'network'),
$VM: link(obj, 'VM')
}
}
// -------------------------------------------------------------------
export function network (obj) {
return {
bridge: obj.bridge,
MTU: +obj.MTU,
name_description: obj.name_description,
name_label: obj.name_label,
PIFs: link(obj, 'PIFs'),
VIFs: link(obj, 'VIFs')
}
}
// -------------------------------------------------------------------
export function message (obj) {
return {
body: obj.body,
name: obj.name,
time: toTimestamp(obj.timestamp),
$object: obj.obj_uuid // Special link as it is already an UUID.
}
}
// -------------------------------------------------------------------
export function task (obj) {
return {
created: toTimestamp(obj.created),
current_operations: obj.current_operations,
finished: toTimestamp(obj.finished),
name_description: obj.name_description,
name_label: obj.name_label,
progress: +obj.progress,
result: obj.result,
status: obj.status,
$host: link(obj, 'resident_on')
}
}
// -------------------------------------------------------------------
export function host_patch (obj) {
return {
applied: obj.applied,
time: toTimestamp(obj.timestamp_applied),
pool_patch: link(obj, 'pool_patch'),
$host: link(obj, 'host')
}
}
// -------------------------------------------------------------------
export function pool_patch (obj) {
return {
applied: obj.pool_applied,
name_description: obj.name_description,
name_label: obj.name_label,
size: +obj.size,
version: obj.version,
// TODO: host.[$]pool_patches ←→ pool.[$]host_patches
$host_patches: link(obj, 'host_patches')
}
}
// -------------------------------------------------------------------
export function pci (obj) {
return {
type: 'PCI',
class_name: obj.class_name,
device_name: obj.device_name,
pci_id: obj.pci_id,
$host: link(obj, 'host')
}
}
// -------------------------------------------------------------------
export function pgpu (obj) {
return {
type: 'PGPU',
pci: link(obj, 'PCI'),
// TODO: dedupe.
host: link(obj, 'host'),
$host: link(obj, 'host'),
vgpus: link(obj, 'resident_VGPUs'),
$vgpus: link(obj, 'resident_VGPUs')
}
}
// -------------------------------------------------------------------
export function vgpu (obj) {
return {
type: 'VGPU',
currentlyAttached: obj.currently_attached,
device: obj.device,
resident_on: link(obj, 'resident_on'),
vm: link(obj, 'VM')
}
}

View File

@ -61,11 +61,17 @@ const getNamespaceForType = (type) => typeToNamespace[type] || type
// ===================================================================
export const isHostRunning = (host) => {
const {$metrics: metrics} = host
return metrics && metrics.live
}
const VM_RUNNING_POWER_STATES = {
Running: true,
Paused: true
}
const isVmRunning = (vm) => VM_RUNNING_POWER_STATES[vm.power_state]
export const isVmRunning = (vm) => VM_RUNNING_POWER_STATES[vm.power_state]
// ===================================================================
@ -76,23 +82,13 @@ export default class Xapi extends XapiBase {
const objectsWatchers = this._objectWatchers = Object.create(null)
const taskWatchers = this._taskWatchers = Object.create(null)
// TODO: This is necessary to get UUIDs for host.patches.
//
// It will no longer be useful when using xen-api >= 0.5.
this._refsToUuids = Object.create(null)
const onAddOrUpdate = objects => {
forEach(objects, object => {
const {
$id: id,
$ref: ref,
uuid
$ref: ref
} = object
if (ref && uuid) {
this._refsToUuids[ref] = uuid
}
// Watched object.
if (id in objectsWatchers) {
objectsWatchers[id].resolve(object)
@ -123,56 +119,6 @@ export default class Xapi extends XapiBase {
this.objects.on('update', onAddOrUpdate)
}
// FIXME: remove this backported methods when xen-api >= 0.5
getObject (idOrUuidOrRef, defaultValue) {
const {_objects: {all: objects}} = this
const object = (
// if there is an UUID, it is also the $id.
objects[idOrUuidOrRef] ||
objects[this._refsToUuids[idOrUuidOrRef]]
)
if (object) return object
if (arguments.length > 1) return defaultValue
throw new Error('no object can be matched to ' + idOrUuidOrRef)
}
getObjectByRef (ref, defaultValue) {
const {
_refsToUuids: refsToUuids,
// Objects ids are already UUIDs if they have one.
_objects: {all: objectsByUuids}
} = this
if (ref in refsToUuids) {
return objectsByUuids[refsToUuids[ref]]
}
if (arguments.length > 1) {
return defaultValue
}
throw new Error('there is no object with the ref ' + ref)
}
getObjectByUuid (uuid, defaultValue) {
const {
// Objects ids are already UUIDs if they have one.
_objects: {all: objectsByUuids}
} = this
if (uuid in objectsByUuids) {
return objectsByUuids[uuid]
}
if (arguments.length > 1) {
return defaultValue
}
throw new Error('there is no object with the UUID ' + uuid)
}
// =================================================================
// Wait for an object to appear or to be updated.
@ -366,10 +312,8 @@ export default class Xapi extends XapiBase {
const all = (await this._getXenUpdates()).versions[version].patches
const installed = Object.create(null)
// TODO: simplify when we start to use xen-api >= 0.5
forEach(host.patches, ref => {
const hostPatch = this.objects.all[this._refsToUuids[ref]]
installed[this._refsToUuids[hostPatch.pool_patch]] = true
forEach(host.$patches, hostPatch => {
installed[hostPatch.$pool_patch.uuid] = true
})
const installable = []
@ -395,11 +339,8 @@ export default class Xapi extends XapiBase {
async uploadPoolPatch (stream, length) {
const task = await this._createTask('Patch upload')
// TODO: Update when xen-api >= 0.5
const poolMaster = this.objects.all[this._refsToUuids[this.pool.master]]
const [, patchRef] = await Promise.all([
gotPromise('http://' + poolMaster.address + '/pool_patch_upload', {
gotPromise('http://' + this.pool.$master.address + '/pool_patch_upload', {
method: 'put',
body: stream,
query: {
@ -489,10 +430,9 @@ export default class Xapi extends XapiBase {
}
if (deleteDisks) {
// TODO: simplify when we start to use xen-api >= 0.5
await Promise.all(map(vm.VBDs, ref => {
await Promise.all(map(vm.$VBDs, vbd => {
try {
return this._deleteVdi(this.getObject(ref).VDI).catch(noop)
return this._deleteVdi(vbd.$VDI).catch(noop)
} catch (_) {}
}))
}
@ -507,10 +447,10 @@ export default class Xapi extends XapiBase {
let host
let snapshotRef
if (isVmRunning(vm)) {
host = this.getObject(vm.resident_on)
host = vm.$resident_on
snapshotRef = await this._snapshotVm(vm)
} else {
host = this.getObject(this.pool.master)
host = this.pool.$master
}
const task = await this._createTask('VM Snapshot', vm.name_label)
@ -548,7 +488,7 @@ export default class Xapi extends XapiBase {
async _doDockerAction (vmId, action, containerId) {
const vm = this.getObject(vmId)
const host = this.getObject(vm.resident_on)
const host = vm.$resident_on
return await this.call('host.call_plugin', host.$ref, 'xscontainer', action, {
vmuuid: vm.uuid,

279
src/xo.js
View File

@ -1,54 +1,50 @@
import Bluebird from 'bluebird'
import createDebug from 'debug'
import forEach from 'lodash.foreach'
import includes from 'lodash.includes'
import isEmpty from 'lodash.isempty'
import isString from 'lodash.isstring'
import pluck from 'lodash.pluck'
import proxyRequest from 'proxy-http-request'
// import XoCollection from 'xo-collection'
import XoCollection from 'xo-collection'
import XoUniqueIndex from 'xo-collection/unique-index'
// import XoView from 'xo-collection/view'
import {createClient as createRedisClient} from 'then-redis'
import {EventEmitter} from 'events'
import {parse as parseUrl} from 'url'
import {ModelAlreadyExists} from './collection'
import * as xapiObjectsToXo from './xapi-objects-to-xo'
import Connection from './connection'
import spec from './spec'
import User, {Users} from './models/user'
import Xapi from './xapi'
import {$MappedCollection as MappedCollection} from './MappedCollection'
import {Acls} from './models/acl'
import {autobind} from './decorators'
import {generateToken} from './utils'
import {JsonRpcError, NoSuchObject, Unauthorized} from './api-errors'
import {ModelAlreadyExists} from './collection'
import {Servers} from './models/server'
import {Tokens} from './models/token'
// ===================================================================
const debug = createDebug('xo:xo')
// ===================================================================
class NoSuchAuthenticationToken extends NoSuchObject {
constructor (id) {
super({
type: 'authentication token',
id
})
super(id, 'authentication token')
}
}
class NoSuchUser extends NoSuchObject {
constructor (id) {
super({
type: 'user',
id
})
super(id, 'user')
}
}
class NoSuchXenServer extends NoSuchObject {
constructor (id) {
super({
type: 'xen server',
id
})
super(id, 'xen server')
}
}
@ -70,7 +66,8 @@ export default class Xo extends EventEmitter {
constructor () {
super()
// this._objects = new XoCollection()
this._objects = new XoCollection()
this._objects.createIndex('byRef', new XoUniqueIndex('ref'))
// These will be initialized in start()
//
@ -88,17 +85,14 @@ export default class Xo extends EventEmitter {
this._nextConId = 0
this._connections = Object.create(null)
// Collections of XAPI objects mapped to XO Api.
this._xobjs = new MappedCollection()
spec.call(this._xobjs)
this._watchXobjs()
this._httpRequestWatchers = Object.create(null)
// TODO: remove when no longer necessary.
this._proxyRequests = Object.create(null)
this._authenticationProviders = new Set()
this._watchObjects()
}
// -----------------------------------------------------------------
@ -146,9 +140,6 @@ export default class Xo extends EventEmitter {
}
}.bind(this))
// Exports the map from UUIDs to keys.
this._UUIDsToKeys = this._xobjs.get('xo').$UUIDsToKeys
// Connects to existing servers.
for (let server of await this._servers.get()) {
this.connectXenServer(server.id).catch(error => {
@ -299,6 +290,45 @@ export default class Xo extends EventEmitter {
return server
}
@autobind
_onXenAdd (xapiObjects) {
const {_objects: objects} = this
forEach(xapiObjects, (xapiObject, id) => {
const transform = xapiObjectsToXo[xapiObject.$type]
if (!transform) {
return
}
const xoObject = transform(xapiObject)
xoObject.id = id
xoObject.ref = xapiObject.$ref
if (!xoObject.type) {
xoObject.type = xapiObject.$type
}
const {$pool: pool} = xapiObject
Object.defineProperties(xoObject, {
poolRef: { value: pool.$ref },
$poolId: {
enumerable: true,
value: pool.$id
}
})
objects.set(id, xoObject)
})
}
@autobind
_onXenRemove (xapiObjects) {
const {_objects: objects} = this
forEach(xapiObjects, (_, id) => {
if (objects.has(id)) {
objects.remove(id)
}
})
}
// TODO the previous state should be marked as connected.
async connectXenServer (id) {
const server = (await this._getXenServer(id)).properties
@ -312,29 +342,14 @@ export default class Xo extends EventEmitter {
})
const {objects} = xapi
objects.on('add', objects => {
this._xapis[xapi.pool.$id] = xapi
objects.on('add', this._onXenAdd)
objects.on('update', this._onXenAdd)
objects.on('remove', this._onXenRemove)
this._xobjs.set(objects, {
add: true,
update: false,
remove: false
})
})
objects.on('update', objects => {
// Each time objects are refreshed, registers the connection with
// the pool identifier.
objects.on('finish', () => {
this._xapis[xapi.pool.$id] = xapi
this._xobjs.set(objects, {
add: true,
update: true,
remove: false
})
})
objects.on('remove', objects => {
this._xobjs.removeWithPredicate(object => (
object.genval &&
object.genval.$id in objects
))
})
try {
@ -390,40 +405,50 @@ export default class Xo extends EventEmitter {
//
// TODO: should throw a NoSuchObject error on failure.
getObject (key, type) {
// Gracefully handles UUIDs.
if (key in this._UUIDsToKeys) {
key = this._UUIDsToKeys[key]
}
const {
all,
indexes: {
byRef
}
} = this._objects
const obj = this._xobjs.get(key)
const obj = all[key] || byRef[key]
if (!obj) {
throw new NoSuchObject(key)
}
if (type != null && (
isString(type) && type !== obj.type ||
!includes(type, obj.type) // Array
)) {
throw new Error(`unexpected type ${obj.type} instead of ${type}`)
throw new NoSuchObject(key, type)
}
return obj
}
getObjects (keys) {
const {
all,
indexes: {
byRef
}
} = this._objects
// Returns all objects if no keys have been passed.
if (!keys) {
return this._xobjs.get()
}
// Resolves all UUIDs.
const {_UUIDsToKeys: UUIDsToKeys} = this
for (let i = 0, n = keys.length; i < n; ++i) {
const key = UUIDsToKeys[keys[i]]
if (key != null) {
keys[i] = key
}
return all
}
// Fetches all objects and ignores those missing.
return this._xobjs.get(keys, true)
const result = []
forEach(keys, key => {
const object = all[key] || byRef[key]
if (object) {
result.push(object)
}
})
return result
}
// -----------------------------------------------------------------
@ -615,100 +640,56 @@ export default class Xo extends EventEmitter {
_objects: objects
} = this
const publicObjects = new XoView(objects, isObjectPublic)
publicObjects.on('add', objects => {
let entered, exited
function reset () {
entered = Object.create(null)
exited = Object.create(null)
}
reset()
})
publicObjects.on('update', objects => {
})
publicObjects.on('remove', objects => {
function onAdd (items) {
forEach(items, (item, id) => {
entered[id] = item
})
}
objects.on('add', onAdd)
objects.on('update', onAdd)
objects.on('remove', (items) => {
forEach(items, (_, id) => {
// We don't care about the value here, so we choose `0`
// because it is small in JSON.
exited[id] = 0
})
})
const persistentObjects = new XoView(objects, isObjectPersistent)
persistentObjects.on('add', objects => {
objects.on('finish', () => {
const enteredMessage = !isEmpty(entered) && {
type: 'enter',
items: entered
}
const exitedMessage = !isEmpty(exited) && {
type: 'exit',
items: exited
}
})
persistentObjects.on('update', objects => {
if (!enteredMessage && !exitedMessage) {
return
}
})
persistentObjects.on('remove', objects => {
})
}
// When objects enter or exists, sends a notification to all
// connected clients.
//
// TODO: remove when all objects are in `this._objects`.
_watchXobjs () {
const {
_connections: connections,
_xobjs: xobjs
} = this
let entered = {}
let exited = {}
let dispatcherRegistered = false
const dispatcher = Bluebird.method(() => {
dispatcherRegistered = false
if (!isEmpty(entered)) {
const enterParams = {
type: 'enter',
items: pluck(entered, 'val')
}
entered = {}
for (let id in connections) {
const connection = connections[id]
if (connection.has('user_id')) {
connection.notify('all', enterParams)
forEach(connections, connection => {
// Notifies only authenticated clients.
if (connection.has('user_id')) {
if (enteredMessage) {
connection.notify('all', enteredMessage)
}
if (exitedMessage) {
connection.notify('all', exitedMessage)
}
}
}
})
if (!isEmpty(exited)) {
const exitParams = {
type: 'exit',
items: pluck(exited, 'val')
}
exited = {}
for (let id in connections) {
const connection = connections[id]
if (connection.has('user_id')) {
connection.notify('all', exitParams)
}
}
}
})
xobjs.on('any', (event, items) => {
if (!dispatcherRegistered) {
dispatcherRegistered = true
process.nextTick(dispatcher)
}
if (event === 'exit') {
forEach(items, item => {
const {key} = item
delete entered[key]
exited[key] = item
})
} else {
forEach(items, item => {
const {key} = item
delete exited[key]
entered[key] = item
})
}
reset()
})
}
}