From efb0dc7cce4fe1ca404a4dd9cf6b97dcfe300b39 Mon Sep 17 00:00:00 2001 From: mathieuRA Date: Thu, 7 Sep 2023 14:44:10 +0200 Subject: [PATCH 1/2] feat(xapi/host_smartReboot): ability to bypass blocked operations --- @xen-orchestra/xapi/host.mjs | 42 ++++++++++++++++++++++++----- CHANGELOG.unreleased.md | 3 +++ packages/xo-server/src/api/host.mjs | 22 +++++++++++++-- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/@xen-orchestra/xapi/host.mjs b/@xen-orchestra/xapi/host.mjs index ad88ac832ad..8de17d41b1e 100644 --- a/@xen-orchestra/xapi/host.mjs +++ b/@xen-orchestra/xapi/host.mjs @@ -1,6 +1,8 @@ import { asyncEach } from '@vates/async-each' +import { asyncMap } from '@xen-orchestra/async-map' import { decorateClass } from '@vates/decorate-with' import { defer } from 'golike-defer' +import { incorrectState, operationFailed } from 'xo-common/api-errors.js' import { getCurrentVmUuid } from './_XenStore.mjs' @@ -31,7 +33,38 @@ class Host { * * @param {string} ref - Opaque reference of the host */ - async smartReboot($defer, ref) { + async smartReboot($defer, ref, bypassBlockedSuspend = false, bypassCurrentVmCheck = false) { + let currentVmRef + try { + currentVmRef = await this.call('VM.get_by_uuid', await getCurrentVmUuid()) + } catch (error) {} + + const residentVmRefs = await this.getField('host', ref, 'resident_VMs') + const vmsWithSuspendBlocked = await asyncMap(residentVmRefs, ref => this.getRecord('VM', ref)).filter( + vm => + vm.$ref !== currentVmRef && + !vm.is_control_domain && + vm.power_state !== 'Halted' && + vm.power_state !== 'Suspended' && + vm.blocked_operations.suspend !== undefined + ) + + if (!bypassBlockedSuspend && vmsWithSuspendBlocked.length > 0) { + throw incorrectState({ actual: vmsWithSuspendBlocked.map(vm => vm.uuid), expected: [], object: 'suspendBlocked' }) + } + + if (!bypassCurrentVmCheck && residentVmRefs.includes(currentVmRef)) { + throw operationFailed({ + objectId: await this.getField('VM', currentVmRef, 'uuid'), + code: 'xoaOnHost', + }) + } + + await asyncEach(vmsWithSuspendBlocked, vm => { + $defer(() => vm.update_blocked_operations('suspend', vm.blocked_operations.suspend ?? null)) + return vm.update_blocked_operations('suspend', null) + }) + const suspendedVms = [] if (await this.getField('host', ref, 'enabled')) { await this.callAsync('host.disable', ref) @@ -42,13 +75,8 @@ class Host { }) } - let currentVmRef - try { - currentVmRef = await this.call('VM.get_by_uuid', await getCurrentVmUuid()) - } catch (error) {} - await asyncEach( - await this.getField('host', ref, 'resident_VMs'), + residentVmRefs, async vmRef => { if (vmRef === currentVmRef) { return diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 77ab5de5ab2..be0b571bec8 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -27,4 +27,7 @@ +- @xen-orchestra/xapi minor +- xo-server minor + diff --git a/packages/xo-server/src/api/host.mjs b/packages/xo-server/src/api/host.mjs index 97b2770ad03..34496a297b8 100644 --- a/packages/xo-server/src/api/host.mjs +++ b/packages/xo-server/src/api/host.mjs @@ -119,7 +119,15 @@ set.resolve = { // FIXME: set force to false per default when correctly implemented in // UI. -export async function restart({ bypassBackupCheck = false, host, force = false, suspendResidentVms }) { +export async function restart({ + bypassBackupCheck = false, + host, + force = false, + suspendResidentVms, + + bypassBlockedSuspend = force, + bypassCurrentVmCheck = force, +}) { if (bypassBackupCheck) { log.warn('host.restart with argument "bypassBackupCheck" set to true', { hostId: host.id }) } else { @@ -127,7 +135,9 @@ export async function restart({ bypassBackupCheck = false, host, force = false, } const xapi = this.getXapi(host) - return suspendResidentVms ? xapi.host_smartReboot(host._xapiRef) : xapi.rebootHost(host._xapiId, force) + return suspendResidentVms + ? xapi.host_smartReboot(host._xapiRef, bypassBlockedSuspend, bypassCurrentVmCheck) + : xapi.rebootHost(host._xapiId, force) } restart.description = 'restart the host' @@ -137,6 +147,14 @@ restart.params = { type: 'boolean', optional: true, }, + bypassBlockedSuspend: { + type: 'boolean', + optional: true, + }, + bypassCurrentVmCheck: { + type: 'boolean', + optional: true, + }, id: { type: 'string' }, force: { type: 'boolean', From 69e48213a04e5f0dc330fca3b1a40dbf2c672d24 Mon Sep 17 00:00:00 2001 From: mathieuRA Date: Thu, 7 Sep 2023 17:00:16 +0200 Subject: [PATCH 2/2] feat(xo-web/host): allow to force smartReboot --- CHANGELOG.unreleased.md | 3 + packages/xo-web/src/common/intl/messages.js | 4 + packages/xo-web/src/common/xo/index.js | 116 +++++++++++++----- .../xo-web/src/xo-app/host/tab-advanced.js | 2 +- 4 files changed, 90 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index be0b571bec8..721ad5cf8c9 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -7,6 +7,8 @@ > Users must be able to say: “Nice enhancement, I'm eager to test it” +- [Host/Advanced] Allow to force _Smart reboot_ if some resident VMs have the suspend operation blocked [Forum#7136](https://xcp-ng.org/forum/topic/7136/suspending-vms-during-host-reboot/23) (PR [#7025](https://github.com/vatesfr/xen-orchestra/pull/7025)) + ### Bug fixes > Users must be able to say: “I had this issue, happy to know it's fixed” @@ -29,5 +31,6 @@ - @xen-orchestra/xapi minor - xo-server minor +- xo-web minor diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js index a1664cf4165..609f8c9f8bb 100644 --- a/packages/xo-web/src/common/intl/messages.js +++ b/packages/xo-web/src/common/intl/messages.js @@ -963,9 +963,13 @@ const messages = { enableHostLabel: 'Enable', disableHostLabel: 'Disable', restartHostAgent: 'Restart toolstack', + smartRebootBypassCurrentVmCheck: + 'As the XOA is hosted on the host that is scheduled for a reboot, it will also be restarted. Consequently, XO won\'t be able to resume VMs, and VMs with the "Protect from accidental shutdown" option enabled will not have this option reactivated automatically.', smartRebootHostLabel: 'Smart reboot', smartRebootHostTooltip: 'Suspend resident VMs, reboot host and resume VMs automatically', forceRebootHostLabel: 'Force reboot', + forceSmartRebootHost: + 'Smart Reboot failed because {nVms, number} VM{nVms, plural, one {} other {s}} ha{nVms, plural, one {s} other {ve}} {nVms, plural, one {its} other {their}} Suspend operation blocked. Would you like to force?', rebootHostLabel: 'Reboot', noHostsAvailableErrorTitle: 'Error while restarting host', noHostsAvailableErrorMessage: diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js index ecd5cc0c206..07ff07d71d7 100644 --- a/packages/xo-web/src/common/xo/index.js +++ b/packages/xo-web/src/common/xo/index.js @@ -16,6 +16,7 @@ import { incorrectState, noHostsAvailable, operationBlocked, + operationFailed, vmLacksFeature, } from 'xo-common/api-errors' @@ -821,42 +822,89 @@ export const setRemoteSyslogHost = (host, syslogDestination) => export const setRemoteSyslogHosts = (hosts, syslogDestination) => Promise.all(map(hosts, host => setRemoteSyslogHost(host, syslogDestination))) -export const restartHost = (host, force = false, suspendResidentVms = false) => - confirm({ +export const restartHost = async ( + host, + force = false, + suspendResidentVms = false, + bypassBlockedSuspend = false, + bypassCurrentVmCheck = false +) => { + await confirm({ title: _('restartHostModalTitle'), body: _('restartHostModalMessage'), - }).then( - () => - _call('host.restart', { id: resolveId(host), force, suspendResidentVms }) - .catch(async error => { - if ( - forbiddenOperation.is(error, { - reason: `A backup may run on the pool: ${host.$poolId}`, - }) || - forbiddenOperation.is(error, { - reason: `A backup is running on the pool: ${host.$poolId}`, - }) - ) { - await confirm({ - body: ( -

- {_('bypassBackupHostModalMessage')} -

- ), - title: _('restartHostModalTitle'), - }) - return _call('host.restart', { id: resolveId(host), force, suspendResidentVms, bypassBackupCheck: true }) - } - throw error - }) - .catch(error => { - if (noHostsAvailable.is(error)) { - alert(_('noHostsAvailableErrorTitle'), _('noHostsAvailableErrorMessage')) - } - throw error - }), - noop - ) + }) + return _restartHost({ host, force, suspendResidentVms, bypassBlockedSuspend, bypassCurrentVmCheck }) +} + +const _restartHost = async ({ host, ...opts }) => { + opts = { ...opts, id: resolveId(host) } + + try { + await _call('host.restart', opts) + } catch (error) { + if (cantSuspend(error)) { + await confirm({ + body: ( +

+ {_('forceSmartRebootHost', { nVms: error.data.actual.length })} +

+ ), + title: _('restartHostModalTitle'), + }) + return _restartHost({ ...opts, host, bypassBlockedSuspend: true }) + } + + if (xoaOnHost(error)) { + await confirm({ + body: ( +

+ {_('smartRebootBypassCurrentVmCheck')} +

+ ), + title: _('restartHostModalTitle'), + }) + return _restartHost({ ...opts, host, bypassCurrentVmCheck: true }) + } + + if (backupIsRunning(error, host.$poolId)) { + await confirm({ + body: ( +

+ {_('bypassBackupHostModalMessage')} +

+ ), + title: _('restartHostModalTitle'), + }) + return _restartHost({ ...opts, host, bypassBackupCheck: true }) + } + + if (noHostsAvailableErrCheck(error)) { + alert(_('noHostsAvailableErrorTitle'), _('noHostsAvailableErrorMessage')) + } + throw error + } +} + +// ---- Restart Host errors +const cantSuspend = err => + err !== undefined && + incorrectState.is(err, { + object: 'suspendBlocked', + }) +const xoaOnHost = err => + err !== undefined && + operationFailed.is(err, { + code: 'xoaOnHost', + }) +const backupIsRunning = (err, poolId) => + err !== undefined && + (forbiddenOperation.is(err, { + reason: `A backup may run on the pool: ${poolId}`, + }) || + forbiddenOperation.is(err, { + reason: `A backup is running on the pool: ${poolId}`, + })) +const noHostsAvailableErrCheck = err => err !== undefined && noHostsAvailable.is(err) export const restartHosts = (hosts, force = false) => { const nHosts = size(hosts) diff --git a/packages/xo-web/src/xo-app/host/tab-advanced.js b/packages/xo-web/src/xo-app/host/tab-advanced.js index a091898e754..5def6dcae5a 100644 --- a/packages/xo-web/src/xo-app/host/tab-advanced.js +++ b/packages/xo-web/src/xo-app/host/tab-advanced.js @@ -76,7 +76,7 @@ const downloadLogs = async uuid => { const forceReboot = host => restartHost(host, true) const smartReboot = ALLOW_SMART_REBOOT - ? host => restartHost(host, false, true) // don't force, suspend resident VMs + ? host => restartHost(host, false, true, false, false) // don't force, suspend resident VMs, don't bypass blocked suspend, don't bypass current VM check : () => {} const formatPack = ({ name, author, description, version }, key) => (