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

386 lines
9.2 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'
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
}
}
// -------------------------------------------------------------------
2015-04-10 16:45:40 +02:00
const noop = () => {}
// -------------------------------------------------------------------
2015-04-13 16:16:30 +02:00
const notConnectedPromise = Bluebird.reject(new Error('not connected'))
// Does nothing but avoid a Bluebird message error.
notConnectedPromise.catch(noop)
2015-03-31 18:44:33 +02:00
// ===================================================================
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-04-13 16:16:30 +02:00
this._sessionId = notConnectedPromise
2015-03-31 18:44:33 +02:00
this._init()
this._pool = null
this._objectsByRefs = Object.create(null)
this._objectsByRefs['OpaqueRef:NULL'] = null
2015-04-10 15:33:39 +02:00
this._objects = new Collection()
this._objects.getId = (object) => object.$id
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')
})
}
this._sessionId = notConnectedPromise
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-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-04-20 15:18:48 +02:00
this._sessionId = notConnectedPromise
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-05 13:45:51 +02:00
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
const REF_RE = /^OpaqueRef:/
2015-04-17 16:22:23 +02:00
forEach(object, function resolveIfLink (value, key, object) {
if (typeof value === 'string' && REF_RE.test(value)) {
Object.defineProperty(object, key, {
enumerable: true,
get: () => objectsByRefs[value]
})
} else if (typeof value === 'object') {
forEach(value, resolveIfLink)
}
})
2015-04-10 15:33:39 +02:00
object.$id = object.uuid || ref
object.$ref = ref
object.$type = type
Object.defineProperty(object, '$pool', {
enumerable: true,
2015-04-13 13:27:29 +02:00
get: () => this._pool
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)