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.pick": "^3.0.0",
"lodash.result": "^3.0.0",
"lodash.snakecase": "^3.0.1",
"lodash.startswith": "^3.0.1",
"make-error": "^1",
"multikey-hash": "^1.0.1",

View File

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

View File

@ -12,6 +12,7 @@ import schemaInspector from 'schema-inspector'
import {
InvalidParameters,
JsonRpcError,
MethodNotFound,
NoSuchObject,
Unauthorized
@ -289,11 +290,12 @@ export default class Api {
context.user = await context._getUser(userId)
}
await checkPermission.call(context, method)
checkParams(method, params)
await resolveParams.call(context, method, params)
try {
await checkPermission.call(context, method)
checkParams(method, params)
await resolveParams.call(context, method, params)
let result = await method.call(context, params)
// If nothing was returned, consider this operation a success
@ -306,7 +308,11 @@ export default class Api {
return result
} 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
}

View File

@ -3,19 +3,10 @@ import {parseSize} from '../utils'
// ===================================================================
export async function create ({name, size, sr}) {
const xapi = this.getXAPI(sr)
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))
const vdi = await this.getXAPI(sr).createVdi(sr.id, parseSize(size), {
name_label: name
})
return (await xapi.call('VDI.get_record', ref)).uuid
return vdi.$id
}
create.description = 'create a new disk on a SR'

View File

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

View File

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

View File

@ -28,143 +28,23 @@ $isVMRunning = do ->
#=====================================================================
# TODO: Implement ACLs
# FIXME: Make the method as atomic as possible.
create = $coroutine ({
installation
name_description
name_label
template
VDIs
VIFs
}) ->
# Gets the corresponding connection.
xapi = @getXAPI template
vm = yield @getXAPI(template).createVm(template.id, {
installRepository: installation && installation.repository,
nameDescription: name_description,
nameLabel: name_label,
vdis: VDIs,
vifs: VIFs
})
# Clones the VM from the template.
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
return vm.$id
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_description: { type: 'string', optional: true }
# TODO: add the install repository!
# VBD.insert/eject
@ -212,7 +93,6 @@ create.params = {
items: {
type: 'object'
properties: {
bootable: { type: 'boolean' }
device: { type: 'string' }
size: { type: 'integer' }
SR: { type: 'string' }
@ -280,41 +160,8 @@ exports.ejectCd = ejectCd
#---------------------------------------------------------------------
insertCd = $coroutine ({vm, vdi, force}) ->
xapi = @getXAPI vm
# 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
yield @getXAPI(vm).insertCdIntoVm(vdi.id, vm.id, force)
return
insertCd.params = {
id: { type: 'string' }
@ -582,13 +429,14 @@ exports.restart = restart
#---------------------------------------------------------------------
clone = $coroutine ({vm, name, full_copy}) ->
xapi = @getXAPI vm
if full_copy
yield xapi.call 'VM.copy', vm.ref, name, ''
else
yield xapi.call 'VM.clone', vm.ref, name
xapi = @getXAPI(vm)
return true
newVm = yield if full_copy
xapi.copyVm(vm.ref, null, name)
else
xapi.cloneVm(vm.ref, name)
return newVm.$id
clone.params = {
id: { type: 'string' }
@ -818,7 +666,11 @@ exports.import = import_
# FIXME: if position is used, all other disks after this position
# should be shifted.
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
attachDisk.params = {
@ -843,7 +695,7 @@ exports.attachDisk = attachDisk
# FIXME: position should be optional and default to last.
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,
mtu,
position

View File

@ -39,15 +39,7 @@ export const generateToken = (function (randomBytes) {
export const formatXml = (function () {
const builder = new xml2js.Builder({
xmldec: {
// 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
}
headless: true
})
return (...args) => builder.buildObject(...args)

View File

@ -6,12 +6,13 @@ import expect from 'must'
import {
ensureArray,
extractProperty
extractProperty,
formatXml
} from './utils'
// ===================================================================
describe('ensureArray', function () {
describe('ensureArray()', function () {
it('wrap the value in an array', function () {
const value = 'foo'
@ -31,7 +32,7 @@ describe('ensureArray', function () {
// -------------------------------------------------------------------
describe('extractProperty', function () {
describe('extractProperty()', function () {
it('returns the value of the property', function () {
const value = {}
const obj = { prop: value }
@ -47,3 +48,21 @@ describe('extractProperty', function () {
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'],
disks: (function () {
const {disks: xml} = otherConfig
if (!xml) {
let data
if (!xml || !(data = parseXml(xml)).provision) {
return []
}
const disks = ensureArray(parseXml(xml).provision.disk)
const disks = ensureArray(data.provision.disk)
forEach(disks, function normalize (disk) {
disk.bootable = disk.bootable === 'true'
disk.size = +disk.size

View File

@ -4,6 +4,7 @@ import find from 'lodash.find'
import forEach from 'lodash.foreach'
import got from 'got'
import map from 'lodash.map'
import snakeCase from 'lodash.snakecase'
import unzip from 'julien-f-unzip'
import {PassThrough} from 'stream'
import {promisify} from 'bluebird'
@ -13,7 +14,11 @@ import {
} from 'xen-api'
import {debounce} from './decorators'
import {ensureArray, noop, parseXml, pFinally} from './utils'
import {
ensureArray,
noop, parseXml,
pFinally
} from './utils'
import {JsonRpcError} from './api-errors'
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 isVmHvm = (vm) => Boolean(vm.HVM_boot_policy)
// ===================================================================
export default class Xapi extends XapiBase {
@ -124,17 +131,15 @@ export default class Xapi extends XapiBase {
_waitObject (idOrUuidOrRef) {
let watcher = this._objectWatchers[idOrUuidOrRef]
if (!watcher) {
let resolve, reject
const promise = new Promise((resolve_, reject_) => {
let resolve
const promise = new Promise(resolve_ => {
resolve = resolve_
reject = reject_
})
// Register the watcher.
watcher = this._objectWatchers[idOrUuidOrRef] = {
promise,
resolve,
reject
resolve
}
}
@ -193,11 +198,11 @@ export default class Xapi extends XapiBase {
// =================================================================
async _setObjectProperties (id, props) {
async _setObjectProperties (object, props) {
const {
$ref: ref,
$type: type
} = this.getObject(id)
} = object
const namespace = getNamespaceForType(type)
@ -205,7 +210,7 @@ export default class Xapi extends XapiBase {
// properties that failed to be set.
await Promise.all(map(props, (value, name) => {
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_description
}) {
await this._setObjectProperties(this.pool.$id, {
await this._setObjectProperties(this.pool, {
name_label,
name_description
})
@ -224,7 +229,7 @@ export default class Xapi extends XapiBase {
name_label,
name_description
}) {
await this._setObjectProperties(id, {
await this._setObjectProperties(this.getObject(id), {
name_label,
name_description
})
@ -404,10 +409,9 @@ export default class Xapi extends XapiBase {
// =================================================================
async _deleteVdi (vdiId) {
const vdi = this.getObject(vdiId)
async _cloneVm (vm, nameLabel = vm.name_label) {
return await this.call('VM.clone', vm.$ref, nameLabel)
await this.call('VDI.destroy', vdi.$ref)
}
async _snapshotVm (vm, nameLabel = vm.name_label) {
@ -419,15 +423,139 @@ export default class Xapi extends XapiBase {
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)
if (name_label == null) {
({name_label} = vm)
const srRef = (srId == null) ?
'' :
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) {
@ -439,6 +567,11 @@ export default class Xapi extends XapiBase {
if (deleteDisks) {
await Promise.all(map(vm.$VBDs, vbd => {
// Do not delete unpluggable VDIs.
if (vbd.unpluggable) {
return
}
try {
return this._deleteVdi(vbd.$VDI).catch(noop)
} catch (_) {}
@ -503,33 +636,27 @@ export default class Xapi extends XapiBase {
)
}
async attachVdiToVm (vdiId, vmId, {
// =================================================================
async _createVbd (vm, vdi, {
bootable = false,
mode = 'RW',
position
} = {}) {
const vdi = this.getObject(vdiId)
const vm = this.getObject(vmId)
position = undefined,
type = 'Disk',
readOnly = (type !== 'Disk')
}) {
if (position == null) {
forEach(vm.$VBDs, vbd => {
const curPos = +vbd.userdevice
if (!(position > curPos)) {
position = curPos
}
})
position = position == null ? 0 : position + 1
position = (await this.call('VM.get_allowed_VBD_devices', vm.$ref))[0]
}
const vbdRef = await this.call('VBD.create', {
bootable,
empty: false,
mode,
mode: readOnly ? 'RO' : 'RW',
other_config: {},
qos_algorithm_params: {},
qos_algorithm_type: '',
type: 'Disk',
type,
unpluggable: (type !== 'Disk'),
userdevice: String(position),
VDI: vdi.$ref,
VM: vm.$ref
@ -538,17 +665,105 @@ export default class Xapi extends XapiBase {
if (isVmRunning(vm)) {
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 = '',
mtu = 1500,
position = 0
position = undefined
} = {}) {
const vm = this.getObject(vmId)
const network = this.getObject(networkId)
// TODO: use VM.get_allowed_VIF_devices()?
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', {
device: String(position),
@ -565,7 +780,26 @@ export default class Xapi extends XapiBase {
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))
}
// =================================================================