fix(xo-web/VM): display a confirmation modal to bypass blockedOperation (#6295)
This commit is contained in:
parent
433851d771
commit
6778d6aa4a
@ -9,6 +9,7 @@
|
||||
|
||||
- [REST API] VDI import now also supports the raw format
|
||||
- Embedded HTTP/HTTPS proxy is now enabled by default
|
||||
- [VM] Display a confirmation modal when stopping/restarting a protected VM (PR [#6295](https://github.com/vatesfr/xen-orchestra/pull/6295))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
@ -41,6 +42,6 @@
|
||||
- @xen-orchestra/xapi patch
|
||||
- xo-cli patch
|
||||
- xo-server minor
|
||||
- xo-web patch
|
||||
- xo-web minor
|
||||
|
||||
<!--packages-end-->
|
||||
|
@ -16,6 +16,9 @@ import { forEach, map, mapFilter, parseSize, safeDateFormat } from '../utils.mjs
|
||||
|
||||
const log = createLogger('xo:vm')
|
||||
|
||||
const RESTART_OPERATIONS = ['reboot', 'clean_reboot', 'hard_reboot']
|
||||
const SHUTDOWN_OPERATIONS = ['shutdown', 'clean_shutdown', 'hard_shutdown']
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export function getHaValues() {
|
||||
@ -666,13 +669,26 @@ set.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function restart({ vm, force = false }) {
|
||||
return this.getXapi(vm).rebootVm(vm._xapiId, { hard: force })
|
||||
}
|
||||
export const restart = defer(async function ($defer, { vm, force = false, bypassBlockedOperation = force }) {
|
||||
const xapi = this.getXapi(vm)
|
||||
if (bypassBlockedOperation) {
|
||||
await Promise.all(
|
||||
RESTART_OPERATIONS.map(async operation => {
|
||||
const reason = vm.blockedOperations[operation]
|
||||
if (reason !== undefined) {
|
||||
await xapi.call('VM.remove_from_blocked_operations', vm._xapiRef, operation)
|
||||
$defer(() => xapi.call('VM.add_to_blocked_operations', vm._xapiRef, operation, reason))
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return xapi.rebootVm(vm._xapiId, { hard: force })
|
||||
})
|
||||
|
||||
restart.params = {
|
||||
id: { type: 'string' },
|
||||
force: { type: 'boolean', optional: true },
|
||||
bypassBlockedOperation: { type: 'boolean', optional: true },
|
||||
}
|
||||
|
||||
restart.resolve = {
|
||||
@ -893,9 +909,21 @@ start.resolve = {
|
||||
// - 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 }) {
|
||||
export const stop = defer(async function ($defer, { vm, force, bypassBlockedOperation = force }) {
|
||||
const xapi = this.getXapi(vm)
|
||||
|
||||
if (bypassBlockedOperation) {
|
||||
await Promise.all(
|
||||
SHUTDOWN_OPERATIONS.map(async operation => {
|
||||
const reason = vm.blockedOperations[operation]
|
||||
if (reason !== undefined) {
|
||||
await xapi.call('VM.remove_from_blocked_operations', vm._xapiRef, operation)
|
||||
$defer(() => xapi.call('VM.add_to_blocked_operations', vm._xapiRef, operation, reason))
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Hard shutdown
|
||||
if (force) {
|
||||
return xapi.shutdownVm(vm._xapiRef, { hard: true })
|
||||
@ -912,11 +940,12 @@ export async function stop({ vm, force }) {
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
stop.params = {
|
||||
id: { type: 'string' },
|
||||
force: { type: 'boolean', optional: true },
|
||||
bypassBlockedOperation: { type: 'boolean', optional: true },
|
||||
}
|
||||
|
||||
stop.resolve = {
|
||||
|
@ -1739,6 +1739,8 @@ const messages = {
|
||||
restartVmModalMessage: 'Are you sure you want to restart {name}?',
|
||||
stopVmModalTitle: 'Stop VM',
|
||||
stopVmModalMessage: 'Are you sure you want to stop {name}?',
|
||||
blockedOperation: 'Blocked operation',
|
||||
stopVmBlockedModalMessage: 'Stop operation for this VM is blocked. Would you like to stop it anyway?',
|
||||
vmHasNoTools: 'No guest tools',
|
||||
vmHasNoToolsMessage: "The VM doesn't have Xen tools installed, which are required to properly stop or reboot it.",
|
||||
confirmForceShutdown: 'Would you like to force shutdown the VM?',
|
||||
@ -1749,6 +1751,7 @@ const messages = {
|
||||
pauseVmsModalMessage: 'Are you sure you want to pause {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
restartVmsModalTitle: 'Restart VM{vms, plural, one {} other {s}}',
|
||||
restartVmsModalMessage: 'Are you sure you want to restart {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
restartVmBlockedModalMessage: 'Restart operation for this VM is blocked. Would you like to restart it anyway?',
|
||||
snapshotSaveMemory: 'save memory',
|
||||
snapshotVmsModalTitle: 'Snapshot VM{vms, plural, one {} other {s}}',
|
||||
deleteVmsModalTitle: 'Delete VM{vms, plural, one {} other {s}}',
|
||||
|
@ -8,10 +8,16 @@ import URL from 'url-parse'
|
||||
import Xo from 'xo-lib'
|
||||
import { createBackoff } from 'jsonrpc-websocket-client'
|
||||
import { get as getDefined } from '@xen-orchestra/defined'
|
||||
import { pFinally, reflect, tap, tapCatch } from 'promise-toolbox'
|
||||
import { pFinally, reflect, retry, tap, tapCatch } from 'promise-toolbox'
|
||||
import { SelectHost } from 'select-objects'
|
||||
import { filter, forEach, get, includes, isEmpty, isEqual, map, once, size, sortBy, throttle } from 'lodash'
|
||||
import { forbiddenOperation, incorrectState, noHostsAvailable, vmLacksFeature } from 'xo-common/api-errors'
|
||||
import {
|
||||
forbiddenOperation,
|
||||
incorrectState,
|
||||
noHostsAvailable,
|
||||
operationBlocked,
|
||||
vmLacksFeature,
|
||||
} from 'xo-common/api-errors'
|
||||
|
||||
import _ from '../intl'
|
||||
import ActionButton from '../action-button'
|
||||
@ -1233,42 +1239,7 @@ export const startVms = vms =>
|
||||
}
|
||||
}, noop)
|
||||
|
||||
export const stopVm = async (vm, force = false) => {
|
||||
try {
|
||||
await confirm({
|
||||
title: _('stopVmModalTitle'),
|
||||
body: _('stopVmModalMessage', { name: vm.name_label }),
|
||||
})
|
||||
|
||||
return await _call('vm.stop', { id: resolveId(vm), force })
|
||||
} catch (error) {
|
||||
if (error === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!vmLacksFeature.is(error) || force) {
|
||||
throw error
|
||||
}
|
||||
|
||||
try {
|
||||
await confirm({
|
||||
title: _('vmHasNoTools'),
|
||||
body: (
|
||||
<div>
|
||||
<p>{_('vmHasNoToolsMessage')}</p>
|
||||
<p>
|
||||
<strong>{_('confirmForceShutdown')}</strong>
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
return await _call('vm.stop', { id: resolveId(vm), force: true })
|
||||
}
|
||||
}
|
||||
export const stopVm = (vm, hardShutdown = false) => stopOrRestartVm(vm, 'stop', hardShutdown)
|
||||
|
||||
export const stopVms = (vms, force = false) =>
|
||||
confirm({
|
||||
@ -1294,43 +1265,51 @@ export const pauseVms = vms =>
|
||||
|
||||
export const recoveryStartVm = vm => _call('vm.recoveryStart', { id: resolveId(vm) })
|
||||
|
||||
export const restartVm = async (vm, force = false) => {
|
||||
try {
|
||||
await confirm({
|
||||
title: _('restartVmModalTitle'),
|
||||
body: _('restartVmModalMessage', { name: vm.name_label }),
|
||||
})
|
||||
const stopOrRestartVm = async (vm, method, force = false) => {
|
||||
let bypassBlockedOperation = false
|
||||
const id = resolveId(vm)
|
||||
|
||||
return await _call('vm.restart', { id: resolveId(vm), force })
|
||||
} catch (error) {
|
||||
if (error === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!vmLacksFeature.is(error) || force) {
|
||||
throw error
|
||||
}
|
||||
|
||||
try {
|
||||
await confirm({
|
||||
title: _('vmHasNoTools'),
|
||||
body: (
|
||||
<div>
|
||||
<p>{_('vmHasNoToolsMessage')}</p>
|
||||
<p>
|
||||
<strong>{_('confirmForceReboot')}</strong>
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
return await _call('vm.restart', { id: resolveId(vm), force: true })
|
||||
if (method !== 'stop' && method !== 'restart') {
|
||||
throw new Error(`invalid ${method}`)
|
||||
}
|
||||
const isStopOperation = method === 'stop'
|
||||
|
||||
await confirm({
|
||||
title: _(isStopOperation ? 'stopVmModalTitle' : 'restartVmModalTitle'),
|
||||
body: _(isStopOperation ? 'stopVmModalMessage' : 'restartVmModalMessage', { name: vm.name_label }),
|
||||
})
|
||||
|
||||
return retry(() => _call(`vm.${isStopOperation ? 'stop' : 'restart'}`, { id, force, bypassBlockedOperation }), {
|
||||
when: err => operationBlocked.is(err) || (vmLacksFeature.is(err) && !force),
|
||||
async onRetry(err) {
|
||||
if (operationBlocked.is(err)) {
|
||||
await confirm({
|
||||
title: _('blockedOperation'),
|
||||
body: _(isStopOperation ? 'stopVmBlockedModalMessage' : 'restartVmBlockedModalMessage'),
|
||||
})
|
||||
bypassBlockedOperation = true
|
||||
}
|
||||
if (vmLacksFeature.is(err) && !force) {
|
||||
await confirm({
|
||||
title: _('vmHasNoTools'),
|
||||
body: (
|
||||
<div>
|
||||
<p>{_('vmHasNoToolsMessage')}</p>
|
||||
<p>
|
||||
<strong>{_(isStopOperation ? 'confirmForceShutdown' : 'confirmForceReboot')}</strong>
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
force = true
|
||||
}
|
||||
},
|
||||
delay: 0,
|
||||
})
|
||||
}
|
||||
|
||||
export const restartVm = (vm, hardRestart = false) => stopOrRestartVm(vm, 'restart', hardRestart)
|
||||
|
||||
export const restartVms = (vms, force = false) =>
|
||||
confirm({
|
||||
title: _('restartVmsModalTitle', { vms: vms.length }),
|
||||
|
Loading…
Reference in New Issue
Block a user