2015-03-31 18:44:33 +02:00
|
|
|
import Bluebird, {promisify} from 'bluebird'
|
2015-04-10 15:33:39 +02:00
|
|
|
import Collection from 'xo-collection'
|
2015-03-31 18:44:33 +02:00
|
|
|
import createDebug from 'debug'
|
2015-06-22 16:19:32 +02:00
|
|
|
import filter from 'lodash.filter'
|
2015-04-10 15:33:39 +02:00
|
|
|
import forEach from 'lodash.foreach'
|
2015-05-14 14:39:36 +02:00
|
|
|
import isArray from 'lodash.isarray'
|
|
|
|
|
import isObject from 'lodash.isobject'
|
2015-06-29 16:14:00 +02:00
|
|
|
import kindOf from 'kindof'
|
2015-05-14 14:39:36 +02:00
|
|
|
import map from 'lodash.map'
|
2015-06-29 16:14:00 +02:00
|
|
|
import ms from 'ms'
|
2015-04-10 15:33:39 +02:00
|
|
|
import startsWith from 'lodash.startswith'
|
|
|
|
|
import {BaseError} from 'make-error'
|
2015-03-31 18:44:33 +02:00
|
|
|
import {
|
|
|
|
|
createClient as createXmlRpcClient,
|
|
|
|
|
createSecureClient as createSecureXmlRpcClient
|
|
|
|
|
} from 'xmlrpc'
|
|
|
|
|
import {EventEmitter} from 'events'
|
|
|
|
|
|
2015-04-10 15:33:39 +02:00
|
|
|
const debug = createDebug('xen-api')
|
2015-03-31 18:44:33 +02:00
|
|
|
|
|
|
|
|
// ===================================================================
|
|
|
|
|
|
2016-05-09 18:11:14 +02:00
|
|
|
function invoke (fn) {
|
|
|
|
|
const n = arguments.length - 1
|
|
|
|
|
if (!n) {
|
|
|
|
|
return fn()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn = arguments[n]
|
|
|
|
|
const args = new Array(n)
|
|
|
|
|
for (let i = 0; i < n; ++i) {
|
|
|
|
|
args[i] = arguments[i]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fn.apply(undefined, args)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================================================================
|
|
|
|
|
|
2015-03-31 18:44:33 +02:00
|
|
|
// http://www.gnu.org/software/libc/manual/html_node/Error-Codes.html
|
|
|
|
|
const NETWORK_ERRORS = {
|
|
|
|
|
// Connection has been closed outside of our control.
|
|
|
|
|
ECONNRESET: true,
|
|
|
|
|
|
|
|
|
|
// Connection has been aborted locally.
|
|
|
|
|
ECONNABORTED: true,
|
|
|
|
|
|
|
|
|
|
// Host is up but refuses connection (typically: no such service).
|
|
|
|
|
ECONNREFUSED: true,
|
|
|
|
|
|
|
|
|
|
// TODO: ??
|
|
|
|
|
EINVAL: true,
|
|
|
|
|
|
|
|
|
|
// Host is not reachable (does not respond).
|
2015-04-10 16:40:45 +02:00
|
|
|
EHOSTUNREACH: true,
|
2015-03-31 18:44:33 +02:00
|
|
|
|
|
|
|
|
// Connection configured timed out has been reach.
|
|
|
|
|
ETIMEDOUT: true
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-17 15:50:52 +02:00
|
|
|
const isNetworkError = ({code}) => NETWORK_ERRORS[code]
|
2015-03-31 18:44:33 +02:00
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
const XAPI_NETWORK_ERRORS = {
|
|
|
|
|
HOST_STILL_BOOTING: true,
|
|
|
|
|
HOST_HAS_NO_MANAGEMENT_IP: true
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-17 15:50:52 +02:00
|
|
|
const isXapiNetworkError = ({code}) => XAPI_NETWORK_ERRORS[code]
|
2015-03-31 18:44:33 +02:00
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2015-06-17 15:50:52 +02:00
|
|
|
const areEventsLost = ({code}) => code === 'EVENTS_LOST'
|
2015-03-31 18:44:33 +02:00
|
|
|
|
2015-06-17 15:50:52 +02:00
|
|
|
const isHostSlave = ({code}) => code === 'HOST_IS_SLAVE'
|
2015-04-10 16:00:20 +02:00
|
|
|
|
2015-06-22 16:19:32 +02:00
|
|
|
const isMethodUnknown = ({code}) => code === 'MESSAGE_METHOD_UNKNOWN'
|
|
|
|
|
|
2015-06-17 15:50:52 +02:00
|
|
|
const isSessionInvalid = ({code}) => code === 'SESSION_INVALID'
|
2015-04-10 16:00:20 +02:00
|
|
|
|
2015-03-31 18:44:33 +02:00
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2015-04-10 15:33:39 +02:00
|
|
|
class XapiError extends BaseError {
|
2015-06-17 15:39:33 +02:00
|
|
|
constructor ([code, ...params]) {
|
|
|
|
|
super(`${code}(${params.join(', ')})`)
|
2015-03-31 18:44:33 +02:00
|
|
|
|
2015-06-17 15:39:33 +02:00
|
|
|
this.code = code
|
|
|
|
|
this.params = params
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-05 13:45:51 +02:00
|
|
|
export const wrapError = error => new XapiError(error)
|
|
|
|
|
|
2015-03-31 18:44:33 +02:00
|
|
|
// ===================================================================
|
|
|
|
|
|
2015-06-22 13:16:38 +02:00
|
|
|
const URL_RE = /^(?:(http(s)?:)\/*)?([^/]+?)(?::([0-9]+))?\/?$/
|
2015-03-31 18:44:33 +02:00
|
|
|
function parseUrl (url) {
|
|
|
|
|
const matches = URL_RE.exec(url)
|
|
|
|
|
if (!matches) {
|
|
|
|
|
throw new Error('invalid URL: ' + url)
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-22 13:16:38 +02:00
|
|
|
let [, protocol, isSecure, hostname, port] = matches
|
2015-04-13 16:16:30 +02:00
|
|
|
if (!protocol) {
|
2015-06-22 13:16:38 +02:00
|
|
|
protocol = 'https:'
|
2015-04-13 16:16:30 +02:00
|
|
|
isSecure = true
|
2015-06-22 13:16:38 +02:00
|
|
|
} else {
|
|
|
|
|
isSecure = Boolean(isSecure)
|
2015-04-13 16:16:30 +02:00
|
|
|
}
|
2015-03-31 18:44:33 +02:00
|
|
|
|
|
|
|
|
return {
|
2015-06-22 13:16:38 +02:00
|
|
|
isSecure,
|
|
|
|
|
protocol, hostname, port,
|
|
|
|
|
path: '/json',
|
|
|
|
|
pathname: '/json'
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-16 16:18:11 +02:00
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2016-03-09 17:41:55 +01:00
|
|
|
const SPECIAL_CHARS = {
|
2016-05-04 15:47:31 +02:00
|
|
|
'\r': '\\r',
|
|
|
|
|
'\t': '\\t'
|
2016-03-09 17:41:55 +01:00
|
|
|
}
|
|
|
|
|
const SPECIAL_CHARS_RE = new RegExp(
|
|
|
|
|
Object.keys(SPECIAL_CHARS).join('|'),
|
|
|
|
|
'g'
|
|
|
|
|
)
|
|
|
|
|
|
2016-05-09 18:11:14 +02:00
|
|
|
const parseResult = invoke(() => {
|
|
|
|
|
const parseJson = JSON.parse
|
|
|
|
|
|
2015-06-22 13:16:38 +02:00
|
|
|
return (result) => {
|
2016-03-09 17:41:55 +01:00
|
|
|
const status = result.Status
|
2015-06-22 13:16:38 +02:00
|
|
|
|
|
|
|
|
// Return the plain result if it does not have a valid XAPI
|
|
|
|
|
// format.
|
|
|
|
|
if (!status) {
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2016-04-19 13:29:51 +01:00
|
|
|
if (status !== 'Success') {
|
|
|
|
|
throw wrapError(result.ErrorDescription)
|
|
|
|
|
}
|
2015-06-22 13:16:38 +02:00
|
|
|
|
2016-04-19 13:29:51 +01:00
|
|
|
const value = result.Value
|
|
|
|
|
|
|
|
|
|
// XAPI returns an empty string (invalid JSON) for an empty
|
|
|
|
|
// result.
|
|
|
|
|
if (!value) {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
2015-06-22 13:16:38 +02:00
|
|
|
|
2016-04-19 13:29:51 +01:00
|
|
|
try {
|
|
|
|
|
return parseJson(value)
|
|
|
|
|
} catch (error) {
|
2016-03-09 17:41:55 +01:00
|
|
|
// XAPI JSON sometimes contains invalid characters.
|
2016-04-19 13:29:51 +01:00
|
|
|
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)
|
|
|
|
|
}
|
2015-06-22 13:16:38 +02:00
|
|
|
}
|
|
|
|
|
|
2016-04-19 13:29:51 +01:00
|
|
|
throw error
|
2015-06-22 13:16:38 +02:00
|
|
|
}
|
|
|
|
|
}
|
2016-05-09 18:11:14 +02:00
|
|
|
})
|
2015-06-22 13:16:38 +02:00
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2015-05-06 18:14:43 +02:00
|
|
|
const {
|
|
|
|
|
create: createObject,
|
2015-05-14 14:58:54 +02:00
|
|
|
defineProperties,
|
2015-06-23 09:17:42 +02:00
|
|
|
defineProperty,
|
2015-12-16 14:15:37 +01:00
|
|
|
freeze: freezeObject,
|
|
|
|
|
prototype: { toString }
|
2015-05-06 18:14:43 +02:00
|
|
|
} = Object
|
|
|
|
|
|
2015-04-10 16:45:40 +02:00
|
|
|
const noop = () => {}
|
|
|
|
|
|
2016-05-09 18:11:14 +02:00
|
|
|
const isString = invoke(toString.call(''), tag =>
|
2015-12-16 14:15:37 +01:00
|
|
|
value => toString.call(value) === tag
|
2016-05-09 18:11:14 +02:00
|
|
|
)
|
2015-12-16 14:15:37 +01:00
|
|
|
|
2015-04-16 16:18:11 +02:00
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2015-05-26 16:35:05 +02:00
|
|
|
let getNotConnectedPromise = function () {
|
|
|
|
|
const promise = Bluebird.reject(new Error('not connected'))
|
2015-04-13 16:16:30 +02:00
|
|
|
|
2015-05-26 16:35:05 +02:00
|
|
|
// Does nothing but avoid a Bluebird message error.
|
|
|
|
|
promise.catch(noop)
|
|
|
|
|
|
|
|
|
|
getNotConnectedPromise = () => promise
|
|
|
|
|
|
|
|
|
|
return promise
|
|
|
|
|
}
|
2015-04-13 16:16:30 +02:00
|
|
|
|
2015-06-22 23:33:52 +02:00
|
|
|
// -------------------------------------------------------------------
|
2015-03-31 18:44:33 +02:00
|
|
|
|
2015-12-18 11:23:24 +01:00
|
|
|
const OPAQUE_REF_PREFIX = 'OpaqueRef:'
|
2015-12-16 14:15:37 +01:00
|
|
|
const isOpaqueRef = value => isString(value) && startsWith(value, OPAQUE_REF_PREFIX)
|
2015-05-14 14:39:36 +02:00
|
|
|
|
2015-06-23 09:13:43 +02:00
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2016-05-09 18:11:14 +02:00
|
|
|
const isReadOnlyCall = invoke(/^[^.]+\.get_/, RE => (method, args) => (
|
2016-04-19 16:27:55 +01:00
|
|
|
args.length === 1 &&
|
|
|
|
|
isOpaqueRef(args[0]) &&
|
|
|
|
|
RE.test(method)
|
2016-05-09 18:11:14 +02:00
|
|
|
))
|
2016-04-19 16:27:55 +01:00
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2015-12-16 14:19:30 +01:00
|
|
|
const getKey = o => o.$id
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2015-06-23 09:17:42 +02:00
|
|
|
const EMPTY_ARRAY = freezeObject([])
|
2015-06-23 09:13:43 +02:00
|
|
|
|
2015-05-14 14:39:36 +02:00
|
|
|
// ===================================================================
|
|
|
|
|
|
2015-04-14 17:57:31 +02:00
|
|
|
const MAX_TRIES = 5
|
|
|
|
|
|
2015-04-16 16:18:11 +02:00
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2015-03-31 18:44:33 +02:00
|
|
|
export class Xapi extends EventEmitter {
|
|
|
|
|
constructor (opts) {
|
|
|
|
|
super()
|
|
|
|
|
|
|
|
|
|
this._url = parseUrl(opts.url)
|
|
|
|
|
this._auth = opts.auth
|
|
|
|
|
|
2015-05-26 16:35:05 +02:00
|
|
|
this._sessionId = getNotConnectedPromise()
|
2015-03-31 18:44:33 +02:00
|
|
|
|
|
|
|
|
this._init()
|
|
|
|
|
|
2015-04-16 16:34:09 +02:00
|
|
|
this._pool = null
|
2015-05-06 18:14:43 +02:00
|
|
|
this._objectsByRefs = createObject(null)
|
2015-04-16 16:18:11 +02:00
|
|
|
this._objectsByRefs['OpaqueRef:NULL'] = null
|
2015-12-16 14:19:30 +01:00
|
|
|
const objects = this._objects = new Collection()
|
|
|
|
|
objects.getKey = getKey
|
2015-04-10 15:33:39 +02:00
|
|
|
|
2016-04-14 17:55:41 +02:00
|
|
|
this._debounce = opts.debounce == null
|
|
|
|
|
? 200
|
|
|
|
|
: opts.debounce
|
2015-03-31 18:44:33 +02:00
|
|
|
this._fromToken = ''
|
2015-04-10 16:41:17 +02:00
|
|
|
this.on('connected', this._watchEvents)
|
|
|
|
|
this.on('disconnected', () => {
|
|
|
|
|
this._fromToken = ''
|
2015-12-16 14:19:30 +01:00
|
|
|
objects.clear()
|
2015-04-10 16:41:17 +02:00
|
|
|
})
|
2015-09-14 16:01:46 +02:00
|
|
|
|
|
|
|
|
this._readOnly = Boolean(opts.readOnly)
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
2015-12-16 13:58:33 +01:00
|
|
|
get readOnly () {
|
|
|
|
|
return this._readOnly
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set readOnly (ro) {
|
|
|
|
|
this._readOnly = Boolean(ro)
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-13 17:38:07 +02:00
|
|
|
get sessionId () {
|
|
|
|
|
if (this.status !== 'connected') {
|
|
|
|
|
throw new Error('sessionId is only available when connected')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this._sessionId.value()
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-13 16:16:30 +02:00
|
|
|
get status () {
|
|
|
|
|
const {_sessionId: sessionId} = this
|
|
|
|
|
|
|
|
|
|
if (sessionId.isFulfilled()) {
|
|
|
|
|
return 'connected'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sessionId.isPending()) {
|
|
|
|
|
return 'connecting'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'disconnected'
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-10 16:00:20 +02:00
|
|
|
get _humanId () {
|
2015-06-22 13:16:38 +02:00
|
|
|
return `${this._auth.user}@${this._url.hostname}`
|
2015-04-10 16:00:20 +02:00
|
|
|
}
|
|
|
|
|
|
2015-04-10 16:41:43 +02:00
|
|
|
connect () {
|
2015-04-13 16:16:30 +02:00
|
|
|
const {status} = this
|
|
|
|
|
|
|
|
|
|
if (status === 'connected') {
|
|
|
|
|
return Bluebird.reject(new Error('already connected'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status === 'connecting') {
|
|
|
|
|
return Bluebird.reject(new Error('already connecting'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._sessionId = this._transportCall('session.login_with_password', [
|
|
|
|
|
this._auth.user,
|
|
|
|
|
this._auth.password
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
return this._sessionId.then(() => {
|
|
|
|
|
debug('%s: connected', this._humanId)
|
|
|
|
|
|
|
|
|
|
this.emit('connected')
|
|
|
|
|
})
|
2015-04-10 16:41:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disconnect () {
|
2015-04-13 16:16:30 +02:00
|
|
|
const {status} = this
|
|
|
|
|
|
|
|
|
|
if (status === 'disconnected') {
|
2015-09-14 16:45:17 +02:00
|
|
|
return Promise.reject(new Error('already disconnected'))
|
2015-04-13 16:16:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status === 'connecting') {
|
|
|
|
|
return this._sessionId.cancel().catch(Bluebird.CancellationError, () => {
|
|
|
|
|
debug('%s: disconnected', this._humanId)
|
|
|
|
|
|
|
|
|
|
this.emit('disconnected')
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-26 16:35:05 +02:00
|
|
|
this._sessionId = getNotConnectedPromise()
|
2015-04-13 16:16:30 +02:00
|
|
|
|
|
|
|
|
return Bluebird.resolve().then(() => {
|
|
|
|
|
debug('%s: disconnected', this._humanId)
|
|
|
|
|
|
|
|
|
|
this.emit('disconnected')
|
|
|
|
|
})
|
2015-04-10 16:41:43 +02:00
|
|
|
}
|
|
|
|
|
|
2015-03-31 18:44:33 +02:00
|
|
|
// High level calls.
|
2015-04-13 16:42:30 +02:00
|
|
|
call (method, ...args) {
|
2016-04-19 16:27:55 +01:00
|
|
|
return this._readOnly && !isReadOnlyCall(method, args)
|
2015-09-14 16:01:46 +02:00
|
|
|
? Promise.reject(new Error(`cannot call ${method}() in read only mode`))
|
|
|
|
|
: this._sessionCall(method, args)
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
2015-05-06 14:06:17 +02:00
|
|
|
// 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) {
|
2016-01-28 10:08:26 +01:00
|
|
|
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]
|
2015-05-06 14:06:17 +02:00
|
|
|
|
|
|
|
|
if (object) return object
|
|
|
|
|
|
|
|
|
|
if (arguments.length > 1) return defaultValue
|
|
|
|
|
|
|
|
|
|
throw new Error('there is not object can be matched to ' + idOrUuidOrRef)
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-06 14:06:25 +02:00
|
|
|
// Returns the object for a given opaque reference (internal to
|
|
|
|
|
// XAPI).
|
2015-05-06 13:27:59 +02:00
|
|
|
getObjectByRef (ref, defaultValue) {
|
2015-05-06 14:06:25 +02:00
|
|
|
const object = this._objectsByRefs[ref]
|
2015-05-06 13:27:59 +02:00
|
|
|
|
2015-05-06 14:06:25 +02:00
|
|
|
if (object) return object
|
2015-05-06 13:27:59 +02:00
|
|
|
|
2015-05-06 14:06:25 +02:00
|
|
|
if (arguments.length > 1) return defaultValue
|
2015-05-06 13:27:59 +02:00
|
|
|
|
|
|
|
|
throw new Error('there is no object with the ref ' + ref)
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-06 14:06:25 +02:00
|
|
|
// Returns the object for a given UUID (unique identifier that some
|
|
|
|
|
// objects have).
|
2015-05-06 13:27:59 +02:00
|
|
|
getObjectByUuid (uuid, defaultValue) {
|
2015-05-06 14:06:25 +02:00
|
|
|
// Objects ids are already UUIDs if they have one.
|
2015-05-06 14:08:59 +02:00
|
|
|
const object = this._objects.all[uuid]
|
2015-05-06 13:27:59 +02:00
|
|
|
|
2015-05-06 14:06:25 +02:00
|
|
|
if (object) return object
|
2015-05-06 13:27:59 +02:00
|
|
|
|
2015-05-06 14:06:25 +02:00
|
|
|
if (arguments.length > 1) return defaultValue
|
2015-05-06 13:27:59 +02:00
|
|
|
|
|
|
|
|
throw new Error('there is no object with the UUID ' + uuid)
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-13 13:27:29 +02:00
|
|
|
get pool () {
|
|
|
|
|
return this._pool
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-10 15:33:39 +02:00
|
|
|
get objects () {
|
|
|
|
|
return this._objects
|
|
|
|
|
}
|
|
|
|
|
|
2015-03-31 18:44:33 +02:00
|
|
|
// Medium level call: handle session errors.
|
|
|
|
|
_sessionCall (method, args) {
|
2015-04-10 15:33:39 +02:00
|
|
|
if (startsWith(method, 'session.')) {
|
|
|
|
|
return Bluebird.reject(
|
|
|
|
|
new Error('session.*() methods are disabled from this interface')
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-13 16:16:30 +02:00
|
|
|
return this._sessionId.then((sessionId) => {
|
2015-03-31 18:44:33 +02:00
|
|
|
return this._transportCall(method, [sessionId].concat(args))
|
2015-06-29 16:14:00 +02:00
|
|
|
}, error => {
|
|
|
|
|
debug('%s: %s(...) =!> NOT CONNECTED', this._humanId, method)
|
|
|
|
|
throw error
|
2015-03-31 18:44:33 +02:00
|
|
|
}).catch(isSessionInvalid, () => {
|
|
|
|
|
// XAPI is sometimes reinitialized and sessions are lost.
|
|
|
|
|
// Try to login again.
|
2015-04-10 16:00:20 +02:00
|
|
|
debug('%s: the session has been reinitialized', this._humanId)
|
|
|
|
|
|
2015-05-26 16:35:05 +02:00
|
|
|
this._sessionId = getNotConnectedPromise()
|
2015-06-19 15:15:23 +02:00
|
|
|
return this.connect().then(() => this._sessionCall(method, args))
|
2015-03-31 18:44:33 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Low level call: handle transport errors.
|
2015-06-29 16:14:00 +02:00
|
|
|
_transportCall (method, args, startTime = Date.now(), tries = 1) {
|
2015-04-15 18:23:49 +02:00
|
|
|
return this._rawCall(method, args)
|
|
|
|
|
.catch(isNetworkError, isXapiNetworkError, error => {
|
|
|
|
|
debug('%s: network error %s', this._humanId, error.code)
|
2015-03-31 18:44:33 +02:00
|
|
|
|
2015-04-15 18:23:49 +02:00
|
|
|
if (!(tries < MAX_TRIES)) {
|
|
|
|
|
debug('%s too many network errors (%s), give up', this._humanId, tries)
|
2015-03-31 18:44:33 +02:00
|
|
|
|
2015-04-15 18:23:49 +02:00
|
|
|
throw error
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
2015-04-15 18:23:49 +02:00
|
|
|
// TODO: ability to cancel the connection
|
|
|
|
|
// TODO: ability to force immediate reconnection
|
|
|
|
|
// TODO: implement back-off
|
|
|
|
|
|
|
|
|
|
return Bluebird.delay(5e3).then(() => {
|
|
|
|
|
// TODO: handling not responding host.
|
|
|
|
|
|
2015-06-29 16:14:00 +02:00
|
|
|
return this._transportCall(method, args, startTime, tries + 1)
|
2015-04-15 18:23:49 +02:00
|
|
|
})
|
2015-03-31 18:44:33 +02:00
|
|
|
})
|
2015-04-10 16:00:20 +02:00
|
|
|
.catch(isHostSlave, ({params: [master]}) => {
|
|
|
|
|
debug('%s: host is slave, attempting to connect at %s', this._humanId, master)
|
|
|
|
|
|
2015-08-10 15:51:45 +02:00
|
|
|
this._url.hostname = master
|
2015-04-10 16:00:20 +02:00
|
|
|
this._init()
|
|
|
|
|
|
2015-06-29 16:14:00 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
)
|
2015-04-15 18:23:49 +02:00
|
|
|
}
|
2015-04-10 16:41:59 +02:00
|
|
|
|
2015-04-15 18:23:49 +02:00
|
|
|
// Lowest level call: do not handle any errors.
|
|
|
|
|
_rawCall (method, args) {
|
|
|
|
|
return this._xmlRpcCall(method, args)
|
2015-08-28 08:51:38 +02:00
|
|
|
.then(
|
|
|
|
|
parseResult,
|
|
|
|
|
error => {
|
2015-12-02 17:31:13 +01:00
|
|
|
// Unwrap error if necessary.
|
|
|
|
|
if (error instanceof Bluebird.OperationalError) {
|
2015-12-02 17:32:17 +01:00
|
|
|
error = error.cause
|
2015-12-02 17:31:13 +01:00
|
|
|
}
|
2015-08-28 08:51:38 +02:00
|
|
|
|
2015-12-03 12:37:24 +01:00
|
|
|
if (error.res) {
|
|
|
|
|
console.error(
|
|
|
|
|
'XML-RPC Error: %s (response status %s)',
|
|
|
|
|
error.message,
|
|
|
|
|
error.res.statusCode
|
|
|
|
|
)
|
|
|
|
|
console.error('%s', error.body)
|
|
|
|
|
}
|
2015-08-28 08:51:38 +02:00
|
|
|
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
)
|
2015-04-10 16:41:43 +02:00
|
|
|
.cancellable()
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_init () {
|
2015-06-22 13:16:38 +02:00
|
|
|
const {isSecure, hostname, port, path} = this._url
|
2015-03-31 18:44:33 +02:00
|
|
|
|
2015-10-23 16:53:05 +02:00
|
|
|
const client = (isSecure
|
|
|
|
|
? createSecureXmlRpcClient
|
|
|
|
|
: createXmlRpcClient
|
2015-03-31 18:44:33 +02:00
|
|
|
)({
|
2015-06-22 13:16:38 +02:00
|
|
|
hostname,
|
2015-03-31 18:44:33 +02:00
|
|
|
port,
|
2015-06-22 13:16:38 +02:00
|
|
|
path,
|
2015-03-31 18:44:33 +02:00
|
|
|
rejectUnauthorized: false,
|
|
|
|
|
timeout: 10
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this._xmlRpcCall = promisify(client.methodCall, client)
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-22 16:19:32 +02:00
|
|
|
_addObject (type, ref, object) {
|
2015-04-16 16:18:11 +02:00
|
|
|
const {_objectsByRefs: objectsByRefs} = this
|
|
|
|
|
|
2015-06-22 23:33:52 +02:00
|
|
|
// Creates resolved properties.
|
|
|
|
|
forEach(object, function resolveObject (value, key, object) {
|
|
|
|
|
if (isArray(value)) {
|
2015-06-23 09:13:43 +02:00
|
|
|
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
|
2015-12-18 11:28:58 +01:00
|
|
|
} else if (isOpaqueRef(value[0])) {
|
2015-06-23 09:13:43 +02:00
|
|
|
// This is an array of refs.
|
|
|
|
|
defineProperty(object, '$' + key, {
|
2015-06-23 09:17:42 +02:00
|
|
|
get: () => freezeObject(map(value, (ref) => objectsByRefs[ref]))
|
2015-06-23 09:13:43 +02:00
|
|
|
})
|
2015-06-23 09:17:42 +02:00
|
|
|
|
|
|
|
|
freezeObject(value)
|
2015-06-22 23:33:52 +02:00
|
|
|
}
|
|
|
|
|
} else if (isObject(value)) {
|
|
|
|
|
forEach(value, resolveObject)
|
2015-06-23 09:17:42 +02:00
|
|
|
|
|
|
|
|
freezeObject(value)
|
2015-12-16 14:15:37 +01:00
|
|
|
} else if (isOpaqueRef(value)) {
|
2015-06-22 23:33:52 +02:00
|
|
|
defineProperty(object, '$' + key, {
|
2015-06-23 09:13:43 +02:00
|
|
|
get: () => objectsByRefs[value]
|
2015-06-22 23:33:52 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
2015-04-16 16:18:11 +02:00
|
|
|
|
2015-06-22 23:33:52 +02:00
|
|
|
// All custom properties are read-only and non enumerable.
|
2015-05-14 14:58:54 +02:00
|
|
|
defineProperties(object, {
|
|
|
|
|
$id: { value: object.uuid || ref },
|
|
|
|
|
$pool: { get: () => this._pool },
|
|
|
|
|
$ref: { value: ref },
|
|
|
|
|
$type: { value: type }
|
2015-04-10 15:33:39 +02:00
|
|
|
})
|
2015-06-22 16:19:32 +02:00
|
|
|
|
2015-06-23 09:17:42 +02:00
|
|
|
// Finally freezes the object.
|
|
|
|
|
freezeObject(object)
|
|
|
|
|
|
2016-04-26 08:46:04 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-22 16:19:32 +02:00
|
|
|
this._objects.set(object)
|
|
|
|
|
objectsByRefs[ref] = object
|
|
|
|
|
|
|
|
|
|
if (type === 'pool') {
|
|
|
|
|
this._pool = object
|
|
|
|
|
}
|
2015-04-10 15:33:39 +02:00
|
|
|
}
|
|
|
|
|
|
2015-06-22 16:19:32 +02:00
|
|
|
_removeObject (ref) {
|
|
|
|
|
const {_objectsByRefs: objectsByRefs} = this
|
2015-03-31 18:44:33 +02:00
|
|
|
|
2015-06-22 16:19:32 +02:00
|
|
|
const object = objectsByRefs[ref]
|
2015-04-10 15:33:39 +02:00
|
|
|
|
2015-06-22 16:19:32 +02:00
|
|
|
if (object) {
|
2015-10-02 14:05:26 +02:00
|
|
|
this._objects.unset(object.$id)
|
2015-06-22 16:19:32 +02:00
|
|
|
delete objectsByRefs[ref]
|
|
|
|
|
}
|
|
|
|
|
}
|
2015-04-10 15:33:39 +02:00
|
|
|
|
2015-06-22 16:19:32 +02:00
|
|
|
_processEvents (events) {
|
|
|
|
|
forEach(events, event => {
|
|
|
|
|
const {operation: op} = event
|
2015-04-10 15:33:39 +02:00
|
|
|
|
2015-06-22 16:19:32 +02:00
|
|
|
const {ref} = event
|
|
|
|
|
if (op === 'del') {
|
|
|
|
|
this._removeObject(ref)
|
|
|
|
|
} else {
|
|
|
|
|
this._addObject(event.class, ref, event.snapshot)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2015-04-10 15:33:39 +02:00
|
|
|
|
2015-06-22 16:19:32 +02:00
|
|
|
_watchEvents () {
|
2016-05-09 18:11:14 +02:00
|
|
|
const loop = invoke(() => {
|
|
|
|
|
const onSuccess = ({token, events}) => {
|
2015-06-30 14:24:43 +02:00
|
|
|
this._fromToken = token
|
|
|
|
|
this._processEvents(events)
|
2015-04-10 15:33:39 +02:00
|
|
|
|
2016-05-09 18:14:23 +02:00
|
|
|
const debounce = this._debounce
|
2016-04-14 17:55:41 +02:00
|
|
|
return debounce != null
|
|
|
|
|
? Bluebird.delay(debounce).then(loop)
|
|
|
|
|
: loop()
|
2016-05-09 18:11:14 +02:00
|
|
|
}
|
|
|
|
|
const onFailure = error => {
|
2015-06-30 14:24:43 +02:00
|
|
|
if (areEventsLost(error)) {
|
|
|
|
|
this._fromToken = ''
|
|
|
|
|
this._objects.clear()
|
|
|
|
|
|
|
|
|
|
return loop()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw error
|
|
|
|
|
}
|
2016-05-09 18:11:14 +02:00
|
|
|
|
|
|
|
|
return () => this._sessionCall('event.from', [
|
|
|
|
|
['*'],
|
|
|
|
|
this._fromToken,
|
|
|
|
|
1e3 + 0.1 // Force float.
|
|
|
|
|
]).then(onSuccess, onFailure)
|
|
|
|
|
})
|
2015-06-30 14:24:43 +02:00
|
|
|
|
|
|
|
|
return loop().catch(error => {
|
2015-12-02 17:32:17 +01:00
|
|
|
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.
|
2016-01-05 16:09:06 +01:00
|
|
|
error && error.res && error.res.statusCode === 500
|
2015-12-02 17:32:17 +01:00
|
|
|
) {
|
2015-06-30 14:24:43 +02:00
|
|
|
return this._watchEventsLegacy()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!(error instanceof Bluebird.CancellationError)) {
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
})
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
2015-06-22 16:19:32 +02:00
|
|
|
|
|
|
|
|
// This method watches events using the legacy `event.next` XAPI
|
|
|
|
|
// methods.
|
|
|
|
|
//
|
|
|
|
|
// It also has to manually get all objects first.
|
|
|
|
|
_watchEventsLegacy () {
|
|
|
|
|
const getAllObjects = () => {
|
2015-10-23 15:33:22 +02:00
|
|
|
return this._sessionCall('system.listMethods', []).then(methods => {
|
2015-06-22 16:19:32 +02:00
|
|
|
// 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,
|
2015-10-23 16:53:42 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
2015-06-22 16:19:32 +02:00
|
|
|
))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-09 18:11:14 +02:00
|
|
|
const watchEvents = invoke(() => {
|
|
|
|
|
const loop = invoke(() => {
|
|
|
|
|
const onSuccess = events => {
|
2015-06-30 14:24:43 +02:00
|
|
|
this._processEvents(events)
|
2016-05-09 18:14:23 +02:00
|
|
|
|
|
|
|
|
const debounce = this._debounce
|
2016-04-14 17:55:41 +02:00
|
|
|
return debounce == null
|
|
|
|
|
? loop()
|
|
|
|
|
: Bluebird.delay(debounce).then(loop)
|
2016-05-09 18:11:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onFailure = error => {
|
2015-06-30 14:24:43 +02:00
|
|
|
if (areEventsLost(error)) {
|
2015-10-23 15:33:22 +02:00
|
|
|
return this._sessionCall('event.unregister', [ ['*'] ]).then(watchEvents)
|
2015-06-30 14:24:43 +02:00
|
|
|
}
|
2015-06-22 16:19:32 +02:00
|
|
|
|
2015-06-30 14:24:43 +02:00
|
|
|
throw error
|
|
|
|
|
}
|
2016-05-09 18:11:14 +02:00
|
|
|
|
|
|
|
|
return () => this._sessionCall('event.next', []).then(onSuccess, onFailure)
|
|
|
|
|
})
|
2015-06-22 16:19:32 +02:00
|
|
|
|
2015-10-23 15:33:22 +02:00
|
|
|
return () => this._sessionCall('event.register', [ ['*'] ]).then(loop)
|
2016-05-09 18:11:14 +02:00
|
|
|
})
|
2015-06-22 16:19:32 +02:00
|
|
|
|
|
|
|
|
return getAllObjects().then(watchEvents)
|
|
|
|
|
}
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================================================================
|
|
|
|
|
|
|
|
|
|
// The default value is a factory function.
|
|
|
|
|
export const createClient = (opts) => new Xapi(opts)
|