1539 lines
35 KiB
JavaScript
1539 lines
35 KiB
JavaScript
import defer from 'golike-defer'
|
|
import { format } from 'json-rpc-peer'
|
|
import { ignoreErrors } from 'promise-toolbox'
|
|
import { assignWith, concat } from 'lodash'
|
|
import {
|
|
forbiddenOperation,
|
|
invalidParameters,
|
|
noSuchObject,
|
|
unauthorized,
|
|
} from 'xo-common/api-errors'
|
|
|
|
import { forEach, map, mapFilter, parseSize } from '../utils'
|
|
|
|
// ===================================================================
|
|
|
|
export function getHaValues() {
|
|
return ['best-effort', 'restart', '']
|
|
}
|
|
|
|
function checkPermissionOnSrs(vm, permission = 'operate') {
|
|
const permissions = []
|
|
forEach(vm.$VBDs, vbdId => {
|
|
const vbd = this.getObject(vbdId, 'VBD')
|
|
const vdiId = vbd.VDI
|
|
|
|
if (vbd.is_cd_drive || !vdiId) {
|
|
return
|
|
}
|
|
return permissions.push([
|
|
this.getObject(vdiId, ['VDI', 'VDI-snapshot']).$SR,
|
|
permission,
|
|
])
|
|
})
|
|
|
|
return this.checkPermissions(this.session.get('user_id'), permissions)
|
|
}
|
|
|
|
// ===================================================================
|
|
|
|
const extract = (obj, prop) => {
|
|
const value = obj[prop]
|
|
delete obj[prop]
|
|
return value
|
|
}
|
|
|
|
// TODO: Implement ACLs
|
|
export async function create(params) {
|
|
const { user } = this
|
|
const resourceSet = extract(params, 'resourceSet')
|
|
const template = extract(params, 'template')
|
|
if (resourceSet === undefined) {
|
|
await this.checkPermissions(this.user.id, [
|
|
[template.$pool, 'administrate'],
|
|
])
|
|
}
|
|
|
|
params.template = template._xapiId
|
|
|
|
const xapi = this.getXapi(template)
|
|
|
|
const objectIds = [template.id]
|
|
const limits = {
|
|
cpus: template.CPUs.number,
|
|
disk: 0,
|
|
memory: template.memory.dynamic[1],
|
|
vms: 1,
|
|
}
|
|
const vdiSizesByDevice = {}
|
|
let highestDevice = -1
|
|
forEach(xapi.getObject(template._xapiId).$VBDs, vbd => {
|
|
let vdi
|
|
highestDevice = Math.max(highestDevice, vbd.userdevice)
|
|
if (vbd.type === 'Disk' && (vdi = vbd.$VDI)) {
|
|
vdiSizesByDevice[vbd.userdevice] = +vdi.virtual_size
|
|
}
|
|
})
|
|
|
|
const vdis = extract(params, 'VDIs')
|
|
params.vdis =
|
|
vdis &&
|
|
map(vdis, vdi => {
|
|
const sr = this.getObject(vdi.SR)
|
|
const size = parseSize(vdi.size)
|
|
|
|
objectIds.push(sr.id)
|
|
limits.disk += size
|
|
|
|
return {
|
|
...vdi,
|
|
device: ++highestDevice,
|
|
size,
|
|
SR: sr._xapiId,
|
|
type: vdi.type,
|
|
}
|
|
})
|
|
|
|
const existingVdis = extract(params, 'existingDisks')
|
|
params.existingVdis =
|
|
existingVdis &&
|
|
map(existingVdis, (vdi, userdevice) => {
|
|
let size, sr
|
|
if (vdi.size != null) {
|
|
size = parseSize(vdi.size)
|
|
vdiSizesByDevice[userdevice] = size
|
|
}
|
|
|
|
if (vdi.$SR) {
|
|
sr = this.getObject(vdi.$SR)
|
|
objectIds.push(sr.id)
|
|
}
|
|
|
|
return {
|
|
...vdi,
|
|
size,
|
|
$SR: sr && sr._xapiId,
|
|
}
|
|
})
|
|
|
|
forEach(vdiSizesByDevice, size => (limits.disk += size))
|
|
|
|
const vifs = extract(params, 'VIFs')
|
|
params.vifs =
|
|
vifs &&
|
|
map(vifs, vif => {
|
|
const network = this.getObject(vif.network)
|
|
|
|
objectIds.push(network.id)
|
|
|
|
return {
|
|
mac: vif.mac,
|
|
network: network._xapiId,
|
|
ipv4_allowed: vif.allowedIpv4Addresses,
|
|
ipv6_allowed: vif.allowedIpv6Addresses,
|
|
}
|
|
})
|
|
|
|
const installation = extract(params, 'installation')
|
|
params.installRepository = installation && installation.repository
|
|
|
|
let checkLimits
|
|
|
|
if (resourceSet) {
|
|
await this.checkResourceSetConstraints(resourceSet, user.id, objectIds)
|
|
checkLimits = async limits2 => {
|
|
await this.allocateLimitsInResourceSet(
|
|
assignWith({}, limits, limits2, (l1 = 0, l2) => l1 + l2),
|
|
resourceSet
|
|
)
|
|
}
|
|
}
|
|
|
|
const xapiVm = await xapi.createVm(template._xapiId, params, checkLimits)
|
|
const vm = xapi.xo.addObject(xapiVm)
|
|
|
|
if (resourceSet) {
|
|
await Promise.all([
|
|
params.share
|
|
? Promise.all(
|
|
map((await this.getResourceSet(resourceSet)).subjects, subjectId =>
|
|
this.addAcl(subjectId, vm.id, 'admin')
|
|
)
|
|
)
|
|
: this.addAcl(user.id, vm.id, 'admin'),
|
|
xapi.xo.setData(xapiVm.$id, 'resourceSet', resourceSet),
|
|
])
|
|
}
|
|
|
|
for (const vif of xapiVm.$VIFs) {
|
|
xapi.xo.addObject(vif)
|
|
await this.allocIpAddresses(
|
|
vif.$id,
|
|
concat(vif.ipv4_allowed, vif.ipv6_allowed)
|
|
).catch(() => xapi.deleteVif(vif))
|
|
}
|
|
|
|
if (params.bootAfterCreate) {
|
|
ignoreErrors.call(xapi.startVm(vm._xapiId))
|
|
}
|
|
|
|
return vm.id
|
|
}
|
|
|
|
create.params = {
|
|
affinityHost: { type: 'string', optional: true },
|
|
|
|
bootAfterCreate: {
|
|
type: 'boolean',
|
|
optional: true,
|
|
},
|
|
|
|
cloudConfig: {
|
|
type: 'string',
|
|
optional: true,
|
|
},
|
|
|
|
coreOs: {
|
|
type: 'boolean',
|
|
optional: true,
|
|
},
|
|
|
|
clone: {
|
|
type: 'boolean',
|
|
optional: true,
|
|
},
|
|
|
|
coresPerSocket: {
|
|
type: ['string', 'number'],
|
|
optional: true,
|
|
},
|
|
|
|
resourceSet: {
|
|
type: 'string',
|
|
optional: true,
|
|
},
|
|
|
|
installation: {
|
|
type: 'object',
|
|
optional: true,
|
|
properties: {
|
|
method: { type: 'string' },
|
|
repository: { type: 'string' },
|
|
},
|
|
},
|
|
|
|
vgpuType: {
|
|
type: 'string',
|
|
optional: true,
|
|
},
|
|
|
|
gpuGroup: {
|
|
type: 'string',
|
|
optional: true,
|
|
},
|
|
|
|
// Name/description of the new VM.
|
|
name_label: { type: 'string' },
|
|
name_description: { type: 'string', optional: true },
|
|
|
|
// PV Args
|
|
pv_args: { type: 'string', optional: true },
|
|
|
|
share: {
|
|
type: 'boolean',
|
|
optional: true,
|
|
},
|
|
|
|
// TODO: add the install repository!
|
|
// VBD.insert/eject
|
|
// Also for the console!
|
|
|
|
// UUID of the template the VM will be created from.
|
|
template: { type: 'string' },
|
|
|
|
// Virtual interfaces to create for the new VM.
|
|
VIFs: {
|
|
optional: true,
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
// UUID of the network to create the interface in.
|
|
network: { type: 'string' },
|
|
|
|
mac: {
|
|
optional: true, // Auto-generated per default.
|
|
type: 'string',
|
|
},
|
|
|
|
allowedIpv4Addresses: {
|
|
optional: true,
|
|
type: 'array',
|
|
items: { type: 'string' },
|
|
},
|
|
|
|
allowedIpv6Addresses: {
|
|
optional: true,
|
|
type: 'array',
|
|
items: { type: 'string' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
// Virtual disks to create for the new VM.
|
|
VDIs: {
|
|
optional: true, // If not defined, use the template parameters.
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
size: { type: ['integer', 'string'] },
|
|
SR: { type: 'string' },
|
|
type: { type: 'string' },
|
|
},
|
|
},
|
|
},
|
|
|
|
// TODO: rename to *existingVdis* or rename *VDIs* to *disks*.
|
|
existingDisks: {
|
|
optional: true,
|
|
type: 'object',
|
|
|
|
// Do not for a type object.
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
size: {
|
|
type: ['integer', 'string'],
|
|
optional: true,
|
|
},
|
|
$SR: {
|
|
type: 'string',
|
|
optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
create.resolve = {
|
|
template: ['template', 'VM-template', ''],
|
|
vgpuType: ['vgpuType', 'vgpuType', ''],
|
|
gpuGroup: ['gpuGroup', 'gpuGroup', ''],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
async function delete_({
|
|
delete_disks, // eslint-disable-line camelcase
|
|
force,
|
|
forceDeleteDefaultTemplate,
|
|
vm,
|
|
|
|
deleteDisks = delete_disks,
|
|
}) {
|
|
const xapi = this.getXapi(vm)
|
|
|
|
this.getAllAcls().then(acls => {
|
|
return Promise.all(
|
|
mapFilter(acls, acl => {
|
|
if (acl.object === vm.id) {
|
|
return ignoreErrors.call(
|
|
this.removeAcl(acl.subject, acl.object, acl.action)
|
|
)
|
|
}
|
|
})
|
|
)
|
|
})
|
|
|
|
// Update IP pools
|
|
await Promise.all(
|
|
map(vm.VIFs, vifId => {
|
|
const vif = xapi.getObject(vifId)
|
|
return ignoreErrors.call(
|
|
this.allocIpAddresses(
|
|
vifId,
|
|
null,
|
|
concat(vif.ipv4_allowed, vif.ipv6_allowed)
|
|
)
|
|
)
|
|
})
|
|
)
|
|
|
|
// Update resource sets
|
|
if (
|
|
vm.type === 'VM' && // only regular VMs
|
|
xapi.xo.getData(vm._xapiId, 'resourceSet') != null
|
|
) {
|
|
this.setVmResourceSet(vm._xapiId, null)::ignoreErrors()
|
|
}
|
|
|
|
return xapi.deleteVm(
|
|
vm._xapiId,
|
|
deleteDisks,
|
|
force,
|
|
forceDeleteDefaultTemplate
|
|
)
|
|
}
|
|
|
|
delete_.params = {
|
|
id: { type: 'string' },
|
|
|
|
deleteDisks: {
|
|
optional: true,
|
|
type: 'boolean',
|
|
},
|
|
|
|
force: {
|
|
optional: true,
|
|
type: 'boolean',
|
|
},
|
|
|
|
forceDeleteDefaultTemplate: {
|
|
optional: true,
|
|
type: 'boolean',
|
|
},
|
|
}
|
|
delete_.resolve = {
|
|
vm: ['id', ['VM', 'VM-snapshot', 'VM-template'], 'administrate'],
|
|
}
|
|
|
|
export { delete_ as delete }
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function ejectCd({ vm }) {
|
|
await this.getXapi(vm).ejectCdFromVm(vm._xapiId)
|
|
}
|
|
|
|
ejectCd.params = {
|
|
id: { type: 'string' },
|
|
}
|
|
|
|
ejectCd.resolve = {
|
|
vm: ['id', 'VM', 'operate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function insertCd({ vm, vdi, force = true }) {
|
|
await this.getXapi(vm).insertCdIntoVm(vdi._xapiId, vm._xapiId, { force })
|
|
}
|
|
|
|
insertCd.params = {
|
|
id: { type: 'string' },
|
|
cd_id: { type: 'string' },
|
|
force: { type: 'boolean', optional: true },
|
|
}
|
|
|
|
insertCd.resolve = {
|
|
vm: ['id', 'VM', 'operate'],
|
|
// Not compatible with resource sets.
|
|
// FIXME: find a workaround.
|
|
vdi: ['cd_id', 'VDI', ''],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function migrate({
|
|
vm,
|
|
host,
|
|
sr,
|
|
mapVdisSrs,
|
|
mapVifsNetworks,
|
|
migrationNetwork,
|
|
}) {
|
|
let mapVdisSrsXapi, mapVifsNetworksXapi
|
|
const permissions = []
|
|
|
|
if (mapVdisSrs) {
|
|
mapVdisSrsXapi = {}
|
|
forEach(mapVdisSrs, (srId, vdiId) => {
|
|
const vdiXapiId = this.getObject(vdiId, ['VDI', 'VDI-snapshot'])._xapiId
|
|
mapVdisSrsXapi[vdiXapiId] = this.getObject(srId, 'SR')._xapiId
|
|
return permissions.push([srId, 'administrate'])
|
|
})
|
|
}
|
|
|
|
if (mapVifsNetworks) {
|
|
mapVifsNetworksXapi = {}
|
|
forEach(mapVifsNetworks, (networkId, vifId) => {
|
|
const vifXapiId = this.getObject(vifId, 'VIF')._xapiId
|
|
mapVifsNetworksXapi[vifXapiId] = this.getObject(
|
|
networkId,
|
|
'network'
|
|
)._xapiId
|
|
return permissions.push([networkId, 'administrate'])
|
|
})
|
|
}
|
|
|
|
await this.checkPermissions(this.user.id, permissions)
|
|
|
|
await this.getXapi(vm).migrateVm(
|
|
vm._xapiId,
|
|
this.getXapi(host),
|
|
host._xapiId,
|
|
{
|
|
sr: sr && this.getObject(sr, 'SR')._xapiId,
|
|
migrationNetworkId:
|
|
migrationNetwork != null ? migrationNetwork._xapiId : undefined,
|
|
mapVifsNetworks: mapVifsNetworksXapi,
|
|
mapVdisSrs: mapVdisSrsXapi,
|
|
}
|
|
)
|
|
}
|
|
|
|
migrate.params = {
|
|
// Identifier of the VM to migrate.
|
|
vm: { type: 'string' },
|
|
|
|
// Identifier of the host to migrate to.
|
|
targetHost: { type: 'string' },
|
|
|
|
// Identifier of the default SR to migrate to.
|
|
sr: { type: 'string', optional: true },
|
|
|
|
// Map VDIs IDs --> SRs IDs
|
|
mapVdisSrs: { type: 'object', optional: true },
|
|
|
|
// Map VIFs IDs --> Networks IDs
|
|
mapVifsNetworks: { type: 'object', optional: true },
|
|
|
|
// Identifier of the Network use for the migration
|
|
migrationNetwork: { type: 'string', optional: true },
|
|
}
|
|
|
|
migrate.resolve = {
|
|
vm: ['vm', 'VM', 'administrate'],
|
|
host: ['targetHost', 'host', 'administrate'],
|
|
migrationNetwork: ['migrationNetwork', 'network', 'administrate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function set(params) {
|
|
const VM = extract(params, 'VM')
|
|
const xapi = this.getXapi(VM)
|
|
const vmId = VM._xapiId
|
|
|
|
const resourceSetId = extract(params, 'resourceSet')
|
|
if (resourceSetId !== undefined) {
|
|
if (this.user.permission !== 'admin') {
|
|
throw unauthorized()
|
|
}
|
|
|
|
await this.setVmResourceSet(vmId, resourceSetId)
|
|
}
|
|
|
|
const share = extract(params, 'share')
|
|
if (share) {
|
|
await this.shareVmResourceSet(vmId)
|
|
}
|
|
|
|
return xapi.editVm(vmId, params, async (limits, vm) => {
|
|
const resourceSet = xapi.xo.getData(vm, 'resourceSet')
|
|
|
|
if (resourceSet) {
|
|
try {
|
|
return await this.allocateLimitsInResourceSet(limits, resourceSet)
|
|
} catch (error) {
|
|
// if the resource set no longer exist, behave as if the VM is free
|
|
if (!noSuchObject.is(error)) {
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
if (limits.cpuWeight && this.user.permission !== 'admin') {
|
|
throw unauthorized()
|
|
}
|
|
})
|
|
}
|
|
|
|
set.params = {
|
|
// Identifier of the VM to update.
|
|
id: { type: 'string' },
|
|
|
|
name_label: { type: 'string', optional: true },
|
|
|
|
name_description: { type: 'string', optional: true },
|
|
|
|
high_availability: {
|
|
optional: true,
|
|
pattern: new RegExp(`^(${getHaValues().join('|')})$`),
|
|
type: 'string',
|
|
},
|
|
|
|
// Number of virtual CPUs to allocate.
|
|
CPUs: { type: 'integer', optional: true },
|
|
|
|
cpusMax: { type: ['integer', 'string'], optional: true },
|
|
|
|
// Memory to allocate (in bytes).
|
|
//
|
|
// Note: static_min ≤ dynamic_min ≤ dynamic_max ≤ static_max
|
|
memory: { type: ['integer', 'string'], optional: true },
|
|
|
|
// Set dynamic_min
|
|
memoryMin: { type: ['integer', 'string'], optional: true },
|
|
|
|
// Set dynamic_max
|
|
memoryMax: { type: ['integer', 'string'], optional: true },
|
|
|
|
// Set static_max
|
|
memoryStaticMax: { type: ['integer', 'string'], optional: true },
|
|
|
|
// Kernel arguments for PV VM.
|
|
PV_args: { type: 'string', optional: true },
|
|
|
|
cpuMask: { type: 'array', optional: true },
|
|
|
|
cpuWeight: { type: ['integer', 'null'], optional: true },
|
|
|
|
cpuCap: { type: ['integer', 'null'], optional: true },
|
|
|
|
affinityHost: { type: ['string', 'null'], optional: true },
|
|
|
|
// Switch from Cirrus video adaptor to VGA adaptor
|
|
vga: { type: 'string', optional: true },
|
|
|
|
videoram: { type: ['string', 'number'], optional: true },
|
|
|
|
coresPerSocket: { type: ['string', 'number', 'null'], optional: true },
|
|
|
|
// Emulate HVM C000 PCI device for Windows Update to fetch or update PV drivers
|
|
hasVendorDevice: { type: 'boolean', optional: true },
|
|
|
|
expNestedHvm: { type: 'boolean', optional: true },
|
|
|
|
// Move the vm In to/Out of Self Service
|
|
resourceSet: { type: ['string', 'null'], optional: true },
|
|
|
|
share: { type: 'boolean', optional: true },
|
|
|
|
startDelay: { type: 'integer', optional: true },
|
|
|
|
// set the VM network interface controller
|
|
nicType: { type: ['string', 'null'], optional: true },
|
|
}
|
|
|
|
set.resolve = {
|
|
VM: ['id', ['VM', 'VM-snapshot', 'VM-template'], 'administrate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function restart({ vm, force = false }) {
|
|
const xapi = this.getXapi(vm)
|
|
|
|
if (force) {
|
|
await xapi.call('VM.hard_reboot', vm._xapiRef)
|
|
} else {
|
|
await xapi.call('VM.clean_reboot', vm._xapiRef)
|
|
}
|
|
}
|
|
|
|
restart.params = {
|
|
id: { type: 'string' },
|
|
force: { type: 'boolean', optional: true },
|
|
}
|
|
|
|
restart.resolve = {
|
|
vm: ['id', 'VM', 'operate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export const clone = defer(async function(
|
|
$defer,
|
|
{ vm, name, full_copy: fullCopy }
|
|
) {
|
|
await checkPermissionOnSrs.call(this, vm)
|
|
const xapi = this.getXapi(vm)
|
|
|
|
const { $id: cloneId } = await xapi.cloneVm(vm._xapiRef, {
|
|
nameLabel: name,
|
|
fast: !fullCopy,
|
|
})
|
|
$defer.onFailure(() => xapi.deleteVm(cloneId))
|
|
|
|
const isAdmin = this.user.permission === 'admin'
|
|
if (!isAdmin) {
|
|
await this.addAcl(this.user.id, cloneId, 'admin')
|
|
}
|
|
|
|
if (vm.resourceSet !== undefined) {
|
|
await this.allocateLimitsInResourceSet(
|
|
await this.computeVmResourcesUsage(vm),
|
|
vm.resourceSet,
|
|
isAdmin
|
|
)
|
|
}
|
|
|
|
return cloneId
|
|
})
|
|
|
|
clone.params = {
|
|
id: { type: 'string' },
|
|
name: { type: 'string' },
|
|
full_copy: { type: 'boolean' },
|
|
}
|
|
|
|
clone.resolve = {
|
|
vm: ['id', ['VM', 'VM-snapshot'], 'administrate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
// TODO: implement resource sets
|
|
export async function copy({ compress, name: nameLabel, sr, vm }) {
|
|
if (vm.$pool === sr.$pool) {
|
|
if (vm.power_state === 'Running') {
|
|
await checkPermissionOnSrs.call(this, vm)
|
|
}
|
|
|
|
return this.getXapi(vm)
|
|
.copyVm(vm._xapiId, sr._xapiId, {
|
|
nameLabel,
|
|
})
|
|
.then(vm => vm.$id)
|
|
}
|
|
|
|
return this.getXapi(vm)
|
|
.remoteCopyVm(vm._xapiId, this.getXapi(sr), sr._xapiId, {
|
|
compress,
|
|
nameLabel,
|
|
})
|
|
.then(({ vm }) => vm.$id)
|
|
}
|
|
|
|
copy.params = {
|
|
compress: {
|
|
type: ['boolean', 'string'],
|
|
optional: true,
|
|
},
|
|
name: {
|
|
type: 'string',
|
|
optional: true,
|
|
},
|
|
vm: { type: 'string' },
|
|
sr: { type: 'string' },
|
|
}
|
|
|
|
copy.resolve = {
|
|
vm: ['vm', ['VM', 'VM-snapshot'], 'administrate'],
|
|
sr: ['sr', 'SR', 'operate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function convertToTemplate({ vm }) {
|
|
// Convert to a template requires pool admin permission.
|
|
await this.checkPermissions(this.user.id, [[vm.$pool, 'administrate']])
|
|
|
|
await this.getXapi(vm).call('VM.set_is_a_template', vm._xapiRef, true)
|
|
}
|
|
|
|
convertToTemplate.params = {
|
|
id: { type: 'string' },
|
|
}
|
|
|
|
convertToTemplate.resolve = {
|
|
vm: ['id', ['VM', 'VM-snapshot'], 'administrate'],
|
|
}
|
|
|
|
// TODO: remove when no longer used.
|
|
export { convertToTemplate as convert }
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
// TODO: implement resource sets
|
|
export const snapshot = defer(async function(
|
|
$defer,
|
|
{
|
|
vm,
|
|
name = `${vm.name_label}_${new Date().toISOString()}`,
|
|
saveMemory = false,
|
|
description,
|
|
}
|
|
) {
|
|
await checkPermissionOnSrs.call(this, vm)
|
|
|
|
const xapi = this.getXapi(vm)
|
|
const { $id: snapshotId } = await (saveMemory
|
|
? xapi.checkpointVm(vm._xapiRef, name)
|
|
: xapi.snapshotVm(vm._xapiRef, name))
|
|
$defer.onFailure(() => xapi.deleteVm(snapshotId))
|
|
|
|
if (description !== undefined) {
|
|
await xapi.editVm(snapshotId, { name_description: description })
|
|
}
|
|
|
|
const { user } = this
|
|
if (user.permission !== 'admin') {
|
|
await this.addAcl(user.id, snapshotId, 'admin')
|
|
}
|
|
return snapshotId
|
|
})
|
|
|
|
snapshot.params = {
|
|
description: { type: 'string', optional: true },
|
|
id: { type: 'string' },
|
|
name: { type: 'string', optional: true },
|
|
saveMemory: { type: 'boolean', optional: true },
|
|
}
|
|
|
|
snapshot.resolve = {
|
|
vm: ['id', 'VM', 'operate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export function rollingDeltaBackup({
|
|
vm,
|
|
remote,
|
|
tag,
|
|
depth,
|
|
retention = depth,
|
|
}) {
|
|
return this.rollingDeltaVmBackup({
|
|
vm,
|
|
remoteId: remote,
|
|
tag,
|
|
retention,
|
|
})
|
|
}
|
|
|
|
rollingDeltaBackup.params = {
|
|
id: { type: 'string' },
|
|
remote: { type: 'string' },
|
|
tag: { type: 'string' },
|
|
retention: { type: ['string', 'number'], optional: true },
|
|
// This parameter is deprecated. It used to support the old saved backups jobs.
|
|
depth: { type: ['string', 'number'], optional: true },
|
|
}
|
|
|
|
rollingDeltaBackup.resolve = {
|
|
vm: ['id', ['VM', 'VM-snapshot'], 'administrate'],
|
|
}
|
|
|
|
rollingDeltaBackup.permission = 'admin'
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export function importDeltaBackup({ sr, remote, filePath, mapVdisSrs }) {
|
|
const mapVdisSrsXapi = {}
|
|
|
|
forEach(mapVdisSrs, (srId, vdiId) => {
|
|
mapVdisSrsXapi[vdiId] = this.getObject(srId, 'SR')._xapiId
|
|
})
|
|
|
|
return this.importDeltaVmBackup({
|
|
sr,
|
|
remoteId: remote,
|
|
filePath,
|
|
mapVdisSrs: mapVdisSrsXapi,
|
|
}).then(_ => _.vm)
|
|
}
|
|
|
|
importDeltaBackup.params = {
|
|
sr: { type: 'string' },
|
|
remote: { type: 'string' },
|
|
filePath: { type: 'string' },
|
|
// Map VDIs UUIDs --> SRs IDs
|
|
mapVdisSrs: { type: 'object', optional: true },
|
|
}
|
|
|
|
importDeltaBackup.resolve = {
|
|
sr: ['sr', 'SR', 'operate'],
|
|
}
|
|
|
|
importDeltaBackup.permission = 'admin'
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export function deltaCopy({ force, vm, retention, sr }) {
|
|
return this.deltaCopyVm(vm, sr, force, retention)
|
|
}
|
|
|
|
deltaCopy.params = {
|
|
force: { type: 'boolean', optional: true },
|
|
id: { type: 'string' },
|
|
retention: { type: 'number', optional: true },
|
|
sr: { type: 'string' },
|
|
}
|
|
|
|
deltaCopy.resolve = {
|
|
vm: ['id', 'VM', 'operate'],
|
|
sr: ['sr', 'SR', 'operate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function rollingSnapshot({ vm, tag, depth, retention = depth }) {
|
|
await checkPermissionOnSrs.call(this, vm)
|
|
return this.rollingSnapshotVm(vm, tag, retention)
|
|
}
|
|
|
|
rollingSnapshot.params = {
|
|
id: { type: 'string' },
|
|
tag: { type: 'string' },
|
|
retention: { type: 'number', optional: true },
|
|
// This parameter is deprecated. It used to support the old saved backups jobs.
|
|
depth: { type: 'number', optional: true },
|
|
}
|
|
|
|
rollingSnapshot.resolve = {
|
|
vm: ['id', 'VM', 'administrate'],
|
|
}
|
|
|
|
rollingSnapshot.description =
|
|
'Snapshots a VM with a tagged name, and removes the oldest snapshot with the same tag according to retention'
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export function backup({ vm, remoteId, file, compress }) {
|
|
return this.backupVm({ vm, remoteId, file, compress })
|
|
}
|
|
|
|
backup.permission = 'admin'
|
|
|
|
backup.params = {
|
|
id: { type: 'string' },
|
|
remoteId: { type: 'string' },
|
|
file: { type: 'string' },
|
|
compress: { type: 'boolean', optional: true },
|
|
}
|
|
|
|
backup.resolve = {
|
|
vm: ['id', 'VM', 'administrate'],
|
|
}
|
|
|
|
backup.description = 'Exports a VM to the file system'
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export function importBackup({ remote, file, sr }) {
|
|
return this.importVmBackup(remote, file, sr)
|
|
}
|
|
|
|
importBackup.permission = 'admin'
|
|
importBackup.description =
|
|
'Imports a VM into host, from a file found in the chosen remote'
|
|
importBackup.params = {
|
|
remote: { type: 'string' },
|
|
file: { type: 'string' },
|
|
sr: { type: 'string' },
|
|
}
|
|
|
|
importBackup.resolve = {
|
|
sr: ['sr', 'SR', 'operate'],
|
|
}
|
|
|
|
importBackup.permission = 'admin'
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export function rollingBackup({
|
|
vm,
|
|
remoteId,
|
|
tag,
|
|
depth,
|
|
retention = depth,
|
|
compress,
|
|
}) {
|
|
return this.rollingBackupVm({
|
|
vm,
|
|
remoteId,
|
|
tag,
|
|
retention,
|
|
compress,
|
|
})
|
|
}
|
|
|
|
rollingBackup.permission = 'admin'
|
|
|
|
rollingBackup.params = {
|
|
id: { type: 'string' },
|
|
remoteId: { type: 'string' },
|
|
tag: { type: 'string' },
|
|
retention: { type: 'number', optional: true },
|
|
// This parameter is deprecated. It used to support the old saved backups jobs.
|
|
depth: { type: 'number', optional: true },
|
|
compress: { type: 'boolean', optional: true },
|
|
}
|
|
|
|
rollingBackup.resolve = {
|
|
vm: ['id', ['VM', 'VM-snapshot'], 'administrate'],
|
|
}
|
|
|
|
rollingBackup.description =
|
|
'Exports a VM to the file system with a tagged name, and removes the oldest backup with the same tag according to retention'
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export function rollingDrCopy({
|
|
vm,
|
|
pool,
|
|
sr,
|
|
tag,
|
|
depth,
|
|
retention = depth,
|
|
deleteOldBackupsFirst,
|
|
}) {
|
|
if (sr === undefined) {
|
|
if (pool === undefined) {
|
|
throw invalidParameters('either pool or sr param should be specified')
|
|
}
|
|
|
|
if (vm.$pool === pool.id) {
|
|
throw forbiddenOperation(
|
|
'Disaster Recovery attempts to copy on the same pool'
|
|
)
|
|
}
|
|
|
|
sr = this.getObject(pool.default_SR, 'SR')
|
|
}
|
|
|
|
return this.rollingDrCopyVm({
|
|
vm,
|
|
sr,
|
|
tag,
|
|
retention,
|
|
deleteOldBackupsFirst,
|
|
})
|
|
}
|
|
|
|
rollingDrCopy.params = {
|
|
retention: { type: 'number', optional: true },
|
|
// This parameter is deprecated. It used to support the old saved backups jobs.
|
|
depth: { type: 'number', optional: true },
|
|
id: { type: 'string' },
|
|
pool: { type: 'string', optional: true },
|
|
sr: { type: 'string', optional: true },
|
|
tag: { type: 'string' },
|
|
deleteOldBackupsFirst: { type: 'boolean', optional: true },
|
|
}
|
|
|
|
rollingDrCopy.resolve = {
|
|
vm: ['id', ['VM', 'VM-snapshot'], 'administrate'],
|
|
pool: ['pool', 'pool', 'administrate'],
|
|
sr: ['sr', 'SR', 'administrate'],
|
|
}
|
|
|
|
rollingDrCopy.description =
|
|
'Copies a VM to a different pool, with a tagged name, and removes the oldest VM with the same tag from this pool, according to retention'
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export function start({ vm, force, host }) {
|
|
return this.getXapi(vm).startVm(vm._xapiId, host?._xapiId, force)
|
|
}
|
|
|
|
start.params = {
|
|
force: { type: 'boolean', optional: true },
|
|
host: { type: 'string', optional: true },
|
|
id: { type: 'string' },
|
|
}
|
|
|
|
start.resolve = {
|
|
host: ['host', 'host', 'operate'],
|
|
vm: ['id', 'VM', 'operate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
// TODO: implements timeout.
|
|
// - if !force → clean shutdown
|
|
// - if force is true → hard shutdown
|
|
// - if force is integer → clean shutdown and after force seconds, hard shutdown.
|
|
export async function stop({ vm, force }) {
|
|
const xapi = this.getXapi(vm)
|
|
|
|
// Hard shutdown
|
|
if (force) {
|
|
return xapi.shutdownVm(vm._xapiRef, { hard: true })
|
|
}
|
|
|
|
// Clean shutdown
|
|
try {
|
|
await xapi.shutdownVm(vm._xapiRef)
|
|
} catch (error) {
|
|
const { code } = error
|
|
if (
|
|
code === 'VM_MISSING_PV_DRIVERS' ||
|
|
code === 'VM_LACKS_FEATURE_SHUTDOWN'
|
|
) {
|
|
throw invalidParameters('clean shutdown requires PV drivers')
|
|
}
|
|
|
|
throw error
|
|
}
|
|
}
|
|
|
|
stop.params = {
|
|
id: { type: 'string' },
|
|
force: { type: 'boolean', optional: true },
|
|
}
|
|
|
|
stop.resolve = {
|
|
vm: ['id', 'VM', 'operate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function suspend({ vm }) {
|
|
await this.getXapi(vm).call('VM.suspend', vm._xapiRef)
|
|
}
|
|
|
|
suspend.params = {
|
|
id: { type: 'string' },
|
|
}
|
|
|
|
suspend.resolve = {
|
|
vm: ['id', 'VM', 'operate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function pause({ vm }) {
|
|
await this.getXapi(vm).call('VM.pause', vm._xapiRef)
|
|
}
|
|
|
|
pause.params = {
|
|
id: { type: 'string' },
|
|
}
|
|
|
|
pause.resolve = {
|
|
vm: ['id', 'VM', 'operate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export function resume({ vm }) {
|
|
return this.getXapi(vm).resumeVm(vm._xapiId)
|
|
}
|
|
|
|
resume.params = {
|
|
id: { type: 'string' },
|
|
}
|
|
|
|
resume.resolve = {
|
|
vm: ['id', 'VM', 'operate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export function revert({ snapshot, snapshotBefore }) {
|
|
return this.getXapi(snapshot).revertVm(snapshot._xapiId, snapshotBefore)
|
|
}
|
|
|
|
revert.params = {
|
|
snapshot: { type: 'string' },
|
|
snapshotBefore: { type: 'boolean', optional: true },
|
|
}
|
|
|
|
revert.resolve = {
|
|
snapshot: ['snapshot', 'VM-snapshot', 'administrate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
async function handleExport(req, res, { xapi, id, compress }) {
|
|
const stream = await xapi.exportVm(id, {
|
|
compress,
|
|
})
|
|
res.on('close', () => stream.cancel())
|
|
// Remove the filename as it is already part of the URL.
|
|
stream.headers['content-disposition'] = 'attachment'
|
|
|
|
res.writeHead(
|
|
stream.statusCode,
|
|
stream.statusMessage != null ? stream.statusMessage : '',
|
|
stream.headers
|
|
)
|
|
stream.pipe(res)
|
|
}
|
|
|
|
// TODO: integrate in xapi.js
|
|
async function export_({ vm, compress }) {
|
|
if (vm.power_state === 'Running') {
|
|
await checkPermissionOnSrs.call(this, vm)
|
|
}
|
|
|
|
const data = {
|
|
xapi: this.getXapi(vm),
|
|
id: vm._xapiId,
|
|
compress,
|
|
}
|
|
|
|
return {
|
|
$getFrom: await this.registerHttpRequest(handleExport, data, {
|
|
suffix: encodeURI(`/${vm.name_label}.xva`),
|
|
}),
|
|
}
|
|
}
|
|
|
|
export_.params = {
|
|
vm: { type: 'string' },
|
|
compress: { type: ['boolean', 'string'], optional: true },
|
|
}
|
|
|
|
export_.resolve = {
|
|
vm: ['vm', ['VM', 'VM-snapshot'], 'administrate'],
|
|
}
|
|
|
|
export { export_ as export }
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
async function handleVmImport(req, res, { data, srId, type, xapi }) {
|
|
// Timeout seems to be broken in Node 4.
|
|
// See https://github.com/nodejs/node/issues/3319
|
|
req.setTimeout(43200000) // 12 hours
|
|
|
|
try {
|
|
const vm = await xapi.importVm(req, { data, srId, type })
|
|
res.end(format.response(0, vm.$id))
|
|
} catch (e) {
|
|
res.writeHead(500)
|
|
res.end(format.error(0, new Error(e.message)))
|
|
}
|
|
}
|
|
|
|
// TODO: "sr_id" can be passed in URL to target a specific SR
|
|
async function import_({ data, sr, type }) {
|
|
if (data && type === 'xva') {
|
|
throw invalidParameters('unsupported field data for the file type xva')
|
|
}
|
|
|
|
return {
|
|
$sendTo: await this.registerHttpRequest(handleVmImport, {
|
|
data,
|
|
srId: sr._xapiId,
|
|
type,
|
|
xapi: this.getXapi(sr),
|
|
}),
|
|
}
|
|
}
|
|
|
|
import_.params = {
|
|
data: {
|
|
type: 'object',
|
|
optional: true,
|
|
properties: {
|
|
descriptionLabel: { type: 'string' },
|
|
disks: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
capacity: { type: 'integer' },
|
|
descriptionLabel: { type: 'string' },
|
|
nameLabel: { type: 'string' },
|
|
path: { type: 'string' },
|
|
position: { type: 'integer' },
|
|
},
|
|
},
|
|
optional: true,
|
|
},
|
|
memory: { type: 'integer' },
|
|
nameLabel: { type: 'string' },
|
|
nCpus: { type: 'integer' },
|
|
networks: {
|
|
type: 'array',
|
|
items: { type: 'string' },
|
|
optional: true,
|
|
},
|
|
},
|
|
},
|
|
type: { type: 'string', optional: true },
|
|
sr: { type: 'string' },
|
|
}
|
|
|
|
import_.resolve = {
|
|
sr: ['sr', 'SR', 'administrate'],
|
|
}
|
|
|
|
export { import_ as import }
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
// FIXME: if position is used, all other disks after this position
|
|
// should be shifted.
|
|
export async function attachDisk({ vm, vdi, position, mode, bootable }) {
|
|
await this.getXapi(vm).createVbd({
|
|
bootable,
|
|
mode,
|
|
userdevice: position,
|
|
vdi: vdi._xapiId,
|
|
vm: vm._xapiId,
|
|
})
|
|
}
|
|
|
|
attachDisk.params = {
|
|
bootable: {
|
|
type: 'boolean',
|
|
optional: true,
|
|
},
|
|
mode: { type: 'string', optional: true },
|
|
position: { type: 'string', optional: true },
|
|
vdi: { type: 'string' },
|
|
vm: { type: 'string' },
|
|
}
|
|
|
|
attachDisk.resolve = {
|
|
vm: ['vm', 'VM', 'administrate'],
|
|
vdi: ['vdi', 'VDI', 'administrate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
// TODO: implement resource sets
|
|
export async function createInterface({
|
|
vm,
|
|
network,
|
|
position,
|
|
mac,
|
|
allowedIpv4Addresses,
|
|
allowedIpv6Addresses,
|
|
}) {
|
|
const { resourceSet } = vm
|
|
if (resourceSet != null) {
|
|
await this.checkResourceSetConstraints(resourceSet, this.user.id, [
|
|
network.id,
|
|
])
|
|
} else {
|
|
await this.checkPermissions(this.user.id, [[network.id, 'view']])
|
|
}
|
|
|
|
let ipAddresses
|
|
const vif = await this.getXapi(vm).createVif(vm._xapiId, network._xapiId, {
|
|
mac,
|
|
position,
|
|
ipv4_allowed: allowedIpv4Addresses,
|
|
ipv6_allowed: allowedIpv6Addresses,
|
|
})
|
|
|
|
const { push } = (ipAddresses = [])
|
|
if (allowedIpv4Addresses) {
|
|
push.apply(ipAddresses, allowedIpv4Addresses)
|
|
}
|
|
if (allowedIpv6Addresses) {
|
|
push.apply(ipAddresses, allowedIpv6Addresses)
|
|
}
|
|
if (ipAddresses.length) {
|
|
ignoreErrors.call(this.allocIpAddresses(vif.$id, ipAddresses))
|
|
}
|
|
|
|
return vif.$id
|
|
}
|
|
|
|
createInterface.params = {
|
|
vm: { type: 'string' },
|
|
network: { type: 'string' },
|
|
position: { type: ['integer', 'string'], optional: true },
|
|
mac: { type: 'string', optional: true },
|
|
allowedIpv4Addresses: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'string',
|
|
},
|
|
optional: true,
|
|
},
|
|
allowedIpv6Addresses: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'string',
|
|
},
|
|
optional: true,
|
|
},
|
|
}
|
|
|
|
createInterface.resolve = {
|
|
// Not compatible with resource sets.
|
|
// FIXME: find a workaround.
|
|
network: ['network', 'network', ''],
|
|
vm: ['vm', 'VM', 'administrate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function attachPci({ vm, pciId }) {
|
|
const xapi = this.getXapi(vm)
|
|
|
|
await xapi.call('VM.add_to_other_config', vm._xapiRef, 'pci', pciId)
|
|
}
|
|
|
|
attachPci.params = {
|
|
vm: { type: 'string' },
|
|
pciId: { type: 'string' },
|
|
}
|
|
|
|
attachPci.resolve = {
|
|
vm: ['vm', 'VM', 'administrate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function detachPci({ vm }) {
|
|
const xapi = this.getXapi(vm)
|
|
|
|
await xapi.call('VM.remove_from_other_config', vm._xapiRef, 'pci')
|
|
}
|
|
|
|
detachPci.params = {
|
|
vm: { type: 'string' },
|
|
}
|
|
|
|
detachPci.resolve = {
|
|
vm: ['vm', 'VM', 'administrate'],
|
|
}
|
|
// -------------------------------------------------------------------
|
|
|
|
export function stats({ vm, granularity }) {
|
|
return this.getXapiVmStats(vm._xapiId, granularity)
|
|
}
|
|
|
|
stats.description = 'returns statistics about the VM'
|
|
|
|
stats.params = {
|
|
id: { type: 'string' },
|
|
granularity: {
|
|
type: 'string',
|
|
optional: true,
|
|
},
|
|
}
|
|
|
|
stats.resolve = {
|
|
vm: ['id', ['VM', 'VM-snapshot'], 'view'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function setBootOrder({ vm, order }) {
|
|
const xapi = this.getXapi(vm)
|
|
|
|
order = { order }
|
|
if (vm.virtualizationMode === 'hvm') {
|
|
await xapi.call('VM.set_HVM_boot_params', vm._xapiRef, order)
|
|
return
|
|
}
|
|
|
|
throw invalidParameters('You can only set the boot order on a HVM guest')
|
|
}
|
|
|
|
setBootOrder.params = {
|
|
vm: { type: 'string' },
|
|
order: { type: 'string' },
|
|
}
|
|
|
|
setBootOrder.resolve = {
|
|
vm: ['vm', 'VM', 'operate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export function recoveryStart({ vm }) {
|
|
return this.getXapi(vm).startVmOnCd(vm._xapiId)
|
|
}
|
|
|
|
recoveryStart.params = {
|
|
id: { type: 'string' },
|
|
}
|
|
|
|
recoveryStart.resolve = {
|
|
vm: ['id', 'VM', 'operate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export function getCloudInitConfig({ template }) {
|
|
return this.getXapi(template).getCloudInitConfig(template._xapiId)
|
|
}
|
|
|
|
getCloudInitConfig.params = {
|
|
template: { type: 'string' },
|
|
}
|
|
|
|
getCloudInitConfig.resolve = {
|
|
template: ['template', 'VM-template', 'administrate'],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function createCloudInitConfigDrive({
|
|
config,
|
|
coreos,
|
|
networkConfig,
|
|
sr,
|
|
vm,
|
|
}) {
|
|
const xapi = this.getXapi(vm)
|
|
if (coreos) {
|
|
// CoreOS is a special CloudConfig drive created by XS plugin
|
|
await xapi.createCoreOsCloudInitConfigDrive(vm._xapiId, sr._xapiId, config)
|
|
} else {
|
|
// use generic Cloud Init drive
|
|
await xapi.createCloudInitConfigDrive(
|
|
vm._xapiId,
|
|
sr._xapiId,
|
|
config,
|
|
networkConfig
|
|
)
|
|
}
|
|
}
|
|
|
|
createCloudInitConfigDrive.params = {
|
|
vm: { type: 'string' },
|
|
sr: { type: 'string' },
|
|
config: { type: 'string' },
|
|
networkConfig: { type: 'string', optional: true },
|
|
}
|
|
|
|
createCloudInitConfigDrive.resolve = {
|
|
vm: ['vm', 'VM', 'administrate'],
|
|
|
|
// Not compatible with resource sets.
|
|
// FIXME: find a workaround.
|
|
sr: ['sr', 'SR', ''], // 'operate' ]
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function createVgpu({ vm, gpuGroup, vgpuType }) {
|
|
// TODO: properly handle device. Can a VM have 2 vGPUS?
|
|
await this.getXapi(vm).createVgpu(
|
|
vm._xapiId,
|
|
gpuGroup._xapiId,
|
|
vgpuType._xapiId
|
|
)
|
|
}
|
|
|
|
createVgpu.params = {
|
|
vm: { type: 'string' },
|
|
gpuGroup: { type: 'string' },
|
|
vgpuType: { type: 'string' },
|
|
}
|
|
|
|
createVgpu.resolve = {
|
|
vm: ['vm', 'VM', 'administrate'],
|
|
gpuGroup: ['gpuGroup', 'gpuGroup', ''],
|
|
vgpuType: ['vgpuType', 'vgpuType', ''],
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
export async function deleteVgpu({ vgpu }) {
|
|
await this.getXapi(vgpu).deleteVgpu(vgpu._xapiId)
|
|
}
|
|
|
|
deleteVgpu.params = {
|
|
vgpu: { type: 'string' },
|
|
}
|
|
|
|
deleteVgpu.resolve = {
|
|
vgpu: ['vgpu', 'vgpu', ''],
|
|
}
|