Compare commits

...

1 Commits

Author SHA1 Message Date
Julien Fontanet
50dc3e10e4 feat(xen-api): rewrite from scratch 2023-11-23 10:20:33 +01:00
31 changed files with 246 additions and 4914 deletions

111
packages/xen-api/.USAGE2.md Normal file
View File

@@ -0,0 +1,111 @@
```js
import { Xapi } from 'xen-api'
// bare-bones XAPI client
const xapi = new Xapi({
// URL to a host belonging to the XCP-ng/XenServer pool we want to connect to
url: 'https://xen1.company.net',
// credentials used to connect to this XAPI
auth: {
user: 'root',
password: 'important secret password',
},
// if true, only side-effects free calls will be allowed
readOnly: false,
})
// ensure that the connection is working
await xapi.checkConnection()
// call a XAPI method
//
// see available methods there: https://xapi-project.github.io/xen-api/
const result = await xapi.call(
// name of the method
'VM.snapshot',
// list of params
[vm.$ref, 'My new snapshot'],
// options
{
// AbortSignal that can be used to stop the call
//
// Note: this will not stop/rollback the side-effects of the call
signal,
}
)
// after a call (or checkConnection) has succeed, the following properties are available
// list of classes available on this XAPI
xapi.classes
// timestamp of the last reply from XAPI
xapi.lastReply
// pool record of this XAPI
xapi.pool
// secret identifier of the current session
//
// it might become obsolete, in that case, it will be automatically renewed by the next call
xapi.sessionId
// invalidate the session identifier
await xapi.logOut()
```
```js
import { Proxy } from 'xen-api/proxy'
const proxy = new Proxy(xapi)
await proxy.VM.snapshot()
```
```js
import { Events } from 'xen-api/events'
const events = new Events(xapi)
// ensure that all events until now have been received and processed
await events.barrier()
// watch events on tasks and wait for a task to finish
const task = await events.waitTask(taskRef, { signal })
// for long running actions, it's better to use an async call which will are based on tasks
const result = await events.asyncCall(method)
const stop = events.watch(
// class that we are interested in
//
// use `*` for all classes
'pool',
// called each time a new event for this class has been received
//
// https://xapi-project.github.io/xen-api/classes/event.html
event => {
stop()
}
)
// when wanting to really stop watching all events, simply remove all watchers
events.clear()
```
```js
import { Cache } from 'xen-api/events'
const cache = new Cache(watcher)
const host = await cache.get('host', 'OpaqueRef:1c3f19c8-f80a-464d-9c48-a2c19d4e4fc3')
const vm = await cache.getByUuid('VM', '355ee47d-ff4c-4924-3db2-fd86ae629676')
cache.clear()
```

View File

@@ -1,32 +0,0 @@
const EMPTY = 'OpaqueRef:NULL'
const PREFIX = 'OpaqueRef:'
export default {
// Reference to use to indicate it's not pointing to an object
EMPTY,
// Whether this value is a reference (probably) pointing to an object
isNotEmpty(val) {
return val !== EMPTY && typeof val === 'string' && val.startsWith(PREFIX)
},
// Whether this value looks like a reference
is(val) {
return (
typeof val === 'string' &&
(val.startsWith(PREFIX) ||
// 2019-02-07 - JFT: even if `value` should not be an empty string for
// a ref property, an user had the case on XenServer 7.0 on the CD VBD
// of a VM created by XenCenter
val === '' ||
// 2021-03-08 - JFT: there is an bug in XCP-ng/XenServer which leads to
// some refs to be `Ref:*` instead of being rewritten
//
// We'll consider them as empty refs in this lib to avoid issues with
// _wrapRecord.
//
// See https://github.com/xapi-project/xen-api/issues/4338
val.startsWith('Ref:'))
)
},
}

View File

@@ -1,30 +0,0 @@
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

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

View File

@@ -1,22 +0,0 @@
import { Cancel } from 'promise-toolbox'
import XapiError from './_XapiError.mjs'
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

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

View File

@@ -1,6 +0,0 @@
const RE = /^[^.]+\.get_/
export default function isReadOnlyCall(method, args) {
const n = args.length
return (n === 0 || (n === 1 && typeof args[0] === 'string')) && RE.test(method)
}

View File

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

View File

@@ -1,26 +0,0 @@
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:', auth, username = '', password = '', hostname, port, pathname = '/'] = matches
const parsedUrl = {
protocol,
hostname,
port,
pathname,
// compat with url.parse
auth,
}
if (username !== '') {
parsedUrl.username = decodeURIComponent(username)
}
if (password !== '') {
parsedUrl.password = decodeURIComponent(password)
}
return parsedUrl
}

View File

@@ -1,50 +0,0 @@
import t from 'tap'
import parseUrl from './_parseUrl.mjs'
const data = {
'xcp.company.lan': {
hostname: 'xcp.company.lan',
pathname: '/',
protocol: 'https:',
},
'[::1]': {
hostname: '[::1]',
pathname: '/',
protocol: 'https:',
},
'http://username:password@xcp.company.lan': {
auth: 'username:password',
hostname: 'xcp.company.lan',
password: 'password',
pathname: '/',
protocol: 'http:',
username: 'username',
},
'https://username@xcp.company.lan': {
auth: 'username',
hostname: 'xcp.company.lan',
pathname: '/',
protocol: 'https:',
username: 'username',
},
}
t.test('invalid url', function (t) {
t.throws(() => parseUrl(''))
t.end()
})
for (const url of Object.keys(data)) {
t.test(url, function (t) {
const parsed = parseUrl(url)
for (const key of Object.keys(parsed)) {
if (parsed[key] === undefined) {
delete parsed[key]
}
}
t.same(parsed, data[url])
t.end()
})
}

View File

@@ -1,17 +0,0 @@
import mapValues from 'lodash/mapValues.js'
export default function replaceSensitiveValues(value, replacement) {
function helper(value, name) {
if (name === 'password' && typeof value === 'string') {
return replacement
}
if (typeof value !== 'object' || value === null) {
return value
}
return Array.isArray(value) ? value.map(helper) : mapValues(value, helper)
}
return helper(value)
}

View File

@@ -1,130 +0,0 @@
/* eslint-disable no-console */
import blocked from 'blocked'
import createDebug from 'debug'
import filter from 'lodash/filter.js'
import find from 'lodash/find.js'
import L from 'lodash'
import minimist from 'minimist'
import pw from 'pw'
import { asCallback, fromCallback, fromEvent } from 'promise-toolbox'
import { diff } from 'jest-diff'
import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
import { start as createRepl } from 'repl'
// ===================================================================
function askPassword(prompt = 'Password: ') {
if (prompt) {
process.stdout.write(prompt)
}
return new Promise(resolve => {
pw(resolve)
})
}
const { getPrototypeOf, ownKeys } = Reflect
function getAllBoundDescriptors(object) {
const descriptors = { __proto__: null }
let current = object
do {
ownKeys(current).forEach(key => {
if (!(key in descriptors)) {
descriptors[key] = getBoundPropertyDescriptor(current, key, object)
}
})
} while ((current = getPrototypeOf(current)) !== null)
return descriptors
}
// ===================================================================
const usage = 'Usage: xen-api <url> [<user> [<password>]]'
export async function main(createClient) {
const opts = minimist(process.argv.slice(2), {
string: ['proxy', 'session-id', 'transport'],
boolean: ['allow-unauthorized', 'help', 'read-only', 'verbose'],
alias: {
'allow-unauthorized': 'au',
debounce: 'd',
help: 'h',
proxy: 'p',
'read-only': 'ro',
verbose: 'v',
transport: 't',
},
})
if (opts.help) {
return usage
}
if (opts.verbose) {
// Does not work perfectly.
//
// https://github.com/visionmedia/debug/pull/156
createDebug.enable('xen-api,xen-api:*')
}
let auth
if (opts._.length > 1) {
const [, user, password = await askPassword()] = opts._
auth = { user, password }
} else if (opts['session-id'] !== undefined) {
auth = { sessionId: opts['session-id'] }
}
{
const debug = createDebug('xen-api:perf')
blocked(ms => {
debug('blocked for %sms', ms | 0)
})
}
const xapi = createClient({
url: opts._[0],
allowUnauthorized: opts.au,
auth,
debounce: opts.debounce != null ? +opts.debounce : null,
httpProxy: opts.proxy,
readOnly: opts.ro,
syncStackTraces: true,
transport: opts.transport || undefined,
})
await xapi.connect()
const repl = createRepl({
prompt: `${xapi._humanId}> `,
})
{
const ctx = repl.context
ctx.xapi = xapi
ctx.diff = (a, b) => console.log('%s', diff(a, b))
ctx.find = predicate => find(xapi.objects.all, predicate)
ctx.findAll = predicate => filter(xapi.objects.all, predicate)
ctx.L = L
Object.defineProperties(ctx, getAllBoundDescriptors(xapi))
}
// Make the REPL waits for promise completion.
repl.eval = (evaluate => (cmd, context, filename, cb) => {
asCallback.call(
fromCallback(cb => {
evaluate.call(repl, cmd, context, filename, cb)
}).then(value => (Array.isArray(value) ? Promise.all(value) : value)),
cb
)
})(repl.eval)
await fromEvent(repl, 'exit')
try {
await xapi.disconnect()
} catch (error) {}
}
/* eslint-enable no-console */

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env node
import { createClient } from './index.mjs'
import { main } from './cli-lib.mjs'
main(createClient).catch(console.error.bind(console, 'FATAL'))

115
packages/xen-api/events.mjs Normal file
View File

@@ -0,0 +1,115 @@
const EVENT_TIMEOUT = 60e3
export class Watcher {
#abortController
#typeWatchers = new Map()
classes = new Map()
xapi
constructor(xapi) {
this.xapi = xapi
}
async asyncCall(method, params, { signal }) {
const taskRef = await this.xapi.call('Async.' + method, params, { signal })
return new Promise((resolve, reject) => {
const stop = this.watch(
'task',
taskRef,
task => {
const { status } = task
if (status === 'success') {
stop()
resolve(task.status)
} else if (status === 'cancelled' || status === 'failure') {
stop()
reject(task.error_info)
}
},
{ signal }
)
})
}
async #start() {
const { xapi } = this
const { signal } = this.#abortController
const watchers = this.#typeWatchers
let token = await xapi.call('event.inject', 'pool', xapi.pool.$ref)
while (true) {
signal.throwIfRequested()
const result = await xapi.call({ signal }, 'event.from', this.classes, token, EVENT_TIMEOUT)
for (const event of result.events) {
}
}
this.#abortController = undefined
}
start() {
if (this.#abortController !== undefined) {
throw new Error('already started')
}
this.#abortController = new AbortController()
this.#start()
}
stop() {
if (this.#abortController === undefined) {
throw new Error('already stopped')
}
this.#abortController.abort()
}
}
export class Cache {
// contains records indexed by type + ref
//
// plain records when retrieved by events
//
// promises to record when retrieved by a get_record call (might be a rejection if the record does not exist)
#recordCache = new Map()
#watcher
constructor(watcher) {
this.#watcher = watcher
}
async #get(type, ref) {
let record
try {
record = await this.#watcher.xapi.call(`${type}.get_record`, ref)
} catch (error) {
if (error.code !== 'HANDLE_INVALID') {
throw error
}
record = Promise.reject(error)
}
this.#recordCache.set(type, Promise.resolve(record))
return record
}
async get(type, ref) {
const cache = this.#recordCache
const key = type + ref
let record = cache.get(key)
if (record === undefined) {
record = this.#get(type, ref)
cache.set(key, record)
}
return record
}
async getByUuid(type, uuid) {
return this.get(type, await this.#watcher.xapi.call(`${type}.get_by_uuid`, uuid))
}
}
exports.Cache = Cache

View File

@@ -1,5 +0,0 @@
'use strict'
module.exports = {
ignorePatterns: ['*'],
}

View File

@@ -1,3 +0,0 @@
if (process.env.DEBUG === undefined) {
process.env.DEBUG = 'xen-api'
}

View File

@@ -1,67 +0,0 @@
#!/usr/bin/env node
import './env.mjs'
import createProgress from 'progress-stream'
import createTop from 'process-top'
import getopts from 'getopts'
import { defer } from 'golike-defer'
import { CancelToken } from 'promise-toolbox'
import { createClient } from '../index.mjs'
import { createOutputStream, formatProgress, pipeline, resolveRecord, throttle } from './utils.mjs'
defer(async ($defer, rawArgs) => {
const {
raw,
throttle: bps,
_: args,
} = getopts(rawArgs, {
boolean: 'raw',
alias: {
raw: 'r',
throttle: 't',
},
})
if (args.length < 2) {
return console.log('Usage: export-vdi [--raw] <XS URL> <VDI identifier> [<VHD file>]')
}
const xapi = createClient({
allowUnauthorized: true,
url: args[0],
watchEvents: false,
})
await xapi.connect()
$defer(() => xapi.disconnect())
const { cancel, token } = CancelToken.source()
process.on('SIGINT', cancel)
const vdi = await resolveRecord(xapi, 'VDI', args[1])
// https://xapi-project.github.io/xen-api/snapshots.html#downloading-a-disk-or-snapshot
const exportStream = await xapi.getResource(token, '/export_raw_vdi/', {
query: {
format: raw ? 'raw' : 'vhd',
vdi: vdi.$ref,
},
})
console.warn('Export task:', exportStream.headers['task-id'])
const top = createTop()
const progressStream = createProgress()
$defer(
clearInterval,
setInterval(() => {
console.warn('\r %s | %s', top.toString(), formatProgress(progressStream.progress()))
}, 1e3)
)
await pipeline(exportStream, progressStream, throttle(bps), createOutputStream(args[2]))
})(process.argv.slice(2)).catch(console.error.bind(console, 'error'))

View File

@@ -1,54 +0,0 @@
#!/usr/bin/env node
import './env.mjs'
import createProgress from 'progress-stream'
import getopts from 'getopts'
import { defer } from 'golike-defer'
import { CancelToken } from 'promise-toolbox'
import { createClient } from '../index.mjs'
import { createOutputStream, formatProgress, pipeline, resolveRecord } from './utils.mjs'
defer(async ($defer, rawArgs) => {
const {
gzip,
zstd,
_: args,
} = getopts(rawArgs, {
boolean: ['gzip', 'zstd'],
})
if (args.length < 2) {
return console.log('Usage: export-vm <XS URL> <VM identifier> [<XVA file>]')
}
const xapi = createClient({
allowUnauthorized: true,
url: args[0],
watchEvents: false,
})
await xapi.connect()
$defer(() => xapi.disconnect())
const { cancel, token } = CancelToken.source()
process.on('SIGINT', cancel)
// https://xapi-project.github.io/xen-api/importexport.html
const exportStream = await xapi.getResource(token, '/export/', {
query: {
ref: (await resolveRecord(xapi, 'VM', args[1])).$ref,
use_compression: zstd ? 'zstd' : gzip ? 'true' : 'false',
},
})
console.warn('Export task:', exportStream.headers['task-id'])
await pipeline(
exportStream,
createProgress({ time: 1e3 }, p => console.warn(formatProgress(p))),
createOutputStream(args[2])
)
})(process.argv.slice(2)).catch(console.error.bind(console, 'error'))

View File

@@ -1,88 +0,0 @@
#!/usr/bin/env node
import './env.mjs'
import getopts from 'getopts'
import { defer } from 'golike-defer'
import { CancelToken } from 'promise-toolbox'
import { createVhdStreamWithLength } from 'vhd-lib'
import { createClient } from '../index.mjs'
import { createInputStream, resolveRef } from './utils.mjs'
defer(async ($defer, argv) => {
const opts = getopts(argv, { boolean: ['events', 'raw', 'remove-length'], string: ['sr', 'vdi'] })
const url = opts._[0]
if (url === undefined) {
return console.log(
'Usage: import-vdi [--events] [--raw] [--sr <SR identifier>] [--vdi <VDI identifier>] <XS URL> [<VHD file>]'
)
}
const { raw, sr, vdi } = opts
const createVdi = vdi === ''
if (createVdi) {
if (sr === '') {
throw 'requires either --vdi or --sr'
}
if (!raw) {
throw 'creating a VDI requires --raw'
}
} else if (sr !== '') {
throw '--vdi and --sr are mutually exclusive'
}
const xapi = createClient({
allowUnauthorized: true,
url,
watchEvents: opts.events && ['task'],
})
await xapi.connect()
$defer(() => xapi.disconnect())
const { cancel, token } = CancelToken.source()
process.on('SIGINT', cancel)
let input = createInputStream(opts._[1])
$defer.onFailure(() => input.destroy())
let vdiRef
if (createVdi) {
vdiRef = await xapi.call('VDI.create', {
name_label: 'xen-api/import-vdi',
other_config: {},
read_only: false,
sharable: false,
SR: await resolveRef(xapi, 'SR', sr),
type: 'user',
virtual_size: input.length,
})
$defer.onFailure(() => xapi.call('VDI.destroy', vdiRef))
} else {
vdiRef = await resolveRef(xapi, 'VDI', vdi)
}
if (opts['remove-length']) {
delete input.length
console.log('length removed')
} else if (!raw && input.length === undefined) {
input = await createVhdStreamWithLength(input)
}
// https://xapi-project.github.io/xen-api/snapshots.html#uploading-a-disk-or-snapshot
const result = await xapi.putResource(token, input, '/import_raw_vdi/', {
query: {
format: raw ? 'raw' : 'vhd',
vdi: vdiRef,
},
})
if (result !== undefined) {
console.log(result)
}
})(process.argv.slice(2)).catch(console.error.bind(console, 'Fatal:'))

View File

@@ -1,33 +0,0 @@
#!/usr/bin/env node
import './env.mjs'
import { defer } from 'golike-defer'
import { CancelToken } from 'promise-toolbox'
import { createClient } from '../index.mjs'
import { createInputStream, resolveRef } from './utils.mjs'
defer(async ($defer, args) => {
if (args.length < 1) {
return console.log('Usage: import-vm <XS URL> [<XVA file>] [<SR identifier>]')
}
const xapi = createClient({
allowUnauthorized: true,
url: args[0],
watchEvents: false,
})
await xapi.connect()
$defer(() => xapi.disconnect())
const { cancel, token } = CancelToken.source()
process.on('SIGINT', cancel)
// https://xapi-project.github.io/xen-api/importexport.html
await xapi.putResource(token, createInputStream(args[1]), '/import/', {
query: args[2] && { sr_id: await resolveRef(xapi, 'SR', args[2]) },
})
})(process.argv.slice(2)).catch(console.error.bind(console, 'error'))

View File

@@ -1,59 +0,0 @@
#!/usr/bin/env node
import 'source-map-support/register.js'
import forEach from 'lodash/forEach.js'
import size from 'lodash/size.js'
import { createClient } from '../index.mjs'
// ===================================================================
if (process.argv.length < 3) {
throw new Error('Usage: log-events <XS URL>')
}
// ===================================================================
// Creation
const xapi = createClient({
allowUnauthorized: true,
url: process.argv[2],
})
// ===================================================================
// Method call
xapi.connect().then(() => {
xapi
.call('VM.get_all_records')
.then(function (vms) {
console.log('%s VMs fetched', size(vms))
})
.catch(function (error) {
console.error(error)
})
})
// ===================================================================
// Objects
const objects = xapi.objects
objects.on('add', objects => {
forEach(objects, object => {
console.log('+ %s: %s', object.$type, object.$id)
})
})
objects.on('update', objects => {
forEach(objects, object => {
console.log('± %s: %s', object.$type, object.$id)
})
})
objects.on('remove', objects => {
forEach(objects, (value, id) => {
console.log('- %s', id)
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +0,0 @@
{
"dependencies": {
"getopts": "^2.2.3",
"golike-defer": "^0.5.1",
"human-format": "^0.11.0",
"lodash": "^4.17.21",
"process-top": "^1.2.0",
"progress-stream": "^2.0.0",
"promise-toolbox": "^0.19.2",
"readable-stream": "^4.4.2",
"source-map-support": "^0.5.21",
"throttle": "^1.0.3",
"vhd-lib": "^4.6.1"
}
}

View File

@@ -1,75 +0,0 @@
import { createReadStream, createWriteStream, statSync } from 'fs'
import { fromCallback } from 'promise-toolbox'
import { PassThrough, pipeline as Pipeline } from 'readable-stream'
import humanFormat from 'human-format'
import Throttle from 'throttle'
import Ref from '../_Ref.mjs'
export const createInputStream = path => {
if (path === undefined || path === '-') {
return process.stdin
}
const { size } = statSync(path)
const stream = createReadStream(path)
stream.length = size
return stream
}
export const createOutputStream = path => {
if (path !== undefined && path !== '-') {
return createWriteStream(path)
}
// introduce a through stream because stdout is not a normal stream!
const stream = new PassThrough()
stream.pipe(process.stdout)
return stream
}
const formatSizeOpts = { scale: 'binary', unit: 'B' }
const formatSize = bytes => humanFormat(bytes, formatSizeOpts)
export const formatProgress = p => {
return [
formatSize(p.transferred),
' / ',
formatSize(p.length),
' | ',
p.runtime,
's / ',
p.eta,
's | ',
formatSize(p.speed),
'/s',
].join('')
}
export const pipeline = (...streams) => {
return fromCallback(cb => {
streams = streams.filter(_ => _ != null)
streams.push(cb)
Pipeline.apply(undefined, streams)
})
}
const resolveRef = (xapi, type, refOrUuidOrNameLabel) =>
Ref.is(refOrUuidOrNameLabel)
? refOrUuidOrNameLabel
: xapi.call(`${type}.get_by_uuid`, refOrUuidOrNameLabel).catch(() =>
xapi.call(`${type}.get_by_name_label`, refOrUuidOrNameLabel).then(refs => {
if (refs.length === 1) {
return refs[0]
}
throw new Error(`no single match for ${type} with name label ${refOrUuidOrNameLabel}`)
})
)
export const resolveRecord = async (xapi, type, refOrUuidOrNameLabel) =>
xapi.getRecord(type, await resolveRef(xapi, type, refOrUuidOrNameLabel))
export { resolveRef }
export const throttle = opts => (opts != null ? new Throttle(opts) : undefined)

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,25 +0,0 @@
// 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,35 +0,0 @@
import jsonRpc from './json-rpc.mjs'
import UnsupportedTransport from './_UnsupportedTransport.mjs'
import xmlRpc from './xml-rpc.mjs'
const factories = [jsonRpc, xmlRpc]
const { length } = factories
export default opts => {
let i = 0
let call
function create() {
const current = factories[i++](opts)
if (i < length) {
const currentI = i
call = (method, args) =>
current(method, args).catch(error => {
if (error instanceof UnsupportedTransport) {
if (currentI === i) {
// not changed yet
create()
}
return call(method, args)
}
throw error
})
} else {
call = current
}
}
create()
return (method, args) => call(method, args)
}

View File

@@ -1,11 +0,0 @@
import auto from './auto.mjs'
import jsonRpc from './json-rpc.mjs'
import xmlRpc from './xml-rpc.mjs'
export default {
__proto__: null,
auto,
'json-rpc': jsonRpc,
'xml-rpc': xmlRpc,
}

View File

@@ -1,37 +0,0 @@
import httpRequestPlus from 'http-request-plus'
import { format, parse } from 'json-rpc-protocol'
import XapiError from '../_XapiError.mjs'
import UnsupportedTransport from './_UnsupportedTransport.mjs'
// https://github.com/xenserver/xenadmin/blob/0df39a9d83cd82713f32d24704852a0fd57b8a64/XenModel/XenAPI/Session.cs#L403-L433
export default ({ secureOptions, url, agent }) => {
url = new URL('./jsonrpc', Object.assign(new URL('http://localhost'), url))
return async function (method, args) {
const res = await httpRequestPlus(url, {
...secureOptions,
body: format.request(0, method, args),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
method: 'POST',
agent,
})
// content-type is `text/xml` on old hosts where JSON-RPC is unsupported
if (res.headers['content-type'] !== 'application/json') {
throw new UnsupportedTransport()
}
const response = parse(await res.text())
if (response.type === 'response') {
return response.result
}
throw XapiError.wrap(response.error)
}
}

View File

@@ -1,45 +0,0 @@
import xmlrpc from 'xmlrpc'
import { promisify } from 'promise-toolbox'
import XapiError from '../_XapiError.mjs'
import prepareXmlRpcParams from './_prepareXmlRpcParams.mjs'
const logError = error => {
if (error.res) {
console.error('XML-RPC Error: %s (response status %s)', error.message, error.res.statusCode)
console.error('%s', error.body)
}
throw error
}
const parseResult = result => {
const status = result.Status
// Return the plain result if it does not have a valid XAPI
// format.
if (status === undefined) {
return result
}
if (status !== 'Success') {
throw XapiError.wrap(result.ErrorDescription)
}
return result.Value
}
export default ({ secureOptions, url: { hostnameRaw, pathname, port, protocol }, agent }) => {
const secure = protocol === 'https:'
const client = (secure ? xmlrpc.createSecureClient : xmlrpc.createClient)({
...(secure ? secureOptions : undefined),
agent,
host: hostnameRaw,
pathname,
port,
})
const call = promisify(client.methodCall, client)
return (method, args) => call(method, prepareXmlRpcParams(args)).then(parseResult, logError)
}