Merge pull request #70 from vatesfr/vm-create-refactoring
vm.create() refactoring
This commit is contained in:
commit
b58826da6e
@ -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",
|
||||||
|
@ -7,6 +7,7 @@ export {
|
|||||||
InvalidJson,
|
InvalidJson,
|
||||||
InvalidParameters,
|
InvalidParameters,
|
||||||
InvalidRequest,
|
InvalidRequest,
|
||||||
|
JsonRpcError,
|
||||||
MethodNotFound
|
MethodNotFound
|
||||||
} from 'json-rpc-protocol'
|
} from 'json-rpc-protocol'
|
||||||
|
|
||||||
|
16
src/api.js
16
src/api.js
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
@ -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' },
|
||||||
|
@ -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}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
10
src/utils.js
10
src/utils.js
@ -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)
|
||||||
|
@ -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>`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -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
|
||||||
|
316
src/xapi.js
316
src/xapi.js
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
Loading…
Reference in New Issue
Block a user