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

462 lines
11 KiB
JavaScript
Raw Normal View History

2015-03-31 18:44:33 +02:00
import Bluebird, {promisify} from 'bluebird'
2015-04-10 15:33:39 +02:00
import Collection from 'xo-collection'
2015-03-31 18:44:33 +02:00
import createDebug from 'debug'
2015-04-10 15:33:39 +02:00
import forEach from 'lodash.foreach'
2015-05-14 14:39:36 +02:00
import isArray from 'lodash.isarray'
import isObject from 'lodash.isobject'
import map from 'lodash.map'
2015-04-10 15:33:39 +02:00
import startsWith from 'lodash.startswith'
import {BaseError} from 'make-error'
2015-03-31 18:44:33 +02:00
import {
createClient as createXmlRpcClient,
createSecureClient as createSecureXmlRpcClient
} from 'xmlrpc'
import {EventEmitter} from 'events'
2015-04-10 15:33:39 +02:00
const debug = createDebug('xen-api')
2015-03-31 18:44:33 +02:00
// ===================================================================
// http://www.gnu.org/software/libc/manual/html_node/Error-Codes.html
const NETWORK_ERRORS = {
// Connection has been closed outside of our control.
ECONNRESET: true,
// Connection has been aborted locally.
ECONNABORTED: true,
// Host is up but refuses connection (typically: no such service).
ECONNREFUSED: true,
// TODO: ??
EINVAL: true,
// Host is not reachable (does not respond).
2015-04-10 16:40:45 +02:00
EHOSTUNREACH: true,
2015-03-31 18:44:33 +02:00
// Connection configured timed out has been reach.
ETIMEDOUT: true
}
const isNetworkError = (error) => NETWORK_ERRORS[error.code]
// -------------------------------------------------------------------
const XAPI_NETWORK_ERRORS = {
HOST_STILL_BOOTING: true,
HOST_HAS_NO_MANAGEMENT_IP: true
}
const isXapiNetworkError = (error) => XAPI_NETWORK_ERRORS[error.code]
// -------------------------------------------------------------------
const areEventsLost = (error) => error.code === 'EVENTS_LOST'
2015-04-10 16:40:45 +02:00
const isHostSlave = (error) => error.code === 'HOST_IS_SLAVE'
2015-04-10 16:00:20 +02:00
const isSessionInvalid = (error) => error.code === 'SESSION_INVALID'
2015-03-31 18:44:33 +02:00
// -------------------------------------------------------------------
2015-04-10 15:33:39 +02:00
class XapiError extends BaseError {
2015-03-31 18:44:33 +02:00
constructor (error) {
2015-04-10 15:33:39 +02:00
super(error[0])
2015-03-31 18:44:33 +02:00
this.code = error[0]
this.params = error.slice(1)
}
}
2015-05-05 13:45:51 +02:00
export const wrapError = error => new XapiError(error)
2015-03-31 18:44:33 +02:00
// ===================================================================
2015-04-13 14:13:56 +02:00
const URL_RE = /^(http(s)?:\/\/)?([^/]+?)(?::([0-9]+))?(?:\/.*)?$/
2015-03-31 18:44:33 +02:00
function parseUrl (url) {
const matches = URL_RE.exec(url)
if (!matches) {
throw new Error('invalid URL: ' + url)
}
2015-04-13 16:16:30 +02:00
const [, protocol, , host, port] = matches
let [, , isSecure] = matches
if (!protocol) {
isSecure = true
}
2015-03-31 18:44:33 +02:00
return {
2015-04-13 16:16:30 +02:00
isSecure: Boolean(isSecure),
2015-03-31 18:44:33 +02:00
host,
port: port !== undefined ?
+port :
isSecure ? 443 : 80
}
}
// -------------------------------------------------------------------
const {
create: createObject,
defineProperties,
defineProperty
} = Object
2015-04-10 16:45:40 +02:00
const noop = () => {}
// -------------------------------------------------------------------
2015-05-26 16:35:05 +02:00
let getNotConnectedPromise = function () {
const promise = Bluebird.reject(new Error('not connected'))
2015-04-13 16:16:30 +02:00
2015-05-26 16:35:05 +02:00
// Does nothing but avoid a Bluebird message error.
promise.catch(noop)
getNotConnectedPromise = () => promise
return promise
}
2015-04-13 16:16:30 +02:00
2015-03-31 18:44:33 +02:00
// ===================================================================
const OPAQUE_REF_RE = /^OpaqueRef:/
2015-05-14 14:39:36 +02:00
function createAutoLinks (collection, object) {
forEach(object, function resolveObject (value, key, object) {
if (isArray(value)) {
2015-05-14 14:58:27 +02:00
// Do not create an array of links unless it is known this is an
// array of refs.
if (value.length && !OPAQUE_REF_RE.test(value)) {
2015-05-14 14:39:36 +02:00
return
}
defineProperty(object, '$' + key, {
get () {
return map(value, (ref) => collection[ref])
}
})
} else if (isObject(value)) {
forEach(value, resolveObject)
} else if (OPAQUE_REF_RE.test(value)) {
defineProperty(object, '$' + key, {
get () {
return collection[value]
}
})
}
})
}
// ===================================================================
const MAX_TRIES = 5
// -------------------------------------------------------------------
2015-03-31 18:44:33 +02:00
export class Xapi extends EventEmitter {
constructor (opts) {
super()
this._url = parseUrl(opts.url)
this._auth = opts.auth
2015-05-26 16:35:05 +02:00
this._sessionId = getNotConnectedPromise()
2015-03-31 18:44:33 +02:00
this._init()
this._pool = null
this._objectsByRefs = createObject(null)
this._objectsByRefs['OpaqueRef:NULL'] = null
2015-04-10 15:33:39 +02:00
this._objects = new Collection()
2015-05-22 10:38:08 +02:00
this._objects.getKey = (object) => object.$id
2015-04-10 15:33:39 +02:00
2015-03-31 18:44:33 +02:00
this._fromToken = ''
2015-04-10 16:41:17 +02:00
this.on('connected', this._watchEvents)
this.on('disconnected', () => {
this._fromToken = ''
2015-04-13 16:16:30 +02:00
this._objects.clear()
2015-04-10 16:41:17 +02:00
})
2015-03-31 18:44:33 +02:00
}
2015-04-13 17:38:07 +02:00
get sessionId () {
if (this.status !== 'connected') {
throw new Error('sessionId is only available when connected')
}
return this._sessionId.value()
}
2015-04-13 16:16:30 +02:00
get status () {
const {_sessionId: sessionId} = this
if (sessionId.isFulfilled()) {
return 'connected'
}
if (sessionId.isPending()) {
return 'connecting'
}
return 'disconnected'
}
2015-04-10 16:00:20 +02:00
get _humanId () {
return `${this._auth.user}@${this._url.host}`
}
2015-04-10 16:41:43 +02:00
connect () {
2015-04-13 16:16:30 +02:00
const {status} = this
if (status === 'connected') {
return Bluebird.reject(new Error('already connected'))
}
if (status === 'connecting') {
return Bluebird.reject(new Error('already connecting'))
}
this._sessionId = this._transportCall('session.login_with_password', [
this._auth.user,
this._auth.password
])
return this._sessionId.then(() => {
debug('%s: connected', this._humanId)
this.emit('connected')
})
2015-04-10 16:41:43 +02:00
}
disconnect () {
2015-04-13 16:16:30 +02:00
const {status} = this
if (status === 'disconnected') {
return Bluebird.reject('already disconnected')
}
if (status === 'connecting') {
return this._sessionId.cancel().catch(Bluebird.CancellationError, () => {
debug('%s: disconnected', this._humanId)
this.emit('disconnected')
})
}
2015-05-26 16:35:05 +02:00
this._sessionId = getNotConnectedPromise()
2015-04-13 16:16:30 +02:00
return Bluebird.resolve().then(() => {
debug('%s: disconnected', this._humanId)
this.emit('disconnected')
})
2015-04-10 16:41:43 +02:00
}
2015-03-31 18:44:33 +02:00
// High level calls.
call (method, ...args) {
2015-03-31 18:44:33 +02:00
return this._sessionCall(method, args)
}
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) {
const object = (
// if there is an UUID, it is also the $id.
this._objects.all[idOrUuidOrRef] ||
2015-05-06 14:06:17 +02:00
this._objectsByRefs[idOrUuidOrRef]
)
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).
getObjectByRef (ref, defaultValue) {
2015-05-06 14:06:25 +02:00
const object = this._objectsByRefs[ref]
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('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).
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('there is no object with the UUID ' + uuid)
}
2015-04-13 13:27:29 +02:00
get pool () {
return this._pool
}
2015-04-10 15:33:39 +02:00
get objects () {
return this._objects
}
2015-03-31 18:44:33 +02:00
// Medium level call: handle session errors.
_sessionCall (method, args) {
2015-04-10 15:33:39 +02:00
if (startsWith(method, 'session.')) {
return Bluebird.reject(
new Error('session.*() methods are disabled from this interface')
)
}
2015-04-13 16:16:30 +02:00
return this._sessionId.then((sessionId) => {
2015-03-31 18:44:33 +02:00
return this._transportCall(method, [sessionId].concat(args))
}).catch(isSessionInvalid, () => {
// XAPI is sometimes reinitialized and sessions are lost.
// Try to login again.
2015-04-10 16:00:20 +02:00
debug('%s: the session has been reinitialized', this._humanId)
2015-05-26 16:35:05 +02:00
this._sessionId = getNotConnectedPromise()
2015-04-10 16:00:20 +02:00
2015-03-31 18:44:33 +02:00
return this._sessionCall(method, args)
})
}
// Low level call: handle transport errors.
_transportCall (method, args, tries = 1) {
2015-04-10 16:00:20 +02:00
debug('%s: %s(...)', this._humanId, method)
2015-03-31 18:44:33 +02:00
return this._rawCall(method, args)
.catch(isNetworkError, isXapiNetworkError, error => {
debug('%s: network error %s', this._humanId, error.code)
2015-03-31 18:44:33 +02:00
if (!(tries < MAX_TRIES)) {
debug('%s too many network errors (%s), give up', this._humanId, tries)
2015-03-31 18:44:33 +02:00
throw error
2015-03-31 18:44:33 +02:00
}
// TODO: ability to cancel the connection
// TODO: ability to force immediate reconnection
// TODO: implement back-off
return Bluebird.delay(5e3).then(() => {
// TODO: handling not responding host.
return this._transportCall(method, args, tries + 1)
})
2015-03-31 18:44:33 +02:00
})
2015-04-10 16:00:20 +02:00
.catch(isHostSlave, ({params: [master]}) => {
debug('%s: host is slave, attempting to connect at %s', this._humanId, master)
this._url.host = master
this._init()
return this._transportCall(method, args)
})
}
2015-04-10 16:41:59 +02:00
// Lowest level call: do not handle any errors.
_rawCall (method, args) {
return this._xmlRpcCall(method, args)
.then(result => {
const {Status: status} = result
// Return the plain result if it does not have a valid XAPI
// format.
if (!status) {
return result
}
2015-03-31 18:44:33 +02:00
if (status === 'Success') {
return result.Value
}
2015-03-31 18:44:33 +02:00
2015-05-13 17:27:19 +02:00
throw wrapError(result.ErrorDescription)
2015-03-31 18:44:33 +02:00
})
2015-04-10 16:41:43 +02:00
.cancellable()
2015-03-31 18:44:33 +02:00
}
_init () {
const {isSecure, host, port} = this._url
const client = (isSecure ?
createSecureXmlRpcClient :
createXmlRpcClient
)({
host,
port,
rejectUnauthorized: false,
timeout: 10
})
this._xmlRpcCall = promisify(client.methodCall, client)
}
2015-04-10 15:33:39 +02:00
_normalizeObject (type, ref, object) {
const {_objectsByRefs: objectsByRefs} = this
2015-05-14 14:39:36 +02:00
createAutoLinks(objectsByRefs, object)
// All custom properties are non read-only and non enumerable.
defineProperties(object, {
$id: { value: object.uuid || ref },
$pool: { get: () => this._pool },
$ref: { value: ref },
$type: { value: type }
2015-04-10 15:33:39 +02:00
})
}
2015-03-31 18:44:33 +02:00
_watchEvents () {
2015-04-13 16:48:01 +02:00
this.call(
'event.from', ['*'], this._fromToken, 1e3 + 0.1
).then(({token, events}) => {
2015-03-31 18:44:33 +02:00
this._fromToken = token
const {
_objects: objects,
_objectsByRefs: objectsByRefs
} = this
2015-04-10 15:33:39 +02:00
forEach(events, event => {
const {operation: op} = event
const {ref} = event
if (op === 'del') {
2015-04-20 19:00:52 +02:00
const object = objectsByRefs[ref]
2015-04-10 15:33:39 +02:00
2015-04-20 19:00:52 +02:00
if (object) {
objects.remove(object.$id)
delete objectsByRefs[ref]
2015-04-10 15:33:39 +02:00
}
} else {
const {class: type, snapshot: object} = event
this._normalizeObject(type, ref, object)
objects.set(object)
objectsByRefs[ref] = object
2015-04-10 15:33:39 +02:00
if (object.$type === 'pool') {
2015-04-13 13:27:29 +02:00
this._pool = object
2015-04-10 15:33:39 +02:00
}
}
})
2015-03-31 18:44:33 +02:00
}).catch(areEventsLost, () => {
2015-04-10 15:33:39 +02:00
this._objects.clear()
2015-03-31 18:44:33 +02:00
}).then(() => {
2015-04-10 16:45:40 +02:00
this._watchEvents()
2015-04-20 19:01:05 +02:00
}).catch(Bluebird.CancellationError, noop)
2015-03-31 18:44:33 +02:00
}
}
// ===================================================================
// The default value is a factory function.
export const createClient = (opts) => new Xapi(opts)