294 lines
8.0 KiB
JavaScript
294 lines
8.0 KiB
JavaScript
'use strict'
|
|
|
|
const assert = require('assert')
|
|
const pRetry = require('promise-toolbox/retry')
|
|
const { utcFormat, utcParse } = require('d3-time-format')
|
|
const { Xapi: Base } = require('xen-api')
|
|
|
|
const { warn } = require('@xen-orchestra/log').createLogger('xo:xapi')
|
|
|
|
exports.isDefaultTemplate = require('./isDefaultTemplate.js')
|
|
|
|
// VDI formats. (Raw is not available for delta vdi.)
|
|
exports.VDI_FORMAT_RAW = 'raw'
|
|
exports.VDI_FORMAT_VHD = 'vhd'
|
|
|
|
// Format a date (pseudo ISO 8601) from one XenServer get by
|
|
// xapi.call('host.get_servertime', host.$ref) for example
|
|
exports.formatDateTime = utcFormat('%Y%m%dT%H:%M:%SZ')
|
|
|
|
const parseDateTimeHelper = utcParse('%Y%m%dT%H:%M:%SZ')
|
|
|
|
/**
|
|
* Parses a date and time input and returns a Unix timestamp in seconds.
|
|
*
|
|
* @param {string|number|Date} input - The input to parse.
|
|
* @returns {number|null} A Unix timestamp in seconds, or null if the field is empty (as encoded by XAPI).
|
|
* @throws {TypeError} If the input is not a string, number or Date object.
|
|
*/
|
|
exports.parseDateTime = function parseDateTime(input) {
|
|
const type = typeof input
|
|
|
|
// If the value is a number, it is assumed to be a timestamp in seconds
|
|
if (type === 'number') {
|
|
return input || null
|
|
}
|
|
|
|
if (typeof input === 'string') {
|
|
let date
|
|
|
|
// Some dates like host.other_config.{agent_start_time,boot_time,rpm_patch_installation_time}
|
|
// are already timestamps
|
|
date = +input
|
|
if (!Number.isNaN(date)) {
|
|
return date || null
|
|
}
|
|
|
|
// This is the case when the date has been retrieved via the JSON-RPC or JSON in XML-RPC APIs.
|
|
date = parseDateTimeHelper(input)
|
|
if (date === null) {
|
|
throw new RangeError(`unable to parse XAPI datetime ${JSON.stringify(input)}`)
|
|
}
|
|
input = date
|
|
}
|
|
|
|
// This is the case when the date has been retrieved using the XML-RPC API or parsed by the block above.
|
|
if (input instanceof Date) {
|
|
const msTimestamp = input.getTime()
|
|
return msTimestamp === 0 ? null : Math.floor(msTimestamp / 1e3)
|
|
}
|
|
|
|
throw new TypeError('unsupported input ' + input)
|
|
}
|
|
|
|
const hasProps = o => {
|
|
// eslint-disable-next-line no-unreachable-loop
|
|
for (const key in o) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
const getPoolInfo = ({ pool } = {}) =>
|
|
pool && {
|
|
uuid: pool.uuid,
|
|
name_label: pool.name_label,
|
|
}
|
|
|
|
function onRetry(error) {
|
|
try {
|
|
warn('retry', {
|
|
attemptNumber: this.attemptNumber,
|
|
delay: this.delay,
|
|
error,
|
|
fn: this.fn.name,
|
|
arguments: this.arguments,
|
|
pool: getPoolInfo(this.this),
|
|
})
|
|
} catch (error) {}
|
|
}
|
|
|
|
const logWatcherError = error => warn('error in watcher', { error })
|
|
|
|
function callWatcher(watcher) {
|
|
try {
|
|
const result = watcher(this)
|
|
let then
|
|
if (result != null && typeof (then = result.then) === 'function') {
|
|
then.call(result, null, logWatcherError)
|
|
}
|
|
} catch (error) {
|
|
logWatcherError(error)
|
|
}
|
|
}
|
|
|
|
function callWatchers(watchers, object) {
|
|
if (watchers !== undefined) {
|
|
if (Array.isArray(watchers)) {
|
|
watchers.forEach(callWatcher, object)
|
|
} else {
|
|
callWatcher.call(object, watchers)
|
|
}
|
|
}
|
|
}
|
|
|
|
function removeWatcher(predicate, cb) {
|
|
const watcher = this[predicate]
|
|
if (watcher !== undefined) {
|
|
if (watcher === cb) {
|
|
delete this[predicate]
|
|
} else if (Array.isArray(watcher)) {
|
|
const i = watcher.indexOf(cb)
|
|
if (i !== -1) {
|
|
if (watcher.length === 1) {
|
|
delete this[predicate]
|
|
} else {
|
|
watcher.splice(i, 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class Xapi extends Base {
|
|
constructor({
|
|
callRetryWhenTooManyPendingTasks = { delay: 5e3, tries: 10 },
|
|
maxUncoalescedVdis,
|
|
preferNbd = false,
|
|
nbdOptions,
|
|
syncHookSecret,
|
|
syncHookTimeout,
|
|
vdiDestroyRetryWhenInUse = { delay: 5e3, tries: 10 },
|
|
...opts
|
|
}) {
|
|
super(opts)
|
|
this._callRetryWhenTooManyPendingTasks = {
|
|
...callRetryWhenTooManyPendingTasks,
|
|
onRetry,
|
|
when: { code: 'TOO_MANY_PENDING_TASKS' },
|
|
}
|
|
this._maxUncoalescedVdis = maxUncoalescedVdis
|
|
this._preferNbd = preferNbd
|
|
this._nbdOptions = nbdOptions
|
|
this._syncHookSecret = syncHookSecret
|
|
this._syncHookTimeout = syncHookTimeout
|
|
this._vdiDestroyRetryWhenInUse = {
|
|
...vdiDestroyRetryWhenInUse,
|
|
onRetry,
|
|
when: { code: 'VDI_IN_USE' },
|
|
}
|
|
|
|
const genericWatchers = (this._genericWatchers = new Set())
|
|
const objectWatchers = (this._objectWatchers = { __proto__: null })
|
|
|
|
const onAddOrUpdate = records => {
|
|
if (genericWatchers.size === 0 && !hasProps(objectWatchers)) {
|
|
// no need to process records
|
|
return
|
|
}
|
|
|
|
Object.keys(records).forEach(id => {
|
|
const object = records[id]
|
|
|
|
genericWatchers.forEach(callWatcher, object)
|
|
|
|
callWatchers(objectWatchers[id], object)
|
|
callWatchers(objectWatchers[object.$ref], object)
|
|
})
|
|
}
|
|
this.objects.on('add', onAddOrUpdate)
|
|
this.objects.on('update', onAddOrUpdate)
|
|
}
|
|
|
|
// Wait for an object to appear or to be updated.
|
|
//
|
|
// Predicate can be either an id, a UUID, an opaque reference or a
|
|
// function.
|
|
//
|
|
// TODO: implements a timeout.
|
|
waitObject(predicate, cb) {
|
|
// backward compatibility
|
|
if (cb === undefined) {
|
|
return new Promise(resolve => this.waitObject(predicate, resolve))
|
|
}
|
|
|
|
const stopWatch = this.watchObject(predicate, object => {
|
|
stopWatch()
|
|
return cb(object)
|
|
})
|
|
return stopWatch
|
|
}
|
|
|
|
// Watch an object for changes.
|
|
//
|
|
// Predicate can be either an id, a UUID, an opaque reference or a
|
|
// function.
|
|
watchObject(predicate, cb) {
|
|
if (typeof predicate === 'function') {
|
|
const genericWatchers = this._genericWatchers
|
|
|
|
const watcher = obj => {
|
|
if (predicate(obj)) {
|
|
return cb(obj)
|
|
}
|
|
}
|
|
genericWatchers.add(watcher)
|
|
return () => genericWatchers.delete(watcher)
|
|
}
|
|
|
|
const watchers = this._objectWatchers
|
|
const watcher = watchers[predicate]
|
|
if (watcher === undefined) {
|
|
watchers[predicate] = cb
|
|
} else if (Array.isArray(watcher)) {
|
|
watcher.push(cb)
|
|
} else {
|
|
watchers[predicate] = [watcher, cb]
|
|
}
|
|
return removeWatcher.bind(watchers, predicate, cb)
|
|
}
|
|
|
|
// wait for an object to be in a specified state
|
|
|
|
waitObjectState(refOrUuid, predicate, { timeout } = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const object = this.getObject(refOrUuid, undefined)
|
|
if (object !== undefined && predicate(object)) {
|
|
return resolve(object)
|
|
}
|
|
|
|
let timeoutHandle
|
|
const stop = this.watchObject(refOrUuid, object => {
|
|
if (predicate(object)) {
|
|
clearTimeout(timeoutHandle)
|
|
stop()
|
|
resolve(object)
|
|
}
|
|
})
|
|
|
|
if (timeout !== undefined) {
|
|
const error = new Error(`waitObjectState: timeout reached before ${refOrUuid} in expected state`)
|
|
timeoutHandle = setTimeout(() => {
|
|
stop()
|
|
reject(error)
|
|
}, timeout)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
function mixin(mixins) {
|
|
const xapiProto = Xapi.prototype
|
|
const { defineProperties, getOwnPropertyDescriptor, getOwnPropertyNames } = Object
|
|
const descriptors = { __proto__: null }
|
|
Object.keys(mixins).forEach(prefix => {
|
|
const mixinProto = mixins[prefix].prototype
|
|
getOwnPropertyNames(mixinProto)
|
|
.filter(_ => _ !== 'constructor')
|
|
.forEach(name => {
|
|
const key = name[0] === '_' ? name : `${prefix}_${name}`
|
|
|
|
assert(!(key in descriptors), `${key} is already defined`)
|
|
|
|
descriptors[key] = getOwnPropertyDescriptor(mixinProto, name)
|
|
})
|
|
})
|
|
defineProperties(xapiProto, descriptors)
|
|
}
|
|
mixin({
|
|
host: require('./host.js'),
|
|
SR: require('./sr.js'),
|
|
task: require('./task.js'),
|
|
VBD: require('./vbd.js'),
|
|
VDI: require('./vdi.js'),
|
|
VIF: require('./vif.js'),
|
|
VM: require('./vm.js'),
|
|
})
|
|
exports.Xapi = Xapi
|
|
|
|
function getCallRetryOpts() {
|
|
return this._callRetryWhenTooManyPendingTasks
|
|
}
|
|
Xapi.prototype.call = pRetry.wrap(Xapi.prototype.call, getCallRetryOpts)
|
|
Xapi.prototype.callAsync = pRetry.wrap(Xapi.prototype.callAsync, getCallRetryOpts)
|