2015-04-10 15:33:39 +02:00
|
|
|
import Collection from 'xo-collection'
|
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 { 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 {
|
|
|
|
|
forEach,
|
2019-02-26 09:45:21 +01:00
|
|
|
forOwn,
|
2017-11-26 19:16:05 +00:00
|
|
|
isArray,
|
|
|
|
|
map,
|
|
|
|
|
noop,
|
|
|
|
|
omit,
|
|
|
|
|
reduce,
|
|
|
|
|
startsWith,
|
|
|
|
|
} from 'lodash'
|
2016-05-11 17:41:58 +02:00
|
|
|
import {
|
2017-05-22 15:51:41 +02:00
|
|
|
cancelable,
|
2017-06-08 12:19:08 +02:00
|
|
|
defer,
|
2017-06-29 15:07:17 +02:00
|
|
|
fromEvents,
|
2018-11-28 10:42:55 +01:00
|
|
|
ignoreErrors,
|
2018-09-07 14:34:47 +02:00
|
|
|
pCatch,
|
|
|
|
|
pDelay,
|
|
|
|
|
pTimeout,
|
2018-02-19 11:45:10 +01:00
|
|
|
TimeoutError,
|
2016-05-11 17:41:58 +02:00
|
|
|
} from 'promise-toolbox'
|
2017-04-28 16:22:35 +02:00
|
|
|
|
|
|
|
|
import autoTransport from './transports/auto'
|
2019-03-28 11:17:25 +01:00
|
|
|
import debug from './_debug'
|
|
|
|
|
import getTaskResult from './_getTaskResult'
|
|
|
|
|
import isGetAllRecordsMethod from './_isGetAllRecordsMethod'
|
|
|
|
|
import isOpaqueRef from './_isOpaqueRef'
|
|
|
|
|
import isReadOnlyCall from './_isReadOnlyCall'
|
|
|
|
|
import makeCallSetting from './_makeCallSetting'
|
|
|
|
|
import parseUrl from './_parseUrl'
|
2018-11-13 12:03:05 +01:00
|
|
|
import replaceSensitiveValues from './_replaceSensitiveValues'
|
2019-03-28 11:17:25 +01:00
|
|
|
import XapiError from './_XapiError'
|
2015-03-31 18:44:33 +02:00
|
|
|
|
|
|
|
|
// ===================================================================
|
|
|
|
|
|
2019-03-01 13:40:03 +01:00
|
|
|
// in seconds!
|
2018-02-19 11:45:10 +01:00
|
|
|
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
|
|
|
|
2018-02-19 11:45:10 +01: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-05-06 18:14:43 +02:00
|
|
|
const {
|
|
|
|
|
create: createObject,
|
2015-05-14 14:58:54 +02:00
|
|
|
defineProperties,
|
2017-11-17 17:42:48 +01:00
|
|
|
freeze: freezeObject,
|
2018-10-08 10:58:03 +02:00
|
|
|
keys: getKeys,
|
2015-05-06 18:14:43 +02:00
|
|
|
} = Object
|
|
|
|
|
|
2015-04-16 16:18:11 +02:00
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2018-08-21 17:46:59 +02:00
|
|
|
export const NULL_REF = 'OpaqueRef:NULL'
|
|
|
|
|
|
2016-04-19 16:27:55 +01:00
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2015-12-16 14:19:30 +01:00
|
|
|
const getKey = o => o.$id
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2018-10-08 10:58:03 +02:00
|
|
|
const RESERVED_FIELDS = {
|
|
|
|
|
id: true,
|
|
|
|
|
pool: true,
|
|
|
|
|
ref: true,
|
|
|
|
|
type: true,
|
2019-02-15 17:29:00 +01:00
|
|
|
xapi: true,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getPool() {
|
|
|
|
|
return this.$xapi.pool
|
2018-10-08 10:58:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
const CONNECTED = 'connected'
|
|
|
|
|
const CONNECTING = 'connecting'
|
|
|
|
|
const DISCONNECTED = 'disconnected'
|
|
|
|
|
|
2019-01-11 10:19:38 +01:00
|
|
|
// timeout of XenAPI HTTP connections
|
|
|
|
|
const HTTP_TIMEOUT = 24 * 3600 * 1e3
|
|
|
|
|
|
2015-04-16 16:18:11 +02:00
|
|
|
// -------------------------------------------------------------------
|
|
|
|
|
|
2015-03-31 18:44:33 +02:00
|
|
|
export class Xapi extends EventEmitter {
|
2018-11-28 10:42:55 +01:00
|
|
|
constructor(opts) {
|
2015-03-31 18:44:33 +02:00
|
|
|
super()
|
|
|
|
|
|
2017-05-11 16:35:02 +02:00
|
|
|
this._allowUnauthorized = opts.allowUnauthorized
|
2018-11-20 10:12:58 +01:00
|
|
|
this._callTimeout = makeCallSetting(opts.callTimeout, 0)
|
2019-03-28 13:55:56 +01:00
|
|
|
this._httpInactivityTimeout = opts.httpInactivityTimeout ?? 5 * 60 * 1e3 // 5 mins
|
2016-05-10 15:35:43 +02:00
|
|
|
this._pool = null
|
|
|
|
|
this._readOnly = Boolean(opts.readOnly)
|
2018-10-08 10:58:03 +02:00
|
|
|
this._RecordsByType = createObject(null)
|
2016-05-11 17:41:58 +02:00
|
|
|
this._sessionId = null
|
2017-06-12 16:41:21 +02:00
|
|
|
|
2019-02-26 09:45:21 +01:00
|
|
|
this._auth = opts.auth
|
|
|
|
|
const url = (this._url = parseUrl(opts.url))
|
2017-06-12 16:41:21 +02:00
|
|
|
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
|
|
|
|
2019-02-26 09:45:21 +01:00
|
|
|
;(this._objects = new Collection()).getKey = getKey
|
|
|
|
|
this._debounce = opts.debounce == null ? 200 : opts.debounce
|
|
|
|
|
this._watchedTypes = undefined
|
|
|
|
|
this._watching = false
|
|
|
|
|
|
|
|
|
|
this.on(DISCONNECTED, this._clearObjects)
|
|
|
|
|
this._clearObjects()
|
|
|
|
|
|
|
|
|
|
const { watchEvents } = opts
|
|
|
|
|
if (watchEvents !== false) {
|
|
|
|
|
if (Array.isArray(watchEvents)) {
|
|
|
|
|
this._watchedTypes = watchEvents
|
|
|
|
|
}
|
2018-11-28 10:42:55 +01:00
|
|
|
this.watchEvents()
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-05-10 15:35:43 +02:00
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
watchEvents() {
|
|
|
|
|
this._eventWatchers = createObject(null)
|
2018-01-24 14:06:46 +01:00
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
this._taskWatchers = Object.create(null)
|
2017-06-08 12:19:08 +02:00
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
if (this.status === CONNECTED) {
|
2019-02-26 09:45:21 +01:00
|
|
|
this._watchEventsWrapper()
|
2016-05-10 15:35:43 +02:00
|
|
|
}
|
2018-11-28 10:42:55 +01:00
|
|
|
|
2019-02-26 09:45:21 +01:00
|
|
|
this.on('connected', this._watchEventsWrapper)
|
2018-11-28 10:42:55 +01:00
|
|
|
this.on('disconnected', () => {
|
|
|
|
|
this._objects.clear()
|
|
|
|
|
})
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
get _url() {
|
2017-04-28 16:22:35 +02:00
|
|
|
return this.__url
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
set _url(url) {
|
2017-04-28 16:22:35 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
get readOnly() {
|
2015-12-16 13:58:33 +01:00
|
|
|
return this._readOnly
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
set readOnly(ro) {
|
2015-12-16 13:58:33 +01:00
|
|
|
this._readOnly = Boolean(ro)
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 10:42:55 +01: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
|
|
|
}
|
|
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
get status() {
|
2016-05-11 17:41:58 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2018-11-28 10:42:55 +01: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
|
2018-11-28 10:42:55 +01:00
|
|
|
barrier(ref) {
|
2017-07-31 16:24:10 +02:00
|
|
|
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')
|
|
|
|
|
)
|
2017-07-31 16:24:10 +02:00
|
|
|
}
|
|
|
|
|
|
2018-02-09 17:56:03 +01:00
|
|
|
const key = `xo:barrier:${Math.random()
|
|
|
|
|
.toString(36)
|
|
|
|
|
.slice(2)}`
|
2017-10-09 11:58:02 +02:00
|
|
|
const poolRef = this._pool.$ref
|
|
|
|
|
|
|
|
|
|
const { promise, resolve } = defer()
|
|
|
|
|
eventWatchers[key] = resolve
|
2017-08-08 12:06:32 +02:00
|
|
|
|
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
|
|
|
|
|
}
|
2017-10-09 11:58:02 +02:00
|
|
|
|
2018-02-09 17:56:03 +01:00
|
|
|
// support legacy params (type, ref)
|
|
|
|
|
if (arguments.length === 2) {
|
|
|
|
|
ref = arguments[1]
|
|
|
|
|
}
|
2017-10-09 11:58:02 +02:00
|
|
|
|
2018-02-09 17:56:03 +01:00
|
|
|
return this.getObjectByRef(ref)
|
|
|
|
|
})
|
|
|
|
|
)
|
2017-07-31 16:24:10 +02:00
|
|
|
}
|
|
|
|
|
|
2019-02-24 14:40:30 +01:00
|
|
|
async connect() {
|
2018-02-09 17:56:03 +01:00
|
|
|
const { status } = this
|
2015-04-13 16:16:30 +02:00
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
if (status === CONNECTED) {
|
2019-02-24 14:40:30 +01:00
|
|
|
throw new Error('already connected')
|
2015-04-13 16:16:30 +02:00
|
|
|
}
|
|
|
|
|
|
2016-05-11 17:41:58 +02:00
|
|
|
if (status === CONNECTING) {
|
2019-02-24 14:40:30 +01:00
|
|
|
throw 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) {
|
2019-02-24 14:40:30 +01:00
|
|
|
throw new Error('missing credentials')
|
2017-07-05 10:08:34 +02:00
|
|
|
}
|
|
|
|
|
|
2017-04-28 16:22:35 +02:00
|
|
|
this._sessionId = CONNECTING
|
2016-05-11 17:41:58 +02:00
|
|
|
|
2019-02-24 14:40:30 +01:00
|
|
|
try {
|
|
|
|
|
const [methods, sessionId] = await Promise.all([
|
2019-02-28 12:35:51 +01:00
|
|
|
this._transportCall('system.listMethods', []),
|
2019-02-24 14:40:30 +01:00
|
|
|
this._transportCall('session.login_with_password', [
|
|
|
|
|
auth.user,
|
|
|
|
|
auth.password,
|
|
|
|
|
]),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// Uses introspection to list available types.
|
|
|
|
|
const types = (this._types = methods
|
|
|
|
|
.filter(isGetAllRecordsMethod)
|
|
|
|
|
.map(method => method.slice(0, method.indexOf('.'))))
|
|
|
|
|
this._lcToTypes = { __proto__: null }
|
|
|
|
|
types.forEach(type => {
|
|
|
|
|
const lcType = type.toLowerCase()
|
|
|
|
|
if (lcType !== type) {
|
|
|
|
|
this._lcToTypes[lcType] = type
|
|
|
|
|
}
|
|
|
|
|
})
|
2015-04-13 16:16:30 +02:00
|
|
|
|
2019-02-24 14:40:30 +01:00
|
|
|
this._sessionId = sessionId
|
|
|
|
|
this._pool = (await this.getAllRecords('pool'))[0]
|
2015-04-13 16:16:30 +02:00
|
|
|
|
2019-02-24 14:40:30 +01:00
|
|
|
debug('%s: connected', this._humanId)
|
|
|
|
|
this.emit(CONNECTED)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this._sessionId = null
|
2016-05-11 17:41:58 +02:00
|
|
|
|
2019-02-24 14:40:30 +01:00
|
|
|
throw error
|
|
|
|
|
}
|
2015-04-10 16:41:43 +02:00
|
|
|
}
|
|
|
|
|
|
2018-11-28 10:42:55 +01: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
|
|
|
|
2018-02-09 17:56:03 +01:00
|
|
|
this._transportCall('session.logout', [this._sessionId]).catch(noop)
|
2017-11-26 19:48:19 +00:00
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2019-03-28 12:14:03 +01:00
|
|
|
// ===========================================================================
|
|
|
|
|
// RPC calls
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
|
|
|
|
// this should be used for instantaneous calls, otherwise use `callAsync`
|
2018-11-28 10:42:55 +01: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`))
|
2019-03-28 11:17:25 +01:00
|
|
|
: this._sessionCall(method, args)
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
2017-06-08 15:22:13 +02:00
|
|
|
@cancelable
|
2019-03-28 12:14:03 +01:00
|
|
|
async callAsync($cancelToken, method, ...args) {
|
|
|
|
|
if (this._readOnly && !isReadOnlyCall(method, args)) {
|
|
|
|
|
throw new Error(`cannot call ${method}() in read only mode`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const taskRef = await this._sessionCall(`Async.${method}`, args)
|
|
|
|
|
$cancelToken.promise.then(() =>
|
|
|
|
|
// TODO: do not trigger if the task is already over
|
|
|
|
|
ignoreErrors.call(this._sessionCall('task.cancel', [taskRef]))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const promise = this.watchTask(taskRef)
|
|
|
|
|
|
|
|
|
|
const destroyTask = () =>
|
|
|
|
|
ignoreErrors.call(this._sessionCall('task.destroy', [taskRef]))
|
|
|
|
|
promise.then(destroyTask, destroyTask)
|
|
|
|
|
|
|
|
|
|
return promise
|
2017-06-08 15:22:13 +02:00
|
|
|
}
|
|
|
|
|
|
2019-03-28 12:18:51 +01:00
|
|
|
// ===========================================================================
|
|
|
|
|
// Objects handling helpers
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
|
|
|
|
async getAllRecords(type) {
|
|
|
|
|
return map(
|
|
|
|
|
await this._sessionCall(`${type}.get_all_records`),
|
|
|
|
|
(record, ref) => this._wrapRecord(type, ref, record)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getRecord(type, ref) {
|
|
|
|
|
return this._wrapRecord(
|
|
|
|
|
type,
|
|
|
|
|
ref,
|
|
|
|
|
await this._sessionCall(`${type}.get_record`, [ref])
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getRecordByUuid(type, uuid) {
|
|
|
|
|
return this.getRecord(
|
|
|
|
|
type,
|
|
|
|
|
await this._sessionCall(`${type}.get_by_uuid`, [uuid])
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getRecords(type, refs) {
|
|
|
|
|
return Promise.all(refs.map(ref => this.getRecord(type, ref)))
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-28 16:01:10 +01:00
|
|
|
getField(type, ref, field) {
|
|
|
|
|
return this._sessionCall(`${type}.get_${field}`, [ref])
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-28 12:18:51 +01:00
|
|
|
setField(type, ref, field, value) {
|
|
|
|
|
return this.call(`${type}.set_${field}`, ref, value).then(noop)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setFieldEntries(type, ref, field, entries) {
|
|
|
|
|
return Promise.all(
|
|
|
|
|
getKeys(entries).map(entry => {
|
|
|
|
|
const value = entries[entry]
|
|
|
|
|
if (value !== undefined) {
|
|
|
|
|
return this.setFieldEntry(type, ref, field, entry, value)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
).then(noop)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async setFieldEntry(type, ref, field, entry, value) {
|
|
|
|
|
if (value === null) {
|
|
|
|
|
return this.call(`${type}.remove_from_${field}`, ref, entry).then(noop)
|
|
|
|
|
}
|
|
|
|
|
while (true) {
|
|
|
|
|
try {
|
|
|
|
|
await this.call(`${type}.add_to_${field}`, ref, entry, value)
|
|
|
|
|
return
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error?.code !== 'MAP_DUPLICATE_KEY') {
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
await this.call(`${type}.remove_from_${field}`, ref, entry)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-28 13:58:23 +01:00
|
|
|
// ===========================================================================
|
|
|
|
|
// HTTP requests
|
|
|
|
|
// ===========================================================================
|
2015-05-06 13:27:59 +02:00
|
|
|
|
2017-05-22 15:51:41 +02:00
|
|
|
@cancelable
|
2019-03-28 13:55:56 +01:00
|
|
|
async getResource($cancelToken, pathname, { host, query, task } = {}) {
|
|
|
|
|
const taskRef = await this._autoTask(task, `Xapi#getResource ${pathname}`)
|
2017-06-22 17:33:32 +02:00
|
|
|
|
2019-03-28 13:55:56 +01:00
|
|
|
query = { ...query, session_id: this.sessionId }
|
2018-02-09 17:56:03 +01:00
|
|
|
|
2019-03-28 13:55:56 +01:00
|
|
|
let pTaskResult
|
|
|
|
|
if (taskRef !== undefined) {
|
|
|
|
|
query.task_id = taskRef
|
|
|
|
|
pTaskResult = this.watchTask(taskRef)
|
2017-06-22 17:33:32 +02:00
|
|
|
|
2019-03-28 13:55:56 +01:00
|
|
|
if (typeof $cancelToken.addHandler === 'function') {
|
|
|
|
|
$cancelToken.addHandler(() => pTaskResult)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await httpRequest(
|
|
|
|
|
$cancelToken,
|
|
|
|
|
this._url,
|
|
|
|
|
host !== undefined && {
|
|
|
|
|
hostname: this.getObject(host).address,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
pathname,
|
|
|
|
|
query,
|
|
|
|
|
rejectUnauthorized: !this._allowUnauthorized,
|
|
|
|
|
|
|
|
|
|
// this is an inactivity timeout (unclear in Node doc)
|
|
|
|
|
timeout: this._httpInactivityTimeout,
|
2017-05-22 15:51:41 +02:00
|
|
|
}
|
2018-02-09 17:56:03 +01:00
|
|
|
)
|
2019-03-28 13:55:56 +01:00
|
|
|
|
|
|
|
|
if (pTaskResult !== undefined) {
|
|
|
|
|
response.task = pTaskResult
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response
|
2017-05-22 15:51:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@cancelable
|
2019-03-28 13:55:56 +01:00
|
|
|
async putResource($cancelToken, body, pathname, { host, query, task } = {}) {
|
2017-11-28 11:08:23 +00:00
|
|
|
if (this._readOnly) {
|
2019-03-28 13:55:56 +01:00
|
|
|
throw new Error('cannot put resource in read only mode')
|
2017-11-28 11:08:23 +00:00
|
|
|
}
|
|
|
|
|
|
2019-03-28 13:55:56 +01:00
|
|
|
const taskRef = await this._autoTask(task, `Xapi#putResource ${pathname}`)
|
2017-05-22 15:51:41 +02:00
|
|
|
|
2019-03-28 13:55:56 +01:00
|
|
|
query = { ...query, session_id: this.sessionId }
|
2017-06-23 15:54:15 +02:00
|
|
|
|
2019-03-28 13:55:56 +01:00
|
|
|
let pTaskResult
|
|
|
|
|
if (taskRef !== undefined) {
|
|
|
|
|
query.task_id = taskRef
|
|
|
|
|
pTaskResult = this.watchTask(taskRef)
|
2017-06-22 19:45:18 +02:00
|
|
|
|
2019-03-28 13:55:56 +01:00
|
|
|
if (typeof $cancelToken.addHandler === 'function') {
|
|
|
|
|
$cancelToken.addHandler(() => pTaskResult)
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-06-13 17:49:55 +02:00
|
|
|
|
2019-03-28 13:55:56 +01:00
|
|
|
const headers = {}
|
|
|
|
|
|
|
|
|
|
// XAPI does not support chunk encoding so there is no proper way to send
|
|
|
|
|
// data without knowing its length
|
|
|
|
|
//
|
|
|
|
|
// as a work-around, a huge content length (1PiB) is added (so that the
|
|
|
|
|
// server won't prematurely cut the connection), and the connection will be
|
|
|
|
|
// cut once all the data has been sent without waiting for a response
|
|
|
|
|
const isStream = typeof body.pipe === 'function'
|
|
|
|
|
const useHack = isStream && body.length === undefined
|
|
|
|
|
if (useHack) {
|
|
|
|
|
console.warn(
|
|
|
|
|
this._humanId,
|
|
|
|
|
'Xapi#putResource',
|
|
|
|
|
pathname,
|
|
|
|
|
'missing length'
|
|
|
|
|
)
|
2017-06-21 16:51:36 +02:00
|
|
|
|
2019-03-28 13:55:56 +01:00
|
|
|
headers['content-length'] = '1125899906842624'
|
|
|
|
|
}
|
2017-06-21 16:51:36 +02:00
|
|
|
|
2019-03-28 13:55:56 +01:00
|
|
|
const doRequest = httpRequest.put.bind(
|
|
|
|
|
undefined,
|
|
|
|
|
$cancelToken,
|
|
|
|
|
this._url,
|
|
|
|
|
host !== undefined && {
|
|
|
|
|
hostname: this.getObject(host).address,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
body,
|
|
|
|
|
headers,
|
|
|
|
|
pathname,
|
|
|
|
|
query,
|
|
|
|
|
rejectUnauthorized: !this._allowUnauthorized,
|
|
|
|
|
|
|
|
|
|
// this is an inactivity timeout (unclear in Node doc)
|
|
|
|
|
timeout: this._httpInactivityTimeout,
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-06-21 16:51:36 +02:00
|
|
|
|
2019-03-28 13:55:56 +01:00
|
|
|
// if body is a stream, sends a dummy request to probe for a redirection
|
|
|
|
|
// before consuming body
|
|
|
|
|
const response = await (isStream
|
|
|
|
|
? doRequest({
|
|
|
|
|
body: '',
|
|
|
|
|
|
|
|
|
|
// omit task_id because this request will fail on purpose
|
|
|
|
|
query: 'task_id' in query ? omit(query, 'task_id') : query,
|
2017-06-21 16:51:36 +02:00
|
|
|
|
2019-03-28 13:55:56 +01:00
|
|
|
maxRedirects: 0,
|
|
|
|
|
}).then(
|
|
|
|
|
response => {
|
2019-01-16 09:39:17 +01:00
|
|
|
response.cancel()
|
2019-03-28 13:55:56 +01:00
|
|
|
return doRequest()
|
|
|
|
|
},
|
|
|
|
|
error => {
|
|
|
|
|
let response
|
|
|
|
|
if (error != null && (response = error.response) != null) {
|
|
|
|
|
response.cancel()
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
headers: { location },
|
|
|
|
|
statusCode,
|
|
|
|
|
} = response
|
|
|
|
|
if (statusCode === 302 && location !== undefined) {
|
|
|
|
|
// ensure the original query is sent
|
|
|
|
|
return doRequest(location, { query })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw error
|
2018-02-09 17:56:03 +01:00
|
|
|
}
|
2019-03-28 13:55:56 +01:00
|
|
|
)
|
|
|
|
|
: doRequest())
|
2017-06-21 16:51:36 +02:00
|
|
|
|
2019-03-28 13:55:56 +01:00
|
|
|
if (pTaskResult !== undefined) {
|
|
|
|
|
pTaskResult = pTaskResult.catch(error => {
|
|
|
|
|
error.url = response.url
|
|
|
|
|
throw error
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!useHack) {
|
|
|
|
|
// consume the response
|
|
|
|
|
response.resume()
|
|
|
|
|
|
|
|
|
|
return pTaskResult
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { req } = response
|
|
|
|
|
if (!req.finished) {
|
|
|
|
|
await fromEvents(req, ['close', 'finish'])
|
|
|
|
|
}
|
|
|
|
|
response.cancel()
|
|
|
|
|
return pTaskResult
|
2017-05-22 15:51:41 +02:00
|
|
|
}
|
|
|
|
|
|
2019-03-28 13:58:23 +01: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,
|
|
|
|
|
nameDescription,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
promise.then(taskRef => {
|
|
|
|
|
const destroy = () =>
|
|
|
|
|
this._sessionCall('task.destroy', [taskRef]).catch(noop)
|
|
|
|
|
this.watchTask(taskRef).then(destroy, destroy)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return promise
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const object =
|
|
|
|
|
this._objects.all[idOrUuidOrRef] || this._objectsByRef[idOrUuidOrRef]
|
|
|
|
|
|
|
|
|
|
if (object !== undefined) return object
|
|
|
|
|
|
|
|
|
|
if (arguments.length > 1) return defaultValue
|
|
|
|
|
|
|
|
|
|
throw new Error('no object with UUID or opaque ref: ' + idOrUuidOrRef)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the object for a given opaque reference (internal to
|
|
|
|
|
// XAPI).
|
|
|
|
|
getObjectByRef(ref, defaultValue) {
|
|
|
|
|
const object = this._objectsByRef[ref]
|
|
|
|
|
|
|
|
|
|
if (object !== undefined) return object
|
|
|
|
|
|
|
|
|
|
if (arguments.length > 1) return defaultValue
|
|
|
|
|
|
|
|
|
|
throw new Error('no object with opaque ref: ' + ref)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the object for a given UUID (unique identifier that some
|
|
|
|
|
// objects have).
|
|
|
|
|
getObjectByUuid(uuid, defaultValue) {
|
|
|
|
|
// Objects ids are already UUIDs if they have one.
|
|
|
|
|
const object = this._objects.all[uuid]
|
|
|
|
|
|
|
|
|
|
if (object) return object
|
|
|
|
|
|
|
|
|
|
if (arguments.length > 1) return defaultValue
|
|
|
|
|
|
|
|
|
|
throw new Error('no object with UUID: ' + uuid)
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
watchTask(ref) {
|
2017-06-08 12:19:08 +02:00
|
|
|
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
|
2018-10-08 10:58:03 +02:00
|
|
|
const task = this._objectsByRef[ref]
|
2017-06-08 15:21:52 +02:00
|
|
|
if (task !== undefined) {
|
2018-04-12 18:02:51 +02:00
|
|
|
const result = getTaskResult(task)
|
|
|
|
|
if (result !== undefined) {
|
|
|
|
|
return result
|
2017-06-08 15:21:52 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-08 12:19:08 +02:00
|
|
|
watcher = watchers[ref] = defer()
|
|
|
|
|
}
|
|
|
|
|
return watcher.promise
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
get pool() {
|
2015-04-13 13:27:29 +02:00
|
|
|
return this._pool
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
get objects() {
|
2015-04-10 15:33:39 +02:00
|
|
|
return this._objects
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-26 09:45:21 +01:00
|
|
|
_clearObjects() {
|
|
|
|
|
;(this._objectsByRef = createObject(null))[NULL_REF] = undefined
|
|
|
|
|
this._nTasks = 0
|
|
|
|
|
this._objects.clear()
|
|
|
|
|
this.objectsFetched = new Promise(resolve => {
|
|
|
|
|
this._resolveObjectsFetched = resolve
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-23 16:45:31 +02:00
|
|
|
// return a promise which resolves to a task ref or undefined
|
2018-11-28 10:42:55 +01:00
|
|
|
_autoTask(task = this._taskWatchers !== undefined, name) {
|
2017-06-23 16:45:31 +02:00
|
|
|
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.
|
2019-02-26 09:45:21 +01:00
|
|
|
_sessionCall(method, args, timeout = this._callTimeout(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
|
|
|
|
2018-01-26 16:16:00 +01:00
|
|
|
const newArgs = [this.sessionId]
|
|
|
|
|
if (args !== undefined) {
|
|
|
|
|
newArgs.push.apply(newArgs, args)
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-20 10:12:58 +01:00
|
|
|
return pTimeout.call(
|
|
|
|
|
pCatch.call(
|
|
|
|
|
this._transportCall(method, newArgs),
|
|
|
|
|
isSessionInvalid,
|
|
|
|
|
() => {
|
|
|
|
|
// XAPI is sometimes reinitialized and sessions are lost.
|
|
|
|
|
// Try to login again.
|
|
|
|
|
debug('%s: the session has been reinitialized', this._humanId)
|
|
|
|
|
|
|
|
|
|
this._sessionId = null
|
|
|
|
|
return this.connect().then(() => this._sessionCall(method, args))
|
|
|
|
|
}
|
|
|
|
|
),
|
2019-02-26 09:45:21 +01:00
|
|
|
timeout
|
2018-02-09 17:56:03 +01:00
|
|
|
)
|
2016-08-18 09:37:31 +02:00
|
|
|
} catch (error) {
|
|
|
|
|
return Promise.reject(error)
|
|
|
|
|
}
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
_addObject(type, ref, object) {
|
2018-10-08 10:58:03 +02:00
|
|
|
object = this._wrapRecord(type, ref, object)
|
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
|
2018-10-08 10:58:03 +02:00
|
|
|
const objectsByRef = this._objectsByRef
|
2016-04-26 08:46:04 +02:00
|
|
|
|
|
|
|
|
// An object's UUID can change during its life.
|
2018-10-08 10:58:03 +02:00
|
|
|
const prev = objectsByRef[ref]
|
2016-04-26 08:46:04 +02:00
|
|
|
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)
|
2018-10-08 10:58:03 +02:00
|
|
|
objectsByRef[ref] = object
|
2015-06-22 16:19:32 +02:00
|
|
|
|
|
|
|
|
if (type === 'pool') {
|
|
|
|
|
this._pool = object
|
2018-01-24 14:06:46 +01:00
|
|
|
|
|
|
|
|
const eventWatchers = this._eventWatchers
|
2018-10-08 10:58:03 +02:00
|
|
|
getKeys(object.other_config).forEach(key => {
|
2018-06-11 17:55:47 +02:00
|
|
|
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]
|
2018-04-12 18:02:51 +02:00
|
|
|
if (taskWatcher !== undefined) {
|
|
|
|
|
const result = getTaskResult(object)
|
|
|
|
|
if (result !== undefined) {
|
|
|
|
|
taskWatcher.resolve(result)
|
|
|
|
|
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-11-28 10:42:55 +01:00
|
|
|
_removeObject(type, ref) {
|
2018-10-08 10:58:03 +02:00
|
|
|
const byRefs = this._objectsByRef
|
2017-06-22 19:03:52 +02:00
|
|
|
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) {
|
2018-03-30 15:28:53 +02:00
|
|
|
const error = new Error('task has been destroyed before completion')
|
|
|
|
|
error.task = object
|
|
|
|
|
error.taskRef = ref
|
|
|
|
|
taskWatcher.reject(error)
|
2017-06-22 19:03:52 +02:00
|
|
|
delete taskWatchers[ref]
|
2015-06-22 16:19:32 +02:00
|
|
|
}
|
|
|
|
|
}
|
2015-04-10 15:33:39 +02:00
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
_processEvents(events) {
|
2015-06-22 16:19:32 +02:00
|
|
|
forEach(events, event => {
|
2019-02-24 14:40:30 +01:00
|
|
|
let type = event.class
|
|
|
|
|
const lcToTypes = this._lcToTypes
|
|
|
|
|
if (type in lcToTypes) {
|
|
|
|
|
type = lcToTypes[type]
|
|
|
|
|
}
|
|
|
|
|
const { 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
|
|
|
|
2019-02-26 09:45:21 +01:00
|
|
|
// - prevent multiple watches
|
|
|
|
|
// - swallow errors
|
|
|
|
|
async _watchEventsWrapper() {
|
|
|
|
|
if (!this._watching) {
|
|
|
|
|
this._watching = true
|
2019-03-01 13:41:50 +01:00
|
|
|
try {
|
|
|
|
|
await this._watchEvents()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('_watchEventsWrapper', error)
|
|
|
|
|
}
|
2019-02-26 09:45:21 +01:00
|
|
|
this._watching = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: cancelation
|
|
|
|
|
async _watchEvents() {
|
|
|
|
|
this._clearObjects()
|
|
|
|
|
|
|
|
|
|
// compute the initial token for the event loop
|
|
|
|
|
//
|
|
|
|
|
// we need to do this before the initial fetch to avoid losing events
|
|
|
|
|
let fromToken
|
|
|
|
|
try {
|
|
|
|
|
fromToken = await this._sessionCall('event.inject', [
|
|
|
|
|
'pool',
|
|
|
|
|
this._pool.$ref,
|
|
|
|
|
])
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (isMethodUnknown(error)) {
|
|
|
|
|
return this._watchEventsLegacy()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const types = this._watchedTypes || this._types
|
|
|
|
|
|
|
|
|
|
// initial fetch
|
2019-02-26 15:03:33 +01:00
|
|
|
const flush = this.objects.bufferEvents()
|
|
|
|
|
try {
|
|
|
|
|
await Promise.all(
|
|
|
|
|
types.map(async type => {
|
|
|
|
|
try {
|
|
|
|
|
// FIXME: use _transportCall to avoid auto-reconnection
|
|
|
|
|
forOwn(
|
|
|
|
|
await this._sessionCall(`${type}.get_all_records`),
|
|
|
|
|
(record, ref) => {
|
|
|
|
|
// we can bypass _processEvents here because they are all *add*
|
|
|
|
|
// event and all objects are of the same type
|
|
|
|
|
this._addObject(type, ref, record)
|
|
|
|
|
}
|
|
|
|
|
)
|
2019-03-01 13:41:50 +01:00
|
|
|
} catch (error) {
|
2019-02-28 16:32:30 +01:00
|
|
|
// there is nothing ideal to do here, do not interrupt event
|
|
|
|
|
// handling
|
2019-03-01 13:41:50 +01:00
|
|
|
if (error != null && error.code !== 'MESSAGE_REMOVED') {
|
|
|
|
|
console.warn('_watchEvents', 'initial fetch', type, error)
|
|
|
|
|
}
|
2019-02-26 09:45:21 +01:00
|
|
|
}
|
2019-02-26 15:03:33 +01:00
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
} finally {
|
|
|
|
|
flush()
|
|
|
|
|
}
|
2019-02-26 09:45:21 +01:00
|
|
|
this._resolveObjectsFetched()
|
|
|
|
|
|
|
|
|
|
// event loop
|
|
|
|
|
const debounce = this._debounce
|
|
|
|
|
while (true) {
|
|
|
|
|
if (debounce != null) {
|
|
|
|
|
await pDelay(debounce)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let result
|
|
|
|
|
try {
|
|
|
|
|
result = await this._sessionCall(
|
|
|
|
|
'event.from',
|
2019-03-01 16:39:49 +01:00
|
|
|
[
|
|
|
|
|
types,
|
|
|
|
|
fromToken,
|
|
|
|
|
EVENT_TIMEOUT + 0.1, // must be float for XML-RPC transport
|
|
|
|
|
],
|
2019-03-01 13:40:03 +01:00
|
|
|
EVENT_TIMEOUT * 1e3 * 1.1
|
2018-08-20 15:48:48 +02:00
|
|
|
)
|
2019-02-26 09:45:21 +01:00
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof TimeoutError) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if (areEventsLost(error)) {
|
|
|
|
|
return this._watchEvents()
|
|
|
|
|
}
|
|
|
|
|
throw error
|
|
|
|
|
}
|
2016-05-11 11:53:39 +02:00
|
|
|
|
2019-02-26 09:45:21 +01:00
|
|
|
fromToken = result.token
|
|
|
|
|
this._processEvents(result.events)
|
2016-05-11 11:53:39 +02:00
|
|
|
|
2019-02-26 09:45:21 +01:00
|
|
|
// detect and fix disappearing tasks (e.g. when toolstack restarts)
|
|
|
|
|
if (result.valid_ref_counts.task !== this._nTasks) {
|
2019-02-28 16:30:39 +01:00
|
|
|
await ignoreErrors.call(
|
2019-02-26 09:45:21 +01:00
|
|
|
this._sessionCall('task.get_all_records').then(tasks => {
|
2018-02-09 17:56:03 +01:00
|
|
|
const toRemove = new Set()
|
2019-02-26 09:45:21 +01:00
|
|
|
forOwn(this.objects.all, object => {
|
2018-02-09 17:56:03 +01:00
|
|
|
if (object.$type === 'task') {
|
|
|
|
|
toRemove.add(object.$ref)
|
|
|
|
|
}
|
|
|
|
|
})
|
2019-02-26 09:45:21 +01:00
|
|
|
forOwn(tasks, (task, ref) => {
|
2018-02-09 17:56:03 +01:00
|
|
|
toRemove.delete(ref)
|
|
|
|
|
this._addObject('task', ref, task)
|
|
|
|
|
})
|
|
|
|
|
toRemove.forEach(ref => {
|
|
|
|
|
this._removeObject('task', ref)
|
|
|
|
|
})
|
2018-01-26 16:28:22 +01:00
|
|
|
})
|
2019-02-26 09:45:21 +01:00
|
|
|
)
|
2015-06-30 14:24:43 +02:00
|
|
|
}
|
2016-05-11 11:53:39 +02:00
|
|
|
}
|
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.
|
2018-11-28 10:42:55 +01:00
|
|
|
_watchEventsLegacy() {
|
2019-02-24 14:40:30 +01:00
|
|
|
const getAllObjects = async () => {
|
2019-02-26 15:03:33 +01:00
|
|
|
const flush = this.objects.bufferEvents()
|
|
|
|
|
try {
|
|
|
|
|
await Promise.all(
|
|
|
|
|
this._types.map(type =>
|
|
|
|
|
this._sessionCall(`${type}.get_all_records`).then(
|
|
|
|
|
objects => {
|
|
|
|
|
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
|
|
|
}
|
2019-02-26 15:03:33 +01:00
|
|
|
)
|
2015-10-23 16:53:42 +02:00
|
|
|
)
|
2018-02-09 17:56:03 +01:00
|
|
|
)
|
2019-02-26 15:03:33 +01:00
|
|
|
} finally {
|
|
|
|
|
flush()
|
|
|
|
|
}
|
|
|
|
|
this._resolveObjectsFetched()
|
2015-06-22 16:19:32 +02:00
|
|
|
}
|
|
|
|
|
|
2018-02-09 17:56:03 +01:00
|
|
|
const watchEvents = () =>
|
|
|
|
|
this._sessionCall('event.register', [['*']]).then(loop)
|
2016-05-09 18:14:23 +02:00
|
|
|
|
2018-02-09 17:56:03 +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
|
2018-02-09 17:56:03 +01:00
|
|
|
return debounce == null ? loop() : 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)) {
|
2018-02-09 17:56:03 +01:00
|
|
|
return this._sessionCall('event.unregister', [['*']]).then(watchEvents)
|
2016-05-11 11:53:39 +02:00
|
|
|
}
|
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)
|
|
|
|
|
}
|
2018-10-08 10:58:03 +02:00
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
_wrapRecord(type, ref, data) {
|
2018-10-08 10:58:03 +02:00
|
|
|
const RecordsByType = this._RecordsByType
|
|
|
|
|
let Record = RecordsByType[type]
|
|
|
|
|
if (Record === undefined) {
|
|
|
|
|
const fields = getKeys(data)
|
|
|
|
|
const nFields = fields.length
|
|
|
|
|
const xapi = this
|
|
|
|
|
|
2019-02-26 09:45:21 +01:00
|
|
|
const getObjectByRef = ref => this._objectsByRef[ref]
|
2018-10-08 10:58:03 +02:00
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
Record = function(ref, data) {
|
2018-10-08 10:58:03 +02:00
|
|
|
defineProperties(this, {
|
|
|
|
|
$id: { value: data.uuid || ref },
|
|
|
|
|
$ref: { value: ref },
|
2019-02-15 17:29:00 +01:00
|
|
|
$xapi: { value: xapi },
|
2018-10-08 10:58:03 +02:00
|
|
|
})
|
|
|
|
|
for (let i = 0; i < nFields; ++i) {
|
|
|
|
|
const field = fields[i]
|
|
|
|
|
this[field] = data[field]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-15 17:29:00 +01:00
|
|
|
const getters = { $pool: getPool }
|
2018-10-08 10:58:03 +02:00
|
|
|
const props = { $type: type }
|
|
|
|
|
fields.forEach(field => {
|
2018-11-28 10:42:55 +01:00
|
|
|
props[`set_${field}`] = function(value) {
|
2019-02-22 18:30:43 +01:00
|
|
|
return xapi.setField(this.$type, this.$ref, field, value)
|
2018-10-08 10:58:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const $field = (field in RESERVED_FIELDS ? '$$' : '$') + field
|
|
|
|
|
|
|
|
|
|
const value = data[field]
|
|
|
|
|
if (isArray(value)) {
|
|
|
|
|
if (value.length === 0 || isOpaqueRef(value[0])) {
|
2018-11-28 10:42:55 +01:00
|
|
|
getters[$field] = function() {
|
2018-10-08 10:58:03 +02:00
|
|
|
const value = this[field]
|
|
|
|
|
return value.length === 0 ? value : value.map(getObjectByRef)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 10:42:55 +01:00
|
|
|
props[`add_to_${field}`] = function(...values) {
|
2018-10-08 10:58:03 +02:00
|
|
|
return xapi
|
|
|
|
|
.call(`${type}.add_${field}`, this.$ref, values)
|
|
|
|
|
.then(noop)
|
|
|
|
|
}
|
|
|
|
|
} else if (value !== null && typeof value === 'object') {
|
2018-11-28 10:42:55 +01:00
|
|
|
getters[$field] = function() {
|
2018-10-08 10:58:03 +02:00
|
|
|
const value = this[field]
|
|
|
|
|
const result = {}
|
|
|
|
|
getKeys(value).forEach(key => {
|
2019-02-26 09:45:21 +01:00
|
|
|
result[key] = xapi._objectsByRef[value[key]]
|
2018-10-08 10:58:03 +02:00
|
|
|
})
|
|
|
|
|
return result
|
|
|
|
|
}
|
2019-02-22 19:47:52 +01:00
|
|
|
props[`update_${field}`] = function(entries, value) {
|
|
|
|
|
return typeof entries === 'string'
|
|
|
|
|
? xapi.setFieldEntry(this.$type, this.$ref, field, entries, value)
|
|
|
|
|
: xapi.setFieldEntries(this.$type, this.$ref, field, entries)
|
2018-10-08 10:58:03 +02:00
|
|
|
}
|
2019-02-07 16:44:42 +01:00
|
|
|
} else if (value === '' || isOpaqueRef(value)) {
|
|
|
|
|
// 2019-02-07 - JFT: even if `value` should not be an empty string for
|
|
|
|
|
// a ref property, an user had the case on XenServer 7.0 on the CD VBD
|
|
|
|
|
// of a VM created by XenCenter
|
2018-11-28 10:42:55 +01:00
|
|
|
getters[$field] = function() {
|
2019-02-26 09:45:21 +01:00
|
|
|
return xapi._objectsByRef[this[field]]
|
2018-10-08 10:58:03 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
const descriptors = {}
|
|
|
|
|
getKeys(getters).forEach(key => {
|
|
|
|
|
descriptors[key] = {
|
|
|
|
|
configurable: true,
|
|
|
|
|
get: getters[key],
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
getKeys(props).forEach(key => {
|
|
|
|
|
descriptors[key] = {
|
|
|
|
|
configurable: true,
|
|
|
|
|
value: props[key],
|
|
|
|
|
writable: true,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
defineProperties(Record.prototype, descriptors)
|
|
|
|
|
|
|
|
|
|
RecordsByType[type] = Record
|
|
|
|
|
}
|
|
|
|
|
return new Record(ref, data)
|
|
|
|
|
}
|
2015-03-31 18:44:33 +02:00
|
|
|
}
|
|
|
|
|
|
2018-02-09 17:56:03 +01:00
|
|
|
Xapi.prototype._transportCall = reduce(
|
|
|
|
|
[
|
2018-11-28 10:42:55 +01:00
|
|
|
function(method, args) {
|
2019-02-17 19:39:11 +01:00
|
|
|
return pTimeout
|
|
|
|
|
.call(this._call(method, args), HTTP_TIMEOUT)
|
|
|
|
|
.catch(error => {
|
|
|
|
|
if (!(error instanceof Error)) {
|
2019-03-28 11:17:25 +01:00
|
|
|
error = XapiError.wrap(error)
|
2019-02-17 19:39:11 +01:00
|
|
|
}
|
2017-05-23 15:45:16 +02:00
|
|
|
|
2019-02-22 17:47:28 +01:00
|
|
|
// do not log the session ID
|
|
|
|
|
//
|
|
|
|
|
// TODO: should log at the session level to avoid logging sensitive
|
|
|
|
|
// values?
|
|
|
|
|
const params = args[0] === this._sessionId ? args.slice(1) : args
|
|
|
|
|
|
2019-02-17 19:39:11 +01:00
|
|
|
error.call = {
|
|
|
|
|
method,
|
2019-02-22 17:47:28 +01:00
|
|
|
params: replaceSensitiveValues(params, '* obfuscated *'),
|
2019-02-17 19:39:11 +01:00
|
|
|
}
|
|
|
|
|
throw error
|
|
|
|
|
})
|
2018-02-09 17:56:03 +01:00
|
|
|
},
|
|
|
|
|
call =>
|
2018-11-28 10:42:55 +01:00
|
|
|
function() {
|
2018-02-09 17:56:03 +01:00
|
|
|
let iterator // lazily created
|
|
|
|
|
const loop = () =>
|
2018-08-20 15:48:48 +02:00
|
|
|
pCatch.call(
|
|
|
|
|
call.apply(this, arguments),
|
|
|
|
|
isNetworkError,
|
|
|
|
|
isXapiNetworkError,
|
|
|
|
|
error => {
|
2018-02-09 17:56:03 +01:00
|
|
|
if (iterator === undefined) {
|
|
|
|
|
iterator = fibonacci()
|
|
|
|
|
.clamp(undefined, 60)
|
|
|
|
|
.take(10)
|
|
|
|
|
.toMs()
|
|
|
|
|
}
|
2017-05-23 15:45:16 +02:00
|
|
|
|
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)
|
|
|
|
|
}
|
2017-05-23 15:45:16 +02:00
|
|
|
|
2018-02-09 17:56:03 +01:00
|
|
|
debug('%s: network error %s, aborting', this._humanId, error.code)
|
2017-05-23 15:45:16 +02:00
|
|
|
|
2018-02-09 17:56:03 +01:00
|
|
|
// mark as disconnected
|
2018-08-20 15:48:48 +02:00
|
|
|
pCatch.call(this.disconnect(), noop)
|
2017-05-23 15:45:16 +02:00
|
|
|
|
2018-02-09 17:56:03 +01:00
|
|
|
throw error
|
2018-08-20 15:48:48 +02:00
|
|
|
}
|
|
|
|
|
)
|
2018-02-09 17:56:03 +01:00
|
|
|
return loop()
|
|
|
|
|
},
|
|
|
|
|
call =>
|
2018-11-28 10:42:55 +01:00
|
|
|
function loop() {
|
2018-08-20 15:48:48 +02:00
|
|
|
return pCatch.call(
|
|
|
|
|
call.apply(this, arguments),
|
|
|
|
|
isHostSlave,
|
|
|
|
|
({ params: [master] }) => {
|
2018-02-09 17:56:03 +01:00
|
|
|
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
|
2017-05-23 15:45:16 +02:00
|
|
|
|
2018-02-09 17:56:03 +01:00
|
|
|
return loop.apply(this, arguments)
|
2018-08-20 15:48:48 +02:00
|
|
|
}
|
|
|
|
|
)
|
2017-05-23 15:45:16 +02:00
|
|
|
},
|
2018-02-09 17:56:03 +01:00
|
|
|
call =>
|
2018-11-28 10:42:55 +01:00
|
|
|
function(method) {
|
2018-02-09 17:56:03 +01:00
|
|
|
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-05-23 15:45:16 +02:00
|
|
|
)
|
2018-02-09 17:56:03 +01:00
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
(call, decorator) => decorator(call)
|
|
|
|
|
)
|
2017-05-23 15:45:16 +02:00
|
|
|
|
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)
|