fix(xo-web/VM): display a confirmation modal to bypass blockedOperation (#6295)

This commit is contained in:
Mathieu 2022-07-28 15:01:22 +02:00 committed by GitHub
parent 433851d771
commit 6778d6aa4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 89 additions and 77 deletions

View File

@ -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-->

View File

@ -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 = {

View File

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

View File

@ -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 }),