Skip to content

Commit

Permalink
feat(xo-server/vm/snapshot): handle Self Service (#3693)
Browse files Browse the repository at this point in the history
See #3304
  • Loading branch information
pdonias authored Apr 28, 2020
1 parent 6fbd325 commit a88798c
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 25 deletions.
124 changes: 116 additions & 8 deletions packages/xo-server/src/api/vm.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import asyncMap from '@xen-orchestra/async-map'
import defer from 'golike-defer'
import { createLogger } from '@xen-orchestra/log'
import { format, JsonRpcError } from 'json-rpc-peer'
import { ignoreErrors } from 'promise-toolbox'
import { assignWith, concat } from 'lodash'
Expand All @@ -12,6 +14,8 @@ import {

import { forEach, map, mapFilter, parseSize, safeDateFormat } from '../utils'

const log = createLogger('xo:vm')

// ===================================================================

export function getHaValues() {
Expand Down Expand Up @@ -383,15 +387,23 @@ const delete_ = defer(async function(
// Update resource sets
let resourceSet
if (
vm.type === 'VM' && // only regular VMs
(vm.type === 'VM' || vm.type === 'VM-snapshot') &&
(resourceSet = xapi.xo.getData(vm._xapiId, 'resourceSet')) != null
) {
await this.setVmResourceSet(vm._xapiId, null)::ignoreErrors()
$defer.onFailure(() =>
this.setVmResourceSet(vm._xapiId, resourceSet)::ignoreErrors()
this.setVmResourceSet(vm._xapiId, resourceSet, true)::ignoreErrors()
)
}

await asyncMap(vm.snapshots, async id => {
const { resourceSet } = this.getObject(id)
if (resourceSet !== undefined) {
await this.setVmResourceSet(id, null)
$defer.onFailure(() => this.setVmResourceSet(id, resourceSet, true))
}
})

return xapi.deleteVm(
vm._xapiId,
deleteDisks,
Expand Down Expand Up @@ -552,7 +564,7 @@ export const set = defer(async function($defer, params) {
throw unauthorized()
}

await this.setVmResourceSet(vmId, resourceSetId)
await this.setVmResourceSet(vmId, resourceSetId, true)
}

const share = extract(params, 'share')
Expand Down Expand Up @@ -782,7 +794,6 @@ export { convertToTemplate as convert }

// -------------------------------------------------------------------

// TODO: implement resource sets
export const snapshot = defer(async function(
$defer,
{
Expand All @@ -792,7 +803,35 @@ export const snapshot = defer(async function(
description,
}
) {
await checkPermissionOnSrs.call(this, vm)
const { user } = this
let resourceSet
try {
if (vm.resourceSet !== undefined) {
resourceSet = await this.getResourceSet(vm.resourceSet)
}
} catch (error) {
if (noSuchObject.is(error)) {
log.warn('cannot find resource set', { resourceSet: vm.resourceSet })
} else {
throw error
}
}

if (resourceSet === undefined || !resourceSet.subjects.includes(user.id)) {
await checkPermissionOnSrs.call(this, vm)
}

if (vm.resourceSet !== undefined) {
const usage = await this.computeVmResourcesUsage(vm)
await this.allocateLimitsInResourceSet(
usage,
vm.resourceSet,
user.permission === 'admin'
)
$defer.onFailure(() =>
this.releaseLimitsInResourceSet(usage, vm.resourceSet)
)
}

const xapi = this.getXapi(vm)
const { $id: snapshotId } = await (saveMemory
Expand All @@ -804,7 +843,6 @@ export const snapshot = defer(async function(
await xapi.editVm(snapshotId, { name_description: description })
}

const { user } = this
if (user.permission !== 'admin') {
await this.addAcl(user.id, snapshotId, 'admin')
}
Expand Down Expand Up @@ -1159,12 +1197,82 @@ resume.resolve = {

// -------------------------------------------------------------------

export async function revert({ snapshot }) {
export const revert = defer(async function($defer, { snapshot }) {
await this.checkPermissions(this.user.id, [
[snapshot.$snapshot_of, 'operate'],
])
const vm = this.getObject(snapshot.$snapshot_of)
const { resourceSet } = vm
if (resourceSet !== undefined) {
const vmUsage = await this.computeVmResourcesUsage(vm)
await this.releaseLimitsInResourceSet(vmUsage, resourceSet)
$defer.onFailure(() =>
this.allocateLimitsInResourceSet(vmUsage, resourceSet, true)
)

// Deallocate IP addresses
const vmIpsByVif = {}
vm.VIFs.forEach(vifId => {
const vif = this.getObject(vifId)
vmIpsByVif[vifId] = [
...vif.allowedIpv4Addresses,
...vif.allowedIpv6Addresses,
]
})
await Promise.all(
Object.entries(vmIpsByVif).map(([vifId, ips]) =>
this.allocIpAddresses(vifId, null, ips)
)
)
$defer.onFailure(() =>
Promise.all(
Object.entries(vmIpsByVif).map(([vifId, ips]) =>
this.allocIpAddresses(vifId, ips)
)
)
)

const snapshotUsage = await this.computeVmResourcesUsage(snapshot)
await this.allocateLimitsInResourceSet(
snapshotUsage,
resourceSet,
this.user.permission === 'admin'
)
$defer.onFailure(() =>
this.releaseLimitsInResourceSet(snapshotUsage, resourceSet)
)

// Reallocate the snapshot's IP addresses
const snapshotIpsByVif = {}
snapshot.VIFs.forEach(vifId => {
const vif = this.getObject(vifId)
snapshotIpsByVif[vifId] = [
...vif.allowedIpv4Addresses,
...vif.allowedIpv6Addresses,
]
})
await Promise.all(
Object.entries(snapshotIpsByVif).map(([vifId, ips]) =>
this.allocIpAddresses(vifId, ips)
)
)
$defer.onFailure(() =>
Promise.all(
Object.entries(snapshotIpsByVif).map(([vifId, ips]) =>
this.allocIpAddresses(vifId, null, ips)
)
)
)
}
await this.getXapi(snapshot).revertVm(snapshot._xapiId)
}

// Reverting a snapshot must not set the VM back to the old resource set
await this.getXapi(vm).xo.setData(
vm._xapiId,
'resourceSet',
resourceSet === undefined ? null : resourceSet
)
})

revert.params = {
snapshot: { type: 'string' },
Expand Down
24 changes: 16 additions & 8 deletions packages/xo-server/src/xo-mixins/resource-sets.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,10 @@ export default class {
(await this._xo.getAclsForSubject(subjectId)).map(async acl => {
try {
const object = this._xo.getObject(acl.object)
if (object.type === 'VM' && object.resourceSet === id) {
if (
(object.type === 'VM' || object.type === 'VM-snapshot') &&
object.resourceSet === id
) {
await this._xo.removeAcl(subjectId, acl.object, acl.action)
$defer.onFailure(() =>
this._xo.addAcl(subjectId, acl.object, acl.action)
Expand Down Expand Up @@ -367,10 +370,7 @@ export default class {
let set
if (
object.$type !== 'VM' ||
object.is_a_snapshot ||
('start' in object.blocked_operations &&
(object.tags.includes('Disaster Recovery') ||
object.tags.includes('Continuous Replication'))) ||
object.other_config['xo:backup:job'] !== undefined ||
// No set for this VM.
!(id = xapi.xo.getData(object, 'resourceSet')) ||
// Not our set.
Expand Down Expand Up @@ -400,7 +400,7 @@ export default class {
}

@deferrable
async setVmResourceSet($defer, vmId, resourceSetId) {
async setVmResourceSet($defer, vmId, resourceSetId, force = false) {
const xapi = this._xo.getXapi(vmId)
const previousResourceSetId = xapi.xo.getData(vmId, 'resourceSet')

Expand All @@ -416,7 +416,11 @@ export default class {
)

if (resourceSetId != null) {
await this.allocateLimitsInResourceSet(resourcesUsage, resourceSetId)
await this.allocateLimitsInResourceSet(
resourcesUsage,
resourceSetId,
force
)
$defer.onFailure(() =>
this.releaseLimitsInResourceSet(resourcesUsage, resourceSetId)
)
Expand All @@ -431,7 +435,11 @@ export default class {
previousResourceSetId
)
$defer.onFailure(() =>
this.allocateLimitsInResourceSet(resourcesUsage, previousResourceSetId)
this.allocateLimitsInResourceSet(
resourcesUsage,
previousResourceSetId,
true
)
)
}

Expand Down
24 changes: 15 additions & 9 deletions packages/xo-web/src/common/xo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1268,7 +1268,7 @@ export const snapshotVm = async (vm, name, saveMemory, description) => {
name,
description,
saveMemory,
})
})::tap(subscribeResourceSets.forceRefresh)
}

import SnapshotVmModalBody from './snapshot-vm-modal' // eslint-disable-line import/first
Expand Down Expand Up @@ -1402,7 +1402,9 @@ export const getCloudInitConfig = template =>
_call('vm.getCloudInitConfig', { template })

export const pureDeleteVm = (vm, props) =>
_call('vm.delete', { id: resolveId(vm), ...props })
_call('vm.delete', { id: resolveId(vm), ...props })::tap(
subscribeResourceSets.forceRefresh
)

export const deleteVm = (vm, retryWithForce = true) =>
confirm({
Expand Down Expand Up @@ -1460,17 +1462,21 @@ export const revertSnapshot = snapshot =>
if (snapshotBefore) {
await _call('vm.snapshot', { id: snapshot.$snapshot_of })
}
await _call('vm.revert', { snapshot: snapshot.id })
await _call('vm.revert', { snapshot: snapshot.id })::tap(
subscribeResourceSets.forceRefresh
)
success(_('vmRevertSuccessfulTitle'), _('vmRevertSuccessfulMessage'))
}, noop)

export const editVm = (vm, props) =>
_call('vm.set', { ...props, id: resolveId(vm) }).catch(err => {
error(
_('setVmFailed', { vm: renderXoItemFromId(resolveId(vm)) }),
err.message
)
})
_call('vm.set', { ...props, id: resolveId(vm) })
.catch(err => {
error(
_('setVmFailed', { vm: renderXoItemFromId(resolveId(vm)) }),
err.message
)
})
::tap(subscribeResourceSets.forceRefresh)

export const fetchVmStats = (vm, granularity) =>
_call('vm.stats', { id: resolveId(vm), granularity })
Expand Down

0 comments on commit a88798c

Please sign in to comment.