fix(xapi/VM_destroy): handle is_default_template (#5644)

This commit is contained in:
Mathieu 2021-03-23 17:28:18 +01:00 committed by GitHub
parent fa56e594b1
commit 5f1c1278e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 44 additions and 94 deletions

View File

@ -21,7 +21,8 @@
"@babel/plugin-proposal-decorators": "^7.3.0", "@babel/plugin-proposal-decorators": "^7.3.0",
"@babel/preset-env": "^7.3.1", "@babel/preset-env": "^7.3.1",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"rimraf": "^3.0.0" "rimraf": "^3.0.0",
"xo-common": "^0.6.0"
}, },
"peerDependencies": { "peerDependencies": {
"xen-api": "^0.31.0" "xen-api": "^0.31.0"

View File

@ -3,6 +3,8 @@ const defer = require('promise-toolbox/defer')
const { utcFormat, utcParse } = require('d3-time-format') const { utcFormat, utcParse } = require('d3-time-format')
const { Xapi: Base } = require('xen-api') const { Xapi: Base } = require('xen-api')
exports.isDefaultTemplate = require('./isDefaultTemplate')
// VDI formats. (Raw is not available for delta vdi.) // VDI formats. (Raw is not available for delta vdi.)
exports.VDI_FORMAT_RAW = 'raw' exports.VDI_FORMAT_RAW = 'raw'
exports.VDI_FORMAT_VHD = 'vhd' exports.VDI_FORMAT_VHD = 'vhd'

View File

@ -0,0 +1 @@
module.exports = vmTpl => vmTpl.is_default_template || vmTpl.other_config.default_template === 'true'

View File

@ -7,9 +7,11 @@ const pCatch = require('promise-toolbox/catch')
const pRetry = require('promise-toolbox/retry') const pRetry = require('promise-toolbox/retry')
const { asyncMap } = require('@xen-orchestra/async-map') const { asyncMap } = require('@xen-orchestra/async-map')
const { createLogger } = require('@xen-orchestra/log') const { createLogger } = require('@xen-orchestra/log')
const { incorrectState } = require('xo-common/api-errors')
const { Ref } = require('xen-api') const { Ref } = require('xen-api')
const extractOpaqueRef = require('./_extractOpaqueRef') const extractOpaqueRef = require('./_extractOpaqueRef')
const isDefaultTemplate = require('./isDefaultTemplate')
const isVmRunning = require('./_isVmRunning') const isVmRunning = require('./_isVmRunning')
const { warn } = createLogger('xo:xapi:vm') const { warn } = createLogger('xo:xapi:vm')
@ -275,8 +277,13 @@ module.exports = class Vm {
throw new Error('destroy is blocked') throw new Error('destroy is blocked')
} }
if (!forceDeleteDefaultTemplate && vm.other_config.default_template === 'true') { if (!forceDeleteDefaultTemplate && isDefaultTemplate(vm)) {
throw new Error('VM is default template') throw incorrectState({
actual: true,
expected: false,
object: vm.$id,
property: 'isDefaultTemplate',
})
} }
// It is necessary for suspended VMs to be shut down // It is necessary for suspended VMs to be shut down
@ -286,6 +293,7 @@ module.exports = class Vm {
} }
await Promise.all([ await Promise.all([
vm.set_is_default_template(false),
vm.set_is_a_template(false), vm.set_is_a_template(false),
vm.update_blocked_operations('destroy', null), vm.update_blocked_operations('destroy', null),
vm.update_other_config('default_template', null), vm.update_other_config('default_template', null),

View File

@ -13,6 +13,7 @@
- [Proxy] _Force upgrade_ no longer fails on broken proxy - [Proxy] _Force upgrade_ no longer fails on broken proxy
- [Proxy] _Redeploy_ now works when the bound VM is missing - [Proxy] _Redeploy_ now works when the bound VM is missing
- [VM template] Fix confirmation modal doesn't appear on deleting a default template (PR [#5644](https://github.com/vatesfr/xen-orchestra/pull/5644))
### Packages to release ### Packages to release
@ -33,5 +34,6 @@
- xo-server-transport-email minor - xo-server-transport-email minor
- @xen-orchestra/fs minor - @xen-orchestra/fs minor
- @xen-orchestra/xapi minor
- xo-server patch - xo-server patch
- xo-web patch - xo-web patch

View File

@ -190,11 +190,12 @@ export const notEnoughResources = create(24, data => ({
message: 'not enough resources in resource set', message: 'not enough resources in resource set',
})) }))
export const incorrectState = create(25, ({ actual, expected, object }) => ({ export const incorrectState = create(25, ({ actual, expected, object, property }) => ({
data: { data: {
actual, actual,
expected, expected,
object, object,
property,
}, },
message: 'incorrect state', message: 'incorrect state',
})) }))

View File

@ -1,3 +1,5 @@
import { isDefaultTemplate } from '@xen-orchestra/xapi'
import * as sensitiveValues from './sensitive-values' import * as sensitiveValues from './sensitive-values'
import ensureArray from './_ensureArray' import ensureArray from './_ensureArray'
import { extractIpFromVmNetworks } from './_extractIpFromVmNetworks' import { extractIpFromVmNetworks } from './_extractIpFromVmNetworks'
@ -445,13 +447,14 @@ const TRANSFORMS = {
vm.snapshot_time = toTimestamp(obj.snapshot_time) vm.snapshot_time = toTimestamp(obj.snapshot_time)
vm.$snapshot_of = link(obj, 'snapshot_of') vm.$snapshot_of = link(obj, 'snapshot_of')
} else if (obj.is_a_template) { } else if (obj.is_a_template) {
const defaultTemplate = isDefaultTemplate(obj)
vm.type += '-template' vm.type += '-template'
if (defaultTemplate) {
if (obj.other_config.default_template === 'true') {
vm.id = obj.$ref // use refs for templates as they vm.id = obj.$ref // use refs for templates as they
} }
vm.CPUs.number = +obj.VCPUs_at_startup vm.CPUs.number = +obj.VCPUs_at_startup
vm.isDefaultTemplate = defaultTemplate
vm.template_info = { vm.template_info = {
arch: otherConfig['install-arch'], arch: otherConfig['install-arch'],
disks: (function () { disks: (function () {

View File

@ -35,7 +35,6 @@ import {
asInteger, asInteger,
extractOpaqueRef, extractOpaqueRef,
filterUndefineds, filterUndefineds,
getVmDisks,
canSrHaveNewVdiOfSize, canSrHaveNewVdiOfSize,
isVmHvm, isVmHvm,
isVmRunning, isVmRunning,
@ -404,7 +403,7 @@ export default class Xapi extends XapiBase {
return await this.call('VM.copy', snapshot ? snapshot.$ref : vm.$ref, nameLabel, sr ? sr.$ref : '') return await this.call('VM.copy', snapshot ? snapshot.$ref : vm.$ref, nameLabel, sr ? sr.$ref : '')
} finally { } finally {
if (snapshot) { if (snapshot) {
await this._deleteVm(snapshot) await this.VM_destroy(snapshot.$ref)
} }
} }
} }
@ -564,67 +563,13 @@ export default class Xapi extends XapiBase {
) )
} }
async _deleteVm(vmOrRef, deleteDisks = true, force = false, forceDeleteDefaultTemplate = false) { /**
const $ref = typeof vmOrRef === 'string' ? vmOrRef : vmOrRef.$ref * @deprecated Use VM_destroy instead
*/
async deleteVm(vmOrId, deleteDisks = true, force = false, forceDeleteDefaultTemplate = false) {
const $ref = typeof vmOrId === 'string' ? this.getObject(vmOrId).$ref : vmOrId.$ref
// ensure the vm record is up-to-date return this.VM_destroy($ref, { deleteDisks, force, forceDeleteDefaultTemplate })
const vm = await this.barrier($ref)
log.debug(`Deleting VM ${vm.name_label}`)
if (!force && 'destroy' in vm.blocked_operations) {
throw forbiddenOperation('destroy', vm.blocked_operations.destroy.reason)
}
if (!forceDeleteDefaultTemplate && vm.other_config.default_template === 'true') {
throw forbiddenOperation('destroy', 'VM is default template')
}
// It is necessary for suspended VMs to be shut down
// to be able to delete their VDIs.
if (vm.power_state !== 'Halted') {
await this.callAsync('VM.hard_shutdown', $ref)
}
await Promise.all([
vm.set_is_a_template(false),
vm.update_blocked_operations('destroy', null),
vm.update_other_config('default_template', null),
])
// must be done before destroying the VM
const disks = getVmDisks(vm)
// this cannot be done in parallel, otherwise disks and snapshots will be
// destroyed even if this fails
await this.callAsync('VM.destroy', $ref)
return Promise.all([
asyncMapSettled(vm.$snapshots, snapshot => this._deleteVm(snapshot))::ignoreErrors(),
vm.power_state === 'Suspended' &&
Ref.isNotEmpty(vm.suspend_VDI) &&
this._deleteVdi(vm.suspend_VDI)::ignoreErrors(),
deleteDisks &&
asyncMapSettled(disks, async vdi => {
// Dont destroy if attached to other (non control domain) VMs
if (vdi.$VBDs.some(vbd => vbd !== undefined && vbd.VM !== $ref && !vbd.$VM.is_control_domain)) {
return
}
return pRetry(() => vdi.$callAsync('destroy'), {
// work around a race condition in XCP-ng/XenServer where the disk is not fully unmounted yet.
delay: 5e3,
retries: 5,
when: { code: 'VDI_IN_USE' },
})
})::ignoreErrors(),
])
}
async deleteVm(vmId, deleteDisks, force, forceDeleteDefaultTemplate) {
return /* await */ this._deleteVm(this.getObject(vmId), deleteDisks, force, forceDeleteDefaultTemplate)
} }
getVmConsole(vmId) { getVmConsole(vmId) {
@ -660,7 +605,7 @@ export default class Xapi extends XapiBase {
}) })
if (useSnapshot) { if (useSnapshot) {
const destroySnapshot = () => this.deleteVm(exportedVm)::ignoreErrors() const destroySnapshot = () => this.VM_destroy(exportedVm.$ref)::ignoreErrors()
promise.then(_ => _.task.finally(destroySnapshot), destroySnapshot) promise.then(_ => _.task.finally(destroySnapshot), destroySnapshot)
} }
@ -696,7 +641,7 @@ export default class Xapi extends XapiBase {
} }
vm = await this._snapshotVm($cancelToken, vm, snapshotNameLabel) vm = await this._snapshotVm($cancelToken, vm, snapshotNameLabel)
$defer.onFailure(() => this._deleteVm(vm)) $defer.onFailure(() => this.VM_destroy(vm.$ref))
} }
const baseVm = baseVmId && this.getObject(baseVmId) const baseVm = baseVmId && this.getObject(baseVmId)
@ -886,7 +831,7 @@ export default class Xapi extends XapiBase {
{ suspend_VDI: suspendVdi?.$ref } { suspend_VDI: suspendVdi?.$ref }
) )
) )
$defer.onFailure(() => this._deleteVm(vm)) $defer.onFailure(() => this.VM_destroy(vm.$ref))
// 2. Delete all VBDs which may have been created by the import. // 2. Delete all VBDs which may have been created by the import.
await asyncMapSettled(vm.$VBDs, vbd => this._deleteVbd(vbd))::ignoreErrors() await asyncMapSettled(vm.$VBDs, vbd => this._deleteVbd(vbd))::ignoreErrors()
@ -997,7 +942,7 @@ export default class Xapi extends XapiBase {
]) ])
if (deleteBase && baseVm) { if (deleteBase && baseVm) {
this._deleteVm(baseVm)::ignoreErrors() this.VM_destroy(baseVm.$ref)::ignoreErrors()
} }
await Promise.all([ await Promise.all([
@ -1246,7 +1191,7 @@ export default class Xapi extends XapiBase {
VCPUs_max: nCpus, VCPUs_max: nCpus,
}) })
) )
$defer.onFailure(() => this._deleteVm(vm)) $defer.onFailure(() => this.VM_destroy(vm.$ref))
// Disable start and change the VM name label during import. // Disable start and change the VM name label during import.
await Promise.all([ await Promise.all([
vm.update_blocked_operations('start', 'OVA import in progress...'), vm.update_blocked_operations('start', 'OVA import in progress...'),
@ -1389,7 +1334,7 @@ export default class Xapi extends XapiBase {
vm.snapshots.map(async ref => { vm.snapshots.map(async ref => {
const nameLabel = await this.getField('VM', ref, 'name_label') const nameLabel = await this.getField('VM', ref, 'name_label')
if (nameLabel.startsWith(snapshotNameLabelPrefix)) { if (nameLabel.startsWith(snapshotNameLabelPrefix)) {
return this._deleteVm(ref) return this.VM_destroy(ref)
} }
}) })
) )

View File

@ -58,24 +58,6 @@ export const extractOpaqueRef = str => {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export const getVmDisks = vm => {
const disks = { __proto__: null }
forEach(vm.$VBDs, vbd => {
let vdi
if (
// Do not remove CDs and Floppies.
vbd.type === 'Disk' &&
// Ignore VBD without VDI.
(vdi = vbd.$VDI)
) {
disks[vdi.$id] = vdi
}
})
return disks
}
// -------------------------------------------------------------------
const parseDateTimeHelper = utcParse('%Y%m%dT%H:%M:%SZ') const parseDateTimeHelper = utcParse('%Y%m%dT%H:%M:%SZ')
export function parseDateTime(str, defaultValue) { export function parseDateTime(str, defaultValue) {

View File

@ -10,7 +10,7 @@ import { get as getDefined } from '@xen-orchestra/defined'
import { pFinally, reflect, tap, tapCatch } from 'promise-toolbox' import { pFinally, reflect, tap, tapCatch } from 'promise-toolbox'
import { SelectHost } from 'select-objects' import { SelectHost } from 'select-objects'
import { filter, forEach, get, includes, isEmpty, isEqual, map, once, size, sortBy, throttle } from 'lodash' import { filter, forEach, get, includes, isEmpty, isEqual, map, once, size, sortBy, throttle } from 'lodash'
import { forbiddenOperation, noHostsAvailable, vmIsTemplate } from 'xo-common/api-errors' import { forbiddenOperation, incorrectState, noHostsAvailable } from 'xo-common/api-errors'
import _ from '../intl' import _ from '../intl'
import fetch, { post } from '../fetch' import fetch, { post } from '../fetch'
@ -1141,7 +1141,12 @@ export const deleteTemplates = templates =>
await Promise.all( await Promise.all(
map(resolveIds(templates), id => map(resolveIds(templates), id =>
_call('vm.delete', { id }).catch(reason => { _call('vm.delete', { id }).catch(reason => {
if (vmIsTemplate.is(reason)) { if (
incorrectState.is(reason, {
expected: false,
property: 'isDefaultTemplate',
})
) {
defaultTemplates.push(id) defaultTemplates.push(id)
} else { } else {
nErrors++ nErrors++