From 1bb0e234e7abc6f13adc083cf4bbb743b64d4a13 Mon Sep 17 00:00:00 2001 From: Julien Fontanet Date: Thu, 28 Mar 2019 11:17:25 +0100 Subject: [PATCH] chore(xen-api): modularize (#4088) --- packages/xen-api/package.json | 1 + packages/xen-api/src/_XapiError.js | 30 ++++ packages/xen-api/src/_debug.js | 3 + packages/xen-api/src/_getTaskResult.js | 22 +++ .../xen-api/src/_isGetAllRecordsMethod.js | 3 + packages/xen-api/src/_isOpaqueRef.js | 3 + packages/xen-api/src/_isReadOnlyCall.js | 4 + packages/xen-api/src/_makeCallSetting.js | 8 + packages/xen-api/src/_parseUrl.js | 18 +++ packages/xen-api/src/index.js | 145 ++---------------- .../src/transports/_UnsupportedTransport.js | 3 + .../src/transports/_prepareXmlRpcParams.js | 25 +++ packages/xen-api/src/transports/_utils.js | 3 - packages/xen-api/src/transports/auto.js | 2 +- packages/xen-api/src/transports/json-rpc.js | 2 +- .../xen-api/src/transports/xml-rpc-json.js | 11 +- packages/xen-api/src/transports/xml-rpc.js | 10 +- yarn.lock | 2 +- 18 files changed, 143 insertions(+), 152 deletions(-) create mode 100644 packages/xen-api/src/_XapiError.js create mode 100644 packages/xen-api/src/_debug.js create mode 100644 packages/xen-api/src/_getTaskResult.js create mode 100644 packages/xen-api/src/_isGetAllRecordsMethod.js create mode 100644 packages/xen-api/src/_isOpaqueRef.js create mode 100644 packages/xen-api/src/_isReadOnlyCall.js create mode 100644 packages/xen-api/src/_makeCallSetting.js create mode 100644 packages/xen-api/src/_parseUrl.js create mode 100644 packages/xen-api/src/transports/_UnsupportedTransport.js create mode 100644 packages/xen-api/src/transports/_prepareXmlRpcParams.js delete mode 100644 packages/xen-api/src/transports/_utils.js diff --git a/packages/xen-api/package.json b/packages/xen-api/package.json index 8e001a243..a0bfe129a 100644 --- a/packages/xen-api/package.json +++ b/packages/xen-api/package.json @@ -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", diff --git a/packages/xen-api/src/_XapiError.js b/packages/xen-api/src/_XapiError.js new file mode 100644 index 000000000..e69230e26 --- /dev/null +++ b/packages/xen-api/src/_XapiError.js @@ -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 + } +} diff --git a/packages/xen-api/src/_debug.js b/packages/xen-api/src/_debug.js new file mode 100644 index 000000000..8421db419 --- /dev/null +++ b/packages/xen-api/src/_debug.js @@ -0,0 +1,3 @@ +import debug from 'debug' + +export default debug('xen-api') diff --git a/packages/xen-api/src/_getTaskResult.js b/packages/xen-api/src/_getTaskResult.js new file mode 100644 index 000000000..804583d17 --- /dev/null +++ b/packages/xen-api/src/_getTaskResult.js @@ -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) + } +} diff --git a/packages/xen-api/src/_isGetAllRecordsMethod.js b/packages/xen-api/src/_isGetAllRecordsMethod.js new file mode 100644 index 000000000..bec1eedf9 --- /dev/null +++ b/packages/xen-api/src/_isGetAllRecordsMethod.js @@ -0,0 +1,3 @@ +const SUFFIX = '.get_all_records' + +export default method => method.endsWith(SUFFIX) diff --git a/packages/xen-api/src/_isOpaqueRef.js b/packages/xen-api/src/_isOpaqueRef.js new file mode 100644 index 000000000..dbaf39d34 --- /dev/null +++ b/packages/xen-api/src/_isOpaqueRef.js @@ -0,0 +1,3 @@ +const PREFIX = 'OpaqueRef:' + +export default value => typeof value === 'string' && value.startsWith(PREFIX) diff --git a/packages/xen-api/src/_isReadOnlyCall.js b/packages/xen-api/src/_isReadOnlyCall.js new file mode 100644 index 000000000..95b9774d3 --- /dev/null +++ b/packages/xen-api/src/_isReadOnlyCall.js @@ -0,0 +1,4 @@ +const RE = /^[^.]+\.get_/ + +export default (method, args) => + args.length === 1 && typeof args[0] === 'string' && RE.test(method) diff --git a/packages/xen-api/src/_makeCallSetting.js b/packages/xen-api/src/_makeCallSetting.js new file mode 100644 index 000000000..d530fad93 --- /dev/null +++ b/packages/xen-api/src/_makeCallSetting.js @@ -0,0 +1,8 @@ +export default (setting, defaultValue) => + setting === undefined + ? () => defaultValue + : typeof setting === 'function' + ? setting + : typeof setting === 'object' + ? method => setting[method] ?? setting['*'] ?? defaultValue + : () => setting diff --git a/packages/xen-api/src/_parseUrl.js b/packages/xen-api/src/_parseUrl.js new file mode 100644 index 000000000..5c5d3236b --- /dev/null +++ b/packages/xen-api/src/_parseUrl.js @@ -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 +} diff --git a/packages/xen-api/src/index.js b/packages/xen-api/src/index.js index 4f709ee3e..3c2620b82 100644 --- a/packages/xen-api/src/index.js +++ b/packages/xen-api/src/index.js @@ -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 diff --git a/packages/xen-api/src/transports/_UnsupportedTransport.js b/packages/xen-api/src/transports/_UnsupportedTransport.js new file mode 100644 index 000000000..6a452f472 --- /dev/null +++ b/packages/xen-api/src/transports/_UnsupportedTransport.js @@ -0,0 +1,3 @@ +import makeError from 'make-error' + +export default makeError('UnsupportedTransport') diff --git a/packages/xen-api/src/transports/_prepareXmlRpcParams.js b/packages/xen-api/src/transports/_prepareXmlRpcParams.js new file mode 100644 index 000000000..287c86cc9 --- /dev/null +++ b/packages/xen-api/src/transports/_prepareXmlRpcParams.js @@ -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 +} diff --git a/packages/xen-api/src/transports/_utils.js b/packages/xen-api/src/transports/_utils.js deleted file mode 100644 index 745ed33ff..000000000 --- a/packages/xen-api/src/transports/_utils.js +++ /dev/null @@ -1,3 +0,0 @@ -import makeError from 'make-error' - -export const UnsupportedTransport = makeError('UnsupportedTransport') diff --git a/packages/xen-api/src/transports/auto.js b/packages/xen-api/src/transports/auto.js index 7785b0a6e..3603fd02a 100644 --- a/packages/xen-api/src/transports/auto.js +++ b/packages/xen-api/src/transports/auto.js @@ -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 diff --git a/packages/xen-api/src/transports/json-rpc.js b/packages/xen-api/src/transports/json-rpc.js index a6b1a4ce7..d340583ed 100644 --- a/packages/xen-api/src/transports/json-rpc.js +++ b/packages/xen-api/src/transports/json-rpc.js @@ -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 }) => { diff --git a/packages/xen-api/src/transports/xml-rpc-json.js b/packages/xen-api/src/transports/xml-rpc-json.js index 8210ef682..a3ed4bd33 100644 --- a/packages/xen-api/src/transports/xml-rpc-json.js +++ b/packages/xen-api/src/transports/xml-rpc-json.js @@ -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) } diff --git a/packages/xen-api/src/transports/xml-rpc.js b/packages/xen-api/src/transports/xml-rpc.js index e88350cb2..69c2b23fa 100644 --- a/packages/xen-api/src/transports/xml-rpc.js +++ b/packages/xen-api/src/transports/xml-rpc.js @@ -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) } diff --git a/yarn.lock b/yarn.lock index 68dd3b2b5..9996d141a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==