Compare commits

..

7 Commits

Author SHA1 Message Date
Julien Fontanet
50dc3e10e4 feat(xen-api): rewrite from scratch 2023-11-23 10:20:33 +01:00
MlssFrncJrg
a9fbcf3962 feat(xo-web/new VM): always show ISO selector (#7166)
Fixes #3464
2023-11-22 11:04:30 +01:00
Michael Bennett
887b49ebbf docs(installation): Fedora & CentOS wrong package libvhd-utils (#7200)
Under Packages the installation of package `libvhdi-utils` is incorrect for Fedora/CentOS. This should be replaced by `libvhdi-tools` instead.
2023-11-21 17:46:17 +01:00
Florent BEAUCHAMP
858ecbc217 fix(xapi/VDI_importContent): other_config entries must be strings (#7198)
Introduced byffd523679de80b36b2eacd30cc98de3c588a2b77
2023-11-21 16:55:53 +01:00
Florent BEAUCHAMP
ffd523679d feat(backups): update VDI importing status its name_label 2023-11-21 14:38:49 +01:00
Florent BEAUCHAMP
bd9db437f1 feat(xapi/VDI_importContent): store task UUID and stream length into other_config 2023-11-21 14:38:49 +01:00
Florent BEAUCHAMP
0365bacfbb feat(backups): show more detail on restored VM (#7186) 2023-11-21 12:28:53 +01:00
40 changed files with 317 additions and 5042 deletions

View File

@@ -84,6 +84,13 @@ export class ImportVmBackup {
vmRef,
`${metadata.vm.name_label} (${formatFilenameDate(metadata.timestamp)})`
),
xapi.call(
'VM.set_name_description',
vmRef,
`Restored on ${formatFilenameDate(+new Date())} from ${adapter._handler._remote.name} -
${metadata.vm.name_description}
`
),
])
return {

View File

@@ -256,7 +256,9 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
if (stream.length === undefined) {
stream = await createVhdStreamWithLength(stream)
}
await xapi.setField('VDI', vdi.$ref, 'name_label', `[Importing] ${vdiRecords[id].name_label}`)
await vdi.$importContent(stream, { cancelToken, format: 'vhd' })
await xapi.setField('VDI', vdi.$ref, 'name_label', vdiRecords[id].name_label)
}
}),

View File

@@ -137,14 +137,16 @@ class Vdi {
const vdi = await this.getRecord('VDI', ref)
const sr = await this.getRecord('SR', vdi.SR)
try {
const taskRef = await this.task_create(`Importing content into VDI ${vdi.name_label} on SR ${sr.name_label}`)
const uuid = await this.getField('task', taskRef, 'uuid')
await vdi.update_other_config({ 'xo:import:task': uuid, 'xo:import:length': stream.length.toString() })
await this.putResource(cancelToken, stream, '/import_raw_vdi/', {
query: {
format,
vdi: ref,
},
task: await this.task_create(`Importing content into VDI ${vdi.name_label} on SR ${sr.name_label}`),
task: taskRef,
})
} catch (error) {
// augment the error with as much relevant info as possible
@@ -153,6 +155,8 @@ class Vdi {
error.SR = sr
error.VDI = vdi
throw error
} finally {
vdi.update_other_config({ 'xo:import:task': null, 'xo:import:length': null }).catch(warn)
}
}
}

View File

@@ -9,6 +9,9 @@
- [Netbox] Ability to synchronize XO users as Netbox tenants (PR [#7158](https://github.com/vatesfr/xen-orchestra/pull/7158))
- [VM/Console] Add a message to indicate that the console view has been [disabled](https://support.citrix.com/article/CTX217766/how-to-disable-the-console-for-the-vm-in-xencenter) for this VM [#6319](https://github.com/vatesfr/xen-orchestra/issues/6319) (PR [#7161](https://github.com/vatesfr/xen-orchestra/pull/7161))
- [Restore] Show source remote and restoration time on a restored VM (PR [#7186](https://github.com/vatesfr/xen-orchestra/pull/7186))
- [Backup/Import] Show disk import status during Incremental Replication or restoration of Incremental Backup (PR [#7171](https://github.com/vatesfr/xen-orchestra/pull/7171))
- [VM Creation] Added ISO option in new VM form when creating from template with a disk [#3464](https://github.com/vatesfr/xen-orchestra/issues/3464) (PR [#7166](https://github.com/vatesfr/xen-orchestra/pull/7166))
### Bug fixes
@@ -36,7 +39,7 @@
<!--packages-start-->
- @vates/nbd-client patch
- @xen-orchestra/backups patch
- @xen-orchestra/backups minor
- @xen-orchestra/cr-seed-cli major
- @xen-orchestra/vmware-explorer patch
- xen-api major

View File

@@ -112,7 +112,7 @@ apt-get install build-essential redis-server libpng-dev git python3-minimal libv
On Fedora/CentOS like:
```sh
dnf install redis libpng-devel git libvhdi-utils lvm2 cifs-utils make automake gcc gcc-c++
dnf install redis libpng-devel git libvhdi-tools lvm2 cifs-utils make automake gcc gcc-c++
```
### Make sure Redis is running

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.body, 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.body,
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

@@ -48,8 +48,7 @@
"promise-toolbox": "^0.21.0",
"proxy-agent": "^5.0.0",
"pw": "0.0.4",
"undici": "^5.27.2",
"xmlrpc-parser": "^1.0.3",
"xmlrpc": "^1.3.2",
"xo-collection": "^0.5.0"
},
"devDependencies": {

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 { 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 ({ agent, client, url }) => {
url = new URL('./jsonrpc', Object.assign(new URL('http://localhost'), url))
const path = url.pathname + url.search
return async function (method, args) {
const res = await client.request({
body: format.request(0, method, args),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
method: 'POST',
path,
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.body.text())
if (response.type === 'response') {
return response.result
}
throw XapiError.wrap(response.error)
}
}

View File

@@ -1,50 +0,0 @@
import { XmlRpcMessage, XmlRpcResponse } from 'xmlrpc-parser'
import prepareXmlRpcParams from './_prepareXmlRpcParams.mjs'
import XapiError from '../_XapiError.mjs'
import UnsupportedTransport from './_UnsupportedTransport.mjs'
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 ({ agent, client, url }) => {
url = new URL('./xmlrpc', Object.assign(new URL('http://localhost'), url))
const path = url.pathname + url.search
return async function (method, args) {
const message = new XmlRpcMessage(method, prepareXmlRpcParams(args))
const res = await client.request({
body: message.xml(),
headers: {
Accept: 'text/xml',
'Content-Type': 'text/xml',
},
method: 'POST',
path,
agent,
})
if (res.headers['content-type'] !== 'text/xml' && res.headers['content-type'] !== 'application/xml') {
throw new UnsupportedTransport()
}
const xml = await res.body.text()
const response = await new XmlRpcResponse().parse(xml)
return parseResult(response.params[0])
}
}

View File

@@ -594,6 +594,7 @@ const TRANSFORMS = {
usage: +obj.physical_utilisation,
VDI_type: obj.type,
current_operations: obj.current_operations,
other_config: obj.other_config,
$SR: link(obj, 'SR'),
$VBDs: link(obj, 'VBDs'),

View File

@@ -1223,40 +1223,6 @@ export default class NewVm extends BaseComponent {
</SectionContent>
) : (
<SectionContent>
<Item>
<span className={styles.item}>
<input
checked={installMethod === 'ISO'}
name='installMethod'
onChange={this._linkState('installMethod')}
type='radio'
value='ISO'
/>
&nbsp;
<span>{_('newVmIsoDvdLabel')}</span>
&nbsp;
<span className={styles.inlineSelect}>
{this.props.pool ? (
<SelectVdi
disabled={installMethod !== 'ISO'}
onChange={this._linkState('installIso')}
predicate={isVdiPresent}
srPredicate={this._getIsoPredicate()}
value={installIso}
/>
) : (
<SelectResourceSetsVdi
disabled={installMethod !== 'ISO'}
onChange={this._linkState('installIso')}
predicate={isVdiPresent}
resourceSet={this._getResolvedResourceSet()}
srPredicate={this._getIsoPredicate()}
value={installIso}
/>
)}
</span>
</span>
</Item>
{template.virtualizationMode === 'pv' ? (
<span>
<Item>
@@ -1295,6 +1261,40 @@ export default class NewVm extends BaseComponent {
)}
</SectionContent>
)}
<SectionContent>
<span className={styles.item}>
<input
checked={installMethod === 'ISO'}
name='installMethod'
onChange={this._linkState('installMethod')}
type='radio'
value='ISO'
/>
&nbsp;
<span>{_('newVmIsoDvdLabel')}</span>
&nbsp;
<span className={styles.inlineSelect}>
{this.props.pool ? (
<SelectVdi
disabled={installMethod !== 'ISO'}
onChange={this._linkState('installIso')}
predicate={isVdiPresent}
srPredicate={this._getIsoPredicate()}
value={installIso}
/>
) : (
<SelectResourceSetsVdi
disabled={installMethod !== 'ISO'}
onChange={this._linkState('installIso')}
predicate={isVdiPresent}
resourceSet={this._getResolvedResourceSet()}
srPredicate={this._getIsoPredicate()}
value={installIso}
/>
)}
</span>
</span>
</SectionContent>
{this._isCoreOs() && (
<div>
<label>{_('newVmCloudConfig')}</label>{' '}

View File

@@ -2106,11 +2106,6 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.54.0.tgz#4fab9a2ff7860082c304f750e94acd644cf984cf"
integrity sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==
"@fastify/busboy@^2.0.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff"
integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==
"@fontsource/poppins@^5.0.8":
version "5.0.8"
resolved "https://registry.yarnpkg.com/@fontsource/poppins/-/poppins-5.0.8.tgz#a1c5540aedb3719a36eba5c7c5dfaa3aed3c9f80"
@@ -18787,21 +18782,16 @@ sass@^1.38.1:
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
sax-parser@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/sax-parser/-/sax-parser-2.0.2.tgz#7b3b4a25fc69bf4e729ad5f0f98430205d461689"
integrity sha512-EjLxlFjZdmv/cpOwV+klYEeOYjR2Dc9C495d2Ruk+N6xknrOnIfjSum2a63hfi9Vox2fCsjYc3NuDVo0YkGpjg==
sax@1.2.x, sax@~1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
sax@>=0.6, sax@>=0.6.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0"
integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==
sax@~1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
scheduler@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
@@ -20819,13 +20809,6 @@ undici-types@~5.26.4:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
undici@^5.27.2:
version "5.27.2"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.27.2.tgz#a270c563aea5b46cc0df2550523638c95c5d4411"
integrity sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==
dependencies:
"@fastify/busboy" "^2.0.0"
unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
@@ -21050,11 +21033,6 @@ use@^3.1.0:
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
utf8-base64@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/utf8-base64/-/utf8-base64-0.1.2.tgz#555806c458f9ba3f089c3ebe0c5f6198348bb57b"
integrity sha512-DNeEx/I7HruiVsfk/DbEl4bpdRR/mv5p6FGDFZVyA8wqdMOqYp0CeCgW4/DzsPIW/skOq5Bxv49/eYfvAYJTWg==
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -22003,6 +21981,11 @@ xml2js@^0.4.19, xml2js@^0.4.23:
sax ">=0.6.0"
xmlbuilder "~11.0.0"
xmlbuilder@8.2.x:
version "8.2.2"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773"
integrity sha512-eKRAFz04jghooy8muekqzo8uCSVNeyRedbuJrp0fovbLIi7wlsYtdUn3vBAAPq2Y3/0xMz2WMEUQ8yhVVO9Stw==
xmlbuilder@^15.1.1:
version "15.1.1"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5"
@@ -22013,13 +21996,13 @@ xmlbuilder@~11.0.0:
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
xmlrpc-parser@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/xmlrpc-parser/-/xmlrpc-parser-1.0.3.tgz#94f21bb74daaa2290a51471635c73f8b6dc1f3d9"
integrity sha512-0197DF6MrKFoiaccl2GuB5mcc3F0jSebPLHIqsahpau4yyztg34bVZDhc6HGzs5hji801prnytCKTo4Kdpa7Rw==
xmlrpc@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/xmlrpc/-/xmlrpc-1.3.2.tgz#26b2ea347848d028aac7e7514b5351976de3e83d"
integrity sha512-jQf5gbrP6wvzN71fgkcPPkF4bF/Wyovd7Xdff8d6/ihxYmgETQYSuTc+Hl+tsh/jmgPLro/Aro48LMFlIyEKKQ==
dependencies:
sax-parser "^2.0.2"
utf8-base64 "^0.1.2"
sax "1.2.x"
xmlbuilder "8.2.x"
xok@^1.0.0:
version "1.0.0"