Compare commits

..

119 Commits

Author SHA1 Message Date
Julien Fontanet
35e8dcc3be 0.8.0 2016-04-26 08:46:22 +02:00
Julien Fontanet
d1600fd058 fix: handle UUID changes (fix #5). (#7) 2016-04-26 08:46:04 +02:00
Julien Fontanet
1416fb0c71 Xapi#call(): only *.get_*() methods in readonly mode. 2016-04-19 16:32:18 +01:00
Julien Fontanet
2975db247d Do not attempt to fix JSON is parsing is successful. 2016-04-19 13:29:51 +01:00
Julien Fontanet
03eaa652ce Optional debounce for events. 2016-04-14 17:55:41 +02:00
Julien Fontanet
eac29993d3 0.7.4 2016-03-22 09:37:55 +01:00
Julien Fontanet
af2a9225b8 Fix invalid \t in JSON. 2016-03-09 17:41:55 +01:00
Julien Fontanet
a24de7fe3f Fix type case in README. 2016-02-29 18:02:30 +01:00
Julien Fontanet
16a9f44d4d Xapi#getObject(): behave nicely when a XAPI object is passed. 2016-01-28 10:08:26 +01:00
Julien Fontanet
6fcc148105 0.7.3 2016-01-05 16:09:19 +01:00
Julien Fontanet
3485cb4ec4 Guard against null/undefined error.res. 2016-01-05 16:09:06 +01:00
Julien Fontanet
b2a51bd658 0.7.2 2015-12-18 11:29:08 +01:00
Julien Fontanet
e5ab1dc154 Fix opaque ref detection in arrays. 2015-12-18 11:28:58 +01:00
Julien Fontanet
6274969635 0.7.1 2015-12-18 11:23:29 +01:00
Julien Fontanet
069c430346 Fix opaque ref detection. 2015-12-18 11:23:24 +01:00
Julien Fontanet
cbcc4dd21d 0.7.0 2015-12-17 16:33:55 +01:00
Julien Fontanet
b4cdf4d277 Minor optimizations. 2015-12-16 14:19:30 +01:00
Julien Fontanet
716d7bfcf6 Optimize opaque refs detection. 2015-12-16 14:15:37 +01:00
Julien Fontanet
b45a169a2f Read only mode can be altered after initial construction. 2015-12-16 13:58:33 +01:00
Julien Fontanet
720b9ef999 0.6.9 2015-12-03 12:40:31 +01:00
Julien Fontanet
9b9e4dddfc Gracefully fails if error.res does not exists. 2015-12-03 12:39:59 +01:00
Julien Fontanet
7434e0352f 0.6.8 2015-12-02 17:37:28 +01:00
Julien Fontanet
26d61af902 Fall back to legacy events on server failure (work around #2). 2015-12-02 17:36:29 +01:00
Julien Fontanet
5bd12c5f9e Fix XML-RPC error display. 2015-12-02 17:31:13 +01:00
Julien Fontanet
e07fae4290 Revert to Babel 5 for now. 2015-12-02 17:30:42 +01:00
Julien Fontanet
e304395179 Work around ESLint bug. 2015-11-11 15:58:15 +01:00
Julien Fontanet
6b83130853 Various updates. 2015-11-11 15:52:12 +01:00
Julien Fontanet
9565718699 Update deps. 2015-11-11 15:52:06 +01:00
Julien Fontanet
ac11885379 0.6.7 2015-10-23 16:53:53 +02:00
Julien Fontanet
277669a13c Fix events watching XenServer < 6.0. 2015-10-23 16:53:42 +02:00
Julien Fontanet
fcbc476462 Fix coding style. 2015-10-23 16:53:05 +02:00
Julien Fontanet
4944b415c7 0.6.6 2015-10-23 16:03:02 +02:00
Julien Fontanet
5da7312d2d Correctly publish .mocha.js 2015-10-23 16:02:42 +02:00
Julien Fontanet
954d19fe50 Fix objects collection in read-only mode. 2015-10-23 15:33:22 +02:00
Julien Fontanet
addd86f5d2 Better stack traces in CLI. 2015-10-23 15:33:02 +02:00
Julien Fontanet
1b90223210 0.6.5 2015-10-23 14:54:02 +02:00
Julien Fontanet
95989ff63b Add find() and findAll() in CLI. 2015-10-23 14:51:25 +02:00
Julien Fontanet
799f758dce Document constructor options. 2015-10-20 15:46:50 +02:00
Julien Fontanet
e075f1c08b 0.6.4 2015-10-06 14:35:33 +02:00
Julien Fontanet
7e0aa719b4 Use Collection.unset() instead of remove() to avoid exceptions. 2015-10-02 14:05:26 +02:00
Julien Fontanet
4ee352fdb2 Always use instances of Error for errors. 2015-09-14 16:45:17 +02:00
Julien Fontanet
96ea3ded4a Better stacktraces in CLI. 2015-09-14 16:02:09 +02:00
Julien Fontanet
8bbc6e9ff5 Initial read only mode (fix #3). 2015-09-14 16:01:46 +02:00
Julien Fontanet
af7029812c 0.6.3 2015-09-11 15:32:16 +02:00
Julien Fontanet
c517b59138 Enable testing on Node 4. 2015-09-10 15:22:09 +02:00
Julien Fontanet
5485e8a322 Test on iojs 3 and use containers on Travis. 2015-09-07 16:23:33 +02:00
Julien Fontanet
2540ac34b3 Be verbose on XML-RPC errors. 2015-08-28 08:51:38 +02:00
Julien Fontanet
76e5d41a34 Upgrade deps. 2015-08-28 08:51:15 +02:00
Julien Fontanet
2c32a4e912 0.6.2 2015-08-10 15:52:55 +02:00
Julien Fontanet
c66f7235b6 Fix master change. 2015-08-10 15:51:45 +02:00
Julien Fontanet
5444381f7d 0.6.1 2015-06-30 17:20:31 +02:00
Julien Fontanet
dc44679031 Optimize and clean _watchEvents() & _watchEventsLegacy(). 2015-06-30 14:24:43 +02:00
Julien Fontanet
2cbd17b745 Better traces. 2015-06-30 11:07:33 +02:00
Julien Fontanet
9ef13696d8 0.6.0 2015-06-23 10:39:55 +02:00
Julien Fontanet
c3f635fd12 Ask JSON encoded values to XenApi.
The XML is therefore much faster to parse (3-4 ×).
2015-06-23 10:39:04 +02:00
Julien Fontanet
e3d1380435 0.5.7 2015-06-23 10:38:41 +02:00
Julien Fontanet
f83737b538 Make objects immutable. 2015-06-23 09:17:42 +02:00
Julien Fontanet
bb1ea4e4d0 Optimization for empty arrays. 2015-06-23 09:13:43 +02:00
Julien Fontanet
9cb4de2ea8 Inline an only-used-once function. 2015-06-22 23:33:52 +02:00
Julien Fontanet
048cbf60ec Added tested Xen Server versions in README. 2015-06-22 16:23:38 +02:00
Julien Fontanet
36f40b4188 Support Xen Server < 6. 2015-06-22 16:19:32 +02:00
Julien Fontanet
a3bba92063 Remove duplicate source-map-support. 2015-06-19 15:53:23 +02:00
Julien Fontanet
ebcc6c9341 Remove unused lodash.{find,findkey,size}. 2015-06-19 15:51:22 +02:00
Julien Fontanet
95f765055e Fix invalid session handling. 2015-06-19 15:15:23 +02:00
Julien Fontanet
49aa5ffccc Password can be supplied on the command line. 2015-06-19 15:13:21 +02:00
Julien Fontanet
d09d3fa80b 0.5.6 2015-06-18 13:40:18 +02:00
Julien Fontanet
4c8cd50643 Better build & tests. 2015-06-18 13:15:27 +02:00
Julien Fontanet
eee72f4f27 Test on both io.js v1 & v2. 2015-06-18 13:13:40 +02:00
Julien Fontanet
45f6a7cb4d Minor changes. 2015-06-17 17:09:46 +02:00
Julien Fontanet
8866bd8663 Minor code simplification. 2015-06-17 15:53:48 +02:00
Julien Fontanet
3f9c515f1d Better error messages. 2015-06-17 15:39:33 +02:00
Julien Fontanet
c92567d4fa 0.5.5 2015-05-27 17:04:04 +02:00
Julien Fontanet
df3c76fa72 Remove useless statement. 2015-05-26 16:47:14 +02:00
Julien Fontanet
cea4157402 Perf traces in the CLI. 2015-05-26 16:35:51 +02:00
Julien Fontanet
29ce3bd05e Source maps support in the CLI. 2015-05-22 10:38:38 +02:00
Julien Fontanet
b3d58f4f0c Update to xo-collection 0.3. 2015-05-22 10:38:08 +02:00
Julien Fontanet
d93d234c71 Fix Travis badge. 2015-05-14 15:06:27 +02:00
Julien Fontanet
7fe9ae8a04 Document custom props. 2015-05-14 15:04:55 +02:00
Julien Fontanet
87cf1ed7cb All custom properties are read-only and non enumerable. 2015-05-14 14:58:54 +02:00
Julien Fontanet
a0ba5c8a57 Fix auto links for arrays. 2015-05-14 14:58:27 +02:00
Julien Fontanet
d7208a15d9 Remove unused dep. 2015-05-14 14:46:06 +02:00
Julien Fontanet
debde0c67a Links do not shadow refs. 2015-05-14 14:39:36 +02:00
Julien Fontanet
97db55156a $pool should not be enumerable. 2015-05-13 17:29:24 +02:00
Julien Fontanet
9d3477d465 Fix propagation of Xapi errors. 2015-05-13 17:27:22 +02:00
Julien Fontanet
031af000e6 Alias Object.create() and Object.defineProperty() for perf. 2015-05-06 18:14:43 +02:00
Julien Fontanet
0512fac3aa Better name for the OpaqueRef regex constant. 2015-05-06 18:14:20 +02:00
Julien Fontanet
4272e8196a Bypass Xapi#objects getter internally for perf. 2015-05-06 14:08:59 +02:00
Julien Fontanet
140f9d05df Code style and comments. 2015-05-06 14:06:25 +02:00
Julien Fontanet
9222733243 Xapi#getObject() 2015-05-06 14:06:17 +02:00
Julien Fontanet
5838c56c4e Xapi#getObjectByRef() and Xapi#getObjectByUuid() 2015-05-06 13:27:59 +02:00
Julien Fontanet
1814e0a260 0.5.4 2015-05-05 13:46:03 +02:00
Julien Fontanet
711c5781e6 Export wrapError(). 2015-05-05 13:45:51 +02:00
Julien Fontanet
7e8c2211d8 The REPL waits for promise completion. 2015-04-21 17:20:38 +02:00
Julien Fontanet
f0858b7d93 0.5.3 2015-04-20 19:14:48 +02:00
Julien Fontanet
3af6c28ab0 Do not swallow all errors. 2015-04-20 19:01:05 +02:00
Julien Fontanet
5c31c7f14c Typo! 2015-04-20 19:00:52 +02:00
Julien Fontanet
2610a9c777 0.5.2 2015-04-20 15:19:06 +02:00
Julien Fontanet
58cf611497 Fix this._sessionId. 2015-04-20 15:18:48 +02:00
Julien Fontanet
61631e405b Coding style. 2015-04-17 16:22:23 +02:00
Julien Fontanet
185e0849b1 0.5.1 2015-04-17 12:04:06 +02:00
Julien Fontanet
f48b9d364b Shebang and executable mode for cli.js 2015-04-17 12:04:01 +02:00
Julien Fontanet
e4f1a7d4c1 0.5.0 2015-04-17 11:58:08 +02:00
Julien Fontanet
e02f19ff67 Typo. 2015-04-17 11:58:05 +02:00
Julien Fontanet
72a2110845 Add CLI. 2015-04-17 10:38:15 +02:00
Julien Fontanet
9baa415249 0.4.0 2015-04-16 16:44:05 +02:00
Julien Fontanet
22b840af14 Add a description. 2015-04-16 16:43:15 +02:00
Julien Fontanet
61f32d89ca Declare Xapi#_pool in the constructor for perf. 2015-04-16 16:34:09 +02:00
Julien Fontanet
3c7da93dfc Xapi#_poolId is no longer used. 2015-04-16 16:33:45 +02:00
Julien Fontanet
5831616fac Merge branch 'linked-objects' 2015-04-16 16:30:46 +02:00
Julien Fontanet
d7b6d9f124 Objects are now linked together!
```javascript
const {pool} = xapi

console.log(pool.master.name_label)
```
2015-04-16 16:22:01 +02:00
Julien Fontanet
245978e2b3 0.3.1 2015-04-15 18:27:02 +02:00
Julien Fontanet
3aae60bde9 Better handling of transport call retries. 2015-04-15 18:23:49 +02:00
Julien Fontanet
91d36122eb Rmove unused JSHint conf. 2015-04-15 10:46:25 +02:00
Julien Fontanet
36c1e2cc73 Limit tries in case of transport errors (ugly). 2015-04-14 17:57:31 +02:00
Julien Fontanet
4a0a09ba3e Update to latest make-error. 2015-04-14 17:56:51 +02:00
Julien Fontanet
04b44cff2b 0.3.0 2015-04-13 17:44:55 +02:00
Julien Fontanet
8309755ee3 Expose session identifier. 2015-04-13 17:44:05 +02:00
Julien Fontanet
41a75d404c 0.2.1 2015-04-13 16:48:45 +02:00
Julien Fontanet
8eb63de201 Stupid fix -_-". 2015-04-13 16:48:32 +02:00
11 changed files with 711 additions and 215 deletions

11
packages/xen-api/.babelrc Normal file
View File

@@ -0,0 +1,11 @@
{
"comments": false,
"compact": true,
"optional": [
"es7.asyncFunctions",
"es7.decorators",
"es7.exportExtensions",
"es7.functionBind",
"runtime"
]
}

View File

@@ -1,7 +1,9 @@
/.nyc_output/
/bower_components/
/dist/
npm-debug.log
npm-debug.log.*
!node_modules/*
node_modules/*/

View File

@@ -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
}

View File

@@ -0,0 +1,5 @@
Error.stackTraceLimit = 100
try { require('trace') } catch (_) {}
try { require('clarify') } catch (_) {}
try { require('source-map-support/register') } catch (_) {}

View File

@@ -0,0 +1 @@
--require ./.mocha.js

View File

@@ -1,2 +1,10 @@
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/test/
/tests/
*.spec.js
*.spec.js.map

View File

@@ -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

View File

@@ -1,6 +1,12 @@
# xen-api [![Build Status](https://travis-ci.org/js-xen-api.png?branch=master)](https://travis-ci.org/js-xen-api)
# xen-api [![Build Status](https://travis-ci.org/julien-f/js-xen-api.png?branch=master)](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

View File

@@ -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
View 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)
}

View File

@@ -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)
}
}