fix(xapi/VM_destroy): handle is_default_template (#5644)
This commit is contained in:
parent
fa56e594b1
commit
5f1c1278e3
@ -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"
|
||||||
|
@ -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'
|
||||||
|
1
@xen-orchestra/xapi/src/isDefaultTemplate.js
Normal file
1
@xen-orchestra/xapi/src/isDefaultTemplate.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = vmTpl => vmTpl.is_default_template || vmTpl.other_config.default_template === 'true'
|
@ -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),
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
}))
|
}))
|
||||||
|
@ -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 () {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -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) {
|
||||||
|
@ -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++
|
||||||
|
Loading…
Reference in New Issue
Block a user