Files
xen-orchestra/packages/xo-server/src/api/vm.js
2019-03-11 15:39:10 +01:00

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', ''],
}