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-29 16:14:00 +02:00
|
|
|
import kindOf from 'kindof'
|
|
|
|
|
import ms from 'ms'
|
2017-05-22 15:51:41 +02:00
|
|
|
import httpRequest from 'http-request-plus'
|
2016-05-11 17:41:58 +02:00
|
|
|
import { BaseError } from 'make-error'
|
|
|
|
|
import { EventEmitter } from 'events'
|
2017-10-28 13:14:15 +02:00
|
|
|
import { fibonacci } from 'iterable-backoff'
|
2017-11-26 19:16:05 +00:00
|
|
|
import {
|
|
|
|
|
filter,
|
|
|
|
|
forEach,
|
|
|
|
|
isArray,
|
|
|
|
|
isInteger,
|
|
|
|
|
isObject,
|
|
|
|
|
map,
|
|
|
|
|
noop,
|
|
|
|
|
omit,
|
|
|
|
|
reduce,
|
|
|
|
|
startsWith,
|
|
|
|
|
} from 'lodash'
|
2016-05-11 17:41:58 +02:00
|
|
|
import {
|
2017-06-08 15:21:52 +02:00
|
|
|
Cancel,
|
2017-05-22 15:51:41 +02:00
|
|
|
cancelable,
|
2016-05-11 17:41:58 +02:00
|
|
|
catchPlus as pCatch,
|
2017-06-08 12:19:08 +02:00
|
|
|
defer,
|
2017-06-08 15:22:13 +02:00
|
|
|
delay as pDelay,
|
2017-06-29 15:07:17 +02:00
|
|
|
fromEvents,
|
2017-11-17 17:42:48 +01:00
|
|
|
lastly,
|
2016-05-11 17:41:58 +02:00
|
|
|
} from 'promise-toolbox'
|
2017-04-28 16:22:35 +02:00
|
|
|
|
|
|
|
|
import autoTransport from './transports/auto'
|
2015-03-31 18:44:33 +02:00
|
|
|
|
2015-04-10 15:33:39 +02:00
|
|
|
const debug = createDebug('xen-api')
|
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.
|
2017-11-17 17:42:48 +01:00
|
|
|
ETIMEDOUT: true,
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
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,
|
2017-11-17 17:42:48 +01:00
|
|
|
HOST_HAS_NO_MANAGEMENT_IP: true,
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
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 {
|
2017-12-15 13:58:31 +01:00
|
|
|
constructor (code, params) {
|
2015-06-17 15:39:33 +02:00
|
|
|
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
|
2017-02-13 15:47:09 +01:00
|
|
|
|
2017-12-15 13:58:31 +01:00
|
|
|
// slots than can be assigned later
|
2017-11-13 15:48:18 +01:00
|
|
|
this.method = undefined
|
|
|
|
|
this.url = undefined
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-15 13:58:31 +01:00
|
|
|
export const wrapError = error => {
|
|
|
|
|
let code, params
|
|
|
|
|
if (isArray(error)) { // < XenServer 7.3
|
|
|
|
|
[ code, ...params ] = error
|
|
|
|
|
} else {
|
|
|
|
|
code = error.message
|
|
|
|
|
params = error.data
|
|
|
|
|
}
|
|
|
|
|
return new XapiError(code, params)
|
|
|
|
|
}
|
2015-05-05 13:45:51 +02:00
|
|
|
|
2015-03-31 18:44:33 +02:00
|
|
|
// ===================================================================
|
|
|
|
|
|
2017-06-12 16:41:21 +02:00
|
|
|
const URL_RE = /^(?:(https?:)\/*)?(?:([^:]+):([^@]+)@)?([^/]+?)(?::([0-9]+))?\/?$/
|
2017-04-28 16:22:35 +02:00
|
|
|
const parseUrl = url => {
|
2015-03-31 18:44:33 +02:00
|
|
|
const matches = URL_RE.exec(url)
|
|
|
|
|
if (!matches) {
|
|
|
|
|
throw new Error('invalid URL: ' + url)
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-12 16:41:21 +02:00
|
|
|
const [ , protocol = 'https:', username, password, hostname, port ] = matches
|
|
|
|
|
return { protocol, username, password, hostname, port }
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
2015-04-16 16:18:11 +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,
|
2017-11-17 17:42:48 +01:00
|
|
|
freeze: freezeObject,
|
2015-05-06 18:14:43 +02:00
|
|
|
} = Object
|
|
|
|
|
|
2015-04-16 16:18:11 +02:00
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2015-12-18 11:23:24 +01:00
|
|
|
const OPAQUE_REF_PREFIX = 'OpaqueRef:'
|
2017-06-13 13:33:19 +02:00
|
|
|
export const isOpaqueRef = value =>
|
2017-05-03 11:02:46 +02:00
|
|
|
typeof value === 'string' &&
|
|
|
|
|
startsWith(value, OPAQUE_REF_PREFIX)
|
2015-05-14 14:39:36 +02:00
|
|
|
|
2015-06-23 09:13:43 +02:00
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2017-05-03 11:02:59 +02:00
|
|
|
const RE_READ_ONLY_METHOD = /^[^.]+\.get_/
|
|
|
|
|
const isReadOnlyCall = (method, args) => (
|
2016-04-19 16:27:55 +01:00
|
|
|
args.length === 1 &&
|
|
|
|
|
isOpaqueRef(args[0]) &&
|
2017-05-03 11:02:59 +02:00
|
|
|
RE_READ_ONLY_METHOD.test(method)
|
|
|
|
|
)
|
2016-04-19 16:27:55 +01:00
|
|
|
|
2017-11-26 19:16:05 +00:00
|
|
|
// Prepare values before passing them to the XenAPI:
|
|
|
|
|
//
|
|
|
|
|
// - cast integers to strings
|
|
|
|
|
const prepareParam = param => {
|
2018-01-09 17:28:13 +01:00
|
|
|
if (isInteger(param)) {
|
2017-11-26 19:16:05 +00:00
|
|
|
return String(param)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof param !== 'object' || param === null) {
|
|
|
|
|
return param
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-09 17:47:46 +01:00
|
|
|
if (isArray(param)) {
|
|
|
|
|
return map(param, prepareParam)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const values = {}
|
|
|
|
|
forEach(param, (value, key) => {
|
|
|
|
|
if (value !== undefined) {
|
|
|
|
|
values[key] = prepareParam(value)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
return values
|
2017-11-26 19:16:05 +00: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
|
|
|
|
2017-06-08 15:21:52 +02:00
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
const getTaskResult = (task, onSuccess, onFailure) => {
|
|
|
|
|
const { status } = task
|
|
|
|
|
if (status === 'cancelled') {
|
|
|
|
|
return [ onFailure(new Cancel('task canceled')) ]
|
|
|
|
|
}
|
|
|
|
|
if (status === 'failure') {
|
2017-06-08 15:22:13 +02:00
|
|
|
return [ onFailure(wrapError(task.error_info)) ]
|
2017-06-08 15:21:52 +02:00
|
|
|
}
|
|
|
|
|
if (status === 'success') {
|
2017-07-05 10:35:00 +02:00
|
|
|
// the result might be:
|
|
|
|
|
// - empty string
|
|
|
|
|
// - an opaque reference
|
|
|
|
|
// - an XML-RPC value
|
|
|
|
|
return [ onSuccess(task.result) ]
|
2017-06-08 15:21:52 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-12 17:36:19 +02:00
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
const CONNECTED = 'connected'
|
|
|
|
|
const CONNECTING = 'connecting'
|
|
|
|
|
const DISCONNECTED = 'disconnected'
|
|
|
|
|
|
2015-04-16 16:18:11 +02:00
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2015-03-31 18:44:33 +02:00
|
|
|
export class Xapi extends EventEmitter {
|
|
|
|
|
constructor (opts) {
|
|
|
|
|
super()
|
|
|
|
|
|
2017-05-11 16:35:02 +02:00
|
|
|
this._allowUnauthorized = opts.allowUnauthorized
|
2015-03-31 18:44:33 +02:00
|
|
|
this._auth = opts.auth
|
2016-05-10 15:35:43 +02:00
|
|
|
this._pool = null
|
|
|
|
|
this._readOnly = Boolean(opts.readOnly)
|
2016-05-11 17:41:58 +02:00
|
|
|
this._sessionId = null
|
2017-06-12 16:41:21 +02:00
|
|
|
const url = this._url = parseUrl(opts.url)
|
|
|
|
|
|
|
|
|
|
if (this._auth === undefined) {
|
|
|
|
|
const user = url.username
|
|
|
|
|
if (user !== undefined) {
|
|
|
|
|
this._auth = {
|
|
|
|
|
user,
|
2017-11-17 17:42:48 +01:00
|
|
|
password: url.password,
|
2017-06-12 16:41:21 +02:00
|
|
|
}
|
|
|
|
|
delete url.username
|
|
|
|
|
delete url.password
|
|
|
|
|
}
|
|
|
|
|
}
|
2015-03-31 18:44:33 +02:00
|
|
|
|
2016-05-10 15:35:43 +02:00
|
|
|
if (opts.watchEvents !== false) {
|
|
|
|
|
this._debounce = opts.debounce == null
|
|
|
|
|
? 200
|
|
|
|
|
: opts.debounce
|
2017-07-31 16:24:10 +02:00
|
|
|
|
|
|
|
|
this._eventWatchers = createObject(null)
|
|
|
|
|
|
2015-04-10 16:41:17 +02:00
|
|
|
this._fromToken = ''
|
2015-09-14 16:01:46 +02:00
|
|
|
|
2016-05-10 15:35:43 +02:00
|
|
|
// Memoize this function _addObject().
|
|
|
|
|
this._getPool = () => this._pool
|
|
|
|
|
|
2018-01-24 14:06:46 +01:00
|
|
|
this._nTasks = 0
|
|
|
|
|
|
2016-05-10 15:35:43 +02:00
|
|
|
const objects = this._objects = new Collection()
|
|
|
|
|
objects.getKey = getKey
|
2016-05-10 14:55:19 +02:00
|
|
|
|
2016-05-10 15:35:43 +02:00
|
|
|
this._objectsByRefs = createObject(null)
|
|
|
|
|
this._objectsByRefs['OpaqueRef:NULL'] = null
|
|
|
|
|
|
2017-06-08 12:19:08 +02:00
|
|
|
this._taskWatchers = Object.create(null)
|
|
|
|
|
|
2016-05-10 15:35:43 +02:00
|
|
|
this.on('connected', this._watchEvents)
|
|
|
|
|
this.on('disconnected', () => {
|
|
|
|
|
this._fromToken = ''
|
|
|
|
|
objects.clear()
|
|
|
|
|
})
|
|
|
|
|
}
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
2017-04-28 16:22:35 +02:00
|
|
|
get _url () {
|
|
|
|
|
return this.__url
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set _url (url) {
|
|
|
|
|
this.__url = url
|
2017-05-11 16:35:02 +02:00
|
|
|
this._call = autoTransport({
|
|
|
|
|
allowUnauthorized: this._allowUnauthorized,
|
2017-11-17 17:42:48 +01:00
|
|
|
url,
|
2017-05-11 16:35:02 +02:00
|
|
|
})
|
2017-04-28 16:22:35 +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 () {
|
2016-05-11 17:41:58 +02:00
|
|
|
const id = this._sessionId
|
|
|
|
|
|
|
|
|
|
if (!id || id === CONNECTING) {
|
2015-04-13 17:38:07 +02:00
|
|
|
throw new Error('sessionId is only available when connected')
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
return id
|
2015-04-13 17:38:07 +02:00
|
|
|
}
|
|
|
|
|
|
2015-04-13 16:16:30 +02:00
|
|
|
get status () {
|
2016-05-11 17:41:58 +02:00
|
|
|
const id = this._sessionId
|
2015-04-13 16:16:30 +02:00
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
return id
|
|
|
|
|
? (
|
|
|
|
|
id === CONNECTING
|
|
|
|
|
? CONNECTING
|
|
|
|
|
: CONNECTED
|
|
|
|
|
)
|
|
|
|
|
: DISCONNECTED
|
2015-04-13 16:16:30 +02:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2017-10-09 11:58:02 +02:00
|
|
|
// ensure we have received all events up to this call
|
|
|
|
|
//
|
|
|
|
|
// optionally returns the up to date object for the given ref
|
|
|
|
|
barrier (ref) {
|
2017-07-31 16:24:10 +02:00
|
|
|
const eventWatchers = this._eventWatchers
|
|
|
|
|
if (eventWatchers === undefined) {
|
|
|
|
|
return Promise.reject(new Error('Xapi#barrier() requires events watching'))
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-09 11:58:02 +02:00
|
|
|
const key = `xo:barrier:${Math.random().toString(36).slice(2)}`
|
|
|
|
|
const poolRef = this._pool.$ref
|
|
|
|
|
|
|
|
|
|
const { promise, resolve } = defer()
|
|
|
|
|
eventWatchers[key] = resolve
|
2017-08-08 12:06:32 +02:00
|
|
|
|
2017-07-31 16:24:10 +02:00
|
|
|
return this._sessionCall(
|
2017-10-09 11:58:02 +02:00
|
|
|
'pool.add_to_other_config',
|
|
|
|
|
[ poolRef, key, '' ]
|
|
|
|
|
).then(() => promise.then(() => {
|
|
|
|
|
this._sessionCall('pool.remove_from_other_config', [ poolRef, key ]).catch(noop)
|
|
|
|
|
|
|
|
|
|
if (ref === undefined) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// support legacy params (type, ref)
|
|
|
|
|
if (arguments.length === 2) {
|
|
|
|
|
ref = arguments[1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.getObjectByRef(ref)
|
|
|
|
|
}))
|
2017-07-31 16:24:10 +02:00
|
|
|
}
|
|
|
|
|
|
2015-04-10 16:41:43 +02:00
|
|
|
connect () {
|
2015-04-13 16:16:30 +02:00
|
|
|
const {status} = this
|
|
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
if (status === CONNECTED) {
|
|
|
|
|
return Promise.reject(new Error('already connected'))
|
2015-04-13 16:16:30 +02:00
|
|
|
}
|
|
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
if (status === CONNECTING) {
|
|
|
|
|
return Promise.reject(new Error('already connecting'))
|
2015-04-13 16:16:30 +02:00
|
|
|
}
|
|
|
|
|
|
2017-07-05 10:08:34 +02:00
|
|
|
const auth = this._auth
|
|
|
|
|
if (auth === undefined) {
|
|
|
|
|
return Promise.reject(new Error('missing credentials'))
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-28 16:22:35 +02:00
|
|
|
this._sessionId = CONNECTING
|
2016-05-11 17:41:58 +02:00
|
|
|
|
|
|
|
|
return this._transportCall('session.login_with_password', [
|
2017-07-05 17:17:54 +02:00
|
|
|
auth.user,
|
2017-11-17 17:42:48 +01:00
|
|
|
auth.password,
|
2016-05-11 17:41:58 +02:00
|
|
|
]).then(
|
|
|
|
|
sessionId => {
|
|
|
|
|
this._sessionId = sessionId
|
2015-04-13 16:16:30 +02:00
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
debug('%s: connected', this._humanId)
|
2015-04-13 16:16:30 +02:00
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
this.emit(CONNECTED)
|
|
|
|
|
},
|
|
|
|
|
error => {
|
|
|
|
|
this._sessionId = null
|
|
|
|
|
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
)
|
2015-04-10 16:41:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disconnect () {
|
2016-05-11 17:41:58 +02:00
|
|
|
return Promise.resolve().then(() => {
|
|
|
|
|
const { status } = this
|
2015-04-13 16:16:30 +02:00
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
if (status === DISCONNECTED) {
|
|
|
|
|
return Promise.reject(new Error('already disconnected'))
|
|
|
|
|
}
|
2015-04-13 16:16:30 +02:00
|
|
|
|
2017-11-26 19:48:19 +00:00
|
|
|
this._transportCall('session.logout', [ this._sessionId ]).catch(noop)
|
|
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
this._sessionId = null
|
2015-04-13 16:16:30 +02:00
|
|
|
|
|
|
|
|
debug('%s: disconnected', this._humanId)
|
|
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
this.emit(DISCONNECTED)
|
2015-04-13 16:16:30 +02:00
|
|
|
})
|
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`))
|
2017-11-26 19:16:05 +00:00
|
|
|
: this._sessionCall(method, prepareParam(args))
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
2017-06-08 15:22:13 +02:00
|
|
|
@cancelable
|
|
|
|
|
callAsync ($cancelToken, method, ...args) {
|
2017-11-26 19:47:59 +00:00
|
|
|
return this._readOnly && !isReadOnlyCall(method, args)
|
|
|
|
|
? Promise.reject(new Error(`cannot call ${method}() in read only mode`))
|
2018-01-26 16:13:18 +01:00
|
|
|
: this._sessionCall(`Async.${method}`, args).then(taskRef => {
|
2017-11-26 19:47:59 +00:00
|
|
|
$cancelToken.promise.then(() => {
|
2018-01-26 16:13:18 +01:00
|
|
|
this._sessionCall('task.cancel', [taskRef]).catch(noop)
|
2017-11-26 19:47:59 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return this.watchTask(taskRef)::lastly(() => {
|
2018-01-26 16:13:18 +01:00
|
|
|
this._sessionCall('task.destroy', [taskRef]).catch(noop)
|
2017-11-26 19:47:59 +00:00
|
|
|
})
|
2017-06-08 15:22:13 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-22 18:23:57 +02:00
|
|
|
// create a task and automatically destroy it when settled
|
|
|
|
|
createTask (nameLabel, nameDescription = '') {
|
2017-11-28 11:08:23 +00:00
|
|
|
if (this._readOnly) {
|
|
|
|
|
return Promise.reject(new Error('cannot create task in read only mode'))
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-22 18:23:57 +02:00
|
|
|
const promise = this._sessionCall('task.create', [
|
|
|
|
|
nameLabel,
|
2017-11-17 17:42:48 +01:00
|
|
|
nameDescription,
|
2017-06-22 18:23:57 +02:00
|
|
|
])
|
|
|
|
|
|
|
|
|
|
promise.then(taskRef => {
|
|
|
|
|
const destroy = () =>
|
2018-01-26 16:13:18 +01:00
|
|
|
this._sessionCall('task.destroy', [taskRef]).catch(noop)
|
2017-06-22 18:23:57 +02:00
|
|
|
this.watchTask(taskRef).then(destroy, destroy)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return promise
|
|
|
|
|
}
|
|
|
|
|
|
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) {
|
2017-05-03 11:02:46 +02:00
|
|
|
const object = typeof idOrUuidOrRef === 'string'
|
2016-01-28 10:08:26 +01:00
|
|
|
? (
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-26 19:19:03 +00:00
|
|
|
getRecord (type, ref) {
|
2018-01-26 16:13:18 +01:00
|
|
|
return this._sessionCall(`${type}.get_record`, [ref])
|
2017-11-26 19:19:03 +00:00
|
|
|
}
|
|
|
|
|
|
2017-05-22 15:51:41 +02:00
|
|
|
@cancelable
|
2017-06-22 17:33:32 +02:00
|
|
|
getResource ($cancelToken, pathname, {
|
|
|
|
|
host,
|
|
|
|
|
query,
|
2017-11-17 17:42:48 +01:00
|
|
|
task,
|
2017-06-22 17:33:32 +02:00
|
|
|
}) {
|
2017-06-23 16:45:31 +02:00
|
|
|
return this._autoTask(
|
|
|
|
|
task,
|
|
|
|
|
`Xapi#getResource ${pathname}`
|
2017-06-22 17:33:32 +02:00
|
|
|
).then(taskRef => {
|
|
|
|
|
query = { ...query, session_id: this.sessionId }
|
|
|
|
|
let taskResult
|
|
|
|
|
if (taskRef !== undefined) {
|
|
|
|
|
query.task_id = taskRef
|
|
|
|
|
taskResult = this.watchTask(taskRef)
|
2017-06-23 15:54:15 +02:00
|
|
|
|
|
|
|
|
if (typeof $cancelToken.addHandler === 'function') {
|
2017-06-27 09:49:26 +02:00
|
|
|
$cancelToken.addHandler(() => taskResult)
|
2017-06-23 15:54:15 +02:00
|
|
|
}
|
2017-06-22 17:33:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let promise = httpRequest(
|
|
|
|
|
$cancelToken,
|
|
|
|
|
this._url,
|
|
|
|
|
host && {
|
2017-11-17 17:42:48 +01:00
|
|
|
hostname: this.getObject(host).address,
|
2017-05-22 15:51:41 +02:00
|
|
|
},
|
2017-06-22 17:33:32 +02:00
|
|
|
{
|
|
|
|
|
pathname,
|
|
|
|
|
query,
|
2017-11-17 17:42:48 +01:00
|
|
|
rejectUnauthorized: !this._allowUnauthorized,
|
2017-06-22 17:33:32 +02:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (taskResult !== undefined) {
|
|
|
|
|
promise = promise.then(response => {
|
|
|
|
|
response.task = taskResult
|
|
|
|
|
return response
|
|
|
|
|
})
|
2017-05-22 15:51:41 +02:00
|
|
|
}
|
2017-06-22 17:33:32 +02:00
|
|
|
|
|
|
|
|
return promise
|
|
|
|
|
})
|
2017-05-22 15:51:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@cancelable
|
2017-05-26 17:49:14 +02:00
|
|
|
putResource ($cancelToken, body, pathname, {
|
2017-05-22 15:51:41 +02:00
|
|
|
host,
|
2017-06-22 19:45:18 +02:00
|
|
|
query,
|
2017-11-17 17:42:48 +01:00
|
|
|
task,
|
2017-05-22 15:51:41 +02:00
|
|
|
} = {}) {
|
2017-11-28 11:08:23 +00:00
|
|
|
if (this._readOnly) {
|
|
|
|
|
return Promise.reject(new Error(new Error('cannot put resource in read only mode')))
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-23 16:45:31 +02:00
|
|
|
return this._autoTask(
|
|
|
|
|
task,
|
|
|
|
|
`Xapi#putResource ${pathname}`
|
2017-06-22 19:45:18 +02:00
|
|
|
).then(taskRef => {
|
|
|
|
|
query = { ...query, session_id: this.sessionId }
|
2017-05-22 15:51:41 +02:00
|
|
|
|
2017-06-22 19:45:18 +02:00
|
|
|
let taskResult
|
|
|
|
|
if (taskRef !== undefined) {
|
|
|
|
|
query.task_id = taskRef
|
|
|
|
|
taskResult = this.watchTask(taskRef)
|
2017-06-23 15:54:15 +02:00
|
|
|
|
|
|
|
|
if (typeof $cancelToken.addHandler === 'function') {
|
2017-06-27 09:49:26 +02:00
|
|
|
$cancelToken.addHandler(() => taskResult)
|
2017-06-23 15:54:15 +02:00
|
|
|
}
|
2017-06-22 19:45:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const headers = {}
|
|
|
|
|
|
|
|
|
|
// Xen API does not support chunk encoding.
|
|
|
|
|
const isStream = typeof body.pipe === 'function'
|
|
|
|
|
const { length } = body
|
|
|
|
|
if (isStream && length === undefined) {
|
|
|
|
|
// add a fake huge content length (1 PiB)
|
|
|
|
|
headers['content-length'] = '1125899906842624'
|
|
|
|
|
}
|
2017-06-13 17:49:55 +02:00
|
|
|
|
2017-06-22 19:45:18 +02:00
|
|
|
const doRequest = override => httpRequest.put(
|
|
|
|
|
$cancelToken,
|
|
|
|
|
this._url,
|
|
|
|
|
host && {
|
2017-11-17 17:42:48 +01:00
|
|
|
hostname: this.getObject(host).address,
|
2017-06-21 16:51:36 +02:00
|
|
|
},
|
2017-06-22 19:45:18 +02:00
|
|
|
{
|
|
|
|
|
body,
|
|
|
|
|
headers,
|
|
|
|
|
pathname,
|
2017-06-26 12:19:52 +02:00
|
|
|
query,
|
2017-11-17 17:42:48 +01:00
|
|
|
rejectUnauthorized: !this._allowUnauthorized,
|
2017-06-21 16:51:36 +02:00
|
|
|
},
|
2017-06-22 19:45:18 +02:00
|
|
|
override
|
|
|
|
|
)
|
2017-06-21 16:51:36 +02:00
|
|
|
|
2017-06-22 19:45:18 +02:00
|
|
|
const promise = isStream
|
|
|
|
|
|
|
|
|
|
// dummy request to probe for a redirection before consuming body
|
|
|
|
|
? doRequest({
|
|
|
|
|
body: '',
|
2017-06-26 12:19:52 +02:00
|
|
|
|
|
|
|
|
// omit task_id because this request will fail on purpose
|
|
|
|
|
query: 'task_id' in query
|
|
|
|
|
? omit(query, 'task_id')
|
|
|
|
|
: query,
|
|
|
|
|
|
2017-11-17 17:42:48 +01:00
|
|
|
maxRedirects: 0,
|
2017-06-22 19:45:18 +02:00
|
|
|
}).then(
|
|
|
|
|
response => {
|
|
|
|
|
response.req.abort()
|
|
|
|
|
return doRequest()
|
|
|
|
|
},
|
|
|
|
|
error => {
|
|
|
|
|
let response
|
|
|
|
|
if (error != null && (response = error.response) != null) {
|
|
|
|
|
response.req.abort()
|
|
|
|
|
|
|
|
|
|
const { headers: { location }, statusCode } = response
|
|
|
|
|
if (statusCode === 302 && location !== undefined) {
|
|
|
|
|
return doRequest(location)
|
|
|
|
|
}
|
2017-06-21 16:51:36 +02:00
|
|
|
}
|
|
|
|
|
|
2017-06-22 19:45:18 +02:00
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-06-21 16:51:36 +02:00
|
|
|
|
2017-06-22 19:45:18 +02:00
|
|
|
// http-request-plus correctly handle redirects if body is not a stream
|
|
|
|
|
: doRequest()
|
2017-06-21 16:51:36 +02:00
|
|
|
|
2017-06-22 19:45:18 +02:00
|
|
|
return promise.then(response => {
|
|
|
|
|
const { req } = response
|
2017-06-21 16:51:36 +02:00
|
|
|
|
2017-11-13 15:48:18 +01:00
|
|
|
if (taskResult !== undefined) {
|
|
|
|
|
taskResult = taskResult.catch(error => {
|
|
|
|
|
error.url = response.url
|
|
|
|
|
throw error
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-22 19:45:18 +02:00
|
|
|
if (req.finished) {
|
|
|
|
|
req.abort()
|
|
|
|
|
return taskResult
|
|
|
|
|
}
|
2017-06-21 16:51:36 +02:00
|
|
|
|
2017-06-29 15:07:17 +02:00
|
|
|
return fromEvents(req, ['close', 'finish']).then(() => {
|
2017-06-22 19:45:18 +02:00
|
|
|
req.abort()
|
|
|
|
|
return taskResult
|
|
|
|
|
})
|
2017-06-12 17:36:19 +02:00
|
|
|
})
|
|
|
|
|
})
|
2017-05-22 15:51:41 +02:00
|
|
|
}
|
|
|
|
|
|
2017-06-08 12:19:08 +02:00
|
|
|
watchTask (ref) {
|
|
|
|
|
const watchers = this._taskWatchers
|
|
|
|
|
if (watchers === undefined) {
|
|
|
|
|
throw new Error('Xapi#watchTask() requires events watching')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// allow task object to be passed
|
|
|
|
|
if (ref.$ref !== undefined) ref = ref.$ref
|
|
|
|
|
|
|
|
|
|
let watcher = watchers[ref]
|
|
|
|
|
if (watcher === undefined) {
|
2017-06-08 15:21:52 +02:00
|
|
|
// sync check if the task is already settled
|
|
|
|
|
const task = this.objects.all[ref]
|
|
|
|
|
if (task !== undefined) {
|
2017-06-20 15:06:43 +02:00
|
|
|
const result = getTaskResult(task, Promise.resolve, Promise.reject)
|
2017-06-08 15:21:52 +02:00
|
|
|
if (result) {
|
|
|
|
|
return result[0]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-08 12:19:08 +02:00
|
|
|
watcher = watchers[ref] = defer()
|
|
|
|
|
}
|
|
|
|
|
return watcher.promise
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-23 16:45:31 +02:00
|
|
|
// return a promise which resolves to a task ref or undefined
|
|
|
|
|
_autoTask (task = this._taskWatchers !== undefined, name) {
|
|
|
|
|
if (task === false) {
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (task === true) {
|
|
|
|
|
return this.createTask(name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// either a reference or a promise to a reference
|
|
|
|
|
return Promise.resolve(task)
|
|
|
|
|
}
|
|
|
|
|
|
2015-03-31 18:44:33 +02:00
|
|
|
// Medium level call: handle session errors.
|
|
|
|
|
_sessionCall (method, args) {
|
2016-08-18 09:37:31 +02:00
|
|
|
try {
|
|
|
|
|
if (startsWith(method, 'session.')) {
|
|
|
|
|
throw new Error('session.*() methods are disabled from this interface')
|
|
|
|
|
}
|
2015-04-10 15:33:39 +02:00
|
|
|
|
2016-08-18 09:37:31 +02:00
|
|
|
return this._transportCall(method, [this.sessionId].concat(args))
|
|
|
|
|
::pCatch(isSessionInvalid, () => {
|
|
|
|
|
// XAPI is sometimes reinitialized and sessions are lost.
|
|
|
|
|
// Try to login again.
|
|
|
|
|
debug('%s: the session has been reinitialized', this._humanId)
|
2015-04-10 16:00:20 +02:00
|
|
|
|
2016-08-18 09:37:31 +02:00
|
|
|
this._sessionId = null
|
|
|
|
|
return this.connect().then(() => this._sessionCall(method, args))
|
|
|
|
|
})
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return Promise.reject(error)
|
|
|
|
|
}
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
2015-06-22 16:19:32 +02:00
|
|
|
_addObject (type, ref, object) {
|
2015-04-16 16:18:11 +02:00
|
|
|
const {_objectsByRefs: objectsByRefs} = this
|
|
|
|
|
|
2016-08-05 10:42:33 +02:00
|
|
|
const reservedKeys = {
|
|
|
|
|
id: true,
|
|
|
|
|
pool: true,
|
|
|
|
|
ref: true,
|
2017-11-17 17:42:48 +01:00
|
|
|
type: true,
|
2016-08-05 10:42:33 +02:00
|
|
|
}
|
|
|
|
|
const getKey = (key, obj) => reservedKeys[key] && obj === object
|
|
|
|
|
? `$$${key}`
|
|
|
|
|
: `$${key}`
|
|
|
|
|
|
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.
|
2016-08-05 10:42:33 +02:00
|
|
|
defineProperty(object, getKey(key, object), {
|
2017-11-17 17:42:48 +01:00
|
|
|
value: EMPTY_ARRAY,
|
2015-06-23 09:13:43 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 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.
|
2016-08-05 10:42:33 +02:00
|
|
|
defineProperty(object, getKey(key, object), {
|
2017-11-17 17:42:48 +01: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)) {
|
2016-08-05 10:42:33 +02:00
|
|
|
defineProperty(object, getKey(key, object), {
|
2017-11-17 17:42:48 +01: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 },
|
2016-05-10 14:55:19 +02:00
|
|
|
$pool: { get: this._getPool },
|
2015-05-14 14:58:54 +02:00
|
|
|
$ref: { value: ref },
|
2017-11-17 17:42:48 +01:00
|
|
|
$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
|
2018-01-24 14:06:46 +01:00
|
|
|
|
|
|
|
|
const eventWatchers = this._eventWatchers
|
|
|
|
|
if (eventWatchers !== undefined) {
|
|
|
|
|
forEach(object.other_config, (_, key) => {
|
|
|
|
|
const eventWatcher = eventWatchers[key]
|
|
|
|
|
if (eventWatcher !== undefined) {
|
|
|
|
|
delete eventWatchers[key]
|
|
|
|
|
eventWatcher(object)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2017-06-08 12:19:08 +02:00
|
|
|
} else if (type === 'task') {
|
2018-01-24 14:06:46 +01:00
|
|
|
if (prev === undefined) {
|
|
|
|
|
++this._nTasks
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-08 12:19:08 +02:00
|
|
|
const taskWatchers = this._taskWatchers
|
2017-11-17 17:42:48 +01:00
|
|
|
const taskWatcher = taskWatchers[ref]
|
2017-06-08 15:21:52 +02:00
|
|
|
if (
|
|
|
|
|
taskWatcher !== undefined &&
|
|
|
|
|
getTaskResult(object, taskWatcher.resolve, taskWatcher.reject)
|
|
|
|
|
) {
|
|
|
|
|
delete taskWatchers[ref]
|
2017-06-08 12:19:08 +02:00
|
|
|
}
|
2015-06-22 16:19:32 +02:00
|
|
|
}
|
2015-04-10 15:33:39 +02:00
|
|
|
}
|
|
|
|
|
|
2018-01-24 14:06:46 +01:00
|
|
|
_removeObject (type, ref) {
|
2017-06-22 19:03:52 +02:00
|
|
|
const byRefs = this._objectsByRefs
|
|
|
|
|
const object = byRefs[ref]
|
|
|
|
|
if (object !== undefined) {
|
2015-10-02 14:05:26 +02:00
|
|
|
this._objects.unset(object.$id)
|
2017-06-22 19:03:52 +02:00
|
|
|
delete byRefs[ref]
|
2018-01-24 14:06:46 +01:00
|
|
|
|
|
|
|
|
if (type === 'task') {
|
|
|
|
|
--this._nTasks
|
|
|
|
|
}
|
2017-06-22 19:03:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const taskWatchers = this._taskWatchers
|
|
|
|
|
const taskWatcher = taskWatchers[ref]
|
|
|
|
|
if (taskWatcher !== undefined) {
|
|
|
|
|
taskWatcher.reject(new Error('task has been detroyed before completion'))
|
|
|
|
|
delete taskWatchers[ref]
|
2015-06-22 16:19:32 +02:00
|
|
|
}
|
|
|
|
|
}
|
2015-04-10 15:33:39 +02:00
|
|
|
|
2015-06-22 16:19:32 +02:00
|
|
|
_processEvents (events) {
|
|
|
|
|
forEach(events, event => {
|
2018-01-24 14:06:46 +01:00
|
|
|
const { class: type, ref } = event
|
2017-08-08 12:06:32 +02:00
|
|
|
if (event.operation === 'del') {
|
2018-01-24 14:06:46 +01:00
|
|
|
this._removeObject(type, ref)
|
2017-08-08 12:06:32 +02:00
|
|
|
} else {
|
2018-01-24 14:06:46 +01:00
|
|
|
this._addObject(type, ref, event.snapshot)
|
2017-07-31 16:24:10 +02:00
|
|
|
}
|
2015-06-22 16:19:32 +02:00
|
|
|
})
|
|
|
|
|
}
|
2015-04-10 15:33:39 +02:00
|
|
|
|
2015-06-22 16:19:32 +02:00
|
|
|
_watchEvents () {
|
2016-12-07 23:18:05 +01:00
|
|
|
const loop = () => this.status === CONNECTED && this._sessionCall('event.from', [
|
2016-11-23 09:42:11 +01:00
|
|
|
['*'],
|
|
|
|
|
this._fromToken,
|
2017-11-17 17:42:48 +01:00
|
|
|
60 + 0.1, // Force float.
|
2016-11-23 09:42:11 +01:00
|
|
|
]).then(onSuccess, onFailure)
|
2016-05-11 11:53:39 +02:00
|
|
|
|
2018-01-24 14:06:46 +01:00
|
|
|
const onSuccess = ({ events, token, valid_ref_counts: { task } }) => {
|
2016-05-11 11:53:39 +02:00
|
|
|
this._fromToken = token
|
|
|
|
|
this._processEvents(events)
|
|
|
|
|
|
2018-01-24 14:06:46 +01:00
|
|
|
if (task !== this._nTasks) {
|
|
|
|
|
forEach(this.objects.all, object => {
|
|
|
|
|
if (object.$type === 'task') {
|
|
|
|
|
this._removeObject('task', object.$ref)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2018-01-26 16:13:18 +01:00
|
|
|
this._sessionCall('task.get_all_records', []).then(tasks => {
|
2018-01-24 14:06:46 +01:00
|
|
|
forEach(tasks, (task, ref) => {
|
|
|
|
|
this._addObject('task', ref, task)
|
|
|
|
|
})
|
|
|
|
|
}).catch(noop)
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-11 11:53:39 +02:00
|
|
|
const debounce = this._debounce
|
|
|
|
|
return debounce != null
|
2016-05-11 17:41:58 +02:00
|
|
|
? pDelay(debounce).then(loop)
|
2016-05-11 11:53:39 +02:00
|
|
|
: loop()
|
|
|
|
|
}
|
|
|
|
|
const onFailure = error => {
|
|
|
|
|
if (areEventsLost(error)) {
|
|
|
|
|
this._fromToken = ''
|
|
|
|
|
this._objects.clear()
|
2015-06-30 14:24:43 +02:00
|
|
|
|
2016-05-11 11:53:39 +02:00
|
|
|
return loop()
|
2015-06-30 14:24:43 +02:00
|
|
|
}
|
2016-05-09 18:11:14 +02:00
|
|
|
|
2016-05-11 11:53:39 +02:00
|
|
|
throw error
|
|
|
|
|
}
|
2015-06-30 14:24:43 +02:00
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
return loop()::pCatch(
|
|
|
|
|
isMethodUnknown,
|
2015-12-02 17:32:17 +01:00
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
// If the server failed, it is probably due to an excessively
|
|
|
|
|
// large response.
|
|
|
|
|
// Falling back to legacy events watch should be enough.
|
|
|
|
|
error => error && error.res && error.res.statusCode === 500,
|
2015-06-30 14:24:43 +02:00
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
() => this._watchEventsLegacy()
|
|
|
|
|
)
|
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-11 11:53:39 +02:00
|
|
|
const watchEvents = () => this._sessionCall('event.register', [ ['*'] ]).then(loop)
|
2016-05-09 18:14:23 +02:00
|
|
|
|
2016-12-07 23:18:05 +01:00
|
|
|
const loop = () => this.status === CONNECTED && this._sessionCall('event.next', []).then(onSuccess, onFailure)
|
2016-05-09 18:11:14 +02:00
|
|
|
|
2016-05-11 11:53:39 +02:00
|
|
|
const onSuccess = events => {
|
|
|
|
|
this._processEvents(events)
|
2015-06-22 16:19:32 +02:00
|
|
|
|
2016-05-11 11:53:39 +02:00
|
|
|
const debounce = this._debounce
|
|
|
|
|
return debounce == null
|
|
|
|
|
? loop()
|
2016-05-11 17:41:58 +02:00
|
|
|
: pDelay(debounce).then(loop)
|
2016-05-11 11:53:39 +02:00
|
|
|
}
|
2016-05-09 18:11:14 +02:00
|
|
|
|
2016-05-11 11:53:39 +02:00
|
|
|
const onFailure = error => {
|
|
|
|
|
if (areEventsLost(error)) {
|
|
|
|
|
return this._sessionCall('event.unregister', [ ['*'] ]).then(watchEvents)
|
|
|
|
|
}
|
2015-06-22 16:19:32 +02:00
|
|
|
|
2016-05-11 11:53:39 +02:00
|
|
|
throw error
|
|
|
|
|
}
|
2015-06-22 16:19:32 +02:00
|
|
|
|
|
|
|
|
return getAllObjects().then(watchEvents)
|
|
|
|
|
}
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
2017-05-23 15:45:16 +02:00
|
|
|
Xapi.prototype._transportCall = reduce([
|
|
|
|
|
function (method, args) {
|
|
|
|
|
return this._call(method, args).catch(error => {
|
2017-12-15 13:58:31 +01:00
|
|
|
if (!(error instanceof Error)) {
|
2017-05-23 15:45:16 +02:00
|
|
|
error = wrapError(error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
error.method = method
|
|
|
|
|
throw error
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
call => function () {
|
2017-10-28 13:14:15 +02:00
|
|
|
let iterator // lazily created
|
2017-05-23 15:45:16 +02:00
|
|
|
const loop = () => call.apply(this, arguments)
|
|
|
|
|
::pCatch(isNetworkError, isXapiNetworkError, error => {
|
2017-10-28 13:14:15 +02:00
|
|
|
if (iterator === undefined) {
|
|
|
|
|
iterator = fibonacci().clamp(undefined, 60).take(10).toMs()
|
|
|
|
|
}
|
2017-05-23 15:45:16 +02:00
|
|
|
|
2017-10-28 13:14:15 +02:00
|
|
|
const cursor = iterator.next()
|
|
|
|
|
if (!cursor.done) {
|
2017-05-23 15:45:16 +02:00
|
|
|
// TODO: ability to cancel the connection
|
|
|
|
|
// TODO: ability to force immediate reconnection
|
|
|
|
|
|
2017-10-28 13:14:15 +02:00
|
|
|
const delay = cursor.value
|
|
|
|
|
debug('%s: network error %s, next try in %s ms', this._humanId, error.code, delay)
|
|
|
|
|
return pDelay(delay).then(loop)
|
2017-05-23 15:45:16 +02:00
|
|
|
}
|
|
|
|
|
|
2017-10-28 13:14:15 +02:00
|
|
|
debug('%s: network error %s, aborting', this._humanId, error.code)
|
2017-05-23 15:45:16 +02:00
|
|
|
|
|
|
|
|
// mark as disconnected
|
|
|
|
|
this.disconnect()::pCatch(noop)
|
|
|
|
|
|
|
|
|
|
throw error
|
|
|
|
|
})
|
|
|
|
|
return loop()
|
|
|
|
|
},
|
|
|
|
|
call => function loop () {
|
|
|
|
|
return call.apply(this, arguments)
|
|
|
|
|
::pCatch(isHostSlave, ({params: [master]}) => {
|
|
|
|
|
debug('%s: host is slave, attempting to connect at %s', this._humanId, master)
|
|
|
|
|
|
|
|
|
|
const newUrl = {
|
|
|
|
|
...this._url,
|
2017-11-17 17:42:48 +01:00
|
|
|
hostname: master,
|
2017-05-23 15:45:16 +02:00
|
|
|
}
|
|
|
|
|
this.emit('redirect', newUrl)
|
|
|
|
|
this._url = newUrl
|
|
|
|
|
|
|
|
|
|
return loop.apply(this, arguments)
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
call => function (method) {
|
|
|
|
|
const startTime = Date.now()
|
|
|
|
|
return call.apply(this, arguments).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
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-11-17 17:42:48 +01:00
|
|
|
},
|
2017-05-23 15:45:16 +02:00
|
|
|
], (call, decorator) => decorator(call))
|
|
|
|
|
|
2015-03-31 18:44:33 +02:00
|
|
|
// ===================================================================
|
|
|
|
|
|
|
|
|
|
// The default value is a factory function.
|
2017-05-23 15:45:16 +02:00
|
|
|
export const createClient = opts => new Xapi(opts)
|