Compare commits
119 Commits
xen-api-v0
...
xen-api-v0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35e8dcc3be | ||
|
|
d1600fd058 | ||
|
|
1416fb0c71 | ||
|
|
2975db247d | ||
|
|
03eaa652ce | ||
|
|
eac29993d3 | ||
|
|
af2a9225b8 | ||
|
|
a24de7fe3f | ||
|
|
16a9f44d4d | ||
|
|
6fcc148105 | ||
|
|
3485cb4ec4 | ||
|
|
b2a51bd658 | ||
|
|
e5ab1dc154 | ||
|
|
6274969635 | ||
|
|
069c430346 | ||
|
|
cbcc4dd21d | ||
|
|
b4cdf4d277 | ||
|
|
716d7bfcf6 | ||
|
|
b45a169a2f | ||
|
|
720b9ef999 | ||
|
|
9b9e4dddfc | ||
|
|
7434e0352f | ||
|
|
26d61af902 | ||
|
|
5bd12c5f9e | ||
|
|
e07fae4290 | ||
|
|
e304395179 | ||
|
|
6b83130853 | ||
|
|
9565718699 | ||
|
|
ac11885379 | ||
|
|
277669a13c | ||
|
|
fcbc476462 | ||
|
|
4944b415c7 | ||
|
|
5da7312d2d | ||
|
|
954d19fe50 | ||
|
|
addd86f5d2 | ||
|
|
1b90223210 | ||
|
|
95989ff63b | ||
|
|
799f758dce | ||
|
|
e075f1c08b | ||
|
|
7e0aa719b4 | ||
|
|
4ee352fdb2 | ||
|
|
96ea3ded4a | ||
|
|
8bbc6e9ff5 | ||
|
|
af7029812c | ||
|
|
c517b59138 | ||
|
|
5485e8a322 | ||
|
|
2540ac34b3 | ||
|
|
76e5d41a34 | ||
|
|
2c32a4e912 | ||
|
|
c66f7235b6 | ||
|
|
5444381f7d | ||
|
|
dc44679031 | ||
|
|
2cbd17b745 | ||
|
|
9ef13696d8 | ||
|
|
c3f635fd12 | ||
|
|
e3d1380435 | ||
|
|
f83737b538 | ||
|
|
bb1ea4e4d0 | ||
|
|
9cb4de2ea8 | ||
|
|
048cbf60ec | ||
|
|
36f40b4188 | ||
|
|
a3bba92063 | ||
|
|
ebcc6c9341 | ||
|
|
95f765055e | ||
|
|
49aa5ffccc | ||
|
|
d09d3fa80b | ||
|
|
4c8cd50643 | ||
|
|
eee72f4f27 | ||
|
|
45f6a7cb4d | ||
|
|
8866bd8663 | ||
|
|
3f9c515f1d | ||
|
|
c92567d4fa | ||
|
|
df3c76fa72 | ||
|
|
cea4157402 | ||
|
|
29ce3bd05e | ||
|
|
b3d58f4f0c | ||
|
|
d93d234c71 | ||
|
|
7fe9ae8a04 | ||
|
|
87cf1ed7cb | ||
|
|
a0ba5c8a57 | ||
|
|
d7208a15d9 | ||
|
|
debde0c67a | ||
|
|
97db55156a | ||
|
|
9d3477d465 | ||
|
|
031af000e6 | ||
|
|
0512fac3aa | ||
|
|
4272e8196a | ||
|
|
140f9d05df | ||
|
|
9222733243 | ||
|
|
5838c56c4e | ||
|
|
1814e0a260 | ||
|
|
711c5781e6 | ||
|
|
7e8c2211d8 | ||
|
|
f0858b7d93 | ||
|
|
3af6c28ab0 | ||
|
|
5c31c7f14c | ||
|
|
2610a9c777 | ||
|
|
58cf611497 | ||
|
|
61631e405b | ||
|
|
185e0849b1 | ||
|
|
f48b9d364b | ||
|
|
e4f1a7d4c1 | ||
|
|
e02f19ff67 | ||
|
|
72a2110845 | ||
|
|
9baa415249 | ||
|
|
22b840af14 | ||
|
|
61f32d89ca | ||
|
|
3c7da93dfc | ||
|
|
5831616fac | ||
|
|
d7b6d9f124 | ||
|
|
245978e2b3 | ||
|
|
3aae60bde9 | ||
|
|
91d36122eb | ||
|
|
36c1e2cc73 | ||
|
|
4a0a09ba3e | ||
|
|
04b44cff2b | ||
|
|
8309755ee3 | ||
|
|
41a75d404c | ||
|
|
8eb63de201 |
11
packages/xen-api/.babelrc
Normal file
11
packages/xen-api/.babelrc
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"comments": false,
|
||||
"compact": true,
|
||||
"optional": [
|
||||
"es7.asyncFunctions",
|
||||
"es7.decorators",
|
||||
"es7.exportExtensions",
|
||||
"es7.functionBind",
|
||||
"runtime"
|
||||
]
|
||||
}
|
||||
2
packages/xen-api/.gitignore
vendored
2
packages/xen-api/.gitignore
vendored
@@ -1,7 +1,9 @@
|
||||
/.nyc_output/
|
||||
/bower_components/
|
||||
/dist/
|
||||
|
||||
npm-debug.log
|
||||
npm-debug.log.*
|
||||
|
||||
!node_modules/*
|
||||
node_modules/*/
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
{
|
||||
// Julien Fontanet JSHint configuration
|
||||
// https://gist.github.com/julien-f/8095615
|
||||
//
|
||||
// Changes from defaults:
|
||||
// - all enforcing options enabled (except `++`, `--`, ES3 and strict mode which is enabled automatically by Babel)
|
||||
// - single quotes
|
||||
// - all relaxing options disabled (except ES5 and ES6)
|
||||
// - environments are set to Browserify, mocha & Node.js
|
||||
//
|
||||
// See http://jshint.com/docs/ for more details
|
||||
|
||||
"maxerr" : 50, // {int} Maximum error before stopping
|
||||
|
||||
// Enforcing
|
||||
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
|
||||
"camelcase" : true, // true: Identifiers must be in camelCase
|
||||
"curly" : true, // true: Require {} for every new block or scope
|
||||
"eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"es3" : false, // true: Require ES3 compatible code (for IE < 9)
|
||||
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
|
||||
"freeze" : true, // true: Prohibit overwriting prototypes of native objects (Array, Date, ...)
|
||||
"futurehostile" : true, // true: Prohibit use of identifiers reserved for future JavaScript versions.
|
||||
"latedef" : true, // true: Require variables/functions to be defined before being used
|
||||
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
|
||||
"nocomma" : true, // true: Prohibit use of the comma operator
|
||||
"nonbsp" : true, // true: Prohibit use of non breakable spaces
|
||||
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
|
||||
"plusplus" : false, // true: Prohibit use of `++` & `--`
|
||||
"quotmark" : "single", // Quotation mark consistency:
|
||||
// false : do nothing (default)
|
||||
// true : ensure whatever is used is consistent
|
||||
// "single" : require single quotes
|
||||
// "double" : require double quotes
|
||||
"singleGroups" : true, // Prohibit unnecessary use of the grouping operator `()`
|
||||
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
|
||||
"unused" : true, // true: Require all defined variables be used
|
||||
"strict" : false, // true: Requires all functions run in ES5 Strict Mode
|
||||
"maxcomplexity" : 7, // {int} Max cyclomatic complexity per function
|
||||
"maxdepth" : 3, // {int} Max depth of nested blocks (within functions)
|
||||
"maxlen" : 80, // {int} Max number of characters per line
|
||||
"maxparams" : 4, // {int} Max number of formal params allowed per function
|
||||
"maxstatements" : 20, // {int} Max number statements per function
|
||||
|
||||
// Relaxing
|
||||
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
|
||||
"boss" : false, // true: Tolerate assignments where comparisons would be expected
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
"elision" : false, // true: Tolerate use of ES3 array elision element
|
||||
"eqnull" : false, // true: Tolerate use of `== null`
|
||||
"es5" : true, // true: Tolerate ES5 syntax
|
||||
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
|
||||
"globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
|
||||
"iterator" : false, // true: Tolerate using the `__iterator__` property
|
||||
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
|
||||
"loopfunc" : false, // true: Tolerate functions being defined in loops
|
||||
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// (ex: `for each`, multiple try/catch, function expression…)
|
||||
"noyield" : false, // true: Tolerate use of generators without `yield`s.
|
||||
"notypeof" : false, // true: Tolerate typeof comparison with unknown values.
|
||||
"proto" : false, // true: Tolerate using the `__proto__` property
|
||||
"scripturl" : false, // true: Tolerate script-targeted URLs
|
||||
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
|
||||
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
|
||||
"validthis" : false, // true: Tolerate using this in a non-constructor function
|
||||
"noyield" : false, // true: Tolerate generators without yields
|
||||
|
||||
// Environments
|
||||
"browser" : false, // Web Browser (window, document, etc)
|
||||
"browserify" : true, // Browserify (node.js code in the browser)
|
||||
"couch" : false, // CouchDB
|
||||
"devel" : false, // Development/debugging (alert, confirm, etc)
|
||||
"dojo" : false, // Dojo Toolkit
|
||||
"jquery" : false, // jQuery
|
||||
"mocha" : true, // mocha
|
||||
"mootools" : false, // MooTools
|
||||
"node" : true, // Node.js
|
||||
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
|
||||
"phantom" : false, // PhantomJS
|
||||
"prototypejs" : false, // Prototype and Scriptaculous
|
||||
"rhino" : false, // Rhino
|
||||
"worker" : false, // Web Workers
|
||||
"wsh" : false, // Windows Scripting Host
|
||||
"yui" : false, // Yahoo User Interface
|
||||
|
||||
// Custom Globals
|
||||
"globals" : {} // additional predefined global variables
|
||||
}
|
||||
5
packages/xen-api/.mocha.js
Normal file
5
packages/xen-api/.mocha.js
Normal file
@@ -0,0 +1,5 @@
|
||||
Error.stackTraceLimit = 100
|
||||
|
||||
try { require('trace') } catch (_) {}
|
||||
try { require('clarify') } catch (_) {}
|
||||
try { require('source-map-support/register') } catch (_) {}
|
||||
1
packages/xen-api/.mocha.opts
Normal file
1
packages/xen-api/.mocha.opts
Normal file
@@ -0,0 +1 @@
|
||||
--require ./.mocha.js
|
||||
@@ -1,2 +1,10 @@
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 'iojs'
|
||||
- 'stable'
|
||||
- '4'
|
||||
- '0.12'
|
||||
- '0.10'
|
||||
- '0.10'
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
sudo: false
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
# xen-api [](https://travis-ci.org/js-xen-api)
|
||||
# xen-api [](https://travis-ci.org/julien-f/js-xen-api)
|
||||
|
||||
> ${pkg.description}
|
||||
> Connector to the Xen API
|
||||
|
||||
Tested with:
|
||||
|
||||
- Xen Server 5.6
|
||||
- Xen Server 6.2
|
||||
- Xen Server 6.5
|
||||
|
||||
## Install
|
||||
|
||||
@@ -12,6 +18,8 @@ Installation of the [npm package](https://npmjs.org/package/xen-api):
|
||||
|
||||
## Usage
|
||||
|
||||
### Library
|
||||
|
||||
```javascript
|
||||
var createClient = require('xen-api').createClient
|
||||
|
||||
@@ -20,9 +28,18 @@ var xapi = createClient({
|
||||
auth: {
|
||||
user: 'root',
|
||||
password: 'important secret password'
|
||||
}
|
||||
},
|
||||
readOnly: false
|
||||
})
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `url`: address of a host in the pool we are trying to connect to
|
||||
- `auth`: credentials used to sign in
|
||||
- `readOnly = false`: if true, no methods with side-effects can be called
|
||||
|
||||
```js
|
||||
// Force connection.
|
||||
xapi.connect().catch(error => {
|
||||
console.error(error)
|
||||
@@ -34,6 +51,44 @@ xapi.objects.on('add', function (objects) {
|
||||
})
|
||||
```
|
||||
|
||||
> Note: all objects are frozen and cannot be altered!
|
||||
|
||||
Custom fields on objects (hidden − ie. non enumerable):
|
||||
- `$type`: the type of the object (`VM`, `task`, …);
|
||||
- `$ref`: the (opaque) reference of the object;
|
||||
- `$id`: the identifier of this object (its UUID if any, otherwise its reference);
|
||||
- `$pool`: the pool object this object belongs to.
|
||||
|
||||
Furthermore, any field containing a reference (or references if an
|
||||
array) can be resolved by prepending the field name with a `$`:
|
||||
|
||||
```javascript
|
||||
console.log(xapi.pool.$master.$resident_VMs[0].name_label)
|
||||
// vm1
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
A CLI is provided to help exploration and discovery of the XAPI.
|
||||
|
||||
```
|
||||
> xen-api https://xen1.company.net root
|
||||
Password: ******
|
||||
root@xen1.company.net> xapi.status
|
||||
'connected'
|
||||
root@xen1.company.net> xapi.pool.master
|
||||
'OpaqueRef:ec7c5147-8aee-990f-c70b-0de916a8e993'
|
||||
root@xen1.company.net> xapi.pool.$master.name_label
|
||||
'xen1'
|
||||
```
|
||||
|
||||
To ease searches, `find()` and `findAll()` functions are available:
|
||||
|
||||
```
|
||||
root@xen1.company.net> findAll({ $type: 'vm' }).length
|
||||
183
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Installing dependencies
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "xen-api",
|
||||
"version": "0.2.0",
|
||||
"version": "0.8.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"description": "Connector to the Xen API",
|
||||
"keywords": [
|
||||
"xen",
|
||||
"api",
|
||||
@@ -22,38 +22,62 @@
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {
|
||||
"xen-api": "dist/cli.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
"dist/",
|
||||
".mocha.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"babel-runtime": "^5",
|
||||
"babel-runtime": "^5.8.34",
|
||||
"blocked": "^1.1.0",
|
||||
"bluebird": "^2.9.21",
|
||||
"debug": "^2.1.3",
|
||||
"lodash.find": "^3.2.0",
|
||||
"lodash.findkey": "^3.0.1",
|
||||
"event-to-promise": "^0.4.0",
|
||||
"exec-promise": "^0.5.1",
|
||||
"kindof": "^2.0.0",
|
||||
"lodash.filter": "^3.1.1",
|
||||
"lodash.find": "^3.2.1",
|
||||
"lodash.foreach": "^3.0.2",
|
||||
"lodash.size": "^3.0.0",
|
||||
"lodash.isarray": "^3.0.2",
|
||||
"lodash.isobject": "^3.0.1",
|
||||
"lodash.map": "^3.1.2",
|
||||
"lodash.startswith": "^3.0.1",
|
||||
"make-error": "^1.0.1",
|
||||
"make-error": "^1.0.2",
|
||||
"minimist": "^1.1.1",
|
||||
"ms": "^0.7.1",
|
||||
"pw": "0.0.4",
|
||||
"source-map-support": "^0.3.1",
|
||||
"xmlrpc": "^1.3.0",
|
||||
"xo-collection": "0.0.1"
|
||||
"xo-collection": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel": "^5",
|
||||
"mocha": "*",
|
||||
"must": "*",
|
||||
"standard": "*"
|
||||
"babel": "^5.8.34",
|
||||
"babel-eslint": "^4.1.5",
|
||||
"clarify": "^1.0.5",
|
||||
"dependency-check": "^2.5.1",
|
||||
"mocha": "^2.2.5",
|
||||
"must": "^0.13.1",
|
||||
"nyc": "^3.2.2",
|
||||
"source-map-support": "^0.3.3",
|
||||
"standard": "^5.1.0",
|
||||
"trace": "^2.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "mkdir --parents dist && babel --optional=runtime --compact=true --source-maps --out-dir=dist/ src/",
|
||||
"dev": "mkdir --parents dist && babel --watch --optional=runtime --source-maps --out-dir=dist/ src/",
|
||||
"build": "babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"lint": "standard",
|
||||
"depcheck": "dependency-check ./package.json",
|
||||
"posttest": "npm run lint && npm run depcheck",
|
||||
"prepublish": "npm run build",
|
||||
"test": "standard && mocha 'dist/**/*.spec.js'",
|
||||
"test-dev": "standard && mocha --watch --reporter=min 'dist/**/*.spec.js'"
|
||||
"test": "nyc mocha --opts .mocha.opts \"dist/**/*.spec.js\"",
|
||||
"test-dev": "mocha --opts .mocha.opts --watch --reporter=min \"dist/**/*.spec.js\""
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist/**"
|
||||
]
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
|
||||
112
packages/xen-api/src/cli.js
Executable file
112
packages/xen-api/src/cli.js
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Imports utils for better stacktraces.
|
||||
import '../.mocha'
|
||||
|
||||
import blocked from 'blocked'
|
||||
import Bluebird, {coroutine} from 'bluebird'
|
||||
import createDebug from 'debug'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import execPromise from 'exec-promise'
|
||||
import filter from 'lodash.filter'
|
||||
import find from 'lodash.find'
|
||||
import minimist from 'minimist'
|
||||
import pw from 'pw'
|
||||
import {start as createRepl} from 'repl'
|
||||
|
||||
import {createClient} from './'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function askPassword (prompt = 'Password: ') {
|
||||
if (prompt) {
|
||||
process.stdout.write(prompt)
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
pw(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
function required (name) {
|
||||
throw new Error(`missing required argument ${name}`)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const usage = `Usage: xen-api <url> <user> [<password>]`
|
||||
|
||||
const main = coroutine(function * (args) {
|
||||
const opts = minimist(args, {
|
||||
boolean: ['help', 'read-only', 'verbose'],
|
||||
|
||||
alias: {
|
||||
debounce: 'd',
|
||||
help: 'h',
|
||||
'read-only': 'ro',
|
||||
verbose: 'v'
|
||||
}
|
||||
})
|
||||
|
||||
if (opts.help) {
|
||||
return usage
|
||||
}
|
||||
|
||||
if (opts.verbose) {
|
||||
// Does not work perfectly.
|
||||
//
|
||||
// https://github.com/visionmedia/debug/pull/156
|
||||
createDebug.enable('xen-api,xen-api:*')
|
||||
}
|
||||
|
||||
const [
|
||||
url = required('url'),
|
||||
user = required('user'),
|
||||
password = yield askPassword()
|
||||
] = opts._
|
||||
|
||||
{
|
||||
const debug = createDebug('xen-api:perf')
|
||||
blocked(ms => {
|
||||
debug('blocked for %sms', ms | 0)
|
||||
})
|
||||
}
|
||||
|
||||
const xapi = createClient({
|
||||
url,
|
||||
auth: { user, password },
|
||||
debounce: opts.debounce != null ? +opts.debounce : null,
|
||||
readOnly: opts.ro
|
||||
})
|
||||
yield xapi.connect()
|
||||
|
||||
const repl = createRepl({
|
||||
prompt: `${xapi._humanId}> `
|
||||
})
|
||||
repl.context.xapi = xapi
|
||||
|
||||
repl.context.find = predicate => find(xapi.objects.all, predicate)
|
||||
repl.context.findAll = predicate => filter(xapi.objects.all, predicate)
|
||||
|
||||
// Make the REPL waits for promise completion.
|
||||
{
|
||||
const evaluate = Bluebird.promisify(repl.eval)
|
||||
repl.eval = (cmd, context, filename, cb) => {
|
||||
evaluate(cmd, context, filename)
|
||||
// See https://github.com/petkaantonov/bluebird/issues/594
|
||||
.then(result => result)
|
||||
.nodeify(cb)
|
||||
}
|
||||
}
|
||||
|
||||
yield eventToPromise(repl, 'exit')
|
||||
|
||||
try {
|
||||
yield xapi.disconnect()
|
||||
} catch (error) {}
|
||||
})
|
||||
export default main
|
||||
|
||||
if (!module.parent) {
|
||||
execPromise(main)
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import Bluebird, {promisify} from 'bluebird'
|
||||
import Collection from 'xo-collection'
|
||||
import createDebug from 'debug'
|
||||
import findKey from 'lodash.findkey'
|
||||
import filter from 'lodash.filter'
|
||||
import forEach from 'lodash.foreach'
|
||||
import isArray from 'lodash.isarray'
|
||||
import isObject from 'lodash.isobject'
|
||||
import kindOf from 'kindof'
|
||||
import map from 'lodash.map'
|
||||
import ms from 'ms'
|
||||
import startsWith from 'lodash.startswith'
|
||||
import {BaseError} from 'make-error'
|
||||
import {
|
||||
@@ -36,7 +41,7 @@ const NETWORK_ERRORS = {
|
||||
ETIMEDOUT: true
|
||||
}
|
||||
|
||||
const isNetworkError = (error) => NETWORK_ERRORS[error.code]
|
||||
const isNetworkError = ({code}) => NETWORK_ERRORS[code]
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -45,61 +50,166 @@ const XAPI_NETWORK_ERRORS = {
|
||||
HOST_HAS_NO_MANAGEMENT_IP: true
|
||||
}
|
||||
|
||||
const isXapiNetworkError = (error) => XAPI_NETWORK_ERRORS[error.code]
|
||||
const isXapiNetworkError = ({code}) => XAPI_NETWORK_ERRORS[code]
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const areEventsLost = (error) => error.code === 'EVENTS_LOST'
|
||||
const areEventsLost = ({code}) => code === 'EVENTS_LOST'
|
||||
|
||||
const isHostSlave = (error) => error.code === 'HOST_IS_SLAVE'
|
||||
const isHostSlave = ({code}) => code === 'HOST_IS_SLAVE'
|
||||
|
||||
const isSessionInvalid = (error) => error.code === 'SESSION_INVALID'
|
||||
const isMethodUnknown = ({code}) => code === 'MESSAGE_METHOD_UNKNOWN'
|
||||
|
||||
const isSessionInvalid = ({code}) => code === 'SESSION_INVALID'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
class XapiError extends BaseError {
|
||||
constructor (error) {
|
||||
super(error[0])
|
||||
constructor ([code, ...params]) {
|
||||
super(`${code}(${params.join(', ')})`)
|
||||
|
||||
this.code = error[0]
|
||||
this.params = error.slice(1)
|
||||
this.code = code
|
||||
this.params = params
|
||||
}
|
||||
}
|
||||
|
||||
export const wrapError = error => new XapiError(error)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const URL_RE = /^(http(s)?:\/\/)?([^/]+?)(?::([0-9]+))?(?:\/.*)?$/
|
||||
const URL_RE = /^(?:(http(s)?:)\/*)?([^/]+?)(?::([0-9]+))?\/?$/
|
||||
function parseUrl (url) {
|
||||
const matches = URL_RE.exec(url)
|
||||
if (!matches) {
|
||||
throw new Error('invalid URL: ' + url)
|
||||
}
|
||||
|
||||
const [, protocol, , host, port] = matches
|
||||
let [, , isSecure] = matches
|
||||
|
||||
let [, protocol, isSecure, hostname, port] = matches
|
||||
if (!protocol) {
|
||||
protocol = 'https:'
|
||||
isSecure = true
|
||||
} else {
|
||||
isSecure = Boolean(isSecure)
|
||||
}
|
||||
|
||||
return {
|
||||
isSecure: Boolean(isSecure),
|
||||
host,
|
||||
port: port !== undefined ?
|
||||
+port :
|
||||
isSecure ? 443 : 80
|
||||
isSecure,
|
||||
protocol, hostname, port,
|
||||
path: '/json',
|
||||
pathname: '/json'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const SPECIAL_CHARS = {
|
||||
['\r']: '\\r',
|
||||
['\t']: '\\t'
|
||||
}
|
||||
const SPECIAL_CHARS_RE = new RegExp(
|
||||
Object.keys(SPECIAL_CHARS).join('|'),
|
||||
'g'
|
||||
)
|
||||
|
||||
const parseResult = (function (parseJson) {
|
||||
return (result) => {
|
||||
const status = result.Status
|
||||
|
||||
// Return the plain result if it does not have a valid XAPI
|
||||
// format.
|
||||
if (!status) {
|
||||
return result
|
||||
}
|
||||
|
||||
if (status !== 'Success') {
|
||||
throw wrapError(result.ErrorDescription)
|
||||
}
|
||||
|
||||
const value = result.Value
|
||||
|
||||
// XAPI returns an empty string (invalid JSON) for an empty
|
||||
// result.
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
try {
|
||||
return parseJson(value)
|
||||
} catch (error) {
|
||||
// XAPI JSON sometimes contains invalid characters.
|
||||
if (error instanceof SyntaxError) {
|
||||
let replaced
|
||||
const fixedValue = value.replace(SPECIAL_CHARS_RE, (match) => {
|
||||
replaced = true
|
||||
return SPECIAL_CHARS[match]
|
||||
})
|
||||
|
||||
if (replaced) {
|
||||
return parseJson(fixedValue)
|
||||
}
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})(JSON.parse)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const {
|
||||
create: createObject,
|
||||
defineProperties,
|
||||
defineProperty,
|
||||
freeze: freezeObject,
|
||||
prototype: { toString }
|
||||
} = Object
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
const notConnectedPromise = Bluebird.reject(new Error('not connected'))
|
||||
const isString = (tag =>
|
||||
value => toString.call(value) === tag
|
||||
)(toString.call(''))
|
||||
|
||||
// Does nothing but avoid a Bluebird message error.
|
||||
notConnectedPromise.catch(noop)
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
let getNotConnectedPromise = function () {
|
||||
const promise = Bluebird.reject(new Error('not connected'))
|
||||
|
||||
// Does nothing but avoid a Bluebird message error.
|
||||
promise.catch(noop)
|
||||
|
||||
getNotConnectedPromise = () => promise
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const OPAQUE_REF_PREFIX = 'OpaqueRef:'
|
||||
const isOpaqueRef = value => isString(value) && startsWith(value, OPAQUE_REF_PREFIX)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const isReadOnlyCall = (RE => (method, args) => (
|
||||
args.length === 1 &&
|
||||
isOpaqueRef(args[0]) &&
|
||||
RE.test(method)
|
||||
))(/^[^.]+\.get_/)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const getKey = o => o.$id
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const EMPTY_ARRAY = freezeObject([])
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const MAX_TRIES = 5
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class Xapi extends EventEmitter {
|
||||
constructor (opts) {
|
||||
super()
|
||||
@@ -107,20 +217,43 @@ export class Xapi extends EventEmitter {
|
||||
this._url = parseUrl(opts.url)
|
||||
this._auth = opts.auth
|
||||
|
||||
this._sessionId = notConnectedPromise
|
||||
this._sessionId = getNotConnectedPromise()
|
||||
|
||||
this._init()
|
||||
|
||||
this._poolId = null
|
||||
this._objects = new Collection()
|
||||
this._objects.getId = (object) => object.$id
|
||||
this._pool = null
|
||||
this._objectsByRefs = createObject(null)
|
||||
this._objectsByRefs['OpaqueRef:NULL'] = null
|
||||
const objects = this._objects = new Collection()
|
||||
objects.getKey = getKey
|
||||
|
||||
this._debounce = opts.debounce == null
|
||||
? 200
|
||||
: opts.debounce
|
||||
this._fromToken = ''
|
||||
this.on('connected', this._watchEvents)
|
||||
this.on('disconnected', () => {
|
||||
this._fromToken = ''
|
||||
this._objects.clear()
|
||||
objects.clear()
|
||||
})
|
||||
|
||||
this._readOnly = Boolean(opts.readOnly)
|
||||
}
|
||||
|
||||
get readOnly () {
|
||||
return this._readOnly
|
||||
}
|
||||
|
||||
set readOnly (ro) {
|
||||
this._readOnly = Boolean(ro)
|
||||
}
|
||||
|
||||
get sessionId () {
|
||||
if (this.status !== 'connected') {
|
||||
throw new Error('sessionId is only available when connected')
|
||||
}
|
||||
|
||||
return this._sessionId.value()
|
||||
}
|
||||
|
||||
get status () {
|
||||
@@ -138,7 +271,7 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
|
||||
get _humanId () {
|
||||
return `${this._auth.user}@${this._url.host}`
|
||||
return `${this._auth.user}@${this._url.hostname}`
|
||||
}
|
||||
|
||||
connect () {
|
||||
@@ -168,7 +301,7 @@ export class Xapi extends EventEmitter {
|
||||
const {status} = this
|
||||
|
||||
if (status === 'disconnected') {
|
||||
return Bluebird.reject('already disconnected')
|
||||
return Promise.reject(new Error('already disconnected'))
|
||||
}
|
||||
|
||||
if (status === 'connecting') {
|
||||
@@ -179,7 +312,7 @@ export class Xapi extends EventEmitter {
|
||||
})
|
||||
}
|
||||
|
||||
this._sessionId = notConnectedPromise
|
||||
this._sessionId = getNotConnectedPromise()
|
||||
|
||||
return Bluebird.resolve().then(() => {
|
||||
debug('%s: disconnected', this._humanId)
|
||||
@@ -190,7 +323,53 @@ export class Xapi extends EventEmitter {
|
||||
|
||||
// High level calls.
|
||||
call (method, ...args) {
|
||||
return this._sessionCall(method, args)
|
||||
return this._readOnly && !isReadOnlyCall(method, args)
|
||||
? Promise.reject(new Error(`cannot call ${method}() in read only mode`))
|
||||
: this._sessionCall(method, args)
|
||||
}
|
||||
|
||||
// Nice getter which returns the object for a given $id (internal to
|
||||
// this lib), UUID (unique identifier that some objects have) or
|
||||
// opaque reference (internal to XAPI).
|
||||
getObject (idOrUuidOrRef, defaultValue) {
|
||||
const object = isString(idOrUuidOrRef)
|
||||
? (
|
||||
// if there is an UUID, it is also the $id.
|
||||
this._objects.all[idOrUuidOrRef] ||
|
||||
this._objectsByRefs[idOrUuidOrRef]
|
||||
)
|
||||
: this._objects.all[idOrUuidOrRef.$id]
|
||||
|
||||
if (object) return object
|
||||
|
||||
if (arguments.length > 1) return defaultValue
|
||||
|
||||
throw new Error('there is not object can be matched to ' + idOrUuidOrRef)
|
||||
}
|
||||
|
||||
// Returns the object for a given opaque reference (internal to
|
||||
// XAPI).
|
||||
getObjectByRef (ref, defaultValue) {
|
||||
const object = this._objectsByRefs[ref]
|
||||
|
||||
if (object) return object
|
||||
|
||||
if (arguments.length > 1) return defaultValue
|
||||
|
||||
throw new Error('there is no object with the ref ' + ref)
|
||||
}
|
||||
|
||||
// Returns the object for a given UUID (unique identifier that some
|
||||
// objects have).
|
||||
getObjectByUuid (uuid, defaultValue) {
|
||||
// Objects ids are already UUIDs if they have one.
|
||||
const object = this._objects.all[uuid]
|
||||
|
||||
if (object) return object
|
||||
|
||||
if (arguments.length > 1) return defaultValue
|
||||
|
||||
throw new Error('there is no object with the UUID ' + uuid)
|
||||
}
|
||||
|
||||
get pool () {
|
||||
@@ -211,48 +390,31 @@ export class Xapi extends EventEmitter {
|
||||
|
||||
return this._sessionId.then((sessionId) => {
|
||||
return this._transportCall(method, [sessionId].concat(args))
|
||||
}, error => {
|
||||
debug('%s: %s(...) =!> NOT CONNECTED', this._humanId, method)
|
||||
throw error
|
||||
}).catch(isSessionInvalid, () => {
|
||||
// XAPI is sometimes reinitialized and sessions are lost.
|
||||
// Try to login again.
|
||||
debug('%s: the session has been reinitialized', this._humanId)
|
||||
|
||||
this._sessionId = null
|
||||
|
||||
return this._sessionCall(method, args)
|
||||
this._sessionId = getNotConnectedPromise()
|
||||
return this.connect().then(() => this._sessionCall(method, args))
|
||||
})
|
||||
}
|
||||
|
||||
// Low level call: handle transport errors.
|
||||
_transportCall (method, args) {
|
||||
debug('%s: %s(...)', this._humanId, method)
|
||||
_transportCall (method, args, startTime = Date.now(), tries = 1) {
|
||||
return this._rawCall(method, args)
|
||||
.catch(isNetworkError, isXapiNetworkError, error => {
|
||||
debug('%s: network error %s', this._humanId, error.code)
|
||||
|
||||
return this._xmlRpcCall(method, args)
|
||||
.then(result => {
|
||||
const {Status: status} = result
|
||||
if (!(tries < MAX_TRIES)) {
|
||||
debug('%s too many network errors (%s), give up', this._humanId, tries)
|
||||
|
||||
// Return the plain result if it does not have a valid XAPI
|
||||
// format.
|
||||
if (!status) {
|
||||
return result
|
||||
throw error
|
||||
}
|
||||
|
||||
if (status === 'Success') {
|
||||
return result.Value
|
||||
}
|
||||
|
||||
throw new XapiError(result.ErrorDescription)
|
||||
})
|
||||
.catch(isHostSlave, ({params: [master]}) => {
|
||||
debug('%s: host is slave, attempting to connect at %s', this._humanId, master)
|
||||
|
||||
this._url.host = master
|
||||
this._init()
|
||||
|
||||
return this._transportCall(method, args)
|
||||
})
|
||||
.catch(isNetworkError, isXapiNetworkError, () => {
|
||||
debug('%s: a network error happened', this._humanId)
|
||||
|
||||
// TODO: ability to cancel the connection
|
||||
// TODO: ability to force immediate reconnection
|
||||
// TODO: implement back-off
|
||||
@@ -260,21 +422,76 @@ export class Xapi extends EventEmitter {
|
||||
return Bluebird.delay(5e3).then(() => {
|
||||
// TODO: handling not responding host.
|
||||
|
||||
return this._transportCall(method, args)
|
||||
return this._transportCall(method, args, startTime, tries + 1)
|
||||
})
|
||||
})
|
||||
.catch(isHostSlave, ({params: [master]}) => {
|
||||
debug('%s: host is slave, attempting to connect at %s', this._humanId, master)
|
||||
|
||||
this._url.hostname = master
|
||||
this._init()
|
||||
|
||||
return this._transportCall(method, args, startTime)
|
||||
}).then(
|
||||
result => {
|
||||
debug(
|
||||
'%s: %s(...) [%s] ==> %s',
|
||||
this._humanId,
|
||||
method,
|
||||
ms(Date.now() - startTime),
|
||||
kindOf(result)
|
||||
)
|
||||
return result
|
||||
},
|
||||
error => {
|
||||
debug(
|
||||
'%s: %s(...) [%s] =!> %s',
|
||||
this._humanId,
|
||||
method,
|
||||
ms(Date.now() - startTime),
|
||||
error
|
||||
)
|
||||
throw error
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Lowest level call: do not handle any errors.
|
||||
_rawCall (method, args) {
|
||||
return this._xmlRpcCall(method, args)
|
||||
.then(
|
||||
parseResult,
|
||||
error => {
|
||||
// Unwrap error if necessary.
|
||||
if (error instanceof Bluebird.OperationalError) {
|
||||
error = error.cause
|
||||
}
|
||||
|
||||
if (error.res) {
|
||||
console.error(
|
||||
'XML-RPC Error: %s (response status %s)',
|
||||
error.message,
|
||||
error.res.statusCode
|
||||
)
|
||||
console.error('%s', error.body)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
)
|
||||
.cancellable()
|
||||
}
|
||||
|
||||
_init () {
|
||||
const {isSecure, host, port} = this._url
|
||||
const {isSecure, hostname, port, path} = this._url
|
||||
|
||||
const client = (isSecure ?
|
||||
createSecureXmlRpcClient :
|
||||
createXmlRpcClient
|
||||
const client = (isSecure
|
||||
? createSecureXmlRpcClient
|
||||
: createXmlRpcClient
|
||||
)({
|
||||
host,
|
||||
hostname,
|
||||
port,
|
||||
path,
|
||||
rejectUnauthorized: false,
|
||||
timeout: 10
|
||||
})
|
||||
@@ -282,52 +499,198 @@ export class Xapi extends EventEmitter {
|
||||
this._xmlRpcCall = promisify(client.methodCall, client)
|
||||
}
|
||||
|
||||
_normalizeObject (type, ref, object) {
|
||||
object.$id = object.uuid || ref
|
||||
object.$ref = ref
|
||||
object.$type = type
|
||||
_addObject (type, ref, object) {
|
||||
const {_objectsByRefs: objectsByRefs} = this
|
||||
|
||||
Object.defineProperty(object, '$pool', {
|
||||
// enumerable: true,
|
||||
get: () => this._pool
|
||||
// Creates resolved properties.
|
||||
forEach(object, function resolveObject (value, key, object) {
|
||||
if (isArray(value)) {
|
||||
if (!value.length) {
|
||||
// If the array is empty, it isn't possible to be sure that
|
||||
// it is not supposed to contain links, therefore, in
|
||||
// benefice of the doubt, a resolved property is defined.
|
||||
defineProperty(object, '$' + key, {
|
||||
value: EMPTY_ARRAY
|
||||
})
|
||||
|
||||
// Minor memory optimization, use the same empty array for
|
||||
// everyone.
|
||||
object[key] = EMPTY_ARRAY
|
||||
} else if (isOpaqueRef(value[0])) {
|
||||
// This is an array of refs.
|
||||
defineProperty(object, '$' + key, {
|
||||
get: () => freezeObject(map(value, (ref) => objectsByRefs[ref]))
|
||||
})
|
||||
|
||||
freezeObject(value)
|
||||
}
|
||||
} else if (isObject(value)) {
|
||||
forEach(value, resolveObject)
|
||||
|
||||
freezeObject(value)
|
||||
} else if (isOpaqueRef(value)) {
|
||||
defineProperty(object, '$' + key, {
|
||||
get: () => objectsByRefs[value]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// All custom properties are read-only and non enumerable.
|
||||
defineProperties(object, {
|
||||
$id: { value: object.uuid || ref },
|
||||
$pool: { get: () => this._pool },
|
||||
$ref: { value: ref },
|
||||
$type: { value: type }
|
||||
})
|
||||
|
||||
// Finally freezes the object.
|
||||
freezeObject(object)
|
||||
|
||||
const objects = this._objects
|
||||
|
||||
// An object's UUID can change during its life.
|
||||
const prev = objectsByRefs[ref]
|
||||
let prevUuid
|
||||
if (prev && (prevUuid = prev.uuid) && prevUuid !== object.uuid) {
|
||||
objects.remove(prevUuid)
|
||||
}
|
||||
|
||||
this._objects.set(object)
|
||||
objectsByRefs[ref] = object
|
||||
|
||||
if (type === 'pool') {
|
||||
this._pool = object
|
||||
}
|
||||
}
|
||||
|
||||
_removeObject (ref) {
|
||||
const {_objectsByRefs: objectsByRefs} = this
|
||||
|
||||
const object = objectsByRefs[ref]
|
||||
|
||||
if (object) {
|
||||
this._objects.unset(object.$id)
|
||||
delete objectsByRefs[ref]
|
||||
}
|
||||
}
|
||||
|
||||
_processEvents (events) {
|
||||
forEach(events, event => {
|
||||
const {operation: op} = event
|
||||
|
||||
const {ref} = event
|
||||
if (op === 'del') {
|
||||
this._removeObject(ref)
|
||||
} else {
|
||||
this._addObject(event.class, ref, event.snapshot)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_watchEvents () {
|
||||
this.call('event.from', [
|
||||
['*'], this._fromToken, 1e3 + 0.1
|
||||
]).then(({token, events}) => {
|
||||
this._fromToken = token
|
||||
const debounce = this._debounce
|
||||
|
||||
const {_objects: objects} = this
|
||||
const loop = ((onSucess, onFailure) => {
|
||||
return () => this._sessionCall('event.from', [
|
||||
['*'],
|
||||
this._fromToken,
|
||||
1e3 + 0.1 // Force float.
|
||||
]).then(onSucess, onFailure)
|
||||
})(
|
||||
({token, events}) => {
|
||||
this._fromToken = token
|
||||
this._processEvents(events)
|
||||
|
||||
forEach(events, event => {
|
||||
const {operation: op} = event
|
||||
return debounce != null
|
||||
? Bluebird.delay(debounce).then(loop)
|
||||
: loop()
|
||||
},
|
||||
error => {
|
||||
if (areEventsLost(error)) {
|
||||
this._fromToken = ''
|
||||
this._objects.clear()
|
||||
|
||||
const {ref} = event
|
||||
if (op === 'del') {
|
||||
// TODO: This should probably be speed up with an index.
|
||||
const key = findKey(objects.all, {$ref: ref})
|
||||
|
||||
if (key !== undefined) {
|
||||
objects.remove(key)
|
||||
}
|
||||
} else {
|
||||
const {class: type, snapshot: object} = event
|
||||
|
||||
this._normalizeObject(type, ref, object)
|
||||
objects.set(object)
|
||||
|
||||
if (object.$type === 'pool') {
|
||||
this._pool = object
|
||||
}
|
||||
return loop()
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
)
|
||||
|
||||
return loop().catch(error => {
|
||||
if (
|
||||
isMethodUnknown(error) ||
|
||||
|
||||
// If the server failed, it is probably due to an excessively
|
||||
// large response.
|
||||
// Falling back to legacy events watch should be enough.
|
||||
error && error.res && error.res.statusCode === 500
|
||||
) {
|
||||
return this._watchEventsLegacy()
|
||||
}
|
||||
|
||||
if (!(error instanceof Bluebird.CancellationError)) {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// This method watches events using the legacy `event.next` XAPI
|
||||
// methods.
|
||||
//
|
||||
// It also has to manually get all objects first.
|
||||
_watchEventsLegacy () {
|
||||
const getAllObjects = () => {
|
||||
return this._sessionCall('system.listMethods', []).then(methods => {
|
||||
// Uses introspection to determine the methods to use to get
|
||||
// all objects.
|
||||
const getAllRecordsMethods = filter(
|
||||
methods,
|
||||
::/\.get_all_records$/.test
|
||||
)
|
||||
|
||||
return Promise.all(map(
|
||||
getAllRecordsMethods,
|
||||
method => this._sessionCall(method, []).then(
|
||||
objects => {
|
||||
const type = method.slice(0, method.indexOf('.')).toLowerCase()
|
||||
forEach(objects, (object, ref) => {
|
||||
this._addObject(type, ref, object)
|
||||
})
|
||||
},
|
||||
error => {
|
||||
if (error.code !== 'MESSAGE_REMOVED') {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
)
|
||||
))
|
||||
})
|
||||
}).catch(areEventsLost, () => {
|
||||
this._objects.clear()
|
||||
}).then(() => {
|
||||
this._watchEvents()
|
||||
}).catch(noop)
|
||||
}
|
||||
|
||||
const watchEvents = (() => {
|
||||
const loop = ((onSuccess, onFailure) => {
|
||||
return () => this._sessionCall('event.next', []).then(onSuccess, onFailure)
|
||||
})(
|
||||
(debounce => events => {
|
||||
this._processEvents(events)
|
||||
return debounce == null
|
||||
? loop()
|
||||
: Bluebird.delay(debounce).then(loop)
|
||||
})(this._debounce),
|
||||
error => {
|
||||
if (areEventsLost(error)) {
|
||||
return this._sessionCall('event.unregister', [ ['*'] ]).then(watchEvents)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
)
|
||||
|
||||
return () => this._sessionCall('event.register', [ ['*'] ]).then(loop)
|
||||
})()
|
||||
|
||||
return getAllObjects().then(watchEvents)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user