Merge pull request #70 from vatesfr/vm-create-refactoring

vm.create() refactoring
This commit is contained in:
Julien Fontanet 2015-06-19 16:59:36 +02:00
commit b58826da6e
11 changed files with 346 additions and 253 deletions

View File

@ -67,6 +67,7 @@
"lodash.map": "^3.0.0", "lodash.map": "^3.0.0",
"lodash.pick": "^3.0.0", "lodash.pick": "^3.0.0",
"lodash.result": "^3.0.0", "lodash.result": "^3.0.0",
"lodash.snakecase": "^3.0.1",
"lodash.startswith": "^3.0.1", "lodash.startswith": "^3.0.1",
"make-error": "^1", "make-error": "^1",
"multikey-hash": "^1.0.1", "multikey-hash": "^1.0.1",

View File

@ -7,6 +7,7 @@ export {
InvalidJson, InvalidJson,
InvalidParameters, InvalidParameters,
InvalidRequest, InvalidRequest,
JsonRpcError,
MethodNotFound MethodNotFound
} from 'json-rpc-protocol' } from 'json-rpc-protocol'

View File

@ -12,6 +12,7 @@ import schemaInspector from 'schema-inspector'
import { import {
InvalidParameters, InvalidParameters,
JsonRpcError,
MethodNotFound, MethodNotFound,
NoSuchObject, NoSuchObject,
Unauthorized Unauthorized
@ -289,11 +290,12 @@ export default class Api {
context.user = await context._getUser(userId) context.user = await context._getUser(userId)
} }
await checkPermission.call(context, method)
checkParams(method, params)
await resolveParams.call(context, method, params)
try { try {
await checkPermission.call(context, method)
checkParams(method, params)
await resolveParams.call(context, method, params)
let result = await method.call(context, params) let result = await method.call(context, params)
// If nothing was returned, consider this operation a success // If nothing was returned, consider this operation a success
@ -306,7 +308,11 @@ export default class Api {
return result return result
} catch (error) { } catch (error) {
debug('Error: %s(...) → %s', name, error) if (error instanceof JsonRpcError) {
debug('Error: %s(...) → %s', name, error)
} else {
console.error(error && error.stack || error)
}
throw error throw error
} }

View File

@ -3,19 +3,10 @@ import {parseSize} from '../utils'
// =================================================================== // ===================================================================
export async function create ({name, size, sr}) { export async function create ({name, size, sr}) {
const xapi = this.getXAPI(sr) const vdi = await this.getXAPI(sr).createVdi(sr.id, parseSize(size), {
name_label: name
const ref = await xapi.call('VDI.create', {
name_label: name,
other_config: {},
read_only: false,
sharable: false,
SR: sr.ref,
type: 'user',
virtual_size: String(parseSize(size))
}) })
return vdi.$id
return (await xapi.call('VDI.get_record', ref)).uuid
} }
create.description = 'create a new disk on a SR' create.description = 'create a new disk on a SR'

View File

@ -9,12 +9,9 @@ $isArray = require 'lodash.isarray'
#===================================================================== #=====================================================================
delete_ = $coroutine ({vdi}) -> delete_ = $coroutine ({vdi}) ->
xapi = @getXAPI vdi yield @getXAPI(vdi).deleteVdi(vdi.id)
# TODO: check if VDI is attached before return
yield xapi.call 'VDI.destroy', vdi.ref
return true
delete_.params = { delete_.params = {
id: { type: 'string' }, id: { type: 'string' },

View File

@ -1,7 +1,6 @@
// TODO: move into vm and rename to removeInterface // TODO: move into vm and rename to removeInterface
async function delete_ ({vif}) { async function delete_ ({vif}) {
// TODO: check if VIF is attached before await this.getXAPI(vif).deleteVif(vif.id)
await this.getXAPI(vif).call('VIF.destroy', vif.ref)
} }
export {delete_ as delete} export {delete_ as delete}

View File

@ -28,143 +28,23 @@ $isVMRunning = do ->
#===================================================================== #=====================================================================
# TODO: Implement ACLs # TODO: Implement ACLs
# FIXME: Make the method as atomic as possible.
create = $coroutine ({ create = $coroutine ({
installation installation
name_description
name_label name_label
template template
VDIs VDIs
VIFs VIFs
}) -> }) ->
# Gets the corresponding connection. vm = yield @getXAPI(template).createVm(template.id, {
xapi = @getXAPI template installRepository: installation && installation.repository,
nameDescription: name_description,
nameLabel: name_label,
vdis: VDIs,
vifs: VIFs
})
# Clones the VM from the template. return vm.$id
vm = yield xapi.cloneVm(template.ref, name_label)
# TODO: if there is an error from now, removes this VM.
# TODO: remove existing VIFs.
# Creates associated virtual interfaces.
#
# FIXME: device n may already exists, we have to find the first
# free device number.
deviceId = 0
yield Bluebird.all(map(VIFs, (VIF) =>
return xapi.createVirtualInterface(vm.$id, VIF.network, {
position: deviceId++
})
))
# TODO: ? yield xapi.call 'VM.set_PV_args', vm.$ref, 'noninteractive'
# Updates the number of existing vCPUs.
if CPUs?
yield xapi.call 'VM.set_VCPUs_at_startup', vm.$ref, CPUs
# TODO: remove existing VDIs (o make sure we have only those we
# asked.
#
# Problem: how to know which VMs to clones for instance.
if VDIs?
# Transform the VDIs specs to conform to XAPI.
$forEach VDIs, (VDI, key) ->
VDI.bootable = if VDI.bootable then 'true' else 'false'
VDI.size = "#{VDI.size}"
VDI.sr = VDI.SR
delete VDI.SR
# Preparation for the XML generation.
VDIs[key] = { $: VDI }
return
# Converts the provision disks spec to XML.
VDIs = $js2xml {
provision: {
disk: VDIs
}
}
# Replace the existing entry in the VM object.
try yield xapi.call 'VM.remove_from_other_config', vm.$ref, 'disks'
yield xapi.call 'VM.add_to_other_config', vm.$ref, 'disks', VDIs
try yield xapi.call(
'VM.remove_from_other_config'
vm.$ref
'install-repository'
)
if installation
switch installation.method
when 'cdrom'
yield xapi.call(
'VM.add_to_other_config', vm.$ref
'install-repository', 'cdrom'
)
when 'ftp', 'http', 'nfs'
yield xapi.call(
'VM.add_to_other_config', vm.$ref
'install-repository', installation.repository
)
else
@throw(
'INVALID_PARAMS'
"Unsupported installation method #{installation.method}"
)
# Creates the VDIs and executes the initial steps of the
# installation.
yield xapi.call 'VM.provision', vm.$ref
# Gets the VM record.
VM = yield xapi.call 'VM.get_record', vm.$ref
if installation.method is 'cdrom'
# Gets the VDI containing the ISO to mount.
try
VDIref = (@getObject installation.repository, 'VDI').ref
catch
@throw 'NO_SUCH_OBJECT', 'installation.repository'
# Finds the VBD associated to the newly created VM which is a
# CD.
CD_drive = null
for ref in VM.VBDs
VBD = yield xapi.call 'VBD.get_record', vm.$ref
# TODO: Checks it has been correctly retrieved.
if VBD.type is 'CD'
CD_drive = VBD.ref
break
# No CD drives have been found, creates one.
unless CD_drive
# See: https://github.com/xenserver/xenadmin/blob/da00b13bb94603b369b873b0a555d44f15fa0ca5/XenModel/Actions/VM/CreateVMAction.cs#L370
CD_drive = yield xapi.call 'VBD.create', {
bootable: true
device: ''
empty: true
mode: 'RO'
other_config: {}
qos_algorithm_params: {}
qos_algorithm_type: ''
type: 'CD'
unpluggable: true
userdevice: (yield xapi.call 'VM.get_allowed_VBD_devices', vm.$ref)[0]
VDI: 'OpaqueRef:NULL'
VM: vm.$ref
}
# If the CD drive as not been found, throws.
@throw 'NO_SUCH_OBJECT' unless CD_drive
# Mounts the VDI into the VBD.
yield xapi.call 'VBD.insert', CD_drive, VDIref
else
yield xapi.call 'VM.provision', vm.$ref
# The VM should be properly created.
return vm.uuid
create.permission = 'admin' create.permission = 'admin'
@ -178,8 +58,9 @@ create.params = {
} }
} }
# Name of the new VM. # Name/description of the new VM.
name_label: { type: 'string' } name_label: { type: 'string' }
name_description: { type: 'string', optional: true }
# TODO: add the install repository! # TODO: add the install repository!
# VBD.insert/eject # VBD.insert/eject
@ -212,7 +93,6 @@ create.params = {
items: { items: {
type: 'object' type: 'object'
properties: { properties: {
bootable: { type: 'boolean' }
device: { type: 'string' } device: { type: 'string' }
size: { type: 'integer' } size: { type: 'integer' }
SR: { type: 'string' } SR: { type: 'string' }
@ -280,41 +160,8 @@ exports.ejectCd = ejectCd
#--------------------------------------------------------------------- #---------------------------------------------------------------------
insertCd = $coroutine ({vm, vdi, force}) -> insertCd = $coroutine ({vm, vdi, force}) ->
xapi = @getXAPI vm yield @getXAPI(vm).insertCdIntoVm(vdi.id, vm.id, force)
return
# Finds the CD drive.
cdDrive = null
$forEach (@getObjects vm.$VBDs), (VBD) ->
if VBD.is_cd_drive
cdDrive = VBD
return false
return
if cdDrive
cdDriveRef = cdDrive.ref
if cdDrive.VDI
@throw 'INVALID_PARAMS' unless force
yield xapi.call 'VBD.eject', cdDriveRef
else
cdDriveRef = yield xapi.call 'VBD.create', {
bootable: true
device: ''
empty: true
mode: 'RO'
other_config: {}
qos_algorithm_params: {}
qos_algorithm_type: ''
type: 'CD'
unpluggable: true
userdevice: (yield xapi.call 'VM.get_allowed_VBD_devices', vm.ref)[0]
VDI: 'OpaqueRef:NULL'
VM: vm.ref
}
yield xapi.call 'VBD.insert', cdDriveRef, vdi.ref
return true
insertCd.params = { insertCd.params = {
id: { type: 'string' } id: { type: 'string' }
@ -582,13 +429,14 @@ exports.restart = restart
#--------------------------------------------------------------------- #---------------------------------------------------------------------
clone = $coroutine ({vm, name, full_copy}) -> clone = $coroutine ({vm, name, full_copy}) ->
xapi = @getXAPI vm xapi = @getXAPI(vm)
if full_copy
yield xapi.call 'VM.copy', vm.ref, name, ''
else
yield xapi.call 'VM.clone', vm.ref, name
return true newVm = yield if full_copy
xapi.copyVm(vm.ref, null, name)
else
xapi.cloneVm(vm.ref, name)
return newVm.$id
clone.params = { clone.params = {
id: { type: 'string' } id: { type: 'string' }
@ -818,7 +666,11 @@ exports.import = import_
# FIXME: if position is used, all other disks after this position # FIXME: if position is used, all other disks after this position
# should be shifted. # should be shifted.
attachDisk = $coroutine ({vm, vdi, position, mode, bootable}) -> attachDisk = $coroutine ({vm, vdi, position, mode, bootable}) ->
yield @getXAPI(vm).attachVdiToVm(vdi.id, vm.id, {bootable, mode, position}) yield @getXAPI(vm).attachVdiToVm(vdi.id, vm.id, {
bootable,
position,
readOnly: mode is 'RO'
})
return return
attachDisk.params = { attachDisk.params = {
@ -843,7 +695,7 @@ exports.attachDisk = attachDisk
# FIXME: position should be optional and default to last. # FIXME: position should be optional and default to last.
createInterface = $coroutine ({vm, network, position, mtu, mac}) -> createInterface = $coroutine ({vm, network, position, mtu, mac}) ->
vif = yield @getXAPI(vm).createVirtualInterface(vm.id, network.id, { vif = yield @getXAPI(vm).createVif(vm.id, network.id, {
mac, mac,
mtu, mtu,
position position

View File

@ -39,15 +39,7 @@ export const generateToken = (function (randomBytes) {
export const formatXml = (function () { export const formatXml = (function () {
const builder = new xml2js.Builder({ const builder = new xml2js.Builder({
xmldec: { headless: true
// Do not include an XML header.
//
// This is not how this setting should be set but due to the
// implementation of both xml2js and xmlbuilder-js it works.
//
// TODO: Find a better alternative.
headless: true
}
}) })
return (...args) => builder.buildObject(...args) return (...args) => builder.buildObject(...args)

View File

@ -6,12 +6,13 @@ import expect from 'must'
import { import {
ensureArray, ensureArray,
extractProperty extractProperty,
formatXml
} from './utils' } from './utils'
// =================================================================== // ===================================================================
describe('ensureArray', function () { describe('ensureArray()', function () {
it('wrap the value in an array', function () { it('wrap the value in an array', function () {
const value = 'foo' const value = 'foo'
@ -31,7 +32,7 @@ describe('ensureArray', function () {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
describe('extractProperty', function () { describe('extractProperty()', function () {
it('returns the value of the property', function () { it('returns the value of the property', function () {
const value = {} const value = {}
const obj = { prop: value } const obj = { prop: value }
@ -47,3 +48,21 @@ describe('extractProperty', function () {
expect(obj).to.not.have.property('prop') expect(obj).to.not.have.property('prop')
}) })
}) })
// -------------------------------------------------------------------
describe('formatXml()', function () {
it('formats a JS object to an XML string', function () {
expect(formatXml({
foo: {
bar: [
{$: {baz: 'plop'}},
{$: {baz: 'plip'}}
]
}
})).to.equal(`<foo>
<bar baz="plop"/>
<bar baz="plip"/>
</foo>`)
})
})

View File

@ -239,11 +239,12 @@ export function vm (obj) {
arch: otherConfig['install-arch'], arch: otherConfig['install-arch'],
disks: (function () { disks: (function () {
const {disks: xml} = otherConfig const {disks: xml} = otherConfig
if (!xml) { let data
if (!xml || !(data = parseXml(xml)).provision) {
return [] return []
} }
const disks = ensureArray(parseXml(xml).provision.disk) const disks = ensureArray(data.provision.disk)
forEach(disks, function normalize (disk) { forEach(disks, function normalize (disk) {
disk.bootable = disk.bootable === 'true' disk.bootable = disk.bootable === 'true'
disk.size = +disk.size disk.size = +disk.size

View File

@ -4,6 +4,7 @@ import find from 'lodash.find'
import forEach from 'lodash.foreach' import forEach from 'lodash.foreach'
import got from 'got' import got from 'got'
import map from 'lodash.map' import map from 'lodash.map'
import snakeCase from 'lodash.snakecase'
import unzip from 'julien-f-unzip' import unzip from 'julien-f-unzip'
import {PassThrough} from 'stream' import {PassThrough} from 'stream'
import {promisify} from 'bluebird' import {promisify} from 'bluebird'
@ -13,7 +14,11 @@ import {
} from 'xen-api' } from 'xen-api'
import {debounce} from './decorators' import {debounce} from './decorators'
import {ensureArray, noop, parseXml, pFinally} from './utils' import {
ensureArray,
noop, parseXml,
pFinally
} from './utils'
import {JsonRpcError} from './api-errors' import {JsonRpcError} from './api-errors'
const debug = createDebug('xo:xapi') const debug = createDebug('xo:xapi')
@ -70,6 +75,8 @@ const VM_RUNNING_POWER_STATES = {
} }
export const isVmRunning = (vm) => VM_RUNNING_POWER_STATES[vm.power_state] export const isVmRunning = (vm) => VM_RUNNING_POWER_STATES[vm.power_state]
export const isVmHvm = (vm) => Boolean(vm.HVM_boot_policy)
// =================================================================== // ===================================================================
export default class Xapi extends XapiBase { export default class Xapi extends XapiBase {
@ -124,17 +131,15 @@ export default class Xapi extends XapiBase {
_waitObject (idOrUuidOrRef) { _waitObject (idOrUuidOrRef) {
let watcher = this._objectWatchers[idOrUuidOrRef] let watcher = this._objectWatchers[idOrUuidOrRef]
if (!watcher) { if (!watcher) {
let resolve, reject let resolve
const promise = new Promise((resolve_, reject_) => { const promise = new Promise(resolve_ => {
resolve = resolve_ resolve = resolve_
reject = reject_
}) })
// Register the watcher. // Register the watcher.
watcher = this._objectWatchers[idOrUuidOrRef] = { watcher = this._objectWatchers[idOrUuidOrRef] = {
promise, promise,
resolve, resolve
reject
} }
} }
@ -193,11 +198,11 @@ export default class Xapi extends XapiBase {
// ================================================================= // =================================================================
async _setObjectProperties (id, props) { async _setObjectProperties (object, props) {
const { const {
$ref: ref, $ref: ref,
$type: type $type: type
} = this.getObject(id) } = object
const namespace = getNamespaceForType(type) const namespace = getNamespaceForType(type)
@ -205,7 +210,7 @@ export default class Xapi extends XapiBase {
// properties that failed to be set. // properties that failed to be set.
await Promise.all(map(props, (value, name) => { await Promise.all(map(props, (value, name) => {
if (value != null) { if (value != null) {
return this.call(`${namespace}.set_${name}`, ref, value) return this.call(`${namespace}.set_${snakeCase(name)}`, ref, value)
} }
})) }))
} }
@ -214,7 +219,7 @@ export default class Xapi extends XapiBase {
name_label, name_label,
name_description name_description
}) { }) {
await this._setObjectProperties(this.pool.$id, { await this._setObjectProperties(this.pool, {
name_label, name_label,
name_description name_description
}) })
@ -224,7 +229,7 @@ export default class Xapi extends XapiBase {
name_label, name_label,
name_description name_description
}) { }) {
await this._setObjectProperties(id, { await this._setObjectProperties(this.getObject(id), {
name_label, name_label,
name_description name_description
}) })
@ -404,10 +409,9 @@ export default class Xapi extends XapiBase {
// ================================================================= // =================================================================
async _deleteVdi (vdiId) { async _cloneVm (vm, nameLabel = vm.name_label) {
const vdi = this.getObject(vdiId) return await this.call('VM.clone', vm.$ref, nameLabel)
await this.call('VDI.destroy', vdi.$ref)
} }
async _snapshotVm (vm, nameLabel = vm.name_label) { async _snapshotVm (vm, nameLabel = vm.name_label) {
@ -419,15 +423,139 @@ export default class Xapi extends XapiBase {
return ref return ref
} }
async cloneVm (vmId, name_label = undefined) { async cloneVm (vmId, nameLabel = undefined) {
return this._getOrWaitObject(
await this._cloneVm(this.getObject(vmId), nameLabel)
)
}
async copyVm (vmId, srId = null, nameLabel = undefined) {
const vm = this.getObject(vmId) const vm = this.getObject(vmId)
if (name_label == null) { const srRef = (srId == null) ?
({name_label} = vm) '' :
this.getObject(srId).$ref
return await this._getOrWaitObject(
await this.call('VM.copy', vm.$ref, nameLabel || vm.nameLabel, srRef)
)
}
// TODO: clean up on error.
async createVm (templateId, {
nameDescription = undefined,
nameLabel = undefined,
cpus = undefined,
installRepository = undefined,
vdis = [],
vifs = []
} = {}) {
const installMethod = (() => {
if (installRepository == null) {
return 'none'
}
try {
installRepository = this.getObject(installRepository)
return 'cd'
} catch (_) {
return 'network'
}
})()
const template = this.getObject(templateId)
// Clones the template.
const vm = await this._getOrWaitObject(
await this._cloneVm(template, nameLabel)
)
// TODO: copy BIOS strings?
// Removes disks from the provision XML, we will create them by
// ourselves.
await this.call('VM.remove_from_other_config', vm.$ref, 'disks').catch(noop)
// Creates the VDIs and executes the initial steps of the
// installation.
await this.call('VM.provision', vm.$ref)
// Set VMs params.
this._setObjectProperties(vm, {
nameDescription,
VCPUs_at_startup: cpus
})
// Sets boot parameters.
{
const isHvm = isVmHvm(vm)
if (isHvm) {
if (!vdis.length || installMethod === 'network') {
// TODO: set boot order
}
} else { // PV
if (vm.PV_bootloader === 'eliloader') {
// Removes any preexisting entry.
await this.call('VM.remove_from_other_config', vm.$ref, 'install-repository').catch(noop)
if (installMethod === 'network') {
// TODO: normalize RHEL URL?
await this.call('VM.add_to_other_config', vm.$ref, 'install-repository', installRepository)
} else if (installMethod === 'cd') {
await this.call('VM.add_to_other_config', vm.$ref, 'install-repository', 'cdrom')
await this._insertCdIntoVm(installRepository, vm)
}
}
// TODO: set PV args.
}
} }
const ref = this.call('VM.clone', vm.$ref, name_label) // Creates the VDIs.
//
// TODO: set vm.suspend_SR
{
const {$default_SR: defaultSr} = this.pool
await Promise.all(map(vdis, (vdiDescription, i) => {
return this._createVdi(
this.getObject(vdiDescription.sr || vdiDescription.SR, defaultSr),
vdiDescription.size,
{
name_label: vdiDescription.name_label,
name_description: vdiDescription.name_description
}
)
.then(ref => this._getOrWaitObject(ref))
.then(vdi => this._createVbd(vm, vdi, {
// Only the first VBD if installMethod is not cd is bootable.
bootable: installMethod !== 'cd' && !i
}))
}))
}
return await this._getOrWaitObject(ref) // Destroys the VIFs cloned from the template.
await Promise.all(map(vm.$vifs, vif => this._deleteVif(vif)))
// Creates the VIFs specified by the user.
{
let position = 0
await Promise.all(map(vifs, vif => this._createVif(
vm,
this.getObject(vif.network),
{
position: position++,
mac: vif.mac,
mtu: vif.mtu
}
)))
}
// TODO: Create Cloud config drives.
// TODO: Assign VGPUs.
return vm
} }
async deleteVm (vmId, deleteDisks = false) { async deleteVm (vmId, deleteDisks = false) {
@ -439,6 +567,11 @@ export default class Xapi extends XapiBase {
if (deleteDisks) { if (deleteDisks) {
await Promise.all(map(vm.$VBDs, vbd => { await Promise.all(map(vm.$VBDs, vbd => {
// Do not delete unpluggable VDIs.
if (vbd.unpluggable) {
return
}
try { try {
return this._deleteVdi(vbd.$VDI).catch(noop) return this._deleteVdi(vbd.$VDI).catch(noop)
} catch (_) {} } catch (_) {}
@ -503,33 +636,27 @@ export default class Xapi extends XapiBase {
) )
} }
async attachVdiToVm (vdiId, vmId, { // =================================================================
async _createVbd (vm, vdi, {
bootable = false, bootable = false,
mode = 'RW', position = undefined,
position type = 'Disk',
} = {}) { readOnly = (type !== 'Disk')
const vdi = this.getObject(vdiId) }) {
const vm = this.getObject(vmId)
if (position == null) { if (position == null) {
forEach(vm.$VBDs, vbd => { position = (await this.call('VM.get_allowed_VBD_devices', vm.$ref))[0]
const curPos = +vbd.userdevice
if (!(position > curPos)) {
position = curPos
}
})
position = position == null ? 0 : position + 1
} }
const vbdRef = await this.call('VBD.create', { const vbdRef = await this.call('VBD.create', {
bootable, bootable,
empty: false, empty: false,
mode, mode: readOnly ? 'RO' : 'RW',
other_config: {}, other_config: {},
qos_algorithm_params: {}, qos_algorithm_params: {},
qos_algorithm_type: '', qos_algorithm_type: '',
type: 'Disk', type,
unpluggable: (type !== 'Disk'),
userdevice: String(position), userdevice: String(position),
VDI: vdi.$ref, VDI: vdi.$ref,
VM: vm.$ref VM: vm.$ref
@ -538,17 +665,105 @@ export default class Xapi extends XapiBase {
if (isVmRunning(vm)) { if (isVmRunning(vm)) {
await this.call('VBD.plug', vbdRef) await this.call('VBD.plug', vbdRef)
} }
return vbdRef
}
async _createVdi (sr, size, {
name_label = '',
name_description = undefined
} = {}) {
return await this.call('VDI.create', {
name_label: name_label,
name_description: name_description,
other_config: {},
read_only: false,
sharable: false,
SR: sr.$ref,
type: 'user',
virtual_size: String(size)
})
}
// TODO: check whether the VDI is attached.
async _deleteVdi (vdi) {
await this.call('VDI.destroy', vdi.$ref)
}
_getVmCdDrive (vm) {
for (let vbd of vm.$VBDs) {
if (vbd.type === 'CD') {
return vbd
}
}
}
async _insertCdIntoVm (cd, vm, force) {
const cdDrive = await this._getVmCdDrive(vm)
if (cdDrive) {
try {
await this.call('VBD.insert', cdDrive.$ref, cd.$ref).catch()
} catch (error) {
if (force && error.code === 'VBD_NOT_EMPTY') {
await this.call('VBD.eject', cdDrive.$ref).catch(noop)
return this._insertCdIntoVm(cd, vm, force)
}
throw error
}
} else {
await this._createVbd(vm, cd, {
bootable: true,
type: 'CD'
})
}
}
async attachVdiToVm (vdiId, vmId, opts = undefined) {
await this._createVbd(
this.getObject(vdiId),
this.getObject(vmId),
opts
)
}
async createVdi (srId, size, opts) {
return await this._getOrWaitObject(
await this._createVdi(this.getObject(srId), size, opts)
)
}
async deleteVdi (vdiId) {
await this._deleteVdi(this.getObject(vdiId))
}
async insertCdIntoVm (cdId, vmId, force = undefined) {
await this._insertCdIntoVm(
this.getObject(cdId),
this.getObject(vmId),
force
)
} }
// ================================================================= // =================================================================
async createVirtualInterface (vmId, networkId, { async _createVif (vm, network, {
mac = '', mac = '',
mtu = 1500, mtu = 1500,
position = 0 position = undefined
} = {}) { } = {}) {
const vm = this.getObject(vmId) // TODO: use VM.get_allowed_VIF_devices()?
const network = this.getObject(networkId) if (position == null) {
forEach(vm.$VIFs, vif => {
const curPos = +vif.device
if (!(position > curPos)) {
position = curPos
}
})
position = position == null ? 0 : position + 1
}
const vifRef = await this.call('VIF.create', { const vifRef = await this.call('VIF.create', {
device: String(position), device: String(position),
@ -565,7 +780,26 @@ export default class Xapi extends XapiBase {
await this.call('VIF.plug', vifRef) await this.call('VIF.plug', vifRef)
} }
return await this._getOrWaitObject(vifRef) return vifRef
}
// TODO: check whether the VIF was unplugged before.
async _deleteVif (vif) {
await this.call('VIF.destroy', vif.$ref)
}
async createVif (vmId, networkId, opts = undefined) {
return await this._getOrWaitObject(
await this._createVif(
this.getObject(vmId),
this.getObject(networkId),
opts
)
)
}
async deleteVif (vifId) {
await this._deleteVif(this.getObject(vifId))
} }
// ================================================================= // =================================================================