chore(xen-api): modularize (#4088)

This commit is contained in:
Julien Fontanet 2019-03-28 11:17:25 +01:00 committed by GitHub
parent b7e14ebf2a
commit 1bb0e234e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 143 additions and 152 deletions

View File

@ -55,6 +55,7 @@
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-decorators": "^7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.2.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",

View File

@ -0,0 +1,30 @@
import { BaseError } from 'make-error'
export default class XapiError extends BaseError {
static wrap(error) {
let code, params
if (Array.isArray(error)) {
// < XenServer 7.3
;[code, ...params] = error
} else {
code = error.message
params = error.data
if (!Array.isArray(params)) {
params = []
}
}
return new XapiError(code, params)
}
constructor(code, params) {
super(`${code}(${params.join(', ')})`)
this.code = code
this.params = params
// slots than can be assigned later
this.call = undefined
this.url = undefined
this.task = undefined
}
}

View File

@ -0,0 +1,3 @@
import debug from 'debug'
export default debug('xen-api')

View File

@ -0,0 +1,22 @@
import { Cancel } from 'promise-toolbox'
import XapiError from './_XapiError'
export default task => {
const { status } = task
if (status === 'cancelled') {
return Promise.reject(new Cancel('task canceled'))
}
if (status === 'failure') {
const error = XapiError.wrap(task.error_info)
error.task = task
return Promise.reject(error)
}
if (status === 'success') {
// the result might be:
// - empty string
// - an opaque reference
// - an XML-RPC value
return Promise.resolve(task.result)
}
}

View File

@ -0,0 +1,3 @@
const SUFFIX = '.get_all_records'
export default method => method.endsWith(SUFFIX)

View File

@ -0,0 +1,3 @@
const PREFIX = 'OpaqueRef:'
export default value => typeof value === 'string' && value.startsWith(PREFIX)

View File

@ -0,0 +1,4 @@
const RE = /^[^.]+\.get_/
export default (method, args) =>
args.length === 1 && typeof args[0] === 'string' && RE.test(method)

View File

@ -0,0 +1,8 @@
export default (setting, defaultValue) =>
setting === undefined
? () => defaultValue
: typeof setting === 'function'
? setting
: typeof setting === 'object'
? method => setting[method] ?? setting['*'] ?? defaultValue
: () => setting

View File

@ -0,0 +1,18 @@
const URL_RE = /^(?:(https?:)\/*)?(?:([^:]+):([^@]+)@)?([^/]+?)(?::([0-9]+))?\/?$/
export default url => {
const matches = URL_RE.exec(url)
if (matches === null) {
throw new Error('invalid URL: ' + url)
}
const [, protocol = 'https:', username, password, hostname, port] = matches
const parsedUrl = { protocol, hostname, port }
if (username !== undefined) {
parsedUrl.username = decodeURIComponent(username)
}
if (password !== undefined) {
parsedUrl.password = decodeURIComponent(password)
}
return parsedUrl
}

View File

@ -1,16 +1,13 @@
import Collection from 'xo-collection'
import createDebug from 'debug'
import kindOf from 'kindof'
import ms from 'ms'
import httpRequest from 'http-request-plus'
import { BaseError } from 'make-error'
import { EventEmitter } from 'events'
import { fibonacci } from 'iterable-backoff'
import {
forEach,
forOwn,
isArray,
isInteger,
map,
noop,
omit,
@ -18,7 +15,6 @@ import {
startsWith,
} from 'lodash'
import {
Cancel,
cancelable,
defer,
fromEvents,
@ -31,9 +27,15 @@ import {
} from 'promise-toolbox'
import autoTransport from './transports/auto'
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'
import replaceSensitiveValues from './_replaceSensitiveValues'
const debug = createDebug('xen-api')
import XapiError from './_XapiError'
// ===================================================================
@ -85,59 +87,8 @@ const isMethodUnknown = ({ code }) => code === 'MESSAGE_METHOD_UNKNOWN'
const isSessionInvalid = ({ code }) => code === 'SESSION_INVALID'
// -------------------------------------------------------------------
class XapiError extends BaseError {
constructor(code, params) {
super(`${code}(${params.join(', ')})`)
this.code = code
this.params = params
// slots than can be assigned later
this.call = undefined
this.url = undefined
this.task = undefined
}
}
export const wrapError = error => {
let code, params
if (isArray(error)) {
// < XenServer 7.3
;[code, ...params] = error
} else {
code = error.message
params = error.data
if (!isArray(params)) {
params = []
}
}
return new XapiError(code, params)
}
// ===================================================================
const URL_RE = /^(?:(https?:)\/*)?(?:([^:]+):([^@]+)@)?([^/]+?)(?::([0-9]+))?\/?$/
const parseUrl = url => {
const matches = URL_RE.exec(url)
if (!matches) {
throw new Error('invalid URL: ' + url)
}
const [, protocol = 'https:', username, password, hostname, port] = matches
const parsedUrl = { protocol, hostname, port }
if (username !== undefined) {
parsedUrl.username = decodeURIComponent(username)
}
if (password !== undefined) {
parsedUrl.password = decodeURIComponent(password)
}
return parsedUrl
}
// -------------------------------------------------------------------
const {
create: createObject,
defineProperties,
@ -149,79 +100,12 @@ const {
export const NULL_REF = 'OpaqueRef:NULL'
const OPAQUE_REF_PREFIX = 'OpaqueRef:'
export const isOpaqueRef = value =>
typeof value === 'string' && startsWith(value, OPAQUE_REF_PREFIX)
// -------------------------------------------------------------------
const isGetAllRecordsMethod = RegExp.prototype.test.bind(/\.get_all_records$/)
const RE_READ_ONLY_METHOD = /^[^.]+\.get_/
const isReadOnlyCall = (method, args) =>
args.length === 1 &&
typeof args[0] === 'string' &&
RE_READ_ONLY_METHOD.test(method)
// Prepare values before passing them to the XenAPI:
//
// - cast integers to strings
const prepareParam = param => {
if (isInteger(param)) {
return String(param)
}
if (typeof param !== 'object' || param === null) {
return param
}
if (isArray(param)) {
return map(param, prepareParam)
}
const values = {}
forEach(param, (value, key) => {
if (value !== undefined) {
values[key] = prepareParam(value)
}
})
return values
}
// -------------------------------------------------------------------
const getKey = o => o.$id
// -------------------------------------------------------------------
const getTaskResult = task => {
const { status } = task
if (status === 'cancelled') {
return Promise.reject(new Cancel('task canceled'))
}
if (status === 'failure') {
const error = wrapError(task.error_info)
error.task = task
return Promise.reject(error)
}
if (status === 'success') {
// the result might be:
// - empty string
// - an opaque reference
// - an XML-RPC value
return Promise.resolve(task.result)
}
}
function defined() {
for (let i = 0, n = arguments.length; i < n; ++i) {
const arg = arguments[i]
if (arg !== undefined) {
return arg
}
}
}
// TODO: find a better name
// TODO: merge into promise-toolbox?
const dontWait = promise => {
@ -232,15 +116,6 @@ const dontWait = promise => {
return null
}
const makeCallSetting = (setting, defaultValue) =>
setting === undefined
? () => defaultValue
: typeof setting === 'function'
? setting
: typeof setting !== 'object'
? () => setting
: method => defined(setting[method], setting['*'], defaultValue)
// -------------------------------------------------------------------
const RESERVED_FIELDS = {
@ -480,7 +355,7 @@ export class Xapi extends EventEmitter {
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))
: this._sessionCall(method, args)
}
@cancelable
@ -1221,7 +1096,7 @@ Xapi.prototype._transportCall = reduce(
.call(this._call(method, args), HTTP_TIMEOUT)
.catch(error => {
if (!(error instanceof Error)) {
error = wrapError(error)
error = XapiError.wrap(error)
}
// do not log the session ID

View File

@ -0,0 +1,3 @@
import makeError from 'make-error'
export default makeError('UnsupportedTransport')

View File

@ -0,0 +1,25 @@
// Prepare values before passing them to the XenAPI:
//
// - cast integers to strings
export default function prepare(param) {
if (Number.isInteger(param)) {
return String(param)
}
if (typeof param !== 'object' || param === null) {
return param
}
if (Array.isArray(param)) {
return param.map(prepare)
}
const values = {}
Object.keys(param).forEach(key => {
const value = param[key]
if (value !== undefined) {
values[key] = prepare(value)
}
})
return values
}

View File

@ -1,3 +0,0 @@
import makeError from 'make-error'
export const UnsupportedTransport = makeError('UnsupportedTransport')

View File

@ -1,7 +1,7 @@
import jsonRpc from './json-rpc'
import UnsupportedTransport from './_UnsupportedTransport'
import xmlRpc from './xml-rpc'
import xmlRpcJson from './xml-rpc-json'
import { UnsupportedTransport } from './_utils'
const factories = [jsonRpc, xmlRpcJson, xmlRpc]
const { length } = factories

View File

@ -1,7 +1,7 @@
import httpRequestPlus from 'http-request-plus'
import { format, parse } from 'json-rpc-protocol'
import { UnsupportedTransport } from './_utils'
import UnsupportedTransport from './_UnsupportedTransport'
// https://github.com/xenserver/xenadmin/blob/0df39a9d83cd82713f32d24704852a0fd57b8a64/XenModel/XenAPI/Session.cs#L403-L433
export default ({ allowUnauthorized, url }) => {

View File

@ -1,7 +1,8 @@
import { createClient, createSecureClient } from 'xmlrpc'
import { promisify } from 'promise-toolbox'
import { UnsupportedTransport } from './_utils'
import prepareXmlRpcParams from './_prepareXmlRpcParams'
import UnsupportedTransport from './_UnsupportedTransport'
const logError = error => {
if (error.res) {
@ -71,10 +72,7 @@ const parseResult = result => {
throw new UnsupportedTransport()
}
export default ({
allowUnauthorized,
url: { hostname, path, port, protocol },
}) => {
export default ({ allowUnauthorized, url: { hostname, port, protocol } }) => {
const client = (protocol === 'https:' ? createSecureClient : createClient)({
host: hostname,
path: '/json',
@ -83,5 +81,6 @@ export default ({
})
const call = promisify(client.methodCall, client)
return (method, args) => call(method, args).then(parseResult, logError)
return (method, args) =>
call(method, prepareXmlRpcParams(args)).then(parseResult, logError)
}

View File

@ -1,6 +1,8 @@
import { createClient, createSecureClient } from 'xmlrpc'
import { promisify } from 'promise-toolbox'
import prepareXmlRpcParams from './_prepareXmlRpcParams'
const logError = error => {
if (error.res) {
console.error(
@ -30,10 +32,7 @@ const parseResult = result => {
return result.Value
}
export default ({
allowUnauthorized,
url: { hostname, path, port, protocol },
}) => {
export default ({ allowUnauthorized, url: { hostname, port, protocol } }) => {
const client = (protocol === 'https:' ? createSecureClient : createClient)({
host: hostname,
port,
@ -41,5 +40,6 @@ export default ({
})
const call = promisify(client.methodCall, client)
return (method, args) => call(method, args).then(parseResult, logError)
return (method, args) =>
call(method, prepareXmlRpcParams(args)).then(parseResult, logError)
}

View File

@ -297,7 +297,7 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-json-strings" "^7.2.0"
"@babel/plugin-proposal-nullish-coalescing-operator@^7.0.0":
"@babel/plugin-proposal-nullish-coalescing-operator@^7.0.0", "@babel/plugin-proposal-nullish-coalescing-operator@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.2.0.tgz#c3fda766187b2f2162657354407247a758ee9cf9"
integrity sha512-QXj/YjFuFJd68oDvoc1e8aqLr2wz7Kofzvp6Ekd/o7MWZl+nZ0/cpStxND+hlZ7DpRWAp7OmuyT2areZ2V3YUA==