chore(xen-api): modularize (#4088)
This commit is contained in:
parent
b7e14ebf2a
commit
1bb0e234e7
@ -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",
|
||||
|
30
packages/xen-api/src/_XapiError.js
Normal file
30
packages/xen-api/src/_XapiError.js
Normal 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
|
||||
}
|
||||
}
|
3
packages/xen-api/src/_debug.js
Normal file
3
packages/xen-api/src/_debug.js
Normal file
@ -0,0 +1,3 @@
|
||||
import debug from 'debug'
|
||||
|
||||
export default debug('xen-api')
|
22
packages/xen-api/src/_getTaskResult.js
Normal file
22
packages/xen-api/src/_getTaskResult.js
Normal 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)
|
||||
}
|
||||
}
|
3
packages/xen-api/src/_isGetAllRecordsMethod.js
Normal file
3
packages/xen-api/src/_isGetAllRecordsMethod.js
Normal file
@ -0,0 +1,3 @@
|
||||
const SUFFIX = '.get_all_records'
|
||||
|
||||
export default method => method.endsWith(SUFFIX)
|
3
packages/xen-api/src/_isOpaqueRef.js
Normal file
3
packages/xen-api/src/_isOpaqueRef.js
Normal file
@ -0,0 +1,3 @@
|
||||
const PREFIX = 'OpaqueRef:'
|
||||
|
||||
export default value => typeof value === 'string' && value.startsWith(PREFIX)
|
4
packages/xen-api/src/_isReadOnlyCall.js
Normal file
4
packages/xen-api/src/_isReadOnlyCall.js
Normal file
@ -0,0 +1,4 @@
|
||||
const RE = /^[^.]+\.get_/
|
||||
|
||||
export default (method, args) =>
|
||||
args.length === 1 && typeof args[0] === 'string' && RE.test(method)
|
8
packages/xen-api/src/_makeCallSetting.js
Normal file
8
packages/xen-api/src/_makeCallSetting.js
Normal 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
|
18
packages/xen-api/src/_parseUrl.js
Normal file
18
packages/xen-api/src/_parseUrl.js
Normal 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
|
||||
}
|
@ -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
|
||||
|
3
packages/xen-api/src/transports/_UnsupportedTransport.js
Normal file
3
packages/xen-api/src/transports/_UnsupportedTransport.js
Normal file
@ -0,0 +1,3 @@
|
||||
import makeError from 'make-error'
|
||||
|
||||
export default makeError('UnsupportedTransport')
|
25
packages/xen-api/src/transports/_prepareXmlRpcParams.js
Normal file
25
packages/xen-api/src/transports/_prepareXmlRpcParams.js
Normal 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
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
import makeError from 'make-error'
|
||||
|
||||
export const UnsupportedTransport = makeError('UnsupportedTransport')
|
@ -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
|
||||
|
@ -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 }) => {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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==
|
||||
|
Loading…
Reference in New Issue
Block a user