feat(xo-server/xen-servers): prevent server connection when pool already connected (#3724)

See #2238
This commit is contained in:
badrAZ 2018-11-28 10:42:55 +01:00 committed by Julien Fontanet
parent 4020081492
commit 05fa76dad3
3 changed files with 186 additions and 169 deletions

View File

@ -6,6 +6,7 @@
- [Perf alert] Ability to trigger an alarm if a host/VM/SR usage value is below the threshold [#3612](https://github.com/vatesfr/xen-orchestra/issues/3612) (PR [#3675](https://github.com/vatesfr/xen-orchestra/pull/3675))
- [Home/VMs] Display pool's name [#2226](https://github.com/vatesfr/xen-orchestra/issues/2226) (PR [#3709](https://github.com/vatesfr/xen-orchestra/pull/3709))
- [Servers] Prevent new connection if pool is already connected [#2238](https://github.com/vatesfr/xen-orchestra/issues/2238) (PR [#3724](https://github.com/vatesfr/xen-orchestra/pull/3724))
### Bug fixes

View File

@ -22,6 +22,7 @@ import {
cancelable,
defer,
fromEvents,
ignoreErrors,
pCatch,
pDelay,
pFinally,
@ -87,7 +88,7 @@ const isSessionInvalid = ({ code }) => code === 'SESSION_INVALID'
// -------------------------------------------------------------------
class XapiError extends BaseError {
constructor (code, params) {
constructor(code, params) {
super(`${code}(${params.join(', ')})`)
this.code = code
@ -209,7 +210,7 @@ const getTaskResult = task => {
}
}
function defined () {
function defined() {
for (let i = 0, n = arguments.length; i < n; ++i) {
const arg = arguments[i]
if (arg !== undefined) {
@ -245,16 +246,19 @@ const DISCONNECTED = 'disconnected'
// -------------------------------------------------------------------
export class Xapi extends EventEmitter {
constructor (opts) {
constructor(opts) {
super()
this._allowUnauthorized = opts.allowUnauthorized
this._auth = opts.auth
this._callTimeout = makeCallSetting(opts.callTimeout, 0)
this._debounce = opts.debounce == null ? 200 : opts.debounce
this._pool = null
this._readOnly = Boolean(opts.readOnly)
this._RecordsByType = createObject(null)
this._sessionId = null
;(this._objects = new Collection()).getKey = getKey
;(this._objectsByRef = createObject(null))[NULL_REF] = undefined
const url = (this._url = parseUrl(opts.url))
if (this._auth === undefined) {
@ -269,39 +273,39 @@ export class Xapi extends EventEmitter {
}
}
// Memoize this function _addObject().
this._getPool = () => this._pool
if (opts.watchEvents !== false) {
this._debounce = opts.debounce == null ? 200 : opts.debounce
this._eventWatchers = createObject(null)
this._fromToken = ''
// Memoize this function _addObject().
this._getPool = () => this._pool
this._nTasks = 0
const objects = (this._objects = new Collection())
objects.getKey = getKey
this._objectsByRef = createObject(null)
this._objectsByRef[NULL_REF] = undefined
this._taskWatchers = Object.create(null)
this.on('connected', this._watchEvents)
this.on('disconnected', () => {
this._fromToken = ''
objects.clear()
})
this.watchEvents()
}
}
get _url () {
watchEvents() {
this._eventWatchers = createObject(null)
this._fromToken = ''
this._nTasks = 0
this._taskWatchers = Object.create(null)
if (this.status === CONNECTED) {
ignoreErrors.call(this._watchEvents())
}
this.on('connected', this._watchEvents)
this.on('disconnected', () => {
this._fromToken = ''
this._objects.clear()
})
}
get _url() {
return this.__url
}
set _url (url) {
set _url(url) {
this.__url = url
this._call = autoTransport({
allowUnauthorized: this._allowUnauthorized,
@ -309,15 +313,15 @@ export class Xapi extends EventEmitter {
})
}
get readOnly () {
get readOnly() {
return this._readOnly
}
set readOnly (ro) {
set readOnly(ro) {
this._readOnly = Boolean(ro)
}
get sessionId () {
get sessionId() {
const id = this._sessionId
if (!id || id === CONNECTING) {
@ -327,20 +331,20 @@ export class Xapi extends EventEmitter {
return id
}
get status () {
get status() {
const id = this._sessionId
return id ? (id === CONNECTING ? CONNECTING : CONNECTED) : DISCONNECTED
}
get _humanId () {
get _humanId() {
return `${this._auth.user}@${this._url.hostname}`
}
// ensure we have received all events up to this call
//
// optionally returns the up to date object for the given ref
barrier (ref) {
barrier(ref) {
const eventWatchers = this._eventWatchers
if (eventWatchers === undefined) {
return Promise.reject(
@ -381,7 +385,7 @@ export class Xapi extends EventEmitter {
)
}
connect () {
connect() {
const { status } = this
if (status === CONNECTED) {
@ -418,7 +422,7 @@ export class Xapi extends EventEmitter {
)
}
disconnect () {
disconnect() {
return Promise.resolve().then(() => {
const { status } = this
@ -437,14 +441,14 @@ export class Xapi extends EventEmitter {
}
// High level calls.
call (method, ...args) {
call(method, ...args) {
return this._readOnly && !isReadOnlyCall(method, args)
? Promise.reject(new Error(`cannot call ${method}() in read only mode`))
: this._sessionCall(method, prepareParam(args))
}
@cancelable
callAsync ($cancelToken, method, ...args) {
callAsync($cancelToken, method, ...args) {
return this._readOnly && !isReadOnlyCall(method, args)
? Promise.reject(new Error(`cannot call ${method}() in read only mode`))
: this._sessionCall(`Async.${method}`, args).then(taskRef => {
@ -463,7 +467,7 @@ export class Xapi extends EventEmitter {
//
// allowed even in read-only mode because it does not have impact on the
// XenServer and it's necessary for getResource()
createTask (nameLabel, nameDescription = '') {
createTask(nameLabel, nameDescription = '') {
const promise = this._sessionCall('task.create', [
nameLabel,
nameDescription,
@ -481,7 +485,7 @@ export class Xapi extends EventEmitter {
// 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) {
getObject(idOrUuidOrRef, defaultValue) {
if (typeof idOrUuidOrRef === 'object') {
idOrUuidOrRef = idOrUuidOrRef.$id
}
@ -498,7 +502,7 @@ export class Xapi extends EventEmitter {
// Returns the object for a given opaque reference (internal to
// XAPI).
getObjectByRef (ref, defaultValue) {
getObjectByRef(ref, defaultValue) {
const object = this._objectsByRef[ref]
if (object !== undefined) return object
@ -510,7 +514,7 @@ export class Xapi extends EventEmitter {
// Returns the object for a given UUID (unique identifier that some
// objects have).
getObjectByUuid (uuid, defaultValue) {
getObjectByUuid(uuid, defaultValue) {
// Objects ids are already UUIDs if they have one.
const object = this._objects.all[uuid]
@ -521,13 +525,20 @@ export class Xapi extends EventEmitter {
throw new Error('no object with UUID: ' + uuid)
}
async getRecord (type, ref) {
async getRecord(type, ref) {
return this._wrapRecord(
await this._sessionCall(`${type}.get_record`, [ref])
)
}
async getRecordByUuid (type, uuid) {
async getAllRecords(type) {
return map(
await this._sessionCall(`${type}.get_all_records`),
(record, ref) => this._wrapRecord(type, ref, record)
)
}
async getRecordByUuid(type, uuid) {
return this.getRecord(
type,
await this._sessionCall(`${type}.get_by_uuid`, [uuid])
@ -535,7 +546,7 @@ export class Xapi extends EventEmitter {
}
@cancelable
getResource ($cancelToken, pathname, { host, query, task }) {
getResource($cancelToken, pathname, { host, query, task }) {
return this._autoTask(task, `Xapi#getResource ${pathname}`).then(
taskRef => {
query = { ...query, session_id: this.sessionId }
@ -575,7 +586,7 @@ export class Xapi extends EventEmitter {
}
@cancelable
putResource ($cancelToken, body, pathname, { host, query, task } = {}) {
putResource($cancelToken, body, pathname, { host, query, task } = {}) {
if (this._readOnly) {
return Promise.reject(
new Error(new Error('cannot put resource in read only mode'))
@ -681,11 +692,11 @@ export class Xapi extends EventEmitter {
)
}
setField ({ $type, $ref }, field, value) {
setField({ $type, $ref }, field, value) {
return this.call(`${$type}.set_${field}`, $ref, value).then(noop)
}
setFieldEntries (record, field, entries) {
setFieldEntries(record, field, entries) {
return Promise.all(
getKeys(entries).map(entry => {
const value = entries[entry]
@ -698,7 +709,7 @@ export class Xapi extends EventEmitter {
).then(noop)
}
async setFieldEntry ({ $type, $ref }, field, entry, value) {
async setFieldEntry({ $type, $ref }, field, entry, value) {
while (true) {
try {
await this.call(`${$type}.add_to_${field}`, $ref, entry, value)
@ -712,11 +723,11 @@ export class Xapi extends EventEmitter {
}
}
unsetFieldEntry ({ $type, $ref }, field, entry) {
unsetFieldEntry({ $type, $ref }, field, entry) {
return this.call(`${$type}.remove_from_${field}`, $ref, entry)
}
watchTask (ref) {
watchTask(ref) {
const watchers = this._taskWatchers
if (watchers === undefined) {
throw new Error('Xapi#watchTask() requires events watching')
@ -741,16 +752,16 @@ export class Xapi extends EventEmitter {
return watcher.promise
}
get pool () {
get pool() {
return this._pool
}
get objects () {
get objects() {
return this._objects
}
// return a promise which resolves to a task ref or undefined
_autoTask (task = this._taskWatchers !== undefined, name) {
_autoTask(task = this._taskWatchers !== undefined, name) {
if (task === false) {
return Promise.resolve()
}
@ -764,7 +775,7 @@ export class Xapi extends EventEmitter {
}
// Medium level call: handle session errors.
_sessionCall (method, args) {
_sessionCall(method, args) {
try {
if (startsWith(method, 'session.')) {
throw new Error('session.*() methods are disabled from this interface')
@ -795,7 +806,7 @@ export class Xapi extends EventEmitter {
}
}
_addObject (type, ref, object) {
_addObject(type, ref, object) {
object = this._wrapRecord(type, ref, object)
// Finally freezes the object.
@ -842,7 +853,7 @@ export class Xapi extends EventEmitter {
}
}
_removeObject (type, ref) {
_removeObject(type, ref) {
const byRefs = this._objectsByRef
const object = byRefs[ref]
if (object !== undefined) {
@ -865,7 +876,7 @@ export class Xapi extends EventEmitter {
}
}
_processEvents (events) {
_processEvents(events) {
forEach(events, event => {
const { class: type, ref } = event
if (event.operation === 'del') {
@ -876,7 +887,7 @@ export class Xapi extends EventEmitter {
})
}
_watchEvents () {
_watchEvents() {
const loop = () =>
this.status === CONNECTED &&
pTimeout
@ -949,7 +960,7 @@ export class Xapi extends EventEmitter {
// methods.
//
// It also has to manually get all objects first.
_watchEventsLegacy () {
_watchEventsLegacy() {
const getAllObjects = () => {
return this._sessionCall('system.listMethods').then(methods => {
// Uses introspection to determine the methods to use to get
@ -1001,7 +1012,7 @@ export class Xapi extends EventEmitter {
return getAllObjects().then(watchEvents)
}
_wrapRecord (type, ref, data) {
_wrapRecord(type, ref, data) {
const RecordsByType = this._RecordsByType
let Record = RecordsByType[type]
if (Record === undefined) {
@ -1012,7 +1023,7 @@ export class Xapi extends EventEmitter {
const objectsByRef = this._objectsByRef
const getObjectByRef = ref => objectsByRef[ref]
Record = function (ref, data) {
Record = function(ref, data) {
defineProperties(this, {
$id: { value: data.uuid || ref },
$ref: { value: ref },
@ -1026,7 +1037,7 @@ export class Xapi extends EventEmitter {
const getters = { $pool: this._getPool }
const props = { $type: type }
fields.forEach(field => {
props[`set_${field}`] = function (value) {
props[`set_${field}`] = function(value) {
return xapi.setField(this, field, value)
}
@ -1035,19 +1046,19 @@ export class Xapi extends EventEmitter {
const value = data[field]
if (isArray(value)) {
if (value.length === 0 || isOpaqueRef(value[0])) {
getters[$field] = function () {
getters[$field] = function() {
const value = this[field]
return value.length === 0 ? value : value.map(getObjectByRef)
}
}
props[`add_to_${field}`] = function (...values) {
props[`add_to_${field}`] = function(...values) {
return xapi
.call(`${type}.add_${field}`, this.$ref, values)
.then(noop)
}
} else if (value !== null && typeof value === 'object') {
getters[$field] = function () {
getters[$field] = function() {
const value = this[field]
const result = {}
getKeys(value).forEach(key => {
@ -1055,11 +1066,11 @@ export class Xapi extends EventEmitter {
})
return result
}
props[`update_${field}`] = function (entries) {
props[`update_${field}`] = function(entries) {
return xapi.setFieldEntries(this, field, entries)
}
} else if (isOpaqueRef(value)) {
getters[$field] = function () {
getters[$field] = function() {
return objectsByRef[this[field]]
}
}
@ -1088,7 +1099,7 @@ export class Xapi extends EventEmitter {
Xapi.prototype._transportCall = reduce(
[
function (method, args) {
function(method, args) {
return this._call(method, args).catch(error => {
if (!(error instanceof Error)) {
error = wrapError(error)
@ -1102,7 +1113,7 @@ Xapi.prototype._transportCall = reduce(
})
},
call =>
function () {
function() {
let iterator // lazily created
const loop = () =>
pCatch.call(
@ -1143,7 +1154,7 @@ Xapi.prototype._transportCall = reduce(
return loop()
},
call =>
function loop () {
function loop() {
return pCatch.call(
call.apply(this, arguments),
isHostSlave,
@ -1166,7 +1177,7 @@ Xapi.prototype._transportCall = reduce(
)
},
call =>
function (method) {
function(method) {
const startTime = Date.now()
return call.apply(this, arguments).then(
result => {

View File

@ -239,7 +239,7 @@ export default class {
async connectXenServer(id) {
const server = (await this._getXenServer(id)).properties
const xapi = (this._xapis[server.id] = new Xapi({
const xapi = new Xapi({
allowUnauthorized: Boolean(server.allowUnauthorized),
readOnly: Boolean(server.readOnly),
@ -250,116 +250,121 @@ export default class {
password: server.password,
},
url: server.host,
}))
watchEvents: false,
})
xapi.xo = (() => {
const conId = server.id
// Maps ids of XAPI objects to ids of XO objects.
const xapiIdsToXo = { __proto__: null }
// Map of XAPI objects which failed to be transformed to XO
// objects.
//
// At each `finish` there will be another attempt to transform
// until they succeed.
let toRetry
let toRetryNext = { __proto__: null }
const dependents = { __proto__: null }
const onAddOrUpdate = objects => {
this._onXenAdd(
objects,
xapiIdsToXo,
toRetryNext,
conId,
dependents,
xapi.objects.all
)
}
const onRemove = objects => {
this._onXenRemove(objects, xapiIdsToXo, toRetry, conId, dependents)
}
try {
await xapi.connect()
const xapisByPool = this._xapisByPool
const onFinish = () => {
const { pool } = xapi
if (pool) {
xapisByPool[pool.$id] = xapi
}
if (!isEmpty(toRetry)) {
onAddOrUpdate(toRetry)
toRetry = null
}
if (!isEmpty(toRetryNext)) {
toRetry = toRetryNext
toRetryNext = { __proto__: null }
}
const [{ $id: poolId }] = await xapi.getAllRecords('pool')
if (xapisByPool[poolId] !== undefined) {
throw new Error("the server's pool is already connected")
}
const { objects } = xapi
this._xapis[server.id] = xapisByPool[poolId] = xapi
const addObject = object => {
// TODO: optimize.
onAddOrUpdate({ [object.$id]: object })
return xapiObjectToXo(object, dependents)
}
xapi.xo = (() => {
const conId = server.id
return {
httpRequest: this._xo.httpRequest.bind(this),
// Maps ids of XAPI objects to ids of XO objects.
const xapiIdsToXo = { __proto__: null }
install() {
objects.on('add', onAddOrUpdate)
objects.on('update', onAddOrUpdate)
objects.on('remove', onRemove)
objects.on('finish', onFinish)
// Map of XAPI objects which failed to be transformed to XO
// objects.
//
// At each `finish` there will be another attempt to transform
// until they succeed.
let toRetry
let toRetryNext = { __proto__: null }
onAddOrUpdate(objects.all)
},
uninstall() {
objects.removeListener('add', onAddOrUpdate)
objects.removeListener('update', onAddOrUpdate)
objects.removeListener('remove', onRemove)
objects.removeListener('finish', onFinish)
const dependents = { __proto__: null }
onRemove(objects.all)
},
addObject,
getData: (id, key) => {
const value = (typeof id === 'string' ? xapi.getObject(id) : id)
.other_config[`xo:${camelToSnakeCase(key)}`]
return value && JSON.parse(value)
},
setData: async (id, key, value) => {
await xapi._updateObjectMapProperty(
xapi.getObject(id),
'other_config',
{
[`xo:${camelToSnakeCase(key)}`]:
value !== null ? JSON.stringify(value) : value,
}
const onAddOrUpdate = objects => {
this._onXenAdd(
objects,
xapiIdsToXo,
toRetryNext,
conId,
dependents,
xapi.objects.all
)
}
const onRemove = objects => {
this._onXenRemove(objects, xapiIdsToXo, toRetry, conId, dependents)
}
// Register the updated object.
addObject(await xapi._waitObject(id))
},
}
})()
const onFinish = () => {
if (!isEmpty(toRetry)) {
onAddOrUpdate(toRetry)
toRetry = null
}
xapi.xo.install()
if (!isEmpty(toRetryNext)) {
toRetry = toRetryNext
toRetryNext = { __proto__: null }
}
}
await xapi.connect().then(
() => this.updateXenServer(id, { error: null }),
error => {
this.updateXenServer(id, { error: serializeError(error) })
const { objects } = xapi
throw error
}
)
const addObject = object => {
// TODO: optimize.
onAddOrUpdate({ [object.$id]: object })
return xapiObjectToXo(object, dependents)
}
return {
httpRequest: this._xo.httpRequest.bind(this),
install() {
objects.on('add', onAddOrUpdate)
objects.on('update', onAddOrUpdate)
objects.on('remove', onRemove)
objects.on('finish', onFinish)
onAddOrUpdate(objects.all)
},
uninstall() {
objects.removeListener('add', onAddOrUpdate)
objects.removeListener('update', onAddOrUpdate)
objects.removeListener('remove', onRemove)
objects.removeListener('finish', onFinish)
onRemove(objects.all)
},
addObject,
getData: (id, key) => {
const value = (typeof id === 'string' ? xapi.getObject(id) : id)
.other_config[`xo:${camelToSnakeCase(key)}`]
return value && JSON.parse(value)
},
setData: async (id, key, value) => {
await xapi._updateObjectMapProperty(
xapi.getObject(id),
'other_config',
{
[`xo:${camelToSnakeCase(key)}`]:
value !== null ? JSON.stringify(value) : value,
}
)
// Register the updated object.
addObject(await xapi._waitObject(id))
},
}
})()
xapi.xo.install()
xapi.watchEvents()
this.updateXenServer(id, { error: null })::ignoreErrors()
} catch (error) {
xapi.disconnect()::ignoreErrors()
this.updateXenServer(id, { error: serializeError(error) })::ignoreErrors()
throw error
}
}
async disconnectXenServer(id) {