Files
xen-orchestra/packages/xen-api/src/index.js

1064 lines
27 KiB
JavaScript
Raw Normal View History

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'
import httpRequest from 'http-request-plus'
import { BaseError } from 'make-error'
import { EventEmitter } from 'events'
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'
import {
Cancel,
cancelable,
catchPlus as pCatch,
defer,
delay as pDelay,
fromEvents,
2017-11-17 17:42:48 +01:00
lastly,
timeout as pTimeout,
TimeoutError,
} from 'promise-toolbox'
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
// ===================================================================
// in seconds
const EVENT_TIMEOUT = 60
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
// network is unreachable
ENETUNREACH: 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
}
2018-02-09 17:56:03 +01: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
}
2018-02-09 17:56:03 +01:00
const isXapiNetworkError = ({ code }) => XAPI_NETWORK_ERRORS[code]
2015-03-31 18:44:33 +02:00
// -------------------------------------------------------------------
2018-02-09 17:56:03 +01:00
const areEventsLost = ({ code }) => code === 'EVENTS_LOST'
2015-03-31 18:44:33 +02:00
2018-02-09 17:56:03 +01:00
const isHostSlave = ({ code }) => code === 'HOST_IS_SLAVE'
2015-04-10 16:00:20 +02:00
2018-02-09 17:56:03 +01:00
const isMethodUnknown = ({ code }) => code === 'MESSAGE_METHOD_UNKNOWN'
2015-06-22 16:19:32 +02:00
2018-02-09 17:56:03 +01: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 {
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
// slots than can be assigned later
this.method = undefined
this.url = undefined
2015-03-31 18:44:33 +02:00
}
}
export const wrapError = error => {
let code, params
2018-02-09 17:56:03 +01:00
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
// ===================================================================
const URL_RE = /^(?:(https?:)\/*)?(?:([^:]+):([^@]+)@)?([^/]+?)(?::([0-9]+))?\/?$/
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)
}
2018-02-09 17:56:03 +01:00
const [, protocol = 'https:', username, password, hostname, port] = matches
return { protocol, username, password, hostname, port }
2015-03-31 18:44:33 +02:00
}
// -------------------------------------------------------------------
const {
create: createObject,
defineProperties,
2015-06-23 09:17:42 +02:00
defineProperty,
2017-11-17 17:42:48 +01:00
freeze: freezeObject,
} = Object
// -------------------------------------------------------------------
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 =>
2018-02-09 17:56:03 +01: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_/
2018-02-09 17:56:03 +01:00
const isReadOnlyCall = (method, args) =>
args.length === 1 && isOpaqueRef(args[0]) && RE_READ_ONLY_METHOD.test(method)
2017-11-26 19:16:05 +00:00
// Prepare values before passing them to the XenAPI:
//
// - cast integers to strings
const prepareParam = param => {
if (isInteger(param)) {
2017-11-26 19:16:05 +00:00
return String(param)
}
if (typeof param !== 'object' || param === null) {
return param
}
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
}
// -------------------------------------------------------------------
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
// -------------------------------------------------------------------
const getTaskResult = (task, onSuccess, onFailure) => {
const { status } = task
if (status === 'cancelled') {
2018-02-09 17:56:03 +01:00
return [onFailure(new Cancel('task canceled'))]
}
if (status === 'failure') {
2018-02-09 17:56:03 +01:00
return [onFailure(wrapError(task.error_info))]
}
if (status === 'success') {
// the result might be:
// - empty string
// - an opaque reference
// - an XML-RPC value
2018-02-09 17:56:03 +01:00
return [onSuccess(task.result)]
}
}
// -------------------------------------------------------------------
const CONNECTED = 'connected'
const CONNECTING = 'connecting'
const DISCONNECTED = 'disconnected'
// -------------------------------------------------------------------
2015-03-31 18:44:33 +02:00
export class Xapi extends EventEmitter {
constructor (opts) {
super()
this._allowUnauthorized = opts.allowUnauthorized
2015-03-31 18:44:33 +02:00
this._auth = opts.auth
this._pool = null
this._readOnly = Boolean(opts.readOnly)
this._sessionId = null
2018-02-09 17:56:03 +01: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,
}
delete url.username
delete url.password
}
}
2015-03-31 18:44:33 +02:00
if (opts.watchEvents !== false) {
2018-02-09 17:56:03 +01:00
this._debounce = opts.debounce == null ? 200 : opts.debounce
this._eventWatchers = createObject(null)
2015-04-10 16:41:17 +02:00
this._fromToken = ''
2015-09-14 16:01:46 +02:00
// Memoize this function _addObject().
this._getPool = () => this._pool
this._nTasks = 0
2018-02-09 17:56:03 +01:00
const objects = (this._objects = new Collection())
objects.getKey = getKey
this._objectsByRefs = createObject(null)
this._objectsByRefs['OpaqueRef:NULL'] = null
this._taskWatchers = Object.create(null)
this.on('connected', this._watchEvents)
this.on('disconnected', () => {
this._fromToken = ''
objects.clear()
})
}
2015-03-31 18:44:33 +02:00
}
get _url () {
return this.__url
}
set _url (url) {
this.__url = url
this._call = autoTransport({
allowUnauthorized: this._allowUnauthorized,
2017-11-17 17:42:48 +01:00
url,
})
}
get readOnly () {
return this._readOnly
}
set readOnly (ro) {
this._readOnly = Boolean(ro)
}
2015-04-13 17:38:07 +02:00
get sessionId () {
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')
}
return id
2015-04-13 17:38:07 +02:00
}
2015-04-13 16:16:30 +02:00
get status () {
const id = this._sessionId
2015-04-13 16:16:30 +02:00
2018-02-09 17:56:03 +01: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 () {
return `${this._auth.user}@${this._url.hostname}`
2015-04-10 16:00:20 +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) {
const eventWatchers = this._eventWatchers
if (eventWatchers === undefined) {
2018-02-09 17:56:03 +01:00
return Promise.reject(
new Error('Xapi#barrier() requires events watching')
)
}
2018-02-09 17:56:03 +01:00
const key = `xo:barrier:${Math.random()
.toString(36)
.slice(2)}`
const poolRef = this._pool.$ref
const { promise, resolve } = defer()
eventWatchers[key] = resolve
2018-02-09 17:56:03 +01:00
return this._sessionCall('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
}
2018-02-09 17:56:03 +01:00
// support legacy params (type, ref)
if (arguments.length === 2) {
ref = arguments[1]
}
2018-02-09 17:56:03 +01:00
return this.getObjectByRef(ref)
})
)
}
2015-04-10 16:41:43 +02:00
connect () {
2018-02-09 17:56:03 +01:00
const { status } = this
2015-04-13 16:16:30 +02:00
if (status === CONNECTED) {
return Promise.reject(new Error('already connected'))
2015-04-13 16:16:30 +02:00
}
if (status === CONNECTING) {
return Promise.reject(new Error('already connecting'))
2015-04-13 16:16:30 +02:00
}
const auth = this._auth
if (auth === undefined) {
return Promise.reject(new Error('missing credentials'))
}
this._sessionId = CONNECTING
return this._transportCall('session.login_with_password', [
auth.user,
2017-11-17 17:42:48 +01:00
auth.password,
]).then(
sessionId => {
this._sessionId = sessionId
2015-04-13 16:16:30 +02:00
debug('%s: connected', this._humanId)
2015-04-13 16:16:30 +02:00
this.emit(CONNECTED)
},
error => {
this._sessionId = null
throw error
}
)
2015-04-10 16:41:43 +02:00
}
disconnect () {
return Promise.resolve().then(() => {
const { status } = this
2015-04-13 16:16:30 +02:00
if (status === DISCONNECTED) {
return Promise.reject(new Error('already disconnected'))
}
2015-04-13 16:16:30 +02:00
2018-02-09 17:56:03 +01:00
this._transportCall('session.logout', [this._sessionId]).catch(noop)
this._sessionId = null
2015-04-13 16:16:30 +02:00
debug('%s: disconnected', this._humanId)
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.
call (method, ...args) {
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
}
@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`))
: this._sessionCall(`Async.${method}`, args).then(taskRef => {
2017-11-26 19:47:59 +00:00
$cancelToken.promise.then(() => {
// TODO: do not trigger if the task is already over
this._sessionCall('task.cancel', [taskRef]).catch(noop)
2017-11-26 19:47:59 +00:00
})
return this.watchTask(taskRef)::lastly(() => {
this._sessionCall('task.destroy', [taskRef]).catch(noop)
2017-11-26 19:47:59 +00:00
})
})
}
// create a task and automatically destroy it when settled
//
// allowed even in read-only mode because it does not have impact on the
// XenServer and it's necessary for getResource()
createTask (nameLabel, nameDescription = '') {
const promise = this._sessionCall('task.create', [
nameLabel,
2017-11-17 17:42:48 +01:00
nameDescription,
])
promise.then(taskRef => {
const destroy = () =>
this._sessionCall('task.destroy', [taskRef]).catch(noop)
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) {
if (typeof idOrUuidOrRef === 'object') {
idOrUuidOrRef = idOrUuidOrRef.$id
}
2018-02-09 17:56:03 +01:00
const object =
this._objects.all[idOrUuidOrRef] || this._objectsByRefs[idOrUuidOrRef]
2015-05-06 14:06:17 +02:00
2018-03-28 11:43:15 +02:00
if (object !== undefined) return object
2015-05-06 14:06:17 +02:00
if (arguments.length > 1) return defaultValue
throw new Error('no object with UUID or opaque ref: ' + idOrUuidOrRef)
2015-05-06 14:06:17 +02:00
}
2015-05-06 14:06:25 +02:00
// Returns the object for a given opaque reference (internal to
// XAPI).
getObjectByRef (ref, defaultValue) {
2015-05-06 14:06:25 +02:00
const object = this._objectsByRefs[ref]
2018-03-28 11:43:15 +02:00
if (object !== undefined) return object
2015-05-06 14:06:25 +02:00
if (arguments.length > 1) return defaultValue
throw new Error('no object with opaque ref: ' + ref)
}
2015-05-06 14:06:25 +02:00
// Returns the object for a given UUID (unique identifier that some
// objects have).
getObjectByUuid (uuid, defaultValue) {
2015-05-06 14:06:25 +02:00
// Objects ids are already UUIDs if they have one.
const object = this._objects.all[uuid]
2015-05-06 14:06:25 +02:00
if (object) return object
2015-05-06 14:06:25 +02:00
if (arguments.length > 1) return defaultValue
throw new Error('no object with UUID: ' + uuid)
}
2017-11-26 19:19:03 +00:00
getRecord (type, ref) {
return this._sessionCall(`${type}.get_record`, [ref])
2017-11-26 19:19:03 +00:00
}
@cancelable
2018-02-09 17:56:03 +01:00
getResource ($cancelToken, pathname, { host, query, task }) {
return this._autoTask(task, `Xapi#getResource ${pathname}`).then(
taskRef => {
query = { ...query, session_id: this.sessionId }
let taskResult
if (taskRef !== undefined) {
query.task_id = taskRef
taskResult = this.watchTask(taskRef)
if (typeof $cancelToken.addHandler === 'function') {
$cancelToken.addHandler(() => taskResult)
}
}
2018-02-09 17:56:03 +01:00
let promise = httpRequest(
$cancelToken,
this._url,
host && {
hostname: this.getObject(host).address,
},
{
pathname,
query,
rejectUnauthorized: !this._allowUnauthorized,
}
)
if (taskResult !== undefined) {
promise = promise.then(response => {
response.task = taskResult
return response
})
}
2018-02-09 17:56:03 +01:00
return promise
}
2018-02-09 17:56:03 +01:00
)
}
@cancelable
2018-02-09 17:56:03 +01:00
putResource ($cancelToken, body, pathname, { host, query, task } = {}) {
if (this._readOnly) {
2018-02-09 17:56:03 +01:00
return Promise.reject(
new Error(new Error('cannot put resource in read only mode'))
)
}
2018-02-09 17:56:03 +01:00
return this._autoTask(task, `Xapi#putResource ${pathname}`).then(
taskRef => {
query = { ...query, session_id: this.sessionId }
2018-02-09 17:56:03 +01:00
let taskResult
if (taskRef !== undefined) {
query.task_id = taskRef
taskResult = this.watchTask(taskRef)
2018-02-09 17:56:03 +01:00
if (typeof $cancelToken.addHandler === 'function') {
$cancelToken.addHandler(() => taskResult)
}
}
2018-02-09 17:56:03 +01:00
const headers = {}
2018-02-09 17:56:03 +01:00
// 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'
}
2018-02-09 17:56:03 +01:00
const doRequest = override =>
httpRequest.put(
$cancelToken,
this._url,
host && {
hostname: this.getObject(host).address,
},
{
body,
headers,
pathname,
query,
rejectUnauthorized: !this._allowUnauthorized,
},
override
)
2018-02-09 17:56:03 +01:00
// if a stream, sends a dummy request to probe for a
// redirection before consuming body
const promise = isStream
? doRequest({
body: '',
2018-02-09 17:56:03 +01:00
// omit task_id because this request will fail on purpose
query: 'task_id' in query ? omit(query, 'task_id') : query,
2018-02-09 17:56:03 +01:00
maxRedirects: 0,
}).then(
response => {
response.req.abort()
2018-02-09 17:56:03 +01:00
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)
}
}
2018-02-09 17:56:03 +01:00
throw error
}
2018-02-09 17:56:03 +01:00
)
: doRequest()
2018-02-09 17:56:03 +01:00
return promise.then(response => {
const { req } = response
2018-02-09 17:56:03 +01:00
if (taskResult !== undefined) {
taskResult = taskResult.catch(error => {
error.url = response.url
throw error
})
}
2018-02-09 17:56:03 +01:00
if (req.finished) {
req.abort()
return taskResult
}
2018-02-09 17:56:03 +01:00
return fromEvents(req, ['close', 'finish']).then(() => {
req.abort()
return taskResult
})
})
2018-02-09 17:56:03 +01: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) {
// sync check if the task is already settled
const task = this.objects.all[ref]
if (task !== undefined) {
const result = getTaskResult(task, Promise.resolve, Promise.reject)
if (result) {
return result[0]
}
}
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
}
// 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) {
try {
if (startsWith(method, 'session.')) {
throw new Error('session.*() methods are disabled from this interface')
}
2015-04-10 15:33:39 +02:00
const newArgs = [this.sessionId]
if (args !== undefined) {
newArgs.push.apply(newArgs, args)
}
2018-02-09 17:56:03 +01:00
return this._transportCall(method, newArgs)::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
this._sessionId = null
return this.connect().then(() => this._sessionCall(method, args))
2018-02-09 17:56:03 +01:00
}
)
} 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) {
2018-02-09 17:56:03 +01:00
const { _objectsByRefs: objectsByRefs } = this
const reservedKeys = {
id: true,
pool: true,
ref: true,
2017-11-17 17:42:48 +01:00
type: true,
}
2018-02-09 17:56:03 +01: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.
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.
defineProperty(object, getKey(key, object), {
2018-02-09 17:56:03 +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)) {
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-06-22 23:33:52 +02:00
// All custom properties are read-only and non enumerable.
defineProperties(object, {
$id: { value: object.uuid || ref },
$pool: { get: this._getPool },
$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)
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
const eventWatchers = this._eventWatchers
if (eventWatchers !== undefined) {
forEach(object.other_config, (_, key) => {
const eventWatcher = eventWatchers[key]
if (eventWatcher !== undefined) {
delete eventWatchers[key]
eventWatcher(object)
}
})
}
} else if (type === 'task') {
if (prev === undefined) {
++this._nTasks
}
const taskWatchers = this._taskWatchers
2017-11-17 17:42:48 +01:00
const taskWatcher = taskWatchers[ref]
if (
taskWatcher !== undefined &&
getTaskResult(object, taskWatcher.resolve, taskWatcher.reject)
) {
delete taskWatchers[ref]
}
2015-06-22 16:19:32 +02:00
}
2015-04-10 15:33:39 +02:00
}
_removeObject (type, ref) {
const byRefs = this._objectsByRefs
const object = byRefs[ref]
if (object !== undefined) {
this._objects.unset(object.$id)
delete byRefs[ref]
if (type === 'task') {
--this._nTasks
}
}
const taskWatchers = this._taskWatchers
const taskWatcher = taskWatchers[ref]
if (taskWatcher !== undefined) {
taskWatcher.reject(new Error('task has been destroyed 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 => {
const { class: type, ref } = event
if (event.operation === 'del') {
this._removeObject(type, ref)
} else {
this._addObject(type, ref, event.snapshot)
}
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 () {
2018-02-09 17:56:03 +01:00
const loop = () =>
this.status === CONNECTED &&
this._sessionCall('event.from', [
['*'],
this._fromToken,
EVENT_TIMEOUT + 0.1, // Force float.
])
::pTimeout(EVENT_TIMEOUT * 1.1e3) // 10% longer than the XenAPI timeout
.then(onSuccess, onFailure)
const onSuccess = ({ events, token, valid_ref_counts: { task } }) => {
this._fromToken = token
this._processEvents(events)
if (task !== this._nTasks) {
2018-02-09 17:56:03 +01:00
this._sessionCall('task.get_all_records')
.then(tasks => {
const toRemove = new Set()
forEach(this.objects.all, object => {
if (object.$type === 'task') {
toRemove.add(object.$ref)
}
})
forEach(tasks, (task, ref) => {
toRemove.delete(ref)
this._addObject('task', ref, task)
})
toRemove.forEach(ref => {
this._removeObject('task', ref)
})
})
2018-02-09 17:56:03 +01:00
.catch(noop)
}
const debounce = this._debounce
2018-02-09 17:56:03 +01:00
return debounce != null ? pDelay(debounce).then(loop) : loop()
}
const onFailure = error => {
if (error instanceof TimeoutError) {
return loop()
}
if (areEventsLost(error)) {
this._fromToken = ''
this._objects.clear()
return loop()
}
throw error
}
return loop()::pCatch(
isMethodUnknown,
// 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,
() => 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 = () => {
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
)
2018-02-09 17:56:03 +01:00
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
}
2015-10-23 16:53:42 +02:00
}
2018-02-09 17:56:03 +01:00
)
2015-10-23 16:53:42 +02:00
)
2018-02-09 17:56:03 +01:00
)
2015-06-22 16:19:32 +02:00
})
}
2018-02-09 17:56:03 +01:00
const watchEvents = () =>
this._sessionCall('event.register', [['*']]).then(loop)
2018-02-09 17:56:03 +01:00
const loop = () =>
this.status === CONNECTED &&
this._sessionCall('event.next').then(onSuccess, onFailure)
const onSuccess = events => {
this._processEvents(events)
2015-06-22 16:19:32 +02:00
const debounce = this._debounce
2018-02-09 17:56:03 +01:00
return debounce == null ? loop() : pDelay(debounce).then(loop)
}
const onFailure = error => {
if (areEventsLost(error)) {
2018-02-09 17:56:03 +01:00
return this._sessionCall('event.unregister', [['*']]).then(watchEvents)
}
2015-06-22 16:19:32 +02:00
throw error
}
2015-06-22 16:19:32 +02:00
return getAllObjects().then(watchEvents)
}
2015-03-31 18:44:33 +02:00
}
2018-02-09 17:56:03 +01:00
Xapi.prototype._transportCall = reduce(
[
function (method, args) {
return this._call(method, args).catch(error => {
if (!(error instanceof Error)) {
error = wrapError(error)
}
2018-02-09 17:56:03 +01:00
error.method = method
throw error
})
},
call =>
function () {
let iterator // lazily created
const loop = () =>
call
.apply(this, arguments)
::pCatch(isNetworkError, isXapiNetworkError, error => {
if (iterator === undefined) {
iterator = fibonacci()
.clamp(undefined, 60)
.take(10)
.toMs()
}
2018-02-09 17:56:03 +01:00
const cursor = iterator.next()
if (!cursor.done) {
// TODO: ability to cancel the connection
// TODO: ability to force immediate reconnection
const delay = cursor.value
debug(
'%s: network error %s, next try in %s ms',
this._humanId,
error.code,
delay
)
return pDelay(delay).then(loop)
}
2018-02-09 17:56:03 +01:00
debug('%s: network error %s, aborting', this._humanId, error.code)
2018-02-09 17:56:03 +01:00
// mark as disconnected
this.disconnect()::pCatch(noop)
2018-02-09 17:56:03 +01:00
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,
hostname: master,
}
this.emit('redirect', newUrl)
this._url = newUrl
2018-02-09 17:56:03 +01:00
return loop.apply(this, arguments)
})
},
2018-02-09 17:56:03 +01:00
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
}
)
2018-02-09 17:56:03 +01:00
},
],
(call, decorator) => decorator(call)
)
2015-03-31 18:44:33 +02:00
// ===================================================================
// The default value is a factory function.
export const createClient = opts => new Xapi(opts)