Compare commits
1 Commits
trustCerti
...
xen-api-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50dc3e10e4 |
111
packages/xen-api/.USAGE2.md
Normal file
111
packages/xen-api/.USAGE2.md
Normal 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()
|
||||
```
|
||||
@@ -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:'))
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import debug from 'debug'
|
||||
|
||||
export default debug('xen-api')
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
const SUFFIX = '.get_all_records'
|
||||
|
||||
export default method => method.endsWith(SUFFIX)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export default (setting, defaultValue) =>
|
||||
setting === undefined
|
||||
? () => defaultValue
|
||||
: typeof setting === 'function'
|
||||
? setting
|
||||
: typeof setting === 'object'
|
||||
? method => setting[method] ?? setting['*'] ?? defaultValue
|
||||
: () => setting
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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
115
packages/xen-api/events.mjs
Normal 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
|
||||
@@ -1,5 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
ignorePatterns: ['*'],
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
if (process.env.DEBUG === undefined) {
|
||||
process.env.DEBUG = 'xen-api'
|
||||
}
|
||||
@@ -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'))
|
||||
@@ -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'))
|
||||
@@ -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:'))
|
||||
@@ -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'))
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
2647
packages/xen-api/examples/package-lock.json
generated
2647
packages/xen-api/examples/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -1,3 +0,0 @@
|
||||
import makeError from 'make-error'
|
||||
|
||||
export default makeError('UnsupportedTransport')
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user